quchip.inverse_design

Inverse-design utilities.

Fit bare chip parameters (device freq/anharmonicity, a coupling’s scalar strength) so that dressed observables (frequencies, anharmonicities, chi, zz, exchange) match user-supplied targets. The fit is classical (scipy.optimize.least_squares) but every downstream use of the returned Chip remains fully JAX-traceable and differentiable — see quchip.inverse_design.fit.fit_a_dress() for details.

quchip.inverse_design.fit_a_dress(chip, *, coupling_targets=None, observable_targets=None, fit_parameters=None, max_hilbert_dim=10000, seed_strength_bounds=(1e-06, 0.25), max_nfev=1000)[source]

Fit bare chip parameters so dressed observables match targets.

With fit_parameters=None (the default), every device freq / anharmonicity and every coupling’s scalar strength (coupling_strength) is a free variable in a bounded non-linear least-squares problem (scipy Trust-Region Reflective). A fit_parameters mapping instead is the complete free-parameter allowlist — see fit_parameters below — so any device or coupling it does not list is frozen instead of free. The fitted chip is returned as a clone — the input chip is never mutated.

Parameters:
  • chip (Chip) – Seed chip. Seeds only set the optimizer’s starting point.

  • coupling_targets (Mapping | None) – Mapping from coupling (or its label) to a target mode: "chi", "zz", or "g". For listed couplings, the coupling’s current strength is interpreted as the target value for that mode. With fit_parameters=None, couplings not listed here are still free — they are optimized, just without a dedicated anchor; a fit_parameters mapping can freeze them regardless (a coupling target does not itself make a coupling free). A "chi" target requires the coupling to have exactly one computational endpoint; both-computational or neither-computational raises ValueError at construction.

  • observable_targets (Mapping | None) – Mapping of target observables. Keys are devices/labels or (device_a, device_b) tuples; values are {kind: value} dicts. Supported kinds: "freq", "anharmonicity" (device), "exchange", "zz" (pair). Device-level targets override the auto-targeted defaults for the same (kind, label).

  • fit_parameters (Mapping | None) – None (default): every declared device tunable (tunable_params()) and every coupling’s scalar strength is free — the pre-existing behavior. A mapping is instead the complete free-parameter allowlist: {component_or_label: name_collection}, where a device’s name_collection is a subset of its declared tunable names and a coupling’s is a subset of (coupling.coupling_strength_name,). A component (device or coupling, given as the object or its label) absent from the mapping is fully frozen — it does not default to free. An empty name_collection explicitly freezes a listed component. A bare string value (e.g. "E_J" instead of ("E_J",)) is rejected, since a string is itself a collection of characters. Selected parameters are packed in chip order and each component’s own declared parameter order, not mapping or tuple order. initial_params / final_params contain only the selected (free) parameters.

  • max_hilbert_dim (int) – Above this total Hilbert-space size the fit switches from dressing the whole chip to dressing one-hop subsystems per target (see quchip.inverse_design.subsystems).

  • seed_strength_bounds (tuple[float, float]) – (lo, hi) magnitude bounds for the bare-coupling-strength seed root solve (_estimate_bare_g()) used for chi/zz coupling targets. The target observable must be bracketed by the values at these two endpoints, or seeding raises ValueError rather than silently returning a saturated endpoint.

  • max_nfev (int) – Maximum number of residual evaluations for the SciPy Trust-Region Reflective solver.

Returns:

Fitted chip clone, loss, residual history, per-target ObservableReport tuples, packed parameters, and solver metadata.

Return type:

FitADressResult

Raises:

ValueError – A "chi" coupling target does not have exactly one computational endpoint, or a chi/zz seed’s target observable is not bracketed within seed_strength_bounds; a fit_parameters key does not resolve to a device or coupling label on chip, names a parameter the resolved component does not declare, resolves the same label twice, or is a bare string rather than a name collection; or fit_parameters selects zero free parameters overall.

Warns:

UserWarning – The number of free parameters exceeds the number of target residuals (underdetermined by count). This is a necessary, not sufficient, identifiability condition: it does not analyze the Jacobian’s rank, so a count-sufficient fit can still be practically underdetermined.

Notes

Residuals are normalized by max(|target|, 1e-9) so every anchor contributes on equal relative-error footing. A coupling’s scalar strength bounds are symmetric around zero — the sign of a capacitive-type coupling is physical and must not be constrained. The solver’s convergence tolerances (ftol/xtol/gtol = 1e-11) and its x_scale floor (1e-3, applied per parameter as max(abs(x0), 1e-3)) are fixed fitter policy, not exposed as options.

Two identifiability hazards the count check above does not catch. The free-parameter-vs-residual count is necessary but not sufficient: it cannot detect a flat Jacobian direction — a free parameter no target observable actually responds to — which stays underdetermined regardless of the count. And a custom DeviceModel whose tunable_param_names is discovered (the derived default, not an explicit declaration) is not automatically fit-ready: an unbounded parameter still needs a tunable_param_bounds() rule before the optimizer can search it.

JAX traceability. When the chip uses a JAX-native backend, the complete parameter-to-residual map and its exact Jacobian are JAX-traceable; SciPy receives their concrete values for bounded trust-region control. The optimizer itself is not differentiated. Other backends retain SciPy’s numerical Jacobian. The returned chip remains fully traceable and differentiable in either case.

class quchip.inverse_design.FitADressResult(chip, loss, history, initial_targets, final_targets, initial_params, final_params, solver_info)[source]

Bases: object

Result of a fit_a_dress() optimization run.

Parameters:
chip

Fitted chip — a clone of the seed with updated freq/anharmonicity and coupling-strength values. The seed chip is never mutated. Exposing .chip makes this satisfy ChipTransform structurally, with no inheritance required.

Type:

Chip

loss

Final objective (sum of squared, scale-normalized residuals).

Type:

float

history

1-D numpy array holding [loss_initial, loss_final]. A compact record of how far the solver moved; it is not a per-iteration trace because scipy.optimize.least_squares does not expose one.

Type:

Any

initial_targets

One ObservableReport per target, evaluated on the seed chip.

Type:

tuple[ObservableReport, …]

final_targets

One ObservableReport per target, evaluated on the fitted chip.

Type:

tuple[ObservableReport, …]

initial_params

{parameter_name: seed_value} — the starting point passed to the optimizer.

Type:

dict[str, float]

final_params

{parameter_name: fitted_value} — the optimizer output. Parameter names follow "<device>.freq", "<device>.anharmonicity", and "<coupling>.<coupling_strength_name>""<coupling>.g" for Capacitive, "<coupling>.g_0" for TunableCapacitive, "<coupling>.chi" for CrossKerr.

Type:

dict[str, float]

solver_info

scipy solver metadata (method, status, message, nfev, jacobian), plus the identifiability receipt recorded for every fit_a_dress() call: n_free_parameters (length of final_params), n_target_residuals (length of final_targets), and underdetermined_by_count (True when the former exceeds the latter — a necessary, not sufficient, identifiability condition; no Jacobian-rank analysis is performed). jacobian is "jax" when a JAX-native backend supplies the exact residual Jacobian and "finite-difference" otherwise.

Type:

dict[str, Any]

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]
rebind(seed: BaseDevice | str, /) BaseDevice[source]
rebind(seed: BaseDevice | str, /, *more: BaseDevice | str) tuple[BaseDevice, ...]

Look up the fitted clones matching one or more seed devices.

fit.rebind(qb, tc, cr) replaces the qb_f = chip.device_map[qb.label] triple most tutorials opened with.

Parameters:

*seeds (BaseDevice or str) – One or more devices (or their labels) from the seed chip passed to fit_a_dress(). At least one is required.

Returns:

The matching device(s) on chip (the fitted clone), in input order. A single positional seed returns that device directly; two or more return a tuple.

Return type:

BaseDevice or tuple[BaseDevice, …]

Raises:

ValueError – No seeds were given.

class quchip.inverse_design.ObservableReport(kind, label, target, initial, final, evaluator)[source]

Bases: object

Per-target record from a fit_a_dress run.

Parameters:
kind

Observable kind: "freq", "anharmonicity", "chi", "zz", "exchange", or "g".

Type:

str

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.

Type:

Any

target

The value the optimizer tried to match (GHz).

Type:

float

initial

Observable value at the seed chip, before optimization (GHz).

Type:

float

final

Observable value at the fitted chip, after optimization (GHz).

Type:

float

evaluator

"full" if this target was evaluated on the whole chip or "local" if it was evaluated on a one-hop subsystem (see max_hilbert_dim in fit_a_dress()).

Type:

str

kind: str
label: Any
target: float
initial: float
final: float
evaluator: str

Modules

fit

Dressed-observable fitting of bare chip parameters.

observables

Target-spec construction for fit_a_dress.

subsystems

Neighborhood extraction for large-chip observables.

types