# 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]
```