From 5fbb62147699be50704083c5b664037c704509be Mon Sep 17 00:00:00 2001 From: lballerio Date: Fri, 17 Apr 2026 12:57:27 +0400 Subject: [PATCH 1/7] adding conditional spectroscopy in inti file --- src/qibocal/protocols/qubit_spectroscopies/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/qibocal/protocols/qubit_spectroscopies/__init__.py b/src/qibocal/protocols/qubit_spectroscopies/__init__.py index b20407b669..e69a9206ed 100644 --- a/src/qibocal/protocols/qubit_spectroscopies/__init__.py +++ b/src/qibocal/protocols/qubit_spectroscopies/__init__.py @@ -1,5 +1,11 @@ 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 -__all__ = ["qubit_power_spectroscopy", "qubit_spectroscopy", "qubit_spectroscopy_ef"] +__all__ = [ + "qubit_power_spectroscopy", + "qubit_spectroscopy", + "qubit_spectroscopy_ef", + "qubit_conditional_spectroscopy", +] From 97a548acb49d3994492c34233e9266dce13ff0f7 Mon Sep 17 00:00:00 2001 From: lballerio Date: Fri, 17 Apr 2026 19:18:13 +0400 Subject: [PATCH 2/7] adding qubit spectroscopy experiments --- .../qubit_spectroscopies/__init__.py | 4 + .../qubit_ampl_spectroscopy.py | 163 ++++++++++ .../qubit_amplitude_spectroscopy.py | 163 ++++++++++ .../qubit_spectroscopy_conditional.py | 303 ++++++++++++++++++ .../qubits_spectroscopy_spectator.py | 288 +++++++++++++++++ .../protocols/qubit_spectroscopies/utils.py | 169 ++++++++++ 6 files changed, 1090 insertions(+) create mode 100644 src/qibocal/protocols/qubit_spectroscopies/qubit_ampl_spectroscopy.py create mode 100644 src/qibocal/protocols/qubit_spectroscopies/qubit_amplitude_spectroscopy.py create mode 100644 src/qibocal/protocols/qubit_spectroscopies/qubit_spectroscopy_conditional.py create mode 100644 src/qibocal/protocols/qubit_spectroscopies/qubits_spectroscopy_spectator.py create mode 100644 src/qibocal/protocols/qubit_spectroscopies/utils.py diff --git a/src/qibocal/protocols/qubit_spectroscopies/__init__.py b/src/qibocal/protocols/qubit_spectroscopies/__init__.py index e69a9206ed..45b1bc4fde 100644 --- a/src/qibocal/protocols/qubit_spectroscopies/__init__.py +++ b/src/qibocal/protocols/qubit_spectroscopies/__init__.py @@ -1,11 +1,15 @@ +from .qubit_ampl_spectroscopy import qubit_amplitude_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", "qubit_conditional_spectroscopy", + "qubit_spectroscopy_spectator_scan", + "qubit_amplitude_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..aa6f47f05b --- /dev/null +++ b/src/qibocal/protocols/qubit_spectroscopies/qubit_ampl_spectroscopy.py @@ -0,0 +1,163 @@ +from dataclasses import dataclass +from typing import Optional + +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: Optional[QubitSpectroscopyResults] = 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..aa6f47f05b --- /dev/null +++ b/src/qibocal/protocols/qubit_spectroscopies/qubit_amplitude_spectroscopy.py @@ -0,0 +1,163 @@ +from dataclasses import dataclass +from typing import Optional + +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: Optional[QubitSpectroscopyResults] = 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_spectroscopy_conditional.py b/src/qibocal/protocols/qubit_spectroscopies/qubit_spectroscopy_conditional.py new file mode 100644 index 0000000000..13c080fa59 --- /dev/null +++ b/src/qibocal/protocols/qubit_spectroscopies/qubit_spectroscopy_conditional.py @@ -0,0 +1,303 @@ +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 Delay, PulseSequence + +from qibocal.auto.operation import QubitId, 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.""" + + spectator_qubits: Optional[list[QubitId]] = None + """List of spectator qubits to set in 1 state before qubit spectroscopy.""" + + +@dataclass +class ConditionalSpectrumResults(QubitSpectroscopyResults): + """ConditionalSpectrum outputs.""" + + spectator_qubits: Optional[list[QubitId]] = None + """List of spectator qubits to set in 1 state before qubit spectroscopy.""" + fitted_parameters: dict[QubitId, dict[QubitId, list[float]]] = field( + default_factory=dict + ) + """Raw fitting output.""" + + +@dataclass +class ConditionalSpectrumData(QubitSpectroscopyData): + """ConditionalSpectrum acquisition outputs.""" + + spectator_qubits: Optional[list[QubitId]] = None + """List of spectator qubits to set in 1 state before qubit spectroscopy.""" + data: dict[QubitId, dict[QubitId, npt.NDArray[ConditionalSpectrumType]]] = field( + default_factory=dict + ) + "Raw data for ConditionalSpectrum." + + def register_qubit( + self, + target: QubitId, + spectator: QubitId, + 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() + + if target not in self.data: + self.data[target] = {} + self.data[target][spectator] = np.rec.array(ar) + + +def _acquisition( + params: ConditionalSpectrumParameters, + platform: CalibrationPlatform, + targets: list[QubitId], +) -> ConditionalSpectrumData: + """Data acquisition for qubit spectroscopy. + + Handles wideband spectroscopy by batching when the frequency range exceeds ±300 MHz from the LO + """ + + if params.spectator_qubits is None: + raise ValueError("No spectator qubit was inserted.") + + # Calculate batches + batches = calculate_batches(params.freq_width) + + spectro_seq, ro_pulses, drive_channels, los_channels, amplitudes = ( + spectroscopy_sequence( + params=params, + platform=platform, + targets=targets, + ) + ) + + # Create data structure and aggregate results + data = ConditionalSpectrumData( + resonator_type=platform.resonator_type, + targets=targets, + amplitudes=amplitudes, + spectator_qubits=params.spectator_qubits, + ) + + # adding the spectators pulses + for spect_q in params.spectator_qubits: + 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] + + # add pi-pulse for the spectator qubit + spectator_seq = PulseSequence(((spect_drive_channel, spect_pulse),)) + # adding delay for target channels + spectator_seq += PulseSequence( + (ch, Delay(duration=spectator_seq.duration)) for ch in spectro_seq.channels + ) + complete_sequence = spectator_seq + spectro_seq + # appending readout pulse for spectator qubit + spect_ro_sequence = PulseSequence( + ( + (spect_ro_channel, Delay(duration=complete_sequence.duration)), + (spect_ro_channel, spect_ro_pulse), + ) + ) + complete_sequence += spect_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=targets, + 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], + [parsweep], + updates=[batch_updates], + **params.execution_parameters, + ) + + # Collect results for spectator qubits + spectator_result = results[spect_ro_pulse.id] + spectator_signal = magnitude(spectator_result) + spectator_phase = phase(spectator_result) + + # Collect results from this batch + for qubit in targets: + result = results[ro_pulses[qubit].id] + f0 = platform.config(drive_channels[qubit]).frequency + + signal = magnitude(result) + _phase = phase(result) + + data.register_qubit( + target=qubit, + spectator=spect_q, + target_freq=delta_frequency_range + f0, + 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.""" + qubits = data.qubits + frequency = {} + fitted_parameters = {} + for qubit in qubits: + frequency[qubit] = {} + fitted_parameters[qubit] = {} + for spect in data.spectator_qubits: + fit_result = lorentzian_fit( + data[qubit][spect], resonator_type=data.resonator_type, fit="qubit" + ) + if fit_result is not None: + fit_freq, fit_params, _ = fit_result + + frequency[qubit][spect] = fit_freq + fitted_parameters[qubit][spect] = fit_params + + return ConditionalSpectrumResults( + frequency=frequency, + fitted_parameters=fitted_parameters, + amplitude=data.amplitudes, + spectator_qubits=data.spectator_qubits, + ) + + +def _plot( + data: ConditionalSpectrumData, target: QubitId, fit: ConditionalSpectrumResults +): + """Plotting function for ConditionalSpectrum.""" + + figures = [] + fitting_report = "" + + for spect in data.spectator_qubits: + 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][spect].freq * HZ_TO_GHZ, + y=data.data[target][spect].signal, + name="Target Signal", + legendgroup="Target Signal", + ), + row=1, + col=1, + ) + + fig.add_trace( + go.Scatter( + x=data.data[target][spect].freq * HZ_TO_GHZ, + y=data.data[target][spect].phase, + name="Target Phase", + legendgroup="Target Phase", + ), + row=1, + col=2, + ) + + fig.add_trace( + go.Scatter( + x=data.data[target][spect].freq * HZ_TO_GHZ, + y=data.data[target][spect].s_signal, + name="Spectator Signal", + legendgroup="Spectator Signal", + ), + row=2, + col=1, + ) + + fig.add_trace( + go.Scatter( + x=data.data[target][spect].freq * HZ_TO_GHZ, + y=data.data[target][spect].s_phase, + name="Spectator Phase", + legendgroup="Spectator Phase", + ), + row=2, + col=2, + ) + + fig.update_layout(title_text=f"Spectroscopy with {spect} 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..976cedf56a --- /dev/null +++ b/src/qibocal/protocols/qubit_spectroscopies/qubits_spectroscopy_spectator.py @@ -0,0 +1,288 @@ +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 qibocal.auto.operation import QubitId, 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 magnitude, phase + +__all__ = ["qubit_spectroscopy_spectator_scan"] + +SpectatorFreqScanType = np.dtype( + [ + ("t_freq", np.float64), + ("t_signal", np.float64), + ("t_phase", np.float64), + ("s_freq", np.float64), + ("s_signal", np.float64), + ("s_phase", np.float64), + ] +) + + +@dataclass +class SpectatorFreqScanParameters(QubitSpectroscopyParameters): + """SpectatorFreqScan runcard inputs.""" + + spectator_qubits: Optional[list[QubitId]] = None + """List of spectator qubits to set in 1 state before qubit spectroscopy.""" + + +@dataclass +class SpectatorFreqScanResults(QubitSpectroscopyResults): + """SpectatorFreqScan outputs.""" + + spectator_qubits: Optional[list[QubitId]] = None + """List of spectator qubits to set in 1 state before qubit spectroscopy.""" + + +@dataclass +class SpectatorFreqScanData(QubitSpectroscopyData): + """SpectatorFreqScan acquisition outputs.""" + + spectator_qubits: Optional[list[QubitId]] = None + """List of spectator qubits to set in 1 state before qubit spectroscopy.""" + data: dict[QubitId, dict[QubitId, npt.NDArray[SpectatorFreqScanType]]] = field( + default_factory=dict + ) + "Raw data for SpectatorFreqScan." + + def register_qubit( + self, + target: QubitId, + spectator: QubitId, + target_freq: npt.NDArray, + spectator_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.""" + 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_signal"] = target_signal.ravel() + ar["t_phase"] = target_phase.ravel() + ar["s_freq"] = s_freqs.ravel() + ar["s_signal"] = spectator_signal.ravel() + ar["s_phase"] = spectator_phase.ravel() + + if target not in self.data: + self.data[target] = {} + self.data[target][spectator] = np.rec.array(ar) + + +def _acquisition( + params: SpectatorFreqScanParameters, + platform: CalibrationPlatform, + targets: list[QubitId], +) -> SpectatorFreqScanData: + """Data acquisition for qubit spectroscopy. + + Handles wideband spectroscopy by batching when the frequency range exceeds ±300 MHz from the LO + """ + + if params.spectator_qubits is None: + raise ValueError("No spectator qubit was inserted.") + + # Calculate batches + batches = calculate_batches(params.freq_width) + + spectro_seq, ro_pulses, drive_channels, los_channels, amplitudes = ( + spectroscopy_sequence( + params=params, + platform=platform, + targets=targets, + ) + ) + + if any(x in targets for x in params.spectator_qubits): + raise ValueError("One or multiple qubits are set as both spectator and target.") + + # Create data structure and aggregate results + data = SpectatorFreqScanData( + resonator_type=platform.resonator_type, + targets=targets, + amplitudes=amplitudes, + spectator_qubits=params.spectator_qubits, + ) + + # executing the experiment for each spectator qubit separately + for spect_q in params.spectator_qubits: + spectator_seq, spectator_ro_pulses, spectator_drive_ch, spectator_los_ch, _ = ( + spectroscopy_sequence( + params=params, + platform=platform, + targets=[spect_q], + ) + ) + + complete_seq = spectator_seq + spectro_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=[spect_q], + 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], + [spectator_parsweep, q_parsweep], + updates=[q_batch_updates, spectator_batch_updates], + **params.execution_parameters, + ) + + # Collect results from this batch + spectator_result = results[spectator_ro_pulses[spect_q].id] + spectator_signal = magnitude(spectator_result) + spectator_phase = phase(spectator_result) + + for qubit in targets: + result = results[ro_pulses[qubit].id] + f0 = platform.config(drive_channels[qubit]).frequency + + signal = magnitude(result) + _phase = phase(result) + + data.register_qubit( + target=qubit, + spectator=spect_q, + target_freq=delta_frequency_range + f0, + spectator_freq=spectator_parsweep[0].values, + target_signal=signal, + target_phase=_phase, + spectator_signal=spectator_signal, + spectator_phase=spectator_phase, + ) + + return data + + +def _fit(data: SpectatorFreqScanData) -> SpectatorFreqScanResults: + """Post-processing function for SpectatorFreqScan.""" + return SpectatorFreqScanResults() + + +def _plot(data: SpectatorFreqScanData, target: QubitId, fit: SpectatorFreqScanResults): + """Plotting function for SpectatorFreqScan.""" + + figures = [] + fitting_report = "" + + for spect in data.spectator_qubits: + 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.data[target][spect]["t_freq"] * HZ_TO_GHZ, + y=data.data[target][spect]["s_freq"] * HZ_TO_GHZ, + z=data.data[target][spect]["t_signal"], + name="Target Signal", + legendgroup="Target Signal", + ), + row=1, + col=1, + ) + + fig.add_trace( + go.Heatmap( + x=data.data[target][spect]["t_freq"] * HZ_TO_GHZ, + y=data.data[target][spect]["s_freq"] * HZ_TO_GHZ, + z=data.data[target][spect]["t_phase"], + name="Target Phase", + legendgroup="Target Phase", + ), + row=1, + col=2, + ) + + fig.add_trace( + go.Heatmap( + x=data.data[target][spect]["t_freq"] * HZ_TO_GHZ, + y=data.data[target][spect]["s_freq"] * HZ_TO_GHZ, + z=data.data[target][spect]["s_signal"], + name="Spectator Signal", + legendgroup="Spectator Signal", + ), + row=2, + col=1, + ) + + fig.add_trace( + go.Heatmap( + x=data.data[target][spect]["t_freq"] * HZ_TO_GHZ, + y=data.data[target][spect]["s_freq"] * HZ_TO_GHZ, + z=data.data[target][spect]["s_phase"], + name="Spectator Phase", + legendgroup="Spectator Phase", + ), + row=2, + col=2, + ) + + fig.update_layout( + title_text=f"Spectroscopy 2D scan with {spect} as Spectator Qubit." + ) + figures.append(fig) + + return figures, fitting_report + + +def _update( + results: SpectatorFreqScanResults, platform: CalibrationPlatform, target: QubitId +): + 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..9e849d6f3e --- /dev/null +++ b/src/qibocal/protocols/qubit_spectroscopies/utils.py @@ -0,0 +1,169 @@ +from dataclasses import dataclass, field +from typing import Optional + +import numpy as np +import numpy.typing as npt +from qibolab import ( + Delay, + ParallelSweepers, + 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: Optional[float] = 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, + dict[QubitId, PulseLike], + dict[QubitId, ChannelId], + dict[QubitId, ChannelId], + dict[QubitId, float], +]: + # Build the pulse sequence + 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)) + sequence.append((ro_channel, Delay(duration=qd_pulse.duration))) + sequence.append((ro_channel, ro_pulse)) + + return 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[ParallelSweepers, dict[str, float]]: + """Create a parallel sweeper 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 = ParallelSweepers() + batch_updates = {} + for q in targets: + f0 = platform.config(drive_channels[q]).frequency + parsweep.append( + 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 From be6b4f5a06ff3d6bd4e77baa0876ecb6729612f1 Mon Sep 17 00:00:00 2001 From: lballerio Date: Tue, 21 Apr 2026 14:54:16 +0400 Subject: [PATCH 3/7] adding conditional rabi experiment (both amplitude and length, but length can be faulty) --- .../qubit_spectroscopy_conditional.py | 276 ++++++------ .../qubits_spectroscopy_spectator.py | 313 +++++++------- .../protocols/qubit_spectroscopies/utils.py | 19 +- src/qibocal/protocols/rabi/__init__.py | 6 + .../rabi/conditional_chevron_ampl.py | 329 +++++++++++++++ .../rabi/conditional_chevron_ampl_signal.py | 399 ++++++++++++++++++ .../conditional_chevron_length_signal copy.py | 317 ++++++++++++++ .../rabi/conditional_chevron_length_signal.py | 317 ++++++++++++++ 8 files changed, 1664 insertions(+), 312 deletions(-) create mode 100644 src/qibocal/protocols/rabi/conditional_chevron_ampl.py create mode 100644 src/qibocal/protocols/rabi/conditional_chevron_ampl_signal.py create mode 100644 src/qibocal/protocols/rabi/conditional_chevron_length_signal copy.py create mode 100644 src/qibocal/protocols/rabi/conditional_chevron_length_signal.py diff --git a/src/qibocal/protocols/qubit_spectroscopies/qubit_spectroscopy_conditional.py b/src/qibocal/protocols/qubit_spectroscopies/qubit_spectroscopy_conditional.py index 13c080fa59..70042e9d03 100644 --- a/src/qibocal/protocols/qubit_spectroscopies/qubit_spectroscopy_conditional.py +++ b/src/qibocal/protocols/qubit_spectroscopies/qubit_spectroscopy_conditional.py @@ -1,5 +1,4 @@ from dataclasses import dataclass, field -from typing import Optional import numpy as np import numpy.typing as npt @@ -7,7 +6,7 @@ from plotly.subplots import make_subplots from qibolab import Delay, PulseSequence -from qibocal.auto.operation import QubitId, Routine +from qibocal.auto.operation import QubitId, QubitPairId, Routine from qibocal.calibration import CalibrationPlatform from qibocal.protocols.qubit_spectroscopies.utils import ( QubitSpectroscopyData, @@ -41,19 +40,12 @@ class ConditionalSpectrumParameters(QubitSpectroscopyParameters): """ConditionalSpectrum runcard inputs.""" - spectator_qubits: Optional[list[QubitId]] = None - """List of spectator qubits to set in 1 state before qubit spectroscopy.""" - @dataclass class ConditionalSpectrumResults(QubitSpectroscopyResults): """ConditionalSpectrum outputs.""" - spectator_qubits: Optional[list[QubitId]] = None - """List of spectator qubits to set in 1 state before qubit spectroscopy.""" - fitted_parameters: dict[QubitId, dict[QubitId, list[float]]] = field( - default_factory=dict - ) + fitted_parameters: dict[QubitPairId, list[float]] = field(default_factory=dict) """Raw fitting output.""" @@ -61,17 +53,14 @@ class ConditionalSpectrumResults(QubitSpectroscopyResults): class ConditionalSpectrumData(QubitSpectroscopyData): """ConditionalSpectrum acquisition outputs.""" - spectator_qubits: Optional[list[QubitId]] = None - """List of spectator qubits to set in 1 state before qubit spectroscopy.""" - data: dict[QubitId, dict[QubitId, npt.NDArray[ConditionalSpectrumType]]] = field( + data: dict[QubitPairId, npt.NDArray[ConditionalSpectrumType]] = field( default_factory=dict ) "Raw data for ConditionalSpectrum." def register_qubit( self, - target: QubitId, - spectator: QubitId, + pair: QubitPairId, target_freq: npt.NDArray, target_signal: npt.NDArray, target_phase: npt.NDArray, @@ -87,23 +76,25 @@ def register_qubit( ar["s_signal"] = spectator_signal.ravel() ar["s_phase"] = spectator_phase.ravel() - if target not in self.data: - self.data[target] = {} - self.data[target][spectator] = np.rec.array(ar) + self.data[pair] = np.rec.array(ar) def _acquisition( params: ConditionalSpectrumParameters, platform: CalibrationPlatform, - targets: list[QubitId], + 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 params.spectator_qubits is None: - raise ValueError("No spectator qubit was inserted.") + 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) @@ -112,7 +103,7 @@ def _acquisition( spectroscopy_sequence( params=params, platform=platform, - targets=targets, + targets=target_qubits_list, ) ) @@ -121,173 +112,176 @@ def _acquisition( resonator_type=platform.resonator_type, targets=targets, amplitudes=amplitudes, - spectator_qubits=params.spectator_qubits, ) # adding the spectators pulses - for spect_q in params.spectator_qubits: + 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 - spectator_seq = PulseSequence(((spect_drive_channel, spect_pulse),)) - # adding delay for target channels - spectator_seq += PulseSequence( - (ch, Delay(duration=spectator_seq.duration)) for ch in spectro_seq.channels - ) - complete_sequence = spectator_seq + spectro_seq + spectators_drive_sequence.append((spect_drive_channel, spect_pulse)) # appending readout pulse for spectator qubit - spect_ro_sequence = PulseSequence( + spectators_ro_sequence.append( ( - (spect_ro_channel, Delay(duration=complete_sequence.duration)), - (spect_ro_channel, spect_ro_pulse), + spect_ro_channel, + Delay( + duration=(spectro_seq.duration + spectators_drive_sequence.duration) + ), ) ) - complete_sequence += spect_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=targets, - drive_channels=drive_channels, - delta_frequency_range=delta_frequency_range, - los_channels=los_channels, - lo_offset=lo_offset, - ) + spectators_ro_sequence.append((spect_ro_channel, spect_ro_pulse)) - # Execute this batch - results = platform.execute( - [complete_sequence], - [parsweep], - updates=[batch_updates], - **params.execution_parameters, - ) + # adding delay for target channels + spect_drive_seq_duration = spectators_drive_sequence.duration + spectators_drive_sequence += PulseSequence( + (ch, Delay(duration=spect_drive_seq_duration)) for ch in spectro_seq.channels + ) + + complete_sequence = spectators_drive_sequence + spectro_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 t, s in targets: # Collect results for spectator qubits - spectator_result = results[spect_ro_pulse.id] + spectator_result = results[spectator_ro_pulses[s].id] spectator_signal = magnitude(spectator_result) spectator_phase = phase(spectator_result) - # Collect results from this batch - for qubit in targets: - result = results[ro_pulses[qubit].id] - f0 = platform.config(drive_channels[qubit]).frequency - - signal = magnitude(result) - _phase = phase(result) - - data.register_qubit( - target=qubit, - spectator=spect_q, - target_freq=delta_frequency_range + f0, - target_signal=signal, - target_phase=_phase, - spectator_signal=spectator_signal, - spectator_phase=spectator_phase, - ) + result = results[ro_pulses[t].id] + signal = magnitude(result) + _phase = phase(result) + + data.register_qubit( + target=t, + spectator=s, + target_freq=parsweep[t], + 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.""" - qubits = data.qubits frequency = {} fitted_parameters = {} - for qubit in qubits: - frequency[qubit] = {} - fitted_parameters[qubit] = {} - for spect in data.spectator_qubits: - fit_result = lorentzian_fit( - data[qubit][spect], resonator_type=data.resonator_type, fit="qubit" - ) - if fit_result is not None: - fit_freq, fit_params, _ = fit_result + 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[qubit][spect] = fit_freq - fitted_parameters[qubit][spect] = fit_params + frequency[pair[0]] = fit_freq + fitted_parameters[pair] = fit_params return ConditionalSpectrumResults( frequency=frequency, fitted_parameters=fitted_parameters, amplitude=data.amplitudes, - spectator_qubits=data.spectator_qubits, ) def _plot( - data: ConditionalSpectrumData, target: QubitId, fit: ConditionalSpectrumResults + data: ConditionalSpectrumData, target: QubitPairId, fit: ConditionalSpectrumResults ): """Plotting function for ConditionalSpectrum.""" figures = [] fitting_report = "" - for spect in data.spectator_qubits: - 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 = 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][spect].freq * HZ_TO_GHZ, - y=data.data[target][spect].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].signal, + name="Target Signal", + legendgroup="Target Signal", + ), + row=1, + col=1, + ) - fig.add_trace( - go.Scatter( - x=data.data[target][spect].freq * HZ_TO_GHZ, - y=data.data[target][spect].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].phase, + name="Target Phase", + legendgroup="Target Phase", + ), + row=1, + col=2, + ) - fig.add_trace( - go.Scatter( - x=data.data[target][spect].freq * HZ_TO_GHZ, - y=data.data[target][spect].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_signal, + name="Spectator Signal", + legendgroup="Spectator Signal", + ), + row=2, + col=1, + ) - fig.add_trace( - go.Scatter( - x=data.data[target][spect].freq * HZ_TO_GHZ, - y=data.data[target][spect].s_phase, - name="Spectator Phase", - legendgroup="Spectator Phase", - ), - row=2, - col=2, - ) + 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 {spect} as Spectator Qubit.") - figures.append(fig) + fig.update_layout(title_text=f"Spectroscopy with {target[1]} as Spectator Qubit.") + figures.append(fig) return figures, fitting_report diff --git a/src/qibocal/protocols/qubit_spectroscopies/qubits_spectroscopy_spectator.py b/src/qibocal/protocols/qubit_spectroscopies/qubits_spectroscopy_spectator.py index 976cedf56a..b7e168df80 100644 --- a/src/qibocal/protocols/qubit_spectroscopies/qubits_spectroscopy_spectator.py +++ b/src/qibocal/protocols/qubit_spectroscopies/qubits_spectroscopy_spectator.py @@ -1,12 +1,11 @@ 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 qibocal.auto.operation import QubitId, Routine +from qibocal.auto.operation import QubitPairId, Routine from qibocal.calibration import CalibrationPlatform from qibocal.protocols.qubit_spectroscopies.utils import ( QubitSpectroscopyData, @@ -17,18 +16,18 @@ spectroscopy_sequence, ) from qibocal.protocols.utils import HZ_TO_GHZ -from qibocal.result import magnitude, phase +from qibocal.result import unpack __all__ = ["qubit_spectroscopy_spectator_scan"] SpectatorFreqScanType = np.dtype( [ ("t_freq", np.float64), - ("t_signal", np.float64), - ("t_phase", np.float64), + ("t_i", np.float64), + ("t_q", np.float64), ("s_freq", np.float64), - ("s_signal", np.float64), - ("s_phase", np.float64), + ("s_i", np.float64), + ("s_q", np.float64), ] ) @@ -37,39 +36,30 @@ class SpectatorFreqScanParameters(QubitSpectroscopyParameters): """SpectatorFreqScan runcard inputs.""" - spectator_qubits: Optional[list[QubitId]] = None - """List of spectator qubits to set in 1 state before qubit spectroscopy.""" - @dataclass class SpectatorFreqScanResults(QubitSpectroscopyResults): """SpectatorFreqScan outputs.""" - spectator_qubits: Optional[list[QubitId]] = None - """List of spectator qubits to set in 1 state before qubit spectroscopy.""" - @dataclass class SpectatorFreqScanData(QubitSpectroscopyData): """SpectatorFreqScan acquisition outputs.""" - spectator_qubits: Optional[list[QubitId]] = None - """List of spectator qubits to set in 1 state before qubit spectroscopy.""" - data: dict[QubitId, dict[QubitId, npt.NDArray[SpectatorFreqScanType]]] = field( + data: dict[QubitPairId, npt.NDArray[SpectatorFreqScanType]] = field( default_factory=dict ) "Raw data for SpectatorFreqScan." def register_qubit( self, - target: QubitId, - spectator: QubitId, + pair: QubitPairId, target_freq: npt.NDArray, spectator_freq: npt.NDArray, - target_signal: npt.NDArray, - target_phase: npt.NDArray, - spectator_signal: npt.NDArray, - spectator_phase: 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 @@ -78,29 +68,39 @@ def register_qubit( t_freqs, s_freqs = np.meshgrid(target_freq, spectator_freq) ar["t_freq"] = t_freqs.ravel() - ar["t_signal"] = target_signal.ravel() - ar["t_phase"] = target_phase.ravel() + ar["t_i"] = target_i.ravel() + ar["t_q"] = target_q.ravel() ar["s_freq"] = s_freqs.ravel() - ar["s_signal"] = spectator_signal.ravel() - ar["s_phase"] = spectator_phase.ravel() + ar["s_i"] = spectator_i.ravel() + ar["s_q"] = spectator_q.ravel() + + self.data[pair] = np.rec.array(ar) - if target not in self.data: - self.data[target] = {} - self.data[target][spectator] = 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[QubitId], + 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 params.spectator_qubits is None: - raise ValueError("No spectator qubit was inserted.") + 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) @@ -109,89 +109,79 @@ def _acquisition( spectroscopy_sequence( params=params, platform=platform, - targets=targets, + targets=target_qubits_list, ) ) - if any(x in targets for x in params.spectator_qubits): - raise ValueError("One or multiple qubits are set as both spectator and target.") - # Create data structure and aggregate results data = SpectatorFreqScanData( resonator_type=platform.resonator_type, targets=targets, amplitudes=amplitudes, - spectator_qubits=params.spectator_qubits, ) # executing the experiment for each spectator qubit separately - for spect_q in params.spectator_qubits: - spectator_seq, spectator_ro_pulses, spectator_drive_ch, spectator_los_ch, _ = ( - spectroscopy_sequence( - params=params, - platform=platform, - targets=[spect_q], - ) + spectator_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_seq + spectro_seq - # Execute each batch - for start, end, lo_offset in batches: - delta_frequency_range = np.arange(start, end, params.freq_step) + # 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 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=[spect_q], - drive_channels=spectator_drive_ch, - delta_frequency_range=delta_frequency_range, - los_channels=spectator_los_ch, - 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], - [spectator_parsweep, q_parsweep], - updates=[q_batch_updates, spectator_batch_updates], - **params.execution_parameters, - ) + # 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) - # Collect results from this batch - spectator_result = results[spectator_ro_pulses[spect_q].id] - spectator_signal = magnitude(spectator_result) - spectator_phase = phase(spectator_result) - - for qubit in targets: - result = results[ro_pulses[qubit].id] - f0 = platform.config(drive_channels[qubit]).frequency - - signal = magnitude(result) - _phase = phase(result) - - data.register_qubit( - target=qubit, - spectator=spect_q, - target_freq=delta_frequency_range + f0, - spectator_freq=spectator_parsweep[0].values, - target_signal=signal, - target_phase=_phase, - spectator_signal=spectator_signal, - spectator_phase=spectator_phase, - ) + 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 @@ -201,84 +191,87 @@ def _fit(data: SpectatorFreqScanData) -> SpectatorFreqScanResults: return SpectatorFreqScanResults() -def _plot(data: SpectatorFreqScanData, target: QubitId, fit: SpectatorFreqScanResults): +def _plot( + data: SpectatorFreqScanData, target: QubitPairId, fit: SpectatorFreqScanResults +): """Plotting function for SpectatorFreqScan.""" figures = [] fitting_report = "" - for spect in data.spectator_qubits: - 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 = 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.data[target][spect]["t_freq"] * HZ_TO_GHZ, - y=data.data[target][spect]["s_freq"] * HZ_TO_GHZ, - z=data.data[target][spect]["t_signal"], - 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_magnitude(target), + name="Target Signal", + legendgroup="Target Signal", + ), + row=1, + col=1, + ) - fig.add_trace( - go.Heatmap( - x=data.data[target][spect]["t_freq"] * HZ_TO_GHZ, - y=data.data[target][spect]["s_freq"] * HZ_TO_GHZ, - z=data.data[target][spect]["t_phase"], - 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_phase(target), + name="Target Phase", + legendgroup="Target Phase", + ), + row=1, + col=2, + ) - fig.add_trace( - go.Heatmap( - x=data.data[target][spect]["t_freq"] * HZ_TO_GHZ, - y=data.data[target][spect]["s_freq"] * HZ_TO_GHZ, - z=data.data[target][spect]["s_signal"], - 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_magnitude(target), + name="Spectator Signal", + legendgroup="Spectator Signal", + ), + row=2, + col=1, + ) - fig.add_trace( - go.Heatmap( - x=data.data[target][spect]["t_freq"] * HZ_TO_GHZ, - y=data.data[target][spect]["s_freq"] * HZ_TO_GHZ, - z=data.data[target][spect]["s_phase"], - name="Spectator Phase", - legendgroup="Spectator Phase", - ), - row=2, - 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_phase(target), + name="Spectator Phase", + legendgroup="Spectator Phase", + ), + row=2, + col=2, + ) - fig.update_layout( - title_text=f"Spectroscopy 2D scan with {spect} as Spectator Qubit." - ) - figures.append(fig) + 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: QubitId + results: SpectatorFreqScanResults, + platform: CalibrationPlatform, + target: QubitPairId, ): return diff --git a/src/qibocal/protocols/qubit_spectroscopies/utils.py b/src/qibocal/protocols/qubit_spectroscopies/utils.py index 9e849d6f3e..5cb6f78b99 100644 --- a/src/qibocal/protocols/qubit_spectroscopies/utils.py +++ b/src/qibocal/protocols/qubit_spectroscopies/utils.py @@ -5,7 +5,6 @@ import numpy.typing as npt from qibolab import ( Delay, - ParallelSweepers, Parameter, PulseLike, PulseSequence, @@ -137,8 +136,8 @@ def create_spectr_sweeper_and_updates( delta_frequency_range: npt.NDArray, los_channels: dict[ChannelId], lo_offset: float, -) -> tuple[ParallelSweepers, dict[str, float]]: - """Create a parallel sweeper configuration for spectroscopy measurements on multiple qubits. +) -> 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 @@ -147,16 +146,14 @@ def create_spectr_sweeper_and_updates( LO channel frequencies are only updated if lo_offset is non-zero and the LO channel exists. """ - parsweep = ParallelSweepers() + parsweep = {} batch_updates = {} for q in targets: f0 = platform.config(drive_channels[q]).frequency - parsweep.append( - Sweeper( - parameter=Parameter.frequency, - values=f0 + delta_frequency_range, - channels=[drive_channels[q]], - ) + 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 @@ -166,4 +163,4 @@ def create_spectr_sweeper_and_updates( if lo_offset != 0 and los_channels[q] is not None: batch_updates[los_channels[q]] = {"frequency": f0 + lo_offset} - return parsweep, batch_updates + return parsweep, batch_updates diff --git a/src/qibocal/protocols/rabi/__init__.py b/src/qibocal/protocols/rabi/__init__.py index 83e0a7ec1b..6c331de3e4 100644 --- a/src/qibocal/protocols/rabi/__init__.py +++ b/src/qibocal/protocols/rabi/__init__.py @@ -2,6 +2,9 @@ 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 .ef import rabi_amplitude_ef from .length import rabi_length from .length_frequency import rabi_length_frequency @@ -18,4 +21,7 @@ "rabi_length_frequency", "rabi_length_signal", "rabi_length", + "conditional_rabi_chevron_len_signal", + "conditional_rabi_chevron_ampl_signal", + "conditional_rabi_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..1dafb6739c --- /dev/null +++ b/src/qibocal/protocols/rabi/conditional_chevron_ampl.py @@ -0,0 +1,329 @@ +"""Rabi experiment that sweeps length and frequency.""" + +from dataclasses import dataclass, field +from typing import Optional, Union + +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 ..utils import HZ_TO_GHZ, readout_frequency +from .utils import fit_amplitude_function, rabi_initial_guess, sequence_amplitude + +__all__ = ["conditional_rabi_chevron_ampl"] + + +@dataclass +class ConditionalRabiChevronAmplParameters(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: Optional[float] = 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): + """ConditionalRabiChevron outputs.""" + + activate_spectators: bool + """Flag for setting spectator qbuit in state 1.""" + amplitude: dict[QubitPairId, Union[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 length.""" + + +@dataclass +class RabiLengthFreqSignalData(Data): + """RabiLengthFreqSignal 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 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 _acquisition( + params: ConditionalRabiChevronAmplParameters, + 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 = {} + spectators_ro_dict = {} + 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())) + compl_seq_len = complete_sequence.duration + complete_sequence += PulseSequence( + (ch, Delay(duration=compl_seq_len)) for ch in t_sequence.channels + ) + complete_sequence += PulseSequence( + (seq[0], Delay(duration=compl_seq_len)) + for seq in spectators_ro_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 = {} + 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 + 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[t_ro_pulses[pair[0]].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 + + +def _fit(data: RabiLengthFreqSignalData) -> ConditionalRabiChevronAmplResults: + """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) + target_prob = data[pair].target + t_prob_matrix = target_prob.reshape(len(amplitudes), len(freqs)).T + + # guess optimal frequency maximizing oscillatio amplitude + index = np.argmax([max(x) - min(x) for x in t_prob_matrix]) + frequency = freqs[index] + + y = t_prob_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=False) + + try: + popt, _, pi_pulse_parameter = fit_amplitude_function( + x, + y, + pguess, + signal=False, + 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 ConditionalRabiChevronAmplResults( + activate_spectators=data.activate_spectators, + amplitude=fitted_amplitudes, + fitted_parameters=fitted_parameters, + ) + + +def _plot( + data: RabiLengthFreqSignalData, + target: QubitPairId, + fit: ConditionalRabiChevronAmplResults = 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 fit.activate_spectators else "") + + f" Prob Target {target[0]}", + ("PI pulse applied -" if fit.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..453bfcf412 --- /dev/null +++ b/src/qibocal/protocols/rabi/conditional_chevron_ampl_signal.py @@ -0,0 +1,399 @@ +"""Rabi experiment that sweeps length and frequency.""" + +from dataclasses import dataclass, field +from typing import Optional, Union + +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_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: Optional[float] = 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, Union[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 computespectator_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 = {} + spectators_ro_dict = {} + 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())) + compl_seq_len = complete_sequence.duration + complete_sequence += PulseSequence( + (ch, Delay(duration=compl_seq_len)) for ch in t_sequence.channels + ) + complete_sequence += PulseSequence( + (seq[0], Delay(duration=compl_seq_len)) + for seq in spectators_ro_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 = {} + 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 fit.activate_spectators else "") + + f"Signal [a.u.] - Target {target[0]}", + ("PI pulse applied -" if fit.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.computespectator_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=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 fit.activate_spectators else "") + + f"Signal [a.u.] - Spectator {target[1]}", + ("PI pulse applied -" if fit.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_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 copy.py b/src/qibocal/protocols/rabi/conditional_chevron_length_signal copy.py new file mode 100644 index 0000000000..72a6d87c73 --- /dev/null +++ b/src/qibocal/protocols/rabi/conditional_chevron_length_signal copy.py @@ -0,0 +1,317 @@ +"""Rabi experiment that sweeps length and frequency.""" + +from dataclasses import dataclass, field +from typing import Optional, Union + +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: Optional[float] = 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, Union[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_chevron_length_signal.py b/src/qibocal/protocols/rabi/conditional_chevron_length_signal.py new file mode 100644 index 0000000000..72a6d87c73 --- /dev/null +++ b/src/qibocal/protocols/rabi/conditional_chevron_length_signal.py @@ -0,0 +1,317 @@ +"""Rabi experiment that sweeps length and frequency.""" + +from dataclasses import dataclass, field +from typing import Optional, Union + +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: Optional[float] = 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, Union[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.""" From 8b0f3e8fa95cd51a0e536f61b3d7b3df65722fed Mon Sep 17 00:00:00 2001 From: lballerio Date: Fri, 24 Apr 2026 11:15:11 +0400 Subject: [PATCH 4/7] adjusting sequences for spectator experiments --- .../qubit_spectroscopies/__init__.py | 4 + .../qubit_spectroscopy_conditional.py | 29 +- .../qubits_spectroscopy_spectator.py | 20 +- .../protocols/qubit_spectroscopies/utils.py | 16 +- .../rabi/conditional_chevron_ampl.py | 14 +- .../rabi/conditional_chevron_ampl_signal.py | 21 +- .../conditional_chevron_length_signal copy.py | 317 ------------------ 7 files changed, 48 insertions(+), 373 deletions(-) delete mode 100644 src/qibocal/protocols/rabi/conditional_chevron_length_signal copy.py diff --git a/src/qibocal/protocols/qubit_spectroscopies/__init__.py b/src/qibocal/protocols/qubit_spectroscopies/__init__.py index 45b1bc4fde..9f21d4c58f 100644 --- a/src/qibocal/protocols/qubit_spectroscopies/__init__.py +++ b/src/qibocal/protocols/qubit_spectroscopies/__init__.py @@ -1,4 +1,7 @@ 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 @@ -12,4 +15,5 @@ "qubit_conditional_spectroscopy", "qubit_spectroscopy_spectator_scan", "qubit_amplitude_spectroscopy", + "conditional_broad_spectator_spectroscopy", ] diff --git a/src/qibocal/protocols/qubit_spectroscopies/qubit_spectroscopy_conditional.py b/src/qibocal/protocols/qubit_spectroscopies/qubit_spectroscopy_conditional.py index 70042e9d03..4cfe94ccbb 100644 --- a/src/qibocal/protocols/qubit_spectroscopies/qubit_spectroscopy_conditional.py +++ b/src/qibocal/protocols/qubit_spectroscopies/qubit_spectroscopy_conditional.py @@ -4,7 +4,7 @@ import numpy.typing as npt import plotly.graph_objects as go from plotly.subplots import make_subplots -from qibolab import Delay, PulseSequence +from qibolab import PulseSequence from qibocal.auto.operation import QubitId, QubitPairId, Routine from qibocal.calibration import CalibrationPlatform @@ -99,7 +99,7 @@ def _acquisition( # Calculate batches batches = calculate_batches(params.freq_width) - spectro_seq, ro_pulses, drive_channels, los_channels, amplitudes = ( + spectro_seq, target_ro_seq, ro_pulses, drive_channels, los_channels, amplitudes = ( spectroscopy_sequence( params=params, platform=platform, @@ -129,23 +129,10 @@ def _acquisition( # 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, - Delay( - duration=(spectro_seq.duration + spectators_drive_sequence.duration) - ), - ) - ) spectators_ro_sequence.append((spect_ro_channel, spect_ro_pulse)) - # adding delay for target channels - spect_drive_seq_duration = spectators_drive_sequence.duration - spectators_drive_sequence += PulseSequence( - (ch, Delay(duration=spect_drive_seq_duration)) for ch in spectro_seq.channels - ) - - complete_sequence = spectators_drive_sequence + spectro_seq + spectators_ro_sequence + 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: @@ -169,7 +156,8 @@ def _acquisition( ) # Collect results from this batch - for t, s in targets: + 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) @@ -180,9 +168,8 @@ def _acquisition( _phase = phase(result) data.register_qubit( - target=t, - spectator=s, - target_freq=parsweep[t], + pair=pair, + target_freq=parsweep[t].values, target_signal=signal, target_phase=_phase, spectator_signal=spectator_signal, diff --git a/src/qibocal/protocols/qubit_spectroscopies/qubits_spectroscopy_spectator.py b/src/qibocal/protocols/qubit_spectroscopies/qubits_spectroscopy_spectator.py index b7e168df80..c3f0238d54 100644 --- a/src/qibocal/protocols/qubit_spectroscopies/qubits_spectroscopy_spectator.py +++ b/src/qibocal/protocols/qubit_spectroscopies/qubits_spectroscopy_spectator.py @@ -105,7 +105,7 @@ def _acquisition( # Calculate batches batches = calculate_batches(params.freq_width) - spectro_seq, ro_pulses, drive_channels, los_channels, amplitudes = ( + spectro_seq, targ_ro_seq, ro_pulses, drive_channels, los_channels, amplitudes = ( spectroscopy_sequence( params=params, platform=platform, @@ -121,15 +121,21 @@ def _acquisition( ) # executing the experiment for each spectator qubit separately - spectator_seq, spectator_ro_pulses, spectator_drive_ch, spectator_los_ch, _ = ( - spectroscopy_sequence( - params=params, - platform=platform, - targets=spectator_qubits_list, - ) + ( + 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: diff --git a/src/qibocal/protocols/qubit_spectroscopies/utils.py b/src/qibocal/protocols/qubit_spectroscopies/utils.py index 5cb6f78b99..1ecad845bc 100644 --- a/src/qibocal/protocols/qubit_spectroscopies/utils.py +++ b/src/qibocal/protocols/qubit_spectroscopies/utils.py @@ -4,7 +4,6 @@ import numpy as np import numpy.typing as npt from qibolab import ( - Delay, Parameter, PulseLike, PulseSequence, @@ -77,6 +76,7 @@ def spectroscopy_sequence( platform: CalibrationPlatform, targets: list[QubitId], ) -> tuple[ + PulseSequence, PulseSequence, dict[QubitId, PulseLike], dict[QubitId, ChannelId], @@ -85,6 +85,7 @@ def spectroscopy_sequence( ]: # Build the pulse sequence sequence = PulseSequence() + ro_sequence = PulseSequence() ro_pulses = {} drive_channels = {} los_channels = {} @@ -111,10 +112,17 @@ def spectroscopy_sequence( los_channels[qubit] = None sequence.append((qd_channel, qd_pulse)) - sequence.append((ro_channel, Delay(duration=qd_pulse.duration))) - sequence.append((ro_channel, ro_pulse)) - return sequence, ro_pulses, drive_channels, los_channels, drive_amplitudes + 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): diff --git a/src/qibocal/protocols/rabi/conditional_chevron_ampl.py b/src/qibocal/protocols/rabi/conditional_chevron_ampl.py index 1dafb6739c..5eb622aa4e 100644 --- a/src/qibocal/protocols/rabi/conditional_chevron_ampl.py +++ b/src/qibocal/protocols/rabi/conditional_chevron_ampl.py @@ -147,16 +147,8 @@ def _acquisition( if params.activate_spectators: complete_sequence += PulseSequence(list(spectators_drive_dict.values())) - compl_seq_len = complete_sequence.duration - complete_sequence += PulseSequence( - (ch, Delay(duration=compl_seq_len)) for ch in t_sequence.channels - ) - complete_sequence += PulseSequence( - (seq[0], Delay(duration=compl_seq_len)) - for seq in spectators_ro_dict.values() - ) - complete_sequence += t_sequence + complete_sequence |= t_sequence complete_sequence += PulseSequence( (spectators_ro_dict[s][0], Delay(duration=t_qd_pulses[t].duration)) for t, s in targets @@ -270,9 +262,9 @@ def _plot( horizontal_spacing=0.1, vertical_spacing=0.2, subplot_titles=( - ("PI pulse applied -" if fit.activate_spectators else "") + ("PI pulse applied -" if data.activate_spectators else "") + f" Prob Target {target[0]}", - ("PI pulse applied -" if fit.activate_spectators else "") + ("PI pulse applied -" if data.activate_spectators else "") + f" Prob Spectator {target[1]}", ), ) diff --git a/src/qibocal/protocols/rabi/conditional_chevron_ampl_signal.py b/src/qibocal/protocols/rabi/conditional_chevron_ampl_signal.py index 453bfcf412..bb672faca9 100644 --- a/src/qibocal/protocols/rabi/conditional_chevron_ampl_signal.py +++ b/src/qibocal/protocols/rabi/conditional_chevron_ampl_signal.py @@ -167,16 +167,8 @@ def _acquisition( if params.activate_spectators: complete_sequence += PulseSequence(list(spectators_drive_dict.values())) - compl_seq_len = complete_sequence.duration - complete_sequence += PulseSequence( - (ch, Delay(duration=compl_seq_len)) for ch in t_sequence.channels - ) - complete_sequence += PulseSequence( - (seq[0], Delay(duration=compl_seq_len)) - for seq in spectators_ro_dict.values() - ) - complete_sequence += t_sequence + complete_sequence |= t_sequence complete_sequence += PulseSequence( (spectators_ro_dict[s][0], Delay(duration=t_qd_pulses[t].duration)) for t, s in targets @@ -287,6 +279,7 @@ def _plot( fit: ConditionalRabiChevronAmplSignalResults = None, ): """Plotting function for ConditionalRabiChevron.""" + figures = [] fitting_report = "" fig = make_subplots( @@ -294,9 +287,9 @@ def _plot( cols=2, horizontal_spacing=0.1, subplot_titles=( - ("PI pulse applied -" if fit.activate_spectators else "") + ("PI pulse applied -" if data.activate_spectators else "") + f"Signal [a.u.] - Target {target[0]}", - ("PI pulse applied -" if fit.activate_spectators else "") + ("PI pulse applied -" if data.activate_spectators else "") + f"Phase [rad] - Target {target[0]}", ), ) @@ -332,6 +325,7 @@ def _plot( 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( @@ -345,9 +339,9 @@ def _plot( cols=2, horizontal_spacing=0.1, subplot_titles=( - ("PI pulse applied -" if fit.activate_spectators else "") + ("PI pulse applied -" if data.activate_spectators else "") + f"Signal [a.u.] - Spectator {target[1]}", - ("PI pulse applied -" if fit.activate_spectators else "") + ("PI pulse applied -" if data.activate_spectators else "") + f"Phase [rad] - Spectator {target[1]}", ), ) @@ -377,6 +371,7 @@ def _plot( 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, diff --git a/src/qibocal/protocols/rabi/conditional_chevron_length_signal copy.py b/src/qibocal/protocols/rabi/conditional_chevron_length_signal copy.py deleted file mode 100644 index 72a6d87c73..0000000000 --- a/src/qibocal/protocols/rabi/conditional_chevron_length_signal copy.py +++ /dev/null @@ -1,317 +0,0 @@ -"""Rabi experiment that sweeps length and frequency.""" - -from dataclasses import dataclass, field -from typing import Optional, Union - -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: Optional[float] = 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, Union[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.""" From 6e32c2d335196691aecc3559462b8a675ee48dc8 Mon Sep 17 00:00:00 2001 From: lballerio Date: Tue, 28 Apr 2026 18:57:41 +0400 Subject: [PATCH 5/7] fix bug in conditional experiment plus adding conditional cross rabi chevron --- src/qibocal/protocols/rabi/__init__.py | 2 + .../rabi/conditional_chevron_ampl.py | 90 ++++-------- .../rabi/conditional_chevron_ampl_signal.py | 19 ++- .../rabi/conditional_cross_chevron_ampl.py | 129 ++++++++++++++++++ src/qibocal/protocols/rabi/utils.py | 27 ++-- 5 files changed, 190 insertions(+), 77 deletions(-) create mode 100644 src/qibocal/protocols/rabi/conditional_cross_chevron_ampl.py diff --git a/src/qibocal/protocols/rabi/__init__.py b/src/qibocal/protocols/rabi/__init__.py index 6c331de3e4..d02733888f 100644 --- a/src/qibocal/protocols/rabi/__init__.py +++ b/src/qibocal/protocols/rabi/__init__.py @@ -5,6 +5,7 @@ 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 @@ -24,4 +25,5 @@ "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 index 5eb622aa4e..9906c57b88 100644 --- a/src/qibocal/protocols/rabi/conditional_chevron_ampl.py +++ b/src/qibocal/protocols/rabi/conditional_chevron_ampl.py @@ -1,4 +1,4 @@ -"""Rabi experiment that sweeps length and frequency.""" +"""Rabi experiment that sweeps amplitude and frequency when toggling a spectator qubit.""" from dataclasses import dataclass, field from typing import Optional, Union @@ -16,19 +16,25 @@ Sweeper, ) -from qibocal.auto.operation import Data, Parameters, QubitPairId, Results, Routine +from qibocal.auto.operation import ( + Data, + Parameters, + QubitId, + QubitPairId, + Results, + Routine, +) from qibocal.calibration import CalibrationPlatform -from qibocal.config import log from ..utils import HZ_TO_GHZ, readout_frequency -from .utils import fit_amplitude_function, rabi_initial_guess, sequence_amplitude +from .utils import sequence_amplitude __all__ = ["conditional_rabi_chevron_ampl"] @dataclass class ConditionalRabiChevronAmplParameters(Parameters): - """ConditionalRabiChevron runcard inputs.""" + """ConditionalRabiAmplChevron runcard inputs.""" amplitude_start: float """Initial pi pulse amplitude [a.u.].""" @@ -66,7 +72,7 @@ def amplitude_range(self): @dataclass class ConditionalRabiChevronAmplResults(Results): - """ConditionalRabiChevron outputs.""" + """ConditionalRabiAmplChevron outputs.""" activate_spectators: bool """Flag for setting spectator qbuit in state 1.""" @@ -84,12 +90,12 @@ class ConditionalRabiChevronAmplResults(Results): ("spectator", np.float64), ] ) -"""Custom dtype for rabi length.""" +"""Custom dtype for rabi amplitude with spectator.""" @dataclass -class RabiLengthFreqSignalData(Data): - """RabiLengthFreqSignal data acquisition.""" +class ConditionalRabiChevronAmplData(Data): + """ConditionalRabiAmplChevron data acquisition.""" activate_spectators: bool """Flag for setting spectator qbuit in state 1.""" @@ -110,7 +116,7 @@ def register_qubit(self, pair, freq, ampl, p_targ, p_spect): self.data[pair] = np.rec.array(data) def amplitudes(self, qubit_pair: QubitPairId) -> npt.NDArray: - """Unique qubit lengths.""" + """Unique qubit amplitudes.""" return np.unique(self[qubit_pair].ampl) def frequencies(self, qubit_pair: QubitPairId) -> npt.NDArray: @@ -122,8 +128,8 @@ def _acquisition( params: ConditionalRabiChevronAmplParameters, platform: CalibrationPlatform, targets: list[QubitPairId], -) -> RabiLengthFreqSignalData: - """Data acquisition for Rabi experiment sweeping length.""" +) -> 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.") @@ -138,8 +144,8 @@ def _acquisition( complete_sequence = PulseSequence() - spectators_drive_dict = {} - spectators_ro_dict = {} + 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] @@ -161,7 +167,7 @@ def _acquisition( pulses=[t_qd_pulses[q] for q in target_qubits_list], ) - freq_sweepers = {} + freq_sweepers: dict[QubitId, Sweeper] = {} for t in target_qubits_list: target_drive_ch = platform.qubits[t].drive freq_sweepers[t] = Sweeper( @@ -170,7 +176,9 @@ def _acquisition( channels=[target_drive_ch], ) - data = RabiLengthFreqSignalData(activate_spectators=params.activate_spectators) + data = ConditionalRabiChevronAmplData( + activate_spectators=params.activate_spectators + ) results = platform.execute( [complete_sequence], @@ -198,60 +206,20 @@ def _acquisition( return data -def _fit(data: RabiLengthFreqSignalData) -> ConditionalRabiChevronAmplResults: +def _fit(data: ConditionalRabiChevronAmplData) -> ConditionalRabiChevronAmplResults: """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) - target_prob = data[pair].target - t_prob_matrix = target_prob.reshape(len(amplitudes), len(freqs)).T - - # guess optimal frequency maximizing oscillatio amplitude - index = np.argmax([max(x) - min(x) for x in t_prob_matrix]) - frequency = freqs[index] - - y = t_prob_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=False) - - try: - popt, _, pi_pulse_parameter = fit_amplitude_function( - x, - y, - pguess, - signal=False, - 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 ConditionalRabiChevronAmplResults( activate_spectators=data.activate_spectators, - amplitude=fitted_amplitudes, - fitted_parameters=fitted_parameters, + amplitude={}, + fitted_parameters={}, ) def _plot( - data: RabiLengthFreqSignalData, + data: ConditionalRabiChevronAmplData, target: QubitPairId, - fit: ConditionalRabiChevronAmplResults = None, + fit: Optional[ConditionalRabiChevronAmplResults] = None, ): """Plotting function for ConditionalRabiChevron.""" figures = [] diff --git a/src/qibocal/protocols/rabi/conditional_chevron_ampl_signal.py b/src/qibocal/protocols/rabi/conditional_chevron_ampl_signal.py index bb672faca9..7d67427f5f 100644 --- a/src/qibocal/protocols/rabi/conditional_chevron_ampl_signal.py +++ b/src/qibocal/protocols/rabi/conditional_chevron_ampl_signal.py @@ -16,7 +16,14 @@ Sweeper, ) -from qibocal.auto.operation import Data, Parameters, QubitPairId, Results, Routine +from qibocal.auto.operation import ( + Data, + Parameters, + QubitId, + QubitPairId, + Results, + Routine, +) from qibocal.calibration import CalibrationPlatform from qibocal.config import log @@ -134,7 +141,7 @@ 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 computespectator_phase(self, qubit_pair: QubitPairId) -> npt.NDArray: + 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)) @@ -158,8 +165,8 @@ def _acquisition( ) complete_sequence = PulseSequence() - spectators_drive_dict = {} - spectators_ro_dict = {} + 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] @@ -181,7 +188,7 @@ def _acquisition( pulses=[t_qd_pulses[q] for q in target_qubits_list], ) - freq_sweepers = {} + freq_sweepers: dict[QubitId, Sweeper] = {} for t in target_qubits_list: target_drive_ch = platform.qubits[t].drive freq_sweepers[t] = Sweeper( @@ -299,7 +306,7 @@ def _plot( t_mag = data.compute_target_magnitude(target) t_phase = data.compute_target_phase(target) s_mag = data.compute_spectator_magnitude(target) - s_phase = data.computespectator_phase(target) + s_phase = data.compute_spectator_phase(target) fig.add_trace( go.Heatmap( 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] From 0707fd912c61c879087f2609562984cdb2548010 Mon Sep 17 00:00:00 2001 From: lballerio Date: Thu, 30 Apr 2026 15:15:09 +0400 Subject: [PATCH 6/7] adding broad frequencies spectroscopy --- .../qubit_broad_spectroscopy_conditional.py | 314 ++++++++++++++++++ .../rabi/conditional_chevron_ampl.py | 26 +- 2 files changed, 325 insertions(+), 15 deletions(-) create mode 100644 src/qibocal/protocols/qubit_spectroscopies/qubit_broad_spectroscopy_conditional.py 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/rabi/conditional_chevron_ampl.py b/src/qibocal/protocols/rabi/conditional_chevron_ampl.py index 9906c57b88..229a7a7cf3 100644 --- a/src/qibocal/protocols/rabi/conditional_chevron_ampl.py +++ b/src/qibocal/protocols/rabi/conditional_chevron_ampl.py @@ -26,7 +26,7 @@ ) from qibocal.calibration import CalibrationPlatform -from ..utils import HZ_TO_GHZ, readout_frequency +from ..utils import HZ_TO_GHZ from .utils import sequence_amplitude __all__ = ["conditional_rabi_chevron_ampl"] @@ -138,12 +138,10 @@ def _acquisition( 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( + complete_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: @@ -151,16 +149,17 @@ def _acquisition( 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())) + if params.activate_spectators: + complete_sequence = ( + PulseSequence(list(spectators_drive_dict.values())) | complete_sequence + ) + ampl_sweeper = Sweeper( parameter=Parameter.amplitude, values=params.amplitude_range, @@ -183,22 +182,19 @@ def _acquisition( 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[t_ro_pulses[pair[0]].id] - spect_result = results[spectators_ro_dict[pair[1]][1].id] + 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[pair[0]].values, + freq=freq_sweepers[t].values, ampl=ampl_sweeper.values, p_targ=target_result, p_spect=spect_result, From ab629922b5a90e140c83a26c9f44c8483c9cf3d6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 11:55:54 +0000 Subject: [PATCH 7/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../qubit_spectroscopies/qubit_ampl_spectroscopy.py | 3 +-- .../qubit_spectroscopies/qubit_amplitude_spectroscopy.py | 3 +-- src/qibocal/protocols/qubit_spectroscopies/utils.py | 3 +-- src/qibocal/protocols/rabi/conditional_chevron_ampl.py | 7 +++---- .../protocols/rabi/conditional_chevron_ampl_signal.py | 5 ++--- .../protocols/rabi/conditional_chevron_length_signal.py | 5 ++--- 6 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/qibocal/protocols/qubit_spectroscopies/qubit_ampl_spectroscopy.py b/src/qibocal/protocols/qubit_spectroscopies/qubit_ampl_spectroscopy.py index aa6f47f05b..2c1109d9af 100644 --- a/src/qibocal/protocols/qubit_spectroscopies/qubit_ampl_spectroscopy.py +++ b/src/qibocal/protocols/qubit_spectroscopies/qubit_ampl_spectroscopy.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from typing import Optional import numpy as np import plotly.graph_objects as go @@ -129,7 +128,7 @@ def _fit(data: QubitAmplitudeSpectroscopyData) -> Results: def _plot( data: ResonatorPunchoutData, target: QubitId, - fit: Optional[QubitSpectroscopyResults] = None, + fit: QubitSpectroscopyResults | None = None, ): """Plot QubitPunchout.""" figures = [] diff --git a/src/qibocal/protocols/qubit_spectroscopies/qubit_amplitude_spectroscopy.py b/src/qibocal/protocols/qubit_spectroscopies/qubit_amplitude_spectroscopy.py index aa6f47f05b..2c1109d9af 100644 --- a/src/qibocal/protocols/qubit_spectroscopies/qubit_amplitude_spectroscopy.py +++ b/src/qibocal/protocols/qubit_spectroscopies/qubit_amplitude_spectroscopy.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from typing import Optional import numpy as np import plotly.graph_objects as go @@ -129,7 +128,7 @@ def _fit(data: QubitAmplitudeSpectroscopyData) -> Results: def _plot( data: ResonatorPunchoutData, target: QubitId, - fit: Optional[QubitSpectroscopyResults] = None, + fit: QubitSpectroscopyResults | None = None, ): """Plot QubitPunchout.""" figures = [] diff --git a/src/qibocal/protocols/qubit_spectroscopies/utils.py b/src/qibocal/protocols/qubit_spectroscopies/utils.py index 1ecad845bc..6febbce53e 100644 --- a/src/qibocal/protocols/qubit_spectroscopies/utils.py +++ b/src/qibocal/protocols/qubit_spectroscopies/utils.py @@ -1,5 +1,4 @@ from dataclasses import dataclass, field -from typing import Optional import numpy as np import numpy.typing as npt @@ -43,7 +42,7 @@ class QubitSpectroscopyParameters(Parameters): """Frequency [Hz] step for sweep.""" drive_duration: int """Drive pulse duration [ns]. Same for all qubits.""" - drive_amplitude: Optional[float] = None + drive_amplitude: float | None = None """Drive pulse amplitude (optional). Same for all qubits.""" hardware_average: bool = True """By default hardware average will be performed.""" diff --git a/src/qibocal/protocols/rabi/conditional_chevron_ampl.py b/src/qibocal/protocols/rabi/conditional_chevron_ampl.py index 229a7a7cf3..7879cb4598 100644 --- a/src/qibocal/protocols/rabi/conditional_chevron_ampl.py +++ b/src/qibocal/protocols/rabi/conditional_chevron_ampl.py @@ -1,7 +1,6 @@ """Rabi experiment that sweeps amplitude and frequency when toggling a spectator qubit.""" from dataclasses import dataclass, field -from typing import Optional, Union import numpy as np import numpy.typing as npt @@ -50,7 +49,7 @@ class ConditionalRabiChevronAmplParameters(Parameters): """Frequency to use as step for the scan.""" activate_spectators: bool = True """Flag for setting spectator qbuit in state 1.""" - pulse_length: Optional[float] = None + pulse_length: float | None = None """Pi pulse duration. Same for all qubits.""" @property @@ -76,7 +75,7 @@ class ConditionalRabiChevronAmplResults(Results): activate_spectators: bool """Flag for setting spectator qbuit in state 1.""" - amplitude: dict[QubitPairId, Union[float, list[float]]] + amplitude: dict[QubitPairId, float | list[float]] """Pi pulse duration for each qubit.""" fitted_parameters: dict[QubitPairId, dict[str, float]] """Raw fitting output.""" @@ -215,7 +214,7 @@ def _fit(data: ConditionalRabiChevronAmplData) -> ConditionalRabiChevronAmplResu def _plot( data: ConditionalRabiChevronAmplData, target: QubitPairId, - fit: Optional[ConditionalRabiChevronAmplResults] = None, + fit: ConditionalRabiChevronAmplResults | None = None, ): """Plotting function for ConditionalRabiChevron.""" figures = [] diff --git a/src/qibocal/protocols/rabi/conditional_chevron_ampl_signal.py b/src/qibocal/protocols/rabi/conditional_chevron_ampl_signal.py index 7d67427f5f..14f202876a 100644 --- a/src/qibocal/protocols/rabi/conditional_chevron_ampl_signal.py +++ b/src/qibocal/protocols/rabi/conditional_chevron_ampl_signal.py @@ -1,7 +1,6 @@ """Rabi experiment that sweeps length and frequency.""" from dataclasses import dataclass, field -from typing import Optional, Union import numpy as np import numpy.typing as npt @@ -52,7 +51,7 @@ class ConditionalRabiChevronAmplSignalParameters(Parameters): """Frequency to use as step for the scan.""" activate_spectators: bool = True """Flag for setting spectator qbuit in state 1.""" - pulse_length: Optional[float] = None + pulse_length: float | None = None """Pi pulse duration. Same for all qubits.""" @property @@ -78,7 +77,7 @@ class ConditionalRabiChevronAmplSignalResults(Results): activate_spectators: bool """Flag for setting spectator qbuit in state 1.""" - amplitude: dict[QubitPairId, Union[float, list[float]]] + amplitude: dict[QubitPairId, float | list[float]] """Pi pulse duration for each qubit.""" fitted_parameters: dict[QubitPairId, dict[str, float]] """Raw fitting output.""" diff --git a/src/qibocal/protocols/rabi/conditional_chevron_length_signal.py b/src/qibocal/protocols/rabi/conditional_chevron_length_signal.py index 72a6d87c73..aacc548e1b 100644 --- a/src/qibocal/protocols/rabi/conditional_chevron_length_signal.py +++ b/src/qibocal/protocols/rabi/conditional_chevron_length_signal.py @@ -1,7 +1,6 @@ """Rabi experiment that sweeps length and frequency.""" from dataclasses import dataclass, field -from typing import Optional, Union import numpy as np import numpy.typing as npt @@ -43,7 +42,7 @@ class ConditionalRabiChevronLenSignalParameters(Parameters): """Maximum frequency as an offset.""" step_freq: int """Frequency to use as step for the scan.""" - pulse_amplitude: Optional[float] = None + pulse_amplitude: float | None = None """Pi pulse amplitude. Same for all qubits.""" @property @@ -67,7 +66,7 @@ def duration_range(self): class ConditionalRabiChevronLenSignalResults(Results): """ConditionalRabiChevron outputs.""" - length: dict[QubitPairId, Union[int, list[int]]] + length: dict[QubitPairId, int | list[int]] """Pi pulse duration for each qubit.""" fitted_parameters: dict[QubitPairId, dict[str, float]] """Raw fitting output."""