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

1"""Superconducting nanowire single-photon detector (SNSPD).""" 

2 

3from __future__ import annotations 

4 

5import gdsfactory as gf 

6import numpy as np 

7from gdsfactory.component import Component 

8from gdsfactory.typings import LayerSpec, Port, Size 

9 

10from qpdk.tech import LAYER 

11 

12 

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. 

25 

26 .. svgbob:: 

27 

28 e1 ─────────────────────╮ 

29 ╭───────────────────────╯ 

30 ╰───────────────────────╮ 

31 ╭───────────────────────╯ 

32 ╰───────────────────────╮ 

33 ╭───────────────────────╯ 

34 ╰───────────────────────╮ 

35 e2 ──────────────────╯ 

36 

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. 

52 

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 

60 

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 

67 

68 num_meanders = int(np.ceil(ysize / wire_pitch)) 

69 

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 ) 

79 

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 

84 

85 port_type = "electrical" 

86 

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 

103 

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) 

109 

110 D.add_port(port=start_nw.ports["e1"], name="e1") 

111 D.add_port(port=finish_se.ports["e1"], name="e2") 

112 

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 

119 

120 

121if __name__ == "__main__": 

122 from qpdk import PDK 

123 

124 PDK.activate() 

125 

126 c = snspd() 

127 c.show()