r"""Unimon qubit models.
This module provides both a SAX microwave S-parameter model and a quantum
Hamiltonian model for the unimon qubit.
**Microwave model** — The unimon is modeled as a capacitively coupled closed
system consisting of two quarter-wave CPW transmission-line sections shunted by
a Josephson junction (SQUID).
**Hamiltonian model** — The effective single-mode Hamiltonian of the unimon is
.. math::
\hat{H} = 4 E_C \hat{n}^2
+ \tfrac{1}{2} E_L (\hat{\varphi} - \varphi_{\text{ext}})^2
- E_J \cos\hat{\varphi}
where :math:`E_C` is the charging energy, :math:`E_L` the inductive energy
from the geometric inductance of the resonator arms, :math:`E_J` the
Josephson energy, and :math:`\varphi_{\text{ext}}` the external flux bias
(in units of the reduced flux quantum).
The unimon operates in the regime :math:`E_L \sim E_J`, which is distinct
from both the transmon (:math:`E_J \gg E_C`, no geometric inductance) and
the fluxonium (:math:`E_L \ll E_J`).
References:
- :cite:`hyyppaUnimonQubit2022`
- :cite:`tuohinoMultimodePhysicsUnimon2024`
- :cite:`dudaParameterOptimizationUnimon2025`
"""
import math
from functools import partial
import jax
import jax.numpy as jnp
import sax
from sax.models.rf import capacitor, electrical_short, tee
from qpdk.models.constants import DEFAULT_FREQUENCY, Φ_0, h
from qpdk.models.generic import lc_resonator
from qpdk.models.waveguides import straight_shorted
# ---------------------------------------------------------------------------
# Helper: energy-scale conversions
# ---------------------------------------------------------------------------
[docs]
@partial(jax.jit, inline=True)
def el_to_arm_inductance(el_ghz: float) -> float:
r"""Convert inductive energy :math:`E_L` to geometric inductance of one arm :math:`L`.
The total inductive energy :math:`E_L` of the unimon is related to the
geometric inductance of its two arms (in series) by:
.. math::
E_L = \frac{\Phi_0^2}{4 \pi^2 \cdot 2 L}
= \frac{(\hbar / 2e)^2}{2 L}
Solving for the inductance :math:`L` of a single arm:
.. math::
L = \frac{\Phi_0^2}{8 \pi^2 E_L}
Args:
el_ghz: Inductive energy in GHz.
Returns:
Geometric inductance of one arm in Henries.
Example:
>>> L = el_to_arm_inductance(5.0) # 5 GHz inductive energy
>>> print(f"{L * 1e9:.2f} nH")
"""
el_joules = el_ghz * 1e9 * h
return Φ_0**2 / (8 * math.pi**2 * el_joules)
# ---------------------------------------------------------------------------
# SAX microwave model
# ---------------------------------------------------------------------------
def _unimon_internal(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
arm_length: float = 3000.0,
cross_section: str = "cpw",
junction_capacitance: float = 5e-15,
junction_inductance: float = -7e-9,
) -> sax.SDict:
r"""Internal SAX S-parameter model for a closed unimon qubit.
The unimon is modeled as two shorted quarter-wave CPW transmission lines
connected by a parallel LC element representing the Josephson junction
(linearised RCSJ model).
.. svgbob::
o1 ── λ/4 TL ──┬──L_J──┬── λ/4 TL ── o2
│ │
└──C_J──┘
(junction)
The two ports ``o1`` and ``o2`` correspond to the nodes on either side of
the Josephson junction. The other ends of the transmission lines are
shorted to ground.
Args:
f: Array of frequency points in Hz.
arm_length: Length of each quarter-wave resonator arm in µm.
cross_section: Cross-section specification for the CPW arms.
junction_capacitance: Junction capacitance :math:`C_J` in Farads.
junction_inductance: Junction inductance :math:`L_J` in Henries.
Returns:
sax.SDict: S-parameters dictionary with internal ports ``o1`` and ``o2``.
"""
f = jnp.asarray(f)
# Two quarter-wave shorted transmission-line arms
arm_left = straight_shorted(f=f, length=arm_length, cross_section=cross_section)
arm_right = straight_shorted(f=f, length=arm_length, cross_section=cross_section)
# Josephson junction as parallel LC
junction = lc_resonator(
f=f,
capacitance=junction_capacitance,
inductance=junction_inductance,
grounded=False,
)
# Tee junctions to connect junction in between arms
tee_l = tee(f=f)
tee_r = tee(f=f)
# Terminal shorts for the shorted ends of the arms
short_l = electrical_short(f=f, n_ports=2)
short_r = electrical_short(f=f, n_ports=2)
instances: dict[str, sax.SType] = {
"arm_left": arm_left,
"arm_right": arm_right,
"junction": junction,
"tee_l": tee_l,
"tee_r": tee_r,
"short_l": short_l,
"short_r": short_r,
}
connections = {
# Left arm connects to left tee
"arm_left,o1": "tee_l,o1",
# Shorted end of left arm
"arm_left,o2": "short_l,o1",
# Right arm connects to right tee
"arm_right,o1": "tee_r,o1",
# Shorted end of right arm
"arm_right,o2": "short_r,o1",
# Junction between tees
"tee_l,o2": "junction,o1",
"junction,o2": "tee_r,o2",
}
ports = {
"o1": "tee_l,o3",
"o2": "tee_r,o3",
}
return sax.evaluate_circuit_fg((connections, ports), instances)
[docs]
def unimon_coupled(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
arm_length: float = 3000.0,
cross_section: str = "cpw",
junction_capacitance: float = 5e-15,
junction_inductance: float = -7e-9,
coupling_capacitance: float = 10e-15,
) -> sax.SDict:
r"""SAX model for a capacitively coupled unimon qubit.
The unimon is modeled as a closed system of two shorted quarter-wave CPW
resonators connected by a Josephson junction. Interaction with the qubit
is provided via a single coupling capacitor connected to one of the arms.
.. svgbob::
o1 ── Cc ──┬── λ/4 TL ── (ground)
│
Junction
│
└── λ/4 TL ── (ground)
Args:
f: Array of frequency points in Hz.
arm_length: Length of each quarter-wave resonator arm in µm.
cross_section: Cross-section specification for the CPW arms.
junction_capacitance: Junction capacitance :math:`C_J` in Farads.
junction_inductance: Junction inductance :math:`L_J` in Henries.
coupling_capacitance: Coupling capacitance :math:`C_c` in Farads.
Returns:
sax.SDict: S-parameters dictionary with a single port ``o1``.
"""
f = jnp.asarray(f)
instances: dict[str, sax.SType] = {
"unimon": _unimon_internal(
f=f,
arm_length=arm_length,
cross_section=cross_section,
junction_capacitance=junction_capacitance,
junction_inductance=junction_inductance,
),
"coupling_cap": capacitor(f=f, capacitance=coupling_capacitance),
}
connections = {
"coupling_cap,o2": "unimon,o1",
}
ports = {
"o1": "coupling_cap,o1",
}
return sax.evaluate_circuit_fg((connections, ports), instances)
# ---------------------------------------------------------------------------
# Quantum Hamiltonian model
# ---------------------------------------------------------------------------
[docs]
def unimon_hamiltonian(
ec_ghz: float = 1.0,
el_ghz: float = 5.0,
ej_ghz: float = 10.0,
phi_ext: float = jnp.pi,
n_max: int = 30,
) -> jax.Array:
r"""Build the unimon Hamiltonian matrix in the charge basis.
The effective single-mode unimon Hamiltonian is
.. math::
\hat{H} = 4 E_C \hat{n}^2
+ \tfrac{1}{2} E_L (\hat{\varphi} - \varphi_{\text{ext}})^2
- E_J \cos\hat{\varphi}
**Energy Scales and Physics**:
- :math:`E_J`: The Josephson energy of the single non-linear junction.
- :math:`E_C`: The effective charging energy. The large geometric capacitance
of the CPW resonator means this is lower than typical transmons.
- :math:`E_L`: The inductive energy provided by the CPW resonator shunting
the junction. The unimon operates in the regime :math:`E_L \sim E_J`.
**Similarities to Fluxonium**:
The effective single-mode circuit model of the unimon is mathematically identical
to a fluxonium qubit. The inductive shunt (provided by the CPW resonators)
guarantees there are no isolated superconducting islands, making the unimon
intrinsically immune to dephasing caused by low-frequency :math:`1/f` charge noise.
**Phase and Charge Operators**:
Because the unimon is inductively shunted, its potential energy features a
quadratic term :math:`\tfrac{1}{2} E_L \hat{\varphi}^2`, meaning the potential
is **not** :math:`2\pi`-periodic. Thus, the phase operator :math:`\hat{\varphi}`
must be treated as an extended, non-compact variable. Consequently, the
conjugate charge variable :math:`n` is continuous, rather than taking discrete
integer values as it does for a transmon.
*Note on this implementation*: To evaluate the Hamiltonian, we use a standard
truncated discrete charge basis approximation
(:math:`n \in \{-n_{\max}, \ldots, +n_{\max}\}`). The charge operator is derived
from `qutip.charge`, and the non-compact phase space operators are approximated
as nearest-neighbor hopping matrices.
See :cite:`hyyppaUnimonQubit2022` and :cite:`tuohinoMultimodePhysicsUnimon2024`.
Args:
ec_ghz: Charging energy :math:`E_C` in GHz.
el_ghz: Inductive energy :math:`E_L` in GHz.
ej_ghz: Josephson energy :math:`E_J` in GHz.
phi_ext: External flux bias :math:`\varphi_{\text{ext}}`
in radians (reduced flux-quantum units).
The optimal operating point is :math:`\pi`.
n_max: Charge-basis truncation; Hilbert-space dimension is
:math:`2 n_{\max} + 1`.
Returns:
A ``(2*n_max+1, 2*n_max+1)`` Hermitian matrix (in GHz).
Raises:
ImportError: If the QuTiP extra is not installed.
ModuleNotFoundError: If a dependency other than QuTiP fails to import.
"""
n_states = 2 * n_max + 1
try:
import qutip # noqa: PLC0415
import qutip_jax # noqa: PLC0415
except ModuleNotFoundError as e:
# Only treat missing top-level qutip/qutip_jax as an extra dependency issue.
# Re-raise any other import-time errors (including nested ones) unchanged.
if e.name in {"qutip", "qutip_jax"}:
raise ImportError(
"The QuTiP extra is required for Hamiltonian models. "
"Please install it with `pip install qpdk[qutip]`."
) from e
raise
# Extract raw JAX arrays from qutip-jax
n_hat = qutip.charge(n_max).to(qutip_jax.JaxArray).data._jxa
ones = jnp.ones(n_states - 1)
phi_hat = qutip.qdiags([ones, ones], [1, -1]).to(qutip_jax.JaxArray).data._jxa
cos_phi = (qutip.qdiags([ones, ones], [1, -1]) / 2).to(qutip_jax.JaxArray).data._jxa
# H = 4 E_C n^2 + 0.5 E_L (phi - phi_ext)^2 - E_J cos(phi)
# Expand the quadratic: (phi - phi_ext)^2 = phi^2 - 2*phi_ext*phi + phi_ext^2
return (
4 * ec_ghz * n_hat @ n_hat
+ 0.5
* el_ghz
* (phi_hat @ phi_hat - 2 * phi_ext * phi_hat + phi_ext**2 * jnp.eye(n_states))
- ej_ghz * cos_phi
)
[docs]
def unimon_energies(
ec_ghz: float = 1.0,
el_ghz: float = 5.0,
ej_ghz: float = 10.0,
phi_ext: float = jnp.pi,
n_max: int = 30,
n_levels: int = 5,
) -> jax.Array:
r"""Compute the lowest energy eigenvalues of the unimon Hamiltonian.
Diagonalises the Hamiltonian returned by :func:`unimon_hamiltonian` and
returns the ``n_levels`` lowest eigenvalues, shifted so that the ground
state energy is zero.
Args:
ec_ghz: Charging energy :math:`E_C` in GHz.
el_ghz: Inductive energy :math:`E_L` in GHz.
ej_ghz: Josephson energy :math:`E_J` in GHz.
phi_ext: External flux :math:`\varphi_{\text{ext}}` in radians.
n_max: Charge-basis truncation.
n_levels: Number of lowest energy levels to return.
Returns:
Array of shape ``(n_levels,)`` with energies in GHz,
referenced to the ground state.
Example:
>>> energies = unimon_energies(ec_ghz=1.0, el_ghz=5.0, ej_ghz=10.0)
>>> f_01 = float(energies[1]) # qubit frequency in GHz
>>> alpha = float(energies[2] - 2 * energies[1]) # anharmonicity
"""
H = unimon_hamiltonian(
ec_ghz=ec_ghz,
el_ghz=el_ghz,
ej_ghz=ej_ghz,
phi_ext=phi_ext,
n_max=n_max,
)
eigenvalues = jnp.linalg.eigvalsh(H)
# Shift to ground state = 0
eigenvalues = eigenvalues - eigenvalues[0]
return eigenvalues[:n_levels]
[docs]
def unimon_frequency_and_anharmonicity(
ec_ghz: float = 1.0,
el_ghz: float = 5.0,
ej_ghz: float = 10.0,
phi_ext: float = jnp.pi,
n_max: int = 30,
) -> tuple[float, float]:
r"""Compute the qubit transition frequency and anharmonicity of the unimon.
The qubit frequency is
.. math::
f_{01} = E_1 - E_0
and the anharmonicity is
.. math::
\alpha = (E_2 - E_1) - (E_1 - E_0) = E_2 - 2 E_1
(since :math:`E_0 = 0` after shifting).
Args:
ec_ghz: Charging energy :math:`E_C` in GHz.
el_ghz: Inductive energy :math:`E_L` in GHz.
ej_ghz: Josephson energy :math:`E_J` in GHz.
phi_ext: External flux :math:`\varphi_{\text{ext}}` in radians.
n_max: Charge-basis truncation.
Returns:
Tuple of (frequency_ghz, anharmonicity_ghz).
Example:
>>> f01, alpha = unimon_frequency_and_anharmonicity(
... ec_ghz=1.0, el_ghz=5.0, ej_ghz=10.0
... )
>>> print(f"f_01 = {f01:.3f} GHz, alpha = {alpha:.3f} GHz")
"""
energies = unimon_energies(
ec_ghz=ec_ghz,
el_ghz=el_ghz,
ej_ghz=ej_ghz,
phi_ext=phi_ext,
n_max=n_max,
n_levels=3,
)
f_01 = float(energies[1])
alpha = float(energies[2] - 2 * energies[1])
return f_01, alpha