The finite-width 3-D fault network: one band, both realisations - #664
Conversation
build(width=...) in 3-D is the 2-D contract one dimension up: the
margin-expanded patches are thickened into ONE fused band (junctions
free) with each un-expanded patch embedded as a conforming mid-surface
(_occ_assembly_3d gains embed=, place_thin_volume a 3-D
mesher="network"), so the same mesh is cut and split
(realisation="split") or left whole for the weak plane ("ti").
Honoured footprints are exact for planar patches, with nearest-plane
ownership so a junction cell carries ONE director; the 3-D TI director
is the patch normal; slips() gains the plane gauge — the in-plane
velocity jump across the layer — validated exactly on a prescribed
shear.
The junction gap is physics and the margin convenience: a margin that
reaches within half a cell of the ligament welds the expanded bands at
the junction line and the split degenerates into self-paired nodes
(measured: a bit-frozen residual floor at rel 8.8e-3, identical under
LU, with machine-zero leak — the constraint row is identically zero).
build() refuses that configuration, and a collective post-split check
raises on any surviving self-pair as a defect. The mid-surface
labelling judges its refusal on the GLOBAL face count: gather-first
placement leaves peer ranks legitimately empty, and the rank-local
raise it replaced was a hang, not an error.
Loud refusals: outcropping bands (the embedded mid-surface cannot yet
be clipped against the boundary), base= on the 3-D width path,
non-network meshers with width, embed= outside mesher="network", and
junction_cells in 3-D (the junction glue there is damage_yield's
tubes).
Tests: test_0863 (one mesh two realisations, footprint/director
oracles, gauge arithmetic, refusals, end-to-end split+TI solve) and
ptest_0863 (np=2 build/split/solve to the serial answer on a
distributed mesh). docs/advanced/fault-networks.md 3-D section
rewritten for the width contract.
Underworld development team with AI support from Claude Code
A healthy pairing is a bijection between disjoint sides; a node appearing as both a minus and a plus (a chain) is the same degeneracy as a self-pair and is now caught by the same collective check. Underworld development team with AI support from Claude Code
|
Adversarial review (posted by the authors' session). What we tried to break, and what held:
Known limitations we chose to note rather than fix here:
Underworld development team with AI support from Claude Code |
There was a problem hiding this comment.
🟡 Changes recommended
_expand_convex_polygon() can divide by zero for convex polygons with collinear consecutive edges, which can propagate NaNs into the 3‑D band geometry build.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Implements the finite-width 3‑D fault-network “one band, both realisations” contract: a single fused volumetric band mesh is built once, then either split into contact faults (realisation="split") or used as an unsplit weak plane ("ti"), with updated geometry/footprint/director and slip-gauge behavior. This extends the 2‑D width behavior to 3‑D networks and adds serial/parallel tests plus documentation.
Changes:
- Add 3‑D width-band build path for
FaultNetwork.build(width=..., realisation=...), including embedded conforming mid-surfaces and post-split self-pair defect checks. - Extend band placement (
place_thin_volume) with embedded mid-surface support and make mid-surface labelling collective-safe in parallel. - Add serial and
np=2parallel tests for shared-mesh realisations, director/footprint/gauge oracles, and refusal cases; update the advanced docs.
File summaries
| File | Description |
|---|---|
| tests/test_0863_fault_network_3d_width.py | New serial regression covering shared-mesh split/TI realisations, analytic oracles, refusals, and end-to-end solves. |
| tests/parallel/ptest_0863_fault_network_3d_width_parallel.py | New np=2 parallel validation that split + solve matches serial slip peaks. |
| src/underworld3/utilities/place_surface.py | Add embedded mid-surfaces to the 3‑D OCC band assembly and make mid-surface labelling refusal collective-safe. |
| src/underworld3/meshing/fault_network.py | Implement 3‑D finite-width band build and update TI director/slip-gauge logic for 3‑D planar patches. |
| docs/advanced/fault-networks.md | Document the 3‑D width contract, parallel split support, and current limitations. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| w = np.cross(d0, d1) | ||
| denom = float(w @ w) | ||
| t = float(np.cross(a1 - a0, d1) @ w) / denom | ||
| out[i] = a0 + t * d0 |
There was a problem hiding this comment.
Confirmed — and the fragility went deeper than the corner intersection: the plane normal itself came from the leading vertex triple (cross(P[1]-P[0], P[2]-P[0])) at five sites, so a collinear leading triple produced NaNs before the corner code was even reached. Fixed in 894effe: one Newell-method _patch_normal now serves every site, and the corner intersection guards the parallel-edge case (on a straight rim the offset corner is the edge's own offset point). Regression test added: a rim with a collinear midpoint expands finitely, in-plane, to the exact offset.
| if realisation != "split": | ||
| raise NotImplementedError( | ||
| "the 3-D network does not place a band: its patches " | ||
| "are meshed conforming (mesher='embed') or placed as " | ||
| "sheets (mesher='place'), both of zero thickness. For " | ||
| "a finite-width 3-D zone call place_thin_volume.") | ||
| "a volumetric 3-D realisation is a LAYER: pass width= " | ||
| "(the weak plane's thickness is constitutive, exactly " | ||
| "as in 2-D).") |
There was a problem hiding this comment.
Confirmed dead: realisation='ti' with width=None is refused earlier by the ti-needs-width check, so with width=None only 'split' can reach this branch. Removed in 894effe, with a comment recording why width=None implies the split.
A clipped rim may carry collinear consecutive vertices (the convexity gate passes them, and the trim inserts them): the two-edge cross product then has no plane normal, and the expansion's corner intersection divides by zero on parallel adjacent edges. One Newell _patch_normal now serves every site that needs a patch plane (expansion, outcrop slab, footprints, TI director, slips gauge), and the corner intersection falls back to the edge's own offset point when adjacent edges are collinear. The unreachable no-band NotImplementedError (dead past the ti-needs-width check) is removed. Underworld development team with AI support from Claude Code
build(width=...)in 3-D is the 2-D contract one dimension up. The margin-expanded patches are thickened into one fused band (junctions free) and each un-expanded patch is embedded in the band as a conforming mid-surface, so the same mesh is cut and split (realisation="split") or left whole for the volumetric weak plane ("ti"). Honoured footprints are exact for planar patches, with nearest-plane ownership so a cell near a junction carries one director; the weak-plane director is the patch normal;slips()gains the plane gauge (the in-plane velocity jump across the layer), validated exactly on a prescribed shear.The junction gap is physics and the tip margin convenience, and the two must not meet: a margin within half a cell of the ligament welds the expanded bands at the junction line and the split degenerates into self-paired nodes. We measured the failure as a bit-frozen residual floor (rel 8.8e-3, identical under direct LU, machine-zero leak — the degenerate constraint row is identically zero, so Newton cannot move it).
build()refuses that configuration, and a collective post-split check raises on any surviving self-pair as a defect.Parallel follows the placement's gather-first design: the placed band lives on the surgery rank, so the mid-surface labelling judges its refusal on the global face count and raises collectively — the rank-local raise it replaces was a hang for the peer ranks, not an error. The np=2 ptest builds, splits and solves the crossing network to the serial answer on a distributed mesh.
Refused loudly outside scope: outcropping bands (the embedded mid-surface cannot yet be clipped against the boundary),
base=on the 3-D width path, non-network meshers withwidth,embed=outsidemesher="network", andjunction_cellsin 3-D (the junction glue there isdamage_yield's tubes).Tests:
test_0863(one mesh two realisations, footprint and director oracles, gauge arithmetic, refusals, end-to-end split and TI solves) andptest_0863(np=2). The 3-D section ofdocs/advanced/fault-networks.mdis rewritten for the width contract. Neighbouring suites (test_0851,test_0858,test_0859) pass unchanged.Underworld development team with AI support from Claude Code