Use CachedSolverBlock for project (-> #4638)#5245
Conversation
|
Not really sure why the |
The ProjectBlock was essentially an extension of SolveVarFormBlock that sets up the mass matrix system for the adjoint machinery. For the original (forward) project() call, the "proper" form is used, but the forward solver on the tape uses a simplified mass matrix. The approach here replicates that behaviour, but it's routed through the new CachedSolverBlock. As a consequence however, we have annotation on the Projector factory, which means that adjoint-friendly projection can occur through either the bare project() call, or by creating a Projector object. The implementation is a bit ugly: because a lot of the caching machinery belongs to the NonlinearVariationalSolverMixin, we essentially dispatch to a dummy initialiser when creating a BasicProjector to ensure the solver caches are created correctly. A call to project() annotates as though a solve occurred, but uses the underlying projection implementation. The SupermeshSolveBlock is actually a standalone object, so we just create that when SupermeshProjector.project() is called. Again, this allows for both bare and object-based projection to be taped. The final case is the "null" project from one function to another on the same space. This creates an Assigner, and I assume it will "just work" through the annotation on assign().
I guess this makes the changes API-breaking, so they should be smoothed over a bit. But for the moment, this makes the tests pass.
c0088c9 to
8952c19
Compare
It stands to reason (maybe), that the cached forward solver should just duplicate the solver parameters from the original forward solver. The TLM/adjoint solvers can probably use linear parameters with a mechanism for overriding. This way we can at least get the recomputation solve for project() to converge.
8952c19 to
c47e9b9
Compare
|
Okay, I've got a slightly more minimal reproducer that avoids the project path. This actually doesn't rely on the from firedrake import *
from firedrake.adjoint import *
continue_annotation()
mesh = UnitSquareMesh(1, 1)
V = FunctionSpace(mesh, "CG", 1)
R = FunctionSpace(mesh, "R", 0)
u = Function(V)
w = TestFunction(V)
c = Function(R, val=1.)
a = inner(TrialFunction(V), w) * dx
L = inner(c*u, w) * dx
if reproduce:
solve(a == L, u) # diverges for this PR, *not* main
else:
# always diverges
problem = LinearVariationalProblem(a, L, u)
NonlinearVariationalSolver(problem).solve()
J = assemble(u**2 * dx)
rf = ReducedFunctional(J, Control(c))
pause_annotation()
rf.derivative()Here, the bilinear form is essentially the projection form that was causing the issues above. When we go through the NLVP, the bilinear form becomes linear during the static method Note that class GenericSolveBlock(Block):
...
def _create_F_form(self):
if self.linear:
tmp_u = Function(self.function_space)
F_form = action(self.lhs, tmp_u) - self.rhs
...This means the form is something like and so the derivative and adjoint thereof only has one term. This is probably the "correct" behaviour, because even if we work around the degenerate solve in the test case: diff --git a/tests/firedrake/adjoint/test_assignment.py
b/tests/firedrake/adjoint/test_assignment.py
index 342355076..d1884fc38 100644
--- a/tests/firedrake/adjoint/test_assignment.py
+++ b/tests/firedrake/adjoint/test_assignment.py
@@ -254,7 +254,7 @@ def test_adjoint_cleanup(scheduler, rg):
r.assign(2.0)
u.project(r * u)
- r.assign(1.0)
+ r.assign(1.5)
u.project(r * u)
J = assemble((u - Function(V).assign(1.0)) ** 2 * dx)The Taylor test fails: |
Description
The
ProjectBlockwas essentially an extension ofSolveVarFormBlockthat sets up the mass matrix system for the adjoint machinery. For the original (forward)project()call, the optimised solver-free path is used. However, any of the adjoint operations including a forward recomputation would solve the system.The approach here replicates that behaviour, but it's routed through the new
CachedSolverBlock. As a consequence however, we have annotation on theProjectorfactory, which means that adjoint-friendly projection can occur through either the bareproject()call, or by creating aProjectorobject.The implementation is a bit ugly: because a lot of the caching machinery belongs to the
NonlinearVariationalSolverMixin, we essentially dispatch to a dummy initialiser when creating aBasicProjectorto ensure the solver caches are created correctly. A call toproject()annotates as though a solve occurred, but uses the underlying projection implementation.The
SupermeshSolveBlockis actually a standalone object, so we just create that whenSupermeshProjector.project()is called. Again, this allows for both bare and object-based projection to be taped.The final case is the "null" project from one function to another on the same space. This creates an
Assigner, and I assume it will "just work" through the annotation onassign().Questions
solve()change) by removing theannotatekwarg? Now we have to wrap things inwith stop_annotating()or similarad_block_taginner(u,w)*dx == inner(u+c,w)*dxfor a Real c and try to evaluate the tape, there's a divergence... guess there aren't any adjoint tests for dead simple solves)