Coverage for qpdk / tech.py: 100%

80 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-14 10:27 +0000

1"""Technology definitions.""" 

2 

3from collections.abc import Callable, Sequence 

4from functools import cache, partial, wraps 

5from typing import Any 

6 

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) 

25 

26from qpdk.config import PATH 

27from qpdk.helper import denest_layerviews_to_layer_tuples 

28 

29nm = 1e-3 

30 

31 

32class LayerMapQPDK(LayerMap): 

33 """Layer map for QPDK technology.""" 

34 

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" 

40 

41 # flip-chip equivalents 

42 M2_DRAW: Layer = (2, 0) 

43 M2_ETCH: Layer = (2, 1) 

44 

45 # Airbridges 

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

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

48 

49 # Junctions 

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

51 JJ_PATCH: Layer = (20, 1) 

52 

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 

57 

58 # Alignment / admin 

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

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

61 

62 ################### 

63 # Non-fabrication # 

64 ################### 

65 

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

67 

68 # labels for gdsfactory 

69 LABEL_SETTINGS: Layer = (100, 0) 

70 LABEL_INSTANCE: Layer = (101, 0) 

71 

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

73 SIM_AREA: Layer = (98, 0) 

74 SIM_ONLY: Layer = (99, 0) 

75 

76 # Marker layer for waveguides 

77 WG: Layer = (102, 0) 

78 

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

80 ERROR_PATH: Layer = (1000, 0) 

81 

82 

83L = LAYER = LayerMapQPDK 

84 

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} 

93 

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} 

109 

110 

111@cache 

112def get_layer_stack() -> LayerStack: 

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

114 

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 ) 

197 

198 

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) 

207 

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] 

214 

215 

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) 

253 

254############################ 

255# Cross-sections functions 

256############################ 

257 

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

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

260 

261 

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

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

264 

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

266 

267 .. code-block:: python 

268 

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__ 

275 

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 

282 

283 cross_sections[func.__name__] = decorated_cross_section 

284 return decorated_cross_section 

285 

286 

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. 

296 

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

298 

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

303 

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 ) 

332 

333 

334cpw = coplanar_waveguide 

335etch = etch_only = partial( 

336 coplanar_waveguide, 

337 waveguide_layer=LAYER.M1_ETCH, 

338) 

339 

340 

341@xsection 

342def launcher_cross_section_big() -> gf.CrossSection: 

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

344 

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

346 providing a large area for probe pads and wirebonding. 

347 

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) 

351 

352 

353@xsection 

354def josephson_junction_cross_section_wide() -> gf.CrossSection: 

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

356 

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 ) 

363 

364 

365@xsection 

366def josephson_junction_cross_section_narrow() -> gf.CrossSection: 

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

368 

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 ) 

375 

376 

377@xsection 

378def microstrip( 

379 width: float = 10, 

380 layer: LayerSpec = "M1_DRAW", 

381) -> CrossSection: 

382 """Return a microstrip cross_section. 

383 

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 ) 

390 

391 

392strip = strip_metal = microstrip 

393 

394############################ 

395# Routing functions 

396############################ 

397 

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) 

417 

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} 

436 

437if __name__ == "__main__": 

438 from gdsfactory.technology.klayout_tech import KLayoutTechnology 

439 

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

447 

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