Skip to content

Expose theta on SemiLagrangian DDt for BE/CN choice#187

Merged
lmoresi merged 3 commits into
developmentfrom
feature/advdiff-theta-exposure
May 14, 2026
Merged

Expose theta on SemiLagrangian DDt for BE/CN choice#187
lmoresi merged 3 commits into
developmentfrom
feature/advdiff-theta-exposure

Conversation

@lmoresi
Copy link
Copy Markdown
Member

@lmoresi lmoresi commented May 14, 2026

Summary

Exposes the Adams-Moulton theta parameter on SemiLagrangian_DDt. Previously hardcoded to 0.5 (Crank-Nicolson) at two sites in the class. Default is unchanged so existing users see no behaviour difference; setting adv_diff.DuDt.theta = 1.0 now gives Backward Euler on the implicit flux integrator.

Why

The AM order-1 coefficients are [θ, 1-θ]:

  • θ = 0.5 → Crank-Nicolson. Trapezoidal. A-stable, second-order accuracy on the flux term. NOT L-stable: for stiff modes the amplification factor (1 − λΔt/2) / (1 + λΔt/2) approaches −1, so the integrator reflects-with-sign-flip rather than damps. On under-resolved sharp gradients in deformed cells this can ignite step-on-step oscillating overshoots in T.
  • θ = 1.0 → Backward Euler. L-stable, monotone for diffusion. First-order accurate on the flux term. The standard fallback for stiff parabolic problems.

The other SemiLagrangian-family classes (Symbolic, Eulerian, Lagrangian, Lagrangian_Swarm) already take theta as an __init__ parameter and use self.theta in their AM update sites. This PR brings SemiLagrangian (the one used by AdvDiffusionSLCN) into line.

Usage

adv_diff = uw.systems.AdvDiffusionSLCN(mesh, u_Field=t_soln, V_fn=v.sym)
# Backward Euler on the diffusive flux:
adv_diff.DuDt.theta = 1.0
adv_diff.DFDt.theta = 1.0
adv_diff.solve(timestep=dt)

Or via the constructor:

custom_DuDt = uw.systems.ddt.SemiLagrangian(..., theta=1.0)
adv_diff = uw.systems.AdvDiffusionSLCN(..., DuDt=custom_DuDt)

Test plan

  • CI suite (default theta=0.5 preserves existing trajectory bit-for-bit)
  • No measurable performance impact (one float lookup)
  • Optional: explicit BE-mode regression test once a stiff-parabolic benchmark exists

Files changed

  • src/underworld3/systems/ddt.py (+15 lines, -2 lines)

Underworld development team with AI support from Claude Code

The Adams-Moulton flux integrator in SemiLagrangian_DDt was hardcoded
to theta=0.5 (Crank-Nicolson, 2nd-order accurate, A-stable). For stiff
parabolic terms — e.g. advection-diffusion with a thin boundary layer
on deformed elements — CN is not L-stable: the amplification factor
(1 - λΔt/2)/(1 + λΔt/2) → -1 for stiff modes, so the integrator
sign-flip-reflects them rather than damping. Combined with the Picard
iteration's coupling, this can produce step-on-step oscillating T
overshoots.

Adds ``theta`` as an __init__ parameter (default 0.5, preserving the
legacy SLCN behaviour) and as an instance attribute settable after
construction:

    adv_diff.DuDt.theta = 1.0   # Backward Euler (L-stable, 1st order)
    adv_diff.DFDt.theta = 1.0

The two hardcoded ``_update_am_values(..., 0.5)`` sites in
SemiLagrangian (initial coefficient setup and per-step refresh in
update_pre_solve) now use ``self.theta``. The matching constructs in
the Symbolic, Eulerian, Lagrangian and Lagrangian_Swarm classes
already used self.theta — this PR brings SemiLagrangian into line.

No behaviour change for existing users (default unchanged). Useful as
a per-solver knob when CN-driven ringing is observed on stiff problems.

Underworld development team with AI support from Claude Code
Copilot AI review requested due to automatic review settings May 14, 2026 10:56
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 exposes the theta parameter on SemiLagrangian DDt so users can choose Crank-Nicolson or Backward Euler behavior for order-1 Adams-Moulton flux integration while preserving the default legacy behavior.

Changes:

  • Adds theta to the SemiLagrangian.__init__ signature with default 0.5.
  • Stores self.theta and uses it when initializing/updating AM coefficients.
  • Replaces hardcoded 0.5 AM coefficient updates with self.theta.

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

smoothing=0.0,
preserve_moments=False,
with_forcing_history: bool = False,
theta: float = 0.5,
# Update coefficient values for current effective_order and dt
_update_bdf_values(self._bdf_coeffs, self.effective_order, self._dt, self._dt_history)
_update_am_values(self._am_coeffs, self.effective_order, 0.5)
_update_am_values(self._am_coeffs, self.effective_order, self.theta)
lmoresi added 2 commits May 14, 2026 21:35
1. Add the new ``theta`` constructor parameter to the SemiLagrangian
   class docstring's Parameters section. Was missing while the other
   DDt classes (Symbolic, Eulerian, Lagrangian, Lagrangian_Swarm)
   already documented theirs.

2. Add a regression test (tests/test_1053_ddt_theta.py) covering:
   - default theta=0.5 yields order-1 AM coefficients [0.5, 0.5] (CN)
   - theta=1.0 yields [1.0, 0.0] (Backward Euler)
   - theta=0.0 yields [0.0, 1.0] (Forward Euler — completeness)
   - mutation after construction (d.theta = 1.0) is picked up on the
     next AM coefficient refresh
   - omitting theta preserves legacy CN behaviour

Underworld development team with AI support from Claude Code
@lmoresi lmoresi merged commit 3739f69 into development May 14, 2026
1 check passed
@lmoresi lmoresi deleted the feature/advdiff-theta-exposure branch May 14, 2026 12:13
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