Coverage for qpdk / cells / helpers.py: 100%
110 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"""Helper functions for QPDK cells."""
3from collections.abc import Iterable, Sequence
4from itertools import starmap
6import gdsfactory as gf
7import klayout.db as kdb
8from gdsfactory.component import Component
9from gdsfactory.typings import Layer, LayerSpec
10from klayout.db import DCplxTrans, Region
12from qpdk.logger import logger
13from qpdk.tech import LAYER, NON_METADATA_LAYERS
16def transform_component(component: gf.Component, transform: DCplxTrans) -> gf.Component:
17 """Applies a complex transformation to a component.
19 For use with :func:`~gdsfactory.container`.
21 Returns:
22 The transformed component.
23 """
24 component.transform(transform)
25 return component
28def add_rect(
29 c: Component,
30 layer: LayerSpec,
31 *,
32 x0: float | None = None,
33 x1: float | None = None,
34 y0: float | None = None,
35 y1: float | None = None,
36 x_center: float | None = None,
37 y_center: float | None = None,
38 width: float | None = None,
39 height: float | None = None,
40) -> None:
41 """Add a rectangle to component *c* using flexible coordinates.
43 Coordinates can be specified using either (x0, x1) or (x_center, width),
44 and similarly for y.
46 Args:
47 c: Component to add the rectangle to.
48 layer: Layer specification for the rectangle.
49 x0: Left x-coordinate.
50 x1: Right x-coordinate.
51 y0: Bottom y-coordinate.
52 y1: Top y-coordinate.
53 x_center: Center x-coordinate.
54 y_center: Center y-coordinate.
55 width: Width of the rectangle.
56 height: Height of the rectangle.
58 Raises:
59 ValueError: If coordinate specification is incomplete or ambiguous.
60 """
61 if x0 is not None and x1 is not None:
62 x_lo, x_hi = min(x0, x1), max(x0, x1)
63 elif x_center is not None and width is not None:
64 x_lo, x_hi = x_center - width / 2, x_center + width / 2
65 else:
66 raise ValueError("Provide (x0, x1) or (x_center, width)")
68 if y0 is not None and y1 is not None:
69 y_lo, y_hi = min(y0, y1), max(y0, y1)
70 elif y_center is not None and height is not None:
71 y_lo, y_hi = y_center - height / 2, y_center + height / 2
72 else:
73 raise ValueError("Provide (y0, y1) or (y_center, height)")
75 c.add_polygon([(x_lo, y_lo), (x_hi, y_lo), (x_hi, y_hi), (x_lo, y_hi)], layer=layer)
78_EXCLUDE_LAYERS_DEFAULT_M1 = [
79 (LAYER.M1_ETCH, 80),
80 (LAYER.M1_DRAW, 80),
81 (LAYER.WG, 80),
82]
83_EXCLUDE_LAYERS_DEFAULT_M2 = [
84 (LAYER.M2_ETCH, 80),
85 (LAYER.M2_DRAW, 80),
86]
89@gf.cell
90def fill_magnetic_vortices(
91 component: Component | None = None,
92 rectangle_size: tuple[float, float] = (15.0, 15.0),
93 gap: float | tuple[float, float] = 15.0,
94 stagger: float | tuple[float, float] = 3.0,
95 exclude_layers: Iterable[tuple[LayerSpec, float]] | None = None,
96 fill_layer: LayerSpec = LAYER.M1_ETCH,
97) -> Component:
98 """Fill a component with small rectangles to trap magnetic vortices.
100 This function fills the bounding box area of a given component with small etch
101 rectangles in an array placed with specified gaps. The purpose is to trap
102 local magnetic vortices in superconducting quantum circuits.
104 This is a simple wrapper over :func:`~gdsfactory.Component.fill` which itself wraps
105 the fill function from kfactory.
107 Args:
108 component: The component to fill with vortex trapping rectangles.
109 If None, a default straight waveguide (:func:`gf.components.straight`)
110 with length 100 µm is used.
111 rectangle_size: Size of the fill rectangles in µm (width, height).
112 gap: Gap between rectangles in µm.
113 A tuple (x_gap, y_gap) can be provided for different gaps in x and y directions.
114 stagger: Amount of staggering in µm to apply to pattern.
115 A tuple (x_stagger, y_stagger) can be provided for different staggering in x and y.
116 exclude_layers: Layers to ignore. Tuples of layer and keepout in µm.
117 Defaults to M1_ETCH, M1_DRAW, and WG layers with 80 µm keepout.
118 fill_layer: Layer for the fill rectangles.
120 Returns:
121 A new component with the original component plus fill rectangles.
123 Example:
124 >>> from qpdk.cells.resonator import resonator_quarter_wave
125 >>> from qpdk.cells.helpers import fill_magnetic_vortices
126 >>> resonator = resonator_quarter_wave()
127 >>> filled_resonator = fill_magnetic_vortices(resonator)
128 >>> # Or use with default component
129 >>> filled_default = fill_magnetic_vortices()
130 """
131 # Use a default component if none is provided
132 if component is None:
133 component = gf.components.straight(length=100.0)
135 c = gf.Component()
136 c.add_ref(component)
138 exclude_layers = exclude_layers or _EXCLUDE_LAYERS_DEFAULT_M1
140 # Create the fill rectangle cell
141 fill_cell = gf.components.rectangle(
142 size=rectangle_size,
143 layer=fill_layer,
144 )
146 gap_x, gap_y = (gap, gap) if isinstance(gap, int | float) else gap
147 stagger_x, stagger_y = (
148 (stagger, stagger) if isinstance(stagger, int | float) else stagger
149 )
151 c.fill(
152 fill_cell=fill_cell,
153 fill_regions=[
154 (
155 Region(c.bbox().to_itype(dbu=c.kcl.dbu)),
156 0,
157 )
158 ], # Fill the entire bounding box area
159 exclude_layers=exclude_layers,
160 row_step=gf.kf.kdb.DVector(rectangle_size[0] + gap_x, stagger_y),
161 col_step=gf.kf.kdb.DVector(-stagger_x, rectangle_size[1] + gap_y),
162 )
164 return c
167def merge_layers_with_etch(
168 component: Component,
169 draw_layer: LayerSpec,
170 wg_layer: LayerSpec,
171 etch_layer: LayerSpec | None,
172) -> Component:
173 """Merge waveguide marker layer with draw layer and create an etch negative.
175 This function:
177 1. Merges the waveguide (WG) marker layer shapes with the draw layer
178 via boolean OR, producing a unified additive component.
179 2. If `etch_layer` is provided, subtracts the merged additive shapes
180 from the etch layer to produce a clean etch negative.
181 3. Returns a fresh component containing the merged layers.
183 This is used in capacitor components to combine the CPW cross-section
184 waveguide markers with the capacitor metal draw layer and generate
185 the corresponding etch layer.
187 Args:
188 component: The component containing both draw and WG layer shapes.
189 draw_layer: The additive metal layer (e.g., M1_DRAW).
190 wg_layer: The waveguide marker layer to merge into the draw layer.
191 etch_layer: Optional etch layer for the negative mask.
193 Returns:
194 A new component with merged draw and (optionally) etch layers.
195 """
196 c_additive = gf.boolean(
197 A=component,
198 B=component,
199 operation="or",
200 layer=draw_layer,
201 layer1=draw_layer,
202 layer2=wg_layer,
203 )
204 result = gf.Component()
205 result.absorb(result << c_additive)
207 if etch_layer is not None:
208 c_negative = gf.boolean(
209 A=component,
210 B=c_additive,
211 operation="A-B",
212 layer=etch_layer,
213 layer1=etch_layer,
214 layer2=draw_layer,
215 )
216 result.absorb(result << c_negative)
218 return result
221def subtract_draw_from_etch(
222 component: Component,
223 etch_shape: Component,
224 etch_layer: LayerSpec,
225 draw_layer: LayerSpec,
226) -> None:
227 """Subtract draw layer from an etch shape and absorb the result into a component.
229 This is commonly used to create etch regions around qubit components where
230 metal is preserved wherever the draw layer defines features, and the remaining
231 area is etched away.
233 Args:
234 component: The target component to absorb the result into.
235 Its draw layer shapes are subtracted from the etch shape.
236 etch_shape: The component defining the full etch area (e.g., a bounding box).
237 etch_layer: The etch layer for the result.
238 draw_layer: The draw layer to subtract from the etch shape.
239 """
240 result = gf.boolean(
241 A=etch_shape,
242 B=component,
243 operation="-",
244 layer=etch_layer,
245 layer1=etch_layer,
246 layer2=draw_layer,
247 )
248 component.absorb(component.add_ref(result))
251def apply_additive_metals(component: Component) -> Component:
252 """Apply additive metal layers and remove them.
254 Removes additive metal layers from etch layers, leading to a negative mask.
256 TODO: Implement without flattening. Maybe with a KLayout dataprep script?
258 Returns:
259 Component with additive metals applied.
260 """
261 for additive, etch in (
262 (LAYER.M1_DRAW, LAYER.M1_ETCH),
263 (LAYER.M2_DRAW, LAYER.M2_ETCH),
264 ):
265 component_etch_only = gf.boolean(
266 A=component,
267 B=component,
268 operation="-",
269 layer=etch,
270 layer1=etch,
271 layer2=additive,
272 )
273 component.flatten()
274 component.remove_layers([etch, additive])
275 component << component_etch_only
276 return component
279@gf.cell
280def invert_mask_polarity(component: Component) -> Component:
281 """Invert mask polarity of a component.
283 Converts DRAW layers to ETCH layers by subtracting the DRAW layer from the
284 component's bounding box, and similarly converts ETCH layers to DRAW layers.
285 This is applied to M1 and M2 layers. All other layers are copied intact.
287 Args:
288 component: The component to invert.
290 Returns:
291 A new component with inverted mask polarity.
292 """
293 c = gf.Component()
295 # Bounding box of the component defines the outer boundary for inversion
296 bbox_region = Region(component.bbox().to_itype(component.kcl.dbu))
298 affected_layers: set[int] = set()
300 for additive, etch in (
301 (LAYER.M1_DRAW, LAYER.M1_ETCH),
302 (LAYER.M2_DRAW, LAYER.M2_ETCH),
303 ):
304 # Determine the layer indices in the layout object
305 add_layer_index = component.kcl.layer(*additive)
306 etch_layer_index = component.kcl.layer(*etch)
307 affected_layers.update([add_layer_index, etch_layer_index])
309 # Extract the shapes of the old component on these layers as regions
310 add_region = Region(component.begin_shapes_rec(add_layer_index))
311 etch_region = Region(component.begin_shapes_rec(etch_layer_index))
313 # Skip if both regions are empty (no shapes on these layers)
314 if add_region.is_empty() and etch_region.is_empty():
315 logger.debug(
316 "Skipping empty layers: {}, {} in component {}",
317 additive,
318 etch,
319 component.name,
320 )
321 continue
323 # Invert the polarities using the bounding box
324 new_add_region = bbox_region - etch_region
325 new_etch_region = bbox_region - add_region
327 # Insert the inverted regions into the new component
328 c.shapes(add_layer_index).insert(new_add_region)
329 c.shapes(etch_layer_index).insert(new_etch_region)
331 # Copy all other layers intact
332 for layer_index in set(component.kcl.layer_indices()) - affected_layers:
333 other_region = Region(component.begin_shapes_rec(layer_index))
334 if not other_region.is_empty():
335 c.shapes(layer_index).insert(other_region)
337 return c
340def add_margin_to_layer(
341 component: Component, layer_margins: Sequence[tuple[Layer, float]]
342) -> Component:
343 """Increase the component bounding box by adding a margin to given draw layers.
345 For each specified layer in the component, it is extended outwards
346 by the specified margin. This effectively increases the bounding box of the
347 component which can be useful to define the simulation area in HFSS.
349 Args:
350 component: The component to modify.
351 layer_margins: Sequence of tuples containing the layer to modify and the margin to add in µm.
353 Returns:
354 A new component with extended layers.
355 """
356 c = gf.Component()
358 bbox = component.bbox()
360 bbox_region = Region(bbox.to_itype(component.kcl.dbu))
362 # Identify layer indices for the specified margins
363 layer_indices_margins = {
364 component.kcl.layer(*layer): margin for layer, margin in layer_margins
365 }
367 # Copy existing shapes and track which specified layers are present
368 present_layer_indices = set()
369 for layer_index in component.kcl.layer_indices():
370 region = Region(component.begin_shapes_rec(layer_index))
371 if not region.is_empty():
372 c.shapes(layer_index).insert(region)
373 if layer_index in layer_indices_margins:
374 present_layer_indices.add(layer_index)
376 # Add margins to the layers that are present in the component
377 bbox_itype = bbox.to_itype(component.kcl.dbu)
378 bbox_region = Region(bbox_itype)
380 for layer_index in present_layer_indices:
381 margin = layer_indices_margins[layer_index]
382 margin_dbu = int(margin / component.kcl.dbu)
383 new_bbox = kdb.Box(
384 bbox_itype.left - margin_dbu,
385 bbox_itype.bottom - margin_dbu,
386 bbox_itype.right + margin_dbu,
387 bbox_itype.top + margin_dbu,
388 )
389 new_bbox_region = Region(new_bbox)
390 margin_region = new_bbox_region - bbox_region
391 c.shapes(layer_index).insert(margin_region)
393 return c
396def remove_metadata_layers(component: Component) -> Component:
397 """Remove metadata layers from a component.
399 Retains only physical and base layers:
400 M1_DRAW, M1_ETCH, M2_DRAW, M2_ETCH, AB_DRAW, AB_VIA,
401 JJ_AREA, JJ_PATCH, IND, TSV, DICE, ALN_TOP, ALN_BOT.
403 All other layers are stripped out.
405 Args:
406 component: The component to clean.
408 Returns:
409 A new component with metadata layers removed.
410 """
411 # Convert allowed layers into kcl layer indices
412 allowed_indices = set(starmap(component.kcl.layer, NON_METADATA_LAYERS))
414 c = gf.Component()
416 for layer_index in component.kcl.layer_indices():
417 if layer_index in allowed_indices:
418 other_region = Region(component.begin_shapes_rec(layer_index))
419 if not other_region.is_empty():
420 c.shapes(layer_index).insert(other_region)
422 return c
425if __name__ == "__main__":
426 from qpdk import PDK
427 from qpdk.cells.resonator import resonator
429 PDK.activate()
430 c = resonator(length=2000)
431 c = apply_additive_metals(c.copy())
432 c = invert_mask_polarity(c)
433 c = add_margin_to_layer(
434 c, layer_margins=[(LAYER.M1_DRAW, 50.0), (LAYER.M2_DRAW, 50.0)]
435 )
436 c.show()