From 9e26a10ddfbe307a1b6a64e2e87271c170ed66fb Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 4 Aug 2021 09:45:11 -0400 Subject: [PATCH 01/11] Migrate measure mitigation from ignis for QuantumInstance This commit migrates the measurement mitigation code from qiskit-ignis into qiskit-terra for use with the QuantumInstance class. The QuantumInstance class's usage of the measurement mitigation from ignis is the one thing blocking us from deprecating qiskit-ignis completely. By embedding the code the quantum instance depends on inside qiskit.utils users of QuantumInstance (and therefore qiskit.algorithms) can construct and use measurement mitigation in it's current form. The use of this migrated module is only supported for use with the QuantumInstance class and is explicitly documented as internal/private except for how it gets used by the QuantumInstance. There is ongoing work to create a standardized mitigation API in #6485 and #6748, this does not preclude that work, but we should adapt this as part of those efforts to use the standardized interface. Ideally this would have been made a private interface and not exposed it as user facing (in deference to the standardization effort), but unfortunately the QuantumInstance expects classes of these classes as it's public interface for selecting a mitigation technique which means users need to be able to use the classes. However, as only the classes are public interfaces we can adapt this as we come up with a standardized mitigation interface and rewrite the internals of this and how the QuantumInstance leverages mitigators to use the new interface. A good follow-up here would be to adapt the mitigator selection kwarg to deprecate the use of classes and then we can make things explicitly private in the migrated code and wait for #6495 and #6748 to be ready for our user facing API. I opted to not include that in this PR to minimize changes to just what we migrated from ignis and update usage of old ignis classes to rely on the migrated version. --- docs/apidocs/terra.rst | 1 + docs/apidocs/utils_mitigation.rst | 6 + qiskit/utils/measurement_error_mitigation.py | 63 +- qiskit/utils/mitigation/__init__.py | 54 ++ qiskit/utils/mitigation/circuits.py | 238 ++++++ qiskit/utils/mitigation/filters.py | 498 ++++++++++++ qiskit/utils/mitigation/fitters.py | 439 +++++++++++ qiskit/utils/quantum_instance.py | 86 ++- .../ignis-mitigators-70492690cbcf99ca.yaml | 26 + test/python/algorithms/test_backendv1.py | 2 +- .../test_measure_error_mitigation.py | 127 +++- test/python/utils/__init__.py | 13 + test/python/utils/mitigation/__init__.py | 13 + test/python/utils/mitigation/test_meas.py | 707 ++++++++++++++++++ 14 files changed, 2215 insertions(+), 58 deletions(-) create mode 100644 docs/apidocs/utils_mitigation.rst create mode 100644 qiskit/utils/mitigation/__init__.py create mode 100644 qiskit/utils/mitigation/circuits.py create mode 100644 qiskit/utils/mitigation/filters.py create mode 100644 qiskit/utils/mitigation/fitters.py create mode 100644 releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml create mode 100644 test/python/utils/__init__.py create mode 100644 test/python/utils/mitigation/__init__.py create mode 100644 test/python/utils/mitigation/test_meas.py diff --git a/docs/apidocs/terra.rst b/docs/apidocs/terra.rst index 0d9e209869ca..91a1770ce15f 100644 --- a/docs/apidocs/terra.rst +++ b/docs/apidocs/terra.rst @@ -32,6 +32,7 @@ Qiskit Terra API Reference transpiler_passes transpiler_preset utils + utils_mitigation visualization opflow algorithms diff --git a/docs/apidocs/utils_mitigation.rst b/docs/apidocs/utils_mitigation.rst new file mode 100644 index 000000000000..a8af5992fd6a --- /dev/null +++ b/docs/apidocs/utils_mitigation.rst @@ -0,0 +1,6 @@ +.. _qiskit-utils-mitigation: + +.. automodule:: qiskit.utils.mitigation + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/qiskit/utils/measurement_error_mitigation.py b/qiskit/utils/measurement_error_mitigation.py index 506bcb3c3c01..a75f92882244 100644 --- a/qiskit/utils/measurement_error_mitigation.py +++ b/qiskit/utils/measurement_error_mitigation.py @@ -14,12 +14,19 @@ import copy from typing import List, Optional, Tuple, Dict, Callable + from qiskit import compiler from qiskit.providers import BaseBackend from qiskit.circuit import QuantumCircuit from qiskit.qobj import QasmQobj from qiskit.assembler.run_config import RunConfig -from ..exceptions import QiskitError, MissingOptionalLibraryError +from qiskit.exceptions import QiskitError, MissingOptionalLibraryError +from qiskit.utils.mitigation import ( + complete_meas_cal, + tensored_meas_cal, + CompleteMeasFitter, + TensoredMeasFitter, +) # pylint: disable=invalid-name @@ -137,35 +144,44 @@ def build_measurement_error_mitigation_circuits( QiskitError: when the fitter_cls is not recognizable. MissingOptionalLibraryError: Qiskit-Ignis not installed """ - try: - from qiskit.ignis.mitigation.measurement import ( - complete_meas_cal, - tensored_meas_cal, - CompleteMeasFitter, - TensoredMeasFitter, - ) - except ImportError as ex: - raise MissingOptionalLibraryError( - libname="qiskit-ignis", - name="build_measurement_error_mitigation_qobj", - pip_install="pip install qiskit-ignis", - ) from ex - circlabel = "mcal" if not qubit_list: raise QiskitError("The measured qubit list can not be [].") + run = False if fitter_cls == CompleteMeasFitter: meas_calibs_circuits, state_labels = complete_meas_cal( qubit_list=range(len(qubit_list)), circlabel=circlabel ) + run = True elif fitter_cls == TensoredMeasFitter: meas_calibs_circuits, state_labels = tensored_meas_cal( mit_pattern=mit_pattern, circlabel=circlabel ) - else: - raise QiskitError(f"Unknown fitter {fitter_cls}") + run = True + if not run: + try: + from qiskit.ignis.mitigation.measurement import ( + CompleteMeasFitter as CompleteMeasFitter_IG, + TensoredMeasFitter as TensoredMeasFitter_IG, + ) + except ImportError as ex: + raise MissingOptionalLibraryError( + libname="qiskit-ignis", + name="build_measurement_error_mitigation_qobj", + pip_install="pip install qiskit-ignis", + ) from ex + if fitter_cls == CompleteMeasFitter_IG: + meas_calibs_circuits, state_labels = complete_meas_cal( + qubit_list=range(len(qubit_list)), circlabel=circlabel + ) + elif fitter_cls == TensoredMeasFitter_IG: + meas_calibs_circuits, state_labels = tensored_meas_cal( + mit_pattern=mit_pattern, circlabel=circlabel + ) + else: + raise QiskitError(f"Unknown fitter {fitter_cls}") # the provided `qubit_list` would be used as the initial layout to # assure the consistent qubit mapping used in the main circuits. @@ -209,19 +225,6 @@ def build_measurement_error_mitigation_qobj( QiskitError: when the fitter_cls is not recognizable. MissingOptionalLibraryError: Qiskit-Ignis not installed """ - try: - from qiskit.ignis.mitigation.measurement import ( - complete_meas_cal, - tensored_meas_cal, - CompleteMeasFitter, - TensoredMeasFitter, - ) - except ImportError as ex: - raise MissingOptionalLibraryError( - libname="qiskit-ignis", - name="build_measurement_error_mitigation_qobj", - pip_install="pip install qiskit-ignis", - ) from ex circlabel = "mcal" diff --git a/qiskit/utils/mitigation/__init__.py b/qiskit/utils/mitigation/__init__.py new file mode 100644 index 000000000000..757900c2b229 --- /dev/null +++ b/qiskit/utils/mitigation/__init__.py @@ -0,0 +1,54 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# 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. + +# This code was originally copied from the qiskit-ignis repsoitory see: +# https://github.com/Qiskit/qiskit-ignis/blob/b91066c72171bcd55a70e6e8993b813ec763cf41/qiskit/ignis/mitigation/measurement/__init__.py +# it was migrated as qiskit-ignis is being deprecated + +""" +============================================================= +Measurement Mitigation Utils (:mod:`qiskit.utils.mitigation`) +============================================================= + +.. currentmodule:: qiskit.utils.mitigation + +Measurement correction +====================== + +The measurement calibration is used to mitigate measurement errors. +The main idea is to prepare all :math:`2^n` basis input states and compute +the probability of measuring counts in the other basis states. +From these calibrations, it is possible to correct the average results +of another experiment of interest. These tools are intended for use solely +with the :class:`~qiskit.utils.QuantumInstance` class as part of +:mod:`qiskit.algorithms` and :mod:`qiskit.opflow`. + +.. warning:: + + The user facing API stability of this module is not guaranteed except for + their use with the :class:`~qiskit.utils.QuantumInstance` (ie using the + :class:`~qiskit.utils.mitigation.CompleteMeasFitter` or + :class:`~qiskit.utils.mitigation.TensoredMeasFitter` class as values for the + ``meas_error_mitigation_cls``). The rest of this module should be treated as + an internal private API that can not be relied upon. + +.. autosummary:: + :toctree: ../stubs/ + + CompleteMeasFitter + TensoredMeasFitter +""" + +# Measurement correction functions +from .circuits import complete_meas_cal, tensored_meas_cal +from .filters import MeasurementFilter, TensoredFilter +from .fitters import CompleteMeasFitter, TensoredMeasFitter diff --git a/qiskit/utils/mitigation/circuits.py b/qiskit/utils/mitigation/circuits.py new file mode 100644 index 000000000000..f7d93c76d51a --- /dev/null +++ b/qiskit/utils/mitigation/circuits.py @@ -0,0 +1,238 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# 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. + +# This code was originally copied from the qiskit-ignis repsoitory see: +# https://github.com/Qiskit/qiskit-ignis/blob/b91066c72171bcd55a70e6e8993b813ec763cf41/qiskit/ignis/mitigation/measurement/circuits.py +# it was migrated to qiskit-terra as qiskit-ignis is being deprecated + +""" +Measurement calibration circuits. To apply the measurement mitigation +use the fitters to produce a filter. +""" +from typing import List, Tuple, Union + + +def count_keys(num_qubits: int) -> List[str]: + """Return ordered count keys. + + Args: + num_qubits: The number of qubits in the generated list. + Returns: + The strings of all 0/1 combinations of the given number of qubits + Example: + >>> count_keys(3) + ['000', '001', '010', '011', '100', '101', '110', '111'] + """ + return [bin(j)[2:].zfill(num_qubits) for j in range(2 ** num_qubits)] + + +def complete_meas_cal( + qubit_list: List[int] = None, + qr: Union[int, List["QuantumRegister"]] = None, + cr: Union[int, List["ClassicalRegister"]] = None, + circlabel: str = "", +) -> Tuple[List["QuantumCircuit"], List[str]]: + """ + Return a list of measurement calibration circuits for the full + Hilbert space. + + If the circuit contains :math:`n` qubits, then :math:`2^n` calibration circuits + are created, each of which creates a basis state. + + Args: + qubit_list: A list of qubits to perform the measurement correction on. + If `None`, and qr is given then assumed to be performed over the entire + qr. The calibration states will be labelled according to this ordering (default `None`). + + qr: Quantum registers (or their size). + If `None`, one is created (default `None`). + + cr: Classical registers (or their size). + If `None`, one is created(default `None`). + + circlabel: A string to add to the front of circuit names for + unique identification(default ' '). + + Returns: + A list of QuantumCircuit objects containing the calibration circuits. + + A list of calibration state labels. + + Additional Information: + The returned circuits are named circlabel+cal_XXX + where XXX is the basis state, + e.g., cal_1001. + + Pass the results of these circuits to the CompleteMeasurementFitter + constructor. + + Raises: + QiskitError: if both `qubit_list` and `qr` are `None`. + + """ + # Runtime imports to avoid circular imports causeed by QuantumInstance + # getting initialized by imported utils/__init__ which is imported + # by qiskit.circuit + from qiskit.circuit.quantumregister import QuantumRegister + from qiskit.circuit.classicalregister import ClassicalRegister + from qiskit.circuit.exceptions import QiskitError + + if qubit_list is None and qr is None: + raise QiskitError("Must give one of a qubit_list or a qr") + + # Create the registers if not already done + if qr is None: + qr = QuantumRegister(max(qubit_list) + 1) + + if isinstance(qr, int): + qr = QuantumRegister(qr) + + if qubit_list is None: + qubit_list = range(len(qr)) + + if isinstance(cr, int): + cr = ClassicalRegister(cr) + + nqubits = len(qubit_list) + + # labels for 2**n qubit states + state_labels = count_keys(nqubits) + + cal_circuits, _ = tensored_meas_cal([qubit_list], qr, cr, circlabel) + + return cal_circuits, state_labels + + +def tensored_meas_cal( + mit_pattern: List[List[int]] = None, + qr: Union[int, List["QuantumRegister"]] = None, + cr: Union[int, List["ClassicalRegister"]] = None, + circlabel: str = "", +) -> Tuple[List["QuantumCircuit"], List[List[int]]]: + """ + Return a list of calibration circuits + + Args: + mit_pattern: Qubits on which to perform the + measurement correction, divided to groups according to tensors. + If `None` and `qr` is given then assumed to be performed over the entire + `qr` as one group (default `None`). + + qr: A quantum register (or its size). + If `None`, one is created (default `None`). + + cr: A classical register (or its size). + If `None`, one is created (default `None`). + + circlabel: A string to add to the front of circuit names for + unique identification (default ' '). + + Returns: + A list of two QuantumCircuit objects containing the calibration circuits + mit_pattern + + Additional Information: + The returned circuits are named circlabel+cal_XXX + where XXX is the basis state, + e.g., cal_000 and cal_111. + + Pass the results of these circuits to the TensoredMeasurementFitter + constructor. + + Raises: + QiskitError: if both `mit_pattern` and `qr` are None. + QiskitError: if a qubit appears more than once in `mit_pattern`. + + """ + # Runtime imports to avoid circular imports causeed by QuantumInstance + # getting initialized by imported utils/__init__ which is imported + # by qiskit.circuit + from qiskit.circuit.quantumregister import QuantumRegister + from qiskit.circuit.classicalregister import ClassicalRegister + from qiskit.circuit.quantumcircuit import QuantumCircuit + from qiskit.circuit.exceptions import QiskitError + + if mit_pattern is None and qr is None: + raise QiskitError("Must give one of mit_pattern or qr") + + if isinstance(qr, int): + qr = QuantumRegister(qr) + + qubits_in_pattern = [] + if mit_pattern is not None: + for qubit_list in mit_pattern: + for qubit in qubit_list: + if qubit in qubits_in_pattern: + raise QiskitError( + "mit_pattern cannot contain \ + multiple instances of the same qubit" + ) + qubits_in_pattern.append(qubit) + + # Create the registers if not already done + if qr is None: + qr = QuantumRegister(max(qubits_in_pattern) + 1) + else: + qubits_in_pattern = range(len(qr)) + mit_pattern = [qubits_in_pattern] + + nqubits = len(qubits_in_pattern) + + # create classical bit registers + if cr is None: + cr = ClassicalRegister(nqubits) + + if isinstance(cr, int): + cr = ClassicalRegister(cr) + + qubits_list_sizes = [len(qubit_list) for qubit_list in mit_pattern] + nqubits = sum(qubits_list_sizes) + size_of_largest_group = max(qubits_list_sizes) + largest_labels = count_keys(size_of_largest_group) + + state_labels = [] + for largest_state in largest_labels: + basis_state = "" + for list_size in qubits_list_sizes: + basis_state = largest_state[:list_size] + basis_state + state_labels.append(basis_state) + + cal_circuits = [] + for basis_state in state_labels: + qc_circuit = QuantumCircuit(qr, cr, name="%scal_%s" % (circlabel, basis_state)) + + end_index = nqubits + for qubit_list, list_size in zip(mit_pattern, qubits_list_sizes): + + start_index = end_index - list_size + substate = basis_state[start_index:end_index] + + for qind in range(list_size): + if substate[list_size - qind - 1] == "1": + qc_circuit.x(qr[qubit_list[qind]]) + + end_index = start_index + + qc_circuit.barrier(qr) + + # add measurements + end_index = nqubits + for qubit_list, list_size in zip(mit_pattern, qubits_list_sizes): + + for qind in range(list_size): + qc_circuit.measure(qr[qubit_list[qind]], cr[nqubits - (end_index - qind)]) + + end_index -= list_size + + cal_circuits.append(qc_circuit) + + return cal_circuits, mit_pattern diff --git a/qiskit/utils/mitigation/filters.py b/qiskit/utils/mitigation/filters.py new file mode 100644 index 000000000000..5d756d55c439 --- /dev/null +++ b/qiskit/utils/mitigation/filters.py @@ -0,0 +1,498 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# 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. + +# This code was originally copied from the qiskit-ignis see: +# https://github.com/Qiskit/qiskit-ignis/blob/b91066c72171bcd55a70e6e8993b813ec763cf41/qiskit/ignis/mitigation/measurement/filters.py +# it was migrated as qiskit-ignis is being deprecated + +# pylint: disable=cell-var-from-loop,invalid-name + + +""" +Measurement correction filters. + +""" + +from typing import List +from copy import deepcopy + +import numpy as np +from scipy.optimize import minimize +import scipy.linalg as la + +import qiskit +from qiskit import QiskitError +from qiskit.tools import parallel_map +from qiskit.utils.mitigation.circuits import count_keys + + +class MeasurementFilter: + """ + Measurement error mitigation filter. + + Produced from a measurement calibration fitter and can be applied + to data. + + """ + + def __init__(self, cal_matrix: np.matrix, state_labels: list): + """ + Initialize a measurement error mitigation filter using the cal_matrix + from a measurement calibration fitter. + + Args: + cal_matrix: the calibration matrix for applying the correction + state_labels: the states for the ordering of the cal matrix + """ + + self._cal_matrix = cal_matrix + self._state_labels = state_labels + + @property + def cal_matrix(self): + """Return cal_matrix.""" + return self._cal_matrix + + @property + def state_labels(self): + """return the state label ordering of the cal matrix""" + return self._state_labels + + @state_labels.setter + def state_labels(self, new_state_labels): + """set the state label ordering of the cal matrix""" + self._state_labels = new_state_labels + + @cal_matrix.setter + def cal_matrix(self, new_cal_matrix): + """Set cal_matrix.""" + self._cal_matrix = new_cal_matrix + + def apply(self, raw_data, method="least_squares"): + """Apply the calibration matrix to results. + + Args: + raw_data (dict or list): The data to be corrected. Can be in a number of forms: + + Form 1: a counts dictionary from results.get_counts + + Form 2: a list of counts of `length==len(state_labels)` + + Form 3: a list of counts of `length==M*len(state_labels)` where M is an + integer (e.g. for use with the tomography data) + + Form 4: a qiskit Result + + method (str): fitting method. If `None`, then least_squares is used. + + ``pseudo_inverse``: direct inversion of the A matrix + + ``least_squares``: constrained to have physical probabilities + + Returns: + dict or list: The corrected data in the same form as `raw_data` + + Raises: + QiskitError: if `raw_data` is not an integer multiple + of the number of calibrated states. + + """ + + # check forms of raw_data + if isinstance(raw_data, dict): + # counts dictionary + for data_label in raw_data.keys(): + if data_label not in self._state_labels: + raise QiskitError( + "Unexpected state label '" + + data_label + + "', verify the fitter's state labels " + "correspond to the input data" + ) + data_format = 0 + # convert to form2 + raw_data2 = [np.zeros(len(self._state_labels), dtype=float)] + for stateidx, state in enumerate(self._state_labels): + raw_data2[0][stateidx] = raw_data.get(state, 0) + + elif isinstance(raw_data, list): + size_ratio = len(raw_data) / len(self._state_labels) + if len(raw_data) == len(self._state_labels): + data_format = 1 + raw_data2 = [raw_data] + elif int(size_ratio) == size_ratio: + data_format = 2 + size_ratio = int(size_ratio) + # make the list into chunks the size of state_labels for easier + # processing + raw_data2 = np.zeros([size_ratio, len(self._state_labels)]) + for i in range(size_ratio): + raw_data2[i][:] = raw_data[ + i * len(self._state_labels) : (i + 1) * len(self._state_labels) + ] + else: + raise QiskitError( + "Data list is not an integer multiple " "of the number of calibrated states" + ) + + elif isinstance(raw_data, qiskit.result.result.Result): + + # extract out all the counts, re-call the function with the + # counts and push back into the new result + new_result = deepcopy(raw_data) + + new_counts_list = parallel_map( + self._apply_correction, + [resultidx for resultidx, _ in enumerate(raw_data.results)], + task_args=(raw_data, method), + ) + + for resultidx, new_counts in new_counts_list: + new_result.results[resultidx].data.counts = new_counts + + return new_result + + else: + raise QiskitError("Unrecognized type for raw_data.") + + if method == "pseudo_inverse": + pinv_cal_mat = la.pinv(self._cal_matrix) + + # Apply the correction + for data_idx, _ in enumerate(raw_data2): + + if method == "pseudo_inverse": + raw_data2[data_idx] = np.dot(pinv_cal_mat, raw_data2[data_idx]) + + elif method == "least_squares": + nshots = sum(raw_data2[data_idx]) + + def fun(x): + return sum((raw_data2[data_idx] - np.dot(self._cal_matrix, x)) ** 2) + + x0 = np.random.rand(len(self._state_labels)) + x0 = x0 / sum(x0) + cons = {"type": "eq", "fun": lambda x: nshots - sum(x)} + bnds = tuple((0, nshots) for x in x0) + res = minimize(fun, x0, method="SLSQP", constraints=cons, bounds=bnds, tol=1e-6) + raw_data2[data_idx] = res.x + + else: + raise QiskitError("Unrecognized method.") + + if data_format == 2: + # flatten back out the list + raw_data2 = raw_data2.flatten() + + elif data_format == 0: + # convert back into a counts dictionary + new_count_dict = {} + for stateidx, state in enumerate(self._state_labels): + if raw_data2[0][stateidx] != 0: + new_count_dict[state] = raw_data2[0][stateidx] + + raw_data2 = new_count_dict + else: + # TODO: should probably change to: + # raw_data2 = raw_data2[0].tolist() + raw_data2 = raw_data2[0] + return raw_data2 + + def _apply_correction(self, resultidx, raw_data, method): + """Wrapper to call apply with a counts dictionary.""" + new_counts = self.apply(raw_data.get_counts(resultidx), method=method) + return resultidx, new_counts + + +class TensoredFilter: + """ + Tensored measurement error mitigation filter. + + Produced from a tensored measurement calibration fitter and can be applied + to data. + """ + + def __init__(self, cal_matrices: np.matrix, substate_labels_list: list, mit_pattern: list): + """ + Initialize a tensored measurement error mitigation filter using + the cal_matrices from a tensored measurement calibration fitter. + A simple usage this class is explained [here] + (https://qiskit.org/documentation/tutorials/noise/3_measurement_error_mitigation.html). + + Args: + cal_matrices: the calibration matrices for applying the correction. + substate_labels_list: for each calibration matrix + a list of the states (as strings, states in the subspace) + mit_pattern: for each calibration matrix + a list of the logical qubit indices (as int, states in the subspace) + """ + + self._cal_matrices = cal_matrices + self._qubit_list_sizes = [] + self._indices_list = [] + self._substate_labels_list = [] + self.substate_labels_list = substate_labels_list + self._mit_pattern = mit_pattern + + @property + def cal_matrices(self): + """Return cal_matrices.""" + return self._cal_matrices + + @cal_matrices.setter + def cal_matrices(self, new_cal_matrices): + """Set cal_matrices.""" + self._cal_matrices = deepcopy(new_cal_matrices) + + @property + def substate_labels_list(self): + """Return _substate_labels_list""" + return self._substate_labels_list + + @substate_labels_list.setter + def substate_labels_list(self, new_substate_labels_list): + """Return _substate_labels_list""" + self._substate_labels_list = new_substate_labels_list + + # get the number of qubits in each subspace + self._qubit_list_sizes = [] + for _, substate_label_list in enumerate(self._substate_labels_list): + self._qubit_list_sizes.append(int(np.log2(len(substate_label_list)))) + + # get the indices in the calibration matrix + self._indices_list = [] + for _, sub_labels in enumerate(self._substate_labels_list): + + self._indices_list.append({lab: ind for ind, lab in enumerate(sub_labels)}) + + @property + def qubit_list_sizes(self): + """Return _qubit_list_sizes.""" + return self._qubit_list_sizes + + @property + def nqubits(self): + """Return the number of qubits. See also MeasurementFilter.apply()""" + return sum(self._qubit_list_sizes) + + def apply( + self, + raw_data, + method="least_squares", + meas_layout=None, + ): + """ + Apply the calibration matrices to results. + + Args: + raw_data (dict or Result): The data to be corrected. Can be in one of two forms: + + * A counts dictionary from results.get_counts + + * A Qiskit Result + + method (str): fitting method. The following methods are supported: + + * 'pseudo_inverse': direct inversion of the cal matrices. + Mitigated counts can contain negative values + and the sum of counts would not equal to the shots. + Mitigation is conducted qubit wise: + For each qubit, mitigate the whole counts using the calibration matrices + which affect the corresponding qubit. + For example, assume we are mitigating the 3rd bit of the 4-bit counts + using '2\times 2' calibration matrix `A_3`. + When mitigating the count of '0110' in this step, + the following formula is applied: + `count['0110'] = A_3^{-1}[1, 0]*count['0100'] + A_3^{-1}[1, 1]*count['0110']`. + + The total time complexity of this method is `O(m2^{n + t})`, + where `n` is the size of calibrated qubits, + `m` is the number of sets in `mit_pattern`, + and `t` is the size of largest set of mit_pattern. + If the `mit_pattern` is shaped like `[[0], [1], [2], ..., [n-1]]`, + which corresponds to the tensor product noise model without cross-talk, + then the time complexity would be `O(n2^n)`. + If the `mit_pattern` is shaped like `[[0, 1, 2, ..., n-1]]`, + which exactly corresponds to the complete error mitigation, + then the time complexity would be `O(2^(n+n)) = O(4^n)`. + + + * 'least_squares': constrained to have physical probabilities. + Instead of directly applying inverse calibration matrices, + this method solve a constrained optimization problem to find + the closest probability vector to the result from 'pseudo_inverse' method. + Sequential least square quadratic programming (SLSQP) is used + in the internal process. + Every updating step in SLSQP takes `O(m2^{n+t})` time. + Since this method is using the SLSQP optimization over + the vector with lenght `2^n`, the mitigation for 8 bit counts + with the `mit_pattern = [[0], [1], [2], ..., [n-1]]` would + take 10 seconds or more. + + * If `None`, 'least_squares' is used. + + meas_layout (list of int): the mapping from classical registers to qubits + + * If you measure qubit `2` to clbit `0`, `0` to `1`, and `1` to `2`, + the list becomes `[2, 0, 1]` + + * If `None`, flatten(mit_pattern) is used. + + Returns: + dict or Result: The corrected data in the same form as raw_data + + Raises: + QiskitError: if raw_data is not in a one of the defined forms. + """ + + all_states = count_keys(self.nqubits) + num_of_states = 2 ** self.nqubits + + if meas_layout is None: + meas_layout = [] + for qubits in self._mit_pattern: + meas_layout += qubits + + # check forms of raw_data + if isinstance(raw_data, dict): + # counts dictionary + # convert to list + raw_data2 = [np.zeros(num_of_states, dtype=float)] + for state, count in raw_data.items(): + stateidx = int(state, 2) + raw_data2[0][stateidx] = count + + elif isinstance(raw_data, qiskit.result.result.Result): + + # extract out all the counts, re-call the function with the + # counts and push back into the new result + new_result = deepcopy(raw_data) + + new_counts_list = parallel_map( + self._apply_correction, + [resultidx for resultidx, _ in enumerate(raw_data.results)], + task_args=(raw_data, method, meas_layout), + ) + + for resultidx, new_counts in new_counts_list: + new_result.results[resultidx].data.counts = new_counts + + return new_result + + else: + raise QiskitError("Unrecognized type for raw_data.") + + if method == "pseudo_inverse": + pinv_cal_matrices = [] + for cal_mat in self._cal_matrices: + pinv_cal_matrices.append(la.pinv(cal_mat)) + + meas_layout = meas_layout[::-1] # reverse endian + qubits_to_clbits = [-1 for _ in range(max(meas_layout) + 1)] + for i, qubit in enumerate(meas_layout): + qubits_to_clbits[qubit] = i + + # Apply the correction + for data_idx, _ in enumerate(raw_data2): + + if method == "pseudo_inverse": + for pinv_cal_mat, pos_qubits, indices in zip( + pinv_cal_matrices, self._mit_pattern, self._indices_list + ): + inv_mat_dot_x = np.zeros([num_of_states], dtype=float) + pos_clbits = [qubits_to_clbits[qubit] for qubit in pos_qubits] + for state_idx, state in enumerate(all_states): + first_index = self.compute_index_of_cal_mat(state, pos_clbits, indices) + for i in range(len(pinv_cal_mat)): # i is index of pinv_cal_mat + source_state = self.flip_state(state, i, pos_clbits) + second_index = self.compute_index_of_cal_mat( + source_state, pos_clbits, indices + ) + inv_mat_dot_x[state_idx] += ( + pinv_cal_mat[first_index, second_index] + * raw_data2[data_idx][int(source_state, 2)] + ) + raw_data2[data_idx] = inv_mat_dot_x + + elif method == "least_squares": + + def fun(x): + mat_dot_x = deepcopy(x) + for cal_mat, pos_qubits, indices in zip( + self._cal_matrices, self._mit_pattern, self._indices_list + ): + res_mat_dot_x = np.zeros([num_of_states], dtype=float) + pos_clbits = [qubits_to_clbits[qubit] for qubit in pos_qubits] + for state_idx, state in enumerate(all_states): + second_index = self.compute_index_of_cal_mat(state, pos_clbits, indices) + for i in range(len(cal_mat)): + target_state = self.flip_state(state, i, pos_clbits) + first_index = self.compute_index_of_cal_mat( + target_state, pos_clbits, indices + ) + res_mat_dot_x[int(target_state, 2)] += ( + cal_mat[first_index, second_index] * mat_dot_x[state_idx] + ) + mat_dot_x = res_mat_dot_x + return sum((raw_data2[data_idx] - mat_dot_x) ** 2) + + x0 = np.random.rand(num_of_states) + x0 = x0 / sum(x0) + nshots = sum(raw_data2[data_idx]) + cons = {"type": "eq", "fun": lambda x: nshots - sum(x)} + bnds = tuple((0, nshots) for x in x0) + res = minimize(fun, x0, method="SLSQP", constraints=cons, bounds=bnds, tol=1e-6) + raw_data2[data_idx] = res.x + + else: + raise QiskitError("Unrecognized method.") + + # convert back into a counts dictionary + new_count_dict = {} + for state_idx, state in enumerate(all_states): + if raw_data2[0][state_idx] != 0: + new_count_dict[state] = raw_data2[0][state_idx] + + return new_count_dict + + def flip_state(self, state: str, mat_index: int, flip_poses: List[int]) -> str: + """Flip the state according to the chosen qubit positions""" + flip_poses = [pos for i, pos in enumerate(flip_poses) if (mat_index >> i) & 1] + flip_poses = sorted(flip_poses) + new_state = "" + pos = 0 + for flip_pos in flip_poses: + new_state += state[pos:flip_pos] + new_state += str(int(state[flip_pos], 2) ^ 1) # flip the state + pos = flip_pos + 1 + new_state += state[pos:] + return new_state + + def compute_index_of_cal_mat(self, state: str, pos_qubits: List[int], indices: dict) -> int: + """Return the index of (pseudo inverse) calibration matrix for the input quantum state""" + sub_state = "" + for pos in pos_qubits: + sub_state += state[pos] + return indices[sub_state] + + def _apply_correction( + self, + resultidx: int, + raw_data, + method, + meas_layout, + ): + """Wrapper to call apply with a counts dictionary.""" + new_counts = self.apply( + raw_data.get_counts(resultidx), method=method, meas_layout=meas_layout + ) + return resultidx, new_counts diff --git a/qiskit/utils/mitigation/fitters.py b/qiskit/utils/mitigation/fitters.py new file mode 100644 index 000000000000..588366374507 --- /dev/null +++ b/qiskit/utils/mitigation/fitters.py @@ -0,0 +1,439 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# 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. + +# This code was originally copied from the qiskit-ignis see: +# https://github.com/Qiskit/qiskit-ignis/blob/b91066c72171bcd55a70e6e8993b813ec763cf41/qiskit/ignis/mitigation/measurement/fitters.py +# it was migrated as qiskit-ignis is being deprecated + +# pylint: disable=cell-var-from-loop + + +""" +Measurement correction fitters. +""" +from typing import List +import copy +import re + +import numpy as np + +from qiskit import QiskitError +from qiskit.utils.mitigation.circuits import count_keys +from qiskit.utils.mitigation.filters import MeasurementFilter, TensoredFilter + + +class CompleteMeasFitter: + """ + Measurement correction fitter for a full calibration + """ + + def __init__( + self, + results, + state_labels: List[str], + qubit_list: List[int] = None, + circlabel: str = "", + ): + """ + Initialize a measurement calibration matrix from the results of running + the circuits returned by `measurement_calibration_circuits` + + A wrapper for the tensored fitter + + .. warning:: + + This class is not a public API the internals are not stable and will + likely change. It's uses is solely for the + ``measurement_error_mitigation_cls`` kwarg of the + :class:`~qiskit.utils.QuantumInstance` class's constructor (as + a class not an instance). Anything outside of that usage does + not have the normal user facing API stability. + + Args: + results: the results of running the measurement calibration + circuits. If this is `None` the user will set a calibration + matrix later. + state_labels: list of calibration state labels + returned from `measurement_calibration_circuits`. + The output matrix will obey this ordering. + qubit_list: List of the qubits (for reference and if the + subset is needed). If `None`, the qubit_list will be + created according to the length of state_labels[0]. + circlabel: if the qubits were labeled. + """ + if qubit_list is None: + qubit_list = range(len(state_labels[0])) + self._qubit_list = qubit_list + + self._tens_fitt = TensoredMeasFitter(results, [qubit_list], [state_labels], circlabel) + + @property + def cal_matrix(self): + """Return cal_matrix.""" + return self._tens_fitt.cal_matrices[0] + + @cal_matrix.setter + def cal_matrix(self, new_cal_matrix): + """set cal_matrix.""" + self._tens_fitt.cal_matrices = [copy.deepcopy(new_cal_matrix)] + + @property + def state_labels(self): + """Return state_labels.""" + return self._tens_fitt.substate_labels_list[0] + + @property + def qubit_list(self): + """Return list of qubits.""" + return self._qubit_list + + @state_labels.setter + def state_labels(self, new_state_labels): + """Set state label.""" + self._tens_fitt.substate_labels_list[0] = new_state_labels + + @property + def filter(self): + """Return a measurement filter using the cal matrix.""" + return MeasurementFilter(self.cal_matrix, self.state_labels) + + def add_data(self, new_results, rebuild_cal_matrix=True): + """ + Add measurement calibration data + + Args: + new_results (list or qiskit.result.Result): a single result or list + of result objects. + rebuild_cal_matrix (bool): rebuild the calibration matrix + """ + + self._tens_fitt.add_data(new_results, rebuild_cal_matrix) + + def subset_fitter(self, qubit_sublist=None): + """ + Return a fitter object that is a subset of the qubits in the original + list. + + Args: + qubit_sublist (list): must be a subset of qubit_list + + Returns: + CompleteMeasFitter: A new fitter that has the calibration for a + subset of qubits + + Raises: + QiskitError: If the calibration matrix is not initialized + """ + + if self._tens_fitt.cal_matrices is None: + raise QiskitError("Calibration matrix is not initialized") + + if qubit_sublist is None: + raise QiskitError("Qubit sublist must be specified") + + for qubit in qubit_sublist: + if qubit not in self._qubit_list: + raise QiskitError("Qubit not in the original set of qubits") + + # build state labels + new_state_labels = count_keys(len(qubit_sublist)) + + # mapping between indices in the state_labels and the qubits in + # the sublist + qubit_sublist_ind = [] + for sqb in qubit_sublist: + for qbind, qubit in enumerate(self._qubit_list): + if qubit == sqb: + qubit_sublist_ind.append(qbind) + + # states in the full calibration which correspond + # to the reduced labels + q_q_mapping = [] + state_labels_reduced = [] + for label in self.state_labels: + tmplabel = [label[index] for index in qubit_sublist_ind] + state_labels_reduced.append("".join(tmplabel)) + + for sub_lab_ind, _ in enumerate(new_state_labels): + q_q_mapping.append([]) + for labelind, label in enumerate(state_labels_reduced): + if label == new_state_labels[sub_lab_ind]: + q_q_mapping[-1].append(labelind) + + new_fitter = CompleteMeasFitter( + results=None, state_labels=new_state_labels, qubit_list=qubit_sublist + ) + + new_cal_matrix = np.zeros([len(new_state_labels), len(new_state_labels)]) + + # do a partial trace + for i in range(len(new_state_labels)): + for j in range(len(new_state_labels)): + + for q_q_i_map in q_q_mapping[i]: + for q_q_j_map in q_q_mapping[j]: + new_cal_matrix[i, j] += self.cal_matrix[q_q_i_map, q_q_j_map] + + new_cal_matrix[i, j] /= len(q_q_mapping[i]) + + new_fitter.cal_matrix = new_cal_matrix + + return new_fitter + + def readout_fidelity(self, label_list=None): + """ + Based on the results, output the readout fidelity which is the + normalized trace of the calibration matrix + + Args: + label_list (bool): If `None`, returns the average assignment fidelity + of a single state. Otherwise it returns the assignment fidelity + to be in any one of these states averaged over the second + index. + + Returns: + numpy.array: readout fidelity (assignment fidelity) + + Additional Information: + The on-diagonal elements of the calibration matrix are the + probabilities of measuring state 'x' given preparation of state + 'x' and so the normalized trace is the average assignment fidelity + """ + return self._tens_fitt.readout_fidelity(0, label_list) + + +class TensoredMeasFitter: + """ + Measurement correction fitter for a tensored calibration. + """ + + def __init__( + self, + results, + mit_pattern: List[List[int]], + substate_labels_list: List[List[str]] = None, + circlabel: str = "", + ): + """ + Initialize a measurement calibration matrix from the results of running + the circuits returned by `measurement_calibration_circuits`. + + .. warning:: + + This class is not a public API the internals are not stable and will + likely change. It's uses is solely for the + ``measurement_error_mitigation_cls`` kwarg of the + :class:`~qiskit.utils.QuantumInstance` class's constructor (as + a class not an instance). Anything outside of that usage does + not have the normal user facing API stability. + + Args: + results: the results of running the measurement calibration + circuits. If this is `None`, the user will set calibration + matrices later. + + mit_pattern: qubits to perform the + measurement correction on, divided to groups according to + tensors + + substate_labels_list: for each + calibration matrix, the labels of its rows and columns. + If `None`, the labels are ordered lexicographically + + circlabel: if the qubits were labeled + + Raises: + ValueError: if the mit_pattern doesn't match the + substate_labels_list + """ + + self._result_list = [] + self._cal_matrices = None + self._circlabel = circlabel + self._mit_pattern = mit_pattern + + self._qubit_list_sizes = [len(qubit_list) for qubit_list in mit_pattern] + + self._indices_list = [] + if substate_labels_list is None: + self._substate_labels_list = [] + for list_size in self._qubit_list_sizes: + self._substate_labels_list.append(count_keys(list_size)) + else: + self._substate_labels_list = substate_labels_list + if len(self._qubit_list_sizes) != len(substate_labels_list): + raise ValueError( + "mit_pattern does not match \ + substate_labels_list" + ) + + self._indices_list = [] + for _, sub_labels in enumerate(self._substate_labels_list): + self._indices_list.append({lab: ind for ind, lab in enumerate(sub_labels)}) + + self.add_data(results) + + @property + def cal_matrices(self): + """Return cal_matrices.""" + return self._cal_matrices + + @cal_matrices.setter + def cal_matrices(self, new_cal_matrices): + """Set _cal_matrices.""" + self._cal_matrices = copy.deepcopy(new_cal_matrices) + + @property + def substate_labels_list(self): + """Return _substate_labels_list.""" + return self._substate_labels_list + + @property + def filter(self): + """Return a measurement filter using the cal matrices.""" + return TensoredFilter(self._cal_matrices, self._substate_labels_list, self._mit_pattern) + + @property + def nqubits(self): + """Return _qubit_list_sizes.""" + return sum(self._qubit_list_sizes) + + def add_data(self, new_results, rebuild_cal_matrix=True): + """ + Add measurement calibration data + + Args: + new_results (list or qiskit.result.Result): a single result or list + of Result objects. + rebuild_cal_matrix (bool): rebuild the calibration matrix + """ + + if new_results is None: + return + + if not isinstance(new_results, list): + new_results = [new_results] + + for result in new_results: + self._result_list.append(result) + + if rebuild_cal_matrix: + self._build_calibration_matrices() + + def readout_fidelity(self, cal_index=0, label_list=None): + """ + Based on the results, output the readout fidelity, which is the average + of the diagonal entries in the calibration matrices. + + Args: + cal_index(integer): readout fidelity for this index in _cal_matrices + label_list (list): Returns the average fidelity over of the groups + f states. In the form of a list of lists of states. If `None`, + then each state used in the construction of the calibration + matrices forms a group of size 1 + + Returns: + numpy.array: The readout fidelity (assignment fidelity) + + Raises: + QiskitError: If the calibration matrix has not been set for the + object. + + Additional Information: + The on-diagonal elements of the calibration matrices are the + probabilities of measuring state 'x' given preparation of state + 'x'. + """ + + if self._cal_matrices is None: + raise QiskitError("Cal matrix has not been set") + + if label_list is None: + label_list = [[label] for label in self._substate_labels_list[cal_index]] + + state_labels = self._substate_labels_list[cal_index] + fidelity_label_list = [] + if label_list is None: + fidelity_label_list = [[label] for label in state_labels] + else: + for fid_sublist in label_list: + fidelity_label_list.append([]) + for fid_statelabl in fid_sublist: + for label_idx, label in enumerate(state_labels): + if fid_statelabl == label: + fidelity_label_list[-1].append(label_idx) + continue + + # fidelity_label_list is a 2D list of indices in the + # cal_matrix, we find the assignment fidelity of each + # row and average over the list + assign_fid_list = [] + + for fid_label_sublist in fidelity_label_list: + assign_fid_list.append(0) + for state_idx_i in fid_label_sublist: + for state_idx_j in fid_label_sublist: + assign_fid_list[-1] += self._cal_matrices[cal_index][state_idx_i][state_idx_j] + assign_fid_list[-1] /= len(fid_label_sublist) + + return np.mean(assign_fid_list) + + def _build_calibration_matrices(self): + """ + Build the measurement calibration matrices from the results of running + the circuits returned by `measurement_calibration`. + """ + + # initialize the set of empty calibration matrices + self._cal_matrices = [] + for list_size in self._qubit_list_sizes: + self._cal_matrices.append(np.zeros([2 ** list_size, 2 ** list_size], dtype=float)) + + # go through for each calibration experiment + for result in self._result_list: + for experiment in result.results: + circ_name = experiment.header.name + # extract the state from the circuit name + # this was the prepared state + circ_search = re.search("(?<=" + self._circlabel + "cal_)\\w+", circ_name) + + # this experiment is not one of the calcs so skip + if circ_search is None: + continue + + state = circ_search.group(0) + + # get the counts from the result + state_cnts = result.get_counts(circ_name) + for measured_state, counts in state_cnts.items(): + end_index = self.nqubits + for cal_ind, cal_mat in enumerate(self._cal_matrices): + + start_index = end_index - self._qubit_list_sizes[cal_ind] + + substate_index = self._indices_list[cal_ind][state[start_index:end_index]] + measured_substate_index = self._indices_list[cal_ind][ + measured_state[start_index:end_index] + ] + end_index = start_index + + cal_mat[measured_substate_index][substate_index] += counts + + for mat_index, _ in enumerate(self._cal_matrices): + sums_of_columns = np.sum(self._cal_matrices[mat_index], axis=0) + # pylint: disable=assignment-from-no-return + self._cal_matrices[mat_index] = np.divide( + self._cal_matrices[mat_index], + sums_of_columns, + out=np.zeros_like(self._cal_matrices[mat_index]), + where=sums_of_columns != 0, + ) diff --git a/qiskit/utils/quantum_instance.py b/qiskit/utils/quantum_instance.py index e48aa61c414f..bed5fd4cc20c 100644 --- a/qiskit/utils/quantum_instance.py +++ b/qiskit/utils/quantum_instance.py @@ -17,12 +17,14 @@ import copy import logging import time +import warnings + import numpy as np from qiskit.qobj import Qobj from qiskit.utils import circuit_utils -from qiskit.exceptions import QiskitError, MissingOptionalLibraryError -from .backend_utils import ( +from qiskit.exceptions import QiskitError +from qiskit.utils.backend_utils import ( is_ibmq_provider, is_statevector_backend, is_simulator_backend, @@ -31,6 +33,10 @@ is_basicaer_provider, support_backend_options, ) +from qiskit.utils.mitigation import ( + CompleteMeasFitter, + TensoredMeasFitter, +) logger = logging.getLogger(__name__) @@ -46,20 +52,34 @@ def type_from_class(meas_class): """ Returns fitter type from class """ + if meas_class == CompleteMeasFitter: + return _MeasFitterType.COMPLETE_MEAS_FITTER + elif meas_class == TensoredMeasFitter: + return _MeasFitterType.TENSORED_MEAS_FITTER try: from qiskit.ignis.mitigation.measurement import ( - CompleteMeasFitter, - TensoredMeasFitter, + CompleteMeasFitter as CompleteMeasFitter_IG, + TensoredMeasFitter as TensoredMeasFitter_IG, + ) + except ImportError: + pass + if meas_class == CompleteMeasFitter_IG: + warnings.warn( + "The use of qiskit-ignis for measurement mitigation is " + "deprecated and will be removed in a future release. Instead " + "use the CompleteMeasFitter class from qiskit.utils.mitigation", + DeprecationWarning, + stacklevel=3, ) - except ImportError as ex: - raise MissingOptionalLibraryError( - libname="qiskit-ignis", - name="QuantumInstance", - pip_install="pip install qiskit-ignis", - ) from ex - if meas_class == CompleteMeasFitter: return _MeasFitterType.COMPLETE_MEAS_FITTER - elif meas_class == TensoredMeasFitter: + elif meas_class == TensoredMeasFitter_IG: + warnings.warn( + "The use of qiskit-ignis for measurement mitigation is " + "deprecated and will be removed in a future release. Instead " + "use the TensoredMeasFitter class from qiskit.utils.mitigation", + DeprecationWarning, + stacklevel=3, + ) return _MeasFitterType.TENSORED_MEAS_FITTER else: raise QiskitError(f"Unknown fitter {meas_class}") @@ -69,20 +89,34 @@ def type_from_instance(meas_instance): """ Returns fitter type from instance """ + if isinstance(meas_instance, CompleteMeasFitter): + return _MeasFitterType.COMPLETE_MEAS_FITTER + elif isinstance(meas_instance, TensoredMeasFitter): + return _MeasFitterType.TENSORED_MEAS_FITTER try: from qiskit.ignis.mitigation.measurement import ( - CompleteMeasFitter, - TensoredMeasFitter, + CompleteMeasFitter as CompleteMeasFitter_IG, + TensoredMeasFitter as TensoredMeasFitter_IG, + ) + except ImportError: + pass + if isinstance(meas_instance, CompleteMeasFitter_IG): + warnings.warn( + "The use of qiskit-ignis for measurement mitigation is " + "deprecated and will be removed in a future release. Instead " + "use the CompleteMeasFitter class from qiskit.utils.mitigation", + DeprecationWarning, + stacklevel=3, ) - except ImportError as ex: - raise MissingOptionalLibraryError( - libname="qiskit-ignis", - name="QuantumInstance", - pip_install="pip install qiskit-ignis", - ) from ex - if isinstance(meas_instance, CompleteMeasFitter): return _MeasFitterType.COMPLETE_MEAS_FITTER - elif isinstance(meas_instance, TensoredMeasFitter): + elif isinstance(meas_instance, TensoredMeasFitter_IG): + warnings.warn( + "The use of qiskit-ignis for measurement mitigation is " + "deprecated and will be removed in a future release. Instead " + "use the TensoredMeasFitter class from qiskit.utils.mitigation", + DeprecationWarning, + stacklevel=3, + ) return _MeasFitterType.TENSORED_MEAS_FITTER else: raise QiskitError(f"Unknown fitter {meas_instance}") @@ -168,10 +202,10 @@ def __init__( skip_qobj_validation: Bypass Qobj validation to decrease circuit processing time during submission to backend. measurement_error_mitigation_cls: The approach to mitigate - measurement errors. Qiskit Ignis provides fitter classes for this functionality - and CompleteMeasFitter or TensoredMeasFitter - from qiskit.ignis.mitigation.measurement module can be used here. - TensoredMeasFitter doesn't support subset fitter. + measurement errors. The :class:`~qiskit.utils.mitigation.CompleteMeasFitter` or + :class:`~qiskit.utils.mitigation.TensoredMeasFitter` from the the + :mod:`qiskit.utils.mitigation` module + ``TensoredMeasFitter`` doesn't support subset fitter. cals_matrix_refresh_period: How often to refresh the calibration matrix in measurement mitigation. in minutes measurement_error_mitigation_shots: The number of shots number for diff --git a/releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml b/releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml new file mode 100644 index 000000000000..14ee2a4e7278 --- /dev/null +++ b/releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml @@ -0,0 +1,26 @@ +--- +features: + - | + Added two new classes, :class:`~qiskit.utils.mitigation.CompleteMeasFitter` + and :class:`~qiskit.utils.mitigation.TensoredMeasFitter` to the + :mod:`qiskit.utils.mitigation` module. These classes are for use only as + values for the ``measurement_error_mitigation_cls`` kwarg of the + :class:`~qiskit.utils.QuantumInstance` class. The instantiation, usage +deprecations: + - | + The use of the measurement mitigation classes + :class:`~qiskit.ignis.mitigation.CompleteMeasFitter` and + :class:`~qiskit.ignis.mitigation.TensoredMeasFitter` from ``qiskit-ignis`` + as values for the ``measurement_error_mitigation_cls`` kwarg of the + constructor for the :class:`~qiskit.utils.QuantumInstance` class is + deprecated and will be removed in a future release. Instead the equivalent + classes from :mod:`qiskit.utils.mitigation`, + :class:`~qiskit.utils.mitigation.CompleteMeasFitter` and + :class:`~qiskit.utils.mitigation.TensoredMeasFitter` should be used. This + was necessary as the ``qiskit-ignis`` project is now deprecated and will + no longer be supported in the near future. + It's worth noting that unlike the equivalent classes from ``qiskit-ignis`` + the versions from :mod:`qiskit.utils.mitigation` are supported only in + there use with :class:`~qiskit.utils.QuantumInstance` (ie as a class not + an instance with the ``measurement_error_mitigation_cls`` kwarg) and not + intended for standalone use. diff --git a/test/python/algorithms/test_backendv1.py b/test/python/algorithms/test_backendv1.py index c2a1f9e8f512..09548a303a78 100644 --- a/test/python/algorithms/test_backendv1.py +++ b/test/python/algorithms/test_backendv1.py @@ -21,6 +21,7 @@ from qiskit.opflow import X, Z, I from qiskit.algorithms.optimizers import SPSA from qiskit.circuit.library import TwoLocal, EfficientSU2 +from qiskit.utils.mitigation import CompleteMeasFitter class TestBackendV1(QiskitAlgorithmsTestCase): @@ -97,7 +98,6 @@ def test_run_circuit_oracle_single_experiment_backend(self): def test_measurement_error_mitigation_with_vqe(self): """measurement error mitigation test with vqe""" try: - from qiskit.ignis.mitigation.measurement import CompleteMeasFitter from qiskit.providers.aer import noise except ImportError as ex: self.skipTest(f"Package doesn't appear to be installed. Error: '{str(ex)}'") diff --git a/test/python/algorithms/test_measure_error_mitigation.py b/test/python/algorithms/test_measure_error_mitigation.py index d7c27ffbd79f..116c4f7c7d45 100644 --- a/test/python/algorithms/test_measure_error_mitigation.py +++ b/test/python/algorithms/test_measure_error_mitigation.py @@ -26,9 +26,9 @@ from qiskit.opflow import I, X, Z, PauliSumOp from qiskit.algorithms.optimizers import SPSA, COBYLA from qiskit.circuit.library import EfficientSU2 +from qiskit.utils.mitigation import CompleteMeasFitter, TensoredMeasFitter try: - from qiskit.ignis.mitigation.measurement import CompleteMeasFitter, TensoredMeasFitter from qiskit import Aer from qiskit.providers.aer import noise @@ -36,6 +36,16 @@ except ImportError as ex: _ERROR_MITIGATION_IMPORT_ERROR = str(ex) +try: + from qiskit.ignis.mitigation.measurement import ( + CompleteMeasFitter as CompleteMeasFitter_IG, + TensoredMeasFitter as TensoredMeasFitter_IG, + ) + + HAS_IGNIS = True +except ImportError: + HAS_IGNIS = False + @ddt class TestMeasurementErrorMitigation(QiskitAlgorithmsTestCase): @@ -212,6 +222,121 @@ def test_measurement_error_mitigation_qaoa(self): result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) self.assertAlmostEqual(result.eigenvalue.real, 3.49, delta=0.05) + @unittest.skipUnless(HAS_IGNIS, "qiskit-ignis is required to run this test") + @data("CompleteMeasFitter", "TensoredMeasFitter") + def test_measurement_error_mitigation_with_diff_qubit_order_ignis(self, fitter_str): + """measurement error mitigation with different qubit order""" + if _ERROR_MITIGATION_IMPORT_ERROR is not None: + self.skipTest( + f"Package doesn't appear to be installed. Error: '{_ERROR_MITIGATION_IMPORT_ERROR}'" + ) + return + + algorithm_globals.random_seed = 0 + + # build noise model + noise_model = noise.NoiseModel() + read_err = noise.errors.readout_error.ReadoutError([[0.9, 0.1], [0.25, 0.75]]) + noise_model.add_all_qubit_readout_error(read_err) + + fitter_cls = ( + CompleteMeasFitter_IG if fitter_str == "CompleteMeasFitter" else TensoredMeasFitter_IG + ) + backend = Aer.get_backend("aer_simulator") + quantum_instance = QuantumInstance( + backend=backend, + seed_simulator=1679, + seed_transpiler=167, + shots=1000, + noise_model=noise_model, + measurement_error_mitigation_cls=fitter_cls, + cals_matrix_refresh_period=0, + ) + # circuit + qc1 = QuantumCircuit(2, 2) + qc1.h(0) + qc1.cx(0, 1) + qc1.measure(0, 0) + qc1.measure(1, 1) + qc2 = QuantumCircuit(2, 2) + qc2.h(0) + qc2.cx(0, 1) + qc2.measure(1, 0) + qc2.measure(0, 1) + + if fitter_cls == TensoredMeasFitter_IG: + with self.assertWarns(DeprecationWarning): + self.assertRaisesRegex( + QiskitError, + "TensoredMeasFitter doesn't support subset_fitter.", + quantum_instance.execute, + [qc1, qc2], + ) + else: + # this should run smoothly + with self.assertWarns(DeprecationWarning): + quantum_instance.execute([qc1, qc2]) + + self.assertGreater(quantum_instance.time_taken, 0.0) + quantum_instance.reset_execution_results() + + # failure case + qc3 = QuantumCircuit(3, 3) + qc3.h(2) + qc3.cx(1, 2) + qc3.measure(2, 1) + qc3.measure(1, 2) + + self.assertRaises(QiskitError, quantum_instance.execute, [qc1, qc3]) + + @unittest.skipUnless(HAS_IGNIS, "qiskit-ignis is required to run this test") + @data(("CompleteMeasFitter", None), ("TensoredMeasFitter", [[0], [1]])) + def test_measurement_error_mitigation_with_vqe_ignis(self, config): + """measurement error mitigation test with vqe""" + if _ERROR_MITIGATION_IMPORT_ERROR is not None: + self.skipTest( + f"Package doesn't appear to be installed. Error: '{_ERROR_MITIGATION_IMPORT_ERROR}'" + ) + return + + fitter_str, mit_pattern = config + algorithm_globals.random_seed = 0 + + # build noise model + noise_model = noise.NoiseModel() + read_err = noise.errors.readout_error.ReadoutError([[0.9, 0.1], [0.25, 0.75]]) + noise_model.add_all_qubit_readout_error(read_err) + + fitter_cls = ( + CompleteMeasFitter_IG if fitter_str == "CompleteMeasFitter" else TensoredMeasFitter_IG + ) + backend = Aer.get_backend("aer_simulator") + quantum_instance = QuantumInstance( + backend=backend, + seed_simulator=167, + seed_transpiler=167, + noise_model=noise_model, + measurement_error_mitigation_cls=fitter_cls, + mit_pattern=mit_pattern, + ) + + h2_hamiltonian = ( + -1.052373245772859 * (I ^ I) + + 0.39793742484318045 * (I ^ Z) + - 0.39793742484318045 * (Z ^ I) + - 0.01128010425623538 * (Z ^ Z) + + 0.18093119978423156 * (X ^ X) + ) + optimizer = SPSA(maxiter=200) + ansatz = EfficientSU2(2, reps=1) + + vqe = VQE(ansatz=ansatz, optimizer=optimizer, quantum_instance=quantum_instance) + with self.assertWarns(DeprecationWarning): + result = vqe.compute_minimum_eigenvalue(operator=h2_hamiltonian) + self.assertGreater(quantum_instance.time_taken, 0.0) + quantum_instance.reset_execution_results() + self.assertAlmostEqual(result.eigenvalue.real, -1.86, delta=0.05) + if __name__ == "__main__": unittest.main() diff --git a/test/python/utils/__init__.py b/test/python/utils/__init__.py new file mode 100644 index 000000000000..655ef60bc94d --- /dev/null +++ b/test/python/utils/__init__.py @@ -0,0 +1,13 @@ +# 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. + +"""Qiskit utils unit tests.""" diff --git a/test/python/utils/mitigation/__init__.py b/test/python/utils/mitigation/__init__.py new file mode 100644 index 000000000000..ce3f4ee22ee0 --- /dev/null +++ b/test/python/utils/mitigation/__init__.py @@ -0,0 +1,13 @@ +# 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. + +"""Qiskit mitigation utils unit tests.""" diff --git a/test/python/utils/mitigation/test_meas.py b/test/python/utils/mitigation/test_meas.py new file mode 100644 index 000000000000..70633dad9acc --- /dev/null +++ b/test/python/utils/mitigation/test_meas.py @@ -0,0 +1,707 @@ +# 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. + +# pylint: disable=invalid-name + +""" +Test of measurement calibration: +1) Preparation of the basis states, generating the calibration circuits +(without noise), computing the calibration matrices, +and validating that they equal +to the identity matrices +2) Generating ideal (equally distributed) results, computing +the calibration output (without noise), +and validating that it is equally distributed +3) Testing the the measurement calibration on a circuit +(without noise), verifying that it is close to the +expected (equally distributed) result +4) Testing the fitters on pre-generated data with noise +""" + +import unittest +import numpy as np + +import qiskit +from qiskit.test import QiskitTestCase +from qiskit.result.result import Result +from qiskit.utils.mitigation import ( + CompleteMeasFitter, + TensoredMeasFitter, + complete_meas_cal, + tensored_meas_cal, + MeasurementFilter, +) +from qiskit.utils.mitigation.circuits import count_keys + +try: + from qiskit.providers.aer import Aer + from qiskit.providers.aer.noise import NoiseModel + from qiskit.providers.aer.noise.errors.standard_errors import pauli_error + + HAS_AER = True +except ImportError: + HAS_AER = False + +# fixed seed for tests - for both simulator and transpiler +SEED = 42 + + +def convert_ndarray_to_list_in_data(data: np.ndarray): + """ + converts ndarray format into list format (keeps all the dicts in the array) + also convert inner ndarrays into lists (recursively) + Args: + data: ndarray containing dicts or ndarrays in it + + Returns: + list: same array, converted to list format (in order to save it as json) + + """ + new_data = [] + for item in data: + if isinstance(item, np.ndarray): + new_item = convert_ndarray_to_list_in_data(item) + elif isinstance(item, dict): + new_item = {} + for key, value in item.items(): + new_item[key] = value.tolist() + else: + new_item = item + new_data.append(new_item) + + return new_data + + +def meas_calib_circ_creation(): + """ + create measurement calibration circuits and a GHZ state circuit for the tests + + Returns: + QuantumCircuit: the measurement calibrations circuits + list[str]: the mitigation pattern + QuantumCircuit: ghz circuit with 5 qubits (3 are used) + + """ + qubit_list = [1, 2, 3] + total_number_of_qubit = 5 + meas_calibs, state_labels = complete_meas_cal(qubit_list=qubit_list, qr=total_number_of_qubit) + + # Choose 3 qubits + qubit_1 = qubit_list[0] + qubit_2 = qubit_list[1] + qubit_3 = qubit_list[2] + ghz = qiskit.QuantumCircuit(total_number_of_qubit, len(qubit_list)) + ghz.h(qubit_1) + ghz.cx(qubit_1, qubit_2) + ghz.cx(qubit_1, qubit_3) + for i in qubit_list: + ghz.measure(i, i - 1) + return meas_calibs, state_labels, ghz + + +def tensored_calib_circ_creation(): + """ + create tensored measurement calibration circuits and a GHZ state circuit for the tests + + Returns: + QuantumCircuit: the tensored measurement calibration circuit + list[list[int]]: the mitigation pattern + QuantumCircuit: ghz circuit with 5 qubits (3 are used) + + """ + mit_pattern = [[2], [4, 1]] + meas_layout = [2, 4, 1] + qr = qiskit.QuantumRegister(5) + # Generate the calibration circuits + meas_calibs, mit_pattern = tensored_meas_cal(mit_pattern, qr=qr) + + cr = qiskit.ClassicalRegister(3) + ghz_circ = qiskit.QuantumCircuit(qr, cr) + ghz_circ.h(mit_pattern[0][0]) + ghz_circ.cx(mit_pattern[0][0], mit_pattern[1][0]) + ghz_circ.cx(mit_pattern[0][0], mit_pattern[1][1]) + ghz_circ.measure(mit_pattern[0][0], cr[0]) + ghz_circ.measure(mit_pattern[1][0], cr[1]) + ghz_circ.measure(mit_pattern[1][1], cr[2]) + return meas_calibs, mit_pattern, ghz_circ, meas_layout + + +def meas_calibration_circ_execution(shots: int, seed: int): + """ + create measurement calibration circuits and simulate them with noise + Args: + shots (int): number of shots per simulation + seed (int): the seed to use in the simulations + + Returns: + list: list of Results of the measurement calibration simulations + list: list of all the possible states with this amount of qubits + dict: dictionary of results counts of GHZ circuit simulation with measurement errors + """ + # define the circuits + meas_calibs, state_labels, ghz = meas_calib_circ_creation() + + # define noise + prob = 0.2 + error_meas = pauli_error([("X", prob), ("I", 1 - prob)]) + noise_model = NoiseModel() + noise_model.add_all_qubit_quantum_error(error_meas, "measure") + + # run the circuits multiple times + backend = qiskit.Aer.get_backend("qasm_simulator") + cal_results = qiskit.execute( + meas_calibs, backend=backend, shots=shots, noise_model=noise_model, seed_simulator=seed + ).result() + + ghz_results = ( + qiskit.execute( + ghz, backend=backend, shots=shots, noise_model=noise_model, seed_simulator=seed + ) + .result() + .get_counts() + ) + + return cal_results, state_labels, ghz_results + + +def tensored_calib_circ_execution(shots: int, seed: int): + """ + create tensored measurement calibration circuits and simulate them with noise + Args: + shots (int): number of shots per simulation + seed (int): the seed to use in the simulations + + Returns: + list: list of Results of the measurement calibration simulations + list: the mitigation pattern + dict: dictionary of results counts of GHZ circuit simulation with measurement errors + """ + # define the circuits + meas_calibs, mit_pattern, ghz_circ, meas_layout = tensored_calib_circ_creation() + # define noise + prob = 0.2 + error_meas = pauli_error([("X", prob), ("I", 1 - prob)]) + noise_model = NoiseModel() + noise_model.add_all_qubit_quantum_error(error_meas, "measure") + + # run the circuits multiple times + backend = qiskit.Aer.get_backend("qasm_simulator") + cal_results = qiskit.execute( + meas_calibs, backend=backend, shots=shots, noise_model=noise_model, seed_simulator=seed + ).result() + + ghz_results = qiskit.execute( + ghz_circ, backend=backend, shots=shots, noise_model=noise_model, seed_simulator=seed + ).result() + + return cal_results, mit_pattern, ghz_results, meas_layout + + +@unittest.skipUnless(HAS_AER, "Qiskit aer is required to run these tests") +class TestMeasCal(QiskitTestCase): + """The test class.""" + + def setUp(self): + super().setUp() + self.nq_list = [1, 2, 3, 4, 5] # Test up to 5 qubits + self.shots = 1024 # Number of shots (should be a power of 2) + + @staticmethod + def choose_calibration(nq, pattern_type): + """ + Generate a calibration circuit + + Args: + nq (int): number of qubits + pattern_type (int): a pattern in range(1, 2**nq) + + Returns: + qubits: a list of qubits according to the given pattern + weight: the weight of the pattern_type, + equals to the number of qubits + + Additional Information: + qr[i] exists if and only if the i-th bit in the binary + expression of + pattern_type equals 1 + """ + qubits = [] + weight = 0 + for i in range(nq): + pattern_bit = pattern_type & 1 + pattern_type = pattern_type >> 1 + if pattern_bit == 1: + qubits.append(i) + weight += 1 + return qubits, weight + + def generate_ideal_results(self, state_labels, weight): + """ + Generate ideal equally distributed results + + Args: + state_labels (list): a list of calibration state labels + weight (int): the number of qubits + + Returns: + results_dict: a dictionary of equally distributed results + results_list: a list of equally distributed results + + Additional Information: + for each state in state_labels: + result_dict[state] = #shots/len(state_labels) + """ + results_dict = {} + results_list = [0] * (2 ** weight) + state_num = len(state_labels) + for state in state_labels: + shots_per_state = self.shots / state_num + results_dict[state] = shots_per_state + # converting state (binary) to an integer + place = int(state, 2) + results_list[place] = shots_per_state + return results_dict, results_list + + def test_ideal_meas_cal(self): + """Test ideal execution, without noise.""" + for nq in self.nq_list: + for pattern_type in range(1, 2 ** nq): + + # Generate the quantum register according to the pattern + qubits, weight = self.choose_calibration(nq, pattern_type) + + # Generate the calibration circuits + meas_calibs, state_labels = complete_meas_cal(qubit_list=qubits, circlabel="test") + + # Perform an ideal execution on the generated circuits + backend = Aer.get_backend("qasm_simulator") + job = qiskit.execute(meas_calibs, backend=backend, shots=self.shots) + cal_results = job.result() + + # Make a calibration matrix + meas_cal = CompleteMeasFitter(cal_results, state_labels, circlabel="test") + + # Assert that the calibration matrix is equal to identity + IdentityMatrix = np.identity(2 ** weight) + self.assertListEqual( + meas_cal.cal_matrix.tolist(), + IdentityMatrix.tolist(), + "Error: the calibration matrix is \ + not equal to identity", + ) + + # Assert that the readout fidelity is equal to 1 + self.assertEqual( + meas_cal.readout_fidelity(), + 1.0, + "Error: the average fidelity \ + is not equal to 1", + ) + + # Generate ideal (equally distributed) results + results_dict, results_list = self.generate_ideal_results(state_labels, weight) + + # Output the filter + meas_filter = meas_cal.filter + + # Apply the calibration matrix to results + # in list and dict forms using different methods + results_dict_1 = meas_filter.apply(results_dict, method="least_squares") + results_dict_0 = meas_filter.apply(results_dict, method="pseudo_inverse") + results_list_1 = meas_filter.apply(results_list, method="least_squares") + results_list_0 = meas_filter.apply(results_list, method="pseudo_inverse") + + # Assert that the results are equally distributed + self.assertListEqual(results_list, results_list_0.tolist()) + self.assertListEqual(results_list, np.round(results_list_1).tolist()) + self.assertDictEqual(results_dict, results_dict_0) + round_results = {} + for key, val in results_dict_1.items(): + round_results[key] = np.round(val) + self.assertDictEqual(results_dict, round_results) + + def test_meas_cal_on_circuit(self): + """Test an execution on a circuit.""" + # Generate the calibration circuits + meas_calibs, state_labels, ghz = meas_calib_circ_creation() + + # Run the calibration circuits + backend = Aer.get_backend("qasm_simulator") + job = qiskit.execute( + meas_calibs, + backend=backend, + shots=self.shots, + seed_simulator=SEED, + seed_transpiler=SEED, + ) + cal_results = job.result() + + # Make a calibration matrix + meas_cal = CompleteMeasFitter(cal_results, state_labels) + # Calculate the fidelity + fidelity = meas_cal.readout_fidelity() + + job = qiskit.execute( + [ghz], backend=backend, shots=self.shots, seed_simulator=SEED, seed_transpiler=SEED + ) + results = job.result() + + # Predicted equally distributed results + predicted_results = {"000": 0.5, "111": 0.5} + + meas_filter = meas_cal.filter + + # Calculate the results after mitigation + output_results_pseudo_inverse = meas_filter.apply( + results, method="pseudo_inverse" + ).get_counts(0) + output_results_least_square = meas_filter.apply(results, method="least_squares").get_counts( + 0 + ) + + # Compare with expected fidelity and expected results + self.assertAlmostEqual(fidelity, 1.0) + self.assertAlmostEqual( + output_results_pseudo_inverse["000"] / self.shots, predicted_results["000"], places=1 + ) + + self.assertAlmostEqual( + output_results_least_square["000"] / self.shots, predicted_results["000"], places=1 + ) + + self.assertAlmostEqual( + output_results_pseudo_inverse["111"] / self.shots, predicted_results["111"], places=1 + ) + + self.assertAlmostEqual( + output_results_least_square["111"] / self.shots, predicted_results["111"], places=1 + ) + + def test_ideal_tensored_meas_cal(self): + """Test ideal execution, without noise.""" + + mit_pattern = [[1, 2], [3, 4, 5], [6]] + meas_layout = [1, 2, 3, 4, 5, 6] + + # Generate the calibration circuits + meas_calibs, _ = tensored_meas_cal(mit_pattern=mit_pattern) + + # Perform an ideal execution on the generated circuits + backend = Aer.get_backend("qasm_simulator") + cal_results = qiskit.execute(meas_calibs, backend=backend, shots=self.shots).result() + + # Make calibration matrices + meas_cal = TensoredMeasFitter(cal_results, mit_pattern=mit_pattern) + + # Assert that the calibration matrices are equal to identity + cal_matrices = meas_cal.cal_matrices + self.assertEqual( + len(mit_pattern), len(cal_matrices), "Wrong number of calibration matrices" + ) + for qubit_list, cal_mat in zip(mit_pattern, cal_matrices): + IdentityMatrix = np.identity(2 ** len(qubit_list)) + self.assertListEqual( + cal_mat.tolist(), + IdentityMatrix.tolist(), + "Error: the calibration matrix is \ + not equal to identity", + ) + + # Assert that the readout fidelity is equal to 1 + self.assertEqual( + meas_cal.readout_fidelity(), + 1.0, + "Error: the average fidelity \ + is not equal to 1", + ) + + # Generate ideal (equally distributed) results + results_dict, _ = self.generate_ideal_results(count_keys(6), 6) + + # Output the filter + meas_filter = meas_cal.filter + + # Apply the calibration matrix to results + # in list and dict forms using different methods + results_dict_1 = meas_filter.apply( + results_dict, method="least_squares", meas_layout=meas_layout + ) + results_dict_0 = meas_filter.apply( + results_dict, method="pseudo_inverse", meas_layout=meas_layout + ) + + # Assert that the results are equally distributed + self.assertDictEqual(results_dict, results_dict_0) + round_results = {} + for key, val in results_dict_1.items(): + round_results[key] = np.round(val) + self.assertDictEqual(results_dict, round_results) + + def test_tensored_meas_cal_on_circuit(self): + """Test an execution on a circuit.""" + + # Generate the calibration circuits + meas_calibs, mit_pattern, ghz, meas_layout = tensored_calib_circ_creation() + + # Run the calibration circuits + backend = Aer.get_backend("qasm_simulator") + cal_results = qiskit.execute( + meas_calibs, + backend=backend, + shots=self.shots, + seed_simulator=SEED, + seed_transpiler=SEED, + ).result() + + # Make a calibration matrix + meas_cal = TensoredMeasFitter(cal_results, mit_pattern=mit_pattern) + # Calculate the fidelity + fidelity = meas_cal.readout_fidelity(0) * meas_cal.readout_fidelity(1) + + results = qiskit.execute( + [ghz], backend=backend, shots=self.shots, seed_simulator=SEED, seed_transpiler=SEED + ).result() + + # Predicted equally distributed results + predicted_results = {"000": 0.5, "111": 0.5} + + meas_filter = meas_cal.filter + + # Calculate the results after mitigation + output_results_pseudo_inverse = meas_filter.apply( + results, method="pseudo_inverse", meas_layout=meas_layout + ).get_counts(0) + output_results_least_square = meas_filter.apply( + results, method="least_squares", meas_layout=meas_layout + ).get_counts(0) + + # Compare with expected fidelity and expected results + self.assertAlmostEqual(fidelity, 1.0) + self.assertAlmostEqual( + output_results_pseudo_inverse["000"] / self.shots, predicted_results["000"], places=1 + ) + + self.assertAlmostEqual( + output_results_least_square["000"] / self.shots, predicted_results["000"], places=1 + ) + + self.assertAlmostEqual( + output_results_pseudo_inverse["111"] / self.shots, predicted_results["111"], places=1 + ) + + self.assertAlmostEqual( + output_results_least_square["111"] / self.shots, predicted_results["111"], places=1 + ) + + +@unittest.skipUnless(HAS_AER, "Qiskit aer is required to run these tests") +class TestMeasCalNoise(TestMeasCal): + """Run measurement mitigation calibration tests with noise.""" + + def test_meas_fitter_with_noise(self): + """Test the MeasurementFitter with noise.""" + tests = [] + runs = 3 + for run in range(runs): + cal_results, state_labels, circuit_results = meas_calibration_circ_execution( + 1000, SEED + run + ) + + meas_cal = CompleteMeasFitter(cal_results, state_labels) + meas_filter = MeasurementFilter(meas_cal.cal_matrix, state_labels) + + # Calculate the results after mitigation + results_pseudo_inverse = meas_filter.apply(circuit_results, method="pseudo_inverse") + results_least_square = meas_filter.apply(circuit_results, method="least_squares") + tests.append( + { + "cal_matrix": convert_ndarray_to_list_in_data(meas_cal.cal_matrix), + "fidelity": meas_cal.readout_fidelity(), + "results": circuit_results, + "results_pseudo_inverse": results_pseudo_inverse, + "results_least_square": results_least_square, + } + ) + + # Set the state labels + state_labels = ["000", "001", "010", "011", "100", "101", "110", "111"] + meas_cal = CompleteMeasFitter(None, state_labels, circlabel="test") + + for tst_index, _ in enumerate(tests): + # Set the calibration matrix + meas_cal.cal_matrix = tests[tst_index]["cal_matrix"] + # Calculate the fidelity + fidelity = meas_cal.readout_fidelity() + + meas_filter = MeasurementFilter(tests[tst_index]["cal_matrix"], state_labels) + + # Calculate the results after mitigation + output_results_pseudo_inverse = meas_filter.apply( + tests[tst_index]["results"], method="pseudo_inverse" + ) + output_results_least_square = meas_filter.apply( + tests[tst_index]["results"], method="least_squares" + ) + + # Compare with expected fidelity and expected results + self.assertAlmostEqual(fidelity, tests[tst_index]["fidelity"], places=0) + self.assertAlmostEqual( + output_results_pseudo_inverse["000"], + tests[tst_index]["results_pseudo_inverse"]["000"], + places=0, + ) + + self.assertAlmostEqual( + output_results_least_square["000"], + tests[tst_index]["results_least_square"]["000"], + places=0, + ) + + self.assertAlmostEqual( + output_results_pseudo_inverse["111"], + tests[tst_index]["results_pseudo_inverse"]["111"], + places=0, + ) + + self.assertAlmostEqual( + output_results_least_square["111"], + tests[tst_index]["results_least_square"]["111"], + places=0, + ) + + def test_tensored_meas_fitter_with_noise(self): + """Test the TensoredFitter with noise.""" + cal_results, mit_pattern, circuit_results, meas_layout = tensored_calib_circ_execution( + 1000, SEED + ) + + meas_cal = TensoredMeasFitter(cal_results, mit_pattern=mit_pattern) + meas_filter = meas_cal.filter + + # Calculate the results after mitigation + results_pseudo_inverse = meas_filter.apply( + circuit_results.get_counts(), method="pseudo_inverse", meas_layout=meas_layout + ) + results_least_square = meas_filter.apply( + circuit_results.get_counts(), method="least_squares", meas_layout=meas_layout + ) + saved_info = { + "cal_results": cal_results.to_dict(), + "results": circuit_results.to_dict(), + "mit_pattern": mit_pattern, + "meas_layout": meas_layout, + "fidelity": meas_cal.readout_fidelity(), + "results_pseudo_inverse": results_pseudo_inverse, + "results_least_square": results_least_square, + } + + saved_info["cal_results"] = Result.from_dict(saved_info["cal_results"]) + saved_info["results"] = Result.from_dict(saved_info["results"]) + + meas_cal = TensoredMeasFitter( + saved_info["cal_results"], mit_pattern=saved_info["mit_pattern"] + ) + + # Calculate the fidelity + fidelity = meas_cal.readout_fidelity(0) * meas_cal.readout_fidelity(1) + # Compare with expected fidelity and expected results + self.assertAlmostEqual(fidelity, saved_info["fidelity"], places=0) + + meas_filter = meas_cal.filter + + # Calculate the results after mitigation + output_results_pseudo_inverse = meas_filter.apply( + saved_info["results"].get_counts(0), + method="pseudo_inverse", + meas_layout=saved_info["meas_layout"], + ) + output_results_least_square = meas_filter.apply( + saved_info["results"], method="least_squares", meas_layout=saved_info["meas_layout"] + ) + + self.assertAlmostEqual( + output_results_pseudo_inverse["000"], + saved_info["results_pseudo_inverse"]["000"], + places=0, + ) + + self.assertAlmostEqual( + output_results_least_square.get_counts(0)["000"], + saved_info["results_least_square"]["000"], + places=0, + ) + + self.assertAlmostEqual( + output_results_pseudo_inverse["111"], + saved_info["results_pseudo_inverse"]["111"], + places=0, + ) + + self.assertAlmostEqual( + output_results_least_square.get_counts(0)["111"], + saved_info["results_least_square"]["111"], + places=0, + ) + + substates_list = [] + for qubit_list in saved_info["mit_pattern"]: + substates_list.append(count_keys(len(qubit_list))[::-1]) + + fitter_other_order = TensoredMeasFitter( + saved_info["cal_results"], + substate_labels_list=substates_list, + mit_pattern=saved_info["mit_pattern"], + ) + + fidelity = fitter_other_order.readout_fidelity(0) * meas_cal.readout_fidelity(1) + + self.assertAlmostEqual(fidelity, saved_info["fidelity"], places=0) + + meas_filter = fitter_other_order.filter + + # Calculate the results after mitigation + output_results_pseudo_inverse = meas_filter.apply( + saved_info["results"].get_counts(0), + method="pseudo_inverse", + meas_layout=saved_info["meas_layout"], + ) + output_results_least_square = meas_filter.apply( + saved_info["results"], method="least_squares", meas_layout=saved_info["meas_layout"] + ) + + self.assertAlmostEqual( + output_results_pseudo_inverse["000"], + saved_info["results_pseudo_inverse"]["000"], + places=0, + ) + + self.assertAlmostEqual( + output_results_least_square.get_counts(0)["000"], + saved_info["results_least_square"]["000"], + places=0, + ) + + self.assertAlmostEqual( + output_results_pseudo_inverse["111"], + saved_info["results_pseudo_inverse"]["111"], + places=0, + ) + + self.assertAlmostEqual( + output_results_least_square.get_counts(0)["111"], + saved_info["results_least_square"]["111"], + places=0, + ) + + +if __name__ == "__main__": + unittest.main() From 32239961633e8b4721bcd8722cc0a2193fc201d9 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 4 Aug 2021 16:40:35 -0400 Subject: [PATCH 02/11] Update releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> --- releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml b/releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml index 14ee2a4e7278..67cced80b0e9 100644 --- a/releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml +++ b/releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml @@ -21,6 +21,6 @@ deprecations: no longer be supported in the near future. It's worth noting that unlike the equivalent classes from ``qiskit-ignis`` the versions from :mod:`qiskit.utils.mitigation` are supported only in - there use with :class:`~qiskit.utils.QuantumInstance` (ie as a class not + their use with :class:`~qiskit.utils.QuantumInstance` (ie as a class not an instance with the ``measurement_error_mitigation_cls`` kwarg) and not intended for standalone use. From 701b2de90eefd1d8ae31ebfb9421c64a02c2fd6e Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 5 Aug 2021 08:02:13 -0400 Subject: [PATCH 03/11] Finish release notes --- releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml b/releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml index 67cced80b0e9..244bcdc38c32 100644 --- a/releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml +++ b/releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml @@ -5,7 +5,10 @@ features: and :class:`~qiskit.utils.mitigation.TensoredMeasFitter` to the :mod:`qiskit.utils.mitigation` module. These classes are for use only as values for the ``measurement_error_mitigation_cls`` kwarg of the - :class:`~qiskit.utils.QuantumInstance` class. The instantiation, usage + :class:`~qiskit.utils.QuantumInstance` class. The instantiation and usage + of these classes (or anything else in :mod:`qiskit.utils.mitigation`) + outside of the ``measurement_error_mitigation_cls`` should be treated as an + internal private API and not relied upon. deprecations: - | The use of the measurement mitigation classes From 733417d44900ce9b9bd8dc1c78b9926d8b8c07e0 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 5 Aug 2021 08:03:03 -0400 Subject: [PATCH 04/11] Move API stability warning to the top --- qiskit/utils/mitigation/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/qiskit/utils/mitigation/__init__.py b/qiskit/utils/mitigation/__init__.py index 757900c2b229..8ab2883b6b1d 100644 --- a/qiskit/utils/mitigation/__init__.py +++ b/qiskit/utils/mitigation/__init__.py @@ -21,6 +21,15 @@ .. currentmodule:: qiskit.utils.mitigation +.. warning:: + + The user facing API stability of this module is not guaranteed except for + their use with the :class:`~qiskit.utils.QuantumInstance` (ie using the + :class:`~qiskit.utils.mitigation.CompleteMeasFitter` or + :class:`~qiskit.utils.mitigation.TensoredMeasFitter` class as values for the + ``meas_error_mitigation_cls``). The rest of this module should be treated as + an internal private API that can not be relied upon. + Measurement correction ====================== @@ -32,15 +41,6 @@ with the :class:`~qiskit.utils.QuantumInstance` class as part of :mod:`qiskit.algorithms` and :mod:`qiskit.opflow`. -.. warning:: - - The user facing API stability of this module is not guaranteed except for - their use with the :class:`~qiskit.utils.QuantumInstance` (ie using the - :class:`~qiskit.utils.mitigation.CompleteMeasFitter` or - :class:`~qiskit.utils.mitigation.TensoredMeasFitter` class as values for the - ``meas_error_mitigation_cls``). The rest of this module should be treated as - an internal private API that can not be relied upon. - .. autosummary:: :toctree: ../stubs/ From 1ace6b02b09a0635ab5b58da9a9add0661c6e2ce Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 29 Sep 2021 15:37:54 -0400 Subject: [PATCH 05/11] Apply suggestions from code review Co-authored-by: Jake Lishman --- qiskit/utils/mitigation/__init__.py | 6 +++--- qiskit/utils/mitigation/circuits.py | 7 +++---- qiskit/utils/mitigation/filters.py | 11 +++++------ qiskit/utils/mitigation/fitters.py | 17 +++++++---------- qiskit/utils/quantum_instance.py | 9 +++++---- .../ignis-mitigators-70492690cbcf99ca.yaml | 6 +++--- test/python/utils/mitigation/test_meas.py | 12 ++++-------- 7 files changed, 30 insertions(+), 38 deletions(-) diff --git a/qiskit/utils/mitigation/__init__.py b/qiskit/utils/mitigation/__init__.py index 8ab2883b6b1d..e76cefeb2cc1 100644 --- a/qiskit/utils/mitigation/__init__.py +++ b/qiskit/utils/mitigation/__init__.py @@ -23,10 +23,10 @@ .. warning:: - The user facing API stability of this module is not guaranteed except for - their use with the :class:`~qiskit.utils.QuantumInstance` (ie using the + The user-facing API stability of this module is not guaranteed except for + its use with the :class:`~qiskit.utils.QuantumInstance` (i.e. using the :class:`~qiskit.utils.mitigation.CompleteMeasFitter` or - :class:`~qiskit.utils.mitigation.TensoredMeasFitter` class as values for the + :class:`~qiskit.utils.mitigation.TensoredMeasFitter` classes as values for the ``meas_error_mitigation_cls``). The rest of this module should be treated as an internal private API that can not be relied upon. diff --git a/qiskit/utils/mitigation/circuits.py b/qiskit/utils/mitigation/circuits.py index f7d93c76d51a..62ed779bd23b 100644 --- a/qiskit/utils/mitigation/circuits.py +++ b/qiskit/utils/mitigation/circuits.py @@ -54,10 +54,10 @@ def complete_meas_cal( qr. The calibration states will be labelled according to this ordering (default `None`). qr: Quantum registers (or their size). - If `None`, one is created (default `None`). + If ``None``, one is created (default ``None``). cr: Classical registers (or their size). - If `None`, one is created(default `None`). + If ``None``, one is created(default ``None``). circlabel: A string to add to the front of circuit names for unique identification(default ' '). @@ -173,8 +173,7 @@ def tensored_meas_cal( for qubit in qubit_list: if qubit in qubits_in_pattern: raise QiskitError( - "mit_pattern cannot contain \ - multiple instances of the same qubit" + "mit_pattern cannot contain multiple instances of the same qubit" ) qubits_in_pattern.append(qubit) diff --git a/qiskit/utils/mitigation/filters.py b/qiskit/utils/mitigation/filters.py index 5d756d55c439..5950989b9e6e 100644 --- a/qiskit/utils/mitigation/filters.py +++ b/qiskit/utils/mitigation/filters.py @@ -113,11 +113,10 @@ def apply(self, raw_data, method="least_squares"): for data_label in raw_data.keys(): if data_label not in self._state_labels: raise QiskitError( - "Unexpected state label '" - + data_label - + "', verify the fitter's state labels " - "correspond to the input data" + f"Unexpected state label '{data_label}'." + " Verify the fitter's state labels correspond to the input data." ) + data_format = 0 # convert to form2 raw_data2 = [np.zeros(len(self._state_labels), dtype=float)] @@ -141,7 +140,7 @@ def apply(self, raw_data, method="least_squares"): ] else: raise QiskitError( - "Data list is not an integer multiple " "of the number of calibrated states" + "Data list is not an integer multiple of the number of calibrated states" ) elif isinstance(raw_data, qiskit.result.result.Result): @@ -486,7 +485,7 @@ def compute_index_of_cal_mat(self, state: str, pos_qubits: List[int], indices: d def _apply_correction( self, - resultidx: int, + resultidx, raw_data, method, meas_layout, diff --git a/qiskit/utils/mitigation/fitters.py b/qiskit/utils/mitigation/fitters.py index 588366374507..1cafb55de207 100644 --- a/qiskit/utils/mitigation/fitters.py +++ b/qiskit/utils/mitigation/fitters.py @@ -51,12 +51,12 @@ def __init__( .. warning:: - This class is not a public API the internals are not stable and will - likely change. It's uses is solely for the + This class is not a public API. The internals are not stable and will + likely change. It is used solely for the ``measurement_error_mitigation_cls`` kwarg of the :class:`~qiskit.utils.QuantumInstance` class's constructor (as a class not an instance). Anything outside of that usage does - not have the normal user facing API stability. + not have the normal user-facing API stability. Args: results: the results of running the measurement calibration @@ -229,12 +229,12 @@ def __init__( .. warning:: - This class is not a public API the internals are not stable and will - likely change. It's uses is solely for the + This class is not a public API. The internals are not stable and will + likely change. It is used solely for the ``measurement_error_mitigation_cls`` kwarg of the :class:`~qiskit.utils.QuantumInstance` class's constructor (as a class not an instance). Anything outside of that usage does - not have the normal user facing API stability. + not have the normal user-facing API stability. Args: results: the results of running the measurement calibration @@ -271,10 +271,7 @@ def __init__( else: self._substate_labels_list = substate_labels_list if len(self._qubit_list_sizes) != len(substate_labels_list): - raise ValueError( - "mit_pattern does not match \ - substate_labels_list" - ) + raise ValueError("mit_pattern does not match substate_labels_list") self._indices_list = [] for _, sub_labels in enumerate(self._substate_labels_list): diff --git a/qiskit/utils/quantum_instance.py b/qiskit/utils/quantum_instance.py index c68edef06da7..b5b459264d01 100644 --- a/qiskit/utils/quantum_instance.py +++ b/qiskit/utils/quantum_instance.py @@ -202,10 +202,11 @@ def __init__( skip_qobj_validation: Bypass Qobj validation to decrease circuit processing time during submission to backend. measurement_error_mitigation_cls: The approach to mitigate - measurement errors. The :class:`~qiskit.utils.mitigation.CompleteMeasFitter` or - :class:`~qiskit.utils.mitigation.TensoredMeasFitter` from the the - :mod:`qiskit.utils.mitigation` module - ``TensoredMeasFitter`` doesn't support subset fitter. + measurement errors. The classes :class:`~qiskit.utils.mitigation.CompleteMeasFitter` + or :class:`~qiskit.utils.mitigation.TensoredMeasFitter` from the + :mod:`qiskit.utils.mitigation` module can be used here as exact values, not + instances. ``TensoredMeasFitter`` doesn't support the ``subset_fitter`` method. + cals_matrix_refresh_period: How often to refresh the calibration matrix in measurement mitigation. in minutes measurement_error_mitigation_shots: The number of shots number for diff --git a/releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml b/releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml index 244bcdc38c32..1ca0e3ee1c72 100644 --- a/releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml +++ b/releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml @@ -7,13 +7,13 @@ features: values for the ``measurement_error_mitigation_cls`` kwarg of the :class:`~qiskit.utils.QuantumInstance` class. The instantiation and usage of these classes (or anything else in :mod:`qiskit.utils.mitigation`) - outside of the ``measurement_error_mitigation_cls`` should be treated as an + outside of the ``measurement_error_mitigation_cls`` kwarg should be treated as an internal private API and not relied upon. deprecations: - | The use of the measurement mitigation classes - :class:`~qiskit.ignis.mitigation.CompleteMeasFitter` and - :class:`~qiskit.ignis.mitigation.TensoredMeasFitter` from ``qiskit-ignis`` + :class:`qiskit.ignis.mitigation.CompleteMeasFitter` and + :class:`qiskit.ignis.mitigation.TensoredMeasFitter` from ``qiskit-ignis`` as values for the ``measurement_error_mitigation_cls`` kwarg of the constructor for the :class:`~qiskit.utils.QuantumInstance` class is deprecated and will be removed in a future release. Instead the equivalent diff --git a/test/python/utils/mitigation/test_meas.py b/test/python/utils/mitigation/test_meas.py index 70633dad9acc..c17501d54b22 100644 --- a/test/python/utils/mitigation/test_meas.py +++ b/test/python/utils/mitigation/test_meas.py @@ -295,16 +295,14 @@ def test_ideal_meas_cal(self): self.assertListEqual( meas_cal.cal_matrix.tolist(), IdentityMatrix.tolist(), - "Error: the calibration matrix is \ - not equal to identity", + "Error: the calibration matrix is not equal to identity", ) # Assert that the readout fidelity is equal to 1 self.assertEqual( meas_cal.readout_fidelity(), 1.0, - "Error: the average fidelity \ - is not equal to 1", + "Error: the average fidelity is not equal to 1", ) # Generate ideal (equally distributed) results @@ -412,16 +410,14 @@ def test_ideal_tensored_meas_cal(self): self.assertListEqual( cal_mat.tolist(), IdentityMatrix.tolist(), - "Error: the calibration matrix is \ - not equal to identity", + "Error: the calibration matrix is not equal to identity", ) # Assert that the readout fidelity is equal to 1 self.assertEqual( meas_cal.readout_fidelity(), 1.0, - "Error: the average fidelity \ - is not equal to 1", + "Error: the average fidelity is not equal to 1", ) # Generate ideal (equally distributed) results From 5a6070f4abec2f9ef536b685a829934f81c34110 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 29 Sep 2021 15:54:58 -0400 Subject: [PATCH 06/11] Remove unecessary test subclassing --- test/python/utils/mitigation/test_meas.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/python/utils/mitigation/test_meas.py b/test/python/utils/mitigation/test_meas.py index c17501d54b22..a71d2d327ef7 100644 --- a/test/python/utils/mitigation/test_meas.py +++ b/test/python/utils/mitigation/test_meas.py @@ -498,11 +498,6 @@ def test_tensored_meas_cal_on_circuit(self): output_results_least_square["111"] / self.shots, predicted_results["111"], places=1 ) - -@unittest.skipUnless(HAS_AER, "Qiskit aer is required to run these tests") -class TestMeasCalNoise(TestMeasCal): - """Run measurement mitigation calibration tests with noise.""" - def test_meas_fitter_with_noise(self): """Test the MeasurementFitter with noise.""" tests = [] From d04d405f673bac0708a33f6c38c730d96fb08c2c Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 29 Sep 2021 16:23:01 -0400 Subject: [PATCH 07/11] Fix skip logic in algorithm mitigation tests --- .../test_measure_error_mitigation.py | 40 ++++--------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/test/python/algorithms/test_measure_error_mitigation.py b/test/python/algorithms/test_measure_error_mitigation.py index 116c4f7c7d45..4c93d384d9d7 100644 --- a/test/python/algorithms/test_measure_error_mitigation.py +++ b/test/python/algorithms/test_measure_error_mitigation.py @@ -32,9 +32,9 @@ from qiskit import Aer from qiskit.providers.aer import noise - _ERROR_MITIGATION_IMPORT_ERROR = None -except ImportError as ex: - _ERROR_MITIGATION_IMPORT_ERROR = str(ex) + HAS_AER = True +except ImportError: + HAS_AER = False try: from qiskit.ignis.mitigation.measurement import ( @@ -51,15 +51,10 @@ class TestMeasurementErrorMitigation(QiskitAlgorithmsTestCase): """Test measurement error mitigation.""" + @unittest.skipUnless(HAS_AER, "qiskit-aer is required for this test") @data("CompleteMeasFitter", "TensoredMeasFitter") def test_measurement_error_mitigation_with_diff_qubit_order(self, fitter_str): """measurement error mitigation with different qubit order""" - if _ERROR_MITIGATION_IMPORT_ERROR is not None: - self.skipTest( - f"Package doesn't appear to be installed. Error: '{_ERROR_MITIGATION_IMPORT_ERROR}'" - ) - return - algorithm_globals.random_seed = 0 # build noise model @@ -115,14 +110,10 @@ def test_measurement_error_mitigation_with_diff_qubit_order(self, fitter_str): self.assertRaises(QiskitError, quantum_instance.execute, [qc1, qc3]) + @unittest.skipUnless(HAS_AER, "qiskit-aer is required for this test") @data(("CompleteMeasFitter", None), ("TensoredMeasFitter", [[0], [1]])) def test_measurement_error_mitigation_with_vqe(self, config): """measurement error mitigation test with vqe""" - if _ERROR_MITIGATION_IMPORT_ERROR is not None: - self.skipTest( - f"Package doesn't appear to be installed. Error: '{_ERROR_MITIGATION_IMPORT_ERROR}'" - ) - return fitter_str, mit_pattern = config algorithm_globals.random_seed = 0 @@ -187,14 +178,9 @@ def _get_operator(self, weight_matrix): opflow_list = [(pauli[1].to_label(), pauli[0]) for pauli in pauli_list] return PauliSumOp.from_list(opflow_list), shift + @unittest.skipUnless(HAS_AER, "qiskit-aer is required for this test") def test_measurement_error_mitigation_qaoa(self): """measurement error mitigation test with QAOA""" - if _ERROR_MITIGATION_IMPORT_ERROR is not None: - self.skipTest( - f"Package doesn't appear to be installed. Error: '{_ERROR_MITIGATION_IMPORT_ERROR}'" - ) - return - algorithm_globals.random_seed = 167 # build noise model @@ -222,16 +208,11 @@ def test_measurement_error_mitigation_qaoa(self): result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) self.assertAlmostEqual(result.eigenvalue.real, 3.49, delta=0.05) + @unittest.skipUnless(HAS_AER, "qiskit-aer is required for this test") @unittest.skipUnless(HAS_IGNIS, "qiskit-ignis is required to run this test") @data("CompleteMeasFitter", "TensoredMeasFitter") def test_measurement_error_mitigation_with_diff_qubit_order_ignis(self, fitter_str): """measurement error mitigation with different qubit order""" - if _ERROR_MITIGATION_IMPORT_ERROR is not None: - self.skipTest( - f"Package doesn't appear to be installed. Error: '{_ERROR_MITIGATION_IMPORT_ERROR}'" - ) - return - algorithm_globals.random_seed = 0 # build noise model @@ -289,16 +270,11 @@ def test_measurement_error_mitigation_with_diff_qubit_order_ignis(self, fitter_s self.assertRaises(QiskitError, quantum_instance.execute, [qc1, qc3]) + @unittest.skipUnless(HAS_AER, "qiskit-aer is required for this test") @unittest.skipUnless(HAS_IGNIS, "qiskit-ignis is required to run this test") @data(("CompleteMeasFitter", None), ("TensoredMeasFitter", [[0], [1]])) def test_measurement_error_mitigation_with_vqe_ignis(self, config): """measurement error mitigation test with vqe""" - if _ERROR_MITIGATION_IMPORT_ERROR is not None: - self.skipTest( - f"Package doesn't appear to be installed. Error: '{_ERROR_MITIGATION_IMPORT_ERROR}'" - ) - return - fitter_str, mit_pattern = config algorithm_globals.random_seed = 0 From 0977d10c07d4f78ece33e1fcb4c005cdc42b9cc0 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 29 Sep 2021 16:37:08 -0400 Subject: [PATCH 08/11] Fix exception handling on missing ignis in algorithms --- qiskit/utils/measurement_error_mitigation.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/qiskit/utils/measurement_error_mitigation.py b/qiskit/utils/measurement_error_mitigation.py index a75f92882244..4b8e998db253 100644 --- a/qiskit/utils/measurement_error_mitigation.py +++ b/qiskit/utils/measurement_error_mitigation.py @@ -167,11 +167,9 @@ def build_measurement_error_mitigation_circuits( TensoredMeasFitter as TensoredMeasFitter_IG, ) except ImportError as ex: - raise MissingOptionalLibraryError( - libname="qiskit-ignis", - name="build_measurement_error_mitigation_qobj", - pip_install="pip install qiskit-ignis", - ) from ex + # If ignis can't be imported we don't have a valid fitter + # class so just fail here with an appropriate error message + raise QiskitError(f"Unknown fitter {fitter_cls}") from ex if fitter_cls == CompleteMeasFitter_IG: meas_calibs_circuits, state_labels = complete_meas_cal( qubit_list=range(len(qubit_list)), circlabel=circlabel From 508d601d47d92afeb5d3fdeea1775c13466fd1b8 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 29 Sep 2021 16:45:01 -0400 Subject: [PATCH 09/11] Assert deprecation message for ignis use mentions ignis --- test/python/algorithms/test_measure_error_mitigation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/python/algorithms/test_measure_error_mitigation.py b/test/python/algorithms/test_measure_error_mitigation.py index 4c93d384d9d7..953ae339e474 100644 --- a/test/python/algorithms/test_measure_error_mitigation.py +++ b/test/python/algorithms/test_measure_error_mitigation.py @@ -246,7 +246,7 @@ def test_measurement_error_mitigation_with_diff_qubit_order_ignis(self, fitter_s qc2.measure(0, 1) if fitter_cls == TensoredMeasFitter_IG: - with self.assertWarns(DeprecationWarning): + with self.assertWarnsRegex(DeprecationWarning, r".*ignis.*"): self.assertRaisesRegex( QiskitError, "TensoredMeasFitter doesn't support subset_fitter.", @@ -255,7 +255,7 @@ def test_measurement_error_mitigation_with_diff_qubit_order_ignis(self, fitter_s ) else: # this should run smoothly - with self.assertWarns(DeprecationWarning): + with self.assertWarnsRegex(DeprecationWarning, r".*ignis.*"): quantum_instance.execute([qc1, qc2]) self.assertGreater(quantum_instance.time_taken, 0.0) @@ -307,7 +307,7 @@ def test_measurement_error_mitigation_with_vqe_ignis(self, config): ansatz = EfficientSU2(2, reps=1) vqe = VQE(ansatz=ansatz, optimizer=optimizer, quantum_instance=quantum_instance) - with self.assertWarns(DeprecationWarning): + with self.assertWarnsRegex(DeprecationWarning, r".*ignis.*"): result = vqe.compute_minimum_eigenvalue(operator=h2_hamiltonian) self.assertGreater(quantum_instance.time_taken, 0.0) quantum_instance.reset_execution_results() From bffd7b3e974e4e783f561fcd6d9ac747a40ceb7c Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 29 Sep 2021 16:49:51 -0400 Subject: [PATCH 10/11] Make filters module properly private --- qiskit/utils/mitigation/__init__.py | 1 - qiskit/utils/mitigation/{filters.py => _filters.py} | 0 qiskit/utils/mitigation/fitters.py | 2 +- test/python/utils/mitigation/test_meas.py | 2 +- 4 files changed, 2 insertions(+), 3 deletions(-) rename qiskit/utils/mitigation/{filters.py => _filters.py} (100%) diff --git a/qiskit/utils/mitigation/__init__.py b/qiskit/utils/mitigation/__init__.py index e76cefeb2cc1..2d2de3040746 100644 --- a/qiskit/utils/mitigation/__init__.py +++ b/qiskit/utils/mitigation/__init__.py @@ -50,5 +50,4 @@ # Measurement correction functions from .circuits import complete_meas_cal, tensored_meas_cal -from .filters import MeasurementFilter, TensoredFilter from .fitters import CompleteMeasFitter, TensoredMeasFitter diff --git a/qiskit/utils/mitigation/filters.py b/qiskit/utils/mitigation/_filters.py similarity index 100% rename from qiskit/utils/mitigation/filters.py rename to qiskit/utils/mitigation/_filters.py diff --git a/qiskit/utils/mitigation/fitters.py b/qiskit/utils/mitigation/fitters.py index 1cafb55de207..0324fc5247d9 100644 --- a/qiskit/utils/mitigation/fitters.py +++ b/qiskit/utils/mitigation/fitters.py @@ -28,7 +28,7 @@ from qiskit import QiskitError from qiskit.utils.mitigation.circuits import count_keys -from qiskit.utils.mitigation.filters import MeasurementFilter, TensoredFilter +from qiskit.utils.mitigation._filters import MeasurementFilter, TensoredFilter class CompleteMeasFitter: diff --git a/test/python/utils/mitigation/test_meas.py b/test/python/utils/mitigation/test_meas.py index a71d2d327ef7..2b2188de0e36 100644 --- a/test/python/utils/mitigation/test_meas.py +++ b/test/python/utils/mitigation/test_meas.py @@ -38,8 +38,8 @@ TensoredMeasFitter, complete_meas_cal, tensored_meas_cal, - MeasurementFilter, ) +from qiskit.utils.mitigation._filters import MeasurementFilter from qiskit.utils.mitigation.circuits import count_keys try: From 9cb0797022473a80113fc7db2a7bed71388d2ff9 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 29 Sep 2021 16:55:44 -0400 Subject: [PATCH 11/11] Fix lint --- qiskit/utils/measurement_error_mitigation.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qiskit/utils/measurement_error_mitigation.py b/qiskit/utils/measurement_error_mitigation.py index 4b8e998db253..441e27c1ecf0 100644 --- a/qiskit/utils/measurement_error_mitigation.py +++ b/qiskit/utils/measurement_error_mitigation.py @@ -20,7 +20,7 @@ from qiskit.circuit import QuantumCircuit from qiskit.qobj import QasmQobj from qiskit.assembler.run_config import RunConfig -from qiskit.exceptions import QiskitError, MissingOptionalLibraryError +from qiskit.exceptions import QiskitError from qiskit.utils.mitigation import ( complete_meas_cal, tensored_meas_cal, @@ -142,7 +142,6 @@ def build_measurement_error_mitigation_circuits( the labels of the calibration circuits Raises: QiskitError: when the fitter_cls is not recognizable. - MissingOptionalLibraryError: Qiskit-Ignis not installed """ circlabel = "mcal"