PDE-constrained optimization problems#
As seen in the previous section, we can use UFL to differentiate our variatonal forms. We can then also use UFL to represent PDE-constrained optimization problems.
In this section, we will consider PDE-constrained optimization problems of the form
subject to
We can use the adjoint method to compute the sensitivity of the functional with respect to the solution of the PDE. This is done by introducing the Lagrangian
We now seek the minimum of the Lagrangian, i.e.
which we can write as
Since \(\lambda\) is arbitrary, we choose \(\lambda\) such that
for any \(\delta u\) (including \(\frac{\mathrm{d}u}{\mathrm{d}c}[\delta c]\)).
This would mean that
To find such a \(\lambda\), we can solve the adjoint problem
With UFL, we do not need to derive these derivatives by hand, and can use symbolic differentiation to get the left and right hand side of the adjoint problem.
Example: The Poisson mother problem#
We will consider the following PDE constrained optimization problem:
such that
We start by formulating the forward problem, i.e, convert the PDE into a variational form
import basix.ufl
import ufl
domain = ufl.Mesh(basix.ufl.element("Lagrange", "triangle", 1, shape=(2,)))
el_u = basix.ufl.element("Lagrange", "triangle", 1)
V = ufl.FunctionSpace(domain, el_u)
el_f = basix.ufl.element("DG", "triangle", 0)
Q = ufl.FunctionSpace(domain, el_f)
f = ufl.Coefficient(Q)
u = ufl.Coefficient(V)
v = ufl.TestFunction(V)
F = ufl.inner(ufl.grad(u), ufl.grad(v)) * ufl.dx - ufl.inner(f, v) * ufl.dx
Next we define the functional we want to minimize
d = ufl.Coefficient(V)
alpha = ufl.Constant(domain)
J = 1 / 2 * (u - d) ** 2 * ufl.dx + alpha / 2 * ufl.inner(f, f) * ufl.dx
As seen previously, we can now differentiate the functional with respect to the solution of the PDE and the control to obtain the components required for the adjoint problem.
dJdu = ufl.derivative(J, u)
dJdf = ufl.derivative(J, f)
dFdu = ufl.derivative(F, u)
dFdf = ufl.derivative(F, f)
adj_rhs = -dJdu
adj_lhs = ufl.adjoint(dFdu)
For solving the linear system, we prepare for using a Newton-method as before.
fwd_lhs = dFdu
fwd_rhs = F
For the derivative of the functional with respect to the control we use
the command ufl.action
to replace the trial function with lmbda
to create the matrix-vector product
without forming the matrix
lmbda = ufl.Coefficient(V)
dLdf = ufl.derivative(J, f) + ufl.action(ufl.adjoint(dFdf), lmbda)
We collect all the forms we care about in a list called forms
, which will be explained in the next section.
forms = [adj_lhs, adj_rhs, fwd_lhs, fwd_rhs, dLdf, J]