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
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-14 10:27 +0000
1"""Waveguide primitives."""
3from functools import partial
5import gdsfactory as gf
6from gdsfactory.typings import CrossSectionSpec, Ints, LayerSpec, Size
7from kfactory import VInstance
8from klayout.db import DCplxTrans
10from qpdk import tech
11from qpdk.helper import show_components
12from qpdk.logger import logger
14_DEFAULT_CROSS_SECTION = tech.cpw
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.
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
46ring = gf.c.ring
48taper_cross_section = partial(
49 gf.c.taper_cross_section, cross_section1="cpw", cross_section2="cpw"
50)
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.
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 )
73straight_shorted = straight
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.
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
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.
110 Note:
111 This may be treated as a :math:`\lambda/2` straight resonator in some contexts.
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
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.
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 )
170@gf.cell
171def tee(cross_section: CrossSectionSpec = "cpw") -> gf.Component:
172 """Returns a three-way tee waveguide.
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)
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 )
208 # center
209 c.center = (0, 0)
211 return c
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.
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 )
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.
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.
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 )
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.
301 stores min_bend_radius property in self.info['min_bend_radius']
302 min_bend_radius depends on height and length
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 )
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)
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.
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.
346 .. code::
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 )
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.
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 )
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.
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 )
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.
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
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 )