Example 2: L2-tracking

In this example, we show how to minimize the shape functional

\[\mathcal{J}(\Omega) = \int_\Omega \big(u(\mathbf{x}) - u_t(\mathbf{x})\big)^2 \,\mathrm{d}\mathbf{x}\,.\]

where \(u:\mathbb{R}^2\to\mathbb{R}\) is the solution to the scalar boundary value problem

\[-\Delta u = 4 \quad \text{in }\Omega\,, \qquad u = 0 \quad \text{on } \partial\Omega\]

and \(u_t:\mathbb{R}^2\to\mathbb{R}\) is a target function. In particular, we consider

\[u_t(x,y) = 0.36 - (x - 0.5)^2 + (y - 0.5)^2 - 0.5\,.\]

Beside the empty set, the domain that minimizes this shape functional is a disc of radius \(0.6\) centered at \((0.5,0.5)\).

Implementing the PDE constraint

We implement the boundary value problem that acts as PDE constraint in a python module named L2tracking_PDEconstraint.py. In the code, we highlight the lines which characterize the weak formulation of this boundary value problem.

 1import firedrake as fd
 2from fireshape import PdeConstraint
 3
 4
 5class PoissonSolver(PdeConstraint):
 6    """A Poisson BVP with hom DirBC as PDE constraint."""
 7    def __init__(self, mesh_m):
 8        super().__init__()
 9
10        # Setup problem
11        V = fd.FunctionSpace(mesh_m, "CG", 1)
12
13        # Weak form of Poisson problem
14        u = fd.Function(V, name="State")
15        v = fd.TestFunction(V)
16        f = fd.Constant(4.)
17        self.F = (fd.inner(fd.grad(u), fd.grad(v)) - f * v) * fd.dx
18        self.bcs = fd.DirichletBC(V, 0., "on_boundary")
19
20        # PDE-solver parameters
21        self.params = {
22            "ksp_type": "cg",
23            "mat_type": "aij",
24            "pc_type": "hypre",
25            "pc_factor_mat_solver_package": "boomerang",
26            "ksp_rtol": 1e-11,
27            "ksp_atol": 1e-11,
28            "ksp_stol": 1e-15,
29        }
30
31        self.solution = u
32
33        # problem = fd.NonlinearVariationalProblem(
34        #     self.F, self.solution, bcs=self.bcs)
35        # self.solver = fd.NonlinearVariationalSolver(
36        #     problem, solver_parameters=self.params)
37
38    def solve(self):
39        super().solve()
40        fd.solve(self.F == 0, self.solution, bcs=self.bcs,
41                 solver_parameters=self.params)
42        # self.solver.solve()

Note

To solve the discretized variational problem, we use CG with a multigrid preconditioner (see self.params in Lines 22-30). For 2D problems, one can also use direct solvers.

Implementing the shape functional

We implement the shape functional \(\mathcal{J}\) in a python module named L2tracking_objective.py. In the code, we highlight the lines which characterize \(\mathcal{J}\).

 1import firedrake as fd
 2from fireshape import ShapeObjective
 3from L2tracking_PDEconstraint import PoissonSolver
 4
 5
 6class L2trackingObjective(ShapeObjective):
 7    """L2 tracking functional for Poisson problem."""
 8
 9    def __init__(self, pde_solver: PoissonSolver, *args, **kwargs):
10        super().__init__(*args, **kwargs)
11        self.u = pde_solver.solution
12
13        # target function, exact soln is disc of radius 0.6 centered at
14        # (0.5,0.5)
15        (x, y) = fd.SpatialCoordinate(self.Q.mesh_m)
16        self.u_target = 0.36 - (x-0.5)*(x-0.5) - (y-0.5)*(y-0.5)
17
18    def value_form(self):
19        """Evaluate misfit functional."""
20        u = self.u
21        return (u - self.u_target)**2 * fd.dx

Setting up and solving the problem

We set up the problem in the script L2tracking_main.py.

To set up the problem, we

  • construct the mesh of the initial guess (Line 8, a unit square with lower left corner in the origin)

  • choose the discretization of the control (Line 9, Lagrangian finite elements of degree 1)

  • choose the metric of the control space (Line 10, based on linear elasticity energy norm)

  • initialize the PDE contraint on the physical mesh mesh_m (Line 15)

  • specify to save the function \(u\) after each iteration in the file u.pvd by setting the function cb appropriately (Lines 19 and 22).

  • initialize the shape functional (Line 22), and the reduce shape functional (Line 23),

  • create a ROL optimization prolem (Lines 26-49), and solve it (Line 50).

 1import firedrake as fd
 2import fireshape as fs
 3import ROL
 4from L2tracking_PDEconstraint import PoissonSolver
 5from L2tracking_objective import L2trackingObjective
 6
 7# setup problem
 8mesh = fd.UnitSquareMesh(100, 100)
 9Q = fs.FeControlSpace(mesh)
10inner = fs.ElasticityInnerProduct(Q)
11q = fs.ControlVector(Q, inner)
12
13# setup PDE constraint
14mesh_m = Q.mesh_m
15e = PoissonSolver(mesh_m)
16
17# save state variable evolution in file u.pvd
18e.solve()
19out = fd.File("u.pvd")
20
21# create PDEconstrained objective functional
22J_ = L2trackingObjective(e, Q, cb=lambda: out.write(e.solution))
23J = fs.ReducedObjective(J_, e)
24
25# ROL parameters
26params_dict = {
27    'Step': {
28        'Type': 'Line Search',
29        'Line Search': {
30            'Descent Method': {
31                'Type': 'Quasi-Newton Step'
32            }
33        },
34    },
35    'General': {
36        'Secant': {
37            'Type': 'Limited-Memory BFGS',
38            'Maximum Storage': 10
39        }
40    },
41    'Status Test': {
42        'Gradient Tolerance': 1e-4,
43        'Step Tolerance': 1e-5,
44        'Iteration Limit': 15
45    }
46}
47params = ROL.ParameterList(params_dict, "Parameters")
48problem = ROL.OptimizationProblem(J, q)
49solver = ROL.OptimizationSolver(problem, params)
50solver.solve()

Result

Typing python3 L2tracking_main.py in the terminal returns:

Dogleg Trust-Region Solver with Limited-Memory BFGS Hessian Approximation
  iter  value          gnorm          snorm          delta          #fval     #grad     tr_flag
  0     3.984416e-03   4.253880e-02                  4.253880e-02
  1     2.380406e-03   3.243635e-02   4.253880e-02   1.063470e-01   3         2         0
  2     8.449408e-04   1.731817e-02   1.063470e-01   1.063470e-01   4         3         0
  3     4.944712e-04   8.530990e-03   2.919058e-02   2.658675e-01   5         4         0
  4     1.783406e-04   5.042658e-03   5.182347e-02   6.646687e-01   6         5         0
  5     2.395524e-05   1.342447e-03   5.472223e-02   1.661672e+00   7         6         0
  6     6.994156e-06   4.090116e-04   2.218345e-02   4.154180e+00   8         7         0
  7     4.011765e-06   2.961338e-04   1.009231e-02   1.038545e+01   9         8         0
  8     1.170509e-06   2.423493e-04   1.715142e-02   2.596362e+01   10        9         0
  9     1.170509e-06   2.423493e-04   1.372960e-02   3.432399e-03   11        9         2
  10    1.085373e-06   4.112702e-04   3.432399e-03   3.432399e-03   12        10        0
  11    7.428449e-07   1.090433e-04   3.432399e-03   8.580998e-03   13        11        0
  12    6.755854e-07   8.358801e-05   2.034473e-03   2.145249e-02   14        12        0
Optimization Terminated with Status: Converged

We can inspect the result by opening the file u.pvd with ParaView.