Skip to content

AuxiliaryOperatorSNES#5119

Open
JHopeCollins wants to merge 26 commits into
releasefrom
JHopeCollins/auxopsnes
Open

AuxiliaryOperatorSNES#5119
JHopeCollins wants to merge 26 commits into
releasefrom
JHopeCollins/auxopsnes

Conversation

@JHopeCollins
Copy link
Copy Markdown
Member

@JHopeCollins JHopeCollins commented May 15, 2026

If I am trying to solve F(u)=0 but for whatever reason Newton isn't working or is slow to converge, this snes type enables specifying an auxiliary nonlinear operator G(u) that can be used as a nonlinear preconditioner.

The simplest form of nonlinear preconditioning is a for a nonlinear Richardson iteration:

$$G(u^{k+1}) = G(u^{k}) - F(u^{k})$$

where the solution uhat of F(uhat)=0 is clearly a fixed point.

There's a demo for the Allen-Cahn equation where Newton fails but using a G that lags a particular term in F succeeds. AuxiliaryOpertorSNES can also be used for continuation methods like Reynolds continuation, and many other methods.

This cherrypicks the AuxiliaryOperatorSNES from #4544 because it is ready to go but the FieldsplitSNES could do with a little more stress testing/tidying up.

Pointing at release because this is strictly new functionality. The edits to firedrake/preconditioners/base.py are just updating the docstrings to be clearer about the distinction between PCBase and SNESBase.

@danshapero danshapero force-pushed the JHopeCollins/auxopsnes branch from 26f7c09 to 0d93801 Compare May 15, 2026 20:57
@JHopeCollins JHopeCollins marked this pull request as ready for review May 19, 2026 13:58
Copy link
Copy Markdown
Member

@dham dham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor changes pablo has requested to the demo.

Comment thread demos/nonlinear_pc/nonlinear_pc_allen_cahn.py.rst
Comment thread demos/nonlinear_pc/nonlinear_pc_allen_cahn.py.rst Outdated
# with forcing b = G(k) - F(k)
Gk = replace(Gk1, {uk1: uk})
b = Cofunction(V.dual())
Gk1 -= b
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gk1 is not G(u^{k+1}) anymore. Is this the right name? How about F_aux?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've gone with Gk1b and added a comment for what it is.

Comment thread firedrake/preconditioners/auxiliary_snes.py Outdated
Comment thread firedrake/preconditioners/auxiliary_snes.py
Comment thread firedrake/preconditioners/auxiliary_snes.py Outdated
Comment thread demos/nonlinear_pc/nonlinear_pc_allen_cahn.py.rst Outdated
Comment thread tests/firedrake/regression/test_auxiliary_snes.py Outdated
y.aypx(-1, x)

@PETSc.Log.EventDecorator()
def form(self, snes, uk: Function, uk1: Function, test: Argument):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe uold, unew would be more clear here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uk1 and uk matches the equations describing the iteration in the class docs and the demo so they are clearer here than old/new

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

@pbrubeck pbrubeck May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

@pbrubeck pbrubeck May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

@JHopeCollins JHopeCollins May 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this comment would be helpful to let users know that the dependence of G on the old state is completely optional

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

@pbrubeck pbrubeck May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread demos/nonlinear_pc/nonlinear_pc_allen_cahn.py.rst Outdated
Comment thread demos/nonlinear_pc/nonlinear_pc_allen_cahn.py.rst Outdated
Comment thread demos/nonlinear_pc/nonlinear_pc_allen_cahn.py.rst Outdated

.. math::

G(u) = \int_\Omega\left(\frac{\epsilon}{2}|\nabla u|^2 + \frac{1}{4}(1 - u^2)^2\right)dx.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now G(u) means a different thing. A better name would be E(u)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you do that, then the demo does not run in complex mode

Copy link
Copy Markdown
Member Author

@JHopeCollins JHopeCollins May 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread firedrake/preconditioners/auxiliary_snes.py
::

solver_parameters = {
"snes_type": "nrichardson",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could try anderson as well, it reduces iterations down to 11 for this example

Suggested change
"snes_type": "nrichardson",
"snes_type": "anderson",
"snes_anderson_m": 2,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done and I added a bit more to mention that NGMRES has more choices to make.

Comment thread demos/nonlinear_pc/nonlinear_pc_allen_cahn.py.rst
Comment thread firedrake/preconditioners/auxiliary_snes.py Outdated
Comment thread firedrake/preconditioners/auxiliary_snes.py Outdated
Comment thread firedrake/preconditioners/auxiliary_snes.py Outdated
Comment on lines +39 to +82
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/>`_)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could go in the manual. This class is not going to be instantiated ever by a user.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it is still a user facing class. This will get rendered in the API docs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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)`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the implementation, G depends on both u^{k+1} and u^{k}. This dependence should be made evident in the docstring.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

@pbrubeck pbrubeck May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@connorjward connorjward added the ci:complex Run the test suite in complex mode label May 22, 2026
::

v = TestFunction(Q)
F = (eps * inner(grad(u), grad(v)) + (u**3 - u) * v) * dx
Copy link
Copy Markdown
Contributor

@pbrubeck pbrubeck May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now we don't test for complex by default. This will break in complex mode

Suggested change
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same complex issue

Suggested change
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

@pbrubeck
Copy link
Copy Markdown
Contributor

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 form(snes, uk, uk1, v) signature drop uk and promote grabbing the current state as uk = appctx["state"]? This aligns with the mechanism we have to access the full state for AssembledPC within a fieldsplit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:complex Run the test suite in complex mode enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants