Observables and diagnostics
Observables
EvolutionObserver collects measurements along the evolution and is itself the step_observer! callback:
obs = EvolutionObserver(
:chi => maxlinkdim,
:entropy => ψ -> entanglement_entropy(ψ),
:energy => (ψ, t) -> instantaneous_energy(channels, t, ψ),
)
observe!(obs, ψ0, 0.0) # optional: record the initial state
ψ = time_evolve(channels, ψ0, 0.0, 10.0; nsteps = 100, step_observer! = obs)
r = results(obs) # (; step, time, chi, entropy, energy)Each measurement is called as f(state) or, if it accepts two arguments, f(state, t). Element types are narrowed, so r.entropy is a Vector{Float64}. Pass every = k to record only every k-th step — use this for diagnostics that cost more than the evolution itself.
Adiabaticity diagnostics
Everything here is opt-in: no driver computes a diagnostic unless you put it in an observer.
| function | cost | what it tells you |
|---|---|---|
instantaneous_energy(ch, t, ψ) | one MPO application | ⟨H(t)⟩ |
energy_variance(ch, t, ψ) | one MPO application | ⟨H²⟩ − ⟨H⟩², zero on an eigenstate |
entanglement_entropy(ψ, b) | one SVD | von Neumann entropy across bond b |
instantaneous_gap(ch, t, ψ) | DMRG solve | E₁(t) − E₀(t) |
adiabatic_report(ch, t, ψ) | cheap by default | bundle; gap = true adds DMRG |
Energy variance is the cheap adiabaticity check. It vanishes exactly when the state is an instantaneous eigenstate, needs no ground-state solve, and never forms H² (it is computed from H|ψ⟩). Use it every step; reserve the DMRG-based gap for a sparse subset via every = k.
adiabatic_report(ch, t, ψ) # energy + variance, no DMRG
adiabatic_report(ch, t, ψ; gap = true) # adds gap, excess energy, GS fidelityReference
ITensorTDMPO.EvolutionObserver — Type
EvolutionObserver(:name => f, ...)Accumulates measurements along a time evolution. Each f is called on the state after every step — as f(state) or, if it accepts two arguments, as f(state, t) — and the results are collected under :name.
An EvolutionObserver is itself the callback the drivers expect, so pass it as step_observer!. Read the accumulated data back with results.
Pass every = k to record only every k-th step. Use this for diagnostics that cost more than the evolution itself — a DMRG-based gap, say — so they can be sampled sparsely without slowing the run. An explicit call to observe! always records, regardless of every.
Examples
obs = EvolutionObserver(
:Sz => ψ -> expect(ψ, "Sz"),
:entropy => ψ -> entanglement_entropy(ψ),
:chi => maxlinkdim,
:energy => (ψ, t) -> instantaneous_energy(channels, t, ψ),
)
observe!(obs, ψ0, 0.0) # record the initial state (optional)
ψ = time_evolve(channels, ψ0, 0.0, 10.0; nsteps = 100, step_observer! = obs)
r = results(obs) # (; step, time, Sz, entropy, chi, energy)
r.time, r.entropyITensorTDMPO.observe! — Function
observe!(obs::EvolutionObserver, state, t; step = <next>)Record one measurement row. The drivers call this for you; call it directly to record the initial state before evolving.
ITensorTDMPO.results — Function
results(obs::EvolutionObserver)The accumulated measurements as a named tuple of vectors, with step and time alongside one entry per measurement. Element types are narrowed, so results(obs).entropy comes back as a Vector{Float64} rather than Vector{Any}.
ITensorTDMPO.entanglement_entropy — Function
entanglement_entropy(ψ::MPS, b::Integer = length(ψ) ÷ 2; base = ℯ)Von Neumann entanglement entropy of the bipartition across bond b (between sites b and b+1). The Schmidt values are renormalized internally, so an unnormalized ψ is handled correctly.
Pass base = 2 for entropy in bits.
ITensorTDMPO.entanglement_profile — Function
entanglement_profile(ψ::MPS; base = ℯ)entanglement_entropy across every bond, as a vector of length length(ψ) - 1.
ITensorTDMPO.instantaneous_energy — Function
instantaneous_energy(channels::DrivingChannels, t, ψ::MPS; cutoff, maxdim)The expectation value ⟨ψ|H(t)|ψ⟩ / ⟨ψ|ψ⟩ of the instantaneous Hamiltonian.
ITensorTDMPO.energy_variance — Function
energy_variance(channels::DrivingChannels, t, ψ::MPS; cutoff, maxdim)⟨H(t)²⟩ - ⟨H(t)⟩², which vanishes exactly when ψ is an eigenstate of H(t).
This is the cheap adiabaticity diagnostic: it needs one MPO application and no ground-state solve, and it tells you directly how far the state has drifted from an instantaneous eigenstate during a ramp. H² is never formed — the variance is computed from H|ψ⟩.
ITensorTDMPO.instantaneous_spectrum — Function
instantaneous_spectrum(channels, t, ψ_guess; nlevels = 2, weight = 10.0, dmrg_kwargs...)The lowest nlevels eigenstates of H(t), via DMRG, returned as (energies, states). Excited states are found by penalizing overlap with the already-converged ones (weight).
This runs a full DMRG solve per call and is by far the most expensive diagnostic here — use it on a coarse subset of times, not every step. dmrg_kwargs are forwarded to ITensorMPS.dmrg (nsweeps, maxdim, cutoff, outputlevel, …).
ITensorTDMPO.instantaneous_gap — Function
instantaneous_gap(channels, t, ψ_guess; kwargs...)The gap E₁(t) - E₀(t) of the instantaneous Hamiltonian. Built on instantaneous_spectrum, and just as expensive.
The gap is what sets the adiabatic time scale: the sweep must be slow compared with 1/Δ² near a gap minimum, so this is the quantity that tells you where a ramp needs to slow down.
ITensorTDMPO.adiabatic_report — Function
adiabatic_report(channels, t, ψ; ψ_guess = ψ, gap = false, kwargs...)A bundle of adiabaticity diagnostics at time t, as a named tuple:
energy—⟨H(t)⟩variance—⟨H²⟩ - ⟨H⟩², zero for an exact eigenstateexcess—⟨H(t)⟩ - E₀(t), the energy above the instantaneous ground state (missingunlessgap = true)gap—E₁(t) - E₀(t)(missingunlessgap = true)fidelity—|⟨ψ₀(t)|ψ⟩|against the instantaneous ground state (missingunlessgap = true)
The gap = false default keeps this cheap: only energy and variance are computed, both without a ground-state solve. Setting gap = true adds a two-level DMRG solve.