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
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-02 17:50 +0000
1"""Inductor and lumped-element resonator components."""
3from __future__ import annotations
5from math import ceil, floor
7import gdsfactory as gf
8from gdsfactory.component import Component
9from gdsfactory.typings import CrossSectionSpec, LayerSpec
11from qpdk.cells.waveguides import straight
12from qpdk.tech import (
13 get_etch_section,
14 meander_inductor_cross_section,
15)
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.
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.
34 .. svgbob::
36 o1 ─────────────────────┐
37 │
38 ┌───────────────────────┘
39 │
40 └───────────────────────┐
41 │
42 ┌───────────────────────┘
43 │
44 └────────────────────── o2
46 Similar structures are described in
47 :cite:`kimThinfilmSuperconductingResonator2011,chenCompactInductorcapacitorResonators2023`.
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.
65 Returns:
66 Component: A gdsfactory component with the meander inductor geometry
67 and two ports ('o1' and 'o2').
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}")
77 xs = gf.get_cross_section(cross_section)
78 wire_width = xs.width
79 layer = xs.layer
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
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
95 c = Component()
96 pitch = wire_width + wire_gap
97 total_height = n_turns * wire_width + max(0, n_turns - 1) * wire_gap
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 )
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 )
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 )
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)
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 )
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 )
187 c.move((-turn_length / 2, -total_height / 2))
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
194 return c
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.
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:
214 .. math::
216 f_r = \frac{1}{2\pi\sqrt{LC}}
218 .. svgbob::
220 +-----------+
221 | Capacitor |
222 o1 --+ (IDC) +-- o2
223 | |
224 | Inductor |
225 | (Meander) |
226 +-----------+
228 Similar structures are described in
229 :cite:`kimThinfilmSuperconductingResonator2011,chenCompactInductorcapacitorResonators2023`.
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.
241 Returns:
242 Component: A gdsfactory component with the lumped-element resonator
243 geometry and two ports ('o1' and 'o2').
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 )
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
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 )
276 c = Component()
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 )
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
290 # Center inductor at the bottom of the internal area
291 ind.dcenter = (0, -total_internal_height / 2 + ind_height / 2)
293 # 2. Capacitor part (fingers and bus bars)
294 cap_y0 = -total_internal_height / 2 + ind_height + bus_bar_spacing
296 x_left_inner = -cap_width / 2 + finger_thickness
297 x_right_inner = cap_width / 2 - finger_thickness
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 )
320 # 3. Bus bars connecting everything
321 # Small overlap to ensure solid connectivity
322 overlap = 0.1
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 )
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 )
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 )
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 )
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))
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 )
446 c = gf.Component()
447 c.absorb(c << c_metal)
448 c.absorb(c << c_etch)
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 )
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
472 return c
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 )
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 )
525if __name__ == "__main__":
526 from qpdk.helper import show_components
528 show_components(
529 meander_inductor,
530 lumped_element_resonator,
531 )