Skip to content

Forward-from-nodes history; semi-Lagrangian schemes named by trace and launch; every history equals serial in parallel - #800

Open
lmoresi wants to merge 5 commits into
feature/log-conformationfrom
feature/forward-from-nodes
Open

lmoresi wants to merge 5 commits into
feature/log-conformationfrom
feature/forward-from-nodes

Conversation

@lmoresi

@lmoresi lmoresi commented Sep 27, 2026

Copy link
Copy Markdown
Member

Stacked on #795.

What changes

One entry point for the semi-Lagrangian histories. uw.systems.ddt.SemiLagrangian(mesh, psi_fn, V_fn, vtype, trace=, launch=) selects one of four schemes:

trace launch class (former name)
backward nodes (default) BackwardNodesSemiLagrangian (SemiLagrangian)
backward integration_points BackwardIntegrationPointsSemiLagrangian (IntegrationPointSemiLagrangian)
forward integration_points ForwardIntegrationPointsSemiLagrangian (ForwardSemiLagrangian)
forward nodes ForwardNodesSemiLagrangian (new)

A keyword the chosen scheme does not take is a TypeError. The former class names and the former stress_transport strings (semi_lagrangian, integration_point, forward) still work, with a FutureWarning. stress_transport takes backward_nodes (default), backward_integration_points, forward_integration_points, forward_nodes, lagrangian, eulerian; AdvDiffusionSLCN(transport=...) chooses its value history among the four semi-Lagrangian schemes.

Forward from nodes. The history launches its values from the field's nodes and from a lattice inside every element (the discontinuous basis two degrees up), carries them one step forward, fits the arrivals in each cell at the field's degree, and projects the per-cell fits onto the continuous store. A cell with too few arrivals takes a linear fit to its own arrivals, or their mean; an arrival on a shared face is fitted in every cell that contains it. A stress is committed by L2 projection onto the store and launched from that field. Rotating diffusing Gaussian, P2, SLCN solver, integral error:

half turn, disc half turn, box full turn, disc
forward from nodes 1.78e-2 6.57e-2 8.14e-2
backward from nodes 2.52e-2 6.43e-2 8.94e-2

The box, where the flow crosses every wall, is slightly worse than the backward scheme. A serial-only version that let thin cells borrow arrivals from their neighbours reached 1.68e-2 and 6.32e-2; that borrowing depends on the partition and was removed. First order; a fixed mesh.

Every history gives the serial answer in parallel. tests/parallel/test_1066 holds the six stress histories (turned-over Maxwell box) and the four semi-Lagrangian value histories (rotating Gaussian with inflow) to 1e-6 of their serial values at np >= 3; verified at np 3, 4 and 6. Five sources of partition dependence were removed:

  1. History projections were solved at the projection default tolerance (1e-4) with an aggregating multigrid; they are now CG with Jacobi to 1e-12 from a zero start (the warm start also made a restart differ from a straight run).
  2. The nodal and integration-point histories recorded the current field by evaluating it at nudged nodes (serial) or by copying it (parallel, and only when handed the variable itself); a field that is a mesh variable is now always copied.
  3. Behaviour change of the default scheme: the nodal trace-back took its start velocity at the node nudged 0.1% toward "the closest cell's" centroid. A seam node was nudged differently on each rank, and a node on a no-slip wall was given a velocity. The trace now starts at the node. Serial answers near walls change; all 388 tests in the transport, Navier-Stokes and viscoelastic set pass unchanged.
  4. global_evaluate: a point that the swarm migration handed to a rank whose cells do not contain it was given the nearest-centroid rbf extrapolation (3e-2 at a steep stress, np 4). The rank whose cell contains it now evaluates it with the FE interpolant. This runs only where the cell hint is authoritative (no DMLocatePoints collective inside), and a finite rbf value is kept where the FE value is NaN.
  5. Cell tie-breaks in the forward fits, described above.

Open

Tests

tests/parallel/test_1066 (new), test_1062 tightened to 1e-6, test_1102 (new, forward from nodes against hard baselines), test_1063 covers forward_nodes, test_1059 covers selection, renamed strings and the Maxwell box for every history. Serial transport/NS/VE set: 388 passed. Parallel np 3/4/6: 10 passed, 1 xfail at np >= 4.

🤖 Generated with Claude Code

https://claude.ai/code/session_017kSkAq7oJ5J3XisuvLBovo

lmoresi and others added 4 commits September 26, 2026 13:24
…lement interiors

Forward from where the values are known best. A field is known exactly at its
own nodes and, since its interpolant is a polynomial inside each element, at
any interior point. ForwardNodesSemiLagrangian launches the values at the nodes
and at the interior lattice of every element (the discontinuous basis one
degree up), carries them one step forward on the shared characteristic trace,
fits the arrivals in each cell at the field's own degree (the cell polynomial
projector, with its thin-cell and conditioning fallbacks) and reads the fit
back at the nodes. The fit never crosses an element boundary, where the
interpolant has a kink; a neighbourhood fit across cells was tried first and
smoothed (disc error 3.6e-2).

As the DuDt of the SLCN advection-diffusion solver on the rotating diffusing
Gaussian (P2 temperature, dt 0.02, kappa 0.01): half revolution on the disc
2.00e-2 (backward nodal history 2.52e-2, swarm 2.04e-2, SUPG 1.72e-2), peak
0.1873 against 0.1865 exact; the square box, where the flow crosses all four
walls, 6.17e-2 (backward 6.43e-2); a full revolution 7.63e-2 (backward 8.94e-2,
SUPG and swarm 8.2e-2). About twice the backward cost per step. First order,
serial (the fit near a partition seam needs the other rank's arrivals).

Test test_1102: disc and box, hard baselines.

Underworld development team with AI support from Claude Code

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017kSkAq7oJ5J3XisuvLBovo
… forward from nodes as an advection-diffusion option

ddt.SemiLagrangian(mesh, psi_fn, V_fn, vtype, trace=, launch=) selects one of
four schemes: BackwardNodesSemiLagrangian (was SemiLagrangian),
BackwardIntegrationPointsSemiLagrangian (was IntegrationPointSemiLagrangian),
ForwardIntegrationPointsSemiLagrangian (was ForwardSemiLagrangian) and
ForwardNodesSemiLagrangian. A keyword the chosen scheme does not take is a
TypeError. The old class names resolve with a FutureWarning.

stress_transport takes "backward_nodes" (default), "backward_integration_points",
"forward_integration_points", "lagrangian", "eulerian"; the old strings map with
a FutureWarning. AdvDiffusionSLCN(transport=...) chooses the value history among
the four semi-Lagrangian schemes.

ForwardNodesSemiLagrangian carries a field known at its nodes and refuses a
flux: a stress is formed at the integration points. A cell nothing reached now
keeps the field it launched (no fit carried between steps); inflow is detected
by the back-reflected point leaving the domain; a moved mesh is refused.
ForwardIntegrationPointsSemiLagrangian takes degree and refuses anything but 1.
BackwardNodesSemiLagrangian's continuous now defaults to True.

Underworld development team with AI support from Claude Code

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017kSkAq7oJ5J3XisuvLBovo
…l; forward from nodes carries a stress

Forward from nodes as a stress history: the stress is committed by L2
projection onto the continuous store and launched from that field. It is
offered by stress_transport="forward_nodes". The per-cell fits are projected
onto the store (a shared node weighs every cell), each cell's fit uses only its
own arrivals (a linear fit, their mean, or the launched field when too few),
the interior lattice is two degrees up, and an arrival on a shared face is
fitted in every cell that contains it. In parallel a seam node is launched by
its owner, and arrivals that left a rank or sit on a face are offered to every
rank.

Partition dependence removed from the existing histories:
- a field that is a mesh variable (T.sym as well as T) is recorded into the
  nodal and integration-point histories by copying its nodal data, in serial
  as in parallel, instead of evaluating it at nudged nodes;
- the nodal trace starts from the node itself: the 0.1% nudge toward the
  closest cell's centroid depended on which cells a rank holds and gave a
  no-slip wall node a velocity;
- every history projection is solved to 1e-10;
- global_evaluate: a point that the migration stranded but that a rank's cell
  contains is evaluated there with the FE interpolant, not by the
  nearest-centroid rbf extrapolation (3e-2 at a steep stress).

tests/parallel/test_1066: six stress and four advection histories equal serial
to 1e-6 at np >= 3 (verified np 3, 4, 6); the particle history is a strict
xfail at np >= 4 (1.2e-5, cause open). test_1062 tightened to 1e-6.

Underworld development team with AI support from Claude Code

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017kSkAq7oJ5J3XisuvLBovo
…tory projections

- forward from nodes: an inflow node is one whose back-reflected point the
  global restore moves (a per-rank "in domain" test called every partition
  face a boundary); ownership of seam nodes read from the global section
  (_owned_rows) instead of a write-and-read-back; the tracked field copied,
  not evaluated, at the nodes; a strictly-inside arrival fitted in its one
  cell only; containing_cells falls back to the locator for a point whose
  cell is not among the nearest centroids; quadrilateral centroids corrected.
- global_evaluate: the containment round keeps a finite rbf value where the FE
  value is NaN, and runs only where the cell hint is authoritative (no
  DMLocatePoints collective inside it); the deadlock note says so.
- every history projection is a CG/Jacobi solve (linear_solver moved to the
  projection mixin) to 1e-12, from zero: the warm start depended on a state a
  restart does not restore.
- Eulerian record uses _tracked_field; _tracked_field refuses another mesh's
  variable; stress_transport names map through one (trace, launch) table.
- tests: test_1066 resets the model per case, asserts the forward exchanges
  ran, gives the value histories an inflow, marks the particle case #797
  (xfail, np >= 4); test_1063 covers forward_nodes; test_1102 held to 1e-4.
  test_1066 at np 4 is the regression test for the containment round.
- TODO(DESIGN) notes: face points offered to every rank; the two forward
  flavours' arrival rules.

np 3/4/6: 10 passed (+1 xfail at 4/6); serial: 388 passed.

Underworld development team with AI support from Claude Code

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017kSkAq7oJ5J3XisuvLBovo
@lmoresi

lmoresi commented Sep 27, 2026

Copy link
Copy Markdown
Member Author

Adversarial review

Two review passes before the push (on c434d8a and on b20c992). Findings and disposition:

Fixed

  • The SemiLagrangian entry point passed positional arguments to schemes whose positional order differs (varsymbol=2, order=True silently); now keyword-only after vtype, and an argument the scheme does not take is a TypeError.
  • Forward from nodes kept the pre-solve fit for a cell with no arrivals, so an empty inlet cell read back the stress from before the solve; the fallback is now the field the cell launched, and nothing is carried between steps.
  • Forward-from-nodes inflow detection used the per-rank points_in_domain, which treats partition faces as boundary: every seam node would have taken the inflow value. Now the global restore test the other flavours use.
  • The global_evaluate containment round overwrote a finite rbf value with NaN for a located-but-NaN point (the case the fallback exists for), and could meet a DMLocatePoints collective on non-authoritative meshes through the interpolation cache. Now keeps finite values and runs only where the hint is authoritative.
  • The owned-node mask relied on a write-then-read-back through the ghost update (wrong inside a deferred update); now read from the global section.
  • A strictly-inside arrival could also be kept by a neighbour whose tolerance band it touched (partition-dependent); now only its own cell.
  • containing_cells could miss the containing cell beyond the k nearest centroids; now falls back to the locator. Quadrilateral centroids used the simplex formula.
  • History projections warm-started from a state a restart does not restore (restart test failed at 1e-12); now zero-start CG/Jacobi to 1e-12.
  • test_1102 windows could not see a default change (+-10%); now 1e-4. test_1066 resets the model per case, asserts the seam exchange ran, runs the inflow path; test_1063 covers forward_nodes.

Deferred (TODO(DESIGN) in the code)

  • Face arrivals are allgathered to every rank, including faces interior to a partition (no-slip wall nodes each step).
  • The forward integration-point exchange gives a face arrival to one cell; forward from nodes to every containing cell.

Filed: #797 (particle history, np >= 4), #798 (evalf extrapolation flag), #799 (serial evaluate near nodes).

Underworld development team with AI support from Claude Code

…Stokes velocity history offers every semi-Lagrangian scheme

global_evaluate applied monotone="clamp" on the rank that asked for a point,
bounding it by that rank's nearest nodes; for a departure point evaluated on
another rank those are the wrong neighbourhood. The bound now runs on the
evaluating rank (first pass and containment round), on non-dimensional values
like the nodal data it compares with. Serial results are unchanged. #682's
level-set reproduction (LeVeque swirl, 128^2 quad, P2, SLCN with clamp):
volume at np 1/2/4/8 now 0.0706771442/415/438/412 (np 8 was 1.6% off).

NavierStokesSLCN(velocity_transport=...) chooses the velocity history among
the four semi-Lagrangian schemes, through one _value_history helper shared
with AdvDiffusionSLCN(transport=...). Forward schemes need order=1; the
forward integration-point fit is linear and refuses a P2 velocity.

tests: test_1103 (lid-driven cavity, Re 100, three schemes against recorded
values and each other); test_1066 adds the cavity (equal to serial at np
3/4/6). tests/parallel at np 4: 168 passed, 1 xfailed, 1 failed
(test_1063_constrained_freeslip_parallel[ti], fails identically on #795's
05d5248). Serial set: 393 passed.

Underworld development team with AI support from Claude Code

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017kSkAq7oJ5J3XisuvLBovo
@lmoresi

lmoresi commented Sep 27, 2026

Copy link
Copy Markdown
Member Author

Added (28314aa):

Underworld development team with AI support from Claude Code

This branch has not been deployed

No deployments
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