Source code for qdk_chemistry.plugins.qiskit.circuit_executor

"""Qiskit Aer Simulator circuit executor for QDK/Chemistry.

This module provides a CircuitExecutor implementation that uses Qiskit Aer Simulator
to execute quantum circuits. It accepts QDK/Chemistry Circuit and QuantumErrorProfile
data classes and returns measurement bitstring results via CircuitExecutorData.
"""

# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from __future__ import annotations

from qiskit import transpile
from qiskit.transpiler import PassManager
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel

import qdk_chemistry.plugins.qiskit as qiskit_plugin
import qdk_chemistry.plugins.qiskit._interop.transpiler as _transpiler_module
from qdk_chemistry.algorithms.circuit_executor.base import CircuitExecutor
from qdk_chemistry.data import (
    Circuit,
    CircuitExecutorData,
    QuantumErrorProfile,
    Settings,
)
from qdk_chemistry.plugins.qiskit._interop.noise_model import (
    get_noise_model_from_profile,
)
from qdk_chemistry.utils import Logger

__all__: list[str] = ["QiskitAerSimulator", "QiskitAerSimulatorSettings"]


def _resolve_passes(names: list[str]) -> list:
    """Resolve pass names to instances using the transpiler module's __all__."""
    passes = []
    for name in names:
        if name not in _transpiler_module.__all__:
            raise ValueError(f"Unknown pass '{name}'. Available passes: {_transpiler_module.__all__}")
        passes.append(getattr(_transpiler_module, name)())
    return passes


[docs] class QiskitAerSimulatorSettings(Settings): """Settings for the Qiskit Aer Simulator circuit executor."""
[docs] def __init__(self) -> None: """Initialize Qiskit Aer Simulator settings.""" Logger.trace_entering() super().__init__() self._set_default("seed", "int", 42) self._set_default( "method", "string", "statevector", "Aer simulation method", [ "automatic", "statevector", "density_matrix", "stabilizer", "extended_stabilizer", "matrix_product_state", "unitary", "superop", "tensor_network", ], ) self._set_default("transpile_optimization_level", "int", 0) self._set_default("device_backend_name", "string", "", "Name of a fake device backend for noise modeling.") self._set_default( "pre_transpilation_passes", "vector<string>", [], "Pass names to apply before transpilation.", ) self._set_default( "post_transpilation_passes", "vector<string>", [], "Pass names to apply after transpilation.", )
[docs] class QiskitAerSimulator(CircuitExecutor): """Qiskit Aer Simulator circuit executor implementation."""
[docs] def __init__( self, method: str = "statevector", seed: int = 42, transpile_optimization_level: int = 0, ) -> None: """Initialize the Qiskit Aer Simulator circuit executor. Args: method: The simulation method to use. seed: The random seed for simulation reproducibility. transpile_optimization_level: The optimization level for transpilation. """ Logger.trace_entering() super().__init__() self._settings = QiskitAerSimulatorSettings() self._settings.set("seed", seed) self._settings.set("method", method) self._settings.set("transpile_optimization_level", transpile_optimization_level)
def _run_impl( self, circuit: Circuit, shots: int, noise: QuantumErrorProfile | None = None, ) -> CircuitExecutorData: """Execute the given quantum circuit using the Qiskit Aer Simulator. Args: circuit: The quantum circuit to execute. shots: The number of shots to execute the circuit. noise: Optional noise profile to apply during execution. **kwargs: Reserved for forward compatibility. Returns: CircuitExecutorData: Object containing the results of the circuit execution. """ Logger.trace_entering() meas_circuit = circuit.get_qiskit_circuit() Logger.debug("Qiskit QuantumCircuit loaded.") device_backend_name = self._settings.get("device_backend_name") or None pre_transpilation_passes = self._settings.get("pre_transpilation_passes") or None post_transpilation_passes = self._settings.get("post_transpilation_passes") or None if noise is not None and device_backend_name is not None: raise ValueError("Cannot specify both a noise model and a device backend. Please choose one or the other.") opt_level = self._settings.get("transpile_optimization_level") if pre_transpilation_passes: meas_circuit = PassManager( _resolve_passes(pre_transpilation_passes), ).run(meas_circuit) if device_backend_name is not None: if not qiskit_plugin.QDK_CHEMISTRY_HAS_QISKIT_IBM_RUNTIME: raise ImportError( "The fake_provider module from qiskit_ibm_runtime is required for device backend simulation. " "Install it with: pip install qiskit-ibm-runtime" ) import qiskit_ibm_runtime.fake_provider.backends as fake_backends # noqa: PLC0415 if not device_backend_name.startswith("fake_") or len(device_backend_name) <= len("fake_"): raise ValueError( f"device_backend_name must start with 'fake_' followed by a backend name" f" (got '{device_backend_name}')." ) # Look up the V2 fake backend class directly by name to avoid # instantiating FakeProviderForBackendV2 (which triggers # deprecation warnings from the plugin discovery catalog). parts = device_backend_name[len("fake_") :].split("_") class_name = "Fake" + "".join(p.capitalize() for p in parts) + "V2" backend_cls = getattr(fake_backends, class_name, None) if backend_cls is None: raise ValueError( f"Unknown device backend '{device_backend_name}' (tried class '{class_name}'). " "Use a valid qiskit-ibm-runtime fake backend name (e.g. 'fake_manila')." ) device_backend = backend_cls() backend = AerSimulator.from_backend(device_backend) backend.set_options( method=self._settings.get("method"), seed_simulator=self._settings.get("seed"), ) transpiled_circuit = transpile( meas_circuit, backend=device_backend, optimization_level=opt_level, ) else: noise_model = get_noise_model_from_profile(noise) if noise else None backend = AerSimulator( method=self._settings.get("method"), seed_simulator=self._settings.get("seed"), noise_model=noise_model, ) if noise_model: transpiled_circuit = transpile( meas_circuit, basis_gates=noise_model.basis_gates, optimization_level=opt_level, ) else: # Use qiskit_aer NoiseModel() default basis gates if no noise model is provided transpiled_circuit = transpile( meas_circuit, basis_gates=NoiseModel().basis_gates, optimization_level=opt_level, ) if post_transpilation_passes: transpiled_circuit = PassManager( _resolve_passes(post_transpilation_passes), ).run(transpiled_circuit) raw_results = backend.run(transpiled_circuit, shots=shots).result() counts = raw_results.get_counts() Logger.debug(f"Measurement results obtained: {counts}") return CircuitExecutorData( bitstring_counts=counts, total_shots=shots, executor=self.name(), executor_metadata=raw_results, )
[docs] def name(self) -> str: """Return the algorithm name as qiskit_aer_simulator.""" return "qiskit_aer_simulator"