Skip to content

Taylor-Green order-reduction benchmark in FEniCS, and a remedy (supersedes #650) - #684

Open
pancetta wants to merge 7 commits into
masterfrom
taylorgreen-fenics
Open

Taylor-Green order-reduction benchmark in FEniCS, and a remedy (supersedes #650)#684
pancetta wants to merge 7 commits into
masterfrom
taylorgreen-fenics

Conversation

@pancetta

Copy link
Copy Markdown
Member

Supersedes #650, which has been stalled since July. That PR added a Taylor–Green
benchmark for order reduction as a new FEniCSx project; this reimplements it with
legacy FEniCS inside the existing StroemungsRaum project, fixes the issues raised in
its review, and adds the order study it was missing along with a remedy for the effect
it measures.

The manufactured solution and forcing term from #650 were verified symbolically and
carried over unchanged — the mathematics there was correct, the implementation is what
needed work.

Why FEniCS instead of FEniCSx

Legacy FEniCS does periodic boundary conditions with constrained_domain on the
function space, so dolfinx_mpc is not needed at all. Together with living in a project
that is already in the CI matrix and already has a registered fenics marker, that
dissolves three of the blockers on #650: the missing dolfinx_mpc dependency (which
made every test in that PR fail at collection), the project not being in CI, and the
unregistered fenicsx marker.

Review issues from #650, addressed

  • No mass matrix inversion. solve_system there applied Mf^-1 to the sweeper's
    right-hand side only for the residual form to apply Mf straight back to it. The round
    trip is exact (verified: 1.7e-15), so it bought nothing while costing a solve per node
    per sweep and capping accuracy at that solve's tolerance. The right-hand side is now
    subtracted as a vector in a NonlinearProblem. The same round trip is removed from the
    already-merged NavierStokes_2D_monolithic_FEniCS.py, where drag and lift agree with the
    previous implementation to 13 significant digits.
  • The residual is fixed only on the actual Dirichlet boundary. Add Taylor–Green NS benchmark studying order reduction induced by time-dependent bcs #650's periodic variant
    zeroed the residual on the periodic boundaries too, letting it stop iterating earlier than
    the Dirichlet variant and biasing exactly the comparison the benchmark exists for.
  • Form, Jacobian, problem and solver are built once, with the step size as a Constant,
    rather than rebuilt on every solve_system call.
  • Scaling by factor instead of dividing by it, so there is no factor == 0 case and the
    residual scale no longer depends on dt.
  • Domain extents are not derived from rank-local coordinates.
  • Plus the smaller review points: logger instead of print, no misleading comments, no
    unused imports, correct .. math:: directives, no prints in tests.

The order study that was missing

This is what #650 was written for, and what neither review comment on it got an answer to.

The number of collocation nodes decides whether anything is visible at all: RADAU-RIGHT with
M nodes has design order 2M-1 and falls back to the stiff order M+1, so the gap is
M-2. Measured at nelems=24, nu=0.1:

M design stiff periodic order(p) Dirichlet order(p)
2 3 3 2.85 2.80 (no gap)
3 5 4 4.69 4.28
4 7 5 7.35 4.96

#650 ran with num_nodes = [2], where the gap is identically zero — that configuration
could not have demonstrated the phenomenon it was built for, whatever else was fixed. The
study and the test here use M = 4.

A remedy

Following the differentiated-constraint idea explored for a time-dependent constraint in
#641, differentiated_bc=True imposes the boundary data on its time derivative and recovers
the stage values by collocation quadrature, u_B(tau_m) = g(t0) + dt * sum_j Q[m,j] gdot(tau_j),
instead of evaluating it pointwise. Pressure order and error at dt = 0.1, M = 4:

boundary condition order error
periodic (best possible) 6.32 7.4e-08
pointwise 5.74 9.7e-07
differentiated 6.30 1.3e-07

It needs no new sweeper beyond a hook, since generic_implicit_mass is already the
y-formulation; generic_implicit_mass_diffbc only hands the problem the collocation data of
the step, which a problem class cannot see on its own.

On what this does and does not show: the observed orders are pre-asymptotic — the periodic
reference does not reach its design order 7 either, and on finer step sizes every variant
collapses against a solver floor near 1e-10. The tests therefore assert the error rather than
the order, and the docstrings say so. Separating 2M-1 from 2M-2 needs a better conditioned
testbed than a 2D nonlinear NSE benchmark. The differentiated variant was checked to be solving
the same problem: its solution converges to the pointwise one at O(dt^(M+1)), the size of the
perturbation.

Also here

Prescribing the exact pressure on x = ±0.5 turns out not to be neutral — it acts as a
partial lifting, worth M to M+1 in the pressure order. Documented, because it means the
7-vs-5 gap understates a setup without prescribed boundary pressure.

The funding ministry is renamed BMBF → BMFTR in the main README (whose logo was already
BMFTR while the text was not) and in the project README.

Tests

Nine tests, all under the fenics marker in a project already in CI. Full StroemungsRaum
suite: 22 passed. Beyond the benchmark itself they cover eval_f against the analytical
du/dt (anchored independently, so a sign error shared with solve_system cannot hide), the
periodicity of the manufactured solution that the whole comparison rests on, and that
differentiated_bc fails loudly rather than silently doing nothing without its sweeper.

🤖 Generated with Claude Code

pancetta and others added 6 commits September 10, 2026 16:57
Reimplements the benchmark from PR #650 with legacy FEniCS in the existing
StroemungsRaum project instead of FEniCSx in a new project. Periodicity comes
from a constrained_domain on the function space, so dolfinx_mpc is not needed,
and the project is already covered by the CI matrix and the `fenics` marker.

The manufactured solution and forcing term of #650 were verified symbolically
and carried over unchanged; the implementation issues raised in review were not:

- No mass matrix inversion. The right-hand side is subtracted from the Newton
  residual as a vector, so the Mf^-1 / Mf round trip in #650 (and in the merged
  NavierStokes_2D_monolithic_FEniCS.py) disappears together with its per-node
  solve and its accuracy floor.
- The residual is only fixed on the actual Dirichlet boundary. In #650 the
  periodic variant zeroed the residual on the periodic boundaries too, which
  let it stop iterating earlier than the Dirichlet variant and biased exactly
  the comparison the benchmark exists for.
- The form, its Jacobian, the Newton problem and the solver are built once,
  with the step size as a Constant, rather than rebuilt per solve_system call.
- Scaling by `factor` instead of dividing by it, so there is no factor == 0
  case and the residual scale, and with it snes/newton tolerances, no longer
  depends on dt.
- Domain extents are not derived from rank-local coordinates.

Adds the order study that was missing: with periodic conditions the method
attains its design order 2M-1 = 5, with time-dependent Dirichlet conditions it
does not. At CI-affordable resolution the order gap is modest (~4.7 vs ~4.3 in
the pressure, the Radau IIA reduction from 2M-1 to M+1); the error constant gap
is close to an order of magnitude, and that is what the test asserts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
solve_system applied Mf^-1 to the right-hand side handed over by the sweeper,
only for the residual form to apply Mf straight back to it via the
dot(rhs, v)*dx terms. The round trip is exact, so it bought nothing while
costing an LU solve per node per sweep and capping attainable accuracy at the
tolerance of that solve. It was there because dolfin's solve(F == 0, ...)
accepts a form and not a vector.

Subtract the right-hand side vector in a NonlinearProblem instead. The same
pattern is needed by the Taylor-Green benchmark, so it lives in newton_step.py
and both problem classes use it.

Building the Newton problem this way also means the form, its Jacobian and the
solver are set up once, with the step size carried by a Constant, rather than
rebuilt on every solve_system call. Previously each distinct value of `factor`
was baked into the form as a literal, giving a different form signature per
step size (verified) and so a separate compilation. FFC caches those across
runs, so this shows up on a cold cache or when new step sizes keep appearing,
not in steady-state runtime: on a warm cache the drag/lift benchmark test is
unchanged at 2.2s against 2.1s before.

Numerically neutral: drag and lift in that benchmark agree with the previous
implementation to 13 significant digits (relative difference 9e-15 and 2e-12),
which is the round-off of the LU solve that has been removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RADAU-RIGHT with M nodes has design order 2M-1 and falls back to the stiff
order M+1 when the boundary data is time dependent, so the gap this benchmark
can show is M-2. That is *identically zero for M = 2*, which is what the
original FEniCSx version ran with: that configuration cannot exhibit the
phenomenon it was written to demonstrate, whatever else is fixed. Measured at
nelems=24, nu=0.1, Tend=0.2, orders from consecutive step sizes:

    M   design  stiff   periodic order(p)   Dirichlet order(p)
    2      3      3           2.85               2.80
    3      5      4           4.69               4.28
    4      7      5           7.35               4.96

M = 4 is the cheapest setting that separates the two unmistakably: order 7
against 5, with the pressure error 52x larger at the finest step size. The
study script and the order test now use it, and the rule is recorded in the
problem class docstring and the project README so the M = 2 trap is not
stepped in again.

The test asserts the gap and the error ratio rather than absolute orders, and
still runs in about a minute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removing the time-dependent pressure condition on x = +-0.5 drops the observed
pressure order from M+1 to M and grows the error by ~24x (measured at M=4:
4.96 -> 4.11). Prescribing it therefore supplies constraint information rather
than being neutral, which places this benchmark at the "constraint lifting"
rung of PR #641's ladder rather than the plain algebraic one, and means the
7-vs-5 gap understates a setup without prescribed boundary pressure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ships the remedy prototyped against the order reduction the benchmark measures.
Instead of evaluating the time-dependent boundary data pointwise at the node,
u_B(tau_m) = g(tau_m), it is imposed on the derivative and the stage value is
recovered by the collocation quadrature,

    u_B(tau_m) = g(t0) + dt * sum_j Q[m,j] * gdot(tau_j).

The two differ by the quadrature error O(dt^(M+1)), but the second is consistent
with the collocation polynomial rather than pointwise exact. This is the
boundary-condition analogue of the differentiated-constraint remedy explored for
a time-dependent constraint in #641.

Measured at M=4, nelems=24, nu=0.1, pressure order and error at dt=0.1:

    periodic (best possible)   6.32   7.4e-08
    pointwise                  5.74   9.7e-07
    differentiated             6.30   1.3e-07

so it removes most of the penalty, leaving a factor of 1.8 against the periodic
case that is a constant rather than a rate.

No new sweeper was needed beyond a hook: generic_implicit_mass is already the
y-formulation, so generic_implicit_mass_diffbc only hands the problem the
collocation data of the step, which a problem class cannot see on its own. The
self-consistency trap #641 warns about does not apply here, because eval_f never
sees the boundary condition -- fix_residual zeroes those rows.

The observed orders are pre-asymptotic: the periodic reference does not reach
its design order 7 either, and on finer step sizes every variant collapses
against a solver floor near 1e-10. The tests therefore assert the error rather
than the order, and the docstrings say so; separating 2M-1 from 2M-2 needs a
better conditioned testbed than a 2D nonlinear NSE benchmark.

Verified that the differentiated variant solves the same problem: its solution
converges to the pointwise one at O(dt^5) = O(dt^(M+1)), the size of the
perturbation. Basing the quadrature on the exact g(t0) or chaining it from the
incoming numerical value gives results identical to three digits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The ministry is now the Federal Ministry of Research, Technology and Space.
The main README already carried the BMFTR logo while its text still said BMBF,
so both it and the StroemungsRaum project README are updated, along with the
link, which now points at bmftr.bund.de (verified to resolve). Grant numbers
are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  pySDC/projects/StroemungsRaum
  run_Navier_Stokes_TaylorGreen_FEniCS.py 235-254
  pySDC/projects/StroemungsRaum/problem_classes
  NavierStokes_2D_TaylorGreen_monolithic_FEniCS.py
  NavierStokes_2D_monolithic_FEniCS.py
  newton_step.py
  pySDC/projects/StroemungsRaum/sweepers
  generic_implicit_mass.py
Project Total  

This report was generated by python-coverage-comment-action

Nine findings from a review of this branch.

Boundary conditions per node are now looked up by exact time rather than by
nearest match, so a `solve_system` call at anything but a node of the prepared
step fails instead of quietly using a neighbouring node's data. The arithmetic
on both sides is identical, so the exact lookup costs nothing.

`prepare_step` moves from `update_nodes` to `predict`, which the controller
calls once per step rather than once per sweep -- what both docstrings already
claimed. Measured over three steps at twelve iterations each: three calls
instead of thirty-six.

The Newton solver drops its `relative_tolerance = Sol_tol` and its reduced
iteration budget, both of which pushed it towards raising on a marginal stall
with `error_on_nonconvergence` left at its default. It now matches the merged
monolithic class. The numbers in the `prepare_step` table were regenerated with
this change in place and are unmoved: 7.411e-08 at dt = 0.1, order 6.32.

`prepare_step` also gains the guard for the one mismatch that was missing, the
`generic_implicit_mass_diffbc` sweeper on a problem with `differentiated_bc`
off, which used to die on a missing attribute.

`test_eval_f` takes `du/dt` from `_boundary_derivatives` instead of re-typing
it, which anchors the velocity derivative the differentiated condition is built
from, and checks its pressure half against a finite difference of `p_ex`, which
nothing covered before.

The rest is removal: the `dt_ref` branch of `order_study` that no caller used,
the `results` dictionary `main` returned to nobody, and the `degree_rise`
enrichment in `relative_errors`, which is pure cost for two functions in the
same space -- verified to leave the result identical to four digits. `Sol_tol`
becomes a keyword on `setup` defaulting to one decade below `restol`.

A tenth finding, that the table in `prepare_step` mislabels its error column,
was wrong: the numbers come from a four-point study whose dt = 0.1 entry is
7.411e-08 with order 6.32, so the label is correct and stands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant