“Know how to solve every problem that has been solved.” “What I cannot create, I do not understand.” — Richard Feynman

Time-Dependent DFT

Perturbation Methods

What you need to know first 12 concepts, 6 layers

The requisite-knowledge inventory for this page, bottom-up: the primitives at the base, combined upward until you reach what this page assumes. Skim the layers you already own; start wherever the ground gets unfamiliar.

  1. base
  2. L1
  3. L2
  4. L3
  5. L4
  6. L5
  7. you are here

1 of these are concepts without a dedicated page yet — the grey chips. Following the linked ones first makes the rest land.

A ground-state DFT calculation hands you a density standing perfectly still. Spectroscopy asks a different question: what does that density do when light shakes it? Time-dependent DFT is the Kohn-Sham trick extended to dynamics — one new object, the kernel , carries all the new physics — and Casida's equation is what it becomes when the shaking is gentle. Five ways in; read the one that lands.

read it as

Ground-state DFT rests on Hohenberg-Kohn: the density determines the potential, so one function of three variables carries everything the many-body wavefunction knows about the ground state. Runge and Gross (1984) proved the moving-picture version: for a given initial state, the time-dependent density determines the time-dependent potential — the whole movie of the density fixes the movie of the system. Same bargain as before: trade the exponentially large wavefunction for the density, and pay for it with a functional you have to approximate.

The Kohn-Sham move extends the same way. Invent a fictitious system of non-interacting electrons in orbitals , choose its potential so it reproduces the interacting density at every instant, and you get the time-dependent Kohn-Sham equations:

Structurally it is the SCF equation with a time derivative where the orbital energy used to be. The catch hides in : in principle it depends on the density's entire history, not just its present value. That memory is where the hard many-body physics went to live.

The kernel, measured pole by pole

Here is the claim "every deviation from a bare orbital gap is the kernel's doing," made checkable. For a pure (semi-local) functional like LDA, the kernel is local, the same matrix element lands in and , and so collapses to the bare diagonal of Kohn-Sham gaps — to machine precision. Mix in nonlocal HF exchange (B3LYP) and the cancellation breaks, because exchange enters the two blocks with different orbital pairings.

import numpy as np
from pyscf import gto, scf, tdscf

mol = gto.M(atom='O 0 0 0.1173; H 0 0.7572 -0.4692; H 0 -0.7572 -0.4692',
            basis='6-31g', verbose=0)
mf = scf.RKS(mol); mf.xc = 'lda,vwn'; mf.kernel()

A, B = tdscf.TDDFT(mf).get_ab()          # spin-adapted singlet blocks
nocc, nvir = A.shape[0], A.shape[1]
n = nocc * nvir
A, B = A.reshape(n, n), B.reshape(n, n)

eps = mf.mo_energy
gaps = np.array([eps[nocc + a] - eps[i]
                 for i in range(nocc) for a in range(nvir)])

# Local kernel => the SAME element lands in A and in B, so A - B
# collapses to the bare diagonal of KS gaps. A hybrid breaks this.
print(np.max(np.abs(A - B - np.diag(gaps))))

# Full TDDFT roots via the Hermitian rewrite; TDA roots from A alone
w, U = np.linalg.eigh(A - B)
S = (U * np.sqrt(np.maximum(w, 0))) @ U.T
omega = np.sqrt(np.linalg.eigvalsh(S @ (A + B) @ S))
tda = np.linalg.eigvalsh(A)
H2O / 6-31G  LDA   nocc = 5, nvir = 8, Casida dim = 40
  max |A - B - diag(KS gaps)| = 3.55e-15   (local kernel)
  same quantity, B3LYP        = 1.86e-01   (HF exchange is nonlocal)
  max |our roots - PySCF TDDFT| = 7.46e-14

  root   parent i->a    bare gap    TDA       TDDFT     (eV)
   S1    5 -> 6         7.176      7.584     7.559
   S2    4 -> 6         8.892      9.531     9.446
   S3    5 -> 7         9.571      9.800     9.795
   S4    4 -> 7        11.287     12.143    12.071
   S5    3 -> 6        13.881     14.688    14.609

Read the table one row at a time: each root is labeled by its dominant parent transition, with the bare gap it started from, the TDA value, and the full TDDFT value. The kernel pushes every one of these singlets up from its bare gap, by 0.2 to 0.8 eV — the transition density's Hartree self-repulsion outweighs the (negative, and in LDA rather weak) contribution. The and lines are the receipts: the locality identity holds exactly, and our four-line Hermitian-rewrite solver agrees with PySCF's TDDFT to floating-point noise.

The smallest possible spectroscopy

The one-pole formula from framing 5, on real numbers. H2 in STO-3G has one occupied and one virtual orbital, so get_ab() returns two numbers and the entire excited-state calculation fits in a square root:

mol = gto.M(atom='H 0 0 0; H 0 0 0.74', basis='sto-3g', verbose=0)
mf = scf.RKS(mol); mf.xc = 'lda,vwn'; mf.kernel()

A, B = tdscf.TDDFT(mf).get_ab()          # shapes (1,1,1,1): plain numbers
D = mf.mo_energy[1] - mf.mo_energy[0]    # the bare gap
K = float(B[0, 0, 0, 0])                 # the one kernel element

omega_hand = np.sqrt(D * (D + 2*K))      # the whole theory, by hand

td = tdscf.TDDFT(mf); td.nstates = 1; td.kernel()
print(omega_hand, float(td.e[0]))        # same number
H2 / STO-3G  LDA   nocc = 1, nvir = 1  ->  A, B are numbers
  D = eps_u - eps_g = 0.748730 Ha,  K = B = 0.223033 Ha,  A - D = 0.223033 Ha
  hand:  omega = sqrt(D(D + 2K)) = 0.945822 Ha = 25.737 eV
  PySCF TDDFT:             omega = 0.945822 Ha = 25.737 eV
  TDA (= A):               omega = 0.971762 Ha = 26.443 eV

to six decimals — the locality identity again, now visible as two printed numbers being equal. The hand formula and the library solver agree to every printed digit, and TDA overshoots by 0.7 eV, exactly the vs gap you get by expanding the square root. (Don't quote 25.7 eV to an experimentalist: minimal-basis H2 is a cartoon. The point is the structure, and the structure is exact.)

Two roads, one spectrum

Finally, the equivalence of the real-time road and the eigenvalue road, demonstrated rather than asserted. We linearize the TDKS dynamics in the particle-hole basis — the generator is built from the same and blocks — kick the water molecule along , and march forward in time with the one-step propagator applied over and over. No diagonalization anywhere on this road. Then we FFT the induced dipole and ask where the peaks fall.

from scipy.linalg import expm

r_ao = mol.intor('int1e_r')              # dipole integrals (3, nao, nao)
C = mf.mo_coeff
d = np.einsum('pi,pq,qa->ia', C[:, :nocc], r_ao[2], C[:, nocc:]).ravel()

M = np.block([[A, B], [-B, -A]])         # the linearized TDKS generator
u = np.concatenate([d, d]).astype(complex)   # state right after a z-kick

dt, nsteps = 0.05, 40_000                # T = 2000 au
U = expm(-1j * M * dt)                   # one-step propagator, built once
signal = np.empty(nsteps)
for s in range(nsteps):
    u = U @ u                            # march; never diagonalize
    signal[s] = np.real(d @ (u[:n] + u[n:]))   # induced dipole <z>(t)

signal -= signal.mean()
t = np.arange(1, nsteps + 1) * dt
spec = np.abs(np.fft.rfft(signal * np.exp(-0.003 * t)))
freqs = np.fft.rfftfreq(nsteps, dt) * 2 * np.pi   # peaks = excitations
Two roads, H2O / 6-31G / LDA (kick along z, T = 2000 au, FFT resolution ~0.085 eV):
  FFT peak (eV)   nearest Casida root (eV)   difference
       9.404            9.446            0.043
      18.038           18.033            0.005
      27.185           27.143            0.042
      36.076           36.095            0.019
      36.845           36.867            0.022
      49.326           49.277            0.049

Every FFT peak lands on a Casida root to well within the FFT's resolution of 0.085 eV. The two roads really do carry the same information — the choice between them is engineering, not physics. Casida wins for a handful of discrete states in a small molecule; real-time propagation wins when the system is large, when you want the whole broadband spectrum at once, or when the field is strong enough that "linear" stops being true. A full real-time code propagates the orbitals themselves on a grid rather than particle-hole amplitudes, but the shape of the calculation — kick, march, FFT — is precisely this one.

Try First

Each prompt asks a checkable question about the working code or math above — predict an output, derive a sign, state an invariant, find a bug. Commit to an answer before clicking "reveal." That commitment is the whole point: if your answer matched, you understand the piece you were looking at; if it didn't, that's the part worth re-reading.

predict
In the pole-by-pole table, the kernel pushed all five singlet roots up from their bare gaps. Predict the lowest triplet of the same molecule with the same functional: above or below the 7.176 eV bare HOMO-LUMO gap? Commit to a direction and a reason before revealing.
why does this work
The bare response is spin-blind: an electron and a electron in the same closed-shell orbital have identical energies, so every particle-hole gap is doubly degenerate. Yet the TDDFT spectrum has distinct singlets and triplets. Where does the splitting come from, mechanically?
predict
The Casida table lists S1 at 7.559 eV, but the FFT table from the z-kick has no peak anywhere near 7.5 eV — its first line is 9.404 eV. Did the propagation miss a root? Decide whether this is a bug before revealing.

Where this goes

This page is the bridge, and both ends of it are on this site. Behind: ground-state DFT supplies the orbitals and gaps, functional derivatives define the kernel, and linear response theory is the poke-and-listen logic both roads share. Ahead: Casida's equation takes the frequency-domain road seriously — spin adaptation, the Hermitian rewrite, triplet instabilities, oscillator strengths — and Liouville-Lanczos handles the regime where even building and is too expensive and the spectrum must come from matrix-vector products alone.