Coverage for qpdk / cells / snspd.py: 92%
48 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"""Superconducting nanowire single-photon detector (SNSPD)."""
3from __future__ import annotations
5import gdsfactory as gf
6import numpy as np
7from gdsfactory.component import Component
8from gdsfactory.typings import LayerSpec, Port, Size
10from qpdk.tech import LAYER
13@gf.cell(tags=("detectors",))
14def snspd(
15 wire_width: float = 0.2,
16 wire_pitch: float = 0.6,
17 size: Size = (10, 8),
18 num_squares: int | None = None,
19 turn_ratio: float = 4,
20 terminals_same_side: bool = False,
21 layer: LayerSpec = LAYER.NbTiN,
22 port_type: str = "electrical",
23) -> Component:
24 """Creates an optimally-rounded SNSPD.
26 .. svgbob::
28 e1 ─────────────────────╮
29 ╭───────────────────────╯
30 ╰───────────────────────╮
31 ╭───────────────────────╯
32 ╰───────────────────────╮
33 ╭───────────────────────╯
34 ╰───────────────────────╮
35 e2 ──────────────────╯
37 Args:
38 wire_width: Width of the wire.
39 wire_pitch: Distance between two adjacent wires. Must be greater than `width`.
40 size: Float2
41 (width, height) of the rectangle formed by the outer boundary of the
42 SNSPD.
43 num_squares: int | None = None
44 Total number of squares inside the SNSPD length.
45 turn_ratio: float
46 Specifies how much of the SNSPD width is dedicated to the 180 degree
47 turn. A `turn_ratio` of 10 will result in 20% of the width being
48 comprised of the turn.
49 terminals_same_side: If True, both ports will be located on the same side of the SNSPD.
50 layer: layer spec to put polygon geometry on.
51 port_type: type of port to add to the component.
53 Returns:
54 A Component containing the SNSPD geometry.
55 """
56 if num_squares is not None:
57 xy = np.sqrt(num_squares * wire_pitch * wire_width)
58 size = (xy, xy)
59 num_squares = None
61 xsize, ysize = size
62 if num_squares is not None:
63 if xsize is None:
64 xsize = num_squares * wire_pitch * wire_width / ysize
65 elif ysize is None:
66 ysize = num_squares * wire_pitch * wire_width / xsize
68 num_meanders = int(np.ceil(ysize / wire_pitch))
70 D = Component()
71 hairpin = gf.c.optimal_hairpin(
72 width=wire_width,
73 pitch=wire_pitch,
74 turn_ratio=turn_ratio,
75 length=xsize / 2,
76 num_pts=20,
77 layer=layer,
78 )
80 if (not terminals_same_side and (num_meanders % 2) == 0) or (
81 terminals_same_side and (num_meanders % 2) == 1
82 ):
83 num_meanders += 1
85 port_type = "electrical"
87 start_nw = D.add_ref(
88 gf.c.compass(size=(xsize / 2, wire_width), layer=layer, port_type=port_type)
89 )
90 hp_prev = D.add_ref(hairpin)
91 hp_prev.connect("e1", start_nw.ports["e3"])
92 alternate = True
93 last_port: Port | None = None
94 for _n in range(2, num_meanders):
95 hp = D.add_ref(hairpin)
96 if alternate:
97 hp.connect("e2", hp_prev.ports["e2"])
98 else:
99 hp.connect("e1", hp_prev.ports["e1"])
100 last_port = hp.ports["e2"] if terminals_same_side else hp.ports["e1"]
101 hp_prev = hp
102 alternate = not alternate
104 finish_se = D.add_ref(
105 gf.c.compass(size=(xsize / 2, wire_width), layer=layer, port_type=port_type)
106 )
107 if last_port is not None:
108 finish_se.connect("e3", last_port)
110 D.add_port(port=start_nw.ports["e1"], name="e1")
111 D.add_port(port=finish_se.ports["e1"], name="e2")
113 D.info["num_squares"] = num_meanders * (xsize / wire_width)
114 D.info["area"] = xsize * ysize
115 D.info["xsize"] = xsize
116 D.info["ysize"] = ysize
117 D.flatten()
118 return D
121if __name__ == "__main__":
122 from qpdk import PDK
124 PDK.activate()
126 c = snspd()
127 c.show()