Skip to content

Empty-rank mesh construction, tensor stats, global metric moments, swarm.points label#406

Merged
lmoresi merged 5 commits into
developmentfrom
bugfix/empty-rank-batch
Jul 24, 2026
Merged

Empty-rank mesh construction, tensor stats, global metric moments, swarm.points label#406
lmoresi merged 5 commits into
developmentfrom
bugfix/empty-rank-batch

Conversation

@lmoresi

@lmoresi lmoresi commented Jul 24, 2026

Copy link
Copy Markdown
Member

Four fixes from the triage backlog plus the public adversarial-review round and its responses. Closes #399, closes #400, closes #351, closes #386.

The fixes

#399 — mesh construction crashed on ranks owning zero cells. Two layers: the navigation kd-tree build passed a 1-D numpy.array([]) into the KDTree memoryview, and the KDTree constructor itself could not represent an empty cloud (&points[0][0]). Empty point sets are now first-class: (0, dim) clouds build no C++ index and answer queries honestly (found=False, infinite distances); 1-D input gets a legible error. StructuredQuadBox(elementRes=(2,2)) now constructs, integrates and solves at np3/np4 (PETSc itself is fine with empty ranks). Follow-ups from the review, also in this PR: the three sentinel sanity guards were off-by-one (> vs >= — the empty-tree sentinel masqueraded as valid index 0), and _get_domain_centroids produced NaN for starved ranks, which gather_data silently stripped — de-aligning the rank/row table and making _route_by_nearest_centroid silently migrate particles to a rank owning zero cells (demonstrated live in the review; now a huge-finite sentinel keeps the table aligned and the starved rank unreachable).

#400stats() crashed for every genuine tensor variable. Two stacked defects: the Frobenius loop indexed the flat component count against the structured array axis, and the temp variable was built through the enhanced factory whose __getattr__ blocks underscore delegation. The review then caught my first fix under-measuring SYM_TENSOR norms (off-diagonals must count twice) — the final form reads the structured (N, d, d) view, correct for both tensor kinds, validated against numpy.

#351mesh_metric_mismatch moments were partition-dependent. The equidistribution target and the returned moments are now globally reduced (median via gather-to-root); starved ranks participate in the reductions with empty contributions; non-simplex refusal is rank-symmetric via mesh.isSimplex. Known limit, documented in place: a metric containing MeshVariable data still cannot run with a starved rank — that needs the evaluate layer (#405).

#386 — deprecated swarm.points labelled metre magnitudes with mesh.units. Scaled values are now labelled meter, the mesh.X.coords convention; verified all three coordinate views agree physically under a km-scale model.

Adversarial review (public, round 1)

Posted in full below. All four primary fixes held their core claims; the review found and demonstrated live: the SYM_TENSOR under-measurement, the silent particle mis-route, the sentinel off-by-ones, a deadlock in #351's empty path with field metrics, and — most valuable — the honest map of where empty-rank support ends: construction, solve, integrals, swarm populate and stats all work; the first evaluate/estimate_dt still fails (rank-asymmetric raise in get_min/max_radius). Everything in that next layer is scoped in #405 (which also ties in #314).

Verification

Reminder for the merge: manual close needed for #399, #400, #351, #386.

Underworld development team with AI support from Claude Code

lmoresi added 5 commits July 24, 2026 10:08
…louds, nav index shapes empty arrays (#399)

Any mesh construction where a rank owned zero cells crashed: the
navigation kd-tree build passed numpy.array([]) — a 1-D array — into the
KDTree memoryview, and the surviving ranks died on the resulting
collective divergence (repro: StructuredQuadBox(elementRes=(2,2)) at
np3/np4).

Two layers, both fixed:
- _build_kd_tree_index shapes its point arrays explicitly to (0, cdim)
  when the cell lists are empty;
- KDTree itself accepts an empty (0, dim) cloud: no C++ index is built
  (taking &points[0][0] would be invalid), and the query methods return
  honest empties (found=False, infinite distances). A 1-D input now gets
  a legible error instead of the memoryview's buffer message. The
  count==0 query guard also removes the same invalid-address hazard for
  empty QUERY arrays against a populated tree.

Regression tests: empty-cloud + 1-D-rejection in test_0101; starved-rank
mesh construction at np4 in tests/parallel/test_0700 (domain integral
exact). Direct repro verified at np3 and np4.

Closes #399

Underworld development team with AI support from Claude Code
…p variable (#400)

Two stacked defects made stats() raise for every genuine TENSOR variable:
- the Frobenius accumulation indexed the flat component count (d*d)
  against the structured .array axis of size d (IndexError);
- the temporary was created through the enhanced factory, whose
  __getattr__ refuses underscore-name delegation, so the _scalar_stats
  call could never be reached (_vector_stats already used
  _BaseMeshVariable).

Components are now read through the flat .data layout and the temp is a
_BaseMeshVariable. Regression: identity-tensor Frobenius = sqrt(d) exact,
plus a repeat-call/cleanup lifecycle test (tests/test_0102).

Closes #400

Underworld development team with AI support from Claude Code
…mpty-rank safe (#351)

A_target/A_mean were built from rank-local sums, so the returned delta
moments (rms/max/median_abs) were partition-dependent diagnostics against
a per-rank equidistribution target. The sums, the cell count and the
moments are now globally reduced (median via gather-to-root + broadcast —
a diagnostic at adapt cadence). A rank owning zero cells — constructible
since the #399 fix — participates in every reduction with empty
contributions instead of raising through the tris-is-None path and
desynchronising the collectives.

Regression: moments identical across ranks to 1e-14 at np2 and np4
(tests/parallel/test_0750_global_statistics).

Closes #351

Underworld development team with AI support from Claude Code
The deprecated swarm.points scales model coordinates by the SI length
scale (metres) but labelled the result with mesh.units — kilometre-unit
meshes got metre magnitudes under a km label, a 1000x label/value
mismatch. Scaled values are now labelled 'meter', the same convention as
mesh.X.coords; the unscaled path keeps its previous label.

Regression: label + magnitude consistency under an active km-scale model
(tests/test_0114).

Closes #386

Underworld development team with AI support from Claude Code
…etric raise, sentinel guards, mis-route)

Responses to the public adversarial review of this branch:

- Tensor stats read the structured (N, d, d) array view, which mirrors
  symmetric storage — SYM_TENSOR off-diagonals now count twice as the
  Frobenius norm requires (the flat read converted the pre-existing crash
  into a silently under-measured norm). Validated against numpy for
  non-symmetric TENSOR and Voigt SYM_TENSOR fields.
- mesh_metric_mismatch refuses non-simplex meshes rank-symmetrically via
  mesh.isSimplex (the per-rank tris check let starved ranks sail into the
  collective reductions while populated ranks raised). The field-valued
  metric limitation on starved ranks is documented in place — it needs
  the evaluate layer to be empty-rank safe first (#405).
- The three kd-tree sentinel guards use >= (valid indices are 0..n-1), so
  the empty-tree sentinel trips the sanity error instead of indexing
  _indexMap out of bounds.
- _get_domain_centroids gives starved ranks a huge finite sentinel
  centroid instead of NaN: gather_data silently strips NaN rows, which
  de-aligned the rank/row correspondence and made
  _route_by_nearest_centroid silently migrate particles to a rank owning
  zero cells (demonstrated live in the review). Finite, not inf —
  infinities poison kd-tree bounding boxes.
- Test hygiene from the review: the swarm.points test asserts _scaled
  rather than hiding behind it, and drops the deprecated units= mesh
  kwarg.

Deeper findings outside this diff are tracked as #405 (evaluate /
points_in_domain / radii / gather_data NaN-stripping — the next
empty-rank layer).

Underworld development team with AI support from Claude Code
Copilot AI review requested due to automatic review settings July 24, 2026 00:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@lmoresi

lmoresi commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

Adversarial review — round 1 (public record)

Independent skeptic pass over the branch diff, instructed to REFUTE each fix; live probes at np2/np4 including starved-rank configurations.

#399 — KDTree empty clouds

  • Class internals HELD: every dereference of the C++ index audited (build_index, both query paths, dealloc-on-NULL verified via live-instance count); rbf interpolators reach the index only through the guarded queries.
  • Consumers BROKEN (fixed in response commit): get_closest_cells indexed _indexMap with the empty-tree sentinel — the sanity guard was off-by-one (> where >=), letting sentinel 0 pass as a valid index (demonstrated at np4: two ranks raise IndexError, two return cells). Same off-by-one at three sites; all now >=.
  • Silent mis-route BROKEN (fixed in response commit): _route_by_nearest_centroid at np4 on a 2x2 box silently migrated 18 particles to a rank owning zero cells — NaN centroid from the empty mean, silently stripped by gather_data, de-aligning the rank/row table. Now a huge-finite sentinel keeps rows aligned and starved ranks unreachable (finite, not inf: infinities poison kd-tree bounding boxes).
  • Honest map of what breaks NEXT on a starved rank: construction OK, Integral OK, Poisson solve OK, swarm populate OK — the wall is the first uw.function.evaluate / estimate_dt (get_min/max_radius does an empty-array reduction, rank-asymmetric raise → deadlock). Scoped as Empty-rank support, layer 2: evaluate/points_in_domain, radii/centroid reductions, gather_data NaN-stripping #405.

#351 — global metric moments

#400 — tensor stats

  • Non-symmetric TENSOR vs numpy: HELD to 1e-15, including at np4 with starved ranks; temp lifecycle clean.
  • SYM_TENSOR BROKEN (fixed in response commit): the flat read counted each Voigt component once — measured 'Frobenius' 2.536 vs true 3.232. The structured (N,d,d) read counts mirrored off-diagonals twice; regression test pins the exact value.

#386 — swarm.points label

HELD: all three coordinate views (points / coords / mesh.X.coords) agree physically under a 1000-km-scale model; the save path is confirmed migrated off self.points; no consumer depends on the old label. Two test-hygiene nits (deprecated kwarg, guarded assertion) fixed in the response commit.

Outside the diff

gather_data NaN-stripping as a general landmine, the collective-crosstalk failure mode (a caught rank-asymmetric raise pairs the survivors' pending collective with the next one the failed ranks enter — observed producing garbage with no error), and the unguarded empty-source rbf/kNN paths — all recorded in #405, which also links #314 as the same family seen from the DMInterpolation side.

Response commit

ad63bc9; the level_1+tier_a gate (443 passed) and the np2/np4 batteries were re-run after it.

Underworld development team with AI support from Claude Code

@ss2098

ss2098 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Professor @lmoresi, this adversarial review is very helpful. Since the remaining failures are in the empty-rank / collective-evaluation family tracked in #405, I will not try to modify the core evaluation path directly.

One place where I may be able to help is with small validation/regression work once the fix direction is clear: running the starved-rank reproducers at np2/np4, checking macOS behaviour, and helping add or update focused tests around the empty-rank cases without touching the solver internals.

I'll keep my contributions limited to small, measurable test/API/documentation fixes unless there is a specific low-risk piece you would like me to try.

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.

3 participants