Coverage for qpdk / tech.py: 100%
80 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"""Technology definitions."""
3from collections.abc import Callable, Sequence
4from functools import cache, partial, wraps
5from typing import Any
7import gdsfactory as gf
8from doroutes.bundles import add_bundle_astar
9from gdsfactory.cross_section import (
10 CrossSection,
11)
12from gdsfactory.technology import (
13 DerivedLayer,
14 LayerLevel,
15 LayerMap,
16 LayerStack,
17 LayerViews,
18 LogicalLayer,
19)
20from gdsfactory.typings import (
21 ConnectivitySpec,
22 Layer,
23 LayerSpec,
24)
26from qpdk.config import PATH
27from qpdk.helper import denest_layerviews_to_layer_tuples
29nm = 1e-3
32class LayerMapQPDK(LayerMap):
33 """Layer map for QPDK technology."""
35 # Base metals
36 M1_DRAW: Layer = (1, 0) # Additive metal / positive mask regions
37 M1_ETCH: Layer = (1, 1) # Subtractive etch / negative mask regions
38 # Additive wins over subtractive where they overlap
39 # i.e., you can draw metal over an etched region to "fill it back in"
41 # flip-chip equivalents
42 M2_DRAW: Layer = (2, 0)
43 M2_ETCH: Layer = (2, 1)
45 # Airbridges
46 AB_DRAW: Layer = (10, 0) # Bridge metal
47 AB_VIA: Layer = (10, 1) # Landing pads / contacts
49 # Junctions
50 JJ_AREA: Layer = (20, 0) # Optional bridge/overlap definition
51 JJ_PATCH: Layer = (20, 1)
53 # Packaging / 3D integration / backside / misc.
54 IND: Layer = (30, 0)
55 TSV: Layer = (31, 0) # Throughs / vias / backside features
56 DICE: Layer = (70, 0) # Dicing lanes
58 # Alignment / admin
59 ALN_TOP: Layer = (80, 0) # Frontside alignment
60 ALN_BOT: Layer = (81, 0) # Backside alignment
62 ###################
63 # Non-fabrication #
64 ###################
66 TEXT: Layer = (90, 0) # Mask text / version labels
68 # labels for gdsfactory
69 LABEL_SETTINGS: Layer = (100, 0)
70 LABEL_INSTANCE: Layer = (101, 0)
72 # Simulation-only helpers (never sent to fab)
73 SIM_AREA: Layer = (98, 0)
74 SIM_ONLY: Layer = (99, 0)
76 # Marker layer for waveguides
77 WG: Layer = (102, 0)
79 # Error marker layer (used by gdsfactory to highlight routing/DRC errors)
80 ERROR_PATH: Layer = (1000, 0)
83L = LAYER = LayerMapQPDK
85material_properties = {
86 "vacuum": {"relative_permittivity": 1},
87 "Nb": {"relative_permittivity": float("inf")},
88 "Si": {"relative_permittivity": 11.45},
89 "AlOx/Al": {"relative_permittivity": float("inf")},
90 "TiN": {"relative_permittivity": float("inf")},
91 "In": {"relative_permittivity": float("inf")},
92}
94NON_METADATA_LAYERS = {
95 LAYER.M1_DRAW,
96 LAYER.M1_ETCH,
97 LAYER.M2_DRAW,
98 LAYER.M2_ETCH,
99 LAYER.AB_DRAW,
100 LAYER.AB_VIA,
101 LAYER.JJ_AREA,
102 LAYER.JJ_PATCH,
103 LAYER.IND,
104 LAYER.TSV,
105 LAYER.DICE,
106 LAYER.ALN_TOP,
107 LAYER.ALN_BOT,
108}
111@cache
112def get_layer_stack() -> LayerStack:
113 """Returns a LayerStack corresponding to the PDK.
115 The stack roughly corresponds to that of :cite:`tuokkolaMethodsAchieveNearmillisecond2025`.
116 """
117 return LayerStack(
118 layers={
119 # Base metal film (e.g., 200 nm of Nb)
120 "M1": LayerLevel(
121 name="M1",
122 # Generate base metal by subtracting (modified) etch from sim. area
123 layer=DerivedLayer(
124 layer1=LogicalLayer(layer=L.SIM_AREA),
125 # additive wins over substractive etch
126 layer2=DerivedLayer(
127 layer1=LogicalLayer(layer=L.M1_ETCH),
128 layer2=LogicalLayer(layer=L.M1_DRAW),
129 operation="-",
130 ),
131 operation="-",
132 ),
133 derived_layer=LogicalLayer(layer=L.M1_DRAW),
134 thickness=200e-9 * 1e6,
135 zmin=0.0, # top of substrate
136 material="Nb",
137 mesh_order=1,
138 ),
139 "Substrate": LayerLevel(
140 name="Substrate",
141 layer=L.SIM_AREA,
142 thickness=500, # 500 microns of silicon
143 zmin=-500, # below metal
144 material="Si",
145 mesh_order=4,
146 ),
147 "Vacuum": LayerLevel(
148 name="Vacuum",
149 layer=L.SIM_AREA,
150 thickness=500e-6 * 1e6, # 500 microns of vacuum above metal
151 zmin=200e-9 * 1e6, # above metal
152 material="vacuum",
153 mesh_order=99,
154 ),
155 # Airbridge metal sitting above M1 (example: +300 nm)
156 "Airbridge": LayerLevel(
157 name="Airbridge",
158 layer=L.AB_DRAW,
159 thickness=200e-9 * 1e6,
160 zmin=300e-9 * 1e6, # stacked above via
161 material="Nb",
162 ),
163 "Airbridge_Via": LayerLevel(
164 name="Airbridge_Via",
165 layer=L.AB_VIA,
166 thickness=100e-9 * 1e6,
167 zmin=200e-9 * 1e6, # stacked above M1
168 material="Nb",
169 ),
170 # JJ_AREA can be exported as a thin film if you use it in EM
171 "JosephsonJunction": LayerLevel(
172 name="JosephsonJunction",
173 layer=L.JJ_AREA,
174 thickness=70e-9,
175 zmin=0,
176 material="AlOx/Al",
177 mesh_order=2,
178 ),
179 "TSV": LayerLevel(
180 name="TSV",
181 layer=L.TSV,
182 thickness=500, # full substrate thickness
183 zmin=-500, # starting at bottom
184 material="TiN",
185 mesh_order=3,
186 ),
187 "IndiumBump": LayerLevel(
188 name="IndiumBump",
189 layer=L.IND,
190 thickness=10, # 10 microns
191 zmin=200e-9 * 1e6, # stacked above M1
192 material="In",
193 mesh_order=3,
194 ),
195 }
196 )
199LAYER_STACK = get_layer_stack()
200# Nicer for 3D visualization
201LAYER_STACK_NO_VACUUM = LayerStack(
202 layers={
203 name: level for name, level in LAYER_STACK.layers.items() if name != "Vacuum"
204 }
205)
206LAYER_VIEWS = gf.technology.LayerViews(PATH.lyp)
208LAYER_CONNECTIVITY: Sequence[ConnectivitySpec] = [
209 ("M1_DRAW", "TSV", "M2_DRAW"),
210 ("M1_DRAW", "IND", "M2_DRAW"),
211 ("M1_DRAW", "AB_DRAW", "M1_DRAW"),
212 ("M2_DRAW", "AB_DRAW", "M2_DRAW"),
213]
216LAYER_STACK_FLIP_CHIP = LayerStack(
217 layers={
218 **LAYER_STACK.layers,
219 "M2": LayerLevel(
220 name="M2",
221 layer=DerivedLayer(
222 layer1=LogicalLayer(layer=L.SIM_AREA),
223 layer2=DerivedLayer(
224 layer1=LogicalLayer(layer=L.M2_ETCH),
225 layer2=LogicalLayer(layer=L.M2_DRAW),
226 operation="-",
227 ),
228 operation="-",
229 ),
230 derived_layer=LogicalLayer(layer=L.M2_DRAW),
231 thickness=0.2,
232 zmin=10.0,
233 material="Nb",
234 mesh_order=1,
235 ),
236 "Substrate_top": LayerLevel(
237 name="Substrate_top",
238 layer=L.SIM_AREA,
239 thickness=500, # 500 microns of silicon
240 zmin=10.2, # below metal
241 material="Si",
242 mesh_order=4,
243 ),
244 }
245)
246LAYER_STACK_FLIP_CHIP_NO_VACUUM = LayerStack(
247 layers={
248 name: level
249 for name, level in LAYER_STACK_FLIP_CHIP.layers.items()
250 if name != "Vacuum"
251 }
252)
254############################
255# Cross-sections functions
256############################
258cross_sections: dict[str, Callable[..., CrossSection]] = {}
259_cross_section_default_names: dict[str, str] = {}
262def xsection(func: Callable[..., CrossSection]) -> Callable[..., CrossSection]:
263 """Returns decorated to register a cross section function.
265 Ensures that the cross-section name matches the name of the function that generated it when created using default parameters
267 .. code-block:: python
269 @xsection
270 def strip(width=TECH.width_strip, radius=TECH.radius_strip):
271 return gf.cross_section.cross_section(width=width, radius=radius)
272 """
273 default_cross_section = func()
274 _cross_section_default_names[default_cross_section.name] = func.__name__
276 @wraps(func)
277 def decorated_cross_section(**kwargs: Any) -> CrossSection:
278 cross_section = func(**kwargs)
279 if cross_section.name in _cross_section_default_names:
280 cross_section._name = _cross_section_default_names[cross_section.name]
281 return cross_section
283 cross_sections[func.__name__] = decorated_cross_section
284 return decorated_cross_section
287@xsection
288def coplanar_waveguide(
289 width: float = 10,
290 gap: float = 6,
291 waveguide_layer: LayerSpec = LAYER.M1_DRAW,
292 etch_layer: LayerSpec = LAYER.M1_ETCH,
293 radius: float | None = 100,
294) -> CrossSection:
295 """Return a coplanar waveguide cross_section.
297 The cross_section is considered negative (etched) on the physical layer.
299 Note:
300 Assuming a silicon substrate thickness of 500 µm and a metal thickness of 100 nm,
301 the default center conductor width and gap dimensions give a characteristic
302 impedance of approximately 50 Ω.
304 Args:
305 width: center conductor width in µm.
306 gap: gap between center conductor and ground in µm.
307 waveguide_layer: for the center conductor (positive) region.
308 etch_layer: for the etch (negative) region.
309 radius: bend radius (if applicable).
310 """
311 return gf.cross_section.cross_section(
312 width=width,
313 layer=waveguide_layer,
314 radius=radius,
315 sections=(
316 gf.Section(
317 width=gap,
318 offset=(gap + width) / 2,
319 layer=etch_layer,
320 name="etch_offset_pos",
321 ),
322 gf.Section(
323 width=gap,
324 offset=-(gap + width) / 2,
325 layer=etch_layer,
326 name="etch_offset_neg",
327 ),
328 gf.Section(width=width, layer=LAYER.WG, name="waveguide"),
329 ),
330 radius_min=(width + 2 * gap) / 2,
331 )
334cpw = coplanar_waveguide
335etch = etch_only = partial(
336 coplanar_waveguide,
337 waveguide_layer=LAYER.M1_ETCH,
338)
341@xsection
342def launcher_cross_section_big() -> gf.CrossSection:
343 """Return a large coplanar waveguide cross-section for a launcher.
345 This cross-section is designed for the wide end of the launcher,
346 providing a large area for probe pads and wirebonding.
348 The default dimensions are taken from :cite:`tuokkolaMethodsAchieveNearmillisecond2025`.
349 """
350 return coplanar_waveguide(width=200.0, gap=110.0, etch_layer=LAYER.M1_ETCH)
353@xsection
354def josephson_junction_cross_section_wide() -> gf.CrossSection:
355 """Return cross-section for the wide end of a Josephson junction wire.
357 The default dimensions are taken from :cite:`tuokkolaMethodsAchieveNearmillisecond2025`.
358 """
359 return gf.cross_section.cross_section(
360 width=0.2,
361 layer=LAYER.JJ_AREA,
362 )
365@xsection
366def josephson_junction_cross_section_narrow() -> gf.CrossSection:
367 """Return cross-section for the narrow end of a Josephson junction wire.
369 The default dimensions are taken from :cite:`tuokkolaMethodsAchieveNearmillisecond2025`.
370 """
371 return gf.cross_section.cross_section(
372 width=0.09,
373 layer=LAYER.JJ_AREA,
374 )
377@xsection
378def microstrip(
379 width: float = 10,
380 layer: LayerSpec = "M1_DRAW",
381) -> CrossSection:
382 """Return a microstrip cross_section.
384 The cross_section is considered additive (positive) on the layer.
385 """
386 return gf.cross_section.cross_section(
387 width=width,
388 layer=layer,
389 )
392strip = strip_metal = microstrip
394############################
395# Routing functions
396############################
398route_bundle = route_bundle_cpw = partial(
399 gf.routing.route_bundle,
400 cross_section=cpw,
401 bend="bend_circular",
402 collision_check_layers=[LAYER.WG],
403 on_collision="error",
404)
405route_bundle_all_angle = route_bundle_all_angle_cpw = partial(
406 gf.routing.route_bundle_all_angle,
407 cross_section=cpw,
408 separation=3,
409 bend="bend_circular_all_angle",
410 straight="straight_all_angle",
411)
412route_bundle_sbend = route_bundle_sbend_cpw = partial(
413 gf.routing.route_bundle_sbend,
414 cross_section=cpw,
415 bend_s="bend_s",
416)
418route_astar = route_astar_cpw = partial(
419 add_bundle_astar,
420 layers=["M1_ETCH"],
421 bend="bend_circular",
422 straight="straight",
423 grid_unit=500,
424 spacing=3,
425)
426routing_strategies = {
427 "route_bundle": route_bundle,
428 "route_bundle_cpw": route_bundle_cpw,
429 "route_bundle_all_angle": route_bundle_all_angle,
430 "route_bundle_all_angle_cpw": route_bundle_all_angle_cpw,
431 "route_bundle_sbend": route_bundle_sbend,
432 "route_bundle_sbend_cpw": route_bundle_sbend_cpw,
433 "route_astar": route_astar,
434 "route_astar_cpw": route_astar_cpw,
435}
437if __name__ == "__main__":
438 from gdsfactory.technology.klayout_tech import KLayoutTechnology
440 LAYER_VIEWS = LayerViews(PATH.lyp_yaml)
441 # De-nest layers
442 LAYERS_ACCORDING_TO_YAML = denest_layerviews_to_layer_tuples(LAYER_VIEWS)
443 print("LAYERS_ACCORDING_TO_YAML = {")
444 for yaml_layer_name, yaml_layer_tuple in LAYERS_ACCORDING_TO_YAML.items():
445 print(f"\t{yaml_layer_name}: Layer = {yaml_layer_tuple}")
446 print("}")
448 klayout_tech = KLayoutTechnology(
449 name="qpdk",
450 layer_map=LAYER,
451 layer_views=LAYER_VIEWS,
452 layer_stack=LAYER_STACK,
453 connectivity=LAYER_CONNECTIVITY,
454 )
455 klayout_tech.write_tech(tech_dir=PATH.klayout)
456 # print(DEFAULT_CROSS_SECTION_NAMES)
457 # print(strip() is strip())
458 # print(strip().name, strip().name)
459 # c = gf.c.bend_euler(cross_section="metal_routing")
460 # c.pprint_ports()