Changelog

All notable changes to this project are documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning in spirit (pre-1.0, so breaking changes can land in minor versions).

[Unreleased]

[0.5.9] - 2026-10-14

Added

  • SimulationState(; retruncate = :never | :gate) (MPS backend only, stored as MPSBackend.retruncate) controls what a projective gate — Measure, Reset, Projection, SpinSectorProjection, SpinSectorMeasurement, any gate with needs_normalization(gate) == true — does to the bonds it did not touch. :never (the new default) leaves them to the next gate that covers them; :gate runs the full-chain truncate!(mps; cutoff) after every such gate, as every earlier version did.

Changed

  • Projective gates on the MPS backend no longer re-truncate the whole chain (retruncate = :never). A projective gate still SVD-truncates the bonds inside its range with cutoff/maxdim and renormalizes the state; the bonds outside its range keep their current dimension until a later gate covers them. Nothing is discarded, so the retained state is never less accurate; what changes is when bonds are compressed, so a v0.5.8 trajectory is reproduced only to its own truncation error ($10^{-10}$–$10^{-5}$ on the workloads below, well inside any ensemble error), not digit for digit. Pass retruncate = :gate to recover the previous digits; the EXTENDED_TESTS golden harness does so.
  • The MPS ortho limits now record the true gauge center after every multi-site gate (the last site of the gate's range, where the SVD chain leaves it), so normalize! after a projective gate rescales that one tensor instead of contracting the whole chain, and the next orthogonalize! does not re-factorize tensors that are already orthonormal. Same state, different rounding: with retruncate = :gate the v0.5.8 goldens are reproduced to $10^{-14}$–$10^{-13}$ (RNG fingerprints exactly), so test/golden_compare.jl now compares physics values at $10^{-12}$ instead of $10^{-14}$.
  • The state-vector backend's default gate-application engine is now engine=:optimized; engine=:builtin (the reshape/permutedims/matmul reference) stays available. The two engines sum the same products in a different order, so they agree to rounding ($\sim 10^{-15}$ per gate), not bitwise, and state-vector trajectories can differ from v0.5.8's at that level; test/statevector/engine_agreement.jl pins the agreement on random gates, sites, and local dimensions and on Reset/HaarRandom trajectories. The default engine updates state.backend.ψ in place, so a reference taken before a gate now sees the new amplitudes — take copy(state.backend.ψ) for a snapshot (:builtin still stores a fresh vector per gate).
  • born_probability on the state-vector backend now rejects a site outside 1:L or an outcome outside 0:local_dim-1 with an ArgumentError, as the Clifford and Gaussian methods already did (it used to return 0.0 for a nonexistent level, or $\lVert\psi\rVert^2$ for site 0). The BornProbability observable already validated its outcome; this covers direct calls.
  • The MPS multi-site SVD chain calls ITensors.firstind instead of its deprecated alias findindex. No numerical change.

Performance

  • Precompile workload (src/precompile.jl, via the new PrecompileTools dependency). The package now runs one tiny trajectory per backend at precompile time — built from the gates, geometries, observables, and recording modes of the four worked examples, plus the remaining public gates and observables, the feedback and event-log mechanisms, bc=:open, and circuit expansion / printing — so the specialized methods are saved in the package image instead of being JIT-compiled in every fresh session. Time to the first trajectory in a fresh julia process, single thread, Julia 1.12.6 (minimum over repeated interleaved runs on a shared machine; observables bit-identical before and after in every run):

    First trajectoryBeforeAfter
    Clifford MIPT, L=165.7 s0.33 s
    State vector CIPT, L=85.9 s0.26 s
    MPS spin-1 AKLT, L=836 s9.0 s
    Gaussian DIII Majorana chain, L=167.2 s0.09 s

    using QuantumCircuitsMPS itself is unchanged (3.4–4.6 s either way). The cost is a longer one-time precompile of the package itself (about 5 s → 2.5 min on an M4, once per install/update), which contributors can switch off locally with a LocalPreferences.toml entry — see CONTRIBUTING.md. No runtime behavior changes.

  • State-vector gate kernels. The :optimized engine only had a stride-loop kernel for 1-site gates; every 2-site gate (HaarRandom, CZ, ...) went through the same reshape → permutedims → matmul → permutedims path as :builtin, three full copies of $\psi$ per gate. It now has an in-place 2-site stride kernel (adjacent or not, any local_dim), both kernels apply the $d^n \times d^n$ block with an inlined matvec instead of a BLAS gemv call per block, and Projection (the Measure/Reset hot path) no longer copies $\psi$ to protect against a rejected postselection: $\lVert P\psi\rVert^2$ is the Born probability, read off before the in-place apply. The state-vector born_probability itself (two calls per Measure/Reset) walks the strided runs of matching amplitudes instead of testing a base-d digit of every index — same summation order, bitwise-identical values, 66 µs → 19 µs at L=16. Per gate at L=16 (BenchmarkTools minimum): 1-site 283 µs → 94 µs, 2-site 340 µs → 158 µs, with no $2^L$-sized allocation. Old default (:builtin) → new default, minimum over repeated runs of one seed, Apple M4, Julia 1.12.6:

    WorkloadBeforeAfterSpeedup
    CIPT trajectory, L=12 (Reset/HaarRandom staircase, bc=:periodic, n_steps=L^2, Magnetization + TripartiteMutualInformation at the end)7.8 ms, 37 MB3.7 ms, 2.4 MB2.1x
    CIPT trajectory, L=160.32 s, 990 MB0.096 s, 24 MB3.3x
    the same L=16 trajectory without the final observable (gates only)0.36 s0.060 s6x
    benchmark/ apply!/haar2_one_bond_sv_L85.54 µs, 72 allocs3.79 µs, 35 allocs1.5x
    benchmark/ simulate!/mipt_step_sv_L833.1 µs, 525 allocs20.1 µs, 291 allocs1.6x

    (L=8 vectors are 4 KB, so the kernel change matters little there; the gain grows with $2^L$.) Of the 0.096 s at L=16, 0.03 s is the single TripartiteMutualInformation evaluation, whose subset partial trace still goes through a generic $L$-dimensional permutedims. The two engines' observables agree to $10^{-15}$ on these trajectories.

  • MPS projective gates. The full-chain truncate! after every projective gate cost two $O(L)$ sweeps per gate, i.e. $O(pL^2)$ factorizations per layer of measurements at rate $p$ against $O(L)$ for the unitary layer, and parked the gauge center at site 1 so the next unitary paid $O(j)$ QRs to bring it back. With the default retruncate = :never only the bonds inside the gate's range are factorized, and normalize! rescales the gauge-center tensor instead of contracting the chain. Single thread, median of 3 trajectories, same seeds, before (v0.5.8) and after on the same machine:

    TrajectoryBeforeAfterSpeedupAllocations
    MIPT, Haar + Measure(:Z) at p=0.15, L=20, maxdim=64, 20 steps1.62 s0.85 s1.9x2.7 → 1.7 GB
    MIPT, p=0.3, L=64, maxdim=32, 4 steps0.86 s0.16 s5.5x2.5 → 0.44 GB
    AKLT forced measurement (SpinSectorProjection, PBC), L=12, maxdim=50, p_nn=0.91.69 s0.36 s4.7x1.6 → 0.62 GB
    AKLT, L=12, maxdim=128, p_nn=0.93.04 s1.04 s2.9x2.4 → 1.2 GB
    AKLT, L=16, maxdim=50, p_nn=0.95.16 s1.88 s2.7x5.3 → 1.9 GB

    With retruncate = :gate the per-gate work is unchanged apart from the gauge-center normalize! (AKLT L=12, maxdim=128: 1.49 s → 1.19 s; single-site Measure already had a cheap normalize!, so the MIPT rows are within timing noise).

  • Gaussian backend kernels. The per-gate cost of GaussianHaar, Measure(:Z), and BondParity is now $O(N^2)$ in the number of Majoranas $N$ instead of $O(N^3)$.

    • The purity diagnostic $\max\lvert\mathrm{diag}(\Gamma^2)+1\rvert$, evaluated after every gate and measurement, formed the full product $\Gamma\Gamma$; it is now computed from the diagonal alone. The re-antisymmetrization after a Haar rotation, the index-set complement, and the complement-block update inside the general contraction no longer allocate temporaries. These steps are bit-identical to v0.5.8.
    • Measure(:Z) and BondParity now use a closed-form parity projection (parity_projection!): for the 2-Majorana parity projector the general Schur-complement contraction reduces to a rank-2 update of the complement block, $\Gamma_{ij} \leftarrow \Gamma_{ij} + \frac{s}{1 - s\,\Gamma_{ab}}(\Gamma_{ia}\Gamma_{jb} - \Gamma_{ib}\Gamma_{ja})$, with rows and columns $a, b$ set to the projected sector — no index slices, no lu/inv/cond on 2×2 blocks, no BLAS calls, and the result is exactly antisymmetric. gaussian_contraction! remains the general-$\Upsilon$ kernel and the test reference. Because every operation now leaves $\Gamma$ exactly antisymmetric, the Haar rotation re-antisymmetrizes only the rotated rows and columns, the purity check is a contiguous column-norm sweep, and the rotation's row/column blocks are staged in the preallocated state.backend.scratch instead of four temporaries (same BLAS calls, same result). The projection's floating-point operation order differs from v0.5.8's, so results are not bit-identical, but on 26 trajectories (both site granularities, $L = 16$–$64$, including runs that force purify!) every measurement outcome is unchanged, covariance-matrix entries agree to $1.5\times10^{-14}$ and observables to $1.3\times10^{-13}$; the tolerance-based test suite is unchanged.

    Steady-state cost per trajectory, single thread (Julia and BLAS), GC included, Apple M4 / Julia 1.12.6 (L sites, $L$ steps, periodic; "DIII" = Majorana chain with BondParity/GaussianHaar bricklayers at $p = 0.5$, "fermionic" = GaussianHaar bricklayers with Measure(:Z) and BondParity at $p = 0.3$):

    CaseBeforeAfterSpeedupAllocations
    DIII, $L = 32$6.8 ms1.5 ms4.5x23.5 → 1.4 MB
    DIII, $L = 64$177 ms9.3 ms19x307.5 → 5.0 MB
    fermionic, $L = 32$65 ms4.0 ms16x115.5 → 3.2 MB
    fermionic, $L = 64$1045 ms36 ms29x1686 → 12.5 MB

    What remains per gate is the rank-2 update itself, the per-gate purity check, and LAPACK's QR behind haar_orthogonal on a 2×2/4×4 block.

[0.5.8] - 2026-10-09

Fixed

  • Projective gates on the MPS backend (Projection, SpinSectorProjection, SpinSectorMeasurement, and through them Measure and Reset) were rejected as impossible whenever the raw projected norm $\lVert P\psi\rVert^2$ fell below POSTSELECTION_PROB_TOL, even for an outcome of probability one: the MPS is not renormalized after truncated unitary layers, so $\lVert\psi\rVert^2$ itself can be that small. The guard now tests the Born probability $\lVert P\psi\rVert^2 / \lVert\psi\rVert^2$ (the SpinSectorMeasurement sector probabilities are likewise divided by $\langle\psi|\psi\rangle$), and its error message reports the probability with both norms. Results on normalized states are unchanged.

[0.5.7] - 2026-10-09

Removed

  • PauliX and Reset on the Gaussian backend. Neither is a fermionic Gaussian operation (the Gaussian PauliX changed the fermion parity), so both now throw an ArgumentError. The supported Gaussian gates are GaussianHaar, Measure(:Z), and BondParity.

Changed

Inputs that used to return a plausible but wrong result now throw an ArgumentError before the state changes:

  • Reset on a non-qubit site (on spin-1 sites it only flipped outcome 1).
  • StringOrder on any site_type other than "S=1" on the MPS and state-vector backends (the state vector used a qubit eigenvalue table; the MPS failed with an ITensor internal error).
  • ProductState(binary_int=n) with $n \ge 2^L$ (each backend prepared a different state).
  • BornProbability(site, k) with k ≥ local_dim (returned 0.0, or 0.5 on the Clifford backend).
  • A region with a repeated site, in Sites, apply!, and every geometry (a RandomClifford on such a region was non-unitary).
  • SpinSectorMeasurement with a repeated sector (the duplicate was double-weighted in the Born draw).

The Gaussian backend guide and the EntanglementEntropy/MutualInformation docstrings now state that Gaussian region entropies are entropies of sets of fermionic modes, which agree with the Jordan–Wigner spin entropies only when the region or its complement is contiguous. No numerical change; a cross-validation test pins the contract.

Fixed

  • Bricklayer(:nnn_odd_2) / Bricklayer(:nnn_even_2) under bc=:periodic at $L \equiv 2 \pmod 4$ now warn once that the sublayer is not a disjoint pair layer (its wrap pair shares a site with its last bulk pair). The gate enumeration is unchanged.
  • DomainWall(order=...) overflowed Int64 for large orders and could return 0. Weights are now exact; results in the non-overflowing regime are bit-identical.
  • initialize!(state, ProductState(...)) on site_type="Qudit" prepared the wrong product state on the MPS and state-vector backends (every bit was shifted up one level, so binary_int=0 gave $|11\cdots1\rangle$). "Qubit" and spin-$S$ sites were never affected.
  • The Gaussian backend's documented Majorana operators had the wrong sign relative to its covariance matrix. The guide, docstrings, and test oracle now state Bravyi's convention consistently ($\gamma_{2i-1} = c_i + c_i^\dagger$, $\gamma_{2i} = -i(c_i^\dagger - c_i)$, $\Gamma_{ab} = \frac{i}{2}\langle[\gamma_a,\gamma_b]\rangle$). No numerical change.
  • The EntanglementEntropy docstring misdescribed cut::Int under bc=:periodic: on the MPS backend cut is a bond of the folded chain and the subsystem is state.ram_phy[1:cut]; the other backends bipartition the physical prefix. The MPS guide, EntropyProfile, and the backend contract were aligned. No numerical change.

[0.5.6] - 2026-10-08

Fixed

  • MPS PauliString, StringOrder, and DomainWall (and hence Correlator and MagnetizationFluctuations) returned the unnormalized contraction $\langle\psi|O|\psi\rangle$ instead of the expectation value, so their values were scaled by the retained norm whenever truncation had occurred. Results on normalized states are unchanged; a zero-norm MPS now throws an ArgumentError.
  • StaircaseRight/StaircaseLeft ignored range during execution: two-site gates acted on (pos, pos+1) instead of the documented (pos, pos+range), and open-boundary advancement walked the pair off the end of the chain for range > 1. range >= L under open boundaries is now rejected with an ArgumentError. The default range=1 is unaffected.
  • Postselecting onto an outcome of zero Born probability (Projection, SpinSectorProjection, SpinSectorMeasurement, user projectors) silently produced a zero MPS or a NaN state vector. It now throws an ArgumentError and leaves the state unchanged. Born-sampled Measure and Reset are unaffected.
  • total_spin_projector(S; s) was numerically inaccurate for large spins (errors of order $10^{-3}$ at $s = 10$), so SpinSectorProjection and SpinSectorMeasurement could leak weight outside the requested sector. Projectors are now accurate to machine precision for every supported spin; $s = 1$ results are byte-identical, $s = 3/2$ and $s = 2$ change only at the $10^{-14}$ level.

[0.5.5] - 2026-08-14

Changed

  • Documentation: mathematical expressions in docstrings and on the Documenter site are now written as LaTeX — fenced math blocks for display equations and double-backtick inline math — instead of Unicode approximations, so formulas render as real typeset math on the docs site. Docstrings that carry TeX now use @doc raw""" where escaping would otherwise be needed. Runtime error messages and internal code comments keep their plain-text Unicode form. No API, numerical, or behavioral changes.

[0.5.4] - 2026-08-13

Added

  • apply_with_prob! accepts a vector probability — one entry per geometry element, in elements(geo, L, bc) order — so each gate location can have its own probability.

Changed

  • Invalid scalar probabilities (negative, NaN/Inf, or > 1) now throw instead of passing silently; only their sum was checked before.

[0.5.3] - 2026-07-26

Added

  • Region-based entanglement entropy on the StateVector, Clifford, and Gaussian backends: EntanglementEntropy(cut=c1:c2) (contiguous range) or EntanglementEntropy(cut=[s1, s2, ...]) (arbitrary Vector{Int}) computes the entropy of the reduced density matrix of any set of physical sites, including non-contiguous and periodic-boundary-wrapped regions (e.g. cut=[L-1, L, 1, 2]). renyi_index, threshold, and base behave identically to the existing cut::Int bipartition path on each backend (Gaussian computes real-renyi_index Rényi-n entropy for both cut::Int and region paths, from the same covariance-spectrum formula).
  • Gaussian Rényi-n entanglement entropy (bipartition cut::Int and region cut::Vector{Int}) and Rényi mutual information, computed from the covariance-matrix eigenvalue spectrum: $S_n = \frac{1}{1-n}\sum_k \ln\!\big[((1+\lambda_k)/2)^n + ((1-\lambda_k)/2)^n\big]$ over the paired spectrum, plus $\ln(2)/2$ per unpaired zero mode on odd-sized Majorana-chain regions. TripartiteMutualInformation and EntropyProfile gain the same support by composition.
  • Documentation: new Custom Gates page (docs/src/custom_gates.md) documenting the AbstractGate extension contract — support, build_operator (MPS), gate_matrix (state vector), the opt-in needs_normalization / is_measurement traits, and the execute! full override — with three worked examples (a custom unitary running unchanged on both dense backends, a projective gate opting into renormalization, and an execute! override that composes stock gates), a per-backend support table recording that the Clifford and Gaussian backends are closed whitelists while the two dense backends resolve any conforming subtype, and the verbatim rejection error. Registered in the site navigation under "Examples" and cross-linked from Custom Observables, the Gates API page, and Tutorials.
  • Documentation: Gaussian class-DIII monitored Majorana-chain tutorial in Tutorials — a standalone snippet running the staggered BondParity() / GaussianHaar() circuit at site_type="Majorana" on a periodic ring and tracking shift-averaged antipodal-quarter mutual information — plus examples/gaussian_example.ipynb added to the notebook table.

Changed

  • renyi_index accepts any Real on all entropy observables (EntanglementEntropy, MutualInformation, TripartiteMutualInformation, EntropyProfile), normalized to Float64 with the normalized value required finite and > 0 (was integer >= 1). n = 0 (Hartley) and Bool remain rejected; each observable's docstring documents the small-n numerical caveat.
  • General-n entropy kernels now evaluate in the log domain for numerical stability at extreme n (e.g. n = 2048 or n = floatmax(Float64)); the n = 1 von Neumann code path is unchanged bit-for-bit.
  • EntanglementEntropy is now a parametric type, EntanglementEntropy{C<:Union{Int,Vector{Int}}}, dispatched on whether cut is an Int (bipartition) or a Vector{Int} (region). Existing code using EntanglementEntropy(cut=...) or annotating fields as ::EntanglementEntropy is unaffected — EntanglementEntropy remains a valid UnionAll annotation — but code that wants a concrete-parameter annotation for the bipartition-only case can now write ::EntanglementEntropy{Int}.
  • The MPS backend does not support region-based entropy: EntanglementEntropy(cut=[...]) on an MPS state throws an informative ArgumentError; cut::Int bipartition remains the only MPS interface.
  • Existing cut::Int bipartition behavior is unchanged and fully backward-compatible on all four backends.

Fixed

  • MPS born_probability was double-normalized: ITensorMPS.expect already divides by $\langle \psi \vert \psi \rangle$ internally, so the additional / inner(ψ, ψ) inflated every probability by $1/\lVert\psi\rVert^2$ whenever the state was not normalized — which is the case right after a truncating unitary layer, since those are not renormalized. Reported values could therefore exceed 1, and the inflated probabilities fed the cumulative-probability outcome sampler. Sampled trajectories on normalized states ($\lVert\psi\rVert^2 = 1$, where the extra division was a no-op) are unaffected, but MPS Born probabilities and any Measure sampling in an actively truncating regime now differ numerically from earlier versions. The projector is also evaluated only at the requested site instead of at all $L$ sites, removing a separate $O(L)$ inner contraction from the measurement hot path.

[0.5.2] - 2026-07-15

Changed

  • Reorganized the Documenter site into an ITensors.jl-style layout: the landing page is now a full Introduction (installation, quick example, citation, known limitations), the API reference is split into per-topic pages under a "Documentation" nav group, and the changelog is part of the site navigation.
  • Slimmed README.md down to badges, pitch, installation, a quick example, and citation — the docs site is now the single source of truth for everything else.

[0.5.1] - 2026-07-15

Changed

  • Applied JuliaFormatter v2.10.1 with SciMLStyle to the package entrypoints, Gaussian backend implementation, and Gaussian test suite. This is a formatting-only maintenance release with no runtime behavior changes.
  • Recorded the bulk formatting commits in .git-blame-ignore-revs so line history remains useful across the mechanical rewrite.

[0.5.0] - 2026-07-13

This release adds a fourth simulation backend: a fermionic Gaussian (free-fermion) backend for circuits built entirely out of Gaussian-preserving gates and parity measurements, along with two new gates, a Majorana-chain site granularity for class-DIII circuits, and a matching documentation and test suite.

Added

  • Gaussian backend (backend=:gaussian): a pure fermionic Gaussian state is represented as a dense $2L \times 2L$ real antisymmetric Majorana covariance matrix $\Gamma$ ($\Gamma_{ab} = (i/2)\langle[\gamma_a,\gamma_b]\rangle$, satisfying $\Gamma^2 = -I$) instead of an MPS, state vector, or stabilizer tableau; GaussianBackend holds corr, a scratch buffer, purify_tol (default 1e-10), and majoranas_per_site. apply!, track!, record!, and simulate! work unchanged — only the SimulationState(...; backend=:gaussian) constructor call differs. Gate/measurement application is $O(L^3)$ per operation (Schur-complement contraction on $\Gamma$, no truncation error, exact for any circuit depth), and a re-purification step (purify!, eigen-clamping $i\Gamma$'s spectrum back to $\pm 1$) fires automatically whenever floating-point drift exceeds purify_tol.
  • Two new gates, dispatched only on the Gaussian backend (informative ArgumentError rejection on MPS/state-vector/Clifford):
    • GaussianHaar() — Haar-random SO(4) rotation (fermionic-mode granularity) or SO(2) rotation (Majorana-chain granularity, $\exp(\theta\gamma_a\gamma_b)$, $\theta \sim U[0,2\pi)$), drawn from :gates_realization and conjugated directly onto $\Gamma$.
    • BondParity() — projective bond-parity measurement $i\gamma_{2i}\gamma_{2i+1}$ between adjacent sites (fermionic-mode) or $i\gamma_i\gamma_{i+1}$ between adjacent Majorana sites (Majorana chain), with PBC wrap support; consumes one :born_measurement draw per bond under the same redundant-draw contract as Measure.
  • PauliX(), Measure(:Z), and Reset() now also work on the Gaussian backend: PauliX flips fermionic occupation parity (a single Majorana row/column sign flip), Measure(:Z) is a projective on-site occupation-parity measurement ($i\gamma_{2i-1}\gamma_{2i}$), and Reset() composes the two, matching the existing semantics on every other backend.
  • Majorana-chain site granularity (site_type="Majorana"): each site is one Majorana mode rather than one fermionic mode ($\Gamma$ is $L \times L$ instead of $2L \times 2L$; requires even L), directly modeling the staggered class-DIII monitored Majorana-chain circuit family — GaussianHaar/BondParity on Bricklayer(:odd)/Bricklayer(:even) reproduce the protocol with no new gate types.
  • RandomGaussianState initial-state type: draws $O \in SO(2L)$ (or $SO(L)$ on the Majorana chain) from :state_init via an exact Haar sampler, haar_orthogonal, using QR decomposition of a Ginibre matrix.
  • Gaussian-backend observable support: EntanglementEntropy (von Neumann only, covariance-matrix formula; renyi_index != 1 rejected), Magnetization(:Z) (fermionic-mode granularity only), BornProbability, MutualInformation and TripartiteMutualInformation (von Neumann only; the only backend that accepts non-contiguous or PBC-wrapped region pairs, since a covariance-matrix reduced state is just an index selection), and EntropyProfile. StringOrder, DomainWall, PauliString, Correlator, and MagnetizationFluctuations are rejected with an informative ArgumentError (no fermionic-Gaussian formula exists for these).
  • New Documenter.jl page docs/src/backends/gaussian.md documenting the backend, and a substantially expanded docs/src/devdocs/backend_interface.md developer-docs page covering the Gaussian method tables (initialize!, _apply_single!, _measure_single_site!, born_probability, observables).
  • examples/gaussian_example.ipynb / examples/gaussian_example.jl: reproduces the class-DIII phase diagram via the staggered monitored Majorana-chain protocol.
  • New test/gaussian/ suite (14 files): construction and rejection checks, gate and measurement tests, an oracle-based cross-validation against an independent reference implementation, golden-value regression tests, and Majorana-chain-specific coverage.

[0.4.0] - 2026-07-07

This release is a quality/hardening pass across the whole package: a systematic audit of every backend and observable, arbitrary spin-S support, four new composed observables, a custom-observable API, a benchmark suite, a full Documenter.jl site, and CI/quality gates (Aqua, JET, ExplicitImports). It also removes one redundant gate and tightens the public export surface.

Added

  • PauliString(i => :P, j => :P', ...) — multi-site Pauli-string expectation value observable, on the MPS, state-vector, and Clifford backends.
  • MutualInformation(regionA, regionB; renyi_index=1, base=ℯ) — $I(A:B) = S(A) + S(B) - S(A\cup B)$ for two disjoint contiguous site regions, on all three backends (MPS via subset-RDM contraction with a size guard, exact state vector via partial trace, Clifford via poly-time GF(2)-rank stabilizer entropies). Regions are physical sites on every backend and under both boundary conditions.
  • Correlator(i => :P, j => :P') — connected two-point correlator $\langle P_iP_j\rangle - \langle P_i\rangle\langle P_j\rangle$, composed from PauliString.
  • EntropyProfile(; renyi_index=1, base=ℯ) — vector observable returning [S(cut=x) for x in 1:L-1] in one call; the first vector-valued built-in observable (see the custom-observable API below for how the vector storage is threaded through track!/record!).
  • TripartiteMutualInformation(A, B, C) — $I_3 = I(A:B) + I(A:C) - I(A:BC)$ (Gullans–Huse MIPT convention), composed from MutualInformation.
  • MagnetizationFluctuations(region; axis=:Z) — Var(M) for $M = \sum_{i\in\text{region}} P_i$, composed from PauliString.
  • Custom-observable API: track! now also accepts Symbol => <any callable> (not just a built-in AbstractObservable), record_value has an untyped fallback (record_value(obs, state) = obs(state)), and per-key storage auto-widens from Vector{Float64} to Vector{Any} the first time a non-scalar value is recorded — this is what makes vector-valued observables like EntropyProfile work without a new trait system. See docs/src/custom_observables.md for the contract and worked examples.
  • Arbitrary spin-S site types ("S=k/2" for S up to 10, half-integer steps), on the MPS and state-vector backends: initialization at any level ("Z<m>" state names, e.g. "Z3/2", "Z-1/2"), Sz/S+/S- operators, per-level projectors ("Proj0" .. "Proj2S"), categorical (2S+1)-outcome single-site Measure(:Z) (reduces bitwise to the old binary Born-sampling algorithm at S=1/2), Projection(k) for any level k, MatrixGate(U; d=...) for explicit local dimension, and total_spin_projector(S; s=...) / verify_spin_projectors(; s=...) generalized from the hardcoded S=1 case to arbitrary spin-s pairs via the Lagrange/Casimir projection formula.
  • RandomStateVector (state-vector-backend random initialization, drawing from the :state_init RNG stream) is now exported.
  • Benchmark suite (benchmark/benchmarks.jl, AirspeedVelocity-compatible, 10 scope-table entries / 14 leaves) with a committed baseline (benchmark/results/baseline-5ab86ab.json) and a CI workflow (.github/workflows/benchmarks.yml, continue-on-error: true on pull requests).
  • Documenter.jl site (docs/): design philosophy, per-backend pages (MPS/state-vector/Clifford), tutorials (MIPT/CIPT/AKLT/feedback quick starts), a restructured API reference (@docs blocks per module area, plus an "Observables Catalog" and an "Arbitrary Spin-S Support" section), a private/internal API page, a developer-docs backend-interface contract page (docs/src/devdocs/backend_interface.md), and the custom-observables page. 100% of names(QuantumCircuitsMPS) are documented on the built site.
  • CI workflows: CI.yml, format-check.yml (JuliaFormatter v2, SciMLStyle), CompatHelper.yml, TagBot.yml, docs.yml (Documenter deploy), benchmarks.yml.
  • Quality gates: Aqua.test_all and ExplicitImports run as standing tests in every Pkg.test(); JET.report_package runs opt-in under JET_TEST=true with a documented, only-decreasing report-count ratchet.
  • Audit test suites (test/audit/) systematically cross-checking every backend/observable/gate against its documented contract; several findings from this audit are the Fixed entries below, and the remainder are recorded as open questions in ROADMAP.md.

Fixed

  • RandomMPS now respects RNGRegistry's :state_init stream (previously ignored it and drew from the global RNG, so random initial states were not reproducible under a fixed seed).
  • Calling StringOrder or DomainWall on a Clifford-backend state no longer crashes with a raw FieldError (no field mps / no field sites); both now raise an informative ArgumentError explaining that these observables are not supported on the stabilizer-tableau backend.
  • plot_circuit's Luxor SVG rendering of non-adjacent (NNN) gates: PBC-wrap labels no longer collide pixel-exactly with unrelated gates' labels at lattice center, and non-wrap NNN gate labels no longer get overdrawn by a later gate's box in the same packed layer; open-boundary long-range gates are no longer misclassified as periodic wraps.
  • CITATION.cff was out of sync (version: 0.1.0, stale date-released); now tracks this release.
  • Bricklayer(:even) at odd system size L under periodic boundary conditions no longer double-touches site L within a single brickwork layer (it now leaves site 1 unpaired, mirroring :odd's existing behavior of leaving site L unpaired). In addition, using Bricklayer(:odd)/Bricklayer(:even) with odd L under bc=:periodic now emits a one-time warning (at circuit-build time and on immediate-mode apply!): an odd ring has no valid brickwork tiling — each layer leaves one site unpaired (:odd → site L, :even → site 1) and the wrap bond (L,1) is gated by neither layer, so an alternating :odd/:even circuit is effectively open across that bond. Enumeration is unchanged; :nn and the NNN sublayers do not warn. Verify intended patterns with print_circuit.
  • SpinSectorProjection gained a gate_matrix method, so it now works on the state-vector backend (previously MethodError: no method matching gate_matrix(::SpinSectorProjection) — the AKLT Quick Start could not run with backend=:statevector).
  • Magnetization(:Z) on "S=1" (and other arbitrary spin-S) MPS/state-vector states no longer throws; it now correctly reports $\langle S_z\rangle \in [-S, S]$ (qubit and "S=1/2" sites keep the existing $\pm 1$ Pauli convention).
  • SimulationState(L=0, ...) (or negative L) now throws an informative ArgumentError on all backends (previously constructed a silent, unusable empty state).
  • ProductState(binary_int=-1) (or any negative value) now throws an informative ArgumentError (previously accepted silently, and initialize! produced a garbage state by parsing the - sign character as a state label).

Changed / BREAKING

  • BREAKING — Clifford Born-draw contract (redundant draw): the Clifford backend's measurement primitive now consumes exactly ONE :born_measurement draw per measured site — always — matching MPS/state-vector. When the stabilizer tableau fixes the outcome (deterministic measurement), the draw is made anyway and its value is discarded (a deliberate redundant draw). This restores absolute cross-backend reproducibility: same RNGRegistry seeds now produce the same measurement record on all three backends. Migration impact: seeded Clifford trajectories generated with earlier versions (which consumed zero draws for deterministic outcomes) are NOT reproducible under the new contract — a one-time break. MPS/state-vector trajectories are completely unaffected. Deterministic outcomes themselves are unchanged (still read off the tableau); only the RNG stream position differs.
  • BREAKING — removed: the Measurement gate. Use Measure(:Z) instead (drop-in replacement: same Born sampling, same one-draw-per-measurement contract, same "Meas" circuit label, bit-identical trajectories under the same seeds). For feedback, use Measure(:Z; feedback=OnOutcome(1 => PauliX())) etc. No deprecation alias is provided.
  • BREAKING — export surface: 11 internal symbols are no longer exported: advance!, get_sites, current_position, reset!, compute_site_staircase_right, compute_site_staircase_left, compute_pair_staircase, apply_op_internal!, compute_basis_mapping, physical_to_ram, ram_to_physical. They remain defined for debugging/introspection — use qualified access, e.g. QuantumCircuitsMPS.advance!(geo, L, bc).
  • BREAKING: CZ/CNOT/SWAP now reject non-qubit sites (local_dim != 2) with an informative ArgumentError, matching the existing guard on Rx/Ry/Rz/Hadamard. Previously these gates silently built an undocumented, non-standard qudit generalization when applied to $S \ge 1$ sites (e.g. a 3-level "CZ" that put -1 only on $\lvert 22\rangle$ instead of the standard $\omega^{jk}$ phases) — a silent-physics-trap that is now a clean error instead.
  • born_probability(state, site, outcome) is now a documented public export (promoted out of the internal-exports block).
  • The SimulationState field-forwarding getproperty shim (state.mps/state.sites/state.cutoff/state.maxdim, MPS backend only) is kept and is now explicitly documented as supported public API, not a deprecated compatibility layer — a usage census found 42 call sites in test/ alone, above the removal threshold.
  • BREAKING: Julia compatibility floor raised from 1.11 to 1.12 (required by QuantumClifford 0.11.5, which itself requires julia≥1.12 — the previously-declared 1.11 floor did not actually resolve).

Performance

Baseline: commit 5ab86ab (benchmark/results/baseline-5ab86ab.json), Julia 1.12.6. All comparisons below are before/after on the same machine.

  • State-vector observable loop optimization (shared _sv_digit stride helper + single-pass algorithms, L=12 random state): | Observable | Before | After | Speedup | |—|—|—|—| | Magnetization(:Z) | 86.7 µs | 2.62 µs | ~33x | | DomainWall(order=1) | 277.9 µs | 8.3 µs | ~33x | | DomainWall(order=2) | 277.8 µs | 7.7 µs | ~36x | | StringOrder(order=1) | 54.1 µs | 7.4 µs | ~7.3x | | StringOrder(order=2) | 53.0 µs | 14.8 µs | ~3.6x | | born_probability | 7.8 µs | 4.1 µs | ~1.9x |

    Corroborated by the benchmark suite: magnetization/sv_L8 3.417 µs → 208 ns (16.4x). All changes verified bitwise-identical or within 1e-15 of the old formulas on 20 random states; no formula or RNG behavior changed.

  • Vectorized CZ/CNOT/SWAP ITensor construction (reshape + ITensor constructor instead of a 16-iteration scalar setindex! loop): gate-construction alone went from 1.584 µs / 65 allocs to 1.167 µs / 39 allocs (1.36x faster, 40% fewer allocations); the full apply!(state, CZ(), AdjacentPair(...)) on MPS L=12 improved from 330.875 µs / 4802 allocs to 318.104 µs / 4749 allocs. Verified bitwise identical to the old operator construction on all 16 index combinations.

  • elements() caching in simulate! (step-invariant static geometries cached per circuit run instead of recomputed every step): | Scenario | Before allocs | After allocs | Δ | |—|—|—|—| | MIPT, state vector, L=8, 50 steps | 18,703 | 16,315 | -12.8% | | MIPT, Clifford, L=100, 20 steps | 173,361 | 163,627 | -5.6% | | MIPT, MPS, L=20, 20 steps | 2,163,183 | 2,161,106 | -0.1% (MPS SVD-dominated) |

    Bitwise-identical trajectories verified before/after (a lazy-vs-cached equality regression suite is now permanent, test/regression/elements_cache.jl).

[0.3.0] - 2026-07-05

Clifford (stabilizer-tableau) backend: CNOT, PhaseGate, SWAP, RandomClifford gates, and a Clifford-backend MIPT demo.

[0.2.1] - 2026-07-05

Removed a stale "Known Limitations" README entry.

[0.2.0] - 2026-07-05

State-vector backend, and periodic-boundary-condition fold alignment for the MPS backend.

[0.1.1] - 2026-07-04

Removed an orphaned imperative.jl; synced dev with main.

[0.1.0] - 2026-07-04

API refactor: MPS quantum circuit simulation core (the "v0.1" API — Gates, Geometry, Observables, the unified stochastic rule, SimulationState).

[0.0.7] - 2026-06-13

Hygiene fix.

[0.0.6] - 2026-06-11

Maintenance release.

[0.0.5] - 2026-06-10

Fixed AKLT plotting; updated the random seed for consistency across example notebooks.

[0.0.4] - 2026-06-08

Added citation metadata.

[0.0.3] - 2026-06-08

Fixed circuit plotting.

[0.0.2] - 2026-02-04

AKLT/qudit support, improved circuit visualization, comprehensive documentation.

[0.0.1] - 2026-01-31

Initial clean release, with CIPT and MIPT example notebooks.

[Unreleased]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.5.9...HEAD [0.5.9]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.5.8...v0.5.9 [0.5.8]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.5.7...v0.5.8 [0.5.7]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.5.6...v0.5.7 [0.5.6]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.5.5...v0.5.6 [0.5.5]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.5.4...v0.5.5 [0.5.4]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.5.3...v0.5.4 [0.5.3]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.5.2...v0.5.3 [0.5.2]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.5.1...v0.5.2 [0.5.1]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.5.0...v0.5.1 [0.5.0]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.2.1...v0.3.0 [0.2.1]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.1.1...v0.2.0 [0.1.1]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.1.0...v0.1.1 [0.1.0]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.0.7...v0.1.0 [0.0.7]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.0.6...v0.0.7 [0.0.6]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.0.5...v0.0.6 [0.0.5]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.0.4...v0.0.5 [0.0.4]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.0.3...v0.0.4 [0.0.3]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.0.2...v0.0.3 [0.0.2]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/compare/v0.0.1...v0.0.2 [0.0.1]: https://github.com/hainingpan/QuantumCircuitsMPS.jl/releases/tag/v0.0.1