Skip to content

Fix SL trace-back FE overshoot + add monotone limiter to DDt#186

Merged
lmoresi merged 2 commits into
developmentfrom
feature/sl-traceback-monotone-limiter
May 14, 2026
Merged

Fix SL trace-back FE overshoot + add monotone limiter to DDt#186
lmoresi merged 2 commits into
developmentfrom
feature/sl-traceback-monotone-limiter

Conversation

@lmoresi
Copy link
Copy Markdown
Member

@lmoresi lmoresi commented May 14, 2026

Summary

  • Plumbs the evalf kwarg through both global_evaluate calls inside SemiLagrangian.update_pre_solve (velocity midpoint and psi_star endpoint). Previously the calling solver's _evalf=True had no effect on the trace-back — it always used FE Lagrange shape functions regardless.
  • Adds a monotone_mode instance attribute + per-call kwarg on SemiLagrangian_DDt with two limiter options applied after the trace-back evaluation. Default unchanged (None = pure FE).

Background

P3 (and higher) Lagrange FE shape functions are non-monotone: they can produce values outside [data_min, data_max] at non-nodal points inside a cell with sharp gradient. The SL trace-back samples psi_star.sym at upstream end_pt_coords — exactly the non-nodal points where the overshoot fires. On free-surface convection at Ra ≥ 1e5 the resulting psi_star values outside [0, 1] feed the implicit SNES and ignite catastrophic ringing (T → [-1.12, +2.56] in one step at step 35 of the canonical RK4-full case, with 432 DOFs out of bounds and the mode-5 plume structure destroyed by global noise pepper).

A --rbf-advection-style flag had been wired into the calling solver but never reached the actual trace-back, so the failure was opaque until we instrumented psi_star pre/post the trace-back call and saw it producing values like [0, +1.1473] from input data strictly in [0, 1].

The two limiter options

Mode Behaviour Cost
None (default) Pure FE trace-back. Can overshoot at sharp gradients. Legacy behaviour. baseline
\"clamp\" (B.2) FE result clipped to [nbr_min, nbr_max] of the k = dim + 1 nearest psi_star DOFs. Bit-identical to FE in smooth regions (clamp is a no-op); saves the run at the catastrophe step. one extra kdtree query per update_pre_solve
\"pick\" (B.1) FE result kept if in-bounds, else re-evaluated via Shepard's-method RBF at out-of-bounds DOFs. More conservative than clamp. doubles trace-back cost when triggered

Set via the same pattern as theta:

adv_diff.DuDt.monotone_mode = \"clamp\"
adv_diff.DFDt.monotone_mode = \"clamp\"

Test (manual, free-surface convection)

Canonical RK4-full free-surface convection: Ra=1e5, ρg=1e5, P3 T, CN diffusion, 35 steps. Default FE behaviour rings catastrophically at step 35.

Configuration post_raw step 35 bad DOFs Nu h_max
FE default (monotone_mode=None) [-1.12, +2.56] 432 245 (artefact) 0.141
monotone_mode=\"clamp\" [0, +1.000] 0 143 0.151
monotone_mode=\"pick\" [0, +1.000] 0 107 0.152

For steps 5–30 the \"clamp\" mode is bit-identical to pure FE (no DOFs out of nbr bounds, clamp is a no-op), so accuracy in the smooth regime is preserved.

Test plan

  • CI suite runs (no API breakage; new kwarg defaults to legacy behaviour)
  • No measurable wall-time regression on standard adv-diff tests (kdtree query is cheap)
  • Optional: convection regression test that exercises monotone_mode=\"clamp\" and confirms the canonical case completes without ringing

Files changed

  • src/underworld3/systems/ddt.py (+81 lines, no deletions)

Underworld development team with AI support from Claude Code

…an DDt

The SL trace-back in SemiLagrangian.update_pre_solve was calling
uw.function.global_evaluate(...) at two sites (velocity midpoint and
psi_star endpoint) without propagating its enclosing evalf kwarg, so
the trace-back always used FE Lagrange shape-function interpolation
regardless of any user-facing RBF flag. P3 Lagrange overshoots at
non-nodal upstream points in cells with sharp gradients (Runge-like
local non-monotonicity), producing psi_star values outside the data
range that feed the implicit SNES and ignite catastrophic ringing on
free-surface convection at high Ra.

Two changes in ddt.py SemiLagrangian:

1. Plumb evalf through both global_evaluate calls (velocity midpoint
   and psi_star endpoint). When evalf=True the trace-back now routes
   through Shepard 3-NN inverse-distance interpolation (bounded by
   neighbour values), as the calling solver could already request via
   AdvDiffusionSLCN.solve(_evalf=True).

2. Add monotone_mode instance attribute + per-call kwarg with two
   limiter options applied after the trace-back evaluation:
     "clamp" (B.2): clip the trace-back result to [nbr_min, nbr_max]
                    of the k=dim+1 nearest psi_star DOFs. Bit-identical
                    to FE in smooth regions; saves the run at the
                    catastrophe step. Cost: one extra kdtree query per
                    update_pre_solve.
     "pick"  (B.1): keep the FE result if it lies within nbr bounds,
                    else re-evaluate via RBF at out-of-bounds DOFs.
                    More conservative than clamp; doubles trace-back
                    cost when triggered.
   Default (None) is unchanged behaviour (pure FE, can overshoot).
   Settable via ``adv_diff.DuDt.monotone_mode = "clamp"`` (same
   pattern as ``theta``).

Tested against the RK4-full free-surface convection canonical case
(Ra=1e5, ρg=1e5, P3 T, CN diffusion, 35 steps; default FE rings
catastrophically at step 35 with post_raw=[-1.12, +2.56], 432 bad
DOFs). With ``monotone_mode = "clamp"`` the run is bit-identical to
FE through step 30 (no overshoots, no DOFs out of nbr bounds; the
clamp is a no-op), then at step 35 it produces a clean physical
solution: post_raw=[0, 1.000], Nu=143 (vs FE artefact 245), vrms=81,
h_max=0.151. Wall time per step unchanged (~19s).

Underworld development team with AI support from Claude Code
Copilot AI review requested due to automatic review settings May 14, 2026 10:41
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the semi-Lagrangian DDt trace-back in SemiLagrangian_DDt to (a) correctly propagate the caller’s evalf choice into global_evaluate calls, and (b) add an optional monotonicity limiter (monotone_mode) to prevent FE overshoot at sharp gradients.

Changes:

  • Plumb evalf=evalf through the velocity midpoint and upstream psi_star global_evaluate calls in update_pre_solve.
  • Add monotone_mode as an instance attribute and per-call kwarg, with "clamp" and "pick" limiter implementations applied after trace-back evaluation.
  • Implement neighbour-range detection via KDTree over psi_star DOF coordinates to compute local [nbr_min, nbr_max] bounds.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/underworld3/systems/ddt.py Outdated
Comment on lines +2020 to +2033
# Plain-numpy coords for kdtree (handle pint/unit-aware)
if hasattr(end_pt_coords, "magnitude"):
epc_nd = np.asarray(end_pt_coords.magnitude)
else:
epc_nd = np.asarray(end_pt_coords)
psi_coords_nd = np.asarray(self.psi_star[i].coords_nd)
if hasattr(psi_coords_nd, "magnitude"):
psi_coords_nd = np.asarray(psi_coords_nd.magnitude)
nnn = self.mesh.dim + 1
kdt = uw.kdtree.KDTree(
np.ascontiguousarray(psi_coords_nd))
_, idxs = kdt.query(
np.ascontiguousarray(epc_nd), k=nnn,
sqr_dists=False)
Comment thread src/underworld3/systems/ddt.py Outdated
Comment on lines +2047 to +2053
# B.1 "pick": re-evaluate via RBF where FE was out of bounds.
value_rbf = uw.function.global_evaluate(
expr_to_evaluate, end_pt_coords, evalf=True)
vrbf_flat = np.asarray(value_rbf).reshape(nbr_min.shape)
out_of_bounds = ((veep_flat < nbr_min)
| (veep_flat > nbr_max))
veep_lim = np.where(out_of_bounds, vrbf_flat, veep_flat)
1. Non-dimensionalise end_pt_coords for the kdtree query. In unit-aware
   runs end_pt_coords is dimensional (e.g. metres) while
   psi_star[i].coords_nd is [0,1] non-dimensional, so without explicit
   conversion the kdtree picks wrong neighbours and the limiter would
   clamp/pick against unrelated DOFs. Match the same conversion pattern
   used earlier in the function for psi_star_0_coords → psi_star_0_coords_nd.

2. In pick mode, only evaluate the RBF for the subset of upstream coords
   whose FE result is out-of-bounds. The previous code unconditionally
   ran a second global_evaluate(..., evalf=True) over ALL coords, paying
   ~2x trace-back cost on every call even when zero DOFs needed
   re-evaluation. Now the cost is dominated by the cheap FE pass when
   most DOFs are in-bounds.

Underworld development team with AI support from Claude Code
@lmoresi lmoresi merged commit 9bf65e6 into development May 14, 2026
1 check passed
@lmoresi lmoresi deleted the feature/sl-traceback-monotone-limiter branch May 14, 2026 11:59
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