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:

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=:optimized (default): in-place stride-loop kernels for 1-site and 2-site gates (adjacent or not), after the pattern of Yao.jl's u1apply! (credited in the top-level README). No reshape/permute, no per-gate copy of the state vector: a gate costs one pass over the local_dim^L amplitudes and allocates only two d^n-element buffers. Gates on 3+ sites (only a user MatrixGate can have them) fall back to the :builtin algorithm 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 region

Region 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.