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)  # 2

time_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.0

A 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.DrivingChannelsType
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.

source
ITensorTDMPO.identity_mpoFunction
identity_mpo(channels::DrivingChannels)

The identity MPO on the sites of channels, i.e. the zeroth-order term of the Dyson series.

source
ITensorTDMPO.commutatorFunction
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..

source
ITensorTDMPO.time_ordered_integralFunction
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.5
source
ITensorTDMPO.cumulative_integralFunction
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.

source