Source code for qpdk.models.generic

"""Generic Models."""

import jax
import jax.numpy as jnp
import sax
from matplotlib import pyplot as plt
from sax.models.rf import (
    admittance,
    capacitor,
    electrical_open,
    electrical_short,
    gamma_0_load,
    impedance,
    inductor,
    tee,
)

from qpdk.models.constants import DEFAULT_FREQUENCY

__all__ = [
    "admittance",
    "capacitor",
    "electrical_open",
    "electrical_short",
    "electrical_short_2_port",
    "gamma_0_load",
    "impedance",
    "inductor",
    "lc_resonator",
    "lc_resonator_coupled",
    "open",
    "series_impedance",
    "short",
    "short_2_port",
    "shunt_admittance",
    "tee",
]


[docs] @jax.jit def series_impedance( f: sax.FloatArrayLike = DEFAULT_FREQUENCY, # noqa: ARG001 z: sax.Float = 0.0, z0: float = 50.0, ) -> sax.SDict: r"""Two-port series impedance Sax model. .. svgbob:: o1 ─── Z ─── o2 See :cite:`m.pozarMicrowaveEngineering2012` (Ch. 4, Table 4.1, Table 4.2, Problem 4.11) for the S-parameter derivation. Args: f: Array of frequency points in Hz. z: Complex impedance in Ohms. z0: Reference characteristic impedance in Ohms. Returns: sax.SDict: S-parameters dictionary with ports o1 and o2. """ zn = jnp.asarray(z) / z0 s11 = zn / (zn + 2.0) s21 = 2.0 / (zn + 2.0) sdict: sax.SDict = { ("o1", "o1"): s11, ("o2", "o2"): s11, ("o1", "o2"): s21, ("o2", "o1"): s21, } return sdict
[docs] @jax.jit def shunt_admittance( f: sax.FloatArrayLike = DEFAULT_FREQUENCY, # noqa: ARG001 y: sax.Float = 0.0, z0: float = 50.0, ) -> sax.SDict: r"""Two-port shunt admittance Sax model. .. svgbob:: o1 ──┬── o2 Y GND See :cite:`m.pozarMicrowaveEngineering2012` (Ch. 4, Table 4.1, Table 4.2, Problem 4.11) for the S-parameter derivation. Args: f: Array of frequency points in Hz. y: Complex admittance in Siemens. z0: Reference characteristic impedance in Ohms. Returns: sax.SDict: S-parameters dictionary with ports o1 and o2. """ yn = jnp.asarray(y) * z0 s11 = -yn / (yn + 2.0) s21 = 2.0 / (yn + 2.0) sdict: sax.SDict = { ("o1", "o1"): s11, ("o2", "o2"): s11, ("o1", "o2"): s21, ("o2", "o1"): s21, } return sdict
[docs] @jax.jit def electrical_short_2_port(f: sax.FloatArrayLike = DEFAULT_FREQUENCY) -> sax.SDict: """Electrical short 2-port connection Sax model. Args: f: Array of frequency points in Hz Returns: sax.SDict: S-parameters dictionary """ return electrical_short(f=f, n_ports=2)
short = electrical_short open = electrical_open # noqa: A001 short_2_port = electrical_short_2_port
[docs] @jax.jit(static_argnames=["grounded"]) def lc_resonator( f: sax.FloatArrayLike = DEFAULT_FREQUENCY, capacitance: float = 100e-15, inductance: float = 1e-9, grounded: bool = False, ground_capacitance: float = 0.0, ) -> sax.SDict: r"""LC resonator Sax model with capacitor and inductor in parallel. The resonance frequency is given by: .. svgbob:: o1 ──┬──L──┬── o2 │ │ └──C──┘ If grounded=True, a 2-port short is connected to port o2: .. svgbob:: o1 ──┬──L──┬──. │ │ | "2-port ground" └──C──┘ | "o2" Optional ground capacitances Cg can be added to both ports: .. svgbob:: ┌────── C ──────┐ o1 ──┼────── L ──────┼── o2 │ │ Cg Cg │ │ GND GND .. math:: f_r = \frac{1}{2 \pi \sqrt{LC}} For theory and relation to superconductors, see :cite:`gaoPhysicsSuperconductingMicrowave2008`. Args: f: Array of frequency points in Hz. capacitance: Capacitance of the resonator in Farads. inductance: Inductance of the resonator in Henries. grounded: If True, add a 2-port ground to the second port. ground_capacitance: Parasitic capacitance to ground Cg at each port in Farads. Returns: sax.SDict: S-parameters dictionary with ports o1 and o2. """ f = jnp.asarray(f) omega = 2 * jnp.pi * f z0 = 50.0 # Calculate physical values y_g = 1j * omega * ground_capacitance y_lc = 1j * omega * capacitance + 1.0 / (1j * omega * inductance + 1e-25) z_lc = 1.0 / (y_lc + 1e-25) instances = { "cg1": shunt_admittance(f=f, y=y_g, z0=z0), "lc": series_impedance(f=f, z=z_lc, z0=z0), "cg2": shunt_admittance(f=f, y=y_g, z0=z0), } connections = { "cg1,o2": "lc,o1", "lc,o2": "cg2,o1", } port_o1 = "cg1,o1" port_o2 = "cg2,o2" if grounded: instances["ground"] = electrical_short(f=f, n_ports=2) connections[port_o2] = "ground,o1" ports = { "o1": port_o1, "o2": "ground,o2", } else: ports = { "o1": port_o1, "o2": port_o2, } return sax.evaluate_circuit_fg((connections, ports), instances)
[docs] @jax.jit(static_argnames=["grounded"]) def lc_resonator_coupled( f: sax.FloatArrayLike = DEFAULT_FREQUENCY, capacitance: float = 100e-15, inductance: float = 1e-9, grounded: bool = False, ground_capacitance: float = 0.0, coupling_capacitance: float = 10e-15, coupling_inductance: float = 0.0, ) -> sax.SDict: r"""Coupled LC resonator Sax model. This model extends the basic LC resonator by adding a coupling network consisting of a parallel capacitor and inductor connected in series to one port of the LC resonator. The resonance frequency of the main LC resonator is given by: .. math:: f_r = \frac{1}{2 \pi \sqrt{LC}} The coupling network modifies the effective coupling to the resonator. .. svgbob:: +──Lc──+ +──L──+ o1 ──────│ │────| │─── o2 or grounded o2 +──Cc──+ +──C──+ "LC resonator" Where :math:`L_\text{c}` and :math:`C_\text{c}` are the coupling inductance and capacitance, respectively. Args: f: Array of frequency points in Hz. capacitance: Capacitance of the main resonator in Farads. inductance: Inductance of the main resonator in Henries. grounded: If True, the resonator is grounded. ground_capacitance: Parasitic capacitance to ground Cg at each port in Farads. coupling_capacitance: Coupling capacitance in Farads. coupling_inductance: Coupling inductance in Henries. Returns: sax.SDict: S-parameters dictionary with ports o1 and o2. """ f = jnp.asarray(f) omega = 2 * jnp.pi * f z0 = 50.0 resonator = lc_resonator( f=f, capacitance=capacitance, inductance=inductance, grounded=grounded, ground_capacitance=ground_capacitance, ) # Combined coupling admittance (parallel Cc and Lc) y_coupling = 1j * omega * coupling_capacitance + 1.0 / ( 1j * omega * coupling_inductance + 1e-25 ) z_coupling = 1.0 / (y_coupling + 1e-25) instances: dict[str, sax.SType] = { "resonator": resonator, "coupling": series_impedance(f=f, z=z_coupling, z0=z0), } connections = { "coupling,o2": "resonator,o1", } ports = { "o1": "coupling,o1", "o2": "resonator,o2", } return sax.evaluate_circuit_fg((connections, ports), instances)
if __name__ == "__main__": f = jnp.linspace(1e9, 25e9, 201) S = gamma_0_load(f=f, gamma_0=0.5 + 0.5j, n_ports=2) for key in S: plt.plot(f / 1e9, abs(S[key]) ** 2, label=key) plt.ylim(-0.05, 1.05) plt.xlabel("Frequency [GHz]") plt.ylabel("S") plt.grid(True) plt.legend() plt.show(block=False) S_cap = capacitor(f=f, capacitance=(capacitance := 100e-15)) # print(S_cap) plt.figure() # Polar plot of S21 and S11 plt.subplot(121, projection="polar") plt.plot(jnp.angle(S_cap["o1", "o1"]), abs(S_cap["o1", "o1"]), label="$S_{11}$") plt.plot(jnp.angle(S_cap["o1", "o2"]), abs(S_cap["o2", "o1"]), label="$S_{21}$") plt.title("S-parameters capacitor") plt.legend() # Magnitude and phase vs frequency ax1 = plt.subplot(122) ax1.plot(f / 1e9, abs(S_cap["o1", "o1"]), label="|S11|", color="C0") ax1.plot(f / 1e9, abs(S_cap["o1", "o2"]), label="|S21|", color="C1") ax1.set_xlabel("Frequency [GHz]") ax1.set_ylabel("Magnitude [unitless]") ax1.grid(True) ax1.legend(loc="upper left") ax2 = ax1.twinx() ax2.plot( f / 1e9, jnp.angle(S_cap["o1", "o1"]), label="∠S11", color="C0", linestyle="--", ) ax2.plot( f / 1e9, jnp.angle(S_cap["o1", "o2"]), label="∠S21", color="C1", linestyle="--", ) ax2.set_ylabel("Phase [rad]") ax2.legend(loc="upper right") plt.title(f"Capacitor $S$-parameters ($C={capacitance * 1e15}\\,$fF)") plt.show(block=False) S_ind = inductor(f=f, inductance=(inductance := 1e-9)) # print(S_ind) plt.figure() plt.subplot(121, projection="polar") plt.plot(jnp.angle(S_ind["o1", "o1"]), abs(S_ind["o1", "o1"]), label="$S_{11}$") plt.plot(jnp.angle(S_ind["o1", "o2"]), abs(S_ind["o2", "o1"]), label="$S_{21}$") plt.title("S-parameters inductor") plt.legend() ax1 = plt.subplot(122) ax1.plot(f / 1e9, abs(S_ind["o1", "o1"]), label="|S11|", color="C0") ax1.plot(f / 1e9, abs(S_ind["o1", "o2"]), label="|S21|", color="C1") ax1.set_xlabel("Frequency [GHz]") ax1.set_ylabel("Magnitude [unitless]") ax1.grid(True) ax1.legend(loc="upper left") ax2 = ax1.twinx() ax2.plot( f / 1e9, jnp.angle(S_ind["o1", "o1"]), label="∠S11", color="C0", linestyle="--", ) ax2.plot( f / 1e9, jnp.angle(S_ind["o1", "o2"]), label="∠S21", color="C1", linestyle="--", ) ax2.set_ylabel("Phase [rad]") ax2.legend(loc="upper right") plt.title(f"Inductor $S$-parameters ($L={inductance * 1e9}\\,$nH)") plt.show()