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

121 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-14 10:27 +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 

14 

15 

16@gf.cell 

17def resonator( 

18 length: float = 4000.0, 

19 meanders: int = 6, 

20 bend_spec: ComponentSpec = bend_circular, 

21 cross_section: CrossSectionSpec = "cpw", 

22 *, 

23 start_with_bend: bool = False, 

24 end_with_bend: bool = False, 

25 open_start: bool = True, 

26 open_end: bool = False, 

27) -> Component: 

28 """Creates a meandering coplanar waveguide resonator. 

29 

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

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

32 

33 .. svgbob:: 

34 

35 o1 ─────┐ 

36 

37 ┌───────┘ 

38 

39 └───────┐ 

40 

41 ┌───────┘ 

42 

43 └────── o2 

44 

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

46 

47 Args: 

48 length: Length of the resonator in μm. 

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

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

51 cross_section: Cross-section specification for the resonator. 

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

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

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

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

56 

57 Returns: 

58 Component: A gdsfactory component with meandering resonator geometry. 

59 """ 

60 c = Component() 

61 cross_section = gf.get_cross_section(cross_section) 

62 bend = gf.get_component( 

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

64 ) 

65 

66 num_straights = meanders + 1 

67 if start_with_bend: 

68 num_straights -= 1 

69 if end_with_bend: 

70 num_straights -= 1 

71 

72 if num_straights < 0: 

73 raise ValueError( 

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

75 ) 

76 

77 straight_comp = None 

78 if num_straights > 0: 

79 length_per_one_straight = ( 

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

81 ) / num_straights 

82 

83 if length_per_one_straight <= 0: 

84 raise ValueError( 

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

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

87 ) 

88 

89 straight_comp = straight( 

90 length=length_per_one_straight, 

91 cross_section=cross_section, 

92 ) 

93 

94 # Route meandering quarter-wave resonator 

95 previous_port = None 

96 first_ref = None 

97 last_ref = None 

98 

99 for i in range(meanders): 

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

101 if i == 0 and start_with_bend: 

102 # First element is a bend 

103 bend_ref = c.add_ref(bend) 

104 if i % 2 == 0: 

105 bend_ref.mirror() 

106 bend_ref.rotate(90) 

107 first_ref = bend_ref 

108 previous_port = bend_ref.ports["o2"] 

109 else: 

110 if straight_comp is None: 

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

112 straight_ref = c.add_ref(straight_comp) 

113 if i == 0: 

114 first_ref = straight_ref 

115 else: 

116 straight_ref.connect("o1", previous_port) 

117 

118 bend_ref = c.add_ref(bend) 

119 if i % 2 == 0: 

120 bend_ref.mirror() 

121 bend_ref.rotate(90) 

122 

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

124 previous_port = bend_ref.ports["o2"] 

125 

126 last_ref = bend_ref 

127 

128 # Final section 

129 if not end_with_bend: 

130 if straight_comp is None: 

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

132 final_straight_ref = c.add_ref(straight_comp) 

133 if previous_port: 

134 final_straight_ref.connect("o1", previous_port) 

135 last_ref = final_straight_ref 

136 if first_ref is None: 

137 first_ref = final_straight_ref 

138 

139 if first_ref is None or last_ref is None: 

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

141 

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

143 if num_straights > 0: 

144 if straight_comp is None: 

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

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

147 

148 # Etch at the open end 

149 if open_end or open_start: 

150 cross_section_etch_section = next( 

151 s 

152 for s in gf.get_cross_section(cross_section).sections 

153 if s.name and "etch_offset" in s.name 

154 ) 

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 

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 Args: 

250 length: Length of the resonator in μm. 

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

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

253 cross_section: Cross-section specification for the resonator. 

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

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

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

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

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

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

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

261 Measured from edges of the center conductors. 

262 

263 Returns: 

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

265 """ 

266 c = Component() 

267 

268 resonator_ref = c.add_ref( 

269 resonator( 

270 length=length, 

271 meanders=meanders, 

272 bend_spec=bend_spec, 

273 cross_section=cross_section, 

274 start_with_bend=start_with_bend, 

275 end_with_bend=end_with_bend, 

276 open_start=open_start, 

277 open_end=open_end, 

278 ) 

279 ) 

280 

281 cross_section_obj = gf.get_cross_section(cross_section_non_resonator) 

282 

283 coupling_wg = straight( 

284 length=coupling_straight_length, 

285 cross_section=cross_section_obj, 

286 ) 

287 coupling_ref = c.add_ref(coupling_wg) 

288 

289 # Position coupling waveguide parallel to resonator with specified gap 

290 coupling_ref.movey(coupling_gap + cross_section_obj.width) 

291 

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

293 

294 for port in resonator_ref.ports: 

295 port_type = ( 

296 "placement" 

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

298 else "optical" 

299 ) 

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

301 

302 for port in coupling_ref.ports: 

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

304 

305 c.info += resonator_ref.cell.info 

306 c.info["coupling_length"] = coupling_straight_length 

307 c.info["coupling_gap"] = coupling_gap 

308 

309 return c 

310 

311 

312@gf.cell 

313def quarter_wave_resonator_coupled( 

314 length: float = 4000.0, 

315 meanders: int = 6, 

316 bend_spec: ComponentSpec = bend_circular, 

317 cross_section: CrossSectionSpec = "cpw", 

318 *, 

319 start_with_bend: bool = False, 

320 end_with_bend: bool = False, 

321 open_start: bool = True, 

322 open_end: bool = False, 

323 cross_section_non_resonator: CrossSectionSpec = "cpw", 

324 coupling_straight_length: float = 200.0, 

325 coupling_gap: float = 20.0, 

326) -> Component: 

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

328 

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

330 removes the shorted end port from the output ports. 

331 

332 Args: 

333 length: Length of the resonator in μm. 

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

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

336 cross_section: Cross-section specification for the resonator. 

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

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

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

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

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

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

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

344 """ 

345 c = Component() 

346 

347 res_ref = c << resonator_coupled( 

348 length=length, 

349 meanders=meanders, 

350 bend_spec=bend_spec, 

351 cross_section=cross_section, 

352 start_with_bend=start_with_bend, 

353 end_with_bend=end_with_bend, 

354 open_start=open_start, 

355 open_end=open_end, 

356 cross_section_non_resonator=cross_section_non_resonator, 

357 coupling_straight_length=coupling_straight_length, 

358 coupling_gap=coupling_gap, 

359 ) 

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

361 res_ref.move(tuple(-movement)) 

362 

363 for port in res_ref.ports: 

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

365 c.add_port(port=port) 

366 

367 return c 

368 

369 

370if __name__ == "__main__": 

371 show_components( 

372 resonator, 

373 resonator_quarter_wave, 

374 resonator_half_wave, 

375 resonator_quarter_wave_bend_start, 

376 resonator_quarter_wave_bend_both, 

377 resonator_coupled, 

378 partial( 

379 resonator_coupled, 

380 length=2000, 

381 meanders=4, 

382 open_start=False, 

383 open_end=True, 

384 ), 

385 )