QrispSimulatorBackend#

class QrispSimulatorBackend(pm: PassManager | None = None)[source]#

The built-in Qrisp statevector simulator backend.

This is the simplest concrete Backend implementation. It executes circuits synchronously. That is, the QrispSimulatorJob returned by run_async() is already DONE before run_async() returns to the caller.

Note

The module-level singleton def_backend (an instance of this class) is used as the default backend throughout Qrisp when no explicit backend is specified. To change the global default, edit qrisp.default_backend.

Parameters:
pmPassManager or None, optional

An optional PassManager that is applied to every circuit before it is submitted to the simulator. This allows users to inject custom transpilation or optimisation passes into the simulation pipeline. Defaults to None.

Examples

Analytic execution (default)

We first create a QrispSimulatorBackend:

>>> from qrisp import QuantumFloat
>>> from qrisp.interface import QrispSimulatorBackend
>>> backend = QrispSimulatorBackend()

When get_measurement is called, Qrisp compiles the computation into a circuit and passes it to the built-in simulator via run_async(). A QrispSimulatorJob is returned immediately. And, because the simulator is synchronous the job is already DONE before get_measurement even calls result():

>>> qf = QuantumFloat(3)
>>> qf[:] = 4
>>> res = qf * qf
>>> res.get_measurement(backend=backend)
Simulating ... {16: 1.0}

With shots=None the result is an exact probability distribution, so {16: 1.0} means the outcome 16 has probability 1.0.

Using a PassManager to pre-process circuits

>>> from qrisp import PassManager
>>> from qrisp import convert_to_cz, fuse_adjacents
>>> pm = PassManager()
>>> pm += convert_to_cz()
>>> pm += fuse_adjacents
>>> backend = QrispSimulatorBackend(pm=pm)
>>> # Circuits are now passed through pm before simulation

Inspecting circuits with visualize when evaluating expectation values

pm is especially useful for understanding what circuits the simulator actually receives. By inserting visualize() as the last pass you can inspect every circuit just before it is executed. This can for instance be used when evaluating expectation values via QubitOperator.expectation_value: under the hood the operator groups terms by commutativity, appends change-of-basis gates, and submits one circuit per group — details that are invisible from the operator expression alone.

from qrisp import QuantumFloat, ry, PassManager, visualize, decompose
from qrisp.operators import X, Z
from qrisp.interface import QrispSimulatorBackend
import numpy as np

def state_prep(theta):
    qv = QuantumFloat(2)
    ry(theta, qv)
    return qv

H = X(0)*Z(1) + Z(0)*X(1) + X(0)

# Attach visualize at the end of the pipeline
pm = PassManager()
pm += decompose()
pm += visualize
backend = QrispSimulatorBackend(pm=pm)

ev_function = H.expectation_value(state_prep, backend=backend)
result = ev_function(np.pi/2)
       ┌─────────┐┌───┐┌─┐
 qv.0: ┤ Ry(π/2) ├┤ H ├┤M├
       ├─────────┤└┬─┬┘└╥┘
 qv.1: ┤ Ry(π/2) ├─┤M├──╫─
       └─────────┘ └╥┘  ║
cb_15: ═════════════╬═══╩═
                    ║
cb_16: ═════════════╩═════

       ┌─────────┐     ┌─┐
 qv.0: ┤ Ry(π/2) ├─────┤M├───
       ├─────────┤┌───┐└╥┘┌─┐
 qv.1: ┤ Ry(π/2) ├┤ H ├─╫─┤M├
       └─────────┘└───┘ ║ └╥┘
cb_21: ═════════════════╩══╬═
                           ║
cb_22: ════════════════════╩═

The measured operator contains three terms where two of them commute (X(0)*Z(1) and X(0)) and a third term that doesn’t commute (Z(0)*X(1)). Non-commuting terms can not be measured simultaneously so we need to distinct simulator calls.

Each circuit sent to the simulator is printed to stdout before execution — revealing the state preparation, the change-of-basis gates (e.g. Hadamards to rotate X to Z), and the qubit measurements.

Updating options after construction

Runtime options can be updated via update_options(). Only keys that were present at construction time may be modified:

>>> backend.update_options(shots=512)
>>> print(backend.options["shots"])
512

Using the Job interface directly

run_async() returns a QrispSimulatorJob that supports the full Job interface, even though the result is already available synchronously:

>>> from qrisp import QuantumFloat
>>> from qrisp.interface import QrispSimulatorBackend
>>> backend = QrispSimulatorBackend()
>>> qf3 = QuantumFloat(2)
>>> qf3[:] = 3
>>> res3 = qf3 * qf3
>>> qc = res3.qs.compile()
>>> qc.measure(qc.qubits)
>>> job = backend.run_async(qc)
Simulating ...
>>> print(job.status())
done
>>> result = job.result()
>>> print(result.get_counts())
{'0100111': 1.0}

QrispSimulatorJob#

class QrispSimulatorJob(backend: QrispSimulatorBackend, circuits: Sequence, shots: int | list[int] | None)[source]#

A synchronous Job produced by QrispSimulatorBackend.