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
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-14 10:27 +0000
1"""Generic Models."""
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)
18from qpdk.models.constants import DEFAULT_FREQUENCY
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]
38@jax.jit
39def electrical_short_2_port(f: sax.FloatArrayLike = DEFAULT_FREQUENCY) -> sax.SDict:
40 """Electrical short 2-port connection Sax model.
42 Args:
43 f: Array of frequency points in Hz
45 Returns:
46 sax.SDict: S-parameters dictionary
47 """
48 return electrical_short(f=f, n_ports=2)
51short = electrical_short
52open = electrical_open
53short_2_port = electrical_short_2_port
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.
65 The resonance frequency is given by:
67 .. svgbob::
69 o1 ──┬──L──┬── o2
70 │ │
71 └──C──┘
73 If grounded=True, a 2-port short is connected to port o2:
75 .. svgbob::
77 o1 ──┬──L──┬──.
78 │ │ | "2-port ground"
79 └──C──┘ |
80 "o2"
82 .. math::
84 f_r = \frac{1}{2 \pi \sqrt{LC}}
86 For theory and relation to superconductors, see :cite:`gaoPhysicsSuperconductingMicrowave2008`.
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.
94 Returns:
95 sax.SDict: S-parameters dictionary with ports o1 and o2.
96 """
97 f = jnp.asarray(f)
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 }
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 }
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 }
126 return sax.evaluate_circuit_fg((connections, ports), instances)
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.
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.
144 The resonance frequency of the main LC resonator is given by:
146 .. math::
148 f_r = \frac{1}{2 \pi \sqrt{LC}}
150 The coupling network modifies the effective coupling to the resonator.
152 .. svgbob::
155 +──Lc──+ +──L──+
156 o1 ──────│ │────| │─── o2 or grounded o2
157 +──Cc──+ +──C──+
158 "LC resonator"
160 Where :math:`L_\text{c}` and :math:`C_\text{c}` are the coupling inductance and capacitance, respectively.
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.
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 )
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 }
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 }
196 ports = {
197 "o1": "tee_outer,o1",
198 "o2": "resonator,o2",
199 }
201 return sax.evaluate_circuit_fg((connections, ports), instances)
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)
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")
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")
252 plt.title(f"Capacitor $S$-parameters ($C={capacitance * 1e15}\\,$fF)")
253 plt.show(block=False)
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")
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")
289 plt.title(f"Inductor $S$-parameters ($L={inductance * 1e9}\\,$nH)")
290 plt.show()