Coverage for qpdk / cells / resonator.py: 97%

122 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-02 17:50 +0000

1"""Resonator components.""" 

2 

3from __future__ import annotations 

4 

5from functools import partial 

6 

7import gdsfactory as gf 

8import numpy as np 

9from gdsfactory.component import Component 

10from gdsfactory.typings import ComponentSpec, CrossSectionSpec 

11 

12from qpdk.cells.waveguides import bend_circular, straight 

13from qpdk.helper import show_components 

14from qpdk.tech import get_etch_section 

15 

16 

17@gf.cell(tags=("resonators",)) 

18def resonator( 

19 length: float = 4000.0, 

20 meanders: int = 6, 

21 bend_spec: ComponentSpec = bend_circular, 

22 cross_section: CrossSectionSpec = "cpw", 

23 *, 

24 start_with_bend: bool = False, 

25 end_with_bend: bool = False, 

26 open_start: bool = True, 

27 open_end: bool = False, 

28) -> Component: 

29 """Creates a meandering coplanar waveguide resonator. 

30 

31 Changing `open_start` and `open_end` appropriately allows creating 

32 a shorted quarter-wave resonator or an open half-wave resonator. 

33 

34 .. svgbob:: 

35 

36 o1 ─────┐ 

37 

38 ┌───────┘ 

39 

40 └───────┐ 

41 

42 ┌───────┘ 

43 

44 └────── o2 

45 

46 See :cite:`m.pozarMicrowaveEngineering2012` for details 

47 

48 Args: 

49 length: Length of the resonator in μm. 

50 meanders: Number of meander sections to fit the resonator in a compact area. 

51 bend_spec: Specification for the bend component used in meanders. 

52 cross_section: Cross-section specification for the resonator. 

53 start_with_bend: If True, starts the resonator with a bend. 

54 end_with_bend: If True, ends the resonator with a bend. 

55 open_start: If True, adds an etch section at the start of the resonator. 

56 open_end: If True, adds an etch section at the end of the resonator. 

57 

58 Returns: 

59 Component: A gdsfactory component with meandering resonator geometry. 

60 

61 Raises: 

62 ValueError: If length is too short for the requested meanders. 

63 """ 

64 c = Component() 

65 cross_section = gf.get_cross_section(cross_section) 

66 bend = gf.get_component( 

67 bend_spec, cross_section=cross_section, angle=180, angular_step=4 

68 ) 

69 

70 num_straights = meanders + 1 

71 if start_with_bend: 

72 num_straights -= 1 

73 if end_with_bend: 

74 num_straights -= 1 

75 

76 if num_straights < 0: 

77 raise ValueError( 

78 "Cannot have fewer than 0 straight sections. Reduce meanders or adjust bend start/end settings." 

79 ) 

80 

81 straight_comp = None 

82 if num_straights > 0: 

83 length_per_one_straight = ( 

84 length - meanders * bend.info["length"] 

85 ) / num_straights 

86 

87 if length_per_one_straight <= 0: 

88 raise ValueError( 

89 f"Resonator length {length} is too short for {meanders} meanders with current bend spec {bend}. " 

90 f"Increase length, reduce meanders, or change the bend spec." 

91 ) 

92 

93 straight_comp = straight( 

94 length=length_per_one_straight, 

95 cross_section=cross_section, 

96 ) 

97 

98 # Route meandering quarter-wave resonator 

99 previous_port = None 

100 first_ref = None 

101 last_ref = None 

102 

103 for i in range(meanders): 

104 # Determine if we should add a straight before this bend 

105 if i == 0 and start_with_bend: 

106 # First element is a bend 

107 bend_ref = c.add_ref(bend) 

108 if i % 2 == 0: 

109 bend_ref.mirror() 

110 bend_ref.rotate(90) 

111 first_ref = bend_ref 

112 previous_port = bend_ref.ports["o2"] 

113 else: 

114 if straight_comp is None: 

115 raise ValueError("straight_comp is required but not initialized.") 

116 straight_ref = c.add_ref(straight_comp) 

117 if i == 0: 

118 first_ref = straight_ref 

119 else: 

120 straight_ref.connect("o1", previous_port) 

121 

122 bend_ref = c.add_ref(bend) 

123 if i % 2 == 0: 

124 bend_ref.mirror() 

125 bend_ref.rotate(90) 

126 

127 bend_ref.connect("o1", straight_ref.ports["o2"]) 

128 previous_port = bend_ref.ports["o2"] 

129 

130 last_ref = bend_ref 

131 

132 # Final section 

133 if not end_with_bend: 

134 if straight_comp is None: 

135 raise ValueError("straight_comp is required but not initialized.") 

136 final_straight_ref = c.add_ref(straight_comp) 

137 if previous_port: 

138 final_straight_ref.connect("o1", previous_port) 

139 last_ref = final_straight_ref 

140 if first_ref is None: 

141 first_ref = final_straight_ref 

142 

143 if first_ref is None or last_ref is None: 

144 raise ValueError("Resonator could not be generated correctly.") 

145 

146 actual_length = meanders * bend.info["length"] 

147 if num_straights > 0: 

148 if straight_comp is None: 

149 raise ValueError("straight_comp is required but not initialized.") 

150 actual_length += num_straights * straight_comp.info["length"] 

151 

152 # Etch at the open end 

153 if open_end or open_start: 

154 cross_section_etch_section = get_etch_section(cross_section) 

155 

156 open_etch_comp = gf.c.rectangle( 

157 size=( 

158 cross_section_etch_section.width, 

159 2 * cross_section_etch_section.width + cross_section.width, 

160 ), 

161 layer=cross_section_etch_section.layer, 

162 centered=True, 

163 port_type="optical", 

164 port_orientations=(0, 180), 

165 ) 

166 

167 def _add_etch_at_port(port_name, ref_port, output_port): 

168 """Helper function to add etch at a specific port.""" 

169 open_etch = c.add_ref(open_etch_comp) 

170 open_etch.connect( 

171 port_name, 

172 ref_port, 

173 allow_width_mismatch=True, 

174 allow_layer_mismatch=True, 

175 ) 

176 c.add_port( 

177 output_port, port=open_etch.ports[output_port], port_type="placement" 

178 ) 

179 

180 if open_end: 

181 _add_etch_at_port("o1", last_ref.ports["o2"], "o2") 

182 if open_start: 

183 _add_etch_at_port("o2", first_ref.ports["o1"], "o1") 

184 

185 if not open_end: 

186 c.add_port("o2", port=last_ref.ports["o2"]) 

187 

188 if not open_start: 

189 c.add_port("o1", port=first_ref.ports["o1"]) 

190 

191 # Add metadata 

192 c.info["length"] = actual_length 

193 c.info["resonator_type"] = "quarter_wave" 

194 c.info["cross_section"] = cross_section.name 

195 # c.info["frequency_estimate"] = ( 

196 # 3e8 / (4 * length * 1e-6) / 1e9 

197 # ) # GHz, rough estimate 

198 

199 return c 

200 

201 

202# A quarter-wave resonator is shorted at one end and has maximum electric field 

203# at the open end, making it suitable for capacitive coupling. 

204resonator_quarter_wave = partial(resonator, open_start=False, open_end=True) 

205# A half-wave resonator is open at both ends 

206resonator_half_wave = partial(resonator, open_start=True, open_end=True) 

207 

208# Quarter-wave resonator starting with a bend 

209resonator_quarter_wave_bend_start = partial( 

210 resonator_quarter_wave, start_with_bend=True 

211) 

212# Half-wave resonator starting with a bend 

213resonator_half_wave_bend_start = partial(resonator_half_wave, start_with_bend=True) 

214 

215# Resonator ending with a bend 

216resonator_quarter_wave_bend_end = partial(resonator_quarter_wave, end_with_bend=True) 

217resonator_half_wave_bend_end = partial(resonator_half_wave, end_with_bend=True) 

218 

219# Both 

220resonator_quarter_wave_bend_both = partial( 

221 resonator_quarter_wave, start_with_bend=True, end_with_bend=True 

222) 

223resonator_half_wave_bend_both = partial( 

224 resonator_half_wave, start_with_bend=True, end_with_bend=True 

225) 

226 

227 

228@gf.cell(tags=("resonators", "couplers")) 

229def resonator_coupled( 

230 length: float = 4000.0, 

231 meanders: int = 6, 

232 bend_spec: ComponentSpec = bend_circular, 

233 cross_section: CrossSectionSpec = "cpw", 

234 *, 

235 start_with_bend: bool = False, 

236 end_with_bend: bool = False, 

237 open_start: bool = True, 

238 open_end: bool = False, 

239 cross_section_non_resonator: CrossSectionSpec = "cpw", 

240 coupling_straight_length: float = 200.0, 

241 coupling_gap: float = 20.0, 

242) -> Component: 

243 """Creates a meandering coplanar waveguide resonator with a coupling waveguide. 

244 

245 This component combines a resonator with a parallel coupling waveguide placed 

246 at a specified gap for proximity coupling. Similar to the design described in 

247 :cite:`besedinQualityFactorTransmission2018a`. 

248 

249 .. svgbob:: 

250 

251 coupling_o1 ─────────────── coupling_o2 

252 coupling_gap 

253 resonator_o1 ───────┐ 

254 

255 ┌───────────────────┘ 

256 

257 └───────────────────┐ 

258 

259 ┌───────────────────┘ 

260 

261 └──── resonator_o2 

262 

263 Args: 

264 length: Length of the resonator in μm. 

265 meanders: Number of meander sections to fit the resonator in a compact area. 

266 bend_spec: Specification for the bend component used in meanders. 

267 cross_section: Cross-section specification for the resonator. 

268 start_with_bend: If True, starts the resonator with a bend. 

269 end_with_bend: If True, ends the resonator with a bend. 

270 open_start: If True, adds an etch section at the start of the resonator. 

271 open_end: If True, adds an etch section at the end of the resonator. 

272 cross_section_non_resonator: Cross-section specification for the coupling waveguide. 

273 coupling_straight_length: Length of the coupling waveguide section in μm. 

274 coupling_gap: Gap between the resonator and coupling waveguide in μm. 

275 Measured from edges of the center conductors. 

276 

277 Returns: 

278 Component: A gdsfactory component with meandering resonator and coupling waveguide. 

279 """ 

280 c = Component() 

281 

282 resonator_ref = c.add_ref( 

283 resonator( 

284 length=length, 

285 meanders=meanders, 

286 bend_spec=bend_spec, 

287 cross_section=cross_section, 

288 start_with_bend=start_with_bend, 

289 end_with_bend=end_with_bend, 

290 open_start=open_start, 

291 open_end=open_end, 

292 ) 

293 ) 

294 

295 cross_section_obj = gf.get_cross_section(cross_section_non_resonator) 

296 

297 coupling_wg = straight( 

298 length=coupling_straight_length, 

299 cross_section=cross_section_obj, 

300 ) 

301 coupling_ref = c.add_ref(coupling_wg) 

302 

303 # Position coupling waveguide parallel to resonator with specified gap 

304 coupling_ref.movey(coupling_gap + cross_section_obj.width) 

305 

306 coupling_ref.xmin = resonator_ref["o1"].x # Align left edges 

307 

308 for port in resonator_ref.ports: 

309 port_type = ( 

310 "placement" 

311 if ((port.name == "o1" and open_start) or (port.name == "o2" and open_end)) 

312 else "optical" 

313 ) 

314 c.add_port(f"resonator_{port.name}", port=port, port_type=port_type) 

315 

316 for port in coupling_ref.ports: 

317 c.add_port(f"coupling_{port.name}", port=port) 

318 

319 c.info += resonator_ref.cell.info 

320 c.info["coupling_length"] = coupling_straight_length 

321 c.info["coupling_gap"] = coupling_gap 

322 

323 return c 

324 

325 

326@gf.cell(tags=("resonators", "couplers")) 

327def quarter_wave_resonator_coupled( 

328 length: float = 4000.0, 

329 meanders: int = 6, 

330 bend_spec: ComponentSpec = bend_circular, 

331 cross_section: CrossSectionSpec = "cpw", 

332 *, 

333 start_with_bend: bool = False, 

334 end_with_bend: bool = False, 

335 open_start: bool = True, 

336 open_end: bool = False, 

337 cross_section_non_resonator: CrossSectionSpec = "cpw", 

338 coupling_straight_length: float = 200.0, 

339 coupling_gap: float = 20.0, 

340) -> Component: 

341 """Creates a quarter-wave resonator with a coupling waveguide. 

342 

343 Uses :func:`~qpdk.cells.resonator.resonator_coupled` as the basis but 

344 removes the shorted end port from the output ports. 

345 

346 .. svgbob:: 

347 

348 coupling_o1 ─────────────── coupling_o2 

349 coupling_gap 

350 resonator_o1 ───────┐ 

351 

352 ┌───────────────────┘ 

353 

354 └───────────────────┐ 

355 

356 ┌───────────────────┘ 

357 

358 └──── (shorted, no port) 

359 

360 Args: 

361 length: Length of the resonator in μm. 

362 meanders: Number of meander sections to fit the resonator in a compact area. 

363 bend_spec: Specification for the bend component used in meanders. 

364 cross_section: Cross-section specification for the resonator. 

365 start_with_bend: If True, starts the resonator with a bend. 

366 end_with_bend: If True, ends the resonator with a bend. 

367 open_start: If True, adds an etch section at the start of the resonator. 

368 open_end: If True, adds an etch section at the end of the resonator. 

369 cross_section_non_resonator: Cross-section specification for the coupling waveguide. 

370 coupling_straight_length: Length of the coupling waveguide section in μm. 

371 coupling_gap: Gap between the resonator and coupling waveguide in μm. 

372 

373 Returns: 

374 The coupled quarter-wave resonator component. 

375 """ 

376 c = Component() 

377 

378 res_ref = c << resonator_coupled( 

379 length=length, 

380 meanders=meanders, 

381 bend_spec=bend_spec, 

382 cross_section=cross_section, 

383 start_with_bend=start_with_bend, 

384 end_with_bend=end_with_bend, 

385 open_start=open_start, 

386 open_end=open_end, 

387 cross_section_non_resonator=cross_section_non_resonator, 

388 coupling_straight_length=coupling_straight_length, 

389 coupling_gap=coupling_gap, 

390 ) 

391 movement = np.array(res_ref.ports["coupling_o1"].center) 

392 res_ref.move(tuple(-movement)) 

393 

394 for port in res_ref.ports: 

395 if port.name != "resonator_o2": # Skip the shorted end port 

396 c.add_port(port=port) 

397 

398 return c 

399 

400 

401if __name__ == "__main__": 

402 show_components( 

403 resonator, 

404 resonator_quarter_wave, 

405 resonator_half_wave, 

406 resonator_quarter_wave_bend_start, 

407 resonator_quarter_wave_bend_both, 

408 resonator_coupled, 

409 partial( 

410 resonator_coupled, 

411 length=2000, 

412 meanders=4, 

413 open_start=False, 

414 open_end=True, 

415 ), 

416 )