AuxiliaryOperatorSNES#5119
Conversation
26f7c09 to
0d93801
Compare
…firedrake into JHopeCollins/auxopsnes
…firedrake into JHopeCollins/auxopsnes
dham
left a comment
There was a problem hiding this comment.
minor changes pablo has requested to the demo.
| # with forcing b = G(k) - F(k) | ||
| Gk = replace(Gk1, {uk1: uk}) | ||
| b = Cofunction(V.dual()) | ||
| Gk1 -= b |
There was a problem hiding this comment.
Gk1 is not G(u^{k+1}) anymore. Is this the right name? How about F_aux?
| self.Gk = Gk | ||
| self.Gk1 = Gk1 |
There was a problem hiding this comment.
Do these need to be stored as class attributes?
|
|
||
| # G(u^{k+1}) - b = 0 | ||
| self.uk1.assign(self.uk) | ||
| self.solver.solve() |
There was a problem hiding this comment.
We might want to enable solver.solve(rhs=b) so the logic with Gk1 becomes clearer. Obviously a separate PR.
| Gk, bcs=bcs, | ||
| form_compiler_parameters=ctx.fcp, | ||
| options_prefix=prefix | ||
| ).assemble |
There was a problem hiding this comment.
Potentially one could reuse the residual assembler in the _SNESContext of the solver defined below, similar to what we did here for AssembledPC
https://github.com/firedrakeproject/firedrake/pull/5079/changes
Up to you if you want to defer this job to a separate PR.
| can create problems for others, for example when using the multigrid method. | ||
| Instead, we've opted to call the parent class's `form` method, which will | ||
| return the original variational form and boundary conditions. We then discard | ||
| the original variational form, which we aren't using here. |
There was a problem hiding this comment.
Maybe mention that we can also manipulate F
There was a problem hiding this comment.
Is that safe? Then the inner problem is mutating the outer problem, which seems impolite
There was a problem hiding this comment.
I mean creating a new G from F, e.g. G = F + extra_terms
There was a problem hiding this comment.
ufl.Forms are supposed to be immutable
There was a problem hiding this comment.
Also F += extra_terms will not modify F "in-place", it'd just assign a new Form to F.
There was a problem hiding this comment.
Ok, I added a note explaining that you can also come up with the form by altering F instead of creating it from whole cloth.
| def form(self, snes, state, func, test): | ||
| prefix = (snes.getOptionsPrefix() or "") + f"snes_{self._prefix}" | ||
| w = fd.PETSc.Options().getScalar(prefix + "w") | ||
| return f(func, test, w=w), None |
There was a problem hiding this comment.
| def form(self, snes, state, func, test): | |
| prefix = (snes.getOptionsPrefix() or "") + f"snes_{self._prefix}" | |
| w = fd.PETSc.Options().getScalar(prefix + "w") | |
| return f(func, test, w=w), None | |
| def form(self, snes, state, func, test): | |
| F, bcs = super().form(snes, state, func, test) | |
| prefix = (snes.getOptionsPrefix() or "") + f"snes_{self._prefix}" | |
| w = fd.PETSc.Options(prefix).getScalar("w") | |
| return f(func, test, w=w), bcs |
| y.aypx(-1, x) | ||
|
|
||
| @PETSc.Log.EventDecorator() | ||
| def form(self, snes, uk: Function, uk1: Function, test: Argument): |
There was a problem hiding this comment.
Maybe uold, unew would be more clear here
There was a problem hiding this comment.
uk1 and uk matches the equations describing the iteration in the class docs and the demo so they are clearer here than old/new
There was a problem hiding this comment.
Sure, with the context of the docstrings, you could tell that uk1 refers to u_{k+1}. But a subclass will not include the docstring, so the context will get lost easily in user code. If a user cargo-culted this notation, it be hard to tell whether uk1 refers to u_{k-1} or u_{k+1}.
There was a problem hiding this comment.
I had to puzzle over this for a moment when I was writing the demo. I agree that a different naming scheme could be beneficial. Is there a convention established elsewhere in Firedrake?
| class AllenCahnAuxSNES(firedrake.AuxiliaryOperatorSNES): | ||
| def form(self, snes, u_k, u, v): | ||
| F, bcs = super().form(snes, u_k, u, v) | ||
| return (eps * inner(grad(u), grad(v)) + (u**3 - u_k) * v) * dx, bcs |
There was a problem hiding this comment.
Please correct me if I'm wrong, but "lagging" the negative term here is equivalent to dropping it from G.
Also having G(u^k, u^{k+1}) = F(u^k) when u^{k+1} = u^k gives you no extra source term (b).
Maybe it'd be worth adding a comment.
There was a problem hiding this comment.
To your second point, this is the exactly the point about the stationary point of the iteration
|
|
||
| .. math:: | ||
|
|
||
| G(u) = \int_\Omega\left(\frac{\epsilon}{2}|\nabla u|^2 + \frac{1}{4}(1 - u^2)^2\right)dx. |
There was a problem hiding this comment.
Now G(u) means a different thing. A better name would be E(u)
There was a problem hiding this comment.
I've changed this as you suggest. It would be a bigger change but I could instead define E(u) at the very beginning and work almost entirely from this, defining F = firedrake.derivative(E, u).
There was a problem hiding this comment.
If you do that, then the demo does not run in complex mode
| with self.uk.dat.vec_wo as vec: | ||
| x.copy(vec) | ||
|
|
||
| # b = G(u^{k}) | ||
| self.assemble_gk(tensor=self.b) | ||
|
|
||
| # b = G(u^{k}) - F(u^{k}) | ||
| with self.b.dat.vec as vec: | ||
| vec -= f | ||
|
|
||
| # G(u^{k+1}) - b = 0 | ||
| self.uk1.assign(self.uk) | ||
| self.solver.solve() |
There was a problem hiding this comment.
We can get Gk-f out of Gk1-b (no need to generate code twice). For the moment, one needs an extra buffer self.b1, but this extra buffer might go away once #5130 lands
| with self.uk.dat.vec_wo as vec: | |
| x.copy(vec) | |
| # b = G(u^{k}) | |
| self.assemble_gk(tensor=self.b) | |
| # b = G(u^{k}) - F(u^{k}) | |
| with self.b.dat.vec as vec: | |
| vec -= f | |
| # G(u^{k+1}) - b = 0 | |
| self.uk1.assign(self.uk) | |
| self.solver.solve() | |
| # u^{k+1} = u^{k} = x | |
| with self.uk.dat.vec_wo as vec: | |
| x.copy(vec) | |
| with self.uk1.dat.vec_wo as vec: | |
| x.copy(vec) | |
| # b = f | |
| with self.b.dat.vec_wo as vec: | |
| f.copy(vec) | |
| # b1 = G(u^{k}; u^{k}) - F(u^{k}) | |
| self.solver._ctx._assemble_residual(tensor=self.b1) | |
| # b = b1 | |
| self.b.assign(self.b1) | |
| # solve G(u^{k}; u^{k+1}) - b = 0 for u^{k+1} | |
| self.solver.solve() |
| :: | ||
|
|
||
| solver_parameters = { | ||
| "snes_type": "nrichardson", |
There was a problem hiding this comment.
We could try anderson as well, it reduces iterations down to 11 for this example
| "snes_type": "nrichardson", | |
| "snes_type": "anderson", | |
| "snes_anderson_m": 2, |
There was a problem hiding this comment.
I considered this but wanted to keep it simple at first. I could add Anderson or NGMRES at the end. But I was planning another demo showing more advanced techniques using the Navier-Stokes equations in a later PR.
There was a problem hiding this comment.
How about just a sentence along the lines of "we could improve the convergence by swapping to an accelerated SNES type such as Anderson or ngmres, which you may want to experiment with in practice/in your own application"
| }, | ||
| } | ||
|
|
||
| solver = NonlinearVariationalSolver(problem, solver_parameters=solver_parameters) |
There was a problem hiding this comment.
It is unclear that the failed solve did not update the value of u. We should make it obvious that we are starting from the same initial guess
| solver = NonlinearVariationalSolver(problem, solver_parameters=solver_parameters) | |
| u.interpolate(initial_guess) | |
| solver = NonlinearVariationalSolver(problem, solver_parameters=solver_parameters) |
There was a problem hiding this comment.
I have this further up, immediately after the initial Newton solve failed.
| def initialize(self, snes): | ||
| from firedrake import ( # circular import if this is at file level | ||
| NonlinearVariationalSolver, NonlinearVariationalProblem, | ||
| Function, TestFunction, Cofunction) |
There was a problem hiding this comment.
Function is imported at the top of this file. Are we sure all of these give circular imports?
If I am trying to solve
F(u)=0but for whatever reason Newton isn't working or is slow to converge, this snes type enables specifying an auxiliary nonlinear operatorG(u)that can be used as a nonlinear preconditioner.The simplest form of nonlinear preconditioning is a for a nonlinear Richardson iteration:
where the solution
uhatofF(uhat)=0is clearly a fixed point.There's a demo for the Allen-Cahn equation where Newton fails but using a
Gthat lags a particular term inFsucceeds.AuxiliaryOpertorSNEScan also be used for continuation methods like Reynolds continuation, and many other methods.This cherrypicks the
AuxiliaryOperatorSNESfrom #4544 because it is ready to go but theFieldsplitSNEScould do with a little more stress testing/tidying up.Pointing at
releasebecause this is strictly new functionality. The edits tofiredrake/preconditioners/base.pyare just updating the docstrings to be clearer about the distinction betweenPCBaseandSNESBase.