"""Assign dressed eigenvectors to bare or eigenstate references, and build dressed results.
:func:`label_eigensystem` supports greedy, global-greedy, and continuation
assignments. :class:`BareProductReference` and :class:`EigenstateReference`
supply vectors and labels, and :func:`compute_overlaps` computes their
overlaps. The assignment kernels are inspired by SuperGrad's
``compute_energy_map``. :class:`DressedResult` is the eager, label-keyed view
of one dressed eigensystem that ``Chip.dress()`` and ``EngineResult.dress()``
return.
"""
from __future__ import annotations
import itertools
import warnings
from dataclasses import dataclass
from functools import partial
from typing import TYPE_CHECKING, Any, Callable, Sequence
import jax
import jax.numpy as jnp
import numpy as np
from jax import lax
from quchip.backend import EigensystemData
from quchip.utils.jax_utils import concrete_array_module, contains_tracer
if TYPE_CHECKING:
from quchip.engine.ir import EngineResult
[docs]
@dataclass(frozen=True)
class Labeling:
"""A labeled assignment of dressed eigenstates to user-meaningful keys.
The dataclass has only array fields and no embedded :class:`Reference`
object, so it is a clean pytree under :func:`jax.jit`, :func:`jax.vmap`,
and :func:`jax.grad`.
``indices`` is the integer assignment (non-differentiable, conceptually
``stop_gradient``'d). ``overlaps`` and ``margins`` are differentiable.
Energies indexed *via* ``indices`` flow gradients through the gathered
values, so ``eigvals[labeling.indices[k]]`` is differentiable with respect
to parameters that affect ``eigvals``.
"""
keys: tuple[Any, ...]
indices: jnp.ndarray
overlaps: jnp.ndarray
margins: jnp.ndarray
duplicates: jnp.ndarray
[docs]
@dataclass(frozen=True)
class LabelingPath:
"""Stacked labelings along a parameter path.
Compared with :class:`Labeling`, each array field has a leading ``n_steps``
axis. A ``swap_events`` array shows where labels changed dressed-index
between adjacent steps. A swap alone is only a reassignment. To distinguish
avoided crossings from routine mode mixing, combine a swap with a margin
collapse (``margins`` near zero) at the same step.
"""
keys: tuple[Any, ...]
indices: jnp.ndarray # (n_steps, n_labels) int
overlaps: jnp.ndarray # (n_steps, n_labels) float
margins: jnp.ndarray # (n_steps, n_labels) float
duplicates: jnp.ndarray # (n_steps, n_labels) bool
swap_events: jnp.ndarray # (n_steps - 1, n_labels) bool
@property
def final(self) -> Labeling:
"""The labeling at the end of the path."""
return Labeling(
keys=self.keys,
indices=self.indices[-1],
overlaps=self.overlaps[-1],
margins=self.margins[-1],
duplicates=self.duplicates[-1],
)
[docs]
@dataclass(frozen=True)
class BareProductReference:
"""Bare product basis states in Kronecker order.
Optional local vectors map each component's energy states into its solver
basis, and None denotes an identity factor. The full product matrix is
never materialized; identity references reduce directly to ``|evecs|**2``.
"""
dims: tuple[int, ...]
local_vectors: tuple[Any | None, ...] = ()
def __post_init__(self) -> None:
if self.local_vectors and len(self.local_vectors) != len(self.dims):
raise ValueError("Product references require one local factor per subsystem.")
for dimension, vectors in zip(self.dims, self.local_vectors):
if vectors is not None and vectors.shape != (dimension, dimension):
raise ValueError("Local reference factors must match their subsystem dimensions.")
@property
def keys(self) -> tuple[tuple[int, ...], ...]:
return tuple(itertools.product(*(range(d) for d in self.dims)))
[docs]
@dataclass(frozen=True)
class EigenstateReference:
"""Reference vectors are explicit eigenvectors with attached label keys.
Used for path continuation: the previous step's selected eigvecs become
the next step's reference. ``vectors`` is row-major: each row is one
reference state in the Kronecker basis.
"""
vectors: jnp.ndarray # complex (n_labels, dim)
keys: tuple[Any, ...]
[docs]
def reference_amplitudes(reference: Any, evecs: jnp.ndarray) -> jnp.ndarray:
"""Express solver eigenvectors in the captured reference coordinates.
Concrete inputs combine on the host, where they compile no device programs.
"""
if isinstance(reference, BareProductReference):
xp = concrete_array_module(evecs, reference.local_vectors)
amplitudes = xp.asarray(evecs).reshape(*reference.dims, evecs.shape[-1])
for axis, vectors in enumerate(reference.local_vectors):
if vectors is not None:
amplitudes = xp.tensordot(xp.asarray(vectors).conj().T, amplitudes, axes=(1, axis))
amplitudes = xp.moveaxis(amplitudes, 0, axis)
return amplitudes.reshape(evecs.shape)
xp = concrete_array_module(evecs, reference.vectors)
return xp.asarray(reference.vectors).conj() @ xp.asarray(evecs)
[docs]
def compute_overlaps(reference: Any, evecs: jnp.ndarray) -> jnp.ndarray:
"""``|<ref_k | psi_j>|**2`` matrix, shape ``(n_labels, n_dressed)``."""
amplitudes = reference_amplitudes(reference, evecs)
return concrete_array_module(amplitudes).abs(amplitudes) ** 2
def _top2_margins(overlaps: jnp.ndarray) -> jnp.ndarray:
"""``top1 - top2`` overlap per row — the shared assignment-confidence margin.
Extracts the two largest per-row overlaps via :func:`jax.lax.top_k` and
returns their difference. All three assignment policies report this exact
margin from the *unmasked* overlap matrix, so it is independent of the
assignment order. For ``n_dressed == 1`` there is no runner-up, so the
margin is the sole (max) overlap.
"""
if overlaps.shape[1] > 1:
top2 = lax.top_k(overlaps, 2)[0] # (n_labels, 2), descending
return top2[:, 0] - top2[:, 1]
return overlaps[:, 0]
[docs]
def assign_argmax(
overlaps: jnp.ndarray,
) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
"""One-shot ``argmax`` per row. Allows duplicate dressed-index assignments.
Returns ``(indices, chosen, margins)``. This policy is cheap and
``jit``-clean, and matches SuperGrad's default greedy mode. Use it in a
weak-coupling regime where each bare label has a clearly dominant dressed
eigenstate.
"""
indices = jnp.argmax(overlaps, axis=1)
chosen = jnp.take_along_axis(overlaps, indices[:, None], axis=1).squeeze(-1)
margins = _top2_margins(overlaps)
return indices, chosen, margins
[docs]
def assign_global_greedy(
overlaps: jnp.ndarray,
) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
"""Global descending-overlap greedy with ``lax.scan`` and masking.
At each step, the function assigns the ``(label, dressed_idx)`` pair with
the highest remaining overlap. It then masks out that label's row and that
column, so this pure-JAX implementation assigns each label and dressed
index at most once.
Margins are computed from the *unmasked* overlap matrix, so they describe
per-label confidence independent of assignment order.
Requires ``n_labels <= n_dressed``.
"""
n_labels, n_dressed = overlaps.shape
if n_labels > n_dressed:
raise ValueError(
f"global-greedy requires n_labels ({n_labels}) <= n_dressed ({n_dressed})"
)
# Per-label confidence margin from the unmasked overlap matrix.
margins = _top2_margins(overlaps)
neg_inf = jnp.asarray(-jnp.inf, dtype=overlaps.dtype)
def step(carry, _):
masked, indices, chosens = carry
flat_idx = jnp.argmax(masked.reshape(-1))
label_idx = flat_idx // n_dressed
dressed_idx = flat_idx % n_dressed
chosen = masked[label_idx, dressed_idx]
indices = indices.at[label_idx].set(dressed_idx)
chosens = chosens.at[label_idx].set(chosen)
masked = masked.at[label_idx, :].set(neg_inf)
masked = masked.at[:, dressed_idx].set(neg_inf)
return (masked, indices, chosens), None
# ``argmax`` returns int64 under ``jax_enable_x64`` and int32 otherwise.
# Match the carry to that dtype so the ``.at[].set`` scatter does not
# tear (JAX has begun rejecting silent int64→int32 downcasts).
index_dtype = jnp.argmax(jnp.zeros((1,), dtype=overlaps.dtype)).dtype
init_indices = jnp.zeros((n_labels,), dtype=index_dtype)
init_chosens = jnp.zeros((n_labels,), dtype=overlaps.dtype)
(_, indices, chosens), _ = lax.scan(
step, (overlaps, init_indices, init_chosens), xs=None, length=n_labels
)
return indices, chosens, margins
[docs]
def assign_rowwise_greedy(
overlaps: jnp.ndarray,
) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
"""Confidence-ordered row-greedy assignment — ``O(n_labels * n_dressed)``.
The function visits labels (rows) in descending row-max-overlap order. Each
label takes its highest-overlap *still-available* dressed index, and the
function masks that column, so the result is a permutation. Each scan step is
a single masked-row ``argmax`` over ``n_dressed`` columns, so the whole
assignment is ``O(n_labels * n_dressed)``. For a square ``D = L**k`` overlap
matrix, this is ``O(L**(2k))``. :func:`assign_global_greedy` is ``O(L**(3k))``
(a full ``L**(2k)``-entry ``argmax`` at each of ``L**k`` steps). This is the
SuperGrad / scqubits confidence-ordered greedy variant.
**Semantics vs. global-greedy.** This function is identical to
:func:`assign_global_greedy` in the near-permutation regime (dispersive / weak
hybridization), where each bare label has a clearly dominant dressed
eigenstate. The two can diverge on strongly-hybridized overlap matrices.
Global-greedy always commits the single largest *remaining* entry, whereas
this function visits rows in (original) row-max order. In exactly that regime
the assignment is intrinsically ambiguous, and ``Chip.dress`` already warns
that bare labels are approximate. For global-greedy ordering, pass
``policy=assign_global_greedy`` to :func:`label_eigensystem`. Row-max ties are
broken by ascending index (``argsort``).
Margins (``top1 - top2`` per label) come from the *unmasked* matrix via
``top_k``, so they match the margins of :func:`assign_global_greedy`.
Requires ``n_labels <= n_dressed``.
"""
# Computing margins via top_k avoids a full per-row sort.
n_labels, n_dressed = overlaps.shape
if n_labels > n_dressed:
raise ValueError(
f"rowwise-greedy requires n_labels ({n_labels}) <= n_dressed ({n_dressed})"
)
margins = _top2_margins(overlaps)
# Most-confident labels first. ``argsort`` is stable, so equal row maxima are
# resolved by ascending label index.
order = jnp.argsort(-jnp.max(overlaps, axis=1))
neg_inf = jnp.asarray(-jnp.inf, dtype=overlaps.dtype)
index_dtype = jnp.argmax(jnp.zeros((1,), dtype=overlaps.dtype)).dtype
def step(carry, label_idx):
col_avail, indices, chosens = carry
row = jnp.where(col_avail, overlaps[label_idx], neg_inf)
dressed_idx = jnp.argmax(row)
indices = indices.at[label_idx].set(dressed_idx.astype(index_dtype))
chosens = chosens.at[label_idx].set(overlaps[label_idx, dressed_idx])
col_avail = col_avail.at[dressed_idx].set(False)
return (col_avail, indices, chosens), None
init = (
jnp.ones((n_dressed,), dtype=bool),
jnp.zeros((n_labels,), dtype=index_dtype),
jnp.zeros((n_labels,), dtype=overlaps.dtype),
)
(_, indices, chosens), _ = lax.scan(step, init, xs=order)
return indices, chosens, margins
@partial(jax.jit, static_argnums=1)
def _labeling_arrays(
overlaps: jnp.ndarray, policy: Callable[..., Any]
) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray, jnp.ndarray]:
"""JIT-compiled numeric core: ``overlaps -> (indices, chosen, margins, duplicates)``.
The assignment policy runs a ``lax.scan`` (global-greedy) or sort
(argmax). Without JIT, every primitive of that scan dispatches separately
through JAX, costing ~30 ms for a modest Hilbert space. Wrapping the core
in :func:`jax.jit` fuses the whole scan into a single compiled XLA call
(compiled once per ``(shape, policy)`` and cached for the session),
which is the dominant cost of the eager dressing path
(``Chip.state``/``Chip.freq``/``Chip.dress``).
``jax.jit`` is transparent to ``grad``/``vmap``: under an outer trace
this inlines, so differentiability and batching are unchanged.
``policy`` is a static argument (a function,
hashable by identity), so distinct policies get distinct caches.
"""
indices, chosen_overlaps, margins = policy(overlaps)
n_dressed = overlaps.shape[1]
counts = jnp.bincount(indices, length=n_dressed)
duplicates = counts[indices] > 1
return indices, chosen_overlaps, margins, duplicates
STATE_OVERLAP_WARNING = 0.9
[docs]
def warn_weak_assignment(label: tuple[int, ...], overlap: Any) -> None:
"""Warn when the dressed state assigned to a label keeps less than 0.9 of its bare product.
Every query that selects the dressed state of a label calls this. A traced
overlap does not warn. The warning names the first caller outside quchip.
Parameters
----------
label : tuple of int
Product label of local energy levels.
overlap : scalar
Squared overlap of the assigned dressed state with the bare product.
"""
import os
import sys
from types import FrameType
from quchip.utils.jax_utils import maybe_concrete_scalar
value = maybe_concrete_scalar(overlap)
if value is None or value >= STATE_OVERLAP_WARNING:
return
package = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
frame: FrameType | None = sys._getframe(1)
level = 2
while frame is not None and os.path.abspath(frame.f_code.co_filename).startswith(package + os.sep):
frame, level = frame.f_back, level + 1
warnings.warn(
f"Dressed state label {tuple(label)} has assignment overlap {value:.3f} (< {STATE_OVERLAP_WARNING:.3f}); "
"the label names the assigned dressed eigenstate. Use chip.bare_state(...) for the product state.",
UserWarning,
stacklevel=level,
)
[docs]
def gauge_phases(amplitudes: Any, indices: Any) -> Any:
"""Return the phase factor that makes each column's assigned reference amplitude real and nonnegative.
Parameters
----------
amplitudes : array_like
``(n_labels, n_dressed)`` amplitudes ``<ref_k|psi_j>`` of the dressed columns.
indices : array_like
Column assigned to each reference label.
Notes
-----
The anchor of a column is its amplitude on the label assigned to it. A
one-to-one assignment can give a column whose assigned amplitude is zero.
When that amplitude is not above a floor, the square root of the
floating-point epsilon, and for an unassigned column, the anchor is the
first amplitude within a relative
``1e-6`` of the column's largest magnitude. The factor of a column is
``conj(anchor) / |anchor|``. The rule is piecewise: the phase can jump
where the assigned amplitude crosses the floor or where the fallback
selects another amplitude. A phase cannot fix a rotation, so an exactly
degenerate eigenspace keeps the basis of the eigensolver.
"""
xp = concrete_array_module(amplitudes, indices)
amplitudes = xp.asarray(amplitudes)
columns = amplitudes.shape[-1]
span = xp.arange(columns)
magnitudes = xp.abs(amplitudes)
peak = xp.max(magnitudes, axis=0)
fallback = amplitudes[xp.argmax(magnitudes >= (1.0 - 1e-6) * peak, axis=0), span]
labels = xp.arange(len(indices))
rows: Any
assigned: Any
if xp is np:
rows, assigned = np.zeros(columns, dtype=int), np.zeros(columns, dtype=bool)
rows[np.asarray(indices)] = labels
assigned[np.asarray(indices)] = True
else:
rows = jnp.zeros(columns, dtype=int).at[indices].set(labels)
assigned = jnp.zeros(columns, dtype=bool).at[indices].set(True)
own = amplitudes[rows, span]
floor = float(np.sqrt(np.finfo(magnitudes.dtype).eps))
anchor = xp.where(assigned & (xp.abs(own) > floor), own, fallback)
return xp.conj(anchor) / xp.abs(anchor)
[docs]
def label_eigensystem(
evecs: jnp.ndarray,
reference: Any,
policy: Callable[
[jnp.ndarray], tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]
] = assign_rowwise_greedy,
) -> Labeling:
"""Pure-JAX labeling kernel.
Parameters
----------
evecs : jnp.ndarray
``(dim, n_dressed)`` eigenvectors as columns.
reference : object
Data-only object (:class:`BareProductReference` or
:class:`EigenstateReference`) defining what label keys mean.
policy : callable
Assignment function ``overlaps -> (indices, chosen, margins)``. Default
:func:`assign_rowwise_greedy`, the confidence-ordered row-greedy policy
that ``Chip.dress`` runs (its sole live caller passes it explicitly).
Notes
-----
The caller gets energies as ``evals[labeling.indices[k]]``. ``policy`` is a
static Python callable resolved once at trace time, not a traced argument.
Under ``jax.jit``, close over ``policy`` or pass it as a
``static_argnames`` entry. Never pass it as a dynamic (traced) argument.
"""
overlaps = compute_overlaps(reference, evecs)
arrays = _labeling_arrays(overlaps, policy)
if not contains_tracer(arrays):
# Host copies of a concrete labeling: looking labels up compiles nothing.
arrays = tuple(np.asarray(array) for array in arrays)
indices, chosen_overlaps, margins, duplicates = arrays
return Labeling(
keys=tuple(reference.keys),
indices=indices,
overlaps=chosen_overlaps,
margins=margins,
duplicates=duplicates,
)
[docs]
def track_path(
evals_path: jnp.ndarray,
evecs_path: jnp.ndarray,
initial_reference: Any,
policy: Callable[
[jnp.ndarray], tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]
] = assign_global_greedy,
) -> LabelingPath:
"""Propagate a labeling along a parameter path of stacked eigensystems.
Step 0 is labeled against ``initial_reference``. At each later step, the
reference becomes the *previous step's selected eigvecs* (an
:class:`EigenstateReference`). This continuation method accepts a stacked
eigvec tensor (usually the output of ``vmap(eigh)`` over a parameter grid),
so it composes with any JAX-side parameter sweep.
Parameters
----------
evals_path : jnp.ndarray
``(n_steps, n_dressed)``.
evecs_path : jnp.ndarray
``(n_steps, dim, n_dressed)``.
initial_reference : object
Bootstrap reference at step 0.
policy : callable
Per-step assignment function. Defaults to
:func:`assign_global_greedy`.
Returns
-------
LabelingPath
Stacked diagnostics including ``swap_events`` per step boundary.
"""
del evals_path # energies are not needed for labeling; caller indexes them later.
labeling_0 = label_eigensystem(evecs_path[0], initial_reference, policy)
keys = labeling_0.keys
def select_refs(evecs: jnp.ndarray, indices: jnp.ndarray) -> jnp.ndarray:
# evecs: (dim, n_dressed) -> reference_vectors: (n_labels, dim).
return evecs[:, indices].T
def step(carry_refs, evecs_k):
ref = EigenstateReference(vectors=carry_refs, keys=keys)
labeling_k = label_eigensystem(evecs_k, ref, policy)
next_refs = select_refs(evecs_k, labeling_k.indices)
return next_refs, (
labeling_k.indices,
labeling_k.overlaps,
labeling_k.margins,
labeling_k.duplicates,
)
refs_0 = select_refs(evecs_path[0], labeling_0.indices)
_, (indices_rest, overlaps_rest, margins_rest, duplicates_rest) = lax.scan(
step, refs_0, evecs_path[1:],
)
def stack(first: jnp.ndarray, rest: jnp.ndarray) -> jnp.ndarray:
return jnp.concatenate([first[None, :], rest], axis=0)
indices_path = stack(labeling_0.indices, indices_rest)
overlaps_path = stack(labeling_0.overlaps, overlaps_rest)
margins_path = stack(labeling_0.margins, margins_rest)
duplicates_path = stack(labeling_0.duplicates, duplicates_rest)
swap_events = indices_path[1:] != indices_path[:-1]
return LabelingPath(
keys=keys,
indices=indices_path,
overlaps=overlaps_path,
margins=margins_path,
duplicates=duplicates_path,
swap_events=swap_events,
)
DRESS_TRACING_ERROR = (
"Dressing returns a concrete dict-keyed DressedResult and is not "
"traceable under jax.jit/grad/vmap. Use Chip.energy(), Chip.freq(), "
"Chip.dispersive_shift(), or Chip.state() inside transforms — they "
"route through the array-only kernel in quchip.engine.dressing and stay "
"differentiable."
)
[docs]
@dataclass
class DressedResult:
"""Store a frozen snapshot of a dressed-state diagonalization.
Attributes
----------
eigenvalues : array-like
Sorted dressed eigenvalues in GHz.
eigenstates : array-like
Backend eigenstate objects in the same order as ``eigenvalues``, with
the phases of ``eigenvector_matrix``. They are materialized lazily on
first access from the underlying
:class:`~quchip.backend.containers.EigensystemData`. The dressing and
sweep hot path never touches them, so the backend builds per-column
kets only on request.
state_map : dict[tuple[int, ...], int]
Map from each bare product-basis label (one int per device) to the
dressed eigenstate index with the largest overlap.
dressed_eigenvalues : dict[tuple[int, ...], Any]
Dressed eigenvalue for each assigned bare label, the direct lookup path
for :meth:`Chip.energy`.
assignment_overlaps : dict[tuple[int, ...], float]
``|⟨bare|dressed⟩|²`` of each assignment. Values below
``overlap_threshold`` flag hybridization.
hybridized_labels : tuple[tuple[int, ...], ...]
Bare labels whose assignment quality is below ``overlap_threshold``. A
non-empty tuple triggers a user warning at dress time.
bare_labels : tuple[tuple[int, ...], ...]
Labels of the product energy levels exposed by the chip's resolved
local dimensions, in chip order.
bare_labels_by_dressed_index : dict[int, tuple[int, ...]]
Inverse of :attr:`state_map`: dressed index → assigned bare label.
eigenvector_matrix : array-like or None
Dressed eigenvectors as columns, in the resolved solver basis, used by
:meth:`ChipAnalysis.operator_in_dressed_basis` and
:meth:`ChipAnalysis.state_components`. Each column has a real,
nonnegative amplitude on its assigned bare state
(:func:`~quchip.engine.dressing.gauge_phases`).
overlap_threshold : float
Minimum overlap for a confident assignment.
labeling : str
Algorithm id (currently only ``"DE"``). It resolves to the confidence-ordered
row-greedy overlap-matching policy run by :meth:`ChipAnalysis.dress`
(:func:`~quchip.engine.dressing.assign_rowwise_greedy`).
"""
eigenvalues: Any
state_map: dict[tuple[int, ...], int]
dressed_eigenvalues: dict[tuple[int, ...], Any]
assignment_overlaps: dict[tuple[int, ...], float]
hybridized_labels: tuple[tuple[int, ...], ...]
bare_labels: tuple[tuple[int, ...], ...]
bare_labels_by_dressed_index: dict[int, tuple[int, ...]]
eigenvector_matrix: Any = None
overlap_threshold: float = 0.5
labeling: str = "DE"
_eigensystem: EigensystemData | None = None
@property
def eigenstates(self) -> Any:
"""Backend eigenstate kets, materialized lazily from the eigensystem."""
if self._eigensystem is None:
raise RuntimeError(
"DressedResult was constructed without an EigensystemData; "
"eigenstates are unavailable."
)
return self._eigensystem.eigenstates
[docs]
def materialize_dressed_result(
eigenvalues: Any,
eigenvector_matrix: Any,
eigensystem: EigensystemData,
kernel_labeling: Labeling,
*,
overlap_threshold: float,
labeling: str,
warning_stacklevel: int,
) -> DressedResult:
"""Build the eager label-keyed result shared by chip and resolved dressing."""
if contains_tracer((kernel_labeling.indices, kernel_labeling.overlaps)):
raise RuntimeError(DRESS_TRACING_ERROR)
bare_labels = kernel_labeling.keys
indices_np = np.asarray(kernel_labeling.indices)
overlaps_np = np.asarray(kernel_labeling.overlaps)
state_map: dict[tuple[int, ...], int] = {}
bare_labels_by_dressed_index: dict[int, tuple[int, ...]] = {}
assignment_overlaps: dict[tuple[int, ...], float] = {}
dressed_eigenvalues: dict[tuple[int, ...], Any] = {}
for k, bare_label in enumerate(bare_labels):
index = int(indices_np[k])
state_map[bare_label] = index
bare_labels_by_dressed_index[index] = bare_label
assignment_overlaps[bare_label] = float(overlaps_np[k])
dressed_eigenvalues[bare_label] = eigenvalues[index]
hybridized_labels = tuple(
bare_label
for bare_label in bare_labels
if assignment_overlaps[bare_label] < overlap_threshold
)
if hybridized_labels:
# Fixed text, so the default warning filter shows it once per call
# site instead of once per sweep point.
warnings.warn(
"Strong hybridization detected during dressed-state assignment; "
f"bare labels with overlap below {overlap_threshold} are approximate. "
"Inspect DressedResult.hybridized_labels and "
"DressedResult.assignment_overlaps for the affected states.",
UserWarning,
stacklevel=warning_stacklevel,
)
return DressedResult(
eigenvalues=eigenvalues,
state_map=state_map,
dressed_eigenvalues=dressed_eigenvalues,
assignment_overlaps=assignment_overlaps,
hybridized_labels=hybridized_labels,
bare_labels=bare_labels,
bare_labels_by_dressed_index=bare_labels_by_dressed_index,
eigenvector_matrix=eigenvector_matrix,
overlap_threshold=float(overlap_threshold),
labeling=labeling,
_eigensystem=eigensystem,
)
[docs]
def dress_engine_result(
result: "EngineResult",
*,
at_time: Any | None = None,
overlap_threshold: float = 0.5,
labeling: str = "DE",
) -> DressedResult:
"""Materialize the instantaneous dressed eigensystem of an engine snapshot."""
if labeling != "DE":
raise ValueError(f"Unsupported labeling {labeling!r}. Only 'DE' is implemented.")
if result._dressing_context is None:
raise RuntimeError(
"EngineResult has no resolved dressing context; obtain it from Chip.resolve() "
"or a built solve problem."
)
if result.dynamic_terms and at_time is None:
raise ValueError(
"A dynamic EngineResult requires dress(at_time=...); this is an "
"instantaneous eigensystem, not a Floquet analysis."
)
context = result._dressing_context
backend = context.backend
_require_eigensystem_memory(result.dims)
hamiltonian = result.hamiltonian().matrix(t=at_time, backend=backend)
if contains_tracer((hamiltonian, context.reference.local_vectors)):
raise RuntimeError(DRESS_TRACING_ERROR)
native_hamiltonian = backend.from_array(
hamiltonian,
dims=[list(result.dims), list(result.dims)],
)
eigensystem = backend.eigensystem_data(native_hamiltonian)
kernel_labeling = label_eigensystem(
eigensystem.eigenvector_matrix,
context.reference,
policy=assign_rowwise_greedy,
)
eigensystem = _gauge_fixed_eigensystem(eigensystem, context.reference, kernel_labeling)
eigenvalues = eigensystem.eigenvalues
eigenvector_matrix = eigensystem.eigenvector_matrix
if result.excitation_space is not None:
from dataclasses import replace
kernel_labeling = replace(kernel_labeling, keys=tuple(map(tuple, result.excitation_space.levels.tolist())))
return materialize_dressed_result(
eigenvalues,
eigenvector_matrix,
eigensystem,
kernel_labeling,
overlap_threshold=float(overlap_threshold),
labeling=labeling,
warning_stacklevel=4,
)
def _gauge_fixed_eigensystem(eigensystem: EigensystemData, reference: Any, labeling: Labeling) -> EigensystemData:
"""Rephase each eigenvector so that its assigned bare amplitude is real and nonnegative.
The kets of the backend take the same phases, so a degenerate eigenspace
keeps the eigensolver's basis.
"""
raw = eigensystem.eigenvector_matrix
phases = gauge_phases(reference_amplitudes(reference, raw), labeling.indices)
xp = concrete_array_module(raw, phases)
vectors = xp.asarray(raw) * phases
if isinstance(raw, jax.Array) and xp is np:
vectors = jnp.asarray(vectors)
factors = [complex(factor) for factor in phases] if xp is np else list(phases)
def build_states() -> list[Any]:
return [state * factor for state, factor in zip(eigensystem.eigenstates, factors, strict=True)]
return EigensystemData(eigenvalues=eigensystem.eigenvalues, eigenvector_matrix=vectors,
_states_builder=build_states)
def _require_eigensystem_memory(dims: Sequence[int]) -> None:
"""Raise :class:`MemoryError` before a full static eigensystem that cannot fit."""
from quchip.utils.memory import eigensystem_bytes, require_memory
dimension = int(np.prod(dims))
require_memory(eigensystem_bytes(dimension), task=f"Full static eigensystem (dimension {dimension})",
remedy="Use a conserving model with labeled sector queries or reduce the model.")
[docs]
def labeled_eigensystem(engine_result: "EngineResult", backend: Any) -> tuple[Any, Any, Any, Labeling]:
"""Diagonalize a static analysis result and assign its bare product labels.
Returns ``(eigenvalues, eigenvector_matrix, eigensystem, labeling)``;
traced Hamiltonians stay traced. Each eigenvector follows the phase
convention of :func:`~quchip.engine.dressing.gauge_phases`.
"""
from quchip.engine.assembly import _analysis_matrix_ghz
context = engine_result._dressing_context
if context is None:
raise RuntimeError("Resolved analysis is missing its captured dressing reference.")
dims = list(engine_result.dims)
_require_eigensystem_memory(dims)
eigensystem = backend.eigensystem_data(
backend.from_array(_analysis_matrix_ghz(engine_result), dims=[dims, dims])
)
labeling = label_eigensystem(eigensystem.eigenvector_matrix, context.reference, policy=assign_rowwise_greedy)
eigensystem = _gauge_fixed_eigensystem(eigensystem, context.reference, labeling)
return eigensystem.eigenvalues, eigensystem.eigenvector_matrix, eigensystem, labeling