diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/analysis.py b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/analysis.py index 86558be2d6..ba1ca2a358 100644 --- a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/analysis.py +++ b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/analysis.py @@ -12,7 +12,19 @@ @dataclass class FitResults: - """Stores the relevant CZ conditional phase experiment fit parameters for a single qubit pair""" + """Fit results for a single qubit pair from the CZ phase compensation calibration. + + Attributes: + ----------- + control_phase_correction : float + Residual single-qubit phase accumulated by the control qubit during the CZ gate (in 2π units). + This value is subtracted from ``phase_shift_control`` in the state update. + target_phase_correction : float + Residual single-qubit phase accumulated by the target qubit during the CZ gate (in 2π units). + This value is subtracted from ``phase_shift_target`` in the state update. + success : bool + True if the sinusoidal fit converged for both qubits. + """ control_phase_correction: float target_phase_correction: float @@ -20,10 +32,19 @@ class FitResults: def fix_oscillation_phi_2pi(fit_data): - """Extract and fix the phase parameter from oscillation fit data.""" - # Extract the phase parameter from the fit results + """Extract and normalise the oscillation phase to the [0, 1) range (representing 0 to 2π). + + Parameters: + ----------- + fit_data : xr.DataArray + Oscillation fit result with a ``fit_vals`` dimension containing ``"phi"``. + + Returns: + -------- + xr.DataArray + Phase values normalised to [0, 1). + """ phase = fit_data.sel(fit_vals="phi") - # Normalize phase to [0, 1] range (representing 0 to 2π) phase = (phase / (2 * np.pi)) % 1 return phase @@ -57,8 +78,25 @@ def log_fitted_results(fit_results: Dict[str, FitResults], log_callable=None): log_callable(log_message) -def process_raw_dataset(ds: xr.Dataset, node: QualibrationNode): - # Convert IQ data into volts +def process_raw_dataset(ds: xr.Dataset, node: QualibrationNode) -> xr.Dataset: + """Convert raw IQ data to volts when state discrimination is not used. + + Parameters: + ----------- + ds : xr.Dataset + Raw dataset from the experiment, containing either ``I_control`` / ``Q_control`` / + ``I_target`` / ``Q_target`` (raw IQ) or ``state_control`` / ``state_target`` + (state-discrimination) variables. + node : QualibrationNode + Calibration node providing ``qubit_pairs`` from its namespace (required by + ``convert_IQ_to_V``). + + Returns: + -------- + xr.Dataset + Dataset with IQ variables converted to volts, or unchanged if state discrimination + was used. + """ if hasattr(ds, "I_control"): ds = convert_IQ_to_V( ds, qubit_pairs=node.namespace["qubit_pairs"], IQ_list=["I_control", "Q_control", "I_target", "Q_target"] @@ -91,7 +129,24 @@ def fit_raw_data(ds: xr.Dataset, node: QualibrationNode) -> Tuple[xr.Dataset, Di def fit_routine(da): + """Fit Ramsey frame-rotation oscillations and extract phase corrections for one qubit pair. + + Fits a sinusoidal oscillation over the ``frame`` axis separately for the control qubit + and the target qubit. The fitted phase of each oscillation gives the residual single-qubit + phase error introduced by the CZ gate. + + Parameters: + ----------- + da : xr.Dataset + Single-pair dataset with ``state_control`` / ``state_target`` (state-discrimination) + or ``I_control`` / ``I_target`` (raw IQ) variables, and a ``frame`` dimension. + Returns: + -------- + xr.Dataset + Input dataset extended with ``fitted_control``, ``fitted_target``, + ``fitted_control_phase``, ``fitted_target_phase``, and ``success`` data variables. + """ if hasattr(da, "state_target"): data_control = "state_control" data_target = "state_target" @@ -168,22 +223,30 @@ def _extract_relevant_parameters( ds_fit: xr.Dataset, node: QualibrationNode ) -> Tuple[xr.Dataset, Dict[str, FitResults]]: """ - Extract relevant fit parameters and create FitResults for each qubit pair. + Assign xarray metadata attributes and build the FitResults dictionary. Parameters: ----------- ds_fit : xr.Dataset - Dataset containing the fit results from fit_routine. + Dataset produced by applying ``fit_routine`` per qubit pair. Must contain + ``fitted_control_phase``, ``fitted_target_phase``, and ``success`` data variables. node : QualibrationNode - The calibration node containing parameters and qubit pairs. + Calibration node providing ``qubit_pairs`` from its namespace. Returns: -------- Tuple[xr.Dataset, Dict[str, FitResults]] - Dataset with additional metadata and dictionary of FitResults for each qubit pair. + Dataset with ``long_name`` / ``units`` attrs set on key variables, and a + dictionary of ``FitResults`` keyed by qubit pair name. """ qubit_pairs = node.namespace["qubit_pairs"] + # Add metadata attributes to the dataset + if "fitted_control_phase" in ds_fit.data_vars: + ds_fit.fitted_control_phase.attrs = {"long_name": "control qubit phase correction", "units": "2π"} + if "fitted_target_phase" in ds_fit.data_vars: + ds_fit.fitted_target_phase.attrs = {"long_name": "target qubit phase correction", "units": "2π"} + # Create FitResults for each qubit pair fit_results = {} for qp in qubit_pairs: diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/plotting.py b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/plotting.py index 7088072ff2..de1eec0105 100644 --- a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/plotting.py +++ b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation/plotting.py @@ -1,52 +1,144 @@ -import matplotlib.pyplot as plt +import numpy as np import xarray as xr -from qualibrate import QualibrationNode -from quam_config import Quam +from matplotlib.axes import Axes +from matplotlib.figure import Figure +from qualibration_libs.plotting import grid_iter -from .parameters import Parameters +from calibration_utils.pair_grid import QubitPairGrid, grid_pair_names -def plot_raw_data_with_fit(ds_raw: xr.Dataset, qubit_pairs: Quam, ds_fit: xr.Dataset = None): +def _mark_fitted_peak( + ax: Axes, + frames: np.ndarray, + fit_curve: np.ndarray, + fitted_phase: float, + color: str, + channel_label: str, +) -> None: + """Mark the fitted-sine maximum.""" + if not np.any(np.isfinite(fit_curve)): + return + i_max = int(np.nanargmax(fit_curve)) + peak_frame = float(frames[i_max]) + peak_val = float(fit_curve[i_max]) + ax.axvline( + peak_frame, + color=color, + ls="--", + lw=1.5, + alpha=0.85, + zorder=3, + label=f"{channel_label} peak @ {peak_frame:.4f} (φ={fitted_phase:.4f})", + ) + ax.plot( + peak_frame, + peak_val, + "*", + ms=12, + mew=1.2, + zorder=5, + markerfacecolor="white", + markeredgecolor=color, + ) + + +def plot_raw_data_with_fit( + ds_raw: xr.Dataset, + qubit_pairs: list, + ds_fit: xr.Dataset = None, +) -> Figure: + """Plot raw Ramsey oscillations with fitted curves for every qubit pair on a chip-topology grid. + + Parameters + ---------- + ds_raw : xr.Dataset + Raw dataset containing ``state_control`` / ``state_target`` or + ``I_control`` / ``I_target`` variables with a ``frame`` dimension. + qubit_pairs : list + Qubit pair objects used for grid placement. + ds_fit : xr.Dataset, optional + Fit dataset containing ``fitted_control``, ``fitted_target``, and ``success``. + If None, only the raw data is shown. + + Returns + ------- + Figure + Matplotlib figure with one panel per qubit pair. """ - Plot the raw data with the fit for each qubit pair in a single figure. + grid_names, pair_names = grid_pair_names(qubit_pairs) + grid = QubitPairGrid(grid_names, pair_names) + + for ax, qubit in grid_iter(grid): + qp_name = qubit["qubit"] + plot_individual_data_with_fit(ax, ds_raw, qp_name, ds_fit) + + grid.fig.suptitle("CZ 1Q phase compensation") + grid.fig.tight_layout() + return grid.fig + + +def plot_individual_data_with_fit( + ax: Axes, + ds_raw: xr.Dataset, + qp_name: str, + ds_fit: xr.Dataset = None, +): + """Plot raw Ramsey oscillations and fitted curves for one qubit pair. + + Parameters + ---------- + ax : Axes + Axis to draw on. + ds_raw : xr.Dataset + Raw dataset with ``state_control`` / ``state_target`` or + ``I_control`` / ``I_target`` variables. + qp_name : str + Qubit pair name used to select data. + ds_fit : xr.Dataset, optional + Fit dataset containing ``fitted_control``, ``fitted_target``, and ``success``. + If None or fit failed, only raw data is shown. """ - n_pairs = len(qubit_pairs) - - fig, axes = plt.subplots(1, n_pairs, figsize=(5 * n_pairs, 4)) - if n_pairs == 1: - axes = [axes] - - for i, qp_name in enumerate(qubit_pairs): - ax = axes[i] - - # Select data for this qubit pair - qp_data = ds_raw.sel(qubit_pair=qp_name.name) - - # Plot raw data - if "state_control" in ds_raw.data_vars: - qp_data.state_control.plot(ax=ax, marker="o", linestyle="", color="blue", label="Control") - qp_data.state_target.plot(ax=ax, marker="o", linestyle="", color="red", label="Target") - else: - qp_data.I_control.sel(control_target="c").plot( - ax=ax, marker="o", linestyle="", color="blue", label="Control" - ) - qp_data.I_target.sel(control_target="t").plot(ax=ax, marker="o", linestyle="", color="red", label="Target") - - # Plot fitted data if available and fit was successful - if ds_fit is not None: - qp_fit_data = ds_fit.sel(qubit_pair=qp_name.name) - if qp_fit_data.success.values: - if "fitted_control" in ds_fit.data_vars: - qp_fit_data.fitted_control.plot(ax=ax, color="blue", alpha=0.5) - if "fitted_target" in ds_fit.data_vars: - qp_fit_data.fitted_target.plot(ax=ax, color="red", alpha=0.5) - if "state_control" in ds_raw.data_vars: - ax.set_ylabel("Measured State") - else: - ax.set_ylabel("Rotated I Quadrature [V]") - - ax.set_xlabel(r"x90 frame rotation [$\mathrm{rad}/2\pi$]") - ax.legend() - - plt.tight_layout() - return fig + qp_data = ds_raw.sel(qubit_pair=qp_name) + + if "state_control" in ds_raw.data_vars: + qp_data.state_control.plot(ax=ax, marker="o", linestyle="", color="blue", label="Control") + qp_data.state_target.plot(ax=ax, marker="o", linestyle="", color="red", label="Target") + ylabel = "Measured State" + else: + qp_data.I_control.sel(control_target="c").plot(ax=ax, marker="o", linestyle="", color="blue", label="Control") + qp_data.I_target.sel(control_target="t").plot(ax=ax, marker="o", linestyle="", color="red", label="Target") + ylabel = "Rotated I Quadrature (V)" + + if ds_fit is not None: + qp_fit = ds_fit.sel(qubit_pair=qp_name) + frames = qp_data.frame.values + if bool(qp_fit.success.values): + if "fitted_control" in ds_fit.data_vars: + fit_c = np.asarray(qp_fit.fitted_control.values, dtype=float) + ax.plot(frames, fit_c, color="blue", alpha=0.5) + if "fitted_control_phase" in ds_fit.data_vars: + _mark_fitted_peak( + ax, + frames, + fit_c, + float(qp_fit.fitted_control_phase.values), + "blue", + "Control", + ) + if "fitted_target" in ds_fit.data_vars: + fit_t = np.asarray(qp_fit.fitted_target.values, dtype=float) + ax.plot(frames, fit_t, color="red", alpha=0.5) + if "fitted_target_phase" in ds_fit.data_vars: + _mark_fitted_peak( + ax, + frames, + fit_t, + float(qp_fit.fitted_target_phase.values), + "red", + "Target", + ) + + ax.set_title(qp_name) + ax.set_xlabel(r"Virtual-Z frame [$2\pi$]") + ax.set_ylabel(ylabel) + ax.legend(fontsize=8) diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/__init__.py b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/__init__.py new file mode 100644 index 0000000000..0ede3d015c --- /dev/null +++ b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/__init__.py @@ -0,0 +1,14 @@ +"""CZ phase compensation with error amplification calibration utilities.""" + +from .analysis import FitResults, fit_raw_data, log_fitted_results, process_raw_dataset +from .parameters import Parameters +from .plotting import plot_raw_data_with_fit + +__all__ = [ + "Parameters", + "process_raw_dataset", + "fit_raw_data", + "log_fitted_results", + "FitResults", + "plot_raw_data_with_fit", +] diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/analysis.py b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/analysis.py new file mode 100644 index 0000000000..266e94b120 --- /dev/null +++ b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/analysis.py @@ -0,0 +1,274 @@ +"""Analysis functions for CZ phase compensation with error amplification calibration. + +For each qubit (control / target), the signal is averaged over the number of CZ +operations and fit with a sinc model to locate the optimal compensation frame: + + f(frame) = B + A * sinc(w * (frame - frame_0) / pi) + +When state discrimination is used the peak should approach 1 at the optimal +compensation point. A parabolic-refinement fallback is used if the sinc fit fails. +""" + +import logging +from dataclasses import dataclass, field +from typing import Dict, Tuple + +import numpy as np +import xarray as xr +from qualibrate import QualibrationNode +from qualibration_libs.data import convert_IQ_to_V +from scipy.optimize import curve_fit + + +@dataclass +class FitResults: + """Stores fit results for a single qubit pair.""" + + fitted_control_phase: float + fitted_target_phase: float + control_mean_at_peak: float + target_mean_at_peak: float + success: bool + control_fit_method: str = "sinc" + target_fit_method: str = "sinc" + control_sinc_params: Dict[str, float] = field(default_factory=dict) + target_sinc_params: Dict[str, float] = field(default_factory=dict) + + +def _sinc_model(x: np.ndarray, A: float, B: float, x_0: float, w: float) -> np.ndarray: + """Sinc model used to localise the central peak of the averaged signal.""" + return B + A * np.sinc(w * (x - x_0) / np.pi) + + +def _parabolic_refine(y: np.ndarray, i: int) -> float: + """Three-point parabolic refinement around an extremum index, returning fractional index.""" + if i <= 0 or i >= len(y) - 1: + return float(i) + y1, y2, y3 = y[i - 1], y[i], y[i + 1] + denom = y1 - 2.0 * y2 + y3 + if denom == 0: + return float(i) + delta = 0.5 * (y1 - y3) / denom + return float(i) + float(np.clip(delta, -0.5, 0.5)) + + +def _argmax_with_refine(x_values: np.ndarray, y: np.ndarray) -> float: + """Argmax of ``y`` over ``x_values`` with 3-point parabolic refinement.""" + i = int(np.argmax(y)) + i_star = _parabolic_refine(y, i) + return float(np.interp(i_star, np.arange(len(x_values)), x_values)) + + +def _estimate_fwhm_around(x_values: np.ndarray, y: np.ndarray, i_max: int) -> float: + """Estimate the full-width at half-maximum of the central peak around ``i_max``.""" + finite = y[np.isfinite(y)] + if finite.size == 0: + return float("nan") + y_max = float(y[i_max]) + y_min = float(np.min(finite)) + half = y_max - (y_max - y_min) / 2.0 + left = i_max + while left > 0 and y[left] > half: + left -= 1 + right = i_max + while right < len(y) - 1 and y[right] > half: + right += 1 + fwhm = float(x_values[right] - x_values[left]) + if fwhm > 0: + return fwhm + return float(x_values[-1] - x_values[0]) / 4.0 + + +def _fit_sinc_peak(x_values: np.ndarray, y: np.ndarray) -> Tuple[float, float, bool, str, Dict[str, float], np.ndarray]: + """Fit a sinc model to a 1D curve and return the peak position.""" + if len(x_values) != len(y) or len(x_values) == 0: + return float("nan"), float("nan"), False, "none", {}, np.full_like(x_values, np.nan, dtype=float) + + y_avg = np.asarray(y, dtype=float) + if not np.any(np.isfinite(y_avg)): + return float("nan"), float("nan"), False, "none", {}, np.full_like(x_values, np.nan, dtype=float) + + y_finite = np.where(np.isfinite(y_avg), y_avg, -np.inf) + i_max = int(np.argmax(y_finite)) + + A_init = float(np.nanmax(y_avg) - np.nanmedian(y_avg)) + if not np.isfinite(A_init) or A_init <= 0: + A_init = 0.5 + B_init = float(np.nanmedian(y_avg)) + if not np.isfinite(B_init): + B_init = 0.5 + x_seed_init = float(x_values[i_max]) + fwhm = _estimate_fwhm_around(x_values, y_avg, i_max) + if np.isfinite(fwhm) and fwhm > 0: + w_init = 3.79 / fwhm + else: + w_init = 3.79 / max(float(x_values[-1] - x_values[0]) / 4.0, 1e-9) + + x_min, x_max = float(np.min(x_values)), float(np.max(x_values)) + + try: + popt, _ = curve_fit( + _sinc_model, + x_values, + y_avg, + p0=[A_init, B_init, x_seed_init, w_init], + bounds=( + [0.0, -0.5, x_min, w_init / 50.0], + [2.0, 1.5, x_max, w_init * 50.0], + ), + maxfev=10000, + ) + A_fit, B_fit, x_0_fit, w_fit = map(float, popt) + if not x_min <= x_0_fit <= x_max: + raise RuntimeError(f"Fitted frame = {x_0_fit} outside swept window.") + fit_curve = _sinc_model(x_values, A_fit, B_fit, x_0_fit, w_fit) + return ( + x_0_fit, + B_fit + A_fit, + True, + "sinc", + {"A": A_fit, "B": B_fit, "frame_0": x_0_fit, "w": w_fit}, + fit_curve, + ) + except Exception: # pylint: disable=broad-except + x_refined = _argmax_with_refine(x_values, y_finite) + y_at_peak = float(np.interp(x_refined, x_values, y_avg)) + return ( + x_refined, + y_at_peak, + bool(np.isfinite(x_refined)), + "parabolic", + {}, + np.full_like(x_values, np.nan, dtype=float), + ) + + +def _find_phase_from_sinc_fit( + signal: xr.DataArray, +) -> Tuple[float, float, bool, str, Dict[str, float], np.ndarray, np.ndarray]: + """Average over operations and fit a sinc model to locate the optimal frame.""" + mean_vs_frame = signal.mean(dim="number_of_operations") + frames = mean_vs_frame.frame.values + values = mean_vs_frame.values + return _fit_sinc_peak(frames, values) + (values,) + + +def log_fitted_results(fit_results: Dict[str, FitResults], log_callable=None): + """Log fit results for each qubit pair.""" + if log_callable is None: + log_callable = logging.getLogger(__name__).info + + for qp_name, fit_result in fit_results.items(): + status = "SUCCESS" if fit_result.success else "FAIL" + log_callable( + f"{qp_name} [{status}] | " + f"control_phase={fit_result.fitted_control_phase:.6f} ({fit_result.control_fit_method}), " + f"target_phase={fit_result.fitted_target_phase:.6f} ({fit_result.target_fit_method}), " + f"control_peak_mean={fit_result.control_mean_at_peak:.4f}, " + f"target_peak_mean={fit_result.target_mean_at_peak:.4f}" + ) + + +def process_raw_dataset(ds: xr.Dataset, node: QualibrationNode): + """Convert IQ data to volts when state discrimination is disabled.""" + if "I_control" in ds.data_vars: + ds = convert_IQ_to_V( + ds, + qubit_pairs=node.namespace["qubit_pairs"], + IQ_list=["I_control", "Q_control", "I_target", "Q_target"], + ) + return ds + + +def _get_signals(qp_data: xr.Dataset): + """Extract control and target signal arrays from a single qubit-pair slice.""" + if "state_control" in qp_data.data_vars and "state_target" in qp_data.data_vars: + return qp_data["state_control"], qp_data["state_target"] + + signal_control = qp_data["I_control"] + signal_target = qp_data["I_target"] + if "control_target" in signal_control.dims: + signal_control = signal_control.sel(control_target="c") + if "control_target" in signal_target.dims: + signal_target = signal_target.sel(control_target="t") + return signal_control, signal_target + + +def fit_raw_data(ds: xr.Dataset, node: QualibrationNode) -> Tuple[xr.Dataset, Dict[str, FitResults]]: + """Find optimal phase compensation by fitting a sinc model to the averaged signal.""" + frames = ds.frame.values + fit_datasets = [] + fit_results: Dict[str, FitResults] = {} + + for qp in node.namespace["qubit_pairs"]: + qp_name = qp.name + qp_data = ds.sel(qubit_pair=qp_name) + + try: + signal_control, signal_target = _get_signals(qp_data) + + ( + control_phase, + control_peak_mean, + control_success, + control_method, + control_params, + control_sinc_fit, + control_mean_curve, + ) = _find_phase_from_sinc_fit(signal_control) + ( + target_phase, + target_peak_mean, + target_success, + target_method, + target_params, + target_sinc_fit, + target_mean_curve, + ) = _find_phase_from_sinc_fit(signal_target) + + success = control_success and target_success + + fit_ds = xr.Dataset( + { + "control_mean_vs_frame": xr.DataArray( + control_mean_curve, dims=("frame",), coords={"frame": frames} + ), + "target_mean_vs_frame": xr.DataArray(target_mean_curve, dims=("frame",), coords={"frame": frames}), + "control_sinc_fit": xr.DataArray(control_sinc_fit, dims=("frame",), coords={"frame": frames}), + "target_sinc_fit": xr.DataArray(target_sinc_fit, dims=("frame",), coords={"frame": frames}), + "fitted_control_phase": xr.DataArray(control_phase), + "fitted_target_phase": xr.DataArray(target_phase), + "control_mean_at_peak": xr.DataArray(control_peak_mean), + "target_mean_at_peak": xr.DataArray(target_peak_mean), + "control_fit_method": xr.DataArray(control_method), + "target_fit_method": xr.DataArray(target_method), + "success": xr.DataArray(success), + } + ) + fit_results[qp_name] = FitResults( + fitted_control_phase=control_phase, + fitted_target_phase=target_phase, + control_mean_at_peak=control_peak_mean, + target_mean_at_peak=target_peak_mean, + success=success, + control_fit_method=control_method, + target_fit_method=target_method, + control_sinc_params=control_params, + target_sinc_params=target_params, + ) + except Exception: + fit_ds = xr.Dataset({"success": xr.DataArray(False)}) + fit_results[qp_name] = FitResults( + fitted_control_phase=np.nan, + fitted_target_phase=np.nan, + control_mean_at_peak=np.nan, + target_mean_at_peak=np.nan, + success=False, + control_fit_method="none", + target_fit_method="none", + ) + + fit_datasets.append(fit_ds.expand_dims(qubit_pair=[qp_name])) + + ds_fit = xr.concat(fit_datasets, dim="qubit_pair") + return ds_fit, fit_results diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/parameters.py b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/parameters.py new file mode 100644 index 0000000000..2829531ab7 --- /dev/null +++ b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/parameters.py @@ -0,0 +1,37 @@ +"""Parameters for CZ phase compensation with error amplification calibration.""" + +# pylint: disable=too-few-public-methods + +from typing import ClassVar, Literal + +from qualibrate import NodeParameters +from qualibrate.core.parameters import RunnableParameters +from qualibration_libs.parameters import CommonNodeParameters, QubitPairExperimentNodeParameters + + +class NodeSpecificParameters(RunnableParameters): + """Node-specific parameters for CZ phase compensation with error amplification.""" + + num_shots: int = 100 + """Number of shots to perform. Default is 100.""" + frame_range: float = 0.1 + """Range of frame rotation to sweep. Default is 0.1.""" + num_frames: int = 17 + """Number of phase frames to sweep. Default is 17.""" + number_of_operations: int = 8 + """Number of CZ operations applied per amplification step. Default is 8.""" + operation: Literal["cz_flattop", "cz_unipolar", "cz_bipolar", "cz_flattop_erf", "cz_SNZ"] = "cz_unipolar" + """Type of CZ operation to perform. Options are 'cz_flattop', 'cz_unipolar', 'cz_bipolar', 'cz_flattop_erf', or 'cz_SNZ'. Default is 'cz_unipolar'.""" + use_state_discrimination: bool = True + """Whether to use state discrimination for readout. Default is True.""" + + +class Parameters( + NodeParameters, + CommonNodeParameters, + NodeSpecificParameters, + QubitPairExperimentNodeParameters, +): + """Combined parameters class for CZ phase compensation with error amplification.""" + + targets_name: ClassVar[str] = "qubit_pairs" diff --git a/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py new file mode 100644 index 0000000000..561edb69ae --- /dev/null +++ b/qualibration_graphs/superconducting/calibration_utils/cz_phase_compensation_error_amp/plotting.py @@ -0,0 +1,205 @@ +"""Plotting utilities for CZ phase compensation with error amplification.""" + +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 + +FRAME_XLABEL = r"Virtual-Z frame [$2\pi$]" + +_PANEL_STYLES = { + "control": { + "data": {"color": "#3b82f6", "markeredgecolor": "#1e3a8a"}, + "fit": {"color": "#ea580c"}, + "optimum": {"color": "#15803d"}, + }, + "target": { + "data": {"color": "#ef4444", "markeredgecolor": "#991b1b"}, + "fit": {"color": "#7c3aed"}, + "optimum": {"color": "#15803d"}, + }, +} + + +def _signal_labels(ds_raw: xr.Dataset) -> tuple[str, str, str]: + """Return y-axis label, mean-curve legend, and peak legend for state vs IQ readout.""" + if "state_control" in ds_raw.data_vars: + return "Measured State", r"$\langle state \rangle_N$", r"peak $\langle state \rangle_N$" + return "Rotated I Quadrature (V)", r"$\langle I \rangle_N$", r"peak $\langle I \rangle_N$" + + +def _split_axis_vertically(ax: Axes, hspace: float = 0.35) -> tuple[Axes, Axes]: + """Replace ``ax`` with stacked top (control) and bottom (target) subplots.""" + sub_gs = ax.get_subplotspec().subgridspec(2, 1, hspace=hspace) + fig = ax.figure + ax.remove() + ax_control = fig.add_subplot(sub_gs[0]) + ax_target = fig.add_subplot(sub_gs[1], sharex=ax_control) + return ax_control, ax_target + + +def plot_raw_data_with_fit( + ds_raw: xr.Dataset, + qubit_pairs: list, + ds_fit: xr.Dataset = None, +) -> Figure: + """Plot N-averaged signal vs frame for control and target on a chip-topology grid. + + Each qubit-pair cell is split into two stacked panels (control on top, target below), + with sinc fits and fitted phase markers when available. + + Parameters + ---------- + ds_raw : xr.Dataset + Raw dataset containing ``state_control`` / ``state_target`` or + ``I_control`` / ``I_target`` variables with ``frame`` and + ``number_of_operations`` dimensions. + qubit_pairs : list + Qubit pair objects used for grid placement. + ds_fit : xr.Dataset, optional + Fit dataset containing ``control_mean_vs_frame``, ``target_mean_vs_frame``, + ``control_sinc_fit``, ``target_sinc_fit``, and ``success``. + + Returns + ------- + Figure + Matplotlib figure with two panels per qubit pair on the chip grid. + """ + grid_names, pair_names = grid_pair_names(qubit_pairs) + grid = QubitPairGrid(grid_names, pair_names, size=4) + width, height = grid.fig.get_size_inches() + grid.fig.set_size_inches(width, height * 2) + + for ax, qubit in grid_iter(grid): + qp_name = qubit["qubit"] + ax_control, ax_target = _split_axis_vertically(ax) + plot_individual_channel_with_fit( + ax_control, + ds_raw, + qp_name, + ds_fit, + channel="control", + title=f"{qp_name} — control", + show_xlabel=False, + ) + plot_individual_channel_with_fit( + ax_target, + ds_raw, + qp_name, + ds_fit, + channel="target", + title=f"{qp_name} — target", + show_xlabel=True, + ) + + grid.fig.suptitle("CZ phase compensation - error amplification (sinc fit)", y=0.98) + grid.fig.tight_layout(rect=(0, 0, 1, 0.96)) + return grid.fig + + +def plot_individual_channel_with_fit( + ax: Axes, + ds_raw: xr.Dataset, + qp_name: str, + ds_fit: xr.Dataset | None, + channel: str, + title: str | None = None, + show_xlabel: bool = True, +): + """Plot one qubit's N-averaged signal, sinc fit, and fitted phase for a single pair.""" + style = _PANEL_STYLES[channel] + ylabel, mean_label, peak_label = _signal_labels(ds_raw) + panel_title = title if title is not None else f"{qp_name} - {channel}" + + if ds_fit is None: + ax.set_title(panel_title) + if show_xlabel: + ax.set_xlabel(FRAME_XLABEL) + else: + ax.tick_params(labelbottom=False) + ax.set_ylabel(ylabel) + ax.text(0.5, 0.5, "no fit", ha="center", va="center", transform=ax.transAxes) + return + + qp_fit = ds_fit.sel(qubit_pair=qp_name) + if not bool(qp_fit.success.values): + ax.set_title(panel_title) + if show_xlabel: + ax.set_xlabel(FRAME_XLABEL) + else: + ax.tick_params(labelbottom=False) + ax.set_ylabel(ylabel) + ax.text(0.5, 0.5, "fit failed", ha="center", va="center", transform=ax.transAxes) + return + + frames = ds_raw.sel(qubit_pair=qp_name).frame.values + mean_var = f"{channel}_mean_vs_frame" + sinc_var = f"{channel}_sinc_fit" + phase_var = f"fitted_{channel}_phase" + peak_mean_var = f"{channel}_mean_at_peak" + method_var = f"{channel}_fit_method" + + if mean_var in qp_fit.data_vars: + mean_curve = qp_fit[mean_var].values + fitted_phase = float(qp_fit[phase_var].values) + peak_mean = float(qp_fit[peak_mean_var].values) + fit_method = str(qp_fit[method_var].values) + + ax.plot( + frames, + mean_curve, + linestyle="none", + marker="o", + ms=5, + mew=0.8, + alpha=0.9, + zorder=2, + label=mean_label, + **style["data"], + ) + if sinc_var in qp_fit.data_vars: + fit_vals = qp_fit[sinc_var].values + if np.any(np.isfinite(fit_vals)): + ax.plot( + frames, + fit_vals, + "-", + lw=2.5, + zorder=4, + label="sinc fit", + **style["fit"], + ) + if np.isfinite(fitted_phase): + ax.axvline( + fitted_phase, + color=style["optimum"]["color"], + ls="--", + lw=1.8, + alpha=0.95, + zorder=3, + label=f"phase = {fitted_phase:.4f} ({fit_method})", + ) + if np.isfinite(peak_mean) and np.isfinite(fitted_phase): + ax.plot( + fitted_phase, + peak_mean, + "*", + ms=14, + mew=1.2, + zorder=5, + markerfacecolor="white", + markeredgecolor=style["fit"]["color"], + label=f"{peak_label} = {peak_mean:.4f}", + ) + ax.legend(loc="best", fontsize=9) + + ax.set_title(panel_title) + if show_xlabel: + ax.set_xlabel(FRAME_XLABEL) + else: + ax.tick_params(labelbottom=False) + ax.set_ylabel(ylabel) + ax.grid(alpha=0.25, color="#94a3b8") diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34_cz_phase_compensation.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34a_cz_phase_compensation.py similarity index 97% rename from qualibration_graphs/superconducting/calibrations/CZ_calibrations/34_cz_phase_compensation.py rename to qualibration_graphs/superconducting/calibrations/CZ_calibrations/34a_cz_phase_compensation.py index 094c28cba6..f08b0dce76 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34_cz_phase_compensation.py +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34a_cz_phase_compensation.py @@ -46,7 +46,7 @@ # Be sure to include [Parameters, Quam] so the node has proper type hinting node = QualibrationNode[Parameters, Quam]( - name="34_cz_phase_compensation", # Name should be unique + name="34a_cz_phase_compensation", # Name should be unique description=description, # Describe what the node is doing, which is also reflected in the QUAlibrate GUI parameters=Parameters(), # Node parameters defined under quam_experiment/experiments/node_name machine=Quam.load(), @@ -82,20 +82,20 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): # Register the sweep axes to be added to the dataset when fetching data node.namespace["sweep_axes"] = { "qubit_pair": xr.DataArray(qubit_pairs.get_names()), - "frame": xr.DataArray(frames, attrs={"long_name": "frame rotation", "units": "2π"}), + "frame": xr.DataArray(frames, attrs={"long_name": "virtual-Z frame", "units": "2π"}), } # The QUA program stored in the node namespace to be transfer to the simulation and execution run_actions with program() as node.namespace["qua_program"]: frame = declare(fixed) n = declare(int) - n_st = declare_stream() + n_st = declare_output_stream() I_c, I_c_st, Q_c, Q_c_st, n, n_st = node.machine.declare_qua_variables() I_t, I_t_st, Q_t, Q_t_st, _, _ = node.machine.declare_qua_variables() state_c = [declare(int) for _ in range(num_qubit_pairs)] state_t = [declare(int) for _ in range(num_qubit_pairs)] - state_c_st = [declare_stream() for _ in range(num_qubit_pairs)] - state_t_st = [declare_stream() for _ in range(num_qubit_pairs)] + state_c_st = [declare_output_stream() for _ in range(num_qubit_pairs)] + state_t_st = [declare_output_stream() for _ in range(num_qubit_pairs)] extra_phase_c = declare(fixed) extra_phase_t = declare(fixed) @@ -153,7 +153,7 @@ def create_qua_program(node: QualibrationNode[Parameters, Quam]): qp.qubit_target.resonator.measure("readout", qua_vars=(I_t[ii], Q_t[ii])) save(I_t[ii], I_t_st[ii]) save(Q_t[ii], Q_t_st[ii]) - + align() with stream_processing(): n_st.save("n") for ii in range(num_qubit_pairs): diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34b_cz_phase_compensation_error_amp.py b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34b_cz_phase_compensation_error_amp.py new file mode 100644 index 0000000000..fa6e7378a9 --- /dev/null +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/34b_cz_phase_compensation_error_amp.py @@ -0,0 +1,249 @@ +# %% {Imports} +from dataclasses import asdict + +import matplotlib.pyplot as plt +import numpy as np +import xarray as xr +from calibration_utils.cz_phase_compensation_error_amp import ( + Parameters, + fit_raw_data, + log_fitted_results, + plot_raw_data_with_fit, + process_raw_dataset, +) +from qm.qua import * +from qualang_tools.loops import from_array +from qualang_tools.multi_user import qm_session +from qualang_tools.results import progress_counter +from qualang_tools.units import unit +from qualibrate import QualibrationNode +from qualibration_libs.data import XarrayDataFetcher +from qualibration_libs.parameters import get_qubit_pairs +from qualibration_libs.runtime import simulate_and_plot +from quam_config import Quam + +# %% {Description} +description = """ +CZ PHASE COMPENSATION WITH ERROR AMPLIFICATION + +This node calibrates residual single-qubit (local Z) phase shifts induced by the CZ macro. +Compared to **34a_cz_phase_compensation**, it repeats the CZ operation a variable +number of times to amplify phase errors before tomography. + +For each selected qubit pair: +1. Prepare either control or target qubit in a Ramsey-like sequence. +2. Apply a train of CZ macros (1..N repetitions). +3. Sweep a final virtual frame rotation and fit the Ramsey oscillation phase per repetition count. +4. Average the signal over repetitions and fit a sinc model to locate the optimal compensation frame. + +State update: + - qp.macros[operation].phase_shift_control + - qp.macros[operation].phase_shift_target +""" + +node = QualibrationNode[Parameters, Quam]( + name="34b_cz_phase_compensation_error_amp", + description=description, + parameters=Parameters(), + machine=Quam.load(), +) + + +@node.run_action(skip_if=node.modes.external) +def custom_param(node: QualibrationNode[Parameters, Quam]): + """Allow local debug parameter overrides when running directly from IDE.""" + pass + + +# %% {Create_QUA_program} +@node.run_action(skip_if=node.parameters.load_data_id is not None) +def create_qua_program(node: QualibrationNode[Parameters, Quam]): # pylint: disable=too-many-statements + """Create sweep axes and generate the QUA program.""" + unit(coerce_to_integer=True) + + node.namespace["qubit_pairs"] = qubit_pairs = get_qubit_pairs(node) + num_qubit_pairs = len(qubit_pairs) + + n_avg = node.parameters.num_shots + frames = np.linspace(-node.parameters.frame_range / 2, node.parameters.frame_range / 2, node.parameters.num_frames) + num_operations = node.parameters.number_of_operations + cz_operation = node.parameters.operation + + node.namespace["sweep_axes"] = { + "qubit_pair": xr.DataArray(qubit_pairs.get_names()), + "number_of_operations": xr.DataArray( + np.arange(1, num_operations + 1), + attrs={"long_name": "number of CZ operations"}, + ), + "frame": xr.DataArray(frames, attrs={"long_name": "virtual-Z frame", "units": "2π"}), + } + + with program() as node.namespace["qua_program"]: + frame = declare(fixed) + n = declare(int) + n_op = declare(int) + count = declare(int) + n_st = declare_output_stream() + I_c, I_c_st, Q_c, Q_c_st, n, n_st = node.machine.declare_qua_variables() + I_t, I_t_st, Q_t, Q_t_st, _, _ = node.machine.declare_qua_variables() + state_c = [declare(int) for _ in range(num_qubit_pairs)] + state_t = [declare(int) for _ in range(num_qubit_pairs)] + state_c_st = [declare_output_stream() for _ in range(num_qubit_pairs)] + state_t_st = [declare_output_stream() for _ in range(num_qubit_pairs)] + extra_phase_c = declare(fixed) + extra_phase_t = declare(fixed) + + for multiplexed_qubit_pairs in qubit_pairs.batch(): + for qp in multiplexed_qubit_pairs.values(): + node.machine.initialize_qpu(target=qp.qubit_control) + node.machine.initialize_qpu(target=qp.qubit_target) + align() + + with for_(n, 0, n < n_avg, n + 1): + save(n, n_st) + with for_(n_op, 1, n_op <= num_operations, n_op + 1): + with for_(*from_array(frame, frames)): + for ii, qp in multiplexed_qubit_pairs.items(): + assign(extra_phase_c, qp.macros[cz_operation].phase_shift_control) + assign(extra_phase_t, qp.macros[cz_operation].phase_shift_target) + for qubit, state_q, state_st in [ + (qp.qubit_control, state_c[ii], state_c_st[ii]), + (qp.qubit_target, state_t[ii], state_t_st[ii]), + ]: + qp.qubit_control.reset( + reset_type=node.parameters.reset_type, simulate=node.parameters.simulate + ) + qp.qubit_target.reset( + reset_type=node.parameters.reset_type, simulate=node.parameters.simulate + ) + qp.align() + + qubit.xy.play("x90") + qubit.align() + + with for_(count, 0, count < n_op, count + 1): + if qubit is qp.qubit_control: + qp.macros[cz_operation].apply(phase_shift_control=extra_phase_c + frame) + elif qubit is qp.qubit_target: + qp.macros[cz_operation].apply(phase_shift_target=extra_phase_t + frame) + + qubit.xy.play("x90") + qp.align() + + if node.parameters.use_state_discrimination: + qubit.readout_state(state_q) + save(state_q, state_st) + else: + if qubit is qp.qubit_control: + qp.qubit_control.resonator.measure("readout", qua_vars=(I_c[ii], Q_c[ii])) + save(I_c[ii], I_c_st[ii]) + save(Q_c[ii], Q_c_st[ii]) + elif qubit is qp.qubit_target: + qp.qubit_target.resonator.measure("readout", qua_vars=(I_t[ii], Q_t[ii])) + save(I_t[ii], I_t_st[ii]) + save(Q_t[ii], Q_t_st[ii]) + align() + + align() + with stream_processing(): + n_st.save("n") + for ii in range(num_qubit_pairs): + if node.parameters.use_state_discrimination: + state_c_st[ii].buffer(len(frames)).buffer(num_operations).average().save(f"state_control{ii + 1}") + state_t_st[ii].buffer(len(frames)).buffer(num_operations).average().save(f"state_target{ii + 1}") + else: + I_c_st[ii].buffer(len(frames)).buffer(num_operations).average().save(f"I_control{ii + 1}") + Q_c_st[ii].buffer(len(frames)).buffer(num_operations).average().save(f"Q_control{ii + 1}") + I_t_st[ii].buffer(len(frames)).buffer(num_operations).average().save(f"I_target{ii + 1}") + Q_t_st[ii].buffer(len(frames)).buffer(num_operations).average().save(f"Q_target{ii + 1}") + + +# %% {Simulate} +@node.run_action(skip_if=node.parameters.load_data_id is not None or not node.parameters.simulate) +def simulate_qua_program(node: QualibrationNode[Parameters, Quam]): + """Connect to QOP and simulate the QUA program.""" + qmm = node.machine.connect() + config = node.machine.generate_config() + samples, fig, wf_report = simulate_and_plot(qmm, config, node.namespace["qua_program"], node.parameters) + node.results["simulation"] = {"figure": fig, "wf_report": wf_report, "samples": samples} + + +# %% {Execute} +@node.run_action(skip_if=node.parameters.load_data_id is not None or node.parameters.simulate) +def execute_qua_program(node: QualibrationNode[Parameters, Quam]): + """Execute the QUA program and fetch raw data.""" + qmm = node.machine.connect() + config = node.machine.generate_config() + with qm_session(qmm, config, timeout=node.parameters.timeout) as qm: + node.namespace["job"] = job = qm.execute(node.namespace["qua_program"]) + data_fetcher = XarrayDataFetcher(job, node.namespace["sweep_axes"]) + for dataset in data_fetcher: + progress_counter( + data_fetcher.get("n", 0), + node.parameters.num_shots, + start_time=data_fetcher.t_start, + ) + node.log(job.execution_report()) + node.results["ds_raw"] = dataset + + +# %% {Load_data} +@node.run_action(skip_if=node.parameters.load_data_id is None) +def load_data(node: QualibrationNode[Parameters, Quam]): + """Load a previously acquired dataset.""" + load_data_id = node.parameters.load_data_id + node.load_from_id(node.parameters.load_data_id) + node.parameters.load_data_id = load_data_id + node.namespace["qubit_pairs"] = get_qubit_pairs(node) + + +# %% {Analyse_data} +@node.run_action(skip_if=node.parameters.simulate) +def analyse_data(node: QualibrationNode[Parameters, Quam]): + """Process raw data, run fits, and set outcomes.""" + node.results["ds_raw"] = process_raw_dataset(node.results["ds_raw"], node) + node.results["ds_fit"], fit_results = fit_raw_data(node.results["ds_raw"], node) + node.results["fit_results"] = {k: asdict(v) for k, v in fit_results.items()} + + log_fitted_results(fit_results, log_callable=node.log) + node.outcomes = { + qp_name: ("successful" if fit_result.success else "failed") for qp_name, fit_result in fit_results.items() + } + + +# %% {Plot_data} +@node.run_action(skip_if=node.parameters.simulate) +def plot_data(node: QualibrationNode[Parameters, Quam]): + """Plot phase-vs-operations fit for each qubit pair.""" + fig_raw_fit = plot_raw_data_with_fit(node.results["ds_raw"], node.namespace["qubit_pairs"], node.results["ds_fit"]) + plt.show() + node.results["figures"] = {"phase_vs_operations": fig_raw_fit} + + +# %% {Update_state} +@node.run_action(skip_if=node.parameters.simulate) +def update_state(node: QualibrationNode[Parameters, Quam]): + """Update local-Z compensation with the fitted phase from the sinc fit.""" + cz_operation = node.parameters.operation + with node.record_state_updates(): + for qp in node.namespace["qubit_pairs"]: + if node.outcomes[qp.name] == "failed": + continue + + old_phase_control = qp.macros[cz_operation].phase_shift_control + old_phase_target = qp.macros[cz_operation].phase_shift_target + fitted_control_phase = float(node.results["ds_fit"].sel(qubit_pair=qp.name).fitted_control_phase.values) + fitted_target_phase = float(node.results["ds_fit"].sel(qubit_pair=qp.name).fitted_target_phase.values) + + qp.macros[cz_operation].phase_shift_control = (old_phase_control + fitted_control_phase) % 1 + qp.macros[cz_operation].phase_shift_target = (old_phase_target + fitted_target_phase) % 1 + + +# %% {Save_results} +@node.run_action() +def save_results(node: QualibrationNode[Parameters, Quam]): + """Save node results and state updates.""" + node.save() + + +# %% 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 ae15d5f5b9..6eabd400ed 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/99_CZ_calibration_graph.py +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/99_CZ_calibration_graph.py @@ -24,7 +24,7 @@ class Parameters(GraphParameters): "conditional_phase_error_amp": library.nodes["32b_cz_conditional_phase_error_amp"].copy( name="conditional_phase_error_amp" ), - "phase_compensation": library.nodes["34_cz_phase_compensation"].copy(name="phase_compensation"), + "phase_compensation": library.nodes["34a_cz_phase_compensation"].copy(name="phase_compensation"), }, connectivity=[ ("chevron", "conditional_phase"), diff --git a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/README.md b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/README.md index b3769ba5be..22e2eb9fbd 100644 --- a/qualibration_graphs/superconducting/calibrations/CZ_calibrations/README.md +++ b/qualibration_graphs/superconducting/calibrations/CZ_calibrations/README.md @@ -107,35 +107,36 @@ Coupler bias is **not** swept in the CZ calibration chain. After distortion cali ```text 17 → 18 → 31 → 32a → 32b - └→ 34 + └→ 34a → 34b (optional) ``` | Order | Node | Summary | | ----- | ------- | --------------------------------------------------------------------------------------------- | | 1 | **31** | Chevron: amplitude × duration → coarse CZ duration/amplitude | -| 2 | **32a** | Fine amplitude scan → π/2 conditional-phase point | +| 2 | **32a** | Fine amplitude scan → π conditional-phase point | | 3 | **32b** | CZ pulse train → error-amplified amplitude fine tune | -| 4 | **34** | Virtual-Z phase compensation (can run after **32a**; **99** runs it in parallel with **32b**) | +| 4 | **34a** | Virtual-Z phase compensation (can run after **32a**; **99** runs it in parallel with **32b**) | +| 4′ | **34b** | Error-amplified virtual-Z fine tune (optional follow-up to **34a**) | --- ## Tunable-coupler workflow -Node **30** finds coupler **decouple (idle)** and **interaction** flux biases plus moving-qubit detuning in one 2D map (CZ or iSWAP). That replaces the coarse amplitude/duration role of chevron (**31**), so the usual path is **30 → 32a → 32b → 33a → 34** without **31**. Pulse duration and macro amplitudes come from **30** and the gate macro already in QUAM. Use **33b** (PALEA) instead of **33a** for improved leakage isolation. +Node **30** finds coupler **decouple (idle)** and **interaction** flux biases plus moving-qubit detuning in one 2D map (CZ or iSWAP). That replaces the coarse amplitude/duration role of chevron (**31**), so the usual path is **30 → 32a → 32b → 33a → 34a** without **31**. Pulse duration and macro amplitudes come from **30** and the gate macro already in QUAM. Use **33b** (PALEA) instead of **33a** for improved leakage isolation. ```text -17 → 18 → 30 → 32a → 32b → 33a/33b - └→ 34 +17 → 18 → 30 → 32a → 32b → 33a/33b → 34a → 34b (optional) ``` | Order | Node | Summary | | ----- | ------- | ----------------------------------------------------------------------------------- | | 1 | **30** | 2D coupler + moving-qubit flux → `decouple_offset`, `detuning`, `macros[operation]` | -| 2 | **32a** | Fine amplitude → π/2 conditional-phase point | +| 2 | **32a** | Fine amplitude → π conditional-phase point | | 3 | **32b** | CZ pulse train → error-amplified amplitude fine tune | | 4 | **33a** | Coupler amplitude via \|11⟩ leakage amplification (standard) | | 4′ | **33b** | Coupler amplitude via PALEA leakage amplification (alternative to **33a**) | -| 5 | **34** | Virtual-Z phase compensation | +| 5 | **34a** | Virtual-Z phase compensation | +| 5′ | **34b** | Error-amplified virtual-Z fine tune (optional follow-up to **34a**) | **31** remains available if you still want an explicit amplitude–duration Chevron after **30** (e.g. new macro shape or duration not set in state). @@ -173,7 +174,7 @@ Prepare |11⟩, sweep CZ flux pulse amplitude and duration on the moving qubit. [(32a_cz_conditional_phase)](./32a_cz_conditional_phase.py) -Use gate duration from **31** (fixed coupler) or from the macro in state after **30** (tunable coupler). Sweep amplitude to the **π/2 conditional-phase** point. Tomography with rotating x90 on target. +Use gate duration from **31** (fixed coupler) or from the macro in state after **30** (tunable coupler). Sweep amplitude to the **π conditional-phase** point (phase difference = 0.5 in normalised units). Frame tomography uses rotating x90 on the stationary qubit.
@@ -217,7 +218,9 @@ Same coupler-amplitude objective as **33a**, with a dynamical-decoupling layer a
## Phase compensation — both workflows
-[(34_cz_phase_compensation)](./34_cz_phase_compensation.py)
+### Standard protocol
+
+[(34a_cz_phase_compensation)](./34a_cz_phase_compensation.py)
|++⟩, apply CZ, reconstruct per-qubit phase; update virtual Z in state.
@@ -227,22 +230,31 @@ Same coupler-amplitude objective as **33a**, with a dynamical-decoupling layer a
**Goal:** Compensate single-qubit phases acquired during CZ.
+### Error amplification
+
+[(34b_cz_phase_compensation_error_amp)](./34b_cz_phase_compensation_error_amp.py)
+
+Same phase-compensation objective as **34a**, with a train of CZ operations to amplify phase errors before fitting a sinc model on the N-averaged signal. Run after **34a** when finer virtual-Z compensation is needed.
+
+**Goal:** Fine-tune `phase_shift_control` / `phase_shift_target`; optional follow-up to **34a**.
+
---
# Project structure
-| Node | File | Fixed coupler | Tunable coupler |
-| ------- | ---------------------------------------------------------------------------------- | :-----------: | :---------------------------------------------------: |
-| **30** | [`30_cz_iswap_flux_bootstrap.py`](./30_cz_iswap_flux_bootstrap.py) | — | ✓ |
-| **31** | [`31_chevron_11_02.py`](./31_chevron_11_02.py) | ✓ | optional |
-| **32a** | [`32a_cz_conditional_phase.py`](./32a_cz_conditional_phase.py) | ✓ | ✓ |
-| **32b** | [`32b_cz_conditional_phase_error_amp.py`](./32b_cz_conditional_phase_error_amp.py) | ✓ | ✓ |
-| **33a** | [`33a_cz_leakage_amplification.py`](./33a_cz_leakage_amplification.py) | — | ✓ |
-| **33b** | [`33b_cz_leakage_amplification_palea.py`](./33b_cz_leakage_amplification_palea.py) | — | ✓ |
-| **34** | [`34_cz_phase_compensation.py`](./34_cz_phase_compensation.py) | ✓ | ✓ |
-| **99** | [`99_CZ_calibration_graph.py`](./99_CZ_calibration_graph.py) | ✓ (31–32b–34) | — (use **30** → 32a–33a/b–34 by hand or custom graph) |
+| Node | File | Fixed coupler | Tunable coupler |
+| ------- | ------------------------------------------------------------------------------------ | :------------: | :--------------------------------------------------------: |
+| **30** | [`30_cz_iswap_flux_bootstrap.py`](./30_cz_iswap_flux_bootstrap.py) | — | ✓ |
+| **31** | [`31_chevron_11_02.py`](./31_chevron_11_02.py) | ✓ | optional |
+| **32a** | [`32a_cz_conditional_phase.py`](./32a_cz_conditional_phase.py) | ✓ | ✓ |
+| **32b** | [`32b_cz_conditional_phase_error_amp.py`](./32b_cz_conditional_phase_error_amp.py) | ✓ | ✓ |
+| **33a** | [`33a_cz_leakage_amplification.py`](./33a_cz_leakage_amplification.py) | — | ✓ |
+| **33b** | [`33b_cz_leakage_amplification_palea.py`](./33b_cz_leakage_amplification_palea.py) | — | ✓ |
+| **34a** | [`34a_cz_phase_compensation.py`](./34a_cz_phase_compensation.py) | ✓ | ✓ |
+| **34b** | [`34b_cz_phase_compensation_error_amp.py`](./34b_cz_phase_compensation_error_amp.py) | ✓ | ✓ |
+| **99** | [`99_CZ_calibration_graph.py`](./99_CZ_calibration_graph.py) | ✓ (31–32b–34a) | — (use **30** → 32a–32b-33a/b–34a by hand or custom graph) |
-Utilities: `cz_iswap_flux_bootstrap`, `chevron_cz`, `cz_conditional_phase`, `cz_conditional_phase_error_amp`, `cz_leakage_amp`, `cz_phase_compensation` under `../calibration_utils/`.
+Utilities: `cz_iswap_flux_bootstrap`, `chevron_cz`, `cz_conditional_phase`, `cz_conditional_phase_error_amp`, `cz_leakage_amp`, `cz_phase_compensation`, `cz_phase_compensation_error_amp` under `../calibration_utils/`.
---
@@ -250,7 +262,7 @@ Utilities: `cz_iswap_flux_bootstrap`, `chevron_cz`, `cz_conditional_phase`, `cz_
[`99_CZ_calibration_graph.py`](./99_CZ_calibration_graph.py) — `CZ_Calibration_Fixed_Couplers`:
-- **31** → **32a** → **32b** → **34**
+- **31** → **32a** → **32b** → **34a** (add **34b** manually for finer virtual-Z tuning)
Leakage nodes (**33a** / **33b**) are tunable-coupler only and are not included in this graph.