diff --git a/qiskit_experiments/framework/restless_mixin.py b/qiskit_experiments/framework/restless_mixin.py new file mode 100644 index 0000000000..82814cafa9 --- /dev/null +++ b/qiskit_experiments/framework/restless_mixin.py @@ -0,0 +1,175 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Restless mixin class.""" + +from typing import Callable, Sequence, Optional + +from qiskit.providers import Backend +from qiskit_experiments.data_processing.data_processor import DataProcessor +from qiskit_experiments.data_processing.exceptions import DataProcessorError +from qiskit_experiments.data_processing import nodes +from qiskit_experiments.framework.base_analysis import BaseAnalysis + + +class RestlessMixin: + """A mixin to facilitate restless experiments. + + This class defines the following methods + + - :meth:`enable_restless` + - :meth:`_get_restless_processor` + - :meth:`_t1_check` + + A restless enabled experiment is an experiment that can be run in a restless + measurement setting. In restless measurements, the qubit is not reset after + each measurement. Instead, the outcome of the previous quantum non-demolition + measurement is the initial state for the current circuit. Restless measurements + therefore require special data processing which is provided by sub-classes of + the :code:`RestlessNode`. Restless experiments are a fast alternative for + several calibration and characterization tasks, for details see + https://arxiv.org/pdf/2202.06981.pdf. + This class makes it possible for users to enter a restless run-mode without having + to manually set all the required run options and the data processor. The required options + are ``rep_delay``, ``init_qubits``, ``memory``, and ``meas_level``. Furthermore, + subclasses can override the :meth:`_get_restless_processor` method if they require more + complex restless data processing such as two-qubit calibrations. In addition, this + class makes it easy to determine if restless measurements are supported for a given + experiments. + """ + + analysis: BaseAnalysis + set_run_options: Callable + backend: Backend + _physical_qubits: Sequence[int] + _num_qubits: int + + def enable_restless( + self, rep_delay: Optional[float] = None, override_processor_by_restless: bool = True + ): + """Enables a restless experiment by setting the restless run options and the + restless data processor. + + Args: + rep_delay: The repetition delay. This is the delay between a measurement + and the subsequent quantum circuit. Since the backends have + dynamic repetition rates, the repetition delay can be set to a small + value which is required for restless experiments. Typical values are + 1 us or less. + override_processor_by_restless: If False, a data processor that is specified in the + analysis options of the experiment is not overridden by the restless data + processor. The default is True. + + Raises: + DataProcessorError: if the attribute rep_delay_range is not defined for the backend. + DataProcessorError: if a data processor has already been set but + override_processor_by_restless is True. + DataProcessorError: if the experiment analysis does not have the data_processor + option. + DataProcessorError: if the rep_delay is equal to or greater than the + T1 time of one of the physical qubits in the experiment. + """ + try: + if not rep_delay: + rep_delay = self.backend.configuration().rep_delay_range[0] + except AttributeError as error: + raise DataProcessorError( + "The restless experiment can not be enabled because " + "the attribute rep_delay_range is not defined for this backend " + "and a minimum rep_delay can not be set." + ) from error + + # The excited state promotion readout analysis option is set to + # False because it is not compatible with a restless experiment. + if self._t1_check(rep_delay): + if not self.analysis.options.get("data_processor", None): + self.set_run_options( + rep_delay=rep_delay, + init_qubits=False, + memory=True, + meas_level=2, + use_measure_esp=False, + ) + if hasattr(self.analysis.options, "data_processor"): + self.analysis.set_options(data_processor=self._get_restless_processor()) + else: + raise DataProcessorError( + "The restless data processor can not be set since the experiment analysis" + "does not have the data_processor option." + ) + else: + if not override_processor_by_restless: + self.set_run_options( + rep_delay=rep_delay, + init_qubits=False, + memory=True, + meas_level=2, + use_measure_esp=False, + ) + else: + raise DataProcessorError( + "Cannot enable restless. Data processor has already been set and " + "override_processor_by_restless is True." + ) + else: + raise DataProcessorError( + f"The specified repetition delay {rep_delay} is equal to or greater " + f"than the T1 time of one of the physical qubits" + f"{self._physical_qubits} in the experiment. Consider choosing " + f"a smaller repetition delay for the restless experiment." + ) + + def _get_restless_processor(self) -> DataProcessor: + """Returns the restless experiments data processor. + + Notes: + Sub-classes can override this method if they need more complex data processing. + """ + outcome = self.analysis.options.get("outcome", "1" * self._num_qubits) + return DataProcessor( + "memory", + [ + nodes.RestlessToCounts(self._num_qubits), + nodes.Probability(outcome), + ], + ) + + def _t1_check(self, rep_delay: float) -> bool: + """Check that repetition delay < T1 of the physical qubits in the experiment. + + Args: + rep_delay: The repetition delay. This is the delay between a measurement + and the subsequent quantum circuit. + + Returns: + True if the repetition delay is smaller than the qubit T1 times. + + Raises: + DataProcessorError: if the T1 values are not defined for the qubits of + the used backend. + """ + + try: + t1_values = [ + self.backend.properties().qubit_property(physical_qubit)["T1"][0] + for physical_qubit in self._physical_qubits + ] + + if all(rep_delay / t1_value < 1.0 for t1_value in t1_values): + return True + except AttributeError as error: + raise DataProcessorError( + "The restless experiment can not be enabled since " + "T1 values are not defined for the qubits of the used backend." + ) from error + + return False diff --git a/qiskit_experiments/library/characterization/fine_amplitude.py b/qiskit_experiments/library/characterization/fine_amplitude.py index e474b3b18b..429ce100b9 100644 --- a/qiskit_experiments/library/characterization/fine_amplitude.py +++ b/qiskit_experiments/library/characterization/fine_amplitude.py @@ -20,10 +20,11 @@ from qiskit.circuit.library import XGate, SXGate from qiskit.providers.backend import Backend from qiskit_experiments.framework import BaseExperiment, Options +from qiskit_experiments.framework.restless_mixin import RestlessMixin from qiskit_experiments.library.characterization.analysis import FineAmplitudeAnalysis -class FineAmplitude(BaseExperiment): +class FineAmplitude(BaseExperiment, RestlessMixin): r"""Error amplifying fine amplitude calibration experiment. # section: overview diff --git a/qiskit_experiments/test/mock_iq_backend.py b/qiskit_experiments/test/mock_iq_backend.py index c1017ab6be..b9fc60eaff 100644 --- a/qiskit_experiments/test/mock_iq_backend.py +++ b/qiskit_experiments/test/mock_iq_backend.py @@ -18,12 +18,153 @@ from qiskit import QuantumCircuit from qiskit.result import Result + from qiskit.providers.aer import AerSimulator from qiskit.test.mock import FakeOpenPulse2Q from qiskit.qobj.utils import MeasLevel from qiskit_experiments.framework import Options from qiskit_experiments.test.utils import FakeJob +from qiskit_experiments.data_processing.exceptions import DataProcessorError + + +class MockRestlessBackend(FakeOpenPulse2Q): + """An abstract backend for testing that can mock restless data.""" + + def __init__(self, rng_seed: int = 0): + """ + Initialize the backend. + """ + self._rng = np.random.default_rng(rng_seed) + self._precomputed_probabilities = None + super().__init__() + + def _default_options(self): + """Default options of the test backend.""" + return Options( + shots=1024, + meas_level=MeasLevel.CLASSIFIED, + meas_return="single", + ) + + @staticmethod + def _get_state_strings(n_qubits: int) -> List[str]: + """Generate all state strings for the system.""" + format_str = "{0:0" + str(n_qubits) + "b}" + return list(format_str.format(state_num) for state_num in range(2**n_qubits)) + + @abstractmethod + def _compute_outcome_probabilities(self, circuits: List[QuantumCircuit]): + """Compute the probabilities of measuring 0 or 1 for each of the given + circuits based on the previous measurement shot. + + This methods computes the dictionary self._precomputed_probabilities where + the keys are a tuple consisting of the circuit index and the previous outcome, + e.g. "0" or "1" for a single qubit. The values are the corresponding probabilities. + + Args: + circuits: The circuits from which to compute the probabilities. + """ + + def run(self, run_input, **options): + """Run the restless backend.""" + + self.options.update_options(**options) + shots = self.options.get("shots") + meas_level = self.options.get("meas_level") + + result = { + "backend_name": f"{self.__class__.__name__}", + "backend_version": "0", + "qobj_id": 0, + "job_id": 0, + "success": True, + "results": [], + } + + self._compute_outcome_probabilities(run_input) + + if run_input[0].num_qubits != 2: + raise DataProcessorError(f"{self.__class__.__name__} is a two qubit mock device.") + + prev_outcome, state_strings = "00", self._get_state_strings(2) + + # Setup the list of dicts where each dict corresponds to a circuit. + sorted_memory = [{"memory": [], "metadata": circ.metadata} for circ in run_input] + + for _ in range(shots): + for circ_idx, _ in enumerate(run_input): + probs = self._precomputed_probabilities[(circ_idx, prev_outcome)] + # Generate the next shot dependent on the pre-computed probabilities. + outcome = self._rng.choice(state_strings, p=probs) + # Append the single shot to the memory of the corresponding circuit. + sorted_memory[circ_idx]["memory"].append(hex(int(outcome, 2))) + + prev_outcome = outcome + + for idx, circ in enumerate(run_input): + counts = {} + for key1, key2 in zip(["00", "01", "10", "11"], ["0x0", "0x1", "0x2", "0x3"]): + counts[key1] = sorted_memory[idx]["memory"].count(key2) + run_result = { + "shots": shots, + "success": True, + "header": {"metadata": circ.metadata}, + "meas_level": meas_level, + "data": { + "counts": counts, + "memory": sorted_memory[idx]["memory"], + }, + } + + result["results"].append(run_result) + + return FakeJob(self, Result.from_dict(result)) + + +class MockRestlessFineAmp(MockRestlessBackend): + """A mock backend for restless single-qubit fine amplitude experiments.""" + + def __init__( + self, angle_error: float, angle_per_gate: float, gate_name: str, rng_seed: int = 0 + ): + """Setup a mock backend to test the restless fine amplitude calibration. + + Args: + angle_error: The rotation error per gate. + angle_per_gate: The angle per gate. + gate_name: The name of the gate to find in the circuit. + rng_seed: The random bit generator seed. + """ + self.angle_error = angle_error + self._gate_name = gate_name + self._angle_per_gate = angle_per_gate + super().__init__(rng_seed=rng_seed) + + self.configuration().basis_gates.extend(["sx", "x"]) + + def _compute_outcome_probabilities(self, circuits: List[QuantumCircuit]): + """Compute the probabilities of being in the excited state or + ground state for all circuits.""" + + self._precomputed_probabilities = {} + + for idx, circuit in enumerate(circuits): + + n_ops = circuit.count_ops().get(self._gate_name, 0) + angle = n_ops * (self._angle_per_gate + self.angle_error) + + if self._gate_name != "sx": + angle += np.pi / 2 * circuit.count_ops().get("sx", 0) + + if self._gate_name != "x": + angle += np.pi * circuit.count_ops().get("x", 0) + + prob_1 = np.sin(angle / 2) ** 2 + prob_0 = 1 - prob_1 + + self._precomputed_probabilities[(idx, "00")] = [prob_0, prob_1, 0, 0] + self._precomputed_probabilities[(idx, "01")] = [prob_1, prob_0, 0, 0] class MockIQBackend(FakeOpenPulse2Q): diff --git a/test/data_processing/test_restless_experiment.py b/test/data_processing/test_restless_experiment.py new file mode 100644 index 0000000000..4165f87f01 --- /dev/null +++ b/test/data_processing/test_restless_experiment.py @@ -0,0 +1,95 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test restless fine amplitude characterization and calibration experiments.""" +from test.base import QiskitExperimentsTestCase + +import numpy as np +from ddt import ddt, data + +from qiskit_experiments.library import ( + FineXAmplitude, +) + +from qiskit_experiments.test.mock_iq_backend import MockRestlessFineAmp + +from qiskit_experiments.data_processing.data_processor import DataProcessor +from qiskit_experiments.data_processing.nodes import Probability +from qiskit_experiments.framework import Options + + +@ddt +class TestFineAmpEndToEndRestless(QiskitExperimentsTestCase): + """Test the fine amplitude experiment in a restless measurement setting.""" + + def test_enable_restless(self): + """Test the enable_restless method.""" + + error = -np.pi * 0.01 + backend = MockRestlessFineAmp(error, np.pi, "x") + + amp_exp = FineXAmplitude(0, backend) + amp_exp.enable_restless(rep_delay=2e-6) + + self.assertTrue( + amp_exp.run_options, + Options( + meas_level=2, rep_delay=2e-6, init_qubits=False, memory=True, use_measure_esp=False + ), + ) + + @data(-0.03, -0.01, 0.02, 0.04) + def test_end_to_end_restless(self, pi_ratio): + """Test the restless experiment end to end.""" + + error = -np.pi * pi_ratio + backend = MockRestlessFineAmp(error, np.pi, "x") + + amp_exp = FineXAmplitude(0, backend) + amp_exp.enable_restless(rep_delay=1e-6) + + expdata = amp_exp.run(backend) + self.assertExperimentDone(expdata) + result = expdata.analysis_results(1) + d_theta = result.value.n + + self.assertAlmostEqual(d_theta, error, delta=0.01) + self.assertEqual(result.quality, "good") + + # check that the fit amplitude is almost 1 as expected. + amp_fit = expdata.analysis_results(0).value[0] + self.assertAlmostEqual(amp_fit, 1.0, delta=0.02) + + @data(-0.02, 0.04) + def test_end_to_end_restless_standard_processor(self, pi_ratio): + """Test the restless experiment with a standard processor end to end.""" + + error = -np.pi * pi_ratio + backend = MockRestlessFineAmp(error, np.pi, "x") + + amp_exp = FineXAmplitude(0, backend) + # standard data processor. + standard_processor = DataProcessor("counts", [Probability("01")]) + amp_exp.analysis.set_options(data_processor=standard_processor) + # enable a restless measurement setting. + amp_exp.enable_restless(rep_delay=1e-6, override_processor_by_restless=False) + + expdata = amp_exp.run(backend) + self.assertExperimentDone(expdata) + result = expdata.analysis_results(1) + d_theta = result.value.n + + self.assertTrue(abs(d_theta - error) > 0.01) + + # check that the fit amplitude is much smaller than 1. + amp_fit = expdata.analysis_results(0).value[0] + self.assertTrue(amp_fit < 0.05)