"""Combined view over per-component solves of a partitioned chip.
Holds the K component :class:`~quchip.results.results.SimulationResult` objects
and answers observable queries locally. The joint state is materialized only on
explicit request, because rebuilding it costs the full tensor-product space
that the partition avoided.
"""
from __future__ import annotations
import warnings
from typing import Any
from quchip.utils.labeling import resolve_label
_JOINT_WARNING = (
"Materializing the joint state of a partitioned result rebuilds the full "
"tensor-product space the partition avoided."
)
[docs]
class PartitionedSimulationResult:
"""Result of one partitioned solve: K component results plus a key plan.
Attributes
----------
component_results : sequence of SimulationResult
Per-component results in ``partition.components`` order.
partition : object
Component and device ownership metadata.
key_plan : mapping
Mapping from requested observable keys to component traces.
"""
def __init__(self, component_results: list, partition: Any, key_plan: dict) -> None:
"""Wrap the per-component solves produced by one partitioned run.
*component_results* must align with ``partition.components``. The
result at index ``i`` solves ``partition.components[i].chip``. When you
build this object by hand instead of via
:func:`~quchip.engine.partitioned.maybe_simulate_partitioned`, keep
that order.
"""
if len(component_results) != len(partition.components):
raise ValueError(
f"component_results has {len(component_results)} entries but partition has "
f"{len(partition.components)} components; they must align one-to-one, in "
"partition.components order."
)
self._results = tuple(component_results)
self._partition = partition
self._key_plan = dict(key_plan)
self.times = self._results[0].times
@property
def components(self) -> tuple:
return self._results
@property
def dissipation(self) -> bool:
"""Return the shared dissipation choice of this partitioned solve."""
choices = {result.dissipation for result in self._results}
if len(choices) != 1:
raise ValueError("Component results have different dissipation choices.")
return choices.pop()
@property
def partition(self) -> Any:
return self._partition
@property
def frame(self) -> Any:
"""Return the joint integration frame, with the frame frequency that each component gives its devices.
The mode is the common mode of the components, or ``"dict"`` when
they differ. A component that plans its own ``"auto"`` frame keeps its
plan in ``components[i].frame``.
"""
from quchip.engine.ir import ResolvedFrame
frames = [result.frame for result in self._results]
modes = {frame.mode for frame in frames}
return ResolvedFrame(
frequencies={label: value for frame in frames for label, value in frame.frequencies.items()},
demod_freqs={label: value for frame in frames for label, value in frame.demod_freqs.items()},
mode=modes.pop() if len(modes) == 1 else "dict",
plan=frames[0].plan if len(frames) == 1 else None,
)
@property
def device_order(self) -> tuple[str, ...]:
"""Return the parent chip's original device-label order.
This is *not* the concatenation of each component's labels, which
follows connected-component discovery order. That order can interleave
differently when the chip's device order does not already group each
component's members together. The result permutes :attr:`states` and
:attr:`final_state` into this order, so they match the joint solve
exactly.
"""
return self._partition.chip_order
def _normalize_key(self, key: Any) -> Any:
return tuple(resolve_label(k) for k in key) if isinstance(key, tuple) else resolve_label(key)
def _local_values(self, entry: Any, index: int | None) -> Any:
effective = entry.index if entry.index is not None else index
return self._results[entry.component].expect(entry.key, effective)
[docs]
def expect(self, key: Any, index: int | None = None) -> Any:
"""Return one captured expectation trace.
Parameters
----------
key : object or str
Captured observable key.
index : int or None, optional
Entry of a list-valued observable.
"""
entry = self._key_plan.get(self._normalize_key(key))
if entry is None:
raise KeyError(f"No observable {key!r} in this partitioned result. Known: {list(self._key_plan)}")
from quchip.chip.partition import CrossEop
if isinstance(entry, CrossEop):
if index is not None:
raise ValueError(
f"Cross-component e_ops key {key!r} is a single correlator trace; "
f"'index' must be None, got {index!r}."
)
return self._local_values(entry.a, None) * self._local_values(entry.b, None)
return self._local_values(entry, index)
[docs]
def observable_at(self, t: Any, values: Any, *, method: str = "exact") -> Any:
"""Select or linearly interpolate observable values on the shared grid.
Parameters
----------
t : scalar or array_like
Query times in ns.
values : array_like
Saved values with time on the final axis.
method : {"exact", "nearest", "interpolate"}, default="exact"
Time-selection rule.
"""
return self._results[0].observable_at(t, values, method=method)
[docs]
def expect_final(self, key: Any, index: int | None = None) -> Any:
"""Return the final expectation value for an observable.
Parameters
----------
key : object or str
Captured observable key.
index : int or None, optional
Entry of a list-valued observable.
"""
return self.expect(key, index)[-1]
expect_values = expect
def _owner_result(self, device: Any) -> Any:
return self._results[self._partition.owner_of(device)]
[docs]
def iq_readout(self, output: Any, **kwargs: Any) -> Any:
"""Build a detector from the component owning an exposed output.
Parameters
----------
output : port object or str
External output reference plane.
**kwargs
Forwarded to :meth:`SimulationResult.iq_readout`.
"""
label = resolve_label(output)
owners = [result for result in self._results if result._readout_wiring is not None
and label in result._readout_wiring.exposed]
if len(owners) != 1:
raise ValueError(f"Output {label!r} must belong to exactly one partition; inspect component results.")
return owners[0].iq_readout(output, **kwargs)
[docs]
def measure(self, *devices: Any, t: Any = None, basis: Any = "energy") -> Any:
"""Measure kept states in local energy bases, without further evolution.
Pass multiple devices for joint outcomes, t for an exact saved time, or
basis='solver'. Write custom local unitary columns in the captured energy
basis of the stored integration frame. Give one matrix for one device, or
a device mapping. No phase-frame conversion is applied. Samples at
different times represent independently terminated experiments.
Parameters
----------
*devices : device object or str
Measured devices. An empty selection measures all devices.
t : scalar, array_like, or None, optional
Saved time or times in ns. ``None`` selects the final state.
basis : {"energy", "solver"}, array_like, or mapping, default="energy"
Captured local measurement basis.
"""
from quchip.results.terminal import measure_result
return measure_result(self, devices, t=t, basis=basis)
[docs]
def population(self, device: Any, level: int = 0) -> Any:
"""Return occupation of a captured isolated energy level.
Parameters
----------
device : device object or str
Captured device.
level : int, default=0
Zero-based isolated energy level.
"""
return self._owner_result(device).population(device, level)
[docs]
def check_truncation(self, threshold: float = 1e-3) -> dict[str, float]:
"""Deprecated method form of :func:`quchip.check_truncation`.
quchip 0.6 removes this method. Call ``quchip.check_truncation(result)``.
Parameters
----------
threshold : float, default=1e-3
Maximum accepted boundary population.
"""
from quchip.engine.truncation import check_truncation
from quchip.utils.deprecation import warn_renamed
warn_renamed("PartitionedSimulationResult.check_truncation()", "quchip.check_truncation(result)",
removal="0.6")
return check_truncation(self, threshold=threshold)
def _chip_order_permutation(self) -> tuple[list[int], list[int]] | None:
"""Return ``(dims, order)`` for :meth:`~quchip.backend.protocol.Backend.permute_state`.
Returns ``None`` when the concatenated component order already
matches :attr:`device_order` and permuting would be a no-op.
"""
current_labels = [label for comp in self._partition.components for label in comp.labels]
index_of = {label: i for i, label in enumerate(current_labels)}
order = [index_of[label] for label in self._partition.chip_order]
if order == list(range(len(order))):
return None
dims = [d for result in self._results for d in result.dims]
return dims, order
def _collect_component_ket_trajectories(self) -> list:
"""Collect each component's saved-state trajectory; requires every component ket-valued.
Raises if a component solve didn't retain states, or stored density
matrices instead of kets — joint-state reconstruction of a
*trajectory* only supports the all-ket case (see :attr:`final_state`
for the mixed ket/density-matrix case, which is well-defined for a
single final state).
"""
trajectories = []
for result in self._results:
if not result._is_ket_trajectory():
raise NotImplementedError(
"Joint-state reconstruction is implemented for ket trajectories only."
)
trajectories.append(result.states)
return trajectories
def _promote_to_common_state_kind(self, backend: Any, states: list) -> list:
"""Promote every state to a density matrix when the list mixes kets and density matrices.
A tensor product of component kets is itself a valid joint ket, and
a tensor product of component density matrices is a valid joint
density matrix — but a mix of the two is neither: tensoring a ket
with a density matrix is a shape mismatch, not a physical state.
"""
kets = [backend.is_ket(s) for s in states]
if any(kets) and not all(kets):
return [backend.as_density_matrix(s) for s in states]
return states
@property
def states(self) -> list:
"""Reconstruct the joint-state trajectory (ket trajectories only), in :attr:`device_order`.
Component states tensor together in connected-component discovery
order, which can interleave differently from the parent chip's device
order. Each reconstructed step is permuted
(:meth:`~quchip.backend.protocol.Backend.permute_state`) into
:attr:`device_order`, so the result matches a joint solve of the
original chip exactly.
See also :attr:`final_state`. Unlike this accessor, :attr:`final_state`
intentionally also accepts density-matrix components, because a tensor
product of component density matrices is itself a valid joint state. A
per-step list of joint kets is well-defined only when every component
stayed pure.
"""
warnings.warn(_JOINT_WARNING, UserWarning, stacklevel=2)
backend = self._results[0]._backend
from math import prod
from quchip.utils.memory import dense_bytes, require_memory
dimension = prod(d for result in self._results for d in result.dims)
require_memory(dense_bytes(len(self.times), dimension, copies=4), task="Joint partitioned history export",
remedy="Read local observables or component states instead.")
per_component_trajectories = self._collect_component_ket_trajectories()
joint_steps = [backend.tensor_states(*step) for step in zip(*per_component_trajectories)]
permutation = self._chip_order_permutation()
if permutation is None:
return joint_steps
dims, order = permutation
return [backend.permute_state(state, dims, order) for state in joint_steps]
@property
def final_state(self) -> Any:
"""Reconstruct the joint final state in :attr:`device_order`, as a ket
if all components stayed pure, else a density matrix.
When components disagree, it first promotes every component to a density
matrix and then tensors them into a valid joint density matrix. Components
tensor together in connected-component discovery order, which can
interleave differently from the parent chip's device order. The result is
permuted (:meth:`~quchip.backend.protocol.Backend.permute_state`) into
:attr:`device_order`, so it matches a joint solve of the original chip
exactly.
"""
# Promotion of the components to a common density-matrix state kind uses
# `_promote_to_common_state_kind`.
return self._joint_state([r.final_state for r in self._results])
[docs]
def state_at(self, t: Any, *, method: str = "exact", frame: str = "integration") -> Any:
"""Reconstruct a joint state at a saved time.
Parameters
----------
t : scalar
Query time in ns.
method : {"exact", "nearest"}, default="exact"
Time-selection rule.
frame : {"integration", "lab"}, default="integration"
Frame of the returned state.
"""
return self._joint_state([r.state_at(t, method=method, frame=frame) for r in self._results])
def _joint_state(self, states: list) -> Any:
warnings.warn(_JOINT_WARNING, UserWarning, stacklevel=3)
backend = self._results[0]._backend
from math import prod
from quchip.utils.memory import dense_bytes, require_memory
dimension = prod(d for result in self._results for d in result.dims)
power = 1 if all(backend.is_ket(state) for state in states) else 2
require_memory(dense_bytes(*(dimension,) * power, copies=4), task="Joint partitioned state export",
remedy="Read local observables or component states instead.")
joint = backend.tensor_states(*self._promote_to_common_state_kind(backend, states))
permutation = self._chip_order_permutation()
if permutation is None:
return joint
dims, order = permutation
return backend.permute_state(joint, dims, order)
[docs]
def describe(self) -> str:
lines = [f"PartitionedSimulationResult: {len(self._results)} components"]
for comp, result in zip(self._partition.components, self._results):
lines.append(f"- {list(comp.labels)}: dims={tuple(result.dims)}, solver={result.solver}")
return "\n".join(lines)
def __repr__(self) -> str:
return (
f"PartitionedSimulationResult({len(self._results)} components, "
f"devices={list(self.device_order)})"
)
def __getattr__(self, name: str) -> Any:
"""Raise a directed failure for any accessor that this class does not implement.
``PartitionedSimulationResult`` aggregates only the surface defined
above (``expect``, ``population``, ``states``/``final_state``,
``check_truncation``, ...). It does not re-implement every
:class:`~quchip.results.results.SimulationResult` method. Use a missing
member per component (``result.components[i].<name>``), or rerun with
``partition=False`` for a full-fidelity joint
:class:`~quchip.results.results.SimulationResult` that has it.
"""
raise AttributeError(
f"PartitionedSimulationResult has no '{name}'. Use "
f"'.components[i].{name}' for a per-component result, or "
"simulate(..., partition=False) for a full-fidelity joint "
"SimulationResult that implements it."
)