Source code for qtest.backends.qiskit_backend

"""Qiskit-based simulator backend for qtest.

This module is import-safe even when Qiskit is not installed: all
``import qiskit ...`` calls are deferred to method bodies. Constructing
:class:`QiskitBackend` does not require Qiskit; calling any of its execution
methods does.

Qiskit 1.0+ removed ``qiskit.execute`` and ``qiskit.BasicAer``; we use
``qiskit.transpile`` + ``backend.run(...)`` for sampling, and
``qiskit.quantum_info.{Statevector, Operator}`` for state / unitary
extraction (no Aer required for those).
"""

from __future__ import annotations

from typing import Any

import numpy as np

from qtest.backends.base import Backend, CircuitResources

# Instructions that are not unitary gates and must not count toward
# two-/multi-qubit gate tallies (a barrier spans every qubit it touches).
_NON_GATE_OPS = frozenset({"barrier", "measure", "reset", "delay", "snapshot"})

_QISKIT_MISSING_MSG = (
    "QiskitBackend requires Qiskit. Install with one of:\n"
    "  pip install 'qtest[aer]'   # qtest + qiskit + qiskit-aer (recommended)\n"
    "  pip install qiskit         # core only (slower BasicSimulator fallback)"
)


def _require_qiskit() -> Any:
    """Import and return the :mod:`qiskit` module, with a friendly error."""
    try:
        import qiskit
    except ImportError as exc:
        raise ImportError(_QISKIT_MISSING_MSG) from exc
    return qiskit


[docs] class QiskitBackend(Backend): """Qiskit simulator backend. Parameters ---------- simulator_name Cosmetic identifier reported via :attr:`name`. The actual simulator chosen at run time is :class:`qiskit_aer.AerSimulator` if available, else :class:`qiskit.providers.basic_provider.BasicSimulator`. shots Default shot count used when :meth:`run_circuit` is called without an explicit ``shots`` argument. optimization_level Default ``transpile`` optimisation level (one of ``0, 1, 2, 3``). Notes ----- The constructor arguments are *defaults*; per-call arguments to :meth:`run_circuit` override them. The backend keeps no other state between calls — two calls with identical arguments produce identical output (assuming the same ``seed``). """ def __init__( self, simulator_name: str = "aer_simulator", shots: int = 1024, optimization_level: int = 1, ) -> None: if not isinstance(simulator_name, str) or not simulator_name: raise ValueError("simulator_name must be a non-empty string") if not isinstance(shots, int) or isinstance(shots, bool) or shots <= 0: raise ValueError(f"shots must be a positive integer, got {shots!r}") if optimization_level not in (0, 1, 2, 3): raise ValueError( f"optimization_level must be one of {{0, 1, 2, 3}}, " f"got {optimization_level!r}" ) self._simulator_name = simulator_name self._default_shots = shots self._default_optimization_level = optimization_level # ------------------------------------------------------------------ # # Metadata # # ------------------------------------------------------------------ # @property def name(self) -> str: return f"qiskit:{self._simulator_name}" @property def supports_statevector(self) -> bool: return True # ------------------------------------------------------------------ # # Simulator selection # # ------------------------------------------------------------------ # @staticmethod def _get_sampling_simulator(aer_noise_model: Any | None = None) -> Any: """Return an instantiated simulator backend suitable for sampling. Prefers ``qiskit_aer.AerSimulator``; falls back to ``qiskit.providers.basic_provider.BasicSimulator`` only when Aer is unavailable *and* no noise model is requested (noise needs Aer). """ try: from qiskit_aer import AerSimulator except ImportError as exc: if aer_noise_model is not None: from qtest.noise.models import _AER_MISSING_MSG raise ImportError(_AER_MISSING_MSG) from exc from qiskit.providers.basic_provider import BasicSimulator return BasicSimulator() if aer_noise_model is not None: return AerSimulator(noise_model=aer_noise_model) return AerSimulator() # ------------------------------------------------------------------ # # Execution # # ------------------------------------------------------------------ #
[docs] def run_circuit( self, circuit: Any, shots: int | None = None, seed: int | None = None, noise_model: Any | None = None, ) -> dict[str, int]: qiskit = _require_qiskit() if circuit is None: raise ValueError("circuit must not be None") shots_val = self._default_shots if shots is None else shots if not isinstance(shots_val, int) or isinstance(shots_val, bool) or shots_val <= 0: raise ValueError(f"shots must be a positive integer, got {shots_val!r}") if seed is not None and (not isinstance(seed, int) or isinstance(seed, bool)): raise ValueError(f"seed must be an integer or None, got {seed!r}") aer_noise_model = noise_model.to_qiskit() if noise_model is not None else None simulator = self._get_sampling_simulator(aer_noise_model) transpiled = qiskit.transpile( circuit, simulator, optimization_level=self._default_optimization_level, ) run_kwargs: dict[str, Any] = {"shots": shots_val} if seed is not None: run_kwargs["seed_simulator"] = seed try: job = simulator.run(transpiled, **run_kwargs) except TypeError: run_kwargs.pop("seed_simulator", None) job = simulator.run(transpiled, **run_kwargs) result = job.result() return dict(result.get_counts())
[docs] def get_statevector(self, circuit: Any) -> np.ndarray: _require_qiskit() if circuit is None: raise ValueError("circuit must not be None") from qiskit.quantum_info import Statevector sv = Statevector.from_instruction(circuit) return np.asarray(sv.data, dtype=complex)
[docs] def get_density_matrix(self, circuit: Any, noise_model: Any | None = None) -> np.ndarray: qiskit = _require_qiskit() if circuit is None: raise ValueError("circuit must not be None") # Noiseless: the exact pure-state projector, no Aer required. if noise_model is None: from qiskit.quantum_info import DensityMatrix dm = DensityMatrix.from_instruction(circuit) return np.asarray(dm.data, dtype=complex) # Noisy: evolve under the noise model on Aer's density-matrix simulator. try: from qiskit_aer import AerSimulator except ImportError as exc: from qtest.noise.models import _AER_MISSING_MSG raise ImportError(_AER_MISSING_MSG) from exc simulator = AerSimulator(method="density_matrix", noise_model=noise_model.to_qiskit()) circ = circuit.copy() circ.save_density_matrix() transpiled = qiskit.transpile(circ, simulator, optimization_level=0) result = simulator.run(transpiled).result() dm = result.data(0)["density_matrix"] return np.asarray(dm.data if hasattr(dm, "data") else dm, dtype=complex)
[docs] def get_unitary(self, circuit: Any) -> np.ndarray: _require_qiskit() if circuit is None: raise ValueError("circuit must not be None") from qiskit.quantum_info import Operator op = Operator(circuit) return np.asarray(op.data, dtype=complex)
[docs] def get_resources(self, circuit: Any) -> CircuitResources: _require_qiskit() if circuit is None: raise ValueError("circuit must not be None") gate_counts = {str(name): int(count) for name, count in circuit.count_ops().items()} two_qubit = 0 multi_qubit = 0 for instruction in circuit.data: if instruction.operation.name in _NON_GATE_OPS: continue n = len(instruction.qubits) if n >= 2: multi_qubit += 1 if n == 2: two_qubit += 1 return CircuitResources( num_qubits=int(circuit.num_qubits), depth=int(circuit.depth()), size=int(circuit.size()), gate_counts=gate_counts, two_qubit_count=two_qubit, multi_qubit_count=multi_qubit, )