Coverage for qpdk / cells / capacitor.py: 100%
89 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-14 10:27 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-14 10:27 +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.waveguides import straight
14from qpdk.helper import show_components
15from qpdk.tech import LAYER
18@gf.cell
19def interdigital_capacitor(
20 fingers: int = 4,
21 finger_length: float = 20.0,
22 finger_gap: float = 2.0,
23 thickness: float = 5.0,
24 etch_layer: LayerSpec | None = "M1_ETCH",
25 etch_bbox_margin: float = 2.0,
26 cross_section: CrossSectionSpec = "cpw",
27 half: bool = False,
28) -> Component:
29 """Generate an interdigital capacitor component with ports on both ends.
31 An interdigital capacitor consists of interleaved metal fingers that create
32 a distributed capacitance. This component creates a planar capacitor with
33 two sets of interleaved fingers extending from opposite ends.
35 .. svgbob::
37 ┌─┐───────┐┌─┐
38 │ │───────┘│ │
39 │ │ ┌──────│ │
40 ┌│ │ └──────│ │┐
41 o1└│ │──────┐ │ │┘o2
42 │ │──────┘ │ │
43 │ │ ┌──────│ │
44 └─┘ └──────└─┘
46 See for example :cite:`leizhuAccurateCircuitModel2000`.
48 Note:
49 ``finger_length=0`` effectively provides a parallel plate capacitor.
50 The capacitance scales approximately linearly with the number of fingers
51 and finger length.
53 Args:
54 fingers: Total number of fingers of the capacitor (must be >= 1).
55 finger_length: Length of each finger in μm.
56 finger_gap: Gap between adjacent fingers in μm.
57 thickness: Thickness of fingers and the base section in μm.
58 etch_layer: Optional layer for etching around the capacitor.
59 etch_bbox_margin: Margin around the capacitor for the etch layer in μm.
60 cross_section: Cross-section for the short straight from the etch box capacitor.
61 half: If True, creates a single-sided capacitor (half of the interdigital capacitor).
63 Returns:
64 Component: A gdsfactory component with the interdigital capacitor geometry
65 and two ports ('o1' and 'o2') on opposing sides.
66 """
67 c = Component()
69 # Used temporarily
70 layer = LAYER.M1_DRAW
72 if fingers < 1:
73 raise ValueError("Must have at least 1 finger")
75 width = (
76 2 * thickness + finger_length + finger_gap
77 if not half
78 else thickness + finger_length
79 ) # total length
80 height = fingers * thickness + (fingers - 1) * finger_gap # total height
81 points_1 = [
82 (0, 0),
83 (0, height),
84 (thickness + finger_length, height),
85 (thickness + finger_length, height - thickness),
86 (thickness, height - thickness),
87 *chain.from_iterable(
88 (
89 (thickness, height - (2 * i) * (thickness + finger_gap)),
90 (
91 thickness + finger_length,
92 height - (2 * i) * (thickness + finger_gap),
93 ),
94 (
95 thickness + finger_length,
96 height - (2 * i) * (thickness + finger_gap) - thickness,
97 ),
98 (thickness, height - (2 * i) * (thickness + finger_gap) - thickness),
99 )
100 for i in range(ceil(fingers / 2))
101 ),
102 (thickness, 0),
103 (0, 0),
104 ]
105 c.add_polygon(points_1, layer=layer)
107 if not half:
108 points_2 = [
109 (width, 0),
110 (width, height),
111 (width - thickness, height),
112 *chain.from_iterable(
113 (
114 (
115 width - thickness,
116 height - (1 + 2 * i) * thickness - (1 + 2 * i) * finger_gap,
117 ),
118 (
119 width - (thickness + finger_length),
120 height - (1 + 2 * i) * thickness - (1 + 2 * i) * finger_gap,
121 ),
122 (
123 width - (thickness + finger_length),
124 height - (2 + 2 * i) * thickness - (1 + 2 * i) * finger_gap,
125 ),
126 (
127 width - thickness,
128 height - (2 + 2 * i) * thickness - (1 + 2 * i) * finger_gap,
129 ),
130 )
131 for i in range(floor(fingers / 2))
132 ),
133 (width - thickness, 0),
134 (width, 0),
135 ]
136 c.add_polygon(points_2, layer=layer)
138 # Add etch layer bbox if specified
139 if etch_layer is not None:
140 etch_bbox = [
141 (-etch_bbox_margin, -etch_bbox_margin),
142 (width + etch_bbox_margin, -etch_bbox_margin),
143 (width + etch_bbox_margin, height + etch_bbox_margin),
144 (-etch_bbox_margin, height + etch_bbox_margin),
145 ]
146 c.add_polygon(etch_bbox, layer=etch_layer)
148 # Add small straights on the left and right sides of the capacitor
149 straight_cross_section = gf.get_cross_section(cross_section)
150 straight_out_of_etch = straight(
151 length=etch_bbox_margin, cross_section=straight_cross_section
152 )
153 straight_left = c.add_ref(straight_out_of_etch).move(
154 (-etch_bbox_margin, height / 2)
155 )
156 straight_right = None
157 if not half:
158 straight_right = c.add_ref(straight_out_of_etch).move((width, height / 2))
160 # Add WG to additive metal
161 c_additive = gf.boolean(
162 A=c,
163 B=c,
164 operation="or",
165 layer=layer,
166 layer1=layer,
167 layer2=straight_cross_section.layer,
168 )
170 # Take boolean negative
171 c_negative = gf.boolean(
172 A=c,
173 B=c_additive,
174 operation="A-B",
175 layer=etch_layer,
176 layer1=etch_layer,
177 layer2=layer,
178 )
180 # Combine results
181 c = gf.Component()
182 c.absorb(c << c_additive)
183 c.absorb(c << c_negative)
185 ports_config: list[tuple[str, gf.Port] | None] = [
186 ("o1", straight_left["o1"]),
187 ]
188 if not half and straight_right is not None:
189 ports_config.append(("o2", straight_right["o2"]))
191 for port_name, port_ref in filter(None, ports_config):
192 c.add_port(
193 name=port_name,
194 width=port_ref.width,
195 center=port_ref.center,
196 orientation=port_ref.orientation,
197 layer=LAYER.M1_DRAW,
198 )
200 # Center at (0,0)
201 c.move((-width / 2, -height / 2))
203 return c
206@gf.cell
207def plate_capacitor(
208 length: float = 26.0,
209 width: float = 5.0,
210 gap: float = 7.0,
211 etch_layer: LayerSpec | None = "M1_ETCH",
212 etch_bbox_margin: float = 2.0,
213 cross_section: CrossSectionSpec = "cpw",
214) -> Component:
215 """Creates a plate capacitor.
217 A capacitive coupler consists of two metal pads separated by a small gap,
218 providing capacitive coupling between circuit elements like qubits and resonators.
220 .. svgbob::
222 ______ ______
223 _________| | | |________
224 | | | |
225 | o1 pad1 | ====gap==== | pad2 o2 |
226 | | | |
227 |_________ | | _________|
228 |______| |______|
230 Args:
231 length: Length (vertical extent) of the capacitor pad in μm.
232 width: Width (horizontal extent) of the capacitor pad in μm.
233 gap: Gap between plates in μm.
234 etch_layer: Optional layer for etching around the capacitor.
235 etch_bbox_margin: Margin around the capacitor for the etch layer in μm.
236 cross_section: Cross-section for the short straight from the etch box capacitor.
238 Returns:
239 A gdsfactory component with the plate capacitor geometry and two ports ('o1' and 'o2') on opposing sides.
240 """
241 if width <= 0:
242 raise ValueError(f"width must be positive, got {width}")
243 if length <= 0:
244 raise ValueError(f"length must be positive, got {length}")
246 c = Component()
247 single_capacitor = plate_capacitor_single(
248 length=length,
249 width=width,
250 etch_layer=etch_layer,
251 etch_bbox_margin=etch_bbox_margin,
252 cross_section=cross_section,
253 )
255 pad1 = c.add_ref(single_capacitor)
256 pad2 = c.add_ref(single_capacitor)
257 pad2.rotate(180)
258 pad2.move((width + gap, 0))
259 c.center = (0, 0)
261 # Add ports
262 c.add_port(name="o1", port=pad1.ports["o1"])
263 c.add_port(name="o2", port=pad2.ports["o1"])
265 # Ensure etch box between pads
266 if etch_layer is not None:
267 missing_width = gap - 2 * etch_bbox_margin
268 if missing_width > 0:
269 etch_bbox = [
270 (-missing_width / 2, -length / 2 - etch_bbox_margin),
271 (missing_width / 2, -length / 2 - etch_bbox_margin),
272 (missing_width / 2, length / 2 + etch_bbox_margin),
273 (-missing_width / 2, length / 2 + etch_bbox_margin),
274 ]
275 c.add_polygon(etch_bbox, layer=etch_layer)
277 return c
280@gf.cell
281def plate_capacitor_single(
282 length: float = 26.0,
283 width: float = 5.0,
284 etch_layer: LayerSpec | None = "M1_ETCH",
285 etch_bbox_margin: float = 2.0,
286 cross_section: CrossSectionSpec = "cpw",
287) -> Component:
288 """Creates a single plate capacitor for coupling.
290 This is essentially half of a :func:`~plate_capacitor`.
292 .. svgbob::
294 ______
295 _________| |
296 | |
297 | o1 pad1 |
298 | |
299 |_________ |
300 |______|
302 Args:
303 length: Length (vertical extent) of the capacitor pad in μm.
304 width: Width (horizontal extent) of the capacitor pad in μm.
305 etch_layer: Optional layer for etching around the capacitor.
306 etch_bbox_margin: Margin around the capacitor for the etch layer in μm.
307 cross_section: Cross-section for the short straight from the etch box capacitor.
309 Returns:
310 A gdsfactory component with the plate capacitor geometry.
311 """
312 if width <= 0:
313 raise ValueError(f"width must be positive, got {width}")
314 if length <= 0:
315 raise ValueError(f"length must be positive, got {length}")
317 c = Component()
319 layer = LAYER.M1_DRAW
320 points = [
321 (0, 0),
322 (0, length),
323 (width, length),
324 (width, 0),
325 ]
326 c.add_polygon(points, layer=layer)
327 # Add etch layer bbox if specified
328 if etch_layer is not None:
329 etch_bbox = [
330 (-etch_bbox_margin, -etch_bbox_margin),
331 (width + etch_bbox_margin, -etch_bbox_margin),
332 (width + etch_bbox_margin, length + etch_bbox_margin),
333 (-etch_bbox_margin, length + etch_bbox_margin),
334 ]
335 c.add_polygon(etch_bbox, layer=etch_layer)
336 # Add small straight on the left side of the capacitor
337 straight_cross_section = gf.get_cross_section(cross_section)
338 straight_out_of_etch = straight(
339 length=etch_bbox_margin, cross_section=straight_cross_section
340 )
341 straight_left = c.add_ref(straight_out_of_etch).move(
342 (-etch_bbox_margin, length / 2)
343 )
344 # Add WG to additive metal
345 c_additive = gf.boolean(
346 A=c,
347 B=c,
348 operation="or",
349 layer=layer,
350 layer1=layer,
351 layer2=straight_cross_section.layer,
352 )
354 # Take boolean negative
355 c_negative = gf.boolean(
356 A=c,
357 B=c_additive,
358 operation="A-B",
359 layer=etch_layer,
360 layer1=etch_layer,
361 layer2=layer,
362 )
363 # Combine results
364 c = gf.Component()
365 c.absorb(c << c_additive)
366 c.absorb(c << c_negative)
368 c.add_port(
369 name="o1",
370 width=straight_left["o1"].width,
371 center=straight_left["o1"].center,
372 orientation=straight_left["o1"].orientation,
373 layer=LAYER.M1_DRAW,
374 )
376 # Center at (0,0)
377 c.move((-width / 2, -length / 2))
379 return c
382if __name__ == "__main__":
383 show_components(
384 plate_capacitor_single,
385 plate_capacitor,
386 interdigital_capacitor,
387 partial(interdigital_capacitor, half=True),
388 )