Source code for pyEPR.calcs.hamiltonian
"""
Hamiltonian and Matrix Operations.
Hamiltonian operations heavily draw on qutip package.
This package must be installed for them to work.
"""
try:
import qutip
from qutip import Qobj # basis, tensor,
except (ImportError, ModuleNotFoundError):
Qobj = None
pass
from ..toolbox.pythonic import fact
[docs]
class MatrixOps(object):
[docs]
@staticmethod
def cos(op_cos_arg: Qobj):
"""Exact cosine operator via matrix exponential: (e^{iφ} + e^{-iφ}) / 2.
Parameters
----------
op_cos_arg : qutip.Qobj
Phase operator φ (Hermitian).
Returns
-------
qutip.Qobj
cos(φ) as a matrix in Fock space.
"""
return 0.5 * ((1j * op_cos_arg).expm() + (-1j * op_cos_arg).expm())
[docs]
@staticmethod
def cos_full_correction(op_cos_arg: Qobj):
"""Exact EPR nonlinear correction: cos(φ) - I + φ²/2 (no truncation).
This is the infinite-order equivalent of :func:`cos_approx`. In the
EPR Hamiltonian the linear eigenfrequencies already account for the
harmonic (φ²/2) Josephson energy, so the nonlinear potential that must
be subtracted is the remainder:
.. math::
H_{\\mathrm{nl}} = -E_J \\bigl[\\cos(\\varphi) - 1 + \\tfrac{\\varphi^2}{2}\\bigr]
= -E_J \\sum_{n\\ge 2} \\frac{(-1)^n \\varphi^{2n}}{(2n)!}
For weakly anharmonic circuits (transmon, φ_zpf ≲ 0.3) the truncated
:func:`cos_approx` is equivalent and faster. For strongly anharmonic
circuits (fluxonium, φ_zpf ≳ 1) use this function to avoid systematic
errors from the truncated series.
Parameters
----------
op_cos_arg : qutip.Qobj
Phase operator φ (Hermitian), in the full Fock-space tensor product.
Returns
-------
qutip.Qobj
cos(φ) - I + φ²/2 as a matrix.
See Also
--------
cos_approx : Truncated Taylor-series version (faster for small φ_zpf).
References
----------
arXiv:2411.15039 — EPR analysis for very anharmonic superconducting circuits.
"""
import qutip
cos_phi = MatrixOps.cos(op_cos_arg)
# Build identity in the same tensor-product Hilbert space as op_cos_arg
I = qutip.qeye(op_cos_arg.dims[0])
return cos_phi - I + op_cos_arg**2 / 2
[docs]
@staticmethod
def apply_scalar_function(op: Qobj, func) -> Qobj:
"""Evaluate a real scalar function on a Hermitian operator via eigendecomposition.
For a Hermitian operator H with real eigenvalues λ_i and eigenvectors :math:`|i\\rangle`:
.. math::
f(H) = \\sum_i f(\\lambda_i) |i\\rangle\\langle i|
This lets you evaluate *any* analytic scalar function—not just polynomials or
exponentials—as an operator, which is needed for generic junction potentials.
Parameters
----------
op : qutip.Qobj
Hermitian operator (e.g., the phase operator φ in Fock space).
func : callable
Real scalar function ``func(float) -> float``.
Returns
-------
qutip.Qobj
``func(op)`` in the same Hilbert space.
Examples
--------
>>> import qutip, numpy as np
>>> from pyEPR.calcs.hamiltonian import MatrixOps
>>> a = qutip.destroy(8)
>>> phi = 0.3 * (a + a.dag())
>>> cos_phi = MatrixOps.apply_scalar_function(phi, np.cos)
"""
import numpy as np
import qutip
mat = op.full()
evals, evecs = np.linalg.eigh(mat) # Hermitian → real eigenvalues
f_evals = np.vectorize(func)(evals)
result = (evecs * f_evals) @ evecs.conj().T # V @ diag(f) @ V†
return qutip.Qobj(result, dims=op.dims)
[docs]
@staticmethod
def cos_approx(x, cos_trunc=5):
"""
Create a Taylor series matrix approximation of the cosine, up to some order.
"""
return sum(
(-1) ** i * x ** (2 * i) / float(fact(2 * i))
for i in range(2, cos_trunc + 1)
)
[docs]
@staticmethod
def dot(ais, bis):
"""
Dot product
"""
return sum(ai * bi for ai, bi in zip(ais, bis))
[docs]
class HamOps(object):
[docs]
@staticmethod
def fock_state_on(d: dict, fock_trunc: int, N_modes: int):
"""d={mode number: # of photons} In the bare eigen basis"""
# give me the value d[i] or 0 if d[i] does not exist
return qutip.tensor(
*[qutip.basis(fock_trunc, d.get(i, 0)) for i in range(N_modes)]
)
[docs]
@staticmethod
def closest_state_to(s: Qobj, energyMHz, evecs):
"""
Returns the energy of the closest state to s
"""
def distance(s2):
inner = s.dag() * s2[1]
# qutip 4 returns a 1x1 Qobj (.norm()); qutip 5 returns a complex scalar (abs())
return inner.norm() if hasattr(inner, "norm") else abs(inner)
return max(zip(energyMHz, evecs), key=distance)
[docs]
@staticmethod
def closest_state_to_idx(s: Qobj, evecs):
"""
Returns the index
"""
def distance(s2):
inner = s.dag() * s2[1]
# qutip 4 returns a 1x1 Qobj (.norm()); qutip 5 returns a complex scalar (abs())
return inner.norm() if hasattr(inner, "norm") else abs(inner)
return max(zip(range(len(evecs)), evecs), key=distance)
[docs]
@staticmethod
def identify_Fock_levels(fock_trunc: int, evecs, N_modes=2, Fock_max=4):
"""
Return quantum numbers in terms of the undiagonalized eigenbasis.
"""
# to do: need to turn Fock_max into arb algo on each mode
def fock_state_on(d):
return HamOps.fock_state_on(d, fock_trunc, N_modes)
def closest_state_to_idx(s):
return HamOps.closest_state_to_idx(s, evecs)
FOCKr = {}
for d1 in range(Fock_max):
for d2 in range(Fock_max):
d = {0: d1, 1: d2}
FOCKr[closest_state_to_idx(fock_state_on(d))[0]] = d
return FOCKr