diff --git a/src/qibocal/protocols/qubit_spectroscopies/__init__.py b/src/qibocal/protocols/qubit_spectroscopies/__init__.py index b20407b669..9f21d4c58f 100644 --- a/src/qibocal/protocols/qubit_spectroscopies/__init__.py +++ b/src/qibocal/protocols/qubit_spectroscopies/__init__.py @@ -1,5 +1,19 @@ +from .qubit_ampl_spectroscopy import qubit_amplitude_spectroscopy +from .qubit_broad_spectroscopy_conditional import ( + conditional_broad_spectator_spectroscopy, +) from .qubit_power_spectroscopy import qubit_power_spectroscopy from .qubit_spectroscopy import qubit_spectroscopy +from .qubit_spectroscopy_conditional import qubit_conditional_spectroscopy from .qubit_spectroscopy_ef import qubit_spectroscopy_ef +from .qubits_spectroscopy_spectator import qubit_spectroscopy_spectator_scan -__all__ = ["qubit_power_spectroscopy", "qubit_spectroscopy", "qubit_spectroscopy_ef"] +__all__ = [ + "qubit_power_spectroscopy", + "qubit_spectroscopy", + "qubit_spectroscopy_ef", + "qubit_conditional_spectroscopy", + "qubit_spectroscopy_spectator_scan", + "qubit_amplitude_spectroscopy", + "conditional_broad_spectator_spectroscopy", +] diff --git a/src/qibocal/protocols/qubit_spectroscopies/qubit_ampl_spectroscopy.py b/src/qibocal/protocols/qubit_spectroscopies/qubit_ampl_spectroscopy.py new file mode 100644 index 0000000000..2c1109d9af --- /dev/null +++ b/src/qibocal/protocols/qubit_spectroscopies/qubit_ampl_spectroscopy.py @@ -0,0 +1,162 @@ +from dataclasses import dataclass + +import numpy as np +import plotly.graph_objects as go +from qibolab import ( + AcquisitionType, + AveragingMode, + Delay, + Parameter, + PulseSequence, + Sweeper, +) + +from qibocal.auto.operation import Parameters, QubitId, Results, Routine +from qibocal.calibration import CalibrationPlatform + +from ...update import replace +from ..resonator_spectroscopies.resonator_punchout import ResonatorPunchoutData +from ..utils import HZ_TO_GHZ, readout_frequency +from .qubit_spectroscopy import QubitSpectroscopyResults + +__all__ = ["qubit_amplitude_spectroscopy"] + + +@dataclass +class QubitAmplitudeSpectroscopyParameters(Parameters): + """QubitPowerSpectroscopy runcard inputs.""" + + freq_width: int + """Width for frequency sweep relative to the drive frequency [Hz].""" + freq_step: int + """Frequency step for sweep [Hz].""" + min_amp: float + """Minimum amplitude.""" + max_amp: float + """Maximum amplitude.""" + step_amp: float + """Step amplitude.""" + duration: int + """Drive duration.""" + + +@dataclass +class QubitAmplitudeSpectroscopyData(ResonatorPunchoutData): + """QubitPowerSpectroscopy data acquisition.""" + + +def _acquisition( + params: QubitAmplitudeSpectroscopyParameters, + platform: CalibrationPlatform, + targets: list[QubitId], +) -> QubitAmplitudeSpectroscopyData: + """Perform a qubit spectroscopy experiment with different amplitudes. + + For high amplitude it should be possible to see more peaks: corresponding to + the (0-2)/2 frequency and the 1-2. + This experiment can be used also to test if a peak is a qubit: if it is, the + peak will get larger while increasing the power of the drive. + """ + # define the sequence: RX - MZ + sequence = PulseSequence() + ro_pulses = {} + qd_pulses = {} + freq_sweepers = {} + delta_frequency_range = np.arange( + -params.freq_width / 2, params.freq_width / 2, params.freq_step + ) + for qubit in targets: + natives = platform.natives.single_qubit[qubit] + qd_channel, qd_pulse = natives.RX()[0] + ro_channel, ro_pulse = natives.MZ()[0] + + qd_pulse = replace(qd_pulse, duration=params.duration) + + qd_pulses[qubit] = qd_pulse + ro_pulses[qubit] = ro_pulse + + sequence.append((qd_channel, qd_pulse)) + sequence.append((ro_channel, Delay(duration=qd_pulse.duration))) + sequence.append((ro_channel, ro_pulse)) + + f0 = platform.config(qd_channel).frequency + freq_sweepers[qubit] = Sweeper( + parameter=Parameter.frequency, + values=f0 + delta_frequency_range, + channels=[qd_channel], + ) + + amp_sweeper = Sweeper( + parameter=Parameter.amplitude, + range=(params.min_amp, params.max_amp, params.step_amp), + pulses=[qd_pulses[qubit] for qubit in targets], + ) + + # data + data = QubitAmplitudeSpectroscopyData( + resonator_type=platform.resonator_type, + amplitudes=amp_sweeper.values.tolist(), + frequencies={qubit: freq_sweepers[qubit].values.tolist() for qubit in targets}, + ) + + results = platform.execute( + [sequence], + [[amp_sweeper], [freq_sweepers[q] for q in targets]], + updates=[ + {platform.qubits[q].probe: {"frequency": readout_frequency(q, platform)}} + for q in targets + ], + nshots=params.nshots, + relaxation_time=params.relaxation_time, + acquisition_type=AcquisitionType.INTEGRATION, + averaging_mode=AveragingMode.CYCLIC, + ) + + # retrieve the results for every qubit + for qubit, ro_pulse in ro_pulses.items(): + # average signal, phase, i and q over the number of shots defined in the runcard + data.data[qubit] = results[ro_pulse.id] + + return data + + +def _fit(data: QubitAmplitudeSpectroscopyData) -> Results: + """Do not perform any fitting procedure.""" + return Results() + + +def _plot( + data: ResonatorPunchoutData, + target: QubitId, + fit: QubitSpectroscopyResults | None = None, +): + """Plot QubitPunchout.""" + figures = [] + fitting_report = "" + fig = go.Figure() + x, y, _ = data.grid(target) + fig.add_trace( + go.Heatmap( + x=x * HZ_TO_GHZ, + y=y, + z=data.normalized_signal(target).ravel(), + colorbar=dict(title="Normalized signal"), + colorscale="Viridis", + ) + ) + + fig.update_layout( + showlegend=True, + legend=dict(orientation="h"), + ) + + fig.update_xaxes(title_text="Drive frequency [GHz]") + fig.update_yaxes(title_text="Drive amplitude [a.u.]") + + figures.append(fig) + + return figures, fitting_report + + +qubit_amplitude_spectroscopy = Routine(_acquisition, _fit, _plot) +"""QubitAmplitudeSpectroscopy Routine object.""" diff --git a/src/qibocal/protocols/qubit_spectroscopies/qubit_amplitude_spectroscopy.py b/src/qibocal/protocols/qubit_spectroscopies/qubit_amplitude_spectroscopy.py new file mode 100644 index 0000000000..2c1109d9af --- /dev/null +++ b/src/qibocal/protocols/qubit_spectroscopies/qubit_amplitude_spectroscopy.py @@ -0,0 +1,162 @@ +from dataclasses import dataclass + +import numpy as np +import plotly.graph_objects as go +from qibolab import ( + AcquisitionType, + AveragingMode, + Delay, + Parameter, + PulseSequence, + Sweeper, +) + +from qibocal.auto.operation import Parameters, QubitId, Results, Routine +from qibocal.calibration import CalibrationPlatform + +from ...update import replace +from ..resonator_spectroscopies.resonator_punchout import ResonatorPunchoutData +from ..utils import HZ_TO_GHZ, readout_frequency +from .qubit_spectroscopy import QubitSpectroscopyResults + +__all__ = ["qubit_amplitude_spectroscopy"] + + +@dataclass +class QubitAmplitudeSpectroscopyParameters(Parameters): + """QubitPowerSpectroscopy runcard inputs.""" + + freq_width: int + """Width for frequency sweep relative to the drive frequency [Hz].""" + freq_step: int + """Frequency step for sweep [Hz].""" + min_amp: float + """Minimum amplitude.""" + max_amp: float + """Maximum amplitude.""" + step_amp: float + """Step amplitude.""" + duration: int + """Drive duration.""" + + +@dataclass +class QubitAmplitudeSpectroscopyData(ResonatorPunchoutData): + """QubitPowerSpectroscopy data acquisition.""" + + +def _acquisition( + params: QubitAmplitudeSpectroscopyParameters, + platform: CalibrationPlatform, + targets: list[QubitId], +) -> QubitAmplitudeSpectroscopyData: + """Perform a qubit spectroscopy experiment with different amplitudes. + + For high amplitude it should be possible to see more peaks: corresponding to + the (0-2)/2 frequency and the 1-2. + This experiment can be used also to test if a peak is a qubit: if it is, the + peak will get larger while increasing the power of the drive. + """ + # define the sequence: RX - MZ + sequence = PulseSequence() + ro_pulses = {} + qd_pulses = {} + freq_sweepers = {} + delta_frequency_range = np.arange( + -params.freq_width / 2, params.freq_width / 2, params.freq_step + ) + for qubit in targets: + natives = platform.natives.single_qubit[qubit] + qd_channel, qd_pulse = natives.RX()[0] + ro_channel, ro_pulse = natives.MZ()[0] + + qd_pulse = replace(qd_pulse, duration=params.duration) + + qd_pulses[qubit] = qd_pulse + ro_pulses[qubit] = ro_pulse + + sequence.append((qd_channel, qd_pulse)) + sequence.append((ro_channel, Delay(duration=qd_pulse.duration))) + sequence.append((ro_channel, ro_pulse)) + + f0 = platform.config(qd_channel).frequency + freq_sweepers[qubit] = Sweeper( + parameter=Parameter.frequency, + values=f0 + delta_frequency_range, + channels=[qd_channel], + ) + + amp_sweeper = Sweeper( + parameter=Parameter.amplitude, + range=(params.min_amp, params.max_amp, params.step_amp), + pulses=[qd_pulses[qubit] for qubit in targets], + ) + + # data + data = QubitAmplitudeSpectroscopyData( + resonator_type=platform.resonator_type, + amplitudes=amp_sweeper.values.tolist(), + frequencies={qubit: freq_sweepers[qubit].values.tolist() for qubit in targets}, + ) + + results = platform.execute( + [sequence], + [[amp_sweeper], [freq_sweepers[q] for q in targets]], + updates=[ + {platform.qubits[q].probe: {"frequency": readout_frequency(q, platform)}} + for q in targets + ], + nshots=params.nshots, + relaxation_time=params.relaxation_time, + acquisition_type=AcquisitionType.INTEGRATION, + averaging_mode=AveragingMode.CYCLIC, + ) + + # retrieve the results for every qubit + for qubit, ro_pulse in ro_pulses.items(): + # average signal, phase, i and q over the number of shots defined in the runcard + data.data[qubit] = results[ro_pulse.id] + + return data + + +def _fit(data: QubitAmplitudeSpectroscopyData) -> Results: + """Do not perform any fitting procedure.""" + return Results() + + +def _plot( + data: ResonatorPunchoutData, + target: QubitId, + fit: QubitSpectroscopyResults | None = None, +): + """Plot QubitPunchout.""" + figures = [] + fitting_report = "" + fig = go.Figure() + x, y, _ = data.grid(target) + fig.add_trace( + go.Heatmap( + x=x * HZ_TO_GHZ, + y=y, + z=data.normalized_signal(target).ravel(), + colorbar=dict(title="Normalized signal"), + colorscale="Viridis", + ) + ) + + fig.update_layout( + showlegend=True, + legend=dict(orientation="h"), + ) + + fig.update_xaxes(title_text="Drive frequency [GHz]") + fig.update_yaxes(title_text="Drive amplitude [a.u.]") + + figures.append(fig) + + return figures, fitting_report + + +qubit_amplitude_spectroscopy = Routine(_acquisition, _fit, _plot) +"""QubitAmplitudeSpectroscopy Routine object.""" diff --git a/src/qibocal/protocols/qubit_spectroscopies/qubit_broad_spectroscopy_conditional.py b/src/qibocal/protocols/qubit_spectroscopies/qubit_broad_spectroscopy_conditional.py new file mode 100644 index 0000000000..7bfbbfd282 --- /dev/null +++ b/src/qibocal/protocols/qubit_spectroscopies/qubit_broad_spectroscopy_conditional.py @@ -0,0 +1,314 @@ +from dataclasses import dataclass, field + +import numpy as np +import numpy.typing as npt +import plotly.graph_objects as go +from plotly.subplots import make_subplots +from qibolab import Custom, Pulse, PulseSequence + +from qibocal.auto.operation import QubitId, QubitPairId, Routine +from qibocal.calibration import CalibrationPlatform +from qibocal.protocols.qubit_spectroscopies.utils import ( + QubitSpectroscopyData, + QubitSpectroscopyParameters, + QubitSpectroscopyResults, + calculate_batches, + create_spectr_sweeper_and_updates, + spectroscopy_sequence, +) +from qibocal.protocols.utils import ( + HZ_TO_GHZ, + lorentzian_fit, +) +from qibocal.result import magnitude, phase + +__all__ = ["conditional_broad_spectator_spectroscopy"] + + +def envelope(duration: int, alpha: float, beta: float, interval: int): + x = np.arange(duration) + xp = x % interval / interval + return np.cos(2 * np.pi * (alpha * xp + beta) * xp) + + +ConditionalBroadSpectatorSpectrumType = np.dtype( + [ + ("freq", np.float64), + ("signal", np.float64), + ("phase", np.float64), + ("s_signal", np.float64), + ("s_phase", np.float64), + ] +) + + +@dataclass +class ConditionalBroadSpectatorSpectrumParameters(QubitSpectroscopyParameters): + """ConditionalBroadSpectatorSpectrum runcard inputs.""" + + spectator_pulse_amplitude: float = 0.0 + """Amplitude for spectator qubit pulse.""" + + +@dataclass +class ConditionalBroadSpectatorSpectrumResults(QubitSpectroscopyResults): + """ConditionalBroadSpectatorSpectrum outputs.""" + + spectator_pulse_amplitude: float = field(default_factory=float) + """Amplitude for spectator qubit pulse.""" + fitted_parameters: dict[QubitPairId, list[float]] = field(default_factory=dict) + """Raw fitting output.""" + + +@dataclass +class ConditionalBroadSpectatorSpectrumData(QubitSpectroscopyData): + """ConditionalBroadSpectatorSpectrum acquisition outputs.""" + + spectator_pulse_amplitude: float = field(default_factory=float) + """Amplitude for spectator qubit pulse.""" + data: dict[QubitPairId, npt.NDArray[ConditionalBroadSpectatorSpectrumType]] = field( + default_factory=dict + ) + "Raw data for ConditionalBroadSpectatorSpectrum." + + def register_qubit( + self, + pair: QubitPairId, + target_freq: npt.NDArray, + target_signal: npt.NDArray, + target_phase: npt.NDArray, + spectator_signal: npt.NDArray, + spectator_phase: npt.NDArray, + ): + """Create custom dtype array for acquired data.""" + ar = np.empty(target_freq.size, dtype=ConditionalBroadSpectatorSpectrumType) + + ar["freq"] = target_freq.ravel() + ar["signal"] = target_signal.ravel() + ar["phase"] = target_phase.ravel() + ar["s_signal"] = spectator_signal.ravel() + ar["s_phase"] = spectator_phase.ravel() + + self.data[pair] = np.rec.array(ar) + + +def _acquisition( + params: ConditionalBroadSpectatorSpectrumParameters, + platform: CalibrationPlatform, + targets: list[QubitPairId], +) -> ConditionalBroadSpectatorSpectrumData: + """Data acquisition for qubit spectroscopy. + + Handles wideband spectroscopy by batching when the frequency range exceeds ±300 MHz from the LO + """ + + if any(isinstance(x, (str, int)) for x in targets): + raise ValueError("At least one target is not a QubitPairId type.") + + target_qubits_list, spectator_qubits_list = map(list, zip(*targets)) + if any(s in target_qubits_list for s in spectator_qubits_list): + raise ValueError("One or multiple qubits are set as both spectator and target.") + + # Calculate batches + batches = calculate_batches(params.freq_width) + + spectro_seq, target_ro_seq, ro_pulses, drive_channels, los_channels, amplitudes = ( + spectroscopy_sequence( + params=params, + platform=platform, + targets=target_qubits_list, + ) + ) + + # Create data structure and aggregate results + data = ConditionalBroadSpectatorSpectrumData( + resonator_type=platform.resonator_type, + targets=targets, + amplitudes=amplitudes, + spectator_pulse_amplitude=params.spectator_pulse_amplitude, + ) + + # adding the spectators pulses + spectator_ro_pulses = {} + spectators_drive_sequence = PulseSequence() + spectators_ro_sequence = PulseSequence() + for spect_q in spectator_qubits_list: + spect_natives = platform.natives.single_qubit[spect_q] + + spect_drive_channel, _ = spect_natives.RX()[0] + spect_ro_channel, spect_ro_pulse = spect_natives.MZ()[0] + spectator_ro_pulses[spect_q] = spect_ro_pulse + + custom_i = envelope(spectro_seq.duration, 8, 0, 500) + spect_pulse = Pulse( + amplitude=params.spectator_pulse_amplitude, + duration=spectro_seq.duration, + envelope=Custom( + i_=custom_i, + q_=np.zeros_like(custom_i), + ), + ) + + # add custom pulse for the spectator qubit + spectators_drive_sequence.append((spect_drive_channel, spect_pulse)) + # appending readout pulse for spectator qubit + spectators_ro_sequence.append((spect_ro_channel, spect_ro_pulse)) + + complete_sequence = spectators_drive_sequence + spectro_seq + complete_sequence |= target_ro_seq + spectators_ro_sequence + + # Execute each batch + for start, end, lo_offset in batches: + delta_frequency_range = np.arange(start, end, params.freq_step) + + parsweep, batch_updates = create_spectr_sweeper_and_updates( + platform=platform, + targets=target_qubits_list, + drive_channels=drive_channels, + delta_frequency_range=delta_frequency_range, + los_channels=los_channels, + lo_offset=lo_offset, + ) + + # Execute this batch + results = platform.execute( + [complete_sequence], + [list(parsweep.values())], + updates=[batch_updates], + **params.execution_parameters, + ) + + # Collect results from this batch + for pair in targets: + t, s = pair + # Collect results for spectator qubits + spectator_result = results[spectator_ro_pulses[s].id] + spectator_signal = magnitude(spectator_result) + spectator_phase = phase(spectator_result) + + result = results[ro_pulses[t].id] + signal = magnitude(result) + _phase = phase(result) + + data.register_qubit( + pair=pair, + target_freq=parsweep[t].values, + target_signal=signal, + target_phase=_phase, + spectator_signal=spectator_signal, + spectator_phase=spectator_phase, + ) + + return data + + +def _fit( + data: ConditionalBroadSpectatorSpectrumData, +) -> ConditionalBroadSpectatorSpectrumResults: + """Post-processing function for ConditionalBroadSpectatorSpectrum.""" + frequency = {} + fitted_parameters = {} + for pair in data.data: + frequency[pair[0]] = {} + fitted_parameters[pair] = {} + fit_result = lorentzian_fit( + data[pair], resonator_type=data.resonator_type, fit="qubit" + ) + if fit_result is not None: + fit_freq, fit_params, _ = fit_result + + frequency[pair[0]] = fit_freq + fitted_parameters[pair] = fit_params + + return ConditionalBroadSpectatorSpectrumResults( + frequency=frequency, + fitted_parameters=fitted_parameters, + amplitude=data.amplitudes, + spectator_pulse_amplitude=data.spectator_pulse_amplitude, + ) + + +def _plot( + data: ConditionalBroadSpectatorSpectrumData, + target: QubitPairId, + fit: ConditionalBroadSpectatorSpectrumResults, +): + """Plotting function for ConditionalBroadSpectatorSpectrum.""" + + figures = [] + fitting_report = "" + + fig = make_subplots( + rows=2, + cols=2, + horizontal_spacing=0.1, + vertical_spacing=0.4, + subplot_titles=( + "Target Signal [a.u.]", + "Target Phase [rad]", + "Spectator Signal [a.u.]", + "Spectator Phase [rad]", + ), + ) + + fig.add_trace( + go.Scatter( + x=data.data[target].freq * HZ_TO_GHZ, + y=data.data[target].signal, + name="Target Signal", + legendgroup="Target Signal", + ), + row=1, + col=1, + ) + + fig.add_trace( + go.Scatter( + x=data.data[target].freq * HZ_TO_GHZ, + y=data.data[target].phase, + name="Target Phase", + legendgroup="Target Phase", + ), + row=1, + col=2, + ) + + fig.add_trace( + go.Scatter( + x=data.data[target].freq * HZ_TO_GHZ, + y=data.data[target].s_signal, + name="Spectator Signal", + legendgroup="Spectator Signal", + ), + row=2, + col=1, + ) + + fig.add_trace( + go.Scatter( + x=data.data[target].freq * HZ_TO_GHZ, + y=data.data[target].s_phase, + name="Spectator Phase", + legendgroup="Spectator Phase", + ), + row=2, + col=2, + ) + + fig.update_layout(title_text=f"Spectroscopy with {target[1]} as Spectator Qubit.") + figures.append(fig) + + return figures, fitting_report + + +def _update( + results: ConditionalBroadSpectatorSpectrumResults, + platform: CalibrationPlatform, + target: QubitId, +): + return + + +conditional_broad_spectator_spectroscopy = Routine(_acquisition, _fit, _plot, _update) +"""Qubit Spectroscopy routine. +""" diff --git a/src/qibocal/protocols/qubit_spectroscopies/qubit_spectroscopy_conditional.py b/src/qibocal/protocols/qubit_spectroscopies/qubit_spectroscopy_conditional.py new file mode 100644 index 0000000000..4cfe94ccbb --- /dev/null +++ b/src/qibocal/protocols/qubit_spectroscopies/qubit_spectroscopy_conditional.py @@ -0,0 +1,284 @@ +from dataclasses import dataclass, field + +import numpy as np +import numpy.typing as npt +import plotly.graph_objects as go +from plotly.subplots import make_subplots +from qibolab import PulseSequence + +from qibocal.auto.operation import QubitId, QubitPairId, Routine +from qibocal.calibration import CalibrationPlatform +from qibocal.protocols.qubit_spectroscopies.utils import ( + QubitSpectroscopyData, + QubitSpectroscopyParameters, + QubitSpectroscopyResults, + calculate_batches, + create_spectr_sweeper_and_updates, + spectroscopy_sequence, +) +from qibocal.protocols.utils import ( + HZ_TO_GHZ, + lorentzian_fit, +) +from qibocal.result import magnitude, phase + +__all__ = ["qubit_conditional_spectroscopy"] + + +ConditionalSpectrumType = np.dtype( + [ + ("freq", np.float64), + ("signal", np.float64), + ("phase", np.float64), + ("s_signal", np.float64), + ("s_phase", np.float64), + ] +) + + +@dataclass +class ConditionalSpectrumParameters(QubitSpectroscopyParameters): + """ConditionalSpectrum runcard inputs.""" + + +@dataclass +class ConditionalSpectrumResults(QubitSpectroscopyResults): + """ConditionalSpectrum outputs.""" + + fitted_parameters: dict[QubitPairId, list[float]] = field(default_factory=dict) + """Raw fitting output.""" + + +@dataclass +class ConditionalSpectrumData(QubitSpectroscopyData): + """ConditionalSpectrum acquisition outputs.""" + + data: dict[QubitPairId, npt.NDArray[ConditionalSpectrumType]] = field( + default_factory=dict + ) + "Raw data for ConditionalSpectrum." + + def register_qubit( + self, + pair: QubitPairId, + target_freq: npt.NDArray, + target_signal: npt.NDArray, + target_phase: npt.NDArray, + spectator_signal: npt.NDArray, + spectator_phase: npt.NDArray, + ): + """Create custom dtype array for acquired data.""" + ar = np.empty(target_freq.size, dtype=ConditionalSpectrumType) + + ar["freq"] = target_freq.ravel() + ar["signal"] = target_signal.ravel() + ar["phase"] = target_phase.ravel() + ar["s_signal"] = spectator_signal.ravel() + ar["s_phase"] = spectator_phase.ravel() + + self.data[pair] = np.rec.array(ar) + + +def _acquisition( + params: ConditionalSpectrumParameters, + platform: CalibrationPlatform, + targets: list[QubitPairId], +) -> ConditionalSpectrumData: + """Data acquisition for qubit spectroscopy. + + Handles wideband spectroscopy by batching when the frequency range exceeds ±300 MHz from the LO + """ + + if any(isinstance(x, (str, int)) for x in targets): + raise ValueError("At least one target is not a QubitPairId type.") + + target_qubits_list, spectator_qubits_list = map(list, zip(*targets)) + if any(s in target_qubits_list for s in spectator_qubits_list): + raise ValueError("One or multiple qubits are set as both spectator and target.") + + # Calculate batches + batches = calculate_batches(params.freq_width) + + spectro_seq, target_ro_seq, ro_pulses, drive_channels, los_channels, amplitudes = ( + spectroscopy_sequence( + params=params, + platform=platform, + targets=target_qubits_list, + ) + ) + + # Create data structure and aggregate results + data = ConditionalSpectrumData( + resonator_type=platform.resonator_type, + targets=targets, + amplitudes=amplitudes, + ) + + # adding the spectators pulses + spectator_ro_pulses = {} + spectators_drive_sequence = PulseSequence() + spectators_ro_sequence = PulseSequence() + for spect_q in spectator_qubits_list: + spect_natives = platform.natives.single_qubit[spect_q] + + spect_drive_channel, spect_pulse = spect_natives.RX()[0] + spect_ro_channel, spect_ro_pulse = spect_natives.MZ()[0] + + spectator_ro_pulses[spect_q] = spect_ro_pulse + + # add pi-pulse for the spectator qubit + spectators_drive_sequence.append((spect_drive_channel, spect_pulse)) + # appending readout pulse for spectator qubit + spectators_ro_sequence.append((spect_ro_channel, spect_ro_pulse)) + + complete_sequence = spectators_drive_sequence | spectro_seq + complete_sequence |= target_ro_seq + spectators_ro_sequence + + # Execute each batch + for start, end, lo_offset in batches: + delta_frequency_range = np.arange(start, end, params.freq_step) + + parsweep, batch_updates = create_spectr_sweeper_and_updates( + platform=platform, + targets=target_qubits_list, + drive_channels=drive_channels, + delta_frequency_range=delta_frequency_range, + los_channels=los_channels, + lo_offset=lo_offset, + ) + + # Execute this batch + results = platform.execute( + [complete_sequence], + [list(parsweep.values())], + updates=[batch_updates], + **params.execution_parameters, + ) + + # Collect results from this batch + for pair in targets: + t, s = pair + # Collect results for spectator qubits + spectator_result = results[spectator_ro_pulses[s].id] + spectator_signal = magnitude(spectator_result) + spectator_phase = phase(spectator_result) + + result = results[ro_pulses[t].id] + signal = magnitude(result) + _phase = phase(result) + + data.register_qubit( + pair=pair, + target_freq=parsweep[t].values, + target_signal=signal, + target_phase=_phase, + spectator_signal=spectator_signal, + spectator_phase=spectator_phase, + ) + + return data + + +def _fit(data: ConditionalSpectrumData) -> ConditionalSpectrumResults: + """Post-processing function for ConditionalSpectrum.""" + frequency = {} + fitted_parameters = {} + for pair in data.data: + frequency[pair[0]] = {} + fitted_parameters[pair] = {} + fit_result = lorentzian_fit( + data[pair], resonator_type=data.resonator_type, fit="qubit" + ) + if fit_result is not None: + fit_freq, fit_params, _ = fit_result + + frequency[pair[0]] = fit_freq + fitted_parameters[pair] = fit_params + + return ConditionalSpectrumResults( + frequency=frequency, + fitted_parameters=fitted_parameters, + amplitude=data.amplitudes, + ) + + +def _plot( + data: ConditionalSpectrumData, target: QubitPairId, fit: ConditionalSpectrumResults +): + """Plotting function for ConditionalSpectrum.""" + + figures = [] + fitting_report = "" + + fig = make_subplots( + rows=2, + cols=2, + horizontal_spacing=0.1, + vertical_spacing=0.4, + subplot_titles=( + "Target Signal [a.u.]", + "Target Phase [rad]", + "Spectator Signal [a.u.]", + "Spectator Phase [rad]", + ), + ) + + fig.add_trace( + go.Scatter( + x=data.data[target].freq * HZ_TO_GHZ, + y=data.data[target].signal, + name="Target Signal", + legendgroup="Target Signal", + ), + row=1, + col=1, + ) + + fig.add_trace( + go.Scatter( + x=data.data[target].freq * HZ_TO_GHZ, + y=data.data[target].phase, + name="Target Phase", + legendgroup="Target Phase", + ), + row=1, + col=2, + ) + + fig.add_trace( + go.Scatter( + x=data.data[target].freq * HZ_TO_GHZ, + y=data.data[target].s_signal, + name="Spectator Signal", + legendgroup="Spectator Signal", + ), + row=2, + col=1, + ) + + fig.add_trace( + go.Scatter( + x=data.data[target].freq * HZ_TO_GHZ, + y=data.data[target].s_phase, + name="Spectator Phase", + legendgroup="Spectator Phase", + ), + row=2, + col=2, + ) + + fig.update_layout(title_text=f"Spectroscopy with {target[1]} as Spectator Qubit.") + figures.append(fig) + + return figures, fitting_report + + +def _update( + results: ConditionalSpectrumResults, platform: CalibrationPlatform, target: QubitId +): + return + + +qubit_conditional_spectroscopy = Routine(_acquisition, _fit, _plot, _update) +"""Qubit Spectroscopy routine. +""" diff --git a/src/qibocal/protocols/qubit_spectroscopies/qubits_spectroscopy_spectator.py b/src/qibocal/protocols/qubit_spectroscopies/qubits_spectroscopy_spectator.py new file mode 100644 index 0000000000..c3f0238d54 --- /dev/null +++ b/src/qibocal/protocols/qubit_spectroscopies/qubits_spectroscopy_spectator.py @@ -0,0 +1,287 @@ +from dataclasses import dataclass, field + +import numpy as np +import numpy.typing as npt +import plotly.graph_objects as go +from plotly.subplots import make_subplots + +from qibocal.auto.operation import QubitPairId, Routine +from qibocal.calibration import CalibrationPlatform +from qibocal.protocols.qubit_spectroscopies.utils import ( + QubitSpectroscopyData, + QubitSpectroscopyParameters, + QubitSpectroscopyResults, + calculate_batches, + create_spectr_sweeper_and_updates, + spectroscopy_sequence, +) +from qibocal.protocols.utils import HZ_TO_GHZ +from qibocal.result import unpack + +__all__ = ["qubit_spectroscopy_spectator_scan"] + +SpectatorFreqScanType = np.dtype( + [ + ("t_freq", np.float64), + ("t_i", np.float64), + ("t_q", np.float64), + ("s_freq", np.float64), + ("s_i", np.float64), + ("s_q", np.float64), + ] +) + + +@dataclass +class SpectatorFreqScanParameters(QubitSpectroscopyParameters): + """SpectatorFreqScan runcard inputs.""" + + +@dataclass +class SpectatorFreqScanResults(QubitSpectroscopyResults): + """SpectatorFreqScan outputs.""" + + +@dataclass +class SpectatorFreqScanData(QubitSpectroscopyData): + """SpectatorFreqScan acquisition outputs.""" + + data: dict[QubitPairId, npt.NDArray[SpectatorFreqScanType]] = field( + default_factory=dict + ) + "Raw data for SpectatorFreqScan." + + def register_qubit( + self, + pair: QubitPairId, + target_freq: npt.NDArray, + spectator_freq: npt.NDArray, + target_i: npt.NDArray, + target_q: npt.NDArray, + spectator_i: npt.NDArray, + spectator_q: npt.NDArray, + ): + """Create custom dtype array for acquired data.""" + size = target_freq.size * spectator_freq.size + ar = np.empty(size, dtype=SpectatorFreqScanType) + + t_freqs, s_freqs = np.meshgrid(target_freq, spectator_freq) + + ar["t_freq"] = t_freqs.ravel() + ar["t_i"] = target_i.ravel() + ar["t_q"] = target_q.ravel() + ar["s_freq"] = s_freqs.ravel() + ar["s_i"] = spectator_i.ravel() + ar["s_q"] = spectator_q.ravel() + + self.data[pair] = np.rec.array(ar) + + def compute_magnitude(self, qubit_pair: QubitPairId) -> npt.NDArray: + """Compute magnitude of the measured signal""" + return np.sqrt(self[qubit_pair].i ** 2 + self[qubit_pair].q ** 2) + + def compute_phase(self, qubit_pair: QubitPairId) -> npt.NDArray: + """Compute phase of the measured signal""" + return np.unwrap(np.arctan2(self[qubit_pair].i, self[qubit_pair].q)) + + +def _acquisition( + params: SpectatorFreqScanParameters, + platform: CalibrationPlatform, + targets: list[QubitPairId], +) -> SpectatorFreqScanData: + """Data acquisition for qubit spectroscopy. + + Handles wideband spectroscopy by batching when the frequency range exceeds ±300 MHz from the LO + """ + + if any(isinstance(x, (str, int)) for x in targets): + raise ValueError("At least one target is not a QubitPairId type.") + + target_qubits_list, spectator_qubits_list = map(list, zip(*targets)) + if any(s in target_qubits_list for s in spectator_qubits_list): + raise ValueError("One or multiple qubits are set as both spectator and target.") + + # Calculate batches + batches = calculate_batches(params.freq_width) + + spectro_seq, targ_ro_seq, ro_pulses, drive_channels, los_channels, amplitudes = ( + spectroscopy_sequence( + params=params, + platform=platform, + targets=target_qubits_list, + ) + ) + + # Create data structure and aggregate results + data = SpectatorFreqScanData( + resonator_type=platform.resonator_type, + targets=targets, + amplitudes=amplitudes, + ) + + # executing the experiment for each spectator qubit separately + ( + spectator_seq, + spectator_ro_seq, + spectator_ro_pulses, + spectator_drive_ch, + spectator_los_ch, + _, + ) = spectroscopy_sequence( + params=params, + platform=platform, + targets=spectator_qubits_list, + ) + + complete_seq = spectator_seq + spectro_seq + complete_seq |= spectator_ro_seq + targ_ro_seq + + # Execute each batch + for start, end, lo_offset in batches: + delta_frequency_range = np.arange(start, end, params.freq_step) + + # sweepers and updates for target qubits + q_parsweep, q_batch_updates = create_spectr_sweeper_and_updates( + platform=platform, + targets=targets, + drive_channels=drive_channels, + delta_frequency_range=delta_frequency_range, + los_channels=los_channels, + lo_offset=lo_offset, + ) + + # sweepers and updates for spectators qubits + spectator_parsweep, spectator_batch_updates = create_spectr_sweeper_and_updates( + platform=platform, + targets=spectator_qubits_list, + drive_channels=spectator_drive_ch, + delta_frequency_range=delta_frequency_range, + los_channels=spectator_los_ch, + lo_offset=lo_offset, + ) + + # Execute this batch + results = platform.execute( + [complete_seq], + [list(spectator_parsweep.values()), list(q_parsweep.values())], + updates=[q_batch_updates, spectator_batch_updates], + **params.execution_parameters, + ) + + # Collect results from this batch + for pair in targets: + targ, spect = pair + + target_result = results[ro_pulses[targ].id] + target_i, target_q = unpack(target_result) + + spectator_result = results[spectator_ro_pulses[spect].id] + spectator_i, spectator_q = unpack(spectator_result) + + data.register_qubit( + pair=pair, + target_freq=q_parsweep[targ].values, + spectator_freq=spectator_parsweep[spect].values, + target_i=target_i, + target_q=target_q, + spectator_i=spectator_i, + spectator_q=spectator_q, + ) + + return data + + +def _fit(data: SpectatorFreqScanData) -> SpectatorFreqScanResults: + """Post-processing function for SpectatorFreqScan.""" + return SpectatorFreqScanResults() + + +def _plot( + data: SpectatorFreqScanData, target: QubitPairId, fit: SpectatorFreqScanResults +): + """Plotting function for SpectatorFreqScan.""" + + figures = [] + fitting_report = "" + + fig = make_subplots( + rows=2, + cols=2, + horizontal_spacing=0.1, + vertical_spacing=0.4, + subplot_titles=( + "Target Signal [a.u.]", + "Target Phase [rad]", + "Spectator Signal [a.u.]", + "Spectator Phase [rad]", + ), + ) + + fig.add_trace( + go.Heatmap( + x=data[target].t_freq * HZ_TO_GHZ, + y=data[target].s_freq * HZ_TO_GHZ, + z=data.compute_magnitude(target), + name="Target Signal", + legendgroup="Target Signal", + ), + row=1, + col=1, + ) + + fig.add_trace( + go.Heatmap( + x=data[target].t_freq * HZ_TO_GHZ, + y=data[target].s_freq * HZ_TO_GHZ, + z=data.compute_phase(target), + name="Target Phase", + legendgroup="Target Phase", + ), + row=1, + col=2, + ) + + fig.add_trace( + go.Heatmap( + x=data[target].t_freq * HZ_TO_GHZ, + y=data[target].s_freq * HZ_TO_GHZ, + z=data.compute_magnitude(target), + name="Spectator Signal", + legendgroup="Spectator Signal", + ), + row=2, + col=1, + ) + + fig.add_trace( + go.Heatmap( + x=data[target].t_freq * HZ_TO_GHZ, + y=data[target].s_freq * HZ_TO_GHZ, + z=data.compute_phase(target), + name="Spectator Phase", + legendgroup="Spectator Phase", + ), + row=2, + col=2, + ) + + fig.update_layout( + title_text=f"Spectroscopy 2D scan with {target[1]} as Spectator Qubit." + ) + figures.append(fig) + + return figures, fitting_report + + +def _update( + results: SpectatorFreqScanResults, + platform: CalibrationPlatform, + target: QubitPairId, +): + return + + +qubit_spectroscopy_spectator_scan = Routine(_acquisition, _fit, _plot, _update) +"""Qubit Spectroscopy routine. +""" diff --git a/src/qibocal/protocols/qubit_spectroscopies/utils.py b/src/qibocal/protocols/qubit_spectroscopies/utils.py new file mode 100644 index 0000000000..6febbce53e --- /dev/null +++ b/src/qibocal/protocols/qubit_spectroscopies/utils.py @@ -0,0 +1,173 @@ +from dataclasses import dataclass, field + +import numpy as np +import numpy.typing as npt +from qibolab import ( + Parameter, + PulseLike, + PulseSequence, + Sweeper, +) +from qibolab._core.components import IqChannel +from qibolab._core.identifier import ChannelId + +from qibocal.auto.operation import Parameters, QubitId, Results +from qibocal.calibration import CalibrationPlatform +from qibocal.update import replace + +from ..resonator_spectroscopies.resonator_spectroscopy import ( + ResonatorSpectroscopyData, + ResSpecType, +) + +__all__ = [ + "QubitSpectroscopyParameters", + "QubitSpectroscopyResults", + "QubitSpectroscopyData", + "calculate_batches", + "create_spectr_sweeper_and_updates", + "QubitSpectrumType", +] + +QubitSpectrumType = ResSpecType + + +@dataclass +class QubitSpectroscopyParameters(Parameters): + """QubitSpectroscopy runcard inputs.""" + + freq_width: int + """Width [Hz] for frequency sweep relative to the qubit frequency.""" + freq_step: int + """Frequency [Hz] step for sweep.""" + drive_duration: int + """Drive pulse duration [ns]. Same for all qubits.""" + drive_amplitude: float | None = None + """Drive pulse amplitude (optional). Same for all qubits.""" + hardware_average: bool = True + """By default hardware average will be performed.""" + + +@dataclass +class QubitSpectroscopyResults(Results): + """QubitSpectroscopy outputs.""" + + frequency: dict[QubitId, float] = field(default_factory=dict) + """Drive frequecy [GHz] for each qubit.""" + amplitude: dict[QubitId, float] = field(default_factory=dict) + """Input drive amplitude. Same for all qubits.""" + fitted_parameters: dict[QubitId, list[float]] = field(default_factory=dict) + """Raw fitting output.""" + + +@dataclass +class QubitSpectroscopyData(ResonatorSpectroscopyData): + """QubitSpectroscopy acquisition outputs.""" + + targets: list[QubitId] = field(default_factory=list) + """List of qubits targeted in the experiment.""" + data: dict[QubitId, npt.NDArray[QubitSpectrumType]] = field(default_factory=dict) + """Raw data acquired.""" + + +def spectroscopy_sequence( + params: QubitSpectroscopyParameters, + platform: CalibrationPlatform, + targets: list[QubitId], +) -> tuple[ + PulseSequence, + PulseSequence, + dict[QubitId, PulseLike], + dict[QubitId, ChannelId], + dict[QubitId, ChannelId], + dict[QubitId, float], +]: + # Build the pulse sequence + sequence = PulseSequence() + ro_sequence = PulseSequence() + ro_pulses = {} + drive_channels = {} + los_channels = {} + drive_amplitudes = {} + + for qubit in targets: + natives = platform.natives.single_qubit[qubit] + qd_channel, qd_pulse = natives.RX()[0] + ro_channel, ro_pulse = natives.MZ()[0] + + qd_pulse = replace(qd_pulse, duration=params.drive_duration) + if params.drive_amplitude is not None: + qd_pulse = replace(qd_pulse, amplitude=params.drive_amplitude) + + ro_pulses[qubit] = ro_pulse + drive_channels[qubit] = qd_channel + drive_amplitudes[qubit] = qd_pulse.amplitude + + # Get the LO channel associated with this drive channel + channel_obj = platform.channels[qd_channel] + if isinstance(channel_obj, IqChannel): + los_channels[qubit] = channel_obj.lo + else: + los_channels[qubit] = None + + sequence.append((qd_channel, qd_pulse)) + + ro_sequence.append((ro_channel, ro_pulse)) + + return ( + sequence, + ro_sequence, + ro_pulses, + drive_channels, + los_channels, + drive_amplitudes, + ) + + +def calculate_batches(freq_width: int, max_if_bandwidth: int = 300_000_000): + """ + Calculate frequency batches for wideband spectroscopy. + + """ + batch_starts = np.arange(-freq_width / 2, freq_width / 2, 2 * max_if_bandwidth) + batch_ends = np.append(batch_starts[1:], freq_width / 2) + batch_limits = np.stack((batch_starts, batch_ends)) + lo_offsets = batch_limits.sum(axis=0) / 2 if len(batch_starts) > 1 else [0] + return np.vstack((batch_limits, lo_offsets)).T + + +def create_spectr_sweeper_and_updates( + platform: CalibrationPlatform, + targets: list[QubitId], + drive_channels: dict[ChannelId], + delta_frequency_range: npt.NDArray, + los_channels: dict[ChannelId], + lo_offset: float, +) -> tuple[dict[QubitId, Sweeper], dict[str, float]]: + """Create a sweeper dictionary configuration for spectroscopy measurements on multiple qubits. + + This function creates a parallel sweeper that sweeps the frequency of drive channels + across a specified range for each target qubit. It also prepares batch updates to + synchronize the LO (local oscillator) frequencies if needed. + The drive channel frequency is updated to f0 + lo_offset to avoid validation errors. + LO channel frequencies are only updated if lo_offset is non-zero and the LO channel exists. + """ + + parsweep = {} + batch_updates = {} + for q in targets: + f0 = platform.config(drive_channels[q]).frequency + parsweep[q] = Sweeper( + parameter=Parameter.frequency, + values=f0 + delta_frequency_range, + channels=[drive_channels[q]], + ) + + # Update the frequency of the drive channel to avoid raising a validation an error + batch_updates[drive_channels[q]] = {"frequency": f0 + lo_offset} + + # If we're batching, update the LO + if lo_offset != 0 and los_channels[q] is not None: + batch_updates[los_channels[q]] = {"frequency": f0 + lo_offset} + + return parsweep, batch_updates diff --git a/src/qibocal/protocols/rabi/__init__.py b/src/qibocal/protocols/rabi/__init__.py index 83e0a7ec1b..d02733888f 100644 --- a/src/qibocal/protocols/rabi/__init__.py +++ b/src/qibocal/protocols/rabi/__init__.py @@ -2,6 +2,10 @@ from .amplitude_frequency import rabi_amplitude_frequency from .amplitude_frequency_signal import rabi_amplitude_frequency_signal from .amplitude_signal import rabi_amplitude_signal +from .conditional_chevron_ampl import conditional_rabi_chevron_ampl +from .conditional_chevron_ampl_signal import conditional_rabi_chevron_ampl_signal +from .conditional_chevron_length_signal import conditional_rabi_chevron_len_signal +from .conditional_cross_chevron_ampl import conditional_crossrabi_chevron_ampl from .ef import rabi_amplitude_ef from .length import rabi_length from .length_frequency import rabi_length_frequency @@ -18,4 +22,8 @@ "rabi_length_frequency", "rabi_length_signal", "rabi_length", + "conditional_rabi_chevron_len_signal", + "conditional_rabi_chevron_ampl_signal", + "conditional_rabi_chevron_ampl", + "conditional_crossrabi_chevron_ampl", ] diff --git a/src/qibocal/protocols/rabi/conditional_chevron_ampl.py b/src/qibocal/protocols/rabi/conditional_chevron_ampl.py new file mode 100644 index 0000000000..7879cb4598 --- /dev/null +++ b/src/qibocal/protocols/rabi/conditional_chevron_ampl.py @@ -0,0 +1,284 @@ +"""Rabi experiment that sweeps amplitude and frequency when toggling a spectator qubit.""" + +from dataclasses import dataclass, field + +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, + PulseSequence, + Sweeper, +) + +from qibocal.auto.operation import ( + Data, + Parameters, + QubitId, + QubitPairId, + Results, + Routine, +) +from qibocal.calibration import CalibrationPlatform + +from ..utils import HZ_TO_GHZ +from .utils import sequence_amplitude + +__all__ = ["conditional_rabi_chevron_ampl"] + + +@dataclass +class ConditionalRabiChevronAmplParameters(Parameters): + """ConditionalRabiAmplChevron runcard inputs.""" + + amplitude_start: float + """Initial pi pulse amplitude [a.u.].""" + amplitude_end: float + """Final pi amplitude amplitude [a.u.].""" + amplitude_step: float + """Step pi pulse amplitude [a.u.].""" + min_freq: int + """Minimum frequency as an offset.""" + max_freq: int + """Maximum frequency as an offset.""" + step_freq: int + """Frequency to use as step for the scan.""" + activate_spectators: bool = True + """Flag for setting spectator qbuit in state 1.""" + pulse_length: float | None = None + """Pi pulse duration. Same for all qubits.""" + + @property + def frequency_range(self): + return np.arange( + self.min_freq, + self.max_freq, + self.step_freq, + ) + + @property + def amplitude_range(self): + return np.arange( + self.amplitude_start, + self.amplitude_end, + self.amplitude_step, + ) + + +@dataclass +class ConditionalRabiChevronAmplResults(Results): + """ConditionalRabiAmplChevron outputs.""" + + activate_spectators: bool + """Flag for setting spectator qbuit in state 1.""" + amplitude: dict[QubitPairId, float | list[float]] + """Pi pulse duration for each qubit.""" + fitted_parameters: dict[QubitPairId, dict[str, float]] + """Raw fitting output.""" + + +CondRabiAmplChevronType = np.dtype( + [ + ("ampl", np.float64), + ("freq", np.float64), + ("target", np.float64), + ("spectator", np.float64), + ] +) +"""Custom dtype for rabi amplitude with spectator.""" + + +@dataclass +class ConditionalRabiChevronAmplData(Data): + """ConditionalRabiAmplChevron data acquisition.""" + + activate_spectators: bool + """Flag for setting spectator qbuit in state 1.""" + data: dict[QubitPairId, npt.NDArray[CondRabiAmplChevronType]] = field( + default_factory=dict + ) + """Raw data acquired.""" + + def register_qubit(self, pair, freq, ampl, p_targ, p_spect): + """Store output for single qubit.""" + size = len(freq) * len(ampl) + amplitude, frequency = np.meshgrid(ampl, freq) + data = np.empty(size, dtype=CondRabiAmplChevronType) + data["freq"] = frequency.ravel() + data["ampl"] = amplitude.ravel() + data["target"] = p_targ.ravel() + data["spectator"] = p_spect.ravel() + self.data[pair] = np.rec.array(data) + + def amplitudes(self, qubit_pair: QubitPairId) -> npt.NDArray: + """Unique qubit amplitudes.""" + return np.unique(self[qubit_pair].ampl) + + def frequencies(self, qubit_pair: QubitPairId) -> npt.NDArray: + """Unique qubit frequency.""" + return np.unique(self[qubit_pair].freq) + + +def _acquisition( + params: ConditionalRabiChevronAmplParameters, + platform: CalibrationPlatform, + targets: list[QubitPairId], +) -> ConditionalRabiChevronAmplData: + """Data acquisition for ConditionalRabiChevron.""" + + if any(isinstance(x, (str, int)) for x in targets): + raise ValueError("At least one target is not a QubitPairId type.") + + target_qubits_list, spectator_qubits_list = map(list, zip(*targets)) + if any(s in target_qubits_list for s in spectator_qubits_list): + raise ValueError("One or multiple qubits are set as both spectator and target.") + + complete_sequence, t_qd_pulses, t_ro_pulses, _ = sequence_amplitude( + target_qubits_list, params, platform, False + ) + + spectators_drive_dict: dict[QubitId, PulseSequence] = {} + spectators_ro_dict: dict[QubitId, PulseSequence] = {} + for s in spectator_qubits_list: + spectator_natives = platform.natives.single_qubit[s] + spectators_drive_dict[s] = spectator_natives.RX()[0] + spectators_ro_dict[s] = spectator_natives.MZ()[0] + + complete_sequence += PulseSequence( + (spectators_ro_dict[s][0], Delay(duration=t_qd_pulses[t].duration)) + for t, s in targets + ) + complete_sequence += PulseSequence(list(spectators_ro_dict.values())) + + if params.activate_spectators: + complete_sequence = ( + PulseSequence(list(spectators_drive_dict.values())) | complete_sequence + ) + + ampl_sweeper = Sweeper( + parameter=Parameter.amplitude, + values=params.amplitude_range, + pulses=[t_qd_pulses[q] for q in target_qubits_list], + ) + + freq_sweepers: dict[QubitId, Sweeper] = {} + for t in target_qubits_list: + target_drive_ch = platform.qubits[t].drive + freq_sweepers[t] = Sweeper( + parameter=Parameter.frequency, + values=platform.config(target_drive_ch).frequency + params.frequency_range, + channels=[target_drive_ch], + ) + + data = ConditionalRabiChevronAmplData( + activate_spectators=params.activate_spectators + ) + + results = platform.execute( + [complete_sequence], + [list(freq_sweepers.values()), [ampl_sweeper]], + nshots=params.nshots, + relaxation_time=params.relaxation_time, + acquisition_type=AcquisitionType.DISCRIMINATION, + averaging_mode=AveragingMode.CYCLIC, + ) + for pair in targets: + t, s = pair + target_result = results[t_ro_pulses[t].id] + spect_result = results[spectators_ro_dict[s][1].id] + + data.register_qubit( + pair=pair, + freq=freq_sweepers[t].values, + ampl=ampl_sweeper.values, + p_targ=target_result, + p_spect=spect_result, + ) + return data + + +def _fit(data: ConditionalRabiChevronAmplData) -> ConditionalRabiChevronAmplResults: + """Do not perform any fitting procedure.""" + + return ConditionalRabiChevronAmplResults( + activate_spectators=data.activate_spectators, + amplitude={}, + fitted_parameters={}, + ) + + +def _plot( + data: ConditionalRabiChevronAmplData, + target: QubitPairId, + fit: ConditionalRabiChevronAmplResults | None = None, +): + """Plotting function for ConditionalRabiChevron.""" + figures = [] + fitting_report = "" + fig = make_subplots( + rows=1, + cols=2, + horizontal_spacing=0.1, + vertical_spacing=0.2, + subplot_titles=( + ("PI pulse applied -" if data.activate_spectators else "") + + f" Prob Target {target[0]}", + ("PI pulse applied -" if data.activate_spectators else "") + + f" Prob Spectator {target[1]}", + ), + ) + qubit_data = data[target] + frequencies = qubit_data.freq * HZ_TO_GHZ + amplitudes = qubit_data.ampl + targ_prob = data[target].target + spect_prob = data[target].spectator + + fig.add_trace( + go.Heatmap( + x=amplitudes, + y=frequencies, + z=targ_prob, + colorbar_x=0.46, + ), + row=1, + col=1, + ) + + fig.add_trace( + go.Heatmap( + x=amplitudes, + y=frequencies, + z=spect_prob, + colorbar_x=1.01, + ), + row=1, + col=2, + ) + + fig.update_xaxes(title_text="Amplitude [a.u.]", row=1, col=1) + fig.update_xaxes(title_text="Amplitude [a.u.]", row=1, col=2) + fig.update_yaxes(title_text="Frequency [GHz]", row=1, col=1) + + fig.update_layout( + showlegend=False, + legend={"orientation": "h"}, + ) + figures.append(fig) + + return figures, fitting_report + + +def _update( + results: ConditionalRabiChevronAmplResults, + platform: CalibrationPlatform, + target: QubitPairId, +): + return + + +conditional_rabi_chevron_ampl = Routine(_acquisition, _fit, _plot, _update) +"""Rabi length with frequency tuning.""" diff --git a/src/qibocal/protocols/rabi/conditional_chevron_ampl_signal.py b/src/qibocal/protocols/rabi/conditional_chevron_ampl_signal.py new file mode 100644 index 0000000000..14f202876a --- /dev/null +++ b/src/qibocal/protocols/rabi/conditional_chevron_ampl_signal.py @@ -0,0 +1,400 @@ +"""Rabi experiment that sweeps length and frequency.""" + +from dataclasses import dataclass, field + +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, + PulseSequence, + Sweeper, +) + +from qibocal.auto.operation import ( + Data, + Parameters, + QubitId, + QubitPairId, + Results, + Routine, +) +from qibocal.calibration import CalibrationPlatform +from qibocal.config import log + +from ...result import unpack +from ..utils import HZ_TO_GHZ, readout_frequency +from .utils import fit_amplitude_function, rabi_initial_guess, sequence_amplitude + +__all__ = ["conditional_rabi_chevron_ampl_signal"] + + +@dataclass +class ConditionalRabiChevronAmplSignalParameters(Parameters): + """ConditionalRabiChevron runcard inputs.""" + + amplitude_start: float + """Initial pi pulse amplitude [a.u.].""" + amplitude_end: float + """Final pi amplitude amplitude [a.u.].""" + amplitude_step: float + """Step pi pulse amplitude [a.u.].""" + min_freq: int + """Minimum frequency as an offset.""" + max_freq: int + """Maximum frequency as an offset.""" + step_freq: int + """Frequency to use as step for the scan.""" + activate_spectators: bool = True + """Flag for setting spectator qbuit in state 1.""" + pulse_length: float | None = None + """Pi pulse duration. Same for all qubits.""" + + @property + def frequency_range(self): + return np.arange( + self.min_freq, + self.max_freq, + self.step_freq, + ) + + @property + def amplitude_range(self): + return np.arange( + self.amplitude_start, + self.amplitude_end, + self.amplitude_step, + ) + + +@dataclass +class ConditionalRabiChevronAmplSignalResults(Results): + """ConditionalRabiChevron outputs.""" + + activate_spectators: bool + """Flag for setting spectator qbuit in state 1.""" + amplitude: dict[QubitPairId, float | list[float]] + """Pi pulse duration for each qubit.""" + fitted_parameters: dict[QubitPairId, dict[str, float]] + """Raw fitting output.""" + + +CondRabiAmplSigChevronType = np.dtype( + [ + ("ampl", np.float64), + ("freq", np.float64), + ("t_i", np.float64), + ("t_q", np.float64), + ("s_i", np.float64), + ("s_q", np.float64), + ] +) +"""Custom dtype for rabi length.""" + + +@dataclass +class RabiLengthFreqSignalData(Data): + """RabiLengthFreqSignal data acquisition.""" + + activate_spectators: bool + """Flag for setting spectator qbuit in state 1.""" + data: dict[QubitPairId, npt.NDArray[CondRabiAmplSigChevronType]] = field( + default_factory=dict + ) + """Raw data acquired.""" + + def register_qubit(self, pair, freq, ampl, ti, tq, si, sq): + """Store output for single qubit.""" + size = len(freq) * len(ampl) + amplitude, frequency = np.meshgrid(ampl, freq) + data = np.empty(size, dtype=CondRabiAmplSigChevronType) + data["freq"] = frequency.ravel() + data["ampl"] = amplitude.ravel() + data["t_i"] = ti.ravel() + data["t_q"] = tq.ravel() + data["s_i"] = si.ravel() + data["s_q"] = sq.ravel() + self.data[pair] = np.rec.array(data) + + def amplitudes(self, qubit_pair: QubitPairId) -> npt.NDArray: + """Unique qubit lengths.""" + return np.unique(self[qubit_pair].ampl) + + def frequencies(self, qubit_pair: QubitPairId) -> npt.NDArray: + """Unique qubit frequency.""" + return np.unique(self[qubit_pair].freq) + + def compute_target_magnitude(self, qubit_pair: QubitPairId) -> npt.NDArray: + """Compute magnitude of the target qubit signal""" + return np.sqrt(self[qubit_pair].t_i ** 2 + self[qubit_pair].t_q ** 2) + + def compute_target_phase(self, qubit_pair: QubitPairId) -> npt.NDArray: + """Compute phase of the target qubit signal""" + return np.unwrap(np.arctan2(self[qubit_pair].t_i, self[qubit_pair].t_q)) + + def compute_spectator_magnitude(self, qubit_pair: QubitPairId) -> npt.NDArray: + """Compute magnitude of the spectator qubit signal""" + return np.sqrt(self[qubit_pair].s_i ** 2 + self[qubit_pair].s_q ** 2) + + def compute_spectator_phase(self, qubit_pair: QubitPairId) -> npt.NDArray: + """Compute phase of the spectator qubit signal""" + return np.unwrap(np.arctan2(self[qubit_pair].s_i, self[qubit_pair].s_q)) + + +def _acquisition( + params: ConditionalRabiChevronAmplSignalParameters, + platform: CalibrationPlatform, + targets: list[QubitPairId], +) -> RabiLengthFreqSignalData: + """Data acquisition for Rabi experiment sweeping length.""" + + if any(isinstance(x, (str, int)) for x in targets): + raise ValueError("At least one target is not a QubitPairId type.") + + target_qubits_list, spectator_qubits_list = map(list, zip(*targets)) + if any(s in target_qubits_list for s in spectator_qubits_list): + raise ValueError("One or multiple qubits are set as both spectator and target.") + + t_sequence, t_qd_pulses, t_ro_pulses, _ = sequence_amplitude( + target_qubits_list, params, platform, False + ) + + complete_sequence = PulseSequence() + spectators_drive_dict: dict[QubitId, PulseSequence] = {} + spectators_ro_dict: dict[QubitId, PulseSequence] = {} + for s in spectator_qubits_list: + spectator_natives = platform.natives.single_qubit[s] + spectators_drive_dict[s] = spectator_natives.RX()[0] + spectators_ro_dict[s] = spectator_natives.MZ()[0] + + if params.activate_spectators: + complete_sequence += PulseSequence(list(spectators_drive_dict.values())) + + complete_sequence |= t_sequence + complete_sequence += PulseSequence( + (spectators_ro_dict[s][0], Delay(duration=t_qd_pulses[t].duration)) + for t, s in targets + ) + complete_sequence += PulseSequence(list(spectators_ro_dict.values())) + + ampl_sweeper = Sweeper( + parameter=Parameter.amplitude, + values=params.amplitude_range, + pulses=[t_qd_pulses[q] for q in target_qubits_list], + ) + + freq_sweepers: dict[QubitId, Sweeper] = {} + for t in target_qubits_list: + target_drive_ch = platform.qubits[t].drive + freq_sweepers[t] = Sweeper( + parameter=Parameter.frequency, + values=platform.config(target_drive_ch).frequency + params.frequency_range, + channels=[target_drive_ch], + ) + + data = RabiLengthFreqSignalData(activate_spectators=params.activate_spectators) + + results = platform.execute( + [complete_sequence], + [list(freq_sweepers.values()), [ampl_sweeper]], + updates=[ + {platform.qubits[q].probe: {"frequency": readout_frequency(q, platform)}} + for q in target_qubits_list + ], + nshots=params.nshots, + relaxation_time=params.relaxation_time, + acquisition_type=AcquisitionType.INTEGRATION, + averaging_mode=AveragingMode.CYCLIC, + ) + for pair in targets: + targ_result = results[t_ro_pulses[pair[0]].id] + t_i, t_q = unpack(targ_result) + + spect_result = results[spectators_ro_dict[pair[1]][1].id] + s_i, s_q = unpack(spect_result) + + data.register_qubit( + pair=pair, + freq=freq_sweepers[pair[0]].values, + ampl=ampl_sweeper.values, + ti=t_i, + tq=t_q, + si=s_i, + sq=s_q, + ) + return data + + +def _fit(data: RabiLengthFreqSignalData) -> ConditionalRabiChevronAmplSignalResults: + """Do not perform any fitting procedure.""" + fitted_frequencies = {} + fitted_amplitudes = {} + fitted_parameters = {} + + for pair in data.data: + amplitudes = data.amplitudes(pair) + freqs = data.frequencies(pair) + t_mag = data.compute_target_magnitude(pair) + t_mag_matrix = t_mag.reshape(len(amplitudes), len(freqs)).T + + # guess optimal frequency maximizing oscillatio amplitude + index = np.argmax([max(x) - min(x) for x in t_mag_matrix]) + frequency = freqs[index] + + y = t_mag_matrix[index] + + y_min = np.min(y) + y_max = np.max(y) + x_min = np.min(amplitudes) + x_max = np.max(amplitudes) + x = (amplitudes - x_min) / (x_max - x_min) + y = (y - y_min) / (y_max - y_min) + + pguess = rabi_initial_guess(x, y, "amplitude", signal=True) + + try: + popt, _, pi_pulse_parameter = fit_amplitude_function( + x, + y, + pguess, + signal=True, + x_limits=(x_min, x_max), + y_limits=(y_min, y_max), + ) + fitted_frequencies[pair] = frequency + fitted_amplitudes[pair] = pi_pulse_parameter + fitted_parameters[pair] = popt + + except Exception as e: + log.warning(f"Rabi fit failed for pair {pair} due to {e}.") + + return ConditionalRabiChevronAmplSignalResults( + activate_spectators=data.activate_spectators, + amplitude=fitted_amplitudes, + fitted_parameters=fitted_parameters, + ) + + +def _plot( + data: RabiLengthFreqSignalData, + target: QubitPairId, + fit: ConditionalRabiChevronAmplSignalResults = None, +): + """Plotting function for ConditionalRabiChevron.""" + + figures = [] + fitting_report = "" + fig = make_subplots( + rows=1, + cols=2, + horizontal_spacing=0.1, + subplot_titles=( + ("PI pulse applied -" if data.activate_spectators else "") + + f"Signal [a.u.] - Target {target[0]}", + ("PI pulse applied -" if data.activate_spectators else "") + + f"Phase [rad] - Target {target[0]}", + ), + ) + qubit_data = data[target] + frequencies = qubit_data.freq * HZ_TO_GHZ + amplitudes = qubit_data.ampl + t_mag = data.compute_target_magnitude(target) + t_phase = data.compute_target_phase(target) + s_mag = data.compute_spectator_magnitude(target) + s_phase = data.compute_spectator_phase(target) + + fig.add_trace( + go.Heatmap( + x=amplitudes, + y=frequencies, + z=t_mag, + colorbar_x=0.46, + ), + row=1, + col=1, + ) + + fig.add_trace( + go.Heatmap( + x=amplitudes, + y=frequencies, + z=t_phase, + colorbar_x=1.01, + ), + row=1, + col=2, + ) + + fig.update_xaxes(title_text="Amplitude [a.u.]", row=1, col=1) + fig.update_xaxes(title_text="Amplitude [a.u.]", row=1, col=2) + fig.update_yaxes(title_text="Frequency [GHz]", row=1, col=1) + fig.update_yaxes(title_text="Frequency [GHz]", row=1, col=2) + + fig.update_layout( + showlegend=False, + legend={"orientation": "h"}, + ) + figures.append(fig) + + fig = make_subplots( + rows=1, + cols=2, + horizontal_spacing=0.1, + subplot_titles=( + ("PI pulse applied -" if data.activate_spectators else "") + + f"Signal [a.u.] - Spectator {target[1]}", + ("PI pulse applied -" if data.activate_spectators else "") + + f"Phase [rad] - Spectator {target[1]}", + ), + ) + + fig.add_trace( + go.Heatmap( + x=amplitudes, + y=frequencies, + z=s_mag, + colorbar_x=0.46, + ), + row=1, + col=1, + ) + + fig.add_trace( + go.Heatmap( + x=amplitudes, + y=frequencies, + z=s_phase, + colorbar_x=1.01, + ), + row=1, + col=2, + ) + + fig.update_xaxes(title_text="Amplitude [a.u.]", row=1, col=1) + fig.update_xaxes(title_text="Amplitude [a.u.]", row=1, col=2) + fig.update_yaxes(title_text="Frequency [GHz]", row=1, col=1) + fig.update_yaxes(title_text="Frequency [GHz]", row=1, col=2) + + fig.update_layout( + showlegend=False, + legend={"orientation": "h"}, + ) + figures.append(fig) + + return figures, fitting_report + + +def _update( + results: ConditionalRabiChevronAmplSignalResults, + platform: CalibrationPlatform, + target: QubitPairId, +): + return + + +conditional_rabi_chevron_ampl_signal = Routine(_acquisition, _fit, _plot, _update) +"""Rabi length with frequency tuning.""" diff --git a/src/qibocal/protocols/rabi/conditional_chevron_length_signal.py b/src/qibocal/protocols/rabi/conditional_chevron_length_signal.py new file mode 100644 index 0000000000..aacc548e1b --- /dev/null +++ b/src/qibocal/protocols/rabi/conditional_chevron_length_signal.py @@ -0,0 +1,316 @@ +"""Rabi experiment that sweeps length and frequency.""" + +from dataclasses import dataclass, field + +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, + PulseSequence, + Sweeper, +) + +from qibocal.auto.operation import Data, Parameters, QubitPairId, Results, Routine +from qibocal.calibration import CalibrationPlatform +from qibocal.config import log + +from ...result import unpack +from ..utils import HZ_TO_GHZ, readout_frequency +from .utils import fit_length_function, rabi_initial_guess, sequence_length + +__all__ = ["conditional_rabi_chevron_len_signal"] + + +@dataclass +class ConditionalRabiChevronLenSignalParameters(Parameters): + """ConditionalRabiChevron runcard inputs.""" + + pulse_duration_start: int + """Initial pi pulse duration [ns].""" + pulse_duration_end: int + """Final pi pulse duration [ns].""" + pulse_duration_step: int + """Step pi pulse duration [ns].""" + min_freq: int + """Minimum frequency as an offset.""" + max_freq: int + """Maximum frequency as an offset.""" + step_freq: int + """Frequency to use as step for the scan.""" + pulse_amplitude: float | None = None + """Pi pulse amplitude. Same for all qubits.""" + + @property + def frequency_range(self): + return np.arange( + self.min_freq, + self.max_freq, + self.step_freq, + ) + + @property + def duration_range(self): + return np.arange( + self.pulse_duration_start, + self.pulse_duration_end, + self.pulse_duration_step, + ) + + +@dataclass +class ConditionalRabiChevronLenSignalResults(Results): + """ConditionalRabiChevron outputs.""" + + length: dict[QubitPairId, int | list[int]] + """Pi pulse duration for each qubit.""" + fitted_parameters: dict[QubitPairId, dict[str, float]] + """Raw fitting output.""" + + +CondRabiLenSigChevronType = np.dtype( + [ + ("len", int), + ("freq", np.float64), + ("i", np.float64), + ("q", np.float64), + ] +) +"""Custom dtype for rabi length.""" + + +@dataclass +class RabiLengthFreqSignalData(Data): + """RabiLengthFreqSignal data acquisition.""" + + data: dict[QubitPairId, npt.NDArray[CondRabiLenSigChevronType]] = field( + default_factory=dict + ) + """Raw data acquired.""" + + def register_qubit(self, pair, freq, lens, i, q): + """Store output for single qubit.""" + size = len(freq) * len(lens) + length, frequency = np.meshgrid(lens, freq) + data = np.empty(size, dtype=CondRabiLenSigChevronType) + data["freq"] = frequency.ravel() + data["len"] = length.ravel() + data["i"] = i.ravel() + data["q"] = q.ravel() + self.data[pair] = np.rec.array(data) + + def durations(self, qubit_pair: QubitPairId) -> npt.NDArray: + """Unique qubit lengths.""" + return np.unique(self[qubit_pair].len) + + def frequencies(self, qubit_pair: QubitPairId) -> npt.NDArray: + """Unique qubit frequency.""" + return np.unique(self[qubit_pair].freq) + + def compute_magnitude(self, qubit_pair: QubitPairId) -> npt.NDArray: + """Compute magnitude of the measured signal""" + return np.sqrt(self[qubit_pair].i ** 2 + self[qubit_pair].q ** 2) + + def compute_phase(self, qubit_pair: QubitPairId) -> npt.NDArray: + """Compute phase of the measured signal""" + return np.unwrap(np.arctan2(self[qubit_pair].i, self[qubit_pair].q)) + + +def _acquisition( + params: ConditionalRabiChevronLenSignalParameters, + platform: CalibrationPlatform, + targets: list[QubitPairId], +) -> RabiLengthFreqSignalData: + """Data acquisition for Rabi experiment sweeping length.""" + + if any(isinstance(x, (str, int)) for x in targets): + raise ValueError("At least one target is not a QubitPairId type.") + + target_qubits_list, spectator_qubits_list = map(list, zip(*targets)) + if any(s in target_qubits_list for s in spectator_qubits_list): + raise ValueError("One or multiple qubits are set as both spectator and target.") + + t_sequence, t_qd_pulses, t_delays, t_ro_pulses, _ = sequence_length( + target_qubits_list, params, platform, False + ) + + complete_sequence = PulseSequence() + for s in spectator_qubits_list: + spectator_natives = platform.natives.single_qubit[s] + spectator_ch, spectator_pulse = spectator_natives.RX()[0] + + complete_sequence.append((spectator_ch, spectator_pulse)) + + complete_seq_duration = complete_sequence.duration + complete_sequence += PulseSequence( + (ch, Delay(duration=complete_seq_duration)) for ch in t_sequence.channels + ) + complete_sequence += t_sequence + + len_sweeper = Sweeper( + parameter=Parameter.duration, + values=params.duration_range, + pulses=[t_qd_pulses[q] for q in target_qubits_list] + + [t_delays[q] for q in target_qubits_list], + ) + + freq_sweepers = {} + for t in target_qubits_list: + target_drive_ch = platform.qubits[t].drive + freq_sweepers[t] = Sweeper( + parameter=Parameter.frequency, + values=platform.config(target_drive_ch).frequency + params.frequency_range, + channels=[target_drive_ch], + ) + + data = RabiLengthFreqSignalData() + + results = platform.execute( + [complete_sequence], + [list(freq_sweepers.values()), [len_sweeper]], + updates=[ + {platform.qubits[q].probe: {"frequency": readout_frequency(q, platform)}} + for q in target_qubits_list + ], + nshots=params.nshots, + relaxation_time=params.relaxation_time, + acquisition_type=AcquisitionType.INTEGRATION, + averaging_mode=AveragingMode.CYCLIC, + ) + for pair in targets: + result = results[t_ro_pulses[pair[0]].id] + i, q = unpack(result) + + data.register_qubit( + pair=pair, + freq=freq_sweepers[pair[0]].values, + lens=len_sweeper.values, + i=i, + q=q, + ) + return data + + +def _fit(data: RabiLengthFreqSignalData) -> ConditionalRabiChevronLenSignalResults: + """Do not perform any fitting procedure.""" + fitted_frequencies = {} + fitted_durations = {} + fitted_parameters = {} + + for pair in data.data: + durations = data.durations(pair) + freqs = data.frequencies(pair) + mag = data.compute_magnitude(pair) + mag_matrix = mag.reshape(len(durations), len(freqs)).T + + # guess optimal frequency maximizing oscillatio amplitude + index = np.argmax([max(x) - min(x) for x in mag_matrix]) + frequency = freqs[index] + + y = mag_matrix[index] + + y_min = np.min(y) + y_max = np.max(y) + x_min = np.min(durations) + x_max = np.max(durations) + x = (durations - x_min) / (x_max - x_min) + y = (y - y_min) / (y_max - y_min) + + pguess = rabi_initial_guess(x, y, "length", signal=True) + + try: + popt, _, pi_pulse_parameter = fit_length_function( + x, + y, + pguess, + signal=True, + x_limits=(x_min, x_max), + y_limits=(y_min, y_max), + ) + fitted_frequencies[pair] = frequency + fitted_durations[pair] = int(pi_pulse_parameter) + fitted_parameters[pair] = popt + + except Exception as e: + log.warning(f"Rabi fit failed for pair {pair} due to {e}.") + + return ConditionalRabiChevronLenSignalResults( + length=fitted_durations, + fitted_parameters=fitted_parameters, + ) + + +def _plot( + data: RabiLengthFreqSignalData, + target: QubitPairId, + fit: ConditionalRabiChevronLenSignalResults = None, +): + """Plotting function for ConditionalRabiChevron.""" + figures = [] + fitting_report = "" + fig = make_subplots( + rows=1, + cols=2, + horizontal_spacing=0.1, + vertical_spacing=0.2, + subplot_titles=( + f"Signal [a.u.] - Target {target[0]}, Spectator {target[1]}", + f"Phase [rad] - Target {target[0]}, Spectator {target[1]}", + ), + ) + qubit_data = data[target] + frequencies = qubit_data.freq * HZ_TO_GHZ + durations = qubit_data.len + mag = data.compute_magnitude(target) + phase = data.compute_phase(target) + + fig.add_trace( + go.Heatmap( + x=durations, + y=frequencies, + z=mag, + colorbar_x=0.46, + ), + row=1, + col=1, + ) + + fig.add_trace( + go.Heatmap( + x=durations, + y=frequencies, + z=phase, + colorbar_x=1.01, + ), + row=1, + col=2, + ) + + fig.update_xaxes(title_text="Durations [ns]", row=1, col=1) + fig.update_xaxes(title_text="Durations [ns]", row=1, col=2) + fig.update_yaxes(title_text="Frequency [GHz]", row=1, col=1) + + figures.append(fig) + + fig.update_layout( + showlegend=False, + legend={"orientation": "h"}, + ) + + return figures, fitting_report + + +def _update( + results: ConditionalRabiChevronLenSignalResults, + platform: CalibrationPlatform, + target: QubitPairId, +): + return + + +conditional_rabi_chevron_len_signal = Routine(_acquisition, _fit, _plot, _update) +"""Rabi length with frequency tuning.""" diff --git a/src/qibocal/protocols/rabi/conditional_cross_chevron_ampl.py b/src/qibocal/protocols/rabi/conditional_cross_chevron_ampl.py new file mode 100644 index 0000000000..3c5bb57bf9 --- /dev/null +++ b/src/qibocal/protocols/rabi/conditional_cross_chevron_ampl.py @@ -0,0 +1,129 @@ +"""Rabi experiment that sweeps amplitude and frequency when toggling a spectator qubit. +In this case the Rabi pulse on the target qubit is applied on the crontrol qubit line (cross).""" + +import numpy as np +from qibolab import ( + AcquisitionType, + AveragingMode, + Parameter, + PulseSequence, + Sweeper, +) + +from qibocal.auto.operation import QubitId, QubitPairId, Routine +from qibocal.calibration import CalibrationPlatform + +from ..utils import readout_frequency +from .conditional_chevron_ampl import ( + ConditionalRabiChevronAmplData, + ConditionalRabiChevronAmplParameters, + _fit, + _plot, + _update, +) + +__all__ = ["conditional_crossrabi_chevron_ampl"] + + +def _acquisition( + params: ConditionalRabiChevronAmplParameters, + platform: CalibrationPlatform, + targets: list[QubitPairId], +) -> ConditionalRabiChevronAmplData: + """Data acquisition for Rabi experiment sweeping length.""" + + if any(isinstance(x, (str, int)) for x in targets): + raise ValueError("At least one target is not a QubitPairId type.") + + target_qubits_list, spectator_qubits_list = map(list, zip(*targets)) + if any(s in target_qubits_list for s in spectator_qubits_list): + raise ValueError("One or multiple qubits are set as both spectator and target.") + + if len(spectator_qubits_list) != np.unique(spectator_qubits_list).size: + raise ValueError("One or multiple spectator qubits are repeated.") + + if any([t not in platform.qubits[s].drive_extra for t, s in targets]): + raise ValueError( + "One or multiple target qubits are not in the cross-drive list of the spectator." + ) + + complete_sequence = PulseSequence() + + spectators_drive_dict: dict[QubitId, PulseSequence] = {} + spectators_ro_dict: dict[QubitId, PulseSequence] = {} + cross_rabi_dict: dict[QubitPairId, PulseSequence] = {} + target_ro_dict: dict[QubitId, PulseSequence] = {} + for t, s in targets: + if s not in spectators_drive_dict and s not in spectators_ro_dict: + spectator_natives = platform.natives.single_qubit[s] + spectators_drive_dict[s] = spectator_natives.RX()[0] + spectators_ro_dict[s] = spectator_natives.MZ()[0] + + if (t, s) not in cross_rabi_dict: + cr_channel = platform.qubits[s].drive_extra[t] + cross_rabi_dict[(t, s)] = PulseSequence( + (cr_channel, spectators_drive_dict[s][1]) + ) + + if t not in target_ro_dict: + target_natives = platform.natives.single_qubit[t] + target_ro_dict[t] = target_natives.MZ()[0] + + if params.activate_spectators: + complete_sequence += PulseSequence(list(spectators_drive_dict.values())) + + # adding cross resonance pulses + complete_sequence |= PulseSequence(list(cross_rabi_dict.values())) + + # adding readout for both spectators and targets + complete_sequence |= PulseSequence( + list(spectators_ro_dict.values()) + list(target_ro_dict.values()) + ) + + ampl_sweeper = Sweeper( + parameter=Parameter.amplitude, + values=params.amplitude_range, + pulses=[cross_rabi_dict[(t, s)][1] for t, s in targets], + ) + + freq_sweepers: dict[QubitId, Sweeper] = {} + for t in target_qubits_list: + target_drive_ch = platform.qubits[t].drive + freq_sweepers[t] = Sweeper( + parameter=Parameter.frequency, + values=platform.config(target_drive_ch).frequency + params.frequency_range, + channels=[target_drive_ch], + ) + + data = ConditionalRabiChevronAmplData( + activate_spectators=params.activate_spectators + ) + + results = platform.execute( + [complete_sequence], + [list(freq_sweepers.values()), [ampl_sweeper]], + updates=[ + {platform.qubits[q].probe: {"frequency": readout_frequency(q, platform)}} + for q in target_qubits_list + spectator_qubits_list + ], + nshots=params.nshots, + relaxation_time=params.relaxation_time, + acquisition_type=AcquisitionType.DISCRIMINATION, + averaging_mode=AveragingMode.CYCLIC, + ) + for pair in targets: + target_result = results[target_ro_dict[pair[0]][1].id] + spect_result = results[spectators_ro_dict[pair[1]][1].id] + + data.register_qubit( + pair=pair, + freq=freq_sweepers[pair[0]].values, + ampl=ampl_sweeper.values, + p_targ=target_result, + p_spect=spect_result, + ) + return data + + +conditional_crossrabi_chevron_ampl = Routine(_acquisition, _fit, _plot, _update) +"""Cross Rabi amplitude with frequency tuning and spectator effect.""" diff --git a/src/qibocal/protocols/rabi/utils.py b/src/qibocal/protocols/rabi/utils.py index 98e258d87b..df2e48cf53 100644 --- a/src/qibocal/protocols/rabi/utils.py +++ b/src/qibocal/protocols/rabi/utils.py @@ -1,7 +1,7 @@ import numpy as np import plotly.graph_objects as go from plotly.subplots import make_subplots -from qibolab import Delay, Platform, PulseSequence +from qibolab import Delay, Platform, Pulse, PulseSequence from scipy.optimize import curve_fit from qibocal.auto.operation import Parameters, QubitId @@ -266,13 +266,18 @@ def sequence_amplitude( params: Parameters, platform: Platform, rx90: bool, -) -> tuple[PulseSequence, dict, dict, dict]: +) -> tuple[ + PulseSequence, + dict[QubitId, Pulse], + dict[QubitId, Pulse], + dict[QubitId, float], +]: """Return sequence for rabi amplitude.""" sequence = PulseSequence() - qd_pulses = {} - ro_pulses = {} - durations = {} + qd_pulses: dict[QubitId, Pulse] = {} + ro_pulses: dict[QubitId, Pulse] = {} + durations: dict[QubitId, float] = {} for q in targets: natives = platform.natives.single_qubit[q] @@ -301,14 +306,16 @@ def sequence_length( platform: Platform, rx90: bool, use_align: bool = False, -) -> tuple[PulseSequence, dict, dict, dict]: +) -> tuple[ + PulseSequence, dict[QubitId, Pulse], dict[QubitId, Delay], dict[QubitId, Pulse] +]: """Return sequence for rabi length.""" sequence = PulseSequence() - qd_pulses = {} - delays = {} - ro_pulses = {} - amplitudes = {} + qd_pulses: dict[QubitId, Pulse] = {} + delays: dict[QubitId, Delay] = {} + ro_pulses: dict[QubitId, Pulse] = {} + amplitudes: dict[QubitId, float] = {} for q in targets: natives = platform.natives.single_qubit[q]