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

57 statements  

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

1"""Generic Models.""" 

2 

3import jax 

4import jax.numpy as jnp 

5import sax 

6from matplotlib import pyplot as plt 

7from sax.models.rf import ( 

8 admittance, 

9 capacitor, 

10 electrical_open, 

11 electrical_short, 

12 gamma_0_load, 

13 impedance, 

14 inductor, 

15 tee, 

16) 

17 

18from qpdk.models.constants import DEFAULT_FREQUENCY 

19 

20__all__ = [ 

21 "admittance", 

22 "capacitor", 

23 "electrical_open", 

24 "electrical_short", 

25 "electrical_short_2_port", 

26 "gamma_0_load", 

27 "impedance", 

28 "inductor", 

29 "lc_resonator", 

30 "lc_resonator_coupled", 

31 "open", 

32 "series_impedance", 

33 "short", 

34 "short_2_port", 

35 "shunt_admittance", 

36 "tee", 

37] 

38 

39 

40@jax.jit 

41def series_impedance( 

42 f: sax.FloatArrayLike = DEFAULT_FREQUENCY, # noqa: ARG001 

43 z: sax.Float = 0.0, 

44 z0: float = 50.0, 

45) -> sax.SDict: 

46 r"""Two-port series impedance Sax model. 

47 

48 .. svgbob:: 

49 

50 o1 ─── Z ─── o2 

51 

52 See :cite:`m.pozarMicrowaveEngineering2012` (Ch. 4, Table 4.1, Table 4.2, Problem 4.11) 

53 for the S-parameter derivation. 

54 

55 Args: 

56 f: Array of frequency points in Hz. 

57 z: Complex impedance in Ohms. 

58 z0: Reference characteristic impedance in Ohms. 

59 

60 Returns: 

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

62 """ 

63 zn = jnp.asarray(z) / z0 

64 s11 = zn / (zn + 2.0) 

65 s21 = 2.0 / (zn + 2.0) 

66 sdict: sax.SDict = { 

67 ("o1", "o1"): s11, 

68 ("o2", "o2"): s11, 

69 ("o1", "o2"): s21, 

70 ("o2", "o1"): s21, 

71 } 

72 return sdict 

73 

74 

75@jax.jit 

76def shunt_admittance( 

77 f: sax.FloatArrayLike = DEFAULT_FREQUENCY, # noqa: ARG001 

78 y: sax.Float = 0.0, 

79 z0: float = 50.0, 

80) -> sax.SDict: 

81 r"""Two-port shunt admittance Sax model. 

82 

83 .. svgbob:: 

84 

85 o1 ──┬── o2 

86 

87 Y 

88 

89 GND 

90 

91 See :cite:`m.pozarMicrowaveEngineering2012` (Ch. 4, Table 4.1, Table 4.2, Problem 4.11) 

92 for the S-parameter derivation. 

93 

94 Args: 

95 f: Array of frequency points in Hz. 

96 y: Complex admittance in Siemens. 

97 z0: Reference characteristic impedance in Ohms. 

98 

99 Returns: 

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

101 """ 

102 yn = jnp.asarray(y) * z0 

103 s11 = -yn / (yn + 2.0) 

104 s21 = 2.0 / (yn + 2.0) 

105 sdict: sax.SDict = { 

106 ("o1", "o1"): s11, 

107 ("o2", "o2"): s11, 

108 ("o1", "o2"): s21, 

109 ("o2", "o1"): s21, 

110 } 

111 return sdict 

112 

113 

114@jax.jit 

115def electrical_short_2_port(f: sax.FloatArrayLike = DEFAULT_FREQUENCY) -> sax.SDict: 

116 """Electrical short 2-port connection Sax model. 

117 

118 Args: 

119 f: Array of frequency points in Hz 

120 

121 Returns: 

122 sax.SDict: S-parameters dictionary 

123 """ 

124 return electrical_short(f=f, n_ports=2) 

125 

126 

127short = electrical_short 

128open = electrical_open # noqa: A001 

129short_2_port = electrical_short_2_port 

130 

131 

132@jax.jit(static_argnames=["grounded"]) 

133def lc_resonator( 

134 f: sax.FloatArrayLike = DEFAULT_FREQUENCY, 

135 capacitance: float = 100e-15, 

136 inductance: float = 1e-9, 

137 grounded: bool = False, 

138 ground_capacitance: float = 0.0, 

139) -> sax.SDict: 

140 r"""LC resonator Sax model with capacitor and inductor in parallel. 

141 

142 The resonance frequency is given by: 

143 

144 .. svgbob:: 

145 

146 o1 ──┬──L──┬── o2 

147 │ │ 

148 └──C──┘ 

149 

150 If grounded=True, a 2-port short is connected to port o2: 

151 

152 .. svgbob:: 

153 

154 o1 ──┬──L──┬──. 

155 │ │ | "2-port ground" 

156 └──C──┘ | 

157 "o2" 

158 

159 Optional ground capacitances Cg can be added to both ports: 

160 

161 .. svgbob:: 

162 

163 ┌────── C ──────┐ 

164 o1 ──┼────── L ──────┼── o2 

165 │ │ 

166 Cg Cg 

167 │ │ 

168 GND GND 

169 

170 .. math:: 

171 

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

173 

174 For theory and relation to superconductors, see :cite:`gaoPhysicsSuperconductingMicrowave2008`. 

175 

176 Args: 

177 f: Array of frequency points in Hz. 

178 capacitance: Capacitance of the resonator in Farads. 

179 inductance: Inductance of the resonator in Henries. 

180 grounded: If True, add a 2-port ground to the second port. 

181 ground_capacitance: Parasitic capacitance to ground Cg at each port in Farads. 

182 

183 Returns: 

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

185 """ 

186 f = jnp.asarray(f) 

187 omega = 2 * jnp.pi * f 

188 z0 = 50.0 

189 

190 # Calculate physical values 

191 y_g = 1j * omega * ground_capacitance 

192 y_lc = 1j * omega * capacitance + 1.0 / (1j * omega * inductance + 1e-25) 

193 z_lc = 1.0 / (y_lc + 1e-25) 

194 

195 instances = { 

196 "cg1": shunt_admittance(f=f, y=y_g, z0=z0), 

197 "lc": series_impedance(f=f, z=z_lc, z0=z0), 

198 "cg2": shunt_admittance(f=f, y=y_g, z0=z0), 

199 } 

200 

201 connections = { 

202 "cg1,o2": "lc,o1", 

203 "lc,o2": "cg2,o1", 

204 } 

205 

206 port_o1 = "cg1,o1" 

207 port_o2 = "cg2,o2" 

208 

209 if grounded: 

210 instances["ground"] = electrical_short(f=f, n_ports=2) 

211 connections[port_o2] = "ground,o1" 

212 ports = { 

213 "o1": port_o1, 

214 "o2": "ground,o2", 

215 } 

216 else: 

217 ports = { 

218 "o1": port_o1, 

219 "o2": port_o2, 

220 } 

221 

222 return sax.evaluate_circuit_fg((connections, ports), instances) 

223 

224 

225@jax.jit(static_argnames=["grounded"]) 

226def lc_resonator_coupled( 

227 f: sax.FloatArrayLike = DEFAULT_FREQUENCY, 

228 capacitance: float = 100e-15, 

229 inductance: float = 1e-9, 

230 grounded: bool = False, 

231 ground_capacitance: float = 0.0, 

232 coupling_capacitance: float = 10e-15, 

233 coupling_inductance: float = 0.0, 

234) -> sax.SDict: 

235 r"""Coupled LC resonator Sax model. 

236 

237 This model extends the basic LC resonator by adding a coupling network 

238 consisting of a parallel capacitor and inductor connected in series 

239 to one port of the LC resonator. 

240 

241 The resonance frequency of the main LC resonator is given by: 

242 

243 .. math:: 

244 

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

246 

247 The coupling network modifies the effective coupling to the resonator. 

248 

249 .. svgbob:: 

250 

251 

252 +──Lc──+ +──L──+ 

253 o1 ──────│ │────| │─── o2 or grounded o2 

254 +──Cc──+ +──C──+ 

255 "LC resonator" 

256 

257 Where :math:`L_\text{c}` and :math:`C_\text{c}` are the coupling inductance and capacitance, respectively. 

258 

259 Args: 

260 f: Array of frequency points in Hz. 

261 capacitance: Capacitance of the main resonator in Farads. 

262 inductance: Inductance of the main resonator in Henries. 

263 grounded: If True, the resonator is grounded. 

264 ground_capacitance: Parasitic capacitance to ground Cg at each port in Farads. 

265 coupling_capacitance: Coupling capacitance in Farads. 

266 coupling_inductance: Coupling inductance in Henries. 

267 

268 Returns: 

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

270 """ 

271 f = jnp.asarray(f) 

272 omega = 2 * jnp.pi * f 

273 z0 = 50.0 

274 

275 resonator = lc_resonator( 

276 f=f, 

277 capacitance=capacitance, 

278 inductance=inductance, 

279 grounded=grounded, 

280 ground_capacitance=ground_capacitance, 

281 ) 

282 

283 # Combined coupling admittance (parallel Cc and Lc) 

284 y_coupling = 1j * omega * coupling_capacitance + 1.0 / ( 

285 1j * omega * coupling_inductance + 1e-25 

286 ) 

287 z_coupling = 1.0 / (y_coupling + 1e-25) 

288 

289 instances: dict[str, sax.SType] = { 

290 "resonator": resonator, 

291 "coupling": series_impedance(f=f, z=z_coupling, z0=z0), 

292 } 

293 

294 connections = { 

295 "coupling,o2": "resonator,o1", 

296 } 

297 

298 ports = { 

299 "o1": "coupling,o1", 

300 "o2": "resonator,o2", 

301 } 

302 

303 return sax.evaluate_circuit_fg((connections, ports), instances) 

304 

305 

306if __name__ == "__main__": 

307 f = jnp.linspace(1e9, 25e9, 201) 

308 S = gamma_0_load(f=f, gamma_0=0.5 + 0.5j, n_ports=2) 

309 for key in S: 

310 plt.plot(f / 1e9, abs(S[key]) ** 2, label=key) 

311 plt.ylim(-0.05, 1.05) 

312 plt.xlabel("Frequency [GHz]") 

313 plt.ylabel("S") 

314 plt.grid(True) 

315 plt.legend() 

316 plt.show(block=False) 

317 

318 S_cap = capacitor(f=f, capacitance=(capacitance := 100e-15)) 

319 # print(S_cap) 

320 plt.figure() 

321 # Polar plot of S21 and S11 

322 plt.subplot(121, projection="polar") 

323 plt.plot(jnp.angle(S_cap["o1", "o1"]), abs(S_cap["o1", "o1"]), label="$S_{11}$") 

324 plt.plot(jnp.angle(S_cap["o1", "o2"]), abs(S_cap["o2", "o1"]), label="$S_{21}$") 

325 plt.title("S-parameters capacitor") 

326 plt.legend() 

327 # Magnitude and phase vs frequency 

328 ax1 = plt.subplot(122) 

329 ax1.plot(f / 1e9, abs(S_cap["o1", "o1"]), label="|S11|", color="C0") 

330 ax1.plot(f / 1e9, abs(S_cap["o1", "o2"]), label="|S21|", color="C1") 

331 ax1.set_xlabel("Frequency [GHz]") 

332 ax1.set_ylabel("Magnitude [unitless]") 

333 ax1.grid(True) 

334 ax1.legend(loc="upper left") 

335 

336 ax2 = ax1.twinx() 

337 ax2.plot( 

338 f / 1e9, 

339 jnp.angle(S_cap["o1", "o1"]), 

340 label="∠S11", 

341 color="C0", 

342 linestyle="--", 

343 ) 

344 ax2.plot( 

345 f / 1e9, 

346 jnp.angle(S_cap["o1", "o2"]), 

347 label="∠S21", 

348 color="C1", 

349 linestyle="--", 

350 ) 

351 ax2.set_ylabel("Phase [rad]") 

352 ax2.legend(loc="upper right") 

353 

354 plt.title(f"Capacitor $S$-parameters ($C={capacitance * 1e15}\\,$fF)") 

355 plt.show(block=False) 

356 

357 S_ind = inductor(f=f, inductance=(inductance := 1e-9)) 

358 # print(S_ind) 

359 plt.figure() 

360 plt.subplot(121, projection="polar") 

361 plt.plot(jnp.angle(S_ind["o1", "o1"]), abs(S_ind["o1", "o1"]), label="$S_{11}$") 

362 plt.plot(jnp.angle(S_ind["o1", "o2"]), abs(S_ind["o2", "o1"]), label="$S_{21}$") 

363 plt.title("S-parameters inductor") 

364 plt.legend() 

365 ax1 = plt.subplot(122) 

366 ax1.plot(f / 1e9, abs(S_ind["o1", "o1"]), label="|S11|", color="C0") 

367 ax1.plot(f / 1e9, abs(S_ind["o1", "o2"]), label="|S21|", color="C1") 

368 ax1.set_xlabel("Frequency [GHz]") 

369 ax1.set_ylabel("Magnitude [unitless]") 

370 ax1.grid(True) 

371 ax1.legend(loc="upper left") 

372 

373 ax2 = ax1.twinx() 

374 ax2.plot( 

375 f / 1e9, 

376 jnp.angle(S_ind["o1", "o1"]), 

377 label="∠S11", 

378 color="C0", 

379 linestyle="--", 

380 ) 

381 ax2.plot( 

382 f / 1e9, 

383 jnp.angle(S_ind["o1", "o2"]), 

384 label="∠S21", 

385 color="C1", 

386 linestyle="--", 

387 ) 

388 ax2.set_ylabel("Phase [rad]") 

389 ax2.legend(loc="upper right") 

390 

391 plt.title(f"Inductor $S$-parameters ($L={inductance * 1e9}\\,$nH)") 

392 plt.show()