Skip to content

GoalAdaptiveNonlinearVariationalSolver#4893

Open
pbrubeck wants to merge 65 commits into
mainfrom
pbrubeck/goal-adaptive-solver
Open

GoalAdaptiveNonlinearVariationalSolver#4893
pbrubeck wants to merge 65 commits into
mainfrom
pbrubeck/goal-adaptive-solver

Conversation

@pbrubeck
Copy link
Copy Markdown
Contributor

@pbrubeck pbrubeck commented Feb 16, 2026

Description

Implements a new GoalAdaptiveNonlinearVariationalSolver class to do adaptive refinement on a NonlinearVariationalProblem defined on a coarse (netgen) mesh. The adaptive procedure relies on a user-specified functional of interest, refered to as goal functional. The solver will iteratively solve -> estimate -> mark -> refine until an automatic error estimate to the goal functional falls below a user specified tolerance.

Comment thread firedrake/adaptive_variational_solver.py Outdated
@pbrubeck pbrubeck force-pushed the pbrubeck/goal-adaptive-solver branch from daf58a1 to b28fbf5 Compare February 26, 2026 13:52
Comment thread firedrake/mg/ufl_utils.py Outdated
@pbrubeck pbrubeck force-pushed the pbrubeck/goal-adaptive-solver branch from f59bd8e to b920833 Compare April 7, 2026 16:54
@pbrubeck pbrubeck force-pushed the pbrubeck/goal-adaptive-solver branch from 645af70 to 37ab7d9 Compare April 7, 2026 17:36
@pbrubeck pbrubeck force-pushed the pbrubeck/goal-adaptive-solver branch from b3dcbb2 to befe0ff Compare April 7, 2026 21:08
@pbrubeck pbrubeck force-pushed the pbrubeck/goal-adaptive-solver branch from befe0ff to b241c80 Compare April 7, 2026 22:29
@pefarrell pefarrell marked this pull request as ready for review May 4, 2026 14:39
@pbrubeck pbrubeck changed the title GoalAdaptiveVariationalSolver GoalAdaptiveNonlinearVariationalSolver May 4, 2026
@pbrubeck pbrubeck requested a review from connorjward May 4, 2026 14:47
Comment thread firedrake/adaptive_variational_solver.py Outdated
Copy link
Copy Markdown
Contributor

@connorjward connorjward left a comment

Choose a reason for hiding this comment

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

This feels a little like we're reinventing a new interface to nonlinear solvers when we already have one: SNES. Logging things is done via monitors and passing variational forms can be done with the appctx.

@JHopeCollins is the expert here, but don't you think that this might be a use case for a Python SNES? I worry that the current approach isn't as composable as other parts of Firedrake.

def __init__(self, base_mesh: MeshGeometry, nested: bool = True):
self.meshes = []
self._meshes = []
self._meshes = self.meshes
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 just creates a reference to the same object

Comment thread firedrake/mg/ufl_utils.py

@singledispatch
def refine(expr, self, coefficient_mapping=None):
return coarsen(expr, self, coefficient_mapping=coefficient_mapping) # fallback to original
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 is very weird. Aren't they supposed to do the opposite things?

__all__ = ["GoalAdaptiveNonlinearVariationalSolver"]


class GoalAdaptiveOptions:
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.

Better as a frozen dataclass, much less boilerplate

* ``False`` (default) – never write.
* ``True`` – write at every iteration.
* A positive integer ``k`` – write every ``k`` iterations.
verbose
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 feel like all of these output options are usually set via PETSc options?

problem: NonlinearVariationalProblem,
goal_functional: ufl.BaseForm,
tolerance: float,
goal_adaptive_options: dict | None = None,
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
goal_adaptive_options: dict | None = None,
goal_adaptive_options: GoalAdaptiveOptions | dict | None = None,


def compute_error_indicators(self, u_err, z_lo, z_err):
"""Compute cell and facet residuals R_cell, R_facet"""
from firedrake.assemble import assemble
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.

Does this import need to be inside?


def print(self, *args, **kwargs):
if self.options.verbose:
PETSc.Sys.Print(*args, **kwargs)
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.

Doesn't this need a comm?

}


class GoalAdaptiveNonlinearVariationalSolver:
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 feels to me like this should inherit from some sort of AbstractNonlinearVariationalSolver type, as it implicitly shares an interface with our other solver classes.

else:
raise ValueError(f"Unrecognised dual_low_method {self.options.dual_low_method}")
z_err = z - z_lo
self.z = z
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 can be dangerous to mutate attributes inside methods like this because it makes errors hard to track down. I wonder if we should make solve_dual into _solve_dual (i.e. make it private) because it only makes sense to call inside solve. The same for many other methods in this class.

Comment thread firedrake/mg/ufl_utils.py
Comment on lines +165 to +170
if self == coarsen:
V_new._fine = V
V._coarse = V_new
elif self == refine:
V_new._coarse = V
V._fine = V_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.

I think that a function called coarsen_function_space that does this is going to cause a huge amount of confusion.

@pefarrell
Copy link
Copy Markdown
Contributor

No, I don't think this is a job for a SNES. A SNES inherently works on a fixed finite-dimensional space: the size of the vectors involved is fixed. An adaptive loop is something that changes the space being used to approximate the PDE. The adaptive loop (solve -> estimate -> mark -> refine) must happen outside the solver loop. We use SNES on the inside, but SNES can't do what this class does.

@connorjward
Copy link
Copy Markdown
Contributor

I see. I still wonder if there are ways that this work could be better integrated into the rest of Firedrake and made to be as composable as possible - I could be wrong though. @pbrubeck can you bring this to the next Firedrake meeting for discussion?

@connorjward
Copy link
Copy Markdown
Contributor

For example we might want to have MeshAdapter and SolverMonitor abstract classes that would own bits of what the GoalAdaptiveVariationalSolver does.

@connorjward
Copy link
Copy Markdown
Contributor

How does this compare to all the mesh adaptivity work that has already been done with Firedrake (https://github.com/mesh-adaptation)? In particular https://github.com/mesh-adaptation/goalie

@stephankramer
@joewallwork

@pefarrell
Copy link
Copy Markdown
Contributor

I can add my perspective. I'm sure @stephankramer and @joewallwork will have other interesting things to say!

Compared to goalie, this PR

  • addresses only stationary problems, whereas goalie (only?) considers transient ones;
  • employs adaptive mesh refinement from netgen, whereas goalie/animate use anisotropic adaptive remeshing or mesh movement from animate/movement;
  • involves a very small amount of work to convert a code that solves one discrete nonlinear problem to apply goal-based adaptivity, whereas using goalie would require substantial rewriting in the goalie format;
  • gives explicit error estimates for goal functionals, which I don't believe goalie does (at least I don't see it in the demos).

This PR also composes nicely with the rest of Firedrake. When the mesh is refined, the integration with netgen ensures the geometry is better approximated (this is not true with goalie). The adaptive mesh hierarchy constructed by the GoalAdaptiveNonlinearVariationalSolver can be used for all solvers Firedrake supports, including geometric multigrid. (I don't believe this is the case with goalie but may be wrong.)

More about adaptive mesh refinement vs adaptive remeshing, as there is a major mathematical difference. In the adaptive mesh refinement used in this PR, the adapted mesh is a refinement of the input mesh (ignoring better geometry approximation). This means that for normal elements the refined function space is a superspace of the original one, which is very advantageous for stable approximation: loosely speaking, your approximation can only get better as you refine. In contrast, with adaptive remeshing, the input and output meshes are in principle unrelated (although they are in practice, because the algorithm transforms the one into the other step by step), and so you don't have nice hierarchical approximations. I suspect this is why 95%+ of the community around adaptive discretisations use adaptive mesh refinement.

@joewallwork
Copy link
Copy Markdown
Contributor

Hi all, great to see this contribution! Nice work. I'll try to find some time to play around with this.

To respond to @pefarrell's points:

* addresses only stationary problems, whereas goalie (only?) considers transient ones;

Yes, Goalie is designed to handle time-dependent problems that consider multiple meshes during the simulation. You can apply it to stationary problems, too.

* employs adaptive mesh refinement from netgen, whereas goalie/animate use anisotropic adaptive remeshing or mesh movement from animate/movement;

I had proposed an MSc project for a student to hook up AMR in Goalie using Netgen but unfortunately it didn't go ahead. It was designed to be general enough to allow different adaptor approaches but we have mainly used Animate so far.

* involves a very small amount of work to convert a code that solves one discrete nonlinear problem to apply goal-based adaptivity, whereas using goalie would require substantial rewriting in the goalie format;

Yeah this is one of the main limitations of Goalie.

* gives explicit error estimates for goal functionals, which I don't believe goalie does (at least I don't see it in the demos).

Currently we make a choice of error estimate based on the dual-weighted residual. There were plans to support more/custom error estimates but we haven't had time.

This PR also composes nicely with the rest of Firedrake. When the mesh is refined, the integration with netgen ensures the geometry is better approximated (this is not true with goalie). The adaptive mesh hierarchy constructed by the GoalAdaptiveNonlinearVariationalSolver can be used for all solvers Firedrake supports, including geometric multigrid. (I don't believe this is the case with goalie but may be wrong.)

I don't recall Goalie having limitations in terms of solvers, although I haven't tried loads of them. However, it is currently limited to simple theta-method timestepping schemes.

@connorjward
Copy link
Copy Markdown
Contributor

Thanks for this useful exposition.

By raising this I wanted to point out that there is a more general appetite for adaptivity using Firedrake and therefore it would be very nice to set this up in a more general way so that potential future contributions would be able to reuse some of the code.

I think this should be brought to a Firedrake team meeting for further discussion.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants