Coverage for qpdk / tech.py: 100%

99 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-02 17:50 +0000

1"""Technology definitions.""" 

2 

3# ruff: noqa: T201 

4 

5from collections.abc import Callable, Generator, Sequence 

6from functools import cache, partial, wraps 

7from typing import Any 

8 

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) 

28 

29from qpdk.config import PATH 

30from qpdk.helper import denest_layerviews_to_layer_tuples 

31 

32nm = 1e-3 

33 

34 

35class LayerMapQPDK(LayerMap): 

36 """Layer map for QPDK technology.""" 

37 

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" 

43 

44 # flip-chip equivalents 

45 M2_DRAW: Layer = (2, 0) 

46 M2_ETCH: Layer = (2, 1) 

47 

48 # Airbridges 

49 AB_DRAW: Layer = (10, 0) # Bridge metal 

50 AB_VIA: Layer = (10, 1) # Landing pads / contacts 

51 

52 # Junctions 

53 JJ_AREA: Layer = (20, 0) # Optional bridge/overlap definition 

54 JJ_PATCH: Layer = (20, 1) 

55 

56 # Nanowire 

57 NbTiN: Layer = (25, 0) 

58 

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 

63 

64 # Alignment / admin 

65 ALN_TOP: Layer = (80, 0) # Frontside alignment 

66 ALN_BOT: Layer = (81, 0) # Backside alignment 

67 

68 ################### 

69 # Non-fabrication # 

70 ################### 

71 

72 TEXT: Layer = (90, 0) # Mask text / version labels 

73 

74 # labels for gdsfactory 

75 LABEL_SETTINGS: Layer = (100, 0) 

76 LABEL_INSTANCE: Layer = (101, 0) 

77 

78 # Simulation-only helpers (never sent to fab) 

79 SIM_AREA: Layer = (98, 0) 

80 SIM_ONLY: Layer = (99, 0) 

81 

82 # Marker layer for waveguides 

83 WG: Layer = (102, 0) 

84 

85 # Error marker layer (used by gdsfactory to highlight routing/DRC errors) 

86 ERROR_PATH: Layer = (1000, 0) 

87 

88 

89L = LAYER = LayerMapQPDK 

90 

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} 

99 

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} 

116 

117 

118@cache 

119def get_layer_stack() -> LayerStack: 

120 """Returns a LayerStack corresponding to the PDK. 

121 

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 ) 

212 

213 

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) 

222 

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] 

229 

230 

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) 

268 

269############################ 

270# Cross-sections functions 

271############################ 

272 

273cross_sections: dict[str, Callable[..., CrossSection]] = {} 

274_cross_section_default_names: dict[str, str] = {} 

275 

276 

277def xsection(func: Callable[..., CrossSection]) -> Callable[..., CrossSection]: 

278 """Returns decorated to register a cross section function. 

279 

280 Ensures that the cross-section name matches the name of the function that generated it when created using default parameters 

281 

282 .. code-block:: python 

283 

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__ 

290 

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 

297 

298 cross_sections[func.__name__] = decorated_cross_section 

299 return decorated_cross_section 

300 

301 

302def get_etch_sections( 

303 cross_section: CrossSectionSpec, 

304) -> Generator[gf.Section, None, None]: 

305 """Yields all etch sections of a cross-section. 

306 

307 Args: 

308 cross_section: The cross-section to search for etch sections. 

309 

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 

317 

318 

319def get_etch_section( 

320 cross_section: CrossSectionSpec, 

321) -> gf.Section: 

322 """Returns the first etch section of a cross-section. 

323 

324 Args: 

325 cross_section: The cross-section to search for an etch section. 

326 

327 Returns: 

328 The first section with "etch" in its name. 

329 

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 

342 

343 

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. 

353 

354 The cross_section is considered negative (etched) on the physical layer. 

355 

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 Ω. 

360 

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 ) 

389 

390 

391cpw = coplanar_waveguide 

392etch = etch_only = partial( 

393 coplanar_waveguide, 

394 waveguide_layer=LAYER.M1_ETCH, 

395) 

396 

397 

398@xsection 

399def meander_inductor_cross_section() -> CrossSection: 

400 """Return a narrow coplanar waveguide cross-section for meander inductors. 

401 

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) 

405 

406 

407@xsection 

408def superinductor_cross_section() -> CrossSection: 

409 """Return an ultra-narrow coplanar waveguide cross-section for fluxonium superinductors. 

410 

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 ) 

417 

418 

419@xsection 

420def launcher_cross_section_big() -> gf.CrossSection: 

421 """Return a large coplanar waveguide cross-section for a launcher. 

422 

423 This cross-section is designed for the wide end of the launcher, 

424 providing a large area for probe pads and wirebonding. 

425 

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) 

429 

430 

431@xsection 

432def josephson_junction_cross_section_wide() -> gf.CrossSection: 

433 """Return cross-section for the wide end of a Josephson junction wire. 

434 

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 ) 

441 

442 

443@xsection 

444def josephson_junction_cross_section_narrow() -> gf.CrossSection: 

445 """Return cross-section for the narrow end of a Josephson junction wire. 

446 

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 ) 

453 

454 

455@xsection 

456def microstrip( 

457 width: float = 10, 

458 layer: LayerSpec = "M1_DRAW", 

459) -> CrossSection: 

460 """Return a microstrip cross_section. 

461 

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 ) 

468 

469 

470strip = strip_metal = microstrip 

471 

472############################ 

473# Routing functions 

474############################ 

475 

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) 

495 

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} 

514 

515if __name__ == "__main__": 

516 from gdsfactory.technology.klayout_tech import KLayoutTechnology 

517 

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("}") 

525 

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()