Example 3: Heat equation
In this example, we show how to minimize the shape function
where \(u_t:\mathbb{R}^3\to\mathbb{R}\) is a target temperature profile and \(u:[0,T]\times\mathbb{R}^3\to\mathbb{R}\) is the solution to the heat equation
In particular, we consider \(T=\pi/2\),
and
The domain that minimizes \(J(\Omega)\) is a ball of radius \(0.8\) centered at \((0.5,0.5,0.5)\).
In the following, we describe how to solve this problem in Fireshape.
The entire script is contained in the Python file
heat.py, which is saved in the Fireshape repository and
can be found at the following link:
link-to-heat-example.
Import modules
We begin by importing Firedrake, Fireshape, and ROL.
1from firedrake import *
2from fireshape import *
3import ROL
Implement the shape function
To implement the shape function \(J\), we use Fireshape’s class
PDEconstrainedObjective. This requires specifying how to evaluate
\(J\) in the method
PDEconstrainedObjective.objective_value. To solve the heat equation,
we use the implicit Euler method with 10 time steps.
6class TargetHeat(PDEconstrainedObjective):
7 """L2 misfit function constrained to the heat equation."""
8 def __init__(self, *args, **kwargs):
9 super().__init__(*args, **kwargs)
10 self.mesh = self.Q.mesh_m
11
12 # Setup problem
13 V = FunctionSpace(self.mesh_m, "CG", 1)
14 self.u = Function(V, name="Temperature")
15 self.u_ = Function(V)
16 self.u0 = Function(V) # initial condition
17
18 # Time discretization parameters
19 self.N = 10 # number of time steps
20 self.dt = (pi / 2) / self.N # time step length
21
22 # target temperature profile, exact solution is a ball of radius 0.8
23 # centered at (0.5,0.5,0.5)
24 (x, y, z) = SpatialCoordinate(self.mesh_m)
25 self.u_t = 0.64 - (x - 0.5)**2 - (y - 0.5)**2 - (z - 0.5)**2
26
27 # Weak form of implicit Euler applied to Heat equation
28 u = self.u
29 u_ = self.u_
30 v = TestFunction(V)
31 self.a = Constant(6 * sin(self.dt))
32 self.b = Constant(cos(self.dt))
33 f = self.a + self.b * self.u_t # manufactured source term
34 F = ((u - u_) * v / self.dt + inner(grad(u), grad(v)) - f * v) * dx
35 bcs = DirichletBC(V, 0., "on_boundary")
36 stateproblem = NonlinearVariationalProblem(F, self.u, bcs=bcs)
37 self.solver = NonlinearVariationalSolver(stateproblem)
38
39 def compute_temperature(self, name=None):
40 """
41 Solve heat equation. If input name is passed, store
42 temperature time evolution in corresponding file.
43 """
44 # assign initial condition
45 self.u_.assign(self.u0)
46 self.u.assign(self.u0)
47
48 if name is not None:
49 print("Storing the " + name + " temperature evolution.")
50 out = VTKFile(name + "_temperature_evolution.pvd")
51 out.write(self.u, time=0)
52
53 for ii in range(self.N):
54 # update source term coefficients
55 t = (ii + 1) * self.dt
56 self.a.assign(6 * sin(t))
57 self.b.assign(cos(t))
58 # perform time step
59 self.solver.solve()
60 self.u_.assign(self.u)
61 if name is not None:
62 out.write(self.u, time=t)
63
64 def objective_value(self):
65 self.compute_temperature()
66 return assemble((self.u - self.u_t)**2 * dx)
Select initial guess, control space, and inner product
We select a unit disk centered at the origin as initial domain.
To modify the domain, we create a control space of geometric
transformations discretized using finite elements. To compute
descent directions, we employ Riesz representatives of shape
derivatives with respect to a full \(H^1\)-inner product.
With these, we create a control variable q that will
be updated by the optimization algorithm.
69# Select initial guess, control space, and inner product
70mesh = UnitBallMesh(refinement_level=3)
71Q = FeControlSpace(mesh)
72IP = H1InnerProduct(Q)
73q = ControlVector(Q, IP)
Instantiate objective function J
We instantiate \(J(\Omega)\) using the class
TargetHeat we have created. During instantiation,
we also pass a call back function cb that stores the
shape iterates whenever J is evaluated. We also store
the evolution of the temperature profile on the initial domain.
75# Instantiate objective function J
76out = VTKFile("domain.pvd")
77J = TargetHeat(Q, cb=lambda: out.write(Q.mesh_m.coordinates))
78J.compute_temperature("initial")
Select the optimization algorithm and solve the problem
Finally, we select a trust-region optimization algorithm with l-BFGS Hessian
updates and set the optimization stopping criteria in the dictionary
pd. This, together with J and q are passed to ROL,
which solves the problem. When the optimization algorithms has stopped,
we store the evolution of the temperature profile
on the optimized domain.
80# Select the optimization algorithm and solve the problem
81pd = {'Step': {'Type': 'Trust Region'},
82 'General': {'Secant': {'Type': 'Limited-Memory BFGS',
83 'Maximum Storage': 25}},
84 'Status Test': {'Gradient Tolerance': 1e-3,
85 'Step Tolerance': 1e-8,
86 'Iteration Limit': 30}}
87params = ROL.ParameterList(pd, "Parameters")
88problem = ROL.OptimizationProblem(J, q)
89solver = ROL.OptimizationSolver(problem, params)
90solver.solve()
91J.compute_temperature("final")
Result
Typing python3 levelset.py in the terminal returns:
Storing the initial temperature evolution.
Dogleg Trust-Region Solver with Limited-Memory BFGS Hessian Approximation
iter value gnorm snorm delta #fval #grad tr_flag
0 7.270316e+00 1.687694e+01 1.687694e+01
1 7.270316e+00 1.687694e+01 1.687694e+01 3.068535e+00 3 1 5
2 7.270316e+00 1.687694e+01 3.068535e+00 3.341968e-01 4 1 5
3 3.166234e+00 8.472555e+00 3.341968e-01 3.341968e-01 5 2 0
4 1.193140e+00 3.822150e+00 3.341968e-01 8.354921e-01 6 3 0
5 4.522666e-01 1.745016e+00 2.762971e-01 2.088730e+00 7 4 0
6 1.692963e-01 7.689023e-01 2.341998e-01 5.221825e+00 8 5 0
7 7.025110e-02 3.313803e-01 1.875483e-01 1.305456e+01 9 6 0
8 3.602387e-02 1.456138e-01 1.508964e-01 3.263641e+01 10 7 0
9 1.990907e-02 9.035061e-02 1.493638e-01 8.159102e+01 11 8 0
10 4.282918e-03 4.940063e-02 2.828876e-01 2.039776e+02 12 9 0
11 2.436980e-03 3.761708e-02 7.220499e-02 5.099439e+02 13 10 0
12 1.130037e-04 6.559129e-03 1.380168e-01 1.274860e+03 14 11 0
13 7.578703e-05 3.833176e-03 1.429239e-02 1.274860e+03 15 12 0
14 6.393627e-05 2.059663e-03 4.817841e-03 3.187149e+03 16 13 0
15 5.661970e-05 1.580813e-03 6.664030e-03 7.967873e+03 17 14 0
16 5.173678e-05 1.058514e-03 6.662573e-03 1.991968e+04 18 15 0
17 4.982127e-05 3.074662e-04 4.933308e-03 4.979921e+04 19 16 0
Optimization Terminated with Status: Converged
Storing the final temperature evolution.
We can inspect the result by opening the file levelset_domain.pvd
with ParaView. In the GIF below, we see that
temperature evolution in the initial domain, the domain (black grid)
converging to the right shape (red ball), and the temperature evolution in
the optimized domain.