Source code for qolumbina.utils.backend_execution

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}")