Skip to content

Outcropping zones: the fault zone leaves its band on the surface - #526

Merged
lmoresi merged 2 commits into
developmentfrom
feature/zone-outcrop-band
Aug 11, 2026
Merged

Outcropping zones: the fault zone leaves its band on the surface#526
lmoresi merged 2 commits into
developmentfrom
feature/zone-outcrop-band

Conversation

@lmoresi

@lmoresi lmoresi commented Aug 11, 2026

Copy link
Copy Markdown
Member

Completes the outcrop implementation (with #525): the thin-volume form. A fault ZONE specified past the box is clipped in OCC; its clipped face becomes the BAND on the surface; the gap's boundary is one closed surface of three discrete pieces (interior shell + cap-annulus with the band outline as its hole, pre-meshed by the 2-D fill + interior skin, rim-matched). Band faces carry BOTH the zone's skin label and the wall's labels; the cap annulus is relabelled explicitly with full closures, counts gated.

Measured serial and np=2,4: identical zone counts, P2 FE-exactness (5e-11–7e-11) with Dirichlet gripping the remeshed Top, full Top-label coverage face-by-face. Interior zones byte-for-byte unaffected — suites 0853–0856 (45 serial) and the placement ptests green. Band contract test in test_0855.

Stated residuals (refuse with reasons): box-edge bands / multi-wall contact; curved or deformed tops (the manifold-prep slot); removal of an OUTCROPPING object (its cavity legitimately touches the wall — the removal carve does not yet take open_wall; next small instalment if needed).

Underworld development team with AI support from Claude Code

The thin-volume form of the outcrop, completing "get all the
implementation done". The assembly is clipped against the domain box in
OCC (specify-long: patches may protrude; clipped faces snap exactly onto
the wall plane), and a clipped face left in ONE wall plane becomes the
zone's BAND. The gap's boundary is then ONE closed surface of three
discrete pieces — the interior shell, the pre-meshed CAP (an annulus in
the wall plane: rim ring verbatim, the band outline as its HOLE, via the
2-D fill), and the interior part of the skin, rim-matched to the cap's
hole. The band's triangles become boundary faces carrying BOTH the zone's
skin label and the wall's own labels; the cap's annulus is relabelled the
same way, full closures, counts gated.

The volume carve gains open_wall (victims may lie in the open wall's
plane; the shell may open there via the shared _closed_shell_3d);
box-edge bands and multi-wall contact refuse with the reason.

Measured serial and np=2,4: identical zone counts, P2 quadratic FE-exact
(5e-11..7e-11) with Dirichlet gripping the remeshed Top, full Top
coverage. Interior zones unchanged: suites 0853-0856 (45 serial) and the
placement ptests green. Band contract test added to test_0855.

Underworld development team with AI support from Claude Code
Copilot AI lite review requested due to automatic review settings August 11, 2026 12:04
@lmoresi

lmoresi commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

Adversarial review (self, posted per project practice):

The three-piece boundary is the risk we attacked. The gap's closed surface is assembled from three independently produced triangulations sharing nodes only by tag identity: shell↔cap at the wall rim, cap↔skin at the band outline. Every sharing is gated — moved-nodes zero across all constrained sets, cap/skin triangle counts refused if remeshed, and every band and cap face recovered by join with the relabel count compared against the expectation. The FE oracle with Dirichlet on the remeshed Top then closes over the whole chain: a single mis-sewn edge or unlabelled patch cannot reproduce the quadratic.

A subtlety we verified rather than assumed: OCC's clipped faces land within rounding of the wall plane, not on it; the band logic keys on EXACT plane values, so assembly nodes within 1e-9 snap onto the plane before anything reads them. Without the snap the band detection silently returns empty and the placement refuses at the wall — loud, but wrong.

Stated residuals: removal of an outcropping object is the one asymmetry left (the removal carve does not yet pass open_wall — a spent outcropping fault currently refuses removal rather than clearing its band); box-edge bands and curved tops refuse with reasons as before.

Underworld development team with AI support from Claude Code

Underworld development team with AI support from Claude Code

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

Completes the “outcropping zones” path for place_thin_volume() by clipping the OCC-built zone assembly to the domain box and treating the clipped wall-face as a surface “band”, including explicit wall relabelling and a contract test that validates the full chain via a P2 Poisson exactness oracle.

Changes:

  • Extend _occ_assembly_3d() to optionally intersect the assembly solids with the domain box (“specify-long”) and snap clipped faces to exact wall-plane coordinates.
  • Add band/cap plumbing for outcropping thin volumes: split skin into interior vs band, allow the carve to open onto one wall, pre-mesh a cap annulus with a hole matching the band outline, and explicitly relabel new wall faces with the replaced wall-label closures.
  • Add test_an_outcropping_zone_leaves_a_band_on_the_surface() to assert band existence/labels and FE exactness after outcrop remeshing.

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 outcropping-zone “band on wall” handling: box-clipped OCC assembly, band extraction, open-wall carve, cap-annulus gap fill, and explicit relabelling gates.
tests/test_0855_place_thin_volume.py Adds a contract test covering the outcropping zone band + relabel + P2 exactness check chain.

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

Comment on lines +4185 to +4199
if wall_code is not None:
axis, side = wall_code // 2, wall_code % 2
open_wall = (int(axis),
float(box_hi[axis] if side else box_lo[axis]))
from collections import Counter as _Counter
band_edge = _Counter()
for t in skin_tris[band_idx]:
a, b, c = sorted(int(v) for v in t)
for e in ((a, b), (a, c), (b, c)):
band_edge[e] += 1
band_outline = _single_loop(
[e for e, k in band_edge.items() if k == 1],
"zone's outcrop band outline")
skin_tris_fill = (skin_tris[interior_idx] if open_wall is not None
else skin_tris)
@lmoresi
lmoresi merged commit 606583e into development Aug 11, 2026
2 of 3 checks passed
@lmoresi
lmoresi deleted the feature/zone-outcrop-band branch August 11, 2026 12:30
lmoresi added a commit that referenced this pull request Aug 13, 2026
Some Azure runners advertise an InfiniBand verbs device (mana_0) that UCX
cannot open; MPI_Init aborts inside the import smoke test before any
underworld3 code runs. Four hits in a week across #466/#526/#527 (issues
#461/#528), each fixed by re-rolling the runner lottery. A single-node CI
runner needs only shared memory and TCP, so pin the transports at the
workflow level - it covers the smoke test and every pytest batch alike.

Underworld development team with AI support from Claude Code
lmoresi added a commit that referenced this pull request Aug 13, 2026
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
lmoresi added a commit that referenced this pull request Aug 13, 2026
…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
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