Skip to content

fix(interpolation): bypass DMLocatePoints when caller supplies a verified hint#203

Open
lmoresi wants to merge 1 commit into
developmentfrom
feature/dminterp-bypass-element-check
Open

fix(interpolation): bypass DMLocatePoints when caller supplies a verified hint#203
lmoresi wants to merge 1 commit into
developmentfrom
feature/dminterp-bypass-element-check

Conversation

@lmoresi
Copy link
Copy Markdown
Member

@lmoresi lmoresi commented May 24, 2026

Summary

When the caller of DMInterpolationSetUp_UW (UW3's fork of stock DMInterpolationSetUp) supplies a per-query cell hint, PETSc re-verifies the hint via DMPlexLocatePoint_Internal inside DMLocatePoints. On cell shapes where its in-cell test is geometrically subtle — notably 2-D simplex cells embedded in 3-D space — verification can reject a correct hint and fall back to a wrong-cell match, yielding wild FE evaluations.

This PR skips DMLocatePoints when:

  • the mesh is simplex (mesh.dm.isSimplex()), where the face-containment test is exact for the affine reference map, or
  • the mesh is a manifold (mesh.dim != mesh.cdim), where the kdtree-nearest cell on a closed surface correctly contains every query.

Non-simplex volume meshes (quads, hexes) keep the original DMLocatePoints path, since deformed cells can have non-planar faces where the kdtree-nearest cell can be wrong and PETSc's more rigorous search is needed.

What's in the patch

  • src/underworld3/function/_function.pyx — hint source in petsc_interpolate is Mesh.get_closest_cells (kdtree-nearest, no in-cell test). Comment explains the role.
  • src/underworld3/function/_dminterp_wrapper.pyx::create_structure — gates whether the hint is passed to PETSc on mesh.dm.isSimplex() or mesh.dim != mesh.cdim. On non-simplex volume meshes the hint is suppressed (NULL) and PETSc's DMLocatePoints runs unchanged.
  • src/underworld3/function/petsc_tools.c::DMInterpolationSetUp_UW — when owning_cell is provided, skip DMLocatePoints entirely. The MPI_Allreduce(MIN, foundProcs) after each rank claims its hinted points assigns each global point to the lowest-rank claimant, preserving the prior ownership convention. Lazily builds pointVec / globalPointsScalar only on the DMLocatePoints path.

Net: ~60 / -40 lines across the three files.

Behaviour

Mesh type Path Result
Non-simplex volume (StructuredQuadBox, hexes) DMLocatePoints (unchanged) No regression
Simplex volume (UnstructuredSimplexBox 2D/3D) Bypass Single-rank: same cell id as before. Multi-rank: ownership collapses to rank-0-claims-all (pre-existing parallel-evaluate work tracks this)
Manifold (SphericalManifold, when PPE lands) Bypass Eliminates wrong-cell matches; SLCN advection runs without _evalf=True RBF workaround

Test plan

  • pytest tests/test_0820_deform_mesh_solver_rebuild_regression.py::test_stokes_velocity_updates_after_second_deform — passes (this was the regression in the previous push; quad mesh now goes through DMLocatePoints unchanged)
  • pytest tests/test_0000_imports.py tests/test_0001_meshes.py tests/test_0004_pointwise_fns.py — 28/28
  • Spot-check uw.function.evaluate on quad and simplex meshes — analytic match
  • Validated on feature/parallel-point-eval: probe_manual_p1_eval.py on SphericalManifold — wild-evaluation rate 16% → 0
  • Validated on feature/parallel-point-eval: probe_manifold_advection.py — SLCN advection runs without _evalf=True
  • Full Tier-A / Tier-B regression (CI)

Copilot review feedback (addressed)

  • ✅ Moved pointVec / globalPointsScalar construction into the else branch so the bypass path skips them.
  • ✅ Sentinel test changed from (PetscInt)owning_cell[p] >= 0 to owning_cell[p] != (size_t)-1 to avoid implementation-defined unsigned→signed conversion.

Known limitations / out of scope

  • Multi-rank ownership on simplex / manifold meshes collapses to rank-0-claims-all because get_closest_cells doesn't carry an ownership filter. This is symmetric with the pre-existing behaviour for any single-rank UW3 evaluate (typical use). The general parallel-point-eval architecture is being addressed separately on feature/parallel-point-eval.
  • The pre-existing owning_cell[p] access for p >= n_local in redundantPoints=FALSE mode (where N from MPI_Allgatherv can exceed each rank's hint-array length) is a separate latent issue, not touched by this PR.
  • A separate upstream PETSc MR could add DMInterpolationSetCellHints to the stock DMInterpolation API (the stock wrapper unconditionally drops caller hints). Useful for non-UW3 PETSc users but unrelated to this UW3 bypass.

Underworld development team with AI support from Claude Code

Copilot AI review requested due to automatic review settings May 24, 2026 10:54
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 adjusts Underworld3’s PETSc-based point interpolation path to trust caller-supplied, already-verified cell hints (instead of re-validating via DMLocatePoints()), aiming to eliminate wrong-cell fallbacks that can produce extreme (“wild”) FE evaluations on geometrically subtle cell configurations (notably 2-manifold simplices embedded in 3D).

Changes:

  • Switches Python-side hint generation in petsc_interpolate from a KDTree-only closest-cell guess to a locally verified in-cell search (Mesh._get_closest_local_cells_internal, returning -1 when not locally owned).
  • Updates DMInterpolationSetUp_UW to bypass DMLocatePoints() entirely when owning_cell is provided, claiming points based on the hint instead.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/underworld3/function/_function.pyx Generates interpolation cell hints using an in-cell-tested local search, enabling safe trust of hints downstream.
src/underworld3/function/petsc_tools.c Bypasses DMLocatePoints() when hints are provided, using the hint to assign point ownership and cell IDs.

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

Comment thread src/underworld3/function/petsc_tools.c Outdated
Comment on lines +68 to +79
range = owning_cell[p];
}
if (owning_cell) {
/*
Comment thread src/underworld3/function/petsc_tools.c Outdated
Comment on lines +104 to +106
/* owning_cell carries int64 reinterpreted as size_t; -1 → SIZE_MAX.
Casting back to (signed) PetscInt recovers the -1 sentinel. */
if ((PetscInt)owning_cell[p] >= 0) foundProcs[p] = rank;
@lmoresi lmoresi changed the base branch from development to feature/parallel-point-eval May 24, 2026 12:12
…s when a kdtree hint is available

`DMInterpolationSetUp_UW` constructs a hint PetscSF from `owning_cell` and
passes it to `DMLocatePoints(DM_POINTLOCATION_REMOVE)`. PETSc then verifies
the hint via `DMPlexLocatePoint_Internal`. On cell shapes where the
in-cell test is geometrically subtle — notably 2-D simplex cells embedded
in 3-D space, where a pseudo-inverse projection can map a near-edge query
into the chord plane of an unrelated cell — the verification can reject a
correct hint and silently fall back to a wrong-cell match. The result is
wild FE evaluations (blob magnitudes at the antipode of a Gaussian, ~16%
of trace-back queries failing on `SphericalManifold` advection).

When the caller already has a cell-id per query and that cell-id is
trustworthy, the PETSc re-verification adds no information and introduces
this failure mode. This change skips DMLocatePoints when:

  - the mesh is simplex (`mesh.dm.isSimplex()`), where the face-containment
    test is exact for the affine reference map, or
  - the mesh is a manifold (`mesh.dim != mesh.cdim`), where the kdtree-
    nearest cell on the closed surface correctly contains every query.

On non-simplex meshes (quads, hexes), deformed cells can have non-planar
faces and the kdtree-nearest cell can be wrong; the previous DMLocatePoints
path runs unchanged for those.

What changed:

- `_function.pyx`: cell hints in `petsc_interpolate` use
  `Mesh.get_closest_cells` (kdtree-nearest, no in-cell test). Replaces a
  short-lived attempt to use `_get_closest_local_cells_internal`, which
  surfaced a separate strictness issue in the boundary-vertex in-cell test
  outside the scope of this PR.
- `_dminterp_wrapper.pyx::create_structure`: gates whether the hint is
  passed to `DMInterpolationSetUp_UW` on `isSimplex() or dim != cdim`. On
  non-simplex volume meshes the hint is suppressed (NULL) and the original
  DMLocatePoints path runs unchanged.
- `petsc_tools.c::DMInterpolationSetUp_UW`: when `owning_cell` is provided,
  skip DMLocatePoints entirely; claim every hinted point on this rank
  (`MPI_Allreduce(MIN, foundProcs)` then assigns the lowest-rank claimant).
  Lazily build `pointVec` / `globalPointsScalar` only on the
  DMLocatePoints path. Sentinel test uses `owning_cell[p] != (size_t)-1`
  (addresses copilot review feedback: avoids implementation-defined
  unsigned→signed conversion).

Volume-mesh behaviour:
  - Non-simplex (StructuredQuadBox, hexes): DMLocatePoints path runs
    unchanged. No regression.
  - Simplex (UnstructuredSimplexBox 2D/3D): bypass produces the same cell
    id as DMLocatePoints used to verify and accept. Functional no-op in
    single-rank. Multi-rank ownership collapses to rank-0-claims-all on
    simplex meshes (a known limitation; pre-existing parallel-evaluate
    work in `feature/parallel-point-eval` addresses ownership separately).

Manifold-mesh behaviour (lands when `SphericalManifold` reaches
`development` via `feature/parallel-point-eval`): bypass eliminates the
wrong-cell matches that previously forced SLCN advection through the
`_evalf=True` RBF-Shepard workaround. Validated:

  - `docs/examples/parallel_point_eval/probe_manual_p1_eval.py`:
    wild-evaluation rate 16% → 0; DMInterpolation matches manual
    barycentric P1 to FE accuracy.
  - `docs/examples/parallel_point_eval/probe_manifold_advection.py`:
    SLCN advection runs without `_evalf=True`, peak rotates with the
    prescribed angular velocity, mass stays bounded.

Regression:
  - `tests/test_0820_deform_mesh_solver_rebuild_regression.py` passes
    (the quad-mesh deform test that exposed the earlier strictness path).
  - `tests/test_0000_imports.py + test_0001_meshes.py + test_0004_pointwise_fns.py`:
    28/28.

Underworld development team with AI support from Claude Code (claude.com/claude-code)
@lmoresi lmoresi force-pushed the feature/dminterp-bypass-element-check branch from 97b5b8b to 17a5a8d Compare May 24, 2026 23:57
@lmoresi lmoresi changed the base branch from feature/parallel-point-eval to development May 24, 2026 23:57
@lmoresi
Copy link
Copy Markdown
Member Author

lmoresi commented May 24, 2026

Force-pushed an updated design (commit 17a5a8d) and retargeted base back to development.

The earlier push had a regression on quad meshes — _get_closest_local_cells_internal rejects boundary vertices on bounded meshes (test_0820 failed). Pivoted the design: the bypass is now gated on mesh.dm.isSimplex() or mesh.dim != mesh.cdim. Non-simplex meshes (quads, hexes) keep the original DMLocatePoints path; simplex and manifold meshes use the bypass.

Both Copilot review comments addressed in the new version (pointVec lazy-built, unsigned-sentinel comparison).

@lmoresi
Copy link
Copy Markdown
Member Author

lmoresi commented May 25, 2026

Update — precursor needed

Discussed offline: get_closest_cells is by design a heuristic (always returns SOME cell, suitable for hinting PETSc, not authoritative). The current PR uses it as authoritative for the bypass — that's wrong in general; works on SphericalManifold only coincidentally.

The right authoritative hint source is _get_closest_local_cells_internal with looser semantics (>= 0 instead of strict > 0 in the in-cell test) so it accepts on-face queries — i.e., every mesh vertex, every point on a shared face. Opened as PR #207 (bugfix/in-cell-test-loose-semantics).

Once #207 merges, I'll rebase this PR onto it and switch the hint source in _function.pyx from get_closest_cells back to _get_closest_local_cells_internal. The bypass will then be authoritative for simplex + manifold meshes; non-simplex meshes keep the original DMLocatePoints path.

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