Source code for quchip.chip.chip

"""Composite quantum system — :class:`Chip` bundles devices, couplings, and control.

A chip owns the devices (which own their local Hamiltonians), the
couplings between them (which own their two-body interaction
Hamiltonians), and, optionally, the :class:`ControlEquipment` wiring
classical control lines. The engine consumes a chip to produce a
solver-ready problem.

All public parameters — device frequencies, coupling strengths, drive
amplitudes, crosstalk coefficients — remain JAX-traceable and sweepable so a
single loss function can span any of them.
"""

from __future__ import annotations

from collections import Counter
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Literal, Mapping, Sequence, overload

import numpy as np

from quchip.backend import _backend_context
from quchip.backend.protocol import Backend, Operator, State
from quchip.approximations import Approximation, RWA, require_approximation
from quchip.chip.analysis import ChipAnalysis, DressedResult, KerrMatrix
from quchip.chip.baths import Bath
from quchip.chip.ports import Port
from quchip.chip.coupling_base import BaseCoupling
from quchip.chip.states import _DEFAULT_LEVEL_SYMBOLS
from quchip.control.drive import BaseDrive, CouplingDrive
from quchip.control.equipment import ControlEquipment
from quchip.control.signal import Crosstalk, SignalTransform
from quchip.declarative.expr import PhysicsExpr
from quchip.declarative.parameters import validate_sign
from quchip.devices.base import BaseDevice, _validate_noise_params
from quchip.utils.jax_utils import contains_tracer, maybe_concrete_scalar
from quchip.utils.labeling import LabelKeyedDict, resolve_label

if TYPE_CHECKING:
    from quchip.chip.partition import PartitionResult
    from quchip.engine.ir import EngineResult, FrameSpec, SolveProblem
    from quchip.results.results import SimulationBatchResult, SimulationResult
    from quchip.results.steady_state import SteadyStateResult


def _format_float(value: float | None) -> str:
    """Compact numeric formatter for status dashboards."""
    if value is None:
        return "n/a"
    return f"{float(value):.6g}"


def _is_scalar_like(value: Any) -> bool:
    """Return True for 0-dim arrays and plain Python numbers (used in frame spec)."""
    return getattr(value, "shape", None) == () or isinstance(value, (int, float))


def _same_concrete_value(a: Any, b: Any) -> bool:
    """True iff both are ``None`` or both concretize to equal scalars.

    Non-concrete (traced) values always compare as different, so traced
    writes are always applied and never hit a Python ``==`` on a tracer.
    """
    if a is None and b is None:
        return True
    if a is None or b is None:
        return False
    concrete_a = maybe_concrete_scalar(a)
    concrete_b = maybe_concrete_scalar(b)
    return concrete_a is not None and concrete_b is not None and concrete_a == concrete_b


def _as_physics_expr(
    authored: Any,
    backend: Backend,
    *,
    labels: tuple[str, ...],
    dims: tuple[int, ...],
    name: str,
) -> PhysicsExpr:
    """Return an authored expression or wrap a native matrix contribution."""
    if isinstance(authored, PhysicsExpr):
        return authored
    return PhysicsExpr.from_matrix(backend.to_array(authored), labels=labels, dims=dims, name=name)


def _concrete_cache_value(value: Any) -> Any:
    """Return one stable scalar cache value or raise for traced/non-scalars."""
    if value is None:
        return None
    concrete = maybe_concrete_scalar(value)
    if concrete is None:
        raise ValueError
    return concrete


def _operator_cache_value(value: Any) -> Any:
    """Return a content-based cache key for one concrete port operator."""
    if value is None or isinstance(value, str):
        return value
    operator = value
    if hasattr(operator, "matrix"):
        operator = operator.matrix()
    elif hasattr(operator, "to_jax"):
        operator = operator.to_jax()
    elif hasattr(operator, "full"):
        operator = operator.full()
    if contains_tracer(operator):
        raise ValueError
    try:
        array = np.asarray(operator)
    except (TypeError, ValueError) as error:
        raise ValueError from error
    return array.shape, array.dtype.str, array.tobytes()


def _frame_cache_value(frame: Any) -> Any:
    """Return a stable cache key for one concrete frame specification."""
    if isinstance(frame, str):
        return frame
    if isinstance(frame, Mapping):
        return tuple(
            sorted(
                (resolve_label(label), _concrete_cache_value(value))
                for label, value in frame.items()
            )
        )
    return _concrete_cache_value(frame)


[docs] class Chip: """Composite quantum system: devices + couplings + (optional) control. The chip is the primary user-facing object. It exposes: - structural access (:attr:`devices`, :attr:`couplings`, :attr:`dims`), - Hamiltonian construction (:meth:`hamiltonian`), - dressed-state analysis (:meth:`dress`, :meth:`energy`, :meth:`freq`, …) delegated to :class:`~quchip.chip.analysis.ChipAnalysis`, - state factories (:meth:`state`, :meth:`bare_state`), - observable construction (:meth:`observable`, :meth:`e_ops`), - solver surface (:meth:`solve`, :meth:`solve_many`, :meth:`steadystate`), - control wiring (:meth:`wire`, :meth:`connect`), - serialization / cloning (:meth:`to_dict`, :meth:`from_dict`, :meth:`clone`, :meth:`updated`). Methods that accept a device reference take either a device object **or** its label string; object references are preferred in examples. Parameters ---------- devices : list[BaseDevice] Ordered device list. Tensor-product position equals list index; labels must be unique. couplings : list[BaseCoupling], optional Two-body couplings. Each coupling's referenced devices must be in ``devices``. control_equipment : ControlEquipment, optional Aggregates drive lines and signal-chain transforms (crosstalk, delays, gains). Can also be attached later via :meth:`wire` or :meth:`connect`. label : str, optional Human-readable chip label. frame : FrameSpec Initial frame specification: - ``"lab"`` — all reference frequencies 0 GHz (default). - ``"rotating"`` — per-device rotating frame at dressed drive frequencies. - scalar-like — one shared reference frequency for all devices. - ``dict`` — per-device references keyed by label or device. approximation : Approximation Engine strategy applied after the complete authored Hamiltonian is assembled. Defaults to :class:`~quchip.approximations.RWA`. basis : {"native", "eigen"} Chip-wide local solver-basis policy. ``"native"`` preserves each device's authored coordinate basis; ``"eigen"`` transforms into its retained local energy subspace. A device-level ``basis`` overrides this policy. backend : str or Backend, optional Chip-specific backend. ``None`` uses the process default. baths : list[Bath], optional Shared or collective unobserved environments. ports : list[Port], optional Accessible Markovian input-output channels. Ports add collapse channels without adding Hilbert-space factors. Examples -------- >>> from quchip import DuffingTransmon, Resonator, Capacitive, Chip >>> q = DuffingTransmon(freq=5.0, anharmonicity=-0.25, levels=3, label="q") >>> r = Resonator(freq=7.0, levels=5, label="r") >>> chip = Chip([q, r], couplings=[Capacitive(q, r, g=0.05)]) >>> chip.energy({q: 1}) # dressed |1,0⟩ eigenvalue, GHz # doctest: +SKIP """ def __init__( self, devices: list[BaseDevice], couplings: list[BaseCoupling] | None = None, control_equipment: ControlEquipment | None = None, label: str | None = None, frame: FrameSpec = "lab", approximation: Approximation = RWA(), basis: Literal["native", "eigen"] = "native", backend: str | Backend | None = None, baths: list[Bath] | None = None, ports: list[Port] | None = None, ) -> None: duplicates = [lbl for lbl, count in Counter(d.label for d in devices).items() if count > 1] if duplicates: raise ValueError(f"Duplicate device labels: {duplicates}. All device labels must be unique.") self.label = label self._devices = tuple(devices) self._device_map: dict[str, BaseDevice] = {d.label: d for d in devices} self._label_to_index: dict[str, int] = {d.label: i for i, d in enumerate(devices)} self._couplings = tuple(couplings) if couplings else () coupling_labels = [c.label for c in self._couplings] dup = [lbl for lbl, n in Counter(coupling_labels).items() if n > 1] if dup: raise ValueError(f"Duplicate coupling labels: {dup}. Labels must be unique.") collision = [lbl for lbl in coupling_labels if lbl in self._device_map] if collision: raise ValueError( f"Coupling labels collide with device labels: {collision}. " "Device and coupling labels share one namespace so control targets resolve unambiguously." ) self._coupling_map: dict[str, BaseCoupling] = {c.label: c for c in self._couplings} bath_duplicates = [lbl for lbl, n in Counter(b.label for b in baths or ()).items() if n > 1] if bath_duplicates: raise ValueError(f"Duplicate bath labels: {bath_duplicates}. Each bath must have a unique label.") for bath in baths or (): self._validate_bath(bath) self._baths = tuple(baths) if baths else () port_duplicates = [lbl for lbl, n in Counter(port.label for port in ports or ()).items() if n > 1] if port_duplicates: raise ValueError(f"Duplicate port labels: {port_duplicates}. Each port must have a unique label.") for port in ports or (): if not isinstance(port, Port): raise TypeError(f"Expected a Port, got {type(port).__name__}: {port!r}") port.resolve_targets(self) self._ports = tuple(ports) if ports else () if basis not in ("native", "eigen"): raise ValueError(f"basis must be 'native' or 'eigen', got {basis!r}") self._basis = basis tuple(d.resolved_dimension(basis) for d in devices) self._frame_spec: FrameSpec = "lab" self._approximation = require_approximation(approximation) if control_equipment is not None: drive_duplicates = [ lbl for lbl, n in Counter(d.label for d in control_equipment.lines).items() if n > 1 ] if drive_duplicates: raise ValueError( f"Duplicate drive labels in equipment: {drive_duplicates}. " "Each drive must have a unique label." ) self._control_equipment = control_equipment self._analysis = ChipAnalysis(self) for device in self._devices: device._attach_chip(self) for coupling in self._couplings: coupling._resolve_devices(self._device_map) if backend is not None: from quchip.backend import _coerce_backend self._backend: Backend | None = _coerce_backend(backend) else: self._backend = None # String-state shorthand — populated by :meth:`set_state_order`. self._state_order: tuple[str, ...] | None = None self._level_symbols: dict[str, int] = dict(_DEFAULT_LEVEL_SYMBOLS) # Both snapshots are keyed by their complete structural inputs below; # values produced under a JAX trace are never retained. self._unresolved_hamiltonian_cache: tuple[Any, PhysicsExpr] | None = None self._resolved_result_cache: tuple[tuple[Any, ...], EngineResult] | None = None if frame != "lab": self.set_frame(frame) # ------------------------------------------------------------------ # Hamiltonian # ------------------------------------------------------------------
[docs] def unresolved_hamiltonian(self) -> PhysicsExpr: """Return the authored lab-frame static Hamiltonian. Embeds every device Hamiltonian into the full tensor space and adds each coupling's embedded interaction. Does **not** apply the rotating-frame transform or any drive envelopes. This is the exact authored lab-frame Hamiltonian. RWA and frame transformations are applied only inside the engine. At solve time, the engine subtracts ``2π Σ_i ω_ref,i n̂_i`` for each device, where ``ω_ref,i`` is the frame reference resolved by :func:`quchip.engine.frames.resolve_frame`. The 2π boundary crossing and the subtraction both live in :func:`quchip.engine.assembly._build_static_h0`. Use :meth:`frame_info` to inspect which reference frequency each device will use. Returns ------- PhysicsExpr Symbolic Hamiltonian on ``⨂_d H_d``. Call ``.matrix()`` for a dense numerical view using current bindings. """ backend = self.backend signature = ( type(backend).__qualname__, tuple((d.label, d.state_version) for d in self._devices), tuple((c.label, c.state_version) for c in self._couplings), ) cache = self._unresolved_hamiltonian_cache if cache is not None and cache[0] == signature and not contains_tracer(cache[1]): return cache[1] with _backend_context(backend): labels = tuple(device.label for device in self._devices) H: PhysicsExpr | None = None for i, dev in enumerate(self._devices): h_local = _as_physics_expr( dev.unresolved_hamiltonian(), backend, labels=(dev.label,), dims=(dev.local_space().dimension,), name=rf"\hat H_{{{dev.label}}}", ) h_emb = h_local.embed(labels, self.authored_dims) H = h_emb if H is None else H + h_emb for coupling in self._couplings: idx_a = self._label_to_index[coupling.device_a_label] idx_b = self._label_to_index[coupling.device_b_label] local_labels = (coupling.device_a_label, coupling.device_b_label) local_dims = ( self._devices[idx_a].local_space().dimension, self._devices[idx_b].local_space().dimension, ) h_int = _as_physics_expr( coupling.interaction_hamiltonian(), backend, labels=local_labels, dims=local_dims, name=rf"\hat H_{{{coupling.label}}}", ) H = H + h_int.embed(labels, self.authored_dims) assert H is not None # Do not cache expressions whose values belong to a JAX trace. if not contains_tracer(H.numeric_values()): self._unresolved_hamiltonian_cache = (signature, H) return H
[docs] def hamiltonian(self) -> PhysicsExpr: """Return the Hamiltonian after the chip's engine policies.""" return self.resolve().hamiltonian()
[docs] def resolve( self, *, frame: FrameSpec | None = None, approximation: Approximation | None = None, ) -> EngineResult: """Return a frozen engine contract without mutating chip intent. ``frame=None`` uses :attr:`frame`; an explicit frame resolves this snapshot only. ``approximation=None`` likewise uses the chip default. Neither override mutates chip intent. """ from quchip.engine.assembly import ( _prepare_engine_assembly, build_engine_result, ) frame_spec = self.frame if frame is None else frame strategy = self.approximation if approximation is None else require_approximation(approximation) equipment = self.control_equipment control_lines = () if equipment is None else equipment.lines try: signature = ( id(self.backend), strategy, self.basis, _frame_cache_value(frame_spec), tuple((device.label, device.state_version) for device in self.devices), tuple( (device.label, _concrete_cache_value(device._reference_freq_override)) for device in self.devices ), tuple( (device.label, id(type(device).dissipation)) for device in self.devices ), tuple((coupling.label, coupling.state_version) for coupling in self.couplings), tuple( ( line.label, type(line), line.target_label, tuple( (name, _concrete_cache_value(getattr(line, name))) for name in line.parameter_values() ), id(type(line).dissipation), ) for line in control_lines ), tuple( ( bath.label, bath.recipe, tuple(bath.resolve_targets(self)), _concrete_cache_value(bath.temperature), _concrete_cache_value(bath.rate), ) for bath in self.baths ), tuple( ( port.label, tuple(port.resolve_targets(self)), tuple( (name, _concrete_cache_value(value)) for name, value in port.parameter_values().items() ), _operator_cache_value(port.operator), ) for port in self.ports ), ) except ValueError: signature = None cache = self._resolved_result_cache if signature is not None and cache is not None and cache[0] == signature: return cache[1] local_resolution, resolved_frame = _prepare_engine_assembly(self, frame_spec) result = build_engine_result( self, [], resolved_frame=resolved_frame, approximation=strategy, _local_resolution=local_resolution, ) if signature is not None and not result._contains_tracer(): self._resolved_result_cache = (signature, result) return result
# ------------------------------------------------------------------ # Component physics enumeration (consumed by the engine) # ------------------------------------------------------------------ # # The chip is the single place that knows which component families # exist (devices, couplings, baths, drive lines). These methods hand # the engine flat lists of local contributions tagged with their # *support* — the device indices each operator acts on — so the engine # embeds by arity and never enumerates families itself. Adding a new # physics-bearing component family is therefore a chip-side change; it # never requires modifying the engine.
[docs] def dynamic_contributions(self) -> list[tuple[Operator, Any, tuple[int, ...], Any, str, str]]: """Chip-owned time-dependent Hamiltonian contributions with their support. Returns ``(local_op, time_dependence, support, owner, origin, tag)`` tuples: one support index for a device-local operator, two for a coupling's two-body operator. Operators are lab-frame, ordinary GHz — the engine applies the 2π boundary. Drive terms are *not* included; they are schedule-owned and enter through engine assembly's drive compilation. """ backend = self.backend out: list[tuple[Operator, Any, tuple[int, ...], Any, str, str]] = [] with _backend_context(backend): for coupling in self._couplings: terms = coupling._time_terms() if not terms: continue idx_a = self._label_to_index[coupling.device_a_label] idx_b = self._label_to_index[coupling.device_b_label] out.extend( ( term.operator, term.coefficient, (idx_a, idx_b), coupling, "coupling", "coupling_dynamic", ) for term in terms ) for i, dev in enumerate(self._devices): out.extend( (term.operator, term.coefficient, (i,), dev, "device", "device_dynamic") for term in dev._time_terms() ) return out
[docs] def collapse_contributions( self, bases: Mapping[str, Any] | None = None, ) -> list[tuple[Operator, Any, tuple[int, ...], str, str, tuple[str, ...]]]: """Every Lindblad collapse operator on the chip, with its support. Returns ``(operator, rate, support, source, channel, parameter_paths)`` tuples: one device index for a device- or drive-line-local operator, two for a coupling's two-body operator, and an empty tuple for an operator already embedded in the full space (baths). Rates are in 1/ns, Lindblad-ready — each component owns its rate physics, including any intrinsic 2π (e.g. a resonator's κ = 2π·f/Q). Every returned operator is authored locally and transformed by the engine into the same fixed local solver bases as the Hamiltonian. This is the standard local-Lindblad approximation (Breuer & Petruccione, *The Theory of Open Quantum Systems*, Oxford, 2002, Ch. 3) rather than a dressed-basis (polaron-frame) master equation, and applies chip-wide regardless of which components carry noise — see the ``"chip"`` entry of :meth:`physics_notes`. """ return [ (operator, rate, support, source, channel, parameter_paths) for operator, rate, support, source, channel, parameter_paths, _owner in ( self._collapse_contributions_with_owners(bases) ) ]
def _collapse_contributions_with_owners( self, bases: Mapping[str, Any] | None = None, ) -> list[tuple[Operator, Any, tuple[int, ...], str, str, tuple[str, ...], Any]]: """Return collapse contributions with exact component ownership for assembly.""" backend = self.backend out: list[ tuple[Operator, Any, tuple[int, ...], str, str, tuple[str, ...], Any] ] = [] with _backend_context(backend): for i, dev in enumerate(self._devices): out.extend( ( channel.operator, channel.rate, (i,), dev.label, channel.name, parameter_paths, dev, ) for channel, parameter_paths in dev._collapse_channels_with_paths( None if bases is None else bases[dev.label] ) ) if self.control_equipment is not None: for line in self.control_equipment.lines: if line.device_label is None: continue idx = self._label_to_index[line.device_label] device = self._devices[idx] for channel, parameter_paths in line._collapse_channels_with_paths(device): out.append( ( channel.operator, channel.rate, (idx,), line.label, channel.name, parameter_paths, line, ) ) for coupling in self._couplings: idx_a = self._label_to_index[coupling.device_a_label] idx_b = self._label_to_index[coupling.device_b_label] for channel, parameter_paths in coupling._collapse_channels_with_paths(): out.append( ( channel.operator, channel.rate, (idx_a, idx_b), coupling.label, channel.name, parameter_paths, coupling, ) ) for bath in self.baths: for channel, parameter_paths in bath._collapse_channels_with_paths(self, bases): out.append( ( channel.operator, channel.rate, (), bath.label, channel.name, parameter_paths, bath, ) ) for port in self.ports: support = tuple(self._label_to_index[label] for label in port.resolve_targets(self)) for channel, parameter_paths in port._collapse_channels_with_paths(self): out.append( ( channel.operator, channel.rate, support, port.label, channel.name, parameter_paths, port, ) ) return out # ------------------------------------------------------------------ # Structural properties # ------------------------------------------------------------------ @property def device_map(self) -> dict[str, BaseDevice]: """Label → device mapping (the chip's own dict; do not mutate).""" return self._device_map @property def coupling_map(self) -> dict[str, "BaseCoupling"]: """Couplings by label, insertion-ordered (do not mutate).""" return self._coupling_map
[docs] def coupling(self, coupling: "str | BaseCoupling") -> "BaseCoupling": """Return a coupling by label or object.""" label = resolve_label(coupling) if label not in self._coupling_map: raise KeyError( f"No coupling labeled '{label}' on this chip. Available: {list(self._coupling_map.keys())}" ) return self._coupling_map[label]
@property def backend(self) -> Backend: """Active backend: per-call override > chip-specific > process default. The per-call override is the ``backend=`` argument of :meth:`~quchip.control.sequence.QuantumSequence.simulate` / ``simulate_batch`` (scoped through :func:`quchip.backend._backend_context`); it outranks even a chip-constructed backend so one chip can serve, e.g., QuTiP sweeps and dynamiqs gradient solves without global state flips. """ from quchip.backend import _backend_override, get_default_backend override = _backend_override.get() if override is not None: return override if self._backend is not None: return self._backend return get_default_backend() @property def frame(self) -> FrameSpec: """Current frame specification (see :meth:`set_frame`).""" return self._frame_spec @property def approximation(self) -> Approximation: """Default engine approximation strategy.""" return self._approximation @property def control_equipment(self) -> ControlEquipment | None: """Attached control equipment, if any.""" return self._control_equipment @property def devices(self) -> tuple[BaseDevice, ...]: """Ordered tuple of devices; index matches tensor-product position.""" return self._devices @property def couplings(self) -> tuple[BaseCoupling, ...]: """Tuple of couplings in insertion order.""" return self._couplings @property def dims(self) -> tuple[int, ...]: """Per-device Hilbert-space dimensions (same order as :attr:`devices`).""" return tuple(device.resolved_dimension(self._basis) for device in self._devices) @property def authored_dims(self) -> tuple[int, ...]: """Per-device dimensions of the exact authored local spaces.""" return tuple(device.local_space().dimension for device in self._devices) @property def basis(self) -> Literal["native", "eigen"]: """Chip-wide local solver-basis policy inherited by devices.""" return self._basis
[docs] def resolve_basis(self, device: str | BaseDevice) -> Literal["native", "eigen"]: """Resolve a device override against the chip basis policy.""" resolved = self[device] return resolved.resolved_basis(self._basis)
@property def total_dim(self) -> int: """Total Hilbert-space dimension (product of per-device :attr:`dims`).""" return int(np.prod(self.dims, dtype=int)) @property def baths(self) -> tuple[Bath, ...]: """Chip-level baths (shared/collective dissipation), insertion order.""" return self._baths @property def ports(self) -> tuple[Port, ...]: """Accessible Markovian input-output channels, in declaration order.""" return self._ports
[docs] def port(self, port: str | Port) -> Port: """Return one declared port by object or label.""" label = resolve_label(port) for candidate in self._ports: if candidate.label == label: return candidate raise KeyError(f"No port labeled '{label}'. Available: {[candidate.label for candidate in self._ports]}")
[docs] def add_bath(self, bath: Bath) -> Bath: """Attach a bath to this chip and return it (for fluent use). Baths may be added at any time after construction — the next simulate/solve collects the bath's collapse operators automatically, no rebuild needed. The bath is validated immediately: a non-``Bath`` argument, a target label not on this chip, or a label colliding with an already-attached bath fails here rather than cryptically at solve time (or, for a label collision, silently overwriting an entry on the :meth:`physics_notes` audit surface). """ self._validate_bath(bath) if bath.label in {b.label for b in self._baths}: raise ValueError( f"Duplicate bath label: '{bath.label}' is already attached to this chip. " "Each bath must have a unique label." ) self._baths = (*self._baths, bath) return bath
def _validate_bath(self, bath: Bath) -> None: """Reject non-Bath objects and unknown target labels at attach time. Devices are fixed at construction, so an unknown target can never become valid later — failing here beats an opaque error at solve time. """ if not isinstance(bath, Bath): raise TypeError(f"Expected a Bath, got {type(bath).__name__}: {bath!r}") unknown = [lbl for lbl in bath.resolve_targets(self) if lbl not in self._device_map] if unknown: raise ValueError( f"Bath {bath.label!r} targets unknown device(s) {unknown}; " f"this chip has {sorted(self._device_map)}" )
[docs] def set_noise( self, config: Mapping[str | BaseDevice, Mapping[str, Any]] | None = None, *, baths: list[Bath] | None = None, ) -> None: """Replace this chip's entire noise description in one call. The call is the chip's complete noise state: for every device, each noise parameter (see :meth:`~quchip.devices.base.BaseDevice.noise_parameter_names`) is set from *config* when given and reset to ``None`` otherwise, and the chip's baths become exactly *baths* (omitted → no baths). ``chip.set_noise()`` therefore clears all noise. Re-running the same call is a silent no-op, so notebook cells converge instead of stacking duplicate baths. Only dissipation knobs are reachable — a key that is not a noise parameter of that device (e.g. ``freq``) raises, so an optimized Hamiltonian cannot be perturbed by this call. The full target state is validated *before* anything is written (the same checks the constructors run); on error the chip is left exactly as it was. Applied changes are printed one per line; a no-op prints nothing. Custom dissipation is supported by declaring it on the device class beforehand with a ``parameter(noise=True)`` rate field and a :meth:`~quchip.devices.base.BaseDevice.dissipation` override. The declared rate is then an ordinary noise parameter here: sweepable, differentiable, serializable. Runtime closure-style channels are deliberately not supported — their rates could be neither swept, nor differentiated, nor serialized. Parameters ---------- config : mapping, optional ``{device_or_label: {noise_param: value}}``. Devices may appear as objects or labels, once each. baths : list[Bath], optional The chip's complete new bath list (validated like :meth:`add_bath`). """ # Resolve config keys (objects or labels; each device at most once). resolved: dict[str, Mapping[str, Any]] = {} for key, params in (config or {}).items(): label = resolve_label(key) if label not in self._device_map: raise ValueError(f"Unknown device {label!r}; this chip has {sorted(self._device_map)}") if label in resolved: raise ValueError(f"Device {label!r} appears more than once in the noise config") resolved[label] = params # Validate the complete target state before writing anything. new_baths = list(baths) if baths else [] bath_duplicates = [lbl for lbl, n in Counter(b.label for b in new_baths).items() if n > 1] if bath_duplicates: raise ValueError(f"Duplicate bath labels: {bath_duplicates}. Each bath must have a unique label.") for bath in new_baths: self._validate_bath(bath) targets: dict[str, dict[str, Any]] = {} for label, device in self._device_map.items(): names = type(device).noise_parameter_names() given = resolved.get(label, {}) unknown = sorted(set(given) - set(names)) if unknown: raise ValueError( f"{unknown} are not noise parameters of {label!r} " f"({type(device).__name__}); valid: {sorted(names)}" ) targets[label] = {name: given.get(name) for name in names} for label, target in targets.items(): device = self._device_map[label] _validate_noise_params(target.get("T1"), target.get("T2"), target.get("thermal_population")) fields = getattr(type(device), "__quchip_param_fields__", {}) for name, value in target.items(): spec = fields.get(name) if spec is not None: validate_sign(name, spec, value) # Apply: ordinary tracked writes; only real changes touch a device, # so an identical call is a true no-op (no state_version bumps). changes: list[str] = [] for label, target in targets.items(): device = self._device_map[label] for name in sorted(target, key=lambda n: n == "T2"): # T2 last: its validator reads the final T1 old = getattr(device, name, None) new = target[name] if _same_concrete_value(old, new): continue setattr(device, name, new) changes.append(f"{label}: {name} {old!r}{new!r}") old_ids = {id(bath) for bath in self._baths} new_ids = {id(bath) for bath in new_baths} for bath in self._baths: if id(bath) not in new_ids: changes.append(f"baths: - {bath.label} (model={bath.recipe})") for bath in new_baths: if id(bath) not in old_ids: extra = f", {bath.temperature} mK" if bath.temperature is not None else "" changes.append(f"baths: + {bath.label} (model={bath.recipe}{extra})") self._baths = tuple(new_baths) for line in changes: print(line)
@property def crosstalks(self) -> list[Crosstalk]: """Convenience view — :class:`Crosstalk` entries from the signal chain.""" if self._control_equipment is None: return [] return self._control_equipment.crosstalks
[docs] def device_index(self, label: str | BaseDevice) -> int: """Tensor-product index of a device. Accepts a label string or object.""" return self._resolve_device_index(label)[0]
def _resolve_device_index(self, device: str | BaseDevice) -> tuple[int, BaseDevice]: """Return ``(index, device)`` for a label string or a device object.""" label = resolve_label(device) idx = self._label_to_index.get(label) if idx is None: raise ValueError(f"Device '{label}' not found. Available: {list(self._device_map.keys())}") return idx, self._devices[idx] def __getitem__(self, label: str | BaseDevice) -> BaseDevice: try: return self._resolve_device_index(label)[1] except ValueError as err: raise KeyError(str(err)) from None # ------------------------------------------------------------------ # Viz — lazy delegates # ------------------------------------------------------------------
[docs] def plot_graph( self, path: str = "chip_topology.html", *, full: bool = True, exclude: set[str] | None = None, values: str = "bare", **kwargs: Any, ) -> str: """Render chip topology — delegates to :mod:`quchip.viz.chip`.""" from quchip.viz.chip import plot_graph return plot_graph( self, path, full=full, exclude=exclude, values=values, **kwargs, )
[docs] def plot_energy_levels(self, *, ax: Any = None, **kwargs: Any) -> Any: """Render dressed spectrum — delegates to :mod:`quchip.viz.chip`.""" from quchip.viz.chip import plot_energy_levels return plot_energy_levels(self, ax=ax, **kwargs)
# ------------------------------------------------------------------ # Frame management # ------------------------------------------------------------------
[docs] def set_frame(self, frame: FrameSpec) -> None: """Set the frame used when assembling simulation inputs. Supported values: - ``"lab"`` — all reference frequencies are 0.0 GHz. - ``"rotating"`` — per-device references use dressed drive frequencies. - scalar-like — shared reference frequency for all devices. - ``dict`` — per-device references keyed by label or device. Frame changes never alter dressed-state data — dressing is always computed from the lab-frame static Hamiltonian. """ if isinstance(frame, str): if frame not in ("lab", "rotating"): raise ValueError(f"frame string must be one of 'lab' or 'rotating', got {frame!r}") self._frame_spec = frame return if _is_scalar_like(frame): self._frame_spec = frame return if isinstance(frame, dict): self._frame_spec = {resolve_label(key): value for key, value in frame.items()} return raise TypeError( f"frame must be 'lab', 'rotating', a scalar-like frequency, or " f"dict[str|BaseDevice, scalar-like], got {type(frame).__name__}" )
# ------------------------------------------------------------------ # Dressed-state analysis — delegates to ChipAnalysis # ------------------------------------------------------------------ @property def analysis(self) -> ChipAnalysis: """Dressed-state analysis namespace — the chip's :class:`ChipAnalysis`. Canonical entry point for the full dressed-analysis surface (power users, less-common methods). The common quantities are also exposed as flat ``chip.*`` forwarders — :meth:`energy`, :meth:`freq`, :meth:`dress`, :meth:`dispersive_shift`, … — which delegate here; reach for ``chip.analysis`` for everything else. """ return self._analysis def _canonical_bare_labels(self) -> tuple[tuple[int, ...], ...]: """Internal delegate used by sweep/result utilities.""" return self._analysis._canonical_bare_labels()
[docs] def dress( self, *, overlap_threshold: float = 0.5, force: bool = False, labeling: str = "DE", ) -> DressedResult: """Compute (or retrieve) the dressed-state decomposition. See :meth:`ChipAnalysis.dress`.""" return self._analysis.dress( overlap_threshold=overlap_threshold, force=force, labeling=labeling, )
def _ensure_dressed(self) -> DressedResult: return self._analysis._ensure_dressed() @property def is_dressed(self) -> bool: """Whether a valid dressed-state result is cached. See :attr:`ChipAnalysis.is_dressed`.""" return self._analysis.is_dressed
[docs] def energy( self, device_states: Mapping[str | BaseDevice, int] | None = None, /, **device_state_kwargs: int, ) -> float: """Dressed eigenenergy (GHz) for a bare-state label. See :meth:`ChipAnalysis.energy`.""" return self._analysis.energy(device_states, **device_state_kwargs)
[docs] def dressed_spectrum(self) -> Any: """Raw dressed eigenvalue array without Python scalar coercion. See :meth:`ChipAnalysis.dressed_spectrum`.""" return self._analysis.dressed_spectrum()
[docs] def dressed_index( self, device_states: Mapping[str | BaseDevice, int] | None = None, /, **device_state_kwargs: int, ) -> int | None: """Dressed-state index matching a bare-state label, or ``None``. See :meth:`ChipAnalysis.dressed_index`.""" return self._analysis.dressed_index(device_states, **device_state_kwargs)
[docs] def bare_label(self, dressed_index: int) -> tuple[int, ...]: """Bare-state label assigned to a dressed-state index. See :meth:`ChipAnalysis.bare_label`.""" return self._analysis.bare_label(dressed_index)
[docs] def operator_in_dressed_basis( self, device: str | BaseDevice, op: str | Any, *, truncate: int | None = None, ) -> Operator: """Embedded device operator transformed to the dressed eigenbasis. See :meth:`ChipAnalysis.operator_in_dressed_basis`. """ return self._analysis.operator_in_dressed_basis(device, op, truncate=truncate)
[docs] def drive_matrix_elements( self, transition: str | BaseDevice | tuple[Mapping[str | BaseDevice, int], Mapping[str | BaseDevice, int]], *, drives: Sequence[str | BaseDrive] | None = None, ) -> LabelKeyedDict: """Return ``<final~|D_j|initial~>`` for wired drive lines. The final dressed state is the row index and the initial dressed state is the column index. In the weak-drive projection these matrix elements set the effective driven-Hamiltonian coefficients. See E. Magesan and J. M. Gambetta, Phys. Rev. A 101, 052308 (2020), DOI 10.1103/PhysRevA.101.052308, and :meth:`ChipAnalysis.drive_matrix_elements` for the transition shorthand, parameters, return type, and error conditions. """ return self._analysis.drive_matrix_elements(transition, drives=drives)
[docs] def state_components( self, state: int | Mapping[str | BaseDevice, int] | None = None, /, *, n_components: int = 5, **device_state_kwargs: int, ) -> dict[tuple[int, ...], float]: """Leading bare-basis probabilities for a dressed eigenstate. See :meth:`ChipAnalysis.state_components`.""" return self._analysis.state_components( state, n_components=n_components, **device_state_kwargs, )
[docs] def dispersive_shift(self, device_a: str | BaseDevice, device_b: str | BaseDevice) -> float: """Dressed cross-Kerr shift (GHz): ``E(1,1) − E(1,0) − E(0,1) + E(0,0)``. See :meth:`ChipAnalysis.dispersive_shift`. """ return self._analysis.dispersive_shift(device_a, device_b)
# ``static_zz`` is the same physics under a different name: the static ZZ # interaction strength equals the dressed dispersive (cross-Kerr) shift. static_zz = dispersive_shift zz = dispersive_shift
[docs] def kerr_matrix(self) -> KerrMatrix: """Return the labeled dressed self-Kerr and cross-Kerr matrix in GHz. See :meth:`ChipAnalysis.kerr_matrix`. """ return self._analysis.kerr_matrix()
[docs] def dressed_anharmonicity(self, device: str | BaseDevice) -> float: """Dressed anharmonicity of one device with others grounded (GHz). See :meth:`ChipAnalysis.dressed_anharmonicity`. """ return self._analysis.dressed_anharmonicity(device)
[docs] def transition_frequency( self, target: str | BaseDevice, lower: int, upper: int, when: dict[str | BaseDevice, int] | None = None, ) -> Any: """Return a dressed transition in GHz with optional spectator levels. Unspecified spectators are in their ground state. ``target`` and entries in ``when`` accept either device objects or labels. """ return self._analysis.transition_frequency( target, lower, upper, when=when, )
[docs] def effective_subspace_hamiltonian( self, states: ( list[Mapping[str | BaseDevice, int] | tuple[int, ...]] | tuple[Mapping[str | BaseDevice, int] | tuple[int, ...], ...] ), ) -> Any: """Dressed effective Hamiltonian in a labeled bare subspace. See :meth:`ChipAnalysis.effective_subspace_hamiltonian`. """ return self._analysis.effective_subspace_hamiltonian(states)
@overload def freq( self, target: None = ..., when: dict[str | BaseDevice, int] | None = ..., ) -> dict[str, float]: ... @overload def freq( self, target: str | BaseDevice, when: dict[str | BaseDevice, int] | None = ..., ) -> float: ...
[docs] def freq( self, target: str | BaseDevice | None = None, when: dict[str | BaseDevice, int] | None = None, ) -> dict[str, float] | float: """All dressed 0→1 frequencies (GHz), or one conditional transition. See :meth:`ChipAnalysis.freq`. Overloaded: no ``target`` returns the full ``{label: freq}`` dict; a single ``target`` (label or device) returns one scalar 0→1 frequency. Under ``jax.jit`` the scalar is a traced 0-d array; the overload is type-only and preserves traceability. """ return self._analysis.freq(target, when=when)
[docs] def frame_info(self) -> dict[str, Any]: """Per-device frame reference frequency ``ω_ref,i`` (GHz). See :meth:`ChipAnalysis.frame_info`.""" return self._analysis.frame_info()
[docs] def physics_notes(self) -> dict[str, list[str]]: """Aggregate :meth:`physics_notes` across every component. Returns a dict keyed ``"chip"`` for the chip-level entry, and ``"<kind>:<label>"`` — ``kind`` one of ``"device"``, ``"coupling"``, ``"drive"``, ``"bath"``, ``"port"`` — for every component, mapping to that component's declared approximations: Hilbert truncation, model regime, RWA status, noise-channel selection, and any other non-obvious assumption the component explicitly declares. Keys are kind-qualified rather than bare labels because the label namespaces are *not* globally disjoint — a device, coupling, drive, and bath may share a label — and this is an audit surface, so one component's entry silently overwriting another's is unacceptable. Drives are enumerated from :attr:`control_equipment`'s wiring rather than per-device ``connected_drives``, so an edge-target :class:`~quchip.control.drive.ParametricDrive` (pumping a coupling, not a device) is included too. The chip-level entry keyed ``"chip"`` states the local-Lindblad approximation every collapse operator this chip assembles is built under (see :meth:`collapse_contributions`) — present even with no baths, since it applies regardless. Intended for inspection/audit rather than for runtime dispatch. """ notes: dict[str, list[str]] = { "chip": [ "Collapse operators are authored per component, transformed into the fixed " "local solver bases, and combined with the interacting Hamiltonian — the " "local-Lindblad approximation, not a global dressed-basis master equation." ] } for device in self._devices: notes[f"device:{device.label}"] = list(device.physics_notes()) if self.control_equipment is not None: for line in self.control_equipment.lines: notes[f"drive:{line.label}"] = list(line.physics_notes()) for coupling in self._couplings: notes[f"coupling:{coupling.label}"] = list(coupling.physics_notes()) for bath in self.baths: notes[f"bath:{bath.label}"] = list(bath.physics_notes()) for port in self.ports: notes[f"port:{port.label}"] = list(port.physics_notes()) return notes
# ------------------------------------------------------------------ # Serialization and cloning # ------------------------------------------------------------------
[docs] def to_dict(self) -> dict[str, Any]: """Serialize chip topology into a JSON-safe dictionary.""" from quchip.chip.serialization import serialize_chip return serialize_chip(self)
[docs] @classmethod def from_dict(cls, d: dict[str, Any]) -> "Chip": """Reconstruct a chip from :meth:`to_dict` output.""" from quchip.chip.serialization import deserialize_chip return deserialize_chip(d)
[docs] def clone(self) -> "Chip": """Isolated structural clone suitable for sweep evaluation.""" from quchip.chip.serialization import clone_chip return clone_chip(self)
def _parameter_targets(self) -> dict[str, tuple[str, int, str, Any]]: """Map public parameter paths to component-owned local values.""" targets: dict[str, tuple[str, int, str, Any]] = {} def add(path: str, target: tuple[str, int, str, Any]) -> None: if path in targets: raise ValueError( f"Parameter path {path!r} is ambiguous; give the colliding components distinct labels." ) targets[path] = target for index, device in enumerate(self._devices): for name, value in device.parameter_values().items(): add(f"{device.label}.{name}", ("device", index, name, value)) for index, coupling in enumerate(self._couplings): for name, value in coupling.parameter_values().items(): add(f"{coupling.label}.{name}", ("coupling", index, name, value)) if self._control_equipment is not None: for index, line in enumerate(self._control_equipment.lines): for name, value in line.parameter_values().items(): add(f"drive.{line.label}.{name}", ("drive", index, name, value)) for index, transform in enumerate(self._control_equipment.signal_chain): for name, value in transform.parameter_values().items(): add(f"control.{index}.{name}", ("control", index, name, value)) for index, bath in enumerate(self._baths): for name, value in bath.parameter_values().items(): add(f"bath.{bath.label}.{name}", ("bath", index, name, value)) for index, port in enumerate(self._ports): for name, value in port.parameter_values().items(): add(f"port.{port.label}.{name}", ("port", index, name, value)) return targets @property def parameters(self) -> Mapping[str, Any]: """Bindable numerical values keyed by stable component paths.""" return MappingProxyType({path: target[3] for path, target in self._parameter_targets().items()}) @property def settings(self) -> Mapping[str, Any]: """Read-only structural choices that are not numerical fit parameters.""" equipment = self._control_equipment lines = () if equipment is None else equipment.lines signal_chain = () if equipment is None else equipment.signal_chain return MappingProxyType( { "devices": tuple( (device.label, type(device).__name__, device.resolved_dimension(self._basis)) for device in self._devices ), "basis": self._basis, "authored_dims": self.authored_dims, "device_bases": tuple( (device.label, self.resolve_basis(device)) for device in self._devices ), "couplings": tuple( ( coupling.label, type(coupling).__name__, coupling.device_a_label, coupling.device_b_label, ) for coupling in self._couplings ), "drives": tuple( (line.label, type(line).__name__, line.target_label) for line in lines ), "signal_chain": tuple( type(transform).__name__ for transform in signal_chain ), "baths": tuple( (bath.label, bath.recipe, tuple(bath.resolve_targets(self))) for bath in self._baths ), "ports": tuple( (port.label, tuple(port.resolve_targets(self))) for port in self._ports ), "frame": self._frame_spec, "approximation": type(self._approximation).__name__, "backend": type(self.backend).__name__, } )
[docs] def with_params(self, bindings: Mapping[str, Any]) -> "Chip": """Return an isolated structural copy with component-owned values rebound.""" targets = self._parameter_targets() unknown = set(bindings) - set(targets) if unknown: raise KeyError( f"Unknown Chip parameter paths: {sorted(unknown)}. " f"Available: {list(targets)}" ) cloned = self.clone() device_bindings: dict[int, dict[str, Any]] = {} for path, value in bindings.items(): kind, index, name, _ = targets[path] if kind == "device": device_bindings.setdefault(index, {})[name] = value elif kind == "coupling": cloned._couplings[index].set_parameter_value(name, value) elif kind == "drive": assert cloned._control_equipment is not None cloned._control_equipment._lines[index].set_parameter_value(name, value) elif kind == "control": assert cloned._control_equipment is not None transform = cloned._control_equipment._signal_chain[index] cloned._control_equipment._signal_chain[index] = transform.with_parameter_value(name, value) elif kind == "bath": cloned._baths[index].set_parameter_value(name, value) else: cloned._ports[index].set_parameter_value(name, value) for index, local_bindings in device_bindings.items(): cloned._devices[index].set_parameter_values(local_bindings) return cloned
[docs] def partition(self) -> "PartitionResult": """Split into independent sub-chips along the independence graph. Couplings, non-separable bath target sets, and drive-crosstalk pairs all count as connections. Exact — the joint solve factorizes as the tensor product of the component solves. ``simulate``/``seq.simulate`` consult this automatically; call it directly to orchestrate solves yourself. """ from quchip.chip.partition import partition_chip return partition_chip(self)
[docs] def status(self) -> None: """Print a lightweight diagnostic dashboard for the chip.""" label = self.label if self.label is not None else "(unlabeled)" print(f"Chip: {label}") print(f"- devices: {len(self._devices)}") print(f"- couplings: {len(self._couplings)}") print(f"- frame: {self._frame_spec!r}") print(f"- approximation: {type(self._approximation).__name__}") print(f"- dressed: {'yes' if self.is_dressed else 'no'}") print("- device list:") for dev in self._devices: bare_freq = getattr(dev, "freq", None) connected = sorted(d.label for d in getattr(dev, "_connected_drives", [])) line_text = ", ".join(connected) if connected else "none" print( f" - {dev.label}: {type(dev).__name__} " f"(freq={_format_float(bare_freq)} GHz, " f"dressed={_format_float(dev.dressed_freq)} GHz, " f"levels={dev.levels}, lines={line_text})" ) print("- couplings:") if self._couplings: for coupling in self._couplings: strength = coupling.coupling_strength print( f" - {type(coupling).__name__}: " f"{coupling.device_a_label} <-> {coupling.device_b_label} " f"(g={_format_float(strength)} GHz)" ) else: print(" - none") if self._control_equipment is not None: signal_chain = self._control_equipment.signal_chain print( "- control equipment: " f"{len(self._control_equipment.lines)} lines, " f"{len(signal_chain)} signal chain transforms" ) else: print("- control equipment: none")
# ------------------------------------------------------------------ # Observable helpers — delegate to quchip.chip.observables # ------------------------------------------------------------------
[docs] def from_array(self, data: Any, device: str | BaseDevice | None = None) -> Any: """Build a backend operator from a raw NumPy array. With *device*, the array is interpreted as a local operator on that device's subspace and embedded into the full tensor-product space. With ``device=None`` the array must already span the full chip Hilbert space. """ from quchip.chip.observables import from_array return from_array(self, data, device)
[docs] def observable(self, device: str | BaseDevice, op: str | Any) -> Any: """Embed a device operator onto the full chip Hilbert space. Accepts either an operator name (``"X"``, ``"Y"``, ``"Z"``, ``"n"``, ``"a"``, ``"a_dag"``, ``"I"``) or an already-built local-space operator, and returns it embedded on the chip's tensor-product space. This is for manual full-space operator construction and analysis; it is *not* a solver ``e_op``. For solver expectation values use :meth:`e_ops`, which keeps operators *local* so the demodulation pipeline can band-decompose and embed them correctly. """ from quchip.chip.observables import observable return observable(self, device, op)
[docs] def e_ops( self, *, correlators: dict[ tuple[str | BaseDevice, str | BaseDevice], tuple[str | Any, str | Any], ] | None = None, **specs: str | list | Any, ) -> dict[str | tuple[str, str], Any]: """Build a dict-form ``e_ops`` mapping for the solver pipeline. Each keyword maps a device label to an operator specification: a name string, a list of names, a raw local-space operator, or a mixed list of strings and operators. Two-device correlators (e.g. ``⟨Z₁⊗Z₂⟩``) are specified via *correlators* as device-label pairs → operator pairs. Returns local-space operators (not embedded) — the demodulation pipeline embeds as needed. Examples -------- >>> from quchip import DuffingTransmon, Capacitive, Chip >>> q1 = DuffingTransmon(freq=5.0, anharmonicity=-0.25, levels=3, label="q1") >>> q2 = DuffingTransmon(freq=5.2, anharmonicity=-0.22, levels=3, label="q2") >>> chip = Chip([q1, q2], couplings=[Capacitive(q1, q2, g=0.02)]) >>> e = chip.e_ops( ... q1=["X", "Y", "Z"], q2=["X", "Y", "Z"], ... correlators={("q1", "q2"): ("Z", "Z")}, ... ) """ from quchip.chip.observables import e_ops return e_ops(self, correlators=correlators, **specs)
# ------------------------------------------------------------------ # State factories # ------------------------------------------------------------------
[docs] def set_state_order( self, *devices: str | BaseDevice, levels: Mapping[str, int] | None = None, ) -> None: """Declare the device order used to parse string-state shorthands. After this is called, :meth:`bare_state`, :meth:`state`, and :meth:`superposition` accept single-string specifications where each character is one level per device in *devices* order. Level symbols default to ``g=0, e=1, f=2, h=3``; digits ``0..9`` are always accepted as energy-level indices. Every chip device must be named exactly once. Examples -------- >>> from quchip import DuffingTransmon, Resonator, Chip >>> qb = DuffingTransmon(freq=5.0, anharmonicity=-0.25, levels=3, label="qb") >>> tc = DuffingTransmon(freq=5.5, anharmonicity=-0.20, levels=3, label="tc") >>> cr = Resonator(freq=7.0, levels=4, label="cr") >>> chip = Chip([qb, tc, cr]) >>> chip.set_state_order(qb, tc, cr) >>> _ = chip.bare_state("eg1") # {qb: 1, tc: 0, cr: 1} """ from quchip.chip.states import set_state_order set_state_order(self, *devices, levels=levels)
[docs] def superposition( self, *components: Mapping[str | BaseDevice, int] | str | tuple[Any, Any], ) -> State: """Normalized bare-basis superposition of tensor-product states. Each component is either a bare-state spec (dict keyed by device or label, or a string when :meth:`set_state_order` has been called) or an ``(amplitude, spec)`` tuple for weighted mixing. Uniform weights by default; results are normalized to unit norm. Unlike :meth:`state`, this stays in the bare product basis — no dressed diagonalization — so the probe basis is explicit. Examples -------- >>> import numpy as np >>> from quchip import DuffingTransmon, Resonator, Chip >>> qb = DuffingTransmon(freq=5.0, anharmonicity=-0.25, levels=3, label="qb") >>> cr = Resonator(freq=7.0, levels=4, label="cr") >>> chip = Chip([qb, cr]) >>> _ = chip.superposition({qb: 0}, {qb: 1}) # equal |00> + |10> >>> _ = chip.superposition( # weighted mix ... (np.sqrt(0.3), {qb: 1, cr: 0}), ... (np.sqrt(0.7), {qb: 1, cr: 1}), ... ) """ from quchip.chip.states import superposition return superposition(self, *components)
[docs] def state( self, device_states: Mapping[str | BaseDevice, int] | str | None = None, /, **device_state_kwargs: int, ) -> State: """Dressed eigenstate assigned from the given product-state level labels. Accepts a string shorthand (e.g. ``"eg1"``) when :meth:`set_state_order` has been called. Safe inside ``jax.jit``/``grad``/``vmap``: under tracing the assigned eigenvector column is selected through the :func:`~quchip.chip.dressing.label_eigensystem` array kernel, so dressed initial states are differentiable end-to-end. The global phase is gauge-dependent (``eigh`` column convention) — populations and ``|overlap|`` figures of merit are unaffected. """ from quchip.chip.states import state return state(self, device_states, **device_state_kwargs)
[docs] def bare_state( self, device_states: Mapping[str | BaseDevice, int | State] | str | None = None, /, **device_state_kwargs: int | State, ) -> State: """Product state from per-device energy levels or authored local kets. Each device may be specified as either an energy-level index (``int``) or a ket vector in that device's authored local space. Devices not mentioned default to the ground state (level 0). Unlike :meth:`state` this does **not** diagonalize the coupled system. Accepts a string shorthand (e.g. ``"eg1"``) when :meth:`set_state_order` has been called. """ from quchip.chip.states import bare_state return bare_state(self, device_states, **device_state_kwargs)
# ------------------------------------------------------------------ # Control equipment # ------------------------------------------------------------------
[docs] def wire( self, *lines: BaseDrive, signal_chain: Sequence[SignalTransform] | None = None, ) -> ControlEquipment: """Attach or replace classical control wiring. Preferred user-facing API for attaching control to a chip. If *lines* is omitted, the existing connected lines are reused and only the signal chain is replaced. Examples -------- >>> # chip.wire(drive_q, drive_r, signal_chain=[Crosstalk(drive_q, drive_r, c=0.05)]) # doctest: +SKIP """ if lines: for line in lines: if not isinstance(line, BaseDrive): raise TypeError( f"chip.wire(...) expects drive objects. Got {type(line).__name__}." ) connected_lines = list(lines) else: if self._control_equipment is None: raise ValueError( "chip.wire() requires at least one drive when no control " "equipment is attached yet." ) connected_lines = self._control_equipment.lines duplicates = [lbl for lbl, count in Counter(d.label for d in connected_lines).items() if count > 1] if duplicates: raise ValueError( f"Duplicate drive labels in equipment: {duplicates}. " "Each drive must have a unique label." ) if signal_chain: drive_labels = {d.label for d in connected_lines} for transform in signal_chain: for value in transform.referenced_lines(): if value not in drive_labels: raise ValueError( f"Signal chain references drive '{value}' not in equipment. " f"Available: {sorted(drive_labels)}" ) equipment = ControlEquipment(lines=connected_lines, signal_chain=list(signal_chain) if signal_chain else None) self.connect(equipment) return equipment
[docs] def unwire(self, line: BaseDrive | str) -> BaseDrive: """Remove one control line and every signal-chain transform referencing it. The inverse of :meth:`wire` for a single line. Accepts the drive object or its label. Returns the removed drive so it can be rewired later. Removing the last line detaches the equipment entirely (``control_equipment`` becomes ``None``). """ if self._control_equipment is None: raise ValueError("chip.unwire(...) requires connected control equipment; nothing is wired.") label = resolve_label(line) lines = self._control_equipment.lines remaining = [d for d in lines if d.label != label] if len(remaining) == len(lines): available = [d.label for d in lines] raise ValueError(f"No control line labeled '{label}' in equipment. Available: {available}") removed = next(d for d in lines if d.label == label) kept_chain = [] for transform in self._control_equipment.signal_chain: retained = transform.without_line(label) if retained is not None: kept_chain.append(retained) if remaining: self._control_equipment = ControlEquipment(lines=remaining, signal_chain=kept_chain or None) else: self._control_equipment = None return removed
[docs] def connect(self, control_equipment: ControlEquipment) -> None: """Attach control equipment to this chip (low-level API). Validates every drive target and rejects duplicate drive labels, then reconnects each drive to this chip's canonical device or coupling instance. User-facing code should prefer :meth:`wire`. """ drive_duplicates = [ lbl for lbl, n in Counter(d.label for d in control_equipment.lines).items() if n > 1 ] if drive_duplicates: raise ValueError( f"Duplicate drive labels in equipment: {drive_duplicates}. " "Each drive must have a unique label." ) for drive in control_equipment.lines: if isinstance(drive, CouplingDrive): target_label = drive.target_label if target_label not in self._coupling_map: raise ValueError( f"Edge line '{drive.label}' targets coupling '{target_label}', which is not on " f"this chip. Available couplings: {list(self._coupling_map.keys())}" ) continue if drive._target is None: raise ValueError( "ControlEquipment contains a drive with no connected target " f"({drive!r}). Connect drives to chip devices before " "calling chip.connect(control_equipment)." ) target_label = drive.target_label assert target_label is not None if target_label not in self._device_map: raise ValueError( f"Drive target '{target_label}' is not on this chip. Available: {list(self._device_map.keys())}" ) self._control_equipment = control_equipment for drive in control_equipment.lines: if isinstance(drive, CouplingDrive): assert drive.target_label is not None coupling = self._coupling_map[drive.target_label] drive.connect(coupling) continue assert drive.target_label is not None drive.connect(self._device_map[drive.target_label])
[docs] def disconnect(self) -> ControlEquipment: """Detach control equipment entirely (low-level API). The inverse of :meth:`connect`/:meth:`wire`: removes all lines and the signal chain at once (``control_equipment`` becomes ``None``). Returns the detached equipment so it can be reconnected later. Returns ------- ControlEquipment The equipment that was attached before detachment. """ if self._control_equipment is None: raise ValueError("chip.disconnect() requires connected control equipment; nothing is wired.") equipment = self._control_equipment self._control_equipment = None return equipment
# ------------------------------------------------------------------ # Typed solve surface # ------------------------------------------------------------------ def _check_problem(self, problem: Any, index: int | None = None) -> None: """Validate that *problem* is a SolveProblem built for *this* chip. Raises ``TypeError`` when *problem* does not duck-type as a :class:`SolveProblem`, or ``ValueError`` when it was built for a different chip. When *index* is given the message is phrased for the ``problems[index]`` list position (used by :meth:`solve_many`); otherwise it is phrased for a single problem (used by :meth:`solve`). """ if not hasattr(problem, "engine_result") or not hasattr(problem, "chip"): type_name = type(problem).__name__ if index is None: raise TypeError(f"Expected SolveProblem, got {type_name}") raise TypeError(f"problems[{index}]: expected SolveProblem, got {type_name}") if getattr(problem, "chip", None) is not self: if index is None: raise ValueError( "SolveProblem was built for a different chip. Use the same chip instance that produced the problem." ) raise ValueError( f"problems[{index}] was built for a different chip. All problems must share the same chip instance." )
[docs] def solve( self, problem: "SolveProblem", *, check_truncation: bool = True, truncation_threshold: float = 1e-3, ) -> "SimulationResult": """Solve a typed :class:`SolveProblem` through this chip's backend. Routes through the common :func:`~quchip.engine.solve_problem` chokepoint, so the Hilbert-truncation safety net applies by default; pass ``check_truncation=False`` to opt out or retune ``truncation_threshold``. """ from quchip.engine import solve_problem self._check_problem(problem) return solve_problem( problem, check_truncation=check_truncation, truncation_threshold=truncation_threshold, )
[docs] def solve_many(self, batch_or_problems: Any, *, progress: bool = True) -> "SimulationBatchResult": """Solve a :class:`SolveBatch` or list of problems. Chip-level validation only enforces what needs ``self`` (every input was built for *this* chip); the input-shape dispatch and batching are delegated to :func:`quchip.engine.solve_many`, which owns the single SolveBatch / list dispatch. """ from quchip.engine import solve_many from quchip.engine.ir import SolveBatch if isinstance(batch_or_problems, SolveBatch): if batch_or_problems.chip is not self: raise ValueError("SolveBatch was built for a different chip.") else: batch_or_problems = list(batch_or_problems) for i, problem in enumerate(batch_or_problems): self._check_problem(problem, index=i) return solve_many(batch_or_problems, progress=progress)
[docs] def steadystate( self, *, e_ops: dict | None = None, options: dict | None = None, frame: FrameSpec | None = None, approximation: Approximation | None = None, ) -> "SteadyStateResult": """Solve this chip's unique static Lindblad steady state.""" from quchip.engine.steady_state import steadystate return steadystate( self, e_ops=e_ops, options=options, frame=frame, approximation=approximation, )
[docs] def steadystate_batch( self, *axes: Any, e_ops: dict | None = None, options: dict | None = None, frame: FrameSpec | None = None, approximation: Approximation | None = None, progress: bool = True, ) -> Any: """Solve static Lindblad steady states over parameter sweep axes.""" from quchip.engine.steady_state import steadystate_batch return steadystate_batch( self, *axes, e_ops=e_ops, options=options, frame=frame, approximation=approximation, progress=progress, )
# ------------------------------------------------------------------ # Misc # ------------------------------------------------------------------
[docs] def describe(self) -> str: """Sectioned plain-text report of everything on the chip. Devices with their declared parameters (units included), noise settings, couplings, control wiring, and baths — the "what did I just build?" view. Returns a string; ``print(chip.describe())``. Traced parameters render as ``<traced>`` and are never concretized. """ from quchip.chip.describe import describe_chip return describe_chip(self)
def __repr__(self) -> str: return ( f"Chip(label={self.label!r}, devices={len(self._devices)}, " f"couplings={len(self._couplings)}, frame={self._frame_spec!r}, " f"approximation={type(self._approximation).__name__}, " f"dressed={'yes' if self.is_dressed else 'no'})" )