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

1"""Helper functions for QPDK cells.""" 

2 

3from collections.abc import Iterable, Sequence 

4from itertools import starmap 

5 

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 

11 

12from qpdk.logger import logger 

13from qpdk.tech import LAYER, NON_METADATA_LAYERS 

14 

15 

16def transform_component(component: gf.Component, transform: DCplxTrans) -> gf.Component: 

17 """Applies a complex transformation to a component. 

18 

19 For use with :func:`~gdsfactory.container`. 

20 

21 Returns: 

22 The transformed component. 

23 """ 

24 component.transform(transform) 

25 return component 

26 

27 

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. 

42 

43 Coordinates can be specified using either (x0, x1) or (x_center, width), 

44 and similarly for y. 

45 

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. 

57 

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

67 

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

74 

75 c.add_polygon([(x_lo, y_lo), (x_hi, y_lo), (x_hi, y_hi), (x_lo, y_hi)], layer=layer) 

76 

77 

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] 

87 

88 

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. 

99 

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. 

103 

104 This is a simple wrapper over :func:`~gdsfactory.Component.fill` which itself wraps 

105 the fill function from kfactory. 

106 

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. 

119 

120 Returns: 

121 A new component with the original component plus fill rectangles. 

122 

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) 

134 

135 c = gf.Component() 

136 c.add_ref(component) 

137 

138 exclude_layers = exclude_layers or _EXCLUDE_LAYERS_DEFAULT_M1 

139 

140 # Create the fill rectangle cell 

141 fill_cell = gf.components.rectangle( 

142 size=rectangle_size, 

143 layer=fill_layer, 

144 ) 

145 

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 ) 

150 

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 ) 

163 

164 return c 

165 

166 

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. 

174 

175 This function: 

176 

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. 

182 

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. 

186 

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. 

192 

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) 

206 

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) 

217 

218 return result 

219 

220 

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. 

228 

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. 

232 

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

249 

250 

251def apply_additive_metals(component: Component) -> Component: 

252 """Apply additive metal layers and remove them. 

253 

254 Removes additive metal layers from etch layers, leading to a negative mask. 

255 

256 TODO: Implement without flattening. Maybe with a KLayout dataprep script? 

257 

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 

277 

278 

279@gf.cell 

280def invert_mask_polarity(component: Component) -> Component: 

281 """Invert mask polarity of a component. 

282 

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. 

286 

287 Args: 

288 component: The component to invert. 

289 

290 Returns: 

291 A new component with inverted mask polarity. 

292 """ 

293 c = gf.Component() 

294 

295 # Bounding box of the component defines the outer boundary for inversion 

296 bbox_region = Region(component.bbox().to_itype(component.kcl.dbu)) 

297 

298 affected_layers: set[int] = set() 

299 

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

308 

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

312 

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 

322 

323 # Invert the polarities using the bounding box 

324 new_add_region = bbox_region - etch_region 

325 new_etch_region = bbox_region - add_region 

326 

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) 

330 

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) 

336 

337 return c 

338 

339 

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. 

344 

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. 

348 

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. 

352 

353 Returns: 

354 A new component with extended layers. 

355 """ 

356 c = gf.Component() 

357 

358 bbox = component.bbox() 

359 

360 bbox_region = Region(bbox.to_itype(component.kcl.dbu)) 

361 

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 } 

366 

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) 

375 

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) 

379 

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) 

392 

393 return c 

394 

395 

396def remove_metadata_layers(component: Component) -> Component: 

397 """Remove metadata layers from a component. 

398 

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. 

402 

403 All other layers are stripped out. 

404 

405 Args: 

406 component: The component to clean. 

407 

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

413 

414 c = gf.Component() 

415 

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) 

421 

422 return c 

423 

424 

425if __name__ == "__main__": 

426 from qpdk import PDK 

427 from qpdk.cells.resonator import resonator 

428 

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