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
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-02 17:50 +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 "series_impedance",
33 "short",
34 "short_2_port",
35 "shunt_admittance",
36 "tee",
37]
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.
48 .. svgbob::
50 o1 ─── Z ─── o2
52 See :cite:`m.pozarMicrowaveEngineering2012` (Ch. 4, Table 4.1, Table 4.2, Problem 4.11)
53 for the S-parameter derivation.
55 Args:
56 f: Array of frequency points in Hz.
57 z: Complex impedance in Ohms.
58 z0: Reference characteristic impedance in Ohms.
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
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.
83 .. svgbob::
85 o1 ──┬── o2
86 │
87 Y
88 │
89 GND
91 See :cite:`m.pozarMicrowaveEngineering2012` (Ch. 4, Table 4.1, Table 4.2, Problem 4.11)
92 for the S-parameter derivation.
94 Args:
95 f: Array of frequency points in Hz.
96 y: Complex admittance in Siemens.
97 z0: Reference characteristic impedance in Ohms.
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
114@jax.jit
115def electrical_short_2_port(f: sax.FloatArrayLike = DEFAULT_FREQUENCY) -> sax.SDict:
116 """Electrical short 2-port connection Sax model.
118 Args:
119 f: Array of frequency points in Hz
121 Returns:
122 sax.SDict: S-parameters dictionary
123 """
124 return electrical_short(f=f, n_ports=2)
127short = electrical_short
128open = electrical_open # noqa: A001
129short_2_port = electrical_short_2_port
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.
142 The resonance frequency is given by:
144 .. svgbob::
146 o1 ──┬──L──┬── o2
147 │ │
148 └──C──┘
150 If grounded=True, a 2-port short is connected to port o2:
152 .. svgbob::
154 o1 ──┬──L──┬──.
155 │ │ | "2-port ground"
156 └──C──┘ |
157 "o2"
159 Optional ground capacitances Cg can be added to both ports:
161 .. svgbob::
163 ┌────── C ──────┐
164 o1 ──┼────── L ──────┼── o2
165 │ │
166 Cg Cg
167 │ │
168 GND GND
170 .. math::
172 f_r = \frac{1}{2 \pi \sqrt{LC}}
174 For theory and relation to superconductors, see :cite:`gaoPhysicsSuperconductingMicrowave2008`.
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.
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
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)
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 }
201 connections = {
202 "cg1,o2": "lc,o1",
203 "lc,o2": "cg2,o1",
204 }
206 port_o1 = "cg1,o1"
207 port_o2 = "cg2,o2"
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 }
222 return sax.evaluate_circuit_fg((connections, ports), instances)
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.
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.
241 The resonance frequency of the main LC resonator is given by:
243 .. math::
245 f_r = \frac{1}{2 \pi \sqrt{LC}}
247 The coupling network modifies the effective coupling to the resonator.
249 .. svgbob::
252 +──Lc──+ +──L──+
253 o1 ──────│ │────| │─── o2 or grounded o2
254 +──Cc──+ +──C──+
255 "LC resonator"
257 Where :math:`L_\text{c}` and :math:`C_\text{c}` are the coupling inductance and capacitance, respectively.
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.
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
275 resonator = lc_resonator(
276 f=f,
277 capacitance=capacitance,
278 inductance=inductance,
279 grounded=grounded,
280 ground_capacitance=ground_capacitance,
281 )
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)
289 instances: dict[str, sax.SType] = {
290 "resonator": resonator,
291 "coupling": series_impedance(f=f, z=z_coupling, z0=z0),
292 }
294 connections = {
295 "coupling,o2": "resonator,o1",
296 }
298 ports = {
299 "o1": "coupling,o1",
300 "o2": "resonator,o2",
301 }
303 return sax.evaluate_circuit_fg((connections, ports), instances)
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)
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")
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")
354 plt.title(f"Capacitor $S$-parameters ($C={capacitance * 1e15}\\,$fF)")
355 plt.show(block=False)
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")
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")
391 plt.title(f"Inductor $S$-parameters ($L={inductance * 1e9}\\,$nH)")
392 plt.show()