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 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/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/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/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 2ca6e5ead696..6dc2e2593953 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 @@ -863,16 +864,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) @@ -1124,7 +1123,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 +1143,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 +1183,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 +1200,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/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/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) 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/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/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/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/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/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, 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/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/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/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 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). 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/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() 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()