Source code for quchip.engine.truncation

"""Captured physical cutoff checks, sampled without storing state histories."""

import warnings
from dataclasses import dataclass, replace
from typing import Any

import numpy as np

from quchip.devices.spaces import TruncationBoundary
from quchip.engine.bands import local_mode_bands
from quchip.engine.observables import BandMeta, recombine_expect
from quchip.utils.values import copy_value
from quchip.utils.jax_utils import contains_tracer, maybe_concrete_scalar

DEFAULT_TRUNCATION_THRESHOLD = 1e-3


[docs] @dataclass(frozen=True) class BoundaryCheck: label: str boundary: TruncationBoundary basis: Any energy: bool = False
[docs] @dataclass(frozen=True) class TruncationPlan: checks: tuple[BoundaryCheck, ...] unavailable: tuple[str, ...] dims: tuple[int, ...] labels: tuple[str, ...] frame_frequencies: dict[str, Any] operators: tuple[Any, ...] = () metadata: tuple[BandMeta, ...] = () sampled: bool = False space: Any = None # Excitation cutoff K and the retained totals that a generator change can move above K. excitation: tuple[int, tuple[int, ...]] | None = None
[docs] def capture_truncation(chip: Any, engine_result: Any) -> TruncationPlan: """Capture boundary selections and bases, with no work that grows with the space.""" checks = [] unavailable = [] for device in chip.devices: basis = engine_result.bases[device.label] try: boundary = device.truncation_boundary() except NotImplementedError as exc: unavailable.append(f"Device {device.label!r}: {exc}.") else: if boundary is not None: if not isinstance(boundary, TruncationBoundary): raise TypeError("truncation_boundary() must return TruncationBoundary or None") if any(not isinstance(i, int) or not 0 <= i < basis.native_dim for i in boundary.indices): raise ValueError(f"Device {device.label!r} has invalid truncation boundary indices") checks.append(BoundaryCheck(device.label, boundary, basis)) if basis.resolved_dim < basis.native_dim: checks.append(BoundaryCheck(device.label, TruncationBoundary( (basis.resolved_dim - 1,), "retained energy boundary", "Increase retained levels and compare observables.", ), basis, energy=True)) reduction = engine_result.metadata.get("excitation_reduction") or {} cutoff, excitation = reduction.get("cutoff"), None if cutoff is not None: boundary = reduction.get("boundary") if boundary is None: unavailable.append(f"Excitation cutoff N <= {cutoff}: the generator's excitation changes are unknown, " "so the sectors that connect above the cutoff are unknown.") else: excitation = (cutoff, tuple(boundary)) return TruncationPlan( tuple(checks), tuple(unavailable), tuple(record.resolved_dim for record in engine_result.bases.values()), tuple(d.label for d in chip.devices), copy_value(engine_result.resolved_frame.frequencies, readonly=True), space=engine_result.excitation_space, excitation=excitation, )
[docs] def excitation_rows(plan: TruncationPlan) -> Any: """Compact rows of the retained totals that a generator change can move above the cutoff.""" assert plan.excitation is not None return np.flatnonzero(np.isin(plan.space.levels.sum(axis=1), plan.excitation[1]))
[docs] def boundary_rows(check: BoundaryCheck, xp: Any) -> Any: """Rows defining a captured cutoff projector in the local solver basis.""" indices = xp.asarray(check.boundary.indices, dtype=int) if check.energy: return xp.eye(check.basis.resolved_dim, dtype=complex)[indices] return xp.asarray(check.basis.vectors)[indices]
[docs] def boundary_sampling_frequency(plan: TruncationPlan | None) -> float | None: """Fastest nonzero frame band in local cutoff reconstruction, in GHz.""" import numpy as np from quchip.engine.bands import decompose_bands maximum = 0.0 if plan is None: return maximum for check in plan.checks: frequency = maybe_concrete_scalar(plan.frame_frequencies[check.label]) if frequency is None: return None if frequency == 0: continue if contains_tracer((check.basis.vectors, check.basis.energy_vectors)): return None rows = boundary_rows(check, np) transform = check.basis.energy_to_solver() if transform is not None: rows = rows @ np.asarray(transform) matrix = rows.conj().T @ rows for weight in decompose_bands(matrix, check.basis.resolved_dim): maximum = max(maximum, abs(frequency * weight)) return maximum
[docs] def compile_truncation(plan: TruncationPlan, backend: Any) -> TruncationPlan: """Lower captured cutoff projectors through the ordinary frame-band machinery.""" if plan.sampled: return plan operators = [] metadata = [] xp = backend.array_module for index, check in enumerate(plan.checks): basis = check.basis rows = boundary_rows(check, xp) matrix = xp.conj(rows.T) @ rows local = backend.from_array(matrix, dims=[[basis.resolved_dim], [basis.resolved_dim]]) device_index = plan.labels.index(check.label) if maybe_concrete_scalar(plan.frame_frequencies[check.label]) == 0.0: bands = [(0, local)] else: bands = local_mode_bands( backend, local, dim=basis.resolved_dim, label=check.label, semantic_to_solver=basis.energy_to_solver(), ) for weight, operator in bands: operators.append(backend.embed(operator, device_index, plan.dims) if plan.space is None else plan.space.embed(backend, operator, (device_index,))) metadata.append(BandMeta(key=str(index), weight=weight, device_labels=check.label)) if plan.excitation is not None: # The projector is diagonal in product energy labels, so every frame leaves it unchanged. diagonal = np.zeros(plan.space.size) diagonal[excitation_rows(plan)] = 1.0 operators.append(backend.diag(diagonal, dims=[[plan.space.size], [plan.space.size]])) metadata.append(BandMeta(key="excitation", weight=0, device_labels=plan.labels[0])) return replace(plan, operators=tuple(operators), metadata=tuple(metadata), sampled=True)
[docs] def with_truncation(problem: Any) -> Any: """Return a request collecting physical cutoff populations without saving states. Parameters ---------- problem : SolveProblem or SolveBatch Captured request or parameter batch. Its existing time grid is kept. Samples can miss excursions between save times and do not prove convergence. Returns ------- SolveProblem or SolveBatch Request with additional diagnostic observables. No solve occurs. """ from quchip.engine.ir import SolveBatch if isinstance(problem, SolveBatch): return replace(problem, problems=tuple(with_truncation(p) for p in problem.problems)) plan = problem.truncation if plan is None or plan.sampled: return problem plan = compile_truncation(plan, problem.backend) return replace(problem, e_ops=list(problem.e_ops or ()) + list(plan.operators), truncation=plan)
[docs] def boundary_traces(plan: TruncationPlan, values: Any, times: Any, backend: Any) -> tuple[Any, ...]: """Reconstruct boundary populations in the physical frame, independent of readout LO. The traces follow ``plan.checks``. An excitation cutoff with known boundary sectors adds their population as the last trace. """ _, traces = recombine_expect( flat_expect=values, meta_list=list(plan.metadata), tlist=times, frame_freqs={label: -frequency for label, frequency in plan.frame_frequencies.items()}, direction="demodulate", ) xp = backend.array_module keys = [str(index) for index in range(len(plan.checks))] + (["excitation"] if plan.excitation else []) return tuple(xp.real(traces.get(key, xp.zeros_like(times))) for key in keys)
[docs] def evaluate_boundaries(result: Any) -> tuple[TruncationPlan, tuple[Any, ...]]: """Use sampled traces or derive them from a kept full history without a rerun.""" plan = result._truncation if plan is None: raise RuntimeError("Truncation context is unavailable; build the calculation through quchip.") if result._boundary_traces is not None: return plan, result._boundary_traces if not plan.checks and plan.excitation is None: return plan, () if result._states is None and result._final_state is None: raise RuntimeError('Boundary samples unavailable; prepare with_truncation(problem) or retain states="all".') backend = result._backend final_only = result._states is None states = backend.stack_states([result._final_state]) if final_only else result._stacked_states() times = result.times[-1:] if final_only else result.times devices = compile_truncation(replace(plan, excitation=None), backend) values = [backend.expect_over_time(operator, states) for operator in devices.operators] traces = boundary_traces(devices, values, times, backend) if devices.operators else () if plan.excitation is not None: # The projector is diagonal in the compact labels, so the saved diagonals give it in every frame. populations = backend.populations_over_time(states) traces += (backend.array_module.sum(populations[:, excitation_rows(plan)], axis=1),) result.stats["truncation_sampling"] = "final state only" if final_only else "saved time grid" if not contains_tracer(traces): result._boundary_traces = traces return plan, traces
[docs] def check_truncation(result: Any, *, threshold: float = DEFAULT_TRUNCATION_THRESHOLD) -> dict[Any, Any]: """Report the maximum sampled boundary population of each device in a solved result. Each device model declares the levels at its cutoff, and a basis with fewer retained than native levels adds its highest retained energy level. The check reads these populations from the saved states, or from the samples that :func:`with_truncation` adds to a request, and warns for each value above ``threshold``. This warning heuristic can miss excursions between samples, and it is not an error bound. To confirm convergence, increase the applicable cutoff and compare observables. Native arrays stay differentiable. Warning thresholds are evaluated only for concrete results. Under an :class:`~quchip.cutoff.ExcitationCutoff`, the report also holds the policy as a key. Its value is the maximum population in the retained sectors that a Hamiltonian term, drive, or jump can move above the cutoff. To check convergence, compare observables at a larger ``max_total``. Parameters ---------- result : SimulationResult, TrajectoryResult, or PartitionedSimulationResult Time-domain result built by quchip. It needs saved states (``states="all"`` or ``states="final"``) or the samples of a request prepared with :func:`with_truncation`. A final state alone covers only the final time. A trajectory result needs each run's states or samples. threshold : float, default=1e-3 Maximum accepted boundary population. Returns ------- dict For a :class:`~quchip.results.results.SimulationResult`, the maximum population at each device label, and at the ``ExcitationCutoff`` policy if one applies. For a :class:`~quchip.results.trajectories.TrajectoryResult`, ``runs`` holds the report of each run, ``deterministic`` the report of each no-click run, and ``maximum`` the largest value of each key over both. For a :class:`~quchip.results.partitioned.PartitionedSimulationResult`, the reports of the components merge into one per-device report. Raises ------ TypeError If ``result`` is not one of these result types. RuntimeError If the result holds neither saved states nor diagnostic samples. ValueError If ``threshold`` is negative or not finite. """ from quchip.results.partitioned import PartitionedSimulationResult from quchip.results.results import SimulationResult from quchip.results.trajectories import TrajectoryResult if not np.isfinite(threshold) or threshold < 0: raise ValueError("truncation threshold must be finite and nonnegative") if isinstance(result, TrajectoryResult): return _check_runs(result, threshold) if isinstance(result, PartitionedSimulationResult): merged: dict[Any, Any] = {} for component in result.components: merged.update(_check_result(component, threshold)) return merged if isinstance(result, SimulationResult): return _check_result(result, threshold) from quchip.results._batch import BatchResult hint = " Check each result of the batch." if isinstance(result, BatchResult) else "" raise TypeError("check_truncation() needs a SimulationResult, TrajectoryResult, or PartitionedSimulationResult, " f"not {type(result).__name__}.{hint}")
def _check_result(result: Any, threshold: float) -> dict[Any, Any]: plan, traces = evaluate_boundaries(result) for reason in plan.unavailable: warnings.warn(f"Truncation diagnostic unavailable: {reason}", UserWarning, stacklevel=3) xp = result._backend.array_module observed: dict[Any, Any] = {} maxima = tuple(xp.maximum(0.0, xp.max(trace)) for trace in traces) maxima, excitation = maxima[:len(plan.checks)], maxima[len(plan.checks):] traced = contains_tracer(maxima) if traced: warnings.warn( "Truncation boundary populations are traced; warning thresholds cannot be evaluated here. " "Check the concrete result afterward.", UserWarning, stacklevel=3, ) sampling = result.stats.get("truncation_sampling", "saved time grid") for check, maximum in zip(plan.checks, maxima, strict=True): observed[check.label] = xp.maximum(observed.get(check.label, 0.0), maximum) if not traced and float(maximum) > threshold: warnings.warn( f"Device {check.label!r}: maximum sampled {check.boundary.description} population " f"({sampling}) {float(maximum):.3g} > threshold {threshold:.3g}. " f"{check.boundary.convergence_hint} This is a sampling heuristic, not an error bound.", UserWarning, stacklevel=3, ) if excitation: from quchip.cutoff import ExcitationCutoff assert plan.excitation is not None (maximum,) = excitation observed[ExcitationCutoff(plan.excitation[0])] = maximum if not traced and float(maximum) > threshold: warnings.warn( f"Excitation cutoff N <= {plan.excitation[0]}: maximum sampled population in sectors that " f"connect above the cutoff ({sampling}) {float(maximum):.3g} > threshold {threshold:.3g}. " "Raise max_total and compare observables. This is a sampling heuristic, not an error bound.", UserWarning, stacklevel=3, ) return observed def _check_runs(result: Any, threshold: float) -> dict[str, Any]: native = result.native count = len(native.trajectories) if hasattr(native, "trajectories") else len(native.keys) if not count: raise RuntimeError("Per-run truncation data unavailable; retain native trajectory results.") runs = tuple(_check_result(result.run(i), threshold) for i in range(count)) deterministic_count = (len(native.deterministic_trajectories) if hasattr(native, "deterministic_trajectories") else int(getattr(native.method, "smart_sampling", False))) deterministic = tuple(_check_result(result._view(i, deterministic=True), threshold) for i in range(deterministic_count)) xp = result._problem.backend.array_module maximum = {key: xp.max(xp.stack([run[key] for run in runs + deterministic])) for key in runs[0]} return {"runs": runs, "deterministic": deterministic, "maximum": maximum}