From c3e1744094787cf4bef7fc5d6462a6356bcf8616 Mon Sep 17 00:00:00 2001 From: paulQM Date: Fri, 19 Jun 2026 12:34:07 +0200 Subject: [PATCH 01/17] feat:add files --- .../__init__.py | 14 + .../analysis.py | 132 +++++++++ .../parameters.py | 35 +++ .../plotting.py | 58 ++++ .../35a_cz_phase_compensation_error_amp.py | 252 ++++++++++++++++++ 5 files changed, 491 insertions(+) create mode 100644 qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/__init__.py create mode 100644 qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/analysis.py create mode 100644 qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/parameters.py create mode 100644 qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py create mode 100644 qualibration_graphs/superconducting/calibrations/CZ_calibrations/35a_cz_phase_compensation_error_amp.py diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/__init__.py b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/__init__.py new file mode 100644 index 000000000..0ede3d015 --- /dev/null +++ b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/__init__.py @@ -0,0 +1,14 @@ +"""CZ phase compensation with error amplification calibration utilities.""" + +from .analysis import FitResults, fit_raw_data, log_fitted_results, process_raw_dataset +from .parameters import Parameters +from .plotting import plot_raw_data_with_fit + +__all__ = [ + "Parameters", + "process_raw_dataset", + "fit_raw_data", + "log_fitted_results", + "FitResults", + "plot_raw_data_with_fit", +] diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/analysis.py b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/analysis.py new file mode 100644 index 000000000..247a592af --- /dev/null +++ b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/analysis.py @@ -0,0 +1,132 @@ +"""Analysis functions for CZ phase compensation with error amplification calibration. + +For each qubit (control / target), the fitted phase is the frame value whose +vertical linecut (across all number_of_operations) has the highest mean signal. +When state discrimination is used this mean should approach 1 at the optimal +compensation point. +""" + +import logging +from dataclasses import dataclass +from typing import Dict, Tuple + +import numpy as np +import xarray as xr +from qualibrate import QualibrationNode +from qualibration_libs.data import convert_IQ_to_V + + +@dataclass +class FitResults: + """Stores fit results for a single qubit pair.""" + + fitted_control_phase: float + fitted_target_phase: float + control_mean_at_peak: float + target_mean_at_peak: float + success: bool + + +def _find_phase_from_max_mean(signal: xr.DataArray) -> Tuple[float, float, np.ndarray]: + """Find the frame value whose vertical linecut has the highest mean across operations. + + Returns (best_frame, best_mean, mean_curve_as_numpy). + """ + mean_vs_frame = signal.mean(dim="number_of_operations") + frames = mean_vs_frame.frame.values + values = mean_vs_frame.values + best_idx = int(np.argmax(values)) + return float(frames[best_idx]), float(values[best_idx]), values + + +def log_fitted_results(fit_results: Dict[str, FitResults], log_callable=None): + """Log fit results for each qubit pair.""" + if log_callable is None: + log_callable = logging.getLogger(__name__).info + + for qp_name, fit_result in fit_results.items(): + status = "SUCCESS" if fit_result.success else "FAIL" + log_callable( + f"{qp_name} [{status}] | " + f"control_phase={fit_result.fitted_control_phase:.6f}, " + f"target_phase={fit_result.fitted_target_phase:.6f}, " + f"control_peak_mean={fit_result.control_mean_at_peak:.4f}, " + f"target_peak_mean={fit_result.target_mean_at_peak:.4f}" + ) + + +def process_raw_dataset(ds: xr.Dataset, node: QualibrationNode): + """Convert IQ data to volts when state discrimination is disabled.""" + if "I_control" in ds.data_vars: + ds = convert_IQ_to_V( + ds, + qubit_pairs=node.namespace["qubit_pairs"], + IQ_list=["I_control", "Q_control", "I_target", "Q_target"], + ) + return ds + + +def _get_signals(qp_data: xr.Dataset): + """Extract control and target signal arrays from a single qubit-pair slice.""" + if "state_control" in qp_data.data_vars and "state_target" in qp_data.data_vars: + return qp_data["state_control"], qp_data["state_target"] + + signal_control = qp_data["I_control"] + signal_target = qp_data["I_target"] + if "control_target" in signal_control.dims: + signal_control = signal_control.sel(control_target="c") + if "control_target" in signal_target.dims: + signal_target = signal_target.sel(control_target="t") + return signal_control, signal_target + + +def fit_raw_data( + ds: xr.Dataset, node: QualibrationNode +) -> Tuple[xr.Dataset, Dict[str, FitResults]]: + """Find the optimal phase compensation by locating the frame with highest mean signal.""" + frames = ds.frame.values + fit_datasets = [] + fit_results: Dict[str, FitResults] = {} + + for qp in node.namespace["qubit_pairs"]: + qp_name = qp.name + qp_data = ds.sel(qubit_pair=qp_name) + + try: + signal_control, signal_target = _get_signals(qp_data) + + control_phase, control_peak_mean, control_mean_curve = _find_phase_from_max_mean(signal_control) + target_phase, target_peak_mean, target_mean_curve = _find_phase_from_max_mean(signal_target) + + fit_ds = xr.Dataset( + { + "control_mean_vs_frame": xr.DataArray(control_mean_curve, dims=("frame",), coords={"frame": frames}), + "target_mean_vs_frame": xr.DataArray(target_mean_curve, dims=("frame",), coords={"frame": frames}), + "fitted_control_phase": xr.DataArray(control_phase), + "fitted_target_phase": xr.DataArray(target_phase), + "control_mean_at_peak": xr.DataArray(control_peak_mean), + "target_mean_at_peak": xr.DataArray(target_peak_mean), + "success": xr.DataArray(True), + } + ) + fit_results[qp_name] = FitResults( + fitted_control_phase=control_phase, + fitted_target_phase=target_phase, + control_mean_at_peak=control_peak_mean, + target_mean_at_peak=target_peak_mean, + success=True, + ) + except Exception: + fit_ds = xr.Dataset({"success": xr.DataArray(False)}) + fit_results[qp_name] = FitResults( + fitted_control_phase=np.nan, + fitted_target_phase=np.nan, + control_mean_at_peak=np.nan, + target_mean_at_peak=np.nan, + success=False, + ) + + fit_datasets.append(fit_ds.expand_dims(qubit_pair=[qp_name])) + + ds_fit = xr.concat(fit_datasets, dim="qubit_pair") + return ds_fit, fit_results diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/parameters.py b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/parameters.py new file mode 100644 index 000000000..6a4632262 --- /dev/null +++ b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/parameters.py @@ -0,0 +1,35 @@ +"""Parameters for CZ phase compensation with error amplification calibration.""" + +# pylint: disable=too-few-public-methods + +from typing import ClassVar, Literal + +from qualibrate import NodeParameters +from qualibrate.core.parameters import RunnableParameters +from qualibration_libs.parameters import CommonNodeParameters, QubitPairExperimentNodeParameters + + +class NodeSpecificParameters(RunnableParameters): + """Node-specific parameters for CZ phase compensation with error amplification.""" + + num_shots: int = 100 + """Number of shots to perform. Default is 100.""" + num_frames: int = 17 + """Number of phase frames to sweep. Default is 17.""" + number_of_operations: int = 8 + """Number of CZ operations applied per amplification step. Default is 8.""" + operation: Literal["cz_flattop", "cz_unipolar", "cz_bipolar", "cz_flattop_erf", "cz_SNZ"] = "cz_unipolar" + """Type of CZ operation to perform. Options are 'cz_flattop', 'cz_unipolar', 'cz_bipolar', 'cz_flattop_erf', or 'cz_SNZ'. Default is 'cz_unipolar'.""" + use_state_discrimination: bool = True + """Whether to use state discrimination for readout. Default is True.""" + + +class Parameters( + NodeParameters, + CommonNodeParameters, + NodeSpecificParameters, + QubitPairExperimentNodeParameters, +): + """Combined parameters class for CZ phase compensation with error amplification.""" + + targets_name: ClassVar[str] = "qubit_pairs" diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py new file mode 100644 index 000000000..d4abc2ce2 --- /dev/null +++ b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py @@ -0,0 +1,58 @@ +"""Plotting utilities for CZ phase compensation with error amplification.""" + +import matplotlib.pyplot as plt +import numpy as np +import xarray as xr + + +def plot_raw_data_with_fit(ds_raw: xr.Dataset, qubit_pairs, ds_fit: xr.Dataset = None): + """Plot mean signal vs frame for control and target, highlighting the peak frame.""" + n_pairs = len(qubit_pairs) + fig, axes = plt.subplots(2, n_pairs, figsize=(6 * n_pairs, 8), squeeze=False) + + for i, qp in enumerate(qubit_pairs): + qp_name = qp.name + + if ds_fit is None or not bool(ds_fit.sel(qubit_pair=qp_name).success.values): + for row in range(2): + axes[row, i].text( + 0.5, 0.5, "fit failed" if ds_fit else "no fit", + ha="center", va="center", transform=axes[row, i].transAxes, + ) + axes[row, i].set_title(f"{qp_name} - {'control' if row == 0 else 'target'}") + axes[row, i].set_xlabel("Frame rotation [2π]") + axes[row, i].set_ylabel("Mean signal") + continue + + qp_fit = ds_fit.sel(qubit_pair=qp_name) + frames = ds_raw.sel(qubit_pair=qp_name).frame.values + + for row, (label, color) in enumerate([("control", "tab:blue"), ("target", "tab:red")]): + ax = axes[row, i] + mean_var = f"{label}_mean_vs_frame" + phase_var = f"fitted_{label}_phase" + peak_mean_var = f"{label}_mean_at_peak" + + if mean_var in qp_fit.data_vars: + mean_curve = qp_fit[mean_var].values + fitted_phase = float(qp_fit[phase_var].values) + peak_mean = float(qp_fit[peak_mean_var].values) + + ax.plot(frames, mean_curve, "o-", color=color) + ax.axvline(fitted_phase, color="k", ls="--", alpha=0.7, label=f"phase = {fitted_phase:.4f}") + ax.plot(fitted_phase, peak_mean, "*", color="gold", ms=15, zorder=5, + label=f"peak mean = {peak_mean:.4f}") + ax.legend(loc="best") + + ax.set_title(f"{qp_name} - {label}") + ax.set_xlabel("Frame rotation [2π]") + ax.set_ylabel("Mean signal") + ax.grid(alpha=0.3) + + for j in range(n_pairs, axes.shape[1]): + axes[0, j].axis("off") + axes[1, j].axis("off") + + fig.suptitle("CZ phase compensation - error amplification (max-mean method)") + fig.tight_layout(rect=(0, 0, 1, 0.95)) + return fig diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/35a_cz_phase_compensation_error_amp.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/35a_cz_phase_compensation_error_amp.py new file mode 100644 index 000000000..4188f2c12 --- /dev/null +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/35a_cz_phase_compensation_error_amp.py @@ -0,0 +1,252 @@ +# %% {Imports} +from dataclasses import asdict + +import matplotlib.pyplot as plt +import numpy as np +import xarray as xr +from calibration_utils.cz_phase_compensation_error_amp import ( + Parameters, + fit_raw_data, + log_fitted_results, + plot_raw_data_with_fit, + process_raw_dataset, +) +from qm.qua import * +from qualang_tools.loops import from_array +from qualang_tools.multi_user import qm_session +from qualang_tools.results import progress_counter +from qualang_tools.units import unit +from qualibrate import QualibrationNode +from qualibration_libs.data import XarrayDataFetcher +from qualibration_libs.parameters import get_qubit_pairs +from qualibration_libs.runtime import simulate_and_plot +from quam_config import Quam + +# %% {Description} +description = """ +CZ PHASE COMPENSATION WITH ERROR AMPLIFICATION + +This node calibrates residual single-qubit (local Z) phase shifts induced by the CZ macro. +Compared to the standard phase-compensation node, it repeats the CZ operation a variable +number of times to amplify phase errors before tomography. + +For each selected qubit pair: +1. Prepare either control or target qubit in a Ramsey-like sequence. +2. Apply a train of CZ macros (1..N repetitions). +3. Sweep a final virtual frame rotation and fit the Ramsey oscillation phase per repetition count. +4. Fit phase vs number of operations with a line; the slope gives the per-CZ local phase. + +State update: + - qp.macros[operation].phase_shift_control + - qp.macros[operation].phase_shift_target +""" + +node = QualibrationNode[Parameters, Quam]( + name="35a_cz_phase_compensation_error_amp", + description=description, + parameters=Parameters(), + machine=Quam.load(), +) + + +@node.run_action(skip_if=node.modes.external) +def custom_param(node: QualibrationNode[Parameters, Quam]): + """Allow local debug parameter overrides when running directly from IDE.""" + pass + + +# %% {Create_QUA_program} +@node.run_action(skip_if=node.parameters.load_data_id is not None) +def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: disable=too-many-statements + """Create sweep axes and generate the QUA program.""" + unit(coerce_to_integer=True) + + node.namespace["qubit_pairs"] = qubit_pairs = get_qubit_pairs(node) + num_qubit_pairs = len(qubit_pairs) + + n_avg = node.parameters.num_shots + frames = np.arange(-0.1, 0.1, 0.2 / node.parameters.num_frames) + num_operations = node.parameters.number_of_operations + cz_operation = node.parameters.operation + + node.namespace["sweep_axes"] = { + "qubit_pair": xr.DataArray(qubit_pairs.get_names()), + "number_of_operations": xr.DataArray( + np.arange(1, num_operations + 1), + attrs={"long_name": "number of CZ operations"}, + ), + "frame": xr.DataArray(frames, attrs={"long_name": "frame rotation", "units": "2π"}), + } + + with program() as node.namespace["qua_program"]: + frame = declare(fixed) + n = declare(int) + n_op = declare(int) + count = declare(int) + n_st = declare_stream() + I_c, I_c_st, Q_c, Q_c_st, n, n_st = node.machine.declare_qua_variables() + I_t, I_t_st, Q_t, Q_t_st, _, _ = node.machine.declare_qua_variables() + state_c = [declare(int) for _ in range(num_qubit_pairs)] + state_t = [declare(int) for _ in range(num_qubit_pairs)] + state_c_st = [declare_stream() for _ in range(num_qubit_pairs)] + state_t_st = [declare_stream() for _ in range(num_qubit_pairs)] + extra_phase_c = declare(fixed) + extra_phase_t = declare(fixed) + + for multiplexed_qubit_pairs in qubit_pairs.batch(): + for qp in multiplexed_qubit_pairs.values(): + node.machine.initialize_qpu(target=qp.qubit_control) + node.machine.initialize_qpu(target=qp.qubit_target) + align() + + with for_(n, 0, n < n_avg, n + 1): + save(n, n_st) + with for_(n_op, 1, n_op <= num_operations, n_op + 1): + with for_(*from_array(frame, frames)): + for ii, qp in multiplexed_qubit_pairs.items(): + assign(extra_phase_c, qp.macros[cz_operation].phase_shift_control) + assign(extra_phase_t, qp.macros[cz_operation].phase_shift_target) + for qubit, state_q, state_st in [ + (qp.qubit_control, state_c[ii], state_c_st[ii]), + (qp.qubit_target, state_t[ii], state_t_st[ii]), + ]: + qp.qubit_control.reset( + reset_type=node.parameters.reset_type, simulate=node.parameters.simulate + ) + qp.qubit_target.reset( + reset_type=node.parameters.reset_type, simulate=node.parameters.simulate + ) + qp.align() + + qubit.xy.play("x90") + qubit.align() + + with for_(count, 0, count < n_op, count + 1): + if qubit is qp.qubit_control: + qp.macros[cz_operation].apply(phase_shift_control=extra_phase_c + frame) + elif qubit is qp.qubit_target: + qp.macros[cz_operation].apply(phase_shift_target=extra_phase_t + frame) + + qubit.xy.play("x90") + qp.align() + + if node.parameters.use_state_discrimination: + qubit.readout_state(state_q) + save(state_q, state_st) + else: + if qubit is qp.qubit_control: + qp.qubit_control.resonator.measure("readout", qua_vars=(I_c[ii], Q_c[ii])) + save(I_c[ii], I_c_st[ii]) + save(Q_c[ii], Q_c_st[ii]) + elif qubit is qp.qubit_target: + qp.qubit_target.resonator.measure("readout", qua_vars=(I_t[ii], Q_t[ii])) + save(I_t[ii], I_t_st[ii]) + save(Q_t[ii], Q_t_st[ii]) + align() + + align() + with stream_processing(): + n_st.save("n") + for ii in range(num_qubit_pairs): + if node.parameters.use_state_discrimination: + state_c_st[ii].buffer(len(frames)).buffer(num_operations).average().save(f"state_control{ii + 1}") + state_t_st[ii].buffer(len(frames)).buffer(num_operations).average().save(f"state_target{ii + 1}") + else: + I_c_st[ii].buffer(len(frames)).buffer(num_operations).average().save(f"I_control{ii + 1}") + Q_c_st[ii].buffer(len(frames)).buffer(num_operations).average().save(f"Q_control{ii + 1}") + I_t_st[ii].buffer(len(frames)).buffer(num_operations).average().save(f"I_target{ii + 1}") + Q_t_st[ii].buffer(len(frames)).buffer(num_operations).average().save(f"Q_target{ii + 1}") + + +# %% {Simulate} +@node.run_action(skip_if=node.parameters.load_data_id is not None or not node.parameters.simulate) +def simulate_qua_program(node: QualibrationNode[Parameters, Quam]): + """Connect to QOP and simulate the QUA program.""" + qmm = node.machine.connect() + config = node.machine.generate_config() + samples, fig, wf_report = simulate_and_plot(qmm, config, node.namespace["qua_program"], node.parameters) + node.results["simulation"] = {"figure": fig, "wf_report": wf_report, "samples": samples} + + +# %% {Execute} +@node.run_action(skip_if=node.parameters.load_data_id is not None or node.parameters.simulate) +def execute_qua_program(node: QualibrationNode[Parameters, Quam]): + """Execute the QUA program and fetch raw data.""" + qmm = node.machine.connect() + config = node.machine.generate_config() + with qm_session(qmm, config, timeout=node.parameters.timeout) as qm: + node.namespace["job"] = job = qm.execute(node.namespace["qua_program"]) + data_fetcher = XarrayDataFetcher(job, node.namespace["sweep_axes"]) + for dataset in data_fetcher: + progress_counter( + data_fetcher.get("n", 0), + node.parameters.num_shots, + start_time=data_fetcher.t_start, + ) + node.log(job.execution_report()) + node.results["ds_raw"] = dataset + + +# %% {Load_data} +@node.run_action(skip_if=node.parameters.load_data_id is None) +def load_data(node: QualibrationNode[Parameters, Quam]): + """Load a previously acquired dataset.""" + load_data_id = node.parameters.load_data_id + node.load_from_id(node.parameters.load_data_id) + node.parameters.load_data_id = load_data_id + node.namespace["qubit_pairs"] = get_qubit_pairs(node) + + +# %% {Analyse_data} +@node.run_action(skip_if=node.parameters.simulate) +def analyse_data(node: QualibrationNode[Parameters, Quam]): + """Process raw data, run fits, and set outcomes.""" + node.results["ds_raw"] = process_raw_dataset(node.results["ds_raw"], node) + node.results["ds_fit"], fit_results = fit_raw_data(node.results["ds_raw"], node) + node.results["fit_results"] = {k: asdict(v) for k, v in fit_results.items()} + + log_fitted_results(fit_results, log_callable=node.log) + node.outcomes = { + qp_name: ("successful" if fit_result.success else "failed") for qp_name, fit_result in fit_results.items() + } + + +# %% {Plot_data} +@node.run_action(skip_if=node.parameters.simulate) +def plot_data(node: QualibrationNode[Parameters, Quam]): + """Plot phase-vs-operations fit for each qubit pair.""" + fig_raw_fit = plot_raw_data_with_fit(node.results["ds_raw"], node.namespace["qubit_pairs"], node.results["ds_fit"]) + plt.show() + node.results["figures"] = {"phase_vs_operations": fig_raw_fit} + + +# %% {Update_state} +@node.run_action(skip_if=node.parameters.simulate) +def update_state(node: QualibrationNode[Parameters, Quam]): + """Update local-Z compensation with the fitted phase from the max-mean method.""" + cz_operation = node.parameters.operation + with node.record_state_updates(): + for qp in node.namespace["qubit_pairs"]: + if node.outcomes[qp.name] == "failed": + continue + + old_phase_control = qp.macros[cz_operation].phase_shift_control + old_phase_target = qp.macros[cz_operation].phase_shift_target + fitted_control_phase = float( + node.results["ds_fit"].sel(qubit_pair=qp.name).fitted_control_phase.values + ) + fitted_target_phase = float( + node.results["ds_fit"].sel(qubit_pair=qp.name).fitted_target_phase.values + ) + + qp.macros[cz_operation].phase_shift_control = (old_phase_control + fitted_control_phase) % 1 + qp.macros[cz_operation].phase_shift_target = (old_phase_target + fitted_target_phase) % 1 + + +# %% {Save_results} +@node.run_action() +def save_results(node: QualibrationNode[Parameters, Quam]): + """Save node results and state updates.""" + node.save() + +# %% From 123304494b253a884fee7094836c6fd670d0d225 Mon Sep 17 00:00:00 2001 From: paulQM Date: Fri, 19 Jun 2026 12:43:32 +0200 Subject: [PATCH 02/17] rename files and add parameter --- .vscode/settings.json | 4 ++++ .../cz_phase_compensation_error_amp/parameters.py | 2 ++ ...compensation.py => 33a_cz_phase_compensation.py} | 2 +- ...mp.py => 33b_cz_phase_compensation_error_amp.py} | 13 +++++-------- 4 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 .vscode/settings.json rename qualibration_graphs/superconducting/calibrations/CZ_calibrations/{33_cz_phase_compensation.py => 33a_cz_phase_compensation.py} (99%) rename qualibration_graphs/superconducting/calibrations/CZ_calibrations/{35a_cz_phase_compensation_error_amp.py => 33b_cz_phase_compensation_error_amp.py} (96%) diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..ba2a6c013 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python-envs.defaultEnvManager": "ms-python.python:system", + "python-envs.pythonProjects": [] +} \ No newline at end of file diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/parameters.py b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/parameters.py index 6a4632262..2829531ab 100644 --- a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/parameters.py +++ b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/parameters.py @@ -14,6 +14,8 @@ class NodeSpecificParameters(RunnableParameters): num_shots: int = 100 """Number of shots to perform. Default is 100.""" + frame_range: float = 0.1 + """Range of frame rotation to sweep. Default is 0.1.""" num_frames: int = 17 """Number of phase frames to sweep. Default is 17.""" number_of_operations: int = 8 diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33_cz_phase_compensation.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_phase_compensation.py similarity index 99% rename from qualibration_graphs/superconducting/calibrations/CZ_calibrations/33_cz_phase_compensation.py rename to qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_phase_compensation.py index 820d12436..c250b0be0 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33_cz_phase_compensation.py +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_phase_compensation.py @@ -46,7 +46,7 @@ # Be sure to include [Parameters, Quam] so the node has proper type hinting node = QualibrationNode[Parameters, Quam]( - name="33_cz_phase_compensation", # Name should be unique + name="33a_cz_phase_compensation", # Name should be unique description=description, # Describe what the node is doing, which is also reflected in the QUAlibrate GUI parameters=Parameters(), # Node parameters defined under quam_experiment/experiments/node_name machine=Quam.load(), diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/35a_cz_phase_compensation_error_amp.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_phase_compensation_error_amp.py similarity index 96% rename from qualibration_graphs/superconducting/calibrations/CZ_calibrations/35a_cz_phase_compensation_error_amp.py rename to qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_phase_compensation_error_amp.py index 4188f2c12..2cb604ddf 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/35a_cz_phase_compensation_error_amp.py +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_phase_compensation_error_amp.py @@ -42,7 +42,7 @@ """ node = QualibrationNode[Parameters, Quam]( - name="35a_cz_phase_compensation_error_amp", + name="33b_cz_phase_compensation_error_amp", description=description, parameters=Parameters(), machine=Quam.load(), @@ -65,7 +65,7 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: dis num_qubit_pairs = len(qubit_pairs) n_avg = node.parameters.num_shots - frames = np.arange(-0.1, 0.1, 0.2 / node.parameters.num_frames) + frames = np.arange(-node.parameters.frame_range / 2, node.parameters.frame_range / 2, node.parameters.num_frames) num_operations = node.parameters.number_of_operations cz_operation = node.parameters.operation @@ -232,12 +232,8 @@ def update_state(node: QualibrationNode[Parameters, Quam]): old_phase_control = qp.macros[cz_operation].phase_shift_control old_phase_target = qp.macros[cz_operation].phase_shift_target - fitted_control_phase = float( - node.results["ds_fit"].sel(qubit_pair=qp.name).fitted_control_phase.values - ) - fitted_target_phase = float( - node.results["ds_fit"].sel(qubit_pair=qp.name).fitted_target_phase.values - ) + fitted_control_phase = float(node.results["ds_fit"].sel(qubit_pair=qp.name).fitted_control_phase.values) + fitted_target_phase = float(node.results["ds_fit"].sel(qubit_pair=qp.name).fitted_target_phase.values) qp.macros[cz_operation].phase_shift_control = (old_phase_control + fitted_control_phase) % 1 qp.macros[cz_operation].phase_shift_target = (old_phase_target + fitted_target_phase) % 1 @@ -249,4 +245,5 @@ def save_results(node: QualibrationNode[Parameters, Quam]): """Save node results and state updates.""" node.save() + # %% From 8053e8f41fe85f8a5153e4b40f9861a280cd1797 Mon Sep 17 00:00:00 2001 From: paulQM Date: Fri, 19 Jun 2026 13:28:17 +0200 Subject: [PATCH 03/17] feat: improve analysis with sync fit --- .../analysis.py | 192 ++++++++++++++++-- .../plotting.py | 76 ++++++- .../1Q_calibrations/07_iq_blobs.py | 4 +- .../33a_cz_phase_compensation.py | 2 +- .../33b_cz_phase_compensation_error_amp.py | 6 +- 5 files changed, 246 insertions(+), 34 deletions(-) diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/analysis.py b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/analysis.py index 247a592af..3f059caab 100644 --- a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/analysis.py +++ b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/analysis.py @@ -1,19 +1,23 @@ """Analysis functions for CZ phase compensation with error amplification calibration. -For each qubit (control / target), the fitted phase is the frame value whose -vertical linecut (across all number_of_operations) has the highest mean signal. -When state discrimination is used this mean should approach 1 at the optimal -compensation point. +For each qubit (control / target), the signal is averaged over the number of CZ +operations and fit with a sinc model to locate the optimal compensation frame: + + f(frame) = B + A * sinc(w * (frame - frame_0) / pi) + +When state discrimination is used the peak should approach 1 at the optimal +compensation point. A parabolic-refinement fallback is used if the sinc fit fails. """ import logging -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Dict, Tuple import numpy as np import xarray as xr from qualibrate import QualibrationNode from qualibration_libs.data import convert_IQ_to_V +from scipy.optimize import curve_fit @dataclass @@ -25,18 +29,130 @@ class FitResults: control_mean_at_peak: float target_mean_at_peak: float success: bool + control_fit_method: str = "sinc" + target_fit_method: str = "sinc" + control_sinc_params: Dict[str, float] = field(default_factory=dict) + target_sinc_params: Dict[str, float] = field(default_factory=dict) + + +def _sinc_model(x: np.ndarray, A: float, B: float, x_0: float, w: float) -> np.ndarray: + """Sinc model used to localise the central peak of the averaged signal.""" + return B + A * np.sinc(w * (x - x_0) / np.pi) + + +def _parabolic_refine(y: np.ndarray, i: int) -> float: + """Three-point parabolic refinement around an extremum index, returning fractional index.""" + if i <= 0 or i >= len(y) - 1: + return float(i) + y1, y2, y3 = y[i - 1], y[i], y[i + 1] + denom = y1 - 2.0 * y2 + y3 + if denom == 0: + return float(i) + delta = 0.5 * (y1 - y3) / denom + return float(i) + float(np.clip(delta, -0.5, 0.5)) + + +def _argmax_with_refine(x_values: np.ndarray, y: np.ndarray) -> float: + """Argmax of ``y`` over ``x_values`` with 3-point parabolic refinement.""" + i = int(np.argmax(y)) + i_star = _parabolic_refine(y, i) + return float(np.interp(i_star, np.arange(len(x_values)), x_values)) + + +def _estimate_fwhm_around(x_values: np.ndarray, y: np.ndarray, i_max: int) -> float: + """Estimate the full-width at half-maximum of the central peak around ``i_max``.""" + finite = y[np.isfinite(y)] + if finite.size == 0: + return float("nan") + y_max = float(y[i_max]) + y_min = float(np.min(finite)) + half = y_max - (y_max - y_min) / 2.0 + left = i_max + while left > 0 and y[left] > half: + left -= 1 + right = i_max + while right < len(y) - 1 and y[right] > half: + right += 1 + fwhm = float(x_values[right] - x_values[left]) + if fwhm > 0: + return fwhm + return float(x_values[-1] - x_values[0]) / 4.0 -def _find_phase_from_max_mean(signal: xr.DataArray) -> Tuple[float, float, np.ndarray]: - """Find the frame value whose vertical linecut has the highest mean across operations. +def _fit_sinc_peak( + x_values: np.ndarray, y: np.ndarray +) -> Tuple[float, float, bool, str, Dict[str, float], np.ndarray]: + """Fit a sinc model to a 1D curve and return the peak position.""" + if len(x_values) != len(y) or len(x_values) == 0: + return float("nan"), float("nan"), False, "none", {}, np.full_like(x_values, np.nan, dtype=float) - Returns (best_frame, best_mean, mean_curve_as_numpy). - """ + y_avg = np.asarray(y, dtype=float) + if not np.any(np.isfinite(y_avg)): + return float("nan"), float("nan"), False, "none", {}, np.full_like(x_values, np.nan, dtype=float) + + y_finite = np.where(np.isfinite(y_avg), y_avg, -np.inf) + i_max = int(np.argmax(y_finite)) + + A_init = float(np.nanmax(y_avg) - np.nanmedian(y_avg)) + if not np.isfinite(A_init) or A_init <= 0: + A_init = 0.5 + B_init = float(np.nanmedian(y_avg)) + if not np.isfinite(B_init): + B_init = 0.5 + x_seed_init = float(x_values[i_max]) + fwhm = _estimate_fwhm_around(x_values, y_avg, i_max) + if np.isfinite(fwhm) and fwhm > 0: + w_init = 3.79 / fwhm + else: + w_init = 3.79 / max(float(x_values[-1] - x_values[0]) / 4.0, 1e-9) + + x_min, x_max = float(np.min(x_values)), float(np.max(x_values)) + + try: + popt, _ = curve_fit( + _sinc_model, + x_values, + y_avg, + p0=[A_init, B_init, x_seed_init, w_init], + bounds=( + [0.0, -0.5, x_min, w_init / 50.0], + [2.0, 1.5, x_max, w_init * 50.0], + ), + maxfev=10000, + ) + A_fit, B_fit, x_0_fit, w_fit = map(float, popt) + if not x_min <= x_0_fit <= x_max: + raise RuntimeError(f"Fitted frame = {x_0_fit} outside swept window.") + fit_curve = _sinc_model(x_values, A_fit, B_fit, x_0_fit, w_fit) + return ( + x_0_fit, + B_fit + A_fit, + True, + "sinc", + {"A": A_fit, "B": B_fit, "frame_0": x_0_fit, "w": w_fit}, + fit_curve, + ) + except Exception: # pylint: disable=broad-except + x_refined = _argmax_with_refine(x_values, y_finite) + y_at_peak = float(np.interp(x_refined, x_values, y_avg)) + return ( + x_refined, + y_at_peak, + bool(np.isfinite(x_refined)), + "parabolic", + {}, + np.full_like(x_values, np.nan, dtype=float), + ) + + +def _find_phase_from_sinc_fit( + signal: xr.DataArray, +) -> Tuple[float, float, bool, str, Dict[str, float], np.ndarray, np.ndarray]: + """Average over operations and fit a sinc model to locate the optimal frame.""" mean_vs_frame = signal.mean(dim="number_of_operations") frames = mean_vs_frame.frame.values values = mean_vs_frame.values - best_idx = int(np.argmax(values)) - return float(frames[best_idx]), float(values[best_idx]), values + return _fit_sinc_peak(frames, values) + (values,) def log_fitted_results(fit_results: Dict[str, FitResults], log_callable=None): @@ -48,8 +164,8 @@ def log_fitted_results(fit_results: Dict[str, FitResults], log_callable=None): status = "SUCCESS" if fit_result.success else "FAIL" log_callable( f"{qp_name} [{status}] | " - f"control_phase={fit_result.fitted_control_phase:.6f}, " - f"target_phase={fit_result.fitted_target_phase:.6f}, " + f"control_phase={fit_result.fitted_control_phase:.6f} ({fit_result.control_fit_method}), " + f"target_phase={fit_result.fitted_target_phase:.6f} ({fit_result.target_fit_method}), " f"control_peak_mean={fit_result.control_mean_at_peak:.4f}, " f"target_peak_mean={fit_result.target_mean_at_peak:.4f}" ) @@ -83,7 +199,7 @@ def _get_signals(qp_data: xr.Dataset): def fit_raw_data( ds: xr.Dataset, node: QualibrationNode ) -> Tuple[xr.Dataset, Dict[str, FitResults]]: - """Find the optimal phase compensation by locating the frame with highest mean signal.""" + """Find optimal phase compensation by fitting a sinc model to the averaged signal.""" frames = ds.frame.values fit_datasets = [] fit_results: Dict[str, FitResults] = {} @@ -95,18 +211,48 @@ def fit_raw_data( try: signal_control, signal_target = _get_signals(qp_data) - control_phase, control_peak_mean, control_mean_curve = _find_phase_from_max_mean(signal_control) - target_phase, target_peak_mean, target_mean_curve = _find_phase_from_max_mean(signal_target) + ( + control_phase, + control_peak_mean, + control_success, + control_method, + control_params, + control_sinc_fit, + control_mean_curve, + ) = _find_phase_from_sinc_fit(signal_control) + ( + target_phase, + target_peak_mean, + target_success, + target_method, + target_params, + target_sinc_fit, + target_mean_curve, + ) = _find_phase_from_sinc_fit(signal_target) + + success = control_success and target_success fit_ds = xr.Dataset( { - "control_mean_vs_frame": xr.DataArray(control_mean_curve, dims=("frame",), coords={"frame": frames}), - "target_mean_vs_frame": xr.DataArray(target_mean_curve, dims=("frame",), coords={"frame": frames}), + "control_mean_vs_frame": xr.DataArray( + control_mean_curve, dims=("frame",), coords={"frame": frames} + ), + "target_mean_vs_frame": xr.DataArray( + target_mean_curve, dims=("frame",), coords={"frame": frames} + ), + "control_sinc_fit": xr.DataArray( + control_sinc_fit, dims=("frame",), coords={"frame": frames} + ), + "target_sinc_fit": xr.DataArray( + target_sinc_fit, dims=("frame",), coords={"frame": frames} + ), "fitted_control_phase": xr.DataArray(control_phase), "fitted_target_phase": xr.DataArray(target_phase), "control_mean_at_peak": xr.DataArray(control_peak_mean), "target_mean_at_peak": xr.DataArray(target_peak_mean), - "success": xr.DataArray(True), + "control_fit_method": xr.DataArray(control_method), + "target_fit_method": xr.DataArray(target_method), + "success": xr.DataArray(success), } ) fit_results[qp_name] = FitResults( @@ -114,7 +260,11 @@ def fit_raw_data( fitted_target_phase=target_phase, control_mean_at_peak=control_peak_mean, target_mean_at_peak=target_peak_mean, - success=True, + success=success, + control_fit_method=control_method, + target_fit_method=target_method, + control_sinc_params=control_params, + target_sinc_params=target_params, ) except Exception: fit_ds = xr.Dataset({"success": xr.DataArray(False)}) @@ -124,6 +274,8 @@ def fit_raw_data( control_mean_at_peak=np.nan, target_mean_at_peak=np.nan, success=False, + control_fit_method="none", + target_fit_method="none", ) fit_datasets.append(fit_ds.expand_dims(qubit_pair=[qp_name])) diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py index d4abc2ce2..f2f7c8aa2 100644 --- a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py +++ b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py @@ -4,9 +4,23 @@ import numpy as np import xarray as xr +# Per-channel colors: muted markers for data, bold contrasting lines for the fit. +_PANEL_STYLES = { + "control": { + "data": {"color": "#3b82f6", "markeredgecolor": "#1e3a8a"}, + "fit": {"color": "#ea580c"}, + "optimum": {"color": "#15803d"}, + }, + "target": { + "data": {"color": "#ef4444", "markeredgecolor": "#991b1b"}, + "fit": {"color": "#7c3aed"}, + "optimum": {"color": "#15803d"}, + }, +} + def plot_raw_data_with_fit(ds_raw: xr.Dataset, qubit_pairs, ds_fit: xr.Dataset = None): - """Plot mean signal vs frame for control and target, highlighting the peak frame.""" + """Plot mean signal vs frame for control and target with the sinc fit overlay.""" n_pairs = len(qubit_pairs) fig, axes = plt.subplots(2, n_pairs, figsize=(6 * n_pairs, 8), squeeze=False) @@ -27,32 +41,76 @@ def plot_raw_data_with_fit(ds_raw: xr.Dataset, qubit_pairs, ds_fit: xr.Dataset = qp_fit = ds_fit.sel(qubit_pair=qp_name) frames = ds_raw.sel(qubit_pair=qp_name).frame.values - for row, (label, color) in enumerate([("control", "tab:blue"), ("target", "tab:red")]): + for row, label in enumerate(["control", "target"]): ax = axes[row, i] + style = _PANEL_STYLES[label] mean_var = f"{label}_mean_vs_frame" + sinc_var = f"{label}_sinc_fit" phase_var = f"fitted_{label}_phase" peak_mean_var = f"{label}_mean_at_peak" + method_var = f"{label}_fit_method" if mean_var in qp_fit.data_vars: mean_curve = qp_fit[mean_var].values fitted_phase = float(qp_fit[phase_var].values) peak_mean = float(qp_fit[peak_mean_var].values) + fit_method = str(qp_fit[method_var].values) - ax.plot(frames, mean_curve, "o-", color=color) - ax.axvline(fitted_phase, color="k", ls="--", alpha=0.7, label=f"phase = {fitted_phase:.4f}") - ax.plot(fitted_phase, peak_mean, "*", color="gold", ms=15, zorder=5, - label=f"peak mean = {peak_mean:.4f}") - ax.legend(loc="best") + ax.plot( + frames, + mean_curve, + linestyle="none", + marker="o", + ms=5, + mew=0.8, + alpha=0.9, + zorder=2, + label=r"$\langle signal \rangle_N$", + **style["data"], + ) + if sinc_var in qp_fit.data_vars: + fit_vals = qp_fit[sinc_var].values + if np.any(np.isfinite(fit_vals)): + ax.plot( + frames, + fit_vals, + "-", + lw=2.5, + zorder=4, + label="sinc fit", + **style["fit"], + ) + ax.axvline( + fitted_phase, + color=style["optimum"]["color"], + ls="--", + lw=1.8, + alpha=0.95, + zorder=3, + label=f"phase = {fitted_phase:.4f} ({fit_method})", + ) + ax.plot( + fitted_phase, + peak_mean, + "*", + ms=14, + mew=1.2, + zorder=5, + markerfacecolor="white", + markeredgecolor=style["fit"]["color"], + label=f"peak mean = {peak_mean:.4f}", + ) + ax.legend(loc="best", fontsize=9) ax.set_title(f"{qp_name} - {label}") ax.set_xlabel("Frame rotation [2π]") ax.set_ylabel("Mean signal") - ax.grid(alpha=0.3) + ax.grid(alpha=0.25, color="#94a3b8") for j in range(n_pairs, axes.shape[1]): axes[0, j].axis("off") axes[1, j].axis("off") - fig.suptitle("CZ phase compensation - error amplification (max-mean method)") + fig.suptitle("CZ phase compensation - error amplification (sinc fit)") fig.tight_layout(rect=(0, 0, 1, 0.95)) return fig diff --git a/qualibration_graphs/superconducting/calibrations/1Q_calibrations/07_iq_blobs.py b/qualibration_graphs/superconducting/calibrations/1Q_calibrations/07_iq_blobs.py index 6e40a4bd3..8f07995a4 100644 --- a/qualibration_graphs/superconducting/calibrations/1Q_calibrations/07_iq_blobs.py +++ b/qualibration_graphs/superconducting/calibrations/1Q_calibrations/07_iq_blobs.py @@ -66,7 +66,7 @@ def custom_param(node: QualibrationNode[Parameters, Quam]): execution in the Python IDE. """ # You can get type hinting in your IDE by typing node.parameters. - # node.parameters.qubits = ["q1", "q2"] + node.parameters.qubits = ["qD3", "qA6"] pass @@ -264,3 +264,5 @@ def update_state(node: QualibrationNode[Parameters, Quam]): @node.run_action() def save_results(node: QualibrationNode[Parameters, Quam]): node.save() + +# %% diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_phase_compensation.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_phase_compensation.py index c250b0be0..de5acf758 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_phase_compensation.py +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_phase_compensation.py @@ -153,7 +153,7 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): qp.qubit_target.resonator.measure("readout", qua_vars=(I_t[ii], Q_t[ii])) save(I_t[ii], I_t_st[ii]) save(Q_t[ii], Q_t_st[ii]) - + align() with stream_processing(): n_st.save("n") for ii in range(num_qubit_pairs): diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_phase_compensation_error_amp.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_phase_compensation_error_amp.py index 2cb604ddf..ea6e034d9 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_phase_compensation_error_amp.py +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_phase_compensation_error_amp.py @@ -34,7 +34,7 @@ 1. Prepare either control or target qubit in a Ramsey-like sequence. 2. Apply a train of CZ macros (1..N repetitions). 3. Sweep a final virtual frame rotation and fit the Ramsey oscillation phase per repetition count. -4. Fit phase vs number of operations with a line; the slope gives the per-CZ local phase. +4. Average the signal over repetitions and fit a sinc model to locate the optimal compensation frame. State update: - qp.macros[operation].phase_shift_control @@ -65,7 +65,7 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: dis num_qubit_pairs = len(qubit_pairs) n_avg = node.parameters.num_shots - frames = np.arange(-node.parameters.frame_range / 2, node.parameters.frame_range / 2, node.parameters.num_frames) + frames = np.linspace(-node.parameters.frame_range / 2, node.parameters.frame_range / 2, node.parameters.num_frames) num_operations = node.parameters.number_of_operations cz_operation = node.parameters.operation @@ -223,7 +223,7 @@ def plot_data(node: QualibrationNode[Parameters, Quam]): # %% {Update_state} @node.run_action(skip_if=node.parameters.simulate) def update_state(node: QualibrationNode[Parameters, Quam]): - """Update local-Z compensation with the fitted phase from the max-mean method.""" + """Update local-Z compensation with the fitted phase from the sinc fit.""" cz_operation = node.parameters.operation with node.record_state_updates(): for qp in node.namespace["qubit_pairs"]: From f4ff5b5311759e234a79c0f045d54b7a971fb7bb Mon Sep 17 00:00:00 2001 From: paulQM Date: Fri, 19 Jun 2026 13:40:28 +0200 Subject: [PATCH 04/17] chore: remove local VS Code settings from repo Co-authored-by: Cursor --- .vscode/settings.json | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index ba2a6c013..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "python-envs.defaultEnvManager": "ms-python.python:system", - "python-envs.pythonProjects": [] -} \ No newline at end of file From 8f6c7140de7e0ea1f047e00702f345633d1667a2 Mon Sep 17 00:00:00 2001 From: paulQM Date: Tue, 30 Jun 2026 10:55:09 +0200 Subject: [PATCH 05/17] fix: black formatting --- .../analysis.py | 20 +++++-------------- .../plotting.py | 8 ++++++-- .../1Q_calibrations/07_iq_blobs.py | 1 + 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/analysis.py b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/analysis.py index 3f059caab..266e94b12 100644 --- a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/analysis.py +++ b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/analysis.py @@ -79,9 +79,7 @@ def _estimate_fwhm_around(x_values: np.ndarray, y: np.ndarray, i_max: int) -> fl return float(x_values[-1] - x_values[0]) / 4.0 -def _fit_sinc_peak( - x_values: np.ndarray, y: np.ndarray -) -> Tuple[float, float, bool, str, Dict[str, float], np.ndarray]: +def _fit_sinc_peak(x_values: np.ndarray, y: np.ndarray) -> Tuple[float, float, bool, str, Dict[str, float], np.ndarray]: """Fit a sinc model to a 1D curve and return the peak position.""" if len(x_values) != len(y) or len(x_values) == 0: return float("nan"), float("nan"), False, "none", {}, np.full_like(x_values, np.nan, dtype=float) @@ -196,9 +194,7 @@ def _get_signals(qp_data: xr.Dataset): return signal_control, signal_target -def fit_raw_data( - ds: xr.Dataset, node: QualibrationNode -) -> Tuple[xr.Dataset, Dict[str, FitResults]]: +def fit_raw_data(ds: xr.Dataset, node: QualibrationNode) -> Tuple[xr.Dataset, Dict[str, FitResults]]: """Find optimal phase compensation by fitting a sinc model to the averaged signal.""" frames = ds.frame.values fit_datasets = [] @@ -237,15 +233,9 @@ def fit_raw_data( "control_mean_vs_frame": xr.DataArray( control_mean_curve, dims=("frame",), coords={"frame": frames} ), - "target_mean_vs_frame": xr.DataArray( - target_mean_curve, dims=("frame",), coords={"frame": frames} - ), - "control_sinc_fit": xr.DataArray( - control_sinc_fit, dims=("frame",), coords={"frame": frames} - ), - "target_sinc_fit": xr.DataArray( - target_sinc_fit, dims=("frame",), coords={"frame": frames} - ), + "target_mean_vs_frame": xr.DataArray(target_mean_curve, dims=("frame",), coords={"frame": frames}), + "control_sinc_fit": xr.DataArray(control_sinc_fit, dims=("frame",), coords={"frame": frames}), + "target_sinc_fit": xr.DataArray(target_sinc_fit, dims=("frame",), coords={"frame": frames}), "fitted_control_phase": xr.DataArray(control_phase), "fitted_target_phase": xr.DataArray(target_phase), "control_mean_at_peak": xr.DataArray(control_peak_mean), diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py index f2f7c8aa2..fa5aea63a 100644 --- a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py +++ b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py @@ -30,8 +30,12 @@ def plot_raw_data_with_fit(ds_raw: xr.Dataset, qubit_pairs, ds_fit: xr.Dataset = if ds_fit is None or not bool(ds_fit.sel(qubit_pair=qp_name).success.values): for row in range(2): axes[row, i].text( - 0.5, 0.5, "fit failed" if ds_fit else "no fit", - ha="center", va="center", transform=axes[row, i].transAxes, + 0.5, + 0.5, + "fit failed" if ds_fit else "no fit", + ha="center", + va="center", + transform=axes[row, i].transAxes, ) axes[row, i].set_title(f"{qp_name} - {'control' if row == 0 else 'target'}") axes[row, i].set_xlabel("Frame rotation [2π]") diff --git a/qualibration_graphs/superconducting/calibrations/1Q_calibrations/07_iq_blobs.py b/qualibration_graphs/superconducting/calibrations/1Q_calibrations/07_iq_blobs.py index 8f07995a4..11106dd59 100644 --- a/qualibration_graphs/superconducting/calibrations/1Q_calibrations/07_iq_blobs.py +++ b/qualibration_graphs/superconducting/calibrations/1Q_calibrations/07_iq_blobs.py @@ -265,4 +265,5 @@ def update_state(node: QualibrationNode[Parameters, Quam]): def save_results(node: QualibrationNode[Parameters, Quam]): node.save() + # %% From 8f81aa321d6b24a14fbf3d3d582f6c7f4614bc09 Mon Sep 17 00:00:00 2001 From: paulQM Date: Wed, 1 Jul 2026 13:37:39 +0200 Subject: [PATCH 06/17] fix: revert unwanted change --- .../superconducting/calibrations/1Q_calibrations/07_iq_blobs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qualibration_graphs/superconducting/calibrations/1Q_calibrations/07_iq_blobs.py b/qualibration_graphs/superconducting/calibrations/1Q_calibrations/07_iq_blobs.py index 11106dd59..3f1c95e23 100644 --- a/qualibration_graphs/superconducting/calibrations/1Q_calibrations/07_iq_blobs.py +++ b/qualibration_graphs/superconducting/calibrations/1Q_calibrations/07_iq_blobs.py @@ -66,7 +66,7 @@ def custom_param(node: QualibrationNode[Parameters, Quam]): execution in the Python IDE. """ # You can get type hinting in your IDE by typing node.parameters. - node.parameters.qubits = ["qD3", "qA6"] + # node.parameters.qubits = ["q1", "q2"] pass From ce6edd99bcb99512acb204e01198f943c486e82f Mon Sep 17 00:00:00 2001 From: paulQM Date: Wed, 1 Jul 2026 13:39:34 +0200 Subject: [PATCH 07/17] fix: rename nodes --- ...3a_cz_phase_compensation.py => 34a_cz_phase_compensation.py} | 2 +- ...tion_error_amp.py => 34b_cz_phase_compensation_error_amp.py} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename qualibration_graphs/superconducting/calibrations/CZ_calibrations/{33a_cz_phase_compensation.py => 34a_cz_phase_compensation.py} (99%) rename qualibration_graphs/superconducting/calibrations/CZ_calibrations/{33b_cz_phase_compensation_error_amp.py => 34b_cz_phase_compensation_error_amp.py} (99%) diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_phase_compensation.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34a_cz_phase_compensation.py similarity index 99% rename from qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_phase_compensation.py rename to qualibration_graphs/superconducting/calibrations/CZ_calibrations/34a_cz_phase_compensation.py index de5acf758..660fb7464 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_phase_compensation.py +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34a_cz_phase_compensation.py @@ -46,7 +46,7 @@ # Be sure to include [Parameters, Quam] so the node has proper type hinting node = QualibrationNode[Parameters, Quam]( - name="33a_cz_phase_compensation", # Name should be unique + name="34a_cz_phase_compensation", # Name should be unique description=description, # Describe what the node is doing, which is also reflected in the QUAlibrate GUI parameters=Parameters(), # Node parameters defined under quam_experiment/experiments/node_name machine=Quam.load(), diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_phase_compensation_error_amp.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34b_cz_phase_compensation_error_amp.py similarity index 99% rename from qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_phase_compensation_error_amp.py rename to qualibration_graphs/superconducting/calibrations/CZ_calibrations/34b_cz_phase_compensation_error_amp.py index ea6e034d9..a0772a281 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_phase_compensation_error_amp.py +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34b_cz_phase_compensation_error_amp.py @@ -42,7 +42,7 @@ """ node = QualibrationNode[Parameters, Quam]( - name="33b_cz_phase_compensation_error_amp", + name="34b_cz_phase_compensation_error_amp", description=description, parameters=Parameters(), machine=Quam.load(), From 5279dfb736bef563bf8679b6a141f3b4cccc686e Mon Sep 17 00:00:00 2001 From: Deepak Khurana <119570568+Deepakkhurrana@users.noreply.github.com> Date: Wed, 1 Jul 2026 14:10:59 +0200 Subject: [PATCH 08/17] dropping the duplicate node --- .../34_cz_phase_compensation.py | 286 ------------------ .../34b_cz_phase_compensation_error_amp.py | 2 +- 2 files changed, 1 insertion(+), 287 deletions(-) delete mode 100644 qualibration_graphs/superconducting/calibrations/CZ_calibrations/34_cz_phase_compensation.py diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34_cz_phase_compensation.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34_cz_phase_compensation.py deleted file mode 100644 index 094c28cba..000000000 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34_cz_phase_compensation.py +++ /dev/null @@ -1,286 +0,0 @@ -# %% {Imports} -from dataclasses import asdict - -import matplotlib.pyplot as plt -import numpy as np -import xarray as xr -from calibration_utils.cz_phase_compensation import ( - Parameters, - fit_raw_data, - log_fitted_results, - plot_raw_data_with_fit, - process_raw_dataset, -) -from qm.qua import * -from qualang_tools.loops import from_array -from qualang_tools.multi_user import qm_session -from qualang_tools.results import progress_counter -from qualang_tools.units import unit -from qualibrate import QualibrationNode -from qualibration_libs.data import XarrayDataFetcher -from qualibration_libs.parameters import get_qubit_pairs, get_qubits -from qualibration_libs.runtime import simulate_and_plot -from quam_config import Quam - -# %% {Description} -description = """ - CZ PHASE COMPENSATION -This program calibrates and compensates the residual single-qubit (local Z) phase shifts induced by a CZ gate. -For each selected qubit pair, the sequence prepares either the control or the target qubit in a Ramsey-like -fragment (x90 – CZ – x90) while sweeping an additional virtual frame rotation (fractional phase in [0,1) → 2π) that -is added on top of the currently stored phase compensation term (phase_shift_control / phase_shift_target). -The resulting oscillations versus frame rotation are sinusoidal; fitting their phase gives the residual phase error. - -From the fitted phases, updated compensation values are computed and written back so that future executions of the -CZ macro implicitly cancel these local phases, leaving only the intended conditional phase. - -Prerequisites: - - Having calibrated single-qubit gates and readout. - - The CZ macro (e.g. "cz_unipolar") must be calibrated and produce a conditional phase near π. - - (Optional) Prior coarse conditional phase calibration (e.g. separate CZ phase characterization) improves fit robustness. - -State update: - - qp.macros[operation].phase_shift_control - - qp.macros[operation].phase_shift_target -""" - -# Be sure to include [Parameters, Quam] so the node has proper type hinting -node = QualibrationNode[Parameters, Quam]( - name="34_cz_phase_compensation", # Name should be unique - description=description, # Describe what the node is doing, which is also reflected in the QUAlibrate GUI - parameters=Parameters(), # Node parameters defined under quam_experiment/experiments/node_name - machine=Quam.load(), -) - - -# Any parameters that should change for debugging purposes only should go in here -# These parameters are ignored when run through the GUI or as part of a graph -@node.run_action(skip_if=node.modes.external) -def custom_param(node: QualibrationNode[Parameters, Quam]): - """Allow the user to locally set the node parameters for debugging purposes, or execution in the Python IDE.""" - # You can get type hinting in your IDE by typing node.parameters. - # node.parameters.qubit_pairs = ["q1-q2"] - pass - - -# %% {Create_QUA_program} -@node.run_action(skip_if=node.parameters.load_data_id is not None) -def create_qua_program(node: QualibrationNode[Parameters, Quam]): - """Create the sweep axes and generate the QUA program from the pulse sequence and the node parameters.""" - - # Class containing tools to help handle units and conversions. - u = unit(coerce_to_integer=True) - # Get the active qubit pairs from the node and organize them by batches - node.namespace["qubit_pairs"] = qubit_pairs = get_qubit_pairs(node) - num_qubit_pairs = len(qubit_pairs) - - # Extract the sweep parameters and axes from the node parameters - n_avg = node.parameters.num_shots - frames = np.arange(0, 1, 1 / node.parameters.num_frames) - cz_operation = node.parameters.operation - - # Register the sweep axes to be added to the dataset when fetching data - node.namespace["sweep_axes"] = { - "qubit_pair": xr.DataArray(qubit_pairs.get_names()), - "frame": xr.DataArray(frames, attrs={"long_name": "frame rotation", "units": "2π"}), - } - - # The QUA program stored in the node namespace to be transfer to the simulation and execution run_actions - with program() as node.namespace["qua_program"]: - frame = declare(fixed) - n = declare(int) - n_st = declare_stream() - I_c, I_c_st, Q_c, Q_c_st, n, n_st = node.machine.declare_qua_variables() - I_t, I_t_st, Q_t, Q_t_st, _, _ = node.machine.declare_qua_variables() - state_c = [declare(int) for _ in range(num_qubit_pairs)] - state_t = [declare(int) for _ in range(num_qubit_pairs)] - state_c_st = [declare_stream() for _ in range(num_qubit_pairs)] - state_t_st = [declare_stream() for _ in range(num_qubit_pairs)] - extra_phase_c = declare(fixed) - extra_phase_t = declare(fixed) - - for multiplexed_qubit_pairs in qubit_pairs.batch(): - # Initialize the QPU in terms of flux points (flux tunable transmons and/or tunable couplers) - for qp in multiplexed_qubit_pairs.values(): - node.machine.initialize_qpu(target=qp.qubit_control) - node.machine.initialize_qpu(target=qp.qubit_target) - align() - # Loop over the number of averages - with for_(n, 0, n < n_avg, n + 1): - save(n, n_st) - # Loop over the virtual frame rotations for phase tomography - with for_(*from_array(frame, frames)): - for ii, qp in multiplexed_qubit_pairs.items(): - # Add the current compensation from the active state - assign(extra_phase_c, qp.macros[cz_operation].phase_shift_control) - assign(extra_phase_t, qp.macros[cz_operation].phase_shift_target) - # Loop over the state preparations for control and target qubits - for qubit, state_q, state_st in [ - (qp.qubit_control, state_c[ii], state_c_st[ii]), - (qp.qubit_target, state_t[ii], state_t_st[ii]), - ]: - # Reset both qubits and align - qp.qubit_control.reset( - reset_type=node.parameters.reset_type, simulate=node.parameters.simulate - ) - qp.qubit_target.reset( - reset_type=node.parameters.reset_type, simulate=node.parameters.simulate - ) - qp.align() - # Prepare the qubit in a superposition state with an x90 pulse - qubit.xy.play("x90") - qubit.align() - # Apply the CZ gate with the added frame rotation for phase tomography of the corresponding qubit - if qubit is qp.qubit_control: - qp.macros[cz_operation].apply(phase_shift_control=frame + extra_phase_c) - elif qubit is qp.qubit_target: - qp.macros[cz_operation].apply(phase_shift_target=frame + extra_phase_t) - # Final x90 pulse to complete the Ramsey sequence - qubit.xy.play("x90") - qp.align() - - # Measure the corresponding qubit and save the results in the appropriate stream - if node.parameters.use_state_discrimination: - # measure both qubits - qubit.readout_state(state_q) - save(state_q, state_st) - else: - if qubit is qp.qubit_control: - qp.qubit_control.resonator.measure("readout", qua_vars=(I_c[ii], Q_c[ii])) - save(I_c[ii], I_c_st[ii]) - save(Q_c[ii], Q_c_st[ii]) - elif qubit is qp.qubit_target: - qp.qubit_target.resonator.measure("readout", qua_vars=(I_t[ii], Q_t[ii])) - save(I_t[ii], I_t_st[ii]) - save(Q_t[ii], Q_t_st[ii]) - - with stream_processing(): - n_st.save("n") - for ii in range(num_qubit_pairs): - if node.parameters.use_state_discrimination: - state_c_st[ii].buffer(len(frames)).average().save(f"state_control{ii + 1}") - state_t_st[ii].buffer(len(frames)).average().save(f"state_target{ii + 1}") - else: - I_c_st[ii].buffer(len(frames)).average().save(f"I_control{ii + 1}") - Q_c_st[ii].buffer(len(frames)).average().save(f"Q_control{ii + 1}") - I_t_st[ii].buffer(len(frames)).average().save(f"I_target{ii + 1}") - Q_t_st[ii].buffer(len(frames)).average().save(f"Q_target{ii + 1}") - - -# %% {Simulate} -@node.run_action(skip_if=node.parameters.load_data_id is not None or not node.parameters.simulate) -def simulate_qua_program(node: QualibrationNode[Parameters, Quam]): - """Connect to the QOP and simulate the QUA program""" - # Connect to the QOP - qmm = node.machine.connect() - # Get the config from the machine - config = node.machine.generate_config() - # Simulate the QUA program, generate the waveform report and plot the simulated samples - samples, fig, wf_report = simulate_and_plot(qmm, config, node.namespace["qua_program"], node.parameters) - # Store the figure, waveform report and simulated samples - node.results["simulation"] = { - "figure": fig, - "wf_report": wf_report, - "samples": samples, - } - - -# %% {Execute} -@node.run_action(skip_if=node.parameters.load_data_id is not None or node.parameters.simulate) -def execute_qua_program(node: QualibrationNode[Parameters, Quam]): - """ - Connect to the QOP, execute the QUA program and fetch the raw data and store it in a xarray dataset - called "ds_raw". - """ - # Connect to the QOP - qmm = node.machine.connect() - # Get the config from the machine - config = node.machine.generate_config() - # Execute the QUA program only if the quantum machine is available (this is to avoid interrupting running jobs). - with qm_session(qmm, config, timeout=node.parameters.timeout) as qm: - # The job is stored in the node namespace to be reused in the fetching_data run_action - node.namespace["job"] = job = qm.execute(node.namespace["qua_program"]) - # Display the progress bar - data_fetcher = XarrayDataFetcher(job, node.namespace["sweep_axes"]) - for dataset in data_fetcher: - progress_counter( - data_fetcher["n"], - node.parameters.num_shots, - start_time=data_fetcher.t_start, - ) - # Display the execution report to expose possible runtime errors - node.log(job.execution_report()) - # Register the raw dataset - node.results["ds_raw"] = dataset - - -# %% {Load_data} -@node.run_action(skip_if=node.parameters.load_data_id is None) -def load_data(node: QualibrationNode[Parameters, Quam]): - """Load a previously acquired dataset.""" - load_data_id = node.parameters.load_data_id - # Load the specified dataset - node.load_from_id(node.parameters.load_data_id) - node.parameters.load_data_id = load_data_id - # Get the active qubits from the loaded node parameters - node.namespace["qubit_pairs"] = get_qubit_pairs(node) - - -# %% {Analyse_data} -@node.run_action(skip_if=node.parameters.simulate) -def analyse_data(node: QualibrationNode[Parameters, Quam]): - """ - Analyse the raw data and store the fitted data in another xarray dataset "ds_fit" and the fitted - results in the "fit_results" dictionary. - """ - node.results["ds_raw"] = process_raw_dataset(node.results["ds_raw"], node) - # Fit the raw data and extract the relevant fitted parameters - node.results["ds_fit"], fit_results = fit_raw_data(node.results["ds_raw"], node) - node.results["fit_results"] = {k: asdict(v) for k, v in fit_results.items()} - - # Log the relevant information extracted from the data analysis - log_fitted_results(node.results["fit_results"], log_callable=node.log) - node.outcomes = { - qubit_name: ("successful" if fit_result.get("success") else "failed") - for qubit_name, fit_result in node.results["fit_results"].items() - } - - -# %% {Plot_data} -@node.run_action(skip_if=node.parameters.simulate) -def plot_data(node: QualibrationNode[Parameters, Quam]): - """Plot the raw and fitted data in specific figures whose shape is given by qubit.grid_location.""" - fig_raw_fit = plot_raw_data_with_fit(node.results["ds_raw"], node.namespace["qubit_pairs"], node.results["ds_fit"]) - plt.show() - # Store the generated figures - node.results["figures"] = { - "raw_and_fit": fig_raw_fit, - } - - -# %% {Update_state} -@node.run_action(skip_if=node.parameters.simulate) -def update_state(node: QualibrationNode[Parameters, Quam]): - """Update the relevant parameters if the qubit data analysis was successful.""" - cz_operation = node.parameters.operation - with node.record_state_updates(): - for qp in node.namespace["qubit_pairs"]: - if node.outcomes[qp.name] == "failed": - continue - old_phase_control = qp.macros[cz_operation].phase_shift_control - old_phase_target = qp.macros[cz_operation].phase_shift_target - qp.macros[cz_operation].phase_shift_control = ( - old_phase_control - float(node.results["ds_fit"].sel(qubit_pair=qp.name).fitted_control_phase.values) - ) % 1 - qp.macros[cz_operation].phase_shift_target = ( - old_phase_target - float(node.results["ds_fit"].sel(qubit_pair=qp.name).fitted_target_phase.values) - ) % 1 - - -# %% {Save_results} -@node.run_action() -def save_results(node: QualibrationNode[Parameters, Quam]): - node.save() - - -# %% diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34b_cz_phase_compensation_error_amp.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34b_cz_phase_compensation_error_amp.py index a0772a281..6bddf8b0a 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34b_cz_phase_compensation_error_amp.py +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34b_cz_phase_compensation_error_amp.py @@ -27,7 +27,7 @@ CZ PHASE COMPENSATION WITH ERROR AMPLIFICATION This node calibrates residual single-qubit (local Z) phase shifts induced by the CZ macro. -Compared to the standard phase-compensation node, it repeats the CZ operation a variable +Compared to **34a_cz_phase_compensation**, it repeats the CZ operation a variable number of times to amplify phase errors before tomography. For each selected qubit pair: From 0417f5444d5c3d4caef72bff87af31c60dbe53cf Mon Sep 17 00:00:00 2001 From: Deepak Khurana <119570568+Deepakkhurrana@users.noreply.github.com> Date: Wed, 1 Jul 2026 14:11:21 +0200 Subject: [PATCH 09/17] minor docstring updates --- .../cz_phase_compensation/analysis.py | 83 ++++++++++++++++--- 1 file changed, 73 insertions(+), 10 deletions(-) diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/analysis.py b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/analysis.py index 86558be2d..ba1ca2a35 100644 --- a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/analysis.py +++ b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/analysis.py @@ -12,7 +12,19 @@ @dataclass class FitResults: - """Stores the relevant CZ conditional phase experiment fit parameters for a single qubit pair""" + """Fit results for a single qubit pair from the CZ phase compensation calibration. + + Attributes: + ----------- + control_phase_correction : float + Residual single-qubit phase accumulated by the control qubit during the CZ gate (in 2π units). + This value is subtracted from ``phase_shift_control`` in the state update. + target_phase_correction : float + Residual single-qubit phase accumulated by the target qubit during the CZ gate (in 2π units). + This value is subtracted from ``phase_shift_target`` in the state update. + success : bool + True if the sinusoidal fit converged for both qubits. + """ control_phase_correction: float target_phase_correction: float @@ -20,10 +32,19 @@ class FitResults: def fix_oscillation_phi_2pi(fit_data): - """Extract and fix the phase parameter from oscillation fit data.""" - # Extract the phase parameter from the fit results + """Extract and normalise the oscillation phase to the [0, 1) range (representing 0 to 2π). + + Parameters: + ----------- + fit_data : xr.DataArray + Oscillation fit result with a ``fit_vals`` dimension containing ``"phi"``. + + Returns: + -------- + xr.DataArray + Phase values normalised to [0, 1). + """ phase = fit_data.sel(fit_vals="phi") - # Normalize phase to [0, 1] range (representing 0 to 2π) phase = (phase / (2 * np.pi)) % 1 return phase @@ -57,8 +78,25 @@ def log_fitted_results(fit_results: Dict[str, FitResults], log_callable=None): log_callable(log_message) -def process_raw_dataset(ds: xr.Dataset, node: QualibrationNode): - # Convert IQ data into volts +def process_raw_dataset(ds: xr.Dataset, node: QualibrationNode) -> xr.Dataset: + """Convert raw IQ data to volts when state discrimination is not used. + + Parameters: + ----------- + ds : xr.Dataset + Raw dataset from the experiment, containing either ``I_control`` / ``Q_control`` / + ``I_target`` / ``Q_target`` (raw IQ) or ``state_control`` / ``state_target`` + (state-discrimination) variables. + node : QualibrationNode + Calibration node providing ``qubit_pairs`` from its namespace (required by + ``convert_IQ_to_V``). + + Returns: + -------- + xr.Dataset + Dataset with IQ variables converted to volts, or unchanged if state discrimination + was used. + """ if hasattr(ds, "I_control"): ds = convert_IQ_to_V( ds, qubit_pairs=node.namespace["qubit_pairs"], IQ_list=["I_control", "Q_control", "I_target", "Q_target"] @@ -91,7 +129,24 @@ def fit_raw_data(ds: xr.Dataset, node: QualibrationNode) -> Tuple[xr.Dataset, Di def fit_routine(da): + """Fit Ramsey frame-rotation oscillations and extract phase corrections for one qubit pair. + + Fits a sinusoidal oscillation over the ``frame`` axis separately for the control qubit + and the target qubit. The fitted phase of each oscillation gives the residual single-qubit + phase error introduced by the CZ gate. + + Parameters: + ----------- + da : xr.Dataset + Single-pair dataset with ``state_control`` / ``state_target`` (state-discrimination) + or ``I_control`` / ``I_target`` (raw IQ) variables, and a ``frame`` dimension. + Returns: + -------- + xr.Dataset + Input dataset extended with ``fitted_control``, ``fitted_target``, + ``fitted_control_phase``, ``fitted_target_phase``, and ``success`` data variables. + """ if hasattr(da, "state_target"): data_control = "state_control" data_target = "state_target" @@ -168,22 +223,30 @@ def _extract_relevant_parameters( ds_fit: xr.Dataset, node: QualibrationNode ) -> Tuple[xr.Dataset, Dict[str, FitResults]]: """ - Extract relevant fit parameters and create FitResults for each qubit pair. + Assign xarray metadata attributes and build the FitResults dictionary. Parameters: ----------- ds_fit : xr.Dataset - Dataset containing the fit results from fit_routine. + Dataset produced by applying ``fit_routine`` per qubit pair. Must contain + ``fitted_control_phase``, ``fitted_target_phase``, and ``success`` data variables. node : QualibrationNode - The calibration node containing parameters and qubit pairs. + Calibration node providing ``qubit_pairs`` from its namespace. Returns: -------- Tuple[xr.Dataset, Dict[str, FitResults]] - Dataset with additional metadata and dictionary of FitResults for each qubit pair. + Dataset with ``long_name`` / ``units`` attrs set on key variables, and a + dictionary of ``FitResults`` keyed by qubit pair name. """ qubit_pairs = node.namespace["qubit_pairs"] + # Add metadata attributes to the dataset + if "fitted_control_phase" in ds_fit.data_vars: + ds_fit.fitted_control_phase.attrs = {"long_name": "control qubit phase correction", "units": "2π"} + if "fitted_target_phase" in ds_fit.data_vars: + ds_fit.fitted_target_phase.attrs = {"long_name": "target qubit phase correction", "units": "2π"} + # Create FitResults for each qubit pair fit_results = {} for qp in qubit_pairs: From 4d18aac4c92262c43ba308f665cb7d64abb3f416 Mon Sep 17 00:00:00 2001 From: Deepak Khurana <119570568+Deepakkhurrana@users.noreply.github.com> Date: Wed, 1 Jul 2026 14:11:45 +0200 Subject: [PATCH 10/17] aligning plotting with pair grid logic --- .../cz_phase_compensation/plotting.py | 131 ++++++---- .../plotting.py | 233 +++++++++--------- 2 files changed, 205 insertions(+), 159 deletions(-) diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/plotting.py b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/plotting.py index 7088072ff..ea9db4ae0 100644 --- a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/plotting.py +++ b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/plotting.py @@ -1,52 +1,91 @@ -import matplotlib.pyplot as plt import xarray as xr -from qualibrate import QualibrationNode -from quam_config import Quam +from matplotlib.axes import Axes +from matplotlib.figure import Figure +from qualibration_libs.plotting import grid_iter -from .parameters import Parameters +from calibration_utils.pair_grid import QubitPairGrid, grid_pair_names -def plot_raw_data_with_fit(ds_raw: xr.Dataset, qubit_pairs: Quam, ds_fit: xr.Dataset = None): +def plot_raw_data_with_fit( + ds_raw: xr.Dataset, + qubit_pairs: list, + ds_fit: xr.Dataset = None, +) -> Figure: + """Plot raw Ramsey oscillations with fitted curves for every qubit pair on a chip-topology grid. + + Parameters + ---------- + ds_raw : xr.Dataset + Raw dataset containing ``state_control`` / ``state_target`` or + ``I_control`` / ``I_target`` variables with a ``frame`` dimension. + qubit_pairs : list + Qubit pair objects used for grid placement. + ds_fit : xr.Dataset, optional + Fit dataset containing ``fitted_control``, ``fitted_target``, and ``success``. + If None, only the raw data is shown. + + Returns + ------- + Figure + Matplotlib figure with one panel per qubit pair. """ - Plot the raw data with the fit for each qubit pair in a single figure. + grid_names, pair_names = grid_pair_names(qubit_pairs) + grid = QubitPairGrid(grid_names, pair_names) + + qp_map = {qp.name: qp for qp in qubit_pairs} + for ax, qubit in grid_iter(grid): + qp_name = qubit["qubit"] + plot_individual_data_with_fit(ax, ds_raw, qp_name, ds_fit) + + grid.fig.suptitle("CZ 1Q phase compensation") + grid.fig.tight_layout() + return grid.fig + + +def plot_individual_data_with_fit( + ax: Axes, + ds_raw: xr.Dataset, + qp_name: str, + ds_fit: xr.Dataset = None, +): + """Plot raw Ramsey oscillations and fitted curves for one qubit pair. + + Parameters + ---------- + ax : Axes + Axis to draw on. + ds_raw : xr.Dataset + Raw dataset with ``state_control`` / ``state_target`` or + ``I_control`` / ``I_target`` variables. + qp_name : str + Qubit pair name used to select data. + ds_fit : xr.Dataset, optional + Fit dataset containing ``fitted_control``, ``fitted_target``, and ``success``. + If None or fit failed, only raw data is shown. """ - n_pairs = len(qubit_pairs) - - fig, axes = plt.subplots(1, n_pairs, figsize=(5 * n_pairs, 4)) - if n_pairs == 1: - axes = [axes] - - for i, qp_name in enumerate(qubit_pairs): - ax = axes[i] - - # Select data for this qubit pair - qp_data = ds_raw.sel(qubit_pair=qp_name.name) - - # Plot raw data - if "state_control" in ds_raw.data_vars: - qp_data.state_control.plot(ax=ax, marker="o", linestyle="", color="blue", label="Control") - qp_data.state_target.plot(ax=ax, marker="o", linestyle="", color="red", label="Target") - else: - qp_data.I_control.sel(control_target="c").plot( - ax=ax, marker="o", linestyle="", color="blue", label="Control" - ) - qp_data.I_target.sel(control_target="t").plot(ax=ax, marker="o", linestyle="", color="red", label="Target") - - # Plot fitted data if available and fit was successful - if ds_fit is not None: - qp_fit_data = ds_fit.sel(qubit_pair=qp_name.name) - if qp_fit_data.success.values: - if "fitted_control" in ds_fit.data_vars: - qp_fit_data.fitted_control.plot(ax=ax, color="blue", alpha=0.5) - if "fitted_target" in ds_fit.data_vars: - qp_fit_data.fitted_target.plot(ax=ax, color="red", alpha=0.5) - if "state_control" in ds_raw.data_vars: - ax.set_ylabel("Measured State") - else: - ax.set_ylabel("Rotated I Quadrature [V]") - - ax.set_xlabel(r"x90 frame rotation [$\mathrm{rad}/2\pi$]") - ax.legend() - - plt.tight_layout() - return fig + qp_data = ds_raw.sel(qubit_pair=qp_name) + + if "state_control" in ds_raw.data_vars: + qp_data.state_control.plot(ax=ax, marker="o", linestyle="", color="blue", label="Control") + qp_data.state_target.plot(ax=ax, marker="o", linestyle="", color="red", label="Target") + ax.set_ylabel("Measured State") + else: + qp_data.I_control.sel(control_target="c").plot( + ax=ax, marker="o", linestyle="", color="blue", label="Control" + ) + qp_data.I_target.sel(control_target="t").plot( + ax=ax, marker="o", linestyle="", color="red", label="Target" + ) + ax.set_ylabel("Rotated I Quadrature (V)") + + if ds_fit is not None: + qp_fit = ds_fit.sel(qubit_pair=qp_name) + if bool(qp_fit.success.values): + if "fitted_control" in ds_fit.data_vars: + qp_fit.fitted_control.plot(ax=ax, color="blue", alpha=0.5) + if "fitted_target" in ds_fit.data_vars: + qp_fit.fitted_target.plot(ax=ax, color="red", alpha=0.5) + + ax.set_title(qp_name) + ax.set_xlabel(r"x90 frame rotation [$\mathrm{rad}/2\pi$]") + ax.legend(fontsize=8) diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py index fa5aea63a..6b10f3c40 100644 --- a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py +++ b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py @@ -1,120 +1,127 @@ """Plotting utilities for CZ phase compensation with error amplification.""" -import matplotlib.pyplot as plt import numpy as np import xarray as xr +from matplotlib.axes import Axes +from matplotlib.figure import Figure +from qualibration_libs.plotting import grid_iter -# Per-channel colors: muted markers for data, bold contrasting lines for the fit. -_PANEL_STYLES = { - "control": { - "data": {"color": "#3b82f6", "markeredgecolor": "#1e3a8a"}, - "fit": {"color": "#ea580c"}, - "optimum": {"color": "#15803d"}, - }, - "target": { - "data": {"color": "#ef4444", "markeredgecolor": "#991b1b"}, - "fit": {"color": "#7c3aed"}, - "optimum": {"color": "#15803d"}, - }, -} - - -def plot_raw_data_with_fit(ds_raw: xr.Dataset, qubit_pairs, ds_fit: xr.Dataset = None): - """Plot mean signal vs frame for control and target with the sinc fit overlay.""" - n_pairs = len(qubit_pairs) - fig, axes = plt.subplots(2, n_pairs, figsize=(6 * n_pairs, 8), squeeze=False) - - for i, qp in enumerate(qubit_pairs): - qp_name = qp.name - - if ds_fit is None or not bool(ds_fit.sel(qubit_pair=qp_name).success.values): - for row in range(2): - axes[row, i].text( - 0.5, - 0.5, - "fit failed" if ds_fit else "no fit", - ha="center", - va="center", - transform=axes[row, i].transAxes, - ) - axes[row, i].set_title(f"{qp_name} - {'control' if row == 0 else 'target'}") - axes[row, i].set_xlabel("Frame rotation [2π]") - axes[row, i].set_ylabel("Mean signal") +from calibration_utils.pair_grid import QubitPairGrid, grid_pair_names + + +def plot_raw_data_with_fit( + ds_raw: xr.Dataset, + qubit_pairs: list, + ds_fit: xr.Dataset = None, +) -> Figure: + """Plot averaged signal vs frame with sinc fits for every qubit pair on a chip-topology grid. + + Parameters + ---------- + ds_raw : xr.Dataset + Raw dataset containing ``state_control`` / ``state_target`` or + ``I_control`` / ``I_target`` variables with ``frame`` and + ``number_of_operations`` dimensions. + qubit_pairs : list + Qubit pair objects used for grid placement. + ds_fit : xr.Dataset, optional + Fit dataset containing ``control_mean_vs_frame``, ``target_mean_vs_frame``, + ``control_sinc_fit``, ``target_sinc_fit``, and ``success``. + If None, only axis labels and titles are shown. + + Returns + ------- + Figure + Matplotlib figure with one panel per qubit pair. + """ + grid_names, pair_names = grid_pair_names(qubit_pairs) + grid = QubitPairGrid(grid_names, pair_names) + + for ax, qubit in grid_iter(grid): + qp_name = qubit["qubit"] + plot_individual_data_with_fit(ax, ds_raw, qp_name, ds_fit) + + grid.fig.suptitle("CZ 1Q phase compensation - error amplification") + grid.fig.tight_layout() + return grid.fig + + +def plot_individual_data_with_fit( + ax: Axes, + ds_raw: xr.Dataset, + qp_name: str, + ds_fit: xr.Dataset = None, +): + """Plot averaged signal and sinc fits for control and target on one axis. + + Parameters + ---------- + ax : Axes + Axis to draw on. + ds_raw : xr.Dataset + Raw dataset with ``state_control`` / ``state_target`` or + ``I_control`` / ``I_target`` variables. + qp_name : str + Qubit pair name used to select data. + ds_fit : xr.Dataset, optional + Fit dataset containing averaged curves, sinc fits, and fitted phases. + If None or fit failed, only raw-data axis labels are applied. + """ + if "state_control" in ds_raw.data_vars: + ax.set_ylabel("Measured State") + else: + ax.set_ylabel("Rotated I Quadrature (V)") + + if ds_fit is None: + ax.set_title(qp_name) + ax.set_xlabel(r"x90 frame rotation [$\mathrm{rad}/2\pi$]") + return + + qp_fit = ds_fit.sel(qubit_pair=qp_name) + frames = ds_raw.sel(qubit_pair=qp_name).frame.values + fit_success = bool(qp_fit.success.values) if "success" in qp_fit.data_vars else False + + plotted = False + for label, color in (("control", "blue"), ("target", "red")): + mean_var = f"{label}_mean_vs_frame" + sinc_var = f"{label}_sinc_fit" + phase_var = f"fitted_{label}_phase" + + if mean_var not in qp_fit.data_vars: continue - qp_fit = ds_fit.sel(qubit_pair=qp_name) - frames = ds_raw.sel(qubit_pair=qp_name).frame.values - - for row, label in enumerate(["control", "target"]): - ax = axes[row, i] - style = _PANEL_STYLES[label] - mean_var = f"{label}_mean_vs_frame" - sinc_var = f"{label}_sinc_fit" - phase_var = f"fitted_{label}_phase" - peak_mean_var = f"{label}_mean_at_peak" - method_var = f"{label}_fit_method" - - if mean_var in qp_fit.data_vars: - mean_curve = qp_fit[mean_var].values - fitted_phase = float(qp_fit[phase_var].values) - peak_mean = float(qp_fit[peak_mean_var].values) - fit_method = str(qp_fit[method_var].values) - - ax.plot( - frames, - mean_curve, - linestyle="none", - marker="o", - ms=5, - mew=0.8, - alpha=0.9, - zorder=2, - label=r"$\langle signal \rangle_N$", - **style["data"], - ) - if sinc_var in qp_fit.data_vars: - fit_vals = qp_fit[sinc_var].values - if np.any(np.isfinite(fit_vals)): - ax.plot( - frames, - fit_vals, - "-", - lw=2.5, - zorder=4, - label="sinc fit", - **style["fit"], - ) - ax.axvline( - fitted_phase, - color=style["optimum"]["color"], - ls="--", - lw=1.8, - alpha=0.95, - zorder=3, - label=f"phase = {fitted_phase:.4f} ({fit_method})", - ) - ax.plot( - fitted_phase, - peak_mean, - "*", - ms=14, - mew=1.2, - zorder=5, - markerfacecolor="white", - markeredgecolor=style["fit"]["color"], - label=f"peak mean = {peak_mean:.4f}", - ) - ax.legend(loc="best", fontsize=9) - - ax.set_title(f"{qp_name} - {label}") - ax.set_xlabel("Frame rotation [2π]") - ax.set_ylabel("Mean signal") - ax.grid(alpha=0.25, color="#94a3b8") - - for j in range(n_pairs, axes.shape[1]): - axes[0, j].axis("off") - axes[1, j].axis("off") - - fig.suptitle("CZ phase compensation - error amplification (sinc fit)") - fig.tight_layout(rect=(0, 0, 1, 0.95)) - return fig + mean_curve = qp_fit[mean_var].values + ax.plot( + frames, + mean_curve, + linestyle="none", + marker="o", + color=color, + label=label.capitalize(), + ) + plotted = True + + if sinc_var in qp_fit.data_vars: + fit_vals = qp_fit[sinc_var].values + if np.any(np.isfinite(fit_vals)): + ax.plot(frames, fit_vals, color=color, alpha=0.5) + + if fit_success and phase_var in qp_fit.data_vars: + fitted_phase = float(qp_fit[phase_var].values) + if np.isfinite(fitted_phase): + ax.axvline(fitted_phase, color=color, ls="--", lw=1.5, alpha=0.7) + + if not plotted: + ax.text( + 0.5, + 0.5, + "fit failed", + ha="center", + va="center", + transform=ax.transAxes, + ) + + ax.set_title(qp_name) + ax.set_xlabel(r"x90 frame rotation [$\mathrm{rad}/2\pi$]") + if plotted: + ax.legend(fontsize=8) From f2dabed37163192f9fc7c7be9087b07642efb48d Mon Sep 17 00:00:00 2001 From: Deepak Khurana <119570568+Deepakkhurrana@users.noreply.github.com> Date: Wed, 1 Jul 2026 14:12:16 +0200 Subject: [PATCH 11/17] upgrading the graph --- .../calibrations/CZ_calibrations/99_CZ_calibration_graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/99_CZ_calibration_graph.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/99_CZ_calibration_graph.py index ae15d5f5b..6eabd400e 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/99_CZ_calibration_graph.py +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/99_CZ_calibration_graph.py @@ -24,7 +24,7 @@ class Parameters(GraphParameters): "conditional_phase_error_amp": library.nodes["32b_cz_conditional_phase_error_amp"].copy( name="conditional_phase_error_amp" ), - "phase_compensation": library.nodes["34_cz_phase_compensation"].copy(name="phase_compensation"), + "phase_compensation": library.nodes["34a_cz_phase_compensation"].copy(name="phase_compensation"), }, connectivity=[ ("chevron", "conditional_phase"), From d61e18f50b7058a20bf42a1c426681174b797bd5 Mon Sep 17 00:00:00 2001 From: Deepak Khurana <119570568+Deepakkhurrana@users.noreply.github.com> Date: Wed, 1 Jul 2026 14:21:18 +0200 Subject: [PATCH 12/17] upgrading the readme with the new nodes --- .../calibrations/CZ_calibrations/README.md | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/README.md b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/README.md index b3769ba5b..22e2eb9fb 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/README.md +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/README.md @@ -107,35 +107,36 @@ Coupler bias is **not** swept in the CZ calibration chain. After distortion cali ```text 17 → 18 → 31 → 32a → 32b - └→ 34 + └→ 34a → 34b (optional) ``` | Order | Node | Summary | | ----- | ------- | --------------------------------------------------------------------------------------------- | | 1 | **31** | Chevron: amplitude × duration → coarse CZ duration/amplitude | -| 2 | **32a** | Fine amplitude scan → π/2 conditional-phase point | +| 2 | **32a** | Fine amplitude scan → π conditional-phase point | | 3 | **32b** | CZ pulse train → error-amplified amplitude fine tune | -| 4 | **34** | Virtual-Z phase compensation (can run after **32a**; **99** runs it in parallel with **32b**) | +| 4 | **34a** | Virtual-Z phase compensation (can run after **32a**; **99** runs it in parallel with **32b**) | +| 4′ | **34b** | Error-amplified virtual-Z fine tune (optional follow-up to **34a**) | --- ## Tunable-coupler workflow -Node **30** finds coupler **decouple (idle)** and **interaction** flux biases plus moving-qubit detuning in one 2D map (CZ or iSWAP). That replaces the coarse amplitude/duration role of chevron (**31**), so the usual path is **30 → 32a → 32b → 33a → 34** without **31**. Pulse duration and macro amplitudes come from **30** and the gate macro already in QUAM. Use **33b** (PALEA) instead of **33a** for improved leakage isolation. +Node **30** finds coupler **decouple (idle)** and **interaction** flux biases plus moving-qubit detuning in one 2D map (CZ or iSWAP). That replaces the coarse amplitude/duration role of chevron (**31**), so the usual path is **30 → 32a → 32b → 33a → 34a** without **31**. Pulse duration and macro amplitudes come from **30** and the gate macro already in QUAM. Use **33b** (PALEA) instead of **33a** for improved leakage isolation. ```text -17 → 18 → 30 → 32a → 32b → 33a/33b - └→ 34 +17 → 18 → 30 → 32a → 32b → 33a/33b → 34a → 34b (optional) ``` | Order | Node | Summary | | ----- | ------- | ----------------------------------------------------------------------------------- | | 1 | **30** | 2D coupler + moving-qubit flux → `decouple_offset`, `detuning`, `macros[operation]` | -| 2 | **32a** | Fine amplitude → π/2 conditional-phase point | +| 2 | **32a** | Fine amplitude → π conditional-phase point | | 3 | **32b** | CZ pulse train → error-amplified amplitude fine tune | | 4 | **33a** | Coupler amplitude via \|11⟩ leakage amplification (standard) | | 4′ | **33b** | Coupler amplitude via PALEA leakage amplification (alternative to **33a**) | -| 5 | **34** | Virtual-Z phase compensation | +| 5 | **34a** | Virtual-Z phase compensation | +| 5′ | **34b** | Error-amplified virtual-Z fine tune (optional follow-up to **34a**) | **31** remains available if you still want an explicit amplitude–duration Chevron after **30** (e.g. new macro shape or duration not set in state). @@ -173,7 +174,7 @@ Prepare |11⟩, sweep CZ flux pulse amplitude and duration on the moving qubit. [(32a_cz_conditional_phase)](./32a_cz_conditional_phase.py) -Use gate duration from **31** (fixed coupler) or from the macro in state after **30** (tunable coupler). Sweep amplitude to the **π/2 conditional-phase** point. Tomography with rotating x90 on target. +Use gate duration from **31** (fixed coupler) or from the macro in state after **30** (tunable coupler). Sweep amplitude to the **π conditional-phase** point (phase difference = 0.5 in normalised units). Frame tomography uses rotating x90 on the stationary qubit.

Conditional phase plot @@ -217,7 +218,9 @@ Same coupler-amplitude objective as **33a**, with a dynamical-decoupling layer a ## Phase compensation — both workflows -[(34_cz_phase_compensation)](./34_cz_phase_compensation.py) +### Standard protocol + +[(34a_cz_phase_compensation)](./34a_cz_phase_compensation.py) |++⟩, apply CZ, reconstruct per-qubit phase; update virtual Z in state. @@ -227,22 +230,31 @@ Same coupler-amplitude objective as **33a**, with a dynamical-decoupling layer a **Goal:** Compensate single-qubit phases acquired during CZ. +### Error amplification + +[(34b_cz_phase_compensation_error_amp)](./34b_cz_phase_compensation_error_amp.py) + +Same phase-compensation objective as **34a**, with a train of CZ operations to amplify phase errors before fitting a sinc model on the N-averaged signal. Run after **34a** when finer virtual-Z compensation is needed. + +**Goal:** Fine-tune `phase_shift_control` / `phase_shift_target`; optional follow-up to **34a**. + --- # Project structure -| Node | File | Fixed coupler | Tunable coupler | -| ------- | ---------------------------------------------------------------------------------- | :-----------: | :---------------------------------------------------: | -| **30** | [`30_cz_iswap_flux_bootstrap.py`](./30_cz_iswap_flux_bootstrap.py) | — | ✓ | -| **31** | [`31_chevron_11_02.py`](./31_chevron_11_02.py) | ✓ | optional | -| **32a** | [`32a_cz_conditional_phase.py`](./32a_cz_conditional_phase.py) | ✓ | ✓ | -| **32b** | [`32b_cz_conditional_phase_error_amp.py`](./32b_cz_conditional_phase_error_amp.py) | ✓ | ✓ | -| **33a** | [`33a_cz_leakage_amplification.py`](./33a_cz_leakage_amplification.py) | — | ✓ | -| **33b** | [`33b_cz_leakage_amplification_palea.py`](./33b_cz_leakage_amplification_palea.py) | — | ✓ | -| **34** | [`34_cz_phase_compensation.py`](./34_cz_phase_compensation.py) | ✓ | ✓ | -| **99** | [`99_CZ_calibration_graph.py`](./99_CZ_calibration_graph.py) | ✓ (31–32b–34) | — (use **30** → 32a–33a/b–34 by hand or custom graph) | +| Node | File | Fixed coupler | Tunable coupler | +| ------- | ------------------------------------------------------------------------------------ | :------------: | :--------------------------------------------------------: | +| **30** | [`30_cz_iswap_flux_bootstrap.py`](./30_cz_iswap_flux_bootstrap.py) | — | ✓ | +| **31** | [`31_chevron_11_02.py`](./31_chevron_11_02.py) | ✓ | optional | +| **32a** | [`32a_cz_conditional_phase.py`](./32a_cz_conditional_phase.py) | ✓ | ✓ | +| **32b** | [`32b_cz_conditional_phase_error_amp.py`](./32b_cz_conditional_phase_error_amp.py) | ✓ | ✓ | +| **33a** | [`33a_cz_leakage_amplification.py`](./33a_cz_leakage_amplification.py) | — | ✓ | +| **33b** | [`33b_cz_leakage_amplification_palea.py`](./33b_cz_leakage_amplification_palea.py) | — | ✓ | +| **34a** | [`34a_cz_phase_compensation.py`](./34a_cz_phase_compensation.py) | ✓ | ✓ | +| **34b** | [`34b_cz_phase_compensation_error_amp.py`](./34b_cz_phase_compensation_error_amp.py) | ✓ | ✓ | +| **99** | [`99_CZ_calibration_graph.py`](./99_CZ_calibration_graph.py) | ✓ (31–32b–34a) | — (use **30** → 32a–32b-33a/b–34a by hand or custom graph) | -Utilities: `cz_iswap_flux_bootstrap`, `chevron_cz`, `cz_conditional_phase`, `cz_conditional_phase_error_amp`, `cz_leakage_amp`, `cz_phase_compensation` under `../calibration_utils/`. +Utilities: `cz_iswap_flux_bootstrap`, `chevron_cz`, `cz_conditional_phase`, `cz_conditional_phase_error_amp`, `cz_leakage_amp`, `cz_phase_compensation`, `cz_phase_compensation_error_amp` under `../calibration_utils/`. --- @@ -250,7 +262,7 @@ Utilities: `cz_iswap_flux_bootstrap`, `chevron_cz`, `cz_conditional_phase`, `cz_ [`99_CZ_calibration_graph.py`](./99_CZ_calibration_graph.py) — `CZ_Calibration_Fixed_Couplers`: -- **31** → **32a** → **32b** → **34** +- **31** → **32a** → **32b** → **34a** (add **34b** manually for finer virtual-Z tuning) Leakage nodes (**33a** / **33b**) are tunable-coupler only and are not included in this graph. From e63f36852d1265168333f1d8bef55f5ad1342bf6 Mon Sep 17 00:00:00 2001 From: Deepak Khurana <119570568+Deepakkhurrana@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:16:39 +0200 Subject: [PATCH 13/17] plotting corrections to make 34a and 34b consistent --- .../cz_phase_compensation/plotting.py | 69 +++++- .../plotting.py | 208 ++++++++++++------ 2 files changed, 206 insertions(+), 71 deletions(-) diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/plotting.py b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/plotting.py index ea9db4ae0..5b406acb3 100644 --- a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/plotting.py +++ b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/plotting.py @@ -1,3 +1,4 @@ +import numpy as np import xarray as xr from matplotlib.axes import Axes from matplotlib.figure import Figure @@ -6,6 +7,41 @@ from calibration_utils.pair_grid import QubitPairGrid, grid_pair_names +def _mark_fitted_peak( + ax: Axes, + frames: np.ndarray, + fit_curve: np.ndarray, + fitted_phase: float, + color: str, + channel_label: str, +) -> None: + """Mark the fitted-sine maximum.""" + if not np.any(np.isfinite(fit_curve)): + return + i_max = int(np.nanargmax(fit_curve)) + peak_frame = float(frames[i_max]) + peak_val = float(fit_curve[i_max]) + ax.axvline( + peak_frame, + color=color, + ls="--", + lw=1.5, + alpha=0.85, + zorder=3, + label=f"{channel_label} peak @ {peak_frame:.4f} (φ={fitted_phase:.4f})", + ) + ax.plot( + peak_frame, + peak_val, + "*", + ms=12, + mew=1.2, + zorder=5, + markerfacecolor="white", + markeredgecolor=color, + ) + + def plot_raw_data_with_fit( ds_raw: xr.Dataset, qubit_pairs: list, @@ -32,7 +68,6 @@ def plot_raw_data_with_fit( grid_names, pair_names = grid_pair_names(qubit_pairs) grid = QubitPairGrid(grid_names, pair_names) - qp_map = {qp.name: qp for qp in qubit_pairs} for ax, qubit in grid_iter(grid): qp_name = qubit["qubit"] plot_individual_data_with_fit(ax, ds_raw, qp_name, ds_fit) @@ -68,7 +103,7 @@ def plot_individual_data_with_fit( if "state_control" in ds_raw.data_vars: qp_data.state_control.plot(ax=ax, marker="o", linestyle="", color="blue", label="Control") qp_data.state_target.plot(ax=ax, marker="o", linestyle="", color="red", label="Target") - ax.set_ylabel("Measured State") + ylabel = "Measured State" else: qp_data.I_control.sel(control_target="c").plot( ax=ax, marker="o", linestyle="", color="blue", label="Control" @@ -76,16 +111,38 @@ def plot_individual_data_with_fit( qp_data.I_target.sel(control_target="t").plot( ax=ax, marker="o", linestyle="", color="red", label="Target" ) - ax.set_ylabel("Rotated I Quadrature (V)") + ylabel = "Rotated I Quadrature (V)" if ds_fit is not None: qp_fit = ds_fit.sel(qubit_pair=qp_name) + frames = qp_data.frame.values if bool(qp_fit.success.values): if "fitted_control" in ds_fit.data_vars: - qp_fit.fitted_control.plot(ax=ax, color="blue", alpha=0.5) + fit_c = np.asarray(qp_fit.fitted_control.values, dtype=float) + ax.plot(frames, fit_c, color="blue", alpha=0.5) + if "fitted_control_phase" in ds_fit.data_vars: + _mark_fitted_peak( + ax, + frames, + fit_c, + float(qp_fit.fitted_control_phase.values), + "blue", + "Control", + ) if "fitted_target" in ds_fit.data_vars: - qp_fit.fitted_target.plot(ax=ax, color="red", alpha=0.5) + fit_t = np.asarray(qp_fit.fitted_target.values, dtype=float) + ax.plot(frames, fit_t, color="red", alpha=0.5) + if "fitted_target_phase" in ds_fit.data_vars: + _mark_fitted_peak( + ax, + frames, + fit_t, + float(qp_fit.fitted_target_phase.values), + "red", + "Target", + ) ax.set_title(qp_name) - ax.set_xlabel(r"x90 frame rotation [$\mathrm{rad}/2\pi$]") + ax.set_xlabel(r"Virtual-Z frame [$2\pi$]") + ax.set_ylabel(ylabel) ax.legend(fontsize=8) diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py index 6b10f3c40..561edb69a 100644 --- a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py +++ b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py @@ -8,13 +8,48 @@ from calibration_utils.pair_grid import QubitPairGrid, grid_pair_names +FRAME_XLABEL = r"Virtual-Z frame [$2\pi$]" + +_PANEL_STYLES = { + "control": { + "data": {"color": "#3b82f6", "markeredgecolor": "#1e3a8a"}, + "fit": {"color": "#ea580c"}, + "optimum": {"color": "#15803d"}, + }, + "target": { + "data": {"color": "#ef4444", "markeredgecolor": "#991b1b"}, + "fit": {"color": "#7c3aed"}, + "optimum": {"color": "#15803d"}, + }, +} + + +def _signal_labels(ds_raw: xr.Dataset) -> tuple[str, str, str]: + """Return y-axis label, mean-curve legend, and peak legend for state vs IQ readout.""" + if "state_control" in ds_raw.data_vars: + return "Measured State", r"$\langle state \rangle_N$", r"peak $\langle state \rangle_N$" + return "Rotated I Quadrature (V)", r"$\langle I \rangle_N$", r"peak $\langle I \rangle_N$" + + +def _split_axis_vertically(ax: Axes, hspace: float = 0.35) -> tuple[Axes, Axes]: + """Replace ``ax`` with stacked top (control) and bottom (target) subplots.""" + sub_gs = ax.get_subplotspec().subgridspec(2, 1, hspace=hspace) + fig = ax.figure + ax.remove() + ax_control = fig.add_subplot(sub_gs[0]) + ax_target = fig.add_subplot(sub_gs[1], sharex=ax_control) + return ax_control, ax_target + def plot_raw_data_with_fit( ds_raw: xr.Dataset, qubit_pairs: list, ds_fit: xr.Dataset = None, ) -> Figure: - """Plot averaged signal vs frame with sinc fits for every qubit pair on a chip-topology grid. + """Plot N-averaged signal vs frame for control and target on a chip-topology grid. + + Each qubit-pair cell is split into two stacked panels (control on top, target below), + with sinc fits and fitted phase markers when available. Parameters ---------- @@ -27,101 +62,144 @@ def plot_raw_data_with_fit( ds_fit : xr.Dataset, optional Fit dataset containing ``control_mean_vs_frame``, ``target_mean_vs_frame``, ``control_sinc_fit``, ``target_sinc_fit``, and ``success``. - If None, only axis labels and titles are shown. Returns ------- Figure - Matplotlib figure with one panel per qubit pair. + Matplotlib figure with two panels per qubit pair on the chip grid. """ grid_names, pair_names = grid_pair_names(qubit_pairs) - grid = QubitPairGrid(grid_names, pair_names) + grid = QubitPairGrid(grid_names, pair_names, size=4) + width, height = grid.fig.get_size_inches() + grid.fig.set_size_inches(width, height * 2) for ax, qubit in grid_iter(grid): qp_name = qubit["qubit"] - plot_individual_data_with_fit(ax, ds_raw, qp_name, ds_fit) + ax_control, ax_target = _split_axis_vertically(ax) + plot_individual_channel_with_fit( + ax_control, + ds_raw, + qp_name, + ds_fit, + channel="control", + title=f"{qp_name} — control", + show_xlabel=False, + ) + plot_individual_channel_with_fit( + ax_target, + ds_raw, + qp_name, + ds_fit, + channel="target", + title=f"{qp_name} — target", + show_xlabel=True, + ) - grid.fig.suptitle("CZ 1Q phase compensation - error amplification") - grid.fig.tight_layout() + grid.fig.suptitle("CZ phase compensation - error amplification (sinc fit)", y=0.98) + grid.fig.tight_layout(rect=(0, 0, 1, 0.96)) return grid.fig -def plot_individual_data_with_fit( +def plot_individual_channel_with_fit( ax: Axes, ds_raw: xr.Dataset, qp_name: str, - ds_fit: xr.Dataset = None, + ds_fit: xr.Dataset | None, + channel: str, + title: str | None = None, + show_xlabel: bool = True, ): - """Plot averaged signal and sinc fits for control and target on one axis. - - Parameters - ---------- - ax : Axes - Axis to draw on. - ds_raw : xr.Dataset - Raw dataset with ``state_control`` / ``state_target`` or - ``I_control`` / ``I_target`` variables. - qp_name : str - Qubit pair name used to select data. - ds_fit : xr.Dataset, optional - Fit dataset containing averaged curves, sinc fits, and fitted phases. - If None or fit failed, only raw-data axis labels are applied. - """ - if "state_control" in ds_raw.data_vars: - ax.set_ylabel("Measured State") - else: - ax.set_ylabel("Rotated I Quadrature (V)") + """Plot one qubit's N-averaged signal, sinc fit, and fitted phase for a single pair.""" + style = _PANEL_STYLES[channel] + ylabel, mean_label, peak_label = _signal_labels(ds_raw) + panel_title = title if title is not None else f"{qp_name} - {channel}" if ds_fit is None: - ax.set_title(qp_name) - ax.set_xlabel(r"x90 frame rotation [$\mathrm{rad}/2\pi$]") + ax.set_title(panel_title) + if show_xlabel: + ax.set_xlabel(FRAME_XLABEL) + else: + ax.tick_params(labelbottom=False) + ax.set_ylabel(ylabel) + ax.text(0.5, 0.5, "no fit", ha="center", va="center", transform=ax.transAxes) return qp_fit = ds_fit.sel(qubit_pair=qp_name) - frames = ds_raw.sel(qubit_pair=qp_name).frame.values - fit_success = bool(qp_fit.success.values) if "success" in qp_fit.data_vars else False - - plotted = False - for label, color in (("control", "blue"), ("target", "red")): - mean_var = f"{label}_mean_vs_frame" - sinc_var = f"{label}_sinc_fit" - phase_var = f"fitted_{label}_phase" + if not bool(qp_fit.success.values): + ax.set_title(panel_title) + if show_xlabel: + ax.set_xlabel(FRAME_XLABEL) + else: + ax.tick_params(labelbottom=False) + ax.set_ylabel(ylabel) + ax.text(0.5, 0.5, "fit failed", ha="center", va="center", transform=ax.transAxes) + return - if mean_var not in qp_fit.data_vars: - continue + frames = ds_raw.sel(qubit_pair=qp_name).frame.values + mean_var = f"{channel}_mean_vs_frame" + sinc_var = f"{channel}_sinc_fit" + phase_var = f"fitted_{channel}_phase" + peak_mean_var = f"{channel}_mean_at_peak" + method_var = f"{channel}_fit_method" + if mean_var in qp_fit.data_vars: mean_curve = qp_fit[mean_var].values + fitted_phase = float(qp_fit[phase_var].values) + peak_mean = float(qp_fit[peak_mean_var].values) + fit_method = str(qp_fit[method_var].values) + ax.plot( frames, mean_curve, linestyle="none", marker="o", - color=color, - label=label.capitalize(), + ms=5, + mew=0.8, + alpha=0.9, + zorder=2, + label=mean_label, + **style["data"], ) - plotted = True - if sinc_var in qp_fit.data_vars: fit_vals = qp_fit[sinc_var].values if np.any(np.isfinite(fit_vals)): - ax.plot(frames, fit_vals, color=color, alpha=0.5) - - if fit_success and phase_var in qp_fit.data_vars: - fitted_phase = float(qp_fit[phase_var].values) - if np.isfinite(fitted_phase): - ax.axvline(fitted_phase, color=color, ls="--", lw=1.5, alpha=0.7) - - if not plotted: - ax.text( - 0.5, - 0.5, - "fit failed", - ha="center", - va="center", - transform=ax.transAxes, - ) - - ax.set_title(qp_name) - ax.set_xlabel(r"x90 frame rotation [$\mathrm{rad}/2\pi$]") - if plotted: - ax.legend(fontsize=8) + ax.plot( + frames, + fit_vals, + "-", + lw=2.5, + zorder=4, + label="sinc fit", + **style["fit"], + ) + if np.isfinite(fitted_phase): + ax.axvline( + fitted_phase, + color=style["optimum"]["color"], + ls="--", + lw=1.8, + alpha=0.95, + zorder=3, + label=f"phase = {fitted_phase:.4f} ({fit_method})", + ) + if np.isfinite(peak_mean) and np.isfinite(fitted_phase): + ax.plot( + fitted_phase, + peak_mean, + "*", + ms=14, + mew=1.2, + zorder=5, + markerfacecolor="white", + markeredgecolor=style["fit"]["color"], + label=f"{peak_label} = {peak_mean:.4f}", + ) + ax.legend(loc="best", fontsize=9) + + ax.set_title(panel_title) + if show_xlabel: + ax.set_xlabel(FRAME_XLABEL) + else: + ax.tick_params(labelbottom=False) + ax.set_ylabel(ylabel) + ax.grid(alpha=0.25, color="#94a3b8") From 006383402cd03f10df652982caadd0f5a823dd06 Mon Sep 17 00:00:00 2001 From: Deepak Khurana <119570568+Deepakkhurrana@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:17:32 +0200 Subject: [PATCH 14/17] suppressing stream warning and x axis label change --- .../CZ_calibrations/34a_cz_phase_compensation.py | 8 ++++---- .../34b_cz_phase_compensation_error_amp.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34a_cz_phase_compensation.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34a_cz_phase_compensation.py index 660fb7464..f08b0dce7 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34a_cz_phase_compensation.py +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34a_cz_phase_compensation.py @@ -82,20 +82,20 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # Register the sweep axes to be added to the dataset when fetching data node.namespace["sweep_axes"] = { "qubit_pair": xr.DataArray(qubit_pairs.get_names()), - "frame": xr.DataArray(frames, attrs={"long_name": "frame rotation", "units": "2π"}), + "frame": xr.DataArray(frames, attrs={"long_name": "virtual-Z frame", "units": "2π"}), } # The QUA program stored in the node namespace to be transfer to the simulation and execution run_actions with program() as node.namespace["qua_program"]: frame = declare(fixed) n = declare(int) - n_st = declare_stream() + n_st = declare_output_stream() I_c, I_c_st, Q_c, Q_c_st, n, n_st = node.machine.declare_qua_variables() I_t, I_t_st, Q_t, Q_t_st, _, _ = node.machine.declare_qua_variables() state_c = [declare(int) for _ in range(num_qubit_pairs)] state_t = [declare(int) for _ in range(num_qubit_pairs)] - state_c_st = [declare_stream() for _ in range(num_qubit_pairs)] - state_t_st = [declare_stream() for _ in range(num_qubit_pairs)] + state_c_st = [declare_output_stream() for _ in range(num_qubit_pairs)] + state_t_st = [declare_output_stream() for _ in range(num_qubit_pairs)] extra_phase_c = declare(fixed) extra_phase_t = declare(fixed) diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34b_cz_phase_compensation_error_amp.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34b_cz_phase_compensation_error_amp.py index 6bddf8b0a..fa6e7378a 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34b_cz_phase_compensation_error_amp.py +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34b_cz_phase_compensation_error_amp.py @@ -75,7 +75,7 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: dis np.arange(1, num_operations + 1), attrs={"long_name": "number of CZ operations"}, ), - "frame": xr.DataArray(frames, attrs={"long_name": "frame rotation", "units": "2π"}), + "frame": xr.DataArray(frames, attrs={"long_name": "virtual-Z frame", "units": "2π"}), } with program() as node.namespace["qua_program"]: @@ -83,13 +83,13 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: dis n = declare(int) n_op = declare(int) count = declare(int) - n_st = declare_stream() + n_st = declare_output_stream() I_c, I_c_st, Q_c, Q_c_st, n, n_st = node.machine.declare_qua_variables() I_t, I_t_st, Q_t, Q_t_st, _, _ = node.machine.declare_qua_variables() state_c = [declare(int) for _ in range(num_qubit_pairs)] state_t = [declare(int) for _ in range(num_qubit_pairs)] - state_c_st = [declare_stream() for _ in range(num_qubit_pairs)] - state_t_st = [declare_stream() for _ in range(num_qubit_pairs)] + state_c_st = [declare_output_stream() for _ in range(num_qubit_pairs)] + state_t_st = [declare_output_stream() for _ in range(num_qubit_pairs)] extra_phase_c = declare(fixed) extra_phase_t = declare(fixed) From 2430421ccda7eb75ceeff3daf0555fd8080e6694 Mon Sep 17 00:00:00 2001 From: Deepak Khurana <119570568+Deepakkhurrana@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:20:30 +0200 Subject: [PATCH 15/17] black fomratting --- .../calibration_utils/cz_phase_compensation/plotting.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/plotting.py b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/plotting.py index 5b406acb3..de1eec010 100644 --- a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/plotting.py +++ b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/plotting.py @@ -105,12 +105,8 @@ def plot_individual_data_with_fit( qp_data.state_target.plot(ax=ax, marker="o", linestyle="", color="red", label="Target") ylabel = "Measured State" else: - qp_data.I_control.sel(control_target="c").plot( - ax=ax, marker="o", linestyle="", color="blue", label="Control" - ) - qp_data.I_target.sel(control_target="t").plot( - ax=ax, marker="o", linestyle="", color="red", label="Target" - ) + qp_data.I_control.sel(control_target="c").plot(ax=ax, marker="o", linestyle="", color="blue", label="Control") + qp_data.I_target.sel(control_target="t").plot(ax=ax, marker="o", linestyle="", color="red", label="Target") ylabel = "Rotated I Quadrature (V)" if ds_fit is not None: From eef02d6e2b541dfbd60f96408b6a731c747fbc12 Mon Sep 17 00:00:00 2001 From: Deepak Khurana <119570568+Deepakkhurrana@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:55:34 +0200 Subject: [PATCH 16/17] reverting unintentional changes --- .../calibrations/1Q_calibrations/07_iq_blobs.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/qualibration_graphs/superconducting/calibrations/1Q_calibrations/07_iq_blobs.py b/qualibration_graphs/superconducting/calibrations/1Q_calibrations/07_iq_blobs.py index 3f1c95e23..ac3a00b97 100644 --- a/qualibration_graphs/superconducting/calibrations/1Q_calibrations/07_iq_blobs.py +++ b/qualibration_graphs/superconducting/calibrations/1Q_calibrations/07_iq_blobs.py @@ -263,7 +263,4 @@ def update_state(node: QualibrationNode[Parameters, Quam]): # %% {Save_results} @node.run_action() def save_results(node: QualibrationNode[Parameters, Quam]): - node.save() - - -# %% + node.save() \ No newline at end of file From 9c34e17e93deabfa25000a2794b33529562d20be Mon Sep 17 00:00:00 2001 From: Deepak Khurana <119570568+Deepakkhurrana@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:57:34 +0200 Subject: [PATCH 17/17] black formatting --- .../superconducting/calibrations/1Q_calibrations/07_iq_blobs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qualibration_graphs/superconducting/calibrations/1Q_calibrations/07_iq_blobs.py b/qualibration_graphs/superconducting/calibrations/1Q_calibrations/07_iq_blobs.py index ac3a00b97..6e40a4bd3 100644 --- a/qualibration_graphs/superconducting/calibrations/1Q_calibrations/07_iq_blobs.py +++ b/qualibration_graphs/superconducting/calibrations/1Q_calibrations/07_iq_blobs.py @@ -263,4 +263,4 @@ def update_state(node: QualibrationNode[Parameters, Quam]): # %% {Save_results} @node.run_action() def save_results(node: QualibrationNode[Parameters, Quam]): - node.save() \ No newline at end of file + node.save()