Source code for qrisp.interface.simulators.stim_backend

"""********************************************************************************
* 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
********************************************************************************
"""

from typing import Sequence, cast

import numpy as np

from qrisp.circuit.quantum_circuit import QuantumCircuit
from qrisp.interface.backend import Backend
from qrisp.interface.job import Job, JobResult, JobStatus


def _run_on_stim(qc: QuantumCircuit, shots: int):
    """Run a single circuit on Stim and return the counts."""
    stim_circuit, measurement_map = qc.to_stim(return_measurement_map=True)

    sampler = stim_circuit.compile_sampler()
    shot_array = sampler.sample(shots=shots)

    # Some clbits may not have been measured (e.g. ancilla clbits).
    # Pad shot_array with zero columns and extend measurement_map so the
    # permutation below works for all clbits — in a single hstack call.
    missing = [cb for cb in qc.clbits if cb not in measurement_map]
    if missing:
        n_missing = len(missing)
        old_cols = shot_array.shape[1]
        zero_cols = np.zeros((shot_array.shape[0], n_missing), dtype=shot_array.dtype)
        shot_array = np.hstack([shot_array, zero_cols])
        for i, cb in enumerate(missing):
            measurement_map[cb] = old_cols + i

    permutation = [measurement_map[cb] for cb in qc.clbits]
    permuted_shot_array = shot_array[:, permutation]

    counts = {}
    for row in permuted_shot_array:
        bitstring = "".join(str(int(b)) for b in reversed(row))
        counts[bitstring] = counts.get(bitstring, 0) + 1

    return counts


class _StimJob(Job):
    """Synchronous :class:`~qrisp.interface.Job` for :class:`StimBackend`."""

    def __init__(self, backend: "StimBackend", circuits: Sequence, shots: int | list[int]):
        super().__init__(backend=backend)
        self._circuits = circuits
        self._shots = shots
        self._result_data = None

    def submit(self) -> None:
        self._last_known_status = JobStatus.RUNNING
        try:
            if isinstance(self._shots, list):
                counts_list = [_run_on_stim(qc, s) for qc, s in zip(self._circuits, self._shots)]
            else:
                counts_list = [_run_on_stim(qc, self._shots) for qc 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=None) -> JobResult:
        self._raise_for_status(self._last_known_status)
        return cast(JobResult, self._result_data)

    def cancel(self) -> bool:
        return False

    def status(self) -> JobStatus:
        return self._last_known_status


[docs] class StimBackend(Backend): """A :class:`~qrisp.interface.Backend` that simulates Clifford circuits via `Stim <https://github.com/quantumlib/Stim>`_. :meth:`run` returns a :class:`~qrisp.interface.MeasurementResult` immediately. For lazy, buffered execution call :meth:`~qrisp.interface.Backend.batched` first:: bb = StimBackend().batched() res = qv.get_measurement(backend=bb) bb.dispatch() Parameters ---------- options : Mapping or None, optional Runtime options. Defaults to ``{"shots": 10000}``. Examples -------- :: from qrisp import QuantumVariable from qrisp.interface import StimBackend qv = QuantumVariable(2) qv[:] = "10" res = qv.get_measurement(backend=StimBackend()) print(res) # Yields: {'10': 1.0} """ @classmethod def _default_options(cls): return {"shots": 10000} def run_async(self, circuits, shots: int | list[int] | None = None) -> _StimJob: 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) default_shots: int = self._default_options()["shots"] n_shots: int | list[int] = shots if shots is not None else self.options.get("shots", default_shots) job = _StimJob(backend=self, circuits=circuits, shots=n_shots) job.submit() return job