Skip to content

Commit bf6ed72

Browse files
JHopeCollinstommbendallconnorjwardjshipton
authored
Add third order Hessian taylor tests (#721)
Co-authored-by: Thomas Bendall <14180399+tommbendall@users.noreply.github.com> Co-authored-by: Connor Ward <c.ward20@imperial.ac.uk> Co-authored-by: jshipton <j.shipton@exeter.ac.uk>
1 parent 872e522 commit bf6ed72

7 files changed

Lines changed: 234 additions & 137 deletions

File tree

gusto/solvers/preconditioners.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ def apply(self, pc, x, y):
346346

347347
unbroken_res_hdiv = self.unbroken_residual.subfunctions[self.vidx]
348348
broken_res_hdiv = self.broken_residual.subfunctions[self.vidx]
349-
broken_res_hdiv.assign(0)
349+
broken_res_hdiv.zero()
350350
self.average_kernel.apply(broken_res_hdiv, self.weight, unbroken_res_hdiv)
351351

352352
# Compute the rhs for the multiplier system
@@ -374,7 +374,7 @@ def apply(self, pc, x, y):
374374
# Compute the hdiv projection of the broken hdiv solution
375375
broken_hdiv = self.broken_solution.subfunctions[self.vidx]
376376
unbroken_hdiv = self.unbroken_solution.subfunctions[self.vidx]
377-
unbroken_hdiv.assign(0)
377+
unbroken_hdiv.zero()
378378

379379
self.average_kernel.apply(unbroken_hdiv, self.weight, broken_hdiv)
380380

@@ -735,7 +735,7 @@ def apply(self, pc, x, y):
735735

736736
# Recover broken u and rho
737737
u_broken, rho, l = self.y_hybrid.subfunctions
738-
self.u_hdiv.assign(0)
738+
self.u_hdiv.zero()
739739
self._average_kernel.apply(self.u_hdiv, self._weight, u_broken)
740740
for bc in self.bcs:
741741
bc.zero(self.u_hdiv)
@@ -745,7 +745,7 @@ def apply(self, pc, x, y):
745745
self.y.subfunctions[1].assign(rho)
746746

747747
# Recover theta
748-
self.theta.assign(0)
748+
self.theta.zero()
749749
self.theta_solver.solve()
750750
self.y.subfunctions[2].assign(self.theta)
751751

gusto/solvers/solver_presets.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def hybridised_solver_parameters(equation, solver_prognostics, alpha=0.5, tau_va
4040

4141
# Callback function for the nullspace of the trace system
4242
def trace_nullsp(T):
43-
return VectorSpaceBasis(constant=True)
43+
return VectorSpaceBasis(constant=True, comm=equation.function_space.mesh().comm)
4444

4545
# Prepare some generic variables to help with logic below ------------------
4646

integration-tests/adjoints/test_diffusion_sensitivity.py

Lines changed: 43 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,16 @@
33

44
from firedrake import *
55
from firedrake.adjoint import *
6-
from pyadjoint import get_working_tape
76
from gusto import *
87

98

109
@pytest.fixture(autouse=True)
11-
def handle_taping():
12-
yield
13-
tape = get_working_tape()
14-
tape.clear_tape()
15-
16-
17-
@pytest.fixture(autouse=True, scope="module")
18-
def handle_annotation():
19-
from firedrake.adjoint import annotate_tape, continue_annotation
20-
if not annotate_tape():
21-
continue_annotation()
22-
yield
23-
# Ensure annotation is paused when we finish.
24-
annotate = annotate_tape()
25-
if annotate:
26-
pause_annotation()
10+
def autouse_set_test_tape(set_test_tape):
11+
pass
2712

2813

2914
@pytest.mark.parametrize("nu_is_control", [True, False])
3015
def test_diffusion_sensitivity(nu_is_control, tmpdir):
31-
assert get_working_tape()._blocks == []
3216
n = 30
3317
mesh = PeriodicUnitSquareMesh(n, n)
3418
output = OutputParameters(dirname=str(tmpdir))
@@ -41,38 +25,59 @@ def test_diffusion_sensitivity(nu_is_control, tmpdir):
4125

4226
R = FunctionSpace(mesh, "R", 0)
4327
# We need to define nu as a function in order to have a control variable.
44-
nu = Function(R, val=0.0001)
28+
nu = Function(R, val=0.2)
4529
diffusion_params = DiffusionParameters(mesh, kappa=nu)
4630
eqn = DiffusionEquation(domain, V, "f", diffusion_parameters=diffusion_params)
4731

48-
diffusion_scheme = BackwardEuler(domain)
32+
# Don't let an inexact solve get in the way of a good Taylor test
33+
solver_parameters = {
34+
'snes_type': 'ksponly',
35+
'ksp_type': 'preonly',
36+
'pc_type': 'lu',
37+
'pc_factor_mat_solver_type': 'mumps',
38+
}
39+
diffusion_scheme = BackwardEuler(domain, solver_parameters=solver_parameters)
4940
diffusion_methods = [CGDiffusion(eqn, "f", diffusion_params)]
5041
timestepper = Timestepper(eqn, diffusion_scheme, io, spatial_methods=diffusion_methods)
5142

5243
x = SpatialCoordinate(mesh)
5344
fexpr = as_vector((sin(2*pi*x[0]), cos(2*pi*x[1])))
54-
timestepper.fields("f").interpolate(fexpr)
45+
ic = Function(V).interpolate(fexpr)
5546

56-
end = 0.1
57-
timestepper.run(0., end)
47+
# These are the only operations we are interested in rerunning with the tape.
48+
with set_working_tape() as tape:
49+
# initialise the solution
50+
timestepper.fields("f").assign(ic)
5851

59-
u = timestepper.fields("f")
60-
J = assemble(inner(u, u)*dx)
52+
# propagate forward
53+
end = 0.1
54+
timestepper.run(0., end)
6155

62-
if nu_is_control:
63-
control = Control(nu)
64-
h = Function(R, val=0.0001) # the direction of the perturbation
65-
else:
66-
control = Control(u)
67-
# the direction of the perturbation
68-
h = Function(V).interpolate(fexpr * np.random.rand())
56+
# Make sure we have more than a quadratic nonlinearity so Jhat.hessian isn't exact.
57+
u = timestepper.fields("f")
58+
J = assemble(inner(u, u)*dx)**2
6959

70-
# the functional as a pure function of nu
71-
Jhat = ReducedFunctional(J, control)
60+
if nu_is_control:
61+
m = nu
62+
else:
63+
m = ic
7264

65+
# the functional as a pure function of the control
66+
Jhat = ReducedFunctional(J, Control(m), tape=tape)
67+
68+
assert np.allclose(J, Jhat(m)), "Functional re-evaluation does not match original evaluation."
69+
70+
# perturbation direction for taylor test
7371
if nu_is_control:
74-
assert np.allclose(J, Jhat(nu))
75-
assert taylor_test(Jhat, nu, h) > 1.95
72+
h = Function(R, val=0.1)
7673
else:
77-
assert np.allclose(J, Jhat(u))
78-
assert taylor_test(Jhat, u, h) > 1.95
74+
h = Function(V).interpolate(as_vector([cos(4*pi*x[1]), sin(4*pi*x[0])]))
75+
76+
# Check the TLM explicitly before checking the Hessian (which relies on the tlm)
77+
assert taylor_test(Jhat, m, h, dJdm=Jhat.tlm(h)) > 1.95, "TLM is not second order accurate."
78+
79+
# Check the re-evaluation, derivative, and Hessian all converge at the expected rates.
80+
taylor = taylor_to_dict(Jhat, m, h)
81+
assert min(taylor['R0']['Rate']) > 0.95, taylor['R0']
82+
assert min(taylor['R1']['Rate']) > 1.95, taylor['R1']
83+
assert min(taylor['R2']['Rate']) > 2.95, taylor['R2']

integration-tests/adjoints/test_moist_thermal_williamson_5_sensitivity.py

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
SpatialCoordinate, as_vector, pi, sqrt, min_value, exp, cos, sin, assemble, dx, inner, Function
2222
)
2323
from firedrake.adjoint import *
24-
from pyadjoint import get_working_tape
2524
from gusto import (
2625
Domain, IO, OutputParameters, Timestepper, RK4, DGUpwind,
2726
ShallowWaterParameters, ThermalShallowWaterEquations, lonlatr_from_xyz,
@@ -31,29 +30,14 @@
3130

3231

3332
@pytest.fixture(autouse=True)
34-
def handle_taping():
35-
yield
36-
tape = get_working_tape()
37-
tape.clear_tape()
38-
39-
40-
@pytest.fixture(autouse=True, scope="module")
41-
def handle_annotation():
42-
from firedrake.adjoint import annotate_tape, continue_annotation
43-
if not annotate_tape():
44-
continue_annotation()
45-
yield
46-
# Ensure annotation is paused when we finish.
47-
annotate = annotate_tape()
48-
if annotate:
49-
pause_annotation()
33+
def autouse_set_test_tape(set_test_tape):
34+
pass
5035

5136

5237
def test_moist_thermal_williamson_5_sensitivity(
5338
tmpdir, ncells_per_edge=8, dt=600, tmax=50.*24.*60.*60.,
5439
dumpfreq=2880
5540
):
56-
assert get_working_tape()._blocks == []
5741
# ------------------------------------------------------------------------ #
5842
# Parameters for test case
5943
# ------------------------------------------------------------------------ #
@@ -150,9 +134,18 @@ def gamma_v(x_in):
150134
DGUpwind(eqns, field_name) for field_name in eqns.field_names
151135
]
152136

137+
# Don't let an inexact solve get in the way of a good Taylor test
138+
solver_parameters = {
139+
'snes_type': 'ksponly',
140+
'ksp_type': 'preonly',
141+
'pc_type': 'lu',
142+
'pc_factor_mat_solver_type': 'mumps',
143+
}
144+
153145
# Timestepper
154146
stepper = Timestepper(
155-
eqns, RK4(domain), io, spatial_methods=transport_methods
147+
eqns, RK4(domain, solver_parameters=solver_parameters),
148+
io, spatial_methods=transport_methods
156149
)
157150

158151
# ------------------------------------------------------------------------ #
@@ -188,29 +181,41 @@ def gamma_v(x_in):
188181
# Expression for initial vapour depends on initial saturation
189182
vexpr = mu2 * q_sat(bexpr, Dexpr)
190183

191-
# Initialise (cloud and rain initially zero)
192-
u0.project(uexpr)
193-
D0.interpolate(Dexpr)
194-
b0.interpolate(bexpr)
195-
v0.interpolate(vexpr)
196-
c0.assign(0.0)
197-
r0.assign(0.0)
184+
# Control
185+
m_D = Function(D0.function_space()).interpolate(Dexpr)
186+
187+
with set_working_tape() as tape:
188+
# Initialise (cloud and rain initially zero)
189+
u0.project(uexpr)
190+
D0.interpolate(m_D)
191+
b0.interpolate(bexpr)
192+
v0.interpolate(vexpr)
193+
c0.zero()
194+
r0.zero()
195+
196+
# ----------------------------------------------------------------- #
197+
# Run
198+
# ----------------------------------------------------------------- #
199+
# two timesteps so we hit the codepath for reshuffling data between steps
200+
stepper.run(t=0., tmax=2*dt)
201+
202+
u_tf = stepper.fields('u') # Final velocity field
203+
D_tf = stepper.fields('D') # Final depth field
204+
205+
J = assemble(0.5*inner(u_tf, u_tf)*dx + 0.5*g*D_tf**2*dx)
206+
207+
Jhat = ReducedFunctional(J, Control(m_D), tape=tape)
198208

199-
# ----------------------------------------------------------------- #
200-
# Run
201-
# ----------------------------------------------------------------- #
202-
stepper.run(t=0, tmax=dt)
209+
assert np.allclose(Jhat(m_D), J), "Functional re-evaluation does not match original evaluation."
203210

204-
u_tf = stepper.fields('u') # Final velocity field
205-
D_tf = stepper.fields('D') # Final depth field
211+
# perturbation direction for taylor test
212+
h_D = Function(D0.function_space())
213+
h_D.interpolate(0.1*(Dexpr - (mean_depth - tpexpr)))
206214

207-
J = assemble(0.5*inner(u_tf, u_tf)*dx + 0.5*g*D_tf**2*dx)
215+
# Check the TLM explicitly before checking the Hessian (which relies on the tlm)
216+
assert taylor_test(Jhat, m_D, h_D, dJdm=Jhat.tlm(h_D)) > 1.95, "TLM is not second order accurate."
208217

209-
Jhat = ReducedFunctional(J, Control(D0))
218+
assert taylor_test(Jhat, m_D, h_D) > 1.95, "Adjoint derivative is not second order accurate."
210219

211-
assert np.allclose(Jhat(D0), J)
212-
with stop_annotating():
213-
# Stop annotation to perform the Taylor test
214-
h0 = Function(D0.function_space())
215-
h0.assign(D0 * np.random.rand())
216-
assert taylor_test(Jhat, D0, h0) > 1.95
220+
# NOTE: Currently computing the Hessian will raise an exception
221+
# see https://github.com/FEniCS/ufl/issues/477

0 commit comments

Comments
 (0)