"""CZ virtual correction experiment for two qubit gates, tune landscape."""
from dataclasses import dataclass, field
from typing import 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, ExecutionParameters
from qibolab.platform import Platform
from qibolab.pulses import Pulse, PulseSequence
from qibolab.qubits import QubitId, QubitPairId
from qibolab.sweeper import Parameter, Sweeper, SweeperType
from scipy.optimize import curve_fit
from qibocal import update
from qibocal.auto.operation import Data, Parameters, Results, Routine
from qibocal.config import log
from qibocal.protocols.utils import table_dict, table_html
from .utils import order_pair
[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.
"""
flux_pulse_amplitude: Optional[float] = None
"""Amplitude of flux pulse implementing CZ."""
flux_pulse_duration: Optional[float] = None
"""Duration of flux pulse implementing CZ."""
dt: Optional[float] = 20
"""Time delay between flux pulses and readout."""
parking: bool = True
"""Wether to park non interacting qubits or not."""
[docs]@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."""
angle: dict[QubitPairId, float]
"""Native angle."""
virtual_phase: dict[QubitPairId, dict[QubitId, float]]
"""Virtual Z phase correction."""
leakage: dict[QubitPairId, dict[QubitId, float]]
"""Leakage on control qubit for pair."""
flux_pulse_amplitude: dict[QubitPairId, float]
"""Amplitude of flux pulse implementing CZ."""
flux_pulse_duration: dict[QubitPairId, int]
"""Duration of flux pulse implementing CZ."""
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.
"""
# TODO: fix this (failing only for qq report)
return key in [
(target, control) for target, control, _ in self.fitted_parameters
]
VirtualZPhasesType = np.dtype([("target", np.float64), ("control", np.float64)])
[docs]@dataclass
class VirtualZPhasesData(Data):
"""VirtualZPhases data."""
data: dict[tuple, npt.NDArray[VirtualZPhasesType]] = field(default_factory=dict)
native: str = "CZ"
thetas: list = field(default_factory=list)
amplitudes: dict[tuple[QubitId, QubitId], float] = field(default_factory=dict)
durations: dict[tuple[QubitId, QubitId], float] = field(default_factory=dict)
def __getitem__(self, pair):
return {
index: value
for index, value in self.data.items()
if set(pair).issubset(index)
}
[docs]def create_sequence(
platform: Platform,
setup: str,
target_qubit: QubitId,
control_qubit: QubitId,
ordered_pair: list[QubitId, QubitId],
native: str,
parking: bool,
dt: float,
amplitude: float = None,
duration: float = None,
) -> tuple[
PulseSequence,
dict[QubitId, Pulse],
dict[QubitId, Pulse],
dict[QubitId, Pulse],
dict[QubitId, Pulse],
]:
"""Create the experiment PulseSequence."""
sequence = PulseSequence()
Y90_pulse = platform.create_RX90_pulse(
target_qubit, start=0, relative_phase=np.pi / 2
)
RX_pulse_start = platform.create_RX_pulse(control_qubit, start=0, relative_phase=0)
flux_sequence, _ = getattr(platform, f"create_{native}_pulse_sequence")(
(ordered_pair[1], ordered_pair[0]),
start=max(Y90_pulse.finish, RX_pulse_start.finish),
)
if amplitude is not None:
flux_sequence.get_qubit_pulses(ordered_pair[1])[0].amplitude = amplitude
if duration is not None:
flux_sequence.get_qubit_pulses(ordered_pair[1])[0].duration = duration
theta_pulse = platform.create_RX90_pulse(
target_qubit,
start=flux_sequence.finish + dt,
relative_phase=0,
)
RX_pulse_end = platform.create_RX_pulse(
control_qubit,
start=flux_sequence.finish + dt,
relative_phase=0,
)
measure_target = platform.create_qubit_readout_pulse(
target_qubit, start=theta_pulse.finish
)
measure_control = platform.create_qubit_readout_pulse(
control_qubit, start=theta_pulse.finish
)
sequence.add(
Y90_pulse,
flux_sequence.get_qubit_pulses(ordered_pair[1]),
flux_sequence.cf_pulses,
theta_pulse,
measure_target,
measure_control,
)
if setup == "X":
sequence.add(
RX_pulse_start,
RX_pulse_end,
)
if parking:
for pulse in flux_sequence:
if pulse.qubit not in ordered_pair:
pulse.duration = theta_pulse.finish
sequence.add(pulse)
return (
sequence,
theta_pulse,
flux_sequence.get_qubit_pulses(ordered_pair[1])[0].amplitude,
flux_sequence.get_qubit_pulses(ordered_pair[1])[0].duration,
)
[docs]def _acquisition(
params: VirtualZPhasesParameters,
platform: Platform,
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 Y90 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. The Id/X gate
is undone in the high frequency qubit and a theta90 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.
"""
theta_absolute = np.arange(params.theta_start, params.theta_end, params.theta_step)
data = VirtualZPhasesData(thetas=theta_absolute.tolist(), native=params.native)
for pair in targets:
# order the qubits so that the low frequency one is the first
ord_pair = order_pair(pair, platform)
for target_q, control_q in (
(ord_pair[0], ord_pair[1]),
(ord_pair[1], ord_pair[0]),
):
for setup in ("I", "X"):
(
sequence,
theta_pulse,
data.amplitudes[ord_pair],
data.durations[ord_pair],
) = create_sequence(
platform,
setup,
target_q,
control_q,
ord_pair,
params.native,
params.dt,
params.parking,
params.flux_pulse_amplitude,
)
theta = np.arange(
params.theta_start,
params.theta_end,
params.theta_step,
dtype=float,
)
sweeper = Sweeper(
Parameter.relative_phase,
theta,
pulses=[theta_pulse],
type=SweeperType.ABSOLUTE,
)
results = platform.sweep(
sequence,
ExecutionParameters(
nshots=params.nshots,
relaxation_time=params.relaxation_time,
acquisition_type=AcquisitionType.DISCRIMINATION,
averaging_mode=AveragingMode.CYCLIC,
),
sweeper,
)
result_target = results[target_q].probability(1)
result_control = results[control_q].probability(1)
data.register_qubit(
VirtualZPhasesType,
(target_q, control_q, setup),
dict(
target=result_target,
control=result_control,
),
)
return data
[docs]def fit_function(x, amplitude, offset, phase):
"""Sinusoidal fit function."""
# return p0 + p1 * np.sin(2*np.pi*p2 * x + p3)
return np.sin(x + phase) * amplitude + offset
[docs]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
pguess = [
np.max(target_data) - np.min(target_data),
np.mean(target_data),
np.pi,
]
try:
popt, _ = curve_fit(
fit_function,
np.array(data.thetas),
target_data,
p0=pguess,
bounds=(
(0, -np.max(target_data), 0),
(np.max(target_data), np.max(target_data), 2 * np.pi),
),
)
fitted_parameters[target, control, setup] = popt.tolist()
except Exception as e:
log.warning(f"CZ fit failed for pair ({target, control}) due to {e}.")
try:
for target_q, control_q in (
pair,
list(pair)[::-1],
):
angle[target_q, control_q] = abs(
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,
flux_pulse_amplitude=data.amplitudes,
flux_pulse_duration=data.durations,
angle=angle,
virtual_phase=virtual_phase,
fitted_parameters=fitted_parameters,
leakage=leakage,
)
# TODO: remove str
[docs]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=fit_function(
angle_range,
*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),
],
)
)
)
fitting_report.add(
table_html(
table_dict(
[qubits[1], qubits[1]],
[
"Flux pulse amplitude [a.u.]",
"Flux pulse duration [ns]",
],
[
np.round(data.amplitudes[qubits], 4),
np.round(data.durations[qubits], 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
[docs]def _update(results: VirtualZPhasesResults, platform: Platform, target: QubitPairId):
# FIXME: quick fix for qubit order
qubit_pair = tuple(sorted(target))
target = tuple(sorted(target))
update.virtual_phases(
results.virtual_phase[target], results.native, platform, target
)
getattr(update, f"{results.native}_duration")(
results.flux_pulse_duration[target], platform, target
)
getattr(update, f"{results.native}_amplitude")(
results.flux_pulse_amplitude[target], platform, target
)
correct_virtual_z_phases = Routine(
_acquisition, _fit, _plot, _update, two_qubit_gates=True
)
"""Virtual phases correction protocol."""