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). - 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).
Arguments
cut: eithercut::Int: physical site where the bipartition cut is made (must satisfy1 <= cut < L;cut < 1throws at construction,cut >= Lat call time), 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:
- Converting the physical cut position to RAM ordering
- Orthogonalizing the MPS at the cut site
- Performing SVD to obtain Schmidt values
- Computing the entropy from the Schmidt spectrum
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.
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 loop-invariant stride d^(L-s) is hoisted out of the loop).
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.
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
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.
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.
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 caveat (cross-backend semantics)
On the MPS backend under bc=:periodic, each cut is the RAM bond index of the FOLDED-PBC MPS (src/Observables/entanglement.jl), NOT the physical prefix bipartition {1..cut} used by the state-vector, Clifford and Gaussian backends (only cut = L÷2 is fold-aligned). A periodic-BC MPS profile is therefore in RAM-bond coordinates and is NOT directly comparable to the other backends' physical-cut profiles. Cross-backend profile comparisons must use bc=:open, where all four backends agree on the physical bipartition.
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$ |
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).