Source code for quchip.control.batch

"""Sweep axes and entry handles for scheduled sequences.

Axes expand into per-point overrides for :class:`~quchip.engine.ir.SolveBatch`.
Numerical pulse, delay, and envelope values remain JAX-traceable.
"""

from __future__ import annotations

from dataclasses import dataclass
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any

from quchip.utils.batching import expand_axis_groups

if TYPE_CHECKING:
    from quchip.control.sequence import QuantumSequence


[docs] @dataclass(frozen=True) class BatchAxis: """One batchable axis over a scheduled entry or sequence-level field. :meth:`PulseHandle.vary`, :meth:`DelayHandle.vary`, or :meth:`QuantumSequence.vary` creates this axis. :meth:`QuantumSequence.build_batch` consumes it. Attributes ---------- owner : QuantumSequence Sequence that owns the axis. field : str Swept field name. values : sequence Values evaluated along this axis. name : str Display name in result metadata. target_kind : {"entry", "sequence", "parameter"} Kind of value the axis overrides. entry_index : int or None Scheduled-entry index for entry axes. entry_field : str or None Normalized field on the scheduled entry. entry : object or None Scheduled entry captured by identity. """ owner: "QuantumSequence" target_kind: str # "entry" | "sequence" | "parameter" field: str values: Any name: str entry_index: int | None = None entry_field: str | None = None #: Scheduled entry, checked by identity when building the batch. #: ``None`` for sequence-level axes. entry: Any = None @property def size(self) -> int: return len(self.values) @property def override_key(self) -> tuple[int | None, str]: """Identify the bound value independently of the display name.""" return self.entry_index, self.entry_field or self.field
[docs] @dataclass(frozen=True) class ZippedBatchAxis: """Multiple :class:`BatchAxis` objects swept pairwise as one dimension. Attributes ---------- axes : tuple of BatchAxis Equal-length axes evaluated at matching indices. """ axes: tuple[BatchAxis, ...] @property def size(self) -> int: return self.axes[0].size
def _axis_metadata(axis: BatchAxis | ZippedBatchAxis) -> tuple[str, Any]: """Return ``(name, values)`` for *axis*, joining zipped-axis names with ``"/"``.""" if isinstance(axis, ZippedBatchAxis): names = tuple(member.name for member in axis.axes) values = tuple({member.name: member.values[i] for member in axis.axes} for i in range(axis.size)) return ("/".join(names), values) return (axis.name, axis.values) class _BaseEntryHandle: """A scheduled entry's index and identity, used to build batch sweeps. Rejects use after the sequence changes which entry occupies that index. """ def __init__(self, sequence: "QuantumSequence", entry_index: int) -> None: self._sequence = sequence self._entry_index = entry_index self._entry = sequence._entries[entry_index] def _resolve_entry(self) -> Any: entries = self._sequence._entries if self._entry_index >= len(entries) or entries[self._entry_index] is not self._entry: raise RuntimeError( "This handle no longer matches its scheduled entry — the sequence was " "modified after the handle was created. Re-schedule and use a fresh handle." ) return self._entry def vary(self, field: str, values: Any, *, name: str | None = None) -> BatchAxis: """Create a batch axis over one entry field. Parameters ---------- field : str Pulse or delay field name. values : array-like Values along the axis. name : str or None, default=None Display name; defaults to the normalized field. """ normalized = self._normalize_field(field) return BatchAxis( owner=self._sequence, target_kind="entry", field=normalized, values=values, name=name or normalized, entry_index=self._entry_index, entry=self._entry, ) def _normalize_field(self, field: str) -> str: raise NotImplementedError #: Pulse-record fields that rebind and sweep apart from envelope parameters. _PULSE_FIELDS = ("freq", "phase", "start_time", "detuning")
[docs] class PulseHandle(_BaseEntryHandle): """Reference to one scheduled pulse entry. Sweepable fields: ``freq``, ``phase``, ``start_time``, ``detuning``, and declared envelope parameters (for example ``amplitude``, ``duration``, ``sigmas``). Parameters ---------- sequence : QuantumSequence Sequence that owns the scheduled pulse. entry_index : int Pulse entry index in the sequence. """ _reserved_fields = _PULSE_FIELDS def _normalize_field(self, field: str) -> str: from quchip.control.sequence import _PulseEntry entry = self._resolve_entry() if not isinstance(entry, _PulseEntry): raise TypeError("PulseHandle does not point to a pulse entry") if field in self._reserved_fields: return field parameters = entry.envelope.parameter_values() if field not in parameters: sweepable = list(self._reserved_fields) + list(parameters) raise ValueError(f"Pulse field '{field}' is not sweepable. Available pulse fields: {sweepable}") return field
[docs] class DelayHandle(_BaseEntryHandle): """Reference to a scheduled delay entry. Only ``duration`` is sweepable. Parameters ---------- sequence : QuantumSequence Sequence that owns the scheduled delay. entry_index : int Delay entry index in the sequence. """ def _normalize_field(self, field: str) -> str: self._resolve_entry() if field != "duration": raise ValueError("Delay entries only support varying 'duration'") return field
def _expand_axis_overrides( axes: Sequence[BatchAxis | ZippedBatchAxis], ) -> tuple[tuple[int, ...], list[tuple[tuple[int, ...], dict[tuple[int | None, str], Any]]]]: """Expand batch axes into ``(shape, [(coord, overrides)...])``. ``overrides`` maps ``(entry_index, field) -> value`` for entry-level axes and ``(None, field) -> value`` for sequence-level axes. """ axis_slices: list[list[dict[tuple[int | None, str], Any]]] = [] for axis in axes: members = axis.axes if isinstance(axis, ZippedBatchAxis) else (axis,) axis_slices.append([ {member.override_key: member.values[i] for member in members} for i in range(axis.size) ]) return expand_axis_groups(axis_slices)