From 8519eee462b735933f3f8d3b1d92ee5e4580e646 Mon Sep 17 00:00:00 2001 From: Deepak Khurana <119570568+Deepakkhurrana@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:59:26 +0200 Subject: [PATCH 01/12] renaming the phase compensation: leakage comes first --- .../{33_cz_phase_compensation.py => 34_cz_phase_compensation.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename qualibration_graphs/superconducting/calibrations/CZ_calibrations/{33_cz_phase_compensation.py => 34_cz_phase_compensation.py} (100%) diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33_cz_phase_compensation.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34_cz_phase_compensation.py similarity index 100% rename from qualibration_graphs/superconducting/calibrations/CZ_calibrations/33_cz_phase_compensation.py rename to qualibration_graphs/superconducting/calibrations/CZ_calibrations/34_cz_phase_compensation.py From 96f90d5db9b4289035af6a540da51faf29de13e8 Mon Sep 17 00:00:00 2001 From: Deepak Khurana <119570568+Deepakkhurrana@users.noreply.github.com> Date: Mon, 15 Jun 2026 17:16:08 +0200 Subject: [PATCH 02/12] coupler flux tuning using 11 state calibration utils --- .../cz_leakage_amp/__init__.py | 17 ++ .../cz_leakage_amp/analysis.py | 191 ++++++++++++++++++ .../cz_leakage_amp/parameters.py | 37 ++++ .../cz_leakage_amp/plotting.py | 135 +++++++++++++ 4 files changed, 380 insertions(+) create mode 100644 qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/__init__.py create mode 100644 qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/analysis.py create mode 100644 qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/parameters.py create mode 100644 qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/plotting.py diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/__init__.py b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/__init__.py new file mode 100644 index 000000000..565d5b0d4 --- /dev/null +++ b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/__init__.py @@ -0,0 +1,17 @@ +"""CZ conditional phase 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 +from calibration_utils.cz_iswap_flux_bootstrap.parameters import QubitRoles, verify_moving_qubit # noqa: F401 + +__all__ = [ + "Parameters", + "QubitRoles", + "verify_moving_qubit", + "process_raw_dataset", + "fit_raw_data", + "log_fitted_results", + "FitResults", + "plot_raw_data_with_fit", +] diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/analysis.py b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/analysis.py new file mode 100644 index 000000000..563b13753 --- /dev/null +++ b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/analysis.py @@ -0,0 +1,191 @@ +"""Analysis module for CZ leakage amplification calibration.""" + +import logging +from dataclasses import dataclass +from typing import Dict, Tuple + +import numpy as np +import xarray as xr +from qualibrate import QualibrationNode +from scipy.ndimage import gaussian_filter1d + + +@dataclass +class FitResults: + """Stores the relevant CZ leakage amplification experiment fit parameters for a single qubit pair.""" + + optimal_amplitude: float + success: bool + + +def log_fitted_results(fit_results: Dict[str, FitResults], log_callable=None): + """ + Logs the node-specific fitted results for all qubit pairs. + + Parameters: + ----------- + fit_results : Dict[str, FitResults] + Dictionary containing FitResults for each qubit pair. + log_callable : callable, optional + Logger for logging the fitted results. If None, a default logger is used. + """ + if log_callable is None: + log_callable = logging.getLogger(__name__).info + + for qp_name, fit_result in fit_results.items(): + s_qubit = f"Results for qubit pair {qp_name}: " + s_amp = f"\tOptimal CZ coupler amplitude: {fit_result.optimal_amplitude:.6f} a.u." + + if fit_result.success: + s_qubit += "SUCCESS!\n" + else: + s_qubit += "FAIL!\n" + + log_message = s_qubit + s_amp + log_callable(log_message) + + +def process_raw_dataset(ds: xr.Dataset, node: QualibrationNode) -> xr.Dataset: + """ + Process the raw dataset by adding amplitude coordinates and pair-combined state variables. + + If the dataset has only state_moving_stationary1, state_moving_stationary2, ... they are concatenated + along qubit_pair to form state_moving_stationary(qubit_pair, amp, number_of_operations). + + Parameters: + ----------- + ds : xr.Dataset + Raw dataset from the experiment + node : QualibrationNode + The calibration node containing qubit pairs information + + Returns: + -------- + xr.Dataset + Processed dataset with additional coordinates and combined state variables. + """ + qubit_pairs = node.namespace["qubit_pairs"] + operation = node.parameters.operation + + def abs_amp(qp, amp): + return amp * qp.macros[operation].coupler_flux_pulse.amplitude + + ds = ds.assign_coords({"amp_full": (["qubit_pair", "amp"], np.array([abs_amp(qp, ds.amp) for qp in qubit_pairs]))}) + + for base_var in ( + "state_moving", + "state_stationary", + "state_moving_stationary", + ): + if base_var in ds.data_vars: + continue + + pair_vars = [ + var + for var in ds.data_vars + if var.startswith(base_var) and (var == base_var or var.replace(base_var, "").isdigit()) + ] + if not pair_vars: + continue + + pair_vars = sorted(pair_vars, key=lambda name: int(name.replace(base_var, "") or 0)) + state_list = [ds[var] for var in pair_vars] + pair_names = np.array(qubit_pairs.get_names())[: len(state_list)] + concatenated = xr.concat(state_list, dim="qubit_pair").assign_coords(qubit_pair=pair_names) + ds = ds.assign({base_var: concatenated}) + + return ds + + +def _optimal_amp_from_mean(X: np.ndarray, mean_vals: np.ndarray, smooth_sigma: float = 1.0) -> Tuple[float, int]: + """Compute optimal amplitude as argmax of mean (optionally smoothed), with parabolic refinement. + + Returns (optimal_amplitude_value, optimal_index). + """ + if smooth_sigma and smooth_sigma > 0: + mean_vals = gaussian_filter1d(mean_vals.astype(float), sigma=smooth_sigma, mode="nearest") + j0 = int(np.nanargmax(mean_vals)) + j_star = float(j0) + n = len(X) + if 0 < j0 < n - 1: + y1, y2, y3 = mean_vals[j0 - 1], mean_vals[j0], mean_vals[j0 + 1] + denom = y1 - 2 * y2 + y3 + if denom and np.isfinite(denom): + delta = 0.5 * (y1 - y3) / denom + j_star = j0 + np.clip(delta, -0.5, 0.5) + x_star = float(np.interp(j_star, np.arange(n), X)) + return x_star, j0 + + +def fit_routine(da: xr.Dataset) -> xr.Dataset: + """Compute mean P(11) over number_of_operations for each amplitude. + + Adds mean_state_moving_stationary(amp). No oscillation fit. + """ + data_var = "state_moving_stationary" if "state_moving_stationary" in da else None + if data_var is None: + return da + arr = da[data_var] + if "number_of_operations" not in arr.dims: + return da + mean_vals = arr.mean(dim="number_of_operations").rename("mean_state_moving_stationary") + return da.assign(mean_state_moving_stationary=mean_vals) + + +def fit_raw_data(ds: xr.Dataset, node: QualibrationNode) -> Tuple[xr.Dataset, Dict[str, FitResults]]: + """Compute mean over number_of_operations per amp and derive optimal amplitude (argmax mean) per qubit pair. + + Returns dataset with added mean state variables and coordinates + optimal_amplitude, optimal_index, success per qubit_pair. Also returns dict of FitResults. + """ + ds_fit = ds.groupby("qubit_pair").apply(fit_routine) + + opt_amps = [] + opt_idxs = [] + successes = [] + qp_names = ds_fit.qubit_pair.values + + for qp in qp_names: + sub = ds_fit.sel(qubit_pair=qp) + mean_var = "mean_state_moving_stationary" if "mean_state_moving_stationary" in sub else None + if mean_var is None: + # Fallback: compute mean from combined P(11) state data. + if "state_moving_stationary" in sub: + mean_vals = sub.state_moving_stationary.mean(dim="number_of_operations") + sub = sub.assign(mean_state_moving_stationary=mean_vals) + mean_var = "mean_state_moving_stationary" + else: + opt_amps.append(np.nan) + opt_idxs.append(0) + successes.append(False) + continue + try: + amp_coord = sub.amp_full if "amp_full" in sub.coords else sub.amp + X = np.asarray(amp_coord.values) + mean_arr = sub[mean_var].values + x_star, idx = _optimal_amp_from_mean(X, mean_arr, smooth_sigma=1.0) + amp_min, amp_max = float(np.min(X)), float(np.max(X)) + if not np.isfinite(x_star) or not (amp_min <= x_star <= amp_max): + opt_amps.append(np.nan) + opt_idxs.append(idx) + successes.append(False) + else: + opt_amps.append(x_star) + opt_idxs.append(idx) + successes.append(True) + except Exception: + opt_amps.append(np.nan) + opt_idxs.append(0) + successes.append(False) + + ds_fit = ds_fit.assign_coords({"optimal_amplitude": ("qubit_pair", np.array(opt_amps))}) + ds_fit["optimal_amplitude"] = ds_fit["optimal_amplitude"].astype(float) + ds_fit = ds_fit.assign_coords({"optimal_index": ("qubit_pair", np.array(opt_idxs))}) + ds_fit["optimal_index"] = ds_fit["optimal_index"].astype(int) + ds_fit = ds_fit.assign_coords({"success": ("qubit_pair", np.array(successes, dtype=bool))}) + + fit_results: Dict[str, FitResults] = {} + for qp, amp, succ in zip(qp_names, opt_amps, successes): + fit_results[str(qp)] = FitResults(optimal_amplitude=float(amp), success=bool(succ)) + + return ds_fit, fit_results diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/parameters.py b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/parameters.py new file mode 100644 index 000000000..6649d24e2 --- /dev/null +++ b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/parameters.py @@ -0,0 +1,37 @@ +"""Parameters module for CZ leakage 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 leakage amplification.""" + + num_shots: int = 100 + """Number of shots to perform. Default is 100.""" + amp_range: float = 0.010 + """Range of amplitude variation around the nominal value (scans center +/- range). Default is 0.010.""" + amp_step: float = 0.001 + """Step size for amplitude scanning. Default is 0.001.""" + operation: Literal["cz_flattop", "cz_unipolar", "cz_bipolar", "cz_flattop_erf", "cz_SNZ"] = "cz_unipolar" + """Type of CZ operation to perform; one of 'cz_flattop', 'cz_unipolar', 'cz_bipolar', 'cz_flattop_erf', or 'cz_SNZ'. Default is 'cz_unipolar'.""" + number_of_operations: int = 10 + """Number of operations to perform for each amplitude. Default is 10.""" + use_state_discrimination: bool = True + """Whether to use state discrimination for readout. Default is True because the CZ leakage amplification is only possible with state discrimination.""" + + +class Parameters( + NodeParameters, + CommonNodeParameters, + NodeSpecificParameters, + QubitPairExperimentNodeParameters, +): + """Combined parameters for CZ leakage amplification calibration.""" + + targets_name: ClassVar[str] = "qubit_pairs" diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/plotting.py b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/plotting.py new file mode 100644 index 000000000..059fa795e --- /dev/null +++ b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/plotting.py @@ -0,0 +1,135 @@ +"""Plotting module for CZ leakage amplification calibration.""" + +from typing import Dict + +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 + +from calibration_utils.pair_grid import QubitPairGrid, grid_pair_names + + +def plot_raw_data_with_fit( + ds_fit: xr.Dataset, + qubit_pairs: list, + node=None, +) -> Dict[str, Figure]: + """Plot CZ leakage amplification data on chip-topology grids. + + Parameters + ---------- + ds_fit : xr.Dataset + Fit dataset containing state-discrimination leakage data and ``optimal_amplitude``. + qubit_pairs : list + Qubit pair objects used for grid placement. + node : optional + Kept for backwards-compatible call sites. + + Returns + ------- + dict[str, Figure] + ``"raw"`` contains P(11) heatmaps and ``"mean"`` contains mean P(11) + traces; both include optimal-amplitude markers. + """ + grid_names, pair_names = grid_pair_names(qubit_pairs) + figures = {} + + raw_grid = QubitPairGrid(grid_names, pair_names) + for ax, qubit in grid_iter(raw_grid): + qp_name = qubit["qubit"] + plot_individual_raw_data_with_fit(ax, ds_fit, qp_name) + raw_grid.fig.suptitle("CZ leakage amplification - raw P(11)") + raw_grid.fig.tight_layout() + figures["raw"] = raw_grid.fig + + mean_grid = QubitPairGrid(grid_names, pair_names) + for ax, qubit in grid_iter(mean_grid): + qp_name = qubit["qubit"] + plot_individual_mean_data_with_fit(ax, ds_fit, qp_name) + mean_grid.fig.suptitle("CZ leakage amplification - mean P(11)") + mean_grid.fig.tight_layout() + figures["mean"] = mean_grid.fig + + return figures + + +def plot_individual_raw_data_with_fit( + ax: Axes, + ds_fit: xr.Dataset, + qp_name: str, +): + """Plot one qubit-pair leakage-amplification heatmap.""" + fr = ds_fit.sel(qubit_pair=qp_name) + + if "state_moving_stationary" not in fr: + ax.text(0.5, 0.5, "No leakage data", ha="center", va="center", transform=ax.transAxes) + ax.set_title(qp_name) + return + + data = fr.state_moving_stationary # dims: amp, number_of_operations (or number_of_operations, amp) + coupler_flux_mV = 1e3 * (fr.amp_full if "amp_full" in fr.coords else fr.amp).values + n_ops = ( + fr.number_of_operations.values + if "number_of_operations" in data.dims + else np.arange(data.sizes.get("number_of_operations", data.shape[0])) + ) + if "amp" in data.dims and "number_of_operations" in data.dims: + z_data = data.transpose("number_of_operations", "amp").values + else: + z_data = data.values + + x_data, y_data = np.meshgrid(coupler_flux_mV, n_ops) + pcm = ax.pcolormesh( + x_data, + y_data, + z_data, + cmap="viridis", + shading="auto", + ) + + success = "success" in fr.coords and bool(fr.success) and np.isfinite(float(fr.optimal_amplitude)) + if success: + opt_flux_mV = 1e3 * float(fr.optimal_amplitude) + ax.axvline(opt_flux_mV, color="red", lw=2, label="optimal") + + ax.figure.colorbar(pcm, ax=ax, shrink=0.85).set_label("P(11)") + ax.set_title(qp_name if success else f"{qp_name} - fit failed") + ax.set_xlabel("Coupler flux (mV)") + ax.set_ylabel("# CZ operations") + if success: + ax.legend(loc="upper right", fontsize=8) + + +def plot_individual_mean_data_with_fit( + ax: Axes, + ds_fit: xr.Dataset, + qp_name: str, +): + """Plot one qubit-pair mean P(11) trace.""" + fr = ds_fit.sel(qubit_pair=qp_name) + + if "mean_state_moving_stationary" not in fr: + ax.text(0.5, 0.5, "No mean P(11) data", ha="center", va="center", transform=ax.transAxes) + ax.set_title(qp_name) + return + + mean_arr = fr.mean_state_moving_stationary + coupler_flux_mV = 1e3 * (fr.amp_full if "amp_full" in fr.coords else fr.amp).values + success = "success" in fr.coords and bool(fr.success) and np.isfinite(float(fr.optimal_amplitude)) + + ax.plot(coupler_flux_mV, mean_arr.values, color="steelblue", lw=1.5, label="mean P(11)") + if success: + opt_flux_mV = 1e3 * float(fr.optimal_amplitude) + opt_idx = int(np.nanargmin(np.abs(coupler_flux_mV - opt_flux_mV))) + ax.axvline(opt_flux_mV, color="red", lw=2, label="optimal") + ax.scatter([opt_flux_mV], [float(mean_arr.values[opt_idx])], color="red", zorder=5) + ax.set_title(qp_name if success else f"{qp_name} - fit failed") + ax.set_xlabel("Coupler flux (mV)") + ax.set_ylabel("Mean P(11)") + ax.set_ylim( + max(0.0, float(np.nanmin(mean_arr.values)) - 0.05), + min(1.0, float(np.nanmax(mean_arr.values)) + 0.05), + ) + ax.legend(loc="best", fontsize=8) From 6894098592c580492586423ef19c293178bd79a2 Mon Sep 17 00:00:00 2001 From: Deepak Khurana <119570568+Deepakkhurrana@users.noreply.github.com> Date: Mon, 15 Jun 2026 17:16:37 +0200 Subject: [PATCH 03/12] coupler flux tuning using 11 state calibration node --- .../33a_cz_leakage_amplification.py | 299 ++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_leakage_amplification.py diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_leakage_amplification.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_leakage_amplification.py new file mode 100644 index 000000000..121b4d7d3 --- /dev/null +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_leakage_amplification.py @@ -0,0 +1,299 @@ +"""CZ leakage amplification calibration node.""" + +# %% {Imports} +from dataclasses import asdict + +import matplotlib.pyplot as plt +import numpy as np +import xarray as xr +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 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 + +from calibration_utils.cz_leakage_amp import ( + Parameters, + fit_raw_data, + log_fitted_results, + QubitRoles, + verify_moving_qubit, + plot_raw_data_with_fit, + process_raw_dataset, +) + +# %% {Initialisation} +description = """ +CALIBRATION OF THE CZ GATE COUPLER AMPLITUDE VIA LEAKAGE with error amplification + +This sequence calibrates the CZ gate by scanning the coupler pulse amplitude and measuring +the probability that both moving and stationary qubits remain in |1> (P(11)) after repeated CZ gates. +Both qubits start in |1>; the CZ is applied a variable number of times for error amplification. + +For each amplitude we measure: +- P(11) = fraction of shots where both moving and stationary qubits are in |1> (state_moving_stationary) +- Same for state_moving and state_stationary (optional) + +**Error amplification:** +The CZ gate is applied repeatedly (number_of_operations) for each measurement. This amplifies +leakage and coherent errors, making the optimal amplitude easier to identify. + +The optimal coupler amplitude is the amplitude at which the mean of P(11) over number_of_operations +is highest (best preservation of |11>). + +Prerequisites: +- Tunable-coupler architecture: ``macros[operation]`` must define ``coupler_flux_pulse`` (fixed-coupler pairs are not supported). +- Calibrated single-qubit gates for both qubits in the pair +- Calibrated readout (with state discrimination for |g>,|e>,|f> if used) +- Initial estimate of the CZ coupler amplitude (typically from node 30) + +State update: +- The optimal CZ coupler amplitude: qubit_pair.macros[operation].coupler_flux_pulse.amplitude +""" + +# Be sure to include [Parameters, Quam] so the node has proper type hinting +node = QualibrationNode[Parameters, Quam]( + name="33a_cz_leakage_amplification", # Name should be unique + description=description, + parameters=Parameters(), # Node parameters: calibration_utils/cz_leakage_amp/parameters.py +) + +# instrument_calibration_node(node) + + +# 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]): + """Set custom parameters for debugging purposes only.""" + # node.parameters.qubit_pairs = ["q1-q2"] + pass + + +# Instantiate the QUAM class from the state file +node.machine = Quam.load() + + +# %% {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 the sweep axes and generate the QUA program from the pulse sequence and the node parameters.""" + if not node.parameters.use_state_discrimination: + raise ValueError("33a_cz_leakage_amplification requires use_state_discrimination=True for P(11) analysis.") + + # 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) + + # Verify qp.moving_qubit against recalculation and precompute roles for QUA program loops. + # Logs a warning and corrects qp.moving_qubit in-memory if they disagree; state is persisted + # at the end of the node. + qubit_roles_map = {} + for qp in qubit_pairs: + verify_moving_qubit(qp, operation=node.parameters.operation, repair_routing=True, log_callable=node.log) + qubit_roles_map[qp.name] = QubitRoles.resolve(qp) + node.namespace["qubit_roles_map"] = qubit_roles_map + + # Extract the sweep parameters and axes from the node parameters + n_avg = node.parameters.num_shots + amplitudes = np.arange(1 - node.parameters.amp_range, 1 + node.parameters.amp_range, node.parameters.amp_step) + + operation = node.parameters.operation + num_operations = node.parameters.number_of_operations + # 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()), + "number_of_operations": xr.DataArray( + np.arange(1, num_operations + 1), + attrs={"long_name": "number of operations"}, + ), + "amp": xr.DataArray(amplitudes, attrs={"long_name": "amplitude scale", "units": "a.u."}), + } + + # 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"]: + amp = declare(fixed) # amplitude scaling factor for the CZ gate coupler pulse + n = declare(int) + n_op = declare(int) # number of CZ operations + count = declare(int) # loop counter + n_st = declare_output_stream() + state_mq = [declare(int) for _ in range(num_qubit_pairs)] + state_sq = [declare(int) for _ in range(num_qubit_pairs)] + state_mq_st = [declare_output_stream() for _ in range(num_qubit_pairs)] + state_sq_st = [declare_output_stream() for _ in range(num_qubit_pairs)] + state_ms_st = [declare_output_stream() for _ in range(num_qubit_pairs)] + + for multiplexed_qubit_pairs in qubit_pairs.batch(): + # Initialize the qubits + for qp in multiplexed_qubit_pairs.values(): + qubit_role = qubit_roles_map[qp.name] + mq, sq = qubit_role.moving, qubit_role.stationary + node.machine.initialize_qpu(target=mq) + node.machine.initialize_qpu(target=sq) + # Loop for averaging + with for_(n, 0, n < n_avg, n + 1): + save(n, n_st) + # Loop over the number of CZ operations for error amplification + with for_(n_op, 1, n_op <= num_operations, n_op + 1): + # Loop over amplitude scale + with for_(*from_array(amp, amplitudes)): + for ii, qp in multiplexed_qubit_pairs.items(): + qubit_role = qubit_roles_map[qp.name] + mq, sq = qubit_role.moving, qubit_role.stationary + # Reset the qubits + mq.reset(node.parameters.reset_type, node.parameters.simulate) + sq.reset(node.parameters.reset_type, node.parameters.simulate) + qp.align() + # Reset the frames of both qubits + reset_frame(sq.xy.name) + reset_frame(mq.xy.name) + # setting both qubits to the initial state + mq.xy.play("x180") + sq.xy.play("x180") + qp.align() + # Loop over the number of CZ operations + with for_(count, 0, count < n_op, count + 1): + # play the CZ gate + qp.macros[operation].apply(amplitude_scale_coupler=amp) + qp.align() + + # measure both qubits + mq.readout_state_gef(state_mq[ii]) + sq.readout_state_gef(state_sq[ii]) + + with if_((state_mq[ii] == 1) & (state_sq[ii] == 1)): + wait(4) + save(1, state_ms_st[ii]) + with else_(): + wait(4) + save(0, state_ms_st[ii]) + + save(state_mq[ii], state_mq_st[ii]) + save(state_sq[ii], state_sq_st[ii]) + + with stream_processing(): + n_st.save("n") + for i in range(num_qubit_pairs): + state_mq_st[i].buffer(len(amplitudes)).buffer(num_operations).average().save(f"state_moving{i + 1}") + state_sq_st[i].buffer(len(amplitudes)).buffer(num_operations).average().save(f"state_stationary{i + 1}") + state_ms_st[i].buffer(len(amplitudes)).buffer(num_operations).average().save( + f"state_moving_stationary{i + 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.to_dict()} + + +# %% {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.""" + # 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.get("n", 0), + 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 and role data needed for reproducible re-analysis + node.results["ds_raw"] = dataset + qubit_roles_map = node.namespace["qubit_roles_map"] + node.results["qubit_roles"] = { + name: {field: getattr(role, field).name for field in role._fields} for name, role in qubit_roles_map.items() + } + + +# %% {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 qubit pairs from the loaded node parameters + node.namespace["qubit_pairs"] = get_qubit_pairs(node) + node.namespace["qubit_roles_map"] = { + name: QubitRoles(**{field: node.machine.qubits[qname] for field, qname in roles.items()}) + for name, roles in node.results["qubit_roles"].items() + } + + +# %% {Analyse_data} +@node.run_action(skip_if=node.parameters.simulate) +def analyse_data(node: QualibrationNode[Parameters, Quam]): + """Analyse raw data, fit, log results, set outcomes and store structured fit results.""" + 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 = { + qubit_pair_name: ("successful" if fit_result.success else "failed") + for qubit_pair_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 the raw and fitted data in a specific figure whose shape is given by qubit pair grid locations.""" + qubit_pairs = node.namespace["qubit_pairs"] + + figures = plot_raw_data_with_fit(node.results["ds_fit"], qubit_pairs) + for fig in figures.values(): + plt.show() + node.results["figures"] = { + "leakage_raw": figures["raw"], + "leakage_mean": figures["mean"], + } + + +# %% {Update_state} +@node.run_action(skip_if=node.parameters.simulate) +def update_state(node: QualibrationNode[Parameters, Quam]): + """Update the relevant parameters if the qubit pair data analysis was successful.""" + + operation = node.parameters.operation + with node.record_state_updates(): + fit_results = node.results["fit_results"] + for qp in node.namespace["qubit_pairs"]: + if node.outcomes[qp.name] == "failed": + node.log(f"Skipping state update for {qp.name}: fit flagged unsuccessful.") + continue + qp.macros[operation].coupler_flux_pulse.amplitude = fit_results[qp.name]["optimal_amplitude"] + + +# %% {Save_results} +@node.run_action() +def save_results(node: QualibrationNode[Parameters, Quam]): + """Save the calibration results.""" + node.save() + + +# %% From 42fa1dd24ee7a914e7cf3be977c339dc798d4371 Mon Sep 17 00:00:00 2001 From: Deepak Khurana <119570568+Deepakkhurrana@users.noreply.github.com> Date: Mon, 15 Jun 2026 17:16:49 +0200 Subject: [PATCH 04/12] coupler flux tuning using 11 state calibration node using PALEA method --- .../33b_cz_leakage_amplification_palea.py | 320 ++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py new file mode 100644 index 000000000..13cd8a004 --- /dev/null +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py @@ -0,0 +1,320 @@ +"""CZ leakage amplification calibration node.""" + +# %% {Imports} +from dataclasses import asdict + +import matplotlib.pyplot as plt +import numpy as np +import xarray as xr +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 qualibrate import QualibrationNode +from qualibration_libs.core import tracked_updates +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 + +from calibration_utils.cz_leakage_amp import ( + Parameters, + fit_raw_data, + log_fitted_results, + QubitRoles, + verify_moving_qubit, + plot_raw_data_with_fit, + process_raw_dataset, +) + +# %% {Initialisation} +description = """ +CALIBRATION OF THE CZ GATE COUPLER AMPLITUDE USING THE PALEA PROTOCOL + +This node calibrates the CZ gate coupler pulse amplitude using the Phase-Averaged Leakage Error +Amplification (PALEA) protocol, as described in Marxer et al., arXiv:2508.16437 +(https://arxiv.org/abs/2508.16437). + +PALEA is designed to coherently amplify population leakage from |11> to |02> caused by imperfect +diabatic transitions during the CZ gate, while remaining robust against other error sources such +as ZZ over-rotation and single-qubit phase errors. Compared to standard leakage amplification, +PALEA achieves at least a factor-of-two reduction in leakage for the same number of gate repetitions. + +**Protocol:** +Both qubits are prepared in |1> (X_pi on each). The CZ gate is then applied repeatedly, with a +dynamical decoupling (DD) layer inserted after each gate. The DD layer consists of: +- An X_pi^{12} (EF) pulse on the leakage qubit (higher-frequency qubit; |11⟩↔|20⟩ channel). +- An X_pi^{01} pulse on the lower-frequency qubit. + +This DD layer coherently amplifies leakage errors with each repetition. The phase averaging inherent in the protocol +eliminates sensitivity to the ZZ rotation angle, isolating the leakage parameter. + +The coupler pulse amplitude is swept while varying the number of CZ+DD cycles. The optimal +amplitude is identified as the one that maximizes the mean population in the wanted state +(best preservation of |11>) averaged over the number of operations. + +Prerequisites: +- Tunable-coupler architecture: ``macros[operation]`` must define ``coupler_flux_pulse`` (fixed-coupler pairs are not supported). +- Calibrated single-qubit gates (X_pi^{01} and X_pi^{12}) for both qubits in the pair +- Calibrated readout with state discrimination for |g>, |e>, |f> +- Initial estimate of the CZ coupler amplitude (typically from node 30 or 33a) + +State update: +- The optimal CZ coupler amplitude: qubit_pair.macros[operation].coupler_flux_pulse.amplitude +""" + +# Be sure to include [Parameters, Quam] so the node has proper type hinting +node = QualibrationNode[Parameters, Quam]( + name="33b_cz_leakage_amplification_palea", # Name should be unique + description=description, + parameters=Parameters(), # Node parameters: calibration_utils/cz_leakage_amp/parameters.py +) + + +# 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]): + """Set custom parameters for debugging purposes only.""" + pass + + +# Instantiate the QUAM class from the state file +node.machine = Quam.load() + + +# %% {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 the sweep axes and generate the QUA program from the pulse sequence and the node parameters.""" + if not node.parameters.use_state_discrimination: + raise ValueError( + "33b_cz_leakage_amplification_palea requires use_state_discrimination=True for P(11) analysis." + ) + + # 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) + + # Verify qp.moving_qubit against recalculation and precompute roles for QUA program loops. + # Logs a warning and corrects qp.moving_qubit in-memory if they disagree; state is persisted + # at the end of the node. + qubit_roles_map = {} + for qp in qubit_pairs: + verify_moving_qubit(qp, operation=node.parameters.operation, repair_routing=True, log_callable=node.log) + qubit_roles_map[qp.name] = QubitRoles.resolve(qp) + node.namespace["qubit_roles_map"] = qubit_roles_map + + # Extract the sweep parameters and axes from the node parameters + n_avg = node.parameters.num_shots + amplitudes = np.arange(1 - node.parameters.amp_range, 1 + node.parameters.amp_range, node.parameters.amp_step) + + operation = node.parameters.operation + num_operations = node.parameters.number_of_operations + # 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()), + "number_of_operations": xr.DataArray( + np.arange(2, num_operations + 1, 2), + attrs={"long_name": "number of operations"}, + ), + "amp": xr.DataArray(amplitudes, attrs={"long_name": "amplitude scale", "units": "a.u."}), + } + + # 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"]: + amp = declare(fixed) # amplitude scaling factor for the CZ gate coupler pulse + n = declare(int) + n_op = declare(int) # number of CZ operations + count = declare(int) # loop counter + n_st = declare_output_stream() + state_mq = [declare(int) for _ in range(num_qubit_pairs)] + state_sq = [declare(int) for _ in range(num_qubit_pairs)] + state_mq_st = [declare_output_stream() for _ in range(num_qubit_pairs)] + state_sq_st = [declare_output_stream() for _ in range(num_qubit_pairs)] + state_ms_st = [declare_output_stream() for _ in range(num_qubit_pairs)] + + for multiplexed_qubit_pairs in qubit_pairs.batch(): + # Initialize the qubits + for qp in multiplexed_qubit_pairs.values(): + qubit_role = qubit_roles_map[qp.name] + mq, sq = qubit_role.moving, qubit_role.stationary + node.machine.initialize_qpu(target=mq) + node.machine.initialize_qpu(target=sq) + # Loop for averaging + with for_(n, 0, n < n_avg, n + 1): + save(n, n_st) + # Loop over the number of CZ operations for error amplification + with for_(n_op, 2, n_op <= num_operations, n_op + 2): + # Loop over amplitude scale + with for_(*from_array(amp, amplitudes)): + for ii, qp in multiplexed_qubit_pairs.items(): + qubit_role = qubit_roles_map[qp.name] + mq, sq = qubit_role.moving, qubit_role.stationary + lq, oq = qubit_role.leakage, qubit_role.low + # Reset the qubits + mq.reset(node.parameters.reset_type, node.parameters.simulate) + sq.reset(node.parameters.reset_type, node.parameters.simulate) + qp.align() + # Reset the frames of both qubits + reset_frame(sq.xy.name) + reset_frame(mq.xy.name) + # setting both qubits to the initial state + mq.xy.play("x180") + sq.xy.play("x180") + qp.align() + # Loop over the number of CZ operations + with for_(count, 0, count < n_op, count + 1): + # play the CZ gate + qp.macros[operation].apply(amplitude_scale_coupler=amp) + # play the PALEA Dynamical decoupling sequence: + # EF (e-f) pi on the leakage qubit (|11⟩↔|20⟩), g-e pi on the low qubit. + lq.xy.update_frequency( + lq.xy.intermediate_frequency - lq.anharmonicity, + keep_phase=True, + ) + lq.xy.play("EF_x180") + lq.xy.update_frequency(lq.xy.intermediate_frequency, keep_phase=True) + oq.xy.play("x180") + qp.align() + + # measure both qubits + mq.readout_state_gef(state_mq[ii]) + sq.readout_state_gef(state_sq[ii]) + + with if_((state_mq[ii] == 1) & (state_sq[ii] == 1)): + wait(4) + save(1, state_ms_st[ii]) + with else_(): + wait(4) + save(0, state_ms_st[ii]) + + save(state_mq[ii], state_mq_st[ii]) + save(state_sq[ii], state_sq_st[ii]) + align() + with stream_processing(): + n_st.save("n") + for i in range(num_qubit_pairs): + state_mq_st[i].buffer(len(amplitudes)).buffer(len(np.arange(2, num_operations + 1, 2))).average().save( + f"state_moving{i + 1}" + ) + state_sq_st[i].buffer(len(amplitudes)).buffer(len(np.arange(2, num_operations + 1, 2))).average().save( + f"state_stationary{i + 1}" + ) + state_ms_st[i].buffer(len(amplitudes)).buffer(len(np.arange(2, num_operations + 1, 2))).average().save( + f"state_moving_stationary{i + 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.to_dict()} + + +# %% {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.""" + # 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.get("n", 0), + 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 and role data needed for reproducible re-analysis + node.results["ds_raw"] = dataset + qubit_roles_map = node.namespace["qubit_roles_map"] + node.results["qubit_roles"] = { + name: {field: getattr(role, field).name for field in role._fields} for name, role in qubit_roles_map.items() + } + + +# %% {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 qubit pairs from the loaded node parameters + node.namespace["qubit_pairs"] = get_qubit_pairs(node) + node.namespace["qubit_roles_map"] = { + name: QubitRoles(**{field: node.machine.qubits[qname] for field, qname in roles.items()}) + for name, roles in node.results["qubit_roles"].items() + } + + +# %% {Analyse_data} +@node.run_action(skip_if=node.parameters.simulate) +def analyse_data(node: QualibrationNode[Parameters, Quam]): + """Analyse raw data, fit, log results, set outcomes and store structured fit results.""" + 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 = { + qubit_pair_name: ("successful" if fit_result.success else "failed") + for qubit_pair_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 the raw and fitted data in a specific figure whose shape is given by qubit pair grid locations.""" + qubit_pairs = node.namespace["qubit_pairs"] + + figures = plot_raw_data_with_fit(node.results["ds_fit"], qubit_pairs) + for fig in figures.values(): + plt.show() + node.results["leakage_raw_figure"] = figures["raw"] + node.results["leakage_mean_figure"] = figures["mean"] + + +# %% {Update_state} +@node.run_action(skip_if=node.parameters.simulate) +def update_state(node: QualibrationNode[Parameters, Quam]): + """Update the relevant parameters if the qubit pair data analysis was successful.""" + + operation = node.parameters.operation + with node.record_state_updates(): + fit_results = node.results["fit_results"] + for qp in node.namespace["qubit_pairs"]: + if node.outcomes[qp.name] == "failed": + continue + qp.macros[operation].coupler_flux_pulse.amplitude = fit_results[qp.name]["optimal_amplitude"] + + +# %% {Save_results} +@node.run_action() +def save_results(node: QualibrationNode[Parameters, Quam]): + """Save the calibration results and revert tracked qubit pair changes.""" + for qp in node.namespace.get("tracked_qubit_pairs", []): + qp.revert_changes() + + node.save() + + +# %% From 4f9ba4c194ce7c21861d8a2cef096a9020d9e200 Mon Sep 17 00:00:00 2001 From: Deepak Khurana <119570568+Deepakkhurrana@users.noreply.github.com> Date: Wed, 17 Jun 2026 17:50:53 +0200 Subject: [PATCH 05/12] updating analysis and plotting after qubit rename --- .../cz_leakage_amp/analysis.py | 59 +++++-------------- .../cz_leakage_amp/plotting.py | 8 +-- 2 files changed, 18 insertions(+), 49 deletions(-) diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/analysis.py b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/analysis.py index 563b13753..90f8c2410 100644 --- a/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/analysis.py +++ b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/analysis.py @@ -47,10 +47,9 @@ def log_fitted_results(fit_results: Dict[str, FitResults], log_callable=None): def process_raw_dataset(ds: xr.Dataset, node: QualibrationNode) -> xr.Dataset: """ - Process the raw dataset by adding amplitude coordinates and pair-combined state variables. + Process the raw dataset by adding amplitude coordinates. - If the dataset has only state_moving_stationary1, state_moving_stationary2, ... they are concatenated - along qubit_pair to form state_moving_stationary(qubit_pair, amp, number_of_operations). + Expects P(11) in ``state`` (stacked by XarrayDataFetcher from ``state1``, ``state2``, ...). Parameters: ----------- @@ -62,7 +61,7 @@ def process_raw_dataset(ds: xr.Dataset, node: QualibrationNode) -> xr.Dataset: Returns: -------- xr.Dataset - Processed dataset with additional coordinates and combined state variables. + Processed dataset with additional coordinates. """ qubit_pairs = node.namespace["qubit_pairs"] operation = node.parameters.operation @@ -72,28 +71,6 @@ def abs_amp(qp, amp): ds = ds.assign_coords({"amp_full": (["qubit_pair", "amp"], np.array([abs_amp(qp, ds.amp) for qp in qubit_pairs]))}) - for base_var in ( - "state_moving", - "state_stationary", - "state_moving_stationary", - ): - if base_var in ds.data_vars: - continue - - pair_vars = [ - var - for var in ds.data_vars - if var.startswith(base_var) and (var == base_var or var.replace(base_var, "").isdigit()) - ] - if not pair_vars: - continue - - pair_vars = sorted(pair_vars, key=lambda name: int(name.replace(base_var, "") or 0)) - state_list = [ds[var] for var in pair_vars] - pair_names = np.array(qubit_pairs.get_names())[: len(state_list)] - concatenated = xr.concat(state_list, dim="qubit_pair").assign_coords(qubit_pair=pair_names) - ds = ds.assign({base_var: concatenated}) - return ds @@ -120,16 +97,15 @@ def _optimal_amp_from_mean(X: np.ndarray, mean_vals: np.ndarray, smooth_sigma: f def fit_routine(da: xr.Dataset) -> xr.Dataset: """Compute mean P(11) over number_of_operations for each amplitude. - Adds mean_state_moving_stationary(amp). No oscillation fit. + Adds mean_state(amp). No oscillation fit. """ - data_var = "state_moving_stationary" if "state_moving_stationary" in da else None - if data_var is None: + if "state" not in da.data_vars: return da - arr = da[data_var] + arr = da["state"] if "number_of_operations" not in arr.dims: return da - mean_vals = arr.mean(dim="number_of_operations").rename("mean_state_moving_stationary") - return da.assign(mean_state_moving_stationary=mean_vals) + mean_vals = arr.mean(dim="number_of_operations").rename("mean_state") + return da.assign(mean_state=mean_vals) def fit_raw_data(ds: xr.Dataset, node: QualibrationNode) -> Tuple[xr.Dataset, Dict[str, FitResults]]: @@ -147,22 +123,15 @@ def fit_raw_data(ds: xr.Dataset, node: QualibrationNode) -> Tuple[xr.Dataset, Di for qp in qp_names: sub = ds_fit.sel(qubit_pair=qp) - mean_var = "mean_state_moving_stationary" if "mean_state_moving_stationary" in sub else None - if mean_var is None: - # Fallback: compute mean from combined P(11) state data. - if "state_moving_stationary" in sub: - mean_vals = sub.state_moving_stationary.mean(dim="number_of_operations") - sub = sub.assign(mean_state_moving_stationary=mean_vals) - mean_var = "mean_state_moving_stationary" - else: - opt_amps.append(np.nan) - opt_idxs.append(0) - successes.append(False) - continue + if "mean_state" not in sub: + opt_amps.append(np.nan) + opt_idxs.append(0) + successes.append(False) + continue try: amp_coord = sub.amp_full if "amp_full" in sub.coords else sub.amp X = np.asarray(amp_coord.values) - mean_arr = sub[mean_var].values + mean_arr = sub.mean_state.values x_star, idx = _optimal_amp_from_mean(X, mean_arr, smooth_sigma=1.0) amp_min, amp_max = float(np.min(X)), float(np.max(X)) if not np.isfinite(x_star) or not (amp_min <= x_star <= amp_max): diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/plotting.py b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/plotting.py index 059fa795e..bc3245857 100644 --- a/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/plotting.py +++ b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/plotting.py @@ -63,12 +63,12 @@ def plot_individual_raw_data_with_fit( """Plot one qubit-pair leakage-amplification heatmap.""" fr = ds_fit.sel(qubit_pair=qp_name) - if "state_moving_stationary" not in fr: + if "state" not in fr: ax.text(0.5, 0.5, "No leakage data", ha="center", va="center", transform=ax.transAxes) ax.set_title(qp_name) return - data = fr.state_moving_stationary # dims: amp, number_of_operations (or number_of_operations, amp) + data = fr.state coupler_flux_mV = 1e3 * (fr.amp_full if "amp_full" in fr.coords else fr.amp).values n_ops = ( fr.number_of_operations.values @@ -110,12 +110,12 @@ def plot_individual_mean_data_with_fit( """Plot one qubit-pair mean P(11) trace.""" fr = ds_fit.sel(qubit_pair=qp_name) - if "mean_state_moving_stationary" not in fr: + if "mean_state" not in fr: ax.text(0.5, 0.5, "No mean P(11) data", ha="center", va="center", transform=ax.transAxes) ax.set_title(qp_name) return - mean_arr = fr.mean_state_moving_stationary + mean_arr = fr.mean_state coupler_flux_mV = 1e3 * (fr.amp_full if "amp_full" in fr.coords else fr.amp).values success = "success" in fr.coords and bool(fr.success) and np.isfinite(float(fr.optimal_amplitude)) From d72806f5d1fdda0bb6bbf1a6dd9ee691d55f7ef2 Mon Sep 17 00:00:00 2001 From: Deepak Khurana <119570568+Deepakkhurrana@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:12:29 +0200 Subject: [PATCH 06/12] updating nodes after qubit rename and qua programme update --- .../33a_cz_leakage_amplification.py | 80 ++++++------- .../33b_cz_leakage_amplification_palea.py | 107 ++++++++++-------- 2 files changed, 100 insertions(+), 87 deletions(-) diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_leakage_amplification.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_leakage_amplification.py index 121b4d7d3..1608e5b76 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_leakage_amplification.py +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_leakage_amplification.py @@ -31,12 +31,12 @@ CALIBRATION OF THE CZ GATE COUPLER AMPLITUDE VIA LEAKAGE with error amplification This sequence calibrates the CZ gate by scanning the coupler pulse amplitude and measuring -the probability that both moving and stationary qubits remain in |1> (P(11)) after repeated CZ gates. +the probability that both qubits remain in |1> (P(11)) after repeated CZ gates. Both qubits start in |1>; the CZ is applied a variable number of times for error amplification. For each amplitude we measure: -- P(11) = fraction of shots where both moving and stationary qubits are in |1> (state_moving_stationary) -- Same for state_moving and state_stationary (optional) +- P(11) = fraction of shots where both qubits are in |1> (``state``) +- Per-qubit GEF readout on the high- and low-frequency qubits (``state_high_q``, ``state_low_q``) **Error amplification:** The CZ gate is applied repeatedly (number_of_operations) for each measurement. This amplifies @@ -60,10 +60,9 @@ name="33a_cz_leakage_amplification", # Name should be unique description=description, parameters=Parameters(), # Node parameters: calibration_utils/cz_leakage_amp/parameters.py + machine=Quam.load(), # Instantiate the QUAM class from the state file ) -# instrument_calibration_node(node) - # 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 @@ -71,13 +70,17 @@ def custom_param(node: QualibrationNode[Parameters, Quam]): """Set custom parameters for debugging purposes only.""" # node.parameters.qubit_pairs = ["q1-q2"] + node.parameters.qubit_pairs = ["coupler_q4_q5"] + node.parameters.operation = "cz_unipolar" + node.parameters.use_state_discrimination = True + node.parameters.reset_type = "active" + node.parameters.amp_range = 0.05 + node.parameters.amp_step = 0.0005 + node.parameters.num_shots = 100 + node.parameters.number_of_operations = 40 pass -# Instantiate the QUAM class from the state file -node.machine = Quam.load() - - # %% {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 @@ -89,12 +92,14 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: dis node.namespace["qubit_pairs"] = qubit_pairs = get_qubit_pairs(node) num_qubit_pairs = len(qubit_pairs) + operation = node.parameters.operation + # Verify qp.moving_qubit against recalculation and precompute roles for QUA program loops. # Logs a warning and corrects qp.moving_qubit in-memory if they disagree; state is persisted # at the end of the node. qubit_roles_map = {} for qp in qubit_pairs: - verify_moving_qubit(qp, operation=node.parameters.operation, repair_routing=True, log_callable=node.log) + verify_moving_qubit(qp, operation=operation, log_callable=node.log) qubit_roles_map[qp.name] = QubitRoles.resolve(qp) node.namespace["qubit_roles_map"] = qubit_roles_map @@ -102,7 +107,6 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: dis n_avg = node.parameters.num_shots amplitudes = np.arange(1 - node.parameters.amp_range, 1 + node.parameters.amp_range, node.parameters.amp_step) - operation = node.parameters.operation num_operations = node.parameters.number_of_operations # Register the sweep axes to be added to the dataset when fetching data node.namespace["sweep_axes"] = { @@ -121,19 +125,19 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: dis n_op = declare(int) # number of CZ operations count = declare(int) # loop counter n_st = declare_output_stream() - state_mq = [declare(int) for _ in range(num_qubit_pairs)] - state_sq = [declare(int) for _ in range(num_qubit_pairs)] - state_mq_st = [declare_output_stream() for _ in range(num_qubit_pairs)] - state_sq_st = [declare_output_stream() for _ in range(num_qubit_pairs)] - state_ms_st = [declare_output_stream() for _ in range(num_qubit_pairs)] + state_high_q = [declare(int) for _ in range(num_qubit_pairs)] + state_low_q = [declare(int) for _ in range(num_qubit_pairs)] + state_high_q_st = [declare_output_stream() for _ in range(num_qubit_pairs)] + state_low_q_st = [declare_output_stream() for _ in range(num_qubit_pairs)] + state_st = [declare_output_stream() for _ in range(num_qubit_pairs)] for multiplexed_qubit_pairs in qubit_pairs.batch(): # Initialize the qubits for qp in multiplexed_qubit_pairs.values(): qubit_role = qubit_roles_map[qp.name] - mq, sq = qubit_role.moving, qubit_role.stationary - node.machine.initialize_qpu(target=mq) - node.machine.initialize_qpu(target=sq) + high_q, low_q = qubit_role.high, qubit_role.low + node.machine.initialize_qpu(target=high_q) + node.machine.initialize_qpu(target=low_q) # Loop for averaging with for_(n, 0, n < n_avg, n + 1): save(n, n_st) @@ -143,17 +147,17 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: dis with for_(*from_array(amp, amplitudes)): for ii, qp in multiplexed_qubit_pairs.items(): qubit_role = qubit_roles_map[qp.name] - mq, sq = qubit_role.moving, qubit_role.stationary + high_q, low_q = qubit_role.high, qubit_role.low # Reset the qubits - mq.reset(node.parameters.reset_type, node.parameters.simulate) - sq.reset(node.parameters.reset_type, node.parameters.simulate) + high_q.reset(node.parameters.reset_type, node.parameters.simulate) + low_q.reset(node.parameters.reset_type, node.parameters.simulate) qp.align() # Reset the frames of both qubits - reset_frame(sq.xy.name) - reset_frame(mq.xy.name) + reset_frame(low_q.xy.name) + reset_frame(high_q.xy.name) # setting both qubits to the initial state - mq.xy.play("x180") - sq.xy.play("x180") + high_q.xy.play("x180") + low_q.xy.play("x180") qp.align() # Loop over the number of CZ operations with for_(count, 0, count < n_op, count + 1): @@ -162,27 +166,25 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: dis qp.align() # measure both qubits - mq.readout_state_gef(state_mq[ii]) - sq.readout_state_gef(state_sq[ii]) + high_q.readout_state_gef(state_high_q[ii]) + low_q.readout_state_gef(state_low_q[ii]) - with if_((state_mq[ii] == 1) & (state_sq[ii] == 1)): + with if_((state_high_q[ii] == 1) & (state_low_q[ii] == 1)): wait(4) - save(1, state_ms_st[ii]) + save(1, state_st[ii]) with else_(): wait(4) - save(0, state_ms_st[ii]) - - save(state_mq[ii], state_mq_st[ii]) - save(state_sq[ii], state_sq_st[ii]) + save(0, state_st[ii]) + save(state_high_q[ii], state_high_q_st[ii]) + save(state_low_q[ii], state_low_q_st[ii]) + align() with stream_processing(): n_st.save("n") for i in range(num_qubit_pairs): - state_mq_st[i].buffer(len(amplitudes)).buffer(num_operations).average().save(f"state_moving{i + 1}") - state_sq_st[i].buffer(len(amplitudes)).buffer(num_operations).average().save(f"state_stationary{i + 1}") - state_ms_st[i].buffer(len(amplitudes)).buffer(num_operations).average().save( - f"state_moving_stationary{i + 1}" - ) + state_high_q_st[i].buffer(len(amplitudes)).buffer(num_operations).average().save(f"state_high_q{i + 1}") + state_low_q_st[i].buffer(len(amplitudes)).buffer(num_operations).average().save(f"state_low_q{i + 1}") + state_st[i].buffer(len(amplitudes)).buffer(num_operations).average().save(f"state{i + 1}") # %% {Simulate} diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py index 13cd8a004..8ba5e5cad 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py @@ -66,8 +66,9 @@ # Be sure to include [Parameters, Quam] so the node has proper type hinting node = QualibrationNode[Parameters, Quam]( name="33b_cz_leakage_amplification_palea", # Name should be unique - description=description, + description=description, # Describe what the node is doing, which is also reflected in the QUAlibrate GUI parameters=Parameters(), # Node parameters: calibration_utils/cz_leakage_amp/parameters.py + machine=Quam.load(), # Instantiate the QUAM class from the state file ) @@ -78,11 +79,6 @@ def custom_param(node: QualibrationNode[Parameters, Quam]): """Set custom parameters for debugging purposes only.""" pass - -# Instantiate the QUAM class from the state file -node.machine = Quam.load() - - # %% {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 @@ -96,22 +92,33 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: dis node.namespace["qubit_pairs"] = qubit_pairs = get_qubit_pairs(node) num_qubit_pairs = len(qubit_pairs) + operation = node.parameters.operation + # Verify qp.moving_qubit against recalculation and precompute roles for QUA program loops. # Logs a warning and corrects qp.moving_qubit in-memory if they disagree; state is persisted # at the end of the node. qubit_roles_map = {} + cz_phase_shifts_map = {} for qp in qubit_pairs: - verify_moving_qubit(qp, operation=node.parameters.operation, repair_routing=True, log_callable=node.log) - qubit_roles_map[qp.name] = QubitRoles.resolve(qp) + verify_moving_qubit(qp, operation=operation, log_callable=node.log) + roles = QubitRoles.resolve(qp) + qubit_roles_map[qp.name] = roles + cz_macro = qp.macros[operation] + # CZGate applies phase_shift_control / phase_shift_target on control / target; + # map each to high_q / low_q by frequency. + if roles.high is qp.qubit_control: + cz_phase_shifts_map[qp.name] = (cz_macro.phase_shift_control, cz_macro.phase_shift_target) + else: + cz_phase_shifts_map[qp.name] = (cz_macro.phase_shift_target, cz_macro.phase_shift_control) node.namespace["qubit_roles_map"] = qubit_roles_map # Extract the sweep parameters and axes from the node parameters n_avg = node.parameters.num_shots amplitudes = np.arange(1 - node.parameters.amp_range, 1 + node.parameters.amp_range, node.parameters.amp_step) - operation = node.parameters.operation num_operations = node.parameters.number_of_operations # 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()), "number_of_operations": xr.DataArray( @@ -128,19 +135,20 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: dis n_op = declare(int) # number of CZ operations count = declare(int) # loop counter n_st = declare_output_stream() - state_mq = [declare(int) for _ in range(num_qubit_pairs)] - state_sq = [declare(int) for _ in range(num_qubit_pairs)] - state_mq_st = [declare_output_stream() for _ in range(num_qubit_pairs)] - state_sq_st = [declare_output_stream() for _ in range(num_qubit_pairs)] - state_ms_st = [declare_output_stream() for _ in range(num_qubit_pairs)] + state_high_q = [declare(int) for _ in range(num_qubit_pairs)] + state_low_q = [declare(int) for _ in range(num_qubit_pairs)] + state_high_q_st = [declare_output_stream() for _ in range(num_qubit_pairs)] + state_low_q_st = [declare_output_stream() for _ in range(num_qubit_pairs)] + state_st = [declare_output_stream() for _ in range(num_qubit_pairs)] + for multiplexed_qubit_pairs in qubit_pairs.batch(): # Initialize the qubits for qp in multiplexed_qubit_pairs.values(): qubit_role = qubit_roles_map[qp.name] - mq, sq = qubit_role.moving, qubit_role.stationary - node.machine.initialize_qpu(target=mq) - node.machine.initialize_qpu(target=sq) + high_q, low_q = qubit_role.high, qubit_role.low + node.machine.initialize_qpu(target=high_q) + node.machine.initialize_qpu(target=low_q) # Loop for averaging with for_(n, 0, n < n_avg, n + 1): save(n, n_st) @@ -150,59 +158,60 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: dis with for_(*from_array(amp, amplitudes)): for ii, qp in multiplexed_qubit_pairs.items(): qubit_role = qubit_roles_map[qp.name] - mq, sq = qubit_role.moving, qubit_role.stationary - lq, oq = qubit_role.leakage, qubit_role.low + high_q, low_q = qubit_role.high, qubit_role.low + phase_high, _ = cz_phase_shifts_map[qp.name] # Reset the qubits - mq.reset(node.parameters.reset_type, node.parameters.simulate) - sq.reset(node.parameters.reset_type, node.parameters.simulate) + high_q.reset(node.parameters.reset_type, node.parameters.simulate) + low_q.reset(node.parameters.reset_type, node.parameters.simulate) qp.align() - # Reset the frames of both qubits - reset_frame(sq.xy.name) - reset_frame(mq.xy.name) # setting both qubits to the initial state - mq.xy.play("x180") - sq.xy.play("x180") + high_q.xy.play("x180") + low_q.xy.play("x180") qp.align() # Loop over the number of CZ operations with for_(count, 0, count < n_op, count + 1): # play the CZ gate qp.macros[operation].apply(amplitude_scale_coupler=amp) + # Undo CZ virtual-Z phase shift before DD, mapped by qubit frequency. + frame_rotation_2pi(-phase_high, high_q.xy.name) # play the PALEA Dynamical decoupling sequence: # EF (e-f) pi on the leakage qubit (|11⟩↔|20⟩), g-e pi on the low qubit. - lq.xy.update_frequency( - lq.xy.intermediate_frequency - lq.anharmonicity, - keep_phase=True, + high_q.xy.update_frequency( + high_q.xy.intermediate_frequency - high_q.anharmonicity, ) - lq.xy.play("EF_x180") - lq.xy.update_frequency(lq.xy.intermediate_frequency, keep_phase=True) - oq.xy.play("x180") + high_q.xy.play("EF_x180") # high freq qubit is the leakage qubit + low_q.xy.play("x180") + high_q.xy.update_frequency(high_q.xy.intermediate_frequency) + # Apply CZ virtual-Z phases after DD, mapped by qubit frequency. + frame_rotation_2pi(phase_high, high_q.xy.name) + wait(4) qp.align() - + # measure both qubits - mq.readout_state_gef(state_mq[ii]) - sq.readout_state_gef(state_sq[ii]) + high_q.readout_state_gef(state_high_q[ii]) + low_q.readout_state_gef(state_low_q[ii]) - with if_((state_mq[ii] == 1) & (state_sq[ii] == 1)): + with if_((state_high_q[ii] == 1) & (state_low_q[ii] == 1)): wait(4) - save(1, state_ms_st[ii]) + save(1, state_st[ii]) with else_(): wait(4) - save(0, state_ms_st[ii]) + save(0, state_st[ii]) - save(state_mq[ii], state_mq_st[ii]) - save(state_sq[ii], state_sq_st[ii]) + save(state_high_q[ii], state_high_q_st[ii]) + save(state_low_q[ii], state_low_q_st[ii]) align() with stream_processing(): n_st.save("n") for i in range(num_qubit_pairs): - state_mq_st[i].buffer(len(amplitudes)).buffer(len(np.arange(2, num_operations + 1, 2))).average().save( - f"state_moving{i + 1}" + state_high_q_st[i].buffer(len(amplitudes)).buffer(len(np.arange(2, num_operations + 1, 2))).average().save( + f"state_high_q{i + 1}" ) - state_sq_st[i].buffer(len(amplitudes)).buffer(len(np.arange(2, num_operations + 1, 2))).average().save( - f"state_stationary{i + 1}" + state_low_q_st[i].buffer(len(amplitudes)).buffer(len(np.arange(2, num_operations + 1, 2))).average().save( + f"state_low_q{i + 1}" ) - state_ms_st[i].buffer(len(amplitudes)).buffer(len(np.arange(2, num_operations + 1, 2))).average().save( - f"state_moving_stationary{i + 1}" + state_st[i].buffer(len(amplitudes)).buffer(len(np.arange(2, num_operations + 1, 2))).average().save( + f"state{i + 1}" ) @@ -289,8 +298,10 @@ def plot_data(node: QualibrationNode[Parameters, Quam]): figures = plot_raw_data_with_fit(node.results["ds_fit"], qubit_pairs) for fig in figures.values(): plt.show() - node.results["leakage_raw_figure"] = figures["raw"] - node.results["leakage_mean_figure"] = figures["mean"] + node.results["figures"] = { + "leakage_raw": figures["raw"], + "leakage_mean": figures["mean"], + } # %% {Update_state} From 805d0edc197017e1e8b9b4db459323b031955e80 Mon Sep 17 00:00:00 2001 From: Deepak Khurana <119570568+Deepakkhurrana@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:19:44 +0200 Subject: [PATCH 07/12] minor formatting changes --- .../cz_leakage_amp/__init__.py | 2 +- .../33a_cz_leakage_amplification.py | 10 +------- .../33b_cz_leakage_amplification_palea.py | 24 +++++++++---------- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/__init__.py b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/__init__.py index 565d5b0d4..d9814c7a4 100644 --- a/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/__init__.py +++ b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/__init__.py @@ -1,4 +1,4 @@ -"""CZ conditional phase error amplification calibration utilities.""" +"""CZ leakage amplification calibration utilities.""" from .analysis import FitResults, fit_raw_data, log_fitted_results, process_raw_dataset from .parameters import Parameters diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_leakage_amplification.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_leakage_amplification.py index 1608e5b76..79f9d3afc 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_leakage_amplification.py +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_leakage_amplification.py @@ -58,7 +58,7 @@ # Be sure to include [Parameters, Quam] so the node has proper type hinting node = QualibrationNode[Parameters, Quam]( name="33a_cz_leakage_amplification", # Name should be unique - description=description, + description=description, # Describe what the node is doing, which is also reflected in the QUAlibrate GUI parameters=Parameters(), # Node parameters: calibration_utils/cz_leakage_amp/parameters.py machine=Quam.load(), # Instantiate the QUAM class from the state file ) @@ -70,14 +70,6 @@ def custom_param(node: QualibrationNode[Parameters, Quam]): """Set custom parameters for debugging purposes only.""" # node.parameters.qubit_pairs = ["q1-q2"] - node.parameters.qubit_pairs = ["coupler_q4_q5"] - node.parameters.operation = "cz_unipolar" - node.parameters.use_state_discrimination = True - node.parameters.reset_type = "active" - node.parameters.amp_range = 0.05 - node.parameters.amp_step = 0.0005 - node.parameters.num_shots = 100 - node.parameters.number_of_operations = 40 pass diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py index 8ba5e5cad..4de8ede76 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py @@ -66,9 +66,9 @@ # Be sure to include [Parameters, Quam] so the node has proper type hinting node = QualibrationNode[Parameters, Quam]( name="33b_cz_leakage_amplification_palea", # Name should be unique - description=description, # Describe what the node is doing, which is also reflected in the QUAlibrate GUI + description=description, # Describe what the node is doing, which is also reflected in the QUAlibrate GUI parameters=Parameters(), # Node parameters: calibration_utils/cz_leakage_amp/parameters.py - machine=Quam.load(), # Instantiate the QUAM class from the state file + machine=Quam.load(), # Instantiate the QUAM class from the state file ) @@ -79,6 +79,7 @@ def custom_param(node: QualibrationNode[Parameters, Quam]): """Set custom parameters for debugging purposes only.""" 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 @@ -118,7 +119,7 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: dis num_operations = node.parameters.number_of_operations # 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()), "number_of_operations": xr.DataArray( @@ -141,7 +142,6 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: dis state_low_q_st = [declare_output_stream() for _ in range(num_qubit_pairs)] state_st = [declare_output_stream() for _ in range(num_qubit_pairs)] - for multiplexed_qubit_pairs in qubit_pairs.batch(): # Initialize the qubits for qp in multiplexed_qubit_pairs.values(): @@ -179,14 +179,14 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: dis high_q.xy.update_frequency( high_q.xy.intermediate_frequency - high_q.anharmonicity, ) - high_q.xy.play("EF_x180") # high freq qubit is the leakage qubit + high_q.xy.play("EF_x180") # high freq qubit is the leakage qubit low_q.xy.play("x180") high_q.xy.update_frequency(high_q.xy.intermediate_frequency) # Apply CZ virtual-Z phases after DD, mapped by qubit frequency. frame_rotation_2pi(phase_high, high_q.xy.name) wait(4) qp.align() - + # measure both qubits high_q.readout_state_gef(state_high_q[ii]) low_q.readout_state_gef(state_low_q[ii]) @@ -204,12 +204,12 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: dis with stream_processing(): n_st.save("n") for i in range(num_qubit_pairs): - state_high_q_st[i].buffer(len(amplitudes)).buffer(len(np.arange(2, num_operations + 1, 2))).average().save( - f"state_high_q{i + 1}" - ) - state_low_q_st[i].buffer(len(amplitudes)).buffer(len(np.arange(2, num_operations + 1, 2))).average().save( - f"state_low_q{i + 1}" - ) + state_high_q_st[i].buffer(len(amplitudes)).buffer( + len(np.arange(2, num_operations + 1, 2)) + ).average().save(f"state_high_q{i + 1}") + state_low_q_st[i].buffer(len(amplitudes)).buffer( + len(np.arange(2, num_operations + 1, 2)) + ).average().save(f"state_low_q{i + 1}") state_st[i].buffer(len(amplitudes)).buffer(len(np.arange(2, num_operations + 1, 2))).average().save( f"state{i + 1}" ) From 3629e4ae6a5efec3e47bc1ed45eaa67fd5998cdd Mon Sep 17 00:00:00 2001 From: Deepak Khurana <119570568+Deepakkhurrana@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:32:54 +0200 Subject: [PATCH 08/12] updating readme and minor changes --- .../34_cz_phase_compensation.py | 2 +- .../99_CZ_calibration_graph.py | 2 +- .../calibrations/CZ_calibrations/README.md | 70 +++++++++++++------ 3 files changed, 50 insertions(+), 24 deletions(-) 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 index 820d12436..094c28cba 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34_cz_phase_compensation.py +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34_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="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(), 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 8f8e4f40b..ae15d5f5b 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["33_cz_phase_compensation"].copy(name="phase_compensation"), + "phase_compensation": library.nodes["34_cz_phase_compensation"].copy(name="phase_compensation"), }, connectivity=[ ("chevron", "conditional_phase"), diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/README.md b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/README.md index 08111d95d..b3769ba5b 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/README.md +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/README.md @@ -1,6 +1,6 @@ # **CZ gate on flux-tunable transmons: physics & calibration** -This folder contains routines for the **flux-activated CZ gate** on flux-tunable transmons. The gate uses the **|11⟩ ↔ |02⟩** avoided crossing; a baseband flux pulse on the moving qubit brings the pair into the interaction region. +This folder contains routines for the **flux-activated CZ gate** on flux-tunable transmons. The gate uses the **|11⟩ ↔ |20⟩** (state convention: |high_freq_qubit, low_freq_qubit⟩) avoided crossing; a baseband flux pulse on the moving qubit brings the pair into the interaction region. Hardware falls into two workflows: @@ -15,7 +15,7 @@ Distortion calibration (**17**, **18**) applies to **both** (moving-qubit flux l ## Table of Contents -1. [Physics of the CZ gate (11–02 interaction)](#physics-of-the-cz-gate-based-on-11-02-interaction) +1. [Physics of the CZ gate (11–20 interaction)](#physics-of-the-cz-gate-based-on-11-02-interaction) 2. [Calibration workflows](#calibration-workflows) - [Shared: flux-line distortions](#shared-flux-line-distortions) - [Fixed-coupler workflow](#fixed-coupler-workflow) @@ -27,9 +27,9 @@ Distortion calibration (**17**, **18**) applies to **both** (moving-qubit flux l --- -# Physics of the CZ gate based on 11–02 Interaction +# Physics of the CZ gate based on 11–20 Interaction -The **controlled-Z (CZ) gate** for flux tunable superconducting qubits operates via the _|11⟩ ↔ |02⟩_ avoided crossing between two transmons coupled with exchange rate **J**. +The **controlled-Z (CZ) gate** for flux tunable superconducting qubits operates via the _|11⟩ ↔ |20⟩_ avoided crossing between two transmons coupled with exchange rate **J**. ### Mechanism @@ -38,7 +38,7 @@ The CZ gate is realized by pulsing the qubit frequencies to an avoided crossing

Spectrum illustration

-At Point II, a useful two-qubit interaction appears in the two-excitation spectrum. This involves a large cavity-mediated avoided crossing between the computational state |11⟩ and the non-computational higher-level transmon excitation |02⟩. +At Point II, a useful two-qubit interaction appears in the two-excitation spectrum. This involves a large cavity-mediated avoided crossing between the computational state |11⟩ and the non-computational higher-level transmon excitation |20⟩. This avoided crossing causes a frequency shift, $\zeta/2\pi$, in the transition frequency of the |11⟩ state. A CZ gate is implemented by selecting a voltage pulse $V_R$ into Point II such that the time integral of the frequency shift satisfies: @@ -107,7 +107,7 @@ Coupler bias is **not** swept in the CZ calibration chain. After distortion cali ```text 17 → 18 → 31 → 32a → 32b - └→ 33 + └→ 34 ``` | Order | Node | Summary | @@ -115,17 +115,17 @@ Coupler bias is **not** swept in the CZ calibration chain. After distortion cali | 1 | **31** | Chevron: amplitude × duration → coarse CZ duration/amplitude | | 2 | **32a** | Fine amplitude scan → π/2 conditional-phase point | | 3 | **32b** | CZ pulse train → error-amplified amplitude fine tune | -| 4 | **33** | Virtual-Z phase compensation (can run after **32a**; **99** runs it in parallel with **32b**) | +| 4 | **34** | Virtual-Z phase compensation (can run after **32a**; **99** runs it in parallel with **32b**) | --- ## 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 → 33** without **31**. Pulse duration and macro amplitudes come from **30** and the gate macro already in QUAM. +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. ```text -17 → 18 → 30 → 32a → 32b - └→ 33 +17 → 18 → 30 → 32a → 32b → 33a/33b + └→ 34 ``` | Order | Node | Summary | @@ -133,7 +133,9 @@ Node **30** finds coupler **decouple (idle)** and **interaction** flux biases pl | 1 | **30** | 2D coupler + moving-qubit flux → `decouple_offset`, `detuning`, `macros[operation]` | | 2 | **32a** | Fine amplitude → π/2 conditional-phase point | | 3 | **32b** | CZ pulse train → error-amplified amplitude fine tune | -| 4 | **33** | Virtual-Z phase compensation | +| 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 | **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). @@ -193,9 +195,29 @@ Train of CZ pulses for finer amplitude tuning. --- +## Leakage amplification — tunable coupler only + +### Standard protocol + +[(33a_cz_leakage_amplification)](./33a_cz_leakage_amplification.py) + +Prepare \|11⟩, sweep **coupler flux pulse amplitude**, repeat CZ `n = 1…N`, measure P(11). Optimal amplitude maximizes mean P(11) over `n`. Requires GEF readout and `macros[operation].coupler_flux_pulse`. + +**Goal:** Tune `coupler_flux_pulse.amplitude` to preserve \|11⟩ under repeated CZ. + +### PALEA protocol + +[(33b_cz_leakage_amplification_palea)](./33b_cz_leakage_amplification_palea.py) + +Same coupler-amplitude objective as **33a**, with a dynamical-decoupling layer after each CZ (EF π on the high-frequency qubit, g–e π on the low-frequency qubit). Sweeps even `n = 2, 4, …`. See Marxer et al., [arXiv:2508.16437](https://arxiv.org/abs/2508.16437). + +**Goal:** Same state update as **33a** with improved leakage-error amplification. + +--- + ## Phase compensation — both workflows -[(33_cz_phase_compensation)](./33_cz_phase_compensation.py) +[(34_cz_phase_compensation)](./34_cz_phase_compensation.py) |++⟩, apply CZ, reconstruct per-qubit phase; update virtual Z in state. @@ -209,16 +231,18 @@ Train of CZ pulses for finer amplitude tuning. # 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) | ✓ | ✓ | -| **33** | [`33_cz_phase_compensation.py`](./33_cz_phase_compensation.py) | ✓ | ✓ | -| **99** | [`99_CZ_calibration_graph.py`](./99_CZ_calibration_graph.py) | ✓ (31–33) | — (use **30** → 32a–33 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) | — | ✓ | +| **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) | -Utilities: `cz_iswap_flux_bootstrap`, `chevron_cz`, `cz_conditional_phase`, `cz_conditional_phase_error_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` under `../calibration_utils/`. --- @@ -226,7 +250,9 @@ 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** → **33** +- **31** → **32a** → **32b** → **34** + +Leakage nodes (**33a** / **33b**) are tunable-coupler only and are not included in this graph. --- From 972fddf8a3d089af5eb41ed00c28b89edb6466d0 Mon Sep 17 00:00:00 2001 From: paulQM Date: Thu, 18 Jun 2026 17:34:05 +0200 Subject: [PATCH 09/12] fix. add palea with a temporary ef element --- .../cz_leakage_amp/__init__.py | 2 + .../cz_leakage_amp/config.py | 75 +++++++++++++++++++ .../33b_cz_leakage_amplification_palea.py | 25 +++---- 3 files changed, 87 insertions(+), 15 deletions(-) create mode 100644 qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/config.py diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/__init__.py b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/__init__.py index d9814c7a4..480127730 100644 --- a/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/__init__.py +++ b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/__init__.py @@ -1,12 +1,14 @@ """CZ leakage amplification calibration utilities.""" from .analysis import FitResults, fit_raw_data, log_fitted_results, process_raw_dataset +from .config import build_palea_qm_config from .parameters import Parameters from .plotting import plot_raw_data_with_fit from calibration_utils.cz_iswap_flux_bootstrap.parameters import QubitRoles, verify_moving_qubit # noqa: F401 __all__ = [ "Parameters", + "build_palea_qm_config", "QubitRoles", "verify_moving_qubit", "process_raw_dataset", diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/config.py b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/config.py new file mode 100644 index 000000000..a233b79e7 --- /dev/null +++ b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/config.py @@ -0,0 +1,75 @@ +"""Runtime QM config helpers for PALEA leakage amplification.""" + +from __future__ import annotations + +import copy +import dataclasses +from typing import Dict, Iterable, List + + +def unique_high_qubits(qubit_roles_map, qubit_pairs) -> List: + """Return the distinct high-frequency qubits across active pairs.""" + seen = set() + high_qubits = [] + for qp in qubit_pairs: + high_q = qubit_roles_map[qp.name].high + if high_q.name in seen: + continue + seen.add(high_q.name) + high_qubits.append(high_q) + return high_qubits + + +def ensure_ef_x180_operation(qubit) -> None: + """Ensure ``EF_x180`` exists on the qubit XY channel (same pattern as node 13).""" + if hasattr(qubit.xy.operations, "EF_x180"): + return + x180 = qubit.xy.operations["x180"] + qubit.xy.operations["EF_x180"] = ( + dataclasses.replace(x180, alpha=0.0) if hasattr(x180, "alpha") else dataclasses.replace(x180) + ) + + +def add_palea_ef_elements(config: dict, high_qubits: Iterable) -> Dict[str, str]: + """Add temporary EF elements at IF = xy_IF - anharmonicity for PALEA DD. + + For each high-frequency qubit, creates ``{xy_element}.ef`` sharing the XY port and + exposing a single ``x180`` operation aliased to the existing ``EF_x180`` pulse. + + Returns + ------- + dict + Mapping from qubit name to EF element name. + """ + ef_element_names: Dict[str, str] = {} + for qubit in high_qubits: + xy_name = qubit.xy.name + ef_name = f"{xy_name}.ef" + try: + xy_element = config["elements"][xy_name] + except KeyError as exc: + raise KeyError(f"XY element {xy_name!r} not found in generated config.") from exc + + try: + ef_x180_pulse = xy_element["operations"]["EF_x180"] + except KeyError as exc: + raise ValueError( + f"Qubit {qubit.name} is missing EF_x180 on {xy_name}; calibrate EF gates first." + ) from exc + + ef_element = copy.deepcopy(xy_element) + ef_element["intermediate_frequency"] = qubit.xy.intermediate_frequency - qubit.anharmonicity + ef_element["operations"] = {"EF_x180": ef_x180_pulse} + config["elements"][ef_name] = ef_element + ef_element_names[qubit.name] = ef_name + return ef_element_names + + +def build_palea_qm_config(machine, qubit_pairs, qubit_roles_map) -> tuple[dict, Dict[str, str]]: + """Generate QM config with PALEA EF elements injected for all high qubits in the pairs.""" + high_qubits = unique_high_qubits(qubit_roles_map, qubit_pairs) + for qubit in high_qubits: + ensure_ef_x180_operation(qubit) + config = machine.generate_config() + ef_element_names = add_palea_ef_elements(config, high_qubits) + return config, ef_element_names diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py index 4de8ede76..e4e25a970 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py @@ -19,6 +19,7 @@ from calibration_utils.cz_leakage_amp import ( Parameters, + build_palea_qm_config, fit_raw_data, log_fitted_results, QubitRoles, @@ -113,6 +114,10 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: dis cz_phase_shifts_map[qp.name] = (cz_macro.phase_shift_target, cz_macro.phase_shift_control) node.namespace["qubit_roles_map"] = qubit_roles_map + qm_config, ef_element_names = build_palea_qm_config(node.machine, qubit_pairs, qubit_roles_map) + node.namespace["qm_config"] = qm_config + node.namespace["ef_element_names"] = ef_element_names + # Extract the sweep parameters and axes from the node parameters n_avg = node.parameters.num_shots amplitudes = np.arange(1 - node.parameters.amp_range, 1 + node.parameters.amp_range, node.parameters.amp_step) @@ -172,21 +177,13 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: dis with for_(count, 0, count < n_op, count + 1): # play the CZ gate qp.macros[operation].apply(amplitude_scale_coupler=amp) - # Undo CZ virtual-Z phase shift before DD, mapped by qubit frequency. - frame_rotation_2pi(-phase_high, high_q.xy.name) + align(ef_element_names[high_q.name], high_q.xy.name) # play the PALEA Dynamical decoupling sequence: # EF (e-f) pi on the leakage qubit (|11⟩↔|20⟩), g-e pi on the low qubit. - high_q.xy.update_frequency( - high_q.xy.intermediate_frequency - high_q.anharmonicity, - ) - high_q.xy.play("EF_x180") # high freq qubit is the leakage qubit + play("EF_x180", ef_element_names[high_q.name]) low_q.xy.play("x180") - high_q.xy.update_frequency(high_q.xy.intermediate_frequency) - # Apply CZ virtual-Z phases after DD, mapped by qubit frequency. - frame_rotation_2pi(phase_high, high_q.xy.name) - wait(4) - qp.align() + qp.align() # measure both qubits high_q.readout_state_gef(state_high_q[ii]) low_q.readout_state_gef(state_low_q[ii]) @@ -221,8 +218,7 @@ 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() + config = node.namespace["qm_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 @@ -235,8 +231,7 @@ 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.""" # Connect to the QOP qmm = node.machine.connect() - # Get the config from the machine - config = node.machine.generate_config() + config = node.namespace["qm_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 From 1046a95b61b55422060c7d6b3e5411657d3723ca Mon Sep 17 00:00:00 2001 From: paulQM Date: Fri, 19 Jun 2026 09:34:25 +0200 Subject: [PATCH 10/12] black formatting --- .../calibration_utils/cz_leakage_amp/config.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/config.py b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/config.py index a233b79e7..6735b3139 100644 --- a/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/config.py +++ b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/config.py @@ -53,9 +53,7 @@ def add_palea_ef_elements(config: dict, high_qubits: Iterable) -> Dict[str, str] try: ef_x180_pulse = xy_element["operations"]["EF_x180"] except KeyError as exc: - raise ValueError( - f"Qubit {qubit.name} is missing EF_x180 on {xy_name}; calibrate EF gates first." - ) from exc + raise ValueError(f"Qubit {qubit.name} is missing EF_x180 on {xy_name}; calibrate EF gates first.") from exc ef_element = copy.deepcopy(xy_element) ef_element["intermediate_frequency"] = qubit.xy.intermediate_frequency - qubit.anharmonicity From a4fb66da3cc71303ab1a5112762fb79243d40dd6 Mon Sep 17 00:00:00 2001 From: Deepak Khurana <119570568+Deepakkhurrana@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:03:06 +0200 Subject: [PATCH 11/12] removing unused refs --- .../CZ_calibrations/33b_cz_leakage_amplification_palea.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py index e4e25a970..f90df0df5 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py @@ -11,7 +11,6 @@ from qualang_tools.multi_user import qm_session from qualang_tools.results import progress_counter from qualibrate import QualibrationNode -from qualibration_libs.core import tracked_updates from qualibration_libs.data import XarrayDataFetcher from qualibration_libs.parameters import get_qubit_pairs from qualibration_libs.runtime import simulate_and_plot @@ -164,7 +163,6 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: dis for ii, qp in multiplexed_qubit_pairs.items(): qubit_role = qubit_roles_map[qp.name] high_q, low_q = qubit_role.high, qubit_role.low - phase_high, _ = cz_phase_shifts_map[qp.name] # Reset the qubits high_q.reset(node.parameters.reset_type, node.parameters.simulate) low_q.reset(node.parameters.reset_type, node.parameters.simulate) @@ -316,10 +314,7 @@ def update_state(node: QualibrationNode[Parameters, Quam]): # %% {Save_results} @node.run_action() def save_results(node: QualibrationNode[Parameters, Quam]): - """Save the calibration results and revert tracked qubit pair changes.""" - for qp in node.namespace.get("tracked_qubit_pairs", []): - qp.revert_changes() - + """Save the calibration results.""" node.save() From f4c8ea0d95afd59ec5844689c456dd6b9c7251af Mon Sep 17 00:00:00 2001 From: Deepak Khurana <119570568+Deepakkhurrana@users.noreply.github.com> Date: Mon, 29 Jun 2026 12:07:09 +0200 Subject: [PATCH 12/12] minor fixes --- .../cz_leakage_amp/plotting.py | 7 +++++-- .../33b_cz_leakage_amplification_palea.py | 21 ++++++++----------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/plotting.py b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/plotting.py index bc3245857..53599758b 100644 --- a/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/plotting.py +++ b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/plotting.py @@ -15,6 +15,7 @@ def plot_raw_data_with_fit( ds_fit: xr.Dataset, qubit_pairs: list, node=None, + title_prefix: str = "CZ leakage amplification", ) -> Dict[str, Figure]: """Plot CZ leakage amplification data on chip-topology grids. @@ -26,6 +27,8 @@ def plot_raw_data_with_fit( Qubit pair objects used for grid placement. node : optional Kept for backwards-compatible call sites. + title_prefix : str + Prefix for figure suptitles (e.g. ``"CZ leakage amplification (PALEA)"``). Returns ------- @@ -40,7 +43,7 @@ def plot_raw_data_with_fit( for ax, qubit in grid_iter(raw_grid): qp_name = qubit["qubit"] plot_individual_raw_data_with_fit(ax, ds_fit, qp_name) - raw_grid.fig.suptitle("CZ leakage amplification - raw P(11)") + raw_grid.fig.suptitle(f"{title_prefix} - raw P(11)") raw_grid.fig.tight_layout() figures["raw"] = raw_grid.fig @@ -48,7 +51,7 @@ def plot_raw_data_with_fit( for ax, qubit in grid_iter(mean_grid): qp_name = qubit["qubit"] plot_individual_mean_data_with_fit(ax, ds_fit, qp_name) - mean_grid.fig.suptitle("CZ leakage amplification - mean P(11)") + mean_grid.fig.suptitle(f"{title_prefix} - mean P(11)") mean_grid.fig.tight_layout() figures["mean"] = mean_grid.fig diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py index f90df0df5..740956a09 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py @@ -35,9 +35,9 @@ Amplification (PALEA) protocol, as described in Marxer et al., arXiv:2508.16437 (https://arxiv.org/abs/2508.16437). -PALEA is designed to coherently amplify population leakage from |11> to |02> caused by imperfect +PALEA is designed to coherently amplify population leakage from |11> to |20> caused by imperfect diabatic transitions during the CZ gate, while remaining robust against other error sources such -as ZZ over-rotation and single-qubit phase errors. Compared to standard leakage amplification, +as ZZ over-rotation and single-qubit phase errors. Compared to standard leakage amplification (node 33a), PALEA achieves at least a factor-of-two reduction in leakage for the same number of gate repetitions. **Protocol:** @@ -99,18 +99,10 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: dis # Logs a warning and corrects qp.moving_qubit in-memory if they disagree; state is persisted # at the end of the node. qubit_roles_map = {} - cz_phase_shifts_map = {} for qp in qubit_pairs: verify_moving_qubit(qp, operation=operation, log_callable=node.log) roles = QubitRoles.resolve(qp) qubit_roles_map[qp.name] = roles - cz_macro = qp.macros[operation] - # CZGate applies phase_shift_control / phase_shift_target on control / target; - # map each to high_q / low_q by frequency. - if roles.high is qp.qubit_control: - cz_phase_shifts_map[qp.name] = (cz_macro.phase_shift_control, cz_macro.phase_shift_target) - else: - cz_phase_shifts_map[qp.name] = (cz_macro.phase_shift_target, cz_macro.phase_shift_control) node.namespace["qubit_roles_map"] = qubit_roles_map qm_config, ef_element_names = build_palea_qm_config(node.machine, qubit_pairs, qubit_roles_map) @@ -177,7 +169,7 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: dis qp.macros[operation].apply(amplitude_scale_coupler=amp) align(ef_element_names[high_q.name], high_q.xy.name) # play the PALEA Dynamical decoupling sequence: - # EF (e-f) pi on the leakage qubit (|11⟩↔|20⟩), g-e pi on the low qubit. + # EF (e-f) pi on the leakage qubit ( high-q qubit for |11⟩↔|20⟩), g-e pi on the low qubit. play("EF_x180", ef_element_names[high_q.name]) low_q.xy.play("x180") @@ -288,7 +280,11 @@ def plot_data(node: QualibrationNode[Parameters, Quam]): """Plot the raw and fitted data in a specific figure whose shape is given by qubit pair grid locations.""" qubit_pairs = node.namespace["qubit_pairs"] - figures = plot_raw_data_with_fit(node.results["ds_fit"], qubit_pairs) + figures = plot_raw_data_with_fit( + node.results["ds_fit"], + qubit_pairs, + title_prefix="CZ leakage amplification (PALEA)", + ) for fig in figures.values(): plt.show() node.results["figures"] = { @@ -307,6 +303,7 @@ def update_state(node: QualibrationNode[Parameters, Quam]): fit_results = node.results["fit_results"] for qp in node.namespace["qubit_pairs"]: if node.outcomes[qp.name] == "failed": + node.log(f"Skipping state update for {qp.name}: fit flagged unsuccessful.") continue qp.macros[operation].coupler_flux_pulse.amplitude = fit_results[qp.name]["optimal_amplitude"]