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
AbstractObservable

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

source
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 at cut (1 <= cut < L). Supported on ALL backends (MPS, state vector, Clifford, Gaussian). This is the historical form; its cut SEMANTICS are unchanged (the renyi_index contract is not — see below, it now accepts any real index that normalizes to a finite Float64 > 0). Under bc=:periodic the MPS backend reads cut as a bond of its folded chain — see the note below.
  • Region (cut::AbstractRange or cut::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 an ArgumentError (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] at L = 4 (see the Gaussian backend guide).

Arguments

  • cut: either
    • cut::Int: bipartition cut (must satisfy 1 <= cut < L; cut < 1 throws at construction, cut >= L at call time). Under bc=:open this is the physical bipartition $\{1,\ldots,\mathrm{cut}\}$ versus $\{\mathrm{cut}+1,\ldots,L\}$ on every backend. Under bc=:periodic the 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 uses cut as a bond of its folded chain — see the note below. Or
    • cut::UnitRange/cut::Vector{Int}: a region of physical sites. Range semantics are INCLUSIVE site sets: cut=3:6 means sites {3,4,5,6}. The region is validated at construction (non-empty, no duplicates, all sites >= 1) and stored sorted as a Vector{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, when L is known.
  • renyi_index::Real=1: Rényi index for entropy. Accepts any Real, NORMALIZED TO Float64 at construction; the normalized value must be finite and > 0. Stating the contract on the normalized value is deliberate: a finite BigFloat("1e400") normalizes to Inf and a finite BigFloat("1e-400") to 0.0, so both are rejected even though each is a "finite real > 0" before conversion. Bool is 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 within 1e-8 of 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 as n = 2048 or n = floatmax(Float64) are finite and accurate rather than overflowing to Inf/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) is NOT supported

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) give log(maxdim) instead of log(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:

  1. Taking cut as a bond index of the stored (RAM-ordered) MPS (under OBC RAM order is physical order; under PBC see the note below)
  2. Orthogonalizing the MPS at the cut site
  3. Performing SVD to obtain Schmidt values
  4. Computing the entropy from the Schmidt spectrum
Periodic MPS: cut indexes a bond of the folded chain

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]):

cutMPS subsystemstate 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.

The MPS backend does NOT support regions

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)
source
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))
source
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).

source
QuantumCircuitsMPS.born_probability — Function
born_probability(state::SimulationState{GaussianBackend}, site::Int, outcome::Int) -> Float64

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

source
born_probability(state::SimulationState{MPSBackend}, physical_site::Int, outcome::Int) -> Float64

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

source
born_probability(state::SimulationState{StateVectorBackend}, physical_site::Int, outcome::Int) -> Float64

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

source
born_probability(state::SimulationState{CliffordBackend}, site::Int, outcome::Int) -> Float64

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

source
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 an ArgumentError on 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=2

References

  • AKLT (1987): Rigorous results on valence-bond ground states
  • String order distinguishes Haldane phase from trivial phases
source
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
source
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!
source
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 any Real, normalized to Float64 at construction; the normalized value must be finite and > 0 (so a finite BigFloat("1e400") — which normalizes to Inf — is rejected, as is Bool).
    • 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 at threshold^2 before taking logs), mirroring EntanglementEntropy
  • base::Real=ℯ: logarithm base (default natural log, so a Bell pair gives $I = 2\log(2) \approx 1.386$; use base=2 for 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 ArgumentError when $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 to Float64 at 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))
source
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))
source
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 any Real, normalized to Float64 at construction; the normalized value must be finite and > 0 (so a finite BigFloat("1e400"), which normalizes to Inf, is rejected, as is Bool). Mirrors EntanglementEntropy, which each per-cut evaluation reconstructs.
  • threshold::Float64=1e-16: singular-value floor (see EntanglementEntropy)
  • base::Real=ℯ: logarithm base (default natural log — NB: EntanglementEntropy itself defaults to base=2; pass base=2 for 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 from EntanglementEntropy on 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())
source
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 three MutualInformation terms, which perform the normalize-then-validate check (any Real, normalized to Float64, and the normalized value must be finite and > 0; Bool rejected). See MutualInformation'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))
source
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))
source
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]:

  • f MUST be side-effect-free with respect to the quantum state: it may freely READ state (e.g. via born_probability, PauliString, EntanglementEntropy, measurements(state)), but must not apply gates, measure, or consume RNG draws.
  • f may 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 f propagate out of record!. For non-AbstractObservable callables the error is wrapped in an ErrorException naming the observable key, with the underlying exception attached as caused by:.

Storage

  • AbstractObservable specs record into a Vector{Float64} (the built-in scalar contract). Should such an observable return a non-scalar value, the storage is transparently widened to Vector{Any} at that record.
  • Generic callables record into a Vector{Any} (their returns are not constrained to Float64).

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 closure

See the "Custom Observables" documentation page for worked examples and the public building blocks to compose.

source
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 tracked DomainWall was registered without an i1_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 an ArgumentError. Used by selective record!(c, names...) circuit markers.
source
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)
source
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"]
source

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):

ObservableFormulaBackend notes
PauliString$\langle \textstyle\prod_k P_k \rangle$ for a product of single-qubit Paulis on distinct sitesQubit-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 regionsPhysical-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).