Source code for quchip.chip.transformations.active_patch

"""Spectator elimination: reduce a chip to kept devices, or a schedule to its active patch.

:func:`eliminate_spectators` removes the spectators of a set of kept devices
one at a time, in an order that the patch structure and the transition
energies set (PHYSICS.md §10.8). :func:`active_patch` keeps the devices that a
schedule touches, plus ``hops`` coupling-graph steps, and eliminates the other
spectators in the same way. The activity analysis does not rank devices by
amplitude, detuning, or an error bound. The results record per-step
Schrieffer-Wolff validity, and a poor fold emits a ``UserWarning``.
"""
# `_warn_on_poor_validity` emits the warning for a poor fold.

from __future__ import annotations

import warnings
from collections.abc import Iterable
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any

import numpy as np

from quchip.chip.partition import _line_device_labels
from quchip.utils.jax_utils import contains_tracer
from quchip.utils.labeling import resolve_label

if TYPE_CHECKING:
    from quchip.chip.chip import Chip
    from quchip.chip.transformations.elimination_order import EliminationOrder
    from quchip.chip.transformations.result import EliminationResult
    from quchip.control.sequence import QuantumSequence


[docs] def coupling_adjacency(chip: "Chip") -> dict[str, set[str]]: """Device-label adjacency of the chip's coupling graph.""" adjacency: dict[str, set[str]] = {d.label: set() for d in chip.devices} for coupling in chip.couplings: adjacency[coupling.device_a_label].add(coupling.device_b_label) adjacency[coupling.device_b_label].add(coupling.device_a_label) return adjacency
[docs] def active_labels(sequence: "QuantumSequence", *, hops: int = 1) -> set[str]: """Devices that the schedule touches, expanded ``hops`` coupling-graph steps. A pulse touches its target and the device whose frame its carrier follows, such as the target qubit of a cross-resonance tone. """ chip = sequence._chip active: set[str] = set() for op in sequence.scheduled_ops: target = op.target_label if target in chip.coupling_map: coupling = chip.coupling_map[target] active.update((coupling.device_a_label, coupling.device_b_label)) else: active.add(target) if op.frame is not None: active.add(op.frame) if not active: raise ValueError("The sequence has an empty schedule; there is no active patch to reduce to.") adjacency = coupling_adjacency(chip) frontier = set(active) for _ in range(hops): frontier = {n for label in frontier for n in adjacency[label]} - active if not frontier: break active |= frontier return active
def _warn_on_poor_validity(step: "EliminationResult", target: str, caller: str) -> None: """Warn (never raise) when a validity entry of a fold is poor. The reduction proceeds regardless, but a poor ``g/Δ``, or a poor ratio of a kept section or response, indicates low approximation quality for that fold. ``is_valid`` may be a *traced* JAX boolean when ``eliminate`` runs under ``jit``/``grad`` (:class:`~quchip.chip.transformations.result.EliminationResult`'s ``validity`` docstring) — branching a Python ``if`` on a tracer would concretize it, so the check is skipped entirely under tracing rather than forcing a concrete comparison. """ for coupling_label, entry in step.validity.items(): is_valid = entry.get("is_valid") if is_valid is None or contains_tracer(is_valid): continue if not is_valid: if "g_over_delta" in entry: problem = (f"folds coupling '{coupling_label}' with poor Schrieffer-Wolff validity " f"(g/Δ={entry['g_over_delta']!r}, needs < 0.1)") else: name = next(key for key in entry if key != "is_valid") problem = f"leaves '{coupling_label}' with {name}={entry[name]!r}, which needs < 0.1" warnings.warn( f"{caller}: eliminating '{target}' {problem}; proceeding " "anyway — inspect this step's effective_params and validity before use.", UserWarning, stacklevel=2, ) class _EliminationChain: """Views shared by the results of a chain of eliminations.""" chip: Any eliminated_labels: tuple[str, ...] steps: tuple @property def validity(self) -> dict[str, Any]: """``{eliminated label: that step's .validity}``, verbatim, with the per-coupling shape unchanged.""" return {label: step.validity for label, step in zip(self.eliminated_labels, self.steps)} @property def effective_params(self) -> dict[str, Any]: """``{eliminated label: that step's .effective_params}``, verbatim, with the per-survivor shape unchanged.""" return {label: step.effective_params for label, step in zip(self.eliminated_labels, self.steps)} @property def mapping(self) -> Any: """Compose the captured state/operator maps from the source into this chip.""" from quchip.chip.transformations.result import ReductionMap from quchip.utils.values import DeferredValue from functools import reduce from math import prod import jax.numpy as jnp maps = tuple(step.mapping for step in self.steps) if not maps: labels, dims = tuple(d.label for d in self.chip.devices), tuple(self.chip.dims) return ReductionMap(labels, dims, labels, dims, self.chip.backend, DeferredValue(lambda: jnp.eye(prod(dims)))) return ReductionMap(maps[0].source_labels, maps[0].source_dims, maps[-1].target_labels, maps[-1].target_dims, maps[0]._backend, DeferredValue(lambda: reduce(jnp.matmul, (m.embedding for m in maps))))
[docs] @dataclass(frozen=True) class SpectatorEliminationResult(_EliminationChain): """A chip reduced to kept devices, and the record of each elimination. Attributes ---------- chip The reduced chip: the kept devices plus the spectators that :func:`eliminate_spectators` did not eliminate (see :attr:`notes`). kept_labels Kept device labels, in chip order. eliminated_labels Device labels that were folded away, in elimination order. steps The :class:`~quchip.chip.transformations.result.EliminationResult` of each :func:`~quchip.chip.transformations.eliminate` call, verbatim and in :attr:`eliminated_labels` order. Read ``.validity`` and ``.effective_params`` of all steps through the properties below. notes Every explicitly dropped or deferred piece of physics, including spectators left in place, each step that breaks the precedence of the default order, and every step's notes. If elimination cannot continue past a spectator, it also includes why the reduction stopped early. omitted_shifts For each kept device that a Schrieffer-Wolff step reaches through a device that stays on the chip, the estimated shift of its transition that the chain omits, in GHz: the sum over steps of ``|J|^2 / (E_k - E_v)`` (PHYSICS.md §10.8). The reduced transition is off by about minus this value. A value is ``None`` when the estimate does not apply, and :attr:`notes` says why. The attribute is ``None`` when traced device parameters prevent the estimate. An empty dict means that no step found such a path, not that the chain omits nothing: the estimate reads number-conserving exchange elements only. """ chip: Any kept_labels: tuple[str, ...] eliminated_labels: tuple[str, ...] steps: tuple notes: tuple[str, ...] omitted_shifts: dict[str, float | None] | None = field(default_factory=dict)
[docs] @dataclass(frozen=True) class ActivePatchResult(_EliminationChain): """A reduced patch chip, the re-bound sequence, and the record of reduction validity. Attributes ---------- chip The patch chip: schedule-active devices plus the spectators that :func:`active_patch` did not eliminate. They are unreachable or declined by elimination itself (see :attr:`notes`). sequence A :class:`~quchip.control.sequence.QuantumSequence` bound to :attr:`chip`, which replays the source sequence's entries verbatim. Delays, barriers and virtual-Z entries on removed devices and stripped lines keep the source timing and have no other effect. active_labels Sorted schedule-active device labels (never eliminated). eliminated_labels Device labels that were folded away, in elimination order. steps The :class:`~quchip.chip.transformations.result.EliminationResult` from each successful :func:`~quchip.chip.transformations.eliminate` call, verbatim and in :attr:`eliminated_labels` order. Do not reshape them here. Read ``.validity``/``.effective_params`` from the step objects through the convenience properties below. notes Every explicitly dropped or deferred piece of physics, including unreachable spectators left in place, control lines stripped as unused, each step that breaks the precedence of the default order, and every step's notes. If elimination cannot continue past a spectator, it also includes why the reduction stopped early. omitted_shifts For each active device that a Schrieffer-Wolff step reaches through a device that stays on the chip, the estimated shift of its transition that the chain omits, in GHz: the sum over steps of ``|J|^2 / (E_k - E_v)`` (PHYSICS.md §10.8). The reduced transition is off by about minus this value. A value is ``None`` when the estimate does not apply, and :attr:`notes` says why. The attribute is ``None`` when traced device parameters prevent the estimate. An empty dict means that no step found such a path, not that the chain omits nothing: the estimate reads number-conserving exchange elements only. """ chip: Any sequence: Any active_labels: tuple[str, ...] eliminated_labels: tuple[str, ...] steps: tuple notes: tuple[str, ...] omitted_shifts: dict[str, float | None] | None = field(default_factory=dict)
[docs] def simulate(self, **kwargs: Any) -> Any: """Solve the patch sequence. Parameters ---------- **kwargs : Any Arguments forwarded to :meth:`QuantumSequence.simulate`. """ return self.sequence.simulate(**kwargs)
def _plan(chip: "Chip", kept: set[str], order: Iterable[Any] | None, caller: str) -> tuple["EliminationOrder | None", list[str], list[str]]: """Return the default-order model or ``None``, the devices due for elimination, and notes. The default order eliminates every spectator that shares a chain of neighbours with the kept devices. An explicit order eliminates exactly the devices it lists. Other spectators stay in place, with a note. """ from quchip.chip.transformations.elimination_order import EliminationOrder spectators = [d.label for d in chip.devices if d.label not in kept] notes: list[str] = [] if order is None: model = EliminationOrder(chip, tuple(label for label in (d.label for d in chip.devices) if label in kept)) doomed = [label for label in spectators if label in model.remaining] left = [label for label in spectators if label not in model.remaining] if left: notes.append( f"spectators {left} share no coupling, effective Hamiltonian or port path with the kept " "devices; left in place for the exact partitioner to split off at solve time" ) return model, doomed, notes doomed = [resolve_label(target) for target in order] unknown = [label for label in doomed if label not in chip.device_map] if unknown: raise KeyError(f"{caller}: order names no device {unknown} on chip '{chip.label}'.") if len(set(doomed)) != len(doomed): raise ValueError(f"{caller}: order lists a device more than once: {doomed}.") named = [label for label in doomed if label in kept] if named: raise ValueError(f"{caller}: order names kept devices {named}.") left = [label for label in spectators if label not in doomed] if left: notes.append(f"spectators {left} are not in order; left in place") return None, doomed, notes def _one_excitation_reader(chip: "Chip", method: str) -> Any: """Return a reader of the chip's single-excitation gaps and exchange elements, in GHz. It reads them at the approximation that the fold of ``method`` reads. """ def read(labels: tuple[str, ...]) -> tuple[dict[str, Any], dict[tuple[str, str], Any]]: from quchip.chip.transformations.eliminate_device import _one_excitation_elements from quchip.chip.transformations.methods import lookup_reduction_method from quchip.engine.assembly import _resolve_system # An unknown method leaves the chip's approximation here; eliminate() then rejects the method. reduction = lookup_reduction_method(method) approximation = chip.approximation if reduction is None else reduction.source_approximation(chip) return _one_excitation_elements(chip, _resolve_system(chip, chip.backend), labels, approximation, pairs=True) return read def _eliminate_chain(chip: "Chip", model: "EliminationOrder | None", doomed: list[str], method: str, local: bool, caller: str, kept: set[str] ) -> tuple[Any, list[Any], list[str], list[str], dict[str, float | None] | None]: """Fold ``doomed`` into ``chip`` one at a time, in the default order of ``model`` or in the given order. Stops early, without raising, if ``eliminate`` declines a step; everything folded before that stays folded. Each Schrieffer-Wolff step adds the shift that it omits from the ``kept`` devices to the estimate (PHYSICS.md §10.8). Returns ``(working_chip, steps, eliminated_labels, notes, omitted)``. ``omitted`` is ``None`` when traced values prevent the estimate. """ from quchip.chip.transformations import eliminate from quchip.chip.transformations.elimination_order import omitted_shifts working = chip steps: list[Any] = [] eliminated: list[str] = [] notes: list[str] = [] omitted: dict[str, float | None] | None = {} pending = list(doomed) while pending: note = None estimate = method == "sw" and omitted is not None labels = tuple(device.label for device in working.devices) gaps, raw = _one_excitation_reader(working, method)(labels) if model is not None or estimate else ({}, {}) def read(needed: tuple[str, ...], gaps: dict = gaps, raw: dict = raw) -> tuple[dict, dict]: return ({label: gaps[label] for label in needed}, {pair: value for pair, value in raw.items() if set(pair) <= set(needed)}) if model is None: target = pending[0] else: target, note = model.choose(read, caller) try: # Unsupported physical reductions leave the remaining spectators # in place. Invalid configuration raises ValueError and propagates. step = eliminate(working, target, method=method, local=local) except NotImplementedError as exc: notes.append( f"stopped eliminating spectators at '{target}' ({exc}); " f"{pending} kept on the chip as-is" ) break _warn_on_poor_validity(step, target, caller) if note is not None: notes.append(note) if estimate and contains_tracer((gaps, raw)): omitted = None notes.append("Omitted exchange shifts are not estimated, because device parameters are traced.") elif estimate: assert omitted is not None # estimate implies it; keeps mypy's Optional narrow position = {label: index for index, label in enumerate(labels)}.__getitem__ energies = {label: float(np.real(np.asarray(value))) for label, value in gaps.items()} elements = {pair: complex(np.asarray(value)) for pair, value in raw.items()} for label, (shift, mediators, reason) in omitted_shifts(target, kept, energies, elements, position).items(): names = ", ".join(map(repr, mediators)) if shift is None: omitted[label] = None notes.append(f"Eliminating {target!r} can omit a fourth-order exchange shift of the kept " f"{label!r} through {names}. It is not estimated, because {reason}. " "method='exact' with local=False keeps it.") continue total = omitted.get(label, 0.0) omitted[label] = None if total is None else total + shift notes.append(f"Eliminating {target!r} omits an estimated {1e6 * shift:+.1f} kHz from the transition " f"of the kept {label!r}, the fourth-order exchange shift through {names}. " "method='exact' with local=False keeps it.") steps.append(step) eliminated.append(target) notes.extend(step.notes) working = step.chip pending.remove(target) if model is not None: model.remove(target) return working, steps, eliminated, notes, omitted
[docs] def eliminate_spectators( chip: "Chip", keep: Any, *, order: Iterable[Any] | None = None, method: str = "sw", local: bool = False, ) -> SpectatorEliminationResult: """Reduce a chip to the kept devices by eliminating the other devices one at a time. Each step is one :func:`~quchip.chip.transformations.eliminate` call, so each keeps second-order terms in its own couplings. The order of the steps sets which fourth-order terms the chain keeps and, with ``local=True``, how far the operators of the kept devices spread along the captured maps (PHYSICS.md §10.8). By default, a spectator waits while a spectator neighbour links it to a kept device that is at least two times closer to it in transition frequency than that neighbour. Among the others, a device whose neighbours are all kept goes last, and the step that keeps the widest operator of a kept device smallest goes first. A step that must break the wait is recorded in :attr:`~SpectatorEliminationResult.notes`. The order is a heuristic for dispersive chips and does not bound the error. Each Schrieffer-Wolff step also estimates the shift that it omits from a kept device, in :attr:`~SpectatorEliminationResult.omitted_shifts`. Spectators that share no coupling, effective Hamiltonian or port path with the kept devices stay on the reduced chip, for the exact partitioner to split off at solve time. If ``eliminate`` declines a step with its typed :class:`NotImplementedError`, the chain stops there, and everything eliminated until then stays folded. Parameters ---------- chip Source chip (never mutated). keep The devices to keep, as labels or objects. order Devices to eliminate, in this order, as labels or objects. The other spectators stay on the reduced chip. The default order compares transition frequencies, so it needs concrete values. Under ``jax.jit`` or ``jax.grad`` with traced device parameters, pass the ``eliminated_labels`` of a call without tracing. method Forwarded to every :func:`~quchip.chip.transformations.eliminate` call. local Forwarded to every :func:`~quchip.chip.transformations.eliminate` call. Each step then computes its generator on the patch around the eliminated device. Returns ------- SpectatorEliminationResult Examples -------- >>> from quchip import Capacitive, Chip, DuffingTransmon, Resonator, eliminate_spectators >>> q = [DuffingTransmon(freq=5.0 + 0.1 * i, anharmonicity=-0.25, levels=3, label=f"q{i}") for i in range(3)] >>> c = [Resonator(freq=6.5 + 0.02 * i, levels=3, label=f"c{i}") for i in range(2)] >>> chip = Chip(q + c, [Capacitive(q[i + j], c[i], g=0.03) for i in range(2) for j in range(2)]) >>> result = eliminate_spectators(chip, ["q0", "q2"], local=True) >>> [d.label for d in result.chip.devices] ['q0', 'q2'] """ from quchip.engine.routes import require_workflow require_workflow(chip, "eliminate_spectators()") if isinstance(keep, str) or not isinstance(keep, Iterable): keep = (keep,) kept = {resolve_label(device) for device in keep} unknown = sorted(label for label in kept if label not in chip.device_map) if unknown: raise KeyError(f"eliminate_spectators(): keep names no device {unknown} on chip '{chip.label}'.") if not kept: raise ValueError("eliminate_spectators(): keep names no device.") model, doomed, notes = _plan(chip, kept, order, "eliminate_spectators()") working, steps, eliminated, chain_notes, omitted = _eliminate_chain(chip, model, doomed, method, local, "eliminate_spectators()", kept) notes.extend(chain_notes) if working is chip: working = chip.clone() return SpectatorEliminationResult( chip=working, kept_labels=tuple(d.label for d in chip.devices if d.label in kept), eliminated_labels=tuple(eliminated), steps=tuple(steps), notes=tuple(notes), omitted_shifts=omitted, )
def _strip_dead_control_lines(chip: "Chip", sequence: "QuantumSequence", doomed: list[str]) -> tuple[Any, list[str]]: """Detach control lines that only ever target a spectator due for elimination. Returns the chip to eliminate on next (a clone only if a line was actually stripped, else ``chip`` itself, untouched) and a note listing what was dropped, when anything was. """ equipment = chip.control_equipment if equipment is None: return chip, [] scheduled_drives = {op.drive_label for op in sequence.scheduled_ops} unused = sorted( ln.label for ln in equipment.lines if ln.label not in scheduled_drives and any(lbl in doomed for lbl in _line_device_labels(chip, ln)) ) if not unused: return chip, [] working = chip.clone() # chip.wire(*keep, signal_chain=...) is the natural-looking way to # strip lines, but chip.wire() treats an *empty* line list as "no # lines given" and reuses the existing connected set instead of # detaching everything — exactly backwards when every wired line # turns out to be unused. unwire() has no such trap: it removes one # line (and the signal-chain entries referencing it) at a time and # clears control_equipment to None once the last line is gone, so # it strips correctly whether one line or all of them are unused. for label in unused: working.unwire(label) return working, [f"dropped unused spectator control lines: {unused}"]
[docs] def active_patch( sequence: "QuantumSequence", *, hops: int = 1, method: str = "sw", local: bool = False, order: Iterable[Any] | None = None, ) -> ActivePatchResult: """Reduce a chip to its schedule-active patch by eliminating spectators. The spectators are eliminated one at a time through :func:`~quchip.chip.transformations.eliminate`, in the default order of :func:`eliminate_spectators` with the active devices kept. The result keeps every step's validity metrics unchanged. A kept device next to an active device, such as a coupler with ``hops=1``, can link a removed spectator to the active device. The step then omits their exchange shift, and :attr:`~ActivePatchResult.omitted_shifts` estimates it. The reduction is an explicit opt-in, because it approximates (Schrieffer-Wolff, or the exact dressed-spectrum route under ``method="exact"``), unlike the exact automatic partitioning that :meth:`~quchip.control.sequence.QuantumSequence.simulate` does internally at solve time. Bridging edges from earlier folds can be :class:`~quchip.chip.couplings.Capacitive` edges that carry ``g`` or :class:`~quchip.chip.couplings.TunableCapacitive` edges that carry ``g_0``. Both fold onward, so cycles among spectators reduce completely. ``eliminate`` can still decline an unsupported step, such as an unsupported accessible-field boundary, with its typed :class:`NotImplementedError`. The reduction then stops there, and everything eliminated until then stays folded. The remaining spectators, including the one that failed, stay unchanged on the patch chip, and :attr:`notes` records the reason. Parameters ---------- sequence The schedule to reduce around. hops Coupling-graph hops by which the active set expands beyond the scheduled targets (see :func:`active_labels`). method Forwarded to :func:`~quchip.chip.transformations.eliminate` for every device elimination. local Forwarded to :func:`~quchip.chip.transformations.eliminate`. Each step then computes its generator on the patch around the eliminated device. order Spectators to eliminate, in this order, as for :func:`eliminate_spectators`. The other spectators stay on the patch chip. Returns ------- ActivePatchResult """ from quchip.engine.routes import require_workflow chip = sequence._chip require_workflow(chip, "active_patch()") active = active_labels(sequence, hops=hops) model, doomed, notes = _plan(chip, active, order, "active_patch()") working, strip_notes = _strip_dead_control_lines(chip, sequence, doomed) notes.extend(strip_notes) working, steps, eliminated, elimination_notes, omitted = _eliminate_chain(working, model, doomed, method, local, "active_patch()", active) notes.extend(elimination_notes) if working is chip: working = chip.clone() patch_sequence = sequence._copy_with_chip(working) return ActivePatchResult( chip=working, sequence=patch_sequence, active_labels=tuple(sorted(active)), eliminated_labels=tuple(eliminated), steps=tuple(steps), notes=tuple(notes), omitted_shifts=omitted, )