Add exact-boundary quality meshing: tessellation.fill_interior#1800
Conversation
Greptile SummaryThis PR adds
Important Files Changed
Reviews (1): Last reviewed commit: "[pre-commit.ci] auto fixes from pre-comm..." | Re-trigger Greptile |
989fe08 to
8bf53a5
Compare
6f68e04 to
769ee2c
Compare
Mesh-native interior filling: a closed codimension-one boundary Mesh in (2D edge loops; any order/orientation, holes, multiple components, and nested islands resolved automatically), a quality volume Mesh out. Engine: from-scratch constrained Delaunay triangulation (Bowyer-Watson + Sloan segment recovery), even-odd hole removal, Ruppert refinement, and optional bound-preserving ODT smoothing. Contract: input vertices are preserved bit-identically (leading rows, input order), boundary facets are only ever subdivided, every output triangle meets the guaranteed minimum-angle bound, and identical inputs give bitwise-identical outputs. Provenance ships on the output as point_data (boundary_marker, source_point). The contract is dimension-generic; n=3 raises NotImplementedError pending exact boundary recovery. Includes 58 tests, API docs, and tutorial 8 updates.
769ee2c to
109050d
Compare
Restores the pre-commit config to main's version: the ci-skip for import-linter is repo-wide infrastructure and belongs to the maintainers (pre-commit.ci cannot run language-system hooks, so that check will fail until addressed upstream; the import contract remains enforced by the 'Run pre-commit hooks' GitHub Action).
- fill_interior raises a descriptive ValueError for a boundary Mesh with zero edges (previously an opaque RuntimeError from torch.cat). - Loop-nesting containment now probes with polygon_interior_point rather than each loop's first vertex, which could lie exactly on another loop's edge where the even-odd test is arbitrary.
fill_interior no longer claims keys in the user-owned point_data namespace by default; boundary_marker and source_point are attached only when provenance=True. Tests assert the default output has empty point_data; tutorial 8 and docs updated.
- Crossing loops that land in different containment components now raise ValueError (bbox-prefiltered pairwise segment intersection); previously they silently produced a double-covered mesh. Regression test added. - A boundary whose loops all sit at odd containment depth (coincident duplicates) raises a descriptive error instead of a bare torch.cat one. - Docstring: the bit-identical guarantee is scoped to vertices referenced by an edge; removed references to a module that does not exist yet. - Loop nesting now reuses the engine's vectorized _points_in_polygon (one call per loop, 13x faster at 400 loops) and loop extraction uses plain Python adjacency (41x faster at 100k edges); private helpers annotated. - New tests: crossing loops, coincident duplicates, device round-trip, and the boundary-subdivision contract (output boundary edges lie on input segments; total length equals the input perimeter).
|
Hey @peterdsharpe, this is super cool! I worked through the jupyter notebook, which was very helpful. You could consider purging out the metadata for execution counts, times, and some cached paths. With the help of some agentic investigation, there are three things that seem worth flagging for this initial PR. Since I'm not an expert in this code and meshing, I'll lay out some cases here that were flagged by the investigation. It would be great to get a read on if these issues would be meaningful in real-world usage. 2 and 3 seem valid, while 1 seems like a more general issue with 'reflex corners' and meshers in general. 1. Segment recovery fails on simple polygons with reflex-adjacent cornersimport torch
from physicsnemo.mesh import Mesh
from physicsnemo.mesh.tessellation import fill_interior
pts = torch.tensor([[-0.6, -0.3], [-0.4, -0.7], [-0.1, -0.3],
[0.1, -1.0], [0.3, -0.7], [0.5, -0.2]], dtype=torch.float64)
i = torch.arange(6)
fill_interior(Mesh(points=pts, cells=torch.stack([i, (i + 1) % 6], 1)))
# RuntimeError: segment recovery could not find a starting wedge; input
# geometry is too degenerate for float64 predicatesSilent form — another exactly-simple hexagon; the picture shows the import torch
import matplotlib.pyplot as plt
from physicsnemo.mesh.tessellation.delaunay import delaunay_mesh_2d
pts = torch.tensor([[0.97, 0.00], [0.32, 0.09], [0.43, 0.26],
[0.23, 0.44], [0.43, -0.83], [0.84, -0.24]], dtype=torch.float64)
# NOTE: the default min_angle_degrees=30 hangs on this input; 0 isolates the constrained triangulation.
p, t, _, _ = delaunay_mesh_2d([pts], max_area=None, min_angle_degrees=0.0)
x, y = pts[:, 0], pts[:, 1]
exact = float(0.5 * (x * y.roll(-1) - x.roll(-1) * y).sum().abs())
tp = p[t]
meshed = float((0.5 * ((tp[:, 1, 0] - tp[:, 0, 0]) * (tp[:, 2, 1] - tp[:, 0, 1])
- (tp[:, 2, 0] - tp[:, 0, 0]) * (tp[:, 1, 1] - tp[:, 0, 1]))).abs().sum())
fig, ax = plt.subplots()
ax.fill(x, y, color="tab:red", alpha=0.25, lw=0) # red = domain the mesh fails to cover
ax.tripcolor(p[:, 0], p[:, 1], t, facecolors=0.0 * t[:, 0], cmap="Greys", vmin=0, vmax=1)
ax.triplot(p[:, 0], p[:, 1], t, lw=0.4, color="tab:blue")
ax.set_aspect("equal")
ax.set_title(f"exactly simple hexagon: area in {exact:.3f}, meshed {meshed:.3f}, no error")
plt.show()Root cause traced: 2. Component grouping misclassifies valid rectilinear nesting (regression in 4c3be42)import torch
from physicsnemo.mesh import Mesh
from physicsnemo.mesh.tessellation import fill_interior
# 6x6 plate with a centered 5x5 cutout: a valid square annulus.
points = torch.tensor([[0.0, 0.0], [6.0, 0.0], [6.0, 6.0], [0.0, 6.0], # outer loop
[0.5, 0.5], [5.5, 0.5], [5.5, 5.5], [0.5, 5.5]], # hole loop
dtype=torch.float64)
cells = torch.tensor([[0, 1], [1, 2], [2, 3], [3, 0],
[4, 5], [5, 6], [6, 7], [7, 4]])
try:
fill_interior(Mesh(points=points, cells=cells))
except ValueError as e:
print("annulus crash:", e) # torch.cat(): expected a non-empty list of Tensors
# Same plate plus a 1x1 island inside the hole: valid domain, area 36 - 25 + 1 = 12.
points = torch.cat([points, torch.tensor([[2.5, 2.5], [3.5, 2.5], [3.5, 3.5], [2.5, 3.5]],
dtype=torch.float64)])
cells = torch.cat([cells, torch.tensor([[8, 9], [9, 10], [10, 11], [11, 8]])])
m = fill_interior(Mesh(points=points, cells=cells))
print(f"island-in-hole: {m.n_points} points, {m.n_cells} cells")
# island-in-hole: 4 points, 2 cells <- the island alone; 92% of the domain silently droppedCause: 3. Corners sharper than
|
1. Segment recovery: the starting-wedge test checked only that the two wedge vertices straddle the ray LINE, which cannot distinguish the wedge containing the forward ray from the backward one; on sparse reflex polygons it selected backward wedges, and the pipe walk then exited the hull through Python negative indexing, silently dropping domain coverage (or raising, depending on geometry). Wedge selection now requires a proper segment-segment intersection with the wedge's opposite edge and takes the crossing nearest the segment start; the walk hard-fails on hull exit instead of reading garbage. The original suite never caught this because densely sampled smooth boundaries make recovery a no-op: added both reported hexagons and a 25-case random star-polygon property test (pure CDT, shoelace-exact area). 2. Loop-nesting containment probes reverted from interior points to boundary vertices -- ALL of them, so touching/crossing loops surface as a mixed-membership error: an outer square's largest-ear interior point can fall inside its own hole, silently dropping 92% of a valid plate-with-cutout domain. Square-annulus and island-in-hole regression tests with exact area assertions. 3. Input corners sharper than ~60 degrees are rejected up front when a min-angle bound is requested: Ruppert refinement otherwise chases the corner, emitting thousands of sub-float32-area triangles near the apex with no error. A 20-degree wedge test asserts the rejection and that the same input still meshes exactly as a pure CDT. Also scrubs execution metadata from the tutorial notebook.
…pe/physicsnemo into psharpe/fill-interior
|
Thanks @melo-gonzo — all three cases were valid, and this was exactly the review the PR needed. Fixed in 3d256a7: 1. Segment recovery (valid — and worse than it looked). Your root-cause diagnosis was correct: the starting-wedge test checked only that the wedge's two vertices straddle the ray line, which can't distinguish the wedge containing the forward ray from the backward one — your second hexagon showed the walk then exits the hull through Python negative indexing and silently drops coverage. Not a general reflex-corner limitation: a simple polygon always admits a CDT, so this was squarely our bug. Wedge selection now requires a proper segment-segment intersection with the wedge's opposite edge (taking the crossing nearest the segment start, since a segment can properly cross a non-convex link polygon more than once), and the pipe walk hard-fails on hull exit. Your observation about why the suite missed it (densely sampled boundaries make recovery a no-op) is now institutionalized: both hexagons are named regression tests, plus a 25-case random-star-polygon property test asserting shoelace-exact area on pure CDTs. 2. Nesting misclassification (valid — a regression from an earlier review round). Exactly as you said: the largest-ear interior point of a square outer loop legitimately falls inside its own hole. Containment between disjoint curves is now probed with the loops' boundary vertices — all of them, so touching/crossing loops raise a mixed-membership error instead of classifying arbitrarily. Your square-annulus and island-in-hole cases are regression tests with exact area assertions. 3. Sharp corners (valid). The ~60° corner condition was documented as an assumption but never enforced; your 20° wedge shows the failure mode is silent and severe (sub-float32-area triangles). It's now validated: a corner sharper than 60° with an angle bound requested raises with the corner's location and the Also scrubbed the notebook execution metadata as suggested. |
|
/ok to test 3d256a7 |
Mesh generation gets its own dedicated tutorial (part 3 of this series), where fill_interior and the implicit-domain mesher can be taught side-by-side with the choosing-between-them framing; a generation section inside the I/O tutorial was off-topic and would duplicate it.
|
/ok to test d77f87b |
|
/ok to test 2c15c2b |
Applied here since NVIDIA#1800 already merged: tessellation.rst restructured per suggestion (title-case heading, shorter sentences, guarantee list, and the 'hard program' phrasing clarified to 'a substantially harder problem that requires its own implementation effort'), module docstring sentence structure, and the CHANGELOG entry rewording.
PhysicsNeMo Pull Request
Description
Part 1 of 3 (part 2: #1801, implicit-domain mesher; part 3: #1802, tutorial). No code dependency on #1801 — land in either order.
Adds exact-boundary quality mesh generation:
tessellation.fill_interiortakes a closed codimension-one boundaryMeshand fills the enclosed interior with quality simplices.Meshin — loops in any order/orientation; holes, multiple components, and islands-inside-holes resolved automatically by containment — and a volumeMeshout, on the input's device/dtype.provenance=True,point_data["boundary_marker"]andpoint_data["source_point"]map output vertices back to input vertices, enabling per-boundary BCs and data propagation. Off by default — the function claims no keys in the user-ownedpoint_datanamespace unless asked.n=3raisesNotImplementedErrorpending exact 3D boundary recovery.Checklist
Dependencies
No new dependencies.
Review Process
All PRs are reviewed by the PhysicsNeMo team before merging.
Depending on which files are changed, GitHub may automatically assign a maintainer for review.
We are also testing AI-based code review tools (e.g., Greptile), which may add automated comments with a confidence score.
This score reflects the AI’s assessment of merge readiness and is not a qualitative judgment of your work, nor is
it an indication that the PR will be accepted / rejected.
AI-generated feedback should be reviewed critically for usefulness.
You are not required to respond to every AI comment, but they are intended to help both authors and reviewers.
Please react to Greptile comments with 👍 or 👎 to provide feedback on their accuracy.