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

89 statements  

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

13from qpdk.tech import get_etch_section 

14 

15_DEFAULT_CROSS_SECTION = tech.cpw 

16 

17 

18@gf.cell(tags=("waveguides",)) 

19def rectangle( 

20 size: Size = (4.0, 2.0), 

21 layer: LayerSpec = "M1_DRAW", 

22 centered: bool = False, 

23 port_type: str | None = "electrical", 

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

25) -> gf.Component: 

26 """Returns a rectangle. 

27 

28 Args: 

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

30 layer: Specific layer to put polygon geometry on. 

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

32 port_type: optical, electrical. 

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

34 """ 

35 c = gf.Component() 

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

37 size=size, layer=layer, port_type=port_type, port_orientations=port_orientations 

38 ) 

39 if not centered: 

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

41 if port_type: 

42 c.add_ports(ref.ports) 

43 c.flatten() 

44 return c 

45 

46 

47ring = gf.c.ring 

48 

49taper_cross_section = partial( 

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

51) 

52 

53 

54@gf.cell(tags=("waveguides",)) 

55def straight( 

56 length: float = 10.0, 

57 cross_section: CrossSectionSpec = _DEFAULT_CROSS_SECTION, 

58 width: float | None = None, 

59 npoints: int = 2, 

60) -> gf.Component: 

61 """Returns a straight waveguide. 

62 

63 Args: 

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

65 cross_section: Cross-section specification. 

66 width: Optional width override in μm. 

67 npoints: Number of points for the waveguide. 

68 """ 

69 return gf.c.straight( 

70 length=length, cross_section=cross_section, width=width, npoints=npoints 

71 ) 

72 

73 

74straight_shorted = straight 

75 

76 

77@gf.cell(tags=("waveguides", "resonators")) 

78def straight_open( 

79 length: float = 10.0, 

80 cross_section: CrossSectionSpec = _DEFAULT_CROSS_SECTION, 

81 width: float | None = None, 

82 npoints: int = 2, 

83) -> gf.Component: 

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

85 

86 Args: 

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

88 cross_section: Cross-section specification. 

89 width: Optional width override in μm. 

90 npoints: Number of points for the waveguide. 

91 """ 

92 c = gf.Component() 

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

94 length=length, cross_section=cross_section, width=width, npoints=npoints 

95 ) 

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

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

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

99 return c 

100 

101 

102@gf.cell(tags=("waveguides", "resonators")) 

103def straight_double_open( 

104 length: float = 10.0, 

105 cross_section: CrossSectionSpec = _DEFAULT_CROSS_SECTION, 

106 width: float | None = None, 

107 npoints: int = 2, 

108) -> gf.Component: 

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

110 

111 Note: 

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

113 

114 Args: 

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

116 cross_section: Cross-section specification. 

117 width: Optional width override in μm. 

118 npoints: Number of points for the waveguide. 

119 """ 

120 c = gf.Component() 

121 straight_ref = c << straight_open( 

122 length=length, cross_section=cross_section, width=width, npoints=npoints 

123 ) 

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

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

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

127 return c 

128 

129 

130@gf.cell(tags=("waveguides",)) 

131def nxn( 

132 xsize: float = 10.0, 

133 ysize: float = 10.0, 

134 wg_width: float = 10.0, 

135 layer: LayerSpec = tech.LAYER.M1_DRAW, 

136 wg_margin: float = 0.0, 

137 north: int = 1, 

138 east: int = 1, 

139 south: int = 1, 

140 west: int = 1, 

141 cross_section: CrossSectionSpec = _DEFAULT_CROSS_SECTION, 

142) -> gf.Component: 

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

144 

145 Args: 

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

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

148 wg_width: Width of the waveguides in μm. 

149 layer: Layer specification. 

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

151 north: Number of ports on the north side. 

152 east: Number of ports on the east side. 

153 south: Number of ports on the south side. 

154 west: Number of ports on the west side. 

155 cross_section: Cross-section specification. 

156 """ 

157 return gf.c.nxn( 

158 xsize=xsize, 

159 ysize=ysize, 

160 wg_width=wg_width, 

161 layer=layer, 

162 wg_margin=wg_margin, 

163 north=north, 

164 east=east, 

165 south=south, 

166 west=west, 

167 cross_section=cross_section, 

168 ) 

169 

170 

171@gf.cell(tags=("waveguides",)) 

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

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

174 

175 Args: 

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

177 """ 

178 c = gf.Component() 

179 cross_section = gf.get_cross_section(cross_section) 

180 etch_section = get_etch_section(cross_section) 

181 nxn_ref = c << nxn(**{ 

182 "north": 1, 

183 "east": 1, 

184 "south": 1, 

185 "west": 1, 

186 }) 

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

188 straight_ref = c << straight( 

189 cross_section=cross_section, length=etch_section.width 

190 ) 

191 straight_ref.connect("o1", port) 

192 

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

194 etch_ref = c << rectangle( 

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

196 layer=etch_section.layer, 

197 centered=True, 

198 ) 

199 etch_ref.transform( 

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

201 ) 

202 

203 # center 

204 c.center = (0, 0) 

205 

206 return c 

207 

208 

209@gf.cell(tags=("waveguides",)) 

210def bend_euler( 

211 angle: float = 90.0, 

212 p: float = 0.5, 

213 with_arc_floorplan: bool = True, 

214 npoints: int = 720, 

215 cross_section: CrossSectionSpec = _DEFAULT_CROSS_SECTION, 

216 allow_min_radius_violation: bool = True, 

217 **kwargs, 

218) -> gf.Component: 

219 """Regular degree euler bend. 

220 

221 Args: 

222 angle: Angle of the bend in degrees. 

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

224 with_arc_floorplan: Include arc floorplan. 

225 npoints: Number of points for the bend. 

226 cross_section: Cross-section specification. 

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

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

229 

230 Returns: 

231 The euler bend component. 

232 """ 

233 return gf.c.bend_euler( 

234 angle=angle, 

235 p=p, 

236 with_arc_floorplan=with_arc_floorplan, 

237 npoints=npoints, 

238 cross_section=cross_section, 

239 allow_min_radius_violation=allow_min_radius_violation, 

240 **kwargs, 

241 ) 

242 

243 

244@gf.cell(tags=("waveguides",)) 

245def bend_circular( 

246 angle: float = 90.0, 

247 radius: float = 100.0, 

248 npoints: int | None = None, 

249 cross_section: CrossSectionSpec = _DEFAULT_CROSS_SECTION, 

250 width: float | None = None, 

251 allow_min_radius_violation: bool = True, 

252 **kwargs, 

253) -> gf.Component: 

254 """Returns circular bend. 

255 

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

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

258 

259 Args: 

260 angle: Angle of the bend in degrees. 

261 radius: Radius of the bend in μm. 

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

263 cross_section: Cross-section specification. 

264 width: Optional width override in μm. 

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

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

267 """ 

268 radius_min = gf.get_cross_section(cross_section).radius_min 

269 if radius_min is not None and radius < radius_min: 

270 radius = radius_min 

271 logger.warning( 

272 ( 

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

274 "Setting it to the minimum acceptable value." 

275 ), 

276 radius_min, 

277 ) 

278 return gf.c.bend_circular( 

279 angle=angle, 

280 radius=radius, 

281 npoints=npoints, 

282 cross_section=cross_section, 

283 width=width, 

284 allow_min_radius_violation=allow_min_radius_violation, 

285 **kwargs, 

286 ) 

287 

288 

289@gf.cell(tags=("waveguides",)) 

290def bend_s( 

291 size: Size = (20.0, 3.0), 

292 cross_section: CrossSectionSpec = _DEFAULT_CROSS_SECTION, 

293 width: float | None = None, 

294 allow_min_radius_violation: bool = True, 

295 **kwargs, 

296) -> gf.Component: 

297 """Return S bend with bezier curve. 

298 

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

300 min_bend_radius depends on height and length 

301 

302 Args: 

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

304 cross_section: Cross-section specification. 

305 width: Optional width override in μm. 

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

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

308 """ 

309 return gf.c.bend_s( 

310 size=size, 

311 cross_section=cross_section, 

312 width=width, 

313 allow_min_radius_violation=allow_min_radius_violation, 

314 **kwargs, 

315 ) 

316 

317 

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

319coupler_ring = partial( 

320 gf.c.coupler_ring, 

321 cross_section="cpw", 

322 length_x=20, 

323 bend=bend_circular, 

324 straight=straight, 

325 gap=16, 

326) 

327 

328 

329@gf.vcell 

330def straight_all_angle( 

331 length: float = 10.0, 

332 npoints: int = 2, 

333 cross_section: CrossSectionSpec = _DEFAULT_CROSS_SECTION, 

334 width: float | None = None, 

335) -> gf.ComponentAllAngle: 

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

337 

338 Args: 

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

340 npoints: Number of points for the waveguide. 

341 cross_section: Cross-section specification. 

342 width: Optional width override in μm. 

343 

344 .. code:: 

345 

346 o1 ──────────────── o2 

347 length 

348 """ 

349 return gf.c.straight_all_angle( 

350 length=length, npoints=npoints, cross_section=cross_section, width=width 

351 ) 

352 

353 

354@gf.vcell 

355def bend_euler_all_angle( 

356 radius: float | None = None, 

357 angle: float = 90.0, 

358 p: float = 0.5, 

359 with_arc_floorplan: bool = True, 

360 npoints: int | None = None, 

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

362 width: float | None = None, 

363 cross_section: CrossSectionSpec = _DEFAULT_CROSS_SECTION, 

364 allow_min_radius_violation: bool = True, 

365) -> gf.ComponentAllAngle: 

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

367 

368 Args: 

369 radius: Radius of the bend in μm. 

370 angle: Angle of the bend in degrees. 

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

372 with_arc_floorplan: Include arc floorplan. 

373 npoints: Number of points for the bend. 

374 layer: Layer specification. 

375 width: Optional width override in μm. 

376 cross_section: Cross-section specification. 

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

378 """ 

379 return gf.c.bend_euler_all_angle( 

380 radius=radius, 

381 angle=angle, 

382 p=p, 

383 with_arc_floorplan=with_arc_floorplan, 

384 npoints=npoints, 

385 layer=layer, 

386 width=width, 

387 cross_section=cross_section, 

388 allow_min_radius_violation=allow_min_radius_violation, 

389 ) 

390 

391 

392@gf.vcell 

393def bend_circular_all_angle( 

394 radius: float | None = 100.0, 

395 angle: float = 90.0, 

396 npoints: int | None = None, 

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

398 width: float | None = None, 

399 cross_section: CrossSectionSpec = _DEFAULT_CROSS_SECTION, 

400 allow_min_radius_violation: bool = True, 

401) -> gf.ComponentAllAngle: 

402 """Returns circular bend with arbitrary angle. 

403 

404 Args: 

405 radius: Radius of the bend in μm. 

406 angle: Angle of the bend in degrees. 

407 npoints: Number of points for the bend. 

408 layer: Layer specification. 

409 width: Optional width override in μm. 

410 cross_section: Cross-section specification. 

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

412 """ 

413 return gf.c.bend_circular_all_angle( 

414 radius=radius, 

415 angle=angle, 

416 npoints=npoints, 

417 layer=layer, 

418 width=width, 

419 cross_section=cross_section, 

420 allow_min_radius_violation=allow_min_radius_violation, 

421 ) 

422 

423 

424def add_etch_gap( 

425 c: gf.Component | gf.ComponentAllAngle, 

426 port: gf.Port, 

427 cross_section: CrossSectionSpec, 

428) -> gf.ComponentReference | VInstance: 

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

430 

431 Args: 

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

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

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

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

436 

437 Returns: 

438 Reference or VInstance of the added etch gap. 

439 """ 

440 cross_section = gf.get_cross_section(cross_section) 

441 etch_section = get_etch_section(cross_section) 

442 etch_ref = c << rectangle( 

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

444 layer=etch_section.layer, 

445 centered=True, 

446 ) 

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

448 return etch_ref 

449 

450 

451if __name__ == "__main__": 

452 show_components( 

453 taper_cross_section, 

454 bend_euler, 

455 bend_circular, 

456 tee, 

457 bend_s, 

458 straight, 

459 coupler_ring, 

460 coupler_straight, 

461 partial(straight_open, length=20), 

462 partial(straight_double_open, length=20), 

463 straight_all_angle, 

464 partial(bend_euler_all_angle, angle=33), 

465 rectangle, 

466 spacing=50, 

467 )