Source code for quchip.inverse_design.types

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, overload

if TYPE_CHECKING:
    from quchip.chip import Chip
    from quchip.devices.base import BaseDevice


[docs] @dataclass(frozen=True) class ObservableReport: """Per-target record from a ``fit_a_dress`` run. Attributes ---------- kind Canonical desired-chip kind (``"freq"``, ``"anharmonicity"``, ``"cross_kerr"``, ``"exchange_rate"``, or ``"coupling_strength"``). label Target locator: a device label for single-device observables, a ``(label_a, label_b)`` tuple for pair observables, or a coupling label for coupling-keyed observables. target Value the optimizer tried to match (GHz). initial Observable value at the seed chip, before optimization (GHz). final Observable value at the fitted chip, after optimization (GHz). evaluator ``"full"`` for evaluation on the whole chip or ``"local"`` for evaluation on a one-hop subsystem (see ``evaluator`` in :func:`fit_a_dress`). source ``"component default"`` or ``"explicit"``. """ kind: str label: Any target: float initial: float final: float evaluator: str source: str = "explicit" @property def residual(self) -> float: """Final signed error, ``final - target`` (GHz).""" return self.final - self.target @property def relative_residual(self) -> float: """Final residual on the fitter's normalized objective scale.""" return self.residual / max(abs(self.target), 1e-9)
[docs] @dataclass(frozen=True) class FitParameterReport: """Record one fitted bare parameter. Attributes ---------- name : str Path in ``chip.parameters``. initial, final : float Seed and fitted values in the parameter's native units. lower_bound, upper_bound : float Bounds used by the optimizer, in the same units. seed_source : str Origin of the starting value, as recorded by the fitter. sign_choice : str or None Coupling-sign decision, or ``None`` when no decision was necessary. """ name: str initial: float final: float lower_bound: float upper_bound: float seed_source: str sign_choice: str | None = None @property def delta(self) -> float: """Signed optimizer displacement, ``final - initial``, in the parameter's native units.""" return self.final - self.initial
_UNMET_RELATIVE = 1e-2 _UNMET_FLOOR_GHZ = 1e-6 def _target_unmet(report: "ObservableReport") -> bool: """True when a target's final error exceeds 1% of its magnitude (floor 1 kHz).""" return abs(report.residual) > max(_UNMET_RELATIVE * abs(report.target), _UNMET_FLOOR_GHZ)
[docs] @dataclass(frozen=True) class FitADressResult: """Result of a :func:`fit_a_dress` optimization run. Attributes ---------- chip Fitted chip: a clone of the desired specification with updated device and coupling parameters. The fit never mutates the input chip. Because the result exposes ``.chip``, it satisfies :class:`~quchip.chip.transformations.ChipTransform` structurally, without inheritance. loss Final objective (sum of squared, scale-normalized residuals). history One-dimensional ``numpy`` array with the normalized objective at each distinct parameter vector passed to the residual function. The first entry is the seed and the last entry is :attr:`loss`. With a numerical Jacobian, the intermediate entries include finite-difference probes and accepted solver iterates. For a monotone best-so-far convergence curve, use ``numpy.minimum.accumulate(history)``. history_params One ``{parameter_name: value}`` dict per :attr:`history` entry, named like :attr:`final_params`. The first entry equals :attr:`initial_params` and the last entry equals :attr:`final_params`. history_residuals Array of shape ``(len(history), len(final_targets))``. Each row holds the observed minus the target value of each target (GHz) at that :attr:`history` entry, in :attr:`final_targets` order. The sum of the squared residuals, each divided by ``max(abs(target), 1e-9)``, is the matching :attr:`history` entry. initial_targets One :class:`ObservableReport` per target, evaluated on the optimizer's initial candidate. final_targets One :class:`ObservableReport` per target, evaluated on the fitted chip. initial_params ``{parameter_name: seed_value}``, the optimizer's starting point. final_params ``{parameter_name: fitted_value}``, the optimizer output. Parameter names follow ``"<device>.freq"``, ``"<device>.anharmonicity"``, and ``"<coupling>.<coupling_strength_name>"``. The coupling names are ``"<coupling>.g"`` for :class:`~quchip.chip.couplings.Capacitive`, ``"<coupling>.g_0"`` for :class:`~quchip.chip.couplings.TunableCapacitive`, ``"<coupling>.chi"`` for :class:`~quchip.chip.couplings.CrossKerr`. parameter_reports One :class:`FitParameterReport` per varied bare parameter, with its bounds, the source of its starting point, and any coupling-sign choice. solver_info ``scipy`` solver metadata (``method``, ``status``, ``message``, ``nfev``, ``jacobian``). It also holds the identifiability receipt that each :func:`~quchip.inverse_design.fit.fit_a_dress` call records. The receipt contains ``n_free_parameters`` (length of ``final_params``) and ``n_target_residuals`` (length of ``final_targets``). Its ``underdetermined_by_count`` is ``True`` when the former exceeds the latter, a necessary but not sufficient identifiability condition. The receipt also contains the final scaled-Jacobian rank, the condition number, the singular values, and any weak parameter directions. Rank uses normalized residuals in the solver's scaled parameter coordinates. ``history_axis`` names the sampling axis used by :attr:`history`, and ``n_recorded_evaluations`` gives its length. ``jacobian`` is ``"jax"`` when a JAX-native backend supplies the exact residual Jacobian and ``"finite-difference"`` otherwise. """ chip: Chip loss: float history: Any initial_targets: tuple[ObservableReport, ...] final_targets: tuple[ObservableReport, ...] initial_params: dict[str, float] final_params: dict[str, float] solver_info: dict[str, Any] parameter_reports: tuple[FitParameterReport, ...] = () history_params: tuple[dict[str, float], ...] = () history_residuals: Any = None @property def converged(self) -> bool | None: """Convergence flag of the optimizer, or None when the status is not available.""" status = self.solver_info.get("status") return None if status is None else int(status) > 0 @property def message(self) -> str | None: """The optimizer's termination reason, when recorded.""" return self.solver_info.get("message")
[docs] def summary(self) -> str: """Return a compact target, parameter, and identifiability receipt.""" status = {True: "converged", False: "stopped", None: "status unavailable"}[self.converged] rank = self.solver_info.get("jacobian_rank", "?") n_parameters = self.solver_info.get("n_free_parameters", len(self.final_params)) condition = self.solver_info.get("jacobian_condition_number") if condition is None: condition_text = "unknown" elif condition == float("inf"): condition_text = "inf" else: condition_text = f"{float(condition):.3g}" unmet = [report for report in self.final_targets if _target_unmet(report)] lines = [ f"fit_a_dress: {status} | loss {self.loss:.3g} | " f"targets: {len(self.final_targets)} | parameters: {n_parameters}", f"identifiability: rank {rank}/{n_parameters} | condition {condition_text}", ] if unmet: lines.append( f"{len(unmet)} of {len(self.final_targets)} targets remain unmet (error above 1% of " "the target): the targets, the model, or the vary selection are inconsistent." ) if self.message is not None: lines.append(self.message) if self.final_targets: lines.append("targets (GHz):") for target_report in self.final_targets: locator = ( " <-> ".join(str(part) for part in target_report.label) if isinstance(target_report.label, tuple) else str(target_report.label) ) marker = " <- unmet" if _target_unmet(target_report) else "" lines.append( f" {locator}.{target_report.kind} [{target_report.source}]: " f"{target_report.target:.6g} -> {target_report.final:.6g} " f"(error {target_report.residual:+.2g}){marker}" ) if self.parameter_reports: lines.append("bare parameters (GHz):") for parameter_report in self.parameter_reports: choice = f"; {parameter_report.sign_choice}" if parameter_report.sign_choice else "" lines.append( f" {parameter_report.name}: {parameter_report.initial:.6g} -> " f"{parameter_report.final:.6g} [{parameter_report.seed_source}{choice}]" ) return "\n".join(lines)
@overload def rebind(self, seed: BaseDevice | str, /) -> BaseDevice: ... @overload def rebind(self, seed: BaseDevice | str, /, *more: BaseDevice | str) -> tuple[BaseDevice, ...]: ...
[docs] def rebind(self, *seeds: BaseDevice | str) -> Any: """Find the fitted clones that match one or more seed devices. For example, call ``fit.rebind(qb, tc, cr)``. Parameters ---------- *seeds : BaseDevice or str One or more devices (or their labels) from the *seed* chip passed to :func:`~quchip.inverse_design.fit.fit_a_dress`. You must give at least one. Returns ------- BaseDevice or tuple[BaseDevice, ...] The matching device(s) on :attr:`chip` (the fitted clone), in input order. A single positional ``seed`` returns that device directly, and two or more seeds return a tuple. Raises ------ ValueError No seeds were given. """ if not seeds: raise ValueError("rebind requires at least one seed device or label") fitted = tuple(self.chip[s] for s in seeds) return fitted[0] if len(fitted) == 1 else fitted