From 9fa95653e92199ff286a42f2c5c257ef0a2b7ccb Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 30 Sep 2021 16:48:52 -0400 Subject: [PATCH 1/8] Remove travis config file (#7088) Due to changes in the travis terms of service and the very limited quotas they offer to open source projects [1] now we haven't been running travis jobs for months due to exceeding the quota. Additionally due to the recent security issues [2] we've disabled travis as a service on the qiskit organization as it wasn't being used anymore and exposed a risk. Since we're not using travis anymore and won't be in the future this commit just deletes the travis config file to avoid confusion. [1] https://blog.travis-ci.com/2020-11-02-travis-ci-new-billing [2] https://nvd.nist.gov/vuln/detail/CVE-2021-41077 Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .travis.yml | 67 ----------------------------------------------------- 1 file changed, 67 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f51b249f97a9..000000000000 --- a/.travis.yml +++ /dev/null @@ -1,67 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -notifications: - email: false - -cache: - pip: true - directories: - - .stestr - -os: linux -dist: bionic -language: python -python: 3.7 -install: - # Install step for jobs that require compilation and qa. - - pip install -U -r requirements.txt -c constraints.txt - - pip install -U -r requirements-dev.txt coveralls -c constraints.txt - - pip install -c constraints.txt -e . - - pip install "qiskit-ibmq-provider" -c constraints.txt - - pip install "qiskit-aer" -script: - # Compile the executables and run the tests. - - python setup.py build_ext --inplace - - export PYTHONHASHSEED=$(python -S -c "import random; print(random. randint(1, 4294967295))") - - echo "PYTHONHASHSEED=$PYTHONHASHSEED" - - stestr run -after_failure: - - python tools/report_ci_failure.py - -jobs: - fast_finish: true - allow_failures: - - name: Randomized tests - include: - - name: Python 3.6 Tests and Coverage Linux - python: 3.6 - env: - - PYTHON="coverage run --source qiskit --parallel-mode" - - QISKIT_TEST_CAPTURE_STREAMS=1 - after_success: - - coverage combine || true - - coveralls || true - - coverage xml || true - - pip install diff-cover || true - - diff-cover --compare-branch main coverage.xml || true - - # Randomized testing - - name: Randomized tests - cache: - pip: true - directories: - - .hypothesis - script: - - pip install -U pip - - python setup.py build_ext --inplace - - make test_randomized From 5b5b37617cb79b383c7007d9c674b11a15c54f83 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 30 Sep 2021 18:39:29 -0400 Subject: [PATCH 2/8] Add min_qubits kwarg to UnitarySynthesis pass (#6349) * Rely on UnitarySynthesis pass for unrolling UnitaryGates This commit adds the UnitarySynthesis pass to the preset pass manager anywhere unrolling of custom or wide gates was done. Previously, we only ever called the UnitarySynthesis pass in the preset pass managers if the basis translation method was set to 'synthesis' or we were using level3 and then it was part of the optimization pass (as part of 2q peephole optimization). This was an issue in that we're implicitly calling the _define() method of the class whenever we're unrolling gates >= 3q or gates with a custom definition. The _define() method is basically identical to the UnitarySynthesis pass except it doesn't expose the options to set a basis gate or approximation degree, which would result in the output gates from unitary gates in the unroll steps are always in the U3 basis. This meant we would be converting unitary gates to u3 (and cx) which would result in a conversion to the target basis later, even if we could just go to the target basis directly. This is also future proofing for #6124 where a plugin interface is added to the UnitarySynthesis pass and can potentially be used for arbitrary sized unitaries. At the same time this change caught an issue qith the SingleQubitUnitary gate where the name was duplicated with the UnitaryGate which would result in errors when UnitarySynthesis was called because the UnitarySynthesis pass looks for gate named 'unitary' to run on. This is fixed and the SingleQubitUnitary gate's name is changed to 'squ' to differentiate it from the UnitaryGate. * Add option to set a minimum size to unitarysynthesis pass This commit adds a new option to the UnitarySynthesis pass constructor, min_qubits, which is used to specify a minimimum size unitary to synthesize. If the unitary is smaller than that it will be skipped. This is then used by the UnitarySynthesis instance in the unroll3q phase so we don't decompose 1 or 2q unitaries before routing. * Fix rebase error * Correct oversights from rebase Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/extensions/quantum_initializer/squ.py | 2 +- .../passes/synthesis/unitary_synthesis.py | 8 +++++- .../transpiler/preset_passmanagers/level0.py | 22 +++++++++++++++- .../transpiler/preset_passmanagers/level1.py | 26 ++++++++++++++++++- .../transpiler/preset_passmanagers/level2.py | 26 ++++++++++++++++++- .../transpiler/preset_passmanagers/level3.py | 22 +++++++++++++++- .../notes/squ-gate-name-785b7896300a92ef.yaml | 18 +++++++++++++ 7 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/squ-gate-name-785b7896300a92ef.yaml diff --git a/qiskit/extensions/quantum_initializer/squ.py b/qiskit/extensions/quantum_initializer/squ.py index 9f3861ebe23e..285ae1b8f22c 100644 --- a/qiskit/extensions/quantum_initializer/squ.py +++ b/qiskit/extensions/quantum_initializer/squ.py @@ -59,7 +59,7 @@ def __init__(self, unitary_matrix, mode="ZYZ", up_to_diagonal=False, u=None): self._diag = None # Create new gate - super().__init__("unitary", 1, [unitary_matrix]) + super().__init__("squ", 1, [unitary_matrix]) def inverse(self): """Return the inverse. diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 2f8739488e10..b4d38d9e3d07 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -91,6 +91,7 @@ def __init__( natural_direction: Union[bool, None] = None, synth_gates: Union[List[str], None] = None, method: str = "default", + min_qubits: int = None, ): """Synthesize unitaries over some basis gates. @@ -131,11 +132,14 @@ def __init__( ['unitary']. If None and `pulse_optimzie` == True, default to ['unitary', 'swap'] method (str): The unitary synthesis method plugin to use. - + min_qubits: The minimum number of qubits in the unitary to synthesize. If this is set + and the unitary is less than the specified number of qubits it will not be + synthesized. """ super().__init__() self._basis_gates = basis_gates self._approximation_degree = approximation_degree + self._min_qubits = min_qubits self.method = method self.plugins = plugin.UnitarySynthesisPluginManager() self._coupling_map = coupling_map @@ -194,6 +198,8 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: plugin_method._approximation_degree = self._approximation_degree for node in dag.named_nodes(*self._synth_gates): + if self._min_qubits is not None and len(node.qargs) < self._min_qubits: + continue if plugin_method.supports_coupling_map: kwargs["coupling_map"] = ( self._coupling_map, diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index f598f5d5d8dc..070eb8740031 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -113,12 +113,14 @@ def _choose_layout_condition(property_set): # 3. Decompose so only 1-qubit and 2-qubit gates remain _unroll3q = [ + # Use unitary synthesis for basis aware decomposition of UnitaryGates UnitarySynthesis( basis_gates, approximation_degree=approximation_degree, coupling_map=coupling_map, backend_props=backend_properties, method=unitary_synthesis_method, + min_qubits=3, ), Unroll3qOrMore(), ] @@ -155,9 +157,27 @@ def _swap_condition(property_set): elif translation_method == "translator": from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel - _unroll = [UnrollCustomDefinitions(sel, basis_gates), BasisTranslator(sel, basis_gates)] + _unroll = [ + UnitarySynthesis( + basis_gates, + approximation_degree=approximation_degree, + coupling_map=coupling_map, + backend_props=backend_properties, + method=unitary_synthesis_method, + ), + UnrollCustomDefinitions(sel, basis_gates), + BasisTranslator(sel, basis_gates), + ] elif translation_method == "synthesis": _unroll = [ + UnitarySynthesis( + basis_gates, + approximation_degree=approximation_degree, + coupling_map=coupling_map, + backend_props=backend_properties, + method=unitary_synthesis_method, + min_qubits=3, + ), Unroll3qOrMore(), Collect2qBlocks(), ConsolidateBlocks(basis_gates=basis_gates), diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index 0cff8a5f5dbd..6483eebffb03 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -133,12 +133,14 @@ def _not_perfect_yet(property_set): # 4. Decompose so only 1-qubit and 2-qubit gates remain _unroll3q = [ + # Use unitary synthesis for basis aware decomposition of UnitaryGates UnitarySynthesis( basis_gates, approximation_degree=approximation_degree, coupling_map=coupling_map, method=unitary_synthesis_method, backend_props=backend_properties, + min_qubits=3, ), Unroll3qOrMore(), ] @@ -175,9 +177,31 @@ def _swap_condition(property_set): elif translation_method == "translator": from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel - _unroll = [UnrollCustomDefinitions(sel, basis_gates), BasisTranslator(sel, basis_gates)] + _unroll = [ + # Use unitary synthesis for basis aware decomposition of UnitaryGates before + # custom unrolling + UnitarySynthesis( + basis_gates, + approximation_degree=approximation_degree, + coupling_map=coupling_map, + method=unitary_synthesis_method, + backend_props=backend_properties, + ), + UnrollCustomDefinitions(sel, basis_gates), + BasisTranslator(sel, basis_gates), + ] elif translation_method == "synthesis": _unroll = [ + # Use unitary synthesis for basis aware decomposition of UnitaryGates before + # collection + UnitarySynthesis( + basis_gates, + approximation_degree=approximation_degree, + coupling_map=coupling_map, + method=unitary_synthesis_method, + backend_props=backend_properties, + min_qubits=3, + ), Unroll3qOrMore(), Collect2qBlocks(), ConsolidateBlocks(basis_gates=basis_gates), diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index 929e9b270c45..4daf5b1c1d1a 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -167,12 +167,14 @@ def _csp_not_found_match(property_set): # 3. Unroll to 1q or 2q gates _unroll3q = [ + # Use unitary synthesis for basis aware decomposition of UnitaryGates UnitarySynthesis( basis_gates, approximation_degree=approximation_degree, coupling_map=coupling_map, backend_props=backend_properties, method=unitary_synthesis_method, + min_qubits=3, ), Unroll3qOrMore(), ] @@ -209,9 +211,31 @@ def _swap_condition(property_set): elif translation_method == "translator": from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel - _unroll = [UnrollCustomDefinitions(sel, basis_gates), BasisTranslator(sel, basis_gates)] + _unroll = [ + # Use unitary synthesis for basis aware decomposition of UnitaryGates before + # custom unrolling + UnitarySynthesis( + basis_gates, + approximation_degree=approximation_degree, + coupling_map=coupling_map, + backend_props=backend_properties, + method=unitary_synthesis_method, + ), + UnrollCustomDefinitions(sel, basis_gates), + BasisTranslator(sel, basis_gates), + ] elif translation_method == "synthesis": _unroll = [ + # Use unitary synthesis for basis aware decomposition of UnitaryGates before + # collection + UnitarySynthesis( + basis_gates, + approximation_degree=approximation_degree, + coupling_map=coupling_map, + backend_props=backend_properties, + method=unitary_synthesis_method, + min_qubits=3, + ), Unroll3qOrMore(), Collect2qBlocks(), ConsolidateBlocks(basis_gates=basis_gates), diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index 8f6cecb7be62..1d0c56264239 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -108,12 +108,14 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: # 1. Unroll to 1q or 2q gates _unroll3q = [ + # Use unitary synthesis for basis aware decomposition of UnitaryGates UnitarySynthesis( basis_gates, approximation_degree=approximation_degree, coupling_map=coupling_map, backend_props=backend_properties, method=unitary_synthesis_method, + min_qubits=3, ), Unroll3qOrMore(), ] @@ -212,9 +214,27 @@ def _swap_condition(property_set): elif translation_method == "translator": from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel - _unroll = [UnrollCustomDefinitions(sel, basis_gates), BasisTranslator(sel, basis_gates)] + _unroll = [ + UnitarySynthesis( + basis_gates, + approximation_degree=approximation_degree, + coupling_map=coupling_map, + backend_props=backend_properties, + method=unitary_synthesis_method, + ), + UnrollCustomDefinitions(sel, basis_gates), + BasisTranslator(sel, basis_gates), + ] elif translation_method == "synthesis": _unroll = [ + UnitarySynthesis( + basis_gates, + approximation_degree=approximation_degree, + coupling_map=coupling_map, + backend_props=backend_properties, + method=unitary_synthesis_method, + min_qubits=3, + ), Unroll3qOrMore(), Collect2qBlocks(), ConsolidateBlocks(basis_gates=basis_gates), diff --git a/releasenotes/notes/squ-gate-name-785b7896300a92ef.yaml b/releasenotes/notes/squ-gate-name-785b7896300a92ef.yaml new file mode 100644 index 000000000000..2b0e6fda182a --- /dev/null +++ b/releasenotes/notes/squ-gate-name-785b7896300a92ef.yaml @@ -0,0 +1,18 @@ +--- +features: + - | + The :class:`~qiskit.transpiler.passes.UnitarySynthesis` transpiler pass in + :mod:`qiskit.transpiler.passes` has a new kwarg in the constructor, + ``min_qubits``. When specified this can be set to an ``int`` value which + is the minimum size :class:`~qiskit.extensions.UnitaryGate` object to + run the unitary synthesis on. If a :class:`~qiskit.extensions.UnitaryGate` + in a :class:`~qiskit.circuit.QuantumCircuit` uses fewer qubits it will + be skipped by that instance of the pass. +upgrade: + - | + The :attr:`~qiskit.extensions.SingleQubitUnitary.name` attribute of the + :class:`~qiskit.extensions.SingleQubitUnitary` gate class has been changed + from ``unitary`` to ``squ``. This was necessary to avoid a conflict with + the :class:`~qiskit.extensions.UnitaryGate` class's name which was also + ``unitary`` since the 2 gates are not the same and don't have the same + implementation (and can't be used interchangeably). From a528e2e0abaaeaf1b19a358683a8312733d47593 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Fri, 1 Oct 2021 15:05:41 +0200 Subject: [PATCH 3/8] Breaking up the version info python row in the qiskit_version_info magic (#6590) * qiskit_version_table with domain specific apps * breacking up the python line in the version_info magic * lint * code packages Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/tools/jupyter/version_table.py | 5 +++-- qiskit/utils/multiprocessing.py | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/qiskit/tools/jupyter/version_table.py b/qiskit/tools/jupyter/version_table.py index 980ef04d13cf..e0a76d827c58 100644 --- a/qiskit/tools/jupyter/version_table.py +++ b/qiskit/tools/jupyter/version_table.py @@ -13,7 +13,6 @@ """A module for monitoring backends.""" -import sys import time from IPython.display import HTML, display from IPython.core.magic import line_magic, Magics, magics_class @@ -50,7 +49,9 @@ def qiskit_version_table(self, line="", cell=None): local_hw_info = local_hardware_info() sys_info = [ - ("Python", sys.version), + ("Python version", local_hw_info["python_version"]), + ("Python compiler", local_hw_info["python_compiler"]), + ("Python build", local_hw_info["python_build"]), ("OS", "%s" % local_hw_info["os"]), ("CPUs", "%s" % local_hw_info["cpus"]), ("Memory (Gb)", "%s" % local_hw_info["memory"]), diff --git a/qiskit/utils/multiprocessing.py b/qiskit/utils/multiprocessing.py index cd4219dfbe21..df3e5ad5a21b 100644 --- a/qiskit/utils/multiprocessing.py +++ b/qiskit/utils/multiprocessing.py @@ -29,6 +29,9 @@ def local_hardware_info(): dict: The hardware information. """ results = { + "python_compiler": platform.python_compiler(), + "python_build": ", ".join(platform.python_build()), + "python_version": platform.python_version(), "os": platform.system(), "memory": psutil.virtual_memory().total / (1024 ** 3), "cpus": psutil.cpu_count(logical=False) or 1, From 0c274336acaadc0ca3efaf6f88d32595e3b16314 Mon Sep 17 00:00:00 2001 From: Qian Jianhua Date: Fri, 1 Oct 2021 23:45:48 +0800 Subject: [PATCH 4/8] Merge condition check codes in compose() method (#6983) * Merge condition check codes in compose() method * change comment * format comment Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: Kevin Krsulich --- qiskit/circuit/quantumcircuit.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 2ca6e5ead696..f50db1d96c28 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -863,16 +863,14 @@ def compose( mapped_instrs.append((n_instr, n_qargs, n_cargs)) if front: + # adjust new instrs before original ones and update all parameters dest._data = mapped_instrs + dest._data - else: - dest._data += mapped_instrs - - if front: dest._parameter_table.clear() for instr, _, _ in dest._data: dest._update_parameter_table(instr) else: - # just append new parameters + # just append new instrs and parameters + dest._data += mapped_instrs for instr, _, _ in mapped_instrs: dest._update_parameter_table(instr) From 75c0e46fafe2c160ae345945842a711e7493b96b Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 1 Oct 2021 17:06:56 -0400 Subject: [PATCH 5/8] Migrate measure mitigation from ignis for QuantumInstance (#6867) * 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. * Update releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> * Finish release notes * Move API stability warning to the top * Apply suggestions from code review Co-authored-by: Jake Lishman * Remove unecessary test subclassing * Fix skip logic in algorithm mitigation tests * Fix exception handling on missing ignis in algorithms * Assert deprecation message for ignis use mentions ignis * Make filters module properly private * Fix lint Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> Co-authored-by: Jake Lishman --- docs/apidocs/terra.rst | 1 + docs/apidocs/utils_mitigation.rst | 6 + qiskit/utils/measurement_error_mitigation.py | 62 +- qiskit/utils/mitigation/__init__.py | 53 ++ qiskit/utils/mitigation/_filters.py | 497 +++++++++++++ qiskit/utils/mitigation/circuits.py | 237 ++++++ qiskit/utils/mitigation/fitters.py | 436 +++++++++++ qiskit/utils/quantum_instance.py | 87 ++- .../ignis-mitigators-70492690cbcf99ca.yaml | 29 + test/python/algorithms/test_backendv1.py | 2 +- .../test_measure_error_mitigation.py | 143 +++- test/python/utils/__init__.py | 1 + test/python/utils/mitigation/__init__.py | 13 + test/python/utils/mitigation/test_meas.py | 698 ++++++++++++++++++ 14 files changed, 2186 insertions(+), 79 deletions(-) create mode 100644 docs/apidocs/utils_mitigation.rst create mode 100644 qiskit/utils/mitigation/__init__.py create mode 100644 qiskit/utils/mitigation/_filters.py create mode 100644 qiskit/utils/mitigation/circuits.py create mode 100644 qiskit/utils/mitigation/fitters.py create mode 100644 releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml 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 19f70f9ce72e..d6f0244e25db 100644 --- a/docs/apidocs/terra.rst +++ b/docs/apidocs/terra.rst @@ -33,5 +33,6 @@ Qiskit Terra API Reference transpiler_preset transpiler_plugins utils + utils_mitigation 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..441e27c1ecf0 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 +from qiskit.utils.mitigation import ( + complete_meas_cal, + tensored_meas_cal, + CompleteMeasFitter, + TensoredMeasFitter, +) # pylint: disable=invalid-name @@ -135,37 +142,43 @@ 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 """ - 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: + # 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 + ) + 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 +222,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..2d2de3040746 --- /dev/null +++ b/qiskit/utils/mitigation/__init__.py @@ -0,0 +1,53 @@ +# 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 + +.. warning:: + + 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` 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. + +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`. + +.. autosummary:: + :toctree: ../stubs/ + + CompleteMeasFitter + TensoredMeasFitter +""" + +# Measurement correction functions +from .circuits import complete_meas_cal, tensored_meas_cal +from .fitters import CompleteMeasFitter, TensoredMeasFitter diff --git a/qiskit/utils/mitigation/_filters.py b/qiskit/utils/mitigation/_filters.py new file mode 100644 index 000000000000..5950989b9e6e --- /dev/null +++ b/qiskit/utils/mitigation/_filters.py @@ -0,0 +1,497 @@ +# 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( + 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)] + 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, + 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/circuits.py b/qiskit/utils/mitigation/circuits.py new file mode 100644 index 000000000000..62ed779bd23b --- /dev/null +++ b/qiskit/utils/mitigation/circuits.py @@ -0,0 +1,237 @@ +# 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/fitters.py b/qiskit/utils/mitigation/fitters.py new file mode 100644 index 000000000000..0324fc5247d9 --- /dev/null +++ b/qiskit/utils/mitigation/fitters.py @@ -0,0 +1,436 @@ +# 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 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. + + 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 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. + + 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 7078a8a8970f..e4f55ad485bd 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}") @@ -169,10 +203,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. 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 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 new file mode 100644 index 000000000000..1ca0e3ee1c72 --- /dev/null +++ b/releasenotes/notes/ignis-mitigators-70492690cbcf99ca.yaml @@ -0,0 +1,29 @@ +--- +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 and usage + of these classes (or anything else in :mod:`qiskit.utils.mitigation`) + 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`` + 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 + 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. 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..953ae339e474 100644 --- a/test/python/algorithms/test_measure_error_mitigation.py +++ b/test/python/algorithms/test_measure_error_mitigation.py @@ -26,30 +26,35 @@ 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 - _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 ( + CompleteMeasFitter as CompleteMeasFitter_IG, + TensoredMeasFitter as TensoredMeasFitter_IG, + ) + + HAS_IGNIS = True +except ImportError: + HAS_IGNIS = False @ddt 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 @@ -105,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 @@ -177,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 @@ -212,6 +208,111 @@ 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""" + 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.assertWarnsRegex(DeprecationWarning, r".*ignis.*"): + self.assertRaisesRegex( + QiskitError, + "TensoredMeasFitter doesn't support subset_fitter.", + quantum_instance.execute, + [qc1, qc2], + ) + else: + # this should run smoothly + with self.assertWarnsRegex(DeprecationWarning, r".*ignis.*"): + 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_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""" + 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.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() + 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 index 58359d7e2d0b..d3078ad04152 100644 --- a/test/python/utils/__init__.py +++ b/test/python/utils/__init__.py @@ -10,4 +10,5 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. + """Qiskit utilities 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..2b2188de0e36 --- /dev/null +++ b/test/python/utils/mitigation/test_meas.py @@ -0,0 +1,698 @@ +# 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, +) +from qiskit.utils.mitigation._filters import 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 + ) + + 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 a8a0d1be26b5df097e7a21016fa21550a3c4efce Mon Sep 17 00:00:00 2001 From: Caroline Tornow <79633854+catornow@users.noreply.github.com> Date: Sat, 2 Oct 2021 00:40:14 +0200 Subject: [PATCH 6/8] EchoRZXWeylDecomposition Transpiler Pass (#6784) * Added new EchoRZXWeylDecomposition transpiler pass * Adapted two_qubit_decompose and calibration_creators and added tests * Black * Small modification * Adapted two_qubit_decompose to match latest upstream/main version * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> * Update qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> * Update qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> * Update qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> * Implemented Daniel's suggestions * Small modifications in calibration_creators * Style and lint errors * Rewrote TwoQubitWeylEchoRZX to remove lint error * Black * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py * Implemented additional tests * Black * Small modifications of the tests * Added release note * Added the class TwoQubitControlledUDecomposer to generalize TwoQubitWeylEchoRZX. * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> * Update qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> * Update qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> * Modified TwoQubitControlledUDecomposer to work for CPhaseGate, CRZGate; removed TwoQubitWeylEchoRZX * Added tests for TwoQubitControlledUDecomposer * Moved is_native to EchoRZXWeylDecomposition transpiler pass * Removed TwoQubitWeylEchoRZX tests (since this class does not exist anymore) * Small modification * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> * Fixed style and lint error * Removed ```node.type == "op"``` in EchoRZXWeylDecomposition transpiler pass to remove deprecation warning * Changed ```gate=node.op.name``` back to ```gate=node.op``` in CalibrationBuilder * Changed the argument of EchoRZXWeylDecomposition from inst_map to backend * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Lev Bishop <18673315+levbishop@users.noreply.github.com> * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Lev Bishop <18673315+levbishop@users.noreply.github.com> * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Lev Bishop <18673315+levbishop@users.noreply.github.com> * Update qiskit/quantum_info/synthesis/two_qubit_decompose.py Co-authored-by: Lev Bishop <18673315+levbishop@users.noreply.github.com> * Fixed bug and slightly modified correct decomposition check * Removed (raise-missing-from) pylint error * Adapted tests. * Black * Modified docstrings * Modified and added tests to also check the RZX gate angles Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Co-authored-by: Lev Bishop <18673315+levbishop@users.noreply.github.com> --- .../synthesis/two_qubit_decompose.py | 167 ++++++++++++- .../echo_rzx_weyl_decomposition.py | 127 ++++++++++ ...x-weyl-decomposition-ef72345a58bea9e0.yaml | 4 + test/python/quantum_info/test_synthesis.py | 30 +++ .../test_echo_rzx_weyl_decomposition.py | 232 ++++++++++++++++++ 5 files changed, 558 insertions(+), 2 deletions(-) create mode 100644 qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py create mode 100644 releasenotes/notes/echo-rzx-weyl-decomposition-ef72345a58bea9e0.yaml create mode 100644 test/python/transpiler/test_echo_rzx_weyl_decomposition.py diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 0d3d89cc64b9..2d209ecf97ed 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -28,7 +28,7 @@ import io import base64 import warnings -from typing import ClassVar, Optional +from typing import ClassVar, Optional, Type import logging @@ -36,7 +36,7 @@ import scipy.linalg as la from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.quantumcircuit import QuantumCircuit, Gate from qiskit.circuit.library.standard_gates import CXGate, RXGate, RYGate, RZGate from qiskit.exceptions import QiskitError from qiskit.quantum_info.operators import Operator @@ -556,6 +556,169 @@ def specialize(self): self.K2r = np.asarray(RYGate(k2rtheta)) @ np.asarray(RXGate(k2rlambda)) +class TwoQubitControlledUDecomposer: + """Decompose two-qubit unitary in terms of a desired U ~ Ud(α, 0, 0) ~ Ctrl-U gate + that is locally equivalent to an RXXGate.""" + + def __init__(self, rxx_equivalent_gate: Type[Gate]): + """Initialize the KAK decomposition. + + Args: + rxx_equivalent_gate: Gate that is locally equivalent to an RXXGate: + U ~ Ud(α, 0, 0) ~ Ctrl-U gate. + Raises: + QiskitError: If the gate is not locally equivalent to an RXXGate. + """ + atol = DEFAULT_ATOL + + scales, test_angles, scale = [], [0.2, 0.3, np.pi / 2], None + + for test_angle in test_angles: + # Check that gate takes a single angle parameter + try: + rxx_equivalent_gate(test_angle, label="foo") + except TypeError as _: + raise QiskitError("Equivalent gate needs to take exactly 1 angle parameter.") from _ + decomp = TwoQubitWeylDecomposition(rxx_equivalent_gate(test_angle)) + + circ = QuantumCircuit(2) + circ.rxx(test_angle, 0, 1) + decomposer_rxx = TwoQubitWeylControlledEquiv(Operator(circ).data) + + circ = QuantumCircuit(2) + circ.append(rxx_equivalent_gate(test_angle), qargs=[0, 1]) + decomposer_equiv = TwoQubitWeylControlledEquiv(Operator(circ).data) + + scale = decomposer_rxx.a / decomposer_equiv.a + + if ( + not isinstance(decomp, TwoQubitWeylControlledEquiv) + or abs(decomp.a * 2 - test_angle / scale) > atol + ): + raise QiskitError( + f"{rxx_equivalent_gate.__name__} is not equivalent to an RXXGate." + ) + + scales.append(scale) + + # Check that all three tested angles give the same scale + if not np.allclose(scales, [scale] * len(test_angles)): + raise QiskitError( + f"Cannot initialize {self.__class__.__name__}: with gate {rxx_equivalent_gate}. " + "Inconsistent scaling parameters in checks." + ) + + self.scale = scales[0] + + self.rxx_equivalent_gate = rxx_equivalent_gate + + def __call__(self, unitary, *, atol=DEFAULT_ATOL) -> QuantumCircuit: + """Returns the Weyl decomposition in circuit form. + + Note: atol ist passed to OneQubitEulerDecomposer. + """ + + # pylint: disable=attribute-defined-outside-init + self.decomposer = TwoQubitWeylDecomposition(unitary) + + oneq_decompose = OneQubitEulerDecomposer("ZYZ") + c1l, c1r, c2l, c2r = ( + oneq_decompose(k, atol=atol) + for k in ( + self.decomposer.K1l, + self.decomposer.K1r, + self.decomposer.K2l, + self.decomposer.K2r, + ) + ) + circ = QuantumCircuit(2, global_phase=self.decomposer.global_phase) + circ.compose(c2r, [0], inplace=True) + circ.compose(c2l, [1], inplace=True) + self._weyl_gate(circ) + circ.compose(c1r, [0], inplace=True) + circ.compose(c1l, [1], inplace=True) + return circ + + def _to_rxx_gate(self, angle: float): + """ + Takes an angle and returns the circuit equivalent to an RXXGate with the + RXX equivalent gate as the two-qubit unitary. + + Args: + angle: Rotation angle (in this case one of the Weyl parameters a, b, or c) + + Returns: + Circuit: Circuit equivalent to an RXXGate. + + Raises: + QiskitError: If the circuit is not equivalent to an RXXGate. + """ + + # The user-provided RXXGate equivalent gate may be locally equivalent to the RXXGate + # but with some scaling in the rotation angle. For example, RXXGate(angle) has Weyl + # parameters (angle, 0, 0) for angle in [0, pi/2] but the user provided gate, i.e. + # :code:`self.rxx_equivalent_gate(angle)` might produce the Weyl parameters + # (scale * angle, 0, 0) where scale != 1. This is the case for the CPhaseGate. + + circ = QuantumCircuit(2) + circ.append(self.rxx_equivalent_gate(self.scale * angle), qargs=[0, 1]) + decomposer_inv = TwoQubitWeylControlledEquiv(Operator(circ).data) + + oneq_decompose = OneQubitEulerDecomposer("ZYZ") + + # Express the RXXGate in terms of the user-provided RXXGate equivalent gate. + rxx_circ = QuantumCircuit(2, global_phase=-decomposer_inv.global_phase) + rxx_circ.compose(oneq_decompose(decomposer_inv.K2r).inverse(), inplace=True, qubits=[0]) + rxx_circ.compose(oneq_decompose(decomposer_inv.K2l).inverse(), inplace=True, qubits=[1]) + rxx_circ.compose(circ, inplace=True) + rxx_circ.compose(oneq_decompose(decomposer_inv.K1r).inverse(), inplace=True, qubits=[0]) + rxx_circ.compose(oneq_decompose(decomposer_inv.K1l).inverse(), inplace=True, qubits=[1]) + + return rxx_circ + + def _weyl_gate(self, circ: QuantumCircuit, atol=1.0e-13): + """Appends Ud(a, b, c) to the circuit.""" + + circ_rxx = self._to_rxx_gate(-2 * self.decomposer.a) + circ.compose(circ_rxx, inplace=True) + + # translate the RYYGate(b) into a circuit based on the desired Ctrl-U gate. + if abs(self.decomposer.b) > atol: + circ_ryy = QuantumCircuit(2) + circ_ryy.sdg(0) + circ_ryy.sdg(1) + circ_ryy.compose(self._to_rxx_gate(-2 * self.decomposer.b), inplace=True) + circ_ryy.s(0) + circ_ryy.s(1) + circ.compose(circ_ryy, inplace=True) + + # translate the RZZGate(c) into a circuit based on the desired Ctrl-U gate. + if abs(self.decomposer.c) > atol: + # Since the Weyl chamber is here defined as a > b > |c| we may have + # negative c. This will cause issues in _to_rxx_gate + # as TwoQubitWeylControlledEquiv will map (c, 0, 0) to (|c|, 0, 0). + # We therefore produce RZZGate(|c|) and append its inverse to the + # circuit if c < 0. + gamma, invert = -2 * self.decomposer.c, False + if gamma > 0: + gamma *= -1 + invert = True + + circ_rzz = QuantumCircuit(2) + circ_rzz.h(0) + circ_rzz.h(1) + circ_rzz.compose(self._to_rxx_gate(gamma), inplace=True) + circ_rzz.h(0) + circ_rzz.h(1) + + if invert: + circ.compose(circ_rzz.inverse(), inplace=True) + else: + circ.compose(circ_rzz, inplace=True) + + return circ + + class TwoQubitWeylMirrorControlledEquiv(TwoQubitWeylDecomposition): """U ~ Ud(𝜋/4, 𝜋/4, α) ~ SWAP . Ctrl-U diff --git a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py new file mode 100644 index 000000000000..76e0ecc13ac9 --- /dev/null +++ b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py @@ -0,0 +1,127 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 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. + +"""Weyl decomposition of two-qubit gates in terms of echoed cross-resonance gates.""" + +from typing import Tuple + +from qiskit import QuantumRegister +from qiskit.circuit.library.standard_gates import RZXGate, HGate, XGate + +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.layout import Layout + +from qiskit.dagcircuit import DAGCircuit +from qiskit.converters import circuit_to_dag + +from qiskit.providers import basebackend + +import qiskit.quantum_info as qi +from qiskit.quantum_info.synthesis.two_qubit_decompose import TwoQubitControlledUDecomposer + + +class EchoRZXWeylDecomposition(TransformationPass): + """Rewrite two-qubit gates using the Weyl decomposition. + + This transpiler pass rewrites two-qubit gates in terms of echoed cross-resonance gates according + to the Weyl decomposition. A two-qubit gate will be replaced with at most six non-echoed RZXGates. + Each pair of RZXGates forms an echoed RZXGate. + """ + + def __init__(self, backend: basebackend): + """EchoRZXWeylDecomposition pass.""" + self._inst_map = backend.defaults().instruction_schedule_map + super().__init__() + + def _is_native(self, qubit_pair: Tuple) -> bool: + """Return the direction of the qubit pair that is native, i.e. with the shortest schedule.""" + cx1 = self._inst_map.get("cx", qubit_pair) + cx2 = self._inst_map.get("cx", qubit_pair[::-1]) + return cx1.duration < cx2.duration + + @staticmethod + def _echo_rzx_dag(theta): + rzx_dag = DAGCircuit() + qr = QuantumRegister(2) + rzx_dag.add_qreg(qr) + rzx_dag.apply_operation_back(RZXGate(theta / 2), [qr[0], qr[1]], []) + rzx_dag.apply_operation_back(XGate(), [qr[0]], []) + rzx_dag.apply_operation_back(RZXGate(-theta / 2), [qr[0], qr[1]], []) + rzx_dag.apply_operation_back(XGate(), [qr[0]], []) + return rzx_dag + + @staticmethod + def _reverse_echo_rzx_dag(theta): + reverse_rzx_dag = DAGCircuit() + qr = QuantumRegister(2) + reverse_rzx_dag.add_qreg(qr) + reverse_rzx_dag.apply_operation_back(HGate(), [qr[0]], []) + reverse_rzx_dag.apply_operation_back(HGate(), [qr[1]], []) + reverse_rzx_dag.apply_operation_back(RZXGate(theta / 2), [qr[1], qr[0]], []) + reverse_rzx_dag.apply_operation_back(XGate(), [qr[1]], []) + reverse_rzx_dag.apply_operation_back(RZXGate(-theta / 2), [qr[1], qr[0]], []) + reverse_rzx_dag.apply_operation_back(XGate(), [qr[1]], []) + reverse_rzx_dag.apply_operation_back(HGate(), [qr[0]], []) + reverse_rzx_dag.apply_operation_back(HGate(), [qr[1]], []) + return reverse_rzx_dag + + def run(self, dag: DAGCircuit): + """Run the EchoRZXWeylDecomposition pass on `dag`. + + Rewrites two-qubit gates in an arbitrary circuit in terms of echoed cross-resonance + gates by computing the Weyl decomposition of the corresponding unitary. Modifies the + input dag. + + Args: + dag (DAGCircuit): DAG to rewrite. + + Returns: + DAGCircuit: The modified dag. + + Raises: + TranspilerError: If the circuit cannot be rewritten. + """ + + if len(dag.qregs) > 1: + raise TranspilerError( + "EchoRZXWeylDecomposition expects a single qreg input DAG," + f"but input DAG had qregs: {dag.qregs}." + ) + + trivial_layout = Layout.generate_trivial_layout(*dag.qregs.values()) + + decomposer = TwoQubitControlledUDecomposer(RZXGate) + + for node in dag.two_qubit_ops(): + + unitary = qi.Operator(node.op).data + dag_weyl = circuit_to_dag(decomposer(unitary)) + dag.substitute_node_with_dag(node, dag_weyl) + + for node in dag.two_qubit_ops(): + if node.name == "rzx": + control = node.qargs[0] + target = node.qargs[1] + + physical_q0 = trivial_layout[control] + physical_q1 = trivial_layout[target] + + is_native = self._is_native((physical_q0, physical_q1)) + + theta = node.op.params[0] + if is_native: + dag.substitute_node_with_dag(node, self._echo_rzx_dag(theta)) + else: + dag.substitute_node_with_dag(node, self._reverse_echo_rzx_dag(theta)) + + return dag diff --git a/releasenotes/notes/echo-rzx-weyl-decomposition-ef72345a58bea9e0.yaml b/releasenotes/notes/echo-rzx-weyl-decomposition-ef72345a58bea9e0.yaml new file mode 100644 index 000000000000..44fcb9695ea5 --- /dev/null +++ b/releasenotes/notes/echo-rzx-weyl-decomposition-ef72345a58bea9e0.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added a new transpiler pass :class:`qiskit.transpiler.passes.optimization.EchoRZXWeylDecomposition` that allows users to decompose an arbitrary two-qubit gate in terms of echoed RZX-gates by leveraging Cartan's decomposition. In combination with other transpiler passes this can be used to transpile arbitrary circuits to RZX-gate-based and pulse-efficient circuits that implement the same unitary. diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index ae59793b276e..c9d85a9a4290 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -37,7 +37,13 @@ CXGate, CZGate, iSwapGate, + SwapGate, RXXGate, + RYYGate, + RZZGate, + RZXGate, + CPhaseGate, + CRZGate, RXGate, RYGate, RZGate, @@ -60,6 +66,7 @@ TwoQubitWeylGeneral, two_qubit_cnot_decompose, TwoQubitBasisDecomposer, + TwoQubitControlledUDecomposer, Ud, decompose_two_qubit_product_gate, ) @@ -1302,6 +1309,29 @@ def test_approx_supercontrolled_decompose_phase_3_use_random(self, seed, delta=0 self.check_approx_decomposition(tgt_unitary, decomposer, num_basis_uses=3) +@ddt +class TestTwoQubitControlledUDecompose(CheckDecompositions): + """Test TwoQubitControlledUDecomposer() for exact decompositions and raised exceptions""" + + @combine(seed=range(10), name="seed_{seed}") + def test_correct_unitary(self, seed): + """Verify unitary for different gates in the decomposition""" + unitary = random_unitary(4, seed=seed) + for gate in [RXXGate, RYYGate, RZZGate, RZXGate, CPhaseGate, CRZGate]: + decomposer = TwoQubitControlledUDecomposer(gate) + circ = decomposer(unitary) + self.assertEqual(Operator(unitary), Operator(circ)) + + def test_not_rxx_equivalent(self): + """Test that an exception is raised if the gate is not equivalent to an RXXGate""" + gate = SwapGate + with self.assertRaises(QiskitError) as exc: + TwoQubitControlledUDecomposer(gate) + self.assertIn( + "Equivalent gate needs to take exactly 1 angle parameter.", exc.exception.message + ) + + class TestDecomposeProductRaises(QiskitTestCase): """Check that exceptions are raised when 2q matrix is not a product of 1q unitaries""" diff --git a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py new file mode 100644 index 000000000000..b9a38871bbad --- /dev/null +++ b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py @@ -0,0 +1,232 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test the EchoRZXWeylDecomposition pass""" + +import unittest +from math import pi +import numpy as np + +from qiskit import QuantumRegister, QuantumCircuit + +from qiskit.transpiler.passes.optimization.echo_rzx_weyl_decomposition import ( + EchoRZXWeylDecomposition, +) +from qiskit.converters import circuit_to_dag, dag_to_circuit +from qiskit.test import QiskitTestCase +from qiskit.test.mock import FakeParis + +import qiskit.quantum_info as qi + +from qiskit.quantum_info.synthesis.two_qubit_decompose import ( + TwoQubitWeylDecomposition, +) + + +class TestEchoRZXWeylDecomposition(QiskitTestCase): + """Tests the EchoRZXWeylDecomposition pass.""" + + def setUp(self): + super().setUp() + self.backend = FakeParis() + + def assertRZXgates(self, unitary_circuit, after): + """Check the number of rzx gates""" + alpha = TwoQubitWeylDecomposition(unitary_circuit).a + beta = TwoQubitWeylDecomposition(unitary_circuit).b + gamma = TwoQubitWeylDecomposition(unitary_circuit).c + + expected_rzx_number = 0 + if not alpha == 0: + expected_rzx_number += 2 + if not beta == 0: + expected_rzx_number += 2 + if not gamma == 0: + expected_rzx_number += 2 + + circuit_rzx_number = QuantumCircuit.count_ops(after)["rzx"] + + self.assertEqual(expected_rzx_number, circuit_rzx_number) + + @staticmethod + def count_gate_number(gate, circuit): + """Count the number of a specific gate type in a circuit""" + if gate not in QuantumCircuit.count_ops(circuit): + gate_number = 0 + else: + gate_number = QuantumCircuit.count_ops(circuit)[gate] + return gate_number + + def test_rzx_number_native_weyl_decomposition(self): + """Check the number of RZX gates for a hardware-native cx""" + qr = QuantumRegister(2, "qr") + circuit = QuantumCircuit(qr) + circuit.cx(qr[0], qr[1]) + + unitary_circuit = qi.Operator(circuit).data + + after = EchoRZXWeylDecomposition(self.backend)(circuit) + + unitary_after = qi.Operator(after).data + + self.assertTrue(np.allclose(unitary_circuit, unitary_after)) + + # check whether the after circuit has the correct number of rzx gates. + self.assertRZXgates(unitary_circuit, after) + + def test_h_number_non_native_weyl_decomposition_1(self): + """Check the number of added Hadamard gates for a native and non-native rzz gate""" + theta = pi / 11 + qr = QuantumRegister(2, "qr") + # rzz gate in native direction + circuit = QuantumCircuit(qr) + circuit.rzz(theta, qr[0], qr[1]) + + # rzz gate in non-native direction + circuit_non_native = QuantumCircuit(qr) + circuit_non_native.rzz(theta, qr[1], qr[0]) + + dag = circuit_to_dag(circuit) + pass_ = EchoRZXWeylDecomposition(self.backend) + after = dag_to_circuit(pass_.run(dag)) + + dag_non_native = circuit_to_dag(circuit_non_native) + pass_ = EchoRZXWeylDecomposition(self.backend) + after_non_native = dag_to_circuit(pass_.run(dag_non_native)) + + circuit_rzx_number = self.count_gate_number("rzx", after) + + circuit_h_number = self.count_gate_number("h", after) + circuit_non_native_h_number = self.count_gate_number("h", after_non_native) + + # for each pair of rzx gates four hadamard gates have to be added in + # the case of a non-hardware-native directed gate. + self.assertEqual( + (circuit_rzx_number / 2) * 4, circuit_non_native_h_number - circuit_h_number + ) + + def test_h_number_non_native_weyl_decomposition_2(self): + """Check the number of added Hadamard gates for a swap gate""" + qr = QuantumRegister(2, "qr") + # swap gate in native direction. + circuit = QuantumCircuit(qr) + circuit.swap(qr[0], qr[1]) + + # swap gate in non-native direction. + circuit_non_native = QuantumCircuit(qr) + circuit_non_native.swap(qr[1], qr[0]) + + dag = circuit_to_dag(circuit) + pass_ = EchoRZXWeylDecomposition(self.backend) + after = dag_to_circuit(pass_.run(dag)) + + dag_non_native = circuit_to_dag(circuit_non_native) + pass_ = EchoRZXWeylDecomposition(self.backend) + after_non_native = dag_to_circuit(pass_.run(dag_non_native)) + + circuit_rzx_number = self.count_gate_number("rzx", after) + + circuit_h_number = self.count_gate_number("h", after) + circuit_non_native_h_number = self.count_gate_number("h", after_non_native) + + # for each pair of rzx gates four hadamard gates have to be added in + # the case of a non-hardware-native directed gate. + self.assertEqual( + (circuit_rzx_number / 2) * 4, circuit_non_native_h_number - circuit_h_number + ) + + def test_weyl_decomposition_gate_angles(self): + """Check the number and angles of the RZX gates for different gates""" + thetas = [pi / 9, 2.1, -0.2] + + qr = QuantumRegister(2, "qr") + circuit_rxx = QuantumCircuit(qr) + circuit_rxx.rxx(thetas[0], qr[1], qr[0]) + + circuit_ryy = QuantumCircuit(qr) + circuit_ryy.ryy(thetas[1], qr[0], qr[1]) + + circuit_rzz = QuantumCircuit(qr) + circuit_rzz.rzz(thetas[2], qr[1], qr[0]) + + circuits = [circuit_rxx, circuit_ryy, circuit_rzz] + + for circuit in circuits: + + unitary_circuit = qi.Operator(circuit).data + + dag = circuit_to_dag(circuit) + pass_ = EchoRZXWeylDecomposition(self.backend) + after = dag_to_circuit(pass_.run(dag)) + dag_after = circuit_to_dag(after) + + unitary_after = qi.Operator(after).data + + # check whether the unitaries are equivalent. + self.assertTrue(np.allclose(unitary_circuit, unitary_after)) + + # check whether the after circuit has the correct number of rzx gates. + self.assertRZXgates(unitary_circuit, after) + + alpha = TwoQubitWeylDecomposition(unitary_circuit).a + + rzx_angles = [] + for node in dag_after.two_qubit_ops(): + if node.name == "rzx": + rzx_angle = node.op.params[0] + # check whether the absolute values of the RZX gate angles + # are equivalent to the corresponding Weyl parameter. + self.assertAlmostEqual(np.abs(rzx_angle), alpha) + rzx_angles.append(rzx_angle) + + # check whether the angles of every RZX gate pair of an echoed RZX gate + # have opposite signs. + for idx in range(1, len(rzx_angles), 2): + self.assertAlmostEqual(rzx_angles[idx - 1], -rzx_angles[idx]) + + def test_weyl_unitaries_random_circuit(self): + """Weyl decomposition for a random two-qubit circuit.""" + theta = pi / 9 + epsilon = 5 + delta = -1 + eta = 0.2 + qr = QuantumRegister(2, "qr") + circuit = QuantumCircuit(qr) + + # random two-qubit circuit. + circuit.rzx(theta, 0, 1) + circuit.rzz(epsilon, 0, 1) + circuit.rz(eta, 0) + circuit.swap(1, 0) + circuit.h(0) + circuit.rzz(delta, 1, 0) + circuit.swap(0, 1) + circuit.cx(1, 0) + circuit.swap(0, 1) + circuit.h(1) + circuit.rxx(theta, 0, 1) + circuit.ryy(theta, 1, 0) + circuit.ecr(0, 1) + + unitary_circuit = qi.Operator(circuit).data + + dag = circuit_to_dag(circuit) + pass_ = EchoRZXWeylDecomposition(self.backend) + after = dag_to_circuit(pass_.run(dag)) + + unitary_after = qi.Operator(after).data + + self.assertTrue(np.allclose(unitary_circuit, unitary_after)) + + +if __name__ == "__main__": + unittest.main() From f59a1fd4bca6c92683d9487c94db10e6c730ebfd Mon Sep 17 00:00:00 2001 From: ElePT <57907331+ElePT@users.noreply.github.com> Date: Tue, 5 Oct 2021 08:00:38 +0900 Subject: [PATCH 7/8] Small improvement QFT code (#6887) * Improve if * Refactor QFT num_qubits setter * remove inline typehint Co-authored-by: Julien Gacon Co-authored-by: Kevin Krsulich --- qiskit/circuit/library/basis_change/qft.py | 5 ++--- .../notes/refactor-set-qft-num-qubits-82e6df88448c2f22.yaml | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/refactor-set-qft-num-qubits-82e6df88448c2f22.yaml diff --git a/qiskit/circuit/library/basis_change/qft.py b/qiskit/circuit/library/basis_change/qft.py index 13d4f2272601..e9f839ce8fc9 100644 --- a/qiskit/circuit/library/basis_change/qft.py +++ b/qiskit/circuit/library/basis_change/qft.py @@ -130,10 +130,9 @@ def num_qubits(self, num_qubits: int) -> None: if num_qubits != self.num_qubits: self._invalidate() - if num_qubits: + self.qregs = [] + if num_qubits is not None and num_qubits > 0: self.qregs = [QuantumRegister(num_qubits, name="q")] - else: - self.qregs = [] @property def approximation_degree(self) -> int: diff --git a/releasenotes/notes/refactor-set-qft-num-qubits-82e6df88448c2f22.yaml b/releasenotes/notes/refactor-set-qft-num-qubits-82e6df88448c2f22.yaml new file mode 100644 index 000000000000..d71859b73897 --- /dev/null +++ b/releasenotes/notes/refactor-set-qft-num-qubits-82e6df88448c2f22.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + Refactored the QFT ``num_qubits`` setter to avoid potential problems when changing the number of qubits of the QFT circuit. \ No newline at end of file From c5d01662627bc7b20186e5f755f784dcef09a9ba Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 5 Oct 2021 15:44:20 +0300 Subject: [PATCH 8/8] first pass on adding CircuitElement mixin to Clifford --- examples/python/circuit_element_test.py | 72 +++++++++++++++ qiskit/circuit/__init__.py | 1 + qiskit/circuit/instructionset.py | 3 +- qiskit/circuit/quantumcircuit.py | 41 ++++++--- .../operators/symplectic/clifford.py | 89 ++++++++++++++++++- 5 files changed, 189 insertions(+), 17 deletions(-) create mode 100644 examples/python/circuit_element_test.py diff --git a/examples/python/circuit_element_test.py b/examples/python/circuit_element_test.py new file mode 100644 index 000000000000..543393c692f7 --- /dev/null +++ b/examples/python/circuit_element_test.py @@ -0,0 +1,72 @@ +from qiskit.circuit import QuantumRegister, QuantumCircuit +from qiskit.quantum_info.operators.symplectic import Clifford +from qiskit import transpile + +# This example runs through to the end +def experiment1(): + # create a new clifford (from a circuit) + qc = QuantumCircuit(3) + qc.s(0) + qc.cx(0, 1) + qc.h(1) + print(qc) + cliff = Clifford(qc) + print(cliff) + print(f"Created cliff of type {type(cliff)}") + + # append our clifford to another circuit + q2 = QuantumRegister(5, "q") + qc2 = QuantumCircuit(q2) + qc2.h(0) + qc2.h(1) + qc2.barrier() + qc2.append(cliff, [q2[0], q2[1], q2[4]], []) + qc2.barrier() + qc2.cx(3, 4) + + # draw the circuit (with our clifford inside) + print(qc2) + + # decompose the circuit + # (and only now decomposing our clifford) + qc3 = qc2.decompose() + + # draw the decomposed circuit + print(qc3) + + +# This example is still not fully working +def experiment2(): + # create a new clifford (from a circuit) + qc = QuantumCircuit(3) + qc.s(0) + qc.cx(0, 1) + qc.h(1) + print(qc) + cliff = Clifford(qc) + print(cliff) + print(f"Created cliff of type {type(cliff)}") + + # append our clifford to another circuit + q2 = QuantumRegister(5, "q") + qc2 = QuantumCircuit(q2) + qc2.h(0) + qc2.h(1) + qc2.barrier() + qc2.append(cliff, [q2[0], q2[1], q2[4]], []) + qc2.barrier() + qc2.cx(3, 4) + + # draw the circuit (with our clifford inside) + print(qc2) + + # transpile the circuit (work-in-progress) + # (and only now decomposing our clifford) + qc3 = transpile(qc2, basis_gates={'rz', 'x', 'sx', 'cx'}) + + # draw the decomposed circuit + print(qc3) + + +# main +experiment1() \ No newline at end of file diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index 0608765a1248..af306bb32b93 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -220,6 +220,7 @@ from .controlledgate import ControlledGate from .instruction import Instruction from .instructionset import InstructionSet +from .circuit_element import CircuitElement from .barrier import Barrier from .delay import Delay from .measure import Measure diff --git a/qiskit/circuit/instructionset.py b/qiskit/circuit/instructionset.py index 263800ddc7e2..bdec5450375a 100644 --- a/qiskit/circuit/instructionset.py +++ b/qiskit/circuit/instructionset.py @@ -16,6 +16,7 @@ from qiskit.circuit.exceptions import CircuitError from .instruction import Instruction from .classicalregister import Clbit +from .circuit_element import CircuitElement class InstructionSet: @@ -46,7 +47,7 @@ def __getitem__(self, i): def add(self, gate, qargs, cargs): """Add an instruction and its context (where it is attached).""" - if not isinstance(gate, Instruction): + if not isinstance(gate, CircuitElement): raise CircuitError("attempt to add non-Instruction" + " to InstructionSet") self.instructions.append(gate) self.qargs.append(qargs) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 2ca6e5ead696..c197f29a8f19 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -56,6 +56,7 @@ from .delay import Delay from .measure import Measure from .reset import Reset +from .circuit_element import CircuitElement try: import pygments @@ -1124,7 +1125,7 @@ def cbit_argument_conversion(self, clbit_representation: ClbitSpecifier) -> List def append( self, - instruction: Instruction, + instruction: CircuitElement, qargs: Optional[Sequence[QubitSpecifier]] = None, cargs: Optional[Sequence[ClbitSpecifier]] = None, ) -> InstructionSet: @@ -1144,18 +1145,30 @@ def append( CircuitError: if object passed is neither subclass nor an instance of Instruction """ # Convert input to instruction - if not isinstance(instruction, Instruction) and not hasattr(instruction, "to_instruction"): - if issubclass(instruction, Instruction): + + # + # + + # New behavior: for CircuitElements we do *not* call to_instruction() + if isinstance(instruction, CircuitElement): + pass + + # Old behavior (to sunset): on this very first pass, QuantumCircuit (and possibly some other classes) + # do not yet inherit from CircuitElement. For example, we still need to call to_instruction() to append + # one QuantumCircuit to another. + else: + if not isinstance(instruction, Instruction) and not hasattr(instruction, "to_instruction"): + if issubclass(instruction, Instruction): + raise CircuitError( + "Object is a subclass of Instruction, please add () to " + "pass an instance of this object." + ) + raise CircuitError( - "Object is a subclass of Instruction, please add () to " - "pass an instance of this object." + "Object to append must be an Instruction or have a to_instruction() method." ) - - raise CircuitError( - "Object to append must be an Instruction or have a to_instruction() method." - ) - if not isinstance(instruction, Instruction) and hasattr(instruction, "to_instruction"): - instruction = instruction.to_instruction() + if not isinstance(instruction, Instruction) and hasattr(instruction, "to_instruction"): + instruction = instruction.to_instruction() # Make copy of parameterized gate instances if hasattr(instruction, "params"): @@ -1172,7 +1185,7 @@ def append( return instructions def _append( - self, instruction: Instruction, qargs: Sequence[Qubit], cargs: Sequence[Clbit] + self, instruction: CircuitElement, qargs: Sequence[Qubit], cargs: Sequence[Clbit] ) -> Instruction: """Append an instruction to the end of the circuit, modifying the circuit in place. @@ -1189,8 +1202,8 @@ def _append( CircuitError: if the gate is of a different shape than the wires it is being attached to. """ - if not isinstance(instruction, Instruction): - raise CircuitError("object is not an Instruction.") + if not isinstance(instruction, CircuitElement): + raise CircuitError("object is not a CircuitElement.") # do some compatibility checks self._check_dups(qargs) diff --git a/qiskit/quantum_info/operators/symplectic/clifford.py b/qiskit/quantum_info/operators/symplectic/clifford.py index 13dd35acec0d..41361f9534d4 100644 --- a/qiskit/quantum_info/operators/symplectic/clifford.py +++ b/qiskit/quantum_info/operators/symplectic/clifford.py @@ -16,7 +16,7 @@ import numpy as np from qiskit.exceptions import QiskitError -from qiskit.circuit import QuantumCircuit, Instruction +from qiskit.circuit import QuantumCircuit, Instruction, CircuitElement from qiskit.circuit.library.standard_gates import IGate, XGate, YGate, ZGate, HGate, SGate from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.quantum_info.operators.operator import Operator @@ -27,7 +27,7 @@ from .clifford_circuits import _append_circuit -class Clifford(BaseOperator, AdjointMixin): +class Clifford(BaseOperator, AdjointMixin, CircuitElement): """An N-qubit unitary operator from the Clifford group. **Representation** @@ -101,6 +101,12 @@ class Clifford(BaseOperator, AdjointMixin): `arXiv:quant-ph/0406196 `_ """ + # hack! + # needed to avoid the error + # AttributeError: 'Clifford' object has no attribute '_directive' + # Caller: dagcircuit.py, line 1444: + _directive = False + def __array__(self, dtype=None): if dtype: return np.asarray(self.to_matrix(), dtype=dtype) @@ -137,6 +143,13 @@ def __init__(self, data, validate=True): # Initialize BaseOperator super().__init__(num_qubits=self._table.num_qubits) + # hack! + # needed to avoid the error + # AttributeError: 'Clifford' object has no attribute '_definition' + # caller clifford.pu, line 592 + self._definition = None + + def __repr__(self): return f"Clifford({repr(self.table)})" @@ -525,6 +538,78 @@ def _pad_with_identity(self, clifford, qargs): return padded + # These implement the required methods of the CircuitElement mixin + + @property + def name(self): + return 'clifford' + + @property + def num_params(self): + return 1 + + @property + def num_clbits(self): + return 0 + + @property + def params(self): + return (self._table,) + + # hack! + # needed to avoid the error + # AttributeError: 'Clifford' object has no attribute 'broadcast_arguments' + # Caller: quantumcircuit.py, line 1183 + # The function below is copied from Instruction class + + def broadcast_arguments(self, qargs, cargs): + """ + Validation of the arguments. + + Args: + qargs (List): List of quantum bit arguments. + cargs (List): List of classical bit arguments. + + Yields: + Tuple(List, List): A tuple with single arguments. + + Raises: + CircuitError: If the input is not valid. For example, the number of + arguments does not match the gate expectation. + """ + if len(qargs) != self.num_qubits: + raise CircuitError( + f"The amount of qubit arguments {len(qargs)} does not match" + f" the instruction expectation ({self.num_qubits})." + ) + + # [[q[0], q[1]], [c[0], c[1]]] -> [q[0], c[0]], [q[1], c[1]] + flat_qargs = [qarg for sublist in qargs for qarg in sublist] + flat_cargs = [carg for sublist in cargs for carg in sublist] + yield flat_qargs, flat_cargs + + # hack! + # needed to avoid the error + # 'Clifford' object has no attribute 'condition' + @property + def condition(self): + return None + + # we need to make Clifford object hashable + # will create a more intelligent hash + def __hash__(self): + return 0 + + # This is where Clifford decomposition code gets called + @property + def definition(self): + """Return definition in terms of other basic gates.""" + print(f"In Clifford::definition") + if self._definition is None: + print("Before to_instruction") + self._definition = self.to_circuit() + return self._definition + # Update docstrings for API docs generate_apidocs(Clifford)