Coverage for qpdk / cells / waveguides.py: 98%

88 statements  

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

1"""Waveguide primitives.""" 

2 

3from functools import partial 

4 

5import gdsfactory as gf 

6from gdsfactory.typings import CrossSectionSpec, Ints, LayerSpec, Size 

7from kfactory import VInstance 

8from klayout.db import DCplxTrans 

9 

10from qpdk import tech 

11from qpdk.helper import show_components 

12from qpdk.logger import logger 

13 

14_DEFAULT_CROSS_SECTION = tech.cpw 

15 

16 

17@gf.cell 

18def rectangle( 

19 size: Size = (4.0, 2.0), 

20 layer: LayerSpec = "M1_DRAW", 

21 centered: bool = False, 

22 port_type: str | None = "electrical", 

23 port_orientations: Ints | None = (180, 90, 0, -90), 

24) -> gf.Component: 

25 """Returns a rectangle. 

26 

27 Args: 

28 size: (tuple) Width and height of rectangle. 

29 layer: Specific layer to put polygon geometry on. 

30 centered: True sets center to (0, 0), False sets south-west to (0, 0). 

31 port_type: optical, electrical. 

32 port_orientations: list of port_orientations to add. None adds no ports. 

33 """ 

34 c = gf.Component() 

35 ref = c << gf.c.compass( 

36 size=size, layer=layer, port_type=port_type, port_orientations=port_orientations 

37 ) 

38 if not centered: 

39 ref.move((size[0] / 2, size[1] / 2)) 

40 if port_type: 

41 c.add_ports(ref.ports) 

42 c.flatten() 

43 return c 

44 

45 

46ring = gf.c.ring 

47 

48taper_cross_section = partial( 

49 gf.c.taper_cross_section, cross_section1="cpw", cross_section2="cpw" 

50) 

51 

52 

53@gf.cell 

54def straight( 

55 length: float = 10.0, 

56 cross_section: CrossSectionSpec = _DEFAULT_CROSS_SECTION, 

57 width: float | None = None, 

58 npoints: int = 2, 

59) -> gf.Component: 

60 """Returns a straight waveguide. 

61 

62 Args: 

63 length: Length of the straight waveguide in μm. 

64 cross_section: Cross-section specification. 

65 width: Optional width override in μm. 

66 npoints: Number of points for the waveguide. 

67 """ 

68 return gf.c.straight( 

69 length=length, cross_section=cross_section, width=width, npoints=npoints 

70 ) 

71 

72 

73straight_shorted = straight 

74 

75 

76@gf.cell 

77def straight_open( 

78 length: float = 10.0, 

79 cross_section: CrossSectionSpec = _DEFAULT_CROSS_SECTION, 

80 width: float | None = None, 

81 npoints: int = 2, 

82) -> gf.Component: 

83 """Returns a straight waveguide with etched gap at one end. 

84 

85 Args: 

86 length: Length of the straight waveguide in μm. 

87 cross_section: Cross-section specification. 

88 width: Optional width override in μm. 

89 npoints: Number of points for the waveguide. 

90 """ 

91 c = gf.Component() 

92 straight_ref = c << gf.c.straight( 

93 length=length, cross_section=cross_section, width=width, npoints=npoints 

94 ) 

95 c.add_port(port=straight_ref.ports["o1"]) 

96 c.add_port(port=straight_ref.ports["o2"], port_type="placement") 

97 add_etch_gap(c, c.ports["o2"], cross_section=cross_section) 

98 return c 

99 

100 

101@gf.cell 

102def straight_double_open( 

103 length: float = 10.0, 

104 cross_section: CrossSectionSpec = _DEFAULT_CROSS_SECTION, 

105 width: float | None = None, 

106 npoints: int = 2, 

107) -> gf.Component: 

108 r"""Returns a straight waveguide with etched gaps at both ends. 

109 

110 Note: 

111 This may be treated as a :math:`\lambda/2` straight resonator in some contexts. 

112 

113 Args: 

114 length: Length of the straight waveguide in μm. 

115 cross_section: Cross-section specification. 

116 width: Optional width override in μm. 

117 npoints: Number of points for the waveguide. 

118 """ 

119 c = gf.Component() 

120 straight_ref = c << straight_open( 

121 length=length, cross_section=cross_section, width=width, npoints=npoints 

122 ) 

123 c.add_port(port=straight_ref.ports["o1"], port_type="placement") 

124 c.add_port(port=straight_ref.ports["o2"], port_type="placement") 

125 add_etch_gap(c, c.ports["o1"], cross_section=cross_section) 

126 return c 

127 

128 

129@gf.cell 

130def nxn( 

131 xsize: float = 10.0, 

132 ysize: float = 10.0, 

133 wg_width: float = 10.0, 

134 layer: LayerSpec = tech.LAYER.M1_DRAW, 

135 wg_margin: float = 0.0, 

136 north: int = 1, 

137 east: int = 1, 

138 south: int = 1, 

139 west: int = 1, 

140 cross_section: CrossSectionSpec = _DEFAULT_CROSS_SECTION, 

141) -> gf.Component: 

142 """Returns an NxN junction with ports on each side. 

143 

144 Args: 

145 xsize: Horizontal size of the junction in μm. 

146 ysize: Vertical size of the junction in μm. 

147 wg_width: Width of the waveguides in μm. 

148 layer: Layer specification. 

149 wg_margin: Margin from edge to waveguide in μm. 

150 north: Number of ports on the north side. 

151 east: Number of ports on the east side. 

152 south: Number of ports on the south side. 

153 west: Number of ports on the west side. 

154 cross_section: Cross-section specification. 

155 """ 

156 return gf.c.nxn( 

157 xsize=xsize, 

158 ysize=ysize, 

159 wg_width=wg_width, 

160 layer=layer, 

161 wg_margin=wg_margin, 

162 north=north, 

163 east=east, 

164 south=south, 

165 west=west, 

166 cross_section=cross_section, 

167 ) 

168 

169 

170@gf.cell 

171def tee(cross_section: CrossSectionSpec = "cpw") -> gf.Component: 

172 """Returns a three-way tee waveguide. 

173 

174 Args: 

175 cross_section: specification (CrossSection, string or dict). 

176 """ 

177 c = gf.Component() 

178 cross_section = gf.get_cross_section(cross_section) 

179 etch_section = next( 

180 s 

181 for s in cross_section.sections 

182 if s.name is not None and s.name.startswith("etch") 

183 ) 

184 nxn_ref = c << nxn( 

185 **{ 

186 "north": 1, 

187 "east": 1, 

188 "south": 1, 

189 "west": 1, 

190 } 

191 ) 

192 for port in list(nxn_ref.ports)[:-1]: 

193 straight_ref = c << straight( 

194 cross_section=cross_section, length=etch_section.width 

195 ) 

196 straight_ref.connect("o1", port) 

197 

198 c.add_port(f"{port.name}", port=straight_ref.ports["o2"]) 

199 etch_ref = c << rectangle( 

200 size=(etch_section.width, cross_section.width), 

201 layer=etch_section.layer, 

202 centered=True, 

203 ) 

204 etch_ref.transform( 

205 list(nxn_ref.ports)[-1].dcplx_trans * DCplxTrans(etch_section.width / 2, 0) 

206 ) 

207 

208 # center 

209 c.center = (0, 0) 

210 

211 return c 

212 

213 

214@gf.cell 

215def bend_euler( 

216 angle: float = 90.0, 

217 p: float = 0.5, 

218 with_arc_floorplan: bool = True, 

219 npoints: int = 720, 

220 cross_section: CrossSectionSpec = _DEFAULT_CROSS_SECTION, 

221 allow_min_radius_violation: bool = True, 

222 **kwargs, 

223) -> gf.Component: 

224 """Regular degree euler bend. 

225 

226 Args: 

227 angle: Angle of the bend in degrees. 

228 p: Fraction of the bend that is curved (0-1). 

229 with_arc_floorplan: Include arc floorplan. 

230 npoints: Number of points for the bend. 

231 cross_section: Cross-section specification. 

232 allow_min_radius_violation: Allow radius smaller than cross-section radius. 

233 **kwargs: Additional arguments passed to gf.c.bend_euler. 

234 """ 

235 return gf.c.bend_euler( 

236 angle=angle, 

237 p=p, 

238 with_arc_floorplan=with_arc_floorplan, 

239 npoints=npoints, 

240 cross_section=cross_section, 

241 allow_min_radius_violation=allow_min_radius_violation, 

242 **kwargs, 

243 ) 

244 

245 

246@gf.cell 

247def bend_circular( 

248 angle: float = 90.0, 

249 radius: float = 100.0, 

250 npoints: int | None = None, 

251 cross_section: CrossSectionSpec = _DEFAULT_CROSS_SECTION, 

252 width: float | None = None, 

253 allow_min_radius_violation: bool = True, 

254 **kwargs, 

255) -> gf.Component: 

256 """Returns circular bend. 

257 

258 Cross-sections have a minimum value of allowed bend radius, which is half their total width. 

259 If the user-specified radius is smaller than this value, it is adjusted to the minimum acceptable one. 

260 

261 Args: 

262 angle: Angle of the bend in degrees. 

263 radius: Radius of the bend in μm. 

264 npoints: Number of points for the bend (optional, cannot be used with angular_step). 

265 cross_section: Cross-section specification. 

266 width: Optional width override in μm. 

267 allow_min_radius_violation: Allow radius smaller than cross-section radius. 

268 **kwargs: Additional arguments passed to gf.c.bend_circular (e.g., angular_step). 

269 """ 

270 radius_min = gf.get_cross_section(cross_section).radius_min 

271 if radius_min is not None and radius < radius_min: 

272 radius = radius_min 

273 logger.warning( 

274 ( 

275 "Bend radius needs to be >= {} for this cross-section. " 

276 "Setting it to the minimum acceptable value." 

277 ), 

278 radius_min, 

279 ) 

280 return gf.c.bend_circular( 

281 angle=angle, 

282 radius=radius, 

283 npoints=npoints, 

284 cross_section=cross_section, 

285 width=width, 

286 allow_min_radius_violation=allow_min_radius_violation, 

287 **kwargs, 

288 ) 

289 

290 

291@gf.cell 

292def bend_s( 

293 size: Size = (20.0, 3.0), 

294 cross_section: CrossSectionSpec = _DEFAULT_CROSS_SECTION, 

295 width: float | None = None, 

296 allow_min_radius_violation: bool = True, 

297 **kwargs, 

298) -> gf.Component: 

299 """Return S bend with bezier curve. 

300 

301 stores min_bend_radius property in self.info['min_bend_radius'] 

302 min_bend_radius depends on height and length 

303 

304 Args: 

305 size: Tuple of (length, offset) for the S bend in μm. 

306 cross_section: Cross-section specification. 

307 width: Optional width override in μm. 

308 allow_min_radius_violation: Allow radius smaller than cross-section radius. 

309 **kwargs: Additional arguments passed to gf.c.bend_s. 

310 """ 

311 return gf.c.bend_s( 

312 size=size, 

313 cross_section=cross_section, 

314 width=width, 

315 allow_min_radius_violation=allow_min_radius_violation, 

316 **kwargs, 

317 ) 

318 

319 

320coupler_straight = partial(gf.c.coupler_straight, cross_section="cpw", gap=16) 

321coupler_ring = partial( 

322 gf.c.coupler_ring, 

323 cross_section="cpw", 

324 length_x=20, 

325 bend=bend_circular, 

326 straight=straight, 

327 gap=16, 

328) 

329 

330 

331@gf.vcell 

332def straight_all_angle( 

333 length: float = 10.0, 

334 npoints: int = 2, 

335 cross_section: CrossSectionSpec = _DEFAULT_CROSS_SECTION, 

336 width: float | None = None, 

337) -> gf.ComponentAllAngle: 

338 """Returns a Straight waveguide with offgrid ports. 

339 

340 Args: 

341 length: Length of the straight waveguide in μm. 

342 npoints: Number of points for the waveguide. 

343 cross_section: Cross-section specification. 

344 width: Optional width override in μm. 

345 

346 .. code:: 

347 

348 o1 ──────────────── o2 

349 length 

350 """ 

351 return gf.c.straight_all_angle( 

352 length=length, npoints=npoints, cross_section=cross_section, width=width 

353 ) 

354 

355 

356@gf.vcell 

357def bend_euler_all_angle( 

358 radius: float | None = None, 

359 angle: float = 90.0, 

360 p: float = 0.5, 

361 with_arc_floorplan: bool = True, 

362 npoints: int | None = None, 

363 layer: gf.typings.LayerSpec | None = None, 

364 width: float | None = None, 

365 cross_section: CrossSectionSpec = _DEFAULT_CROSS_SECTION, 

366 allow_min_radius_violation: bool = True, 

367) -> gf.ComponentAllAngle: 

368 """Returns regular degree euler bend with arbitrary angle. 

369 

370 Args: 

371 radius: Radius of the bend in μm. 

372 angle: Angle of the bend in degrees. 

373 p: Fraction of the bend that is curved (0-1). 

374 with_arc_floorplan: Include arc floorplan. 

375 npoints: Number of points for the bend. 

376 layer: Layer specification. 

377 width: Optional width override in μm. 

378 cross_section: Cross-section specification. 

379 allow_min_radius_violation: Allow radius smaller than cross-section radius. 

380 """ 

381 return gf.c.bend_euler_all_angle( 

382 radius=radius, 

383 angle=angle, 

384 p=p, 

385 with_arc_floorplan=with_arc_floorplan, 

386 npoints=npoints, 

387 layer=layer, 

388 width=width, 

389 cross_section=cross_section, 

390 allow_min_radius_violation=allow_min_radius_violation, 

391 ) 

392 

393 

394@gf.vcell 

395def bend_circular_all_angle( 

396 radius: float | None = 100.0, 

397 angle: float = 90.0, 

398 npoints: int | None = None, 

399 layer: gf.typings.LayerSpec | None = None, 

400 width: float | None = None, 

401 cross_section: CrossSectionSpec = _DEFAULT_CROSS_SECTION, 

402 allow_min_radius_violation: bool = True, 

403) -> gf.ComponentAllAngle: 

404 """Returns circular bend with arbitrary angle. 

405 

406 Args: 

407 radius: Radius of the bend in μm. 

408 angle: Angle of the bend in degrees. 

409 npoints: Number of points for the bend. 

410 layer: Layer specification. 

411 width: Optional width override in μm. 

412 cross_section: Cross-section specification. 

413 allow_min_radius_violation: Allow radius smaller than cross-section radius. 

414 """ 

415 return gf.c.bend_circular_all_angle( 

416 radius=radius, 

417 angle=angle, 

418 npoints=npoints, 

419 layer=layer, 

420 width=width, 

421 cross_section=cross_section, 

422 allow_min_radius_violation=allow_min_radius_violation, 

423 ) 

424 

425 

426def add_etch_gap( 

427 c: gf.Component | gf.ComponentAllAngle, 

428 port: gf.Port, 

429 cross_section: CrossSectionSpec, 

430) -> gf.ComponentReference | VInstance: 

431 """Adds an etch gap rectangle at the given port of the component. 

432 

433 Args: 

434 c: Component to which the etch gap will be added. 

435 port: Port where the etch gap will be added. 

436 cross_section: Cross-section specification to determine etch dimensions. 

437 The etch width is taken from a :class:`~Section` that includes "etch" in its name. 

438 """ 

439 cross_section = gf.get_cross_section(cross_section) 

440 etch_section = next( 

441 s 

442 for s in cross_section.sections 

443 if s.name is not None and s.name.startswith("etch") 

444 ) 

445 etch_ref = c << rectangle( 

446 size=(etch_section.width, cross_section.width + 2 * etch_section.width), 

447 layer=etch_section.layer, 

448 centered=True, 

449 ) 

450 etch_ref.transform(port.dcplx_trans * DCplxTrans(etch_section.width / 2, 0)) 

451 return etch_ref 

452 

453 

454if __name__ == "__main__": 

455 show_components( 

456 taper_cross_section, 

457 bend_euler, 

458 bend_circular, 

459 tee, 

460 bend_s, 

461 straight, 

462 coupler_ring, 

463 coupler_straight, 

464 partial(straight_open, length=20), 

465 partial(straight_double_open, length=20), 

466 straight_all_angle, 

467 partial(bend_euler_all_angle, angle=33), 

468 rectangle, 

469 spacing=50, 

470 )