Skip to content

global_evaluate: faithful parallel evaluate() (fix out-of-domain mislocation)#222

Merged
lmoresi merged 3 commits into
developmentfrom
bugfix/global-evaluate-parallel-extrapolation
Jun 12, 2026
Merged

global_evaluate: faithful parallel evaluate() (fix out-of-domain mislocation)#222
lmoresi merged 3 commits into
developmentfrom
bugfix/global-evaluate-parallel-extrapolation

Conversation

@lmoresi

@lmoresi lmoresi commented Jun 6, 2026

Copy link
Copy Markdown
Member

Problem

global_evaluate's parallel path silently returned wrong values for query points outside the (old) domain. The evaluation-swarm migrate routes unclaimed points by nearest rank centre-of-mass and strands them on an arbitrary rank, which extrapolates from a geometrically-far local cell. Deterministic reproduction (rotation gate, linear field T=x, np=5): a point at x=+0.64 read −0.42 (opposite side), max_err=1.06. This corrupted mesh-variable transfer on parallel mover-adapted meshes.

Fix

Make global_evaluate a faithful parallel evaluate() — interpolate inside, extrapolate from the true nearest cell outside, flag inside/outside. A best-claim out-of-domain fallback in global_evaluate_nd:

  • allgather the (small, boundary-layer) extrapolated set,
  • each rank reports nearest-local-cell distance + its local rbf extrapolation,
  • Allreduce(MIN dist / MIN rank / SUM winner-value) picks the globally-nearest rank's value.

Only unconditional collectives + a local rbf_evaluate (never the collective FE interpolation, which would desync per-rank → hang). O(boundary points), no dense global tree. Deadlock-safe by construction.

Default on; GE_LOCAL_FALLBACK=0 restores legacy. Serial path unchanged (gated mpi.size>1).

Validation

Rotation gate, T=x, np=5: max_err 1.06 → 0.003 (bit-identical to serial 0.003); turning the fallback off reproduces 1.06. tier-A green; used throughout the parallel adaptive-convection runs.

Underworld development team with AI support from Claude Code

…location

global_evaluate's parallel path had no correct handling for query points
outside the (old) domain: the evaluation-swarm migrate routes unclaimed points
by nearest rank centre-of-mass and strands them on an arbitrary rank, which
then extrapolates from a geometrically-far cell -> silently-wrong values,
parallel-only (e.g. an annulus boundary point reading the opposite side). This
corrupted mesh-variable transfer on parallel mover-adapted meshes.

Restore the serial evaluate() contract (interpolate inside / extrapolate from
the TRUE nearest cell outside / flag inside-outside) with a best-claim
out-of-domain fallback in global_evaluate_nd: allgather the (small,
boundary-layer) extrapolated set; every rank reports its nearest-local-cell
distance + its LOCAL rbf extrapolation; Allreduce(MIN dist / MIN rank / SUM
winner value) picks the globally-nearest rank's value. Only unconditional
collectives + local rbf_evaluate (never the collective FE interpolation, which
would desync) -> deadlock-safe. O(boundary points), no dense global tree.

Default on; GE_LOCAL_FALLBACK=0 restores legacy. Serial unchanged (gated
mpi.size>1). Validated: deterministic-rotation gate, linear field T=x, np=5,
max_err 1.06 -> 0.003 (== serial).

Underworld development team with AI support from Claude Code
Copilot AI review requested due to automatic review settings June 6, 2026 08:51

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.

Pull request overview

This PR updates global_evaluate_nd to correct MPI-parallel evaluation for points that end up outside any rank’s owned cells after the swarm migrate round-trip, aiming to make parallel behavior match the serial evaluate() contract (interpolate in-domain; extrapolate just outside; return inside/outside via check_extrapolated) and to avoid deadlocks by using only unconditional collectives and rank-local RBF evaluation.

Changes:

  • Adds a parallel “best-claim” out-of-domain fallback: allgather stranded points, compute per-rank distance/value, and Allreduce to select the globally-best rank’s extrapolation.
  • Adds an environment-variable escape hatch (GE_LOCAL_FALLBACK) to disable the new fallback and restore legacy behavior.
  • Expands the global_evaluate_nd docstring/comments to document the parallel contract and deadlock-safety constraints.

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

Comment on lines +593 to +603
ext_vals, ext_flag = evaluate_nd(
expr, all_ext, rbf=True, evalf=False, verbose=False,
check_extrapolated=True,)
ext_vals = np.ascontiguousarray(
np.asarray(ext_vals, dtype=np.double).reshape((n_ext_total,) + expr_shape))
ext_flag = np.asarray(ext_flag).reshape(n_ext_total).astype(np.int32)

# Nearest-local-cell distance for every point (local kd-tree query).
mesh._build_kd_tree_index()
dist2, _ = mesh._centroid_index.query(all_ext, k=1, sqr_dists=True)
dist2 = np.ascontiguousarray(np.asarray(dist2, dtype=np.double).ravel())
Comment thread src/underworld3/function/_function.pyx Outdated
Comment on lines +574 to +576
import os
if uw.mpi.size > 1 and os.environ.get("GE_LOCAL_FALLBACK", "1") not in (
"0", "off", "false", "no", ""):
Comment on lines +371 to +374
Contract: this is a faithful *parallel* counterpart of :func:`evaluate` —
a query point is interpolated wherever in the mesh it lands (on any rank),
a point just outside the mesh is extrapolated from its true nearest cell,
and ``check_extrapolated`` returns an inside/outside flag per point. The
Comment on lines +533 to +537
# ------------------------------------------------------------------
# Out-of-domain extrapolation — keep the parallel result a faithful
# match for the serial ``evaluate()`` contract: interpolate a point
# wherever it lands across ranks, extrapolate a point just outside the
# mesh, and flag inside/outside.
lmoresi added 2 commits June 6, 2026 16:28
…K env)

The parallel out-of-domain best-claim fallback was controlled only by the
GE_LOCAL_FALLBACK environment variable (Copilot review flag). Promote it to a
real local_fallback=True kwarg threaded through global_evaluate ->
_global_evaluate_impl -> global_evaluate_nd. The kwarg is the supported
control surface; the env var, if explicitly set, still overrides it — an
operator escape hatch retained because of the parallel-deadlock debugging
history (a no-code kill switch for the added collectives). Serial is untouched
and bit-identical regardless of the kwarg.

Underworld development team with AI support from Claude Code
- GE_LOCAL_FALLBACK parsing is now case/whitespace-insensitive (.strip().lower()),
  so False/OFF/No disable the fallback as expected.
- clarify the contract docstring: out-of-domain points extrapolate from the
  globally nearest cell by a centroid-distance heuristic (lowest rank wins ties),
  not necessarily the exact nearest cell.

Underworld development team with AI support from Claude Code
@lmoresi

lmoresi commented Jun 12, 2026

Copy link
Copy Markdown
Member Author

Pushed fixes for the review nits:

  • GE_LOCAL_FALLBACK case-sensitivity — now normalized with .strip().lower(), so False/OFF/No disable the fallback. (Also: local_fallback is now a proper kwarg, not env-only.)
  • "true nearest cell" docstring — reworded to reflect the actual centroid-distance heuristic (lowest rank wins ties in parallel).

On the other two threads:

  • ext_flag always-true — this is correct by construction, not a bug: the points fed to the rbf fallback (all_ext) are exactly the points no rank could locate in-cell, so flagging them all as extrapolated is right, and writing return_mask back to True for them is a no-op. Happy to drop the redundant ext_flag plumbing as cleanup if preferred.
  • Missing regression test — agreed this is the real gap. A parallel out-of-domain test (asserting rank-independence) would lock the behaviour; flagging for a follow-up unless we want it in this PR.

@lmoresi lmoresi merged commit 58385b9 into development Jun 12, 2026
1 check passed
@lmoresi lmoresi deleted the bugfix/global-evaluate-parallel-extrapolation branch June 13, 2026 00:48
lmoresi added a commit that referenced this pull request Jul 6, 2026
…og, value-first call-site sweep (WE-01..03,05,06,08,09,10) (#338)

* docs(WE-01): adopt the one-governing-doc-per-topic authority map

Repoint CLAUDE.md's Data Access 'Authoritative Reference' from the stale
UW3_Style_and_Patterns_Guide.md to subsystems/data-access.md (the guide it
crowned teaches patterns the code deprecates at runtime — DOC-04), and
record the Style Charter §10 authority table in docs/developer/index.md as
the master authority index. The Charter is added to the Getting Started
toctree (removes a baseline 'not included in any toctree' warning).

Finding: DOC-04 (docs/reviews/2026-07/DOCS-STANDARDS-COHERENCE.md).

Underworld development team with AI support from Claude Code

* docs(WE-02): de-drift the Style Guide's four stale normative sections

Rewrites the sections DOC-01 verified as contradicting the settled standards:

- Docstring format: the 'Markdown Docstrings for pdoc/pdoc3' section is
  replaced by the NumPy/Sphinx RST standard (worked example with :math: and
  Parameters/Returns/Examples/Notes; conversion tracked in
  docs/plans/docstring-conversion-plan.md), per Style Charter section 6.
- Doc file format: Quarto .qmd prescription (zero .qmd files exist in the
  repo) replaced by MyST .md/Sphinx guidance matching CLAUDE.md; migration
  table row updated.
- Data access examples: 'Preferred' coordinate examples now use the real,
  runnable API — mesh.X.coords (read), mesh.deform() (coordinate changes),
  and the swarm.coords getter/setter for particle positions. The previous
  'Preferred' example swarm.data += displacement raises AttributeError
  (getter-only property — SWARM-13 evidence); mesh.data warns at runtime.
  The private-attribute migration advice (swarm._particle_coordinates,
  mesh._deform_mesh presented as the NEW pattern) is deleted.
- Front matter: the 21-line Quarto YAML header is replaced by a minimal
  MyST title block, and the guide now states that the UW3 Style Charter is
  the normative contract and wins on conflict.

All replacement examples verified against current source: Swarm.coords
setter (swarm.py), Mesh.deform (discretisation_mesh.py:3133),
uw.synchronised_array_update / NDArray_With_Callback.delay_callbacks_global.

Findings: DOC-01, SWARM-13 (style-guide part).

Underworld development team with AI support from Claude Code

* docs(WE-03): regenerate the docstring review queue; add the sweep to the release checklist

The queue (last generated 2026-01-13, cdf5bb2) misrepresented the codebase
both ways: it flagged now-complete items (solve, SNES_Scalar) as missing and
contained zero entries for the June 2026 API (DOC-02). Regenerated over
src/underworld3/**/*.py + **/*.pyx at the current tip.

Two bugs in scripts/docstring_sweep.py's regex-based Cython parser made the
regenerated queue lie about .pyx docstrings and are fixed as part of making
the regeneration meaningful:

- the indent group '(\s*)' with re.MULTILINE consumed preceding blank lines,
  shifting the computed definition line so the docstring search started ON
  the def/class line and always missed;
- the docstring search started at the definition line rather than after the
  (possibly multi-line) signature, so long signatures hid their docstrings;
- raw-string docstrings (r""", the norm in the solver .pyx) were not
  recognised.

DOC-02 cross-validation on the regenerated queue now passes: solve /
SNES_Scalar in the solver pyx are no longer flagged 'none'; the queue
contains the June API (add_nitsche_bc, add_rotated_freeslip_bc,
boundary_flux, set_custom_fmg, consistent_jacobian: 13 mentions) and flags
the DOC-05 targets (Swarm.advection x2, read_timestep, write_proxy) as
undocumented.

Also adds the sweep to the quarterly release checklist
(guides/release-process.md) so the queue cannot go stale unnoticed again.

Findings: DOC-02 (docs/reviews/2026-07/DOCS-STANDARDS-COHERENCE.md).

Underworld development team with AI support from Claude Code

* docs(WE-05): backfill the changelog for May - early July 2026; add the changelog sweep to the release checklist

The changelog (the quarterly CIG/stakeholder record) ended in April 2026
while ~117 first-parent commits landed May through early July (DOC-03).
Backfilled at the existing conceptual granularity — 14 grouped entries,
grouped by subsystem rather than by PR, matching the established format
(### Title (Month Year), bold lead sentence, hyphen bullets, inline PR
references):

- New '2026 Q3 (July - September)' section: the July 2026 quality campaign
  (#309-#313, #317, #322-#326, #329, #334 as grouped entries), rotated
  strong free-slip / boundary traction / dynamic topography (#293, #294,
  #298, #306), generalized geometric multigrid via custom prolongation
  (#290, #297), consistent Jacobian tangent (#258), swarm correctness
  (#216, #313, #323, #329), numpy 2 support (#301, #305).
- Extended '2026 Q2' section with the May-June entries: mesh adaptation
  movers (#190, #209, #213, #228, #259, #264, #266), moving-mesh field
  transfer / deform() (#246, #249, #251), semi-Lagrangian accuracy controls
  (#164, #183, #185-#189, #208, #220), snapshot/checkpoint toolkit (#146,
  #195, #196, #198), Stokes_Constrained (#224, #229, #240, #265), local-h
  Nitsche + boundary-slip surfaces (#225, #241, #275), units
  interoperability (#277, #278, #283, #284), memory/evaluation/solver
  infrastructure (#161, #177-#179, #181, #182, #222, #237, #250, ...).

Every entry is backed by a merged commit on development (verified against
git log --first-parent aed517f..3184a40). Also adds a quarterly-changelog
sweep step beside the docstring sweep in the release checklist
(guides/release-process.md) per DOC-03's proposed fix.

Findings: DOC-03 (docs/reviews/2026-07/DOCS-STANDARDS-COHERENCE.md).

Underworld development team with AI support from Claude Code

* docs(WE-06): status headers on the unmarked design docs (per-doc git verification)

Adds one-to-three-line Status markers to the 13 design docs that lacked one,
following the directory's existing conventions (**Status**: line under the
title; status: key inside existing YAML frontmatter for the three
frontmatter-only docs), and corrects the stale 'Design Phase' marker on
MATHEMATICAL_MIXIN_DESIGN.md (the mixin ships in
utilities/mathematical_mixin.py).

Every stamp was verified against git history (git log --follow dates) and
the current source tree before writing:

- Implemented: jacobian-consistent-tangent (PR #258, c63cd70),
  fmg-checkpoint-hierarchy (3cd73cd), petsc-dmplex-checkpoint-reload-plan
  (PR #146, write_timestep(petsc_reload=True) in tree),
  fault-refinement-simplification (smooth_mesh_interior /
  metric_density_from_gradient / fault_comb_metric all in tree),
  MATHEMATICAL_MIXIN_DESIGN.
- Current reference/contract: mesh-adaptation-formulation,
  ND_UNITS_BOUNDARY_CONTRACT (PR #278, e0ece9a).
- Investigation records (preserved via PR #245, 34a9dd4; production
  geometric-MG is custom prolongation, PR #290): snesfas-feasibility,
  snesfas-vanka-feasibility-study.
- Design notes / prototypes with honest gaps: in_memory_checkpoint_design
  (not implemented, per its own trailing Status section),
  submesh-solver-architecture (extract_region/extract_surface exist;
  coarsened_companion does not).
- Historical: ARCHITECTURE_ANALYSIS (persistence.py layout superseded),
  COORDINATE_MIGRATION_GUIDE (transition shipped),
  WHY_UNITS_NOT_DIMENSIONALITY (decision record).

The audit's ~16 estimate over-counted: re-derived at this tip, 13 docs were
unmarked plus one marked-but-stale (DOC-07).

Findings: DOC-07 (docs/reviews/2026-07/DOCS-STANDARDS-COHERENCE.md).

Underworld development team with AI support from Claude Code

* docs(WE-08): convert units.py public docstrings Google -> NumPy style

Docstring-only conversion of the 18 public module-level functions that
carried Google-style Args:/Returns:/Raises:/Examples: labels
(check_units_consistency, get_dimensionality, get_units,
non_dimensionalise, show_nondimensional_form, simplify_units,
create_quantity, convert_units, to_base_units, to_reduced_units,
to_compact, get_scaling_coefficients, set_scaling_coefficients,
validate_expression_units, assert_dimensionality,
validate_coordinates_dimensionality, enforce_units_consistency,
require_units_if_active, convert_angle_to_degrees) to the NumPy/Sphinx
standard (Style Charter section 6). dimensionalise was already NumPy
style; one-line docstrings and private helpers are untouched. No code,
signature, or behaviour changes (verified: every diff hunk is inside a
docstring; ast.parse clean).

Finding: API-12 (docs/reviews/2026-07/API-CONSISTENCY-REVIEW.md).

Underworld development team with AI support from Claude Code

* docs(WE-09): sweep call sites of the newer BC methods to value-first (conds, boundary, ...) order

Wave C (#334) made the ORIGINAL value-first order canonical for
add_nitsche_bc / add_rotated_freeslip_bc / add_constraint_bc (maintainer
decisions D2/D3; Style Charter section 6) with deprecation shims for the
legacy boundary-first and g= spellings. This sweep updates every call site
of those THREE methods to the canonical order so nothing in the repository
exercises the shims — 74 sites total:

- tests/: 63 call sites across 12 files (test_1017, test_1018, test_1060,
  test_1061, test_1062, test_1064, test_1065 x2 serial;
  parallel test_1017, test_1062, test_1063, test_1064).
  tests/test_0641_wave_c_api_shims.py is deliberately untouched — its
  legacy-order calls ARE the deprecation contract.
- docs/: 7 sites (curved-boundary-conditions.md x4,
  CONSTRAINED_FREESLIP_MULTIPLIER.md call + signature line,
  examples/submesh_investigation/test_region_ds_nitsche.py).
- .claude/skills/: 3 sites (adapt-on-top-faults x2,
  free-surface-convection x1).
- CLAUDE.md: 1 signature reference (free-slip BC preference section).

The ~1,370 legacy-trio (add_dirichlet_bc/add_natural_bc/add_essential_bc)
sites already conform and are untouched per the D2 decision. The audit
review documents under docs/reviews/2026-07/ record the pre-decision
state as evidence and are not swept.

Discovered while verifying the swept tests run warning-free: the Wave C
zero-datum guard in add_rotated_freeslip_bc rejects FLOAT zero
(sympy.sympify(0.0) != 0 is structurally True), so the canonical
add_rotated_freeslip_bc(0.0, boundary) raises NotImplementedError while
conds=0 works. Filed as issue #336 with a TODO(BUG) marker at the guard
(comment-only src touch); the swept call sites use the working integer
form add_rotated_freeslip_bc(0, boundary). No fix applied here (Charter
section 9 scope discipline).

Findings: API-01/API-02 sweep (WE-09, REMEDIATION-WORKLIST.md).

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.

2 participants