Coverage for qpdk / cells / helpers.py: 100%

86 statements  

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

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

2 

3from collections.abc import Iterable, Sequence 

4 

5import gdsfactory as gf 

6import klayout.db as kdb 

7from gdsfactory.component import Component 

8from gdsfactory.typings import Layer, LayerSpec 

9from klayout.db import DCplxTrans, Region 

10 

11from qpdk.logger import logger 

12from qpdk.tech import LAYER, NON_METADATA_LAYERS 

13 

14 

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

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

17 

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

19 """ 

20 component.transform(transform) 

21 return component 

22 

23 

24_EXCLUDE_LAYERS_DEFAULT_M1 = [ 

25 (LAYER.M1_ETCH, 80), 

26 (LAYER.M1_DRAW, 80), 

27 (LAYER.WG, 80), 

28] 

29_EXCLUDE_LAYERS_DEFAULT_M2 = [ 

30 (LAYER.M2_ETCH, 80), 

31 (LAYER.M2_DRAW, 80), 

32] 

33 

34 

35@gf.cell 

36def fill_magnetic_vortices( 

37 component: Component | None = None, 

38 rectangle_size: tuple[float, float] = (15.0, 15.0), 

39 gap: float | tuple[float, float] = 15.0, 

40 stagger: float | tuple[float, float] = 3.0, 

41 exclude_layers: Iterable[tuple[LayerSpec, float]] | None = None, 

42 fill_layer: LayerSpec = LAYER.M1_ETCH, 

43) -> Component: 

44 """Fill a component with small rectangles to trap magnetic vortices. 

45 

46 This function fills the bounding box area of a given component with small etch 

47 rectangles in an array placed with specified gaps. The purpose is to trap 

48 local magnetic vortices in superconducting quantum circuits. 

49 

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

51 the fill function from kfactory. 

52 

53 Args: 

54 component: The component to fill with vortex trapping rectangles. 

55 If None, a default straight waveguide (:func:`gf.components.straight`) 

56 with length 100 µm is used. 

57 rectangle_size: Size of the fill rectangles in µm (width, height). 

58 gap: Gap between rectangles in µm. 

59 A tuple (x_gap, y_gap) can be provided for different gaps in x and y directions. 

60 stagger: Amount of staggering in µm to apply to pattern. 

61 A tuple (x_stagger, y_stagger) can be provided for different staggering in x and y. 

62 exclude_layers: Layers to ignore. Tuples of layer and keepout in µm. 

63 Defaults to M1_ETCH, M1_DRAW, and WG layers with 80 µm keepout. 

64 fill_layer: Layer for the fill rectangles. 

65 

66 Returns: 

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

68 

69 Example: 

70 >>> from qpdk.cells.resonator import resonator_quarter_wave 

71 >>> from qpdk.cells.helpers import fill_magnetic_vortices 

72 >>> resonator = resonator_quarter_wave() 

73 >>> filled_resonator = fill_magnetic_vortices(resonator) 

74 >>> # Or use with default component 

75 >>> filled_default = fill_magnetic_vortices() 

76 """ 

77 # Use a default component if none is provided 

78 if component is None: 

79 component = gf.components.straight(length=100.0) 

80 

81 c = gf.Component() 

82 c.add_ref(component) 

83 

84 exclude_layers = exclude_layers or _EXCLUDE_LAYERS_DEFAULT_M1 

85 

86 # Create the fill rectangle cell 

87 fill_cell = gf.components.rectangle( 

88 size=rectangle_size, 

89 layer=fill_layer, 

90 ) 

91 

92 gap_x, gap_y = (gap, gap) if isinstance(gap, int | float) else gap 

93 stagger_x, stagger_y = ( 

94 (stagger, stagger) if isinstance(stagger, int | float) else stagger 

95 ) 

96 

97 c.fill( 

98 fill_cell=fill_cell, 

99 fill_regions=[ 

100 ( 

101 Region(c.bbox().to_itype(dbu=c.kcl.dbu)), 

102 0, 

103 ) 

104 ], # Fill the entire bounding box area 

105 exclude_layers=exclude_layers, 

106 row_step=gf.kf.kdb.DVector(rectangle_size[0] + gap_x, stagger_y), 

107 col_step=gf.kf.kdb.DVector(-stagger_x, rectangle_size[1] + gap_y), 

108 ) 

109 

110 return c 

111 

112 

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

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

115 

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

117 

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

119 """ 

120 for additive, etch in ( 

121 (LAYER.M1_DRAW, LAYER.M1_ETCH), 

122 (LAYER.M2_DRAW, LAYER.M2_ETCH), 

123 ): 

124 component_etch_only = gf.boolean( 

125 A=component, 

126 B=component, 

127 operation="-", 

128 layer=etch, 

129 layer1=etch, 

130 layer2=additive, 

131 ) 

132 component.flatten() 

133 component.remove_layers([etch, additive]) 

134 component << component_etch_only 

135 

136 return component 

137 

138 

139@gf.cell 

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

141 """Invert mask polarity of a component. 

142 

143 Converts DRAW layers to ETCH layers by subtracting the DRAW layer from the 

144 component's bounding box, and similarly converts ETCH layers to DRAW layers. 

145 This is applied to M1 and M2 layers. All other layers are copied intact. 

146 

147 Args: 

148 component: The component to invert. 

149 

150 Returns: 

151 A new component with inverted mask polarity. 

152 """ 

153 c = gf.Component() 

154 

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

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

157 

158 affected_layers: set[int] = set() 

159 

160 for additive, etch in ( 

161 (LAYER.M1_DRAW, LAYER.M1_ETCH), 

162 (LAYER.M2_DRAW, LAYER.M2_ETCH), 

163 ): 

164 # Determine the layer indices in the layout object 

165 add_layer_index = component.kcl.layer(*additive) 

166 etch_layer_index = component.kcl.layer(*etch) 

167 affected_layers.update([add_layer_index, etch_layer_index]) 

168 

169 # Extract the shapes of the old component on these layers as regions 

170 add_region = Region(component.begin_shapes_rec(add_layer_index)) 

171 etch_region = Region(component.begin_shapes_rec(etch_layer_index)) 

172 

173 # Skip if both regions are empty (no shapes on these layers) 

174 if add_region.is_empty() and etch_region.is_empty(): 

175 logger.debug( 

176 "Skipping empty layers: {}, {} in component {}", 

177 additive, 

178 etch, 

179 component.name, 

180 ) 

181 continue 

182 

183 # Invert the polarities using the bounding box 

184 new_add_region = bbox_region - etch_region 

185 new_etch_region = bbox_region - add_region 

186 

187 # Insert the inverted regions into the new component 

188 c.shapes(add_layer_index).insert(new_add_region) 

189 c.shapes(etch_layer_index).insert(new_etch_region) 

190 

191 # Copy all other layers intact 

192 for layer_index in set(component.kcl.layer_indices()) - affected_layers: 

193 other_region = Region(component.begin_shapes_rec(layer_index)) 

194 if not other_region.is_empty(): 

195 c.shapes(layer_index).insert(other_region) 

196 

197 return c 

198 

199 

200def add_margin_to_layer( 

201 component: Component, layer_margins: Sequence[tuple[Layer, float]] 

202) -> Component: 

203 """Increase the component bounding box by adding a margin to given draw layers. 

204 

205 For each specified layer in the component, it is extended outwards 

206 by the specified margin. This effectively increases the bounding box of the 

207 component which can be useful to define the simulation area in HFSS. 

208 

209 Args: 

210 component: The component to modify. 

211 layer_margins: Sequence of tuples containing the layer to modify and the margin to add in µm. 

212 

213 Returns: 

214 A new component with extended layers. 

215 """ 

216 c = gf.Component() 

217 

218 bbox = component.bbox() 

219 

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

221 

222 # Identify layer indices for the specified margins 

223 layer_indices_margins = { 

224 component.kcl.layer(*layer): margin for layer, margin in layer_margins 

225 } 

226 

227 # Copy existing shapes and track which specified layers are present 

228 present_layer_indices = set() 

229 for layer_index in component.kcl.layer_indices(): 

230 region = Region(component.begin_shapes_rec(layer_index)) 

231 if not region.is_empty(): 

232 c.shapes(layer_index).insert(region) 

233 if layer_index in layer_indices_margins: 

234 present_layer_indices.add(layer_index) 

235 

236 # Add margins to the layers that are present in the component 

237 bbox_itype = bbox.to_itype(component.kcl.dbu) 

238 bbox_region = Region(bbox_itype) 

239 

240 for layer_index in present_layer_indices: 

241 margin = layer_indices_margins[layer_index] 

242 margin_dbu = int(margin / component.kcl.dbu) 

243 new_bbox = kdb.Box( 

244 bbox_itype.left - margin_dbu, 

245 bbox_itype.bottom - margin_dbu, 

246 bbox_itype.right + margin_dbu, 

247 bbox_itype.top + margin_dbu, 

248 ) 

249 new_bbox_region = Region(new_bbox) 

250 margin_region = new_bbox_region - bbox_region 

251 c.shapes(layer_index).insert(margin_region) 

252 

253 return c 

254 

255 

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

257 """Remove metadata layers from a component. 

258 

259 Retains only physical and base layers: 

260 M1_DRAW, M1_ETCH, M2_DRAW, M2_ETCH, AB_DRAW, AB_VIA, 

261 JJ_AREA, JJ_PATCH, IND, TSV, DICE, ALN_TOP, ALN_BOT. 

262 

263 All other layers are stripped out. 

264 

265 Args: 

266 component: The component to clean. 

267 

268 Returns: 

269 A new component with metadata layers removed. 

270 """ 

271 # Convert allowed layers into kcl layer indices 

272 allowed_indices = {component.kcl.layer(*layer) for layer in NON_METADATA_LAYERS} 

273 

274 c = gf.Component() 

275 

276 for layer_index in component.kcl.layer_indices(): 

277 if layer_index in allowed_indices: 

278 other_region = Region(component.begin_shapes_rec(layer_index)) 

279 if not other_region.is_empty(): 

280 c.shapes(layer_index).insert(other_region) 

281 

282 return c 

283 

284 

285if __name__ == "__main__": 

286 from qpdk import PDK 

287 from qpdk.cells.resonator import resonator 

288 

289 PDK.activate() 

290 c = resonator(length=2000) 

291 c = apply_additive_metals(c.copy()) 

292 c = invert_mask_polarity(c) 

293 c = add_margin_to_layer( 

294 c, layer_margins=[(LAYER.M1_DRAW, 50.0), (LAYER.M2_DRAW, 50.0)] 

295 ) 

296 c.show()