Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c713dec
AuxiliaryOperatorSNES
JHopeCollins May 15, 2026
26f7c09
docs links
JHopeCollins May 15, 2026
a9f770b
AuxiliaryOperatorSNES
JHopeCollins May 15, 2026
0d93801
docs links
JHopeCollins May 15, 2026
bbf35e9
Merge branch 'JHopeCollins/auxopsnes' of github.com:firedrakeproject/…
JHopeCollins May 16, 2026
c50f8b9
Add Allen-Cahn demo of NPC
danshapero May 19, 2026
3004e63
Merge branch 'JHopeCollins/auxopsnes' of github.com:firedrakeproject/…
JHopeCollins May 19, 2026
3066ff7
Merge branch 'release' into JHopeCollins/auxopsnes
JHopeCollins May 19, 2026
3de898d
add AuxiliaryOperatorSNES demo to website and tests
JHopeCollins May 19, 2026
f12861a
updates to AuxiliaryOperatorSNES demo
JHopeCollins May 19, 2026
f37c18b
updates to AuxiliaryOperatorSNES demo
JHopeCollins May 19, 2026
06fa563
Fix citation in Allen-Cahn demo
danshapero May 19, 2026
6347def
Show some PETSc log output
danshapero May 19, 2026
b416461
Wordsmithing demo
danshapero May 20, 2026
26994b0
Update AuxiliaryOperatorSNES docstring
JHopeCollins May 21, 2026
696f38e
Explicitly pass through bcs from outer problem in AuxiliaryOperatorSN…
JHopeCollins May 21, 2026
960e48f
Merge branch 'release' into JHopeCollins/auxopsnes
JHopeCollins May 21, 2026
7a0f3f1
rename variables in AuxiliaryOperatorSNES
JHopeCollins May 21, 2026
a721c29
move imports to top of file in AuxiliaryOperatorSNES
JHopeCollins May 21, 2026
233249d
reuse solver assembler in AuxiliaryOperatorSNES to reduce symbolic ov…
JHopeCollins May 21, 2026
0855b07
AuxiliaryOperatorSNES demo words
JHopeCollins May 21, 2026
8409dd0
Update firedrake/preconditioners/auxiliary_snes.py
JHopeCollins May 21, 2026
193f3f3
remove cyclic import in auxiliary_snes.py
JHopeCollins May 21, 2026
87fef34
Merge branch 'JHopeCollins/auxopsnes' of github.com:firedrakeproject/…
JHopeCollins May 21, 2026
9269718
Describe NGMRES
danshapero May 22, 2026
38065b6
Fixes for complex mode
danshapero May 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions demos/demo_references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -404,3 +404,15 @@ @Article{Farrell2015
pages = {A2026--A2045},
doi = {10.1137/140984798},
}

@article{brune2015composing,
title={Composing scalable nonlinear algebraic solvers},
author={Brune, Peter R and Knepley, Matthew G and Smith, Barry F and Tu, Xuemin},
journal={SIAM Review},
volume={57},
number={4},
pages={535--565},
year={2015},
publisher={SIAM},
doi={10.1137/130936725}
}
282 changes: 282 additions & 0 deletions demos/nonlinear_pc/nonlinear_pc_allen_cahn.py.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
Nonlinear preconditioning applied to the Allen-Cahn equation
============================================================

Contributed by `Daniel Shapero <https://psc.apl.uw.edu/people/investigators/daniel-shapero/>`_
and `Josh Hope-Collins <https://profiles.imperial.ac.uk/joshua.hope-collins13>`_.

In the other demonstrations on nonlinear PDE, we've used Newton's method to
solve finite- dimensional systems of nonlinear equations. Suppose we want to
solve a system

.. math::

F(u) = 0.

At each step of Newton's method, we first compute a *search direction*
:math:`v` by solving the linear system

.. math::

dF(u)v = -F(u).

We can then use a line search along :math:`v` to obtain the next candidate
solution. When we talked about preconditioning, we have always meant applying
a preconditioner to the linear system for the search direction. A linear
preconditioner transforms a linear system into an equivalent one that is
easier to solve.

For some highly nonlinear problems, however, Newton's method can stagnate or
fail to converge, even with globalization strategies such as a line search.
For example, if :math:`dF(u)` behaves badly, we might not be able to compute a
search direction at all.

An alternative strategy is to use *nonlinear preconditioning*, abbreviated as
NPC in the following. Nonlinear preconditioning does the same thing as linear
preconditioning: it transforms the problem into an equivalent one that is
(hopefully) easier to solve. Rather than work on the linear system for the
search direction, however, NPC works directly on the nonlinear system itself.
Typically NPC is applied within a higher-level nonlinear solver. For example,
there is a nonlinear analogue of GMRES. For a review of nonlinear solver and
preconditioning strategies beyond Newton, see :cite:`brune2015composing`.

Here we will demonstrate how to use nonlinear preconditioning for the steady-
state Allen-Cahn equation

.. math::

F(u) = -\epsilon\Delta u - u + u^3 = 0
Comment thread
pbrubeck marked this conversation as resolved.
Outdated

on the unit interval with Dirichlet boundary conditions :math:`u(0) = -1` and
:math:`u(1) = +1`. The Jacobian of this residual is indefinite wherever
:math:`|u| < 1/\sqrt{3}`. Newton's method can fail to compute a search
direciton when starting from an initial guess that crosses this region.

This demo is adapted from this `Chebfun example`_.

.. _Chebfun example: https://www.chebfun.org/examples/ode-nonlin/AllenCahn.html

::

import numpy as np
from firedrake import *

Here we use a domain of length 10, a small diffusion coefficient of 0.003,
and an initial guess that ramps from +1 at the left-hand boundary to -1 at
the right.

::

nx = 128
lx = 10.0
eps = Constant(3e-3)

mesh = IntervalMesh(nx, lx)
Q = FunctionSpace(mesh, "CG", 1)

x, = SpatialCoordinate(mesh)
u_1 = Constant(1)
u_2 = Constant(-1)
Lx = Constant(lx)
initial_guess = (1 - x / Lx) * u_1 + x / Lx * u_2

bcs = [DirichletBC(Q, u_1, [1]), DirichletBC(Q, u_2, [2])]

u = Function(Q)
u.interpolate(initial_guess)

In order to have a good baseline to compare against, we want to use as good
a nonlinear solution strategy as possible. Here we use Newton's method with
the *critical point* line search, which is specially adapted for problems
like Allen-Cahn which can be derived from minimizing some free energy. Even
with 10 iterations of a line search, Newton's method will fail. You can try
other line search methods (secant, backtracking, etc.) and find similar
outcomes. If you pump the number of line search iterations way up, you can
make Newton's method converge... to the wrong solution!

::

v = TestFunction(Q)
F = (eps * inner(grad(u), grad(v)) - (u - u**3) * v) * dx
Comment thread
pbrubeck marked this conversation as resolved.
Outdated

problem = NonlinearVariationalProblem(F, u, bcs)

newton_parameters = {
"snes_type": "newtonls",
"snes_monitor": None,
"snes_converged_reason": None,
"snes_linesearch_type": "cp",
"snes_linesearch_max_it": 10,
"snes_linesearch_monitor": None,
}
solver = NonlinearVariationalSolver(problem, solver_parameters=newton_parameters)
try:
solver.solve()
except ConvergenceError as err:
print(err)
print("--------------------------")
print("Solver failed to converge!")
print("Resetting `u`")
u.interpolate(initial_guess)

The residual decreases at first but eventually diverges. Here's some of the PETSc
log output:

.. code-block:: console

$ python nonlinear_pc_allen_cahn.py
0 SNES Function norm 2.439229081145e-01
1 SNES Function norm 5.663027251974e+01
2 SNES Function norm 1.667065795962e+01
3 SNES Function norm 4.864679262673e+00
4 SNES Function norm 1.388964354434e+00
5 SNES Function norm 3.744562754242e-01
6 SNES Function norm 8.820230192544e-02
7 SNES Function norm 3.682817028776e-02
8 SNES Function norm 5.405858541080e-02
9 SNES Function norm 5.368629859638e-02
...
26 SNES Function norm 5.280237868253e-02
27 SNES Function norm 1.098413882561e+00
28 SNES Function norm 1.082944223664e+00
29 SNES Function norm 6.520280025999e+03
Nonlinear firedrake_0_ solve did not converge due to DIVERGED_DTOL iterations 29

In other scenarios, the solver doesn't explode as dramatically but rather
stagnates and exceeds the number of allowable Newton iterations.

To salvage the wreck, we'll use nonlinear preconditioning. Here we use
the simplest solution strategy possible: preconditioned nonlinear
Richardson iterations.
The idea is that if :math:`F(u)=0` is too difficult to solve, we can
instead solve a auxiliary operator :math:`G(u)=0` which is in some sense
*nearby* to :math:`F`, and use that solution to iterate towards a solution
for :math:`F(u)=0`.
At each iteration :math:`k` we solve the following system for the next
iterate :math:`u_{k+1}` using the value of the current iterate :math:`u_k`.

.. math::

G_{k}(u_{k+1}) = G_{k}(u_{k}) - F(u_{k}),
\quad
G_{k}(u) = G(u; u_{k}).

We can note several properties of this iteration. Firstly, if
:math:`F(u_{*})=0` then :math:`u_{k}=u_{*}` is a fixed point of the iteration.
Secondly, we never have to solve :math:`F(u)=0`, we only have to evaluate
its residual at a given state. Thirdly, we can intuitively hope that if
:math:`G` is *close enough* to :math:`F` then the iteration will converge,
although proving this is much more difficult than in the linear case.
Lastly, we can define a different :math:`G_k` at each iteration by
parameterising with the current iterate, as we will see below.

Our approach for defining :math:`G` here is to hold back the linear part of
the reaction term in the Allen-Cahn equation to the value at the previous
iteration. In other words, at each step, we define the PDE

.. math::

G(u; u_k) = -\epsilon\Delta u - u_k + u^3 = 0.
Comment thread
pbrubeck marked this conversation as resolved.
Outdated

The Jacobian for this problem w.r.t. :math:`u` is symmetric and positive-
definite. We have good guarantees about the convergence of Newton's method
in that case, so we can reuse the solver parameters that we tried the first
time around for this inner problem.

Here we define a custom nonlinear preconditioner which inherits from
:class:`~firedrake.preconditioners.auxiliary_snes.AuxiliaryOperatorSNES`.
Similar to ``AuxiliaryOperatorPC``, we have to implement the ``form`` method.
This method returns (1) a residual form :math:`G(u; u^{k}, v)` where :math:`v`
is the test function and (2) the boundary conditions for this sub-problem.
The arguments that it takes are first the PETSc SNES object,
then the value :math:`u_k` of the solution at the previous iteration;
the current value :math:`u` to be solved for; and a test function.

::

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

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


The contract for the ``form`` method requires it to supply the boundary
conditions. We could have obtained the boundary conditions by pulling them out
of the global context. That will work for this particular set of solvers but
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.
Comment thread
JHopeCollins marked this conversation as resolved.
Outdated

We now set ``-snes_type nrichardson`` for nonlinear Richardson iterations,
and specify the additional parameters for the nonlinear preconditioner under
the key ``"npc"``. Firstly we need to specify our python SNES type, and then
in the ``"aux"`` key we specify the parameters to actually solve :math:`G` -
here we use Newton iterations.

::

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.

"snes_monitor": None,
"snes_converged_reason": None,
"npc": {
"snes_type": "python",
"snes_python_type": f"{__name__}.AllenCahnAuxSNES",
"aux": newton_parameters,
},
}

solver = NonlinearVariationalSolver(problem, solver_parameters=solver_parameters)
Comment thread
JHopeCollins marked this conversation as resolved.
solver.solve()

Now we actually converge! The convergence on nonlinear Richardson iterations is
Comment thread
pbrubeck marked this conversation as resolved.
usually linear, as opposed to the quadratic convergence of Newton, but with
suitable preconditioning they will usually have a wider basin of convergence than
Newton. From the log output, we can observe that the residual is decreasing by a
factor of 2 or more at each iteration.

.. code-block:: console

0 SNES Function norm 2.439229081145e-01
1 SNES Function norm 2.405339859939e-01
2 SNES Function norm 1.540442351803e-01
3 SNES Function norm 7.166137071498e-02
4 SNES Function norm 2.854773301463e-02
5 SNES Function norm 1.070593887487e-02
6 SNES Function norm 3.943106335233e-03
7 SNES Function norm 1.451230558440e-03
...
18 SNES Function norm 3.491990058865e-08
19 SNES Function norm 1.349411703695e-08
20 SNES Function norm 5.217958176918e-09
21 SNES Function norm 2.018693509573e-09
Nonlinear firedrake_1_ solve converged due to CONVERGED_FNORM_RELATIVE iterations 21

The Allen-Cahn equation is derivable through minimization of the free energy
functional

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


To close, let's evaluate the free energy at the starting guess and at the
computed solution.

::

G = (0.5 * eps * inner(grad(u), grad(u)) + 0.25 * (1 - u**2) ** 2) * dx
G_initial = firedrake.assemble(firedrake.replace(G, {u: initial_guess}))
G_final = firedrake.assemble(G)
print(f"Initial free energy: {G_initial:0.04f}")
print(f"Final: {G_final:0.04f}")

.. code-block:: console

Initial free energy: 1.3339
Final: 0.0534

This demo can be found as a script in :demo:`nonlinear_pc_allen_cahn.py <nonlinear_pc_allen_cahn.py>`.

.. rubric:: References

.. bibliography:: demo_references.bib
:filter: docname in docnames
1 change: 1 addition & 0 deletions docs/source/advanced_tut.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ element systems.
Steady multicomponent flow -- microfluidic mixing of hydrocarbons.<demos/multicomponent.py>
Deflation techniques for computing multiple solutions of nonlinear problems.<demos/deflation.py>
Coupled volume-surface reaction-diffusion on a torus using Submesh and geometric multigrid.<demos/submesh_reaction_diffusion.py>
Nonlinear preconditioning using an auxiliary SNES for the Allen-Cahn equation.<demos/nonlinear_pc_allen_cahn.py>
1 change: 1 addition & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,7 @@
'h5py': ('http://docs.h5py.org/en/latest/', None),
'h5py.h5p': ('https://api.h5py.org/', None),
'matplotlib': ('https://matplotlib.org/', None),
'petsc4py': ('https://petsc.org/release/petsc4py/', None),
'python': ('https://docs.python.org/3/', None),
'pyadjoint': ('https://pyadjoint.org/', None),
'numpy': ('https://numpy.org/doc/stable/', None),
Expand Down
2 changes: 1 addition & 1 deletion firedrake/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def init_petsc():
MassInvPC, PCDPC, PatchPC, PlaneSmoother, PatchSNES, P1PC, P1SNES,
LORPC, GTMGPC, PMGPC, PMGSNES, HypreAMS, HypreADS, FDMPC,
PoissonFDMPC, TwoLevelPC, HiptmairPC, FacetSplitPC, BDDCPC,
CovariancePC
CovariancePC, AuxiliaryOperatorSNES
)
from firedrake.mesh import ( # noqa: F401
Mesh, ExtrudedMesh, VertexOnlyMesh, RelabeledMesh,
Expand Down
1 change: 1 addition & 0 deletions firedrake/preconditioners/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@
from firedrake.preconditioners.facet_split import FacetSplitPC # noqa: F401
from firedrake.preconditioners.bddc import BDDCPC # noqa: F401
from firedrake.preconditioners.covariance import CovariancePC # noqa: F401
from firedrake.preconditioners.auxiliary_snes import AuxiliaryOperatorSNES # noqa: F401
Loading
Loading