Repository: qiboteam/qibocal

Issue:
Add detuning option to flipping to improve performances

Comments:

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e2312594..358c45b5 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -11,7 +11,7 @@ repos:
       - id: check-merge-conflict
       - id: debug-statements
   - repo: https://github.com/psf/black
-    rev: 24.4.0
+    rev: 24.4.2
     hooks:
       - id: black
   - repo: https://github.com/pycqa/isort
diff --git a/src/qibocal/protocols/characterization/flipping.py b/src/qibocal/protocols/characterization/flipping.py
index defbbeb1..f9ada5a9 100644
--- a/src/qibocal/protocols/characterization/flipping.py
+++ b/src/qibocal/protocols/characterization/flipping.py
@@ -21,6 +21,7 @@ from .flipping_signal import (
     FlippingSignalResults,
     _update,
     flipping_fit,
+    flipping_sequence,
 )
 from .utils import COLORBAND, COLORBAND_LINE, chi2_reduced
 
@@ -29,14 +30,6 @@ from .utils import COLORBAND, COLORBAND_LINE, chi2_reduced
 class FlippingParameters(FlippingSignalParameters):
     """Flipping runcard inputs."""
 
-    nflips_max: int
-    """Maximum number of flips ([RX(pi) - RX(pi)] sequences). """
-    nflips_step: int
-    """Flip step."""
-    unrolling: bool = False
-    """If ``True`` it uses sequence unrolling to deploy multiple sequences in a single instrument call.
-    Defaults to ``False``."""
-
 
 @dataclass
 class FlippingResults(FlippingSignalResults):
@@ -55,7 +48,6 @@ FlippingType = np.dtype(
 class FlippingData(FlippingSignalData):
     """Flipping acquisition outputs."""
 
-    """Pi pulse amplitudes for each qubit."""
     data: dict[QubitId, npt.NDArray[FlippingType]] = field(default_factory=dict)
     """Raw data acquired."""
 
@@ -82,6 +74,7 @@ def _acquisition(
 
     data = FlippingData(
         resonator_type=platform.resonator_type,
+        detuning=params.detuning,
         pi_pulse_amplitudes={
             qubit: platform.qubits[qubit].native_gates.RX.amplitude for qubit in targets
         },
@@ -100,26 +93,13 @@ def _acquisition(
     for flips in flips_sweep:
         # create a sequence of pulses for the experiment
         sequence = PulseSequence()
-        ro_pulses = {}
         for qubit in targets:
-            RX90_pulse = platform.create_RX90_pulse(qubit, start=0)
-            sequence.add(RX90_pulse)
-            # execute sequence RX(pi/2) - [RX(pi) - RX(pi)] from 0...flips times - RO
-            start1 = RX90_pulse.duration
-            for _ in range(flips):
-                RX_pulse1 = platform.create_RX_pulse(qubit, start=start1)
-                start2 = start1 + RX_pulse1.duration
-                RX_pulse2 = platform.create_RX_pulse(qubit, start=start2)
-                sequence.add(RX_pulse1)
-                sequence.add(RX_pulse2)
-                start1 = start2 + RX_pulse2.duration
-
-            # add ro pulse at the end of the sequence
-            ro_pulses[qubit] = platform.create_qubit_readout_pulse(qubit, start=start1)
-            sequence.add(ro_pulses[qubit])
+            sequence += flipping_sequence(
+                platform=platform, qubit=qubit, detuning=params.detuning, flips=flips
+            )
 
         sequences.append(sequence)
-        all_ro_pulses.append(ro_pulses)
+        all_ro_pulses.append(sequence.ro_pulses)
 
     # execute the pulse sequence
     if params.unrolling:
@@ -132,7 +112,7 @@ def _acquisition(
 
     for ig, (flips, ro_pulses) in enumerate(zip(flips_sweep, all_ro_pulses)):
         for qubit in targets:
-            serial = ro_pulses[qubit].serial
+            serial = ro_pulses.get_qubit_pulses(qubit)[0].serial
             if params.unrolling:
                 result = results[serial][0]
             else:
@@ -164,11 +144,12 @@ def _fit(data: FlippingData) -> FlippingResults:
     qubits = data.qubits
     corrected_amplitudes = {}
     fitted_parameters = {}
-    amplitude_correction_factors = {}
+    delta_amplitude = {}
+    delta_amplitude_detuned = {}
     chi2 = {}
     for qubit in qubits:
         qubit_data = data[qubit]
-        pi_pulse_amplitude = data.pi_pulse_amplitudes[qubit]
+        detuned_pi_pulse_amplitude = data.pi_pulse_amplitudes[qubit]
         y = qubit_data.prob
         x = qubit_data.flips
 
@@ -185,6 +166,7 @@ def _fit(data: FlippingData) -> FlippingResults:
         )
         f = x[index] / (x[1] - x[0]) if index is not None else 1
         pguess = [0.5, 0.5, 1 / f, np.pi, 0]
+
         try:
             popt, perr = curve_fit(
                 flipping_fit,
@@ -207,9 +189,9 @@ def _fit(data: FlippingData) -> FlippingResults:
                 signed_correction = popt[2] / 2
             # The amplitude is directly proportional to the rotation angle
             corrected_amplitudes[qubit] = (
-                float((pi_pulse_amplitude * np.pi) / (np.pi + signed_correction)),
+                float(detuned_pi_pulse_amplitude * np.pi / (np.pi + signed_correction)),
                 float(
-                    pi_pulse_amplitude
+                    detuned_pi_pulse_amplitude
                     * np.pi
                     * 1
                     / (np.pi + signed_correction) ** 2
@@ -217,11 +199,26 @@ def _fit(data: FlippingData) -> FlippingResults:
                     / 2
                 ),
             )
+
             fitted_parameters[qubit] = popt
-            amplitude_correction_factors[qubit] = (
-                float(signed_correction / np.pi * pi_pulse_amplitude),
-                float(perr[2] * pi_pulse_amplitude / np.pi / 2),
+
+            delta_amplitude[qubit] = (
+                -signed_correction
+                * detuned_pi_pulse_amplitude
+                / (np.pi + signed_correction),
+                np.abs(
+                    np.pi
+                    * detuned_pi_pulse_amplitude
+                    * np.power(np.pi + signed_correction, -2)
+                )
+                * perr[2]
+                / 2,
+            )
+            delta_amplitude_detuned[qubit] = (
+                delta_amplitude[qubit][0] - data.detuning,
+                delta_amplitude[qubit][1],
             )
+
             chi2[qubit] = (
                 chi2_reduced(
                     y,
@@ -234,7 +231,11 @@ def _fit(data: FlippingData) -> FlippingResults:
             log.warning(f"Error in flipping fit for qubit {qubit} due to {e}.")
 
     return FlippingResults(
-        corrected_amplitudes, amplitude_correction_factors, fitted_parameters, chi2
+        corrected_amplitudes,
+        delta_amplitude,
+        delta_amplitude_detuned,
+        fitted_parameters,
+        chi2,
     )
 
 
@@ -243,7 +244,6 @@ def _plot(data: FlippingData, target: QubitId, fit: FlippingResults = None):
 
     figures = []
     fig = go.Figure()
-
     fitting_report = ""
     qubit_data = data[target]
 
@@ -298,13 +298,15 @@ def _plot(data: FlippingData, target: QubitId, fit: FlippingResults = None):
             table_dict(
                 target,
                 [
-                    "Amplitude correction factor",
+                    "Delta amplitude [a.u.]",
+                    "Delta amplitude (with detuning) [a.u.]",
                     "Corrected amplitude [a.u.]",
                     "chi2 reduced",
                 ],
                 [
-                    np.round(fit.amplitude_factors[target], 4),
-                    np.round(fit.amplitude[target], 4),
+                    fit.delta_amplitude[target],
+                    fit.delta_amplitude_detuned[target],
+                    fit.amplitude[target],
                     fit.chi2[target],
                 ],
                 display_error=True,
diff --git a/src/qibocal/protocols/characterization/flipping_signal.py b/src/qibocal/protocols/characterization/flipping_signal.py
index db7f60f4..c1ccd7d4 100644
--- a/src/qibocal/protocols/characterization/flipping_signal.py
+++ b/src/qibocal/protocols/characterization/flipping_signal.py
@@ -28,6 +28,8 @@ class FlippingSignalParameters(Parameters):
     unrolling: bool = False
     """If ``True`` it uses sequence unrolling to deploy multiple sequences in a single instrument call.
     Defaults to ``False``."""
+    detuning: float = 0
+    """Amplitude detuning."""
 
 
 @dataclass
@@ -36,8 +38,10 @@ class FlippingSignalResults(Results):
 
     amplitude: dict[QubitId, tuple[float, Optional[float]]]
     """Drive amplitude for each qubit."""
-    amplitude_factors: dict[QubitId, tuple[float, Optional[float]]]
-    """Drive amplitude correction factor for each qubit."""
+    delta_amplitude: dict[QubitId, tuple[float, Optional[float]]]
+    """Difference in amplitude between initial value and fit."""
+    delta_amplitude_detuned: dict[QubitId, tuple[float, Optional[float]]]
+    """Difference in amplitude between detuned value and fit."""
     fitted_parameters: dict[QubitId, dict[str, float]]
     """Raw fitting output."""
 
@@ -51,12 +55,39 @@ class FlippingSignalData(Data):
 
     resonator_type: str
     """Resonator type."""
+    detuning: float
+    """Amplitude detuning."""
     pi_pulse_amplitudes: dict[QubitId, float]
     """Pi pulse amplitudes for each qubit."""
     data: dict[QubitId, npt.NDArray[FlippingType]] = field(default_factory=dict)
     """Raw data acquired."""
 
 
+def flipping_sequence(platform: Platform, qubit: QubitId, detuning: float, flips: int):
+
+    sequence = PulseSequence()
+    RX90_pulse = platform.create_RX90_pulse(qubit, start=0)
+    sequence.add(RX90_pulse)
+    # execute sequence RX(pi/2) - [RX(pi) - RX(pi)] from 0...flips times - RO
+    start1 = RX90_pulse.duration
+    drive_frequency = platform.qubits[qubit].native_gates.RX.frequency
+    for _ in range(flips):
+        RX_pulse1 = platform.create_RX_pulse(qubit, start=start1)
+        RX_pulse1.frequency = drive_frequency + detuning
+        start2 = start1 + RX_pulse1.duration
+        RX_pulse2 = platform.create_RX_pulse(qubit, start=start2)
+        RX_pulse2.frequency = drive_frequency + detuning
+
+        sequence.add(RX_pulse1)
+        sequence.add(RX_pulse2)
+        start1 = start2 + RX_pulse2.duration
+
+    # add ro pulse at the end of the sequence
+    sequence.add(platform.create_qubit_readout_pulse(qubit, start=start1))
+
+    return sequence
+
+
 def _acquisition(
     params: FlippingSignalParameters,
     platform: Platform,
@@ -79,6 +110,7 @@ def _acquisition(
 
     data = FlippingSignalData(
         resonator_type=platform.resonator_type,
+        detuning=params.detuning,
         pi_pulse_amplitudes={
             qubit: platform.qubits[qubit].native_gates.RX.amplitude for qubit in targets
         },
@@ -97,26 +129,13 @@ def _acquisition(
     for flips in flips_sweep:
         # create a sequence of pulses for the experiment
         sequence = PulseSequence()
-        ro_pulses = {}
         for qubit in targets:
-            RX90_pulse = platform.create_RX90_pulse(qubit, start=0)
-            sequence.add(RX90_pulse)
-            # execute sequence RX(pi/2) - [RX(pi) - RX(pi)] from 0...flips times - RO
-            start1 = RX90_pulse.duration
-            for _ in range(flips):
-                RX_pulse1 = platform.create_RX_pulse(qubit, start=start1)
-                start2 = start1 + RX_pulse1.duration
-                RX_pulse2 = platform.create_RX_pulse(qubit, start=start2)
-                sequence.add(RX_pulse1)
-                sequence.add(RX_pulse2)
-                start1 = start2 + RX_pulse2.duration
-
-            # add ro pulse at the end of the sequence
-            ro_pulses[qubit] = platform.create_qubit_readout_pulse(qubit, start=start1)
-            sequence.add(ro_pulses[qubit])
+            sequence += flipping_sequence(
+                platform=platform, qubit=qubit, detuning=params.detuning, flips=flips
+            )
 
         sequences.append(sequence)
-        all_ro_pulses.append(ro_pulses)
+        all_ro_pulses.append(sequence.ro_pulses)
 
     # execute the pulse sequence
     if params.unrolling:
@@ -129,7 +148,7 @@ def _acquisition(
 
     for ig, (flips, ro_pulses) in enumerate(zip(flips_sweep, all_ro_pulses)):
         for qubit in targets:
-            serial = ro_pulses[qubit].serial
+            serial = ro_pulses.get_qubit_pulses(qubit)[0].serial
             if params.unrolling:
                 result = results[serial][0]
             else:
@@ -162,10 +181,11 @@ def _fit(data: FlippingSignalData) -> FlippingSignalResults:
     qubits = data.qubits
     corrected_amplitudes = {}
     fitted_parameters = {}
-    amplitude_correction_factors = {}
+    delta_amplitude = {}
+    delta_amplitude_detuned = {}
     for qubit in qubits:
         qubit_data = data[qubit]
-        pi_pulse_amplitude = data.pi_pulse_amplitudes[qubit]
+        detuned_pi_pulse_amplitude = data.pi_pulse_amplitudes[qubit] + data.detuning
         voltages = qubit_data.signal
         flips = qubit_data.flips
         y_min = np.min(voltages)
@@ -217,18 +237,24 @@ def _fit(data: FlippingSignalData) -> FlippingSignalResults:
             else:
                 signed_correction = -translated_popt[2] / 2
             # The amplitude is directly proportional to the rotation angle
-            corrected_amplitudes[qubit] = (pi_pulse_amplitude * np.pi) / (
+            corrected_amplitudes[qubit] = (detuned_pi_pulse_amplitude * np.pi) / (
                 np.pi + signed_correction
             )
             fitted_parameters[qubit] = translated_popt
-            amplitude_correction_factors[qubit] = (
-                signed_correction / np.pi * pi_pulse_amplitude
+            delta_amplitude[qubit] = (
+                -signed_correction
+                * detuned_pi_pulse_amplitude
+                / (np.pi + signed_correction)
             )
+            delta_amplitude_detuned[qubit] = delta_amplitude[qubit] - data.detuning
         except Exception as e:
             log.warning(f"Error in flipping fit for qubit {qubit} due to {e}.")
 
     return FlippingSignalResults(
-        corrected_amplitudes, amplitude_correction_factors, fitted_parameters
+        corrected_amplitudes,
+        delta_amplitude,
+        delta_amplitude_detuned,
+        fitted_parameters,
     )
 
 
@@ -276,9 +302,14 @@ def _plot(data: FlippingSignalData, target, fit: FlippingSignalResults = None):
         fitting_report = table_html(
             table_dict(
                 target,
-                ["Amplitude correction factor", "Corrected amplitude [a.u.]"],
                 [
-                    np.round(fit.amplitude_factors[target], 4),
+                    "Delta amplitude [a.u.]",
+                    "Delta amplitude (with detuning) [a.u.]",
+                    "Corrected amplitude [a.u.]",
+                ],
+                [
+                    np.round(fit.delta_amplitude[target], 4),
+                    np.round(fit.delta_amplitude_detuned[target], 4),
                     np.round(fit.amplitude[target], 4),
                 ],
             )


Test patch:
diff --git a/tests/runcards/protocols.yml b/tests/runcards/protocols.yml
index 798c0188..b3b69a2c 100644
--- a/tests/runcards/protocols.yml
+++ b/tests/runcards/protocols.yml
@@ -550,6 +550,7 @@ actions:
       nflips_max: 10
       nflips_step: 1
       nshots: 50
+      detuning: 0.1
 
   - id: flipping_signal
     operation: flipping_signal
@@ -557,6 +558,7 @@ actions:
       nflips_max: 10
       nflips_step: 1
       nshots: 50
+      detuning: 0.1
 
   - id: flipping unrolling
     operation: flipping