Skip to content

Fuse overlapping fault zones instead of fragmenting them - #541

Merged
lmoresi merged 4 commits into
developmentfrom
feature/fuse-zone
Aug 14, 2026
Merged

Fuse overlapping fault zones instead of fragmenting them#541
lmoresi merged 4 commits into
developmentfrom
feature/fuse-zone

Conversation

@lmoresi

@lmoresi lmoresi commented Aug 13, 2026

Copy link
Copy Markdown
Member

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, 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 in test_0855:

minimum angle cells under 5 deg
fragment 0.00 deg (degenerate) 22
fuse 37.34 deg 0

The 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 Surface objects 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_area is 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_slivering asserts the fused mesh has no cell under 5 degrees, and uses the fragment branch as the negative control — it must show the slivers, or the geometry no longer exercises the defect. test_an_unknown_assembly_boolean_is_refused covers the argument validation.

Serial test_0855 (14) and test_0856 (6) pass, ptest_0855 passes 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

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
Copilot AI lite review requested due to automatic review settings August 13, 2026 04:17
@lmoresi

lmoresi commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

Adversarial review

We reviewed this change against the claim it makes: that fuse is a safe default because the seams it removes carry no physics.

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 CROSS_3D pair (serial and np=2) and the single-patch outcrop, but the combination that would actually exercise the difference — two overlapping patches that also protrude through a wall — refuses under both booleans before the boolean can matter: two patches crossing below the wall give a band outline that is not a single closed loop, and two crossing at the wall trip the cavity-reaches-the-wall guard. So fuse introduces no regression there, but the 3-D default rests on the 2-D argument plus two orthogonal-crossing cases, not on a measurement of the failure mode itself. We think that is the right call — the geometric argument (a shallow overlap has a spike; the union does not) is dimension-independent — but it is an argument, not a measurement, and this is where to look if a 3-D zone network meshes worse than expected.

2. The negative control's margin is 4x, and it is load-bearing. test_a_tangential_merge_fuses_instead_of_slivering asserts fragment produces more than 5 cells under 5 degrees; it produces 22. If a future gmsh meshes the lens more kindly the control fires falsely and the test reports a geometry problem when there is none. We accept this: the failure mode is a loud one that names its own cause, which is what we want over a test that quietly stops testing anything.

3. The private helpers silently fall back to fragment. _occ_assembly_2d/_occ_assembly_3d branch on assembly == "fuse" with else: fragment, so any typo reaching them fragments rather than refusing. Validation is at place_thin_volume, which is the only caller and the only public entry, so the state is unreachable — we left it rather than add a guard that implies otherwise (Charter §5).

4. The area gate: we expected to have to change it and did not. The plan for this work recorded that the cad_area equality check would fire under fuse, since a fused Y has less area than the sum of its ribbons. That is true of the ribbons but not of the check: cad_area sums the masses of the faces that survive the boolean, and both booleans leave exactly the union. Measured CAD-vs-meshed relative difference is 1e-16 under both, in 2-D and 3-D. The check was never comparing against the sum of the inputs. Worth recording because the same wrong expectation would be easy to re-derive.

5. What this does not fix. fuse cleans the assembly's interior. The union's outline still has a re-entrant notch wherever two zones converge, and the annular fill has to mesh into it: on a 13.5-degree crossing the standalone assembly improves from 12.8 to 37.8 degrees, but the embedded mesh reads 12.8 against 13.5 — both approximately the convergence angle. The win is the removal of the sliver chain (22 cells under 5 degrees to none), not of the single worst cell. Anyone reading info["min_angle"] alone on a converging network will not see the improvement this PR makes.

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 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"} to place_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 unknown assembly values.
  • 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.

Comment on lines 4085 to +4087
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
@lmoresi
lmoresi merged commit ffbcf98 into development Aug 14, 2026
1 of 2 checks passed
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