Source code for quchip.analysis.effective_hamiltonian

"""Static-ZZ pathways and des-Cloizeaux effective Hamiltonians.

:func:`analyze_static_zz` reports the exact dressed ZZ and a second-order
Schrieffer-Wolff attribution to virtual transitions. The pathway estimates are
diagnostics, and the reported ZZ comes from the dressed spectrum.

:func:`effective_hamiltonian` returns a dense GHz matrix on a selected
computational subspace, whose eigenvalues equal its labeled dressed energies.
Differentiation requires backend support for the eigensolver and a fixed
assignment.

References: Bravyi, DiVincenzo & Loss, *Schrieffer-Wolff transformation for
quantum many-body systems*, Ann. Phys. 326, 2793 (2011); Blais et al.,
*Circuit quantum electrodynamics*, RMP 93, 025005 (2021), §IV.C.
"""

from __future__ import annotations

import itertools
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Mapping, Sequence

import numpy as np

from quchip.chip.sw import bare_hamiltonian, pathway_attribution, sylvester_generator
from quchip.devices.base import BaseDevice
from quchip.utils.jax_utils import contains_tracer, maybe_concrete_scalar

if TYPE_CHECKING:
    from quchip.chip.chip import Chip


# ---------------------------------------------------------------------------
# analyze_static_zz
# ---------------------------------------------------------------------------


def _computational_p_mask(dims: tuple[int, ...], idx_a: int, idx_b: int) -> np.ndarray:
    """Boolean mask over the product basis selecting the four computational states of ``(a, b)``.

    ``P`` is ``{|0,0>, |0,1>, |1,0>, |1,1>}`` of the ``(a, b)`` pair with
    every other device grounded; ``Q`` is everything else. This is the
    partition :func:`~quchip.chip.sw.sylvester_generator` needs to attribute
    the ZZ-carrying ``(1_a, 1_b)`` diagonal element — unlike
    :func:`~quchip.chip.sw.mode_blocks`, no single mode is eliminated here.
    """
    occupations = np.indices(dims).reshape(len(dims), -1)
    spectators_grounded = np.ones(occupations.shape[1], dtype=bool)
    for i in range(len(dims)):
        if i not in (idx_a, idx_b):
            spectators_grounded &= occupations[i] == 0
    computational = (occupations[idx_a] <= 1) & (occupations[idx_b] <= 1)
    return spectators_grounded & computational


def _bare_index_pair(dims: tuple[int, ...], idx_a: int, idx_b: int, level_a: int, level_b: int) -> int:
    """Full product-basis index for ``(a, b)`` at the given levels, spectators grounded."""
    occ = [0] * len(dims)
    occ[idx_a] = level_a
    occ[idx_b] = level_b
    return int(np.ravel_multi_index(tuple(occ), dims))


[docs] @dataclass(frozen=True) class StaticZZResult: """Store the exact static ZZ between two devices and its 2nd-order SW pathway attribution. Attributes ---------- zz ``E(1,1) - E(1,0) - E(0,1) + E(0,0)``, identical to :meth:`~quchip.chip.chip.Chip.dispersive_shift(device_a, device_b)`. This value is exact, not perturbative. pathways ``(bare_occupation, amount)`` pairs. Each pair gives one virtual intermediate state's contribution to the 2nd-order SW correction of the ``(1_a, 1_b)`` diagonal matrix element. The contribution is ``amount = 1/2 * V_ik*V_ki*(1/(E_i - E_k) + 1/(E_i - E_k))``, where ``i`` is the ``(1_a, 1_b)`` bare index. This decomposes that one energy correction, not ``zz`` itself, which combines four dressed energies exactly. ``bare_occupation`` is a full chip-length Fock tuple, in device order. device_a, device_b Resolved device labels. device_labels Chip device labels in tensor-product order, for reading the occupation tuples of ``pathways``. Amounts stay in the array namespace of the chip's parameters (JAX in, JAX out) — traceable and differentiable. The concrete path lists every nonzero contribution, so the amounts sum to the second-order correction (:func:`~quchip.chip.sw.pathway_attribution`). """ zz: Any pathways: list[tuple[tuple[int, ...], Any]] device_a: str device_b: str device_labels: tuple[str, ...]
[docs] def describe(self) -> str: """Print and return the exact ZZ and the leading virtual pathways. Concrete values only. Call it outside ``jax.jit``/``grad`` regions. Traced amounts show as ``<traced>``. """ zz_value = maybe_concrete_scalar(self.zz) zz_text = f"{zz_value * 1e3:+.4g} MHz" if zz_value is not None else "<traced>" lines = [f"Static ZZ({self.device_a}, {self.device_b}) = {zz_text}"] lines.append(" leading virtual pathways (2nd-order SW correction to E(1,1)):") if contains_tracer([amount for _, amount in self.pathways]): lines.append(" <traced>") else: # Each amount is |V_ik|^2 * (...) for the diagonal element attributed here — real-valued, # but carried in the complex dtype of the bare Hamiltonian. ranked = sorted(self.pathways, key=lambda kv: -abs(complex(kv[1]).real)) for occupation, amount in ranked[:5]: ket = ",".join(f"{lab}={n}" for lab, n in zip(self.device_labels, occupation)) lines.append(f" |{ket}>: {complex(amount).real * 1e3:+.4g} MHz") text = "\n".join(lines) print(text) return text
[docs] def analyze_static_zz(chip: "Chip", device_a: str | BaseDevice, device_b: str | BaseDevice) -> StaticZZResult: """Calculate the exact static ZZ between two devices and its 2nd-order SW pathway attribution. ``zz`` is :meth:`~quchip.chip.chip.Chip.dispersive_shift`, unchanged: the exact, all-orders residual coupling. ``pathways`` decomposes the 2nd-order SW correction to the ``(1_a, 1_b)`` diagonal energy into virtual intermediate-state contributions (:func:`~quchip.chip.sw.pathway_attribution`). The pathways come from a partition that keeps only the four computational states of ``(a, b)``, with all other devices grounded. This is the natural loss primitive for a calibration sweep that holds ``zz`` near zero while a different exchange, e.g. a :func:`~quchip.chip.transformations.eliminate`-mediated ``J``, stays on target. Parameters ---------- chip : Chip Chip whose dressed spectrum and couplings define the static model. device_a, device_b : str or BaseDevice The two devices to analyze. Returns ------- StaticZZResult Examples -------- >>> from quchip import Capacitive, Chip, DuffingTransmon >>> from quchip.analysis import analyze_static_zz >>> q0 = DuffingTransmon(freq=5.0, anharmonicity=-0.25, levels=3, label="q0") >>> q1 = DuffingTransmon(freq=5.2, anharmonicity=-0.24, levels=3, label="q1") >>> chip = Chip([q0, q1], couplings=[Capacitive(q0, q1, g=0.01)]) >>> result = analyze_static_zz(chip, "q0", "q1") >>> zz = result.zz # exact residual ZZ, GHz """ zz = chip.dispersive_shift(device_a, device_b) idx_a, dev_a = chip._resolve_device_index(device_a) idx_b, dev_b = chip._resolve_device_index(device_b) device_labels = tuple(dev.label for dev in chip.devices) h, _, dims = bare_hamiltonian(chip) p_mask = _computational_p_mask(dims, idx_a, idx_b) s, _ = sylvester_generator(h, p_mask) i_idx = _bare_index_pair(dims, idx_a, idx_b, 1, 1) raw_pathways = pathway_attribution(h, s, p_mask, i_idx, i_idx) pathways = [(tuple(int(x) for x in np.unravel_index(k, dims)), amount) for k, amount in raw_pathways] return StaticZZResult( zz=zz, pathways=pathways, device_a=dev_a.label, device_b=dev_b.label, device_labels=device_labels, )
# --------------------------------------------------------------------------- # effective_hamiltonian # --------------------------------------------------------------------------- def _normalize_subspace( chip: "Chip", subspace: Mapping[str | BaseDevice, int] | Sequence[str | BaseDevice], ) -> list[tuple[int, ...]]: """Full chip-length bare-occupation tuples spanning *subspace*. A ``{device: levels}`` mapping keeps ``range(levels)`` of each named device; a bare sequence of devices keeps each one's full qubit (Fock 0/1) subspace. Devices not named are grounded (Fock 0). Both forms accept a device label string or the object itself. """ n = len(chip.devices) per_device_range: dict[int, range] = {} if isinstance(subspace, Mapping): for device_key, levels in subspace.items(): idx, _ = chip._resolve_device_index(device_key) per_device_range[idx] = range(int(levels)) else: for device_key in subspace: idx, _ = chip._resolve_device_index(device_key) per_device_range[idx] = range(2) ranges = [per_device_range.get(i, range(1)) for i in range(n)] return list(itertools.product(*ranges))
[docs] @dataclass(frozen=True) class EffectiveHamiltonianResult: """Store the des-Cloizeaux effective Hamiltonian on a labeled computational subspace. The function builds ``h_eff`` as ``S^-1/2 (W E W^dagger) S^-1/2``. Here ``W`` is the overlap block between the requested bare states and their assigned dressed states, and ``E`` is the labeled dressed energies. ``S = W W^dagger`` is the overlap Gram matrix, which in general is not orthonormal. Exact mode reductions use the same symmetric (Löwdin) orthonormalization. ``S^-1/2 W`` is unitary by construction, so ``h_eff`` is unitarily similar to ``diag(E)``. Its eigenvalues are therefore exactly the labeled dressed energies, to numerical precision, for any hybridization strength between the kept states and the rest of the chip. The off-diagonal entries hold the effective couplings between kept states. When couplings mix the kept states, a diagonal entry in general does not equal one specific dressed energy. Attributes ---------- h_eff Dense Hermitian matrix, GHz, ordered as :attr:`basis`. basis Full chip-length bare-occupation tuples that span the subspace, in the row/column order of :attr:`h_eff`. device_labels Chip device labels in tensor-product order, for reading the :attr:`basis` tuples. """ h_eff: Any basis: tuple[tuple[int, ...], ...] device_labels: tuple[str, ...]
[docs] def describe(self) -> str: """Print and return the matrix with labeled row/column kets. Concrete values only. Call it outside ``jax.jit``/``grad`` regions. A traced matrix shows as ``<traced>``. """ lines = ["Effective Hamiltonian (GHz):"] for i, occupation in enumerate(self.basis): ket = ",".join(f"{lab}={n}" for lab, n in zip(self.device_labels, occupation)) lines.append(f" [{i}] |{ket}>") if contains_tracer(self.h_eff): lines.append(" <traced>") else: matrix = np.asarray(self.h_eff) for row in matrix: lines.append(" " + " ".join(f"{v.real:+.4f}{v.imag:+.4f}j" for v in row)) text = "\n".join(lines) print(text) return text
[docs] def effective_hamiltonian( chip: "Chip", subspace: Mapping[str | BaseDevice, int] | Sequence[str | BaseDevice], ) -> EffectiveHamiltonianResult: """Calculate the des-Cloizeaux effective Hamiltonian on a user-selected computational subspace. See :class:`EffectiveHamiltonianResult` for the construction and its exactness guarantee. Parameters ---------- chip : Chip Chip whose dressed spectrum defines the effective subspace. subspace : mapping or sequence ``{device: levels}`` keeps ``range(levels)`` of each named device. A bare sequence of devices keeps each device's full qubit (Fock 0/1) subspace. In both forms spectators are grounded, and a device can be a label string or the device object. Returns ------- EffectiveHamiltonianResult Examples -------- >>> from quchip import Capacitive, Chip, DuffingTransmon >>> from quchip.analysis import effective_hamiltonian >>> q0 = DuffingTransmon(freq=5.0, anharmonicity=-0.25, levels=3, label="q0") >>> q1 = DuffingTransmon(freq=5.2, anharmonicity=-0.24, levels=3, label="q1") >>> chip = Chip([q0, q1], couplings=[Capacitive(q0, q1, g=0.01)]) >>> result = effective_hamiltonian(chip, ["q0", "q1"]) >>> result.h_eff.shape (4, 4) """ # effective_hamiltonian reuses the chip's dressed spectrum instead of # diagonalizing again, so one eigensystem per sector, or one full-chip # eigensystem, supplies both this function and `dispersive_shift`. device_labels = tuple(dev.label for dev in chip.devices) basis = _normalize_subspace(chip, subspace) h_eff = chip._analysis.effective_hamiltonian_on_basis(basis) return EffectiveHamiltonianResult(h_eff=h_eff, basis=tuple(basis), device_labels=device_labels)
[docs] def effective_hamiltonian_between_states( chip: "Chip", state_a: tuple[int, ...], state_b: tuple[int, ...] ) -> Any: r"""Calculate the 2x2 Löwdin-orthonormalized effective Hamiltonian between two explicit bare states. This function uses the des-Cloizeaux construction of :func:`effective_hamiltonian` (see :class:`EffectiveHamiltonianResult`), but on exactly the two-state subspace that *state_a* and *state_b* span. It does not use the four-state product subspace that a ``["a", "b"]`` bare-sequence spec to :func:`effective_hamiltonian` builds from each device's full qubit subspace. It is the natural primitive for a static exchange rate between the single-excitation bare states :math:`|1_a, 0_b\rangle` and :math:`|0_a, 1_b\rangle`. The returned matrix's off-diagonal entry is that exchange rate, in GHz. Parameters ---------- chip : Chip Chip whose dressed spectrum defines the effective Hamiltonian. state_a, state_b : tuple[int, ...] Full chip-length bare-occupation tuples (one entry for each device, in :attr:`~quchip.chip.chip.Chip.devices` order). Returns ------- Any ``(2, 2)`` Hermitian matrix, GHz, in the array namespace of the chip's backend. Its eigenvalues are exactly the labeled dressed energies of *state_a* and *state_b*, to numerical precision. Examples -------- >>> from quchip import Capacitive, Chip, DuffingTransmon >>> from quchip.analysis import effective_hamiltonian_between_states >>> q0 = DuffingTransmon(freq=5.0, anharmonicity=-0.25, levels=3, label="q0") >>> q1 = DuffingTransmon(freq=5.2, anharmonicity=-0.24, levels=3, label="q1") >>> chip = Chip([q0, q1], couplings=[Capacitive(q0, q1, g=0.01)]) >>> h_eff = effective_hamiltonian_between_states(chip, (1, 0), (0, 1)) >>> h_eff.shape (2, 2) """ return chip._analysis.effective_hamiltonian_on_basis((state_a, state_b))