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

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 qibocal.auto.operation import (
    Data,
    Parameters,
    QubitId,
    QubitPairId,
    Results,
    Routine,
)
from qibocal.calibration import CalibrationPlatform
from qibocal.protocols.utils import table_dict, table_html

from ... import update
from ...update import replace
from .utils import fit_sinusoid, fit_virtualz, order_pair, phase_diff, sinusoid

__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: float | None = 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: dict[QubitPairId, float] | None = None """Native angle.""" virtual_phase: dict[QubitPairId, dict[QubitId, float]] | None = 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.angle] 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) } @property def order_pairs(self): pairs = [] for key in self.data.keys(): pairs.append((key[0], key[1])) return np.unique(pairs, axis=0).tolist()
[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, flux_pulse: list | None = None, ) -> tuple[PulseSequence, Pulse, 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() flux_channel = platform.qubits[ordered_pair[1]].flux # CZ if flux_pulse is None: cz_sequence = platform.natives.two_qubit[ordered_pair].CZ flux_pulse = list(cz_sequence.channel(flux_channel))[0] 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 _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.order_pairs virtual_phase = {} angle = {} leakage = {} for pair in pairs: new_fitted_parameter, new_phases, new_angle, new_leak = fit_virtualz( data[pair], tuple(pair), data.thetas, data.gate_repetition ) fitted_parameters |= new_fitted_parameter virtual_phase |= new_phases angle |= new_angle leakage |= new_leak 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 and setup == "I": 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[target_q, control_q], 4, ), np.round(fit.leakage[target_q, 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, results.native, platform, target) correct_virtual_z_phases = Routine( _acquisition, _fit, _plot, _update, two_qubit_gates=True ) """Virtual phases correction protocol."""