Simulation

MicroQuantum ships four real local simulators plus a deterministic mock. All of them implement one execution contract, so a circuit runs identically across engines - only the memory scaling, the noise support and the feature breadth differ.

Simulator overview

Backend

Engine

Representation

Noise

Sampling

StatevectorBackend

statevector

2**n complex amplitudes

no

full-distribution numpy.default_rng draws

DensityMatrixBackend

density_matrix

2**n x 2**n complex matrix

yes (NoiseModel)

full-distribution numpy.default_rng draws

MPSBackend

mps

n tensors, bond dimension chi

no

sequential (peeling) Born-rule sampling

TreeTensorNetworkBackend

ttn

tensor tree, leaves per qubit

no

capped at 18 qubits (dense reconstruction)

MockBackend

mock

stub

no

stub counts

Each backend advertises its engine through capabilities.metadata and its gate set through target:

from microquantum import (
    DensityMatrixBackend, MPSBackend, StatevectorBackend,
    TreeTensorNetworkBackend,
)
from microquantum.backends.capabilities import EXECUTION_DENSITY_MATRIX

for backend in (
    StatevectorBackend(), DensityMatrixBackend(),
    MPSBackend(), TreeTensorNetworkBackend(),
):
    caps = backend.capabilities
    print(backend.name, "->", caps.metadata["engine"])
    print("   target:", backend.target.name)
    print("   density-matrix execution:", caps.supports_execution(EXECUTION_DENSITY_MATRIX))

Execution contract

Every simulator backend implements run(circuit) / run_circuit(...) with the same shots rules:

  • shots defaults to 1024 and must be a positive integer - 0, negative values and non-numeric values raise ValueError instead of being silently accepted.

  • shots=None requests deterministic execution: no sampling happens, counts is {}, samples is None and the exact final state (state vector for the statevector/MPS/TTN engines, plus the density matrix for the density-matrix engine) is returned.

  • seed makes sampling reproducible: the same circuit, shots and seed reproduce the same counts.

from microquantum import QuantumCircuit, StatevectorBackend

qc = QuantumCircuit(2).h(0).cnot(0, 1)

exact = StatevectorBackend().run(qc, shots=None)
print(exact.counts, exact.samples, exact.shots)  # {} None None
print(abs(exact.statevector[0]) ** 2)            # 0.5

seeded = StatevectorBackend().run(qc, shots=1000, seed=42)
again = StatevectorBackend().run(qc, shots=1000, seed=42)
print(seeded.counts == again.counts)             # True

Density-matrix simulator

The density-matrix engine tracks the full 2**n x 2**n matrix, so it can represent noisy and mixed states. For a pure circuit it is exactly equivalent to the state vector, and it exposes off-diagonal (coherence) elements:

import numpy as np
from microquantum import DensityMatrixBackend, StatevectorBackend

qc = QuantumCircuit(2).h(0).cnot(0, 1)

sv = StatevectorBackend().run(qc, shots=None).statevector
rho = DensityMatrixBackend().run(qc, shots=None).density_matrix
print(np.max(np.abs(np.abs(sv) ** 2 - np.real(np.diag(rho)))))  # ~0
print(abs(rho[0, 3]))                            # 0.5 (entanglement)

Noise

The density-matrix backend is the sanctioned noise vehicle. Build a NoiseModel, then pass it through DensityMatrixBackend.run_circuit(..., noise_model=...). With shots=None the exact noisy probabilities are returned without sampling:

from microquantum import DensityMatrixBackend, NoiseModel

x = [QuantumCircuit(1).x(0).gates[0][0].matrix]          # one X gate
model = NoiseModel().depolarizing(0.6)
result = DensityMatrixBackend().run_circuit(
    num_qubits=1, gates=[(x[0], [0])], noise_model=model, shots=None,
)
print(np.real(np.diag(result.density_matrix)))           # P(0) ~ 0.4

Unsupported options fail loudly: passing a non-NoiseModel raises TypeError, and combining a noise model with a backend in the Executor (which would silently ignore one of them) raises ValueError.

from microquantum import Executor

try:
    Executor(noise_model="junk")
except TypeError as exc:
    print("TypeError:", exc)

Tensor-network simulators

The MPS engine keeps memory polynomial in the qubit count. A bond-dimension cap trades a small, tracked fidelity loss for massive compression:

from microquantum import MPSBackend

ladder = QuantumCircuit(20)
ladder.h(0)
for q in range(19):
    ladder.cx(q, q + 1)

result = MPSBackend(max_bond_dim=1).run_circuit(
    num_qubits=20,
    gates=[(op.matrix, targets) for op, targets in ladder.gates],
    shots=None,
)
print(result.metadata["max_bond_dim"], result.metadata["truncation_error"])

The tree tensor network reconstructs a dense state vector for verification. Because that reconstruction needs 2**n memory, TreeTensorNetwork.sample is capped at 18 qubits and raises ValueError above it, pointing you to MatrixProductState.sample (sequential, memory-scalable sampling):

from microquantum import MatrixProductState, TreeTensorNetwork

try:
    TreeTensorNetwork.from_zeros(19).sample(16)
except ValueError as exc:
    print("ValueError:", exc)
counts = MatrixProductState.from_zeros(19).sample(16, seed=1)
print(sum(counts.values()))                         # 16

Memory boundaries

Dense allocations are checked before memory is requested: the SDK refuses a state vector larger than 2**31 bytes (16 * 2**27) and a density matrix that would exceed the same budget, instead of letting the process run out of memory. StateVector(28) and DensityMatrix(14) therefore raise ValueError.

from microquantum import StateVector

try:
    StateVector(28)                      # ~4 GiB, over the 2 GiB budget
except ValueError as exc:
    print("refused:", type(exc).__name__)

Cross-simulator consistency

All four simulators agree on the physics. For a given seeded run the state-vector and TTN engines share the same numpy.default_rng sampling path (identical counts), while the MPS peeling sampler is statistically consistent:

from microquantum import TreeTensorNetworkBackend

bell = QuantumCircuit(2).h(0).cnot(0, 1)
sv_counts = StatevectorBackend().run(bell, shots=2000, seed=1).counts
tn_counts = TreeTensorNetworkBackend().run(bell).counts
print(sv_counts)
print("TTN without seed uses the default 1024 shots:",
      sum(tn_counts.values()))