"""Native trajectories with captured model context and lazy analysis views."""
from dataclasses import dataclass
from typing import Any
import numpy as np
from quchip.backend.containers import SolverResult
from quchip.results.results import SimulationResult, wrap_solver_result
[docs]
@dataclass(frozen=True)
class MeasurementRecords:
"""Diffusive measurement currents of a stochastic solve, in one layout on both backends.
A monitored channel with collapse operator L, efficiency η and phase φ
records the current ``2 Re⟨c⟩ + ξ`` on each interval, in 1/sqrt(ns), with
``c = √η e^(−iφ) L``. Here L includes the square root of its rate. The
homodyne noise ξ has variance 1/Δt. A QuTiP heterodyne solve records the
quadratures ``2 Re⟨c⟩ + ξ_X`` and ``2 Im⟨c⟩ + ξ_Y``, and each noise has
variance 2/Δt. The values keep this native normalization and the native
sampling of the signal on each interval.
Attributes
----------
values : array_like
Current of each run, channel and interval, with shape
``(runs, channels, intervals)``. A QuTiP heterodyne solve adds a
quadrature axis of length 2 before the intervals.
channels : tuple of str or None
Channel of each row, as :attr:`TrajectoryResult.monitor_labels` gives
them. None when a traced efficiency leaves the native order unknown.
edges : array_like, shape (intervals + 1,)
Interval edges in ns: the save times of the solve.
sampling : {"start", "middle", "end", "mean"}
Signal sample of each interval. QuTiP takes the signal at the start or
the end of an interval, or the mean of both, as its option
``store_measurement`` selects, and adds the noise increment divided by
Δt. Dynamiqs divides the increment of the integrated record by Δt, so
the signal is its mean over the interval.
efficiencies : tuple
Efficiency η of each row.
phases : tuple
Phase φ of each row, in rad, relative to the integration frame.
heterodyne : bool
Whether :attr:`values` has a quadrature axis.
"""
values: Any
channels: tuple[str, ...] | None
edges: Any
sampling: str
efficiencies: tuple[Any, ...]
phases: tuple[Any, ...]
heterodyne: bool
[docs]
@dataclass(frozen=True)
class ClickRecords:
"""Jump events of a stochastic solve, in one layout on both backends.
An ensemble mean over runs is the weighted sum ``Σ_r w_r x_r``, plus the
no-click run with its weight :attr:`no_click_weight` when the native
sampling solves that run apart: QuTiP ``improved_sampling`` and the
dynamiqs ``Event(smart_sampling=True)``. The weights then sum to
``1 − no_click_weight``.
Attributes
----------
times : tuple of array_like
Click times of each run in ns, in increasing order.
channels : tuple of tuple of str
Channel of each click, from :attr:`TrajectoryResult.channel_labels`.
weights : array_like, shape (runs,)
Sampling weight of each run.
no_click_weight : float
Weight of the no-click run that the native sampling solves apart, or 0.
full : array_like of bool, shape (runs,)
Whether a run filled its dynamiqs ``nmaxclick`` buffer. Its evolution
may stop before the end of the solve. QuTiP has no buffer.
"""
times: tuple[Any, ...]
channels: tuple[tuple[str, ...], ...]
weights: Any
no_click_weight: float
full: Any
[docs]
def counts(self, channel: Any = None) -> Any:
"""Return the number of clicks of each run, shape ``(runs,)``.
Parameters
----------
channel : str or None, default=None
Count only this channel. None counts every channel.
"""
from quchip.utils.labeling import resolve_label
label = None if channel is None else resolve_label(channel)
return np.asarray([sum(1 for name in names if label is None or name == label) for names in self.channels])
[docs]
class TrajectoryResult(SimulationResult):
"""Keep a native stochastic result with its storage and weights unchanged.
Parameters
----------
native : object
QuTiP or Dynamiqs stochastic result, including seeds/keys and records.
problem : SolveProblem
Captured model, observables, channel order, and integration frame.
label_targets : LabelTargets or None, optional
Snapshot of the solve's chip that prepares label targets.
Attributes
----------
native : object
Unmodified native result, with native record conventions and event padding.
times : array_like, shape (T,)
Requested save times in ns.
channel_labels : tuple of str
Unique resolved channel keys in native jump-operator order.
monitor_labels : tuple of str or None
Native record channel order for an explicit selection, or None when unknown.
"""
def __init__(self, native: Any, problem: Any, *, label_targets: Any = None):
super().__init__(SolverResult(times=problem.tlist, solver=problem.solver),
problem.backend, problem.engine_result.dims, device_info=problem.device_info,
bases=problem.engine_result.bases, dissipation=problem.dissipation,
frame=problem.resolved_frame, label_targets=label_targets)
self.native = native
self._problem = problem
labels = tuple(channel.key for channel in problem.engine_result.slh.channels) if problem.dissipation else ()
selection = problem.monitoring
if selection is None and problem.solver in ("ssesolve", "dssesolve"):
selection = {i: (1.0, 0.0) for i in range(len(labels))}
self.channel_labels = labels
self.monitor_labels = None
if selection is not None:
from quchip.utils.jax_utils import maybe_concrete_scalar
selected = tuple(i for i in range(len(labels)) if i in selection)
if problem.solver in ("dssesolve", "dsmesolve"):
self.channel_labels = tuple(labels[i] for i in range(len(labels)) if i not in selection)
self.channel_labels += tuple(labels[i] for i in selected)
if any(maybe_concrete_scalar(selection[i][0]) is None for i in selected):
return
selected = tuple(i for i in selected if maybe_concrete_scalar(selection[i][0]) != 0)
self.monitor_labels = tuple(labels[i] for i in selected)
def _view(self, index: int | None, *, states: bool = True, deterministic: bool = False) -> SimulationResult:
values, history, final = self._backend.trajectory_view(
self.native, self._problem, index=index, states=states, deterministic=deterministic)
payload = SolverResult(times=self.times, states=history, final_state=final,
expect=list(values) if values is not None else None, solver=self.solver)
return wrap_solver_result(payload, self._problem, self._problem.backend, label_targets=self._label_targets)
[docs]
def average(self) -> SimulationResult:
"""Return native ensemble averages, including native trajectory weights."""
return self._view(None)
[docs]
def run(self, index: int) -> SimulationResult:
"""Return the analysis of one kept conditional trajectory.
Parameters
----------
index : int
Zero-based trajectory index of a run that native storage keeps.
"""
return self._view(index)
[docs]
def expect(self, key: Any, index: int | None = None) -> Any:
"""Return a named native ensemble expectation, shape (T,).
Parameters
----------
key : object or str
Captured observable key.
index : int or None, default None
Entry in a list-valued observable. Trajectory selection uses run().
"""
return self._view(None, states=False).expect(key, index)
[docs]
def records(self) -> MeasurementRecords:
"""Return the diffusive measurement currents of every kept run.
QuTiP keeps them with the native option ``store_measurement``.
:attr:`result.native <native>` keeps the unmodified native fields.
"""
values, sampling = self._backend.trajectory_records(self.native, self._problem)
selection = self._problem.monitoring
if selection is None:
selection = {i: (1.0, 0.0) for i in range(len(self.channel_labels))}
keys = tuple(channel.key for channel in self._problem.engine_result.slh.channels)
rows = [] if self.monitor_labels is None else [selection[keys.index(label)] for label in self.monitor_labels]
return MeasurementRecords(
values=values,
channels=self.monitor_labels,
edges=self.times,
sampling=sampling,
efficiencies=tuple(eta for eta, _ in rows),
phases=tuple(phase for _, phase in rows),
heterodyne=bool(np.ndim(values) == 4),
)
[docs]
def clicks(self) -> ClickRecords:
"""Return the jump events of every kept run, with the sampling weight of each run."""
events, weights, no_click, full = self._backend.trajectory_clicks(self.native, self._problem)
return ClickRecords(
times=tuple(times for times, _ in events),
channels=tuple(tuple(self.channel_labels[int(index)] for index in which) for _, which in events),
weights=weights,
no_click_weight=no_click,
full=full,
)
[docs]
def run_expect(self, key: Any, index: int | None = None) -> Any:
"""Return a named expectation of every kept run, shape ``(runs, T)``.
Row i equals ``run(i).expect(key, index)``. The native sampling
weights do not enter; :meth:`expect` gives the weighted ensemble mean.
Parameters
----------
key : object or str
Captured observable key.
index : int or None, default None
Entry in a list-valued observable.
"""
values = self._backend.trajectory_run_expect(self.native, self._problem)
payload = SolverResult(times=self.times, expect=list(values), solver=self.solver)
return wrap_solver_result(payload, self._problem, self._backend, label_targets=self._label_targets).expect(
key, index)