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?
There was a problem hiding this comment.
Fair enough I guess. Gk1 - b is also not the auxiliary form though. Maybe Gk1b? It is the auxiliary form plus a forcing term (that we happen to know we will assemble Gk - Fk into later).
There was a problem hiding this comment.
It think any meaningful name should do, as long as it is not the same one.
The in-place subtraction makes it seem that we are mutating it, but that is not what is happening under the hood, so it'd be good to get rid of it.
There was a problem hiding this comment.
I've gone with Gk1b and added a comment for what it is.
| 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?
There was a problem hiding this comment.
Is there a convention established elsewhere in Firedrake?
Not really I don't think.
In the base class the method arguments really should stay as uk1 and uk because it corresponds directly to the description of the fixed point iteration in the docstrings of the method and the class.
If uold and unew would be clearer in the demo where you don't have the docstrings, only the form method definition, then I'm fine with that.
There was a problem hiding this comment.
Is there a convention established elsewhere in Firedrake?
While single-character names are far from ideal, in Firedrake we have well established lingo. For instance, u in the solution or TrialFunction, v is the TestFunction, F is the residual Form, V is the FunctionSpace, etc. Situations where we need to distinguish between two related variables rarely arise in public API, but I can point out a few cases.
In the (non-Irksome) time-stepping demos, we define u and uold to denote the current and previous solutions.
Throughout the multigrid codebase we use uc and uf to denote the coarse and fine solutions.
There was a problem hiding this comment.
The multigrid literature often uses u^{l} and u^{l+1}. In latex or mathjax this works well because l+1 is quite explicit. If instead we opted for names like ul and ul1 in our code, as opposed to uc anf uf, we would be decreasing the readability of the code by itself.
There was a problem hiding this comment.
The issue with uk1 is that this variable name needs further explanation, due to the conflict between u^{k-1} and u^{k+1}
| 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 in the paragraph discussing the full expression for the preconditioned iteration.
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.
Once you expand the full Gk1 = Gk - Fk yes that term cancels in Gk1 and Gk. But I think it is better to include it in the definition of G here because it keeps the equivalence of the fixed point iteration with G(u^{k+1}; u^{k}) = 0.
There was a problem hiding this comment.
Also having G(u^k, u^{k+1}) = F(u^k) when u^{k+1} = u^k gives you no extra source term (b).
Yes for this specific choice of G this is true. But we cannot rely on that in general because it's perfectly acceptable for it not to be true.
There was a problem hiding this comment.
The source term b is zero for this choice of F. But one could also have obtained G by dropping the negative term in F, in which case b is nonzero, but yet you get an identical algorithm.
I think we should comment on the alternative choice of dropping the negative term.
There was a problem hiding this comment.
Also this comment would be helpful to let users know that the dependence of G on the old state is completely optional
There was a problem hiding this comment.
I think explaining at this level of detail will only confuse someone who's new to NPC. For that matter, I'd rather change some of the text that Josh already added to the demo back to considering only the particular case where G(u_k; u_k) = F(u_k). In that case the right-hand side of the fixed-point iteration cancels and you end up solving just G(u; u_k) = 0. This is often sufficient to obtain a good NPC and for an introduction I think that's more important to communicate than anything else. You can show people later that it isn't a necessary condition. The full details are already in the docstring for AuxOpSNES.
There was a problem hiding this comment.
Sure, I'm not too fussy about this. You might have a point, some people might get confused if we casually mention that dropping the term that depends on the old state from both sides of the equation gives an equivalent nonlinear system
|
|
||
| .. 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
There was a problem hiding this comment.
Is Allen-Cahn ill posed in complex arithmetic then? Surely if the derivation is not well posed then it doesn't make sense to solve the equation in complex mode. We can add the skipcomplex marker to the demo in the test suite.
There was a problem hiding this comment.
Lev Landau has entered the chat. Is it a small change to the algebra in the form to make this work in complex mode? If that's more involved then I say we punt on complex mode for later but we do want to get to that eventually.
There was a problem hiding this comment.
It's not that the PDE is not well posed in complex arithmetic, when we symbolically differentiate inner(u,u)*dx twice to get the Jacobian we get inner(test, trial)*dx + inner(trial, test)*dx, and the form assembler requires only the test function to be conjugated.
| :: | ||
|
|
||
| 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"
There was a problem hiding this comment.
Done and I added a bit more to mention that NGMRES has more choices to make.
Co-authored-by: Pablo Brubeck <brubeck@protonmail.com>
…ES test. Co-authored-by: Pablo Brubeck <brubeck@protonmail.com>
Co-authored-by: Pablo Brubeck <brubeck@protonmail.com>
…firedrake into JHopeCollins/auxopsnes
| The following solver parameters will specify the above Richardson | ||
| iteration, assuming that the user has defined a class | ||
| ``UserAuxiliarySNES`` which inherits from | ||
| :class:`~.AuxiliaryOperatorSNES` and implements the | ||
| :meth:`~.AuxiliaryOperatorSNES.form` method. In this example, the | ||
| inner solve uses a Newton method with a relative tolerance of 1e-4. | ||
|
|
||
| .. code-block:: python3 | ||
|
|
||
| solver_parameters = { | ||
| "snes_rtol": 1e-8, | ||
| "snes_type": "python", | ||
| "snes_python_type": f"{__name__}.UserAuxiliarySNES", | ||
| "aux": { | ||
| "snes_rtol": 1e-4, | ||
| "snes_type": "newtonls", | ||
| ... | ||
| } | ||
| } | ||
|
|
||
| The following parameters describe the same Richardson iteration | ||
| as the parameters above, but explicitly specifying the auxiliary | ||
| form as a nonlinear preconditioner using the ``npc_`` prefix. | ||
|
|
||
| .. code-block:: python3 | ||
|
|
||
| solver_parameters = { | ||
| "snes_rtol": 1e-8, | ||
| "snes_type": "nrichardson", | ||
| "npc_snes_max_it": 1, | ||
| "npc_snes_type": "python", | ||
| "npc_snes_python_type": f"{__name__}.UserAuxiliarySNES", | ||
| "npc_aux": { | ||
| "snes_rtol": 1e-4, | ||
| "snes_type": "newtonls", | ||
| ... | ||
| } | ||
| } | ||
|
|
||
| Although using ``"npc_"`` to specify the parameters is more verbose | ||
| than the original, it allows for a wider variety of methods. For | ||
| example, by changing the outer ``"snes_type"`` to ``"anderson"``, | ||
| we can use preconditioned Anderson acceleration | ||
| (`<https://petsc.org/release/manualpages/SNES/SNESANDERSON/>`_) |
There was a problem hiding this comment.
This could go in the manual. This class is not going to be instantiated ever by a user.
There was a problem hiding this comment.
But it is still a user facing class. This will get rendered in the API docs
There was a problem hiding this comment.
Regardless of what we decide to leave in the class docstring, we should be adding an entry for AuxiliaryOperatorSNES in this manual section.
I think this level of detail could live in that other section, and we could keep a brief summary in the docstring, with a cross-reference.
We documented things in a similar way when we introduced the quad_scheme kwarg for FunctionSpace in https://github.com/firedrakeproject/firedrake/pull/4607/changes
| nullspace=ctx._nullspace, | ||
| transpose_nullspace=ctx._nullspace_T, | ||
| near_nullspace=ctx._near_nullspace, | ||
| appctx=ctx.appctx, options_prefix=prefix) |
There was a problem hiding this comment.
| appctx=ctx.appctx, options_prefix=prefix) | |
| appctx=ctx.appctx, | |
| options_prefix=prefix, | |
| pre_apply_bcs=ctx.pre_apply_bcs, | |
| ) |
| class AuxiliaryOperatorSNES(SNESBase): | ||
| """ | ||
| Solve a residual form :math:`F(u) = 0` using a nonlinear Richardson | ||
| iteration preconditioned with an auxiliary form :math:`G(u)`. |
There was a problem hiding this comment.
In the implementation, G depends on both u^{k+1} and u^{k}. This dependence should be made evident in the docstring.
There was a problem hiding this comment.
There is one note in form that explains this, but I think the main mathematical description in this docstring should at least suggest the possibility of G depending on the old state.
There was a problem hiding this comment.
The demo refers to G as G_{k}(u) = G(u; u_{k}), should we adopt that convention here? It'd be good to unify notation.
| :: | ||
|
|
||
| v = TestFunction(Q) | ||
| F = (eps * inner(grad(u), grad(v)) + (u**3 - u) * v) * dx |
There was a problem hiding this comment.
Now we don't test for complex by default. This will break in complex mode
| F = (eps * inner(grad(u), grad(v)) + (u**3 - u) * v) * dx | |
| F = (eps * inner(grad(u), grad(v)) + inner(u**3 - u, v)) * dx |
| 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.
Same complex issue
| return (eps * inner(grad(u), grad(v)) + (u**3 - u_k) * v) * dx, bcs | |
| return (eps * inner(grad(u), grad(v)) + inner(u**3 - u_k, v)) * dx, bcs |
|
Just a thought. What is so special about G depending on the current state? It might as well depend on any other coefficient in the original problem. Should we instead of having the |
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.