"""Generic SAX models from stored N-port scattering or Maxwell matrices."""
from pathlib import Path
import jax.numpy as jnp
import sax
from jax.typing import ArrayLike
from qpdk.models.constants import DEFAULT_FREQUENCY
from qpdk.models.datasets.capacitance import check_maxwell
from qpdk.models.datasets.interpolation import GridInterpolator
from qpdk.models.datasets.metadata import QuantityKind
from qpdk.models.datasets.table import Dataset
from qpdk.models.jit import jit
[docs]
def s_parameters_model(
dataset: Dataset | Path | str,
quantity: str = "s_parameters",
*,
frequency_axis: str = "frequency",
**variants: str,
) -> sax.SDictModel:
"""Load stored N-port S-parameters and return a jittable SAX lookup.
``f`` is in Hz and supplies ``frequency_axis``. Other continuous grid axes
are passed by name; discrete variants are fixed when creating the model.
Terminals become port names. Reference impedances and planes are those of
the stored simulation, without renormalization. Out-of-domain values are
NaN. Multilinear interpolation does not enforce losslessness between points.
Unspecified geometry axes use their middle stored coordinates.
Returns:
SAX model with the dataset terminal names.
Raises:
ValueError: If the quantity or frequency axis has the wrong convention.
"""
data = dataset if isinstance(dataset, Dataset) else Dataset(dataset)
grid = data.grid(quantity, allow_missing=False, **variants)
if grid.quantity.kind != QuantityKind.S_PARAMETERS or not grid.quantity.complex:
raise ValueError("Expected complex S-parameters")
if not any(axis.name == frequency_axis and axis.unit == "Hz" for axis in grid.axes):
raise ValueError("The frequency axis must be present and in Hz")
lookup = GridInterpolator(grid)
defaults = {
name: float(values[len(values) // 2])
for name, values in zip(grid.axis_names, grid.coords, strict=True)
if name != frequency_axis
}
def model(f: ArrayLike = DEFAULT_FREQUENCY, **params: ArrayLike) -> sax.SDict:
values = lookup(**{**defaults, frequency_axis: f, **params})
return {
(a, b): values[..., i, j]
for i, a in enumerate(grid.terminals)
for j, b in enumerate(grid.terminals)
}
sax.replace_kwargs(model, f=DEFAULT_FREQUENCY, **defaults)
return jit(model)
[docs]
def capacitance_model(
dataset: Dataset | Path | str,
quantity: str = "maxwell_capacitance",
**variants: str,
) -> sax.SDictModel:
r"""Return a lumped N-port SAX model from a Maxwell capacitance dataset.
Continuous geometry axes are passed by name; discrete variants are fixed
here. ``f`` is in Hz and ``z_ref`` is a common real port impedance in ohms.
The admittance is :math:`Y = j 2 \pi f C`; all terminals reference the
dataset ground. This model includes terminal-to-ground capacitances and
mutual branches, with no distributed propagation or inductance.
Unspecified geometry axes use their middle stored coordinates.
Returns:
SAX model with the dataset terminal names.
Raises:
ValueError: If the quantity is not a physical real Maxwell matrix.
"""
data = dataset if isinstance(dataset, Dataset) else Dataset(dataset)
grid = data.grid(quantity, allow_missing=False, **variants)
if grid.quantity.kind != QuantityKind.MAXWELL_CAPACITANCE or grid.quantity.complex:
raise ValueError("Expected real Maxwell capacitances")
check_maxwell(grid.values)
lookup = GridInterpolator(grid)
identity = jnp.eye(len(grid.terminals))
defaults = {
name: float(values[len(values) // 2])
for name, values in zip(grid.axis_names, grid.coords, strict=True)
}
def model(
f: ArrayLike = DEFAULT_FREQUENCY,
z_ref: ArrayLike = 50.0,
**params: ArrayLike,
) -> sax.SDict:
capacitance = lookup(**{**defaults, **params})
admittance = 2j * jnp.pi * jnp.asarray(f)[..., None, None] * capacitance
normalized = jnp.asarray(z_ref)[..., None, None] * admittance
values = jnp.linalg.solve(identity + normalized, identity - normalized)
return {
(a, b): values[..., i, j]
for i, a in enumerate(grid.terminals)
for j, b in enumerate(grid.terminals)
}
sax.replace_kwargs(model, f=DEFAULT_FREQUENCY, z_ref=50.0, **defaults)
return jit(model)
[docs]
def junction_model(
dataset: Dataset | Path | str,
*,
junction: tuple[str, str],
inductance: float,
junction_capacitance: float = 0.0,
terminal_groups: tuple[tuple[str, ...], ...] | None = None,
**variants: str,
) -> sax.SDictModel:
r"""Build a small-signal circuit from electrode capacitances and one junction.
The dataset supplies capacitances between electrodes and to ground. Between
the two ``junction`` ports, this adds an inductor in parallel with an optional
junction capacitor. The inductor approximates the Josephson junction for
small oscillations around its operating point; it does not model nonlinear
dynamics or qubit anharmonicity. See :cite:`voolIntroductionQuantumElectromagnetic2017`,
sections 2.1.6 and 4.1, for this linearization and the Josephson element.
The branch admittance is :math:`Y_\text{J}=1/(j\omega L_\text{J})+j\omega C_\text{J}`.
Its inductance and capacitance are explicit inputs, since an electrostatic
dataset cannot determine the Josephson inductance. Only add junction
capacitance that is not already included in the dataset.
``terminal_groups`` optionally connects electrodes with ideal wires. For
example, ``(("island", "top"), ("ring",))`` joins ``island`` and ``top`` and
leaves ``ring`` separate. Each group's first name becomes its SAX port.
Every dataset terminal must appear exactly once. This applies
:math:`B^{\text{T}} C B`, with :math:`B` mapping group voltages to electrodes,
and retains capacitance to ground. The wires add no field or inductance.
All SAX ports reference dataset ground, and frequency is in Hz.
Args:
dataset: Dataset or path containing real Maxwell capacitance matrices in F.
junction: Names of the two ports connected by the junction, after grouping.
inductance: Positive small-signal Josephson inductance in H.
junction_capacitance: Additional parallel junction capacitance in F.
terminal_groups: Electrodes to connect together; defaults to separate ports.
**variants: Discrete dataset selections.
Returns:
Jittable small-signal SAX model, with geometry in dataset axis units.
Raises:
ValueError: If terminals, grouping, matrix type or explicit inputs are invalid.
"""
if inductance <= 0 or junction_capacitance < 0:
raise ValueError(
"Require positive inductance and nonnegative junction capacitance"
)
data = dataset if isinstance(dataset, Dataset) else Dataset(dataset)
grid = data.grid("maxwell_capacitance", allow_missing=False, **variants)
if grid.quantity.kind != QuantityKind.MAXWELL_CAPACITANCE or grid.quantity.complex:
raise ValueError("Expected real Maxwell capacitances")
check_maxwell(grid.values)
groups = (
tuple((name,) for name in grid.terminals)
if terminal_groups is None
else terminal_groups
)
members = [name for group in groups for name in group]
if any(not group for group in groups) or sorted(members) != sorted(grid.terminals):
raise ValueError(
"Terminal groups must partition every dataset terminal exactly once"
)
ports = tuple(group[0] for group in groups)
if len(set(junction)) != 2 or not set(junction) <= set(ports):
raise ValueError("The junction must name two distinct grouped ports")
connection = jnp.asarray([
[float(name in group) for group in groups] for name in grid.terminals
])
d = jnp.asarray([
float(name == junction[0]) - float(name == junction[1]) for name in ports
])
branch = d[:, None] * d[None, :]
lookup = GridInterpolator(grid)
identity = jnp.eye(len(ports))
defaults = {
name: float(values[len(values) // 2])
for name, values in zip(grid.axis_names, grid.coords, strict=True)
}
def model(
f: ArrayLike = DEFAULT_FREQUENCY,
z_ref: ArrayLike = 50.0,
inductance: ArrayLike = inductance,
junction_capacitance: ArrayLike = junction_capacitance,
**params: ArrayLike,
) -> sax.SDict:
"""Evaluate a lossless linearized junction circuit at positive frequencies."""
capacitance = connection.T @ lookup(**{**defaults, **params}) @ connection
omega = 2 * jnp.pi * jnp.asarray(f)
y_junction = 1j * omega * jnp.asarray(junction_capacitance) + 1 / (
1j * omega * jnp.asarray(inductance)
)
admittance = (
1j * omega[..., None, None] * capacitance
+ y_junction[..., None, None] * branch
)
normalized = jnp.asarray(z_ref)[..., None, None] * admittance
values = jnp.linalg.solve(identity + normalized, identity - normalized)
valid = (
(jnp.asarray(f) > 0)
& (jnp.asarray(inductance) > 0)
& (jnp.asarray(junction_capacitance) >= 0)
)
values = jnp.where(valid[..., None, None], values, jnp.nan)
return {
(a, b): values[..., i, j]
for i, a in enumerate(ports)
for j, b in enumerate(ports)
}
sax.replace_kwargs(
model,
f=DEFAULT_FREQUENCY,
z_ref=50.0,
inductance=inductance,
junction_capacitance=junction_capacitance,
**defaults,
)
return jit(model)