Fuse overlapping fault zones instead of fragmenting them - #541
Conversation
place_thin_volume gains assembly={"fuse","fragment"}, defaulting to
"fuse". Where two zones overlap, fragment cuts along the boundary of the
overlap; for a tangential merge — a ribbon soling into another, the
listric case — that boundary is a lens closing at the convergence angle,
and the mesher resolves it as a chain of slivers. Measured on the sole
geometry now in test_0855: minimum angle 0 degrees and 22 cells under 5,
against 37 degrees and none for the fused union.
Nothing downstream needs those internal boundaries. The zone carries one
label, and a cell's fault properties are read from the Surface objects by
proximity, not from the CAD piece it was meshed in — so the seams are
conditioning cost with no physics on them.
The same swap applies in 3-D (_occ_assembly_3d) and is the default there
too. The CAD area/volume gate is unaffected: it is computed from the
faces that survive the boolean, which are the union either way, and
matches the meshed area to 1e-16 relative under both.
The fragment branch is kept and tested as the negative control: a test
that only asserted the fused mesh is clean could not tell whether the
geometry still exercises the defect.
Underworld development team with AI support from Claude Code
Adversarial reviewWe reviewed this change against the claim it makes: that 1. The 3-D default changes behaviour on a path we cannot test. The measured case is 2-D. In 3-D we verified the orthogonal 2. The negative control's margin is 4x, and it is load-bearing. 3. The private helpers silently fall back to 4. The area gate: we expected to have to change it and did not. The plan for this work recorded that the 5. What this does not fix. |
There was a problem hiding this comment.
Pull request overview
This PR updates the thin-volume fault-zone embedding workflow to fuse overlapping zones by default (unioning overlaps into a single CAD region) instead of fragmenting overlaps into multiple pieces. This targets mesh-quality failures caused by shallow-angle/tangential overlaps producing sliver elements and downstream solver divergence.
Changes:
- Add
assembly={"fuse","fragment"}toplace_thin_volume, defaulting to"fuse", and apply the same behavior in both 2-D and 3-D OCC assembly paths. - Add tests covering (1) tangential merge sliver avoidance under
"fuse"with"fragment"as a negative control, and (2) validation of unknownassemblyvalues. - Update developer documentation to describe the new assembly behavior and why
"fuse"is the default.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/underworld3/utilities/place_surface.py |
Implements assembly option, switches OCC boolean behavior to fuse-by-default, and validates argument values. |
tests/test_0855_place_thin_volume.py |
Adds regression test for tangential overlap slivering and argument-validation coverage. |
docs/developer/subsystems/conforming-surfaces-and-fault-zones.md |
Documents "fuse" vs "fragment" semantics and rationale. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| def place_thin_volume(dm, patches, width, label=ZONE_LABEL, label_value=1, | ||
| clearance=0.7, size=None, verbose=False): | ||
| clearance=0.7, size=None, assembly="fuse", | ||
| verbose=False): |
The plan for this work lived outside the repository. The architecture belongs in docs/developer/design/ where collaborators can read it: the two representations and what separates them, the tip-pinning mechanism behind the seam defect, the fuse ruling landed here, and the geometric slice criterion that is proposed and not yet built. Marked throughout as to what is implemented and what is not, and it records two things that are decided but not obvious: the CAD area gate did not need the change the plan expected, and the question of what owns a cell where two zones overlap is still open — contacts refuse to overlap and pay a gap, zones overlap freely and resolve ownership by nearest surface vertex, which is neither continuous nor independent of how the traces were sampled. Underworld development team with AI support from Claude Code
Four settings on one mesh, measured. Keeping the weak zone under the contacts reproduces the pure-zone answer to 99% and solves about three times faster; stripping the zone back to the handover buys a further factor of two and loses a quarter of the slip on the fault that receives through the merge. Those are the same mechanism. With the zone intact a contact tip ends inside weak material and is never pinned; stripped, it ends at the weak material's edge, which is the abutting case measured at 74.6% on the straight-fault control. So the criterion only has to be geometrically right for the stripped variant. With the zone kept, slicing less is slower, slicing more is faster, and slicing past what the split machinery supports is an explicit refusal — never a wrong answer. Also records a negative result, so it is not re-derived: the three-times speedup is NOT contrast-driven conditioning relief. It is flat across three decades of viscosity contrast and the zone-only cost barely moves, because transverse isotropy had already removed that penalty. The reason is still open, and the rotated path's iteration counters read zero, which is what has to be fixed to settle it. Underworld development team with AI support from Claude Code
Review finding on #541. `assembly` was inserted ahead of `verbose`, so a caller passing `verbose` positionally would have had it bound to `assembly` — silently, and with a value that fails the new validation only if it happens not to be "fuse" or "fragment". Both are now keyword-only, which is what they should have been. Underworld development team with AI support from Claude Code
place_thin_volumegainsassembly={"fuse","fragment"}, defaulting to"fuse".Where two zones overlap,
fragmentcuts along the boundary of the overlap. For a tangential merge — a ribbon soling into another, which is the listric case — that boundary is a lens closing at the convergence angle, and the mesher has to resolve it as a chain of slivers. Measured on the sole geometry now intest_0855:fragmentfuseThe exploratory campaign hit this as a hard failure rather than a quality warning: on a converging listric pair the fragmented assembly meshed at 0.13 deg minimum and the Stokes SNES diverged on it.
Nothing downstream needs the seams. The zone carries one label, and a cell's fault properties are read from the
Surfaceobjects by proximity, not from the CAD piece it was meshed in — so the internal boundaries are conditioning cost with no physics on them."fragment"is kept for the case where those boundaries are themselves of interest.The same swap applies in 3-D (
_occ_assembly_3d) and is the default there too.The area gate is unaffected
The plan for this work expected the CAD area contract to need updating, on the grounds that a fused Y has less area than the sum of its ribbons. It does not:
cad_areais computed from the faces that survive the boolean, and those are the union under either choice. Measured relative difference between CAD and meshed area, both booleans, 2-D and 3-D:1e-16. No change needed.Tests
test_a_tangential_merge_fuses_instead_of_sliveringasserts the fused mesh has no cell under 5 degrees, and uses thefragmentbranch as the negative control — it must show the slivers, or the geometry no longer exercises the defect.test_an_unknown_assembly_boolean_is_refusedcovers the argument validation.Serial
test_0855(14) andtest_0856(6) pass,ptest_0855passes at np=2, and the neighbouring placement/fault-network suites (test_0853,test_0854,test_0850_fault_network_toolkit,test_0851_fault_network_3d) pass unchanged with fuse as the default.Underworld development team with AI support from Claude Code