Driving channels
The Dyson and Magnus constructions both start from the paper's decomposition of the Hamiltonian into driving channels,
H(t) = Σₐ fₐ(t) H⁽ᵃ⁾where each H⁽ᵃ⁾ is a time-independent MPO and each fₐ a scalar driving function. This is what makes the expansions tractable: the operator content (products and commutators of the H⁽ᵃ⁾) is computed once, and all time dependence enters through scalar integrals of the fₐ.
channels = DrivingChannels([(1.0, Hzz), (ramp, Hx)])
channels(0.5) # assembles H(0.5) as an MPO (useful for testing)
nchannels(channels) # 2time_evolve builds this internally when handed a plain list, so you only need it explicitly to reuse one decomposition across several calls.
Channel specification
Channels are given as a list of (driving, MPO) tuples. The MPO and the driving are told apart by type, so (H, f) works as well as (f, H), and H => f pairs are accepted too.
A driving is either a function of time or a plain number for a constant coefficient — (J, H) is shorthand for the static term J * H:
[(1.0, Hzz), (ramp, Hx)] # unit coupling
[(-2.5, Hzz), (ramp, Hx)] # any constant
[(one, Hzz), (ramp, Hx)] # a function works too; `one` ≡ 1.0A driving that is neither callable nor a number is rejected when the channels are built, rather than failing later inside the quadrature.
If you want to reuse the decomposition across calls, build it explicitly with DrivingChannels([(1.0, Hzz), (ramp, Hx)]) and pass that instead — every entry point accepts either.
Time-ordered integrals
The scalar prefactors in both the Dyson and Magnus expansions are the time-ordered integrals
[f₁ f₂ … fₙ] = (-i)ⁿ ∫dt₁ ∫^{t₁}dt₂ … ∫^{t_{n-1}}dtₙ f₁(t₁) f₂(t₂) … fₙ(tₙ)with t₁ > t₂ > … > tₙ, so f₁ is evaluated at the latest time. The factors of -i are folded into the bracket.
time_ordered_integral([f, g], t0, t1) # [f g]
time_ordered_integral(fill(one, 3), 0, Δ) # ≈ (-i)³ Δ³/3!These are evaluated by repeated cumulative Simpson quadrature on a uniform grid — O(n · npoints) rather than the O(npointsⁿ) of naive nested quadrature. The paper instead uses a quantics tensor-train representation (QuanticsTT.jl), which matters at high order; the grid approach here is simpler and adequate for the orders implemented.
The implementation satisfies the paper's factoring property [fₐfᵦ] + [fᵦfₐ] = [fₐ][fᵦ], which is checked in the test suite.
prefactor (default -im) sets the factor carried per order; pass -1 for imaginary time — see Imaginary time.
Reference
ITensorTDMPO.DrivingChannels — Type
DrivingChannels(operators::Vector{MPO}, drivings::Vector, sites)
DrivingChannels(H1 => f1, H2 => f2, ...)
DrivingChannels([H1 => f1, H2 => f2, ...])A time-dependent Hamiltonian in the "driving channel" form of Vanthilt et al.:
\[H(t) = \sum_a f_a(t) \, H^{(a)}\]
where each H^{(a)} is a time-independent MPO and each f_a is a scalar driving function. This is the decomposition that both the Dyson and Magnus MPO constructions are built on: it separates the operator content (the H^{(a)}, which can be multiplied and commuted once and for all) from the time dependence (the f_a, which enter only through the scalar time-ordered integrals of time_ordered_integral).
Calling channels(t) assembles H(t) as an ordinary MPO, which is mainly useful as a reference for testing.
Examples
sites = siteinds("S=1/2", 8)
Hzz = MPO(OpSum() + ..., sites) # static coupling
Hx = MPO(OpSum() + ..., sites) # transverse field
ramp = Ramp(SmoothstepRamp(), 0.0, 1.0, 0.0, 2.0)
channels = DrivingChannels([(1.0, Hzz), (ramp, Hx)])A driving may be any callable of time, or a Number for a channel with a constant coefficient — (J, H) is shorthand for the static term J * H.
ITensorTDMPO.nchannels — Function
nchannels(channels::DrivingChannels)The number of driving channels.
ITensorTDMPO.identity_mpo — Function
identity_mpo(channels::DrivingChannels)The identity MPO on the sites of channels, i.e. the zeroth-order term of the Dyson series.
ITensorTDMPO.commutator — Function
commutator(A::MPO, B::MPO; cutoff, maxdim)The commutator A*B - B*A as an MPO.
For two extensive Hamiltonians the commutator is automatically free of the "disjoint" contributions that spoil size-extensivity, since those terms are identical in A*B and B*A and cancel; see Sec. III of Vanthilt et al..
ITensorTDMPO.time_ordered_integral — Function
time_ordered_integral(fs, t0, t; npoints = 1025)The time-ordered integral of the driving functions fs = (f₁, …, fₙ) over [t0, t], written [f₁ f₂ … fₙ] in Vanthilt et al.:
\[[f_1 f_2 … f_n] = (-i)^n ∫_{t_0}^{t} dt_1 ∫_{t_0}^{t_1} dt_2 ⋯ ∫_{t_0}^{t_{n-1}} dt_n \; f_1(t_1) f_2(t_2) ⋯ f_n(t_n)\]
Note the ordering convention: t₁ > t₂ > ⋯ > tₙ, so f₁ is evaluated at the latest time. These are exactly the scalar prefactors multiplying the operator strings H^{(a₁)} H^{(a₂)} ⋯ H^{(aₙ)} in the Dyson series, and the prefactors of the nested commutators in the Magnus expansion. The factors of -i are folded into the bracket, so no further factors of -i are needed downstream.
The empty bracket [] (n == 0) is 1.
The nested integrals are evaluated by repeated cumulative quadrature on a uniform grid of npoints points, which costs O(n · npoints) rather than the O(npoints^n) of naive nested quadrature. npoints is rounded up to an odd number. Two work buffers are allocated per call regardless of n.
prefactor is the factor carried per order, -im for real-time evolution. Pass -1 for imaginary time, which turns the bracket into (-1)^n ∫⋯ and makes the resulting generator Hermitian rather than anti-Hermitian.
Examples
For a constant driving function f ≡ 1, the bracket reduces to the Taylor coefficient (-i)ⁿ (t - t₀)ⁿ / n!:
time_ordered_integral((one, one), 0.0, 1.0) # ≈ -0.5ITensorTDMPO.cumulative_integral — Function
cumulative_integral(y::AbstractVector, h)Cumulative integral of samples y on a uniform grid of spacing h, returning a vector F with F[i] ≈ ∫_{x_1}^{x_i} y(x) dx and F[1] == 0.
Uses the composite Simpson rule (error O(h⁴)), with the standard three-point formula for the first sub-interval so that odd-indexed points are also fourth-order accurate.