diff --git a/src/underworld3/systems/solvers.py b/src/underworld3/systems/solvers.py index 5e26d4cc..b3e36046 100644 --- a/src/underworld3/systems/solvers.py +++ b/src/underworld3/systems/solvers.py @@ -2496,6 +2496,28 @@ class SNES_AdvectionDiffusion(SNES_Scalar): Time derivative operator for the unknown. DFDt : SemiLagrangian_DDt or Lagrangian_DDt, optional Time derivative operator for the flux. + monotone_mode : str or None, optional + Monotonicity limiter for the semi-Lagrangian trace-back. + Forwarded to the internally-constructed ``SemiLagrangian_DDt`` + instances for ``DuDt`` and ``DFDt``. + + - ``None`` (default): pure FE trace-back. Can overshoot at + non-nodal upstream points in cells with sharp gradients + (e.g. thin boundary layers in deformed cells); legacy + behaviour. + - ``"clamp"``: clip the FE trace-back result to + ``[nbr_min, nbr_max]`` of the ``k = dim + 1`` nearest + ``psi_star`` DOFs. Bit-identical to pure FE in smooth + regions; bounds overshoots where the FE interpolant would + otherwise leave the local data range. + - ``"pick"``: keep the FE result where in-bounds; for + out-of-bounds DOFs only, re-evaluate via Shepard's-method + RBF. More conservative than clamp at the catastrophe edge. + + When a user supplies a pre-built ``DuDt``, this kwarg is + applied to the internally-constructed ``DFDt`` only — the + user's ``DuDt`` already encodes whatever ``monotone_mode`` + it was constructed with. Notes ----- @@ -2536,6 +2558,7 @@ def __init__( verbose=False, DuDt: Union[SemiLagrangian_DDt, Lagrangian_DDt] = None, DFDt: Union[SemiLagrangian_DDt, Lagrangian_DDt] = None, + monotone_mode: Optional[str] = None, ): ## Parent class will set up default values etc super().__init__( @@ -2581,6 +2604,7 @@ def __init__( bcs=self.essential_bcs, order=1, smoothing=0.0, + monotone_mode=monotone_mode, ) else: @@ -2612,6 +2636,7 @@ def __init__( bcs=None, order=order, smoothing=0.0, + monotone_mode=monotone_mode, ) return diff --git a/tests/test_1054_advdiff_monotone_mode_kwarg.py b/tests/test_1054_advdiff_monotone_mode_kwarg.py new file mode 100644 index 00000000..3ef11469 --- /dev/null +++ b/tests/test_1054_advdiff_monotone_mode_kwarg.py @@ -0,0 +1,78 @@ +"""Regression tests for the ``monotone_mode`` kwarg on +``SNES_AdvectionDiffusion`` / ``AdvDiffusionSLCN``. + +The underlying ``SemiLagrangian_DDt`` already accepts ``monotone_mode`` +(landed in PR #186). This kwarg forwards it through the solver +constructor so users can write the one-line idiom +``adv_diff = AdvDiffusionSLCN(..., monotone_mode="clamp")`` instead of +the two-step ``adv_diff.DuDt.monotone_mode = "clamp"; +adv_diff.DFDt.monotone_mode = "clamp"`` dance. +""" + +import pytest + +import underworld3 as uw + + +pytestmark = [pytest.mark.level_1, pytest.mark.tier_a] + + +def _make_mesh_and_field(): + mesh = uw.meshing.StructuredQuadBox( + elementRes=(4, 4), + minCoords=(0.0, 0.0), + maxCoords=(1.0, 1.0), + ) + v = uw.discretisation.MeshVariable( + "V_advtest", mesh, mesh.dim, degree=1) + T = uw.discretisation.MeshVariable( + "T_advtest", mesh, 1, degree=2) + return mesh, v, T + + +class TestMonotoneModeKwarg: + + def test_default_is_none(self): + mesh, v, T = _make_mesh_and_field() + adv = uw.systems.AdvDiffusionSLCN( + mesh, u_Field=T, V_fn=v.sym) + assert adv.DuDt.monotone_mode is None + assert adv.DFDt.monotone_mode is None + + def test_clamp_forwarded_to_both(self): + mesh, v, T = _make_mesh_and_field() + adv = uw.systems.AdvDiffusionSLCN( + mesh, u_Field=T, V_fn=v.sym, + monotone_mode="clamp") + assert adv.DuDt.monotone_mode == "clamp" + assert adv.DFDt.monotone_mode == "clamp" + + def test_pick_forwarded_to_both(self): + mesh, v, T = _make_mesh_and_field() + adv = uw.systems.AdvDiffusionSLCN( + mesh, u_Field=T, V_fn=v.sym, + monotone_mode="pick") + assert adv.DuDt.monotone_mode == "pick" + assert adv.DFDt.monotone_mode == "pick" + + def test_explicit_DuDt_overrides_kwarg(self): + """If the caller supplies a pre-built ``DuDt``, the + constructor must not silently rewrite its ``monotone_mode`` + — the caller-supplied DDt is the source of truth.""" + from underworld3.systems.ddt import SemiLagrangian as SL_DDt + mesh, v, T = _make_mesh_and_field() + # Build a DuDt with mode 'pick' explicitly + custom = SL_DDt( + mesh, psi_fn=T.sym, V_fn=v.sym, + vtype=uw.VarType.SCALAR, degree=T.degree, + continuous=T.continuous, order=1, + monotone_mode="pick", + ) + adv = uw.systems.AdvDiffusionSLCN( + mesh, u_Field=T, V_fn=v.sym, + DuDt=custom, + monotone_mode="clamp", # ignored for the supplied DuDt + ) + assert adv.DuDt.monotone_mode == "pick" # preserved + # DFDt is constructed internally → uses the kwarg + assert adv.DFDt.monotone_mode == "clamp"