Source code for quchip.engine.states

"""State preparation in the solver basis.

Product states from energy labels or authored local kets, authored states of
the full chip, the dressed initial state of a label, and reusable states in
captured excitation coordinates. The chip's state queries and the solve
problems call these functions.
"""

from __future__ import annotations

import warnings
from collections.abc import Mapping
from dataclasses import dataclass
from math import prod
from typing import TYPE_CHECKING, Any

import jax
import numpy as np

from quchip.backend.protocol import State
from quchip.engine.pulse_space import PulseSpace
from quchip.utils.jax_utils import array_namespace, maybe_concrete_scalar

if TYPE_CHECKING:
    from quchip.chip.chip import Chip


[docs] def bare_state_from_bases(chip: "Chip", resolved: Mapping[str, Any], bases: Mapping[str, Any]) -> State: """Build a product state from the shared local state preparation.""" kets = bare_local_kets(chip, resolved, bases) return kets[0] if len(kets) == 1 else chip.backend.tensor_states(*kets)
[docs] def bare_local_kets( chip: "Chip", resolved: Mapping[str, Any], bases: Mapping[str, Any], ) -> list[State]: """Resolve each local ket without allocating its tensor product.""" backend = chip.backend available = list(chip._device_map.keys()) prepared = dict(resolved) for label in resolved: if label not in chip._device_map: raise ValueError(f"Unknown device label '{label}'. Available labels: {available}") for label, val in resolved.items(): if isinstance(val, bool): raise ValueError(f"Level index for '{label}' must be an integer, got {type(val).__name__}: {val!r}") basis = bases[label] if isinstance(val, int): if val < 0: raise ValueError(f"Level index for '{label}' must be >= 0, got {val}") if val >= basis.resolved_dim: raise ValueError( f"Level index {val} for '{label}' exceeds the resolved " f"dimension {basis.resolved_dim}." ) else: from quchip.declarative.expr import ( as_state_expr, materialize_expr, ) device = chip[label] expression = as_state_expr( val, labels=(device.label,), dims=(basis.native_dim,), name=rf"\lvert\psi_{{{device.label}}}\rangle", owner=device, scope=device.label, ) val = materialize_expr(expression, backend) prepared[label] = val shape = getattr(val, "shape", None) is_array_ket = shape is not None and ( len(shape) == 1 or (len(shape) == 2 and shape[1] == 1) ) if not is_array_ket and not backend.is_ket(val): raise ValueError(f"State for '{label}' must be a ket vector, got a non-ket state") if shape is not None and shape[0] != basis.native_dim: raise ValueError( f"Authored state dimension for '{label}' is {val.shape[0]}, " f"expected {basis.native_dim}." ) kets: list[State] = [] for dev in chip.devices: val = prepared.get(dev.label) basis = bases[dev.label] if val is None: level = 0 elif isinstance(val, int): level = val else: authored = backend.to_array(val).reshape(basis.native_dim, -1) projected = basis.vectors.conj().T @ authored authored_norm = backend.array_module.linalg.norm(authored) projected_norm = backend.array_module.linalg.norm(projected) lost = maybe_concrete_scalar(authored_norm**2 - projected_norm**2) if lost is not None and lost > 1e-10: warnings.warn( f"Projection discarded {lost:.3g} of the state norm on {dev.label!r}.", stacklevel=2, ) kets.append( backend.from_array( projected, dims=[[basis.resolved_dim], [1]], ) ) continue vector = basis.energy_state(level).reshape(basis.resolved_dim, 1) kets.append(backend.from_array(vector, dims=[[basis.resolved_dim], [1]])) return kets
[docs] def label_levels(chip: "Chip", spec: Any, dims: Any, *, role: str = "An initial label") -> tuple[int, ...]: """Return the product label of a level mapping or state shorthand, in device order. Devices without a level are in level 0. A mapping that holds a local ket is not a label, and ``chip.bare_state`` takes it. Parameters ---------- chip : Chip Chip whose devices and state order read the label. spec : mapping or str Energy level of each named device, or a state shorthand. dims : sequence of int Level count of each device. role : str, optional Name of the label in error messages. """ from quchip.chip.states import normalize_device_state_mapping labels = tuple(device.label for device in chip.devices) prepared = normalize_device_state_mapping(chip, spec, {}) if set(prepared) - set(labels): raise ValueError(f"{role} names a device that the chip does not have: {sorted(set(prepared) - set(labels))}.") levels = tuple(prepared.get(label, 0) for label in labels) if any(not isinstance(n, int) or isinstance(n, bool) for n in levels): raise TypeError( f"{role} takes integer energy levels. Use chip.bare_state(...) for a product of local kets. " "On a large chip, chip.compact_state(..., dressed=False) keeps compact coordinates." ) if any(not 0 <= n < d for n, d in zip(levels, dims)): raise ValueError(f"{role} needs integer energy levels below {tuple(dims)}; got {levels}.") return levels
[docs] def label_state(chip: "Chip", spec: Any, result: Any, start: Any) -> State: """Return the dressed state of a label at the start of a solve, in the solve's coordinates and frame. The state is the eigenstate ``|d_k⟩`` assigned to label k of the undriven static lab-frame model that ``result.approximation`` keeps, with real, nonnegative overlap on the bare product ``|k⟩``, as ``chip.state()`` assigns it. Under an excitation cutoff the model is the truncated ``P H P``. The frame map places the state at ``start`` as the label target of ``overlap()``: ``U(start)†|d_k⟩ exp(−i 2π start Σᵢ fᵢ kᵢ)``. The labels of an uncoupled chip start in ``|k⟩`` in every frame. Parameters ---------- chip : Chip Chip whose static model defines the state. spec : mapping, str, or None Level label. None is the all-ground label. result : EngineResult Solve contract with the frame, approximation, bases, and excitation space. start : scalar Start time of the solve, in ns. Returns ------- state Native backend ket in the coordinates of the solve. """ from quchip.engine.assembly import _may_raise_ground from quchip.engine.frames import FrameMap from quchip.utils.constants import TWO_PI space = result.excitation_space # A compact solve has one coordinate axis, and its space keeps the device dimensions. dims = result.dims if space is None else space.dims levels = (0,) * len(chip.devices) if spec is None else label_levels(chip, spec, dims) targets = LabelTargets(chip, result.approximation) vector = None # A ground that no retained term can move is the bare vacuum, without an eigensystem. Every other label # takes its assigned eigenvector, because a residual test cannot prove an eigenstate near a degeneracy. if any(levels) or space is not None or result.slh.has_network_hamiltonian or _may_raise_ground( chip, result.approximation, result.bases): vector = targets.dressed(levels, result.bases, space) if vector is None: vector = targets.bare(levels, result.bases, space) else: labels = tuple(device.label for device in chip.devices) bases = result.bases if space is None else space.bases xp = array_namespace(vector) frame_map = FrameMap.create(result.resolved_frame, labels, bases, xp, space=space) vector = frame_map.kets(vector, start) * xp.exp(-1j * TWO_PI * start * frame_map.label_energy(levels)) if space is not None: return chip.backend.from_array(vector.reshape(-1, 1), dims=[[space.size], [1]]) dims = list(result.dims) return chip.backend.from_array(vector.reshape(-1, 1), dims=[dims, [1] * len(dims)])
[docs] def materialize_state_spec( chip: "Chip", state_spec: Any, bases: Mapping[str, Any], ) -> State: """Materialize one authored state specification in the engine's solver basis. Labels and an omitted state are dressed states, which :func:`label_state` prepares from the solve's engine result. """ if isinstance(state_spec, (Mapping, str)): raise TypeError("A level label is a dressed state of a solve. Use chip.bare_state(...) for a product state.") from quchip.declarative.expr import as_state_expr, materialize_expr backend = chip.backend ordered = [bases[device.label] for device in chip.devices] authored_dims = tuple(record.native_dim for record in ordered) resolved_dims = tuple(record.resolved_dim for record in ordered) authored_dim = prod(authored_dims) resolved_dim = prod(resolved_dims) # QuTiP ``Qobj`` (``full``) and dynamiqs ``QArray`` (``to_jax``) states are in # solver coordinates whichever backend solves; plain arrays and symbolic # specifications are authored. resolved_native = ( backend.is_native_state(state_spec) or hasattr(state_spec, "full") or hasattr(state_spec, "to_jax") ) shape = getattr(state_spec, "shape", None) if shape is None: state_spec = as_state_expr( state_spec, labels=tuple(device.label for device in chip.devices), dims=authored_dims, name=r"\lvert\psi\rangle", owner=chip, scope="chip", ) state = materialize_expr(state_spec, backend) shape = getattr(state, "shape", None) if shape is None: raise TypeError("Initial state must be a semantic specification, callable, or ket.") if resolved_native: if shape[0] != resolved_dim: raise ValueError( f"A solver-native initial state must have dimension {resolved_dim}, got {shape}." ) return state if shape[0] != authored_dim or len(shape) not in (1, 2) or (len(shape) == 2 and shape[1] != 1): raise ValueError( "Initial-state dimension must match the authored or resolved chip space; " f"got {shape}, authored {authored_dim}, resolved {resolved_dim}." ) authored = backend.to_array(state).reshape(authored_dim, 1) xp = array_namespace(ordered[0].vectors) transform = ordered[0].vectors for record in ordered[1:]: transform = xp.kron(transform, record.vectors) projected = transform.conj().T @ authored authored_norm = xp.linalg.norm(authored) projected_norm = xp.linalg.norm(projected) lost = maybe_concrete_scalar(authored_norm**2 - projected_norm**2) if lost is not None and lost > 1e-10: warnings.warn( f"Projection discarded {lost:.3g} of the initial-state norm.", stacklevel=2, ) return backend.from_array(projected, dims=[list(resolved_dims), [1]])
[docs] @jax.tree_util.register_pytree_node_class @dataclass(frozen=True) class CompactState: """Keep a ket or density matrix without expanding its product coordinates. Create this value with ``chip.compact_state`` or ``result.compact_state``. Pass it as ``initial_state`` to a later simulation of the same device bases. Numerical arrays are JAX pytree leaves. Parameters ---------- values : array_like Ket columns or density matrix in the captured excitation space. space : object Captured coordinate map, supplied by the state factory. """ values: Any space: PulseSpace
[docs] def tree_flatten(self): """Return numerical leaves and immutable coordinate metadata for JAX.""" records = tuple(self.space.bases[label] for label in self.space.labels) leaves = (self.values, *((r.vectors, r.energies, r.energy_vectors) for r in records)) metadata = ( self.space.dims, self.space.labels, self.space.totals, tuple((r.kind, r.native_dim, r.resolved_dim) for r in records), ) return leaves, metadata
[docs] @classmethod def tree_unflatten(cls, metadata, leaves): """Restore a state from JAX's coordinate metadata and numerical leaves. Parameters ---------- metadata : tuple Captured dimensions, labels, totals, and basis kinds. leaves : tuple State values and numerical basis arrays. """ from quchip.engine.basis import BasisRecord from quchip.engine.excitation import sector_levels dims, labels, totals, records = metadata bases = { label: BasisRecord(kind, arrays[0], arrays[1], arrays[2], native, resolved) for label, (kind, native, resolved), arrays in zip(labels, records, leaves[1:]) } levels = np.concatenate([sector_levels(dims, total) for total in totals]) return cls(leaves[0], PulseSpace(dims, labels, totals, levels, bases))
[docs] def compact_state(chip, *components, dressed=True): """Prepare a normalized state on the excitation totals of its components. Parameters ---------- chip : Chip Chip whose resolved energy bases define the coordinates. *components : mapping, str, or (amplitude, specification) pairs Energy-level labels, as in ``Chip.compact_state``. With ``dressed=False``, a mapping can also give a device a local ket. dressed : bool, default=True Use the static dressed eigenstate assigned to each label. False uses the bare product of each label. Returns ------- CompactState Captured state accepted as a pulse simulation's initial_state. """ from quchip.chip.states import normalize_device_state_mapping from quchip.cutoff import active_cutoff from quchip.engine.assembly import _resolve_system from quchip.engine.dressing import warn_weak_assignment from quchip.engine.excitation import excitation_structure from quchip.engine.sectors import SectorModel from quchip.utils.jax_utils import maybe_concrete_scalar resolution = _resolve_system(chip, chip.backend) labels = tuple(d.label for d in chip.devices) policy = active_cutoff(chip) entries = [] for component in components or ({},): amplitude, spec = component if isinstance(component, tuple) and len(component) == 2 else (1, component) prepared = normalize_device_state_mapping(chip, spec, {}) if set(prepared) - set(labels): raise ValueError("Compact state contains an unknown device.") if not dressed and not all(isinstance(n, int) and not isinstance(n, bool) for n in prepared.values()): entries.append((amplitude, *_bare_factors(chip, prepared, resolution, policy))) continue levels = tuple(prepared.get(label, 0) for label in labels) if any(not isinstance(n, int) or isinstance(n, bool) for n in levels): raise TypeError("A dressed compact state takes integer energy levels. Use dressed=False for local kets.") if any(not 0 <= n < d for n, d in zip(levels, resolution.dims)): raise ValueError("Compact state specifications require valid integer energy levels.") if policy is not None and sum(levels) > policy.max_total: raise ValueError( f"Compact state label {levels} has total excitation {sum(levels)}, outside the excitation " f"cutoff N <= {policy.max_total}." ) entries.append((amplitude, levels, {sum(levels)})) if dressed and policy is None and not excitation_structure( chip, chip.approximation, resolution=resolution).conserves: raise ValueError( "Compact dressed preparation requires proven static excitation conservation or an excitation cutoff. " "Use chip.compact_state(..., dressed=False) for bare products." ) model = None totals = set().union(*(occupied for *_, occupied in entries)) if dressed: # Under a cutoff, a dressed state spans the totals of its class. model = chip._analysis._sector_model() if policy is not None else SectorModel(chip, resolution=resolution) totals = {total for n in totals for total in model.class_of(n)} space = PulseSpace.create(resolution.dims, labels, totals, resolution.bases) xp = chip.backend.array_module vector = xp.zeros(space.size, dtype=complex) for amplitude, levels, _ in entries: if model is None and all(isinstance(n, int) for n in levels): contribution = xp.asarray(np.arange(space.size) == space.rows[levels], dtype=complex) elif model is None: # A product of local kets, read on the retained labels. contribution = xp.ones(space.size, dtype=complex) for axis, local in enumerate(levels): contribution = contribution * local[space.levels[:, axis]] else: system = model.eigensystem(sum(levels)) row = system.space.row(levels) warn_weak_assignment(levels, system.labeling.overlaps[row]) eigenvector = xp.asarray(system.eigenvectors)[:, system.labeling.indices[row]] indices = np.asarray([space.rows[label] for label in system.space.labels]) contribution = xp.asarray(np.eye(space.size)[:, indices]) @ eigenvector vector = vector + amplitude * contribution norm = xp.linalg.norm(vector) if maybe_concrete_scalar(norm) == 0: raise ValueError("Compact state superposition has zero norm.") return CompactState((vector / xp.where(norm > 0, norm, 1)).reshape(-1, 1), space)
def _bare_factors(chip, prepared, resolution, policy): """Return the energy-basis factors of a bare product and the totals that it occupies. Under a cutoff, a product with more than round-off weight above it raises, and only the totals up to the cutoff are kept. """ from quchip.engine.pulse_space import _require_inside from quchip.utils.jax_utils import contains_tracer xp = chip.backend.array_module factors, totals, weights = [], {0}, np.ones(1) for device, ket, dim in zip(chip.devices, bare_local_kets(chip, prepared, resolution.bases), resolution.dims): level = prepared.get(device.label, 0) if isinstance(level, int): # A level is an exact unit vector, so a basis conversion or a trace adds no support. weight = (np.arange(dim) == level).astype(float) local, support = xp.asarray(weight, dtype=complex), (level,) else: local = xp.asarray(chip.backend.to_array(ket)).reshape(-1) transform = resolution.bases[device.label].energy_to_solver() if transform is not None: local = xp.conj(xp.asarray(transform).T) @ local # A traced ket can occupy every level. weight = None if contains_tracer(local) else np.abs(np.asarray(local)) ** 2 support = range(dim) if weight is None else np.flatnonzero(weight != 0) factors.append(local) totals = {n + int(index) for n in totals for index in support} weights = None if weight is None or weights is None else np.convolve(weights, weight) if policy is not None: if weights is None and max(totals) > policy.max_total: raise ValueError( f"A traced local ket can occupy totals above the excitation cutoff N <= {policy.max_total}, so " "quchip cannot prove that the product lies inside it. Pass integer energy levels." ) if weights is not None: outside = float(np.sum(weights[policy.max_total + 1:])) _require_inside(policy.max_total, outside, float(np.sum(weights)), "bare product") totals = {n for n in totals if n <= policy.max_total} return tuple(factors), totals
[docs] def block_model(chip, approximation): """Return the static block model of ``approximation``, or None when dressing needs the full eigensystem. Under an excitation cutoff, every chip has a block model. Without one, the static model must conserve the total energy-level index. Parameters ---------- chip : Chip Chip whose static model is split. approximation : Approximation Approximation of the static model. """ from quchip.cutoff import active_cutoff from quchip.engine.excitation import excitation_structure from quchip.engine.sectors import SectorModel if approximation == chip.approximation: return chip._analysis._sector_model(_advise=False) if active_cutoff(chip) is None and not excitation_structure(chip, approximation).conserves: return None return SectorModel(chip, approximation=approximation, advisories=[])
[docs] def sector_label_ket(chip, model, space, levels): """Return the dressed eigenstate assigned to a label in the rows of an excitation space. The eigenvector comes from the block of ``model`` that holds the label. Rows of the block outside ``space`` are dropped, because the states of the solve have no amplitude there. Parameters ---------- chip : Chip Chip whose backend sets the array module. model : SectorModel Static block model of the solve's approximation. space : PulseSpace Excitation space of the solve. levels : tuple of int Product label of local energy levels. """ import jax.numpy as jnp from quchip.engine.basis import _differentiable_eigenvector from quchip.engine.dressing import gauge_phases, warn_weak_assignment from quchip.utils.jax_utils import contains_tracer system = model.eigensystem(sum(levels)) row = system.space.row(tuple(levels)) index = system.labeling.indices[row] warn_weak_assignment(levels, system.labeling.overlaps[row]) traced = contains_tracer((system.eigenvalues, system.eigenvectors, index)) xp = jnp if traced else chip.backend.array_module column = xp.asarray(system.eigenvectors)[:, index] if contains_tracer(system.eigenvectors): # JAX's eigh derivative can be NaN at an unrelated exact tie. matrix = jnp.asarray(model.hamiltonian(system.space.totals)) column = _differentiable_eigenvector(matrix, system.eigenvalues[index], system.eigenvalues, system.eigenvectors, column) # That derivative transports the phase. The gauge restores the derivative of the anchor's phase. # Rolling the rows puts the label's own amplitude first, where the gauge anchors. column = column * gauge_phases(jnp.roll(column[:, None], -row, axis=0), jnp.zeros(1, dtype=int))[0] kept = np.asarray([i for i, label in enumerate(system.space.labels) if label in space.rows], dtype=int) indices = np.asarray([space.rows[system.space.labels[i]] for i in kept], dtype=int) vector = xp.zeros(space.size, dtype=complex) return vector.at[indices].set(column[kept]) if xp is jnp else _scatter(vector, indices, column[kept])
[docs] @dataclass(frozen=True, eq=False) class LabelTargets: """Prepare the label targets of one solve from a snapshot of its chip. :meth:`capture` takes an isolated clone when the solve finishes, so later changes to the source chip do not change the targets. The clone runs its dressed analysis only when a dressed target is requested. Parameters ---------- chip : Chip Isolated clone of the solve's chip. approximation : Approximation Approximation of the solve's static model. """ chip: Any approximation: Any
[docs] @classmethod def capture(cls, chip, approximation): """Record an isolated clone of a chip. Parameters ---------- chip : Chip Source chip of the solve. approximation : Approximation Approximation of the solve's static model. """ return cls(chip.clone(), approximation)
[docs] def levels(self, spec, dims): """Return the product label of a mapping or state shorthand, in device order. Parameters ---------- spec : mapping or str Local energy level of each named device. Devices without a level are in level 0. dims : sequence of int Level count of each device. """ return label_levels(self.chip, spec, dims, role="A label target")
[docs] def bare(self, levels, bases, space=None): """Return the bare product state of a label, in solver coordinates or in the rows of ``space``. Parameters ---------- levels : tuple of int Product label of local energy levels. bases : mapping Resolved basis record of each device. space : PulseSpace or None, optional Excitation space of the solve. A label outside it gives a zero vector. """ backend = self.chip.backend if space is not None: return backend.array_module.asarray(np.arange(space.size) == space.rows.get(levels, -1), dtype=complex) labels = tuple(device.label for device in self.chip.devices) return backend.to_array(bare_state_from_bases(self.chip, dict(zip(labels, levels)), bases)).reshape(-1)
[docs] def dressed(self, levels, bases, space=None): """Return the lab-frame dressed eigenstate assigned to a label, as :meth:`bare` places it. The static model is the one that the solve's approximation keeps. None means the all-ground label of a conserving model, whose bare product is the only state with total level 0. Parameters ---------- levels : tuple of int Product label of local energy levels. bases : mapping Resolved basis record of each device. space : PulseSpace or None, optional Excitation space of the solve. """ backend = self.chip.backend if space is not None: model = block_model(self.chip, self.approximation) if model is not None: return sector_label_ket(self.chip, model, space, levels) ket = self.chip.analysis._label_ket(levels, self.approximation) if ket is None: return None if space is not None: dims = list(space.dims) native = backend.from_array(ket.reshape(-1, 1), dims=[dims, [1] * len(dims)]) ket = backend.to_array(space.project(native, backend)).reshape(-1) return ket
[docs] def energy(self, levels): """Return the energy in GHz of the dressed eigenstate that :meth:`dressed` assigns to a label. Parameters ---------- levels : tuple of int Product label of local energy levels. """ return self.chip.analysis._label_energy(tuple(levels), self.approximation)
def _scatter(vector, indices, values): """Return ``vector`` with ``values`` written at ``indices``.""" vector[indices] = values return vector
[docs] def compatible_values(state, labels, dims, bases, backend): """Validate captured coordinates before interpreting a saved energy state.""" from quchip.results._time import require_valid if state.space.labels != tuple(labels) or state.space.dims != tuple(dims): raise ValueError("Compact state coordinates do not match this calculation.") xp = backend.array_module values = xp.asarray(state.values) for label in labels: old, new = state.space.bases[label], bases[label] values = require_valid( values, xp.any(xp.asarray(old.energy_vectors) != xp.asarray(new.energy_vectors)), "Compact state energy bases changed. Prepare the state again for this chip.", ) return values
[docs] def materialize_compact(chip, spec, space): """Prepare an initial state in the coordinates of an exact pulse space. Parameters ---------- chip : Chip Chip whose resolved energy bases define the state. spec : state_like or CompactState Initial state. :func:`label_state` prepares labels and an omitted state. space : PulseSpace Exact excitation space of the solve. Returns ------- state Native backend ket or density matrix in ``space`` coordinates. """ backend = chip.backend xp = backend.array_module if isinstance(spec, CompactState): values = compatible_values(spec, space.labels, space.dims, space.bases, backend) indices = np.asarray([space.rows[tuple(label)] for label in spec.space.levels]) transform = xp.asarray(np.eye(space.size)[:, indices]) values = transform @ values if spec.values.shape[-1] != 1: values = values @ xp.conj(transform.T) return backend.from_array(values, dims=[[space.size], [1] if values.shape[-1] == 1 else [space.size]]) return space.project(materialize_state_spec(chip, spec, space.bases), backend)