The time_evolve interface

time_evolve is the single entry point for every algorithm in this package. The Hamiltonian is always given as driving channels; only alg changes which integrator runs.

ψ = time_evolve(channels, ψ0, 0.0, 10.0; nsteps = 100)                        # cfet (default)
ψ = time_evolve(channels, ψ0, 0.0, 10.0; alg = "magnus", nsteps = 100)
ψ = time_evolve(channels, ψ0, 0.0, 10.0; alg = "dyson", order = 3, nsteps = 100)

Each algorithm also has a direct driver — piecewise_constant_tdvp, dyson_evolve, magnus_evolve, cfet_evolve — which time_evolve forwards to; use those when you want a method-specific option without going through alg_kwargs.

Keywords

keyworddefaultmeaning
alg"cfet"integrator; a String, Symbol or ITensors.Algorithm
nsteps / dttime grid, following ITensorMPS.tdvp conventions; or pass times directly
orderper-algorithmexpansion order — 4 for CFET (2 or 4 only), 2 for Magnus (1–3) and Dyson (≥ 0). Errors for "piecewise_constant", which does not expand the step
cutoff / maxdim1e-10 / typemax(Int)truncation of the evolving state
operator_cutoff / operator_maxdim1e-12 / typemax(Int)truncation of the operators built along the way — Ω, the Dyson MPO, the frozen H(t)
generator_prefactor-im-im for real time, -1 for imaginary time
normalizeper-algorithmrenormalize after each step; needed for imaginary time
adaptivefalsechoose step sizes automatically to meet tol — see Adaptive stepping
tol1e-6target local error per step when adaptive = true
step_observer!nothingcalled as step_observer!(; step, t_start, t_stop, state) after each step
outputlevel0≥ 1 prints progress
alg_kwargs(;)forwarded verbatim to the underlying driver (schedule, eval_at, generator_prefactor, normalize, npoints)

Unknown algorithm names raise an ArgumentError listing the valid ones (EVOLUTION_ALGORITHMS).

Which algorithm to use

Use alg = "cfet" (the default) — see Commutator-free propagator (CFET), measured faster and more accurate than alg = "magnus" at equal step count, and uniformly good across the whole accuracy range with no cliff at either end. alg = "dyson" is size-extensive — its accuracy does not degrade with chain length, see Scope and limitations — but CFET/Magnus's exponentiation resums disjoint higher-order terms for free, which the plain truncated Dyson series does not, so CFET/Magnus are markedly more accurate at the same order. That said, if your accuracy target is moderate (roughly 1e-61e-8), alg = "dyson" at order 2–3 measured 3–6× cheaper than cfet/magnus for comparable accuracy, because it applies a single MPO per step rather than running an internal TDVP sweep — see When Dyson wins on cost for the full comparison and where that advantage reverses.

Measured infidelity against a dense RK4 reference for a driven TFIM chain (Sz·Sz + an oscillating transverse field) at fixed dt = 0.05, order 2, versus chain length:

Npiecewise_constant_tdvpdyson_evolvemagnus_evolve
41.8e-76.2e-81.1e-12
62.6e-71.1e-73.3e-12
83.5e-71.7e-75.4e-12
104.4e-72.2e-77.6e-12

dyson_evolve's error grows only mildly over this range (roughly linearly in N) and it beats piecewise_constant_tdvp at every size — a direct consequence of the size-extensive construction it is built on (see Dyson series). Magnus remains the most accurate of the three by several orders of magnitude, for the reason above.

The direct construction dyson_mpo/dyson_terms does not share this scaling: its error grows by a factor of ~1600 over the same range (≈ N^4.7 for this model), worse than simply freezing the Hamiltonian by N ≈ 4 — see Scope and limitations for the mechanism and Dyson series for when to reach for it directly regardless.

Equal-error runtime: Magnus vs. piecewise-constant TDVP

Measured on the paper's modulated TFIM over one period at N = 8 with exact bond dimension (so truncation contributes nothing), against an exact dense RK4 reference:

methodconvergence ordercost per step
piecewise_constant_tdvp2.01.0×
magnus_evolve order 2~3.9~2.1×
magnus_evolve order 3~3.8~2.6×

To be precise about where these orders come from: TDVP is not the limiting factor in either. Two-site TDVP applied to a time-independent generator at exact bond dimension is accurate to ~1e-7 within a few sweeps — it acts as an essentially exact exponentiator. The second order of piecewise_constant_tdvp is entirely the Hamiltonian-freezing error: switching eval_at from :midpoint to :start drops the method to first order (measured ratios 1.99 vs 4.00), with everything else unchanged. So the comparison above is really midpoint freezing (2nd order) versus Ω₁ + Ω₂ with exact time-ordered integrals (4th order), both exponentiated by the same near-exact TDVP.

With truncation, TDVP additionally contributes a projection error, but that is controlled by bond dimension rather than by dt and so does not change these orders.

Magnus costs about twice as much per step but converges at roughly fourth order rather than second, so the equal-accuracy speedup grows as the tolerance tightens:

target errormagnus_evolve order 2piecewise_constant_tdvpspeedup
5.6e-48 steps, 4.4 s~65 steps, 16 s3.7×
3.7e-516 steps, 9.4 s~252 steps, 64 s6.8×
2.4e-632 steps, 19 s~988 steps, 249 s13.3×

Asymptotically the ratio scales as ε^(-1/4): reaching error ε needs ns ∝ ε^(-1/2) segmented steps but only ns ∝ ε^(-1/4) Magnus steps. Below about 1e-6 the order-2 Magnus error flattens against the internal TDVP exponentiation error — raise tdvp_kwargs.nsteps there.

CFET (see its own page) beats Magnus on both axes simultaneously and is the better default in practice.

Imaginary time

Pass generator_prefactor = -1 (with normalize = true, since the evolution is no longer unitary) to project toward the ground state:

ψ = time_evolve(channels, ψ0, 0.0, 6.0;
                alg = "cfet", nsteps = 60,
                generator_prefactor = -1, normalize = true)

This works for every algorithm: the factor is carried by the time-ordered integrals for Magnus and Dyson, and applied directly to each exponent for CFET and the piecewise-constant driver.

Reference

ITensorTDMPO.time_evolveFunction
time_evolve(channels::DrivingChannels, ψ0::MPS, times; alg = "cfet", kwargs...)
time_evolve(channels, ψ0, t_start, t_stop; dt = nothing, nsteps = nothing, alg = "cfet", kwargs...)

Evolve ψ0 under the time-dependent Hamiltonian carried by channels, choosing the integrator with alg. This is the single entry point for every method in the package: the Hamiltonian is always described the same way — as a DrivingChannels decomposition H(t) = Σₐ fₐ(t) H^{(a)} — and only alg changes.

Algorithms

  • "cfet" (default) — cfet_evolve. Product of exponentials at Gauss–Legendre nodes, no commutators formed. Unitary, 4th order, and measured faster and more accurate than "magnus" at equal step count — see Commutator-free propagator (CFET).
  • "magnus"magnus_evolve. Expands the step in the Magnus series and applies exp(Ω). Unitary, ~4th order.
  • "piecewise_constant"piecewise_constant_tdvp. Freezes H(t) on each interval and runs a TDVP sweep. 2nd order.
  • "dyson"dyson_evolve. Expands the step in the Dyson series and applies the resulting MPO. Size-extensive and does not degrade with chain length, but "cfet"/"magnus" are markedly more accurate at the same order — see Scope and limitations.

alg accepts a String, a Symbol, or an ITensors.Algorithm.

Keywords

  • order: expansion order. Defaults to each algorithm's own choice — 4 for "cfet" (2 or 4 only), 2 for "magnus" (1–3) and "dyson" (≥ 0). Passing it with alg = "piecewise_constant" is an error, since that method does not expand the step.
  • cutoff = 1e-10, maxdim: truncation of the evolving state.
  • operator_cutoff = 1e-12, operator_maxdim: truncation of the operators built along the way — the Magnus generator, the Dyson MPO, or the frozen H(t).
  • (step_observer!) = nothing: callback step_observer!(; step, t_start, t_stop, state) after each step.
  • outputlevel = 0: set >= 1 to print progress.
  • alg_kwargs = (;): forwarded verbatim to the underlying driver, for options specific to one method (schedule, eval_at and generator_prefactor for "piecewise_constant"; normalize for "dyson"; npoints via the operator builders).

The time grid is given either as times directly, or as t_start, t_stop with dt or nsteps, following the same conventions as ITensorMPS.tdvp.

Examples

sites = siteinds("S=1/2", 20)
ramp = Ramp(SmoothstepRamp(), 0.0, 10.0, 0.0, 2.0)

# Channels can be given inline as (driving function, MPO) tuples.
ψ = time_evolve([(one, Hzz), (ramp, Hx)], ψ0, 0.0, 10.0;
                nsteps = 100, cutoff = 1e-10, maxdim = 128)

# Same Hamiltonian, different integrator — nothing else changes.
ψ_pc = time_evolve([(one, Hzz), (ramp, Hx)], ψ0, 0.0, 10.0;
                   alg = "piecewise_constant", nsteps = 100)
source
time_evolve([(f1, H1), (f2, H2), ...], ψ0, t_start, t_stop; kwargs...)

Convenience form taking the channels as a plain list of (driving function, MPO) tuples (or H => f pairs, in either order); the DrivingChannels object is built internally. Construct one explicitly only if you want to reuse it across several calls.

source