Coverage for qpdk / tech.py: 100%
99 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"""Technology definitions."""
3# ruff: noqa: T201
5from collections.abc import Callable, Generator, Sequence
6from functools import cache, partial, wraps
7from typing import Any
9import gdsfactory as gf
10from doroutes.bundles import add_bundle_astar
11from gdsfactory.cross_section import (
12 CrossSection,
13)
14from gdsfactory.technology import (
15 DerivedLayer,
16 LayerLevel,
17 LayerMap,
18 LayerStack,
19 LayerViews,
20 LogicalLayer,
21)
22from gdsfactory.typings import (
23 ConnectivitySpec,
24 CrossSectionSpec,
25 Layer,
26 LayerSpec,
27)
29from qpdk.config import PATH
30from qpdk.helper import denest_layerviews_to_layer_tuples
32nm = 1e-3
35class LayerMapQPDK(LayerMap):
36 """Layer map for QPDK technology."""
38 # Base metals
39 M1_DRAW: Layer = (1, 0) # Additive metal / positive mask regions
40 M1_ETCH: Layer = (1, 1) # Subtractive etch / negative mask regions
41 # Additive wins over subtractive where they overlap
42 # i.e., you can draw metal over an etched region to "fill it back in"
44 # flip-chip equivalents
45 M2_DRAW: Layer = (2, 0)
46 M2_ETCH: Layer = (2, 1)
48 # Airbridges
49 AB_DRAW: Layer = (10, 0) # Bridge metal
50 AB_VIA: Layer = (10, 1) # Landing pads / contacts
52 # Junctions
53 JJ_AREA: Layer = (20, 0) # Optional bridge/overlap definition
54 JJ_PATCH: Layer = (20, 1)
56 # Nanowire
57 NbTiN: Layer = (25, 0)
59 # Packaging / 3D integration / backside / misc.
60 IND: Layer = (30, 0)
61 TSV: Layer = (31, 0) # Throughs / vias / backside features
62 DICE: Layer = (70, 0) # Dicing lanes
64 # Alignment / admin
65 ALN_TOP: Layer = (80, 0) # Frontside alignment
66 ALN_BOT: Layer = (81, 0) # Backside alignment
68 ###################
69 # Non-fabrication #
70 ###################
72 TEXT: Layer = (90, 0) # Mask text / version labels
74 # labels for gdsfactory
75 LABEL_SETTINGS: Layer = (100, 0)
76 LABEL_INSTANCE: Layer = (101, 0)
78 # Simulation-only helpers (never sent to fab)
79 SIM_AREA: Layer = (98, 0)
80 SIM_ONLY: Layer = (99, 0)
82 # Marker layer for waveguides
83 WG: Layer = (102, 0)
85 # Error marker layer (used by gdsfactory to highlight routing/DRC errors)
86 ERROR_PATH: Layer = (1000, 0)
89L = LAYER = LayerMapQPDK
91material_properties = {
92 "vacuum": {"relative_permittivity": 1},
93 "Nb": {"relative_permittivity": float("inf")},
94 "Si": {"relative_permittivity": 11.45},
95 "AlOx/Al": {"relative_permittivity": float("inf")},
96 "TiN": {"relative_permittivity": float("inf")},
97 "In": {"relative_permittivity": float("inf")},
98}
100NON_METADATA_LAYERS = {
101 LAYER.M1_DRAW,
102 LAYER.M1_ETCH,
103 LAYER.M2_DRAW,
104 LAYER.M2_ETCH,
105 LAYER.AB_DRAW,
106 LAYER.AB_VIA,
107 LAYER.JJ_AREA,
108 LAYER.JJ_PATCH,
109 LAYER.IND,
110 LAYER.TSV,
111 LAYER.DICE,
112 LAYER.ALN_TOP,
113 LAYER.ALN_BOT,
114 LAYER.NbTiN,
115}
118@cache
119def get_layer_stack() -> LayerStack:
120 """Returns a LayerStack corresponding to the PDK.
122 The stack roughly corresponds to that of :cite:`tuokkolaMethodsAchieveNearmillisecond2025`.
123 """
124 return LayerStack(
125 layers={
126 # Base metal film (e.g., 200 nm of Nb)
127 "M1": LayerLevel(
128 name="M1",
129 # Generate base metal by subtracting (modified) etch from sim. area
130 layer=DerivedLayer(
131 layer1=LogicalLayer(layer=L.SIM_AREA),
132 # additive wins over substractive etch
133 layer2=DerivedLayer(
134 layer1=LogicalLayer(layer=L.M1_ETCH),
135 layer2=LogicalLayer(layer=L.M1_DRAW),
136 operation="-",
137 ),
138 operation="-",
139 ),
140 derived_layer=LogicalLayer(layer=L.M1_DRAW),
141 thickness=200e-9 * 1e6,
142 zmin=0.0, # top of substrate
143 material="Nb",
144 mesh_order=1,
145 ),
146 "NbTiN": LayerLevel(
147 name="NbTiN",
148 layer=L.NbTiN,
149 thickness=15e-9 * 1e6,
150 zmin=0.0, # top of substrate
151 material="NbTiN",
152 mesh_order=1,
153 ),
154 "Substrate": LayerLevel(
155 name="Substrate",
156 layer=L.SIM_AREA,
157 thickness=500, # 500 microns of silicon
158 zmin=-500, # below metal
159 material="Si",
160 mesh_order=4,
161 ),
162 "Vacuum": LayerLevel(
163 name="Vacuum",
164 layer=L.SIM_AREA,
165 thickness=500e-6 * 1e6, # 500 microns of vacuum above metal
166 zmin=200e-9 * 1e6, # above metal
167 material="vacuum",
168 mesh_order=99,
169 ),
170 # Airbridge metal sitting above M1 (example: +300 nm)
171 "Airbridge": LayerLevel(
172 name="Airbridge",
173 layer=L.AB_DRAW,
174 thickness=200e-9 * 1e6,
175 zmin=300e-9 * 1e6, # stacked above via
176 material="Nb",
177 ),
178 "Airbridge_Via": LayerLevel(
179 name="Airbridge_Via",
180 layer=L.AB_VIA,
181 thickness=100e-9 * 1e6,
182 zmin=200e-9 * 1e6, # stacked above M1
183 material="Nb",
184 ),
185 # JJ_AREA can be exported as a thin film if you use it in EM
186 "JosephsonJunction": LayerLevel(
187 name="JosephsonJunction",
188 layer=L.JJ_AREA,
189 thickness=70e-9,
190 zmin=0,
191 material="AlOx/Al",
192 mesh_order=2,
193 ),
194 "TSV": LayerLevel(
195 name="TSV",
196 layer=L.TSV,
197 thickness=500, # full substrate thickness
198 zmin=-500, # starting at bottom
199 material="TiN",
200 mesh_order=3,
201 ),
202 "IndiumBump": LayerLevel(
203 name="IndiumBump",
204 layer=L.IND,
205 thickness=10, # 10 microns
206 zmin=200e-9 * 1e6, # stacked above M1
207 material="In",
208 mesh_order=3,
209 ),
210 }
211 )
214LAYER_STACK = get_layer_stack()
215# Nicer for 3D visualization
216LAYER_STACK_NO_VACUUM = LayerStack(
217 layers={
218 name: level for name, level in LAYER_STACK.layers.items() if name != "Vacuum"
219 }
220)
221LAYER_VIEWS = gf.technology.LayerViews(PATH.lyp)
223LAYER_CONNECTIVITY: Sequence[ConnectivitySpec] = [
224 ("M1_DRAW", "TSV", "M2_DRAW"),
225 ("M1_DRAW", "IND", "M2_DRAW"),
226 ("M1_DRAW", "AB_DRAW", "M1_DRAW"),
227 ("M2_DRAW", "AB_DRAW", "M2_DRAW"),
228]
231LAYER_STACK_FLIP_CHIP = LayerStack(
232 layers={
233 **LAYER_STACK.layers,
234 "M2": LayerLevel(
235 name="M2",
236 layer=DerivedLayer(
237 layer1=LogicalLayer(layer=L.SIM_AREA),
238 layer2=DerivedLayer(
239 layer1=LogicalLayer(layer=L.M2_ETCH),
240 layer2=LogicalLayer(layer=L.M2_DRAW),
241 operation="-",
242 ),
243 operation="-",
244 ),
245 derived_layer=LogicalLayer(layer=L.M2_DRAW),
246 thickness=0.2,
247 zmin=10.0,
248 material="Nb",
249 mesh_order=1,
250 ),
251 "Substrate_top": LayerLevel(
252 name="Substrate_top",
253 layer=L.SIM_AREA,
254 thickness=500, # 500 microns of silicon
255 zmin=10.2, # below metal
256 material="Si",
257 mesh_order=4,
258 ),
259 }
260)
261LAYER_STACK_FLIP_CHIP_NO_VACUUM = LayerStack(
262 layers={
263 name: level
264 for name, level in LAYER_STACK_FLIP_CHIP.layers.items()
265 if name != "Vacuum"
266 }
267)
269############################
270# Cross-sections functions
271############################
273cross_sections: dict[str, Callable[..., CrossSection]] = {}
274_cross_section_default_names: dict[str, str] = {}
277def xsection(func: Callable[..., CrossSection]) -> Callable[..., CrossSection]:
278 """Returns decorated to register a cross section function.
280 Ensures that the cross-section name matches the name of the function that generated it when created using default parameters
282 .. code-block:: python
284 @xsection
285 def strip(width=TECH.width_strip, radius=TECH.radius_strip):
286 return gf.cross_section.cross_section(width=width, radius=radius)
287 """
288 default_cross_section = func()
289 _cross_section_default_names[default_cross_section.name] = func.__name__
291 @wraps(func)
292 def decorated_cross_section(**kwargs: Any) -> CrossSection:
293 cross_section = func(**kwargs)
294 if cross_section.name in _cross_section_default_names:
295 cross_section._name = _cross_section_default_names[cross_section.name]
296 return cross_section
298 cross_sections[func.__name__] = decorated_cross_section
299 return decorated_cross_section
302def get_etch_sections(
303 cross_section: CrossSectionSpec,
304) -> Generator[gf.Section, None, None]:
305 """Yields all etch sections of a cross-section.
307 Args:
308 cross_section: The cross-section to search for etch sections.
310 Yields:
311 Generator of sections with "etch" in their name.
312 """
313 xs = gf.get_cross_section(cross_section)
314 for s in xs.sections:
315 if s.name and "etch" in s.name:
316 yield s
319def get_etch_section(
320 cross_section: CrossSectionSpec,
321) -> gf.Section:
322 """Returns the first etch section of a cross-section.
324 Args:
325 cross_section: The cross-section to search for an etch section.
327 Returns:
328 The first section with "etch" in its name.
330 Raises:
331 ValueError: If no etch section is found.
332 """
333 try:
334 return next(get_etch_sections(cross_section))
335 except StopIteration as e:
336 xs = gf.get_cross_section(cross_section)
337 msg = (
338 f"Cross-section '{xs.name}' does not have a section with 'etch' in the name. "
339 f"Found sections: {[s.name for s in xs.sections]}"
340 )
341 raise ValueError(msg) from e
344@xsection
345def coplanar_waveguide(
346 width: float = 10,
347 gap: float = 6,
348 waveguide_layer: LayerSpec = LAYER.M1_DRAW,
349 etch_layer: LayerSpec = LAYER.M1_ETCH,
350 radius: float | None = 100,
351) -> CrossSection:
352 """Return a coplanar waveguide cross_section.
354 The cross_section is considered negative (etched) on the physical layer.
356 Note:
357 Assuming a silicon substrate thickness of 500 µm and a metal thickness of 100 nm,
358 the default center conductor width and gap dimensions give a characteristic
359 impedance of approximately 50 Ω.
361 Args:
362 width: center conductor width in µm.
363 gap: gap between center conductor and ground in µm.
364 waveguide_layer: for the center conductor (positive) region.
365 etch_layer: for the etch (negative) region.
366 radius: bend radius (if applicable).
367 """
368 return gf.cross_section.cross_section(
369 width=width,
370 layer=waveguide_layer,
371 radius=radius,
372 sections=(
373 gf.Section(
374 width=gap,
375 offset=(gap + width) / 2,
376 layer=etch_layer,
377 name="etch_offset_pos",
378 ),
379 gf.Section(
380 width=gap,
381 offset=-(gap + width) / 2,
382 layer=etch_layer,
383 name="etch_offset_neg",
384 ),
385 gf.Section(width=width, layer=LAYER.WG, name="waveguide"),
386 ),
387 radius_min=(width + 2 * gap) / 2,
388 )
391cpw = coplanar_waveguide
392etch = etch_only = partial(
393 coplanar_waveguide,
394 waveguide_layer=LAYER.M1_ETCH,
395)
398@xsection
399def meander_inductor_cross_section() -> CrossSection:
400 """Return a narrow coplanar waveguide cross-section for meander inductors.
402 The default dimensions are width=2.0 µm and gap=2.0 µm.
403 """
404 return coplanar_waveguide(width=2.0, gap=2.0)
407@xsection
408def superinductor_cross_section() -> CrossSection:
409 """Return an ultra-narrow coplanar waveguide cross-section for fluxonium superinductors.
411 The default dimensions (width=0.04 µm, gap=0.09 µm) are based on NbTiN superinductor and SNSPD
412 reports :cite:`hazardNanowireSuperinductanceFluxonium2019,yangComparisonSuperconductingNanowire2018`.
413 """
414 return coplanar_waveguide(
415 width=0.04, gap=0.09, waveguide_layer=LAYER.NbTiN, etch_layer=LAYER.M1_ETCH
416 )
419@xsection
420def launcher_cross_section_big() -> gf.CrossSection:
421 """Return a large coplanar waveguide cross-section for a launcher.
423 This cross-section is designed for the wide end of the launcher,
424 providing a large area for probe pads and wirebonding.
426 The default dimensions are taken from :cite:`tuokkolaMethodsAchieveNearmillisecond2025`.
427 """
428 return coplanar_waveguide(width=200.0, gap=110.0, etch_layer=LAYER.M1_ETCH)
431@xsection
432def josephson_junction_cross_section_wide() -> gf.CrossSection:
433 """Return cross-section for the wide end of a Josephson junction wire.
435 The default dimensions are taken from :cite:`tuokkolaMethodsAchieveNearmillisecond2025`.
436 """
437 return gf.cross_section.cross_section(
438 width=0.2,
439 layer=LAYER.JJ_AREA,
440 )
443@xsection
444def josephson_junction_cross_section_narrow() -> gf.CrossSection:
445 """Return cross-section for the narrow end of a Josephson junction wire.
447 The default dimensions are taken from :cite:`tuokkolaMethodsAchieveNearmillisecond2025`.
448 """
449 return gf.cross_section.cross_section(
450 width=0.09,
451 layer=LAYER.JJ_AREA,
452 )
455@xsection
456def microstrip(
457 width: float = 10,
458 layer: LayerSpec = "M1_DRAW",
459) -> CrossSection:
460 """Return a microstrip cross_section.
462 The cross_section is considered additive (positive) on the layer.
463 """
464 return gf.cross_section.cross_section(
465 width=width,
466 layer=layer,
467 )
470strip = strip_metal = microstrip
472############################
473# Routing functions
474############################
476route_bundle = route_bundle_cpw = partial(
477 gf.routing.route_bundle,
478 cross_section=cpw,
479 bend="bend_circular",
480 collision_check_layers=[LAYER.WG],
481 on_collision="error",
482)
483route_bundle_all_angle = route_bundle_all_angle_cpw = partial(
484 gf.routing.route_bundle_all_angle,
485 cross_section=cpw,
486 separation=3,
487 bend="bend_circular_all_angle",
488 straight="straight_all_angle",
489)
490route_bundle_sbend = route_bundle_sbend_cpw = partial(
491 gf.routing.route_bundle_sbend,
492 cross_section=cpw,
493 bend_s="bend_s",
494)
496route_astar = route_astar_cpw = partial(
497 add_bundle_astar,
498 layers=["M1_ETCH"],
499 bend="bend_circular",
500 straight="straight",
501 grid_unit=500,
502 spacing=3,
503)
504routing_strategies = {
505 "route_bundle": route_bundle,
506 "route_bundle_cpw": route_bundle_cpw,
507 "route_bundle_all_angle": route_bundle_all_angle,
508 "route_bundle_all_angle_cpw": route_bundle_all_angle_cpw,
509 "route_bundle_sbend": route_bundle_sbend,
510 "route_bundle_sbend_cpw": route_bundle_sbend_cpw,
511 "route_astar": route_astar,
512 "route_astar_cpw": route_astar_cpw,
513}
515if __name__ == "__main__":
516 from gdsfactory.technology.klayout_tech import KLayoutTechnology
518 LAYER_VIEWS = LayerViews(PATH.lyp_yaml)
519 # De-nest layers
520 LAYERS_ACCORDING_TO_YAML = denest_layerviews_to_layer_tuples(LAYER_VIEWS)
521 print("LAYERS_ACCORDING_TO_YAML = {")
522 for yaml_layer_name, yaml_layer_tuple in LAYERS_ACCORDING_TO_YAML.items():
523 print(f"\t{yaml_layer_name}: Layer = {yaml_layer_tuple}")
524 print("}")
526 klayout_tech = KLayoutTechnology(
527 name="qpdk",
528 layer_map=LAYER,
529 layer_views=LAYER_VIEWS,
530 layer_stack=LAYER_STACK,
531 connectivity=LAYER_CONNECTIVITY,
532 )
533 klayout_tech.write_tech(tech_dir=PATH.klayout)
534 # print(DEFAULT_CROSS_SECTION_NAMES)
535 # print(strip() is strip())
536 # print(strip().name, strip().name)
537 # c = gf.c.bend_euler(cross_section="metal_routing")
538 # c.pprint_ports()