Skip to content

Thin-volume fault zones + the 2-D fill delegated to gmsh (walk retired) - #519

Merged
lmoresi merged 4 commits into
developmentfrom
feature/thin-volume-fault
Aug 10, 2026
Merged

Thin-volume fault zones + the 2-D fill delegated to gmsh (walk retired)#519
lmoresi merged 4 commits into
developmentfrom
feature/thin-volume-fault

Conversation

@lmoresi

Copy link
Copy Markdown
Member

One development sweep, per the design ruling: land the finite-width fault representation and remove the dead end it exposes.

place_thin_volume — the embedded thin-volume fault mesh (new)

Each fault patch is thickened by ±width/2; the network's volumes are resolved against one another with OCC fragment (the only junction resolver, and CAD-only); the assembly is meshed standalone at layer scale; and the meshed assembly is embedded whole into the existing mesh — cavity carved, annular gap filled by gmsh with the assembly's boundary skin as an interior HOLE, both constraint surfaces discrete and verbatim. Junctions get no geometric treatment: they are ordinary cells of the union, and the rheology decides.

  • Zone CELLS carry (label, value); the skin's faces carry (label_skin, value) — deliberately separate, because a cell-bearing stratum is a volume label and invisible to the interface machinery; the skin label is what makes later surgery hold clear of the zone (asserted both ways in the tests).
  • Sub-h widths measured and supported (w = h/4 passes every gate; V = 2 edot w makes the width constitutive).
  • 3-D is serial AND parallel through place_sheet's gather-first skeleton; results bit-identical np = 1..4; every refusal collective.
  • 2-D is the ribbon: one mitre-joined outline polygon per polyline. Per-segment quads fragmented together were measured to sliver at every kink (inner-side overlap lens meshes at 2.3 degrees); one outline has no internal seam to lens (kinked trace: 27 degrees).
  • New carve machinery for fat objects: reach is a length max(clearance*h, 0.6*width), and the cavity around a volume can PINCH — the drop set grows at non-manifold shell edges (3-D) / pinch vertices (2-D) until the boundary is simple.

Measured basis: thin_volume_spike.py in the mesh_reconnection_study (widths h, h/2, h/4; junction angles 90/45/20/10 degrees, zero slivers at every angle — glancing junctions cost cells, not quality).

The 2-D fill delegated to gmsh; the walk retired (dead-end removal)

The 2-D placed surface now fills its cavity the way the 3-D sheet always has: cavity ring as a discrete curve (verbatim), the surface's chain embedded, chain ends sharing the ring's own node tags where the surface meets the cavity boundary. Gated per call: zero moved nodes, every input segment an edge of the fill, orientation checked. Measured basis: gmsh_2d_fill_spike.py (boundary + sub-h hole + free-end embed in one fill) and gmsh_2d_ends_spike.py (crossing and mixed end-on-boundary).

Retired: the traverse parameter, the angle-fan tip, the zip walk with its ear-clipping third move — ~420 lines whose hardest cases are gmsh's ordinary free-end embed and boundary recovery. A new precondition check refuses, with the cause, a surface whose points fall outside the cavity cleared for it (previously the walk wedged and said so; gmsh would have said "moved 37 nodes").

Tests and contract changes

  • tests/test_0855_place_thin_volume.py (10 serial: 3-D crossing pair, sub-h, zone-vs-sheet both ways, wall/planarity/kink/width refusals, 2-D X junction, kink quality, second-zone safety) and tests/parallel/ptest_0855_place_thin_volume_parallel.py (np = 2..4: serial contract + collective refusals). Test number 0855 claimed in the coordination ledger.
  • test_0853 (the 2-D contract, 20 pass): two legitimate updates — the 3-D refusal now points at place_sheet instead of explaining the walk's limits, and the repair test presses a tight clearance for its material and SKIPS loudly when the fill leaves nothing under 15 degrees (the gmsh fill is well-shaped natively; asserting an improvement of nothing is the vacuous pass that test's own docstring warns against). One test in test_0854 matched the old refusal wording.
  • docs/developer/subsystems/conforming-surfaces-and-fault-zones.md brought up to date (it still described a serial, 2-D-only, walk-filled capability).

Net: +1.6k/−0.5k lines, of which the deletions are the walk and the stale doc text. Shared file place_surface.py changes only via this PR to development, per the coordination ledger rules; the ledger carries a note for the contact session (walk internals no longer exist as patch targets).

Underworld development team with AI support from Claude Code

The 2-D placed surface now fills its cavity the way the 3-D sheet always
has: the cavity ring goes to gmsh as a discrete curve carrying its exact
segmentation, the surface's chain as an embedded discrete curve whose end
nodes are shared with the ring where the surface meets the cavity boundary
(a crossing, or an end at the wall) and free at a tip. The delegation is
gated, not trusted — zero moved nodes, every input segment an edge of the
fill, anticlockwise triangles — and refused rather than accepted degraded.
Measured basis: gmsh_2d_fill_spike.py (boundary + sub-h hole + free-end
embed in one fill) and gmsh_2d_ends_spike.py (crossing and mixed
end-on-boundary), both in ~/+Simulations/mesh_reconnection_study/.
Retired with it: the arc-length traverse parameter, the angle-fan tip, the
zip walk with its ear-clipping third move, and the flank parameter — some
420 lines whose hardest cases (the tip, the wall-collinear sliver) are
gmsh's ordinary free-end embed and boundary recovery. One fill mechanism
for both dimensions.
A new precondition check refuses, with the cause, a surface whose points
fall outside the cavity cleared for it (cells held for an earlier surface):
previously the walk wedged and said so; gmsh would have said "moved 37
nodes". The repair test now presses a tight clearance for its material and
skips loudly when the fill leaves nothing under 15 degrees — the gmsh fill
is well-shaped natively, and asserting an improvement of nothing is the
vacuous pass the suite's own docstring warns against. The 3-D refusal
message now points at place_sheet rather than explaining the walk's limits.
Contract: tests/test_0853 (20 pass, 1 principled skip), test_0854 and
test_0844 green. n_fill_points joins the info dict for the interior points
a fill may insert; the chain identity n_surface_facets == n_placed +
n_on_surface - 1 is unchanged.
Underworld development team with AI support from Claude Code
The finite-width fault representation, Louis's design ruling made a mesh
primitive: each patch is thickened by +-width/2, the thickened volumes of a
network are resolved against one another with OCC fragment — the only
junction resolver, and CAD-only — the assembly is meshed standalone at layer
scale (sub-h widths measured and supported; V = 2 edot w makes the width
constitutive), and the meshed assembly is embedded WHOLE into the existing
mesh: a cavity is carved around it and gmsh fills the annular gap with the
assembly's boundary skin as an interior HOLE, both constraint surfaces
discrete and verbatim. Junctions get no geometric treatment: they are
ordinary cells of the union and the rheology decides.
In the result the layer's CELLS carry (label, value) — the zone exists to
hand cells to the rheology — and the skin's faces carry (label_skin, value),
deliberately separate: a stratum containing cells is a volume label,
invisible to the interface machinery, and it is the skin label that makes a
later placement hold its cavity clear of the zone (asserted: a sheet through
the zone refuses naming the cause; one placed clear embeds with the zone
intact).
3-D is serial AND parallel through place_sheet's gather-first skeleton: the
assembly is meshed once (rank 0) and broadcast, the region gathers to one
rank, every rank rebuilds collectively; results are bit-identical np=1..4.
2-D is the ribbon — one MITRE-JOINED outline polygon per polyline, after
measuring that per-segment quads fragmented together sliver at every kink
(the inner-side overlap lens meshes at 2.3 degrees; one outline has no
internal seam to lens — kinked-trace min angle 27 degrees). Serial, sharing
place_along_lines' scope.
New carve machinery for FAT objects, both dimensions: the reach is a length
max(clearance*h, 0.6*width) so sub-h layers are covered, and the cavity
around a volume can PINCH — the drop set grows at every non-manifold shell
edge (3-D) or pinch vertex (2-D) until the boundary is simple, the
thin_volume_spike lesson. _gmsh_fill_2d gains hole loops; the annular 3-D
fill is new.
Gates per call, all collective: assembly meshed volume/area exact against
CAD, zero moved constraint nodes, skin not remeshed, every zone cell and
skin facet recovered by join, domain volume/area conserved, global Euler 1,
held surfaces intact. tests/test_0855 (10 serial) and
ptest_0855 (np=2..4); measured basis thin_volume_spike.py in
~/+Simulations/mesh_reconnection_study/.
Underworld development team with AI support from Claude Code
…l and the thin volume
The placement half described the retired walk and a serial-only, 2-D-only
capability. It now describes what exists: the gated gmsh fill (one mechanism,
every dimension, with the walk's retirement recorded in one paragraph), the
parallel 3-D forms (gather-first, partition-independent, collective
refusals), and place_thin_volume — the finite-width zone with junctions in
the volume, which is also the promised lifting of the close-pair limit.
Underworld development team with AI support from Claude Code
CopilotAI lite review requested due to automatic review settings August 10, 2026 04:15
@lmoresi

Copy link
Copy Markdown
MemberAuthor

Adversarial review (self, posted per project practice). What we attacked and what survived:

The kink sliver, and a wrong fix caught by its own numbers. First diagnosis blamed the outer-notch wedge at polyline kinks; the wedge-triangle patch changed NOTHING (min angle 2.263°, min area 8.67e-07 — bit-identical before/after, so the probe never fired). Re-measured on the assembly alone: the sliver is the INNER overlap lens that fragment makes a separate face at every kink. The mitre-joined outline removes the seam entirely: 2.26° → 27.2°, and the fix is asserted by test_a_kinked_ribbon_does_not_sliver.

The label split is load-bearing, not taste. A single (label, value) stratum holding cells AND skin faces would be skipped whole by _interface_faces_3d ("a volume label, not an interface") — the zone would be invisible to the hold machinery and a later placement could eat it silently. Split into label (cells) + label_skin (faces); test_0855 asserts both the point-kinds and, both ways, that a sheet through the zone refuses while one placed clear leaves the zone intact.

Refusals name causes. A surface whose points fall outside its cleared cavity (cells held for an earlier surface) previously wedged the walk with a message saying so; raw gmsh says "moved 37 constrained node(s)". A precondition check now refuses with the cause before gmsh sees the input; the T-junction contract test enforces the wording.

A vacuous pass converted to a loud skip. The repair test's fixture existed to give flips walk-era thin cells to fix; the delegated fill leaves none (min angle 38° on the old fixture). It now presses clearance=0.35 and SKIPS with a reason when there is nothing repair may legally act on — the surface-preservation half still runs.

Residual risks we accept, stated: (1) cell orientation of gmsh output — 2-D is re-oriented explicitly and gated by the inverted-cell check; 3-D relies on gmsh's positive-tet convention exactly as the landed place_sheet does, behind the same volume-conservation gate. (2) The assembly is meshed on rank 0 and broadcast, so cross-rank identity is by construction; np=1..4 measured bit-identical. (3) 3-D patches must be planar — curved manifolds are the fault-prep lifecycle item, refused with the reason meanwhile. (4) _carve_cavity_3d (sheet) and _carve_around_volume_3d share shell-extraction structure and could unify; we left the landed sheet path untouched in this PR deliberately.

Underworld development team with AI support from Claude Code

CopilotAI 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

Adds first-class support for embedding finite-width “thin-volume” fault zones into existing meshes, while simplifying 2‑D surface cavity filling by delegating it to gmsh (retiring the bespoke walk).

Changes:

  • Introduces place_surface.place_thin_volume() for 2‑D ribbons and 3‑D thin volumes (OCC fragment assembly + mesh-then-embed workflow, with separate zone-cell vs skin-face labels).
  • Replaces the 2‑D cavity “walk” fill in place_along_lines() with a gated gmsh constrained triangulation (verbatim constraints, no moved nodes).
  • Adds/updates tests and developer documentation to reflect the new contracts, refusal messages, and parallel behavior.

Reviewed changes

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

Show a summary per file
FileDescription
tests/test_0855_place_thin_volume.pyNew serial test coverage for thin-volume embedding behavior, refusals, label strata split, and quality/area-volume gates.
tests/parallel/ptest_0855_place_thin_volume_parallel.pyNew MPI tests asserting collective refusals and rank-consistent deterministic results.
tests/test_0854_place_sheet.pyUpdates refusal wording expectation to match new messaging.
tests/test_0853_place_surface.pyUpdates repair test expectations/fixture and refusal wording for 3‑D dispatch guidance.
src/underworld3/utilities/place_surface.pyImplements gmsh-gated 2‑D fill, removes retired walk components, and adds place_thin_volume() (2‑D + 3‑D).
docs/developer/subsystems/conforming-surfaces-and-fault-zones.mdUpdates subsystem documentation to describe gmsh fill delegation and the new thin-volume capability/limits.

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

Comment on lines +2500 to +2502
label, label_value : str, int
Label carried by the layer's cells AND the skin's faces.
clearance : float
Comment on lines +2140 to +2145
def _occ_assembly_2d(polylines, width, size):
"""Thicken each polyline segment into a quad, fragment together, mesh.

The 2-D thin volume: a ribbon is the union of one quad per polyline
segment, kinks and crossings resolved by ``fragment`` exactly as the 3-D
junctions are. Returns ``(points, triangles, cad_area)``.
Comment on lines +2514 to +2516
info : dict
Global counts: ``n_zone_cells``, ``n_skin_faces``, ``n_placed``
(vertices added), ``n_removed`` (vertices deleted), ``min_volume``.
CI caught the routine's own Euler gate firing on linux (global Euler 2, not
1) where macOS passed: the cavity growth — and, in the sheet's carve, the
straddle rule — can swallow the WHOLE star of a vertex that is not itself a
victim. Such a vertex sits on no shell face (a shell face keeps a surviving
cell), so it rides through the rebuild referenced by nothing: an isolated
point, chi = 1 + 1. Which vertices get orphaned depends on the drop pattern,
which follows the gmsh assembly mesh — hence version-dependent, hence the
platform split.
The fix is the unconditional invariant, applied in all three carves (thin
volume 3-D and 2-D, and the sheet, whose hazard was latent): after the drop
set is final, any surviving vertex with no surviving cell is promoted to
victim; a wall vertex in that position is a collective refusal. A direct
no-isolated-vertex probe joins test_0855.
Underworld development team with AI support from Claude Code
@lmoresi

Copy link
Copy Markdown
MemberAuthor

Review addendum — CI round 1 found a real defect and the gates did their job.

The linux run failed the routine's OWN collective gate: "the sewn mesh has global Euler number 2, not 1" (macOS passed — the drop pattern follows the gmsh assembly mesh, so the trigger is version-dependent). Mechanism: the cavity growth (and, latently, the sheet carve's straddle rule) can swallow the whole star of a vertex that is not itself a victim; that vertex is on no shell face, so it rides through the rebuild as an ISOLATED point — chi = 1 + 1 = 2.

Fix (2528d0f): the unconditional invariant — after the drop set is final, any surviving vertex with no surviving cell is promoted to victim; a wall vertex in that position is a collective refusal. Applied in all three carves including the landed sheet path, whose hazard was latent behind geometric luck. A direct no-isolated-vertex probe joins test_0855.

Underworld development team with AI support from Claude Code

@lmoresi
lmoresi merged commit eb8d2d3 into developmentAug 10, 2026
2 checks passed
@lmoresi
lmoresi deleted the feature/thin-volume-fault branch August 10, 2026 05:02
lmoresi added a commit that referenced this pull request Aug 10, 2026
…n test
The interface-cone fix (#521, closing #520) verifies independently on
this branch: CheckFaces green at np=2 on the rebuilt mesh, placed-mesh
plain Stokes 24 s (was 3594 s), and — the acceptance that motivated
the whole ladder — the COMPOSED chain place_sheet -> split_fault ->
frictionless contact at np=2 converges in 18 s with the serial answer
(peak slip 0.1461 vs 0.1462) and machine-zero no-opening leak.
ptest_0852 locks that chain as a regression gate: the solve IS the
gate, because both #518-era defects passed every topological check.
Underworld development team with AI support from Claude Code
Sign up for freeto 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

@lmoresi