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

89 statements  

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

1"""Capacitive coupler components.""" 

2 

3from __future__ import annotations 

4 

5from functools import partial 

6from itertools import chain 

7from math import ceil, floor 

8 

9import gdsfactory as gf 

10from gdsfactory.component import Component 

11from gdsfactory.typings import CrossSectionSpec, LayerSpec 

12 

13from qpdk.cells.waveguides import straight 

14from qpdk.helper import show_components 

15from qpdk.tech import LAYER 

16 

17 

18@gf.cell 

19def interdigital_capacitor( 

20 fingers: int = 4, 

21 finger_length: float = 20.0, 

22 finger_gap: float = 2.0, 

23 thickness: float = 5.0, 

24 etch_layer: LayerSpec | None = "M1_ETCH", 

25 etch_bbox_margin: float = 2.0, 

26 cross_section: CrossSectionSpec = "cpw", 

27 half: bool = False, 

28) -> Component: 

29 """Generate an interdigital capacitor component with ports on both ends. 

30 

31 An interdigital capacitor consists of interleaved metal fingers that create 

32 a distributed capacitance. This component creates a planar capacitor with 

33 two sets of interleaved fingers extending from opposite ends. 

34 

35 .. svgbob:: 

36 

37 ┌─┐───────┐┌─┐ 

38 │ │───────┘│ │ 

39 │ │ ┌──────│ │ 

40 ┌│ │ └──────│ │┐ 

41 o1└│ │──────┐ │ │┘o2 

42 │ │──────┘ │ │ 

43 │ │ ┌──────│ │ 

44 └─┘ └──────└─┘ 

45 

46 See for example :cite:`leizhuAccurateCircuitModel2000`. 

47 

48 Note: 

49 ``finger_length=0`` effectively provides a parallel plate capacitor. 

50 The capacitance scales approximately linearly with the number of fingers 

51 and finger length. 

52 

53 Args: 

54 fingers: Total number of fingers of the capacitor (must be >= 1). 

55 finger_length: Length of each finger in μm. 

56 finger_gap: Gap between adjacent fingers in μm. 

57 thickness: Thickness of fingers and the base section in μm. 

58 etch_layer: Optional layer for etching around the capacitor. 

59 etch_bbox_margin: Margin around the capacitor for the etch layer in μm. 

60 cross_section: Cross-section for the short straight from the etch box capacitor. 

61 half: If True, creates a single-sided capacitor (half of the interdigital capacitor). 

62 

63 Returns: 

64 Component: A gdsfactory component with the interdigital capacitor geometry 

65 and two ports ('o1' and 'o2') on opposing sides. 

66 """ 

67 c = Component() 

68 

69 # Used temporarily 

70 layer = LAYER.M1_DRAW 

71 

72 if fingers < 1: 

73 raise ValueError("Must have at least 1 finger") 

74 

75 width = ( 

76 2 * thickness + finger_length + finger_gap 

77 if not half 

78 else thickness + finger_length 

79 ) # total length 

80 height = fingers * thickness + (fingers - 1) * finger_gap # total height 

81 points_1 = [ 

82 (0, 0), 

83 (0, height), 

84 (thickness + finger_length, height), 

85 (thickness + finger_length, height - thickness), 

86 (thickness, height - thickness), 

87 *chain.from_iterable( 

88 ( 

89 (thickness, height - (2 * i) * (thickness + finger_gap)), 

90 ( 

91 thickness + finger_length, 

92 height - (2 * i) * (thickness + finger_gap), 

93 ), 

94 ( 

95 thickness + finger_length, 

96 height - (2 * i) * (thickness + finger_gap) - thickness, 

97 ), 

98 (thickness, height - (2 * i) * (thickness + finger_gap) - thickness), 

99 ) 

100 for i in range(ceil(fingers / 2)) 

101 ), 

102 (thickness, 0), 

103 (0, 0), 

104 ] 

105 c.add_polygon(points_1, layer=layer) 

106 

107 if not half: 

108 points_2 = [ 

109 (width, 0), 

110 (width, height), 

111 (width - thickness, height), 

112 *chain.from_iterable( 

113 ( 

114 ( 

115 width - thickness, 

116 height - (1 + 2 * i) * thickness - (1 + 2 * i) * finger_gap, 

117 ), 

118 ( 

119 width - (thickness + finger_length), 

120 height - (1 + 2 * i) * thickness - (1 + 2 * i) * finger_gap, 

121 ), 

122 ( 

123 width - (thickness + finger_length), 

124 height - (2 + 2 * i) * thickness - (1 + 2 * i) * finger_gap, 

125 ), 

126 ( 

127 width - thickness, 

128 height - (2 + 2 * i) * thickness - (1 + 2 * i) * finger_gap, 

129 ), 

130 ) 

131 for i in range(floor(fingers / 2)) 

132 ), 

133 (width - thickness, 0), 

134 (width, 0), 

135 ] 

136 c.add_polygon(points_2, layer=layer) 

137 

138 # Add etch layer bbox if specified 

139 if etch_layer is not None: 

140 etch_bbox = [ 

141 (-etch_bbox_margin, -etch_bbox_margin), 

142 (width + etch_bbox_margin, -etch_bbox_margin), 

143 (width + etch_bbox_margin, height + etch_bbox_margin), 

144 (-etch_bbox_margin, height + etch_bbox_margin), 

145 ] 

146 c.add_polygon(etch_bbox, layer=etch_layer) 

147 

148 # Add small straights on the left and right sides of the capacitor 

149 straight_cross_section = gf.get_cross_section(cross_section) 

150 straight_out_of_etch = straight( 

151 length=etch_bbox_margin, cross_section=straight_cross_section 

152 ) 

153 straight_left = c.add_ref(straight_out_of_etch).move( 

154 (-etch_bbox_margin, height / 2) 

155 ) 

156 straight_right = None 

157 if not half: 

158 straight_right = c.add_ref(straight_out_of_etch).move((width, height / 2)) 

159 

160 # Add WG to additive metal 

161 c_additive = gf.boolean( 

162 A=c, 

163 B=c, 

164 operation="or", 

165 layer=layer, 

166 layer1=layer, 

167 layer2=straight_cross_section.layer, 

168 ) 

169 

170 # Take boolean negative 

171 c_negative = gf.boolean( 

172 A=c, 

173 B=c_additive, 

174 operation="A-B", 

175 layer=etch_layer, 

176 layer1=etch_layer, 

177 layer2=layer, 

178 ) 

179 

180 # Combine results 

181 c = gf.Component() 

182 c.absorb(c << c_additive) 

183 c.absorb(c << c_negative) 

184 

185 ports_config: list[tuple[str, gf.Port] | None] = [ 

186 ("o1", straight_left["o1"]), 

187 ] 

188 if not half and straight_right is not None: 

189 ports_config.append(("o2", straight_right["o2"])) 

190 

191 for port_name, port_ref in filter(None, ports_config): 

192 c.add_port( 

193 name=port_name, 

194 width=port_ref.width, 

195 center=port_ref.center, 

196 orientation=port_ref.orientation, 

197 layer=LAYER.M1_DRAW, 

198 ) 

199 

200 # Center at (0,0) 

201 c.move((-width / 2, -height / 2)) 

202 

203 return c 

204 

205 

206@gf.cell 

207def plate_capacitor( 

208 length: float = 26.0, 

209 width: float = 5.0, 

210 gap: float = 7.0, 

211 etch_layer: LayerSpec | None = "M1_ETCH", 

212 etch_bbox_margin: float = 2.0, 

213 cross_section: CrossSectionSpec = "cpw", 

214) -> Component: 

215 """Creates a plate capacitor. 

216 

217 A capacitive coupler consists of two metal pads separated by a small gap, 

218 providing capacitive coupling between circuit elements like qubits and resonators. 

219 

220 .. svgbob:: 

221 

222 ______ ______ 

223 _________| | | |________ 

224 | | | | 

225 | o1 pad1 | ====gap==== | pad2 o2 | 

226 | | | | 

227 |_________ | | _________| 

228 |______| |______| 

229 

230 Args: 

231 length: Length (vertical extent) of the capacitor pad in μm. 

232 width: Width (horizontal extent) of the capacitor pad in μm. 

233 gap: Gap between plates in μm. 

234 etch_layer: Optional layer for etching around the capacitor. 

235 etch_bbox_margin: Margin around the capacitor for the etch layer in μm. 

236 cross_section: Cross-section for the short straight from the etch box capacitor. 

237 

238 Returns: 

239 A gdsfactory component with the plate capacitor geometry and two ports ('o1' and 'o2') on opposing sides. 

240 """ 

241 if width <= 0: 

242 raise ValueError(f"width must be positive, got {width}") 

243 if length <= 0: 

244 raise ValueError(f"length must be positive, got {length}") 

245 

246 c = Component() 

247 single_capacitor = plate_capacitor_single( 

248 length=length, 

249 width=width, 

250 etch_layer=etch_layer, 

251 etch_bbox_margin=etch_bbox_margin, 

252 cross_section=cross_section, 

253 ) 

254 

255 pad1 = c.add_ref(single_capacitor) 

256 pad2 = c.add_ref(single_capacitor) 

257 pad2.rotate(180) 

258 pad2.move((width + gap, 0)) 

259 c.center = (0, 0) 

260 

261 # Add ports 

262 c.add_port(name="o1", port=pad1.ports["o1"]) 

263 c.add_port(name="o2", port=pad2.ports["o1"]) 

264 

265 # Ensure etch box between pads 

266 if etch_layer is not None: 

267 missing_width = gap - 2 * etch_bbox_margin 

268 if missing_width > 0: 

269 etch_bbox = [ 

270 (-missing_width / 2, -length / 2 - etch_bbox_margin), 

271 (missing_width / 2, -length / 2 - etch_bbox_margin), 

272 (missing_width / 2, length / 2 + etch_bbox_margin), 

273 (-missing_width / 2, length / 2 + etch_bbox_margin), 

274 ] 

275 c.add_polygon(etch_bbox, layer=etch_layer) 

276 

277 return c 

278 

279 

280@gf.cell 

281def plate_capacitor_single( 

282 length: float = 26.0, 

283 width: float = 5.0, 

284 etch_layer: LayerSpec | None = "M1_ETCH", 

285 etch_bbox_margin: float = 2.0, 

286 cross_section: CrossSectionSpec = "cpw", 

287) -> Component: 

288 """Creates a single plate capacitor for coupling. 

289 

290 This is essentially half of a :func:`~plate_capacitor`. 

291 

292 .. svgbob:: 

293 

294 ______ 

295 _________| | 

296 | | 

297 | o1 pad1 | 

298 | | 

299 |_________ | 

300 |______| 

301 

302 Args: 

303 length: Length (vertical extent) of the capacitor pad in μm. 

304 width: Width (horizontal extent) of the capacitor pad in μm. 

305 etch_layer: Optional layer for etching around the capacitor. 

306 etch_bbox_margin: Margin around the capacitor for the etch layer in μm. 

307 cross_section: Cross-section for the short straight from the etch box capacitor. 

308 

309 Returns: 

310 A gdsfactory component with the plate capacitor geometry. 

311 """ 

312 if width <= 0: 

313 raise ValueError(f"width must be positive, got {width}") 

314 if length <= 0: 

315 raise ValueError(f"length must be positive, got {length}") 

316 

317 c = Component() 

318 

319 layer = LAYER.M1_DRAW 

320 points = [ 

321 (0, 0), 

322 (0, length), 

323 (width, length), 

324 (width, 0), 

325 ] 

326 c.add_polygon(points, layer=layer) 

327 # Add etch layer bbox if specified 

328 if etch_layer is not None: 

329 etch_bbox = [ 

330 (-etch_bbox_margin, -etch_bbox_margin), 

331 (width + etch_bbox_margin, -etch_bbox_margin), 

332 (width + etch_bbox_margin, length + etch_bbox_margin), 

333 (-etch_bbox_margin, length + etch_bbox_margin), 

334 ] 

335 c.add_polygon(etch_bbox, layer=etch_layer) 

336 # Add small straight on the left side of the capacitor 

337 straight_cross_section = gf.get_cross_section(cross_section) 

338 straight_out_of_etch = straight( 

339 length=etch_bbox_margin, cross_section=straight_cross_section 

340 ) 

341 straight_left = c.add_ref(straight_out_of_etch).move( 

342 (-etch_bbox_margin, length / 2) 

343 ) 

344 # Add WG to additive metal 

345 c_additive = gf.boolean( 

346 A=c, 

347 B=c, 

348 operation="or", 

349 layer=layer, 

350 layer1=layer, 

351 layer2=straight_cross_section.layer, 

352 ) 

353 

354 # Take boolean negative 

355 c_negative = gf.boolean( 

356 A=c, 

357 B=c_additive, 

358 operation="A-B", 

359 layer=etch_layer, 

360 layer1=etch_layer, 

361 layer2=layer, 

362 ) 

363 # Combine results 

364 c = gf.Component() 

365 c.absorb(c << c_additive) 

366 c.absorb(c << c_negative) 

367 

368 c.add_port( 

369 name="o1", 

370 width=straight_left["o1"].width, 

371 center=straight_left["o1"].center, 

372 orientation=straight_left["o1"].orientation, 

373 layer=LAYER.M1_DRAW, 

374 ) 

375 

376 # Center at (0,0) 

377 c.move((-width / 2, -length / 2)) 

378 

379 return c 

380 

381 

382if __name__ == "__main__": 

383 show_components( 

384 plate_capacitor_single, 

385 plate_capacitor, 

386 interdigital_capacitor, 

387 partial(interdigital_capacitor, half=True), 

388 )