Source code for divi.backends._circuit_runner

# SPDX-FileCopyrightText: 2025-2026 Qoro Quantum Ltd <divi@qoroquantum.de>
#
# SPDX-License-Identifier: Apache-2.0

from abc import ABC, abstractmethod
from collections.abc import Mapping
from threading import Event
from typing import Protocol, runtime_checkable

import numpy as np

from divi.backends import ExecutionResult
from divi.circuits import TemplateEntry


[docs] @runtime_checkable class SupportsCircuitTemplates(Protocol): """Capability protocol for backends that resolve parametric QASM templates server-side. The pipeline's deferred-binding path is gated on ``isinstance(backend, SupportsCircuitTemplates)``; a backend opts in simply by implementing :meth:`submit_circuit_templates` — no inheritance is required, and no capability flag has to be plumbed through the base class for backends that have nothing else to share with the protocol. """
[docs] def submit_circuit_templates(
self, templates: list[TemplateEntry], *, cancellation_event: Event | None = None, **kwargs, ) -> ExecutionResult: ...
[docs] class CircuitRunner(ABC): """ A generic interface for anything that can "run" quantum circuits. """ def __init__(self, shots: int, track_depth: bool = False): if shots <= 0: raise ValueError(f"Shots must be a positive integer. Got {shots}.") self._shots = shots self.track_depth = track_depth self._depth_history: list[list[int]] = [] @property def shots(self): """ Get the number of measurement shots for circuit execution. Returns: int: Number of shots configured for this runner. """ return self._shots @property @abstractmethod def supports_expval(self) -> bool: """ Whether the backend supports expectation value measurements. """ return False @property @abstractmethod def is_async(self) -> bool: """ Whether the backend executes circuits asynchronously. Returns: bool: True if the backend returns a job ID and requires polling for results (e.g., QoroService). False if the backend returns results immediately (e.g., QiskitSimulator). """ return False
[docs] def set_seed(self, seed: int) -> None: """Seed the backend's random number generator, if supported. The default implementation is a no-op. Backends that can seed their simulation RNG should override this method. Args: seed: Seed value for the backend's RNG. """
[docs] @abstractmethod def submit_circuits( self, circuits: Mapping[str, str], *, cancellation_event: Event | None = None, **kwargs, ) -> ExecutionResult: """ Submit quantum circuits for execution. This abstract method must be implemented by subclasses to define how circuits are executed on their respective backends (simulator, hardware, etc.). Args: circuits (dict[str, str]): Dictionary mapping circuit labels to their OpenQASM string representations. cancellation_event: When set, the backend aborts the batch and raises :class:`~divi.exceptions.ExecutionCancelledError`. Sync backends honour it between items; async backends thread it into their poll loop. **kwargs: Additional backend-specific parameters for circuit execution. Returns: ExecutionResult: For synchronous backends, contains results directly. For asynchronous backends, contains a job_id that can be used to fetch results later. """
@property def depth_history(self) -> list[list[int]]: """Circuit depth per batch when ``track_depth`` is True. Each element is a list of depths (one per circuit) for that submission. Empty when ``track_depth`` is False or before any circuits have been run. """ return self._depth_history.copy()
[docs] def average_depth(self) -> float: """Average circuit depth across all tracked submissions. Returns 0.0 when depth history is empty. """ all_depths = [d for batch in self._depth_history for d in batch] return float(np.mean(all_depths)) if all_depths else 0.0
[docs] def std_depth(self) -> float: """Standard deviation of circuit depth across all tracked submissions. Returns 0.0 when depth history is empty or has a single value. """ all_depths = [d for batch in self._depth_history for d in batch] return float(np.std(all_depths)) if len(all_depths) > 1 else 0.0
[docs] def clear_depth_history(self) -> None: """Clear the depth history. Use when reusing the backend for a new run.""" self._depth_history.clear()