Coverage for qpdk / models / inductor.py: 100%

51 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-02 17:50 +0000

1"""Inductor and lumped-element resonator models.""" 

2 

3from functools import partial 

4 

5import jax 

6import jax.numpy as jnp 

7import sax 

8from gdsfactory.typings import CrossSectionSpec 

9 

10from qpdk.models.capacitor import interdigital_capacitor_capacitance_analytical 

11from qpdk.models.constants import DEFAULT_FREQUENCY, μ_0, π 

12from qpdk.models.cpw import ( 

13 cpw_ep_r_from_cross_section, 

14 get_cpw_dimensions, 

15 get_cpw_substrate_params, 

16) 

17from qpdk.models.generic import inductor, lc_resonator 

18 

19 

20@partial(jax.jit, inline=True) 

21def self_inductance_strip(length: float, width: float, thickness: float) -> jax.Array: 

22 r"""Analytical formula for the self-inductance of a rectangular metal strip. 

23 

24 Uses the formula from :cite:`chenCompactInductorcapacitorResonators2023`: 

25 

26 .. math:: 

27 

28 L_s = \frac{\mu_0 l}{2\pi} \left[ \ln\left(\frac{2l}{w+t}\right) + 0.5 + \frac{w+t}{3l} \right] 

29 

30 Args: 

31 length: Length of the strip in m. 

32 width: Width of the strip in m. 

33 thickness: Thickness of the strip in m. 

34 

35 Returns: 

36 Self-inductance in Henries. 

37 """ 

38 return (μ_0 * length / (2 * π)) * ( 

39 jnp.log(2 * length / (width + thickness)) 

40 + 0.5 

41 + (width + thickness) / (3 * length) 

42 ) 

43 

44 

45@partial(jax.jit, inline=True) 

46def mutual_inductance_parallel_strips(length: float, d: float) -> jax.Array: 

47 r"""Analytical formula for the mutual inductance between two parallel metal strips. 

48 

49 Uses the formula from :cite:`chenCompactInductorcapacitorResonators2023`: 

50 

51 .. math:: 

52 

53 L_m(d) = \frac{\mu_0 l}{2\pi} \left[ \ln \left( \frac{l}{d} + \sqrt{1 + \frac{l^2}{d^2}} \right) - \sqrt{1 + \frac{d^2}{l^2}} + \frac{d}{l} \right] 

54 

55 Args: 

56 length: Length of the strips in m. 

57 d: Center-to-center distance between the strips in m. 

58 

59 Returns: 

60 Mutual inductance in Henries. 

61 """ 

62 return (μ_0 * length / (2 * π)) * ( 

63 jnp.log(length / d + jnp.sqrt(1 + (length / d) ** 2)) 

64 - jnp.sqrt(1 + (d / length) ** 2) 

65 + d / length 

66 ) 

67 

68 

69@partial(jax.jit, inline=True) 

70def meander_inductor_inductance_analytical( 

71 n_turns: int, 

72 turn_length: float, 

73 wire_width: float, 

74 wire_gap: float, 

75 sheet_inductance: float, 

76 thickness: float | None = None, 

77) -> jax.Array: 

78 r"""Analytical formula for meander inductor inductance. 

79 

80 The total inductance is the sum of geometric and kinetic contributions: 

81 

82 .. math:: 

83 

84 L_{\text{total}} = L_g + L_k 

85 

86 The geometric inductance :math:`L_g` is calculated by summing the 

87 self-inductances of all horizontal segments and the mutual inductances 

88 between all pairs of parallel segments, following 

89 :cite:`chenCompactInductorcapacitorResonators2023`: 

90 

91 .. math:: 

92 

93 L_g = N L_s + 2 \sum_{k=1}^{N-1} (N-k) (-1)^k L_m(k p) 

94 

95 where :math:`N` is the number of turns and :math:`p` is the pitch. 

96 

97 The kinetic inductance :math:`L_k` is calculated from the sheet 

98 inductance :math:`L_\square`: 

99 

100 .. math:: 

101 

102 L_k = L_\square \cdot \frac{\ell_{\text{total}}}{w} 

103 

104 Args: 

105 n_turns: Number of horizontal meander runs. 

106 turn_length: Length of each horizontal run in µm. 

107 wire_width: Width of the meander wire in µm. 

108 wire_gap: Gap between adjacent meander runs in µm. 

109 sheet_inductance: Sheet inductance per square in H/□. 

110 thickness: Thickness of the metal film in µm. If None, it is 

111 fetched from the PDK technology parameters. 

112 

113 Returns: 

114 Total inductance in Henries. 

115 """ 

116 if thickness is None: 

117 _h, thickness, _ep_r = get_cpw_substrate_params() 

118 

119 # Convert to SI (meters) 

120 l_m = turn_length * 1e-6 

121 w_m = wire_width * 1e-6 

122 g_m = wire_gap * 1e-6 

123 t_m = thickness * 1e-6 

124 p_m = w_m + g_m # Pitch (center-to-center) 

125 

126 # 1. Geometric Inductance 

127 # Self-inductance of horizontal segments (turns) 

128 L_s_horiz = self_inductance_strip(l_m, w_m, t_m) 

129 

130 # Self-inductance of vertical connection segments 

131 L_s_vert = self_inductance_strip(p_m, w_m, t_m) 

132 

133 # Mutual inductance sum between horizontal segments 

134 # Formula: L_g = sum(L_s_i) + sum_{i!=j} M_ij 

135 # Current directions alternate: sign(i, j) = (-1)**(i-j) 

136 # L_g_horiz = N*L_s_horiz + 2 * sum_{i=0 to N-2} sum_{j=i+1 to N-1} (-1)**(j-i) * L_m(abs(j-i)*p) 

137 # This simplifies to the sum used in Chen et al. (2023): 

138 # L_g_horiz = N*L_s_horiz + 2 * sum_{k=1 to N-1} (N-k) * (-1)**k * L_m(k*p) 

139 

140 offsets = jnp.arange(1, 501) 

141 mask = offsets < n_turns 

142 L_m_sum = jnp.sum( 

143 jnp.where( 

144 mask, 

145 (n_turns - offsets) 

146 * ((-1.0) ** offsets) 

147 * mutual_inductance_parallel_strips(l_m, offsets * p_m), 

148 0.0, 

149 ) 

150 ) 

151 

152 # Ensure L_g_horiz calculation is accurate. The negative mutual inductance 

153 # should be outweighed by the self-inductance for physically valid meanders. 

154 L_g_horiz = n_turns * L_s_horiz + 2 * L_m_sum 

155 L_g = L_g_horiz + (n_turns - 1) * L_s_vert 

156 

157 # 2. Kinetic Inductance 

158 # Total wire length in µm (horizontal runs + vertical connections) 

159 total_length_um = n_turns * turn_length + jnp.maximum(0, n_turns - 1) * wire_gap 

160 n_squares = total_length_um / wire_width 

161 L_k = sheet_inductance * n_squares 

162 

163 return L_g + L_k 

164 

165 

166def meander_inductor( 

167 *, 

168 f: sax.FloatArrayLike = DEFAULT_FREQUENCY, 

169 n_turns: int = 5, 

170 turn_length: float = 200.0, 

171 cross_section: CrossSectionSpec = "meander_inductor_cross_section", 

172 sheet_inductance: float = 0.4e-12, 

173) -> sax.SDict: 

174 r"""Meander inductor SAX model. 

175 

176 Computes the inductance from the meander geometry and returns 

177 S-parameters of an equivalent lumped inductor. 

178 

179 The model extracts the center conductor width and gap from the provided 

180 cross-section. To ensure the etched regions of adjacent meander runs 

181 do not overlap and interfere with the characteristic impedance of each other, 

182 the vertical pitch is calculated as: 

183 

184 .. math:: 

185 

186 p = w + 2 \cdot g 

187 

188 where :math:`w` is the center conductor width and :math:`g` is the gap 

189 width. This corresponds to a metal-to-metal spacing of :math:`2g`. 

190 

191 Args: 

192 f: Array of frequency points in Hz. 

193 n_turns: Number of horizontal meander runs. 

194 turn_length: Length of each horizontal run in µm. 

195 cross_section: Cross-section specification for the meander wire. 

196 Used to determine the wire width and the gap between runs. 

197 sheet_inductance: Sheet inductance per square in H/□. 

198 

199 Returns: 

200 sax.SDict: S-parameters dictionary. 

201 """ 

202 f_arr = jnp.asarray(f) 

203 wire_width, wire_gap_half = get_cpw_dimensions(cross_section) 

204 wire_gap = 2 * wire_gap_half 

205 

206 inductance = meander_inductor_inductance_analytical( 

207 n_turns=n_turns, 

208 turn_length=turn_length, 

209 wire_width=wire_width, 

210 wire_gap=wire_gap, 

211 sheet_inductance=sheet_inductance, 

212 ) 

213 return inductor(f=f_arr, inductance=inductance) 

214 

215 

216def lumped_element_resonator( 

217 *, 

218 f: sax.FloatArrayLike = DEFAULT_FREQUENCY, 

219 fingers: int = 20, 

220 finger_length: float = 20.0, 

221 finger_gap: float = 2.0, 

222 finger_thickness: float = 5.0, 

223 n_turns: int = 5, 

224 sheet_inductance: float = 0.4e-12, 

225 cross_section: CrossSectionSpec = "meander_inductor_cross_section", 

226 grounded: bool = False, 

227) -> sax.SDict: 

228 r"""Lumped-element LC resonator SAX model. 

229 

230 Combines an interdigital capacitor and a meander inductor in parallel 

231 to form an LC resonator. The resonance frequency is: 

232 

233 .. math:: 

234 

235 f_r = \frac{1}{2\pi\sqrt{LC}} 

236 

237 where :math:`C` is computed from the interdigital capacitor geometry 

238 using :func:`~qpdk.models.capacitor.interdigital_capacitor_capacitance_analytical` 

239 and :math:`L` is computed from the meander inductor geometry using 

240 :func:`meander_inductor_inductance_analytical`. 

241 

242 The inductor section uses the width and gap derived from the 

243 `cross_section` to ensure consistent RF behavior across the meander. 

244 The vertical spacing between meander runs is set to twice the etch gap 

245 to prevent overlap of the etched regions. 

246 

247 See :cite:`kimThinfilmSuperconductingResonator2011,chenCompactInductorcapacitorResonators2023`. 

248 

249 Args: 

250 f: Array of frequency points in Hz. 

251 fingers: Number of interdigital capacitor fingers. 

252 finger_length: Length of each capacitor finger in µm. 

253 finger_gap: Gap between adjacent capacitor fingers in µm. 

254 finger_thickness: Width of each capacitor finger in µm. 

255 n_turns: Number of horizontal meander inductor runs (must be odd to 

256 match the cell geometry where the path spans left-to-right bus bars). 

257 sheet_inductance: Sheet inductance per square in H/□. 

258 cross_section: Cross-section specification. Used for substrate 

259 permittivity and to determine inductor wire width and gap. 

260 grounded: If True, one port of the resonator is grounded. 

261 

262 Returns: 

263 sax.SDict: S-parameters dictionary with ports o1 and o2. 

264 """ 

265 f_arr = jnp.asarray(f) 

266 

267 ep_r = cpw_ep_r_from_cross_section(cross_section) 

268 

269 capacitance = interdigital_capacitor_capacitance_analytical( 

270 fingers=fingers, 

271 finger_length=finger_length, 

272 finger_gap=finger_gap, 

273 thickness=finger_thickness, 

274 ep_r=ep_r, 

275 ) 

276 

277 wire_width, wire_gap_half = get_cpw_dimensions(cross_section) 

278 wire_gap = 2 * wire_gap_half 

279 

280 cap_width = 2 * finger_thickness + finger_length + finger_gap 

281 meander_turn_length = cap_width - 4 * wire_width 

282 

283 inductance = meander_inductor_inductance_analytical( 

284 n_turns=n_turns, 

285 turn_length=meander_turn_length, 

286 wire_width=wire_width, 

287 wire_gap=wire_gap, 

288 sheet_inductance=sheet_inductance, 

289 ) 

290 

291 return lc_resonator( 

292 f=f_arr, 

293 capacitance=capacitance, 

294 inductance=inductance, 

295 grounded=grounded, 

296 )