Observables
Callable-struct observables (obs(state)), tracked via track!/record!. See Observables Catalog below for a backend-support summary of the new v0.4.0 observables, and Custom Observables for writing your own.
QuantumCircuitsMPS.AbstractObservable — Type
AbstractObservableAbstract base type for observable specifications.
Observables are CALLABLE STRUCTS: an instance holds the observable's parameters (e.g. the entanglement cut) and is invoked on a state as obs(state) -> Float64. Register one for recording with track!(state, :name => obs); record! then appends obs(state) (via the record_value hook) to state.observables[:name].
Backend support is per-method multiple dispatch: the generic call (obs::MyObs)(state) implements the MPS path, and backend-specific methods (obs::MyObs)(state::SimulationState{StateVectorBackend}) / (obs::MyObs)(state::SimulationState{CliffordBackend}) override it. An observable that cannot be computed on a backend defines a method throwing an informative ArgumentError instead (e.g. StringOrder on the Clifford backend) — see docs/src/devdocs/backend_interface.md for the full support matrix.
User-defined observables subtype AbstractObservable, implement (obs::MyObs)(state) -> Float64, and are then accepted by track! and recorded like the built-ins. Observables needing extra recording-time context override the record_value hook (see DomainWall).
Subtyping is OPTIONAL for custom observables: track! accepts ANY callable f(state) -> Number | AbstractVector (a closure, a function, or a callable struct) — see track! for the full callable contract. Subtype AbstractObservable when you want the built-in scalar Float64 storage and the ability to override record_value.
QuantumCircuitsMPS.EntanglementEntropy — Type
EntanglementEntropy(; cut, renyi_index::Real=1, threshold::Float64=1e-16, base::Real=2)Entanglement entropy observable.
cut selects between two forms via multiple dispatch on its TYPE, giving a parametric observable EntanglementEntropy{C} with C === Int or C === Vector{Int}:
- Bipartition (
cut::Int) — entropy of the bipartition atcut(1 <= cut < L). Supported on ALL backends (MPS, state vector, Clifford, Gaussian). This is the historical form; itscutSEMANTICS are unchanged (therenyi_indexcontract is not — see below, it now accepts any real index that normalizes to a finiteFloat64 > 0). Underbc=:periodicthe MPS backend readscutas a bond of its folded chain — see the note below. - Region (
cut::AbstractRangeorcut::AbstractVector{<:Integer}) — entropy of the reduced density matrix of an arbitrary set of PHYSICAL sites. Supported on the state-vector, Clifford and Gaussian backends only; the MPS backend throws anArgumentError(see below). On the Gaussian backend the region is a set of fermionic modes and the entropy is read off the restricted covariance matrix; this equals the Jordan–Wigner spin entropy of the same sites when the region or its complement is contiguous, but not for a doubly non-contiguous region such as[1, 3]atL = 4(see the Gaussian backend guide).
Arguments
cut: eithercut::Int: bipartition cut (must satisfy1 <= cut < L;cut < 1throws at construction,cut >= Lat call time). Underbc=:openthis is the physical bipartition $\{1,\ldots,\mathrm{cut}\}$ versus $\{\mathrm{cut}+1,\ldots,L\}$ on every backend. Underbc=:periodicthe state-vector, Clifford, and Gaussian backends keep that physical meaning (the ring arc bounded by the bonds(cut, cut+1)and(L, 1)), and the MPS backend usescutas a bond of its folded chain — see the note below. Orcut::UnitRange/cut::Vector{Int}: a region of physical sites. Range semantics are INCLUSIVE site sets:cut=3:6means sites{3,4,5,6}. The region is validated at construction (non-empty, no duplicates, all sites>= 1) and stored sorted as aVector{Int}— entropy is permutation-invariant, so sorting also canonicalizes PBC-wrapped input such as[L-1, L, 1, 2]. Bounds against the system size (max <= L) and properness (length < L) are checked at CALL time, whenLis known.
renyi_index::Real=1: Rényi index for entropy. Accepts anyReal, NORMALIZED TOFloat64at construction; the normalized value must be finite and> 0. Stating the contract on the normalized value is deliberate: a finiteBigFloat("1e400")normalizes toInfand a finiteBigFloat("1e-400")to0.0, so both are rejected even though each is a "finite real > 0" before conversion.Boolis rejected outright.renyi_index=1: von Neumann entropy $S_1 = -\sum_i \lambda_i \log(\lambda_i)$ (default)renyi_index=2: Rényi-2 entropy $S_2 = \log\!\big(\sum_i \lambda_i^2\big) / (1-2)$renyi_index=n: Rényi-n entropy $S_n = \log\!\big(\sum_i \lambda_i^n\big) / (1-n)$ for any real $n \neq 1$. Indices within1e-8of 1 evaluate the von Neumann formula instead (its continuous limit; error O(1e-8)), and the general branch is evaluated in a scale-before-overflow log domain, so extreme indices such asn = 2048orn = floatmax(Float64)are finite and accurate rather than overflowing toInf/NaN.
threshold::Float64=1e-16: Minimum threshold for singular values (default: 1e-16)base::Real=2: Base of logarithm for entropy computation (default: 2 for bits)
Hartley entropy (renyi_index=0) measures $\log_2(\text{Schmidt rank})$, but is not available via this interface because:
- MPS compression retains singular values above a cutoff threshold (~1e-10)
- Numerically, "zero" singular values are never truly zero in floating-point arithmetic
- This makes
log(rank)givelog(maxdim)instead oflog(true_rank) - Result is threshold-dependent and unreliable
Alternative: Access MPS singular values directly via orthogonalize! + svd, then apply your own threshold to determine the Schmidt rank.
The same mechanism degrades CONTINUOUSLY as $n \to 0^+$, so small $0 < n \ll 1$ is accepted but should be read with care: on the MPS and state-vector backends the probability FLOOR (threshold^2 per clamped Schmidt value) dominates $\sum p^n$ as $n \to 0^+$ — a floor of 1e-32 contributes $(1\text{e-32})^n$, i.e. $\approx 6\text{e-4}$ at n = 0.1 but $\approx 0.5$ at n = 0.01 — so the reported entropy becomes threshold-dependent long before n reaches 0. On the Gaussian backend there is no floor, but covariance-eigenvalue roundoff makes the $n \to 0^+$ rank limit numerically sensitive in the same way. The Clifford backend is EXEMPT: its GF(2)-rank entropy is exact and independent of every accepted n (stabilizer entanglement spectra are flat).
Implementation (MPS, cut::Int)
The entropy is computed by:
- Taking
cutas a bond index of the stored (RAM-ordered) MPS (under OBC RAM order is physical order; under PBC see the note below) - Orthogonalizing the MPS at the cut site
- Performing SVD to obtain Schmidt values
- Computing the entropy from the Schmidt spectrum
Under bc=:periodic the MPS backend stores the ring in the zig-zag folded order of src/Core/basis.jl, and cut is a bond index of that chain. The subsystem is the first cut sites in RAM, state.ram_phy[1:cut] — a contiguous ring arc growing outward from the fold seam at pbc_fold_start (default L÷4+1). The state-vector, Clifford, and Gaussian backends have no fold and bipartition the physical prefix $\{1,\ldots,\mathrm{cut}\}$. For L = 8 with the default fold (ram_phy = [3, 2, 4, 1, 5, 8, 6, 7]):
cut | MPS subsystem | state vector / Clifford / Gaussian |
|---|---|---|
| 1 | {3} | {1} |
| 2 | {2, 3} | {1, 2} |
| 3 | {2, 3, 4} | {1, 2, 3} |
| 4 | {1, 2, 3, 4} | {1, 2, 3, 4} |
| 6 | {8, 1, 2, 3, 4, 5} | {1, 2, 3, 4, 5, 6} |
With the default fold the half cut cut = L÷2 is the physical half $\{1,\ldots,L/2\}$ on every backend (one neighbouring cut is aligned as well); sort(state.ram_phy[1:cut]) == 1:cut tells whether a given cut is. For cross-backend comparison at other cuts use bc=:open; on the non-MPS backends the region form cut = 1:k names the physical prefix explicitly.
MPS entanglement entropy comes from the SVD of a single bond adjacent to the orthogonality center, which is exactly a bipartition — an arbitrary site region has no such native representation. Evaluating a region observable on an MPSBackend state throws an ArgumentError; use backend=:statevector, :clifford, or :gaussian for region entropies.
Example
ee = EntanglementEntropy(; cut=2, renyi_index=1) # bipartition, all backends
entropy = ee(state)
ee_region = EntanglementEntropy(; cut=3:6) # sites {3,4,5,6}, non-MPS
ee_region.cut # [3, 4, 5, 6]
EntanglementEntropy(; cut=[8, 1, 2, 7]).cut # [1, 2, 7, 8] (sorted)QuantumCircuitsMPS.Magnetization — Type
Magnetization(axis::Symbol)Average single-site expectation value along axis.
Computes $M_a = \frac{1}{L}\sum_i\langle \mathrm{axis}_i\rangle$ where $\mathrm{axis} \in \{X, Y, Z\}$.
Example
track!(state, :Mz => Magnetization(:Z))QuantumCircuitsMPS.BornProbability — Type
BornProbability(site::Int, outcome::Int)Observable for Born rule probability $P(\text{outcome} \mid \text{state})$ at a physical site.
outcome is a level index 0 .. local_dim-1 (0 or 1 for qubits). It is checked against the state's local_dim when the observable is evaluated: a nonexistent level is an ArgumentError, never a silent 0.0 (or, on the Clifford backend, a silent 0.5).
QuantumCircuitsMPS.born_probability — Function
born_probability(state::SimulationState{GaussianBackend}, site::Int, outcome::Int) -> Float64Compute the Born probability of measuring occupation outcome (0 = unoccupied, 1 = occupied) at physical site for a fermionic Gaussian state.
For a Gaussian state the on-site occupation probability is an affine function of a SINGLE covariance-matrix element $g = \Gamma[2i-1,2i]$ (with i the RAM-mapped mode index):
\[P(0)=\frac{1+g}{2},\qquad P(1)=\frac{1-g}{2}\]
(equivalently $P(1) = \langle c_i^\dagger c_i\rangle$, verified convention: vacuum has g = +1 ⇒ P(0) = 1.0 exactly). This is a NON-DESTRUCTIVE, read-only query — a single matrix-element read, no copies, state.backend.corr is untouched.
The raw affine value is returned without clamping: for a purified state $\lvert g\rvert \le 1$ up to machine precision, so any excursion outside [0, 1] is at the 1e-15 level and harmless to the rand() < p₀ threshold convention.
born_probability(state::SimulationState{MPSBackend}, physical_site::Int, outcome::Int) -> Float64Compute the Born probability P(outcome | state) at a physical site:
\[P(k) = \frac{\langle\psi|P_k|\psi\rangle}{\langle\psi|\psi\rangle} \,, \qquad P_k = |k\rangle\langle k| .\]
physical_site is converted to its RAM index via state.phy_ram (handles the :periodic fold). ProjK is defined for "Qubit" by ITensors and for the spin site types by src/Core/spin_sites.jl.
Non-mutating (neither the tensors nor the orthogonality limits are touched), and correctly normalized even when $\lVert\psi\rVert^2 \neq 1$ — e.g. after a truncated unitary layer, which is not renormalized. Evaluates only the requested site, but still pays the gauge walk to it, so this is a local query rather than an $O(1)$ one. Throws on a zero-norm MPS.
This is the MPS-backend implementation. SimulationState{StateVectorBackend} and SimulationState{CliffordBackend} have their own, more specific overrides (see src/StateVector/measurement.jl, src/Clifford/measurement.jl); narrowing this signature to MPSBackend ensures any future/unknown backend gets a clear MethodError here instead of silently crashing on a backend-specific field (state.backend.mps) that doesn't exist.
born_probability(state::SimulationState{StateVectorBackend}, physical_site::Int, outcome::Int) -> Float64Compute Born probability $P(\text{outcome} \mid \psi)$ at physical_site for the state-vector backend.
For a state vector ψ of length d^L, sums |ψ_n|² over all basis integers n (0-indexed, 0 to d^L - 1) whose base-d digit at position physical_site equals outcome. Site 1 is MSB (slowest index).
Digit extraction: for 0-indexed basis integer n, physical site s (1-indexed, site 1 = MSB), and local dimension d: digit = (n ÷ d^(L-s)) % d (see _sv_digit). The amplitudes with that digit equal to outcome form runs of stride = d^(L-s) consecutive entries, one per block of stride*d, so the loop walks those runs directly (no per-amplitude division) in ascending index order — the same summation order as a digit test over all n, hence the same Float64 result. This is also the $\lVert P\psi\rVert^2$ of the Projection postselection guard (_sv_projected_norm2), so Measure/Reset call it twice per site.
born_probability(state::SimulationState{CliffordBackend}, site::Int, outcome::Int) -> Float64Compute the Born (Z-measurement) probability of outcome (0 or 1) at physical site for a stabilizer state. Any other outcome is an ArgumentError: a qubit has no third level, and the undetermined branch below would otherwise report 0.5 for it.
For a stabilizer state, a single-qubit Z measurement is either DETERMINISTIC (probability exactly 0.0 or 1.0) or perfectly UNDETERMINED (probability exactly 0.5 for either outcome) — no other value is possible, a mathematical fact of the stabilizer formalism. This function is a NON-DESTRUCTIVE, read-only query: it operates on a copy of the tableau via QuantumClifford.projectZ!, so state.backend.tableau is left unmodified.
QuantumCircuitsMPS.StringOrder — Type
StringOrder(i::Int, j::Int; order::Int=1)String order parameter observable for spin-1 chains.
Formulas
order=1 (nearest-neighbor AKLT):
\[O^1(i,j) = \big\langle S_z[i]\, \exp\!\big(i\pi \textstyle\sum_{k=i+1}^{j-1} S_z[k]\big)\, S_z[j]\big\rangle\]
Expected: $|O^1| \approx 4/9 \approx 0.444$ for NN AKLT ground state
order=2 (next-nearest-neighbor AKLT):
\[O^2(n,m) = \big\langle S_z[n]\cdot S_z[n+1]\, \exp\!\big(i\pi \textstyle\sum_{k=n+2}^{m-2} S_z[k]\big)\, S_z[m-1]\cdot S_z[m]\big\rangle\]
Expected: $|O^2| \approx (4/9)^2 \approx 0.198$ for NNN AKLT ground state
Arguments
i: First site index (physical indexing)j: Second site index (physical indexing, must be j > i)order: 1 for NN formula (default), 2 for NNN formula with paired endpoints
Notes
- order=2 requires j >= i+4 (for non-overlapping endpoint pairs)
- NNN AKLT creates two decoupled chains; paired endpoints project onto both
- Defined for spin-1 chains only (
site_type="S=1"): $S_z$ and $\exp(i\pi S_z)$ are the spin-1 operators. Evaluating it on any other site type (qubits, higher spins, qudits) throws anArgumentErroron the MPS and state-vector backends alike; Clifford and Gaussian states reject it regardless of site type.
Example
s = SimulationState(L=8, bc=:periodic, site_type="S=1")
initialize!(s, ProductState(binary_int=0))
# ... apply AKLT protocol ...
so1 = compute(StringOrder(1, 5), s) # order=1 (default)
so2 = compute(StringOrder(1, 7, order=2), s) # order=2References
- AKLT (1987): Rigorous results on valence-bond ground states
- String order distinguishes Haldane phase from trivial phases
QuantumCircuitsMPS.DomainWall — Type
DomainWall(; order::Int, i1_fn::Union{Function, Nothing}=nothing)Domain wall observable for CT model. Only supports xj=Set([0]) case (first "1" in bit string).
The domain wall computes:
\[DW = \sum_j (L-j+1)^{\text{order}} \, P(\text{first } 1 \text{ at position } j \text{ starting from } i_1)\]
where position j is measured cyclically starting from i1.
Parameters:
- order: The order of the domain wall ($\geq 1$)
i1_fn: Optional function that returns the sampling site i1 when called If provided, record! can be called without i1 parameter
QuantumCircuitsMPS.PauliString — Type
PauliString(ops::Pair{Int,Symbol}...)Expectation value $\langle \textstyle\prod_i P_i\rangle$ of a product of single-qubit Pauli operators.
Each argument is a site => pauli pair with pauli ∈ (:X, :Y, :Z); identity is implied on every site not listed. Sites must be distinct and positive; range validation against the system size happens at evaluation time. Supported on all three backends (MPS, state vector, Clifford).
Qubit-only in v0.4.0: evaluating on a non-qubit state (e.g. site_type="S=1") throws an ArgumentError (spin-S operator strings are on the roadmap).
Sign convention
Matches Magnetization: $\langle Z_i\rangle = +1$ on $|0\rangle$ and $-1$ on $|1\rangle$ at site i.
Normalization
The value is always $\langle\psi|P|\psi\rangle / \langle\psi|\psi\rangle$. On the MPS backend a truncating unitary layer leaves the stored MPS with $\langle\psi|\psi\rangle < 1$ (unitaries are not renormalized); the division makes the result the expectation value of the retained state, consistent with born_probability and Magnetization.
Examples
state = SimulationState(L=4, bc=:open,
rng=RNGRegistry(gates_spacetime=1, gates_realization=2, born_measurement=3))
initialize!(state, ProductState(binary_int=0))
PauliString(1 => :Z)(state) # ⟨Z₁⟩ = +1.0 on |0000⟩
PauliString(1 => :Z, 4 => :Z)(state) # ⟨Z₁Z₄⟩ = +1.0
track!(state, :zz => PauliString(1 => :Z, 2 => :Z)) # record during simulate!QuantumCircuitsMPS.MutualInformation — Type
MutualInformation(regionA, regionB; renyi_index=1, threshold=1e-16, base=ℯ)Mutual information $I(A\!:\!B) = S(A) + S(B) - S(A\cup B)$ between two site regions.
Each region is a non-empty, duplicate-free collection of physical sites (a UnitRange like 2:4, a single Int, or a Vector{Int}; stored sorted), and the two regions must be DISJOINT. Regions refer to physical sites on all backends and under all boundary conditions.
On the MPS, state-vector, and Clifford backends each region must additionally be CONTIGUOUS (a plain ascending range): non-contiguous input is accepted at construction (so one observable object can serve every backend) but rejected with an ArgumentError when EVALUATED on those backends. The Gaussian (covariance-matrix) backend supports arbitrary site subsets — including non-contiguous and PBC-wrapped regions such as [7, 8, 1, 2] at L=8. There the regions are sets of fermionic modes, so for two non-adjacent blocks the result is the fermionic mutual information, which differs in general from the Jordan–Wigner spin mutual information of the same sites (the three entropies agree only when each region, or its complement, is contiguous).
Arguments
regionA,regionB: the two site regions (contiguous, disjoint)renyi_index::Real=1: entropy index used for all three terms. Accepts anyReal, normalized toFloat64at construction; the normalized value must be finite and> 0(so a finiteBigFloat("1e400")— which normalizes toInf— is rejected, as isBool).renyi_index=1: von Neumann entropies (default)renyi_index=n, real $n \neq 1$: Rényi-n entropies. NOTE: the Rényi "mutual information" $I_n = S_n(A)+S_n(B)-S_n(A\cup B)$ is NOT guaranteed non-negative for $n \neq 1$ — it is a commonly used diagnostic, not a proper mutual information. Documented, not forbidden.
threshold::Float64=1e-16: singular-value floor (probabilities are clamped atthreshold^2before taking logs), mirroringEntanglementEntropybase::Real=ℯ: logarithm base (default natural log, so a Bell pair gives $I = 2\log(2) \approx 1.386$; usebase=2for bits)
Backend cost
- MPS: $S(A\cup B)$ for disjoint A, B requires a two-block reduced density matrix, contracted with cost/memory $\sim \chi^2 d^{|A|+|B|}$ ($\chi$ = bond dimension). A size guard throws an informative
ArgumentErrorwhen $d^{|A|+|B|} > 256$ (e.g. more than 8 qubits combined). - StateVector: exact dense partial trace — practical for L ≲ 20 only (memory/time scale as d^L).
- Clifford: poly-time GF(2)-rank stabilizer entropies; every Rényi index gives the same value (flat entanglement spectrum).
- Gaussian: three covariance-submatrix eigendecompositions, $O((|A|+|B|)^3)$ — arbitrary site subsets; any real
renyi_index(normalized toFloat64at construction, normalized value finite and> 0) is supported, computed from the covariance spectrum.
Properties (analytic anchors)
- Product state: I = 0
- Pure global state with $B = \mathrm{complement}(A)$: $I = 2S(A)$
- Bell pair, $A = \{1\}$, $B = \{2\}$: $I = 2\log(2)$
- GHZ(4), $A = \{1\}$, $B = \{4\}$: $I = \log(2)$
Examples
mi = MutualInformation(1:2, 5:6) # blocks {1,2} and {5,6}
mi = MutualInformation(1, 4; base=2) # single sites, result in bits
value = mi(state)
track!(state, :I => MutualInformation(1, 4))QuantumCircuitsMPS.Correlator — Type
Correlator(pi::Pair{Int,Symbol}, pj::Pair{Int,Symbol})Connected two-point correlator $C(i,j) = \langle P_iP_j\rangle - \langle P_i\rangle\langle P_j\rangle$.
Each argument is a site => pauli pair with pauli ∈ (:X, :Y, :Z), matching PauliString's argument style. The two sites must be DISTINCT (the self-correlation $C(i,i)$ with $P_i^2 = I$ is identically $1 - \langle P_i\rangle^2$ and is not computed here — an ArgumentError is thrown for i == j).
Supported on all three backends (composition of three PauliString evaluations, which dispatch per backend). Qubit-only in v0.4.0 (inherited from PauliString).
Properties (analytic anchors)
- Product state |0…0⟩: $C(i,j) = 1 - 1\cdot 1 = 0$ for Z operators
- Bell pair,
Correlator(1 => :Z, 2 => :Z)= $1 - 0\cdot 0 = 1$
Examples
c = Correlator(1 => :Z, 4 => :Z)
value = c(state)
track!(state, :czz => Correlator(1 => :Z, 2 => :Z))QuantumCircuitsMPS.EntropyProfile — Type
EntropyProfile(; renyi_index=1, threshold=1e-16, base=ℯ)Entanglement-entropy profile: the vector [S(cut=x) for x in 1:L-1] of bipartite entropies at every cut, computed by the existing per-cut EntanglementEntropy on each backend.
Arguments
renyi_index::Real=1: Rényi index for all cuts (1 = von Neumann). Accepts anyReal, normalized toFloat64at construction; the normalized value must be finite and> 0(so a finiteBigFloat("1e400"), which normalizes toInf, is rejected, as isBool). MirrorsEntanglementEntropy, which each per-cut evaluation reconstructs.threshold::Float64=1e-16: singular-value floor (seeEntanglementEntropy)base::Real=ℯ: logarithm base (default natural log — NB:EntanglementEntropyitself defaults tobase=2; passbase=2for bits)
Backend cost
- MPS: $O(L)$ orthogonalized MPS copies — $O(L^2\chi^3)$ total
- StateVector: $O(L)$ dense reshapes + SVDs
- Clifford: $O(L)$ tableau copies + GF(2)-rank computations
- Gaussian: $O(L)$ covariance-submatrix eigendecompositions — $O(L^4)$ total (real
renyi_index, inherited fromEntanglementEntropyon that backend)
PBC convention (cross-backend semantics)
On the MPS backend under bc=:periodic, each cut is a bond index of the folded MPS (src/Observables/entanglement.jl), i.e. the ring arc state.ram_phy[1:cut] growing from the fold seam, whereas the state-vector, Clifford and Gaussian backends bipartition the physical prefix {1..cut} (with the default fold the half cut L÷2 and one neighbouring cut coincide; see the PBC note in EntanglementEntropy's docstring). A periodic-BC MPS profile is therefore indexed by RAM bond rather than by physical cut; cross-backend profile comparisons use bc=:open, where all four backends agree at every cut.
Recording
Returns a Vector{Float64} (one entry per cut). When tracked, each record point appends the whole vector as ONE entry; the observable storage is transparently widened from Vector{Float64} to Vector{Any} at the first record (see track!).
Examples
ep = EntropyProfile(; base=2)
profile = ep(state) # Vector{Float64} of length L-1
track!(state, :Sx => EntropyProfile())QuantumCircuitsMPS.TripartiteMutualInformation — Type
TripartiteMutualInformation(A, B, C; renyi_index=1, threshold=1e-16, base=ℯ)Tripartite mutual information $I_3 = I(A\!:\!B) + I(A\!:\!C) - I(A\!:\!BC)$ between three site regions (Gullans–Huse MIPT convention).
Each region must be a CONTIGUOUS, non-empty range of physical sites, the three regions must be pairwise DISJOINT, and $B \cup C$ must itself be contiguous (i.e. B and C adjacent) — inherited from MutualInformation's contiguous-region constraint on the I(A:BC) term. Note that A and C need NOT be adjacent: I(A:C) uses MutualInformation's two disjoint blocks.
Sign convention (MIPT usage)
The standard diagnostic partitions the chain into four QUARTERS A, B, C, D and computes $I_3(A\!:\!B\!:\!C)$ with D traced out — NOT equal thirds of the whole system: for ANY pure state, a full tripartition ($A\cup B\cup C = \text{everything}$) gives $I_3 \equiv 0$ identically, so it carries no information (useful only as a consistency check). With a fourth region traced out, scrambled volume-law states give $I_3 \leq 0$ (the MIPT scrambling diagnostic), while e.g. GHZ-type global correlations give $I_3 > 0$: GHZ(L=8) with A=1:2, B=3:4, C=5:6 (D=7:8 traced) gives $I_3 = +\log 2$.
Arguments
A,B,C: the three site regions (contiguous, pairwise disjoint, B and C adjacent)renyi_index::Real=1,threshold::Float64=1e-16,base::Real=ℯ: forwarded UNVALIDATED to all threeMutualInformationterms, which perform the normalize-then-validate check (anyReal, normalized toFloat64, and the normalized value must be finite and > 0;Boolrejected). SeeMutualInformation's docstring; Rényi-n I₃ for real $n \neq 1$ is a diagnostic, not a proper mutual information.
Backend cost
Inherits MutualInformation: on the MPS backend the largest term I(A:BC) requires $d^{|A|+|B|+|C|} \leq 256$ (e.g. $\leq 8$ qubits combined); state vector is exact dense (L ≲ 20); Clifford is poly-time; Gaussian is three covariance-submatrix eigendecompositions per term, $O((|A|+|B|+|C|)^3)$.
Under bc = :periodic every backend reads the three regions as PHYSICAL sites (inherited from MutualInformation), so I₃ is cross-backend unambiguous — unlike EntanglementEntropy's MPS cut, which is a RAM bond index of the folded MPS.
Examples
tmi = TripartiteMutualInformation(1:2, 3:4, 5:6) # quarters of L=8
value = tmi(state)
track!(state, :I3 => TripartiteMutualInformation(1:2, 3:4, 5:6; base=2))QuantumCircuitsMPS.MagnetizationFluctuations — Type
MagnetizationFluctuations(region; axis=:Z)Variance of the total magnetization $M = \sum_{i\in R} P_i$ over the site region R, with P the Pauli operator selected by axis (:X, :Y, or :Z).
Computed as
\[\mathrm{Var}(M) = |R| + \sum_{i\neq j} \langle P_iP_j\rangle - \Big(\sum_i \langle P_i\rangle\Big)^2\]
using $P_i^2 = I$ for the diagonal of $\langle M^2\rangle$ (the analytic constant |R|), so the formula never evaluates a same-site Pauli-string product. This is $O(\lvert R\rvert^2)$ PauliString evaluations. Supported on all three backends (composition of PauliString, which dispatches per backend); qubit-only in v0.4.0 (inherited from PauliString).
region may be any collection of distinct positive sites (a range like 1:6, an Int, or a vector — contiguity is NOT required, since M is a sum of single-site operators).
Properties (analytic anchors)
- Product Z-basis state,
axis=:Z: Var = 0 (M is sharp) - GHZ(L=6), R=1:6,
axis=:Z: $\langle Z_iZ_j\rangle=1$ for all $i\neq j$ and $\langle Z_i\rangle=0$, so $\mathrm{Var} = 6 + 30 - 0 = 36$ exactly
Examples
vm = MagnetizationFluctuations(1:6) # Var of Σ Zᵢ over sites 1..6
value = vm(state)
track!(state, :varM => MagnetizationFluctuations(2:5; axis=:X))QuantumCircuitsMPS.track! — Function
track!(state::SimulationState, spec::Pair{Symbol, <:Any})Register an observable to be tracked. Values are stored in state.observables[name], one entry per record point.
The pair's value may be a built-in observable instance (any AbstractObservable, e.g. EntanglementEntropy, Magnetization, PauliString) — or any callable f(state): a closure, a plain function, or a user-defined callable struct.
The callable contract
At every record point (an eager record!(state) call, or the points selected by simulate!'s record_when policy), each tracked observable is invoked as f(state) (through the record_value hook) and the returned value is appended to state.observables[name]:
fMUST be side-effect-free with respect to the quantum state: it may freely READstate(e.g. viaborn_probability,PauliString,EntanglementEntropy,measurements(state)), but must not apply gates, measure, or consume RNG draws.fmay return a scalar (Number) or a vector (AbstractVector), e.g. a per-site profile. Each record point appends exactly ONE entry (a returned vector is stored as a single element, not splatted).- Errors thrown by
fpropagate out ofrecord!. For non-AbstractObservablecallables the error is wrapped in anErrorExceptionnaming the observable key, with the underlying exception attached ascaused by:.
Storage
AbstractObservablespecs record into aVector{Float64}(the built-in scalar contract). Should such an observable return a non-scalar value, the storage is transparently widened toVector{Any}at that record.- Generic callables record into a
Vector{Any}(their returns are not constrained toFloat64).
Examples
track!(state, :dw1 => DomainWall(order=1)) # built-in observable
track!(state, :p1 => s -> born_probability(s, 1, 0)) # custom closure
track!(state, :zprofile =>
s -> [PauliString(i => :Z)(s) for i in 1:s.L]) # vector-valued closureSee the "Custom Observables" documentation page for worked examples and the public building blocks to compose.
QuantumCircuitsMPS.record! — Function
record!(state::SimulationState; i1::Union{Int,Nothing}=nothing, only=nothing)Compute tracked observables and append values to state.observables.
Each observable's value is obtained through the record_value hook (default obs(state); DomainWall uses i1_fn/i1 — see record_value). Errors thrown by a tracked observable propagate; for generic callables (non-AbstractObservable specs, see track!) they are wrapped in an ErrorException naming the observable key.
Keyword Arguments
i1: Explicit DomainWall sampling site (required when the trackedDomainWallwas registered without ani1_fn)only: Optional collection of tracked-observable names (Symbols). When given, ONLY those observables are recorded — the others' vectors do not grow. Naming an untracked observable throws anArgumentError. Used by selectiverecord!(c, names...)circuit markers.
record!(builder::CircuitBuilder)
record!(builder::CircuitBuilder, names::Symbol...)Insert a recording MARKER into the circuit at this position (v0.1).
Stores a pseudo-operation: (type=:record_mark, names=Symbol[names...]).
Markers are pure annotations: they never draw from any RNG stream, never advance the gate_idx element-slot counter, and never touch staircase positions. They fire only under record_when=:marks (which records the tracked observables exactly at each marker position) or inside custom predicates (which receive RecordingContexts with at_mark=true).
With no names, ALL tracked observables are recorded at the marker. With explicit names (e.g. record!(c, :entropy)), only the named observables are recorded there — each observable's vector grows at its own cadence. Naming an observable that is not tracked on the state raises an ArgumentError at simulate! time.
simulate! refuses (ArgumentError) to run a marker-containing circuit under record_when ∈ (:every_step, :every_gate, :final_only) — those policies would silently ignore the markers.
Example
circuit = Circuit(L=L, bc=:periodic) do c
apply!(c, HaarRandom(), Bricklayer(:even))
apply_with_prob!(c; outcomes=[(probability=p, gate=Measure(:Z), geometry=EachSite(2:L-1))])
record!(c) # record all tracked observables here
apply!(c, HaarRandom(), Bricklayer(:odd))
record!(c, :entropy) # record only :entropy here
end
simulate!(circuit, state; n_steps=25, record_when=:marks)QuantumCircuitsMPS.list_observables — Function
list_observables() -> Vector{String}Return a list of available observable type names.
Returns the names of all observable types that can be used with the tracking API.
Example:
obs_types = list_observables()
# Returns: ["DomainWall", "BornProbability"]Observables Catalog
New in v0.4.0: PauliString and MutualInformation, plus four observables composed on top of them. All six are supported on all three backends (MPS, state vector, Clifford):
| Observable | Formula | Backend notes |
|---|---|---|
PauliString | $\langle \textstyle\prod_k P_k \rangle$ for a product of single-qubit Paulis on distinct sites | Qubit-only; MPS via local op insertion, SV via direct amplitude action, Clifford via QuantumClifford.expect (poly-time, exact ∈ {−1,0,+1}) |
MutualInformation | $I(A\!:\!B) = S(A)+S(B)-S(A\cup B)$ for two contiguous, disjoint regions | Physical-site regions on every backend, cross-backend unambiguous under both boundary conditions (unlike EntanglementEntropy's PBC cut); MPS size-guarded at $d^{\lvert A\rvert+\lvert B\rvert} \le 256$. On the Gaussian backend the regions are sets of fermionic modes, and for non-adjacent $A$, $B$ the value is the fermionic MI rather than the Jordan–Wigner spin MI — see Fermionic vs. spin subsystems |
Correlator | $C(i,j)=\langle P_iP_j\rangle-\langle P_i\rangle\langle P_j\rangle$ | Pure composition of three PauliString calls; i == j rejected |
EntropyProfile | $[S(\text{cut}=x)\ \text{for}\ x=1..L-1]$ (vector-valued) | Composition of EntanglementEntropy at every cut; inherits its MPS PBC RAM-bond caveat — use bc=:open for cross-backend comparison |
TripartiteMutualInformation | $I_3 = I(A\!:\!B)+I(A\!:\!C)-I(A\!:\!BC)$ (Gullans–Huse convention) | Composition of three MutualInformation calls; B∪C must be contiguous; standard MIPT usage is four quarters with a fourth region traced out |
MagnetizationFluctuations | $\mathrm{Var}(M)$ for $M=\sum_{i\in R}P_i$ | $O(\lvert R\rvert^2)$ composition of PauliString; diagonal $P_i^2=I$ special-cased analytically |
Vector-valued observables (EntropyProfile) record through the same track!/record!/simulate! pipeline as scalar ones — the storage container transparently widens from Vector{Float64} to Vector{Any} at the first vector-valued record (see Custom Observables and track!'s docstring).