Source code for qc_executor.qiskit.qiskit_executor

"""Qiskit backend executor supporting local simulation and IBM Quantum hardware."""

from __future__ import annotations

import logging
from typing import List, Literal, Tuple

import numpy as np
from qiskit.primitives import (
    StatevectorEstimator,
    StatevectorSampler,
)
from qiskit.providers import Backend
from qiskit.quantum_info import Statevector

from qc_executor.base.circuit_base import QuantumCircuitBase
from qc_executor.base.executor_base import ExecutorBase
from qc_executor.base.operator_base import QuantumOperatorBase
from qc_executor.qiskit.optree import OpTreeDerivative, OpTreeEvaluate
from qc_executor.qiskit.optree.optree import (
    OpTreeCircuit,
    OpTreeList,
    OpTreeNodeBase,
    OpTreeOperator,
)
from qc_executor.qiskit.qiskit_circuit import QiskitCircuit
from qc_executor.qiskit.qiskit_operator import QiskitOperator
from qc_executor.utils.qiskit_compat import (
    QISKIT_RUNTIME_AVAILABLE,
    QISKIT_RUNTIME_SMALLER_0_21,
    QISKIT_RUNTIME_SMALLER_0_23,
    QISKIT_RUNTIME_SMALLER_0_28,
    QISKIT_SMALLER_1_2,
    QISKIT_SMALLER_2_0,
)

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Version-gated Qiskit primitive imports (local simulators + base classes)
# ---------------------------------------------------------------------------
# pylint: disable=import-error,no-name-in-module,ungrouped-imports,too-few-public-methods

if QISKIT_SMALLER_1_2:
    from qiskit.circuit import ParameterExpression as ParameterVectorElement
    from qiskit.primitives import (
        BackendEstimator,
        BackendSampler,
    )
    from qiskit.primitives import BaseEstimator as BaseEstimatorV1
    from qiskit.primitives import BaseSampler as BaseSamplerV1

    class BaseEstimatorV2:
        """Dummy BaseEstimatorV2 for Qiskit < 1.0 compat."""

    class BaseSamplerV2:
        """Dummy BaseSamplerV2 for Qiskit < 1.0 compat."""

elif QISKIT_SMALLER_2_0:
    from qiskit.circuit import ParameterExpression as ParameterVectorElement
    from qiskit.primitives import BackendEstimatorV2 as BackendEstimator
    from qiskit.primitives import BackendSamplerV2 as BackendSampler
    from qiskit.primitives import (
        BaseEstimatorV1,
        BaseEstimatorV2,
        BaseSamplerV1,
        BaseSamplerV2,
    )
else:
    from qiskit.circuit import ParameterVectorElement
    from qiskit.primitives import BackendEstimatorV2 as BackendEstimator
    from qiskit.primitives import BackendSamplerV2 as BackendSampler
    from qiskit.primitives import (
        BaseEstimatorV1,
        BaseEstimatorV2,
        BaseSamplerV1,
        BaseSamplerV2,
    )


# Runtime primitive base classes — used for type annotation and isinstance checks.
# Defined as dummies when qiskit-ibm-runtime is not installed.
if QISKIT_RUNTIME_AVAILABLE:
    if QISKIT_RUNTIME_SMALLER_0_21:
        from qiskit_ibm_runtime import Estimator as RuntimeEstimatorV1
        from qiskit_ibm_runtime import Sampler as RuntimeSamplerV1

        class RuntimeEstimatorV2:
            """Dummy RuntimeEstimatorV2 for runtime < 0.21."""

        class RuntimeSamplerV2:
            """Dummy RuntimeSamplerV2 for runtime < 0.21."""

    elif QISKIT_RUNTIME_SMALLER_0_28:
        from qiskit_ibm_runtime import EstimatorV1 as RuntimeEstimatorV1
        from qiskit_ibm_runtime import EstimatorV2 as RuntimeEstimatorV2
        from qiskit_ibm_runtime import SamplerV1 as RuntimeSamplerV1
        from qiskit_ibm_runtime import SamplerV2 as RuntimeSamplerV2
    else:
        from qiskit_ibm_runtime import Estimator as RuntimeEstimatorV2
        from qiskit_ibm_runtime import Sampler as RuntimeSamplerV2

        class RuntimeEstimatorV1:
            """Dummy RuntimeEstimatorV1 for runtime >= 0.28."""

        class RuntimeSamplerV1:
            """Dummy RuntimeSamplerV1 for runtime >= 0.28."""

else:

    class RuntimeEstimatorV1:
        """Dummy RuntimeEstimatorV1 — qiskit-ibm-runtime not installed."""

    class RuntimeEstimatorV2:
        """Dummy RuntimeEstimatorV2 — qiskit-ibm-runtime not installed."""

    class RuntimeSamplerV1:
        """Dummy RuntimeSamplerV1 — qiskit-ibm-runtime not installed."""

    class RuntimeSamplerV2:
        """Dummy RuntimeSamplerV2 — qiskit-ibm-runtime not installed."""


# Session and Batch: real imports when runtime is available, dummies otherwise.
if QISKIT_RUNTIME_AVAILABLE:
    from qiskit_ibm_runtime import Batch, Session
else:

    class Session:
        """Dummy Session — qiskit-ibm-runtime not installed."""

        def close(self) -> None:
            """No-op stub."""

        def status(self) -> str:
            """No-op stub."""

    class Batch:
        """Dummy Batch — qiskit-ibm-runtime not installed."""

        def close(self) -> None:
            """No-op stub."""


# pylint: enable=import-error,no-name-in-module,ungrouped-imports,too-few-public-methods


def _reverse_qubit_ordering(statevector: np.ndarray) -> np.ndarray:
    """Reorder statevector amplitudes from Qiskit little-endian to big-endian.

    Qiskit indexes amplitude *i* with q[0] as the LSB, while PennyLane and
    Qulacs index it with q[0] as the MSB.  The conversion is a permutation
    that reverses the bit-pattern of each index.

    Example for 2 qubits::

        Qiskit index 1 = 0b01 → reversed = 0b10 = 2  (q[0]=1, q[1]=0)
        Qiskit index 2 = 0b10 → reversed = 0b01 = 1  (q[0]=0, q[1]=1)
    """
    n = int(np.log2(len(statevector)))
    perm = [int(format(i, f"0{n}b")[::-1], 2) for i in range(len(statevector))]
    return statevector[perm]


def _reverse_bitstring(bitstring: str) -> str:
    """Reverse a Qiskit measurement bitstring to big-endian qubit ordering.

    Qiskit writes bitstrings as ``q[n-1]...q[1]q[0]`` (q[0] rightmost).
    PennyLane / Qulacs write ``q[0]q[1]...q[n-1]`` (q[0] leftmost).
    """
    return bitstring[::-1]


def _convert_counts_endianness(counts: dict) -> dict:
    """Return a new counts dict with all bitstrings converted to big-endian."""
    result: dict = {}
    for bitstring, count in counts.items():
        key = _reverse_bitstring(bitstring)
        result[key] = result.get(key, 0) + count
    return result


# ---------------------------------------------------------------------------
# Lazy-loaded helpers for optional dependencies
# ---------------------------------------------------------------------------


def _load_aer_simulator():
    try:
        from qiskit_aer import AerSimulator  # pylint: disable=import-outside-toplevel
    except ImportError as e:
        raise ImportError(
            "qiskit-aer is required for 'backend=\"aer\"' and for shot-based "
            "sampling with 'backend=\"statevector\"'. Install with: "
            "pip install qc-executor[qiskit-full]"
        ) from e
    return AerSimulator


def _check_runtime_available():
    """Guard: raise :class:`ImportError` when *qiskit-ibm-runtime* is missing."""
    if not QISKIT_RUNTIME_AVAILABLE:
        raise ImportError(
            "qiskit-ibm-runtime is required for IBM backend support. "
            "Install with: pip install qc-executor[qiskit-full]"
        )


# pylint: disable=import-outside-toplevel,import-error,no-name-in-module,redefined-outer-name,reimported


def _load_runtime_primitives_v1():
    """Return ``(RuntimeEstimatorV1, RuntimeSamplerV1)`` for *qiskit-ibm-runtime < 0.21*."""
    _check_runtime_available()
    from qiskit_ibm_runtime import Estimator as RuntimeEstimatorV1
    from qiskit_ibm_runtime import Sampler as RuntimeSamplerV1

    return RuntimeEstimatorV1, RuntimeSamplerV1


def _load_runtime_primitives_v2():
    """Return ``(RuntimeEstimatorV2, RuntimeSamplerV2)`` from *qiskit-ibm-runtime*.

    * ``0.21 – 0.27``: explicit ``EstimatorV2`` / ``SamplerV2`` exports.
    * ``>= 0.28``: ``Estimator`` / ``Sampler`` **are** the V2 primitives.
    """
    _check_runtime_available()
    if QISKIT_RUNTIME_SMALLER_0_28:
        from qiskit_ibm_runtime import EstimatorV2 as RuntimeEstimatorV2
        from qiskit_ibm_runtime import SamplerV2 as RuntimeSamplerV2
    else:
        from qiskit_ibm_runtime import Estimator as RuntimeEstimatorV2
        from qiskit_ibm_runtime import Sampler as RuntimeSamplerV2
    return RuntimeEstimatorV2, RuntimeSamplerV2


def _load_runtime_options_v1():
    """Return the ``Options`` class for V1 runtime primitives (< 0.21)."""
    _check_runtime_available()
    from qiskit_ibm_runtime.options import Options

    return Options


def _load_runtime_session():
    """Return the ``Session`` class from *qiskit-ibm-runtime*."""
    _check_runtime_available()
    from qiskit_ibm_runtime import Session

    return Session


def _load_runtime_batch():
    """Return the ``Batch`` class from *qiskit-ibm-runtime*."""
    _check_runtime_available()
    from qiskit_ibm_runtime import Batch

    return Batch


def _is_backend_instance(obj) -> bool:
    """Return *True* if *obj* is a Qiskit ``Backend`` (V1 or V2) instance."""
    try:
        from qiskit.providers import Backend

        return isinstance(obj, Backend)
    except ImportError:
        return False


def _is_session_or_batch_instance(obj) -> bool:
    """Return *True* if *obj* is a ``qiskit_ibm_runtime.Session`` or ``Batch``."""
    if not QISKIT_RUNTIME_AVAILABLE:
        return False
    try:
        from qiskit_ibm_runtime import Session

        if isinstance(obj, Session):
            return True
    except ImportError:
        pass
    try:
        from qiskit_ibm_runtime import Batch

        if isinstance(obj, Batch):
            return True
    except ImportError:
        pass
    return False


# pylint: enable=import-outside-toplevel,import-error,no-name-in-module,redefined-outer-name,reimported


def _resolve_backend_from_session_or_batch(session_or_batch):
    """Best-effort backend extraction from runtime Session/Batch.

    Supports both direct backend objects and backend-name strings returned by
    some runtime versions via ``backend()``.
    """
    backend = getattr(session_or_batch, "_backend", None)
    if _is_backend_instance(backend):
        return backend

    backend_value = None
    if hasattr(session_or_batch, "backend") and callable(getattr(session_or_batch, "backend")):
        try:
            backend_value = session_or_batch.backend()
        except Exception:  # pylint: disable=broad-exception-caught
            backend_value = None

    if _is_backend_instance(backend_value):
        return backend_value

    if isinstance(backend_value, str):
        service = getattr(session_or_batch, "service", None)
        if service is not None and hasattr(service, "backend"):
            try:
                resolved = service.backend(backend_value)
                if _is_backend_instance(resolved):
                    return resolved
            except Exception:  # pylint: disable=broad-exception-caught
                return None

    return None


def _is_primitive_instance(obj) -> Tuple[bool, bool]:
    """Detect whether *obj* is a Qiskit primitive."""
    # Qiskit base classes (covers BackendEstimator/Sampler and StatevectorEstimator/Sampler)
    if isinstance(obj, (BaseSamplerV1, BaseSamplerV2)):
        return (True, True)
    if isinstance(obj, (BaseEstimatorV1, BaseEstimatorV2)):
        return (True, False)

    # Qiskit concrete statevector primitives
    if isinstance(obj, StatevectorSampler):
        return (True, True)
    if isinstance(obj, StatevectorEstimator):
        return (True, False)

    # IBM Runtime primitives — may NOT inherit from Qiskit base classes
    # (especially runtime >= 0.28 where Estimator/Sampler are the V2 primitives)
    if isinstance(obj, (RuntimeSamplerV1, RuntimeSamplerV2)):
        return (True, True)
    if isinstance(obj, (RuntimeEstimatorV1, RuntimeEstimatorV2)):
        return (True, False)

    return (False, False)


def _classify_backend(backend) -> Tuple[bool, bool, bool]:
    """Classify backend properties in a single pass.

    Returns
    -------
    (remote, ibm_quantum, ibm_fake) : Tuple[bool, bool, bool]
        ``remote`` is *True* for real remote backends.
        ``ibm_quantum`` is *True* only for real IBM Quantum hardware.
        ``ibm_fake`` is *True* only for IBM fake backends.
    """
    # 1. Real IBM hardware
    try:
        from qiskit_ibm_runtime import (  # pylint: disable=import-outside-toplevel,import-error
            IBMBackend,
        )

        if isinstance(backend, IBMBackend):
            return (True, True, False)
    except ImportError:
        pass

    # 2. IBM fake backend
    try:
        from qiskit_ibm_runtime.fake_provider.fake_backend import (  # pylint: disable=import-outside-toplevel,import-error
            FakeBackendV2,
        )

        if isinstance(backend, FakeBackendV2):
            return (False, False, True)
    except ImportError:
        pass

    # 3. Generic local fake backend
    try:
        from qiskit.providers.fake_provider import (  # pylint: disable=import-outside-toplevel
            GenericBackendV2,
        )

        if isinstance(backend, GenericBackendV2):
            return (False, False, False)
    except ImportError:
        pass

    # 4. Fallback: string matching for unrecognised third-party backends
    backend_str = str(backend).lower()
    if "ibm" in backend_str:
        is_fake = "fake" in backend_str
        logger.warning(
            "Backend type %r not recognised via isinstance checks; "
            "falling back to string matching. Consider filing a bug report.",
            type(backend).__name__,
        )
        return (not is_fake, not is_fake, is_fake)

    return (False, False, False)


[docs] class QiskitExecutor(ExecutorBase): """Class for executing Qiskit circuits. Supports local simulation (``"statevector"``, ``"aer"``) **and** execution on real IBM Quantum hardware or noise-aware fake backends via `qiskit-ibm-runtime <https://github.com/Qiskit/qiskit-ibm-runtime>`_. The ``backend`` parameter is the single entry point and accepts all supported configurations: * ``"statevector"`` / ``"aer"`` — local simulation string shortcuts. * A :class:`~qiskit.providers.Backend` / ``BackendV2`` instance (e.g. from ``QiskitRuntimeService`` or ``fake_provider``). * A ``qiskit_ibm_runtime.Session`` or ``Batch`` — ownership is transferred to the executor, which closes it on exit. * A pre-configured Qiskit primitive (``BaseSamplerV2`` / ``BaseEstimatorV2`` or their V1 equivalents) — injected directly; the missing counterpart primitive is created automatically when possible. Context-manager use is **strongly recommended** for real IBM backends to guarantee that sessions are properly closed:: with QiskitExecutor(backend=ibm_backend, execution_mode="session") as exe: result = exe.expectation_value(circuit, observable, theta=params) Args: backend: Backend to use for execution. Accepts: ``"statevector"`` (default) or ``"aer"`` string shortcuts, a Qiskit :class:`~qiskit.providers.Backend` instance (IBM hardware or fake), a ``qiskit_ibm_runtime.Session`` / ``Batch``, or a pre-configured Qiskit primitive (``BaseSamplerV1/V2`` / ``BaseEstimatorV1/V2``). shots (int | None, optional): Number of shots for sampling. seed (int | None, optional): Random seed for reproducibility. log_file (str | None, optional): Path to the log file. log_level (str, optional): Logging level. caching (bool | None, optional): Whether to use in-memory caching. cache_dir (str, optional): Directory for caching. max_cache_size (int | None, optional): Maximum number of entries kept in each in-memory cache. execution_mode (str, optional): ``"job"`` (default), ``"session"``, or ``"batch"``. Only relevant for real IBM Quantum backends. Use ``"session"`` for iterative algorithms (VQE, QAOA) and ``"batch"`` for independent parallel jobs. options (dict | None, optional): Options forwarded to IBM Runtime primitives (e.g. ``{"resilience_level": 1}``). Ignored for local backends. """ _native_circuit_class = QiskitCircuit _native_operator_class = QiskitOperator def __init__( self, backend: ( str | Backend | Session | Batch | BaseEstimatorV1 | BaseSamplerV1 | BaseEstimatorV2 | BaseSamplerV2 ) = "statevector", shots: int | None = None, seed: int | None = None, log_file: str | None = None, log_level: str = "WARNING", caching: bool | None = None, cache_dir: str = "cache", max_cache_size: int | None = None, execution_mode: Literal["job", "session", "batch"] = "job", options: dict | None = None, ): super().__init__( shots=shots, seed=seed, log_file=log_file, log_level=log_level, caching=caching, cache_dir=cache_dir, max_cache_size=max_cache_size, ) # Internal state for IBM backend support self._session = None self._inside_context_manager: bool = False self._remote_backend: bool = False self._ibm_quantum_backend: bool = False self._execution_mode = execution_mode self._options = options # Safe defaults — overwritten in the relevant branches below self._runtime_primitives_version: str = "v2" self._sampler_uses_v1_api: bool = QISKIT_SMALLER_1_2 self._isa_transpile: bool = False # ── 1. Direct primitive injection via backend ────────────────────── # User passes a pre-configured Sampler or Estimator directly as the # backend. The missing counterpart primitive is auto-created. is_primitive, is_sampler = _is_primitive_instance(backend) if is_primitive and options is not None: raise ValueError( "Ambiguous initialization: 'options' cannot be combined with injected " "primitives. Configure the primitive objects directly." ) if (is_primitive or _is_session_or_batch_instance(backend)) and execution_mode != "job": raise ValueError( "Ambiguous initialization: 'execution_mode' applies only to backend-based " "initialization and must remain 'job' when passing a Session/Batch/primitive." ) if is_primitive: self._backend = None self._session = None mode = getattr(backend, "_mode", None) if _is_backend_instance(mode): self._backend = mode elif _is_session_or_batch_instance(mode): self._session = mode if is_sampler: self._sampler = backend logger.info( "QiskitExecutor initialised with user-provided Sampler (%s).", type(backend).__name__, ) else: self._estimator = backend logger.info( "QiskitExecutor initialised with user-provided Estimator (%s).", type(backend).__name__, ) # Extract backend and session from runtime primitives so that # _classify_backend, ISA transpilation, and session awareness # work correctly even when primitives are injected directly. # This mirrors the approach used in the sQUlearn Executor. if isinstance(backend, (RuntimeEstimatorV2, RuntimeSamplerV2)): self._backend = self._backend or getattr(backend, "_backend", None) # Runtime >= 0.23 uses _mode; older versions use _session. self._session = self._session or getattr(backend, "_session", None) elif isinstance(backend, (RuntimeEstimatorV1, RuntimeSamplerV1)): self._backend = self._backend or getattr(backend, "_backend", None) self._session = self._session or getattr(backend, "_session", None) elif hasattr(backend, "backend"): # BackendEstimatorV2 / BackendSamplerV2 expose .backend as a property self._backend = self._backend or backend.backend elif hasattr(backend, "_backend"): # BackendEstimatorV1 / BackendSamplerV1 use ._backend self._backend = self._backend or backend._backend if self._backend is not None: self._remote_backend, self._ibm_quantum_backend, _ = _classify_backend( self._backend ) self._isa_transpile = self._ibm_quantum_backend # Determine runtime primitive generation for counterpart creation. self._runtime_primitives_version = ( "v1" if isinstance(backend, (BaseEstimatorV1, BaseSamplerV1)) else "v2" ) # Auto-create missing counterpart primitive from same context. if is_sampler: if isinstance(backend, StatevectorSampler): self._estimator = StatevectorEstimator() elif isinstance(backend, (RuntimeSamplerV1, RuntimeSamplerV2)): self._estimator = self._create_runtime_estimator() elif self._backend is not None: self._estimator = BackendEstimator(backend=self._backend) else: # No context available — leave estimator unset; the user # may only need the sampler. self._estimator = None else: if isinstance(backend, StatevectorEstimator): self._sampler = StatevectorSampler() elif isinstance(backend, (RuntimeEstimatorV1, RuntimeEstimatorV2)): self._sampler = self._create_runtime_sampler() elif self._backend is not None: self._sampler = BackendSampler(backend=self._backend) else: # No context available — leave sampler unset; the user # may only need the estimator. self._sampler = None # V1 vs V2 sampler API detection should be based on sampler instance. self._sampler_uses_v1_api = isinstance(self._sampler, BaseSamplerV1) # ── 2. Injected Session / Batch ──────────────────────────────────── # Ownership is intentionally transferred to the executor: close_session() # will close even externally created objects. elif _is_session_or_batch_instance(backend): _check_runtime_available() self._session = backend # Retrieve the target backend from the session for ISA transpilation self._backend = _resolve_backend_from_session_or_batch(backend) if self._backend is None: self._backend = None logger.warning( "Could not retrieve backend from %s; ISA transpilation will be skipped.", type(backend).__name__, ) if self._backend is not None: self._remote_backend, self._ibm_quantum_backend, _ = _classify_backend( self._backend ) else: self._remote_backend = False self._ibm_quantum_backend = False self._isa_transpile = self._backend is not None self._runtime_primitives_version = "v1" if QISKIT_RUNTIME_SMALLER_0_21 else "v2" self._estimator = self._create_runtime_estimator() self._sampler = self._create_runtime_sampler() self._sampler_uses_v1_api = self._runtime_primitives_version == "v1" logger.info( "QiskitExecutor attached to injected %s (ownership transferred).", type(backend).__name__, ) # ── 3. Local simulator backends (string shortcuts) ──────────────── elif isinstance(backend, str): if backend == "statevector": if shots is None: # Exact statevector mode — no Aer required self._estimator = StatevectorEstimator() self._sampler = StatevectorSampler() self._backend = None else: # Shot-based statevector via Aer aer_simulator_cls = _load_aer_simulator() self._backend = aer_simulator_cls(method="statevector") self._estimator = BackendEstimator(backend=self._backend) self._sampler = BackendSampler(backend=self._backend) elif backend == "aer": aer_simulator_cls = _load_aer_simulator() self._backend = aer_simulator_cls() self._estimator = BackendEstimator(backend=self._backend) self._sampler = BackendSampler(backend=self._backend) else: raise ValueError( f"Unknown backend string: {backend!r}. " "Use 'statevector', 'aer', or pass a Backend / Session instance." ) # ── 4. Backend object (IBMBackend / FakeBackend / any BackendV2) ── elif _is_backend_instance(backend): self._backend = backend ( self._remote_backend, self._ibm_quantum_backend, is_ibm_fake, ) = _classify_backend(backend) self._isa_transpile = True # Primitive creation strategy: # - Real IBM hardware: always use runtime primitives (V1 or V2). # - IBM fake backends: use runtime primitives if available (>= 0.21); # fall back to local BackendEstimator/BackendSampler for runtime < 0.21 # because V1 runtime primitives require an active IBM account. # - Non-IBM backends (AerSimulator, etc.): use local primitives only; # qiskit-ibm-runtime is not required. needs_runtime = self._ibm_quantum_backend or is_ibm_fake if needs_runtime and QISKIT_RUNTIME_AVAILABLE: self._runtime_primitives_version = "v1" if QISKIT_RUNTIME_SMALLER_0_21 else "v2" if self._runtime_primitives_version == "v1" and not self._ibm_quantum_backend: # V1 runtime requires IBM account even for fakes — use local fallback self._estimator = BackendEstimator(backend=self._backend) self._sampler = BackendSampler(backend=self._backend) self._sampler_uses_v1_api = QISKIT_SMALLER_1_2 else: _check_runtime_available() if self._uses_managed_session(): # Delay session + primitive creation until the first # real execution or context-manager entry. This avoids # noisy warnings for recommended ``with`` usage. self._estimator = None self._sampler = None else: self._estimator = self._create_runtime_estimator() self._sampler = self._create_runtime_sampler() self._sampler_uses_v1_api = self._runtime_primitives_version == "v1" elif needs_runtime and not QISKIT_RUNTIME_AVAILABLE and self._ibm_quantum_backend: # Real IBM hardware always needs runtime _check_runtime_available() else: # Generic / non-IBM backend — no runtime required self._estimator = BackendEstimator(backend=self._backend) self._sampler = BackendSampler(backend=self._backend) self._sampler_uses_v1_api = QISKIT_SMALLER_1_2 logger.info( "Initialised QiskitExecutor with %s (remote=%s, mode=%s).", backend, self._remote_backend, execution_mode, ) else: raise TypeError( f"'backend' must be a string ('statevector', 'aer'), a Qiskit Backend " f"instance, a qiskit-ibm-runtime Session/Batch, or a Qiskit primitive " f"(BaseSamplerV1/V2 / BaseEstimatorV1/V2). Got {type(backend)!r}." ) if seed is not None: self._random = np.random.default_rng(seed) else: self._random = np.random.default_rng() # ------------------------------------------------------------------ # Properties # ------------------------------------------------------------------ @property def shots(self) -> int | None: """Return the number of shots.""" return self._shots @shots.setter def shots(self, value: int | None) -> None: """Set the number of shots.""" self._shots = value @property def remote(self) -> bool: """Return ``True`` if the executor targets a remote backend.""" return self._remote_backend @property def ibm_quantum(self) -> bool: """Return ``True`` if the executor targets a real IBM Quantum device.""" return self._ibm_quantum_backend @property def session(self): """Return the active runtime Session or Batch, or ``None``.""" return self._session # ------------------------------------------------------------------ # Session lifecycle # ------------------------------------------------------------------ def _uses_managed_session(self) -> bool: """Return *True* when this executor should own a runtime Session/Batch.""" return self._ibm_quantum_backend and self._execution_mode in ("session", "batch") def _create_session(self) -> None: """Create (or re-create) a :class:`~qiskit_ibm_runtime.Session` or :class:`~qiskit_ibm_runtime.Batch` depending on ``execution_mode``. ``"batch"`` mode uses ``Batch`` for independent, parallelisable jobs. ``"session"`` mode uses ``Session`` for iterative algorithms (VQE/QAOA) that require tight coupling between successive jobs. """ if self._backend is None: raise RuntimeError("Cannot create a runtime session without a backend.") if self._execution_mode == "batch": batch_cls = _load_runtime_batch() self._session = batch_cls(backend=self._backend) logger.debug("Created new runtime Batch for %s.", self._backend) else: session_cls = _load_runtime_session() self._session = session_cls(backend=self._backend) logger.debug("Created new runtime Session for %s.", self._backend) if not self._inside_context_manager: logger.warning( "IBM Runtime %s opened outside of a context manager. " "Use 'with QiskitExecutor(...) as exe:' to ensure the session " "is closed when done.", type(self._session).__name__, )
[docs] def close_session(self) -> None: """Close the current runtime session/batch if one is active. Session ownership is always managed by the executor, including injected ``Session`` / ``Batch`` objects. """ if self._session is None: return try: self._session.close() logger.info("Closed IBM Runtime %s.", type(self._session).__name__) except Exception: # pylint: disable=broad-exception-caught logger.debug( "%s.close() raised; ignoring.", type(self._session).__name__, exc_info=True, ) finally: self._session = None
def __enter__(self): """Support ``with QiskitExecutor(...) as exe:`` usage. For real IBM backends this ensures sessions are properly closed on exit. Context-managed use is strongly recommended whenever ``execution_mode`` is ``"session"`` or ``"batch"``. """ self._inside_context_manager = True if self._uses_managed_session() and self._session is None: self._create_session() self._refresh_primitives() return self def __exit__(self, exc_type, exc_val, exc_tb): self._inside_context_manager = False self.close_session() return False def __del__(self): """Attempt to close any open session when the executor is garbage-collected.""" session = getattr(self, "_session", None) if session is not None: try: session.close() except Exception: # pylint: disable=broad-exception-caught pass # ------------------------------------------------------------------ # Runtime primitive factories # ------------------------------------------------------------------ def _ensure_session_active(self) -> None: """Re-create the session when it has expired. Uses the public ``Session.status()`` API rather than private ``_active`` attributes, which are not guaranteed across runtime versions. """ if not self._ibm_quantum_backend: return if self._uses_managed_session() and self._session is None: self._create_session() return if self._session is None: return try: status = self._session.status() if status not in ("open", "pending_new"): logger.info( "%s expired (status=%r), recreating.", type(self._session).__name__, status, ) self._create_session() except Exception: # pylint: disable=broad-exception-caught logger.debug( "Could not check %s status; assuming still active.", type(self._session).__name__, exc_info=True, ) def _create_runtime_estimator(self): """Instantiate the runtime Estimator for the current backend / session.""" self._ensure_session_active() if self._runtime_primitives_version == "v1": cls, _ = _load_runtime_primitives_v1() return self._instantiate_runtime_primitive_v1(cls, self._options) cls, _ = _load_runtime_primitives_v2() return self._instantiate_runtime_primitive_v2(cls, self._options) def _create_runtime_sampler(self): """Instantiate the runtime Sampler for the current backend / session.""" self._ensure_session_active() if self._runtime_primitives_version == "v1": _, cls = _load_runtime_primitives_v1() return self._instantiate_runtime_primitive_v1(cls, self._options) _, cls = _load_runtime_primitives_v2() return self._instantiate_runtime_primitive_v2(cls, self._options) # -- V1 instantiation (qiskit-ibm-runtime < 0.21) --------------------- def _instantiate_runtime_primitive_v1(self, primitive_cls, options): """Create a V1 runtime primitive (``qiskit-ibm-runtime < 0.21``).""" if options: runtime_options_v1_cls = _load_runtime_options_v1() opts = runtime_options_v1_cls() # pylint: disable=not-callable for key, val in options.items(): try: setattr(opts, key, val) except (AttributeError, TypeError): pass else: opts = None if self._ibm_quantum_backend and self._session is not None: self._ensure_session_active() return ( primitive_cls(session=self._session, options=opts) if opts is not None else primitive_cls(session=self._session) ) return ( primitive_cls(backend=self._backend, options=opts) if opts is not None else primitive_cls(backend=self._backend) ) # -- V2 instantiation (qiskit-ibm-runtime >= 0.21) -------------------- def _instantiate_runtime_primitive_v2(self, primitive_cls, options): """Create a V2 runtime primitive. * ``0.21 – 0.22``: uses ``session=`` / ``backend=`` kwargs. * ``>= 0.23``: uses ``mode=`` (accepts both Session and backend). """ opts = options or {} if self._ibm_quantum_backend and self._session is not None: self._ensure_session_active() if QISKIT_RUNTIME_SMALLER_0_23: return ( primitive_cls(session=self._session, options=opts) if opts else primitive_cls(session=self._session) ) return ( primitive_cls(mode=self._session, options=opts) if opts else primitive_cls(mode=self._session) ) # Fake backend or real backend in job mode (no session) if QISKIT_RUNTIME_SMALLER_0_23: return ( primitive_cls(backend=self._backend, options=opts) if opts else primitive_cls(backend=self._backend) ) return ( primitive_cls(mode=self._backend, options=opts) if opts else primitive_cls(mode=self._backend) ) def _refresh_primitives(self) -> None: """Re-create primitives after a session renewal.""" self._estimator = self._create_runtime_estimator() self._sampler = self._create_runtime_sampler() def _ensure_runtime_primitives(self) -> None: """Lazily create runtime primitives when session management is deferred.""" if self._estimator is None: self._estimator = self._create_runtime_estimator() if self._sampler is None: self._sampler = self._create_runtime_sampler() self._sampler_uses_v1_api = isinstance(self._sampler, BaseSamplerV1) # ------------------------------------------------------------------ # ISA transpilation (for IBM / fake backends) # ------------------------------------------------------------------ def _isa_transpile_qiskit_circuit(self, circuit): """Transpile a raw Qiskit ``QuantumCircuit`` to ISA form. Returns the circuit unchanged when no IBM backend is configured. The resulting circuit retains its :class:`Parameter` objects so that it can still be parameterised afterwards. """ if not self._isa_transpile or self._backend is None: return circuit try: from qiskit.transpiler.preset_passmanagers import ( # pylint: disable=import-outside-toplevel generate_preset_pass_manager, ) pm = generate_preset_pass_manager( optimization_level=1, backend=self._backend, ) return pm.run(circuit) except ImportError: from qiskit import transpile # pylint: disable=import-outside-toplevel return transpile(circuit, backend=self._backend) def _isa_apply_layout_to_observable(self, observable, circuit): """Apply the transpiled circuit's layout to an observable. After ISA transpilation the virtual-to-physical qubit mapping may have changed. ``SparsePauliOp.apply_layout`` re-orders the observable to match the new mapping. """ from qiskit.quantum_info import SparsePauliOp # pylint: disable=import-outside-toplevel if not isinstance(observable, SparsePauliOp): return observable layout = getattr(circuit, "layout", None) if layout is None: return observable try: return observable.apply_layout(layout) except (ValueError, TypeError): logger.warning( "Failed to apply layout to observable; using original observable. " "This may lead to incorrect expectation values if the observable " "does not match the transpiled circuit's qubit mapping.", exc_info=True, ) return observable def _convert_to_optree( self, circuit: QuantumCircuitBase | List[QuantumCircuitBase], operator: QuantumOperatorBase | List[QuantumOperatorBase] | None = None, ) -> Tuple[OpTreeCircuit | OpTreeNodeBase, OpTreeOperator | OpTreeNodeBase | None]: """ Convert circuits and operators to OpTree format. When an IBM backend is configured the circuits are ISA-transpiled and operators are re-mapped to match the transpiled qubit layout. Args: circuit: Circuit(s) to convert operator: Operator(s) to convert (optional) Returns: Tuple of (circuit_tree, operator_tree) """ uses_ibm_backend = self._isa_transpile def _to_qiskit(c): return getattr(c, "qiskit_circuit", c) if isinstance(circuit, List): raw_circuits = [_to_qiskit(c) for c in circuit] else: raw_circuits = [_to_qiskit(circuit)] if uses_ibm_backend: transpiled_circuits = [self._isa_transpile_qiskit_circuit(c) for c in raw_circuits] else: transpiled_circuits = raw_circuits if len(transpiled_circuits) == 1: circuit_tree = OpTreeCircuit(transpiled_circuits[0]) else: circuit_tree = OpTreeList([OpTreeCircuit(c) for c in transpiled_circuits]) if operator is None: return circuit_tree, None def _to_operator(o): return getattr(o, "qiskit_operator", o) if isinstance(operator, List): ops = [_to_operator(o) for o in operator] if uses_ibm_backend: if len(ops) == len(transpiled_circuits): ops = [ self._isa_apply_layout_to_observable(o, c) for o, c in zip(ops, transpiled_circuits) ] else: ops = [ self._isa_apply_layout_to_observable(o, transpiled_circuits[0]) for o in ops ] operator_tree = OpTreeList([OpTreeOperator(o) for o in ops]) else: operator = _to_operator(operator) if uses_ibm_backend: operator = self._isa_apply_layout_to_observable(operator, transpiled_circuits[0]) operator_tree = OpTreeOperator(operator) return circuit_tree, operator_tree def _prepare_parameter_dicts( self, circuit: QuantumCircuitBase | List[QuantumCircuitBase], observable: QuantumOperatorBase | List[QuantumOperatorBase] | None = None, **parameters, ) -> Tuple[dict, dict]: """ Prepare separate parameter dictionaries for circuits and operators. Args: circuit: The quantum circuit(s) observable: The quantum observable(s) **parameters: Keyword arguments with parameter values Returns: Tuple of (circuit_param_dict, observable_param_dict) """ # helper to get the underlying qiskit objects def _unwrap(obj): if hasattr(obj, "qiskit_circuit"): return obj.qiskit_circuit if hasattr(obj, "qiskit_operator"): return obj.qiskit_operator return obj def _collect_objects(obj_or_list): if isinstance(obj_or_list, list): return [_unwrap(o) for o in obj_or_list] return [_unwrap(obj_or_list)] # Collect all circuits and observables circuits = _collect_objects(circuit) observables = _collect_objects(observable) if observable is not None else [] def _build_param_dict(qiskit_objects): param_dict = {} for qobj in qiskit_objects: for p in qobj.parameters: # Support both ParameterVector elements and standalone Parameters name = p.vector.name if hasattr(p, "vector") else p.name if name not in parameters: continue supplied = parameters[name] # Normalize to numpy if isinstance(supplied, (list, tuple, np.ndarray)): arr = np.asarray(supplied) if hasattr(p, "index"): # ParameterVector element – bind by index try: val = arr[p.index] except (IndexError, TypeError) as exc: if arr.size == 1: val = arr.flat[0] else: raise ValueError( f"Provided values for parameter '{name}' have length " f"{arr.size} but parameter index {p.index} is requested." ) from exc else: # Standalone Parameter – scalar expected; take first element val = arr.flat[0] if arr.size == 1 else arr else: val = supplied param_dict[p] = val return param_dict circuit_dict = _build_param_dict(circuits) observable_dict = _build_param_dict(observables) if observables else {} return circuit_dict, observable_dict def _extract_counts(self, pub_result, n_qubits=None): """Extract measurement counts from a primitive result object. Handles both the Qiskit 2.x / V2 PUB result format and the Qiskit 1.x / V1 ``quasi_dists`` format. """ # --- Qiskit 2.x / V2 primitives --- # PrimitiveResult is iterable but not necessarily subscriptable; # materialise to a list before indexing. if hasattr(pub_result, "__iter__") and not isinstance(pub_result, (str, dict)): pubs = list(pub_result) if pubs and hasattr(pubs[0], "data"): counts_list = [] for i, pub in enumerate(pubs): data = getattr(pub, "data", None) meas = getattr(data, "meas", None) if data is not None else None if meas is None or not hasattr(meas, "get_counts"): raise ValueError( f"Unsupported sampler result format at pub index {i}: " f"'data.meas.get_counts()' is not available " f"(got type {type(pub)!r})." ) counts_list.append(_convert_counts_endianness(meas.get_counts())) return counts_list # --- Qiskit 1.x / V1 primitives --- if hasattr(pub_result, "quasi_dists"): quasi_dists = pub_result.quasi_dists metadata = getattr(pub_result, "metadata", None) if metadata is None: raise ValueError( "Unsupported sampler result format: 'metadata' attribute is " "missing for quasi_dists." ) counts_list = [] for idx, qd in enumerate(quasi_dists): if idx >= len(metadata): raise ValueError( f"Unsupported sampler result format: 'metadata' has " f"{len(metadata)} entries but quasi_dists has " f"{len(quasi_dists)}." ) if "shots" not in metadata[idx]: raise ValueError( f"Unsupported sampler result format: " f"'metadata[{idx}][\"shots\"]' is missing." ) shots = metadata[idx]["shots"] counts = {format(k, f"0{n_qubits}b"): int(round(v * shots)) for k, v in qd.items()} counts_list.append(_convert_counts_endianness(counts)) return counts_list raise ValueError("Unsupported primitive result format: cannot extract counts.") def _expectation_value( self, circuit: QuantumCircuitBase | List[QuantumCircuitBase], observable: QuantumOperatorBase | List[QuantumOperatorBase], **parameter_values, ) -> float | np.ndarray: """ Calculate the expectation value using OpTree and Qiskit Estimator. Args: circuit: The quantum circuit or a list of circuits. observable: The quantum observable or a list of observables. parameter_values: Parameter values as keyword arguments. Returns: The expectation value(s). """ if self._ibm_quantum_backend: self._ensure_runtime_primitives() if self._estimator is None: raise RuntimeError( "No estimator is configured. Pass `backend` as an Estimator primitive " "or use a backend/session that supports estimation." ) # Convert to OpTree format circuit_tree, observable_tree = self._convert_to_optree(circuit, observable) # Prepare separate parameter dictionaries circuit_dict, observable_dict = self._prepare_parameter_dicts( circuit, observable, **parameter_values ) # Use OpTree evaluation with Estimator return OpTreeEvaluate.evaluate_with_estimator( circuit=circuit_tree, operator=observable_tree, dictionary_circuit=circuit_dict, dictionary_operator=observable_dict, estimator=self._estimator, dictionaries_combined=False, detect_duplicates=True, ) def _expectation_value_derivatives( self, circuit: QuantumCircuitBase | List[QuantumCircuitBase], observable: QuantumOperatorBase | List[QuantumOperatorBase], *derivative_params, **parameter_values, ) -> np.ndarray | dict: """ Calculate the derivatives using OpTree parameter shift. Args: circuit: The quantum circuit. observable: The quantum observable. derivative_params: Parameters to differentiate with respect to. parameter_values: Parameter values as keyword arguments. Returns: Derivative values. """ if self._ibm_quantum_backend: self._ensure_runtime_primitives() if self._estimator is None: raise RuntimeError( "No estimator is configured. Pass `backend` as an Estimator primitive " "or use a backend/session that supports estimation." ) # If no derivative parameters specified, return expectation value if len(derivative_params) == 0: return self._expectation_value(circuit, observable, **parameter_values) # Convert to OpTree format circuit_tree, observable_tree = self._convert_to_optree(circuit, observable) # Prepare separate parameter dictionaries circuit_dict, observable_dict = self._prepare_parameter_dicts( circuit, observable, **parameter_values ) # Build separate parameter sets for circuit and observable so we can # apply the product rule correctly. if isinstance(circuit, list): circuit_param_set = set(getattr(circuit[0], "qiskit_circuit", circuit[0]).parameters) else: circ = getattr(circuit, "qiskit_circuit", circuit) circuit_param_set = set(circ.parameters) if observable is not None: if isinstance(observable, list): observable_param_set: set = set() for obs in observable: obs_obj = getattr(obs, "qiskit_operator", obs) observable_param_set |= set(obs_obj.parameters) else: obs_obj = getattr(observable, "qiskit_operator", observable) observable_param_set = set(obs_obj.parameters) else: observable_param_set = set() all_params = circuit_param_set | observable_param_set def _param_name(p) -> str: return p.vector.name if hasattr(p, "vector") else p.name def _derivative_for_single_param(p) -> float: """∂E/∂p = circuit contribution + observable contribution (product rule).""" total = 0.0 # Circuit contribution: ⟨∂ψ/∂p|H|ψ⟩ (parameter shift on circuit) if p in circuit_param_set: circ_deriv = OpTreeDerivative.differentiate(circuit_tree, [p]) total += OpTreeEvaluate.evaluate_with_estimator( circuit=circ_deriv, operator=observable_tree, dictionary_circuit=circuit_dict, dictionary_operator=observable_dict, estimator=self._estimator, detect_duplicates=True, ) # Observable contribution: ⟨ψ|∂H/∂p|ψ⟩ if p in observable_param_set: op_deriv = OpTreeDerivative.differentiate(observable_tree, [p]) total += OpTreeEvaluate.evaluate_with_estimator( circuit=circuit_tree, operator=op_deriv, dictionary_circuit=circuit_dict, dictionary_operator=observable_dict, estimator=self._estimator, detect_duplicates=True, ) return total results: dict = {} for dp in derivative_params: if isinstance(dp, str): matching = [p for p in all_params if _param_name(p) == dp] if not matching: results[dp] = 0.0 continue # Sort ParameterVector elements by index; standalone Parameters have no index. matching.sort(key=lambda p: p.index if hasattr(p, "index") else 0) if len(matching) == 1: results[dp] = _derivative_for_single_param(matching[0]) else: # ParameterVector: return one derivative value per element. results[dp] = np.array([_derivative_for_single_param(p) for p in matching]) elif isinstance(dp, ParameterVectorElement): results[dp] = _derivative_for_single_param(dp) else: raise ValueError(f"Unknown derivative parameter type: {type(dp)}") if len(derivative_params) == 1: return results[derivative_params[0]] return results def _sample( self, circuit: QuantumCircuitBase | List[QuantumCircuitBase], **parameter_values ) -> List[dict]: """Sample from the circuit using OpTree and Qiskit Sampler.""" if self._ibm_quantum_backend: self._ensure_runtime_primitives() if self._sampler is None: raise RuntimeError( "No sampler is configured. Pass `backend` as a Sampler primitive " "or use a backend/session that supports sampling." ) if self._shots is None: raise ValueError("Shots must be set for sampling.") # Convert to OpTree format (just for consistent handling) circuit_tree, _ = self._convert_to_optree(circuit, operator=None) # Prepare parameter dictionary (only for circuits) circuit_dict, _ = self._prepare_parameter_dicts( circuit, observable=None, **parameter_values ) # Extract circuits from OpTree if isinstance(circuit_tree, OpTreeCircuit): circuits = [circuit_tree.circuit] else: circuits = [child.circuit for child in circuit_tree.children] # Bind parameters to circuits bound_circuits = [] for circ in circuits: # Bind only parameters that exist in this circuit params_to_bind = {p: circuit_dict[p] for p in circ.parameters if p in circuit_dict} bound_circ = circ.assign_parameters(params_to_bind) if params_to_bind else circ if bound_circ.num_clbits == 0: bound_circ.measure_all() bound_circuits.append(bound_circ) if self._sampler_uses_v1_api: job = self._sampler.run(bound_circuits, shots=self._shots) else: pubs = [(circ,) for circ in bound_circuits] job = self._sampler.run(pubs, shots=self._shots) result = job.result() is_list_input = isinstance(circuit, list) raw_circuits_for_nq = circuit if is_list_input else [circuit] n_qubits_list = [getattr(c, "qiskit_circuit", c).num_qubits for c in raw_circuits_for_nq] counts_list = self._extract_counts(result, n_qubits_list[0]) if not is_list_input: return counts_list[0] if isinstance(counts_list, list) else counts_list return counts_list def _statevector( self, circuit: QuantumCircuitBase | List[QuantumCircuitBase], **parameter_values ) -> np.ndarray: """Compute the statevector of the circuit using local Qiskit simulation. Statevector computation is always performed locally regardless of the configured backend, since it is a purely classical computation. """ # Convert to OpTree but without ISA transpilation — use the raw circuit if isinstance(circuit, list): raw_circuits = [getattr(c, "qiskit_circuit", c) for c in circuit] else: raw_circuits = [getattr(circuit, "qiskit_circuit", circuit)] # Prepare parameter dictionary circuit_dict, _ = self._prepare_parameter_dicts( circuit, observable=None, **parameter_values ) statevectors = [] for circ in raw_circuits: params_to_bind = {p: circuit_dict[p] for p in circ.parameters if p in circuit_dict} bound_circ = circ.assign_parameters(params_to_bind) if params_to_bind else circ statevectors.append(_reverse_qubit_ordering(Statevector(bound_circ).data)) statevectors = np.array(statevectors) return statevectors[0] if len(raw_circuits) == 1 else statevectors def _transpile_circuit(self, circuit: QuantumCircuitBase) -> QiskitCircuit: """Transpile a generic QuantumCircuit to a Qiskit QuantumCircuit. For remote IBM backends the circuit is additionally transpiled through ``generate_preset_pass_manager`` to produce an ISA-compliant circuit that conforms to the target backend's instruction set. Args: circuit (QuantumCircuitBase): The generic QuantumCircuit to transpile. Returns: QiskitCircuit: The corresponding QiskitCircuit. """ qc = QiskitCircuit(circuit) isa_circuit = self._isa_transpile_qiskit_circuit(qc.qiskit_circuit) if isa_circuit is not qc.qiskit_circuit: return QiskitCircuit.from_qiskit(isa_circuit) return qc def _transpile_operator(self, operator: QuantumOperatorBase) -> QiskitOperator: """Transpile a generic QuantumOperator to a Qiskit QuantumOperator. Args: operator (QuantumOperatorBase): The generic QuantumOperator to transpile. Returns: QiskitOperator: The corresponding QiskitOperator. """ if isinstance(operator, self._native_operator_class): return operator return self._native_operator_class.from_quantum_operator(operator)
[docs] @classmethod def get_accepted_backend_types(cls) -> list[type]: """Return all types accepted as the ``backend`` argument. Covers: * Qiskit local backends (``Backend`` / ``BackendV2``) * Qiskit statevector primitives (``StatevectorEstimator``, ``StatevectorSampler``) * Qiskit local primitive base classes (V1 and V2) * IBM Runtime ``Session`` / ``Batch`` (when *qiskit-ibm-runtime* is installed) * IBM Runtime primitive classes (V1 and V2, version-gated) Dummy sentinel classes defined when optional dependencies are absent are intentionally excluded so that ``isinstance`` checks never yield false positives. """ types: list[type] = [ Backend, StatevectorEstimator, StatevectorSampler, ] for cls_ in (BaseEstimatorV1, BaseEstimatorV2, BaseSamplerV1, BaseSamplerV2): if getattr(cls_, "__module__", "").startswith("qiskit."): types.append(cls_) # ── IBM Runtime types (only when the real classes are importable) ─── if QISKIT_RUNTIME_AVAILABLE: # Session and Batch are real at this point (imported at module top) types.extend([Session, Batch]) # Runtime primitives: include only the non-dummy versions. # The module-level conditionals guarantee that the V1/V2 splits are # correctly resolved; we just need to skip the local dummy sentinels. for rt_cls in ( RuntimeEstimatorV1, RuntimeEstimatorV2, RuntimeSamplerV1, RuntimeSamplerV2, ): # Dummy classes are defined in this module and have no __module__ # pointing to qiskit_ibm_runtime — use that as the guard. if "qiskit_ibm_runtime" in getattr(rt_cls, "__module__", ""): types.append(rt_cls) return types
[docs] @classmethod def get_accepted_backend_aliases(cls) -> list[str]: """Return string aliases accepted by this executor in ``Executor.create``.""" return ["statevector", "aer"]