diff --git a/doc/source/protocols/cpmg/cpmg.png b/doc/source/protocols/cpmg/cpmg.png new file mode 100644 index 000000000..eaf7c175a Binary files /dev/null and b/doc/source/protocols/cpmg/cpmg.png differ diff --git a/doc/source/protocols/cpmg/cpmg.rst b/doc/source/protocols/cpmg/cpmg.rst new file mode 100644 index 000000000..bde28739c --- /dev/null +++ b/doc/source/protocols/cpmg/cpmg.rst @@ -0,0 +1,51 @@ +CPMG sequence +============= + +In this section we show how to run the dynamical decoupling sequence CPMG. + +The CPMG sequence consists in applying N equally spaced :math:`\pi` pulses +within two :math:`\pi / 2` pulses. By increasing the number of :math:`\pi` pulses :math:`T_2` +should increase since the estimation is less sensitive to noises of the type :math:`1/f` +eventually reaching the :math:`2 T_1` limit. + + +The fit is again a dumped exponential of the following form: + +.. math:: + + p_e(t) = A + B e^{ - t / T^{(N)}_2} + + +Parameters +^^^^^^^^^^ + +.. autoclass:: qibocal.protocols.coherence.cpmg.CpmgParameters + :noindex: + +Example +^^^^^^^ + +A possible runcard to launch a CPMG experiment could be the following: + +.. code-block:: yaml + + - id: CPMG + operation: cpmg + parameters: + delay_between_pulses_end: 100000 + delay_between_pulses_start: 4 + delay_between_pulses_step: 1000 + n: 10 + nshots: 1000 + +The expected output is the following: + +.. image:: cpmg.png + +:math:`T_2` is determined by fitting the output signal using +the formula presented above. + +Requirements +^^^^^^^^^^^^ + +- :ref:`single-shot` diff --git a/doc/source/protocols/index.rst b/doc/source/protocols/index.rst index 65d13f95d..95c5600a2 100644 --- a/doc/source/protocols/index.rst +++ b/doc/source/protocols/index.rst @@ -22,6 +22,7 @@ In this section we introduce the basics of all protocols supported by ``qibocal` t1/t1 t2/t2 t2_echo/t2_echo + cpmg/cpmg flux/single flux/crosstalk singleshot diff --git a/src/qibocal/protocols/__init__.py b/src/qibocal/protocols/__init__.py index f6aa87e61..8ab73abcd 100644 --- a/src/qibocal/protocols/__init__.py +++ b/src/qibocal/protocols/__init__.py @@ -1,6 +1,7 @@ from .allxy.allxy import allxy from .allxy.allxy_resonator_depletion_tuning import allxy_resonator_depletion_tuning from .classification import single_shot_classification +from .coherence.cpmg import cpmg from .coherence.spin_echo import spin_echo from .coherence.spin_echo_signal import spin_echo_signal from .coherence.t1 import t1 @@ -104,4 +105,5 @@ "standard_rb_2q_inter", "optimize_two_qubit_gate", "ramsey_zz", + "cpmg", ] diff --git a/src/qibocal/protocols/coherence/cpmg.py b/src/qibocal/protocols/coherence/cpmg.py new file mode 100644 index 000000000..a81bb0458 --- /dev/null +++ b/src/qibocal/protocols/coherence/cpmg.py @@ -0,0 +1,92 @@ +from dataclasses import dataclass + +import numpy as np +from qibolab import AcquisitionType, AveragingMode, Parameter, Sweeper + +from qibocal.auto.operation import QubitId, Routine +from qibocal.calibration import CalibrationPlatform +from qibocal.result import probability + +from . import t1 +from .spin_echo import SpinEchoParameters, SpinEchoResults +from .utils import dynamical_decoupling_sequence, exponential_fit_probability, plot + + +@dataclass +class CpmgParameters(SpinEchoParameters): + """Cpmg runcard inputs.""" + + n: int = 1 + """Number of pi rotations.""" + + +@dataclass +class CpmgResults(SpinEchoResults): + """SpinEcho outputs.""" + + +class CpmgData(t1.T1Data): + """SpinEcho acquisition outputs.""" + + +def _acquisition( + params: CpmgParameters, + platform: CalibrationPlatform, + targets: list[QubitId], +) -> CpmgData: + """Data acquisition for Cpmg""" + # create a sequence of pulses for the experiment: + sequence, delays = dynamical_decoupling_sequence( + platform, targets, n=params.n, kind="CPMG" + ) + + # define the parameter to sweep and its range: + # delay between pulses + wait_range = np.arange( + params.delay_between_pulses_start, + params.delay_between_pulses_end, + params.delay_between_pulses_step, + ) + + sweeper = Sweeper( + parameter=Parameter.duration, + values=wait_range / 2 / params.n, + pulses=delays, + ) + + results = platform.execute( + [sequence], + [[sweeper]], + nshots=params.nshots, + relaxation_time=params.relaxation_time, + acquisition_type=AcquisitionType.DISCRIMINATION, + averaging_mode=AveragingMode.SINGLESHOT, + ) + + data = CpmgData() + for qubit in targets: + ro_pulse = list(sequence.channel(platform.qubits[qubit].acquisition))[-1] + result = results[ro_pulse.id] + prob = probability(result, state=1) + error = np.sqrt(prob * (1 - prob) / params.nshots) + data.register_qubit( + t1.CoherenceProbType, + (qubit), + dict( + wait=wait_range, + prob=prob, + error=error, + ), + ) + + return data + + +def _fit(data: CpmgData) -> CpmgResults: + """Post-processing for Cpmg.""" + t2Echos, fitted_parameters, pcovs, chi2 = exponential_fit_probability(data) + return CpmgResults(t2Echos, fitted_parameters, pcovs, chi2) + + +cpmg = Routine(_acquisition, _fit, plot) +"""Cpmg Routine object.""" diff --git a/src/qibocal/protocols/coherence/spin_echo.py b/src/qibocal/protocols/coherence/spin_echo.py index 22b90c390..acc12f8f4 100644 --- a/src/qibocal/protocols/coherence/spin_echo.py +++ b/src/qibocal/protocols/coherence/spin_echo.py @@ -2,17 +2,15 @@ from typing import Optional import numpy as np -import plotly.graph_objects as go from qibolab import AcquisitionType, AveragingMode, Parameter, Sweeper from qibocal.auto.operation import QubitId, Routine from qibocal.calibration import CalibrationPlatform from qibocal.result import probability -from ..utils import table_dict, table_html from . import t1 from .spin_echo_signal import SpinEchoSignalParameters, SpinEchoSignalResults, _update -from .utils import exp_decay, exponential_fit_probability, spin_echo_sequence +from .utils import dynamical_decoupling_sequence, exponential_fit_probability, plot @dataclass @@ -41,11 +39,11 @@ def _acquisition( ) -> SpinEchoData: """Data acquisition for SpinEcho""" # create a sequence of pulses for the experiment: - sequence, delays = spin_echo_sequence(platform, targets) + sequence, delays = dynamical_decoupling_sequence(platform, targets, kind="CP") # define the parameter to sweep and its range: # delay between pulses - ro_wait_range = np.arange( + wait_range = np.arange( params.delay_between_pulses_start, params.delay_between_pulses_end, params.delay_between_pulses_step, @@ -53,7 +51,7 @@ def _acquisition( sweeper = Sweeper( parameter=Parameter.duration, - values=ro_wait_range, + values=wait_range / 2, pulses=delays, ) @@ -70,13 +68,13 @@ def _acquisition( for qubit in targets: ro_pulse = list(sequence.channel(platform.qubits[qubit].acquisition))[-1] result = results[ro_pulse.id] - prob = probability(result, state=0) + prob = probability(result, state=1) error = np.sqrt(prob * (1 - prob) / params.nshots) data.register_qubit( t1.CoherenceProbType, (qubit), dict( - wait=ro_wait_range, + wait=wait_range, prob=prob, error=error, ), @@ -91,77 +89,5 @@ def _fit(data: SpinEchoData) -> SpinEchoResults: return SpinEchoResults(t2Echos, fitted_parameters, pcovs, chi2) -def _plot(data: SpinEchoData, target: QubitId, fit: SpinEchoResults = None): - """Plotting for SpinEcho""" - - figures = [] - # iterate over multiple data folders - fitting_report = "" - - qubit_data = data[target] - waits = qubit_data.wait - probs = qubit_data.prob - error_bars = qubit_data.error - - fig = go.Figure( - [ - go.Scatter( - x=waits, - y=probs, - opacity=1, - name="Probability of 0", - showlegend=True, - legendgroup="Probability of 0", - mode="lines", - ), - go.Scatter( - x=np.concatenate((waits, waits[::-1])), - y=np.concatenate((probs + error_bars, (probs - error_bars)[::-1])), - fill="toself", - fillcolor=t1.COLORBAND, - line=dict(color=t1.COLORBAND_LINE), - showlegend=True, - name="Errors", - ), - ] - ) - - if fit is not None: - # add fitting trace - waitrange = np.linspace( - min(waits), - max(waits), - 2 * len(qubit_data), - ) - params = fit.fitted_parameters[target] - - fig.add_trace( - go.Scatter( - x=waitrange, - y=exp_decay(waitrange, *params), - name="Fit", - line=go.scatter.Line(dash="dot"), - ), - ) - fitting_report = table_html( - table_dict( - target, - ["T2 Spin Echo [ns]", "chi2 reduced"], - [fit.t2_spin_echo[target], fit.chi2[target]], - display_error=True, - ) - ) - - fig.update_layout( - showlegend=True, - xaxis_title="Time [ns]", - yaxis_title="Probability of State 0", - ) - - figures.append(fig) - - return figures, fitting_report - - -spin_echo = Routine(_acquisition, _fit, _plot, _update) +spin_echo = Routine(_acquisition, _fit, plot, _update) """SpinEcho Routine object.""" diff --git a/src/qibocal/protocols/coherence/spin_echo_signal.py b/src/qibocal/protocols/coherence/spin_echo_signal.py index 8321e6217..9be106488 100644 --- a/src/qibocal/protocols/coherence/spin_echo_signal.py +++ b/src/qibocal/protocols/coherence/spin_echo_signal.py @@ -12,7 +12,12 @@ from ... import update from ..utils import table_dict, table_html from .t1_signal import T1SignalData -from .utils import CoherenceType, exp_decay, exponential_fit, spin_echo_sequence +from .utils import ( + CoherenceType, + dynamical_decoupling_sequence, + exp_decay, + exponential_fit, +) @dataclass @@ -26,15 +31,13 @@ class SpinEchoSignalParameters(Parameters): delay_between_pulses_step: int """Step delay between pulses [ns].""" single_shot: bool = False - """If ``True`` save single shot signal data.""" - unrolling: bool = False @dataclass class SpinEchoSignalResults(Results): """SpinEchoSignal outputs.""" - t2_spin_echo: dict[QubitId, Union[float, list[float]]] + t2: dict[QubitId, Union[float, list[float]]] """T2 echo for each qubit.""" fitted_parameters: dict[QubitId, dict[str, float]] """Raw fitting output.""" @@ -53,11 +56,11 @@ def _acquisition( ) -> SpinEchoSignalData: """Data acquisition for SpinEcho""" # create a sequence of pulses for the experiment: - sequence, delays = spin_echo_sequence(platform, targets) + sequence, delays = dynamical_decoupling_sequence(platform, targets, kind="CP") # define the parameter to sweep and its range: # delay between pulses - ro_wait_range = np.arange( + wait_range = np.arange( params.delay_between_pulses_start, params.delay_between_pulses_end, params.delay_between_pulses_step, @@ -65,7 +68,7 @@ def _acquisition( sweeper = Sweeper( parameter=Parameter.duration, - values=ro_wait_range, + values=wait_range / 2, pulses=delays, ) @@ -86,9 +89,9 @@ def _acquisition( result = results[ro_pulse.id] signal = magnitude(result) if params.single_shot: - _wait = np.array(len(signal) * [ro_wait_range]) + _wait = np.array(len(signal) * [wait_range]) else: - _wait = ro_wait_range + _wait = wait_range data.register_qubit( CoherenceType, (qubit), @@ -157,7 +160,7 @@ def _plot(data: SpinEchoSignalData, target: QubitId, fit: SpinEchoSignalResults table_dict( target, ["T2 Spin Echo [ns]"], - [np.round(fit.t2_spin_echo[target])], + [np.round(fit.t2[target])], display_error=True, ) ) @@ -176,7 +179,7 @@ def _plot(data: SpinEchoSignalData, target: QubitId, fit: SpinEchoSignalResults def _update( results: SpinEchoSignalResults, platform: CalibrationPlatform, target: QubitId ): - update.t2_spin_echo(results.t2_spin_echo[target], platform, target) + update.t2_spin_echo(results.t2[target], platform, target) spin_echo_signal = Routine(_acquisition, _fit, _plot, _update) diff --git a/src/qibocal/protocols/coherence/t1.py b/src/qibocal/protocols/coherence/t1.py index 0db2a3a97..d6630a3aa 100644 --- a/src/qibocal/protocols/coherence/t1.py +++ b/src/qibocal/protocols/coherence/t1.py @@ -13,9 +13,6 @@ from ..utils import table_dict, table_html from . import t1_signal, utils -COLORBAND = "rgba(0,100,80,0.2)" -COLORBAND_LINE = "rgba(255,255,255,0)" - @dataclass class T1Parameters(t1_signal.T1SignalParameters): @@ -125,8 +122,8 @@ def _plot(data: T1Data, target: QubitId, fit: T1Results = None): x=np.concatenate((waits, waits[::-1])), y=np.concatenate((probs + error_bars, (probs - error_bars)[::-1])), fill="toself", - fillcolor=COLORBAND, - line=dict(color=COLORBAND_LINE), + fillcolor=utils.COLORBAND, + line=dict(color=utils.COLORBAND_LINE), showlegend=True, name="Errors", ), diff --git a/src/qibocal/protocols/coherence/t2.py b/src/qibocal/protocols/coherence/t2.py index 6a6d9d672..7d8111249 100644 --- a/src/qibocal/protocols/coherence/t2.py +++ b/src/qibocal/protocols/coherence/t2.py @@ -2,7 +2,6 @@ from typing import Optional import numpy as np -import plotly.graph_objects as go from qibolab import AcquisitionType, AveragingMode, Parameter, Sweeper from qibocal.auto.operation import QubitId, Routine @@ -10,7 +9,6 @@ from ...result import probability from ..ramsey.utils import ramsey_sequence -from ..utils import table_dict, table_html from . import t1, t2_signal, utils @@ -93,80 +91,5 @@ def _fit(data: T2Data) -> T2Results: return T2Results(t2s, fitted_parameters, pcovs, chi2) -def _plot(data: T2Data, target: QubitId, fit: T2Results = None): - """Plotting function for Ramsey Experiment.""" - - figures = [] - fitting_report = "" - qubit_data = data[target] - waits = qubit_data.wait - probs = qubit_data.prob - error_bars = qubit_data.error - - fig = go.Figure( - [ - go.Scatter( - x=waits, - y=probs, - opacity=1, - name="Probability of 1", - showlegend=True, - legendgroup="Probability of 1", - mode="lines", - ), - go.Scatter( - x=np.concatenate((waits, waits[::-1])), - y=np.concatenate((probs + error_bars, (probs - error_bars)[::-1])), - fill="toself", - fillcolor=t1.COLORBAND, - line=dict(color=t1.COLORBAND_LINE), - showlegend=True, - name="Errors", - ), - ] - ) - - if fit is not None: - # add fitting trace - waitrange = np.linspace( - min(qubit_data.wait), - max(qubit_data.wait), - 2 * len(qubit_data), - ) - - params = fit.fitted_parameters[target] - fig.add_trace( - go.Scatter( - x=waitrange, - y=utils.exp_decay( - waitrange, - *params, - ), - name="Fit", - line=go.scatter.Line(dash="dot"), - ) - ) - fitting_report = table_html( - table_dict( - target, - [ - "T2 [ns]", - "chi2 reduced", - ], - [fit.t2[target], fit.chi2[target]], - display_error=True, - ) - ) - fig.update_layout( - showlegend=True, - xaxis_title="Time [ns]", - yaxis_title="Probability of State 1", - ) - - figures.append(fig) - - return figures, fitting_report - - -t2 = Routine(_acquisition, _fit, _plot, t2_signal._update) +t2 = Routine(_acquisition, _fit, utils.plot, t2_signal._update) """T2 Routine object.""" diff --git a/src/qibocal/protocols/coherence/utils.py b/src/qibocal/protocols/coherence/utils.py index 3b686b576..418590fca 100644 --- a/src/qibocal/protocols/coherence/utils.py +++ b/src/qibocal/protocols/coherence/utils.py @@ -1,17 +1,21 @@ import numpy as np +import plotly.graph_objects as go from qibolab import Delay, Platform, PulseSequence from scipy.optimize import curve_fit from qibocal.auto.operation import QubitId from qibocal.config import log -from ..utils import chi2_reduced +from ..utils import chi2_reduced, table_dict, table_html CoherenceType = np.dtype( [("wait", np.float64), ("signal", np.float64), ("phase", np.float64)] ) """Custom dtype for coherence routines.""" +COLORBAND = "rgba(0,100,80,0.2)" +COLORBAND_LINE = "rgba(255,255,255,0)" + def average_single_shots(data_type, single_shots): """Convert single shot acquisition results of signal routines to averaged. @@ -30,43 +34,53 @@ def average_single_shots(data_type, single_shots): return data -def spin_echo_sequence(platform: Platform, targets: list[QubitId], wait: int = 0): - """Create pulse sequence for spin-echo routine. +def dynamical_decoupling_sequence( + platform: Platform, + targets: list[QubitId], + wait: int = 0, + n: int = 1, + kind: str = "CPMG", +) -> tuple[PulseSequence, list[Delay]]: + """Create dynamical decoupling sequence. - Spin Echo 3 Pulses: RX(pi/2) - wait t(rotates z) - RX(pi) - wait t(rotates z) - RX(pi/2) - readout + Two sequences are available: + - CP: RX90 (wait RX wait )^N RX90 + - CPMG: RX90 (wait RY wait )^N RX90 """ + + assert kind in ["CPMG", "CP"], f"Unknown sequence {kind}, please use CP or CPMG" sequence = PulseSequence() all_delays = [] for qubit in targets: natives = platform.natives.single_qubit[qubit] qd_channel, rx90_pulse = natives.R(theta=np.pi / 2)[0] - _, rx_pulse = natives.RX()[0] + _, pulse = natives.R(phi=np.pi / 2)[0] if kind == "CPMG" else natives.RX()[0] ro_channel, ro_pulse = natives.MZ()[0] - delays = [ - Delay(duration=wait), - Delay(duration=wait), - Delay(duration=wait), - Delay(duration=wait), - ] + drive_delays = 2 * n * [Delay(duration=wait)] + ro_delays = 2 * n * [Delay(duration=wait)] - sequence.extend( - [ - (qd_channel, rx90_pulse), - (qd_channel, delays[0]), - (qd_channel, rx_pulse), - (qd_channel, delays[1]), - (qd_channel, rx90_pulse), - ( - ro_channel, - Delay(duration=2 * rx90_pulse.duration + rx_pulse.duration), - ), - (ro_channel, delays[2]), - (ro_channel, delays[3]), - (ro_channel, ro_pulse), - ] + sequence.append((qd_channel, rx90_pulse)) + + for i in range(n): + sequence.append((qd_channel, drive_delays[2 * i])) + sequence.append((ro_channel, ro_delays[2 * i])) + sequence.append((qd_channel, pulse)) + sequence.append((qd_channel, drive_delays[2 * i + 1])) + sequence.append((ro_channel, ro_delays[2 * i + 1])) + + sequence.append((qd_channel, rx90_pulse)) + + sequence.append( + ( + ro_channel, + Delay(duration=2 * rx90_pulse.duration + n * pulse.duration), + ) ) - all_delays.extend(delays) + + sequence.append((ro_channel, ro_pulse)) + all_delays.extend(drive_delays) + all_delays.extend(ro_delays) return sequence, all_delays @@ -182,3 +196,72 @@ def exponential_fit_probability(data, zeno=False): log.warning(f"Exponential decay fit failed for qubit {qubit} due to {e}") return decay, fitted_parameters, pcovs, chi2 + + +def plot(data, target: QubitId, fit=None) -> tuple[list[go.Figure], str]: + """Plotting function for spin-echo or CPMG protocol.""" + + figures = [] + fitting_report = "" + qubit_data = data[target] + waits = qubit_data.wait + probs = qubit_data.prob + error_bars = qubit_data.error + + fig = go.Figure( + [ + go.Scatter( + x=waits, + y=probs, + opacity=1, + name="Probability of 1", + showlegend=True, + legendgroup="Probability of 1", + mode="lines", + ), + go.Scatter( + x=np.concatenate((waits, waits[::-1])), + y=np.concatenate((probs + error_bars, (probs - error_bars)[::-1])), + fill="toself", + fillcolor=COLORBAND, + line=dict(color=COLORBAND_LINE), + showlegend=True, + name="Errors", + ), + ] + ) + + if fit is not None: + waitrange = np.linspace( + min(waits), + max(waits), + 2 * len(qubit_data), + ) + params = fit.fitted_parameters[target] + + fig.add_trace( + go.Scatter( + x=waitrange, + y=exp_decay(waitrange, *params), + name="Fit", + line=go.scatter.Line(dash="dot"), + ), + ) + fitting_report = table_html( + table_dict( + target, + ["T2", "chi2 reduced"], + [fit.t2[target], fit.chi2[target]], + display_error=True, + ) + ) + + fig.update_layout( + showlegend=True, + xaxis_title="Time [ns]", + yaxis_title="Probability of State 1", + ) + + figures.append(fig) + + return figures, fitting_report diff --git a/src/qibocal/protocols/coherence/zeno.py b/src/qibocal/protocols/coherence/zeno.py index 2ed4d4fec..50b4e7eb4 100644 --- a/src/qibocal/protocols/coherence/zeno.py +++ b/src/qibocal/protocols/coherence/zeno.py @@ -10,7 +10,7 @@ from qibocal.calibration import CalibrationPlatform from ...result import probability -from ..utils import table_dict, table_html +from ..utils import COLORBAND, COLORBAND_LINE, table_dict, table_html from . import t1, utils @@ -150,8 +150,8 @@ def _plot(data: ZenoData, fit: ZenoResults, target: QubitId): x=np.concatenate((readouts, readouts[::-1])), y=np.concatenate((probs + error_bars, (probs - error_bars)[::-1])), fill="toself", - fillcolor=t1.COLORBAND, - line=dict(color=t1.COLORBAND_LINE), + fillcolor=COLORBAND, + line=dict(color=COLORBAND_LINE), showlegend=True, name="Errors", ), diff --git a/tests/runcards/protocols.yml b/tests/runcards/protocols.yml index e02fe071d..ddb5fa968 100644 --- a/tests/runcards/protocols.yml +++ b/tests/runcards/protocols.yml @@ -468,42 +468,41 @@ actions: delay_between_pulses_step: 1 nshots: 10 - - id: spin_echo unrolling + - id: spin_echo operation: spin_echo parameters: delay_between_pulses_start: 0 delay_between_pulses_end: 5 delay_between_pulses_step: 1 nshots: 10 - unrolling: true - - id: spin_echo_signal - operation: spin_echo_signal + + - id: CPMG + operation: cpmg parameters: delay_between_pulses_start: 0 - delay_between_pulses_end: 5 - delay_between_pulses_step: 1 + delay_between_pulses_end: 400 + delay_between_pulses_step: 10 nshots: 10 + n: 4 - - id: spin_echo_signal_single_shot - priority: 0 + - id: spin_echo_signal operation: spin_echo_signal parameters: delay_between_pulses_start: 0 delay_between_pulses_end: 5 delay_between_pulses_step: 1 - single_shot: True nshots: 10 - - id: spin_echo_signal unrolling + - id: spin_echo_signal_single_shot + priority: 0 operation: spin_echo_signal parameters: delay_between_pulses_start: 0 delay_between_pulses_end: 5 delay_between_pulses_step: 1 + single_shot: True nshots: 10 - unrolling: true - - id: flipping operation: flipping