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

32 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-14 10:27 +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 "short", 

33 "short_2_port", 

34 "tee", 

35] 

36 

37 

38@jax.jit 

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

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

41 

42 Args: 

43 f: Array of frequency points in Hz 

44 

45 Returns: 

46 sax.SDict: S-parameters dictionary 

47 """ 

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

49 

50 

51short = electrical_short 

52open = electrical_open 

53short_2_port = electrical_short_2_port 

54 

55 

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

57def lc_resonator( 

58 f: sax.FloatArrayLike = DEFAULT_FREQUENCY, 

59 capacitance: float = 100e-15, 

60 inductance: float = 1e-9, 

61 grounded: bool = False, 

62) -> sax.SDict: 

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

64 

65 The resonance frequency is given by: 

66 

67 .. svgbob:: 

68 

69 o1 ──┬──L──┬── o2 

70 │ │ 

71 └──C──┘ 

72 

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

74 

75 .. svgbob:: 

76 

77 o1 ──┬──L──┬──. 

78 │ │ | "2-port ground" 

79 └──C──┘ | 

80 "o2" 

81 

82 .. math:: 

83 

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

85 

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

87 

88 Args: 

89 f: Array of frequency points in Hz. 

90 capacitance: Capacitance of the resonator in Farads. 

91 inductance: Inductance of the resonator in Henries. 

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

93 

94 Returns: 

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

96 """ 

97 f = jnp.asarray(f) 

98 

99 instances = { 

100 "capacitor": capacitor(f=f, capacitance=capacitance), 

101 "inductor": inductor(f=f, inductance=inductance), 

102 "tee_1": tee(f=f), 

103 "tee_2": tee(f=f), 

104 } 

105 

106 connections = { 

107 "tee_1,o2": "capacitor,o1", 

108 "tee_1,o3": "inductor,o1", 

109 "capacitor,o2": "tee_2,o2", 

110 "inductor,o2": "tee_2,o3", 

111 } 

112 

113 if grounded: 

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

115 connections["tee_2,o1"] = "ground,o1" 

116 ports = { 

117 "o1": "tee_1,o1", 

118 "o2": "ground,o2", 

119 } 

120 else: 

121 ports = { 

122 "o1": "tee_1,o1", 

123 "o2": "tee_2,o1", 

124 } 

125 

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

127 

128 

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

130def lc_resonator_coupled( 

131 f: sax.FloatArrayLike = DEFAULT_FREQUENCY, 

132 capacitance: float = 100e-15, 

133 inductance: float = 1e-9, 

134 grounded: bool = False, 

135 coupling_capacitance: float = 10e-15, 

136 coupling_inductance: float = 0.0, 

137) -> sax.SDict: 

138 r"""Coupled LC resonator Sax model. 

139 

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

141 consisting of a parallel capacitor and inductor connected to one port 

142 of the LC resonator via a tee junction. 

143 

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

145 

146 .. math:: 

147 

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

149 

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

151 

152 .. svgbob:: 

153 

154 

155 +──Lc──+ +──L──+ 

156 o1 ──────│ │────| │─── o2 or grounded o2 

157 +──Cc──+ +──C──+ 

158 "LC resonator" 

159 

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

161 

162 Args: 

163 f: Array of frequency points in Hz. 

164 capacitance: Capacitance of the main resonator in Farads. 

165 inductance: Inductance of the main resonator in Henries. 

166 grounded: If True, the resonator is grounded. 

167 coupling_capacitance: Coupling capacitance in Farads. 

168 coupling_inductance: Coupling inductance in Henries. 

169 

170 Returns: 

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

172 """ 

173 f = jnp.asarray(f) 

174 resonator = lc_resonator( 

175 f=f, capacitance=capacitance, inductance=inductance, grounded=grounded 

176 ) 

177 

178 # Always use the full tee network topology for consistent behavior 

179 # When an element has zero value, it naturally produces the correct S-parameters 

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

181 "resonator": resonator, 

182 "tee_between": tee(f=f), 

183 "tee_outer": tee(f=f), 

184 "inductive_coupling": inductor(f=f, inductance=coupling_inductance), 

185 "capacitive_coupling": capacitor(f=f, capacitance=coupling_capacitance), 

186 } 

187 

188 connections = { 

189 "tee_outer,o2": "inductive_coupling,o1", 

190 "tee_outer,o3": "capacitive_coupling,o1", 

191 "inductive_coupling,o2": "tee_between,o2", 

192 "capacitive_coupling,o2": "tee_between,o3", 

193 "tee_between,o1": "resonator,o1", 

194 } 

195 

196 ports = { 

197 "o1": "tee_outer,o1", 

198 "o2": "resonator,o2", 

199 } 

200 

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

202 

203 

204if __name__ == "__main__": 

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

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

207 for key in S: 

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

209 plt.ylim(-0.05, 1.05) 

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

211 plt.ylabel("S") 

212 plt.grid(True) 

213 plt.legend() 

214 plt.show(block=False) 

215 

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

217 # print(S_cap) 

218 plt.figure() 

219 # Polar plot of S21 and S11 

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

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

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

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

224 plt.legend() 

225 # Magnitude and phase vs frequency 

226 ax1 = plt.subplot(122) 

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

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

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

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

231 ax1.grid(True) 

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

233 

234 ax2 = ax1.twinx() 

235 ax2.plot( 

236 f / 1e9, 

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

238 label="∠S11", 

239 color="C0", 

240 linestyle="--", 

241 ) 

242 ax2.plot( 

243 f / 1e9, 

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

245 label="∠S21", 

246 color="C1", 

247 linestyle="--", 

248 ) 

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

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

251 

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

253 plt.show(block=False) 

254 

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

256 # print(S_ind) 

257 plt.figure() 

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

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

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

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

262 plt.legend() 

263 ax1 = plt.subplot(122) 

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

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

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

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

268 ax1.grid(True) 

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

270 

271 ax2 = ax1.twinx() 

272 ax2.plot( 

273 f / 1e9, 

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

275 label="∠S11", 

276 color="C0", 

277 linestyle="--", 

278 ) 

279 ax2.plot( 

280 f / 1e9, 

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

282 label="∠S21", 

283 color="C1", 

284 linestyle="--", 

285 ) 

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

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

288 

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

290 plt.show()