from dataclasses import dataclass, field
from itertools import chain
from typing import Literal, cast
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from qibolab import (
AcquisitionType,
AveragingMode,
Delay,
Parameter,
PulseSequence,
Sweeper,
)
from qibocal.auto.operation import Protocol, QubitId, QubitPairId
from qibocal.calibration import CalibrationPlatform
from qibocal.config import log
from qibocal.protocols.ramsey.acquisition import single_qubit_ramsey_sequence
from qibocal.protocols.ramsey.processing import fitting as ramsey_fitting
from qibocal.protocols.ramsey.processing import ramsey_fit
from qibocal.protocols.utils import GHZ_TO_HZ
from .utils import (
ZZInteractionData,
ZZInteractionParameters,
ZZInteractionResults,
ZZIntType,
coupling_strength,
create_report_table,
signal_plot,
zz_update,
)
__all__ = ["ramsey_zz"]
EPS = 1 # Hz
"""we add 1Hz when computing Delta frequency between the two qubit frequencies in order to avoid numerical error."""
[docs]
@dataclass
class RamseyZZParameters(ZZInteractionParameters):
"""RamseyZZ acquisition outputs."""
detuning: float | None = None
"""Frequency detuning [Hz]."""
@dataclass
class RamseyZZData(ZZInteractionData):
"""RamseyZZ acquisition outputs."""
detuning: float | None = None
"""Frequency detuning [Hz]."""
data: dict[tuple[QubitId, QubitId, Literal["I", "X"]], ZZIntType] = field(
default_factory=dict
)
"""Raw data acquired."""
@dataclass
class RamseyZZResults(ZZInteractionResults):
"""Container for RamseyZZ experiment results."""
fitted_parameters: dict[QubitPairId, dict[Literal["I", "X"], list[float]]] = field(
default_factory=dict
)
"""Parameters fitted during the execution."""
def _acquisition(
params: RamseyZZParameters,
platform: CalibrationPlatform,
targets: list[QubitPairId],
) -> RamseyZZData:
"""Data acquisition for RamseyZZ Experiment.
Targets is a list of qubit pair in the order: (target, spectator).
Standard Ramsey experiment repeated twice.
In the second execution one qubit is brought to the excited state.
"""
qubits_list = list(chain.from_iterable(targets))
qubits_set = set(qubits_list)
if len(qubits_set) != len(qubits_list):
raise ValueError(
"In the pair list there are repeated qubits: "
"Parallel execution is not possible."
)
data = RamseyZZData(
detuning=params.detuning,
qubit_freqs={
q: platform.config(platform.qubits[q].drive).frequency for q in qubits_set
},
anharmonicity={
q: platform.calibration.single_qubits[q].qubit.anharmonicity
for q in qubits_set
},
)
sequences: dict[Literal["I", "X"], PulseSequence] = {}
parsweepers: dict[Literal["I", "X"], Sweeper] = {}
updates = {}
for setup in cast(tuple[Literal["I", "X"], ...], ("I", "X")):
setup_sequence = PulseSequence()
setup_delays: list[Delay] = []
for pair in targets:
targ, spect = pair
if params.detuning is not None:
channel = platform.qubits[targ].drive
f0 = platform.config(channel).frequency
if channel not in updates:
updates[channel] = {"frequency": f0 + params.detuning}
qubit_ramsey, qubit_ro_seq, target_delays = single_qubit_ramsey_sequence(
platform=platform,
target=targ,
wait=0,
)
# adding sequence for measuring also spectator qubit
spect_natives = platform.natives.single_qubit[spect]
spect_ro_ch, spect_ro_pulse = spect_natives.MZ()[0]
spect_ro_delay = Delay(duration=0)
qubit_ro_seq.extend(
(
(
spect_ro_ch,
Delay(
duration=2
* platform.natives.single_qubit[targ]
.R(theta=np.pi / 2)
.duration
),
),
(spect_ro_ch, spect_ro_delay),
(spect_ro_ch, spect_ro_pulse),
)
)
if setup == "X":
# adding X gate on spectator qubit
qubit_sequence = spect_natives.RX() | (qubit_ramsey + qubit_ro_seq)
else:
qubit_sequence = qubit_ramsey + qubit_ro_seq
setup_sequence += qubit_sequence
setup_delays += target_delays + [spect_ro_delay]
sequences[setup] = setup_sequence
parsweepers[setup] = Sweeper(
parameter=Parameter.duration,
range=params.delay_range,
pulses=setup_delays,
)
# execute the sweep
results = platform.execute(
list(sequences.values()),
[list(parsweepers.values())],
nshots=params.nshots,
relaxation_time=params.relaxation_time,
acquisition_type=AcquisitionType.DISCRIMINATION,
averaging_mode=AveragingMode.CYCLIC,
updates=[updates],
)
for targ, spect in targets:
for setup in ("I", "X"):
targ_ro_pulse = list(
sequences[setup].channel(platform.qubits[targ].acquisition)
)[-1]
targ_probs = results[targ_ro_pulse.id]
spect_ro_pulse = list(
sequences[setup].channel(platform.qubits[spect].acquisition)
)[-1]
spect_probs = results[spect_ro_pulse.id]
data.register_qubit(
ZZIntType,
(targ, spect, setup),
{
"delay": parsweepers[setup].values,
"targ_prob": np.array(targ_probs),
"targ_error": np.sqrt(
targ_probs * (1 - targ_probs) / params.nshots
),
"spect_prob": np.array(spect_probs),
"spect_error": np.sqrt(
spect_probs * (1 - spect_probs) / params.nshots
),
},
)
return data
def _fit(data: RamseyZZData) -> RamseyZZResults:
"""Fitting procedure for RamseyZZ protocol.
Standard Ramsey fitting procedure is applied for both version of
the experiment.
"""
delays = data.delays
popts: dict[QubitPairId, dict[Literal["I", "X"], list[float]]] = {}
delta_fitting_measure: dict[
tuple[QubitId, QubitId, Literal["I", "X"]], list[float]
] = {}
zz: dict[QubitPairId, list[float]] = {}
coupling: dict[QubitPairId, list[float]] = {}
for pair in data.pairs:
target, spectator = pair
setup_param_dict = {}
try: # try outside of the for loop because we need both signals to fit (both control in 0 or 1)
for setup in cast(tuple[Literal["I", "X"], ...], ("I", "X")):
setup_data = data.data[target, spectator, setup]
popt, perr = ramsey_fitting(
delays, setup_data["targ_prob"], setup_data["targ_error"]
)
delta_fitting_measure[target, spectator, setup] = [
-popt[2] / (2 * np.pi) * GHZ_TO_HZ,
perr[2] * GHZ_TO_HZ / (2 * np.pi),
]
setup_param_dict[setup] = popt
popts[pair] = setup_param_dict
# compute zz and qq coupling
# zz the difference in frequency between the two measurement
zz[pair] = [
float(
delta_fitting_measure[target, spectator, "X"][0]
- delta_fitting_measure[target, spectator, "I"][0]
),
float(
np.sqrt(
delta_fitting_measure[target, spectator, "X"][1] ** 2
+ delta_fitting_measure[target, spectator, "I"][1] ** 2
)
),
]
# here we compute coupling as a frequency
coupling[pair] = coupling_strength(
omega1=data.qubit_freqs[target],
omega2=data.qubit_freqs[spectator],
anharmonicity1=data.anharmonicity[target],
anharmonicity2=data.anharmonicity[spectator],
zz=zz[pair],
)
# here we are catching two possible errors: fitting error in curve_fit
# call or error in estimating coupling strenght because anharmonicities
# are unknown
except (RuntimeError, ValueError) as e:
log.warning(f"Ramsey fitting failed for qubit pair {pair} due to {e}.")
return RamseyZZResults(
fitted_parameters=popts,
zz=zz,
coupling=coupling,
)
def _plot(
data: RamseyZZData, target: QubitPairId, fit: RamseyZZResults | None = None
) -> tuple[list[go.Figure], str]:
"""Plotting function for Ramsey Experiment."""
targ, spect = target
fig = make_subplots(
rows=1,
cols=2,
horizontal_spacing=0.1,
subplot_titles=(
f"Target qubit {targ}",
f"Spectator qubit {spect}",
),
)
fitting_report = ""
if fit is not None and all(
term in fit.fitted_parameters[target] for term in ("I", "X")
):
for s in ("I", "X"):
target_traces, spectator_trace = signal_plot(
signal=data.data[targ, spect, s],
module=ramsey_fit,
fit_params=fit.fitted_parameters[target][s],
label=s,
)
fig.add_traces(
target_traces + spectator_trace,
rows=1,
cols=[1] * len(target_traces) + [2],
)
fig.update_layout(showlegend=True)
fig.update_xaxes(title_text="Time [ns]", col=1)
fig.update_xaxes(title_text="Time [ns]", col=2)
fig.update_yaxes(title_text="Excited state probability", col=1)
fig.update_yaxes(title_text="Excited state probability", col=2)
fitting_report = create_report_table(target, fit)
return [fig], fitting_report
ramsey_zz = Protocol(_acquisition, _fit, _plot, zz_update)
"""Ramsey ZZ Protocol object.
This protocol measures the state-dependent frequency shift (ZZ interaction)
between a selected target qubit and one spectator qubit. It
performs two Ramsey experiments for each qubit in the target list:
- "I" setup: spectator qubit remain in the ground state.
- "X" setup: spectator qubit is excited before the Ramsey sequence.
The fitted Ramsey frequencies from these two experiments are compared to
extract the conditional ZZ shift experienced by each measured qubit. The
difference between the two fitted frequencies is reported as the
ZZ interaction strength. Using the measured qubit frequencies, the target
qubit frequency, and the anharmonicities, the routine also estimates the
effective coupling strength between each qubit and the target qubit.
This protocol is useful for characterizing residual static coupling and
frequency shifts induced by an excited neighboring qubit. The plot output
shows Ramsey traces in probability for both the I and X setups.
"""