import inspect
from typing import Any, Dict, Optional, Literal, Type
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel
from qiskit_ibm_runtime.fake_provider import fake_provider
from qiskit_ibm_runtime.fake_provider.fake_backend import FakeBackendV2
from qiskit.quantum_info import Statevector, Operator, DensityMatrix
import re
def _camel_to_snake(name: str) -> str:
return re.sub(r'(?<!^)(?=[A-Z])', '_', name).lower()
def _discover_fake_backends() -> Dict[str, Type[FakeBackendV2]]:
registry: Dict[str, Type[FakeBackendV2]] = {}
for _, cls in inspect.getmembers(fake_provider, inspect.isclass):
if issubclass(cls, FakeBackendV2) and cls is not FakeBackendV2:
# FakeLimaV2 -> fake_lima
base_name = cls.__name__.replace("V2", "")
name = _camel_to_snake(base_name)
registry[name] = cls
return registry
[docs]
class ShotBasedBackend:
"""
Execute ``QuantumCircuit`` on a Qiskit backend (Qiskit 2.0 compatible)
and return measurement outcomes via multiple shots.
This class supports both ideal and noisy backends. For noisy backends, users can
either use the default noise model derived from the fake backend or provide a custom
noise model.
.. note::
Regarding the scenario of quantum software testing, using this backend indicates
that the test technique under evaluation is expected to work in a real (future)
quantum computers. In this situation, quantum measurements are necessary to
extract classical information from quantum states.
"""
def __init__(
self,
qc: QuantumCircuit,
backend: str,
noise_model: Optional[NoiseModel],
shots: int,
if_noisy: bool,
random_seed: int | None = None
) -> None:
"""
Args:
qc: The quantum circuit to be executed.
backend: Backend name for circuit execution. Supported backends include:
- ``"ideal"`` -> ideal AerSimulator
- ``"fake_xxx"`` -> AerSimulator with FakeBackend
See the complete list of fake backends:
:doc:`available_fake_backends`.
noise_model: Optional custom NoiseModel
shots: The number of quantum measurements per circuit execution.
if_noisy: Whether to enable noise when using fake backend. Ignored for "ideal"
backend.
random_seed: Optional random seed for simulator to ensure reproducibility.
Raises:
TypeError: If `backend` is not a string.
.. note::
For `"ideal"` backend, `noise_model` is ignored and `if_noisy` is always False.
"""
if not isinstance(backend, str):
raise TypeError("backend must be a string")
self._qc = qc
self._shots = shots
self._backend_name = backend.lower()
self._noise_model = noise_model
self._whether_noisy = if_noisy
self._supported_fake_backends = sorted(self._FAKE_BACKEND_REGISTRY.keys())
self._seed = random_seed
# Obtain after processing
self.transpiled_cut: QuantumCircuit
self.backend_method: Optional[str] = None
# Run the circuit upon initialization
self.output = self._run()
_FAKE_BACKEND_REGISTRY = _discover_fake_backends()
@property
def backend(self): # Return the backend instance
if self._backend_name == "ideal":
return AerSimulator(seed_simulator=self._seed)
elif self._backend_name.startswith("fake_"):
return self._build_fake_backend()
else:
raise ValueError(
f"Unsupported backend string: {self._backend_name}\n"
f'Use "ideal" or "fake_xxx".'
)
@property
def whether_noisy(self) -> bool:
if self._backend_name == "ideal":
return False
else:
return self._whether_noisy
@property
def noise_model(self) -> Optional[NoiseModel]:
if self._backend_name == "ideal":
return None
else:
return self._noise_model
def _build_fake_backend(self) -> AerSimulator:
"""
Build AerSimulator from fake backend name using internal state.
"""
if self._backend_name not in self._FAKE_BACKEND_REGISTRY:
available = ", ".join(sorted(self._FAKE_BACKEND_REGISTRY))
raise ValueError(
f"Unknown fake backend: {self._backend_name}\n"
f"Available fake backends: {available}"
)
fake_backend = self._FAKE_BACKEND_REGISTRY[self._backend_name]()
if self.whether_noisy:
noise_model = self.noise_model or NoiseModel.from_backend(fake_backend)
else:
noise_model = None
return AerSimulator(
noise_model=noise_model,
coupling_map=fake_backend.coupling_map,
basis_gates=fake_backend.operation_names,
seed_simulator=self._seed
)
def _run(self) -> dict[str, int]:
"""
Execute a quantum circuit and return measurement counts.
"""
# Transpile the circuit for the backend to match the backend's basis gates.
# Otherwise, results may be invalid.
# Optimization level 0 to avoid changing the circuit structure too much and
# preserve the intended behavior.
transpiled_qc = transpile(self._qc, self.backend, optimization_level=0)
# Update transpiled circuit metrics
self.transpiled_cut = transpiled_qc
# Return measurement counts
result = self.backend.run(transpiled_qc, shots=self._shots).result()
self.backend_method = result.results[0].metadata.get("method", "unknown")
return result.get_counts()
[docs]
class ShotIndependentBackend:
"""
Execute ``QuantumCircuit`` on a Qiskit backend (Qiskit 2.0 compatible)
and return exact mathematical objects for analytical / exact quantum simulation.
Therefore, this class is only suitable for ideal simulation scenarios without
quantum measurements.
.. note::
Regarding the scenario of quantum software testing, using this backend indicates
that the test technique under evaluation is expected to merely work in classical
computers but not necessarily in real quantum computers. This holds true in current
NISQ era where quantum noise is inevitable and the accessiblity to real quantum
hardware is limited. To this end, this class provides a convenient way to check
the correctness of quantum software in an ideal setting, which can be easily
achieved by classical simulation.
"""
def __init__(self, qc: QuantumCircuit, backend: str) -> None:
"""
Args:
qc: The QuantumCircuit to be executed.
backend: Backend name for circuit execution. Supported backends:
- ``"statevector"``
- ``"unitary"``
- ``"density_matrix"``
Raises:
TypeError: If `backend` is not a string.
"""
if not isinstance(backend, str):
raise TypeError("backend must be a string")
self._backend_name = backend.lower()
self._qc = qc
# Execute upon initialization
self.output = self._run()
_SUPPORTED_BACKENDS = {
"statevector",
"unitary",
"density_matrix",
}
@property
def backend(self) -> Optional[AerSimulator]:
if self._backend_name not in self._SUPPORTED_BACKENDS:
available = ", ".join(sorted(self._SUPPORTED_BACKENDS))
raise ValueError(
f"Unsupported backend: {self._backend_name}\n"
f"Available shot-independent backends: {available}"
)
# Aer is only needed for state-based simulations
if self._backend_name == "unitary":
return None # unitary does not need Aer
else:
return AerSimulator(method=self._backend_name)
def _run(self) -> Any:
# Unitary: purely analytical
if self._backend_name == "unitary":
return Operator(self._qc)
# Copy circuit to avoid side effects
qc_exec = self._qc.copy()
# Analytic backends
if self._backend_name == "statevector":
return Statevector.from_instruction(qc_exec)
elif self._backend_name == "density_matrix":
return DensityMatrix.from_instruction(qc_exec)
else:
raise RuntimeError(f"Unsupported backend execution path: {self._backend_name}")