Skip to content

Rework PMGBase.coarsen() and add NonlinearVariationalProblem.rediscretise() - #5324

Merged
pbrubeck merged 30 commits into
mainfrom
pbrubeck/pmg-reconstruct-rework
Aug 6, 2026
Merged

Rework PMGBase.coarsen() and add NonlinearVariationalProblem.rediscretise()#5324
pbrubeck merged 30 commits into
mainfrom
pbrubeck/pmg-reconstruct-rework

Conversation

@pbrubeck

@pbrubeck pbrubeck commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Gives NonlinearVariationalProblem and the nullspace
basis classes a generic reediscretise() method, and reworks
PMGBase.coarsen() to build the coarse problem, _SNESContext, and
nullspaces through those reconstruct()/rediscretise() methods instead of hand-rolling
each piece and special-casing bc coarsening per PMGPC/PMGSNES.

Also replaces PMGBase.reconstruct_degree's element-degree-shift
tree-walk with finat.ufl's own native FiniteElement.reconstruct(degree=...)
support (firedrakeproject/fiat#273).

Before / after

  • Before: PMGBase.coarsen() hand-built the coarse NonlinearVariationalProblem
    and _SNESContext, and coarsened bcs via bespoke coarsen_bcs/
    coarsen_bc_value methods (with a separate per-bc cache) that special-cased
    PMGPC (bc value always 0) vs PMGSNES (eager Function.interpolate()).
  • After: NonlinearVariationalProblem.redisctretise() and _SNESContext.reconstruct()
    do this generically for any caller, so PMGPC and PMGSNES coarsen bcs (and F)
    identically, and the PMGPC-only cproblem.F = action(J, cu) simplification is replaced with
    ZeroBaseForm.
  • Before: element degree coarsening/refinement went through
    PMGBase.reconstruct_degree's manual tree-walk over
    EnrichedElement/TensorProductElement/MixedElement/etc.
  • After: callers use the element's own reconstruct(degree=...).

pbrubeck and others added 4 commits August 4, 2026 15:25
…method

NonlinearVariationalProblem.reconstruct(function_space) rebuilds the
problem's solution, forms, and bcs on a new function space living on
the same mesh, substituting the coefficient mapping symbolically
through an optional form_transform hook (defaulting to ufl.replace).

VectorSpaceBasis.reconstruct and MixedVectorSpaceBasis.reconstruct do
the analogous thing for a nullspace basis, interpolating each basis
Function onto the new space and re-orthonormalizing.

Neither caller is wired in yet; PMGBase.coarsen() picks these up next.
…ct()

coarsen() used to hand-roll the coarse problem (coarsening J/Jp/F/bcs/
appctx itself, then constructing a fresh NonlinearVariationalProblem
and _SNESContext) and separately special-case bc coarsening per PMGPC
vs PMGSNES via coarsen_bcs/coarsen_bc_value and a bespoke per-bc cache.

None of that is necessary now: a bc's value is just a UFL expression,
and NonlinearVariationalProblem.reconstruct() already builds the
coefficient_mapping needed to substitute it symbolically, the same way
it already does for forms, so PMGPC and PMGSNES coarsen F, J, Jp, and
bcs identically. _SNESContext gets the analogous treatment via its own
reconstruct(), and nullspace coarsening goes through the new
VectorSpaceBasis/MixedVectorSpaceBasis.reconstruct() instead of a
hand-rolled interpolation + cache.

Also drop the PMGPC-only "cproblem.F = action(J, cu)" workaround now
that geometric multigrid supports non-Lagrange elements (the
mixed-block bug it worked around no longer applies), so PMGPC and
PMGSNES coarsen F identically too.

Drop PMGBase.reconstruct_degree's element-degree-shift tree-walk; its
callers move to the element's own reconstruct(degree=...) next.
…econstruct_degree

finat.ufl's FiniteElement subclasses now support reconstruct(degree=...)
natively (firedrakeproject/fiat#273), including the element-degree-shift
tree-walk through EnrichedElement/TensorProductElement/MixedElement/etc.
that PMGBase.reconstruct_degree used to carry. P1PC/P1SNES/LORPC.
coarsen_element and the p-multigrid tests call the element's own
reconstruct() directly.

test_p_multigrid_mixed's iteration count moves from <=7 to <=9: PMGPC's
coarse-grid Jacobian is no longer forced through action(J, cu) (see
previous commit), which slightly changes the coarse operator for this
problem.
Pin firedrake-fiat to pbrubeck/element-reconstruct-degree
(firedrakeproject/fiat#273) so CI exercises the composite-element
reconstruct(degree=...) fix this branch depends on. Revert to @main once
that fiat PR merges.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread tests/firedrake/multigrid/test_p_multigrid.py Outdated
Comment thread pyproject.toml Outdated
Comment thread firedrake/preconditioners/pmg.py Outdated
Comment thread firedrake/preconditioners/pmg.py
Comment thread firedrake/variational_solver.py
Comment thread firedrake/solving_utils.py Outdated
pbrubeck and others added 5 commits August 4, 2026 17:00
Covers PMGBase.coarsen_element/coarsen/coarsen_quadrature,
NonlinearVariationalProblem.reconstruct, VectorSpaceBasis.reconstruct,
MixedVectorSpaceBasis.reconstruct, and _SNESContext.reconstruct.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ufl.replace can't move a Cofunction's implicit test argument to the
coarse space (it isn't a literal node in the expression tree), so
coarsening F = action(J, u) - L for a LinearVariationalProblem left L
on the fine space, giving F two distinct arguments and tripping the
"residual is not a linear form" check.

PC-only coarse levels never assemble their own residual, so give them
F = action(cJ, cu) instead of coarsening fproblem.F. SNES/FAS levels do
need a real residual: fall back to only replacing coefficients and
re-pairing the result with the coarse test function via interpolation,
mirroring the DirichletBC-restriction code path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
action(cJ, cu) worked but redundantly coarsened J a second time (reconstruct()
already does it for J itself). Use a bare ZeroBaseForm with the coarse test
argument instead: cheaper, and the recently-fixed
ZeroBaseForm._ufl_expr_reconstruct_ (upstream UFL) makes it safe to carry
through reconstruction.

This needs two supporting fixes:
- reconstruct_nlvp in mg/ufl_utils.py looked up the coarsened solution via
  coefficient_mapping[problem.u_restrict], relying on it having been
  populated as a side effect of walking F for coefficients. A
  coefficient-free F doesn't trigger that, so look it up explicitly instead.
- test_p_fas_nonlinear_scalar's check_coarsen_quadrature inspected
  p.F.integrals() directly; skip non-Form entries since PC-only levels no
  longer carry a real Form there (J alone is sufficient to verify the
  quadrature-degree coarsening).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pin fenics-ufl to pbrubeck/zero-base-form-holes (FEniCS/ufl#494) so CI
exercises the ZeroBaseForm.extract_domains and
_ufl_expr_reconstruct_ fixes PMG coarsening now depends on. Revert to
@main once that ufl PR merges.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread firedrake/preconditioners/pmg.py Outdated
Comment thread firedrake/preconditioners/pmg.py Outdated
Comment thread firedrake/preconditioners/pmg.py
pbrubeck and others added 10 commits August 5, 2026 08:28
Co-authored-by: Pablo Brubeck <brubeck@protonmail.com>
Covers PMGBase.coarsen_element/coarsen/coarsen_quadrature,
NonlinearVariationalProblem.reconstruct, VectorSpaceBasis.reconstruct,
MixedVectorSpaceBasis.reconstruct, and _SNESContext.reconstruct.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ufl.replace can't move a Cofunction's implicit test argument to the
coarse space (it isn't a literal node in the expression tree), so
coarsening F = action(J, u) - L for a LinearVariationalProblem left L
on the fine space, giving F two distinct arguments and tripping the
"residual is not a linear form" check.

PC-only coarse levels never assemble their own residual, so give them
F = action(cJ, cu) instead of coarsening fproblem.F. SNES/FAS levels do
need a real residual: fall back to only replacing coefficients and
re-pairing the result with the coarse test function via interpolation,
mirroring the DirichletBC-restriction code path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
action(cJ, cu) worked but redundantly coarsened J a second time (reconstruct()
already does it for J itself). Use a bare ZeroBaseForm with the coarse test
argument instead: cheaper, and the recently-fixed
ZeroBaseForm._ufl_expr_reconstruct_ (upstream UFL) makes it safe to carry
through reconstruction.

This needs two supporting fixes:
- reconstruct_nlvp in mg/ufl_utils.py looked up the coarsened solution via
  coefficient_mapping[problem.u_restrict], relying on it having been
  populated as a side effect of walking F for coefficients. A
  coefficient-free F doesn't trigger that, so look it up explicitly instead.
- test_p_fas_nonlinear_scalar's check_coarsen_quadrature inspected
  p.F.integrals() directly; skip non-Form entries since PC-only levels no
  longer carry a real Form there (J alone is sufficient to verify the
  quadrature-degree coarsening).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pin firedrake-fiat to pbrubeck/element-reconstruct-degree
(firedrakeproject/fiat#273) so CI exercises the composite-element
reconstruct(degree=...) fix this branch depends on. Revert to @main once
that fiat PR merges.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pin fenics-ufl to pbrubeck/zero-base-form-holes (FEniCS/ufl#494) so CI
exercises the ZeroBaseForm.extract_domains and
_ufl_expr_reconstruct_ fixes PMG coarsening now depends on. Revert to
@main once that ufl PR merges.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Drop the fiat/ufl pins that leaked back in through the merge; they
belong in the DROP BEFORE MERGE action.yml commits only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread .github/actions/install/action.yml Outdated
@pbrubeck
pbrubeck force-pushed the pbrubeck/pmg-reconstruct-rework branch from a820726 to e07c9b4 Compare August 5, 2026 10:31
Comment thread firedrake/mg/ufl_utils.py

@connorjward connorjward left a comment

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.

Close

Comment thread firedrake/nullspace.py Outdated
Comment thread firedrake/variational_solver.py Outdated
Comment thread firedrake/nullspace.py Outdated
Comment thread firedrake/nullspace.py Outdated
Comment thread firedrake/preconditioners/pmg.py Outdated
Co-authored-by: Pablo Brubeck <brubeck@protonmail.com>
@pbrubeck
pbrubeck force-pushed the pbrubeck/pmg-reconstruct-rework branch 2 times, most recently from adad029 to 2972461 Compare August 5, 2026 15:06
@pbrubeck
pbrubeck force-pushed the pbrubeck/pmg-reconstruct-rework branch from 2972461 to 83b5017 Compare August 5, 2026 16:52
@pbrubeck pbrubeck changed the title Rework NonlinearVariationalProblem.reconstruct() and PMGBase.coarsen() Rework PMGBase.coarsen() and add NonlinearVariationalProblem.rediscretise() Aug 6, 2026
@pbrubeck
pbrubeck requested a review from connorjward August 6, 2026 09:01
@pbrubeck

pbrubeck commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

CI is green here

@pbrubeck
pbrubeck merged commit 450aa79 into main Aug 6, 2026
19 of 21 checks passed
@pbrubeck
pbrubeck deleted the pbrubeck/pmg-reconstruct-rework branch August 6, 2026 09:19
leo-collins pushed a commit that referenced this pull request Aug 6, 2026
…tise() (#5324)

* Give NonlinearVariationalProblem and nullspace bases a rediscretise() method

* Rework PMGBase.coarsen() to use NonlinearVariationalProblem.rediscretise()
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.

2 participants