Coverage for qpdk / models / unimon.py: 93%
56 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
1r"""Unimon qubit models.
3This module provides both a SAX microwave S-parameter model and a quantum
4Hamiltonian model for the unimon qubit.
6**Microwave model** — The unimon is modeled as a capacitively coupled closed
7system consisting of two quarter-wave CPW transmission-line sections shunted by
8a Josephson junction (SQUID).
10**Hamiltonian model** — The effective single-mode Hamiltonian of the unimon is
12.. math::
14 \hat{H} = 4 E_C \hat{n}^2
15 + \tfrac{1}{2} E_L (\hat{\varphi} - \varphi_{\text{ext}})^2
16 - E_J \cos\hat{\varphi}
18where :math:`E_C` is the charging energy, :math:`E_L` the inductive energy
19from the geometric inductance of the resonator arms, :math:`E_J` the
20Josephson energy, and :math:`\varphi_{\text{ext}}` the external flux bias
21(in units of the reduced flux quantum).
23The unimon operates in the regime :math:`E_L \sim E_J`, which is distinct
24from both the transmon (:math:`E_J \gg E_C`, no geometric inductance) and
25the fluxonium (:math:`E_L \ll E_J`).
27References:
28 - :cite:`hyyppaUnimonQubit2022`
29 - :cite:`tuohinoMultimodePhysicsUnimon2024`
30 - :cite:`dudaParameterOptimizationUnimon2025`
31"""
33import math
34from functools import partial
36import jax
37import jax.numpy as jnp
38import sax
39from sax.models.rf import capacitor, electrical_short, tee
41from qpdk.models.constants import DEFAULT_FREQUENCY, Φ_0, h
42from qpdk.models.generic import lc_resonator
43from qpdk.models.waveguides import straight_shorted
45# ---------------------------------------------------------------------------
46# Helper: energy-scale conversions
47# ---------------------------------------------------------------------------
50@partial(jax.jit, inline=True)
51def el_to_arm_inductance(el_ghz: float) -> float:
52 r"""Convert inductive energy :math:`E_L` to geometric inductance of one arm :math:`L`.
54 The total inductive energy :math:`E_L` of the unimon is related to the
55 geometric inductance of its two arms (in series) by:
57 .. math::
59 E_L = \frac{\Phi_0^2}{4 \pi^2 \cdot 2 L}
60 = \frac{(\hbar / 2e)^2}{2 L}
62 Solving for the inductance :math:`L` of a single arm:
64 .. math::
66 L = \frac{\Phi_0^2}{8 \pi^2 E_L}
68 Args:
69 el_ghz: Inductive energy in GHz.
71 Returns:
72 Geometric inductance of one arm in Henries.
74 Example:
75 >>> L = el_to_arm_inductance(5.0) # 5 GHz inductive energy
76 >>> print(f"{L * 1e9:.2f} nH")
77 """
78 el_joules = el_ghz * 1e9 * h
79 return Φ_0**2 / (8 * math.pi**2 * el_joules)
82# ---------------------------------------------------------------------------
83# SAX microwave model
84# ---------------------------------------------------------------------------
87def _unimon_internal(
88 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
89 arm_length: float = 3000.0,
90 cross_section: str = "cpw",
91 junction_capacitance: float = 5e-15,
92 junction_inductance: float = -7e-9,
93) -> sax.SDict:
94 r"""Internal SAX S-parameter model for a closed unimon qubit.
96 The unimon is modeled as two shorted quarter-wave CPW transmission lines
97 connected by a parallel LC element representing the Josephson junction
98 (linearised RCSJ model).
100 .. svgbob::
102 o1 ── λ/4 TL ──┬──L_J──┬── λ/4 TL ── o2
103 │ │
104 └──C_J──┘
105 (junction)
107 The two ports ``o1`` and ``o2`` correspond to the nodes on either side of
108 the Josephson junction. The other ends of the transmission lines are
109 shorted to ground.
111 Args:
112 f: Array of frequency points in Hz.
113 arm_length: Length of each quarter-wave resonator arm in µm.
114 cross_section: Cross-section specification for the CPW arms.
115 junction_capacitance: Junction capacitance :math:`C_J` in Farads.
116 junction_inductance: Junction inductance :math:`L_J` in Henries.
118 Returns:
119 sax.SDict: S-parameters dictionary with internal ports ``o1`` and ``o2``.
120 """
121 f = jnp.asarray(f)
123 # Two quarter-wave shorted transmission-line arms
124 arm_left = straight_shorted(f=f, length=arm_length, cross_section=cross_section)
125 arm_right = straight_shorted(f=f, length=arm_length, cross_section=cross_section)
127 # Josephson junction as parallel LC
128 junction = lc_resonator(
129 f=f,
130 capacitance=junction_capacitance,
131 inductance=junction_inductance,
132 grounded=False,
133 )
135 # Tee junctions to connect junction in between arms
136 tee_l = tee(f=f)
137 tee_r = tee(f=f)
139 # Terminal shorts for the shorted ends of the arms
140 short_l = electrical_short(f=f, n_ports=2)
141 short_r = electrical_short(f=f, n_ports=2)
143 instances: dict[str, sax.SType] = {
144 "arm_left": arm_left,
145 "arm_right": arm_right,
146 "junction": junction,
147 "tee_l": tee_l,
148 "tee_r": tee_r,
149 "short_l": short_l,
150 "short_r": short_r,
151 }
153 connections = {
154 # Left arm connects to left tee
155 "arm_left,o1": "tee_l,o1",
156 # Shorted end of left arm
157 "arm_left,o2": "short_l,o1",
158 # Right arm connects to right tee
159 "arm_right,o1": "tee_r,o1",
160 # Shorted end of right arm
161 "arm_right,o2": "short_r,o1",
162 # Junction between tees
163 "tee_l,o2": "junction,o1",
164 "junction,o2": "tee_r,o2",
165 }
167 ports = {
168 "o1": "tee_l,o3",
169 "o2": "tee_r,o3",
170 }
172 return sax.evaluate_circuit_fg((connections, ports), instances)
175def unimon_coupled(
176 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
177 arm_length: float = 3000.0,
178 cross_section: str = "cpw",
179 junction_capacitance: float = 5e-15,
180 junction_inductance: float = -7e-9,
181 coupling_capacitance: float = 10e-15,
182) -> sax.SDict:
183 r"""SAX model for a capacitively coupled unimon qubit.
185 The unimon is modeled as a closed system of two shorted quarter-wave CPW
186 resonators connected by a Josephson junction. Interaction with the qubit
187 is provided via a single coupling capacitor connected to one of the arms.
189 .. svgbob::
191 o1 ── Cc ──┬── λ/4 TL ── (ground)
192 │
193 Junction
194 │
195 └── λ/4 TL ── (ground)
197 Args:
198 f: Array of frequency points in Hz.
199 arm_length: Length of each quarter-wave resonator arm in µm.
200 cross_section: Cross-section specification for the CPW arms.
201 junction_capacitance: Junction capacitance :math:`C_J` in Farads.
202 junction_inductance: Junction inductance :math:`L_J` in Henries.
203 coupling_capacitance: Coupling capacitance :math:`C_c` in Farads.
205 Returns:
206 sax.SDict: S-parameters dictionary with a single port ``o1``.
207 """
208 f = jnp.asarray(f)
210 instances: dict[str, sax.SType] = {
211 "unimon": _unimon_internal(
212 f=f,
213 arm_length=arm_length,
214 cross_section=cross_section,
215 junction_capacitance=junction_capacitance,
216 junction_inductance=junction_inductance,
217 ),
218 "coupling_cap": capacitor(f=f, capacitance=coupling_capacitance),
219 }
221 connections = {
222 "coupling_cap,o2": "unimon,o1",
223 }
225 ports = {
226 "o1": "coupling_cap,o1",
227 }
229 return sax.evaluate_circuit_fg((connections, ports), instances)
232# ---------------------------------------------------------------------------
233# Quantum Hamiltonian model
234# ---------------------------------------------------------------------------
237def unimon_hamiltonian(
238 ec_ghz: float = 1.0,
239 el_ghz: float = 5.0,
240 ej_ghz: float = 10.0,
241 phi_ext: float = jnp.pi,
242 n_max: int = 30,
243) -> jax.Array:
244 r"""Build the unimon Hamiltonian matrix in the charge basis.
246 The effective single-mode unimon Hamiltonian is
248 .. math::
250 \hat{H} = 4 E_C \hat{n}^2
251 + \tfrac{1}{2} E_L (\hat{\varphi} - \varphi_{\text{ext}})^2
252 - E_J \cos\hat{\varphi}
254 **Energy Scales and Physics**:
255 - :math:`E_J`: The Josephson energy of the single non-linear junction.
256 - :math:`E_C`: The effective charging energy. The large geometric capacitance
257 of the CPW resonator means this is lower than typical transmons.
258 - :math:`E_L`: The inductive energy provided by the CPW resonator shunting
259 the junction. The unimon operates in the regime :math:`E_L \sim E_J`.
261 **Similarities to Fluxonium**:
262 The effective single-mode circuit model of the unimon is mathematically identical
263 to a fluxonium qubit. The inductive shunt (provided by the CPW resonators)
264 guarantees there are no isolated superconducting islands, making the unimon
265 intrinsically immune to dephasing caused by low-frequency :math:`1/f` charge noise.
267 **Phase and Charge Operators**:
268 Because the unimon is inductively shunted, its potential energy features a
269 quadratic term :math:`\tfrac{1}{2} E_L \hat{\varphi}^2`, meaning the potential
270 is **not** :math:`2\pi`-periodic. Thus, the phase operator :math:`\hat{\varphi}`
271 must be treated as an extended, non-compact variable. Consequently, the
272 conjugate charge variable :math:`n` is continuous, rather than taking discrete
273 integer values as it does for a transmon.
275 *Note on this implementation*: To evaluate the Hamiltonian, we use a standard
276 truncated discrete charge basis approximation
277 (:math:`n \in \{-n_{\max}, \ldots, +n_{\max}\}`). The charge operator is derived
278 from `qutip.charge`, and the non-compact phase space operators are approximated
279 as nearest-neighbor hopping matrices.
281 See :cite:`hyyppaUnimonQubit2022` and :cite:`tuohinoMultimodePhysicsUnimon2024`.
283 Args:
284 ec_ghz: Charging energy :math:`E_C` in GHz.
285 el_ghz: Inductive energy :math:`E_L` in GHz.
286 ej_ghz: Josephson energy :math:`E_J` in GHz.
287 phi_ext: External flux bias :math:`\varphi_{\text{ext}}`
288 in radians (reduced flux-quantum units).
289 The optimal operating point is :math:`\pi`.
290 n_max: Charge-basis truncation; Hilbert-space dimension is
291 :math:`2 n_{\max} + 1`.
293 Returns:
294 A ``(2*n_max+1, 2*n_max+1)`` Hermitian matrix (in GHz).
296 Raises:
297 ImportError: If the QuTiP extra is not installed.
298 ModuleNotFoundError: If a dependency other than QuTiP fails to import.
299 """
300 n_states = 2 * n_max + 1
302 try:
303 import qutip # noqa: PLC0415
304 import qutip_jax # noqa: PLC0415
305 except ModuleNotFoundError as e:
306 # Only treat missing top-level qutip/qutip_jax as an extra dependency issue.
307 # Re-raise any other import-time errors (including nested ones) unchanged.
308 if e.name in {"qutip", "qutip_jax"}:
309 raise ImportError(
310 "The QuTiP extra is required for Hamiltonian models. "
311 "Please install it with `pip install qpdk[qutip]`."
312 ) from e
313 raise
315 # Extract raw JAX arrays from qutip-jax
316 n_hat = qutip.charge(n_max).to(qutip_jax.JaxArray).data._jxa
318 ones = jnp.ones(n_states - 1)
319 phi_hat = qutip.qdiags([ones, ones], [1, -1]).to(qutip_jax.JaxArray).data._jxa
320 cos_phi = (qutip.qdiags([ones, ones], [1, -1]) / 2).to(qutip_jax.JaxArray).data._jxa
322 # H = 4 E_C n^2 + 0.5 E_L (phi - phi_ext)^2 - E_J cos(phi)
323 # Expand the quadratic: (phi - phi_ext)^2 = phi^2 - 2*phi_ext*phi + phi_ext^2
324 return (
325 4 * ec_ghz * n_hat @ n_hat
326 + 0.5
327 * el_ghz
328 * (phi_hat @ phi_hat - 2 * phi_ext * phi_hat + phi_ext**2 * jnp.eye(n_states))
329 - ej_ghz * cos_phi
330 )
333def unimon_energies(
334 ec_ghz: float = 1.0,
335 el_ghz: float = 5.0,
336 ej_ghz: float = 10.0,
337 phi_ext: float = jnp.pi,
338 n_max: int = 30,
339 n_levels: int = 5,
340) -> jax.Array:
341 r"""Compute the lowest energy eigenvalues of the unimon Hamiltonian.
343 Diagonalises the Hamiltonian returned by :func:`unimon_hamiltonian` and
344 returns the ``n_levels`` lowest eigenvalues, shifted so that the ground
345 state energy is zero.
347 Args:
348 ec_ghz: Charging energy :math:`E_C` in GHz.
349 el_ghz: Inductive energy :math:`E_L` in GHz.
350 ej_ghz: Josephson energy :math:`E_J` in GHz.
351 phi_ext: External flux :math:`\varphi_{\text{ext}}` in radians.
352 n_max: Charge-basis truncation.
353 n_levels: Number of lowest energy levels to return.
355 Returns:
356 Array of shape ``(n_levels,)`` with energies in GHz,
357 referenced to the ground state.
359 Example:
360 >>> energies = unimon_energies(ec_ghz=1.0, el_ghz=5.0, ej_ghz=10.0)
361 >>> f_01 = float(energies[1]) # qubit frequency in GHz
362 >>> alpha = float(energies[2] - 2 * energies[1]) # anharmonicity
363 """
364 H = unimon_hamiltonian(
365 ec_ghz=ec_ghz,
366 el_ghz=el_ghz,
367 ej_ghz=ej_ghz,
368 phi_ext=phi_ext,
369 n_max=n_max,
370 )
371 eigenvalues = jnp.linalg.eigvalsh(H)
372 # Shift to ground state = 0
373 eigenvalues = eigenvalues - eigenvalues[0]
374 return eigenvalues[:n_levels]
377def unimon_frequency_and_anharmonicity(
378 ec_ghz: float = 1.0,
379 el_ghz: float = 5.0,
380 ej_ghz: float = 10.0,
381 phi_ext: float = jnp.pi,
382 n_max: int = 30,
383) -> tuple[float, float]:
384 r"""Compute the qubit transition frequency and anharmonicity of the unimon.
386 The qubit frequency is
388 .. math::
390 f_{01} = E_1 - E_0
392 and the anharmonicity is
394 .. math::
396 \alpha = (E_2 - E_1) - (E_1 - E_0) = E_2 - 2 E_1
398 (since :math:`E_0 = 0` after shifting).
400 Args:
401 ec_ghz: Charging energy :math:`E_C` in GHz.
402 el_ghz: Inductive energy :math:`E_L` in GHz.
403 ej_ghz: Josephson energy :math:`E_J` in GHz.
404 phi_ext: External flux :math:`\varphi_{\text{ext}}` in radians.
405 n_max: Charge-basis truncation.
407 Returns:
408 Tuple of (frequency_ghz, anharmonicity_ghz).
410 Example:
411 >>> f01, alpha = unimon_frequency_and_anharmonicity(
412 ... ec_ghz=1.0, el_ghz=5.0, ej_ghz=10.0
413 ... )
414 >>> print(f"f_01 = {f01:.3f} GHz, alpha = {alpha:.3f} GHz")
415 """
416 energies = unimon_energies(
417 ec_ghz=ec_ghz,
418 el_ghz=el_ghz,
419 ej_ghz=ej_ghz,
420 phi_ext=phi_ext,
421 n_max=n_max,
422 n_levels=3,
423 )
424 f_01 = float(energies[1])
425 alpha = float(energies[2] - 2 * energies[1])
426 return f_01, alpha