Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
476b538
Add files from pr/qd-ld-02-resonator-spectroscopy
KU-QM Jul 21, 2026
fd366a4
remove annotation and update progress_counter
TheoLaudatQM Jul 23, 2026
b8666dc
CLean up parameters
TheoLaudatQM Jul 23, 2026
f5ae5df
Clean up parameters docstrings
TheoLaudatQM Jul 23, 2026
b6b9032
Simulated_date imports
TheoLaudatQM Jul 23, 2026
09e6b9a
Black formatting
TheoLaudatQM Jul 23, 2026
3bbec12
Clean up parameters
TheoLaudatQM Jul 23, 2026
c1d5871
Clean node 02a
TheoLaudatQM Jul 23, 2026
fff8ac7
Clean node 02b
TheoLaudatQM Jul 23, 2026
6ee45fa
Improved comments for node 02b
TheoLaudatQM Jul 23, 2026
acec95b
Improved comments for node 02a
TheoLaudatQM Jul 23, 2026
c654285
Improved comments for node 02b & c
TheoLaudatQM Jul 23, 2026
6bcdd0e
Improved explanations about FitParameters
TheoLaudatQM Jul 24, 2026
91ff251
Improved logging of the results
TheoLaudatQM Jul 24, 2026
3d4e0bb
Improved uniformity across the three nodes
TheoLaudatQM Jul 24, 2026
20525d5
Improved analysis flow ds_raw is untouched and processed_ds is added …
TheoLaudatQM Jul 24, 2026
562af2f
Improved naming convention across the nodes
TheoLaudatQM Jul 24, 2026
760bdda
Improved 02b analysis (dump intermediate variables from ds_fit)
TheoLaudatQM Jul 24, 2026
36cd7ba
First draft for the main readme about the node structure
TheoLaudatQM Jul 24, 2026
b49b5c2
Update plotting framework to be more uniform and consistent
TheoLaudatQM Jul 24, 2026
bd793d4
Add plot_all() for making the nodes more uniform
TheoLaudatQM Jul 24, 2026
e508774
Add fmt_hz() for converting frequency units
TheoLaudatQM Jul 24, 2026
f1f7f8e
remove temp figures
TheoLaudatQM Jul 24, 2026
0119137
Improve plots
TheoLaudatQM Jul 24, 2026
42245a8
final optimizations
TheoLaudatQM Jul 24, 2026
23c20c2
remove res spec vs detuning that hasn't been tested
TheoLaudatQM Jul 24, 2026
e93e4f2
formatting
TheoLaudatQM Jul 24, 2026
5cac6b9
black formatting
TheoLaudatQM Jul 24, 2026
ec73f87
Robustify tha analysis of node 02b to fail cleanly when no peak is fo…
TheoLaudatQM Jul 24, 2026
a3721ad
Remove simulated samples due to serialization error
TheoLaudatQM Jul 31, 2026
79fb0c9
Tests simulation and execute
TheoLaudatQM Jul 31, 2026
9ee214c
Update res spec analysis test
TheoLaudatQM Jul 31, 2026
e8e632d
Created res spec vs power analysis test
TheoLaudatQM Jul 31, 2026
9215eb4
Clean up tests folder
TheoLaudatQM Aug 3, 2026
e0f5ff7
Black formatting
TheoLaudatQM Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Frequency display helpers for logs, summaries, and plots."""

from __future__ import annotations

from typing import Tuple, Union

import numpy as np

FrequencyLike = Union[float, np.ndarray, list, tuple]


def fmt_hz(value: float, *, na: str = "—") -> str:
"""Format a scalar frequency in Hz with automatic GHz/MHz/kHz/Hz scaling."""
if value != value: # NaN
return na
if abs(value) >= 1e9:
return f"{value / 1e9:.6f} GHz"
if abs(value) >= 1e6:
return f"{value / 1e6:.2f} MHz"
if abs(value) >= 1e3:
return f"{value / 1e3:.1f} kHz"
return f"{value:.3g} Hz"


def scale_frequency_hz(values: FrequencyLike) -> Tuple[float, str]:
"""Return ``(divisor, unit_label)`` to express Hz *values* for plotting or axes."""
arr = np.asarray(values, dtype=float)
max_abs = float(np.nanmax(np.abs(arr))) if arr.size else 0.0
if max_abs >= 1e9:
return 1e9, "GHz"
if max_abs >= 1e6:
return 1e6, "MHz"
if max_abs >= 1e3:
return 1e3, "kHz"
return 1.0, "Hz"
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Shared matplotlib styling for quantum-dots calibration plots."""

from __future__ import annotations

from typing import Optional

from matplotlib.axes import Axes


def sensor_success(ds_fit, sensor_name: str) -> Optional[bool]:
"""Return per-sensor fit success from ``ds_fit`` summary coords, if present."""
if "success" not in ds_fit.coords:
return None
try:
return bool(ds_fit.sel(sensor=sensor_name).success.values)
except Exception:
return None


def apply_sensor_outcome_style(ax: Axes, sensor_name: str, success: Optional[bool]) -> None:
"""Style a subplot title to reflect fit success or failure."""
if success is False:
ax.set_title(f"{sensor_name} — FAILED", color="crimson", fontweight="bold")
else:
ax.set_title(sensor_name)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from .parameters import Parameters
from .analysis import process_raw_dataset, fit_raw_data, log_fitted_results
from .plotting import plot_all, plot_raw_phase, plot_raw_amplitude_with_fit
from .simulated_data_generator import generate_simulated_dataset

__all__ = [
"Parameters",
"process_raw_dataset",
"fit_raw_data",
"log_fitted_results",
"plot_all",
"plot_raw_phase",
"plot_raw_amplitude_with_fit",
"generate_simulated_dataset",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import logging
from dataclasses import dataclass
from typing import Tuple, Dict
import numpy as np
import xarray as xr

from qualibrate.core import QualibrationNode
from qualibration_libs.data import add_amplitude_and_phase
from qualibration_libs.analysis import peaks_dips

from calibration_utils.common_utils.helpers import fmt_hz


@dataclass
class FitParameters:
"""Fitted resonator spectroscopy results for a single sensor (02a)."""

success: bool
"""True if the fit passed sanity checks and is safe for the state update."""

resonator_frequency: float
"""Absolute readout frequency at the resonance dip, in Hz."""

frequency_shift: float
"""Fitted readout frequency offset from IF, in Hz."""

fwhm: float
"""Lorentzian linewidth (FWHM of the |I + iQ| dip), in Hz."""


def log_fitted_results(fit_results: Dict, log_callable=None):
"""Log fitted results for all sensors.

Parameters
----------
fit_results : dict
``fit_results[sensor_name]`` mapping to the fitted values.
log_callable : callable, optional
Logging function (typically ``node.log``). Defaults to the module logger.
"""
if log_callable is None:
log_callable = logging.getLogger(__name__).info

for sensor_name, result in fit_results.items():
if result["success"]:
msg = (
f"[{sensor_name}] SUCCESS | "
f"resonator_frequency = {fmt_hz(result['resonator_frequency'])} | "
f"frequency_shift = {fmt_hz(result['frequency_shift'])} | "
f"fwhm = {fmt_hz(result['fwhm'])}"
)
else:
msg = f"[{sensor_name}] FAIL | fit did not pass sanity checks"
log_callable(msg)


def process_raw_dataset(ds: xr.Dataset, node: QualibrationNode):
"""Process raw dataset to add amplitude and phase information."""
ds = add_amplitude_and_phase(ds, "frequency_detuning", subtract_slope_flag=True)
full_freq = np.array(
[ds.frequency_detuning + q.readout_resonator.intermediate_frequency for q in node.namespace["sensors"]]
)
ds = ds.assign_coords(full_freq=(["sensor", "frequency_detuning"], full_freq))
ds.full_freq.attrs = {"long_name": "RF frequency", "units": "Hz"}
return ds


def fit_raw_data(ds: xr.Dataset, node: QualibrationNode) -> Tuple[xr.Dataset, dict[str, FitParameters]]:
"""
Fit the resonator dip for each sensor and return the processed dataset with fit outputs.

Parameters:
-----------
ds : xr.Dataset
Processed dataset containing amplitude and phase information.
node : QualibrationNode
The QUAlibrate node.

Returns:
--------
xr.Dataset
Processed dataset with Lorentzian fit variables and summary coordinates added.
"""
fit_vars = peaks_dips(ds.IQ_abs, "frequency_detuning")
ds_fit = xr.merge([ds, fit_vars])
ds_fit, fit_results = _extract_relevant_fit_parameters(ds_fit, node)
return ds_fit, fit_results


def _extract_relevant_fit_parameters(fit: xr.Dataset, node: QualibrationNode):
"""Add metadata to the dataset and fit results."""
full_freq = np.array([q.readout_resonator.intermediate_frequency for q in node.namespace["sensors"]])
fitted_frequency_shift = fit.position.data
res_freq = fitted_frequency_shift + full_freq
fit = fit.assign_coords(res_freq=("sensor", res_freq))
fit.res_freq.attrs = {"long_name": "resonator frequency", "units": "Hz"}
fit = fit.assign_coords(frequency_shift=("sensor", fitted_frequency_shift))
fit.frequency_shift.attrs = {"long_name": "readout frequency offset from IF", "units": "Hz"}
# Get the fitted FWHM
fwhm = np.abs(fit.width)
fit = fit.assign_coords(fwhm=("sensor", fwhm.data))
fit.fwhm.attrs = {"long_name": "resonator fwhm", "units": "Hz"}
# Assess whether the fit was successful or not
freq_success = np.abs(res_freq.data) < node.parameters.frequency_span_in_mhz * 1e6 + full_freq
fwhm_success = np.abs(fwhm.data) < node.parameters.frequency_span_in_mhz * 1e6 + full_freq
success_criteria = freq_success & fwhm_success
fit = fit.assign_coords(success=("sensor", success_criteria))

fit_results = {
s: FitParameters(
success=fit.sel(sensor=s).success.values.__bool__(),
resonator_frequency=fit.sel(sensor=s).res_freq.values.__float__(),
frequency_shift=fit.sel(sensor=s).frequency_shift.values.__float__(),
fwhm=fit.sel(sensor=s).fwhm.values.__float__(),
)
for s in fit.sensor.values
}
return fit, fit_results
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from typing import List, Optional

from qualibrate.core import NodeParameters
from qualibrate.core.parameters import RunnableParameters
from qualibration_libs.parameters import CommonNodeParameters
from qualibration_libs.parameters.experiment import BaseExperimentNodeParameters


class NodeSpecificParameters(RunnableParameters):
sensor_names: Optional[List[str]] = None
"""The list of sensor dot names to be included in the measurement. """
num_shots: int = 100
"""Number of averages to perform. Default is 100."""
frequency_span_in_mhz: int = 30
"""Span of frequencies to sweep in MHz. Default is 30 MHz."""
frequency_step_in_mhz: float = 0.1
"""Step size for frequency sweep in MHz. Default is 0.1 MHz."""
use_simulated_data: bool = False
"""Whether to generate simulated data instead of measuring via the OPX. Default False."""


class Parameters(
NodeParameters,
BaseExperimentNodeParameters,
CommonNodeParameters,
NodeSpecificParameters,
):
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from typing import Dict, List

import matplotlib.pyplot as plt
import xarray as xr
from matplotlib.axes import Axes
from matplotlib.figure import Figure

from qualang_tools.units import unit
from qualibration_libs.analysis import lorentzian_dip

from calibration_utils.common_utils.plot_style import apply_sensor_outcome_style, sensor_success

u = unit(coerce_to_integer=True)


def plot_all(ds_fit: xr.Dataset, sensors: List) -> Dict[str, Figure]:
"""Return all standard figures for 1D resonator spectroscopy."""
return {
"phase": plot_raw_phase(ds_fit, sensors),
"amplitude": plot_raw_amplitude_with_fit(ds_fit, sensors),
}


def plot_raw_phase(ds_fit: xr.Dataset, sensors: List) -> Figure:
"""Plot phase vs readout frequency for each sensor."""
num_sensors = len(sensors)
fig, axes = plt.subplots(1, num_sensors, figsize=(max(5 * num_sensors, 8), 4), squeeze=False)
axes = axes.flatten()

for ax, sensor in zip(axes, sensors):
sensor_data = ds_fit.sel(sensor=sensor.name)
success = sensor_success(ds_fit, sensor.name)

ax.plot(sensor_data.full_freq / u.MHz, sensor_data.phase, "o-", markersize=2)
ax.set_xlabel("Readout frequency [MHz]")
ax.set_ylabel("Phase [rad]")

ax2 = ax.twiny()
ax2.plot(sensor_data.frequency_detuning / u.MHz, sensor_data.phase, "o-", markersize=2, alpha=0)
ax2.set_xlabel("Frequency detuning [MHz]")
apply_sensor_outcome_style(ax, sensor.name, success)

fig.suptitle("Resonator spectroscopy (phase)")
fig.tight_layout()
return fig


def plot_raw_amplitude_with_fit(ds_fit: xr.Dataset, sensors: List) -> Figure:
"""Plot |I + iQ| with Lorentzian fit overlay for each sensor."""
num_sensors = len(sensors)
fig, axes = plt.subplots(1, num_sensors, figsize=(max(5 * num_sensors, 8), 4), squeeze=False)
axes = axes.flatten()

for ax, sensor in zip(axes, sensors):
plot_individual_amplitude_with_fit(
ax,
ds_fit.sel(sensor=sensor.name),
sensor.name,
ds_fit.sel(sensor=sensor.name),
sensor_success(ds_fit, sensor.name),
)

fig.suptitle("Resonator spectroscopy (amplitude + fit)")
fig.tight_layout()
return fig


def plot_individual_amplitude_with_fit(
ax: Axes,
sensor_data: xr.Dataset,
sensor_id: str,
fit: xr.Dataset,
success: bool | None,
):
"""Plot one sensor amplitude trace with optional Lorentzian fit and markers."""
ax.plot(
sensor_data.full_freq / u.MHz,
sensor_data.IQ_abs / u.mV,
"o-",
markersize=2,
label="Measured |I+iQ|",
)
ax.set_xlabel("Readout frequency [MHz]")
ax.set_ylabel(r"$R=\sqrt{I^2 + Q^2}$ [mV]")

ax2 = ax.twiny()
ax2.plot(
sensor_data.frequency_detuning / u.MHz,
sensor_data.IQ_abs / u.mV,
"o-",
markersize=2,
alpha=0,
)
ax2.set_xlabel("Frequency detuning [MHz]")

has_fit_vars = fit is not None and all(k in fit for k in ["amplitude", "position", "width", "base_line"])
if has_fit_vars and success is not False:
fitted_data = lorentzian_dip(
sensor_data.frequency_detuning,
float(fit.amplitude.values),
float(fit.position.values),
float(fit.width.values) / 2,
float(fit.base_line.mean().values),
)
ax2.plot(
sensor_data.frequency_detuning / u.MHz,
fitted_data / u.mV,
"r--",
label="Lorentzian fit",
)

if success and "frequency_shift" in fit.coords:
shift_mhz = float(fit.frequency_shift.values) * 1e-6
ax2.axvline(shift_mhz, color="blue", linestyle="--", label="Fitted frequency shift")

apply_sensor_outcome_style(ax, sensor_id, success)
if success is not False and has_fit_vars:
handles, labels = ax.get_legend_handles_labels()
handles2, labels2 = ax2.get_legend_handles_labels()
ax2.legend(handles + handles2, labels + labels2, loc="upper right", fontsize=8)

ax.grid(True, alpha=0.3)
Loading
Loading