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 \lesssim 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 \approx 25\text{-}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:
| System | local_dim | Memory |
|---|---|---|
| L=20 qubits | 2 | ≈ 16 MB |
| L=25 qubits | 2 | ≈ 512 MB |
| L=30 qubits | 2 | ≈ 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=:optimized(default): in-place stride-loop kernels for 1-site and 2-site gates (adjacent or not), after the pattern of Yao.jl'su1apply!(credited in the top-levelREADME). No reshape/permute, no per-gate copy of the state vector: a gate costs one pass over thelocal_dim^Lamplitudes and allocates only twod^n-element buffers. Gates on 3+ sites (only a userMatrixGatecan have them) fall back to the:builtinalgorithm applied in place.engine=:builtin: reshape → matrix-multiply → reshape-back, allocating a fresh vector per gate. Simple and easy to audit; the reference implementation the kernels are validated against.
state = SimulationState(L=20, bc=:open, backend=:statevector, engine=:builtin,
rng=RNGRegistry(gates_spacetime=1, gates_realization=2, born_measurement=3))The two engines compute the same products in a different order, so they agree to rounding ($\sim 10^{-15}$ per gate), not bitwise; test/statevector/engine_agreement.jl pins this on random gates, sites and local dimensions, and on full Reset/HaarRandom trajectories. Use :builtin when auditability matters most (e.g. debugging an observable formula); the default is the right choice for production sweeps — at L=16 its gate application is about six times faster and allocates no $2^L$-sized temporaries (numbers in the changelog).
Region Entanglement Entropy
Beyond the bipartition form (cut::Int), EntanglementEntropy also accepts an arbitrary set of physical sites on the state-vector backend — cut=c1:c2 for a contiguous range, or cut=[s1, s2, ...] for a non-contiguous or PBC-wrapped selection — and returns the entropy of that region's reduced density matrix:
using QuantumCircuitsMPS
L = 6
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))
apply!(state, HaarRandom(2), AdjacentPair(1))
apply!(state, HaarRandom(2), AdjacentPair(4))
EntanglementEntropy(cut=1:3)(state) # region {1, 2, 3}
EntanglementEntropy(cut=[1, 3, 5])(state) # non-contiguous regionRegion sites are always physical sites, so the region path is well-defined under both open and periodic boundary conditions, including PBC-wrapped regions like [L, 1]. This is a deliberate contrast with EntanglementEntropy(cut=k) on the MPS backend, whose cut addresses a RAM bond of the folded PBC chain rather than a physical bipartition (see the MPS Backend's PBC caveat) — the state-vector, Clifford, and Gaussian region path has no such caveat. The region path is more expensive than the bipartition path: it computes an arbitrary-subset reduced density matrix rather than reusing the cheap prefix reshape the cut::Int path relies on.