Skip to content
This repository has been archived by the owner on Jul 13, 2022. It is now read-only.

Commit

Permalink
Restless Experiments (qiskit-community#689)
Browse files Browse the repository at this point in the history
* This PR adds a restless Mixin class that allows experiments to run in the restless operation mode where qubits are not reset between two shots. The mix-in makes it easy for users to enable restless measurements and adds functionality for developers to specify restless data processor.

Co-authored-by: Daniel J. Egger <[email protected]>
  • Loading branch information
catornow and eggerdj authored Mar 14, 2022
1 parent 71361f3 commit 8b8d65a
Show file tree
Hide file tree
Showing 4 changed files with 413 additions and 1 deletion.
175 changes: 175 additions & 0 deletions qiskit_experiments/framework/restless_mixin.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
141 changes: 141 additions & 0 deletions qiskit_experiments/test/mock_iq_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading

0 comments on commit 8b8d65a

Please sign in to comment.