State Vector Backend

Alongside the default MPS backend, QuantumCircuitsMPS.jl also ships an exact, dense state-vector backend. It stores the full wavefunction as a Vector{ComplexF64} and applies gates by direct matrix multiplication, no SVD, no bond-dimension truncation, no cutoff/maxdim bookkeeping. Every gate, measurement, and observable that works on the MPS backend works identically here (apply!, track!, record!, simulate! are all backend-agnostic); only the SimulationState(...) constructor call changes.

When to use it:

  • Cross-validating MPS results against an exact reference with zero truncation error
  • Small systems (L ≲ 25 qubits) where the dense wavefunction fits comfortably in RAM
  • Producing exact reference trajectories for debugging suspected MPS truncation artifacts

When not to use it: anything beyond L ≈ 25-27 qubits, or whenever you need the MPS backend's L=100+ scalability (see MPS Backend). Memory grows as local_dim^L, exponentially, with no way around it for a dense representation.

Quick Example

using QuantumCircuitsMPS

# Exact state-vector simulation for small systems (L ≲ 25)
L = 8
state = SimulationState(L=L, bc=:open, backend=:statevector,
    rng=RNGRegistry(gates_spacetime=1, gates_realization=2, born_measurement=3))
initialize!(state, ProductState(binary_int=0))
track!(state, :entropy => EntanglementEntropy(cut=L÷2))

apply!(state, HaarRandom(2), AdjacentPair(L÷2))
record!(state)
println("Entropy: $(state.observables[:entropy][end])")

Physics: AdjacentPair(L÷2) places the Haar-random two-qubit gate directly across the entanglement cut at L÷2, so the resulting nonzero entropy is the exact entanglement generated by that single gate, no truncation, unlike an MPS run at finite maxdim.

Memory Requirements

The state vector is dense: local_dim^L × 16 bytes (ComplexF64, 16 bytes per amplitude), with no compression:

Systemlocal_dimMemory
L=20 qubits2≈ 16 MB
L=25 qubits2≈ 512 MB
L=30 qubits2≈ 16 GB

The MPS backend's memory usage instead scales with bond dimension (maxdim), staying roughly flat in L — this is why MPS remains the default for L=100+ production runs, while the state-vector backend serves as an exact, small-L cross-validation and debugging tool.

Engine Selection

The state-vector backend has two interchangeable gate-application engines, chosen via the engine keyword:

  • engine=:builtin (default): reshape → matrix-multiply → reshape-back. Simple and easy to audit; the reference implementation every correctness check is validated against.
  • engine=:optimized: a stride-loop gate-application kernel that skips the reshape/permute overhead, faster for larger L. Produces bitwise-identical results to engine=:builtin on the same input (see the Yao.jl pattern it's based on, credited in the top-level README).
state = SimulationState(L=20, bc=:open, backend=:statevector, engine=:optimized,
    rng=RNGRegistry(gates_spacetime=1, gates_realization=2, born_measurement=3))

Use :builtin when auditability matters most (e.g. debugging an observable formula); reach for :optimized once you're pushing L toward the upper end of the state-vector backend's practical range and want faster gate application.