"""Experiment to compute detuning from flux pulses."""
import math
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,
Delay,
Parameter,
Platform,
Pulse,
PulseSequence,
Rectangular,
Sweeper,
)
from qibocal import update
from qibocal.auto.operation import Data, Parameters, QubitId, Results, Routine
from qibocal.calibration import CalibrationPlatform
from .utils import HZ_TO_GHZ, table_dict, table_html
[docs]@dataclass
class FluxAmplitudeFrequencyParameters(Parameters):
"""FluxAmplitudeFrequency runcard inputs."""
amplitude_min: int
"""Minimum flux pulse amplitude."""
amplitude_max: int
"""Maximum flux amplitude."""
amplitude_step: int
"""Flux pulse amplitude step."""
duration: float
"""Flux pulse duration."""
crosstalk_qubit: Optional[QubitId] = None
"""If provided a flux pulse will be applied on this qubit.
Enable to compute the crosstalk matrix.
"""
flux_pulse_amplitude: float = 0
"""Flux pulse amplitude on target qubits to bias from sweetstpot.
It should be provided only if crosstalk is not None.
"""
[docs]@dataclass
class FluxAmplitudeFrequencyResults(Results):
"""FluxAmplitudeFrequency outputs."""
crosstalk: bool = False
"""Check if this is crosstalk protocol."""
detuning: dict[QubitId, float] = field(default_factory=dict)
"""Frequency detuning."""
flux: dict[QubitId, float] = field(default_factory=dict)
"""Derived flux """
fitted_parameters_detuning: dict[tuple[QubitId, str], list[float]] = field(
default_factory=dict
)
"""Fitted parameters for every qubit."""
fitted_parameters_flux: dict[tuple[QubitId, str], list[float]] = field(
default_factory=dict
)
def __contains__(self, target: QubitId):
return target in self.detuning
FluxAmplitudeFrequencyType = np.dtype([("amplitude", float), ("prob_1", np.float64)])
"""Custom dtype for FluxAmplitudeFrequency."""
[docs]def ramsey_flux(
platform: Platform,
qubit: QubitId,
amplitude: float,
duration: int,
measure: str,
target_qubit: QubitId,
target_amplitude: float,
):
"""Compute sequences at fixed amplitude of flux pulse for <X> and <Y>"""
assert measure in ["X", "Y"]
native = platform.natives.single_qubit[qubit]
drive_channel, ry90 = native.R(theta=np.pi / 2, phi=np.pi / 2)[0]
_, rx90 = native.R(theta=np.pi / 2)[0]
ro_channel, ro_pulse = native.MZ()[0]
flux_channel = platform.qubits[
target_qubit if target_qubit is not None else qubit
].flux
flux_pulse = Pulse(duration=duration, amplitude=amplitude, envelope=Rectangular())
# create the sequences
sequence = PulseSequence()
if measure == "X":
sequence.extend(
[
(drive_channel, ry90),
(flux_channel, Delay(duration=ry90.duration)),
(flux_channel, flux_pulse),
(drive_channel, Delay(duration=flux_pulse.duration)),
(drive_channel, ry90),
(
ro_channel,
Delay(duration=ry90.duration + flux_pulse.duration + ry90.duration),
),
(ro_channel, ro_pulse),
]
)
else:
sequence.extend(
[
(drive_channel, ry90),
(flux_channel, Delay(duration=ry90.duration)),
(flux_channel, flux_pulse),
(drive_channel, Delay(duration=flux_pulse.duration)),
(drive_channel, rx90),
(
ro_channel,
Delay(duration=ry90.duration + flux_pulse.duration + rx90.duration),
),
(ro_channel, ro_pulse),
]
)
if target_qubit is not None:
flux_channel = platform.qubits[qubit].flux
flux_pulse = Pulse(
duration=duration, amplitude=target_amplitude, envelope=Rectangular()
)
sequence.extend(
[
(flux_channel, Delay(duration=ry90.duration)),
(flux_channel, flux_pulse),
]
)
return sequence
[docs]@dataclass
class FluxAmplitudeFrequencyData(Data):
"""FluxAmplitudeFrequency acquisition outputs."""
crosstalk_qubit: Optional[QubitId]
"""Qubit where crosstalk will be measured."""
flux_pulse_duration: float
"""Flux pulse amplitude."""
qubit_frequency: dict = field(default_factory=dict)
"""Frequency of the qubits."""
detuning: dict = field(default_factory=dict)
"""Detuning of the qubits."""
data: dict[tuple[QubitId, str], npt.NDArray[FluxAmplitudeFrequencyType]] = field(
default_factory=dict
)
[docs]def _acquisition(
params: FluxAmplitudeFrequencyParameters,
platform: Platform,
targets: list[QubitId],
) -> FluxAmplitudeFrequencyData:
detuning = {}
for qubit in targets:
if params.crosstalk_qubit is None and math.isclose(params.amplitude_min, 0):
detuning[qubit] = 0
else:
assert (
platform.calibration.single_qubits[qubit].qubit.flux_coefficients
is not None
), (
f"Flux coefficients for {qubit} missing. Re-run experiment starting with zero amplitude_min and without crosstalk qubit."
)
if params.crosstalk_qubit is not None:
detuning[qubit] = platform.calibration.single_qubits[
qubit
].qubit.detuning(params.flux_pulse_amplitude)
else:
detuning[qubit] = platform.calibration.single_qubits[
qubit
].qubit.detuning(params.amplitude_min)
qubit_frequency = {
qubit: platform.calibration.single_qubits[qubit].qubit.frequency_01 * HZ_TO_GHZ
for qubit in targets
}
data = FluxAmplitudeFrequencyData(
crosstalk_qubit=params.crosstalk_qubit,
flux_pulse_duration=params.duration,
qubit_frequency=qubit_frequency,
detuning=detuning,
)
amplitudes = np.arange(
params.amplitude_min, params.amplitude_max, params.amplitude_step
)
options = dict(
nshots=params.nshots,
acquisition_type=AcquisitionType.DISCRIMINATION,
averaging_mode=AveragingMode.CYCLIC,
)
for measure in ["X", "Y"]:
sequence = PulseSequence()
for qubit in targets:
sequence += ramsey_flux(
platform,
qubit,
duration=params.duration,
amplitude=params.amplitude_max / 2,
measure=measure,
target_amplitude=params.flux_pulse_amplitude,
target_qubit=params.crosstalk_qubit,
)
sweeper = Sweeper(
parameter=Parameter.amplitude,
range=(params.amplitude_min, params.amplitude_max, params.amplitude_step),
pulses=[
pulse[1]
for pulse in sequence
if pulse[0]
in [
platform.qubits[
(
params.crosstalk_qubit
if params.crosstalk_qubit is not None
else target
)
].flux
for target in targets
]
and isinstance(pulse[1], Pulse)
],
)
result = platform.execute([sequence], [[sweeper]], **options)
for qubit in targets:
ro_pulse = list(sequence.channel(platform.qubits[qubit].acquisition))[-1]
data.register_qubit(
FluxAmplitudeFrequencyType,
(qubit, measure),
dict(
amplitude=amplitudes,
prob_1=result[ro_pulse.id],
),
)
return data
[docs]def _fit(data: FluxAmplitudeFrequencyData) -> FluxAmplitudeFrequencyResults:
fitted_parameters_detuning = {}
fitted_parameters_flux = {}
crosstalk = data.crosstalk_qubit
detuning = {}
flux = {}
qubits = np.unique([i[0] for i in data.data]).tolist()
for qubit in qubits:
amplitudes = data[qubit, "X"].amplitude
X_exp = 2 * data[qubit, "X"].prob_1 - 1
# TODO: check if sign of Y_exp is correct
Y_exp = 1 - 2 * data[qubit, "Y"].prob_1
phase = np.unwrap(np.angle(X_exp + 1j * Y_exp))
# normalization required to avoid problems with arccos
phase -= phase[0]
other_det = data.detuning[qubit]
f = data.qubit_frequency[qubit]
det = phase / data.flux_pulse_duration / 2 / np.pi + other_det
# to make sure that flux is invertible
det[np.abs(det) < 1e-3] = 0
# from inversion of flux dependence formula assuming negligible Ec and asymmetry
derived_flux = 1 / np.pi * np.arccos(((f + det) / f) ** 2)
flux[qubit] = derived_flux.tolist()
fitted_parameters_detuning[qubit] = np.polyfit(amplitudes, det, 2).tolist()
fitted_parameters_flux[qubit] = np.polyfit(amplitudes, derived_flux, 1).tolist()
detuning[qubit] = det.tolist()
return FluxAmplitudeFrequencyResults(
crosstalk=crosstalk,
detuning=detuning,
fitted_parameters_detuning=fitted_parameters_detuning,
flux=flux,
fitted_parameters_flux=fitted_parameters_flux,
)
[docs]def _plot(
data: FluxAmplitudeFrequencyData,
fit: FluxAmplitudeFrequencyResults,
target: QubitId,
):
"""FluxAmplitudeFrequency plots."""
fig = make_subplots(
rows=1,
cols=2,
)
fitting_report = ""
amplitude = data[(target, "X")].amplitude
if fit is not None:
fig.add_trace(
go.Scatter(
x=amplitude,
y=fit.detuning[target],
name="Detuning",
),
row=1,
col=1,
)
fig.add_trace(
go.Scatter(
x=amplitude,
y=fit.flux[target],
name="Flux",
),
row=1,
col=2,
)
fig.add_trace(
go.Scatter(
x=amplitude,
y=np.polyval(fit.fitted_parameters_detuning[target], amplitude),
name="Fit Detuning",
),
row=1,
col=1,
)
fig.add_trace(
go.Scatter(
x=amplitude,
y=np.polyval(fit.fitted_parameters_flux[target], amplitude),
name="Fit Flux",
),
row=1,
col=2,
)
if fit.crosstalk is None:
fitting_report = table_html(
table_dict(
target,
[
"Flux coefficients",
"Flux normalization",
],
[
[
np.round(i, 3)
for i in fit.fitted_parameters_detuning[target]
],
np.round(fit.fitted_parameters_flux[target][0], 3),
],
)
)
else:
fitting_report = table_html(
table_dict(
target,
[
f"Flux crosstalk with {fit.crosstalk}",
],
[
np.round(fit.fitted_parameters_flux[target][0], 4),
],
)
)
fig.update_layout(
showlegend=True,
xaxis1_title="Flux pulse amplitude [a.u.]",
xaxis2_title="Flux pulse amplitude [a.u.]",
yaxis1_title="Detuning [GHz]",
yaxis2_title="Flux [Flux quantum]",
)
return [fig], fitting_report
[docs]def _update(
results: FluxAmplitudeFrequencyResults,
platform: CalibrationPlatform,
target: QubitId,
):
if results.crosstalk is None:
platform.calibration.single_qubits[
target
].qubit.flux_coefficients = results.fitted_parameters_detuning[target]
# TODO: needs to be inverted
flux_qubit = results.crosstalk if results.crosstalk is not None else target
update.crosstalk_matrix(
results.fitted_parameters_flux[target][0], platform, target, flux_qubit
)
flux_amplitude_frequency = Routine(_acquisition, _fit, _plot, _update)