Time-dependent problems
TimeDependentPDEVariationalProblem
solves an initial-condition or parameter inversion for a time-dependent PDE. The
residual density takes four fields: the new time level, the previous one, the
parameter and the adjoint.
def varf(u_new, u_old, m, p, x, t, dt):
theta = 0.5 # Crank-Nicolson
u = theta * u_new.val + (1 - theta) * u_old.val
g = theta * u_new.grad + (1 - theta) * u_old.grad
return ((u_new.val - u_old.val) / dt * p.val
+ jnp.exp(m.val) * jnp.dot(g, p.grad))
pde = hm.TimeDependentPDEVariationalProblem(
[Vu, Vm, Vu], varf, bc, bc0, u0, t_init=0.0, t_final=1.0, dt=0.01)
where u0 is the initial state, a vector of Vu.
The initial state
The initial state may instead depend on the parameter through a weak equation of its
own, given by an initial density init_varf(u0, m, p, x) in place of u0:
def init_varf(u0, m, p, x): # u(0) = m
return (u0.val - m.val) * p.val
pde = hm.TimeDependentPDEVariationalProblem(
[Vu, Vu, Vu], varf, bc, bc0, None, t_init=0.0, t_final=1.0, dt=0.01,
init_varf=init_varf)
A stationary problem of its own, pde.init_problem, solves the initial equation with
the state’s boundary conditions and differentiates it like the steps; its terms join
theirs in the forward and adjoint marches, the gradient and every block of the Hessian.
The parameter may therefore set the initial state through any map that JAX
differentiates twice, (u0.val - g(m.val)) * p.val; init_linear=False declares a
density that is nonlinear in u0, which Newton’s method then solves. Set the solvers
of pde.init_problem as for any stationary problem. applications/ad_diff runs
its initial-condition inversion with a hand-written wrapper or, with
--initial-density, with this density. With essential conditions the two differ at
the boundary: the wrapper zeroes the parameter’s boundary values, while the projection
lets them reach the rows next to the boundary.
Why the previous level is a differentiation slot
The adjoint of a time-stepping scheme needs \(\partial r_{n+1}/\partial u_n\): the derivative of one step’s residual with respect to the previous state. Treating the old level as a frozen coefficient would force a finite difference there and make the gradient inexact. Making it a real slot keeps every block exact, and the stationary problem is then just the three-slot case.
The per-step blocks are A, B (the cross-time coupling), C, and the
second-order blocks W_nn, W_no, W_oo, W_nm, W_om, W_mm.
applyWuu accumulates into both time levels, which is what makes the
time-dependent reduced Hessian symmetric; the test suite checks that
W_um and W_mu are exact transposes and that the Hessian is symmetric to
1e-15 on a quasilinear problem where all four cross-time blocks are nonzero.
Operators along the trajectory
Nothing is assembled, and no multigrid hierarchy set up, inside a solve loop.
The step operators of a trajectory (A_n, its transpose and B_n) are built
once when the adjoint is first solved at that state and parameter, kept while the
point does not change, and completed with C_n and the W blocks by
setLinearizationPoint; every block of a step comes from one differentiation
pass. The forward solve of a residual that is linear in the new level keeps
its Jacobian across calls at the same parameter.
The common case of a step Jacobian that does not change in time (a linear PDE
with coefficients independent of t and a fixed dt) is detected rather
than assumed: the operators of every step are compared with those of the step
before through their action on a random vector, and steps that agree share one
matrix, one transpose and one solver with its hierarchy. When all steps agree,
one of each serves the whole march. A step that differs gets its own, cloned
from the solver you set (solver_fwd_inc, solver_adj_inc; solver_adj
defaults to a clone of solver). On the 16-step advection-diffusion test
problem this takes a forward or adjoint solve from 16 BoomerAMG setups to one,
and a Hessian action from 32 to none. The comparison needs the operators of
every step, because equal first steps say nothing about the later ones (a state
at rest until a source switches on, a coefficient that changes in time): they
are assembled at every step of a trajectory, and at every step of a linear
forward solve at a new parameter. The comparison tolerance is
TimeDependentPDEVariationalProblem.INVARIANCE_PROBE_TOL (1e-13); -1
treats every step as different.
Scalar parameters
t and dt arrive as traced values rather than constants, so changing the
step size between steps reuses the compiled kernel instead of triggering a
recompile. Declare them with nparams when building a kernel directly.
Time-dependent vectors
TimeDependentVector holds a
snapshot per time level with the arithmetic the algorithms need, and
MisfitTD compares snapshots against data at
chosen times.
Note
apply_ijk, third derivatives, is not implemented for the time-dependent
problem, because Newton-CG does not use it. A time-dependent Taylor
approximation of a QoI would need it.
The time-dependent problem takes a domain density and a boundary density,
bdr_varf(u, u_old, m, p, x, n, t, dt) over the boundary elements of
bdr_attributes, which is where a Robin condition or a prescribed flux goes
(test_timedependent.py checks a Robin term against MFEM’s boundary mass
matrix and through an inversion). It takes no facet_varf: DG formulations
are for the stationary class.