Skip to content

Add Tutorial 9: mesh generation (stacked on #1800, #1801)#1802

Draft
peterdsharpe wants to merge 60 commits into
NVIDIA:mainfrom
peterdsharpe:psharpe/mesh-generation-tutorial
Draft

Add Tutorial 9: mesh generation (stacked on #1800, #1801)#1802
peterdsharpe wants to merge 60 commits into
NVIDIA:mainfrom
peterdsharpe:psharpe/mesh-generation-tutorial

Conversation

@peterdsharpe

@peterdsharpe peterdsharpe commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

PhysicsNeMo Pull Request

Description

Note

Stacked PR — draft until its bases merge. This branch is stacked on #1800 (tessellation.fill_interior) and #1801 (generate.mesh_implicit_domain); their diffs appear here until they land. Once both merge, this rebases to a docs-only diff (one tutorial notebook + README + docs-overview edits). Review only the last commit (Add Tutorial 9) until then.

Part 3 of 3. Adds examples/minimal/mesh/tutorial_9_mesh_generation.ipynb: a visual, executable guide to the mesh-generation tools and how to choose between them.

  • Filling multiply-connected boundary meshes exactly, with per-boundary provenance (a gear + star hole + island from one edge Mesh)
  • Verifying the minimum-angle guarantee across resolutions
  • Implicit-domain galleries: CSG, raw (non-SDF) level sets, near-tangent unions; the coverage guard
  • Pinning sharp corners with feature_points (side-by-side with the default rounding)
  • Tetrahedralizing 3D implicit domains
  • Differentiable meshing: dV/dr through the mesh vs the analytic perimeter
  • Also lists mesh generation as a first-class workflow in the mesh docs overview

differentiable meshing: autograd shape gradients vs analytic

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.

@copy-pr-bot

copy-pr-bot Bot commented Jul 8, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@peterdsharpe peterdsharpe force-pushed the psharpe/mesh-generation-tutorial branch from 1d6eb81 to e422022 Compare July 8, 2026 15:40
@peterdsharpe peterdsharpe requested a review from melo-gonzo July 8, 2026 16:21
@peterdsharpe peterdsharpe force-pushed the psharpe/mesh-generation-tutorial branch 2 times, most recently from b622db8 to 6d48224 Compare July 8, 2026 17:01
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.
Dimension-generic simplex mesh generation for implicit domains
{x : phi(x) < 0} — SDFs, level sets, neural fields — in 2D/3D/ND on CPU
or CUDA, in pure PyTorch tensor ops.

Pipeline: Kuhn-lattice initialization, validity-gated ODT smoothing with
boundary projection, quality-greedy bistellar flips (Radon-partition
formulation; no exact arithmetic, pure torch), pinch splitting and boundary-pancake peeling.
Structurally robust: every update is gated and the budget fixed, so a
valid, watertight mesh is always returned; difficult inputs degrade
quality (reported in diagnostics), never existence. A coverage guard
raises when the domain has features below the target edge length instead
of dropping them silently; feature_points pins sharp corners exactly via
topological insertion. refit_mesh_to_implicit re-projects the boundary
onto phi=0 with graph-preserving Newton steps at fixed topology, so
d(mesh)/d(shape parameters) flows to autograd. Measured on an RTX 4090:
738k tets end-to-end in ~6-10 s, valid, 2.7% slivers.

Includes 68 tests and API docs with a 'choosing a
mesher' comparison.
A visual, executable guide to the mesh-generation tools: exact-boundary
filling with provenance (gear/star/island domain), the minimum-angle
guarantee verified across resolutions, implicit-domain galleries (CSG,
raw level sets, near-tangent unions), corner pinning with
feature_points, a 3D tet-meshed shell cutaway, and differentiable
meshing (dV/dr through the mesh vs the analytic perimeter). Also lists
mesh generation as a first-class workflow in the mesh docs overview.
@peterdsharpe peterdsharpe force-pushed the psharpe/mesh-generation-tutorial branch from d481b9b to 8371ebb Compare July 8, 2026 17:24
pre-commit-ci Bot and others added 18 commits July 8, 2026 17:25
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).
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).
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.
- _coverage_gap processes probe rows in chunks of 256, bounding the
  distance-matrix allocation that could exceed GPU memory on fine meshes.
- sdf_polygon_2d now honors the documented (..., d) implicit-function
  contract by flattening and restoring leading batch dimensions.
- Fix a doctest that built a one-element tuple instead of the tensor.
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).
- Coverage guard judges projection convergence in DISTANCE units
  (|phi|/|grad phi| < 0.05h) instead of raw |phi|, scales its probe count
  with the box-to-h ratio (capped), and returns +inf — a guard failure —
  when no probe converges; a quantized/noisy level set can no longer
  silently disable it. Regression test with float-quantized phi.
- pin_feature_points gains vertex-coincidence and on-facet-split cases,
  and tents are half-space-tested against the owner cell so they can
  never overlap the mesh (previously undetectable). Tests added.
- phi-unit thresholds (erosion, peel, escape band) are normalized by the
  sampled gradient magnitude near the zero set: phi = c*sdf now behaves
  identically for any c > 0, matching the documented contract. Scale
  regression test at c = 1e-3, 1, 1e3.
- Escape-band cache fully refreshed every reconnect_every iterations,
  bounding drift staleness.
- q_p01 diagnostic uses kthvalue (torch.quantile hard-fails above 2^24
  cells); flip volume-conservation tolerance is dtype-aware (a fixed
  1e-9 rejected ~11% of legitimate float32 flips); flip priorities are
  float64 with a deterministic index tie-break.
- Typed overloads for full_output (Literal[True/False]); pinned vertex
  indices returned in diagnostics; dead code removed; bounds-tightness
  memory note documented.
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.
In 2D both Radon classes have size k_share, so identifying the current
configuration by class size always resolved the tie to the negative SVD
sign class -- whose sign is arbitrary -- and proposed the identity
retriangulation (zero gain, silently rejected) for about half of all
improving 2-2 flips, deterministically per geometry: affected quads
never improved regardless of seed. Identify the target class by
membership instead -- the sub-simplex shared by all current cells,
whose vertices generate the new cells by removal -- keeping the Radon
sign pattern purely as a validity filter. This also covers the 4D 3-3
tie; 3D decisions are unchanged (the sign rule and the membership rule
provably coincide when the class sizes differ, and end-to-end 3D
quality/flip counts are identical).

Found by adversarial review on NVIDIA#1801 (242/477 improving diagonal flips
silently missed in the reviewer's harness; 0/477 after this change).
A zero-length segment (repeated consecutive vertex, common in real
polygon data) made the point-to-segment parameter t = 0/0 = NaN, and
the min over segments then poisoned EVERY query point -- downstream,
mesh_implicit_domain misdiagnosed the polygon as a NaN neural field.
Clamp the denominator so a degenerate segment reduces to
distance-to-point; collinear vertices were already handled.

Found by adversarial audit of NVIDIA#1801.
…rd failures

Fixes for the adversarial review findings on NVIDIA#1801, plus hardening
found while stress-testing the fixes:

- Box-touching domains silently detached from the box faces: face
  vertices Newton-projected onto the only zero set the raw phi has
  (the interior one), so a "box minus obstacle" external-flow domain
  lost 10-48% of its volume with zero face vertices left and the
  coverage guard blind to it. The meshed set is now explicitly
  {phi < 0} intersected with the box: phi is clipped by the box's own
  SDF (scaled into phi's gradient units), making the faces first-class
  zero set, and face vertices are pinned per-coordinate so the box's
  own edges and corners stay exact instead of chamfering at the h
  scale. refit_mesh_to_implicit gains an optional bounds= argument for
  the same case. Measured: half-plane area 1.740 -> 1.992 of 2.0
  (residual is domain-corner rounding, closable with feature_points);
  box-minus-disk 2D loss 9.9% -> 0.0%; box-minus-sphere 3D 25% ->
  0.1%; 733k-tet GPU benchmark unchanged in wall time and quality.

- The coverage guard blessed unmeasurable input: probes landing in a
  NaN phi region (a neural field queried outside its training range)
  were silently dropped from the gap max, reporting near-perfect
  coverage of a half-missing domain, and a gradient-free phi (step
  function) took the no-gradient early return of 0.0 for a staircase
  boundary. Failure to measure is now failure: NaN at any probe and
  no-gradient-with-a-sign-change both return +inf, and a Monte-Carlo
  volume cross-check (from the probe signs; census always 65536
  probes) catches covered-but-hollow meshes -- the two-sided
  complement to the zero-set gap. Stalled projections are retried at
  4x iterations, then dropped rather than failed: a probe oscillating
  in a positive CSG dead zone (e.g. between an obstacle and the box)
  certifies nothing and hides nothing, and hard-failing on it rejects
  in-contract composites.

- The validity gate classified NaN cell volumes as good (every
  comparison is False for NaN), letting a NaN projection target
  replace a valid vertex with the guard disabled.

- Boundary projections that fail to land near the zero set now anchor
  their vertex: with a discontinuous or plateaued phi the Newton step
  strands vertices in regions with no zero set, and the unanchored
  staircase then creeps outward. Erosion uses the unclipped phi -- the
  lattice conforms to the box exactly, and the box term spuriously
  eroded whole face layers whenever ceil-rounding made the lattice
  spacing smaller than nominal h.

- Peeling can expose a boundary pinched at a vertex, which the
  ridge-pairing manifoldness check cannot see (every boundary ridge
  still pairs); every peel is now followed by a pinch split, keeping
  the documented closed-manifold contract.
Comment thread docs/api/mesh/generate.rst Outdated
Comment thread docs/api/mesh/generate.rst Outdated
Comment thread docs/api/mesh/generate.rst Outdated
Comment thread docs/api/mesh/generate.rst Outdated
Comment thread docs/api/mesh/generate.rst Outdated
Comment thread physicsnemo/mesh/generate/__init__.py Outdated
Comment thread physicsnemo/mesh/generate/__init__.py Outdated
Comment thread physicsnemo/mesh/generate/__init__.py Outdated
Comment thread CHANGELOG.md
Comment thread CHANGELOG.md
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.
torch.cdist's matmul path computes |x - y| as sqrt(x^2 + y^2 - 2xy),
whose cancellation returns ~1e-8 for IDENTICAL points; the compute-mode
heuristics changed across torch versions, so the feature-point
exact-interpolation assertions passed on torch 2.12 locally and failed
on 2.15 in CI (off by exactly 2^-27). The mesher itself is unaffected --
its coincidence detection already used direct subtraction, and its
argmin uses of cdist are residue-tolerant. Assertions tightened to
1e-12 now that the measurement is exact.
Comment thread docs/api/mesh/generate.rst Outdated
Comment thread docs/api/mesh/generate.rst Outdated
Comment thread docs/api/mesh/generate.rst Outdated
Comment thread docs/api/mesh/generate.rst Outdated
Comment thread docs/api/mesh/generate.rst Outdated
@peterdsharpe

Copy link
Copy Markdown
Collaborator Author

/ok to test d9c5bd8

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