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
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-02 17:50 +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
13from qpdk.tech import get_etch_section
15_DEFAULT_CROSS_SECTION = tech.cpw
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.
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
47ring = gf.c.ring
49taper_cross_section = partial(
50 gf.c.taper_cross_section, cross_section1="cpw", cross_section2="cpw"
51)
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.
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 )
74straight_shorted = straight
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.
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
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.
111 Note:
112 This may be treated as a :math:`\lambda/2` straight resonator in some contexts.
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
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.
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 )
171@gf.cell(tags=("waveguides",))
172def tee(cross_section: CrossSectionSpec = "cpw") -> gf.Component:
173 """Returns a three-way tee waveguide.
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)
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 )
203 # center
204 c.center = (0, 0)
206 return c
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.
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.
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 )
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.
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.
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 )
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.
299 stores min_bend_radius property in self.info['min_bend_radius']
300 min_bend_radius depends on height and length
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 )
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)
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.
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.
344 .. code::
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 )
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.
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 )
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.
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 )
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.
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.
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
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 )