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

116 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-02 17:50 +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.helpers import merge_layers_with_etch as _merge_layers_with_etch 

14from qpdk.cells.waveguides import add_etch_gap, bend_circular, straight 

15from qpdk.helper import show_components 

16from qpdk.tech import LAYER, get_etch_section, get_etch_sections 

17 

18 

19@gf.cell(tags=("capacitors", "couplers")) 

20def half_circle_coupler( 

21 radius: float = 50.0, 

22 angle: float = 180.0, 

23 extension_length: float = 10.0, 

24 cross_section: CrossSectionSpec = "cpw", 

25 extra_straight_length: float = 20.0, 

26 open_end: bool = True, 

27) -> Component: 

28 """Creates a half-circle coupler for readout. 

29 

30 This coupler consists of a circular bend (typically 180 degrees) that wraps 

31 around a resonator arm for capacitive coupling. 

32 

33 Args: 

34 radius: Inner radius of the half-circle in μm. 

35 angle: Angle of the circular arc in degrees. 

36 extension_length: Length of the straight sections extending from the 

37 ends of the half-circle in μm. 

38 cross_section: Cross-section specification for the coupler. 

39 extra_straight_length: Length of the straight section extending from the 

40 bottom of the half-circle in μm. 

41 open_end: If True, adds an etched gap at the ends of the extensions. 

42 

43 Returns: 

44 Component: A gdsfactory component with the half-circle coupler geometry. 

45 """ 

46 c = Component() 

47 

48 bend = c.add_ref( 

49 bend_circular( 

50 radius=radius, 

51 angle=angle, 

52 cross_section=cross_section, 

53 ) 

54 ) 

55 

56 # Position bend such that it's centered and opening upwards 

57 bend.rotate(-angle / 2) 

58 bend.move((-bend.dcenter[0], -bend.dcenter[1])) 

59 

60 # Add extensions to the ends of the bend 

61 if extension_length > 0: 

62 ext1 = c.add_ref(straight(length=extension_length, cross_section=cross_section)) 

63 ext1.connect("o1", bend.ports["o1"]) 

64 ext2 = c.add_ref(straight(length=extension_length, cross_section=cross_section)) 

65 ext2.connect("o1", bend.ports["o2"]) 

66 where_to_add_gaps = [ext1.ports["o2"], ext2.ports["o2"]] 

67 else: 

68 where_to_add_gaps = [bend.ports["o1"], bend.ports["o2"]] 

69 

70 if open_end: 

71 for port in where_to_add_gaps: 

72 add_etch_gap(c, port, cross_section=cross_section) 

73 

74 # Get cross section details to calculate overlap 

75 xs = gf.get_cross_section(cross_section) 

76 cross_section_etch_section = get_etch_section(xs) 

77 # Ensure significant overlap by moving stem into the bend metal 

78 # and considering the bend radius 

79 overlap = xs.width / 2 + cross_section_etch_section.width 

80 

81 # Add a stem/lead straight from the center of the arc. 

82 # The stem uses the CPW cross-section but does NOT extend into the bend, 

83 # to avoid M1_ETCH overlapping with the bend's M1_DRAW. 

84 stem = c.add_ref( 

85 straight( 

86 length=extra_straight_length, 

87 cross_section=cross_section, 

88 ) 

89 ) 

90 

91 stem.rotate(-90) 

92 stem.movey(bend.dbbox().bottom) 

93 stem.movex(bend.dcenter[0]) # Center it 

94 

95 # Bridge the stem into the bend with M1_DRAW center conductor and 

96 # M1_ETCH gap sections, matching the CPW cross-section pattern. 

97 # Unlike using a straight ref, these polygons avoid hierarchical 

98 # M1_ETCH-over-M1_DRAW conflicts with the bend. 

99 bridge_x = bend.dcenter[0] 

100 bridge_y_bottom = bend.dbbox().bottom 

101 bridge_y_top = bridge_y_bottom + overlap 

102 c.add_polygon( 

103 [ 

104 (bridge_x - xs.width / 2, bridge_y_bottom), 

105 (bridge_x + xs.width / 2, bridge_y_bottom), 

106 (bridge_x + xs.width / 2, bridge_y_top), 

107 (bridge_x - xs.width / 2, bridge_y_top), 

108 ], 

109 layer=LAYER.M1_DRAW, 

110 ) 

111 etch_height = overlap / 3 

112 for etch_s in get_etch_sections(xs): 

113 etch_x_center = bridge_x + etch_s.offset 

114 c.add_polygon( 

115 [ 

116 (etch_x_center - etch_s.width / 2, bridge_y_bottom), 

117 (etch_x_center + etch_s.width / 2, bridge_y_bottom), 

118 (etch_x_center + etch_s.width / 2, bridge_y_bottom + etch_height), 

119 (etch_x_center - etch_s.width / 2, bridge_y_bottom + etch_height), 

120 ], 

121 layer=LAYER.M1_ETCH, 

122 ) 

123 c.add_port("o3", port=stem.ports["o2"]) 

124 

125 # Place anchor at the arc center, computed as the midpoint of the 

126 # bend ports for a circular arc, for concentric alignment with an 

127 # inner resonator bend. 

128 arc_center_x = (bend.ports["o1"].dx + bend.ports["o2"].dx) / 2 

129 arc_center_y = (bend.ports["o1"].dy + bend.ports["o2"].dy) / 2 

130 c.add_port( 

131 name="anchor", 

132 center=(arc_center_x, arc_center_y), 

133 width=xs.width, 

134 orientation=90, 

135 layer=LAYER.M1_DRAW, 

136 port_type="placement", 

137 ) 

138 

139 return c 

140 

141 

142@gf.cell(tags=("capacitors",)) 

143def interdigital_capacitor( 

144 fingers: int = 4, 

145 finger_length: float = 20.0, 

146 finger_gap: float = 2.0, 

147 thickness: float = 5.0, 

148 layer_metal: LayerSpec = LAYER.M1_DRAW, 

149 etch_layer: LayerSpec | None = "M1_ETCH", 

150 etch_bbox_margin: float = 2.0, 

151 cross_section: CrossSectionSpec = "cpw", 

152 half: bool = False, 

153) -> Component: 

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

155 

156 An interdigital capacitor consists of interleaved metal fingers that create 

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

158 two sets of interleaved fingers extending from opposite ends. 

159 

160 .. svgbob:: 

161 

162 ┌─┐───────┐┌─┐ 

163 │ │───────┘│ │ 

164 │ │ ┌──────│ │ 

165 ┌│ │ └──────│ │┐ 

166 o1└│ │──────┐ │ │┘o2 

167 │ │──────┘ │ │ 

168 │ │ ┌──────│ │ 

169 └─┘ └──────└─┘ 

170 

171 See for example :cite:`leizhuAccurateCircuitModel2000`. 

172 

173 Note: 

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

175 The capacitance scales approximately linearly with the number of fingers 

176 and finger length. 

177 

178 Args: 

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

180 finger_length: Length of each finger in μm. 

181 finger_gap: Gap between adjacent fingers in μm. 

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

183 layer_metal: Layer for the metal fingers. 

184 etch_layer: Optional layer for etching around the capacitor. 

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

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

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

188 

189 Returns: 

190 Component: A gdsfactory component with the interdigital capacitor geometry 

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

192 

193 Raises: 

194 ValueError: If fingers is less than 1. 

195 """ 

196 c = Component() 

197 

198 if fingers < 1: 

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

200 

201 width = ( 

202 2 * thickness + finger_length + finger_gap 

203 if not half 

204 else thickness + finger_length 

205 ) # total length 

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

207 points_1 = [ 

208 (0, 0), 

209 (0, height), 

210 (thickness + finger_length, height), 

211 (thickness + finger_length, height - thickness), 

212 (thickness, height - thickness), 

213 *chain.from_iterable( 

214 ( 

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

216 ( 

217 thickness + finger_length, 

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

219 ), 

220 ( 

221 thickness + finger_length, 

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

223 ), 

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

225 ) 

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

227 ), 

228 (thickness, 0), 

229 (0, 0), 

230 ] 

231 c.add_polygon(points_1, layer=layer_metal) 

232 

233 if not half: 

234 points_2 = [ 

235 (width, 0), 

236 (width, height), 

237 (width - thickness, height), 

238 *chain.from_iterable( 

239 ( 

240 ( 

241 width - thickness, 

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

243 ), 

244 ( 

245 width - (thickness + finger_length), 

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

247 ), 

248 ( 

249 width - (thickness + finger_length), 

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

251 ), 

252 ( 

253 width - thickness, 

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

255 ), 

256 ) 

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

258 ), 

259 (width - thickness, 0), 

260 (width, 0), 

261 ] 

262 c.add_polygon(points_2, layer=layer_metal) 

263 

264 # Add etch layer bbox if specified 

265 if etch_layer is not None: 

266 etch_bbox = [ 

267 (-etch_bbox_margin, -etch_bbox_margin), 

268 (width + etch_bbox_margin, -etch_bbox_margin), 

269 (width + etch_bbox_margin, height + etch_bbox_margin), 

270 (-etch_bbox_margin, height + etch_bbox_margin), 

271 ] 

272 c.add_polygon(etch_bbox, layer=etch_layer) 

273 

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

275 straight_cross_section = gf.get_cross_section(cross_section) 

276 straight_out_of_etch = straight( 

277 length=etch_bbox_margin, cross_section=straight_cross_section 

278 ) 

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

280 -etch_bbox_margin, 

281 height / 2, 

282 )) 

283 straight_right = None 

284 if not half: 

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

286 

287 # Merge WG marker layer with draw metal and create etch negative 

288 c = _merge_layers_with_etch( 

289 component=c, 

290 draw_layer=layer_metal, 

291 wg_layer=straight_cross_section.layer, 

292 etch_layer=etch_layer, 

293 ) 

294 

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

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

297 ] 

298 if not half and straight_right is not None: 

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

300 

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

302 c.add_port( 

303 name=port_name, 

304 width=port_ref.width, 

305 center=port_ref.center, 

306 orientation=port_ref.orientation, 

307 layer=LAYER.M1_DRAW, 

308 ) 

309 

310 # Center at (0,0) 

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

312 

313 return c 

314 

315 

316@gf.cell(tags=("capacitors",)) 

317def plate_capacitor( 

318 length: float = 26.0, 

319 width: float = 5.0, 

320 gap: float = 7.0, 

321 etch_layer: LayerSpec | None = "M1_ETCH", 

322 etch_bbox_margin: float = 2.0, 

323 cross_section: CrossSectionSpec = "cpw", 

324) -> Component: 

325 """Creates a plate capacitor. 

326 

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

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

329 

330 .. svgbob:: 

331 

332 ______ ______ 

333 _________| | | |________ 

334 | | | | 

335 | o1 pad1 | ====gap==== | pad2 o2 | 

336 | | | | 

337 |_________ | | _________| 

338 |______| |______| 

339 

340 Args: 

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

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

343 gap: Gap between plates in μm. 

344 etch_layer: Optional layer for etching around the capacitor. 

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

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

347 

348 Returns: 

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

350 

351 Raises: 

352 ValueError: If width or length is not positive. 

353 """ 

354 if width <= 0: 

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

356 if length <= 0: 

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

358 

359 c = Component() 

360 single_capacitor = plate_capacitor_single( 

361 length=length, 

362 width=width, 

363 etch_layer=etch_layer, 

364 etch_bbox_margin=etch_bbox_margin, 

365 cross_section=cross_section, 

366 ) 

367 

368 pad1 = c.add_ref(single_capacitor) 

369 pad2 = c.add_ref(single_capacitor) 

370 pad2.rotate(180) 

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

372 c.center = (0, 0) 

373 

374 # Add ports 

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

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

377 

378 # Ensure etch box between pads 

379 if etch_layer is not None: 

380 missing_width = gap - 2 * etch_bbox_margin 

381 if missing_width > 0: 

382 etch_bbox = [ 

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

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

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

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

387 ] 

388 c.add_polygon(etch_bbox, layer=etch_layer) 

389 

390 return c 

391 

392 

393@gf.cell(tags=("capacitors", "couplers")) 

394def plate_capacitor_single( 

395 length: float = 26.0, 

396 width: float = 5.0, 

397 layer_metal: LayerSpec = LAYER.M1_DRAW, 

398 etch_layer: LayerSpec | None = "M1_ETCH", 

399 etch_bbox_margin: float = 2.0, 

400 cross_section: CrossSectionSpec = "cpw", 

401) -> Component: 

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

403 

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

405 

406 .. svgbob:: 

407 

408 ______ 

409 _________| | 

410 | | 

411 | o1 pad1 | 

412 | | 

413 |_________ | 

414 |______| 

415 

416 Args: 

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

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

419 layer_metal: Layer for the metal pad. 

420 etch_layer: Optional layer for etching around the capacitor. 

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

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

423 

424 Returns: 

425 A gdsfactory component with the plate capacitor geometry. 

426 

427 Raises: 

428 ValueError: If width or length is not positive. 

429 """ 

430 if width <= 0: 

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

432 if length <= 0: 

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

434 

435 c = Component() 

436 

437 points = [ 

438 (0, 0), 

439 (0, length), 

440 (width, length), 

441 (width, 0), 

442 ] 

443 c.add_polygon(points, layer=layer_metal) 

444 # Add etch layer bbox if specified 

445 if etch_layer is not None: 

446 etch_bbox = [ 

447 (-etch_bbox_margin, -etch_bbox_margin), 

448 (width + etch_bbox_margin, -etch_bbox_margin), 

449 (width + etch_bbox_margin, length + etch_bbox_margin), 

450 (-etch_bbox_margin, length + etch_bbox_margin), 

451 ] 

452 c.add_polygon(etch_bbox, layer=etch_layer) 

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

454 straight_cross_section = gf.get_cross_section(cross_section) 

455 straight_out_of_etch = straight( 

456 length=etch_bbox_margin, cross_section=straight_cross_section 

457 ) 

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

459 -etch_bbox_margin, 

460 length / 2, 

461 )) 

462 # Merge WG marker layer with draw metal and create etch negative 

463 c = _merge_layers_with_etch( 

464 component=c, 

465 draw_layer=layer_metal, 

466 wg_layer=straight_cross_section.layer, 

467 etch_layer=etch_layer, 

468 ) 

469 

470 c.add_port( 

471 name="o1", 

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

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

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

475 layer=LAYER.M1_DRAW, 

476 ) 

477 

478 # Center at (0,0) 

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

480 

481 return c 

482 

483 

484if __name__ == "__main__": 

485 show_components( 

486 half_circle_coupler, 

487 plate_capacitor_single, 

488 plate_capacitor, 

489 interdigital_capacitor, 

490 partial(interdigital_capacitor, half=True), 

491 )