Skip to content

place_sheet: the 3-D placed surface (serial), with a gated gmsh cavity fill - #517

Merged
lmoresi merged 1 commit into
developmentfrom
feature/mesh-surgery-3d
Aug 9, 2026
Merged

place_sheet: the 3-D placed surface (serial), with a gated gmsh cavity fill#517
lmoresi merged 1 commit into
developmentfrom
feature/mesh-surgery-3d

Conversation

@lmoresi

@lmoresi lmoresi commented Aug 9, 2026

Copy link
Copy Markdown
Member

The 3-D single-cut surface on an existing mesh — the capability the fault streams were waiting on. A triangulated sheet's points become mesh vertices and every sheet triangle a labelled interior face, with the rim free inside the mesh: the case a cut cannot represent and a two-sub-volume construction cannot fake.

Mechanism

Carve (clearance + crossed tets, every victim's whole star), fill, sew on a compacted vertex set, carry every label topologically (vertex numbering through the rebuild is defined, so faces and edges transfer by vertex tuple — coordinates are never consulted). The fill is delegated to gmsh — a cavity shell has no total order to walk, and constrained tetrahedralisation with Steiner insertion is what a mesh generator is for — and gated, not trusted: both constraint surfaces bit-identical, every sheet triangle an interior face, conformity / Euler / degeneracy / wall-label preservation checked per call.

Measured basis: sheet_cavity_spike.py (study dir), 6/6 across tilt, resolution, clearance and sub-h sheet spacing — gmsh inserting its own Steiner points without touching either constraint, which retires the plan's constrained-tetrahedralisation STOP-gate.

Guards, ported from the 2-D lessons

Cells owning an interior labelled face are held out of the cavity; a sheet needing them is refused; every earlier surface's interior-face count is re-read off the result mesh (a partial corruption satisfies any identity summed over per-placement bookkeeping — learned the hard way in 2-D).

Scope

Serial by design: the parallel route is gather-first (redistribute so the sheet's star is rank-interior — the contact stream's measured policy — then this operation is the rank-local step, and no placed point is ever shared). reconnect.py untouched; the chart-surgery generalisation belongs to the parallel stage. Test number 0854 claimed in the coordination ledger.

Handoff

The deliverable contract for split_along_label_3d (fault-split-node): a labelled facet patch — every sheet triangle a face under one (name, value), interior with two cells, an open manifold with single rim edges — asserted as test_0854 (5 tests). Full surgery regression: 77 tests green (0844 x2, 0853, 0854); style gate clean.

Underworld development team with AI support from Claude Code

The 3-D form of the placed surface. The sheet's points become mesh vertices
and every sheet triangle becomes a labelled interior face, with the rim free
inside the mesh - the case a cut cannot represent and a two-sub-volume trick
cannot fake. Carve (clearance plus crossed tets, every victim's star), fill,
sew (createFromCellList on a compacted vertex set), and carry every label
topologically: vertex numbering through the rebuild is DEFINED, so faces and
edges transfer by vertex tuple and coordinates are never consulted.

The fill is delegated to gmsh and GATED, not trusted: the cavity shell and
the sheet must come through bit-identical, every sheet triangle must be an
interior face of the sewn mesh, and conformity, Euler number, degeneracy and
the wall labels are checked on every call. Measured basis: the cavity spike,
6/6 across tilt, resolution, clearance and sub-h sheet spacing - including
gmsh inserting its own Steiner points without touching either constraint,
which retires the plan's constrained-tetrahedralisation STOP gate.

The 2-D guards are ported, faces standing where edges stood: cells owning an
interior labelled face are held out of the cavity, a sheet needing them is
refused, and every earlier surface's interior-face count is re-read off the
RESULT mesh - the lesson that a partial corruption satisfies any identity
summed over per-placement bookkeeping.

Serial by design: the parallel route is gather-first (redistribute so the
sheet's star is rank-interior, place locally, renumber), after which this
serial operation is the rank-local step and no placed point is ever shared.
reconnect.py is untouched; the chart-surgery generalisation belongs to that
stage.

Deliverable contract for the fault module: a labelled facet patch - every
sheet triangle a face under one (name, value), interior with two cells,
an open manifold with its rim edges single - asserted as test_0854.

Underworld development team with AI support from Claude Code
Copilot AI lite review requested due to automatic review settings August 9, 2026 21:59

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 introduces a new 3‑D “placed surface” capability (place_sheet) that embeds a triangulated sheet into an existing 3‑D simplex mesh by carving a cavity, delegating the cavity fill to gmsh, and then sewing the result back while preserving labels topologically. It also adds a dedicated regression test suite establishing the contract expected by downstream fault-splitting functionality.

Changes:

  • Add place_sheet() and supporting 3‑D cavity-carve / gmsh-fill / sew / label-transfer helpers to underworld3.utilities.place_surface.
  • Add test_0854_place_sheet.py to assert the 3‑D placed-sheet contract (interior labelled faces, open-manifold rim, label preservation, and dimension-dispatch guards).
  • Expand module “Scope” documentation in place_surface.py to reflect 3‑D support and the gated gmsh fill approach.

Reviewed changes

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

File Description
src/underworld3/utilities/place_surface.py Implements 3‑D sheet placement with gated gmsh cavity fill, sewing, and topological label transfer.
tests/test_0854_place_sheet.py Adds contract/regression coverage for the new 3‑D place_sheet() behavior and key guards.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

def _interior_face_counts_3d(dm):
"""{(label, value): count of interior faces} — the breach detector."""
interface = _interface_faces_3d(dm)
fS, fE = dm.getHeightStratum(1)
Comment on lines +1535 to +1536
sheet_pts = np.asarray(points, dtype=float).reshape(-1, 3)
sheet_tris = np.asarray(triangles, dtype=np.int64).reshape(-1, 3)
Comment on lines +1577 to +1583
keep_cell = np.ones(len(cells), dtype=bool)
keep_cell[drop_ids] = False
keep_vertex = np.ones(len(X), dtype=bool)
keep_vertex[victims] = False
old_to_new = -np.ones(len(X), dtype=np.int64)
old_to_new[keep_vertex] = np.arange(int(keep_vertex.sum()))
n_survivors = int(keep_vertex.sum())

@lmoresi lmoresi left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review (authoring session).

Where we attacked it: (1) The fill's trust boundary — gmsh output is gated per call (constraint surfaces bit-identical, sheet triangles all interior faces, conformity/Euler/degeneracy/wall counts); the inert-DB-write and misattributed-crash lessons from #515/#510 are why nothing here trusts a library return without reading the result back. (2) The silent-corruption class from 2-D — both cells of a labelled facet cleared while every corner is protected — is guarded with faces standing where edges stood, and the too-close-sheet test accepts either refusal path (held-cell or shell-manifold) while asserting the first sheet is intact afterwards, since which guard trips first is geometry-dependent. (3) Label transfer is topological (vertex tuples through a defined renumbering), never coordinate matching — the property the 2-D rebuilds were built on. (4) Serial-only refusal states the gather-first rationale so the parallel stage has its design recorded at the refusal site.

Known limits, stated rather than discovered later: one sheet per cavity (close pairs are one thin volume — the next work item); no cavity-growth retry loop in 3-D yet (a wedged fill raises with guidance rather than growing, unlike 2-D); test_0854 asserts the handoff contract shape only — the wire-up to split_along_label_3d runs on the fault branch, per the ledger.

Recommend merge (squash — single-purpose branch, no shared ancestry).

Underworld development team with AI support from Claude Code

@lmoresi
lmoresi merged commit 7174b6d into development Aug 9, 2026
3 checks passed
lmoresi added a commit that referenced this pull request Aug 13, 2026
adapt-on-top-faults: the rotated free-slip path now picks up an adapt
child's mesh-owned MG tail automatically (custom_mg.build_transfers,
the #467 fix) — the "FUNDAMENTAL, not a quick fix" gotcha row was
describing a bug that is gone. Point the band-sizing section at the
interface-alignment primitives that now exist (place_sheet /
place_thin_volume / remove_embedded, #517-#526).

nonlinear-solver: three stale claims corrected — rotated free-slip is
no longer the exception to the automatic tail pickup (#467); the adapt
tail is one MG level per DOUBLING of h, not per refinement generation
(mg_coarsening_ratio=2.0, #515); refinement=0 still yields a hierarchy
that starts at the base, so "no coarse grid" overstated it. Three
capabilities that landed since the branch: preconditioner="gamg" is
respected on adapt children (#530), single-field FMG (#478/#534), and
solver.pc_fallbacks as the observability hook (#534). The supersession
note no longer names the removed in-SNES ramp API.

Underworld development team with AI support from Claude Code
lmoresi added a commit that referenced this pull request Aug 13, 2026
…current; plasticity-solvers rewritten (#454) (#489)

* skills: parallel adapt engines, band pinning, and FMG on adapt children

adapt-on-top-faults
  - engines section: nvb vs edge_split, both parallel in 2-D and 3-D and
    bit-confluent; edge_split has no conforming closure so refinement cannot
    escape the marked region, and marks on the DIAMETER (the volume proxy
    reported the target met while the mesh was 3.2x coarser across the feature).
  - repair=True: gates on reducing the largest angle, NOT on Delaunay. Delaunay
    maximises the minimum angle while P1 depends on the maximum, and flipping a
    gmsh mesh toward Delaunay raised the 99th-percentile max angle. Worth it on a
    poor base (156 -> 115 degrees, slivers 3.84% -> 0.00%), marginal on a clean
    one, and it gives up bit-confluence, so it is opt-in.
  - relax on a mesh refined onto an interface makes things WORSE (+77% leak);
    pin_bands is the fix.
  - new section on sizing the band and representing the fault margin: the
    -2 Cov(eta, edot) leak metric, why a within-cell marking rule loses to the
    plain distance size field, why the optimal band width depends on which
    objective you pick, and what a step-edged margin buys and costs.
  - gotchas: Mesh(dm) takes the DM over (bare SIGSEGV if you keep using the old
    handle); Mesh(dm) without boundaries= loses the boundary enum; evaluate()
    "Total components 8 != 6" on a variable-heavy mesh.

adaptive-meshing
  - PIN THE INTERFACE section for relax(pin_bands=...), including the
    signed-vs-unsigned distance rule and the pinned_labels merge trap.
  - cross-reference to nonlinear-solver for the FMG setup.

nonlinear-solver
  - new section: FMG on an adapt-on-top child. The child carries its own graded
    custom-P tail and solvers pick it up automatically; the base must have
    refinement>=1; a base-only tail triples the V-cycle count; V-cycle counts are
    insensitive to element quality (a pass, not a failed measurement) so use GAMG
    as the quality probe; relax can trip #424 into the dense RBF fallback;
    repair invalidates the any-degree transfer but not the vertex prolongation.
  - cross-references to adapt-on-top-faults and adaptive-meshing.

Underworld development team with AI support from Claude Code

* skills: bring the two adapt/solver skills current with development

adapt-on-top-faults: the rotated free-slip path now picks up an adapt
child's mesh-owned MG tail automatically (custom_mg.build_transfers,
the #467 fix) — the "FUNDAMENTAL, not a quick fix" gotcha row was
describing a bug that is gone. Point the band-sizing section at the
interface-alignment primitives that now exist (place_sheet /
place_thin_volume / remove_embedded, #517-#526).

nonlinear-solver: three stale claims corrected — rotated free-slip is
no longer the exception to the automatic tail pickup (#467); the adapt
tail is one MG level per DOUBLING of h, not per refinement generation
(mg_coarsening_ratio=2.0, #515); refinement=0 still yields a hierarchy
that starts at the base, so "no coarse grid" overstated it. Three
capabilities that landed since the branch: preconditioner="gamg" is
respected on adapt children (#530), single-field FMG (#478/#534), and
solver.pc_fallbacks as the observability hook (#534). The supersession
note no longer names the removed in-SNES ramp API.

Underworld development team with AI support from Claude Code

* skills: rewrite plasticity-solvers around what the yield campaigns measured

Fixes #454. The skill taught the retired yield-homotopy doctrine in
~8 places, headlined by an enable method that no longer exists in src/
and an in-SNES delta-ramp that is separately proven to diverge. The
doctrine rested on a unit-scaling error: re-measured on the corrected
problem, the delta-march never succeeded where a direct hard-Min solve
failed, and the ruling was to regularise the problem, not the solver.

Rewritten around the evidence: Newton with the automatic Picard entry
(Picard is an entry requirement, not an accelerator), rescue on failure
OR stagnation, grid sequencing as the validated warm start, the per-model
tangent table, and the #475 yield_mode / yield_smoother / yield_anchor
substrate presented as a modelling choice with the multi-solve-only
discipline for any delta march. yield_continuation is described honestly
per open issue #473 (the cold-start guarantee does not hold on a
Piecewise yield stress; the step control is effectively one-shot).
Floors updated to the post-#475 semantics (viscosity_min_rounding).
Kept: the Newton-confirmation check, the VEP-indefinite ruling, the
SNESFAS ruling, the Picard-folklore footnote.

Underworld development team with AI support from Claude Code

* The nonlinear-solver recipe demotes the delta-march to a rescue

The recipe's step 2 still presented solve(homotopy=True) as the default
entry point ("one call - automatic"), which is the retracted doctrine in
its post-API form: the evidence that recommended a delta-march first
rested on a unit-scaling error, and the driver's cold-start guarantee is
broken (issue #473). The step now escalates honestly - grid sequencing
first, the delta-continuation as rescue of last resort with the #473
caveat stated - and the Layer-2 status note carries the same demotion.
The delta-discipline itself (constant per solve, never in-SNES) is
unchanged; plasticity-solvers holds the ruling and evidence.

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