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 asMPSBackend.retruncate) controls what a projective gate —Measure,Reset,Projection,SpinSectorProjection,SpinSectorMeasurement, any gate withneeds_normalization(gate) == true— does to the bonds it did not touch.:never(the new default) leaves them to the next gate that covers them;:gateruns the full-chaintruncate!(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 withcutoff/maxdimand 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. Passretruncate = :gateto recover the previous digits; theEXTENDED_TESTSgolden 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 nextorthogonalize!does not re-factorize tensors that are already orthonormal. Same state, different rounding: withretruncate = :gatethe v0.5.8 goldens are reproduced to $10^{-14}$–$10^{-13}$ (RNG fingerprints exactly), sotest/golden_compare.jlnow 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.jlpins the agreement on random gates, sites, and local dimensions and onReset/HaarRandomtrajectories. The default engine updatesstate.backend.ψin place, so a reference taken before a gate now sees the new amplitudes — takecopy(state.backend.ψ)for a snapshot (:builtinstill stores a fresh vector per gate). born_probabilityon the state-vector backend now rejects a site outside1:Lor an outcome outside0:local_dim-1with anArgumentError, as the Clifford and Gaussian methods already did (it used to return0.0for a nonexistent level, or $\lVert\psi\rVert^2$ for site0). TheBornProbabilityobservable already validated its outcome; this covers direct calls.- The MPS multi-site SVD chain calls
ITensors.firstindinstead of its deprecated aliasfindindex. 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 freshjuliaprocess, 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 trajectory Before After Clifford MIPT, L=16 5.7 s 0.33 s State vector CIPT, L=8 5.9 s 0.26 s MPS spin-1 AKLT, L=8 36 s 9.0 s Gaussian DIII Majorana chain, L=16 7.2 s 0.09 s using QuantumCircuitsMPSitself 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 aLocalPreferences.tomlentry — see CONTRIBUTING.md. No runtime behavior changes.State-vector gate kernels. The
:optimizedengine only had a stride-loop kernel for 1-site gates; every 2-site gate (HaarRandom,CZ, ...) went through the same reshape →permutedims→ matmul →permutedimspath as:builtin, three full copies of $\psi$ per gate. It now has an in-place 2-site stride kernel (adjacent or not, anylocal_dim), both kernels apply the $d^n \times d^n$ block with an inlined matvec instead of a BLASgemvcall per block, andProjection(theMeasure/Resethot 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-vectorborn_probabilityitself (two calls perMeasure/Reset) walks the strided runs of matching amplitudes instead of testing a base-ddigit of every index — same summation order, bitwise-identical values, 66 µs → 19 µs atL=16. Per gate atL=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:Workload Before After Speedup CIPT trajectory, L=12(Reset/HaarRandomstaircase,bc=:periodic,n_steps=L^2,Magnetization+TripartiteMutualInformationat the end)7.8 ms, 37 MB 3.7 ms, 2.4 MB 2.1x CIPT trajectory, L=160.32 s, 990 MB 0.096 s, 24 MB 3.3x the same L=16trajectory without the final observable (gates only)0.36 s 0.060 s 6x benchmark/apply!/haar2_one_bond_sv_L85.54 µs, 72 allocs 3.79 µs, 35 allocs 1.5x benchmark/simulate!/mipt_step_sv_L833.1 µs, 525 allocs 20.1 µs, 291 allocs 1.6x (
L=8vectors are 4 KB, so the kernel change matters little there; the gain grows with $2^L$.) Of the 0.096 s atL=16, 0.03 s is the singleTripartiteMutualInformationevaluation, whose subset partial trace still goes through a generic $L$-dimensionalpermutedims. 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 defaultretruncate = :neveronly the bonds inside the gate's range are factorized, andnormalize!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:Trajectory Before After Speedup Allocations MIPT, Haar + Measure(:Z)at p=0.15, L=20, maxdim=64, 20 steps1.62 s 0.85 s 1.9x 2.7 → 1.7 GB MIPT, p=0.3, L=64, maxdim=32, 4 steps 0.86 s 0.16 s 5.5x 2.5 → 0.44 GB AKLT forced measurement ( SpinSectorProjection, PBC), L=12, maxdim=50, p_nn=0.91.69 s 0.36 s 4.7x 1.6 → 0.62 GB AKLT, L=12, maxdim=128, p_nn=0.9 3.04 s 1.04 s 2.9x 2.4 → 1.2 GB AKLT, L=16, maxdim=50, p_nn=0.9 5.16 s 1.88 s 2.7x 5.3 → 1.9 GB With
retruncate = :gatethe per-gate work is unchanged apart from the gauge-centernormalize!(AKLT L=12, maxdim=128: 1.49 s → 1.19 s; single-siteMeasurealready had a cheapnormalize!, so the MIPT rows are within timing noise).Gaussian backend kernels. The per-gate cost of
GaussianHaar,Measure(:Z), andBondParityis 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)andBondParitynow 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, nolu/inv/condon 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 preallocatedstate.backend.scratchinstead 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 forcepurify!) 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 (
Lsites, $L$ steps, periodic; "DIII" = Majorana chain withBondParity/GaussianHaarbricklayers at $p = 0.5$, "fermionic" =GaussianHaarbricklayers withMeasure(:Z)andBondParityat $p = 0.3$):Case Before After Speedup Allocations DIII, $L = 32$ 6.8 ms 1.5 ms 4.5x 23.5 → 1.4 MB DIII, $L = 64$ 177 ms 9.3 ms 19x 307.5 → 5.0 MB fermionic, $L = 32$ 65 ms 4.0 ms 16x 115.5 → 3.2 MB fermionic, $L = 64$ 1045 ms 36 ms 29x 1686 → 12.5 MB What remains per gate is the rank-2 update itself, the per-gate purity check, and LAPACK's QR behind
haar_orthogonalon a 2×2/4×4 block.
[0.5.8] - 2026-10-09
Fixed
- Projective gates on the MPS backend (
Projection,SpinSectorProjection,SpinSectorMeasurement, and through themMeasureandReset) were rejected as impossible whenever the raw projected norm $\lVert P\psi\rVert^2$ fell belowPOSTSELECTION_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$ (theSpinSectorMeasurementsector 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
PauliXandReseton the Gaussian backend. Neither is a fermionic Gaussian operation (the GaussianPauliXchanged the fermion parity), so both now throw anArgumentError. The supported Gaussian gates areGaussianHaar,Measure(:Z), andBondParity.
Changed
Inputs that used to return a plausible but wrong result now throw an ArgumentError before the state changes:
Reseton a non-qubit site (on spin-1 sites it only flipped outcome 1).StringOrderon anysite_typeother 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)withk ≥ local_dim(returned0.0, or0.5on the Clifford backend).- A region with a repeated site, in
Sites,apply!, and every geometry (aRandomCliffordon such a region was non-unitary). SpinSectorMeasurementwith 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)underbc=:periodicat $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=...)overflowedInt64for large orders and could return0. Weights are now exact; results in the non-overflowing regime are bit-identical.initialize!(state, ProductState(...))onsite_type="Qudit"prepared the wrong product state on the MPS and state-vector backends (every bit was shifted up one level, sobinary_int=0gave $|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
EntanglementEntropydocstring misdescribedcut::Intunderbc=:periodic: on the MPS backendcutis a bond of the folded chain and the subsystem isstate.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, andDomainWall(and henceCorrelatorandMagnetizationFluctuations) 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 anArgumentError. StaircaseRight/StaircaseLeftignoredrangeduring 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 forrange > 1.range >= Lunder open boundaries is now rejected with anArgumentError. The defaultrange=1is unaffected.- Postselecting onto an outcome of zero Born probability (
Projection,SpinSectorProjection,SpinSectorMeasurement, user projectors) silently produced a zero MPS or aNaNstate vector. It now throws anArgumentErrorand leaves the state unchanged. Born-sampledMeasureandResetare unaffected. total_spin_projector(S; s)was numerically inaccurate for large spins (errors of order $10^{-3}$ at $s = 10$), soSpinSectorProjectionandSpinSectorMeasurementcould 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
mathblocks 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 vectorprobability— one entry per geometry element, inelements(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) orEntanglementEntropy(cut=[s1, s2, ...])(arbitraryVector{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, andbasebehave identically to the existingcut::Intbipartition path on each backend (Gaussian computes real-renyi_indexRényi-n entropy for bothcut::Intand region paths, from the same covariance-spectrum formula). - Gaussian Rényi-n entanglement entropy (bipartition
cut::Intand regioncut::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.TripartiteMutualInformationandEntropyProfilegain the same support by composition. - Documentation: new Custom Gates page (
docs/src/custom_gates.md) documenting theAbstractGateextension contract —support,build_operator(MPS),gate_matrix(state vector), the opt-inneeds_normalization/is_measurementtraits, and theexecute!full override — with three worked examples (a custom unitary running unchanged on both dense backends, a projective gate opting into renormalization, and anexecute!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 atsite_type="Majorana"on a periodic ring and tracking shift-averaged antipodal-quarter mutual information — plusexamples/gaussian_example.ipynbadded to the notebook table.
Changed
renyi_indexaccepts anyRealon all entropy observables (EntanglementEntropy,MutualInformation,TripartiteMutualInformation,EntropyProfile), normalized toFloat64with the normalized value required finite and> 0(was integer>= 1).n = 0(Hartley) andBoolremain rejected; each observable's docstring documents the small-n numerical caveat.- General-
nentropy kernels now evaluate in the log domain for numerical stability at extremen(e.g.n = 2048orn = floatmax(Float64)); then = 1von Neumann code path is unchanged bit-for-bit. EntanglementEntropyis now a parametric type,EntanglementEntropy{C<:Union{Int,Vector{Int}}}, dispatched on whethercutis anInt(bipartition) or aVector{Int}(region). Existing code usingEntanglementEntropy(cut=...)or annotating fields as::EntanglementEntropyis unaffected —EntanglementEntropyremains 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 informativeArgumentError;cut::Intbipartition remains the only MPS interface. - Existing
cut::Intbipartition behavior is unchanged and fully backward-compatible on all four backends.
Fixed
- MPS
born_probabilitywas double-normalized:ITensorMPS.expectalready 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 anyMeasuresampling 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)$innercontraction 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.mddown 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
SciMLStyleto 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-revsso 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;GaussianBackendholdscorr, ascratchbuffer,purify_tol(default1e-10), andmajoranas_per_site.apply!,track!,record!, andsimulate!work unchanged — only theSimulationState(...; 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 exceedspurify_tol. - Two new gates, dispatched only on the Gaussian backend (informative
ArgumentErrorrejection on MPS/state-vector/Clifford):GaussianHaar()— Haar-randomSO(4)rotation (fermionic-mode granularity) orSO(2)rotation (Majorana-chain granularity, $\exp(\theta\gamma_a\gamma_b)$, $\theta \sim U[0,2\pi)$), drawn from:gates_realizationand 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_measurementdraw per bond under the same redundant-draw contract asMeasure.
PauliX(),Measure(:Z), andReset()now also work on the Gaussian backend:PauliXflips 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}$), andReset()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 evenL), directly modeling the staggered class-DIII monitored Majorana-chain circuit family —GaussianHaar/BondParityonBricklayer(:odd)/Bricklayer(:even)reproduce the protocol with no new gate types. RandomGaussianStateinitial-state type: draws $O \in SO(2L)$ (or $SO(L)$ on the Majorana chain) from:state_initvia 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 != 1rejected),Magnetization(:Z)(fermionic-mode granularity only),BornProbability,MutualInformationandTripartiteMutualInformation(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), andEntropyProfile.StringOrder,DomainWall,PauliString,Correlator, andMagnetizationFluctuationsare rejected with an informativeArgumentError(no fermionic-Gaussian formula exists for these). - New Documenter.jl page
docs/src/backends/gaussian.mddocumenting the backend, and a substantially expandeddocs/src/devdocs/backend_interface.mddeveloper-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 fromPauliString.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 throughtrack!/record!).TripartiteMutualInformation(A, B, C)— $I_3 = I(A:B) + I(A:C) - I(A:BC)$ (Gullans–Huse MIPT convention), composed fromMutualInformation.MagnetizationFluctuations(region; axis=:Z)—Var(M)for $M = \sum_{i\in\text{region}} P_i$, composed fromPauliString.- Custom-observable API:
track!now also acceptsSymbol => <any callable>(not just a built-inAbstractObservable),record_valuehas an untyped fallback (record_value(obs, state) = obs(state)), and per-key storage auto-widens fromVector{Float64}toVector{Any}the first time a non-scalar value is recorded — this is what makes vector-valued observables likeEntropyProfilework without a new trait system. Seedocs/src/custom_observables.mdfor the contract and worked examples. - Arbitrary spin-S site types (
"S=k/2"forSup 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-siteMeasure(:Z)(reduces bitwise to the old binary Born-sampling algorithm atS=1/2),Projection(k)for any levelk,MatrixGate(U; d=...)for explicit local dimension, andtotal_spin_projector(S; s=...)/verify_spin_projectors(; s=...)generalized from the hardcodedS=1case to arbitrary spin-spairs via the Lagrange/Casimir projection formula. RandomStateVector(state-vector-backend random initialization, drawing from the:state_initRNG 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: trueon 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 (@docsblocks 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% ofnames(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_allandExplicitImportsrun as standing tests in everyPkg.test();JET.report_packageruns opt-in underJET_TEST=truewith 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 theFixedentries below, and the remainder are recorded as open questions in ROADMAP.md.
Fixed
RandomMPSnow respectsRNGRegistry's:state_initstream (previously ignored it and drew from the global RNG, so random initial states were not reproducible under a fixed seed).- Calling
StringOrderorDomainWallon a Clifford-backend state no longer crashes with a rawFieldError(no field mps/no field sites); both now raise an informativeArgumentErrorexplaining 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.cffwas out of sync (version: 0.1.0, staledate-released); now tracks this release.Bricklayer(:even)at odd system sizeLunder periodic boundary conditions no longer double-touches siteLwithin a single brickwork layer (it now leaves site 1 unpaired, mirroring:odd's existing behavior of leaving siteLunpaired). In addition, usingBricklayer(:odd)/Bricklayer(:even)with oddLunderbc=:periodicnow emits a one-time warning (at circuit-build time and on immediate-modeapply!): an odd ring has no valid brickwork tiling — each layer leaves one site unpaired (:odd→ siteL,:even→ site 1) and the wrap bond(L,1)is gated by neither layer, so an alternating:odd/:evencircuit is effectively open across that bond. Enumeration is unchanged;:nnand the NNN sublayers do not warn. Verify intended patterns withprint_circuit.SpinSectorProjectiongained agate_matrixmethod, so it now works on the state-vector backend (previouslyMethodError: no method matching gate_matrix(::SpinSectorProjection)— the AKLT Quick Start could not run withbackend=: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 negativeL) now throws an informativeArgumentErroron all backends (previously constructed a silent, unusable empty state).ProductState(binary_int=-1)(or any negative value) now throws an informativeArgumentError(previously accepted silently, andinitialize!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_measurementdraw 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: sameRNGRegistryseeds 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
Measurementgate. UseMeasure(: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, useMeasure(: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/SWAPnow reject non-qubit sites (local_dim != 2) with an informativeArgumentError, matching the existing guard onRx/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-1only 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
SimulationStatefield-forwardinggetpropertyshim (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 intest/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_digitstride 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_L83.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/SWAPITensor construction (reshape +ITensorconstructor instead of a 16-iteration scalarsetindex!loop): gate-construction alone went from 1.584 µs / 65 allocs to 1.167 µs / 39 allocs (1.36x faster, 40% fewer allocations); the fullapply!(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 insimulate!(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