place_sheet: the 3-D placed surface (serial), with a gated gmsh cavity fill - #517
Conversation
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
There was a problem hiding this comment.
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 tounderworld3.utilities.place_surface. - Add
test_0854_place_sheet.pyto 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.pyto 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) |
| sheet_pts = np.asarray(points, dtype=float).reshape(-1, 3) | ||
| sheet_tris = np.asarray(triangles, dtype=np.int64).reshape(-1, 3) |
| 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
left a comment
There was a problem hiding this comment.
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
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
…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
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.pyuntouched; 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 astest_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