From 0c631de8ca5447845dce5e1b7b352331bfa7ea61 Mon Sep 17 00:00:00 2001 From: Samuele Ferracin Date: Fri, 4 Oct 2024 10:01:56 -0400 Subject: [PATCH] Debugger API, phase I (#1950) * debugger * truediv * oops * debugger results and tests * simplified * more tests * tests * done * pylint * black * scalarlike * CR * Update qiskit_ibm_runtime/debugger/debugger.py Co-authored-by: Jessie Yu * Update qiskit_ibm_runtime/debugger/debugger.py Co-authored-by: Jessie Yu * CR * neat * test * lint * docs * docs * Update qiskit_ibm_runtime/debug_tools/neat.py * CR * removed redundant validation * simulate --> sim_ideal and sim_noisy * precision * precision2 * CR * reno --------- Co-authored-by: Jessie Yu --- qiskit_ibm_runtime/debug_tools/__init__.py | 16 + qiskit_ibm_runtime/debug_tools/neat.py | 277 ++++++++++++++++++ .../debug_tools/neat_results.py | 138 +++++++++ release-notes/unreleased/1950.feat.rst | 1 + test/unit/debug_tools/__init__.py | 13 + test/unit/debug_tools/test_neat.py | 151 ++++++++++ test/unit/debug_tools/test_neat_results.py | 150 ++++++++++ .../cliffordization/test_to_clifford.py | 33 +++ 8 files changed, 779 insertions(+) create mode 100644 qiskit_ibm_runtime/debug_tools/__init__.py create mode 100644 qiskit_ibm_runtime/debug_tools/neat.py create mode 100644 qiskit_ibm_runtime/debug_tools/neat_results.py create mode 100644 release-notes/unreleased/1950.feat.rst create mode 100644 test/unit/debug_tools/__init__.py create mode 100644 test/unit/debug_tools/test_neat.py create mode 100644 test/unit/debug_tools/test_neat_results.py diff --git a/qiskit_ibm_runtime/debug_tools/__init__.py b/qiskit_ibm_runtime/debug_tools/__init__.py new file mode 100644 index 000000000..7ebc2c6ef --- /dev/null +++ b/qiskit_ibm_runtime/debug_tools/__init__.py @@ -0,0 +1,16 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# 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. + +"""Functions and classes for debugging and analyzing qiskit-ibm-runtime jobs.""" + +from .neat import Neat +from .neat_results import NeatPubResult, NeatResult diff --git a/qiskit_ibm_runtime/debug_tools/neat.py b/qiskit_ibm_runtime/debug_tools/neat.py new file mode 100644 index 000000000..b9a790bd2 --- /dev/null +++ b/qiskit_ibm_runtime/debug_tools/neat.py @@ -0,0 +1,277 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# 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. + +"""A class to help understand the expected performance of estimator jobs.""" + +from __future__ import annotations +from typing import Optional, Sequence + +from qiskit.exceptions import QiskitError +from qiskit.transpiler.passmanager import PassManager +from qiskit.primitives.containers import EstimatorPubLike +from qiskit.primitives.containers.estimator_pub import EstimatorPub +from qiskit.providers import BackendV2 as Backend + +from qiskit_ibm_runtime.debug_tools.neat_results import NeatPubResult, NeatResult +from qiskit_ibm_runtime.transpiler.passes.cliffordization import ConvertISAToClifford + + +try: + from qiskit_aer.noise import NoiseModel + from qiskit_aer.primitives.estimator_v2 import EstimatorV2 as AerEstimator + + HAS_QISKIT_AER = True +except ImportError: + HAS_QISKIT_AER = False + + +class Neat: + r"""A class to help understand the expected performance of estimator jobs. + + The "Noisy Estimator Analyzer Tool" (or "NEAT") is a convenience tool that users of the + :class:`~.Estimator` primitive can employ to analyze and predict the performance of + their queries. Its simulate method uses ``qiskit-aer`` to simulate the estimation task + classically efficiently, either in ideal conditions or in the presence of noise. The + simulations' results can be compared with other simulation results or with primitive results + results to draw custom figures of merit. + + .. code::python + + # Initialize a Neat object + analyzer = Neat(backend) + + # Map arbitrary PUBs to Clifford PUBs + cliff_pubs = analyzer.to_clifford(pubs) + + # Calculate the expectation values in the absence of noise + r_ideal = analyzer.ideal_sim(cliff_pubs) + + # Calculate the expectation values in the presence of noise + r_noisy = analyzer.noisy_sim(cliff_pubs) + + # Calculate the expectation values for a different noise model + analyzer.noise_model = another_noise_model + another_r_noisy = analyzer.noisy_sim(cliff_pubs) + + # Run the Clifford PUBs on a QPU + r_qpu = estimator.run(cliff_pubs) + + # Calculate useful figures of merit using mathematical operators, for example the relative + # difference between experimental and noisy results, ... + rel_diff = abs(r_noisy[0] - r_qpu[0]) / r_noisy[0] + + # ... the signal-to-noise ratio between experimental and ideal results, ... + ratio = r_qpu[0] / r_ideal[0] + + # ... or the absolute difference between results obtained with different noise models + abs_diff = abs(r_noisy[0] - another_r_noisy[0]) + + Args: + backend: A backend. + noise_model: A noise model for the operations of the given backend. If ``None``, it + defaults to the noise model generated by :meth:`NoiseModel.from_backend`. + """ + + def __init__(self, backend: Backend, noise_model: Optional[NoiseModel] = None) -> None: + if not HAS_QISKIT_AER: + raise ValueError( + "Cannot initialize object of type 'Neat' since 'qiskit-aer' is not installed. " + "Install 'qiskit-aer' and try again." + ) + + self._backend = backend + self.noise_model = ( + noise_model + if noise_model is not None + else NoiseModel.from_backend(backend, thermal_relaxation=False) + ) + + @property + def noise_model(self) -> NoiseModel: + r""" + The noise model used by this analyzer tool for the noisy simulations. + """ + return self._noise_model + + @noise_model.setter + def noise_model(self, value: NoiseModel) -> NoiseModel: + """Sets a new noise model. + + Args: + value: A new noise model. + """ + self._noise_model = value + + def backend(self) -> Backend: + r""" + The backend used by this analyzer tool. + """ + return self._backend + + def _simulate( + self, + pubs: Sequence[EstimatorPubLike], + with_noise: bool, + cliffordize: bool, + seed_simulator: Optional[int], + precision: float = 0, + ) -> NeatResult: + r""" + Perform a noisy or noiseless simulation of the estimator task specified by ``pubs``. + + Args: + pubs: The PUBs specifying the estimation task of interest. + with_noise: Whether to perform an ideal, noiseless simulation (``False``) or a noisy + simulation (``True``). + cliffordize: Whether or not to automatically apply the + :class:`.~ConvertISAToClifford` transpiler pass to the given ``pubs`` before + performing the simulations. + seed_simulator: A seed for the simulator. + precision: The target precision for the estimates of each expectation value in the + returned results. + + Returns: + The results of the simulation. + """ + if cliffordize: + coerced_pubs = self.to_clifford(pubs) + else: + coerced_pubs = [EstimatorPub.coerce(p) for p in pubs] + + backend_options = { + "method": "stabilizer", + "noise_model": self.noise_model if with_noise else None, + "seed_simulator": seed_simulator, + } + estimator = AerEstimator( + options={"backend_options": backend_options, "default_precision": precision} + ) + + aer_job = estimator.run(coerced_pubs) + try: + aer_result = aer_job.result() + except QiskitError as err: + if "invalid parameters" in str(err): + raise ValueError( + "Couldn't run the simulation, likely because the given PUBs contain one or " + "more non-Clifford instructions. To fix, try setting ``cliffordize`` to " + "``True``." + ) from err + raise err + + pub_results = [NeatPubResult(r.data.evs) for r in aer_result] + return NeatResult(pub_results) + + def ideal_sim( + self, + pubs: Sequence[EstimatorPubLike], + cliffordize: bool = False, + seed_simulator: Optional[int] = None, + precision: float = 0, + ) -> NeatResult: + r""" + Perform an ideal, noiseless simulation of the estimator task specified by ``pubs``. + + This function uses ``qiskit-aer``'s ``Estimator`` class to simulate the estimation task + classically. + + .. note:: + To ensure scalability, every circuit in ``pubs`` is required to be a Clifford circuit, + so that it can be simulated efficiently regardless of its size. For estimation tasks + that involve non-Clifford circuits, the recommended workflow consists of mapping + the non-Clifford circuits to the nearest Clifford circuits using the + :class:`.~ConvertISAToClifford` transpiler pass, or equivalently, to use the Neat's + :meth:`to_clifford` convenience method. Alternatively, setting ``cliffordize`` to + ``True`` ensures that the :meth:`to_clifford` method is applied automatically to the + given ``pubs`` prior to the simulation. + + Args: + pubs: The PUBs specifying the estimation task of interest. + cliffordize: Whether or not to automatically apply the + :class:`.~ConvertISAToClifford` transpiler pass to the given ``pubs`` before + performing the simulations. + seed_simulator: A seed for the simulator. + precision: The target precision for the estimates of each expectation value in the + returned results. + + Returns: + The results of the simulation. + """ + return self._simulate(pubs, False, cliffordize, seed_simulator, precision) + + def noisy_sim( + self, + pubs: Sequence[EstimatorPubLike], + cliffordize: bool = False, + seed_simulator: Optional[int] = None, + precision: float = 0, + ) -> NeatResult: + r""" + Perform a noisy simulation of the estimator task specified by ``pubs``. + + This function uses ``qiskit-aer``'s ``Estimator`` class to simulate the estimation task + classically. + + .. note:: + To ensure scalability, every circuit in ``pubs`` is required to be a Clifford circuit, + so that it can be simulated efficiently regardless of its size. For estimation tasks + that involve non-Clifford circuits, the recommended workflow consists of mapping + the non-Clifford circuits to the nearest Clifford circuits using the + :class:`.~ConvertISAToClifford` transpiler pass, or equivalently, to use the Neat's + :meth:`to_clifford` convenience method. Alternatively, setting ``cliffordize`` to + ``True`` ensures that the :meth:`to_clifford` method is applied automatically to the + given ``pubs`` prior to the simulation. + + Args: + pubs: The PUBs specifying the estimation task of interest. + cliffordize: Whether or not to automatically apply the + :class:`.~ConvertISAToClifford` transpiler pass to the given ``pubs`` before + performing the simulations. + seed_simulator: A seed for the simulator. + precision: The target precision for the estimates of each expectation value in the + returned results. + + Returns: + The results of the simulation. + """ + return self._simulate(pubs, True, cliffordize, seed_simulator, precision) + + def to_clifford(self, pubs: Sequence[EstimatorPubLike]) -> list[EstimatorPub]: + r""" + Return the cliffordized version of the given ``pubs``. + + This convenience method runs the :class:`.~ConvertISAToClifford` transpiler pass on the + PUBs' circuits. + + Args: + pubs: The PUBs to turn into Clifford PUBs. + + Returns: + The Clifford PUBs. + """ + coerced_pubs = [] + for pub in pubs: + coerced_pub = EstimatorPub.coerce(pub) + coerced_pubs.append( + EstimatorPub( + PassManager([ConvertISAToClifford()]).run(coerced_pub.circuit), + coerced_pub.observables, + coerced_pub.parameter_values, + coerced_pub.precision, + False, + ) + ) + + return coerced_pubs + + def __repr__(self) -> str: + return f'Neat(backend="{self.backend().name}")' diff --git a/qiskit_ibm_runtime/debug_tools/neat_results.py b/qiskit_ibm_runtime/debug_tools/neat_results.py new file mode 100644 index 000000000..f4523a4b4 --- /dev/null +++ b/qiskit_ibm_runtime/debug_tools/neat_results.py @@ -0,0 +1,138 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# 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. + +""" A class to store Neat results.""" + +from __future__ import annotations + +from typing import Iterable, Union +from numpy.typing import ArrayLike +import numpy as np + +from qiskit.primitives.containers import PubResult, DataBin + +# Type aliases +NeatPubResultLike = Union["NeatPubResult", PubResult, DataBin] +ScalarLike = Union[int, float] + + +class NeatPubResult: + r"""A class to store the PUB results of :class:`.~Neat`. + + It allows performing mathematical operations (``+``, ``-``, ``*``, ``/``, ``abs``, and ``**``) + with other objects of type :class:`.~NeatPubResultLike` and with scalars. + + .. code::python + + from qiskit_ibm_runtime.debug_tools import NeatPubResult + + res = NeatPubResult([[1, 2], [3, 4]]) + + # this returns NeatPubResult([[3, 4], [5, 6]]) + res + 2 + + # this returns NeatPubResult([[3, 8], [15, 24]]) + res * (res + 2) + + Args: + vals: The values in this :class:`.~NeatPubResult`. + """ + + def __init__(self, vals: ArrayLike) -> None: + self._vals = np.array(vals, dtype=float) + + @property + def vals(self) -> np.ndarray: + r"""The values in this result.""" + return self._vals + + def _coerced_operation( + self, other: Union[ScalarLike, NeatPubResultLike], op_name: str + ) -> NeatPubResult: + r""" + Coerces ``other`` to a compatible format and applies ``op_name`` to ``self`` and ``other``. + """ + if not isinstance(other, (int, float)): + if isinstance(other, NeatPubResult): + other = other.vals + elif isinstance(other, PubResult): + other = other.data.evs + elif isinstance(other, DataBin): + try: + other = other.evs + except AttributeError: + raise ValueError( + f"Cannot apply operator '{op_name}' between 'NeatPubResult' and 'DataBin'" + " that has no attribute ``evs``." + ) + else: + raise ValueError( + f"Cannot apply operator '{op_name}' to objects of type 'NeatPubResult' and " + f"'{other.__class__}'." + ) + return NeatPubResult(getattr(self.vals, f"{op_name}")(other)) + + def __abs__(self) -> NeatPubResult: + return NeatPubResult(np.abs(self.vals)) + + def __add__(self, other: Union[ScalarLike, NeatPubResultLike]) -> NeatPubResult: + return self._coerced_operation(other, "__add__") + + def __mul__(self, other: Union[ScalarLike, NeatPubResultLike]) -> NeatPubResult: + return self._coerced_operation(other, "__mul__") + + def __sub__(self, other: Union[ScalarLike, NeatPubResultLike]) -> NeatPubResult: + return self._coerced_operation(other, "__sub__") + + def __truediv__(self, other: Union[ScalarLike, NeatPubResultLike]) -> NeatPubResult: + return self._coerced_operation(other, "__truediv__") + + def __radd__(self, other: Union[ScalarLike, NeatPubResultLike]) -> NeatPubResult: + return self._coerced_operation(other, "__radd__") + + def __rmul__(self, other: Union[ScalarLike, NeatPubResultLike]) -> NeatPubResult: + return self._coerced_operation(other, "__rmul__") + + def __rsub__(self, other: Union[ScalarLike, NeatPubResultLike]) -> NeatPubResult: + return self._coerced_operation(other, "__rsub__") + + def __rtruediv__(self, other: Union[ScalarLike, NeatPubResultLike]) -> NeatPubResult: + return self._coerced_operation(other, "__rtruediv__") + + def __pow__(self, p: ScalarLike) -> NeatPubResult: + return NeatPubResult(self._vals**p) + + def __repr__(self) -> str: + return f"NeatPubResult(vals={repr(self.vals)})" + + +class NeatResult: + """A container for multiple :class:`~.NeatPubResult` objects. + + Args: + pub_results: An iterable of :class:`~.NeatPubResult` objects. + """ + + def __init__(self, pub_results: Iterable[NeatPubResult]) -> None: + self._pub_results = list(pub_results) + + def __getitem__(self, index: int) -> NeatPubResult: + return self._pub_results[index] + + def __len__(self) -> int: + return len(self._pub_results) + + def __repr__(self) -> str: + return f"NeatResult({self._pub_results})" + + def __iter__(self) -> Iterable[NeatPubResult]: + return iter(self._pub_results) diff --git a/release-notes/unreleased/1950.feat.rst b/release-notes/unreleased/1950.feat.rst new file mode 100644 index 000000000..afb603496 --- /dev/null +++ b/release-notes/unreleased/1950.feat.rst @@ -0,0 +1 @@ +Added Noisy Estimator Analyzer Tool (or NEAT), a class to help understand the expected performance of estimator jobs. \ No newline at end of file diff --git a/test/unit/debug_tools/__init__.py b/test/unit/debug_tools/__init__.py new file mode 100644 index 000000000..3b997a1c6 --- /dev/null +++ b/test/unit/debug_tools/__init__.py @@ -0,0 +1,13 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Unit tests for the debug tools.""" diff --git a/test/unit/debug_tools/test_neat.py b/test/unit/debug_tools/test_neat.py new file mode 100644 index 000000000..61b8bee4f --- /dev/null +++ b/test/unit/debug_tools/test_neat.py @@ -0,0 +1,151 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# 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. + +"""Tests for Neat class.""" + +import numpy as np + +from qiskit_aer import AerSimulator +from qiskit_aer.noise import NoiseModel, depolarizing_error + +from qiskit import QuantumCircuit +from qiskit.quantum_info import SparsePauliOp +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager + +from qiskit_ibm_runtime.debug_tools import Neat, NeatResult + +from ...ibm_test_case import IBMTestCase + + +class TestNeat(IBMTestCase): + """Class for testing the Neat class.""" + + def setUp(self): + super().setUp() + + noise_strength = 0.05 + self.noise_model = NoiseModel() + self.noise_model.add_quantum_error(depolarizing_error(noise_strength, 2), ["cx"], [0, 1]) + self.noise_model.add_quantum_error(depolarizing_error(noise_strength, 2), ["cx"], [1, 0]) + self.noise_model.add_quantum_error(depolarizing_error(noise_strength, 2), ["cx"], [1, 2]) + self.backend = AerSimulator( + noise_model=self.noise_model, coupling_map=[[0, 1], [1, 0], [1, 2]] + ) + + pm = generate_preset_pass_manager(backend=self.backend, optimization_level=0) + + self.c1 = QuantumCircuit(2) + self.c1.h(0) + self.c1.cx(0, 1) + self.c1 = pm.run(self.c1) + self.obs1_xx = SparsePauliOp(["XX"]).apply_layout(self.c1.layout) + self.obs1_zi = SparsePauliOp(["ZI"]).apply_layout(self.c1.layout) + + self.c2 = QuantumCircuit(3) + self.c2.h(0) + self.c2.cx(0, 1) + self.c2.cx(1, 2) + self.c2 = pm.run(self.c2) + self.obs2_xxx = SparsePauliOp(["XXX"]).apply_layout(self.c2.layout) + self.obs2_zzz = SparsePauliOp(["ZZZ"]).apply_layout(self.c2.layout) + self.obs2_ziz = SparsePauliOp(["ZIZ"]).apply_layout(self.c2.layout) + + def test_ideal_sim(self): + r"""Test the ``ideal_sim`` method.""" + analyzer = Neat(self.backend) + + r1 = analyzer.ideal_sim([(self.c1, self.obs1_xx)]) + self.assertIsInstance(r1, NeatResult) + self.assertEqual(r1[0].vals, 1) + + r2 = analyzer.ideal_sim([(self.c1, [self.obs1_xx, self.obs1_zi])]) + self.assertIsInstance(r2, NeatResult) + self.assertListEqual(r2[0].vals.tolist(), [1, 0]) + + pubs3 = [ + (self.c1, [self.obs1_xx, self.obs1_zi]), + (self.c2, [self.obs2_xxx, self.obs2_zzz, self.obs2_ziz]), + ] + r3 = analyzer.ideal_sim(pubs3) + self.assertIsInstance(r3, NeatResult) + self.assertListEqual(r3[0].vals.tolist(), [1, 0]) + self.assertListEqual(r3[1].vals.tolist(), [1, 0, 1]) + + def test_noisy_sim(self): + r"""Test the ``noisy_sim`` method.""" + analyzer = Neat(self.backend, self.noise_model) + + r1 = analyzer.noisy_sim([(self.c1, self.obs1_xx)]) + self.assertIsInstance(r1, NeatResult) + self.assertListEqual(list(r1[0].vals.shape), []) + + r2 = analyzer.noisy_sim([(self.c1, [self.obs1_xx, self.obs1_zi])]) + self.assertIsInstance(r2, NeatResult) + self.assertListEqual(list(r2[0].vals.shape), [2]) + + pubs3 = [ + (self.c1, [self.obs1_xx, self.obs1_zi]), + (self.c2, [self.obs2_xxx, self.obs2_zzz, self.obs2_ziz]), + ] + r3 = analyzer.noisy_sim(pubs3) + self.assertIsInstance(r3, NeatResult) + self.assertListEqual(list(r3[0].vals.shape), [2]) + self.assertListEqual(list(r3[1].vals.shape), [3]) + + def test_non_clifford_error(self): + r""" + Tests that ``_simulate`` errors when pubs are not Clifford if ``cliffordize`` is ``False``. + """ + qc = QuantumCircuit(3) + qc.rz(0.02, 0) + pubs = [(qc, "ZZZ")] + + with self.assertRaisesRegex(ValueError, "Couldn't run"): + Neat(self.backend).ideal_sim(pubs) + + with self.assertRaisesRegex(ValueError, "Couldn't run."): + Neat(self.backend).noisy_sim(pubs) + + r1 = Neat(self.backend).ideal_sim(pubs, cliffordize=True) + self.assertIsInstance(r1, NeatResult) + self.assertEqual(r1[0].vals, 1) + + r2 = Neat(self.backend).noisy_sim(pubs, cliffordize=True) + self.assertIsInstance(r2, NeatResult) + self.assertEqual(r2[0].vals, 1) + + def test_to_clifford(self): + r"""Tests the ``to_clifford`` method.""" + qc = QuantumCircuit(2, 2) + qc.id(0) + qc.sx(0) + qc.barrier() + qc.measure(0, 1) + qc.rz(0, 0) + qc.rz(np.pi / 2 - 0.1, 0) + qc.rz(np.pi, 0) + qc.rz(3 * np.pi / 2 + 0.1, 1) + qc.cx(0, 1) + transformed = Neat(self.backend).to_clifford([(qc, "ZZ")])[0] + + expected = QuantumCircuit(2, 2) + expected.id(0) + expected.sx(0) + expected.barrier() + expected.measure(0, 1) + expected.rz(0, 0) + expected.rz(np.pi / 2, 0) + expected.rz(np.pi, 0) + expected.rz(3 * np.pi / 2, 1) + expected.cx(0, 1) + + self.assertEqual(transformed.circuit, expected) diff --git a/test/unit/debug_tools/test_neat_results.py b/test/unit/debug_tools/test_neat_results.py new file mode 100644 index 000000000..5945061c5 --- /dev/null +++ b/test/unit/debug_tools/test_neat_results.py @@ -0,0 +1,150 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# 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. + +"""Tests for result classes for Neat objects.""" + +import ddt + +from qiskit.primitives.containers import PubResult, DataBin + +from qiskit_ibm_runtime.debug_tools import NeatPubResult, NeatResult + +from ...ibm_test_case import IBMTestCase +from ...utils import combine + + +@ddt.ddt +class TestNeatPubResult(IBMTestCase): + """Class for testing the NeatPubResult class.""" + + def setUp(self) -> None: + super().setUp() + + result1 = NeatPubResult([1, 2, 3]) + result2 = NeatPubResult([[1, 2], [3, 4]]) + self.results = [result1, result2] + + databin1 = DataBin(evs=[4, 5, 6]) + databin2 = DataBin(evs=[[5, 6], [7, 8]]) + self.databins = [databin1, databin2] + + self.pub_results = [PubResult(databin1), PubResult(databin2)] + + @combine( + scalar=[2, 4.5], + idx=[0, 1], + op_name=["add", "mul", "sub", "truediv", "radd", "rmul", "rsub", "rtruediv"], + ) + def test_operations_with_scalarlike(self, scalar, idx, op_name): + r"""Test operations between ``NeatPubResult`` and ``ScalarLike`` objects.""" + result = self.results[idx] + + new_result = getattr(result, f"__{op_name}__")(scalar) + new_vals = getattr(result.vals, f"__{op_name}__")(scalar) + + self.assertListEqual(new_result.vals.tolist(), new_vals.tolist()) + + @combine( + idx=[0, 1], + op_name=["add", "mul", "sub", "truediv", "radd", "rmul", "rsub", "rtruediv"], + ) + def test_operations_with_debugger_result(self, idx, op_name): + r"""Test operations between two ``NeatPubResult`` objects.""" + result1 = self.results[idx] + result2 = 2 * result1 + + new_result = getattr(result1, f"__{op_name}__")(result2) + new_vals = getattr(result1.vals, f"__{op_name}__")(result2.vals) + + self.assertListEqual(new_result.vals.tolist(), new_vals.tolist()) + + @combine( + idx=[0, 1], + op_name=["add", "mul", "sub", "truediv", "radd", "rmul", "rsub", "rtruediv"], + ) + def test_operations_with_databins(self, idx, op_name): + r"""Test operations between ``NeatPubResult`` and ``DataBin`` objects.""" + result = self.results[idx] + databin = self.databins[idx] + + new_result = getattr(result, f"__{op_name}__")(databin) + new_vals = getattr(result.vals, f"__{op_name}__")(databin.evs) + + self.assertListEqual(new_result.vals.tolist(), new_vals.tolist()) + + @combine(op_name=["add", "mul", "sub", "truediv", "radd", "rmul", "rsub", "rtruediv"]) + def test_error_for_operations_with_databins(self, op_name): + r"""Test the errors for operations between ``NeatPubResult`` and ``DataBin``.""" + result = self.results[0] + databin = DataBin(wrong_kwarg=result.vals) + + with self.assertRaisesRegex(ValueError, f"Cannot apply operator '__{op_name}__'"): + getattr(result, f"__{op_name}__")(databin) + + @combine( + idx=[0, 1], + op_name=["add", "mul", "sub", "truediv", "radd", "rmul", "rsub", "rtruediv"], + ) + def test_operations_with_pub_results(self, idx, op_name): + r"""Test operations between ``NeatPubResult`` and ``PubResult`` objects.""" + result = self.results[idx] + pub_result = self.pub_results[idx] + + new_result = getattr(result, f"__{op_name}__")(pub_result) + new_vals = getattr(result.vals, f"__{op_name}__")(pub_result.data.evs) + + self.assertListEqual(new_result.vals.tolist(), new_vals.tolist()) + + def test_abs(self): + r"""Test the ``abs`` operator.""" + result = NeatPubResult([-1, 0, 1]) + new_result = abs(result) + new_vals = abs(result.vals) + + self.assertListEqual(new_result.vals.tolist(), new_vals.tolist()) + + @ddt.data(2, 4.5) + def test_pow(self, p): + r"""Test the ``pow`` operator.""" + result = self.results[0] + new_result = result**p + new_vals = result.vals**p + + self.assertListEqual(new_result.vals.tolist(), new_vals.tolist()) + + +@ddt.ddt +class TestNeatResult(IBMTestCase): + """Class for testing the NeatResult class.""" + + def setUp(self) -> None: + super().setUp() + + pub_result1 = NeatPubResult([1, 2, 3]) + pub_result2 = NeatPubResult([[1, 2], [3, 4]]) + self.pub_results = [pub_result1, pub_result2] + + def test_getitem(self): + r"""Test the ``__getitem__`` method of NeatResult.""" + r = NeatResult(self.pub_results) + + self.assertListEqual(r[0].vals.tolist(), self.pub_results[0].vals.tolist()) + self.assertListEqual(r[1].vals.tolist(), self.pub_results[1].vals.tolist()) + + def test_len(self): + r"""Test the ``__len__`` method of NeatResult.""" + self.assertEqual(len(NeatResult(self.pub_results)), 2) + + def test_iter(self): + r"""Test the ``__iter__`` method of NeatResult.""" + for i, j in zip(NeatResult(self.pub_results), self.pub_results): + self.assertListEqual(i.vals.tolist(), j.vals.tolist()) diff --git a/test/unit/transpiler/passes/cliffordization/test_to_clifford.py b/test/unit/transpiler/passes/cliffordization/test_to_clifford.py index e436db9ff..48dd7042d 100644 --- a/test/unit/transpiler/passes/cliffordization/test_to_clifford.py +++ b/test/unit/transpiler/passes/cliffordization/test_to_clifford.py @@ -45,6 +45,39 @@ def test_clifford_isa_circuit(self): self.assertEqual(qc, transformed) + def test_non_clifford_isa_circuits(self): + """Test the pass on a non-Clifford circuit with ISA gates.""" + qc = QuantumCircuit(2, 2) + qc.id(0) + qc.sx(0) + qc.barrier() + qc.measure(0, 1) + qc.rz(0, 0) + qc.rz(np.pi / 2 - 0.1, 0) + qc.rz(np.pi, 0) + qc.rz(3 * np.pi / 2 + 0.1, 1) + qc.cx(0, 1) + qc.cz(0, 1) + qc.ecr(0, 1) + + pm = PassManager([ConvertISAToClifford()]) + transformed = pm.run(qc) + + expected = QuantumCircuit(2, 2) + expected.id(0) + expected.sx(0) + expected.barrier() + expected.measure(0, 1) + expected.rz(0, 0) + expected.rz(np.pi / 2, 0) + expected.rz(np.pi, 0) + expected.rz(3 * np.pi / 2, 1) + expected.cx(0, 1) + expected.cz(0, 1) + expected.ecr(0, 1) + + self.assertEqual(transformed, expected) + def test_error_clifford_non_isa_circuit(self): """Test that the pass errors when run on a Clifford circuit with non-ISA gates.""" qc = QuantumCircuit(2)