Skip to content

fix(meshing): loose semantics on _test_if_points_in_cells_internal — accept on-face queries#207

Open
lmoresi wants to merge 1 commit into
developmentfrom
bugfix/in-cell-test-loose-semantics
Open

fix(meshing): loose semantics on _test_if_points_in_cells_internal — accept on-face queries#207
lmoresi wants to merge 1 commit into
developmentfrom
bugfix/in-cell-test-loose-semantics

Conversation

@lmoresi
Copy link
Copy Markdown
Member

@lmoresi lmoresi commented May 25, 2026

Summary

Mesh._test_if_points_in_cells_internal used a strict > 0 test on the squared-distance difference between mirrored inner/outer control points placed ±1e-3 along each face normal. A query exactly on a cell face has zero distance difference and was rejected. That meant mesh vertices — which by definition sit on the faces of every cell containing them in their closure — failed the in-cell test for every candidate cell, and _get_closest_local_cells_internal returned -1 for them.

For evaluation use cases this is wrong: the FE basis at a shared vertex / face is consistent across the adjacent cells (DM consistency requirement of FE assembly), so "any cell whose closure contains the point" is the right semantics.

What's in the patch

  • src/underworld3/discretisation/discretisation_mesh.py — add strict: bool = False kwarg to _test_if_points_in_cells_internal. Loose default accepts diff >= -1e-12; strict preserves the previous > 0. The public test_if_points_in_cells wrapper forwards the kwarg.
  • tests/test_0820_in_cell_test_loose_semantics.py — 5 tests locking the contract (loose default accepts vertices on 2D/3D simplex and 2D quad meshes; strict explicit still rejects on-face queries; returned cells genuinely contain the query in their closure).

Net: +131 / -12 lines.

Empirical impact (probed locally before/after)

Mesh Before (strict >0) After (loose >=-1e-12)
2D quad (50×50, vertex queries) 1131/2601 (43%) rejected 0/2601 rejected
3D simplex (cellSize=0.4, vertex queries) 99/140 (71%) rejected 0/140 rejected
Cells returned (loose) every cell's closure contains the query

Strict vs loose — when to use which

  • Loose (new default): FE-evaluation hints, points-in-domain queries, "is this point in or on this cell" checks. The natural semantics for any query that just needs some cell containing the point.
  • Strict (strict=True): callers that need uniqueness — strict-ownership partitioning where a shared-face point being claimed by all adjacent cells would be a bug.

The strict→loose default change is observable for one caller: points_in_domain now reports boundary-vertex queries as "in the domain" instead of "outside". This matches user intuition (a point on the boundary of a closed domain IS in the closed domain).

Why this matters

_get_closest_local_cells_internal is now an authoritative cell-id source — returns a cell whose closure contains the query, or -1 if no local cell does. That's the missing piece for PR #203 (feature/dminterp-bypass-element-check): the bypass needs a trustworthy hint and this provides it.

Multi-rank ownership: a vertex on a partition seam will be claimed by both adjacent ranks under loose semantics. For FE evaluation this is harmless (the basis values agree at shared DOFs by DM consistency), so the MPI_Allreduce(MIN, foundProcs) tie-break is correct. If a future use case needs strict ownership, callers can pass strict=True.

Test plan

  • New tests tests/test_0820_in_cell_test_loose_semantics.py — 5/5 pass
  • tests/test_0000_imports.py + test_0001_meshes.py + test_0004_pointwise_fns.py + test_0800_unit_aware_functions.py — 32/32 pass (no regressions)
  • Full Tier-A regression (CI)

Underworld development team with AI support from Claude Code

…accept on-face queries

`_test_if_points_in_cells_internal` used a strict `> 0` test on the
squared-distance difference between mirrored inner/outer control points
placed ±1e-3 along each face normal. A query exactly on a cell face has
zero distance difference and was rejected. That meant mesh vertices —
which by definition sit on the faces of every cell containing them in
their closure — failed the in-cell test for every candidate cell, and
`_get_closest_local_cells_internal` returned -1 for them.

For evaluation use cases (the dominant caller), this is wrong: the FE
basis at a shared vertex / face is consistent across the adjacent cells
(it's a DM consistency requirement of FE assembly), so "any cell whose
closure contains the point" is the right semantics. Picking one specific
adjacent cell and returning its id lets downstream code evaluate
correctly.

Add a `strict` keyword (default False) to `_test_if_points_in_cells_internal`:

  - strict=False (new default): accept diff >= -1e-12. A point on a
    face counts as inside the cell. The -1e-12 tolerance is well below
    the 1e-3 control-point offset (test resolution) and well above
    64-bit float roundoff. Use this for FE-evaluation hints,
    points-in-domain queries, and similar "is this point in or on
    this cell" checks.
  - strict=True: preserve the previous `> 0` behaviour. Use this when
    uniqueness matters — e.g. a strict-ownership partitioning scheme
    where each point should be claimed by exactly one cell, and a
    shared-face point being claimed by all adjacent cells would be a
    bug.

Behavioural consequences:

  - `_get_closest_local_cells_internal` now returns a valid cell id for
    every vertex query (was returning -1 for ~43% of vertices on a
    structured quad, ~71% on a 3D simplex — all of them on cell
    boundaries by definition).
  - `points_in_domain` (via the public `test_if_points_in_cells`
    wrapper) reports boundary-vertex queries as "in the domain"
    instead of "outside". This matches user intuition.
  - Existing strict-mode behaviour is preserved by passing
    `strict=True` explicitly.

New tests in `tests/test_0820_in_cell_test_loose_semantics.py` lock
the contract: loose default accepts vertices on 2D/3D simplex and 2D
quad meshes, strict explicit still rejects on-face queries, and the
returned cells genuinely contain the queries in their closure.

This change unblocks the bypass design in
`feature/dminterp-bypass-element-check` (PR #203), which needs an
authoritative per-rank cell-id source — `_get_closest_local_cells_internal`
with loose semantics gives exactly that.

Underworld development team with AI support from Claude Code (claude.com/claude-code)
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 mesh cell-containment semantics so queries exactly on a cell face/edge/vertex are treated as “inside” by default. This fixes cases where mesh vertices (which lie on the closure of adjacent cells) were being rejected by _test_if_points_in_cells_internal, causing _get_closest_local_cells_internal to return -1 for valid vertex queries—impacting downstream evaluation and domain queries.

Changes:

  • Added a strict: bool = False kwarg to Mesh._test_if_points_in_cells_internal() and to the public Mesh.test_if_points_in_cells() wrapper; loose mode accepts on-face queries via a small negative tolerance.
  • Updated the internal in-cell test implementation to branch between strict (> 0) and loose (>= -1e-12) comparisons.
  • Added regression tests covering loose-default acceptance for vertex queries and strict-mode rejection, plus a closure-containment sanity check.

Reviewed changes

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

File Description
src/underworld3/discretisation/discretisation_mesh.py Adds strict kwarg and implements loose vs strict face-containment thresholding; forwards the kwarg through the public wrapper.
tests/test_0820_in_cell_test_loose_semantics.py Adds regression tests to lock in loose-default behavior and verify strict-mode distinction and closure containment.

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

@@ -3247,6 +3247,20 @@
Coordinate array in any physical unit system (will be auto-converted)
@@ -3260,15 +3274,23 @@
inside = numpy.ones_like(cells, dtype=bool)
Comment on lines 3577 to +3584
points : array-like
Coordinate array in any physical unit system (will be auto-converted)
cells : array-like
Cell indices to test
strict : bool, default False
If True, points exactly on a cell face are reported as NOT in the
cell (useful when uniqueness matters). If False, points on the
closure of a cell count as in it (natural for FE evaluation).
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