Source code for qibocal.protocols.two_qubit_interaction.virtual_z_phases

"""CZ virtual correction experiment for two qubit gates, tune landscape."""

from dataclasses import dataclass, field
from typing import Literal, Optional

import numpy as np
import numpy.typing as npt
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from qibolab import (
    AcquisitionType,
    AveragingMode,
    Delay,
    Parameter,
    Pulse,
    PulseSequence,
    Sweeper,
    VirtualZ,
)
from scipy.optimize import curve_fit

from qibocal.auto.operation import (
    Data,
    Parameters,
    QubitId,
    QubitPairId,
    Results,
    Routine,
)
from qibocal.calibration import CalibrationPlatform
from qibocal.config import log
from qibocal.protocols.utils import table_dict, table_html

from ... import update
from ...update import replace
from .utils import order_pair

__all__ = ["correct_virtual_z_phases", "create_sequence", "fit_sinusoid", "phase_diff"]


[docs] @dataclass class VirtualZPhasesParameters(Parameters): """VirtualZ runcard inputs.""" theta_start: float """Initial angle for the low frequency qubit measurement in radians.""" theta_end: float """Final angle for the low frequency qubit measurement in radians.""" theta_step: float """Step size for the theta sweep in radians.""" native: str = "CZ" """Two qubit interaction to be calibrated. iSWAP and CZ are the possible options. """ dt: Optional[float] = 16 """Time delay between flux pulses and readout.""" gate_repetition: int = 1 """Number of CZ repetition"""
@dataclass class VirtualZPhasesResults(Results): """VirtualZ outputs when fitting will be done.""" fitted_parameters: dict[tuple[str, QubitId],] """Fitted parameters""" native: str """Native two qubit gate.""" gate_repetition: int leakage: dict[QubitPairId, dict[QubitId, float]] """Leakage on control qubit for pair.""" angle: Optional[dict[QubitPairId, float]] = None """Native angle.""" virtual_phase: Optional[dict[QubitPairId, dict[QubitId, float]]] = None """Virtual Z phase correction.""" def __contains__(self, key: QubitPairId): """Check if key is in class. While key is a QubitPairId both chsh and chsh_mitigated contain an additional key which represents the basis chosen. """ return key in [ (target, control) for target, control, _ in self.fitted_parameters ] VirtualZPhasesType = np.dtype([("target", np.float64), ("control", np.float64)]) @dataclass class VirtualZPhasesData(Data): """VirtualZPhases data.""" gate_repetition: int data: dict[tuple, npt.NDArray[VirtualZPhasesType]] = field(default_factory=dict) native: str = "CZ" thetas: list = field(default_factory=list) def __getitem__(self, pair): return { index: value for index, value in self.data.items() if set(pair).issubset(index) }
[docs] def create_sequence( platform: CalibrationPlatform, setup: Literal["I", "X"], target_qubit: QubitId, control_qubit: QubitId, ordered_pair: list[QubitId, QubitId], native: Literal["CZ", "iSWAP"], dt: float, flux_pulse_max_duration: float = None, gate_repetition: int = 1, ) -> tuple[PulseSequence, Pulse, list[Pulse]]: """ Create the pulse sequence for the calibration of two-qubit gate virtual phases. This function constructs a pulse sequence for a given two-qubit native gate `native` (CZ or iSWAP) on the specified qubits. The sequence includes: - A preliminary RX90 pulse on the `target_qubit`. - An optional X pulse on the `control_qubit` based on the `setup` type. - A flux pulse implementing the two-qubit native gate. - A delay of duration `dt` before the final X90 pulse on the target qubit. - Measurement pulses. It is possible to specify the maximum duration for the flux pulses with the `flux_pulse_max_duration` parameter. The function returns: - The full experiment pulse sequence. - The applied flux pulse. - The final `VirtualZPhase` pulses to be used for phase sweeping. """ target_natives = platform.natives.single_qubit[target_qubit] control_natives = platform.natives.single_qubit[control_qubit] sequence = PulseSequence() # X90 sequence += target_natives.R(theta=np.pi / 2) # X if setup == "X": sequence += control_natives.RX() # CZ flux_sequence = getattr(platform.natives.two_qubit[ordered_pair], native)() flux_channel = platform.qubits[ordered_pair[1]].flux flux_pulse = list(flux_sequence.channel(flux_channel))[ 0 ] # Expecting only one flux pulse if flux_pulse_max_duration is not None: flux_pulse = replace(flux_pulse, duration=flux_pulse_max_duration) flux_sequence = PulseSequence([(flux_channel, flux_pulse)]) virtual_phases = [] align_channels = [ platform.qubits[control_qubit].drive, platform.qubits[target_qubit].drive, flux_channel, platform.qubits[target_qubit].acquisition, platform.qubits[control_qubit].acquisition, ] sequence.align(align_channels) for _ in range(gate_repetition): sequence.append((flux_channel, Delay(duration=dt))) sequence += flux_sequence sequence.append((flux_channel, Delay(duration=dt))) # Instead of having many RZ as expressed in gate_repetition, # a single RZ with angle (theta*gate_repetition) is added because qm ignores the first one. # This work for CZ since it commutes with the RZ, but break the iSWAP compatibility. # See https://github.com/qiboteam/qibolab/discussions/1198. virtual_phases.append(VirtualZ(phase=0)) sequence.append((platform.qubits[target_qubit].drive, virtual_phases[-1])) theta_sequence = PulseSequence() # RX90 (angle to be swept) sequence.align(align_channels) theta_sequence += target_natives.R(theta=np.pi / 2) sequence += theta_sequence # X gate for the leakage if setup == "X": sequence += control_natives.RX() sequence.align(align_channels) ro_sequence = PulseSequence( [ target_natives.MZ()[0], control_natives.MZ()[0], ] ) sequence += ro_sequence return sequence, flux_pulse, virtual_phases
def _acquisition( params: VirtualZPhasesParameters, platform: CalibrationPlatform, targets: list[QubitPairId], ) -> VirtualZPhasesData: r""" Acquisition for VirtualZPhases. Check the two-qubit landscape created by a flux pulse of a given duration and amplitude. The system is initialized with a X90 pulse on the low frequency qubit and either an Id or an X gate on the high frequency qubit. Then the flux pulse is applied to the high frequency qubit in order to perform a two-qubit interaction. A $X_{\beta}90$ pulse is applied to the low frequency qubit before measurement. That is, a pi-half pulse around the relative phase parametereized by the angle theta. Measurements on the low frequency qubit yield the 2Q-phase of the gate and the remnant single qubit Z phase aquired during the execution to be corrected. Population of the high frequency qubit yield the leakage to the non-computational states during the execution of the flux pulse. """ assert params.native == "CZ", "This protocol supports only CZ gate." theta_absolute = np.arange(params.theta_start, params.theta_end, params.theta_step) data = VirtualZPhasesData( gate_repetition=params.gate_repetition, thetas=theta_absolute.tolist(), native=params.native, ) for pair in targets: # order the qubits so that the low frequency one is the first ordered_pair = order_pair(pair, platform) for target_q, control_q in ( (ordered_pair[0], ordered_pair[1]), (ordered_pair[1], ordered_pair[0]), ): for setup in ("I", "X"): ( sequence, _, vz_pulses, ) = create_sequence( platform, setup, target_q, control_q, ordered_pair, params.native, dt=params.dt, gate_repetition=params.gate_repetition, ) # The virtual phase values are the opposite of beta, this is # because, according to the circuit we would like to reproduce # after the CZ, an RZ is applied. The RZ gate with `theta` angle # is compiled into a VirtualPhase pulse with phase `-theta`. # (See https://github.com/qiboteam/qibolab/pull/1044#issuecomment-2354622956) sweeper = Sweeper( parameter=Parameter.phase, range=( -params.gate_repetition * params.theta_start, -params.gate_repetition * params.theta_end, -params.gate_repetition * params.theta_step, ), pulses=vz_pulses, ) results = platform.execute( [sequence], [[sweeper]], nshots=params.nshots, relaxation_time=params.relaxation_time, acquisition_type=AcquisitionType.DISCRIMINATION, averaging_mode=AveragingMode.CYCLIC, ) ro_target = list( sequence.channel(platform.qubits[target_q].acquisition) )[-1] ro_control = list( sequence.channel(platform.qubits[control_q].acquisition) )[-1] result_target = results[ro_target.id] result_control = results[ro_control.id] data.register_qubit( VirtualZPhasesType, (target_q, control_q, setup), dict( target=result_target, control=result_control, ), ) return data def sinusoid(x, gate_repetition, amplitude, offset, phase): """Sinusoidal fit function.""" return np.cos(gate_repetition * (x + phase)) * amplitude + offset
[docs] def phase_diff(phase_1, phase_2): """Return the phase difference of two sinusoids, normalized in the range [0, 2*pi].""" return np.mod(phase_2 - phase_1, 2 * np.pi)
[docs] def fit_sinusoid(thetas, data, gate_repetition): """Fit sinusoid to the given data.""" pguess = [ np.max(data) - np.min(data), np.mean(data), np.pi, ] popt, _ = curve_fit( lambda x, amplitude, offset, phase: sinusoid( x, gate_repetition, amplitude, offset, phase ), thetas, data, p0=pguess, bounds=( (0, -np.max(data), 0), (np.max(data), np.max(data), 2 * np.pi), ), ) return popt.tolist()
def _fit( data: VirtualZPhasesData, ) -> VirtualZPhasesResults: r"""Fitting routine for the experiment. The used model is .. math:: y = p_0 sin\Big(x + p_2\Big) + p_1. """ fitted_parameters = {} pairs = data.pairs virtual_phase = {} angle = {} leakage = {} for pair in pairs: virtual_phase[pair] = {} leakage[pair] = {} for target, control, setup in data[pair]: target_data = data[pair][target, control, setup].target try: params = fit_sinusoid( np.array(data.thetas), target_data, data.gate_repetition ) fitted_parameters[target, control, setup] = params except Exception as e: log.warning(f"CZ fit failed for pair ({target, control}) due to {e}.") for target_q, control_q in ( pair, list(pair)[::-1], ): # leakage estimate: L = m /2 # See NZ paper from Di Carlo # approximation which does not need qutrits # https://arxiv.org/pdf/1903.02492.pdf leakage[pair][control_q] = 0.5 * float( np.mean( data[pair][target_q, control_q, "X"].control - data[pair][target_q, control_q, "I"].control ) ) try: for target_q, control_q in ( pair, list(pair)[::-1], ): angle[target_q, control_q] = phase_diff( fitted_parameters[target_q, control_q, "X"][2], fitted_parameters[target_q, control_q, "I"][2], ) virtual_phase[pair][target_q] = fitted_parameters[ target_q, control_q, "I" ][2] # leakage estimate: L = m /2 # See NZ paper from Di Carlo # approximation which does not need qutrits # https://arxiv.org/pdf/1903.02492.pdf leakage[pair][control_q] = 0.5 * float( np.mean( data[pair][target_q, control_q, "X"].control - data[pair][target_q, control_q, "I"].control ) ) except KeyError: pass # exception covered above return VirtualZPhasesResults( native=data.native, gate_repetition=data.gate_repetition, angle=angle, virtual_phase=virtual_phase, fitted_parameters=fitted_parameters, leakage=leakage, ) def _plot(data: VirtualZPhasesData, fit: VirtualZPhasesResults, target: QubitPairId): """Plot routine for VirtualZPhases.""" pair_data = data[target] qubits = next(iter(pair_data))[:2] fig1 = make_subplots( rows=1, cols=2, subplot_titles=( f"Qubit {qubits[0]}", f"Qubit {qubits[1]}", ), ) fitting_report = set() fig2 = make_subplots( rows=1, cols=2, subplot_titles=( f"Qubit {qubits[0]}", f"Qubit {qubits[1]}", ), ) thetas = data.thetas for target_q, control_q, setup in pair_data: target_prob = pair_data[target_q, control_q, setup].target control_prob = pair_data[target_q, control_q, setup].control fig = fig1 if (target_q, control_q) == qubits else fig2 fig.add_trace( go.Scatter( x=np.array(thetas), y=target_prob, name=f"{setup} sequence", legendgroup=setup, ), row=1, col=1 if fig == fig1 else 2, ) fig.add_trace( go.Scatter( x=np.array(thetas), y=control_prob, name=f"{setup} sequence", legendgroup=setup, ), row=1, col=2 if fig == fig1 else 1, ) if fit is not None: angle_range = np.linspace(thetas[0], thetas[-1], 100) fitted_parameters = fit.fitted_parameters[target_q, control_q, setup] fig.add_trace( go.Scatter( x=angle_range, y=sinusoid( angle_range, data.gate_repetition, *fitted_parameters, ), name="Fit", line=go.scatter.Line(dash="dot"), ), row=1, col=1 if fig == fig1 else 2, ) fitting_report.add( table_html( table_dict( [target_q, target_q, control_q], [ f"{fit.native} angle [rad]", "Virtual Z phase [rad]", "Leakage [a.u.]", ], [ np.round(fit.angle[target_q, control_q], 4), np.round( fit.virtual_phase[tuple(sorted(target))][target_q], 4, ), np.round(fit.leakage[tuple(sorted(target))][control_q], 4), ], ) ) ) fig1.update_layout( title_text=f"Phase correction Qubit {qubits[0]}", showlegend=True, xaxis1_title="Virtual phase[rad]", xaxis2_title="Virtual phase [rad]", yaxis_title="State 1 Probability", ) fig2.update_layout( title_text=f"Phase correction Qubit {qubits[1]}", showlegend=True, xaxis1_title="Virtual phase[rad]", xaxis2_title="Virtual phase[rad]", yaxis_title="State 1 Probability", ) return [fig1, fig2], "".join(fitting_report) # target and control qubit def _update( results: VirtualZPhasesResults, platform: CalibrationPlatform, target: QubitPairId ): if results.gate_repetition == 1: # FIXME: quick fix for qubit order target = tuple(sorted(target)) update.virtual_phases( results.virtual_phase[target], results.native, platform, target ) correct_virtual_z_phases = Routine( _acquisition, _fit, _plot, _update, two_qubit_gates=True ) """Virtual phases correction protocol."""