Source code for qtest.backends.base

"""Abstract backend interface for qtest.

A :class:`Backend` is a stateless adapter that translates qtest's public API
into a specific quantum SDK's primitives (Qiskit today; Cirq / PennyLane
later). It exposes three core operations — running a circuit with
measurements, extracting the post-circuit state vector, and extracting the
circuit's unitary — plus a couple of metadata properties.

Concrete backends MUST be free of cross-call mutable state: a given
configuration must produce the same output for the same input on every
call. Constructor arguments are immutable defaults; per-call overrides
(e.g. ``shots``) are supported where applicable.
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any

import numpy as np


[docs] @dataclass(frozen=True) class CircuitResources: """Structural / resource metrics of a circuit, free of any execution. These describe how *expensive* a circuit is to run on hardware — the quantities transpilers and optimisers try to minimise — and are the basis for :mod:`qtest.assertions.resources`. """ #: Number of qubits the circuit acts on. num_qubits: int #: Circuit depth (length of the critical path of gates). depth: int #: Total number of operations (gate instances). size: int #: Mapping ``{gate_name: count}``; gate names are backend-specific. gate_counts: dict[str, int] = field(default_factory=dict) #: Number of gates acting on exactly two qubits. two_qubit_count: int = 0 #: Number of gates acting on two *or more* qubits. multi_qubit_count: int = 0 @property def t_count(self) -> int: """Number of T / T-dagger gates (a key fault-tolerant cost metric).""" return self.gate_counts.get("t", 0) + self.gate_counts.get("tdg", 0)
[docs] class Backend(ABC): """Abstract base class for quantum circuit execution backends."""
[docs] @abstractmethod def run_circuit( self, circuit: Any, shots: int | None = None, seed: int | None = None, noise_model: Any | None = None, ) -> dict[str, int]: """Run *circuit* and return measurement counts. Parameters ---------- circuit A circuit object native to the backend (e.g. ``QuantumCircuit``). shots Number of shots. If ``None``, the backend's configured default is used. seed Optional seed for reproducible sampling. noise_model Optional :class:`qtest.noise.NoiseModel` describing the noise to apply during simulation. ``None`` (the default) runs an ideal, noiseless simulation. Backends that cannot simulate noise should raise :class:`NotImplementedError` when a non-``None`` model is passed. Returns ------- dict[str, int] Mapping ``{bitstring: count}`` whose values sum to *shots*. """
[docs] @abstractmethod def get_statevector(self, circuit: Any) -> np.ndarray: """Return the state vector produced by *circuit* (no measurement). The returned array has ``dtype=complex`` and length ``2**n_qubits``. """
[docs] @abstractmethod def get_unitary(self, circuit: Any) -> np.ndarray: """Return the unitary matrix of *circuit*. The returned array has ``dtype=complex`` and shape ``(2**n_qubits, 2**n_qubits)``. Raises if *circuit* contains non-unitary instructions (measurement, reset, ...). """
[docs] def get_density_matrix(self, circuit: Any, noise_model: Any | None = None) -> np.ndarray: """Return the density matrix produced by *circuit*. With ``noise_model=None`` this is the pure-state projector :math:`|\\psi\\rangle\\langle\\psi|`; with a noise model it is the mixed state resulting from noisy evolution. The returned array has ``dtype=complex`` and shape ``(2**n_qubits, 2**n_qubits)``. The default implementation raises :class:`NotImplementedError`; backends capable of density-matrix simulation override it. """ raise NotImplementedError( f"Backend {self.name!r} does not support density-matrix extraction." )
[docs] def get_resources(self, circuit: Any) -> CircuitResources: """Return structural resource metrics for *circuit*. This is a static analysis of the circuit — no simulation is run, so it works on circuits of any width. The default implementation raises :class:`NotImplementedError`; backends able to introspect their native circuit type override it. """ raise NotImplementedError(f"Backend {self.name!r} does not support resource extraction.")
@property @abstractmethod def name(self) -> str: """Human-readable backend identifier.""" @property @abstractmethod def supports_statevector(self) -> bool: """Whether :meth:`get_statevector` / :meth:`get_unitary` are supported."""