diff --git a/src/qibocal/protocols/characterization/__init__.py b/src/qibocal/protocols/characterization/__init__.py index b20782945..8beb1eda7 100644 --- a/src/qibocal/protocols/characterization/__init__.py +++ b/src/qibocal/protocols/characterization/__init__.py @@ -14,6 +14,8 @@ from .coherence.t2_sequences import t2_sequences from .coherence.zeno import zeno from .coherence.zeno_msr import zeno_msr +from .couplers.coupler_qubit_spectroscopy import coupler_qubit_spectroscopy +from .couplers.coupler_resonator_spectroscopy import coupler_resonator_spectroscopy from .dispersive_shift import dispersive_shift from .dispersive_shift_qutrit import dispersive_shift_qutrit from .fast_reset.fast_reset import fast_reset @@ -103,3 +105,5 @@ class Operation(Enum): qutrit_classification = qutrit_classification resonator_amplitude = resonator_amplitude dispersive_shift_qutrit = dispersive_shift_qutrit + coupler_resonator_spectroscopy = coupler_resonator_spectroscopy + coupler_qubit_spectroscopy = coupler_qubit_spectroscopy diff --git a/src/qibocal/protocols/characterization/couplers/__init__.py b/src/qibocal/protocols/characterization/couplers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/qibocal/protocols/characterization/couplers/coupler_qubit_spectroscopy.py b/src/qibocal/protocols/characterization/couplers/coupler_qubit_spectroscopy.py new file mode 100644 index 000000000..da82abcf0 --- /dev/null +++ b/src/qibocal/protocols/characterization/couplers/coupler_qubit_spectroscopy.py @@ -0,0 +1,123 @@ +from typing import Optional + +import numpy as np +from qibolab import AcquisitionType, AveragingMode, ExecutionParameters +from qibolab.platform import Platform +from qibolab.pulses import PulseSequence +from qibolab.sweeper import Parameter, Sweeper, SweeperType + +from qibocal.auto.operation import Qubits, Routine + +from ..two_qubit_interaction.utils import order_pair +from .coupler_resonator_spectroscopy import _fit, _plot, _update +from .utils import CouplerSpectroscopyData, CouplerSpectroscopyParameters + + +class CouplerSpectroscopyParametersQubit(CouplerSpectroscopyParameters): + drive_duration: Optional[int] = 2000 + """Drive pulse duration to excite the qubit before the measurement""" + + +def _acquisition( + params: CouplerSpectroscopyParametersQubit, platform: Platform, qubits: Qubits +) -> CouplerSpectroscopyData: + """ + Data acquisition for CouplerQubit spectroscopy. + + This consist on a frequency sweep on the qubit frequency while we change the flux coupler pulse amplitude of + the coupler pulse. We expect to enable the coupler during the amplitude sweep and detect an avoided crossing + that will be followed by the frequency sweep. This needs the qubits at resonance, the routine assumes a sweetspot + value for the higher frequency qubit that moves it to the lower frequency qubit instead of trying to calibrate both pulses at once. This should be run after + qubit_spectroscopy to further adjust the coupler sweetspot if needed and get some information + on the flux coupler pulse amplitude requiered to enable 2q interactions. + + """ + + # TODO: Do we want to measure both qubits on the pair ? + # Different acquisition, for now only measure one and reduce possible crosstalk. + + # create a sequence of pulses for the experiment: + # Coupler pulse while Drive pulse - MZ + + sequence = PulseSequence() + ro_pulses = {} + qd_pulses = {} + couplers = [] + for i, pair in enumerate(qubits): + qubit = platform.qubits[params.measured_qubits[i]].name + # TODO: Qubit pair patch + ordered_pair = order_pair(pair, platform.qubits) + couplers.append(platform.pairs[tuple(sorted(ordered_pair))].coupler) + + ro_pulses[qubit] = platform.create_qubit_readout_pulse( + qubit, start=params.drive_duration + ) + qd_pulses[qubit] = platform.create_qubit_drive_pulse( + qubit, start=0, duration=params.drive_duration + ) + if params.amplitude is not None: + qd_pulses[qubit].amplitude = params.amplitude + + sequence.add(qd_pulses[qubit]) + sequence.add(ro_pulses[qubit]) + + # define the parameter to sweep and its range: + delta_frequency_range = np.arange( + -params.freq_width // 2, params.freq_width // 2, params.freq_step + ) + + sweeper_freq = Sweeper( + Parameter.frequency, + delta_frequency_range, + pulses=[qd_pulses[qubit] for qubit in params.measured_qubits], + type=SweeperType.OFFSET, + ) + + # define the parameter to sweep and its range: + delta_bias_range = np.arange( + -params.bias_width / 2, params.bias_width / 2, params.bias_step + ) + + # This sweeper is implemented in the flux pulse amplitude and we need it to be that way. + sweeper_bias = Sweeper( + Parameter.bias, + delta_bias_range, + couplers=couplers, + type=SweeperType.ABSOLUTE, + ) + + data = CouplerSpectroscopyData( + resonator_type=platform.resonator_type, + ) + + results = platform.sweep( + sequence, + ExecutionParameters( + nshots=params.nshots, + relaxation_time=params.relaxation_time, + acquisition_type=AcquisitionType.INTEGRATION, + averaging_mode=AveragingMode.CYCLIC, + ), + sweeper_bias, + sweeper_freq, + ) + + # retrieve the results for every qubit + for i, pair in enumerate(qubits): + # TODO: May measure both qubits on the pair + qubit = platform.qubits[params.measured_qubits[i]].name + # average msr, phase, i and q over the number of shots defined in the runcard + result = results[ro_pulses[qubit].serial] + # store the results + data.register_qubit( + qubit, + msr=result.magnitude, + phase=result.phase, + freq=delta_frequency_range + qd_pulses[qubit].frequency, + bias=delta_bias_range, + ) + return data + + +coupler_qubit_spectroscopy = Routine(_acquisition, _fit, _plot, _update) +"""CouplerQubitSpectroscopy Routine object.""" diff --git a/src/qibocal/protocols/characterization/couplers/coupler_resonator_spectroscopy.py b/src/qibocal/protocols/characterization/couplers/coupler_resonator_spectroscopy.py new file mode 100644 index 000000000..5408817e2 --- /dev/null +++ b/src/qibocal/protocols/characterization/couplers/coupler_resonator_spectroscopy.py @@ -0,0 +1,177 @@ +from typing import Optional + +import numpy as np +from qibolab import AcquisitionType, AveragingMode, ExecutionParameters +from qibolab.platform import Platform +from qibolab.pulses import PulseSequence +from qibolab.qubits import QubitId +from qibolab.sweeper import Parameter, Sweeper, SweeperType + +from qibocal.auto.operation import Qubits, Routine + +from ..flux_dependence.utils import flux_dependence_plot +from ..two_qubit_interaction.utils import order_pair +from .utils import ( + CouplerSpectroscopyData, + CouplerSpectroscopyParameters, + CouplerSpectroscopyResults, +) + + +class CouplerSpectroscopyParametersResonator(CouplerSpectroscopyParameters): + readout_delay: Optional[int] = 1000 + """Readout delay before the measurement is done to let the flux coupler pulse act""" + + +def _acquisition( + params: CouplerSpectroscopyParametersResonator, platform: Platform, qubits: Qubits +) -> CouplerSpectroscopyData: + """ + Data acquisition for CouplerResonator spectroscopy. + + This consist on a frequency sweep on the readout frequency while we change the flux coupler pulse amplitude of + the coupler pulse. We expect to enable the coupler during the amplitude sweep and detect an avoided crossing + that will be followed by the frequency sweep. No need to have the qubits at resonance. This should be run after + resonator_spectroscopy to detect couplers and adjust the coupler sweetspot if needed and get some information + on the flux coupler pulse amplitude requiered to enable 2q interactions. + + """ + + # TODO: Do we want to measure both qubits on the pair ? + # Different acquisition, for now only measure one and reduce possible crosstalk. + + # create a sequence of pulses for the experiment: + # Coupler pulse while MZ + + # taking advantage of multiplexing, apply the same set of gates to all qubits in parallel + sequence = PulseSequence() + ro_pulses = {} + fx_pulses = {} + couplers = [] + + for i, pair in enumerate(qubits): + qubit = platform.qubits[params.measured_qubits[i]].name + # TODO: Qubit pair patch + ordered_pair = order_pair(pair, platform.qubits) + coupler = platform.pairs[tuple(sorted(ordered_pair))].coupler + couplers.append(coupler) + + # TODO: May measure both qubits on the pair + ro_pulses[qubit] = platform.create_qubit_readout_pulse( + qubit, start=params.readout_delay + ) + if params.amplitude is not None: + ro_pulses[qubit].amplitude = params.amplitude + + sequence.add(ro_pulses[qubit]) + + # define the parameter to sweep and its range: + delta_frequency_range = np.arange( + -params.freq_width // 2, params.freq_width // 2, params.freq_step + ) + + sweeper_freq = Sweeper( + Parameter.frequency, + delta_frequency_range, + pulses=[ro_pulses[qubit] for qubit in params.measured_qubits], + type=SweeperType.OFFSET, + ) + + # define the parameter to sweep and its range: + delta_bias_range = np.arange( + -params.bias_width / 2, params.bias_width / 2, params.bias_step + ) + + # This sweeper is implemented in the flux pulse amplitude and we need it to be that way. + sweeper_bias = Sweeper( + Parameter.bias, + delta_bias_range, + couplers=couplers, + type=SweeperType.ABSOLUTE, + ) + + data = CouplerSpectroscopyData( + resonator_type=platform.resonator_type, + ) + + results = platform.sweep( + sequence, + ExecutionParameters( + nshots=params.nshots, + relaxation_time=params.relaxation_time, + acquisition_type=AcquisitionType.INTEGRATION, + averaging_mode=AveragingMode.CYCLIC, + ), + sweeper_bias, + sweeper_freq, + ) + + # retrieve the results for every qubit + for i, pair in enumerate(qubits): + # TODO: May measure both qubits on the pair + qubit = platform.qubits[params.measured_qubits[i]].name + # average msr, phase, i and q over the number of shots defined in the runcard + result = results[ro_pulses[qubit].serial] + # store the results + data.register_qubit( + qubit, + msr=result.magnitude, + phase=result.phase, + freq=delta_frequency_range + ro_pulses[qubit].frequency, + bias=delta_bias_range, + ) + return data + + +def _fit(data: CouplerSpectroscopyData) -> CouplerSpectroscopyResults: + """Post-processing function for CouplerResonatorSpectroscopy.""" + qubits = data.qubits + pulse_amp = {} + sweetspot = {} + fitted_parameters = {} + + for qubit in qubits: + # TODO: Implement fit + """It should get two things: + Coupler sweetspot: the value that makes both features centered and symmetric + Pulse_amp: That turn on the feature taking into account the shift introduced by the coupler sweetspot + + Issues: Coupler sweetspot it measured in volts while pulse_amp is a pulse amplitude, this routine just sweeps pulse amplitude + and relies on manual shifting of that sweetspot by repeated scans as current chips are already symmetric for this feature. + Maybe another routine sweeping the bias in volts would be needed and that sweeper implement on Zurich driver. + """ + # spot, amp, fitted_params = coupler_fit(data[qubit]) + + sweetspot[qubit] = 0 + pulse_amp[qubit] = 0 + fitted_parameters[qubit] = {} + + return CouplerSpectroscopyResults( + pulse_amp=pulse_amp, + sweetspot=sweetspot, + fitted_parameters=fitted_parameters, + ) + + +def _plot( + data: CouplerSpectroscopyData, + qubit, + fit: CouplerSpectroscopyResults, +): + """ + We may want to measure both qubits on the pair, + that will require a different plotting that takes both. + """ + qubit_pair = qubit # TODO: Patch for 2q gate routines + + for qubit in qubit_pair: + if qubit in data.data.keys(): + return flux_dependence_plot(data, fit, qubit) + + +def _update(results: CouplerSpectroscopyResults, platform: Platform, qubit: QubitId): + pass + + +coupler_resonator_spectroscopy = Routine(_acquisition, _fit, _plot, _update) +"""CouplerResonatorSpectroscopy Routine object.""" diff --git a/src/qibocal/protocols/characterization/couplers/utils.py b/src/qibocal/protocols/characterization/couplers/utils.py new file mode 100644 index 000000000..311d036c7 --- /dev/null +++ b/src/qibocal/protocols/characterization/couplers/utils.py @@ -0,0 +1,73 @@ +from dataclasses import dataclass, field +from typing import Optional + +import numpy as np +import numpy.typing as npt +from qibolab.qubits import QubitId + +from qibocal.auto.operation import Data, Parameters, Results + +from ..flux_dependence.utils import create_data_array + + +@dataclass +class CouplerSpectroscopyParameters(Parameters): + """CouplerResonatorSpectroscopy and CouplerQubitSpectroscopy runcard inputs.""" + + bias_width: int + """Width for bias (V).""" + bias_step: int + """Frequency step for bias sweep (V).""" + freq_width: int + """Width for frequency sweep relative to the readout frequency (Hz).""" + freq_step: int + """Frequency step for frequency sweep (Hz).""" + # TODO: It may be better not to use readout multiplex to avoid readout crosstalk + measured_qubits: list[QubitId] + """Qubit to readout from the pair""" + amplitude: Optional[float] = None + """Readout or qubit drive amplitude (optional). If defined, same amplitude will be used in all qubits. + Otherwise the default amplitude defined on the platform runcard will be used""" + nshots: Optional[int] = None + """Number of shots.""" + relaxation_time: Optional[int] = None + """Relaxation time (ns).""" + + +CouplerSpecType = np.dtype( + [ + ("freq", np.float64), + ("bias", np.float64), + ("msr", np.float64), + ("phase", np.float64), + ] +) +"""Custom dtype for coupler resonator spectroscopy.""" + + +@dataclass +class CouplerSpectroscopyResults(Results): + """CouplerResonatorSpectroscopy or CouplerQubitSpectroscopy outputs.""" + + sweetspot: dict[QubitId, float] + """Sweetspot for each coupler.""" + pulse_amp: dict[QubitId, float] + """Pulse amplitude for the coupler.""" + fitted_parameters: dict[QubitId, dict[str, float]] + """Raw fitted parameters.""" + + +@dataclass +class CouplerSpectroscopyData(Data): + """Data structure for CouplerResonatorSpectroscopy or CouplerQubitSpectroscopy.""" + + resonator_type: str + """Resonator type.""" + data: dict[QubitId, npt.NDArray[CouplerSpecType]] = field(default_factory=dict) + """Raw data acquired.""" + + def register_qubit(self, qubit, freq, bias, msr, phase): + """Store output for single qubit.""" + self.data[qubit] = create_data_array( + freq, bias, msr, phase, dtype=CouplerSpecType + ) diff --git a/src/qibocal/protocols/characterization/flux_dependence/utils.py b/src/qibocal/protocols/characterization/flux_dependence/utils.py index 2ba0c892a..76f470696 100644 --- a/src/qibocal/protocols/characterization/flux_dependence/utils.py +++ b/src/qibocal/protocols/characterization/flux_dependence/utils.py @@ -51,15 +51,23 @@ def flux_dependence_plot(data, fit, qubit): qubit_data = data[qubit] + if not data.__class__.__name__ == "CouplerSpectroscopyData": + subplot_titles = ( + "MSR [V]", + "Phase [rad]", + ) + else: + subplot_titles = ( + "MSR [V] Qubit" + str(qubit), + "Phase [rad] Qubit" + str(qubit), + ) + fig = make_subplots( rows=1, cols=2, horizontal_spacing=0.1, vertical_spacing=0.1, - subplot_titles=( - "MSR [V]", - "Phase [rad]", - ), + subplot_titles=subplot_titles, ) frequencies = qubit_data.freq * HZ_TO_GHZ msr = qubit_data.msr @@ -67,7 +75,10 @@ def flux_dependence_plot(data, fit, qubit): msr_mask = 0.5 if data.resonator_type == "3D": msr = -msr - elif data.__class__.__name__ == "QubitFluxData": + elif ( + data.__class__.__name__ == "QubitFluxData" + or data.__class__.__name__ == "CouplerSpectroscopyData" + ): msr_mask = 0.3 if data.resonator_type == "2D": msr = -msr @@ -85,19 +96,22 @@ def flux_dependence_plot(data, fit, qubit): col=1, ) - fig.add_trace( - go.Scatter( - x=frequencies1, - y=biases1, - mode="markers", - marker_color="green", - showlegend=True, - name="Curve estimation", - ), - row=1, - col=1, - ) - if fit is not None: + if not data.__class__.__name__ == "CouplerSpectroscopyData": + fig.add_trace( + go.Scatter( + x=frequencies1, + y=biases1, + mode="markers", + marker_color="green", + showlegend=True, + name="Curve estimation", + ), + row=1, + col=1, + ) + + # TODO: This fit is for frequency, can it be reused here, do we even want the fit ? + if fit is not None and not data.__class__.__name__ == "CouplerSpectroscopyData": fitting_report = "" params = fit.fitted_parameters[qubit] fitting_report_label = "Frequency" @@ -177,7 +191,10 @@ def flux_dependence_plot(data, fit, qubit): row=1, col=1, ) - fig.update_yaxes(title_text="Bias (V)", row=1, col=1) + if not data.__class__.__name__ == "CouplerSpectroscopyData": + fig.update_yaxes(title_text="Bias (V)", row=1, col=1) + else: + fig.update_yaxes(title_text="Pulse Amplitude", row=1, col=1) fig.add_trace( go.Heatmap( @@ -194,7 +211,11 @@ def flux_dependence_plot(data, fit, qubit): row=1, col=2, ) - fig.update_yaxes(title_text="Bias (V)", row=1, col=2) + + if not data.__class__.__name__ == "CouplerSpectroscopyData": + fig.update_yaxes(title_text="Bias (V)", row=1, col=2) + else: + fig.update_yaxes(title_text="Pulse Amplitude", row=1, col=2) fig.update_layout(xaxis1=dict(range=[np.min(frequencies), np.max(frequencies)])) @@ -248,6 +269,7 @@ def flux_crosstalk_plot(data, fit, qubit): row=1, col=col + 1, ) + fig.update_yaxes( title_text=f"Qubit {flux_qubit[1]}: Bias (V)", row=1, col=col + 1 ) diff --git a/tests/runcards/protocols.yml b/tests/runcards/protocols.yml index d1fd5cbcc..460e1d1a4 100644 --- a/tests/runcards/protocols.yml +++ b/tests/runcards/protocols.yml @@ -80,6 +80,19 @@ actions: power_level: low nshots: 10 + - id: coupler_resonator_spectroscopy + priority: 0 + operation: coupler_resonator_spectroscopy + qubits: [[1, 2], [0, 2]] + parameters: + bias_width: 1 + bias_step: 0.1 + freq_width: 10_000_000 + freq_step: 1_000_000 + measured_qubits: [1, 0] + amplitude: .3 + nshots: 10 + relaxation_time: 3_000 - id: qubit spectroscopy @@ -92,6 +105,20 @@ actions: freq_step: 500_000 nshots: 10 + - id: coupler qubit spectroscopy + priority: 0 + operation: coupler_qubit_spectroscopy + qubits: [[1, 2], [0, 2]] + parameters: + bias_width: 1 + bias_step: 0.1 + freq_width: 10_000_000 + freq_step: 1_000_000 + measured_qubits: [1, 0] + amplitude: .1 + nshots: 10 + relaxation_time: 3_000 + - id: qubit spectroscopy ef priority: 0