Skip to content

Commit

Permalink
CR
Browse files Browse the repository at this point in the history
  • Loading branch information
SamFerracin committed Sep 26, 2024
1 parent 9a6105f commit f85035d
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 82 deletions.
95 changes: 62 additions & 33 deletions qiskit_ibm_runtime/debugger/debugger.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -66,25 +78,32 @@ 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"""
The noise model used by this debugger for the noisy simulations.
"""
return self._noise_model

def backend(self) -> Backend:
r"""
The backend used by this debugger.
"""
return self._backend

def simulate(
self,
pubs: Sequence[EstimatorPubLike],
Expand All @@ -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,
Expand All @@ -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",
Expand All @@ -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),
Expand All @@ -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}")'
54 changes: 5 additions & 49 deletions qiskit_ibm_runtime/debugger/debugger_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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__")

Expand Down Expand Up @@ -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,
# )
8 changes: 8 additions & 0 deletions test/unit/debugger/test_debugger_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down

0 comments on commit f85035d

Please sign in to comment.