Skip to content

Particle stress history: refill reads the right rows after a removal (#784), and the inflow datum reaches the particles that entered (#783) - #785

Merged
lmoresi merged 5 commits into
developmentfrom
bugfix/lagrangian-inflow
Oct 8, 2026
Merged

lmoresi merged 5 commits into
developmentfrom
bugfix/lagrangian-inflow

Conversation

@lmoresi

@lmoresi lmoresi commented Sep 24, 2026

Copy link
Copy Markdown
Member

Two defects in the particle Lagrangian stress history, found on the cross-slot cell of the 2026-09-23 domain-of-applicability sweep.

#784, the refill after a removal. PETSc removes a point by copying the last point into the freed slot. Swarm.repopulate kept its coordinate array in the original order while reading the reconstruction values in storage order, so a particle created in a call that also removed took the values of unrelated particles. The Lagrangian flavour caps over-full cells every step, and any clamped outflow pile-up trips the cap, so on an open flow every refill was garbage. The proxy's per-cell least-squares fit averaged it down, which is why the closed-flow validations did not see it. Fix: replay the same moves on the local arrays.

#783, the inflow datum. The flavour declared applies_inflow_value = False; a refilled inlet cell carried a reconstruction from stale neighbours. The manager now runs the refill itself after the advection, so it knows which particles it created, and gives inflow_value to every particle whose back-trace leaves the domain where the boundary velocity crosses inward. The trace is one step for a particle that moved and one cell crossing for one created this step. A no-slip wall has no boundary velocity and a free-slip wall only a tangential one, so a trace through a wall does not count.

Maxwell Poiseuille channel, 50 steps, stress_transport="lagrangian":

per-particle shear-stress error, median / max inlet column, proxy error
before 0.12 / 4.8 (sign flips; field max 3.2)
#784 fixed, no datum 0.008 / 0.125 0.17
#784 and #783 0.008 / 0.125 0.002

Tests: test_0068::test_a_refill_after_a_removal_reads_the_right_neighbours (97 before, 1e-8 after) and test_0075_lagrangian_history_inflow_value (baseline 0.01 against 0.002 measured, negative control 0.1 against 0.17). The particle-history, stress-transport, restart and integration-point files pass (71 tests).

Also moved: _write_inflow (with its units reduction) and _nondim_timestep to the base class. One extra commit replaces the deprecated mesh.data in the forward flavour's geometry stamp, which failed the Charter gate on this branch.

Not fixed here, marked TODO(BUG): the particle flavours write the evaluated psi_fn into non-dimensional storage without a units reduction. Not covered: a parallel test with a rank that creates no particles.

The cross-slot cell is not rescued. Re-run with both fixes: clean to step 26, then a linear-solve divergence and a 329 s step, stalled by the guard, with the stagnation point jumping. That is the same failure family as the nodal flavour on that cell at solvent fraction zero, so it is a solver question, not a refill one. The sweep's lagrangian column ran with #784 present and is not a measurement of the scheme.

Fixes #783, fixes #784.

Underworld development team with AI support from Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_017kSkAq7oJ5J3XisuvLBovo

lmoresi and others added 3 commits September 23, 2026 21:22
…emoval (#784)

PETSc removes a point by copying the last point into the freed slot, so a
removal reorders the surviving particles in storage. repopulate kept its
coordinate array in the original filtered order while reading the values for
the reconstruction fresh from PETSc, so a particle created in a call that
also removed took the values of unrelated particles. The same moves are now
replayed on the local coordinate and cell arrays.

Measured on a Maxwell Poiseuille channel with the particle stress history:
per-particle shear-stress error median 0.12, maximum 4.8 with sign flips on a
field whose maximum is 3.2; now median 0.008. The cells proxy averaged the
garbage down, which is why the closed-flow validations did not see it.

Regression test: a call that both thins a band and refills the emptied one
reproduces a linear field to 1e-8 (97 before).

Underworld development team with AI support from Claude Code

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017kSkAq7oJ5J3XisuvLBovo
…ntered (#783)

No particle arrives from outside: the population control creates the
particles of an emptied inlet cell and gives them a reconstruction from the
nearest old particles, the wrong state for fluid that has just entered. The
manager now runs the refill itself after the advection, so it knows which
particles it created, and gives inflow_value to every particle whose
back-trace leaves the domain where the boundary velocity crosses inward:
one step back for a particle that moved, one cell crossing for one created
this step. A no-slip wall has no boundary velocity and a free-slip wall only
a tangential one, so a trace through a wall does not count.

_write_inflow (with its units reduction) and _nondim_timestep move to the
base class; the Lagrangian flavour now carries _components and _psi_units.

Measured on a Maxwell Poiseuille channel, inlet column after 50 steps:
shear-stress error 0.17 with no datum, 0.002 with it.

Underworld development team with AI support from Claude Code

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017kSkAq7oJ5J3XisuvLBovo
mesh.data is the deprecated accessor and fails the Charter deprecated-pattern
gate on this branch (36eb788). Same shape and sum, so the stamp is unchanged.

Underworld development team with AI support from Claude Code

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017kSkAq7oJ5J3XisuvLBovo
@lmoresi

lmoresi commented Sep 24, 2026

Copy link
Copy Markdown
Member Author

Adversarial review before the push, two independent passes over the diff. What they found and what we did:

Defects, fixed in the pushed commits

  • The inflow datum was written into non-dimensional storage without the units reduction the integration-point flavour applies. _write_inflow moved to the base class and the particle flavour now carries _components and _psi_units; _nondim_timestep moved with it so dt is reduced too.
  • An MPI rank holding no particles raised on reshape(0, -1) before the collective calls and would have deadlocked the others. Reshapes are dimension-explicit.
  • The stated "no datum" baseline of 4.2 had been measured with Swarm.repopulate: after a removal the coordinate rows no longer match the value rows, so every refill in that call reads unrelated particles #784 present. Re-measured with Swarm.repopulate: after a removal the coordinate rows no longer match the value rows, so every refill in that call reads unrelated particles #784 fixed: 0.17. The test's negative control and the PR table use that number; the one-step-horizon figure quoted earlier is withdrawn.
  • The created-particle count was a swarm attribute set only inside repopulate, stale whenever the control was cleared. The manager now calls repopulate itself and uses the returned count.
  • The back-trace test counted every boundary. A trace along a resolved curved wall, at a corner, or where the flow separates from a wall leaves through that wall and would have stamped the inlet datum there. The boundary velocity where the trace left now decides; a wall has none, or only a tangential one.
  • After a removal the fix re-located the whole swarm. It now replays PETSc's swap-with-last on the local arrays.
  • The Charter deprecated-pattern gate fails on this branch on a line from 36eb788; fixed in its own commit.

Recorded, not fixed here

Checked and fine: PETSc's removal semantics (verified empirically), the descending removal order, storage order after advection, return_coords_to_bounds in place on box and facet paths, the collective sequence, the symmetric-tensor column layout, both tests failing on the old build for the stated reason.

@lmoresi

lmoresi commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

Why this is red, and what it needs

The two CI failures are not this PR's. They are byte-identical across #785, #789, #795 and #800:

tests/test_1060_nitsche_freeslip.py::test_nitsche_normal_velocity_zero
    assert np.float64(0.0001008010278702676) < 0.0001
tests/test_1070_free_surface_plume.py::test_freesurface_strong_constraint_beats_penalty
    AssertionError: strong constraint not better: 1.73e-02 vs penalty 3.25e-02

Both tests were already fixed on development, for exactly these numbers, and neither name exists there any more. development's CI is green at 1391e4aa. The replacements name the failures in their own docstrings:

  • test_1060 → test_nitsche_constrains_the_wall_normal_velocity, a relative bound of 0.1: "the previous version of this test asserted an ABSOLUTE 1e-4, which scaled with the buoyancy forcing rather than with anything about Nitsche, and sat a fraction of a percent from failing for unrelated reasons."
  • test_1070 → test_freesurface_strong_constraint_tracks_the_prescribed_rate, an absolute bound of 5e-2 against the datum: "It replaces a comparison against the penalty path (errors["strong"] < 0.5 * errors["penalty"]) … measured 2026-09-14: 1.06e-2 on macOS, 1.73e-2 on the Linux CI runner, where the annulus triangulates differently."

So this branch is stale, not broken — 58 commits behind development.

The merge is not clean, and the conflicts are the same on all four

git merge origin/development conflicts in exactly two files on every one of the four branches (#800 adds a third, tests/test_0066_integration_point_slcn.py):

scripts/test.sh — one hunk, take THIS branch's side. development narrowed the glob to tests/test_1100*py; this side has tests/test_110*py. test_1101_advdiff_swarm_rotating_gaussian.py matches none of development's globs (test_1100*, test_1110*, test_1120*), so the narrow side would leave a test file dark — #721 recurring. scripts/check_test_coverage.py verifies the resolution.

src/underworld3/systems/ddt.py — five hunks, and four of them are mechanical. Four have a zero-line development side: they are this branch's own additions (applies_inflow_value, commits_flux_in_post_solve, the inflow_value setter — the #745/#783 inflow work), flagged only because surrounding context moved. None of those three names exists on development. Take this branch's side.

The fifth, at the top of _DDtBase, is a real design conflict and needs the author: this branch adds update_exp_coefficients / _exp_alpha / _exp_phi to the base class; development has since put update_exp_coefficients in three per-flavour classes (ddt.py:1299, 1734, 3612) with _exp_alpha/_exp_phi at :3623, 3628, and adds _note_history_shift in that same place. Keeping both gives a base-class definition under three overrides. development's per-flavour layout is the later design, but which one this branch's ETD work is written against is the author's call, not a merge-tool decision — and this is the DDt core, where CLAUDE.md asks for benchmarking rather than a judgement.

Order matters

All four touch ddt.py in the same region, so they conflict with each other as well as with development. They want merging one at a time, smallest first (#789, #785, #795, then #800), with a rebuild and the transport tests between each — the base moves for the rest after every merge.

Found in the backlog sweep (#819). No commits pushed to this branch.

The same two files and the same five hunks as the other stale transport
branches, resolved the same way.

scripts/test.sh keeps this branch's broader `tests/test_110*py`; development
narrowed it to `test_1100*py`, which leaves the other test_110* files matching
no glob at all — #721 recurring. scripts/check_test_coverage.py verifies it.

Four of the five ddt.py hunks have a zero-line development side: they are this
branch's own `applies_inflow_value`, `commits_flux_in_post_solve` and the
inflow-value docstrings (#745/#783), flagged only because surrounding context
moved. None exists on development.

The fifth takes both sides — this branch's base-class `update_exp_coefficients`
/ `_exp_alpha` / `_exp_phi` (#739) and development's `_note_history_shift`.
Verified on the merged tree: exactly one definition of each of the four, because
the branch's own change already removed the three April per-flavour copies.

Underworld development team with AI support from Claude Code
lmoresi added a commit that referenced this pull request Oct 6, 2026
This branch is the base of a four-PR stack (#785 -> #789 -> #795 -> #800) and
was 71 commits behind development, which is why all four showed the same two
CI failures: tests development had already renamed
(test_1060_nitsche_freeslip's absolute 1e-4 bound and
test_1070_free_surface_plume's strong-vs-penalty ratio). The refresh belongs
here, at the root, so each PR's diff against its base stays its own work rather
than growing development's history.

Two conflicts, both this branch's own additions. scripts/test.sh keeps the
broader `tests/test_110*py` glob -- development narrowed it to `test_1100*py`,
which matches none of the other test_110* files here (#721 recurring);
scripts/check_test_coverage.py verifies it. Four of the five ddt.py hunks have
an empty development side, and the fifth takes both: this branch's base-class
`update_exp_coefficients` / `_exp_alpha` / `_exp_phi` (#739, which replaced
three per-flavour copies dating from b6b0e7a in April) and development's
`_note_history_shift`. Verified on the merged tree: one definition of each.

Underworld development team with AI support from Claude Code
The root (feature/forward-parallel) now carries development, so this brings it
up one level. The single conflict is this branch's own applies_inflow_value
attribute and its docstring (#745/#783), which the root does not have.

Underworld development team with AI support from Claude Code
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