diff --git a/qiskit_ibm_runtime/debugger/debugger.py b/qiskit_ibm_runtime/debugger/debugger.py index a608bbe12..e7e4a0422 100644 --- a/qiskit_ibm_runtime/debugger/debugger.py +++ b/qiskit_ibm_runtime/debugger/debugger.py @@ -14,9 +14,7 @@ from __future__ import annotations from typing import Optional, Sequence, List - -from qiskit_aer.noise import NoiseModel -from qiskit_aer.primitives.estimator_v2 import EstimatorV2 as AerEstimator +import numpy as np from qiskit.transpiler.passmanager import PassManager from qiskit.primitives.containers import EstimatorPubLike @@ -28,36 +26,50 @@ from qiskit_ibm_runtime.utils import validate_estimator_pubs, validate_isa_circuits +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 + + def _validate_pubs( backend: Backend, pubs: List[EstimatorPub], validate_clifford: bool = True ) -> None: - r"""Validates a list PUBs by running the :meth:`.~validate_estimator_pubs` and - :meth:`.~validate_isa_circuits` methods, and optionally, by checking if the PUBs - are Clifford. + r"""Validate a list of PUBs for use inside the debugger. + + This funtion runs the :meth:`.~validate_estimator_pubs` and :meth:`.~validate_isa_circuits` + checks. Optionally, it also validates that every PUB's circuit is a Clifford circuit. Args: backend: A backend. pubs: A set of PUBs. validate_clifford: Whether or not to validate that the PUB's circuit do not contain non-Clifford gates. + + Raises: + ValueError: If the PUBs contain non-Clifford circuits. """ validate_estimator_pubs(pubs) validate_isa_circuits([pub.circuit for pub in pubs], backend.target) if validate_clifford: for pub in pubs: - cliff_circ = PassManager([ConvertISAToClifford()]).run(pub.circuit) - if pub.circuit != cliff_circ: - raise ValueError( - "Given ``pubs`` contain a non-Clifford circuit. To fix, consider using the " - "``ConvertISAToClifford`` pass to map your circuits to the nearest Clifford" - " circuits, then try again." - ) + for instr in pub.circuit: + op = instr.operation + # ISA circuits contain only one type of non-Clifford gate, namely RZ + if op.name == "rz" and op.params[0] not in [0, np.pi / 2, np.pi, 3 * np.pi / 2]: + raise ValueError( + "Given ``pubs`` contain non-Clifford circuits. To fix, consider using the " + ":meth:`Debugger.to_clifford` method to map the PUBs' circuits to Clifford" + " circuits, then try again." + ) class Debugger: - r"""A class that users of the Estimator primitive can use to understand the expected - performance of their queries. + r"""A class to help understanding the expected performance of Estimator jobs. Args: backend: A backend. @@ -66,18 +78,19 @@ class Debugger: """ def __init__(self, backend: Backend, noise_model: Optional[NoiseModel] = None) -> None: + if not HAS_QISKIT_AER: + raise ValueError( + "Cannot initialize object of type 'Debugger' since 'qiskit-aer' is not installed. " + "Install 'qiskit-aer' and try again." + ) + self._backend = backend - self._noise_model = noise_model or NoiseModel.from_backend( - backend, thermal_relaxation=False + self._noise_model = ( + noise_model + if noise_model is not None + else NoiseModel.from_backend(backend, thermal_relaxation=False) ) - @property - def backend(self) -> Backend: - r""" - The backend used by this debugger. - """ - return self._backend - @property def noise_model(self) -> NoiseModel: r""" @@ -85,6 +98,12 @@ def noise_model(self) -> NoiseModel: """ return self._noise_model + def backend(self) -> Backend: + r""" + The backend used by this debugger. + """ + return self._backend + def simulate( self, pubs: Sequence[EstimatorPubLike], @@ -105,14 +124,21 @@ def simulate( # Initialize a debugger debugger = Debugger(backend) + # Map arbitrary PUBs to Clifford PUBs + cliff_pubs = debugger.to_clifford(pubs) + # Calculate the expectation values in the absence of noise - r_ideal = debugger.simulate(pubs, with_noise=False) + r_ideal = debugger.simulate(cliff_pubs, with_noise=False) # Calculate the expectation values in the presence of noise - r_noisy = debugger.simulate(pubs, with_noise=True) + r_noisy = debugger.simulate(cliff_pubs, with_noise=True) - # Calculate the ratio between the two - signal_to_noise_ratio = r_noisy[0]/r_ideal[0] + # Run the Clifford PUBs on a QPU + r_qpu = estimator.run(cliff_pubs) + + # Calculate useful figures of merit using mathematical operators + signal_to_noise_ratio = r_noisy[0] / r_ideal[0] + rel_diff = abs(r_ideal[0] - r_qpu.data[0]) / r_ideal[0] .. note:: To ensure scalability, every circuit in ``pubs`` is required to be a Clifford circuit, @@ -129,7 +155,7 @@ def simulate( seed_simulator: A seed for the simulator. default_precision: The default precision used to run the ideal and noisy simulations. """ - _validate_pubs(self.backend, coerced_pubs := [EstimatorPub.coerce(pub) for pub in pubs]) + _validate_pubs(self.backend(), coerced_pubs := [EstimatorPub.coerce(pub) for pub in pubs]) backend_options = { "method": "stabilizer", @@ -143,17 +169,20 @@ def simulate( def to_clifford(self, pubs: Sequence[EstimatorPubLike]) -> list[EstimatorPub]: r""" - A convenience method that returns the cliffordized version of the given ``pubs``, obtained - by run the :class:`.~ConvertISAToClifford` transpiler pass on the PUBs' circuits. + 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: - _validate_pubs(self.backend, [coerced_pub := EstimatorPub.coerce(pub)], False) + _validate_pubs(self.backend(), [coerced_pub := EstimatorPub.coerce(pub)], False) coerced_pubs.append( EstimatorPub( PassManager([ConvertISAToClifford()]).run(coerced_pub.circuit), @@ -167,4 +196,4 @@ def to_clifford(self, pubs: Sequence[EstimatorPubLike]) -> list[EstimatorPub]: return coerced_pubs def __repr__(self) -> str: - return f'Debugger(backend="{self.backend.name}")' + return f'Debugger(backend="{self.backend().name}")' diff --git a/qiskit_ibm_runtime/debugger/debugger_results.py b/qiskit_ibm_runtime/debugger/debugger_results.py index fdb3e3f82..9696a10cf 100644 --- a/qiskit_ibm_runtime/debugger/debugger_results.py +++ b/qiskit_ibm_runtime/debugger/debugger_results.py @@ -29,8 +29,8 @@ class DebuggerResult: r"""A class to store the results of the ``Debugger``. - It allows performing mathematical operations (``+``, ``-``, ``*``, ``/``, and ``**``) with - other objects of type :class:`.~DebuggerResultLike` and with scalars. + It allows performing mathematical operations (``+``, ``-``, ``*``, ``/``, ``abs``, and ``**``) + with other objects of type :class:`.~DebuggerResultLike` and with scalars. .. code::python @@ -82,6 +82,9 @@ def _coerced_operation( ) return DebuggerResult(getattr(self.vals, f"{op_name}")(other)) + def __abs__(self) -> DebuggerResult: + return DebuggerResult(np.abs(self.vals)) + def __add__(self, other: Union[ScalarLike, DebuggerResultLike]) -> DebuggerResult: return self._coerced_operation(other, "__add__") @@ -111,50 +114,3 @@ def __pow__(self, p: ScalarLike) -> DebuggerResult: def __repr__(self) -> str: return f"DebuggerResult(vals={repr(self.vals)})" - - -###### -# Alternative implementation where the dunder methods are added to DebuggerResult dynamically at -# import time. -# Pros: -# - Adding a new dunder method is as easy as increasing the list of supported operations. -# Cons: -# - May be harder to read and understand. - -# SUPPORTED_OPERATIONS = ["add", "mul", "sub", "truediv", "radd", "rmul", "rsub", "rtruediv"] - -# # Initialize -# for op_name in SUPPORTED_OPERATIONS: - -# def _coerced_operation( -# this: DebuggerResult, other: Union[ScalarLike, DebuggerResult], op_name: str = op_name -# ) -> DebuggerResult: -# r""" -# Coerces ``other`` to a compatible format and applies ``__op_name__`` to ``self`` and -# ``other``. -# """ -# if not isinstance(other, ScalarLike): -# if isinstance(other, DebuggerResult): -# 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 'DebuggerResult' and" -# "'DataBin' that has no attribute ``evs``." -# ) -# else: -# raise ValueError( -# f"Cannot apply operator {'__{op_name}__'} to objects of type " -# f"'DebuggerResult' and '{other.__class__}'." -# ) -# return DebuggerResult(getattr(this.vals, f"__{op_name}__")(other)) - -# setattr( -# DebuggerResult, -# f"__{op_name}__", -# _coerced_operation, -# ) diff --git a/test/unit/debugger/test_debugger_results.py b/test/unit/debugger/test_debugger_results.py index 9aee666c8..8a6a3039d 100644 --- a/test/unit/debugger/test_debugger_results.py +++ b/test/unit/debugger/test_debugger_results.py @@ -104,6 +104,14 @@ def test_operations_with_pub_results(self, idx, op_name): self.assertListEqual(new_result.vals.tolist(), new_vals.tolist()) + def test_abs(self): + r"""Test the ``abs`` operator.""" + result = DebuggerResult([-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."""