"""Physical stationary acquisition, independent of receiver configuration."""
from __future__ import annotations
from dataclasses import dataclass, replace
from typing import Any
import numpy as np
from quchip.engine.field_statistics import propagate_noise, proper_spectrum, quadrature_spectrum
from quchip.engine.routes import (
WORKFLOWS, SolveRoute, array_name, automatic_cutoff, largest_change, plan_sectors, sector_diagnostics, warn_cutoff,
)
from quchip.utils.memory import fits_memory
from quchip.cutoff import active_cutoff
from quchip.results.receiver import _engineering_iq, noise_grid
from quchip.engine.ir import LinearResponseProblem
from quchip.devices.spaces import FockSpace
from quchip.engine.linear_response import try_build_linear_response_problem
from quchip.engine.reference import (
FieldChannel, cw_transfer,
)
from quchip.results.measurement import VNAMeasurement
from quchip.results.input_output import MeanFieldResponseResult, _LazyDiagnostics
from quchip.sweep import Sweep, ZippedSweep, _iter_axis_points
from quchip.utils.jax_utils import contains_tracer
from quchip.utils.labeling import resolve_label
# The noise comparison runs when N <= K + 1 has at most this many states.
_NOISE_CHECK_STATES = 1024
_HARMONIC = ("The chip is passive and harmonic, so its mode equations give the response and the noise without a "
"Fock truncation.")
@dataclass(frozen=True)
class _Acquisition:
mean: Any
components: dict[str, tuple[Any, Any]]
delays: Any
diagnostics: Any
mode_amplitudes: Any
photon_numbers: Any
mode_frequencies: Any
[docs]
def capture_modes(chip: Any, operating: Any, modes: tuple[str, ...]) -> tuple[Any, Any, Any]:
"""Read authored Fock-mode observables in the solved stationary frames."""
from quchip.engine.observables import prepare_local_op
backend = chip.backend
xp = backend.array_module
space = operating.engine.excitation_space
labels = tuple(device.label for device in chip.devices)
amplitudes, numbers, frequencies = [], [], []
for label in modes:
device = chip.device_map[label]
basis = operating.engine.bases[label]
# Under an excitation cutoff, the state holds only the retained labels.
state = (operating.state.reduced_state(label) if space is None
else space.reduced(operating.state.state, backend, (labels.index(label),)))
amplitudes.append(backend.expect(prepare_local_op(device, "a", basis, backend), state))
numbers.append(xp.real(backend.expect(prepare_local_op(device, "n", basis, backend), state)))
frequencies.append(operating.engine.resolved_frame.frequencies[label])
return xp.asarray(amplitudes), xp.asarray(numbers), xp.asarray(frequencies)
[docs]
def capture_noise(operating: Any, backend: Any, labels: tuple[str, ...], frequency: Any, offsets: Any) -> Any:
"""Calculate joint physical IQ spectra and source budgets from one operating point."""
xp = backend.array_module
engine = operating.engine
rho = xp.asarray(backend.to_array(operating.state.state))
regression_labels = tuple(channel.key for channel in engine.slh.channels) if engine.slh.output_network else labels
excess = quadrature_spectrum(engine, rho, backend, operating.prepared, regression_labels, offsets)
fields = tuple(FieldChannel(c.key, c.reference, c.input_occupation) for c in engine.slh.channels)
return propagate_noise(fields, engine.slh.S, engine.slh.output_network, excess, labels, frequency, offsets, xp)
[docs]
def capture_linear(
problem: LinearResponseProblem, backend: Any, labels: tuple[str, ...], input_label: str,
frequency: Any, amplitude: Any, offsets: Any,
) -> _Acquisition:
"""Acquire harmonic means and normal spectra without a Fock-space truncation."""
xp = backend.array_module
count = problem.scattering.shape[0]
indices = {channel.key: i for i, channel in enumerate(problem.field_channels)}
selected = xp.asarray([indices[label] for label in labels])
probe = indices[input_label]
# The validated symmetric grid supplies both sidebands and the carrier.
frequencies = frequency + offsets
solved = backend.linear_response(replace(problem, frequencies=frequencies, plane_indices=tuple(range(count))))
response = solved.responses[:, selected, :]
gains = xp.stack([cw_transfer(problem.field_channels[indices[label]].reference.outbound, frequency, xp)
for label in labels])
incoming = cw_transfer(problem.field_channels[probe].reference.inbound, frequency, xp)
mean = gains * response[len(offsets)//2, :, probe] * incoming * amplitude
occupations = xp.asarray([0.0 if c.input_occupation is None else c.input_occupation
for c in problem.field_channels] + [0.0]*(count-len(problem.field_channels)))
normal = (response * occupations) @ xp.conj(xp.swapaxes(response, -1, -2))
direct = xp.asarray(problem.scattering)[selected]
background = (direct * occupations) @ xp.conj(direct.T)
excess = proper_spectrum(normal-background, normal[::-1]-background, xp)
components, delays = propagate_noise(problem.field_channels, problem.scattering, None, excess,
labels, frequency, offsets, xp)
mode_amplitudes = solved.mode_amplitudes[len(offsets)//2, :, probe] * incoming * amplitude
numbers = xp.abs(mode_amplitudes)**2 + xp.real(xp.diag(solved.mode_covariance))
route = SolveRoute("linear_response", _HARMONIC, len(problem.mode_labels), array_name(backend))
return _Acquisition(mean, components, delays,
{"solver": route.name, "route": route, "mode_count": len(problem.mode_labels),
"residual": xp.max(solved.residuals)},
mode_amplitudes, numbers, xp.full((len(problem.mode_labels),), frequency))
[docs]
def measure(
vna: Any, frequencies: Any, amplitudes: Any, variations: tuple[Sweep | ZippedSweep, ...], *,
input: Any, outputs: Any, noise_frequencies: Any, options: Any, progress: bool,
) -> VNAMeasurement:
"""Acquire physical fields and correlations once for each probe/sweep point."""
from quchip.analysis.vna import _resolve_exposure
if isinstance(outputs, str):
raise TypeError("outputs must be a sequence of plane objects or labels, not a string.")
labels = tuple(vna.ports) if outputs is None else tuple(_resolve_exposure(vna.chip, plane) for plane in outputs)
if not labels or len(set(labels)) != len(labels):
raise ValueError("outputs must contain distinct exposed planes.")
if input is None:
if len(vna.ports) != 1:
raise ValueError("measure() requires input= when more than one port is selected.")
input_label = vna.ports[0]
else:
input_label = _resolve_exposure(vna.chip, input)
if any(tone.port == input_label for tone in vna._tones):
raise ValueError("The measurement probe must be the only tone on its input.")
return _acquire(vna, frequencies, amplitudes, variations, labels=labels, input_label=input_label,
options=options, progress=progress, noise=True, noise_frequencies=noise_frequencies)
def _acquire(
vna: Any, frequencies: Any, amplitudes: Any, variations: tuple[Sweep | ZippedSweep, ...], *,
labels: tuple[str, ...], input_label: str, options: Any, progress: bool,
noise: bool = False, noise_frequencies: Any = None,
) -> Any:
"""Acquire one finite-probe grid, with optional noise and mode observables."""
from quchip.analysis.vna import _axis_values, _operating_point, _plane_means, _public_axes
frequency_values, frequency_axis = _axis_values(frequencies)
amplitude_values, amplitude_axis = _axis_values(amplitudes)
vna._validate_variations(variations, reserved=(
*(("frequency",) if frequency_axis else ()), *(("amplitude",) if amplitude_axis else ())))
shape, variation_points = _iter_axis_points(variations)
axes = _public_axes(variations)
if amplitude_axis:
shape += (len(amplitude_values),)
axes += (("amplitude", amplitude_values),)
if frequency_axis:
shape += (len(frequency_values),)
axes += (("frequency", frequency_values),)
offsets = noise_grid(noise_frequencies) if noise else None
xp = vna.chip.backend.array_module
modes = tuple(device.label for device in vna.chip.devices
if isinstance(device.local_space(), FockSpace)) if noise else ()
incident, parameters = [], []
points: list[Any] = []
concrete_grid = not contains_tracer((frequency_values, amplitude_values))
method = "VNA.measure()" if noise else "VNA.finite_power()"
workflow = WORKFLOWS[method]
for _, params in variation_points:
chip, pumps = vna._chip_at(params), vna._tone_values(params)
cutoff = active_cutoff(chip)
# Under a cutoff, the projected model is not the harmonic chip, so the harmonic route does not apply.
linear = (try_build_linear_response_problem(chip, frequency_values, plane_labels=labels)
if noise and not vna._tones and options is None and cutoff is None else None)
if linear is not None and not contains_tracer((linear.hamiltonian, linear.couplings)):
couplings = np.asarray(linear.couplings)
drift = -1j*np.asarray(linear.hamiltonian)-couplings.conj().T@couplings/2
if np.any(np.linalg.eigvals(drift).real >= 0):
raise ValueError("A stationary harmonic measurement requires all modes to decay.")
sector, reason = None, _HARMONIC
if linear is None:
# Pumps share the frame of the probe only at their own frequency.
sector, reason = plan_sectors(
chip, workflow, labels=labels, inputs=(input_label, *(port for port, _, _ in pumps)), modes=modes,
frequencies=(*frequency_values, *(frequency for _, frequency, _ in pumps)) if pumps else (),
options=options, concrete=concrete_grid and not contains_tracer(pumps))
points.extend((chip, linear, sector, pumps, amplitude, frequency, reason)
for amplitude in amplitude_values for frequency in frequency_values)
captured: list[Any] = [None] * len(points)
sector_points = [index for index, point in enumerate(points) if point[2] is not None]
if sector_points:
solved = _sector_acquire([points[index] for index in sector_points], labels=labels, offsets=offsets,
noise=noise, progress=progress, method=method, xp=xp)
for index, acquired in zip(sector_points, solved, strict=True):
captured[index] = acquired
remaining = [index for index, point in enumerate(points) if point[2] is None]
iterator: Any = remaining
if progress and remaining:
from tqdm import tqdm
iterator = tqdm(remaining, desc="VNA measurement" if noise else "VNA power")
for index in iterator:
chip, linear, _, pumps, amplitude, frequency, reason = points[index]
tones = pumps + ((input_label, frequency, xp.conj(amplitude)),)
if linear is None:
operating = _operating_point(chip, tones, frequency, (), options, reason=reason)
mean = _plane_means(operating.engine, operating.state.state, chip.backend, labels, tones, frequency)
components, output_delays = (capture_noise(operating, chip.backend, labels, frequency, xp.asarray(offsets))
if noise else ({}, None))
amplitudes_at_point, numbers_at_point, frames_at_point = (
capture_modes(chip, operating, modes) if noise else (None, None, None))
acquired = _Acquisition(mean, components, output_delays, operating.diagnostics,
amplitudes_at_point, numbers_at_point, frames_at_point)
else:
acquired = capture_linear(
linear, chip.backend, labels, input_label, frequency, xp.conj(amplitude), xp.asarray(offsets))
captured[index] = acquired
for chip, _, _, _, amplitude, _, _ in points:
if noise:
parameters.append(dict(chip.parameters))
incident.append(xp.asarray(amplitude))
response = dict(
ports=labels, input=resolve_label(input_label),
frequencies=frequency_values if frequency_axis else frequency_values[0],
amplitudes=amplitude_values if amplitude_axis else amplitude_values[0], axes=axes, shape=shape,
diagnostics=tuple(point.diagnostics for point in captured),
values=xp.conj(xp.stack([point.mean for point in captured]).reshape((*shape, len(labels)))),
incident=xp.stack(incident).reshape(shape),
)
if not noise:
return MeanFieldResponseResult(**response)
assert offsets is not None
size = 2 * len(labels)
names = tuple(captured[0].components)
if any(tuple(point.components) != names for point in captured):
raise ValueError("Measurement variations must preserve noise source structure.")
components = {
name: (_engineering_iq(xp.stack([point.components[name][0] for point in captured])
.reshape((*shape, size, size)), xp),
_engineering_iq(xp.stack([point.components[name][1] for point in captured])
.reshape((*shape, len(offsets), size, size)), xp))
for name in names
}
return VNAMeasurement(
**response, noise_frequencies=offsets, noise_components=components,
output_delays=xp.stack([point.delays for point in captured]).reshape((*shape, len(labels))),
parameters=tuple(parameters), modes=modes,
mode_amplitudes=xp.stack([point.mode_amplitudes for point in captured]).reshape((*shape, len(modes))),
photon_numbers=xp.stack([point.photon_numbers for point in captured]).reshape((*shape, len(modes))),
mode_frequencies=xp.stack([point.mode_frequencies for point in captured]).reshape((*shape, len(modes))),
)
@dataclass(frozen=True)
class _SectorPoint:
"""One sector solve at one grid point, in the physics convention."""
solve: Any
mean: Any
mode_amplitudes: Any
photon_numbers: Any
excess: Any
noise: dict[str, Any]
def _sector_pass(entries: list[Any], cutoff: int, *, offsets: Any, noise: bool, guesses: list[_SectorPoint] | None,
progress: bool, method: str) -> list[_SectorPoint]:
"""Solve every grid point on ``N <= cutoff``, starting from nearby solutions."""
iterator: Any = range(len(entries))
if progress:
from tqdm import tqdm
iterator = tqdm(iterator, desc=f"{method} N<={cutoff}")
points: list[_SectorPoint] = []
previous: tuple[Any, Any] | None = None
for index in iterator:
chip, _, model, pumps, amplitude, frequency, _ = entries[index]
frequency = float(frequency)
internal = (complex(np.conj(amplitude)), *(complex(value) for _, _, value in pumps))
guess = None
if guesses is not None:
guess = _embed(guesses[index].solve.state, model.size(cutoff))
elif previous is not None and previous[0] is chip:
guess = previous[1]
solved = model.stationary(cutoff, frequency, internal, guess)
amplitudes, numbers = model.mode_values(cutoff, solved.state)
excess, info = (model.quadrature_spectrum(cutoff, frequency, internal, solved.state, offsets) if noise
else (None, {}))
points.append(_SectorPoint(solved, model.means(cutoff, frequency, internal, solved.state),
amplitudes, numbers, excess, info))
previous = (chip, solved.state)
return points
def _embed(state: Any, size: int) -> Any:
"""Return a state on a smaller cutoff as a state on ``size`` labels, which list it first."""
embedded = np.zeros((size, size), dtype=complex)
embedded[:state.shape[0], :state.shape[1]] = state
return embedded
def _sector_change(lower: list[_SectorPoint], upper: list[_SectorPoint], noise: bool) -> float:
"""Compare the means, mode observables and, optionally, the noise spectra of two cutoffs."""
changes = [largest_change([p.mean for p in lower], [p.mean for p in upper]),
largest_change([p.mode_amplitudes for p in lower], [p.mode_amplitudes for p in upper]),
largest_change([p.photon_numbers for p in lower], [p.photon_numbers for p in upper])]
if noise:
changes.append(largest_change([spectrum for p in lower for spectrum in p.excess],
[spectrum for p in upper for spectrum in p.excess]))
return max(changes)
def _sector_acquire(entries: list[Any], *, labels: tuple[str, ...], offsets: Any, noise: bool,
progress: bool, method: str, xp: Any) -> list[_Acquisition]:
"""Acquire grid points on excitation sectors at the chip's cutoff, or at an automatic one."""
models = [entry[2] for entry in entries]
first = models[0]
fixed = first.sectors.cutoff
def solve(cutoff: int, lower: list[_SectorPoint] | None) -> list[_SectorPoint]:
# A comparison above the noise limit compares the means and mode observables only.
spectra = noise and (lower is None or first.size(cutoff) <= _NOISE_CHECK_STATES)
return _sector_pass(entries, cutoff, offsets=offsets, noise=spectra, guesses=lower, progress=progress,
method=method)
def complete(cutoff: int, points: list[_SectorPoint]) -> list[_SectorPoint]:
if not noise or points[0].excess is not None:
return points
return _sector_pass(entries, cutoff, offsets=offsets, noise=True, guesses=points, progress=progress,
method=method)
if fixed is not None:
cutoff, choice, check = fixed, "explicit", None
points = solve(cutoff, None)
else:
unique = {id(model): model for model in models}.values()
choice = "automatic"
cutoff, points, check = automatic_cutoff(
first.max_total, solve, lambda lower, upper: _sector_change(lower, upper, upper[0].excess is not None),
complete=complete, states=first.size,
fits=lambda total: all(fits_memory(model.required_memory(total)) for model in unique))
if check is not None:
quantities = "means" + (", mode amplitudes and photon numbers" if first.modes else "")
note = ""
if noise and check.status == "passed":
if check.states <= _NOISE_CHECK_STATES:
quantities = quantities.replace(" and ", ", ") + " and noise spectra"
else:
note = (f" The noise spectra were not compared, because N <= {check.compared_with} has "
f"{check.states:,} states (the comparison limit is {_NOISE_CHECK_STATES:,}).")
warn_cutoff(method, cutoff, first.size(cutoff), check, quantities, note=note,
stacklevel=5 if noise else 4)
acquired = []
for entry, point in zip(entries, points, strict=True):
_, _, model, _, _, frequency, reason = entry
components: dict[str, tuple[Any, Any]] = {}
delays = None
frames: np.ndarray = np.zeros(0)
if noise:
components, delays = propagate_noise(model.fields, model.scattering, None, point.excess, labels,
float(frequency), np.asarray(offsets), np)
components = {name: (xp.asarray(white), xp.asarray(excess)) for name, (white, excess) in components.items()}
# Every mode is solved in the probe frame.
frames = np.full(len(model.modes), float(frequency))
diagnostics = {**sector_diagnostics(model, cutoff, point.solve, reason, choice, check), **point.noise}
if check is not None:
diagnostics["cutoff_check"]["noise_compared"] = (noise and check.status == "passed"
and check.states <= _NOISE_CHECK_STATES)
acquired.append(_Acquisition(xp.asarray(point.mean), components, None if delays is None else xp.asarray(delays),
_LazyDiagnostics(diagnostics), xp.asarray(point.mode_amplitudes),
xp.asarray(point.photon_numbers), xp.asarray(frames)))
return acquired