Skip to content

Commit d0d13e6

Browse files
authored
Merge pull request #84 from scientificcomputing/dokken/replace-taylor-third-order
Replace third order taylor test with finite difference check of hessian against gradient
2 parents 2791859 + dad03a8 commit d0d13e6

3 files changed

Lines changed: 117 additions & 47 deletions

File tree

‎tests/conftest.py‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import numpy as np
2+
import pyadjoint
3+
import pytest
4+
5+
6+
@pytest.fixture
7+
def assert_hessian_matches_finite_difference():
8+
"""A Hessian-accuracy checker, as a more numerically robust
9+
alternative to {py:class}`pyadjoint.taylor_test`'s standard rate-3 Hessian-corrected check.
10+
11+
That check needs cancelling several O(1) quantities down to an O(eps**3) remainder
12+
at eps <= 0.01, which the direct (MUMPS) LU factorization behind the
13+
adjoint/TLM/second-order-adjoint solves cannot always resolve to the precision it
14+
requires -- observed for saddle-point (e.g. Taylor-Hood velocity/pressure) and other
15+
blocked/nonlinear ``NonlinearProblem``/``LinearProblem`` systems in this suite, where
16+
``mat_mumps_icntl_24`` alone does not fully resolve it and PETSc's ``SNESSolve`` can
17+
even intermittently fail to converge (error code 91) under repeated nearby re-solves.
18+
The returned checker instead only needs the *gradient*'s own precision (already
19+
validated wherever a rate-2 ``taylor_test`` passes), comparing
20+
``Jhat.hessian(h)._ad_dot(h)`` directly against a central difference of
21+
``Jhat.derivative()._ad_dot(h)``.
22+
23+
Returns:
24+
A callable ``check(Jhat, m, h, *, fd_eps=1e-3, rtol=1e-2, atol=1e-2)`` -- see
25+
its own docstring for details. Exposed as a fixture (rather than a plain
26+
module-level function) so every test can use it with no import of its own,
27+
matching this project's ``--import-mode=importlib`` pytest configuration.
28+
"""
29+
30+
def _check(
31+
Jhat: pyadjoint.ReducedFunctional,
32+
m: pyadjoint.OverloadedType,
33+
h: pyadjoint.OverloadedType,
34+
*,
35+
fd_eps: float = 1e-3,
36+
rtol: float = 1e-2,
37+
atol: float = 1e-2,
38+
) -> None:
39+
"""Verify ``Jhat``'s Hessian-vector product against a central difference of its own gradient.
40+
41+
Uses ``m``/``h``'s own ``_ad_add``/``_ad_mul`` (the same primitives
42+
``pyadjoint.taylor_test`` perturbs its own evaluation points with) rather than
43+
type-specific perturbation code, so this works unchanged for a
44+
{py:class}`dolfinx_adjoint.Function`,
45+
{py:class}`dolfinx_adjoint.Constant``, or any other
46+
{py:class}`pyadjoint.OverloadedType` control.
47+
48+
Leaves ``Jhat`` evaluated at ``m`` on return.
49+
50+
``Hm`` and ``Hm_fd`` are two independent estimates of the same mathematical
51+
quantity, so they should agree up to two, unrelated, and much smaller error
52+
sources: (1) central-difference truncation, ``O(fd_eps**2)`` relative --
53+
`<1e-5` relative at the default ``fd_eps=1e-3``, negligible here; and (2)
54+
whatever precision the adjoint/TLM/second-order-adjoint linear solves and the
55+
forward (possibly SNES) solve actually achieve at the two perturbed evaluation
56+
points. If a particular problem's own solves are markedly less precise (an
57+
iterative KSP/SNES rather than a direct LU factorization, say), loosen
58+
``rtol``/``atol`` explicitly for that call rather than lowering the default.
59+
60+
Args:
61+
Jhat: The reduced functional to check.
62+
m: The control value to evaluate the Hessian at.
63+
h: The direction to evaluate the Hessian-vector product/gradient in.
64+
fd_eps: Finite-difference step size, in units of ``h``.
65+
rtol: Relative tolerance passed to ``numpy.isclose`` -- see above for why
66+
``1e-2`` is the default.
67+
atol: Absolute tolerance passed to ``numpy.isclose`` -- see above for why
68+
``1e-2`` is the default.
69+
"""
70+
71+
def dJdm_at(scale: float) -> float:
72+
Jhat(m._ad_add(h._ad_mul(scale)))
73+
return Jhat.derivative()._ad_dot(h)
74+
75+
Jhat(m)
76+
Jhat.derivative()
77+
Hm = Jhat.hessian(h)._ad_dot(h)
78+
Hm_fd = (dJdm_at(fd_eps) - dJdm_at(-fd_eps)) / (2 * fd_eps)
79+
Jhat(m)
80+
assert np.isclose(Hm, Hm_fd, rtol=rtol, atol=atol), (
81+
f"Hessian-vector product {Hm} did not match central-difference-of-gradient "
82+
f"estimate {Hm_fd} (fd_eps={fd_eps})"
83+
)
84+
85+
return _check

‎tests/test_blocked_problem.py‎

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def mesh_2D():
2424

2525

2626
@pytest.mark.parametrize("use_mixed_space", [True, False])
27-
def test_solver(use_mixed_space: bool, mesh_2D):
27+
def test_solver(use_mixed_space: bool, mesh_2D, assert_hessian_matches_finite_difference):
2828
pyadjoint.get_working_tape().clear_tape()
2929
mesh = mesh_2D
3030
el_u = basix.ufl.element("P", mesh.basix_cell(), 2, shape=(mesh.geometry.dim,))
@@ -116,13 +116,10 @@ def L1(mesh, q):
116116
f"Expected convergence rate close to 2.0, got {min_rate}"
117117
)
118118

119-
# Scale perturbation for hessian
120-
Jh(d)
121-
dJdm = Jh.derivative()._ad_dot(e)
122-
hessian = Jh.hessian(e)
123-
dHddu = hessian._ad_dot(e)
124-
min_rate = pyadjoint.taylor_test(Jh, d, e, dJdm=dJdm, Hm=dHddu)
125-
assert np.isclose(min_rate, 3.0, rtol=0.1, atol=0.1), f"Expected convergence rate close to 3.0, got {min_rate}"
119+
# This is a saddle-point (velocity/pressure) system -- the standard rate-3
120+
# taylor_test's cubic remainder is not a robust check on it (see
121+
# assert_hessian_matches_finite_difference's own docstring in conftest.py).
122+
assert_hessian_matches_finite_difference(Jh, d, e)
126123

127124
z = Function(Z)
128125
z.interpolate(
@@ -133,16 +130,11 @@ def L1(mesh, q):
133130
lambda x: (0.8 * baseline * x[1] ** 2, 2 * baseline * x[0] ** 2)
134131
) # NOTE: Has to be divergence free
135132
f.x.scatter_forward()
136-
Jh(z)
137-
dJdm = Jh.derivative()._ad_dot(f)
138-
hessian = Jh.hessian(f)
139-
dHddu = hessian._ad_dot(f)
140-
min_rate = pyadjoint.taylor_test(Jh, z, f, dJdm=dJdm, Hm=dHddu)
141-
assert np.isclose(min_rate, 3.0, rtol=0.1, atol=0.1), f"Expected convergence rate close to 3.0, got {min_rate}"
133+
assert_hessian_matches_finite_difference(Jh, z, f)
142134

143135

144136
@pytest.mark.parametrize("use_mixed_space", [True, False])
145-
def test_nonlinear_solver(use_mixed_space: bool, mesh_2D):
137+
def test_nonlinear_solver(use_mixed_space: bool, mesh_2D, assert_hessian_matches_finite_difference):
146138
"""As ``test_solver``, but for a blocked ``NonlinearProblem``: a Navier-Stokes-like
147139
velocity/pressure system with a viscosity control, genuinely nonlinear in the state
148140
via the convective term, exercising the same forward/adjoint/TLM/Hessian paths as
@@ -241,12 +233,10 @@ def test_nonlinear_solver(use_mixed_space: bool, mesh_2D):
241233
f"Expected convergence rate close to 2.0, got {min_rate}"
242234
)
243235

244-
Jh(d)
245-
dJdm = Jh.derivative()._ad_dot(e)
246-
hessian = Jh.hessian(e)
247-
dHddu = hessian._ad_dot(e)
248-
min_rate = pyadjoint.taylor_test(Jh, d, e, dJdm=dJdm, Hm=dHddu)
249-
assert np.isclose(min_rate, 3.0, rtol=0.1, atol=0.1), f"Expected convergence rate close to 3.0, got {min_rate}"
236+
# This is a saddle-point (velocity/pressure) system -- the standard rate-3
237+
# taylor_test's cubic remainder is not a robust check on it (see
238+
# assert_hessian_matches_finite_difference's own docstring in conftest.py).
239+
assert_hessian_matches_finite_difference(Jh, d, e)
250240

251241
# A second, independent evaluation point/direction: a cached-but-unrefreshed
252242
# adjoint/TLM/Hessian operator (see tests/test_tlm_update.py) could pass the
@@ -258,9 +248,4 @@ def test_nonlinear_solver(use_mixed_space: bool, mesh_2D):
258248
assert np.min(mu2.x.array - h2.x.array) > 0.0, (
259249
"Taylor test perturbation must not violate positivity of viscosity"
260250
)
261-
Jh(mu2)
262-
dJdm = Jh.derivative()._ad_dot(h2)
263-
hessian = Jh.hessian(h2)
264-
dHddu = hessian._ad_dot(h2)
265-
min_rate = pyadjoint.taylor_test(Jh, mu2, h2, dJdm=dJdm, Hm=dHddu)
266-
assert np.isclose(min_rate, 3.0, rtol=0.1, atol=0.1), f"Expected convergence rate close to 3.0, got {min_rate}"
251+
assert_hessian_matches_finite_difference(Jh, mu2, h2)

‎tests/test_tlm_update.py‎

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ def _viscous_stokes(mesh):
9696

9797

9898
@pytest.mark.parametrize("warm_up_at_another_point", [False, True])
99-
def test_hessian_is_independent_of_previous_evaluation_points(warm_up_at_another_point, mesh_2D):
99+
def test_hessian_is_independent_of_previous_evaluation_points(
100+
warm_up_at_another_point, mesh_2D, assert_hessian_matches_finite_difference
101+
):
100102
"""The Hessian at ``m2`` must not depend on whether ``J`` was evaluated at ``m1`` first.
101103
102104
Both parametrizations run the identical second-order Taylor test at ``m2``. The only
@@ -120,12 +122,10 @@ def test_hessian_is_independent_of_previous_evaluation_points(warm_up_at_another
120122
Jh.hessian(h)
121123

122124
assert np.min(m2.x.array - h.x.array) > 0.0, "Taylor test perturbation must not violate positivity of viscosity"
123-
Jh(m2)
124-
dJdm = Jh.derivative()._ad_dot(h)
125-
Hm = Jh.hessian(h)._ad_dot(h)
126-
127-
min_rate = pyadjoint.taylor_test(Jh, m2, h, dJdm=dJdm, Hm=Hm)
128-
assert np.isclose(min_rate, 3.0, rtol=0.1, atol=0.1), f"Expected convergence rate close to 3.0, got {min_rate}"
125+
# This is a saddle-point (velocity/pressure) system -- the standard rate-3
126+
# taylor_test's cubic remainder is not a robust check on it (see
127+
# assert_hessian_matches_finite_difference's own docstring in conftest.py).
128+
assert_hessian_matches_finite_difference(Jh, m2, h)
129129

130130

131131
def _navier_stokes(mesh):
@@ -206,7 +206,9 @@ def _navier_stokes(mesh):
206206
return pyadjoint.ReducedFunctional(J, pyadjoint.Control(mu)), Z
207207

208208

209-
def test_hessian_is_independent_of_previous_evaluation_points_navier_stokes(mesh_2D):
209+
def test_hessian_is_independent_of_previous_evaluation_points_navier_stokes(
210+
mesh_2D, assert_hessian_matches_finite_difference
211+
):
210212
"""``test_hessian_is_independent_of_previous_evaluation_points``'s ``NonlinearProblem``
211213
sibling: the same second-order Taylor test, but on a genuinely nonlinear, blocked
212214
(multi-output) residual, exercising the blocked Hessian path in
@@ -224,12 +226,10 @@ def test_hessian_is_independent_of_previous_evaluation_points_navier_stokes(mesh
224226
h = Function(Z)
225227
h.interpolate(lambda x: 0.4 * baseline * np.sin(3 * x[0]))
226228
assert np.min(m2.x.array - h.x.array) > 0.01, "Taylor test perturbation must not violate positivity of viscosity"
227-
Jh(m2)
228-
dJdm = Jh.derivative()._ad_dot(h)
229-
Hm = Jh.hessian(h)._ad_dot(h)
230-
231-
min_rate = pyadjoint.taylor_test(Jh, m2, h, dJdm=dJdm, Hm=Hm)
232-
assert np.isclose(min_rate, 3.0, rtol=0.1, atol=0.1), f"Expected convergence rate close to 3.0, got {min_rate}"
229+
# This is a saddle-point (velocity/pressure) system -- the standard rate-3
230+
# taylor_test's cubic remainder is not a robust check on it (see
231+
# assert_hessian_matches_finite_difference's own docstring in conftest.py).
232+
assert_hessian_matches_finite_difference(Jh, m2, h)
233233

234234

235235
def _diffusive_poisson(mesh):
@@ -309,7 +309,7 @@ def test_hessian_is_independent_of_previous_evaluation_points_scalar(warm_up_at_
309309
assert np.isclose(min_rate, 3.0, rtol=0.1, atol=0.1), f"Expected convergence rate close to 3.0, got {min_rate}"
310310

311311

312-
def test_hessian_mpi_breakdown(mesh_2D):
312+
def test_hessian_mpi_breakdown(mesh_2D, assert_hessian_matches_finite_difference):
313313
pyadjoint.get_working_tape().clear_tape()
314314
Jh, Z = _viscous_stokes(mesh_2D)
315315
baseline = 23.2
@@ -358,8 +358,8 @@ def test_hessian_mpi_breakdown(mesh_2D):
358358
hess_diff = np.linalg.norm(H_cold_array - H_warm_array)
359359
assert hess_diff < 1e-10, f"Rank {rank}: Hessian differs after warm up! Diff: {hess_diff}"
360360

361-
# 4. If arrays match, run Taylor test
362-
dJdm = dJ_warm._ad_dot(h)
363-
Hm = H_warm._ad_dot(h)
364-
min_rate = pyadjoint.taylor_test(Jh, m2, h, dJdm=dJdm, Hm=Hm)
365-
assert np.isclose(min_rate, 3.0, rtol=0.1, atol=0.1), f"Expected 3.0, got {min_rate}"
361+
# 4. If arrays match, verify the Hessian value itself is correct. This is a
362+
# saddle-point (velocity/pressure) system -- the standard rate-3 taylor_test's
363+
# cubic remainder is not a robust check on it (see
364+
# assert_hessian_matches_finite_difference's own docstring in conftest.py).
365+
assert_hessian_matches_finite_difference(Jh, m2, h)

0 commit comments

Comments
 (0)