"""********************************************************************************
* Copyright (c) 2026 the Qrisp authors
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License, v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is
* available at https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************
"""
"""This module defines :class:`VirtualBackend` and its associated :class:`VirtualJob`."""
import warnings
from typing import Sequence, cast
from qrisp.circuit.quantum_circuit import QuantumCircuit
from qrisp.interface.backend import Backend
from qrisp.interface.job import Job, JobResult, JobStatus
from qrisp.misc.exceptions import QrispDeprecationWarning
class VirtualJob(Job):
"""A synchronous :class:`~qrisp.interface.Job` produced by :class:`VirtualBackend`.
Execution is performed inline inside :meth:`submit` by calling the
user-provided ``run_func`` once per circuit with the circuit's QASM
representation, shot count, and token.
"""
def __init__(
self,
backend: "VirtualBackend",
circuits: Sequence[QuantumCircuit],
shots: int | list[int] | None,
token: str,
):
"""Initialise the job with the backend, normalised circuit list, shot count, and token."""
super().__init__(backend=backend)
self._circuits = circuits
self._shots = shots
self._token = token
self._result_data = None
# ------------------------------------------------------------------
# Abstract interface
# ------------------------------------------------------------------
def submit(self) -> None:
"""Execute all circuits synchronously via the user-provided ``run_func``.
Each circuit is serialised to QASM and passed to ``run_func`` along
with the shot count and token. The collected counts dictionaries
are wrapped in a :class:`~qrisp.interface.JobResult`.
"""
self._last_known_status = JobStatus.RUNNING
try:
run_func = self._backend.run_func
if isinstance(self._shots, list):
counts_list = [
run_func(circuit.qasm(), s, self._token) for circuit, s in zip(self._circuits, self._shots)
]
else:
counts_list = [run_func(circuit.qasm(), self._shots, self._token) for circuit in self._circuits]
self._result_data = JobResult(counts_list)
self._last_known_status = JobStatus.DONE
except Exception as exc:
self._failure_cause = exc
self._last_known_status = JobStatus.ERROR
def result(self, timeout: float | None = None) -> JobResult:
"""Return the :class:`~qrisp.interface.JobResult`.
Because execution is synchronous the result is already available as
soon as :meth:`submit` has been called. The *timeout* parameter is
accepted for interface compatibility but has no effect.
Returns
-------
JobResult
Raises
------
JobFailureError
If the user-provided ``run_func`` raised an exception.
"""
self._raise_for_status(self._last_known_status)
return cast(JobResult, self._result_data)
def cancel(self) -> bool:
"""Return ``False``: synchronous jobs cannot be cancelled after submission."""
return False
def status(self) -> JobStatus:
"""Return the current :class:`~qrisp.interface.JobStatus` of the job."""
return self._last_known_status
[docs]
class VirtualBackend(Backend):
"""A :class:`~qrisp.interface.Backend` that wraps a user-provided circuit
execution function.
This class replaces the legacy ``VirtualBackend`` (which depended on the
now-removed ``BackendClient`` / ``BackendServer`` infrastructure). It
follows the standard :class:`~qrisp.interface.Backend` interface and
executes circuits synchronously by calling the user-supplied
``run_func`` for each circuit.
.. deprecated:: 0.8
``VirtualBackend`` is deprecated. New code should subclass
:class:`~qrisp.interface.Backend` directly instead. See
:class:`~qrisp.interface.simulators.qrisp_simulator_backend.QrispSimulatorBackend`
for a reference implementation.
Parameters
----------
run_func : callable
A function with signature ``run_func(qasm_str, shots, token)`` that
receives the circuit's QASM string, an integer shot count (or
``None``), and a token string. It must return a ``dict`` mapping
bitstring outcomes to counts (or probabilities when ``shots`` is
``None``).
name : str, optional
A human-readable name for this backend. Defaults to
``"VirtualBackend"``.
Examples
--------
We set up a ``VirtualBackend`` that prints the received circuit and
returns the results of the QASM simulator. It is recommended that
``run_func`` provides a default value of ``None`` for the ``shots``
parameter, substituting its own default::
def run_func(qasm_str, shots=None, token=""):
if shots is None:
shots = 1000
from qiskit import QuantumCircuit
qiskit_qc = QuantumCircuit.from_qasm_str(qasm_str)
print(qiskit_qc)
from qiskit_aer import AerSimulator
qiskit_backend = AerSimulator()
return qiskit_backend.run(qiskit_qc, shots=shots).result().get_counts()
>>> from qrisp.interface import VirtualBackend
>>> example_backend = VirtualBackend(run_func)
>>> from qrisp import QuantumFloat
>>> qf = QuantumFloat(3)
>>> qf[:] = 4
>>> qf.get_measurement(backend=example_backend)
┌─┐
qf.0: ─────┤M├──────
└╥┘┌─┐
qf.1: ──────╫─┤M├───
┌───┐ ║ └╥┘┌─┐
qf.2: ┤ X ├─╫──╫─┤M├
└───┘ ║ ║ └╥┘
cb_0: 1/══════╩══╬══╬═
0 ║ ║
cb_1: 1/═════════╩══╬═
0 ║
cb_2: 1/════════════╩═
{4: 1.0}
"""
def __init__(self, run_func, name=None):
warnings.warn(
"DeprecationWarning: VirtualBackend is deprecated and will be removed in "
"a later release of Qrisp. Please subclass the ``Backend`` abstract class "
"to implement custom backends.",
QrispDeprecationWarning,
)
super().__init__(name=name or "VirtualBackend")
self.run_func = run_func
# ------------------------------------------------------------------
# Backend interface
# ------------------------------------------------------------------
@classmethod
def _default_options(cls):
"""Return the default runtime options.
``shots=None`` enables analytic (exact probability) execution.
``token`` is forwarded to the user-provided ``run_func``.
"""
return {"shots": None, "token": ""}
def run_async(
self,
circuits: QuantumCircuit | Sequence[QuantumCircuit],
shots: int | list[int] | None = None,
) -> VirtualJob:
"""Submit one or more circuits for execution via the user-provided ``run_func``.
This method returns a :class:`VirtualJob` that is already
:attr:`~qrisp.interface.JobStatus.DONE` before :meth:`run_async`
returns, because execution is synchronous.
Parameters
----------
circuits : QuantumCircuit or list[QuantumCircuit]
One circuit or a sequence of circuits to execute.
shots : int or None, optional
Number of shots. ``None`` selects analytic execution.
If not provided, the backend's ``shots`` option is used.
Returns
-------
VirtualJob
"""
self._check_circuit_limit(circuits)
if isinstance(circuits, QuantumCircuit):
circuits = [circuits]
else:
circuits = list(circuits)
if isinstance(shots, list):
self._validate_shots_length(shots, circuits)
n_shots = shots if shots is not None else self.options.get("shots")
token = self.options.get("token", "")
job = VirtualJob(backend=self, circuits=circuits, shots=n_shots, token=token)
job.submit()
return job