Coverage for qpdk / cells / capacitor.py: 99%
116 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"""Capacitive coupler components."""
3from __future__ import annotations
5from functools import partial
6from itertools import chain
7from math import ceil, floor
9import gdsfactory as gf
10from gdsfactory.component import Component
11from gdsfactory.typings import CrossSectionSpec, LayerSpec
13from qpdk.cells.helpers import merge_layers_with_etch as _merge_layers_with_etch
14from qpdk.cells.waveguides import add_etch_gap, bend_circular, straight
15from qpdk.helper import show_components
16from qpdk.tech import LAYER, get_etch_section, get_etch_sections
19@gf.cell(tags=("capacitors", "couplers"))
20def half_circle_coupler(
21 radius: float = 50.0,
22 angle: float = 180.0,
23 extension_length: float = 10.0,
24 cross_section: CrossSectionSpec = "cpw",
25 extra_straight_length: float = 20.0,
26 open_end: bool = True,
27) -> Component:
28 """Creates a half-circle coupler for readout.
30 This coupler consists of a circular bend (typically 180 degrees) that wraps
31 around a resonator arm for capacitive coupling.
33 Args:
34 radius: Inner radius of the half-circle in μm.
35 angle: Angle of the circular arc in degrees.
36 extension_length: Length of the straight sections extending from the
37 ends of the half-circle in μm.
38 cross_section: Cross-section specification for the coupler.
39 extra_straight_length: Length of the straight section extending from the
40 bottom of the half-circle in μm.
41 open_end: If True, adds an etched gap at the ends of the extensions.
43 Returns:
44 Component: A gdsfactory component with the half-circle coupler geometry.
45 """
46 c = Component()
48 bend = c.add_ref(
49 bend_circular(
50 radius=radius,
51 angle=angle,
52 cross_section=cross_section,
53 )
54 )
56 # Position bend such that it's centered and opening upwards
57 bend.rotate(-angle / 2)
58 bend.move((-bend.dcenter[0], -bend.dcenter[1]))
60 # Add extensions to the ends of the bend
61 if extension_length > 0:
62 ext1 = c.add_ref(straight(length=extension_length, cross_section=cross_section))
63 ext1.connect("o1", bend.ports["o1"])
64 ext2 = c.add_ref(straight(length=extension_length, cross_section=cross_section))
65 ext2.connect("o1", bend.ports["o2"])
66 where_to_add_gaps = [ext1.ports["o2"], ext2.ports["o2"]]
67 else:
68 where_to_add_gaps = [bend.ports["o1"], bend.ports["o2"]]
70 if open_end:
71 for port in where_to_add_gaps:
72 add_etch_gap(c, port, cross_section=cross_section)
74 # Get cross section details to calculate overlap
75 xs = gf.get_cross_section(cross_section)
76 cross_section_etch_section = get_etch_section(xs)
77 # Ensure significant overlap by moving stem into the bend metal
78 # and considering the bend radius
79 overlap = xs.width / 2 + cross_section_etch_section.width
81 # Add a stem/lead straight from the center of the arc.
82 # The stem uses the CPW cross-section but does NOT extend into the bend,
83 # to avoid M1_ETCH overlapping with the bend's M1_DRAW.
84 stem = c.add_ref(
85 straight(
86 length=extra_straight_length,
87 cross_section=cross_section,
88 )
89 )
91 stem.rotate(-90)
92 stem.movey(bend.dbbox().bottom)
93 stem.movex(bend.dcenter[0]) # Center it
95 # Bridge the stem into the bend with M1_DRAW center conductor and
96 # M1_ETCH gap sections, matching the CPW cross-section pattern.
97 # Unlike using a straight ref, these polygons avoid hierarchical
98 # M1_ETCH-over-M1_DRAW conflicts with the bend.
99 bridge_x = bend.dcenter[0]
100 bridge_y_bottom = bend.dbbox().bottom
101 bridge_y_top = bridge_y_bottom + overlap
102 c.add_polygon(
103 [
104 (bridge_x - xs.width / 2, bridge_y_bottom),
105 (bridge_x + xs.width / 2, bridge_y_bottom),
106 (bridge_x + xs.width / 2, bridge_y_top),
107 (bridge_x - xs.width / 2, bridge_y_top),
108 ],
109 layer=LAYER.M1_DRAW,
110 )
111 etch_height = overlap / 3
112 for etch_s in get_etch_sections(xs):
113 etch_x_center = bridge_x + etch_s.offset
114 c.add_polygon(
115 [
116 (etch_x_center - etch_s.width / 2, bridge_y_bottom),
117 (etch_x_center + etch_s.width / 2, bridge_y_bottom),
118 (etch_x_center + etch_s.width / 2, bridge_y_bottom + etch_height),
119 (etch_x_center - etch_s.width / 2, bridge_y_bottom + etch_height),
120 ],
121 layer=LAYER.M1_ETCH,
122 )
123 c.add_port("o3", port=stem.ports["o2"])
125 # Place anchor at the arc center, computed as the midpoint of the
126 # bend ports for a circular arc, for concentric alignment with an
127 # inner resonator bend.
128 arc_center_x = (bend.ports["o1"].dx + bend.ports["o2"].dx) / 2
129 arc_center_y = (bend.ports["o1"].dy + bend.ports["o2"].dy) / 2
130 c.add_port(
131 name="anchor",
132 center=(arc_center_x, arc_center_y),
133 width=xs.width,
134 orientation=90,
135 layer=LAYER.M1_DRAW,
136 port_type="placement",
137 )
139 return c
142@gf.cell(tags=("capacitors",))
143def interdigital_capacitor(
144 fingers: int = 4,
145 finger_length: float = 20.0,
146 finger_gap: float = 2.0,
147 thickness: float = 5.0,
148 layer_metal: LayerSpec = LAYER.M1_DRAW,
149 etch_layer: LayerSpec | None = "M1_ETCH",
150 etch_bbox_margin: float = 2.0,
151 cross_section: CrossSectionSpec = "cpw",
152 half: bool = False,
153) -> Component:
154 """Generate an interdigital capacitor component with ports on both ends.
156 An interdigital capacitor consists of interleaved metal fingers that create
157 a distributed capacitance. This component creates a planar capacitor with
158 two sets of interleaved fingers extending from opposite ends.
160 .. svgbob::
162 ┌─┐───────┐┌─┐
163 │ │───────┘│ │
164 │ │ ┌──────│ │
165 ┌│ │ └──────│ │┐
166 o1└│ │──────┐ │ │┘o2
167 │ │──────┘ │ │
168 │ │ ┌──────│ │
169 └─┘ └──────└─┘
171 See for example :cite:`leizhuAccurateCircuitModel2000`.
173 Note:
174 ``finger_length=0`` effectively provides a parallel plate capacitor.
175 The capacitance scales approximately linearly with the number of fingers
176 and finger length.
178 Args:
179 fingers: Total number of fingers of the capacitor (must be >= 1).
180 finger_length: Length of each finger in μm.
181 finger_gap: Gap between adjacent fingers in μm.
182 thickness: Thickness of fingers and the base section in μm.
183 layer_metal: Layer for the metal fingers.
184 etch_layer: Optional layer for etching around the capacitor.
185 etch_bbox_margin: Margin around the capacitor for the etch layer in μm.
186 cross_section: Cross-section for the short straight from the etch box capacitor.
187 half: If True, creates a single-sided capacitor (half of the interdigital capacitor).
189 Returns:
190 Component: A gdsfactory component with the interdigital capacitor geometry
191 and two ports ('o1' and 'o2') on opposing sides.
193 Raises:
194 ValueError: If fingers is less than 1.
195 """
196 c = Component()
198 if fingers < 1:
199 raise ValueError("Must have at least 1 finger")
201 width = (
202 2 * thickness + finger_length + finger_gap
203 if not half
204 else thickness + finger_length
205 ) # total length
206 height = fingers * thickness + (fingers - 1) * finger_gap # total height
207 points_1 = [
208 (0, 0),
209 (0, height),
210 (thickness + finger_length, height),
211 (thickness + finger_length, height - thickness),
212 (thickness, height - thickness),
213 *chain.from_iterable(
214 (
215 (thickness, height - (2 * i) * (thickness + finger_gap)),
216 (
217 thickness + finger_length,
218 height - (2 * i) * (thickness + finger_gap),
219 ),
220 (
221 thickness + finger_length,
222 height - (2 * i) * (thickness + finger_gap) - thickness,
223 ),
224 (thickness, height - (2 * i) * (thickness + finger_gap) - thickness),
225 )
226 for i in range(ceil(fingers / 2))
227 ),
228 (thickness, 0),
229 (0, 0),
230 ]
231 c.add_polygon(points_1, layer=layer_metal)
233 if not half:
234 points_2 = [
235 (width, 0),
236 (width, height),
237 (width - thickness, height),
238 *chain.from_iterable(
239 (
240 (
241 width - thickness,
242 height - (1 + 2 * i) * thickness - (1 + 2 * i) * finger_gap,
243 ),
244 (
245 width - (thickness + finger_length),
246 height - (1 + 2 * i) * thickness - (1 + 2 * i) * finger_gap,
247 ),
248 (
249 width - (thickness + finger_length),
250 height - (2 + 2 * i) * thickness - (1 + 2 * i) * finger_gap,
251 ),
252 (
253 width - thickness,
254 height - (2 + 2 * i) * thickness - (1 + 2 * i) * finger_gap,
255 ),
256 )
257 for i in range(floor(fingers / 2))
258 ),
259 (width - thickness, 0),
260 (width, 0),
261 ]
262 c.add_polygon(points_2, layer=layer_metal)
264 # Add etch layer bbox if specified
265 if etch_layer is not None:
266 etch_bbox = [
267 (-etch_bbox_margin, -etch_bbox_margin),
268 (width + etch_bbox_margin, -etch_bbox_margin),
269 (width + etch_bbox_margin, height + etch_bbox_margin),
270 (-etch_bbox_margin, height + etch_bbox_margin),
271 ]
272 c.add_polygon(etch_bbox, layer=etch_layer)
274 # Add small straights on the left and right sides of the capacitor
275 straight_cross_section = gf.get_cross_section(cross_section)
276 straight_out_of_etch = straight(
277 length=etch_bbox_margin, cross_section=straight_cross_section
278 )
279 straight_left = c.add_ref(straight_out_of_etch).move((
280 -etch_bbox_margin,
281 height / 2,
282 ))
283 straight_right = None
284 if not half:
285 straight_right = c.add_ref(straight_out_of_etch).move((width, height / 2))
287 # Merge WG marker layer with draw metal and create etch negative
288 c = _merge_layers_with_etch(
289 component=c,
290 draw_layer=layer_metal,
291 wg_layer=straight_cross_section.layer,
292 etch_layer=etch_layer,
293 )
295 ports_config: list[tuple[str, gf.Port] | None] = [
296 ("o1", straight_left["o1"]),
297 ]
298 if not half and straight_right is not None:
299 ports_config.append(("o2", straight_right["o2"]))
301 for port_name, port_ref in filter(None, ports_config):
302 c.add_port(
303 name=port_name,
304 width=port_ref.width,
305 center=port_ref.center,
306 orientation=port_ref.orientation,
307 layer=LAYER.M1_DRAW,
308 )
310 # Center at (0,0)
311 c.move((-width / 2, -height / 2))
313 return c
316@gf.cell(tags=("capacitors",))
317def plate_capacitor(
318 length: float = 26.0,
319 width: float = 5.0,
320 gap: float = 7.0,
321 etch_layer: LayerSpec | None = "M1_ETCH",
322 etch_bbox_margin: float = 2.0,
323 cross_section: CrossSectionSpec = "cpw",
324) -> Component:
325 """Creates a plate capacitor.
327 A capacitive coupler consists of two metal pads separated by a small gap,
328 providing capacitive coupling between circuit elements like qubits and resonators.
330 .. svgbob::
332 ______ ______
333 _________| | | |________
334 | | | |
335 | o1 pad1 | ====gap==== | pad2 o2 |
336 | | | |
337 |_________ | | _________|
338 |______| |______|
340 Args:
341 length: Length (vertical extent) of the capacitor pad in μm.
342 width: Width (horizontal extent) of the capacitor pad in μm.
343 gap: Gap between plates in μm.
344 etch_layer: Optional layer for etching around the capacitor.
345 etch_bbox_margin: Margin around the capacitor for the etch layer in μm.
346 cross_section: Cross-section for the short straight from the etch box capacitor.
348 Returns:
349 A gdsfactory component with the plate capacitor geometry and two ports ('o1' and 'o2') on opposing sides.
351 Raises:
352 ValueError: If width or length is not positive.
353 """
354 if width <= 0:
355 raise ValueError(f"width must be positive, got {width}")
356 if length <= 0:
357 raise ValueError(f"length must be positive, got {length}")
359 c = Component()
360 single_capacitor = plate_capacitor_single(
361 length=length,
362 width=width,
363 etch_layer=etch_layer,
364 etch_bbox_margin=etch_bbox_margin,
365 cross_section=cross_section,
366 )
368 pad1 = c.add_ref(single_capacitor)
369 pad2 = c.add_ref(single_capacitor)
370 pad2.rotate(180)
371 pad2.move((width + gap, 0))
372 c.center = (0, 0)
374 # Add ports
375 c.add_port(name="o1", port=pad1.ports["o1"])
376 c.add_port(name="o2", port=pad2.ports["o1"])
378 # Ensure etch box between pads
379 if etch_layer is not None:
380 missing_width = gap - 2 * etch_bbox_margin
381 if missing_width > 0:
382 etch_bbox = [
383 (-missing_width / 2, -length / 2 - etch_bbox_margin),
384 (missing_width / 2, -length / 2 - etch_bbox_margin),
385 (missing_width / 2, length / 2 + etch_bbox_margin),
386 (-missing_width / 2, length / 2 + etch_bbox_margin),
387 ]
388 c.add_polygon(etch_bbox, layer=etch_layer)
390 return c
393@gf.cell(tags=("capacitors", "couplers"))
394def plate_capacitor_single(
395 length: float = 26.0,
396 width: float = 5.0,
397 layer_metal: LayerSpec = LAYER.M1_DRAW,
398 etch_layer: LayerSpec | None = "M1_ETCH",
399 etch_bbox_margin: float = 2.0,
400 cross_section: CrossSectionSpec = "cpw",
401) -> Component:
402 """Creates a single plate capacitor for coupling.
404 This is essentially half of a :func:`~plate_capacitor`.
406 .. svgbob::
408 ______
409 _________| |
410 | |
411 | o1 pad1 |
412 | |
413 |_________ |
414 |______|
416 Args:
417 length: Length (vertical extent) of the capacitor pad in μm.
418 width: Width (horizontal extent) of the capacitor pad in μm.
419 layer_metal: Layer for the metal pad.
420 etch_layer: Optional layer for etching around the capacitor.
421 etch_bbox_margin: Margin around the capacitor for the etch layer in μm.
422 cross_section: Cross-section for the short straight from the etch box capacitor.
424 Returns:
425 A gdsfactory component with the plate capacitor geometry.
427 Raises:
428 ValueError: If width or length is not positive.
429 """
430 if width <= 0:
431 raise ValueError(f"width must be positive, got {width}")
432 if length <= 0:
433 raise ValueError(f"length must be positive, got {length}")
435 c = Component()
437 points = [
438 (0, 0),
439 (0, length),
440 (width, length),
441 (width, 0),
442 ]
443 c.add_polygon(points, layer=layer_metal)
444 # Add etch layer bbox if specified
445 if etch_layer is not None:
446 etch_bbox = [
447 (-etch_bbox_margin, -etch_bbox_margin),
448 (width + etch_bbox_margin, -etch_bbox_margin),
449 (width + etch_bbox_margin, length + etch_bbox_margin),
450 (-etch_bbox_margin, length + etch_bbox_margin),
451 ]
452 c.add_polygon(etch_bbox, layer=etch_layer)
453 # Add small straight on the left side of the capacitor
454 straight_cross_section = gf.get_cross_section(cross_section)
455 straight_out_of_etch = straight(
456 length=etch_bbox_margin, cross_section=straight_cross_section
457 )
458 straight_left = c.add_ref(straight_out_of_etch).move((
459 -etch_bbox_margin,
460 length / 2,
461 ))
462 # Merge WG marker layer with draw metal and create etch negative
463 c = _merge_layers_with_etch(
464 component=c,
465 draw_layer=layer_metal,
466 wg_layer=straight_cross_section.layer,
467 etch_layer=etch_layer,
468 )
470 c.add_port(
471 name="o1",
472 width=straight_left["o1"].width,
473 center=straight_left["o1"].center,
474 orientation=straight_left["o1"].orientation,
475 layer=LAYER.M1_DRAW,
476 )
478 # Center at (0,0)
479 c.move((-width / 2, -length / 2))
481 return c
484if __name__ == "__main__":
485 show_components(
486 half_circle_coupler,
487 plate_capacitor_single,
488 plate_capacitor,
489 interdigital_capacitor,
490 partial(interdigital_capacitor, half=True),
491 )