Coverage for qpdk / cells / inductor.py: 92%

120 statements  

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

1"""Inductor and lumped-element resonator components.""" 

2 

3from __future__ import annotations 

4 

5from math import ceil, floor 

6 

7import gdsfactory as gf 

8from gdsfactory.component import Component 

9from gdsfactory.typings import CrossSectionSpec, LayerSpec 

10 

11from qpdk.cells.waveguides import straight 

12from qpdk.tech import ( 

13 get_etch_section, 

14 meander_inductor_cross_section, 

15) 

16 

17 

18@gf.cell(tags=("inductors",)) 

19def meander_inductor( 

20 n_turns: int = 5, 

21 turn_length: float = 200.0, 

22 cross_section: CrossSectionSpec = meander_inductor_cross_section, 

23 wire_gap: float | None = None, 

24 etch_bbox_margin: float = 2.0, 

25 add_etch: bool = True, 

26) -> Component: 

27 r"""Creates a meander inductor with Manhattan routing using a narrow wire. 

28 

29 The inductor consists of multiple horizontal runs connected by short 

30 vertical segments at alternating ends, forming a serpentine (meander) 

31 path. The total inductance is dominated by kinetic inductance for 

32 superconducting thin films. 

33 

34 .. svgbob:: 

35 

36 o1 ─────────────────────┐ 

37 

38 ┌───────────────────────┘ 

39 

40 └───────────────────────┐ 

41 

42 ┌───────────────────────┘ 

43 

44 └────────────────────── o2 

45 

46 Similar structures are described in 

47 :cite:`kimThinfilmSuperconductingResonator2011,chenCompactInductorcapacitorResonators2023`. 

48 

49 Args: 

50 n_turns: Number of horizontal meander runs (must be >= 1). 

51 turn_length: Length of each horizontal run in µm. 

52 cross_section: Cross-section specification for the meander wire. 

53 The center conductor width and etch gap are derived from this 

54 specification. The meander's vertical pitch is set to ensure that 

55 the etched regions of adjacent runs do not overlap, maintaining 

56 the characteristic impedance of each run. Specifically, the pitch 

57 is calculated as :math:`w + 2g`, where :math:`w` is the wire width 

58 and :math:`g` is the etch gap. 

59 wire_gap: Optional explicit gap between adjacent inductor runs in µm. 

60 If None (default), it's inferred as 2x the etch gap from the cross-section. 

61 etch_bbox_margin: Extra margin around the inductor for the etch bounding box in µm. 

62 This margin is added in addition to the etch region defined in the cross-section. 

63 add_etch: Whether to add the etch bounding box. Defaults to True. 

64 

65 Returns: 

66 Component: A gdsfactory component with the meander inductor geometry 

67 and two ports ('o1' and 'o2'). 

68 

69 Raises: 

70 ValueError: If `n_turns` < 1 or `turn_length` <= 0. 

71 """ 

72 if n_turns < 1: 

73 raise ValueError("Must have at least 1 turn") 

74 if turn_length <= 0: 

75 raise ValueError(f"turn_length must be positive, got {turn_length}") 

76 

77 xs = gf.get_cross_section(cross_section) 

78 wire_width = xs.width 

79 layer = xs.layer 

80 

81 # Infer etch parameters and spacing from cross section 

82 try: 

83 etch_section = get_etch_section(xs) 

84 etch_layer = etch_section.layer 

85 except ValueError: 

86 etch_section = None 

87 etch_layer = None 

88 

89 # For CPW-like structures, we assume a pitch that allows for non-overlapping etches 

90 # i.e. pitch = width + 2 * gap, which means wire_gap = 2 * etch_width 

91 # If no etch section is found, we use a default gap equal to the wire width 

92 if wire_gap is None: 

93 wire_gap = 2 * etch_section.width if etch_section is not None else wire_width 

94 

95 c = Component() 

96 pitch = wire_width + wire_gap 

97 total_height = n_turns * wire_width + max(0, n_turns - 1) * wire_gap 

98 

99 for i in range(n_turns): 

100 y0 = i * pitch 

101 c.add_polygon( 

102 [ 

103 (0, y0), 

104 (turn_length, y0), 

105 (turn_length, y0 + wire_width), 

106 (0, y0 + wire_width), 

107 ], 

108 layer=layer, 

109 ) 

110 

111 for i in range(n_turns - 1): 

112 y0 = i * pitch + wire_width 

113 y1 = (i + 1) * pitch 

114 if i % 2 == 0: 

115 c.add_polygon( 

116 [ 

117 (turn_length - wire_width, y0), 

118 (turn_length, y0), 

119 (turn_length, y1), 

120 (turn_length - wire_width, y1), 

121 ], 

122 layer=layer, 

123 ) 

124 else: 

125 c.add_polygon( 

126 [(0, y0), (wire_width, y0), (wire_width, y1), (0, y1)], 

127 layer=layer, 

128 ) 

129 

130 if add_etch and etch_section is not None: 

131 # Extra margin on top of the implicit etch margin from the cross-section 

132 margin = etch_section.width + etch_bbox_margin 

133 c.add_polygon( 

134 [ 

135 (-margin, -margin), 

136 (turn_length + margin, -margin), 

137 (turn_length + margin, total_height + margin), 

138 (-margin, total_height + margin), 

139 ], 

140 layer=etch_layer, 

141 ) 

142 

143 c_metal = gf.boolean( 

144 A=c, B=c, operation="or", layer=layer, layer1=layer, layer2=layer 

145 ) 

146 c_etch = gf.boolean( 

147 A=c, 

148 B=c_metal, 

149 operation="A-B", 

150 layer=etch_layer, 

151 layer1=etch_layer, 

152 layer2=layer, 

153 ) 

154 c = gf.Component() 

155 c.absorb(c << c_metal) 

156 c.absorb(c << c_etch) 

157 

158 c.add_port( 

159 name="o1", 

160 center=(0, wire_width / 2), 

161 width=wire_width, 

162 orientation=180, 

163 layer=layer, 

164 cross_section=xs, 

165 ) 

166 

167 last_run_center_y = (n_turns - 1) * pitch + wire_width / 2 

168 if n_turns % 2 == 1: 

169 c.add_port( 

170 name="o2", 

171 center=(turn_length, last_run_center_y), 

172 width=wire_width, 

173 orientation=0, 

174 layer=layer, 

175 cross_section=xs, 

176 ) 

177 else: 

178 c.add_port( 

179 name="o2", 

180 center=(0, last_run_center_y), 

181 width=wire_width, 

182 orientation=180, 

183 layer=layer, 

184 cross_section=xs, 

185 ) 

186 

187 c.move((-turn_length / 2, -total_height / 2)) 

188 

189 total_wire_length = n_turns * turn_length + max(0, n_turns - 1) * wire_gap 

190 c.info["total_wire_length"] = total_wire_length 

191 c.info["n_squares"] = total_wire_length / wire_width 

192 c.info["cross_section"] = xs.name 

193 

194 return c 

195 

196 

197@gf.cell(tags=("resonators", "inductors", "capacitors")) 

198def lumped_element_resonator( 

199 fingers: int = 20, 

200 finger_length: float = 20.0, 

201 finger_gap: float = 2.0, 

202 finger_thickness: float = 5.0, 

203 n_turns: int = 15, 

204 bus_bar_spacing: float = 4.0, 

205 cross_section: CrossSectionSpec = meander_inductor_cross_section, 

206 etch_bbox_margin: float = 2.0, 

207) -> Component: 

208 r"""Creates a lumped-element resonator combining an interdigital capacitor and a meander inductor. 

209 

210 The resonator consists of an interdigital capacitor section (providing 

211 capacitance) connected in parallel with a meander inductor section 

212 (providing inductance) via shared bus bars. The resonance frequency is: 

213 

214 .. math:: 

215 

216 f_r = \frac{1}{2\pi\sqrt{LC}} 

217 

218 .. svgbob:: 

219 

220 +-----------+ 

221 | Capacitor | 

222 o1 --+ (IDC) +-- o2 

223 | | 

224 | Inductor | 

225 | (Meander) | 

226 +-----------+ 

227 

228 Similar structures are described in 

229 :cite:`kimThinfilmSuperconductingResonator2011,chenCompactInductorcapacitorResonators2023`. 

230 

231 Args: 

232 fingers: Number of interdigital capacitor fingers. 

233 finger_length: Length of each capacitor finger in µm. 

234 finger_gap: Gap between adjacent capacitor fingers in µm. 

235 finger_thickness: Width of each capacitor finger and bus bar in µm. 

236 n_turns: Number of horizontal meander inductor runs. 

237 bus_bar_spacing: Vertical spacing between the capacitor and inductor sections in µm. 

238 cross_section: Cross-section specification for the inductor and ports. 

239 etch_bbox_margin: Margin around the structure for the etch region in µm. 

240 

241 Returns: 

242 Component: A gdsfactory component with the lumped-element resonator 

243 geometry and two ports ('o1' and 'o2'). 

244 

245 Raises: 

246 ValueError: If `n_turns` is even, `bus_bar_spacing` <= 0, or if the 

247 resultant meander run length is non-positive. 

248 """ 

249 if n_turns % 2 == 0: 

250 raise ValueError( 

251 "n_turns must be odd so that the meander path spans from the " 

252 "left bus bar to the right bus bar" 

253 ) 

254 if bus_bar_spacing <= 0: 

255 raise ValueError( 

256 "bus_bar_spacing must be positive to electrically isolate the " 

257 "last inductor run from the full-width bus bar sections" 

258 ) 

259 

260 xs = gf.get_cross_section(cross_section) 

261 wire_width = xs.width 

262 etch_section = get_etch_section(xs) 

263 wire_gap = 2 * etch_section.width 

264 layer = xs.layer 

265 etch_layer = etch_section.layer 

266 etch_width = etch_section.width 

267 

268 cap_width = 2 * finger_thickness + finger_length + finger_gap 

269 short_length = cap_width - 4 * wire_width 

270 if short_length <= 0: 

271 raise ValueError( 

272 f"Meander run length would be non-positive ({short_length} µm). " 

273 "Increase finger_length/finger_gap/finger_thickness or decrease wire_width." 

274 ) 

275 

276 c = Component() 

277 

278 # 1. Inductor part 

279 ind = c << meander_inductor( 

280 n_turns=n_turns, 

281 turn_length=short_length, 

282 cross_section=cross_section, 

283 etch_bbox_margin=0, 

284 ) 

285 

286 cap_height = fingers * finger_thickness + (fingers - 1) * finger_gap 

287 ind_height = ind.size_info.height 

288 total_internal_height = cap_height + bus_bar_spacing + ind_height 

289 

290 # Center inductor at the bottom of the internal area 

291 ind.dcenter = (0, -total_internal_height / 2 + ind_height / 2) 

292 

293 # 2. Capacitor part (fingers and bus bars) 

294 cap_y0 = -total_internal_height / 2 + ind_height + bus_bar_spacing 

295 

296 x_left_inner = -cap_width / 2 + finger_thickness 

297 x_right_inner = cap_width / 2 - finger_thickness 

298 

299 _draw_interdigital_fingers_left( 

300 c, 

301 layer, 

302 x_inner=x_left_inner, 

303 y_offset=cap_y0, 

304 fingers=fingers, 

305 finger_length=finger_length, 

306 finger_gap=finger_gap, 

307 thickness=finger_thickness, 

308 ) 

309 _draw_interdigital_fingers_right( 

310 c, 

311 layer, 

312 x_inner=x_right_inner, 

313 y_offset=cap_y0, 

314 fingers=fingers, 

315 finger_length=finger_length, 

316 finger_gap=finger_gap, 

317 thickness=finger_thickness, 

318 ) 

319 

320 # 3. Bus bars connecting everything 

321 # Small overlap to ensure solid connectivity 

322 overlap = 0.1 

323 

324 # Left bus bar: connects to turn 0 (bottom) 

325 # Use the metal bottom edge of the inductor, not the component bbox bottom (which includes etch) 

326 left_bb_ymin = ind.ports["o1"].center[1] - wire_width / 2 

327 c.add_polygon( 

328 [ 

329 (-cap_width / 2, left_bb_ymin), 

330 (-cap_width / 2 + wire_width, left_bb_ymin), 

331 (-cap_width / 2 + wire_width, cap_y0 + overlap), 

332 (-cap_width / 2, cap_y0 + overlap), 

333 ], 

334 layer=layer, 

335 ) 

336 # Top wide part 

337 c.add_polygon( 

338 [ 

339 (-cap_width / 2, cap_y0), 

340 (-cap_width / 2 + finger_thickness, cap_y0), 

341 (-cap_width / 2 + finger_thickness, total_internal_height / 2), 

342 (-cap_width / 2, total_internal_height / 2), 

343 ], 

344 layer=layer, 

345 ) 

346 

347 # Right bus bar: connects to turn n_turns-1 (top) 

348 # Redundant section below top run is removed 

349 right_bb_ymin = ind.ports["o2"].center[1] - wire_width / 2 

350 c.add_polygon( 

351 [ 

352 (cap_width / 2 - wire_width, right_bb_ymin), 

353 (cap_width / 2, right_bb_ymin), 

354 (cap_width / 2, cap_y0 + overlap), 

355 (cap_width / 2 - wire_width, cap_y0 + overlap), 

356 ], 

357 layer=layer, 

358 ) 

359 # Top wide part 

360 c.add_polygon( 

361 [ 

362 (cap_width / 2 - finger_thickness, cap_y0), 

363 (cap_width / 2, cap_y0), 

364 (cap_width / 2, total_internal_height / 2), 

365 (cap_width / 2 - finger_thickness, total_internal_height / 2), 

366 ], 

367 layer=layer, 

368 ) 

369 

370 # Tabs to inductor 

371 # Left tab connects o1 to the left bus bar 

372 c.add_polygon( 

373 [ 

374 ( 

375 -cap_width / 2 + wire_width - overlap, 

376 ind.ports["o1"].center[1] - wire_width / 2, 

377 ), 

378 ( 

379 ind.ports["o1"].center[0] + overlap, 

380 ind.ports["o1"].center[1] - wire_width / 2, 

381 ), 

382 ( 

383 ind.ports["o1"].center[0] + overlap, 

384 ind.ports["o1"].center[1] + wire_width / 2, 

385 ), 

386 ( 

387 -cap_width / 2 + wire_width - overlap, 

388 ind.ports["o1"].center[1] + wire_width / 2, 

389 ), 

390 ], 

391 layer=layer, 

392 ) 

393 # Right tab connects o2 to the right bus bar 

394 c.add_polygon( 

395 [ 

396 ( 

397 ind.ports["o2"].center[0] - overlap, 

398 ind.ports["o2"].center[1] - wire_width / 2, 

399 ), 

400 ( 

401 cap_width / 2 - wire_width + overlap, 

402 ind.ports["o2"].center[1] - wire_width / 2, 

403 ), 

404 ( 

405 cap_width / 2 - wire_width + overlap, 

406 ind.ports["o2"].center[1] + wire_width / 2, 

407 ), 

408 ( 

409 ind.ports["o2"].center[0] - overlap, 

410 ind.ports["o2"].center[1] + wire_width / 2, 

411 ), 

412 ], 

413 layer=layer, 

414 ) 

415 

416 # 4. Etch bounding box 

417 margin = etch_width + etch_bbox_margin 

418 c.add_polygon( 

419 [ 

420 (-cap_width / 2 - margin, -total_internal_height / 2 - margin), 

421 (cap_width / 2 + margin, -total_internal_height / 2 - margin), 

422 (cap_width / 2 + margin, total_internal_height / 2 + margin), 

423 (-cap_width / 2 - margin, total_internal_height / 2 + margin), 

424 ], 

425 layer=etch_layer, 

426 ) 

427 

428 # 5. Ports 

429 straight_out = straight(length=margin, cross_section=cross_section) 

430 center_y = 0 

431 straight_left = c.add_ref(straight_out).move((-cap_width / 2 - margin, center_y)) 

432 straight_right = c.add_ref(straight_out).move((cap_width / 2, center_y)) 

433 

434 c_metal = gf.boolean( 

435 A=c, B=c, operation="or", layer=layer, layer1=layer, layer2=xs.layer 

436 ) 

437 c_etch = gf.boolean( 

438 A=c, 

439 B=c_metal, 

440 operation="A-B", 

441 layer=etch_layer, 

442 layer1=etch_layer, 

443 layer2=layer, 

444 ) 

445 

446 c = gf.Component() 

447 c.absorb(c << c_metal) 

448 c.absorb(c << c_etch) 

449 

450 c.add_port( 

451 name="o1", 

452 port=straight_left.ports["o1"], 

453 layer=layer, 

454 port_type="electrical", 

455 cross_section=xs, 

456 ) 

457 c.add_port( 

458 name="o2", 

459 port=straight_right.ports["o2"], 

460 layer=layer, 

461 port_type="electrical", 

462 cross_section=xs, 

463 ) 

464 

465 c.info["total_wire_length"] = ( 

466 2 * wire_width + n_turns * short_length + max(0, n_turns - 1) * wire_gap 

467 ) 

468 c.info["inductor_n_squares"] = c.info["total_wire_length"] / wire_width 

469 c.info["capacitor_fingers"] = fingers 

470 c.info["capacitor_finger_length"] = finger_length 

471 

472 return c 

473 

474 

475def _draw_interdigital_fingers_left( 

476 c: Component, 

477 layer: LayerSpec, 

478 x_inner: float, 

479 y_offset: float, 

480 fingers: int, 

481 finger_length: float, 

482 finger_gap: float, 

483 thickness: float, 

484) -> None: 

485 """Draw left-side interdigital capacitor fingers (even-indexed, extending right).""" 

486 for i in range(ceil(fingers / 2)): 

487 finger_idx = 2 * i 

488 y0 = y_offset + finger_idx * (thickness + finger_gap) 

489 c.add_polygon( 

490 [ 

491 (x_inner, y0), 

492 (x_inner + finger_length, y0), 

493 (x_inner + finger_length, y0 + thickness), 

494 (x_inner, y0 + thickness), 

495 ], 

496 layer=layer, 

497 ) 

498 

499 

500def _draw_interdigital_fingers_right( 

501 c: Component, 

502 layer: LayerSpec, 

503 x_inner: float, 

504 y_offset: float, 

505 fingers: int, 

506 finger_length: float, 

507 finger_gap: float, 

508 thickness: float, 

509) -> None: 

510 """Draw right-side interdigital capacitor fingers (odd-indexed, extending left).""" 

511 for i in range(floor(fingers / 2)): 

512 finger_idx = 1 + 2 * i 

513 y0 = y_offset + finger_idx * (thickness + finger_gap) 

514 c.add_polygon( 

515 [ 

516 (x_inner - finger_length, y0), 

517 (x_inner, y0), 

518 (x_inner, y0 + thickness), 

519 (x_inner - finger_length, y0 + thickness), 

520 ], 

521 layer=layer, 

522 ) 

523 

524 

525if __name__ == "__main__": 

526 from qpdk.helper import show_components 

527 

528 show_components( 

529 meander_inductor, 

530 lumped_element_resonator, 

531 )