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..480127730 --- /dev/null +++ b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/__init__.py @@ -0,0 +1,19 @@ +"""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", + "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..90f8c2410 --- /dev/null +++ b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/analysis.py @@ -0,0 +1,160 @@ +"""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. + + Expects P(11) in ``state`` (stacked by XarrayDataFetcher from ``state1``, ``state2``, ...). + + 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. + """ + 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]))}) + + 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(amp). No oscillation fit. + """ + if "state" not in da.data_vars: + return da + arr = da["state"] + if "number_of_operations" not in arr.dims: + return da + 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]]: + """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) + 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_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): + 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/config.py b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/config.py new file mode 100644 index 000000000..6735b3139 --- /dev/null +++ b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/config.py @@ -0,0 +1,73 @@ +"""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/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..53599758b --- /dev/null +++ b/qualibration_graphs/superconducting/calibration_utils/cz_leakage_amp/plotting.py @@ -0,0 +1,138 @@ +"""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, + title_prefix: str = "CZ leakage amplification", +) -> 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. + title_prefix : str + Prefix for figure suptitles (e.g. ``"CZ leakage amplification (PALEA)"``). + + 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(f"{title_prefix} - 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(f"{title_prefix} - 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" 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 + 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" 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 + 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) 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..79f9d3afc --- /dev/null +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33a_cz_leakage_amplification.py @@ -0,0 +1,293 @@ +"""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 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 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 +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, # 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 +) + + +# 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 + + +# %% {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) + + 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=operation, 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) + + 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_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] + 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) + # 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] + high_q, low_q = qubit_role.high, qubit_role.low + # Reset the qubits + 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(low_q.xy.name) + reset_frame(high_q.xy.name) + # setting both qubits to the initial state + 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) + qp.align() + + # measure both qubits + high_q.readout_state_gef(state_high_q[ii]) + low_q.readout_state_gef(state_low_q[ii]) + + with if_((state_high_q[ii] == 1) & (state_low_q[ii] == 1)): + wait(4) + save(1, state_st[ii]) + with else_(): + wait(4) + 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_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} +@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() + + +# %% 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..740956a09 --- /dev/null +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33b_cz_leakage_amplification_palea.py @@ -0,0 +1,318 @@ +"""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, + build_palea_qm_config, + 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 |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 (node 33a), +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, # 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 +) + + +# 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 + + +# %% {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) + + 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=operation, log_callable=node.log) + roles = QubitRoles.resolve(qp) + qubit_roles_map[qp.name] = roles + 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) + + 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_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] + 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) + # 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] + high_q, low_q = qubit_role.high, qubit_role.low + # Reset the qubits + high_q.reset(node.parameters.reset_type, node.parameters.simulate) + low_q.reset(node.parameters.reset_type, node.parameters.simulate) + qp.align() + # setting both qubits to the initial state + 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) + 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 ( 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") + + qp.align() + # measure both qubits + high_q.readout_state_gef(state_high_q[ii]) + low_q.readout_state_gef(state_low_q[ii]) + + with if_((state_high_q[ii] == 1) & (state_low_q[ii] == 1)): + wait(4) + save(1, state_st[ii]) + with else_(): + wait(4) + 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_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}" + ) + + +# %% {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() + 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 + 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() + 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 + 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, + title_prefix="CZ leakage amplification (PALEA)", + ) + 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() + + +# %% 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 99% 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 index 820d12436..094c28cba 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/33_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. ---