Skip to content

The general 3-D outcrop cap: a zone may meet any boundary - #589

Merged
lmoresi merged 10 commits into
bugfix/fault-clip-boundaryfrom
feature/fault-outcrop-3d-cap
Aug 18, 2026
Merged

lmoresi merged 10 commits into
bugfix/fault-clip-boundaryfrom
feature/fault-outcrop-3d-cap

Conversation

@lmoresi

@lmoresi lmoresi commented Aug 16, 2026

Copy link
Copy Markdown
Member

Stacked on #582 (base is its branch); only the last three commits are this PR.

A fault zone specified past the mesh boundary now embeds whatever that boundary is: a tilted or rotated wall, a box edge, a faceted sphere. The NotImplementedError at the old _trace_wall_code — "the zone outcrops on a non-axis-aligned boundary" — is gone, and the function with it.

What the cap is now

The cap over the outcrop bowl is re-triangulated per coplanar boundary region, each piece meshed flat in its region's own plane, so the cap sits exactly on the faceted surface and volume conservation survives to 1e-12. Where the band crosses a crease between regions, the two sides segment the crease line differently — the bowl's nodes are mesh vertices, the band's crossing nodes are assembly nodes — so a 1-D overlay merges both node sets by arc coordinate and keeps each elementary sub-segment for the side(s) whose bowl covers it and the band does not. A sub-segment covered from one side only must be a whole mesh edge, because a cavity-shell face matches it in the fill's closed surface; anything else is a refusal, not a crack.

The frame rule (_outcrop_frame_3d) separates two notions the third dimension forces apart. Where the cavity may open is by smooth wall: coplanar regions grouped across low-dihedral creases (under 45 degrees), so a faceted sphere is one wall and its bowl legitimately spills onto facets beside the band's own, while a box's other walls stay refused across their sharp edges. What the carve may delete is by shape: band-covered vertices, single-region interiors, and interior vertices of straight creases between touched regions. A vertex where three or more regions meet — a box corner, every vertex of a faceted sphere — is the domain's shape and is never deleted off the band. A protected vertex whose whole cell star drops is not stranded: it is a collar node, and the gap fill's tets reference it. That is how a curved boundary keeps its faceting through the surgery.

Wall labels are restored per removed face (the 2-D per-segment rule of the corner fix, one level up), so a bowl spanning Top and Front restores each wall's own labels. The Euler gate now demands conservation of the input's Euler number rather than the value 1: a spherical shell is S²×I, Euler 2, and was refused before for its topology rather than for any defect. The same assumption remains in place_sheet and remove_embedded, marked TODO(BUG).

Evidence

  • The box oracle did not move: the full placement suite (0853–0856, 0859) passes unchanged, 62 tests.
  • New test_0860_outcrop_general_boundary_3d: a rotated box (no wall axis-aligned), a band across the Top/Front box edge (trace on both walls, each wall's labels restored on its side), and a spherical shell outer surface. Each runs its negative control first — the identical census on an interior twin counts no trace — and each ends in the P2 Poisson oracle, exact through the zone.
  • Parallel: the rotated-box outcrop added to ptest_0855 passes at np=2 and np=4; the trace count (28 facets) is identical at np=1, 2 and 4 with the info dict identical on every rank.

Known limits, stated as refusals

  • A band running along a domain crease (rather than across it) refuses with the reason.
  • A band outline node landing within rounding of a kept mesh vertex refuses: the 2-D imprint collapse (_collapse_boundary_imprints) has no 3-D counterpart yet.
  • place_sheet's outcrop stays box-framed, per the plan's own advice — unify after, not during.

Underworld development team with AI support from Claude Code

…h walls

The zone outcrop no longer needs an axis-aligned wall. The cap over the
bowl is re-triangulated PER COPLANAR BOUNDARY REGION, each piece meshed
flat in its region's own plane, so the cap stays exactly on the faceted
surface and volume conservation survives. Where the band crosses a
crease the two sides segment the line differently (mesh vertices against
assembly nodes), so a 1-D overlay merges both node sets by arc
coordinate and keeps each elementary sub-segment for the side(s) whose
bowl covers it and the band does not; a one-side segment must be a whole
mesh edge, because a cavity-shell face matches it.

The frame rule (_outcrop_frame_3d) separates the two notions the third
dimension forces apart. Where the cavity may OPEN is by SMOOTH WALL —
coplanar regions grouped across low-dihedral creases — so a faceted
sphere's bowl legitimately spills onto facets beside the band's own
while a box's other walls stay refused. What the carve may DELETE is by
shape: band-covered vertices, single-region interiors, and interior
vertices of straight creases between touched regions; a vertex where
three or more regions meet is the domain's shape and is protected. A
protected vertex whose whole cell star drops is not stranded: it is a
collar node, and the gap fill's tets reference it — that is how a
curved boundary keeps its faceting through the surgery.

Wall labels are restored per removed face (the 2-D per-segment rule one
level up): each new wall triangle takes the labels of the removed face
it lies on, so a bowl spanning several walls restores each wall's own.
The Euler gate now demands CONSERVATION of the input's Euler number
rather than 1 — a spherical shell is S^2 x I, Euler 2, and refused
before for its topology, not for any defect. The same assumption in
place_sheet and remove_embedded is marked TODO(BUG).

_trace_wall_code and its refusals are deleted; the box path runs
through the general machinery and the box oracle tests are unchanged.
Driven on a rotated box (no wall axis-aligned), a band across the
Top/Front box edge (trace on both walls), and a spherical shell outer
surface (75 trace facets, 6 vertices removed): volume conserved to
1e-12 in each.

Underworld development team with AI support from Claude Code
…n a sphere

Three configurations, each the failure mode of a different assumption
the box-framed cap made: a rotated box (no wall axis-aligned; the
single-region collar), a band across the Top/Front box edge (the crease
overlay conforms the two sides' differing segmentations, and each
wall's labels are restored on its own side), and a spherical shell
outer surface (every facet its own region, every vertex protected
faceting — the case the trace design exists for). Each runs its
negative control first — the identical census on an interior twin
counts no trace — and each ends in the P2 Poisson oracle, exact for a
quadratic through the zone. Volume conserved to 1e-12 throughout; the
shell's Euler number 2 comes through the conservation gate.

The parallel file gains the rotated-box outcrop at np>=2 with the same
mesh and patch as the serial test: the info dict is identical on every
rank and the trace count matches the serial value (28 facets at
np=1, 2 and 4), so the general path is partition-independent. No
boundary face is left without a wall label at any rank count.

The spherical control sits mid-gap of a 0.75-thick shell: a one-cell
chain from a victim corner spans h, so an interior zone needs
~(clearance + 1) * h to spare on both sides — thinner shells refuse
interior zones at this resolution by the carve's own clearance gate.

Underworld development team with AI support from Claude Code
The collar-vs-bowl consistency check raised on the surgery rank inside
the relabel block, which is outside any try — a hang at np>=2. It now
sets the block's failure and flows through the allgather like every
other refusal there.

Underworld development team with AI support from Claude Code
@lmoresi

lmoresi commented Aug 16, 2026

Copy link
Copy Markdown
Member Author

Adversarial review (self)

We reviewed the three commits against the plan's gates and our own claims. Findings, most severe first.

1. A rank-local raise shipped in the first commit and would hang at np≥2. The collar-vs-bowl consistency check raised on the surgery rank inside the relabel block, which is outside any try — exactly the defect class the file's own comments warn about. Found by re-reading the diff, not by any test: the parallel tests pass because the check never fires on healthy geometry. Fixed in the third commit by routing through the block's collective failure. A test that forces this refusal at np≥2 does not exist and would need deliberately inconsistent geometry; we did not construct one.

2. The general outcrops are 2–4× more slivery than the box outcrop. Minimum dihedral angles, same mesh scale (cellSize 0.12, width 0.05): interior zone 9.5°, axis-aligned box outcrop 13.8°, rotated box 6.4°, box-edge crossing 3.6°, spherical shell 3.1°. The mechanism is the one the 2-D corner work already met: the band outline lands where it lands, and a collar polygon between an outline node and a nearby kept mesh vertex is thin. The 2-D cure (_collapse_boundary_imprints) has no 3-D counterpart; until it does, near-coincidence refuses at rounding distance but produces slivers just above it. All fills pass the conformity gates and the P2 oracle is exact — the slivers are a quality cost, not a correctness one.

3. A dead end we built and removed. Our first treatment of stranded protected wall vertices kept one cell of the vertex's star ("rescue"). It produced floating kept islands the fill wrapped as bubbles (global Euler defect, measured on the sphere) and was replaced wholesale: a protected vertex needs no kept cell, because the collar re-triangulation references it and the gap fill's tets give it cells. The final code is simpler than what it replaced; the connectivity gate we wrote for the islands went with the rescue that caused them.

4. The Euler gate was wrong before this PR, for domains nobody could reach. != 1 encodes a ball; a spherical shell is Euler 2. The general clip made shells reachable, so the thin-volume gate now demands conservation of the input's number. The identical assumption remains live in place_sheet and remove_embedded — a sheet placed inside a spherical shell refuses today for its topology. Marked TODO(BUG) at both sites rather than fixed, per scope discipline.

5. Untested paths we know about. The polygon-nesting logic in _outcrop_collar_3d is exercised only at depth ≤ 1 (outer ring plus hole); depth-2 nesting (an island inside a hole) is written but unreached. The three refusal guards — band along a crease, outline node within rounding of a kept vertex, a band node splitting a one-side crease edge — fire on geometry none of our cases construct; they are read-verified only. The 45° smooth-wall threshold means a curved boundary faceted coarser than ~45° facet-to-facet splits into separate walls and refuses; documented at the constant, not tested.

6. Partition independence is asserted, not proven, for the sphere. The np=1/2/4 identity (trace 28) is measured on the rotated box only. The sphere runs serial-only; its parallel behaviour rests on the same gather-first mechanism the box exercises, which is an argument, not a measurement.

Underworld development team with AI support from Claude Code

The same gate place_thin_volume already fixed: demanding global Euler
number 1 encodes a ball-topology domain, and a spherical shell —
S^2 x I, Euler 2 — was refused for its topology rather than for any
defect of the surgery. Both gates now compare against the input mesh's
own number. Pinned by a regression test: a sheet embeds mid-gap in a
spherical shell and its removal clears the label again, both passing
their volume and conservation gates.

Underworld development team with AI support from Claude Code
@lmoresi

lmoresi commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

Review finding 4 is resolved in 41eb60a rather than left as a TODO: place_sheet and remove_embedded now gate on conservation of the input's Euler number, the same fix place_thin_volume carries. A sheet embeds mid-gap in a spherical shell and removes again, pinned by a regression test in test_0854. Every placement primitive now works in non-ball domains.

Underworld development team with AI support from Claude Code

The gate catches unmeshed solids — an O(1) relative defect — but its
reference, OCC's getMass on the clipped boolean, is only accurate to
~5e-7 relative when the domain tool carries many faces (measured on a
16.6k-facet adapted spherical boundary: the honest mesh volume exceeds
the reported CAD mass before any snap runs). At 1e-9 the gate refused
correct assemblies; it now allows 1e-6, far above the kernel noise and
far below any real missing-solid defect.

Underworld development team with AI support from Claude Code
OCC's boolean leaves clipped nodes up to ~4e-7 off the tool's own
planes on O(1) geometry against a many-faceted tool; the 1e-9 snap
missed them, so a crease-crossing node of the band outline was not
recognised as on-crease and the collar meshed a 2-metre sliver beside
the crease (measured on a 1000 km megathrust against an adapted
spherical boundary; the 2-D fill then refused with moved nodes). The
snap tolerance now sits above that noise and below any layer mesh
size, and candidates are masked by distance to the boundary FACETS
first — the planes are infinite, and at this tolerance every point in
space is near some plane of a many-faceted tool.

Underworld development team with AI support from Claude Code
Two mechanisms a 1000 km outcrop band forced, plus the diagnostic that
found them. _collapse_boundary_imprints_3d is the 2-D imprint collapse
one dimension up: where the band outline grazes a boundary vertex
(measured: a 2.4 m gap the collar meshed as a sliver and the fill
refused as moved nodes), the outline is rerouted THROUGH the vertex —
the nearest outline node moves onto it, or the outline edge splits at
it with every incident tetrahedron bisected. A move or split must keep
every incident cell's volume healthy, else the vertex is skipped and
keeps its sliver (the status quo, not a defect); the band still tiles
the same faceted surface, so the domain's shape is untouched.

An outline edge lying ALONG a crease is no longer refused when the
cavity covers both sides: the band reaches the crease there, so the
edge bounds the collar piece on the FAR side of the crease, not the
owning band triangle's own region. The refusal remains for a band
bounded by a crease with no bowl beyond it.

A collar piece that fails to mesh now names its pinch — the thinnest
node-to-segment gap and the node kinds — which is what separated this
sliver class from the OCC placement noise the snap fix covers.

Known limit, measured and deliberately not papered over: refining a
SPHERICAL boundary projects new facet vertices onto the true sphere,
and the creases between the resulting nearly-coplanar sub-facets are
laterally fuzzy at (placement noise)/sin(theta) — wider than the
band's own feature spacing, which defeats the crease overlay. A
widened, fuzz-aware overlay tolerance was tried and withdrawn: it
traded the sliver for misclassified chains on the honest-crease tests.
Outcrops on adapt-refined spherical boundaries stay refused by the
fill's own gates until the crease representation is rethought.

Underworld development team with AI support from Claude Code
@lmoresi

lmoresi commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

Review addendum: what a 1000 km outcrop band taught us

We drove the general cap at Earth scale — a 1000 km, 4 km-wide megathrust band against a spherical shell, background mesh ~350 km — and it found four defects the 300 km tests could not see. Three are fixed on this branch, one is characterised and open.

Fixed. (1) OCC's boolean mass on a many-faceted tool is only accurate to ~5e-7 relative, so the 1e-9 meshed-vs-CAD gate refused correct assemblies; the gate now runs before the snap and allows 1e-6 (807a912, restructured in db68864). (2) OCC leaves clipped nodes up to ~4e-7 off the tool's own planes; the snap now covers that noise, masked to the boundary facets so infinite-plane projection cannot reach interior nodes (34ea192). (3) The band outline grazes the wall's own vertices — measured, a 2.4 m gap the collar meshed as a sliver — and _collapse_boundary_imprints_3d now reroutes the outline through such vertices, with a volume guard so a move or split can never flatten a cell; on-crease outline edges are reassigned to the far collar rather than refused (db68864). All 31 placement oracles pass throughout.

Open, characterised. Refining a spherical boundary projects new facet vertices onto the true sphere; the creases between the resulting nearly-coplanar sub-facets are laterally fuzzy at (placement noise)/sin(theta) ~ 1e-4 — wider than a 4 km band's own feature spacing — and the crease overlay cannot classify crossings against them. A fuzz-aware tolerance was tried and withdrawn: it traded the sliver for misclassified chains on the honest-crease tests. Until the crease representation is rethought, outcrops on adapt-refined spherical boundaries refuse at the fill's own gates; the plain-shell 1000 km case also still trips a skin/cap facet overlap that is not yet diagnosed. Negative results reported as found; the collar refusal now names its pinch (gap and node kinds), which is the diagnostic that separated all four causes.

Underworld development team with AI support from Claude Code

@lmoresi

lmoresi commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Adversarial review — fault-placement stack (#582, #589)

Reviewed together. Each already carries its own review; this adds the
relationship between them, which neither says.

S1. #589 is stacked on #582. pr582 is an ancestor of pr589. Both edit
src/underworld3/utilities/place_surface.py and
tests/parallel/ptest_0855_place_thin_volume_parallel.py; #589 adds
test_0854_place_sheet.py and test_0860_outcrop_general_boundary_3d.py on top
of #582's test_0855_place_thin_volume.py and test_0859_domain_boundary_tool.py.

Consequences for the backlog:

Recommended order: #582, then #589.

S2. Both touch ptest_0855, which is a ptest_ file. Those are not
collected by a default pytest tests/ run — they are driven under mpirun
explicitly. Two PRs editing the same manually-driven parallel test is the case
where a regression is least likely to be noticed by CI. Worth confirming the
combined file runs at np=2 and np=4 after both land, rather than after each.

Underworld development team with AI support from Claude Code

The sheet path joins the general boundary machinery: one clip, one
frame, one collar (maintainer ruling 2026-08-18 — multiple placement
paths are themselves the defect). The Sutherland-Hodgman box clip and
the wall-code frame are deleted.

The discrete clip primitive cuts the authored triangulation against the
gathered boundary complex directly: per-component outward orientation
(a shell's inner surface signs opposite to its outer), signed-distance
classification, sequential cuts by the COPLANAR REGIONS' planes — a box
wall cuts as one plane however it is faceted, or the two triangles
sharing a cut edge key their cuts by different facets and the sheet
tears (measured: a duplicated node on the box top wall). Side cuts are
re-derived from the original sheet edge's endpoints and interned by
(edge, region), so neighbours whose sides are truncated differently
produce the bitwise-identical node (measured: ~1e-17 duplicate pairs on
the sphere without it). A crossing that can touch a locally concave
crease (an inner boundary) refuses loudly — the polyline cut is not
built — and every kept node is gated inside the domain on exit.

The carve takes the frame's two masks (open_deletable / open_near) like
the volume's carve, with the same protected-collar-node rescue; the
outcrop frame accepts a trace CHAIN of edges as the footprint alongside
a band of triangles; and the collar embeds the chain through its pieces
instead of cutting a hole — chain nodes at crease crossings land ON the
crease and enter the 1-D overlay as 'a' nodes, the runs between
crossings embed per region (_gmsh_fill_2d now takes several polylines),
free ends as gmsh's ordinary free-end embed. A trace edge along a
crease, or off the bowl, refuses with the reason. The overlay's local
loop variable is renamed crease_chain — it shadowed the new parameter,
which made the volume path read a stale trace.

The trace chain's edges carry <label>_trace in the result, the wall's
labels are restored per removed face (the volume path's rule), and the
counts are gated collectively. Box oracle unchanged (test_0854, 8/8;
area matches Sutherland-Hodgman bit-for-bit in the spike); the sphere
outcrop that could not run at all now embeds with volume conserved and
the P2 oracle exact (test_0860); trace counts identical at np=1/2/4
(ptest_0854).

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9
The hybrid-seam study measured the defect this guards against: a split
surface terminating against something that cannot slip ends as a free
crack tip, slip pins to zero, and the composite is worse than either
pure representation (74.6% vs 100/105%). The outcrop is that join one
level up, so the acceptance test is kinematic — measured slip > 0 at
the trace against a pinned control — not mesh gates alone.

The placement half asserts today: every trace edge on the wall AND
bounding a labelled fault face, the wall's labels restored beside it.
The kinematic half attempts split_along_label_3d through the wall and
SKIPS at its daylighting refusal — the body below the gate is the
acceptance criterion, ready to run when feature/fault-split-node learns
to duplicate the trace chain.

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9
The boundary snap's tolerance was raised to 1e-6 to cover OCC's ~4e-7
boolean placement noise (34ea192), but the test's untouched-control
point stayed at 2e-9 — inside the new tolerance, so it snapped and the
test failed. The control moves to 2e-5: above the tolerance, below any
real feature scale. A branch push does not trigger CI, which is how
this shipped red.

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9
@lmoresi

lmoresi commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

The sheet path joins the general machinery (111ae09..24cbd56)

place_sheet now clips, carves and caps against the mesh's own boundary — the Sutherland–Hodgman box clip, the wall-code frame and the single-loop rim cap are deleted. The discrete clip cuts the authored triangulation directly (no OCC): per-component outward orientation, signed-distance classification, sequential cuts by the coplanar regions' planes, cut nodes interned by (original-edge, region) identity. The carve takes the frame's two masks like the volume's carve; the collar embeds the trace chain through its pieces instead of cutting a hole, with crossings on the creases via the existing 1-D overlay. The trace chain's edges carry <label>_trace through to the wall — the splittable trace the split machinery needs for a daylighting fault (test_0861 holds the kinematic acceptance criterion, gated on the split side's refusal until feature/fault-split-node lands).

Evidence: box oracle unchanged (test_0854 8/8; clip area matches Sutherland–Hodgman bit-for-bit); the spherical sheet outcrop that previously could not run at all embeds with the trace labelled, volume conserved to 1e-12 and the P2 oracle exact (test_0860, 4/4 with interior negative control); trace counts identical at np=1/2/4; full suite 1505 passed.

Adversarial review (our own)

Defects found and fixed before the push:

  • Coplanar facets tore the sheet. Cutting per facet plane keyed the two triangles sharing a cut edge by different (coplanar) facets of one wall — a duplicated node on the box top wall. Cuts now key and compute by the coplanar REGION's plane.
  • Sub-segment interpolation tore the sphere. A side truncated by an earlier plane interpolated its next cut from the sub-segment, its neighbour from the full edge: ~1e-17-apart duplicate pairs. Side cuts now re-derive from the original sheet edge's endpoints.
  • The collar's overlay shadowed the new chain parameter (for chain in chains.values()), so the VOLUME path read a stale crease chain as a sheet trace and indexed with tuples — caught by test_0860's box-edge case, renamed crease_chain.
  • petsc4py segfault in our own census: getStratumIS on an empty stratum hands back an IS whose getIndices SEGVs (PETSc traps it, so it presents as a silent pytest death, exit 59). Guarded by getStratumSize first — the same trap class as the Null_Boundary lesson.
  • Pre-existing red test: 34ea192 widened the snap tolerance to 1e-6 and left test_0859's untouched-control at 2e-9, inside it. A branch push does not trigger CI, which is how it shipped red. Fixed (24cbd56).

Known limitations, deliberately loud rather than fixed here:

  • A crossing that can touch a locally concave crease (an inner boundary) refuses — the polyline cut is not built. On adapted spheres with fuzzy creases the refusal may now fire at the clip rather than the fill's gates; same posture as the crease-classification redesign note, earlier trigger.
  • Tolerances are absolute (clip 1e-12, membership 1e-9), consistent with the subsystem; at 1000 km-in-metres scale the rounding floor approaches them. Campaign-wide, not new here.
  • The bbox near-set is a superset heuristic: an over-keep is caught by the every-node-inside exit gate (refusal); an over-clip cannot happen where the boundary is locally convex; a torn configuration surfaces at the fill's PLC as a loud error. No silent path found.
  • The shell sheet outcrop's min cell volume is ~1e-10 — the sliver follow-on already recorded for general outcrops. Sheets have no 3-D graze collapse; the collar's near-coincidence refusal is the guard.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

@lmoresi

lmoresi commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Merging as the second half of the #582 stack (#582 landed as 87232f0). Its CI was red when we reviewed; it re-ran against a development that now contains #582 and is green — the failure was the stack, not the change.

Per S2 in the cluster review, both PRs edit ptest_0855_place_thin_volume_parallel.py, which is a ptest_ file and so is not collected by a default pytest tests/ run. The combined file is worth a run at np=2 and np=4 now that both halves are in, rather than after each.

@lmoresi
lmoresi merged commit 112c277 into bugfix/fault-clip-boundary Aug 18, 2026
1 check passed
@lmoresi

lmoresi commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

The daylighting ruling lands (aa79ec5, ae7f9f6)

Maintainer ruling (2026-08-18): split-node faults do not split through the surface. A fault stops an element or two below, blind, with a damaged region above carrying the deformation to the surface. This supersedes the split-through-the-wall acceptance test_0861 first carried; the body gated on a daylighting split is removed.

The kinematic acceptance is now validated by running, not gated. The listric handover rule (a contact tip inside weak material is free; a tip at the weak zone's edge is pinned) turned vertical against a FREE surface: a blind frictionless split fault two cells under the top, driven in antisymmetric dip-slip, solved bare / damage-abutting / damage-enclosing. Measured (probe in ~/+Simulations/blind_fault_surface_expression/): near-tip slip 0.45 / 0.65 / 0.80 of peak, surface localization 37% / 44% / 50% — strict ordering on both, enclosing the tip worth 1.77x over bare pinning, and the abutting seam (the hybrid-seam defect) measurably worse than the enclosed tip. The overlap margin is the deliberate design parameter.

place_sheet(..., setback=d) does the deliberate part: the job is still to figure out the intersection so the fault can stop before it hits it — it just never meshes or splits all the way there. The sheet clips against the boundary offset inward by d, arrives blind with a strictly interior rim, and split_along_label_3d takes it as it stands; the would-be intersection with the true boundary comes back as info['surface_trace'] — the damage region's locator. Verified end-to-end in test_0861: place with setback, trace located on the true wall, shallowest sheet vertex exactly a setback below it, split succeeds with the Plus side carrying every placed face.

Adversarial notes on our own additions: an all-rim corner face (two side-rim originals + cut nodes) admits no triangulation the split accepts, at any sheet density — fixed by longest-interior-edge midpoint splits. A centroid split was tried first and rejected on measurement: inside a sliver corner face it drove the gap fill to 1e-23 cell volumes and a P2 error of 0.64 on the shell outcrop, caught by the P2 oracle after every topological gate passed — the #520 lesson holding again. The blind-fault probe's margins are modest at unit scale (the far field dominates a 1x1 box); the committed thresholds hold generous slack under the measured values.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

@lmoresi

lmoresi commented Aug 19, 2026

Copy link
Copy Markdown
Member Author

Physics-first blind depth, verified on an adapted shell (de6f5ba)

Ruling refinement: the blind depth is chosen from the physics (km-scale) and the resolution adapts to meet it — never a coarse mesh dictating the setback. Verified end to end at Earth scale: a 19,768-cell shell adapts to 288,525 cells and the megathrust places blind at 15 km = 2.5 local elements, trace located, all gates green (probe ~/+Simulations/spherical_slab_outcrop/megathrust_blind_adapted.py).

Two library changes carried it, both replacing an existence test with a measurement:

  • The clip's concave refusal now weighs the crease DEPTH against the setback (over-cut bound ≤ depth): a snapped graded sphere's resolution-transition concavity is metres — admissible under a km-scale setback; an inner boundary's is the facet size — refused; setback zero still refuses any concavity (outcrop exactness).
  • surface_trace is computed by marching triangles (contouring the boundary signed distance over the sheet's own edges): exact chaining by construction on any boundary. We tried direct tri-tri intersection first and it fragmented a 300 km trace into 148 components at crease endpoints — rejected on measurement.

Findings against our own work, recorded for the next user: the carve's wall clearance requires PROPORTIONAL grading (h ≤ 0.5·distance-to-sheet) — a linear ramp left 570 km deep-rim tets spanning to the wall and the carve rightly refused; NVB's volume-proxy marking leaves cell DIAMETERS ~3× coarser than target while the carve reads diameters — engine="edge_split" is the one that meets a setback budget; and the adapt metric must be closed-form (it re-evaluates every bisection generation — a triangle-loop distance blew a 7-level adapt past 10 minutes). Min cell volume of the placed adapted-shell mesh is ~1.5e-18 (a ~7 m sliver): placement gates pass; solvability at that sliver level is the standing follow-on.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

@lmoresi

lmoresi commented Aug 19, 2026

Copy link
Copy Markdown
Member Author

The sheet's resolution is a mesh choice (925b34f)

Ruling: the authored fault triangulation is data; the embedded fault's resolution must match the mesh it cuts. place_sheet(size=...) now re-triangulates the clipped sheet in its own plane at the embedding resolution — the counterpart of place_thin_volume's size, closing the asymmetry the unification left. Rim corners are kept exactly (collinear runs compress, so a too-fine authored rim also coarsens), gmsh meshes the interior, and the all-rim split-safety pass (now factored as _split_safe_triangulation) runs on the result. Planar sheets, blind/interior placements only — an outcropping sheet's trace must stay verbatim on the boundary complex, and a curved surface needs a parametric remesh; both refuse with the reason.

Measured at Earth scale (the blind megathrust on the 288k-cell adapted shell, identical configuration): a 50-triangle sheet authored at 25 km, resampled to the 6 km local mesh size, embeds as 1744 gmsh-quality faces and the worst cell in the placed mesh improves from 1.5e-18 to 6.8e-14 — four and a half orders of magnitude — with the blind rim still exactly on the offset plane and the patch still splitting as it stands. The remaining worst cell sits in the gap fill, not the sheet triangulation; that residue is the standing gradation follow-on. Box workflow test in test_0861 (coarse 3x3 authored sheet -> min face quality > 0.3, split intact); suites 22/22.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

@lmoresi

lmoresi commented Aug 20, 2026

Copy link
Copy Markdown
Member Author

Adversarial review, commits 4b8b0cd..f8183b2 (the Mesh-level 3-D conforming sheet).

What we tried to break, and what held:

  • The refactor changes 2-D behaviour. _adopt_cut_child is extracted verbatim from add_conforming_surface (one comment generalised); test_0844 passes complete, including the tail-composition and subsample assertions, so the 2-D contract is unchanged by construction and by test.
  • The 3-D tail is decorative. test_0862 asserts the child's tail is the base's own level objects (coordinates equal to dm_hierarchy[-2]), that no level carries the label, that the cut REPLACES the base finest rather than stacking on it, and that a Poisson solve with a per-cell contrast across the sheet runs on the cut child as it stands.
  • Chaining breaks the tail. Two stacked sheets on a refinement=1 base leave exactly the base L0 in the tail — each same-resolution cut replaced its parent, per the subsample rule. Asserted.
  • The zone was silently empty. It was — in any 3-D use (cells_supporting returns an empty fault zone on a 3-D mesh #620): cells_supporting walked _cells_on_edge from FACE points, which lands on nothing. Fixed by height dispatch; the test asserts zone == union of the faces' support cells. This fix predates the branch conceptually (the method was 2-D-only in practice) and is a candidate for extraction to development.
  • The fault_network rewire changes the place route's output. Label VALUES now come from _boundaries_with (sequential) rather than the local max+4 rule; split_fault resolves by NAME, and the end-to-end smoke (place → split → MainPlus/MainMinus present) passes. The route's known solve pathology (3850 s vs 125 s embed) predates the handoff and should be re-measured now the cut mesh actually carries a tail.

What remains open, honestly:

  • The split at the end of the chain still drops the tail (add_fault's documented contract) — carrying geometric MG through duplicated DOFs is future work; the essential-BC warning in add_conforming_surface applies to the sheet unchanged.
  • test geometry margins: the carve gates are tight on coarse boxes (cellSize 0.25 + refinement=1 refuses; 0.2 places). The test fixtures sit inside the measured window; a platform with a different gmsh could conceivably land outside it. If 0862 flakes on CI, that is where to look.
  • Serial-only coverage here; the parallel gate for the underlying chain remains ptest_0852, which does not go through the new wrapper.

Suites: test_0862 6/6; guards 0844/0845/0850/0851/0853/0854 81 passed, 1 pre-existing skip; full level_1 1574 passed.

Underworld development team with AI support from Claude Code

@lmoresi

lmoresi commented Aug 20, 2026

Copy link
Copy Markdown
Member Author

Adversarial review, commits 5f07bc3..1afaa78 (the solver-side pair: custom-P re-install guard, fgmres outer default).

What we tried to break, and what held:

  • The guard skips a NEEDED re-install. The verdict comes from the live PC (managed block exists, is PCMG, level count matches), not from the marker alone; a rebuilt SNES, a preconditioner change, or an unreachable fieldsplit all answer False and re-install. The correctness case is measured, not assumed: perturbing the coefficient data and re-solving under the guard converges (mg:4its) and moves the answer — PETSc's re-Galerkin at PCSetUp carries the new operator values.
  • The guard's win was overstated by a benchmarking artifact — and we caught our own error. A repeat solve with unchanged data warm-starts from the converged solution and is a near-no-op, so the first "60 s -> 5.5 s" claim compared trivia. The honest arena (genuine solve, perturbed operator) still favours the pair of changes: 543.9 -> 284.6 s.
  • fgmres changes answers. vrms agrees to the converged tolerance across all arms (0.14723x); the level_1 sweep is green (1574 passed).
  • fgmres masks a real diagnostic. The one test that failed asserted a STRICT first-solve undercount in solver_health — measured to be an artifact of left-preconditioned gmres applying the preconditioner once before its first monitor fire. Right-preconditioned fgmres forms the unpreconditioned residual first, the bound goes tight, and the test now asserts the contract (lower bound + honesty flag) rather than the artifact. The flag itself still reads False on a first solve, which remains honest: the instrumentation cannot know it caught everything.
  • A user's explicit ksp_type is clobbered. No: pushed via _push_managed_option in both defaults sites (constructor and strategy setter), so a latched user choice wins — the same doctrine as the Preconditioner defaults: PETSc's GAMG cycle, node aggregation, and a flexible outer Krylov #584-era option management.

Open, honestly: the genuine-solve bottleneck is now the pressure sub-solve sitting AT its 200-iteration gasm cap, silently unconverged (#625) — it bounds what any velocity-block improvement can show (custom-P vs GAMG is 284.6 vs 307.7 s for an 11x iteration advantage). The hybrid cut-crossing transfer (#622) and the place-route sizing (#621) remain open. All measurements: ~/+Simulations/place_route_health/.

Underworld development team with AI support from Claude Code

lmoresi added a commit that referenced this pull request Aug 22, 2026
… in-repo, composed benchmark test

The fault-zone patch key settles into API: set_custom_fmg(...,
fac_zone=mask | [masks]), validated against the finest mesh at
registration; the solver._fac_zone_cells attribute spelling is retired
loudly (setting it raises rather than declining silently — the #629
campaign's sharpest instrumentation lesson).

The #589 empty-stratum getIndices() segfault is fixed at source:
utilities/dm_labels.py provides label_stratum_indices() gated on
getStratumSize (safe on both the null-IS wrapper and values outside the
live set), and the dead `is None` guards in nvb/reconnect/fault_split —
petsc4py returns a non-None NULL-handle wrapper, so they never fired —
are routed through it. mesh.cells_labelled(name, value) is the
empty-safe cell-mask accessor for placement labels (the natural
fac_zone builder).

The transfinite ladder band (#595: three nodes across, rails + exact
centreline, mandatory for spine cuts) moves in-repo from the campaign
scripts as place_thin_volume(..., mesher="ladder").

tests/test_1022_composed_ribbon_fmg.py (tier B) enshrines the composed
2-D benchmark of record: native level densities, the 2-block finest
patch (zone + automatic structural union), iteration bounds, slip/leak
invariants. Negative control verified (FAC disabled fails the block
assertion). The benchmark reproduces bit-for-bit on the in-repo path;
the 3-D pure-contact composition measures 7 velocity iterations vs
GAMG 56 with identical physics (design note updated).

Underworld development team with AI support from Claude Code
lmoresi added a commit that referenced this pull request Aug 26, 2026
…grid (#629) (#638)

* The general 3-D outcrop cap: per-region collar, crease overlay, smooth walls

The zone outcrop no longer needs an axis-aligned wall. The cap over the
bowl is re-triangulated PER COPLANAR BOUNDARY REGION, each piece meshed
flat in its region's own plane, so the cap stays exactly on the faceted
surface and volume conservation survives. Where the band crosses a
crease the two sides segment the line differently (mesh vertices against
assembly nodes), so a 1-D overlay merges both node sets by arc
coordinate and keeps each elementary sub-segment for the side(s) whose
bowl covers it and the band does not; a one-side segment must be a whole
mesh edge, because a cavity-shell face matches it.

The frame rule (_outcrop_frame_3d) separates the two notions the third
dimension forces apart. Where the cavity may OPEN is by SMOOTH WALL —
coplanar regions grouped across low-dihedral creases — so a faceted
sphere's bowl legitimately spills onto facets beside the band's own
while a box's other walls stay refused. What the carve may DELETE is by
shape: band-covered vertices, single-region interiors, and interior
vertices of straight creases between touched regions; a vertex where
three or more regions meet is the domain's shape and is protected. A
protected vertex whose whole cell star drops is not stranded: it is a
collar node, and the gap fill's tets reference it — that is how a
curved boundary keeps its faceting through the surgery.

Wall labels are restored per removed face (the 2-D per-segment rule one
level up): each new wall triangle takes the labels of the removed face
it lies on, so a bowl spanning several walls restores each wall's own.
The Euler gate now demands CONSERVATION of the input's Euler number
rather than 1 — a spherical shell is S^2 x I, Euler 2, and refused
before for its topology, not for any defect. The same assumption in
place_sheet and remove_embedded is marked TODO(BUG).

_trace_wall_code and its refusals are deleted; the box path runs
through the general machinery and the box oracle tests are unchanged.
Driven on a rotated box (no wall axis-aligned), a band across the
Top/Front box edge (trace on both walls), and a spherical shell outer
surface (75 trace facets, 6 vertices removed): volume conserved to
1e-12 in each.

Underworld development team with AI support from Claude Code

* Tests: the general 3-D outcrop on a rotated box, across a box edge, on a sphere

Three configurations, each the failure mode of a different assumption
the box-framed cap made: a rotated box (no wall axis-aligned; the
single-region collar), a band across the Top/Front box edge (the crease
overlay conforms the two sides' differing segmentations, and each
wall's labels are restored on its own side), and a spherical shell
outer surface (every facet its own region, every vertex protected
faceting — the case the trace design exists for). Each runs its
negative control first — the identical census on an interior twin
counts no trace — and each ends in the P2 Poisson oracle, exact for a
quadratic through the zone. Volume conserved to 1e-12 throughout; the
shell's Euler number 2 comes through the conservation gate.

The parallel file gains the rotated-box outcrop at np>=2 with the same
mesh and patch as the serial test: the info dict is identical on every
rank and the trace count matches the serial value (28 facets at
np=1, 2 and 4), so the general path is partition-independent. No
boundary face is left without a wall label at any rank count.

The spherical control sits mid-gap of a 0.75-thick shell: a one-cell
chain from a victim corner spans h, so an interior zone needs
~(clearance + 1) * h to spare on both sides — thinner shells refuse
interior zones at this resolution by the carve's own clearance gate.

Underworld development team with AI support from Claude Code

* A relabel refusal must be collective

The collar-vs-bowl consistency check raised on the surgery rank inside
the relabel block, which is outside any try — a hang at np>=2. It now
sets the block's failure and flows through the allgather like every
other refusal there.

Underworld development team with AI support from Claude Code

* place_sheet and remove_embedded conserve the domain's Euler number

The same gate place_thin_volume already fixed: demanding global Euler
number 1 encodes a ball-topology domain, and a spherical shell —
S^2 x I, Euler 2 — was refused for its topology rather than for any
defect of the surgery. Both gates now compare against the input mesh's
own number. Pinned by a regression test: a sheet embeds mid-gap in a
spherical shell and its removal clears the label again, both passing
their volume and conservation gates.

Underworld development team with AI support from Claude Code

* The assembly volume gate sits above OCC's boolean-mass noise

The gate catches unmeshed solids — an O(1) relative defect — but its
reference, OCC's getMass on the clipped boolean, is only accurate to
~5e-7 relative when the domain tool carries many faces (measured on a
16.6k-facet adapted spherical boundary: the honest mesh volume exceeds
the reported CAD mass before any snap runs). At 1e-9 the gate refused
correct assemblies; it now allows 1e-6, far above the kernel noise and
far below any real missing-solid defect.

Underworld development team with AI support from Claude Code

* The boundary snap covers OCC's placement noise, masked to the facets

OCC's boolean leaves clipped nodes up to ~4e-7 off the tool's own
planes on O(1) geometry against a many-faceted tool; the 1e-9 snap
missed them, so a crease-crossing node of the band outline was not
recognised as on-crease and the collar meshed a 2-metre sliver beside
the crease (measured on a 1000 km megathrust against an adapted
spherical boundary; the 2-D fill then refused with moved nodes). The
snap tolerance now sits above that noise and below any layer mesh
size, and candidates are masked by distance to the boundary FACETS
first — the planes are infinite, and at this tolerance every point in
space is near some plane of a many-faceted tool.

Underworld development team with AI support from Claude Code

* The 3-D imprint collapse; on-crease outline edges bound the far collar

Two mechanisms a 1000 km outcrop band forced, plus the diagnostic that
found them. _collapse_boundary_imprints_3d is the 2-D imprint collapse
one dimension up: where the band outline grazes a boundary vertex
(measured: a 2.4 m gap the collar meshed as a sliver and the fill
refused as moved nodes), the outline is rerouted THROUGH the vertex —
the nearest outline node moves onto it, or the outline edge splits at
it with every incident tetrahedron bisected. A move or split must keep
every incident cell's volume healthy, else the vertex is skipped and
keeps its sliver (the status quo, not a defect); the band still tiles
the same faceted surface, so the domain's shape is untouched.

An outline edge lying ALONG a crease is no longer refused when the
cavity covers both sides: the band reaches the crease there, so the
edge bounds the collar piece on the FAR side of the crease, not the
owning band triangle's own region. The refusal remains for a band
bounded by a crease with no bowl beyond it.

A collar piece that fails to mesh now names its pinch — the thinnest
node-to-segment gap and the node kinds — which is what separated this
sliver class from the OCC placement noise the snap fix covers.

Known limit, measured and deliberately not papered over: refining a
SPHERICAL boundary projects new facet vertices onto the true sphere,
and the creases between the resulting nearly-coplanar sub-facets are
laterally fuzzy at (placement noise)/sin(theta) — wider than the
band's own feature spacing, which defeats the crease overlay. A
widened, fuzz-aware overlay tolerance was tried and withdrawn: it
traded the sliver for misclassified chains on the honest-crease tests.
Outcrops on adapt-refined spherical boundaries stay refused by the
fill's own gates until the crease representation is rethought.

Underworld development team with AI support from Claude Code

* place_sheet clips, carves and caps against the mesh's own boundary

The sheet path joins the general boundary machinery: one clip, one
frame, one collar (maintainer ruling 2026-08-18 — multiple placement
paths are themselves the defect). The Sutherland-Hodgman box clip and
the wall-code frame are deleted.

The discrete clip primitive cuts the authored triangulation against the
gathered boundary complex directly: per-component outward orientation
(a shell's inner surface signs opposite to its outer), signed-distance
classification, sequential cuts by the COPLANAR REGIONS' planes — a box
wall cuts as one plane however it is faceted, or the two triangles
sharing a cut edge key their cuts by different facets and the sheet
tears (measured: a duplicated node on the box top wall). Side cuts are
re-derived from the original sheet edge's endpoints and interned by
(edge, region), so neighbours whose sides are truncated differently
produce the bitwise-identical node (measured: ~1e-17 duplicate pairs on
the sphere without it). A crossing that can touch a locally concave
crease (an inner boundary) refuses loudly — the polyline cut is not
built — and every kept node is gated inside the domain on exit.

The carve takes the frame's two masks (open_deletable / open_near) like
the volume's carve, with the same protected-collar-node rescue; the
outcrop frame accepts a trace CHAIN of edges as the footprint alongside
a band of triangles; and the collar embeds the chain through its pieces
instead of cutting a hole — chain nodes at crease crossings land ON the
crease and enter the 1-D overlay as 'a' nodes, the runs between
crossings embed per region (_gmsh_fill_2d now takes several polylines),
free ends as gmsh's ordinary free-end embed. A trace edge along a
crease, or off the bowl, refuses with the reason. The overlay's local
loop variable is renamed crease_chain — it shadowed the new parameter,
which made the volume path read a stale trace.

The trace chain's edges carry <label>_trace in the result, the wall's
labels are restored per removed face (the volume path's rule), and the
counts are gated collectively. Box oracle unchanged (test_0854, 8/8;
area matches Sutherland-Hodgman bit-for-bit in the spike); the sphere
outcrop that could not run at all now embeds with volume conserved and
the P2 oracle exact (test_0860); trace counts identical at np=1/2/4
(ptest_0854).

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* The daylighting acceptance test: slip at the trace, gated on the split

The hybrid-seam study measured the defect this guards against: a split
surface terminating against something that cannot slip ends as a free
crack tip, slip pins to zero, and the composite is worse than either
pure representation (74.6% vs 100/105%). The outcrop is that join one
level up, so the acceptance test is kinematic — measured slip > 0 at
the trace against a pinned control — not mesh gates alone.

The placement half asserts today: every trace edge on the wall AND
bounding a labelled fault face, the wall's labels restored beside it.
The kinematic half attempts split_along_label_3d through the wall and
SKIPS at its daylighting refusal — the body below the gate is the
acceptance criterion, ready to run when feature/fault-split-node learns
to duplicate the trace chain.

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* The snap test's control sits above the widened tolerance

The boundary snap's tolerance was raised to 1e-6 to cover OCC's ~4e-7
boolean placement noise (34ea1928), but the test's untouched-control
point stayed at 2e-9 — inside the new tolerance, so it snapped and the
test failed. The control moves to 2e-5: above the tolerance, below any
real feature scale. A branch push does not trigger CI, which is how
this shipped red.

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* Faults reach daylight blind, under a damage zone — the acceptance test

Ruling (2026-08-18): split-node faults do not split through the surface.
They stop an element or two below, blind, with a damaged region above
carrying the deformation to the surface — the junction lessons applied:
a contact tip inside weak material is free, a tip at the weak zone's
edge is pinned, and an abutting composite is worse than either pure
form. This supersedes the split-through-the-wall acceptance this file
first carried; the gated body that waited on a daylighting split is
removed.

The kinematic acceptance turns the listric handover rule vertical,
against a FREE surface, and is validated by running (probe in
~/+Simulations/blind_fault_surface_expression/): a blind frictionless
split fault two cells under the surface, solved bare / damage-abutting /
damage-enclosing, orders strictly on near-tip slip (0.45 / 0.65 / 0.80)
and on surface localization (37% / 44% / 50%). The abutting case is the
deliberate negative control — the seam defect the overlap margin exists
to avoid. The structural test keeps the placement contract: the trace
chain labelled through to the wall, now as the surface LOCATOR for the
damage region rather than a split path.

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* place_sheet stops short on request: setback clips against the offset boundary

The ruling's other half: the job is still to figure out the
intersection so the fault can stop BEFORE it hits the surface — it just
never meshes or splits all the way there. place_sheet(..., setback=d)
clips the sheet against the boundary offset INWARD by d (each coplanar
region's plane shifted along its inward normal; on a curved boundary
the shifted faceted planes, within a sagitta of the true offset): the
placed sheet arrives BLIND, its rim strictly interior, so
split_along_label_3d takes it as it stands — the daylighting refusal
never fires. The would-be intersection with the true boundary is still
computed from the unclipped input and returned as
info['surface_trace'] — the locator for the damage region above.

The clip's output is made split-safe by construction: a cut corner
polygon (two side-rim originals plus cut nodes) admits no triangulation
without an all-rim face, which the split refuses however fine the sheet
(measured: 3 such faces at every density tried). The face's longest
interior edge is split at its midpoint — bisecting both sharing faces,
rim edges (the trace included) untouched, no child worse-shaped than
its parent. A centroid split was tried first and REJECTED: a centroid
inside a sliver corner face drove the gap fill to 1e-23 cell volumes
and a P2 error of 0.64 on the shell outcrop.

test_0861 gains the end-to-end workflow test: a through-running sheet
placed with setback=0.25 arrives with no trace on the wall, its
shallowest vertex exactly a setback below it (the box's offset plane is
exact), surface_trace on the true boundary, and the placed patch splits
with the Plus side carrying every placed face. Box oracle and the
outcrop suite unchanged (test_0854 8/8, test_0860 4/4, suite green,
np=2/4).

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* The concavity gate measures depth against the setback; the trace locator marches

Physics-first ruling (2026-08-19): choose the fault's blind depth from
the physics and adapt the resolution to meet it. Two library changes
make that workable on the meshes it needs — locally refined shells
whose boundaries snap to the true sphere.

The clip's concave refusal becomes a MEASURED gate. A snapped
multi-resolution boundary is genuinely non-convex where fine facets
meet coarse chords, but the concavity DEPTH there is the sagitta
mismatch — metres at Earth scale — while an inner boundary's is the
facet size. The sequential plane clip over-cuts by at most that depth,
so a crossing is allowed when the deepest concave crease among the
near facets stays under 20% of the setback, and refused otherwise;
setback zero (an outcrop, where cut nodes must land on the complex
exactly) still refuses any concavity at all, as before.

The surface-trace locator contours the boundary signed distance over
the sheet's own triangulation (marching triangles): every crossing
point interpolates on a sheet EDGE from that edge's two vertex
distances, so the two triangles sharing it produce the identical point
and the polyline chains exactly, with no tolerance welding, on ANY
boundary — concave, graded, snapped. A direct triangle-triangle
intersection was tried first and REJECTED by measurement: robust-
geometry endpoint mismatches fragmented a 300 km trace into 148
components. Locator accuracy is the linear interpolant's,
O(spacing^2/R) — metres, which is the locator contract.

Verified end to end at Earth scale (probe:
~/+Simulations/spherical_slab_outcrop/megathrust_blind_adapted.py): a
19,768-cell shell adapts to 288,525 cells (proportional grading
h <= 0.5*distance — required, or coarse deep-rim tets span to the wall
and the carve refuses; edge_split engine — NVB's volume proxy leaves
diameters ~3x coarser than the carve's reach rules read), and the
megathrust places blind at 15 km = 2.5 local elements with the trace
located. Suites unchanged: test_0854/0859/0860/0861 21/21, spike green.

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* The sheet's resolution is a mesh choice: place_sheet(size=) re-triangulates

The ruling (2026-08-19): the authored fault triangulation is DATA, at
whatever spacing the source provided — but the embedded fault's
resolution must match the mesh it cuts, or the verbatim embed forces
sliver cells around every mismatched sheet triangle. place_sheet gains
size= — the counterpart of place_thin_volume's size, closing the
asymmetry the unification left: the clipped sheet's rim is compressed
to its exact corners (a too-fine authored rim coarsens here), each
straight run is resampled at the target, and the interior is re-meshed
by gmsh in the sheet's own plane. Planar sheets only (a curved surface
needs a parametric remesh, refused with the reason), and not for an
outcropping sheet — its trace chain must stay on the boundary complex
verbatim, so blind (setback) and interior placements only.

The all-rim split-safety pass is factored out (_split_safe_triangulation)
and applied to the resampled triangulation too: a gmsh planar mesh of a
polygon produces corner faces with all three vertices on the rim just
as the clip's corner polygons did.

test_0861 gains the workflow test: a deliberately coarse 3x3 authored
sheet placed blind with size=0.1 refines to gmsh-quality faces (min
quality > 0.3 asserted), keeps the blind rim exactly on the offset
plane, and splits with the Plus side carrying every face. Suites
unchanged (0854/0859/0860/0861 22/22, spike green).

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* Point-to-sheet sweeps cost the near-band, not the domain (#613)

A reach-aware spatial hash (_reach_query: points binned per octave of
their own threshold) and a culled distance (_sheet_distance_within:
exact wherever it is below the per-point reach, sentinel above it)
replace the whole-domain per-triangle sweeps at the placement's nine
hot sites — the gather marks, the carve's victim distances, its
straddle sweep and centroid rule, in the sheet and volume paths alike.
Per-point reaches matter: the thresholds scale with the local cell
size, so a single cutoff cannot cull a graded mesh. Every caller
thresholds where it reads the distance, so decisions are identical —
verified: the placed mesh is bit-identical on the 452k-cell
middle-ground benchmark, and place_sheet drops from 555 s to 67 s
(the 446 s _sheet_distance share to under a second; the sewn rebuild
is now the placement's largest piece at 30 s).

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* edge_split adapt reads each pass's topology once (#610 tier 1)

Three repeated whole-mesh walks removed from the adapt loop, none
changing a single cell of the output (bit-identical child on the
452k-cell benchmark):

- cell_diameters(return_tables=True) hands its cell-edge and
  edge-length tables to bisect_longest_edges, which was re-deriving
  both in the same pass — the per-cell closure walk is the loop's
  dominant Python cost and was paid twice per pass (309 sweeps -> 155).
- cell_diameters' per-cell max is vectorised (a simplex has a fixed
  edge count, so the ragged list stacks).
- The MG level selection re-measured every retained generation's
  5th-percentile diameter at the end — 76 more topology walks — when
  the marking pass had just computed exactly those numbers; they are
  cached per-dm and passed as resolution_hint.

Adapt on the benchmark: 658 s -> 551 s. The remaining ledger is the
engine surgery recorded on #610 — per-pass clone + label writes inside
the split call (242 s), MG parent/prolongation maps built per pass
(107 s), independent-edge selection (54 s) — the plan-in-numpy /
apply-back-to-back and multi-edge-template items.

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* MG parent maps are built for the retained levels, not per pass (#610)

The subsampler was already discarding every per-pass parent map whose
span covered more than one pass — nearly all of them — so ~80 s of the
benchmark's 107 s of map building produced maps that were thrown away,
while the retained multi-pass levels got None and the any-degree
transfer fell back to the geometric builder. The engine loop now
records nothing; the subsampler derives parents for the 3-4 RETAINED
pairs from the composed vertex transfer (nested_cell_parents is
topological through the transfer, and a descendant's referenced coarse
vertices are corners of its ancestor at any depth), so the deferral is
also an upgrade: multi-pass levels now carry exact parents.

Measured on the 452k benchmark, bit-identical child: parent-map cost
86 s (x75) -> 7.8 s (x9); adapt 551 -> 473 s. The bisect interior was
also split with instrumentation: of its 242 s, the native transform's
setUp+apply is 187 s (~2.5 s per application — the per-pass C cost is
honest, the x76 pass count is the waste) and the independence pruning
54 s; clone and label writes are negligible. The remaining #610 levers
are therefore pass-count reduction (multi-edge templates, C-side) and
the plan-in-numpy replacement of the per-pass Python (~170 s).

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* adapt-on-top skill: the h_far-below-base-diameters trap

The metric's far clip must sit above the base mesh's measured cell
DIAMETERS, not its nominal gmsh cellSize: diameters run 1.2-2.5x the
target edge length, edge_split marks on diameter, and a clip below them
refines the entire domain once — wasted cells AND a measured
de-conditioning of the far field (median shape quality 0.372 -> 0.295),
which every later adapt and solve inherits. Found by eye in the viewer
(scattered one-level patches across the globe), confirmed by
measurement, fixed by h_far >= 1.05 * cell_diameters(base.dm).max().

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* cells_supporting reads a facet's support directly, in any dimension (#620)

The zone walk went through _cells_on_edge, an EDGE walk: correct for a
2-D facet (an edge), one level too low for a 3-D facet (a face), where
support-of-support lands on nothing and the zone comes back all-False,
silently. The method had only ever seen 2-D meshes. Facets now take
getSupport directly — the cells, in any dimension — and the edge walk
is kept for labelled edges (a trace chain).

Fixes #620.

Underworld development team with AI support from Claude Code

* add_conforming_sheet: the 3-D cut-at-the-finest-level, with the tail

The Mesh-level form of place_sheet, mirroring add_conforming_surface:
the sheet is cut at the finest level ONLY, and the child inherits this
mesh plus everything below it as its coarse multigrid tail — the coarse
levels carry neither the cut nor the label, and do not need to (the
Galerkin coarse operators are formed from the fine operator; measured
in add_conforming_surface's note). The adoption — wrap, bookkeeping,
and the "a cut earns a level only if it is genuinely finer" subsample —
is extracted from add_conforming_surface into _adopt_cut_child and
shared, so the two dimensions cannot drift.

The method takes explicit (points, triangles, name): a sheet is DATA —
a slab model, an authored parameter-space triangulation — whose
connectivity must be embedded verbatim, and FaultSurface re-derives its
triangulation so it cannot carry an authored one. setback (blind
faults, surface_trace in child._surface_info), size (resample to match
the mesh being cut) and clearance pass through to place_sheet.

test_0862 mirrors the 2-D contract of test_0844: the tail composition
and the replace-not-stack decision, the named boundary and its zone,
the untouched base, chaining, the duplicate-name refusal, and a solve
with a per-cell contrast across the sheet consuming the cut mesh as it
stands.

Underworld development team with AI support from Claude Code

* The fault-network place route inherits the adapt hierarchy

_build_3d(mesher="place") placed each sheet at DM level and wrapped the
result in a bare Mesh, so the adapt child's multigrid tail died at
placement and every solve on the composed mesh fell back to algebraic
multigrid — not by design, but because the handoff that
add_conforming_surface performs in 2-D was never written here. The
route now chains add_conforming_sheet, so each cut child inherits the
hierarchy; the split at the end still forfeits it (see add_fault), but
the intermediate cut mesh now carries the tail that split-aware
multigrid would need.

The recorded place-route pathology (solve 3850 s vs 125 s embed, on
edge_split children) predates this handoff and wants re-measuring on
top of it.

Underworld development team with AI support from Claude Code

* The place route's recorded pathology re-measured: cell count, not operator health (#621)

The 3850-vs-125 note said the composed chain's solve was pathological on
adapt children. Measured again with the tail handoff in place: 27x the
cells embed builds for the same nominal sizes, 41x the time — which is
proportionate under 3-D Stokes scaling, with comparable per-cell cost,
one nonlinear iteration, machine-zero leak and agreeing slip. The
over-build is the route's own sizing (base at cellSize=h_far with
refinement=1 puts the far field at h_far/2). Docstring restated; the
sizing fix is #621. Companion measurements: #622 (custom-P V-cycles
across a cut level cost 7x more than GAMG saves), #623 (fill volume
drift on steep gradings).

Underworld development team with AI support from Claude Code

* Skip the custom-P re-install when the hierarchy is already live (#622)

inject_custom_mg re-ran build + install on every solve: a duplicate
Jacobian assembly (done only to make the fieldsplit reachable), a PCMG
reset + Galerkin setup, and a from-scratch geometric transfer build —
measured at 55 s of every repeat Stokes solve on an 85k-cell cut child,
while the transfers depend only on the meshes and PETSc re-Galerkins
changed operator values at PCSetUp by itself. The guard checks the LIVE
PC (the managed block exists, is a PCMG, carries the hierarchy's level
count) against a marker written at install; any doubt re-installs — the
cost of a wrong False is one redundant install, the cost of a wrong
True would be a solve on a stale PC.

Measured (place_route_health benchmark): repeat-solve overhead 55 s -> 0;
a genuine solve (operator values changed) keeps mg:4its on the velocity
block against gamg:46 and now comes in ahead on wall time as well
(284.6 s vs 307.7 s with the fgmres outer).

Underworld development team with AI support from Claude Code

* The Stokes OUTER Krylov is fgmres: the flexible-outer rule reached the sub-blocks but never the top (#624)

SNES_Stokes_SaddlePt sets both fieldsplit sub-solves to fgmres but
never pushed an outer ksp_type, so every saddle-point solve ran PETSc's
default plain gmres around variable-iteration Krylov inner solves — a
non-constant preconditioner under which standard GMRES's recurrence
does not hold, the same reasoning as the velocity-block FGMRES note
(#147) one level up. Measured on a genuine 85k-cell contrast solve:
14,400 inner iterations / 543.9 s under gmres; 307.7 s under fgmres
(gamg velocity), 284.6 s (guarded custom-P). Pushed as a managed
option in both the constructor and the strategy setter, so an explicit
user ksp_type still wins.

test_0203's lower-bound test asserted a STRICT undercount on the first
solve's velocity count — an artifact of left-preconditioned gmres,
which burned one full preconditioner application before its first
monitor fired. Right-preconditioned fgmres forms the unpreconditioned
residual first, so the bound is now tight; the test asserts the
contract (second >= first, plus the honesty flag) instead of the
artifact.

The remaining genuine-solve bottleneck is the pressure sub-solve at its
200-iteration gasm cap, silently unconverged — #625.

Underworld development team with AI support from Claude Code

* custom_mg's rbf builder is the standard sparse local interpolator (#429), plus a zero-column repair for placed levels

The "rbf" transfer builder assembled and solved the GLOBAL dense
coarse-cloud kernel matrix and returned nnz/row == n_coarse — dense
Galerkin coarse operators, no conditioning path, a rescue that could
not scale (#429, agreed long ago and never landed; the interpolation
note recorded it as "Not rewritten"). It now delegates to the standard
kd-tree local linear-exact interpolator (kdtree.interpolation_matrix,
order=1) — the same polyharmonic kernel and reproduction guarantees,
sparse support, the interpolator the rest of the code uses.

A row-wise kNN builder guarantees nonzeros per ROW, never per COLUMN
(#424): on NON-NESTED level pairs — two independently placed meshes
(#626), a relaxed child — a coarse DOF outside every fine stencil gives
an empty column and a singular PtAP. The serial build loop now repairs
such columns by nearest-fine-DOF injection (weight 1, same component),
warns with the count, and still refuses if any column stays empty.
These are preconditioner transfers, not the discretisation: an injected
row costs iterations at worst, never correctness. First measured use:
the split-finest-over-unsplit-band ribbon hierarchy (6 of ~50k columns
repaired; converged, machine-zero leak, answer unchanged).

Suites: test_1015 / test_1016 / test_0753 — 28 passed, 1 pre-existing
conditional skip.

Underworld development team with AI support from Claude Code

* The rotated PCMG coarse solve is SVD only when rotation modes exist (#622)

The rotated custom-P path hardcoded coarse="svd" for the velocity
PCMG because a free-slip enclosure's Galerkin-coarsened velocity block
inherits the rigid-rotation null modes and redundant/LU zero-pivots on
them. But nsp being non-None cannot discriminate — it also carries the
constant-PRESSURE mode (enclosed domains), which lives outside the
velocity block — so a Dirichlet-walled problem with no rotation modes
still paid a DENSE SVD factorisation of a 3-D P2 coarse level.
_rotated_nullspace now records the count of VERIFIED rotation modes on
the solver, and the coarse solve is SVD only when that count is
nonzero; the fallback (count unknown) keeps SVD whenever nsp exists,
so free-slip shells are untouched.

Measured on the split-fault contact benchmark: the SVD was NOT the
dominant cost (the per-cycle weight is the level-smoothing footprint
of a localized-refinement hierarchy — the background rides at full
size on every band level — recorded on #622); the gate is correctness
of configuration, not the headline saving.

Suites: 0844 / 0845 / 1015 / 1016 / 0203 — 63 passed.

Underworld development team with AI support from Claude Code

* Mesh._cell_node_indices: which DOF rows belong to which cell

The multigrid transfer needs more than DOF coordinates. To evaluate a coarse
Lagrange basis inside a parent cell it must know WHICH rows of
_get_coords_for_basis are that cell's own nodes -- and the section that answers
that lives on a coordinate DM _get_coords_for_basis builds and then destroys.

_basis_coordinate_dm is extracted from _get_coords_for_basis (second occurrence
of the same construction) so both read the same layout, and _cell_node_indices
walks that DM's local section over each cell's transitive closure. Degree 3,
where an edge carries two DOFs, falls out of the offset/ndof expansion; a
discontinuous space hangs every DOF off its own cell and needs no special case.
Non-simplex meshes are refused explicitly: a tensor-product Q_k cell carries
(k+1)^dim nodes, so the total-degree monomial basis the transfer builds would
not be square against them.

Cached beside _coord_array and cleared with it -- both describe the same node
layout, and it is the DS re-creation, not node motion, that invalidates either.

Tested geometrically rather than against itself: every node a cell claims must
lie inside that cell, checked with barycentric coordinates built from the cell's
vertices, at P1/P2/P3 in 2D and 3D for both continuities. A wrong section offset
points at another cell's node, which is somewhere else in the mesh, so it cannot
pass. Also asserted: node counts equal C(k+d, d), continuous rows are all
covered, discontinuous rows partition cell-block-contiguously (the layout
_build_kd_tree_index_DS already assumes), and Q_k is refused.

Underworld development team with AI support from Claude Code

* Exact nested transfers for native refine() pairs, structurally-clean geometric transfers, and FAC patch smoothing (#629)

Item 1 of the #629 program, plus the fill mechanism it exposed. A level
pair that is a native refine() pair (tagged by _coarse_level_meshes via
hierarchy-slot sentinels) now gets the EXACT nested prolongation at any
polynomial degree: parent cells recovered topologically from the two DMs,
weights by the dual-basis identity W = B M^-1 under the parent's affine
pullback (the #425 construction; verified exact to 1e-15 at P1/P2 in 2D
and 3D). PETSc's DMCreateInterpolation general path was measured NOT to
be the embedding (row sums to 1.375, quadratic reproduction error 1e-2)
and is not used.

The larger density win turned out to sit beside it: the point-located
builders emit ~1e-16 junk weights that are STRUCTURAL nonzeros, and
Galerkin RAP fills by structure, compounding level over level. Dropping
them (_drop_structural_zeros) collapsed the split-contact benchmark's
Galerkin chain from 319/481/265/90 nnz/row to 138/119/173/90 — the item-1
acceptance band — and the warm full-tail contact solve from 97 s to 52 s
at bit-identical answers (slip 0.1509, leak 0). Full-tail iterations
reproduce the recorded 6-7 under the robust smoother (the 6-its dial row
was gmres/4; the script's fast row runs 12).

Item 2: FAC/MLAT patch-restricted smoothing. Each level's patch is read
off its own transfer (identity row + bit-coincident node = background,
per NODE so the rotated path's per-node Q cannot disturb it; slit
duplicates count as patch; halo = one coarse-cell layer through the
transfer graph), stashed on the hierarchy, and consumed by
_configure_pcmg: the level smoother PC becomes ASM with that single
subdomain. Three configuration findings are load-bearing:

* BASIC, not restricted, ASM: discarding the halo correction stalls the
  outer KSP at 80-375 iterations where basic runs 6 against a
  whole-level baseline of 4 (banded Poisson, 4-level tail).
* The subdomain solve is SOR, the patch twin of the whole-level
  smoother: PCASM's default ILU-0 takes a NUMERIC_ZEROPIVOT on the
  rotated Galerkin patch block (min |diag| 8e-5 near the constraint;
  PC_FAILED -11 before the first iteration).
* The rotated path must force the FAC levels' smoother setup before its
  options-DB cleanup: PCASM creates the sub-KSP lazily at first apply,
  after the keys are gone, so sub options silently never applied.

A uniform pair (patch = everything) declines to whole-level smoothing.
Measured on the split-contact ribbon: correct answers with rotation and
contact, but a net cost at THIS toy's proportions — the ribbon band is
66-82% of every refined level's DOFs, so there is no background to save
and the ASM gather/scatter is overhead (warm 60 vs 52 s). The FAC win
requires production proportions (thin patch in a large domain), which is
where the item-3 contrast test goes next. The UW_FAC_* / UW_CUSTOM_MG_*
environment knobs are TODO(MEASURE)-marked A/B affordances for that
campaign.

Tests: test_1017_nested_native_and_fac.py (exactness by degree-k
reproduction, tag discipline, split classification with a geometric
oracle, end-to-end ASM solve vs GAMG); test_1015/1016/0753/0846/1014/
1017/1018 suites green.

Underworld development team with AI support from Claude Code

* FAC strong patch solves restore contrast-independent V-cycles (#629 item 3 diagnosis)

The contrast sweep's FMG degradation (velocity 7 -> 10 -> 36 its over
1e0 -> 1e4, where constant V-cycles are the expectation) is DIAGNOSED
and FIXED at the smoother:

* Level ablation at 1e4: full/no_mid/two tails converge in 20/19/24
  its — the coarse corrections contribute nothing under zonal contrast;
  the weak-band modes are invisible to every coarse space, including
  the band-carrying mid level.
* Doubling the smoother (gmres/8 vs /4) exactly halves the count
  (20 -> 10): convergence is proportional to total fine-level smoothing
  work — the signature of smoother-limited band modes.
* FAC with a DIRECT subdomain solve (sub_pc lu) on the patch — which
  contains the weak zone — collapses the count to 2 its at 1e4 and
  3 at 1e6: V-cycle constancy across six decades of contrast, below
  the flat-viscosity count (7).

Two supporting changes: factorization sub-solvers get
sub_pc_factor_shift_type=nonzero (the rotated Galerkin patch block
carries near-zero pivots from the constraint-zeroed transfer rows —
unshifted LU takes NUMERIC_ZEROPIVOT even as an exact factorization),
and UW_MG_SMOOTH_ITS joins the TODO(MEASURE) campaign knobs.

Remaining engineering to make the strong-patch cycle cheap: shrink the
subdomain from the whole refined patch to the weak zone + halo, and
reuse factors across cycles; the wall-time gate at contrast stays the
pressure gasm cap (#625).

Underworld development team with AI support from Claude Code

* A physics-keyed strong-patch mode: UW_FAC_PATCH=slit takes the split-duplicated nodes, not the refinement patch (#629)

The refinement patch conflates the smooth refined bulk (well served by
ordinary multigrid) with the fault zone (the only set needing the strong
solve). The slit mode keys the subdomain on the split-duplicated nodes
with operator-sparsity overlap as the halo — proportional to the fault
trace, not the refinement. Validated on the 2-D line rig: constant 6-7
velocity iterations across six decades of viscosity contrast on a 5%%
subdomain, against a GAMG control that degrades to its iteration cap.

Underworld development team with AI support from Claude Code

* Slit patches detect coincident FINE pairs, and split into per-segment ASM blocks (#629)

Two corrections from the 2-D campaign. The slit detection keyed on
transfer identity rows onto a coarse node — empty for a cut mesh, whose
slit vertices are NEW points at edge crossings coinciding with no coarse
node, so the mode silently declined to whole-level smoothing (caught by
a k-sweep flat at plain-FMG counts with no ASM in the probe). The trace
is now the coincident FINE pairs — the split's plus/minus nodes at
bit-identical coordinates — with the transfer-dup rule kept as a union.

A patch entry may now be a LIST of (owned, subdomain) blocks and
_configure_pcmg installs one ASM subdomain per block; UW_FAC_SEGMENTS=k
splits the trace into k along-strike chunks (by the coordinate along the
trace's leading principal component) for the segmentation experiment.

Measured on the 2-D rig (contrast 1e4, shifted-LU blocks, overlap 1):
7 velocity iterations at k = 1, 2, 4, 8, 16 — block-wise segment solving
is FREE; across-fault stiffness is block-local and the along-fault mode
is smooth, so the ordinary coarse ladder carries it. k=16 at contrast
1e6: still 7. And with a strong lid (eta 1e4, jump OR smooth profile)
crossed by the 1e-4 gouge — 1e8 across the fault walls — still 7/7,
against GAMG 88/80 and patchless FMG 16/16: the extended co-dim-1 lid
jump is benign for the Galerkin ladder; the malignant sharp feature is
the thin cross-cutting gouge, which is exactly what the patch owns.

Underworld development team with AI support from Claude Code

* Zone-keyed strong patches: painted fault models need no split to be patch-solved (#629)

The patch SOLVE is rheology-agnostic — ASM blocks + shifted LU over a
row set, blind to the constitutive model — and only the DETECTION was
split-specific (coincident fine pairs). A painted weak or TI band has no
topology to detect and needs none: the modeler painted the cells. A
boolean cell mask on the solver (solver._fac_zone_cells) now keys the
finest level's patch to those cells' DOFs directly.

Measured on the 2-D rig, UNSPLIT painted weak band (the weak-fault
model, standard Stokes path): zone-patched FMG holds 9 its at contrast
1e4 and 7 at 1e6 against GAMG's 40-and-degrading — the same constancy
the split-contact runs show, with no split anywhere. The fault
representation (split / weak / TI) returns to being a physics choice;
the solver architecture is indifferent to it.

Underworld development team with AI support from Claude Code

* The finest patch always contains the STRUCTURAL patch: zone blocks union the non-identity rows (#629)

The patch smoother REPLACES whole-level smoothing, so it inherits every
row the coarse level cannot represent — the cut/split-inserted DOFs the
transfer's non-identity rows identify — whether or not the physics zone
covers them. A zone away from the cut left those rows smoothed nowhere:
the velocity sub-solve capped at 200 iterations on every application
(measured on a V-junction, on parallel strips, and on a single band
crossing the cut, while zones ALONG the cut worked by accident of
geometry; the overlap-0 slit stagnation was the same omission). The
zone-block builder now appends the uncovered structural rows as one
more ASM block.

Confirmed: V-junction 8 its at contrast 1e2 and 1e4, merged or
per-segment blocks; parallel strips 8 its — all previously stagnant,
and the junction itself is a NON-EVENT. The pressure sub-solve also
returns to 14 iterations from its 200 cap once the velocity PC is
whole: part of the recorded pressure-cap pathology (#625) is downstream
of an incomplete velocity preconditioner.

Underworld development team with AI support from Claude Code

* Design note: fault-patch multigrid — the measured skeleton and parallel rules for 3-D (#629)

The architecture (ordinary multigrid + physics-keyed strong patches),
the evidence, the configuration rules each learned from a failure —
led by the structural-patch rule — the scaling statement, and the
parallel design rules for the 3-D deployment.

Underworld development team with AI support from Claude Code

* The ribbon is part of the FMG (design ruling, #629)

The placed ribbon joins the multigrid design in three roles: the bridge
level structure between the standard mesh and the patch, the AUTHORED
damage-zone identifier (the placement zone label survives the split as
cell children — never a distance-mask staircase or a cells_supporting
zipper), and one mesh discipline for split, TI, and weak fault
representations alike. Junction default: split segments a short
distance apart, the intact gap is the linkage; painted cores are
opt-in physics. Open engineering: partition weighting for the band and
split-pair co-residency in one mechanism.

Underworld development team with AI support from Claude Code

* Pair co-residency is automatic under local-frame splitting (design correction, #629)

Louis's correction: split surgery runs in the local frame after
distribution, so a pair is born on its parent facet's rank and cannot
be separated — no partitioner constraint exists. The real rules: never
distribute a pre-split mesh (the slit is a zero-cost graph cut, so a
partitioner would preferentially separate the sides while the contact
coupling lives outside the graph — gate that pipeline), and keep the
star forest consistent where a fault crosses a rank seam along strike
(the known np>=3 line-cut item). Ribbon balance reduces to ordinary
cell-count weighting.

Underworld development team with AI support from Claude Code

* What the fault ribbon is for (design ruling, #629)

The finite-width ribbon is a modelling object in its own right, with
three sanctioned physics: a damage zone (when a damage evolution
equation is solved — damage is a field, not a paint), a nonlinear
plastic/yielding material (so failure patterns EMERGE in the resolved
band — the mechanism by which junctions form rather than being
authored), and a permeable zone for fluid flow. A hand-painted static
weak viscosity is none of these — neither gouge nor a fault. The fault
itself is always the split surface in the mesh; ribbon physics
complements the slip surface. With none of the three physics present,
the ribbon carries background rheology and is purely resolution. All
roles compose with the solver design unchanged.

Underworld development team with AI support from Claude Code

* The meaning of w follows from the fault representation (design clarification, #629)

A fault may legitimately be REPRESENTED as a weak / TI weak zone
carried by the ribbon — then w is a PHYSICAL parameter (the fault-zone
width), chosen appropriately and resolved by ~2 elements across. In a
split-node model, w is a mesh-bridging convenience with no physical
reading, and nothing rheological may be keyed to it. The recurring
campaign error was mixing the readings: split nodes plus band-wide
weakness with w chosen as a mesh number is neither model.

Underworld development team with AI support from Claude Code

* #629 productionizing: fac_zone API, #589 fixed at source, ladder band in-repo, composed benchmark test

The fault-zone patch key settles into API: set_custom_fmg(...,
fac_zone=mask | [masks]), validated against the finest mesh at
registration; the solver._fac_zone_cells attribute spelling is retired
loudly (setting it raises rather than declining silently — the #629
campaign's sharpest instrumentation lesson).

The #589 empty-stratum getIndices() segfault is fixed at source:
utilities/dm_labels.py provides label_stratum_indices() gated on
getStratumSize (safe on both the null-IS wrapper and values outside the
live set), and the dead `is None` guards in nvb/reconnect/fault_split —
petsc4py returns a non-None NULL-handle wrapper, so they never fired —
are routed through it. mesh.cells_labelled(name, value) is the
empty-safe cell-mask accessor for placement labels (the natural
fac_zone builder).

The transfinite ladder band (#595: three nodes across, rails + exact
centreline, mandatory for spine cuts) moves in-repo from the campaign
scripts as place_thin_volume(..., mesher="ladder").

tests/test_1022_composed_ribbon_fmg.py (tier B) enshrines the composed
2-D benchmark of record: native level densities, the 2-block finest
patch (zone + automatic structural union), iteration bounds, slip/leak
invariants. Negative control verified (FAC disabled fails the block
assertion). The benchmark reproduces bit-for-bit on the in-repo path;
the 3-D pure-contact composition measures 7 velocity iterations vs
GAMG 56 with identical physics (design note updated).

Underworld development team with AI support from Claude Code

* The patch-keying ruling: fac_zone is for volumetric fault zones only (#629)

A split-node fault is the efficient fault representation and runs no
zone patch: the structural patch is automatic where it matters, and
under pure contact the strong patch is redundant outright — measured in
3-D, velocity iterations are 7 with the zone patch, 7 with the
structural patch, and 7 with no finest patch at all (the cover gate
declines the band-shaped patch of the non-nested placed pair). The
fac_zone key is reserved for volumetric representations — weak / TI /
damage / gouge rheology, where the zone width is physics — and the
ribbon is never the key: it is resolution, not rheology.

The design note records the ruling under patch keying, plus the answer
to why 2-D and 3-D economics differ: the 2-D ladder's band levels nest
by construction (36 -> 72 rungs, an exact 2:1 transfinite pair), so
placed pairs behave like native refinement; the 3-D ribbon layers are
independent unstructured fills at unrelated sizes and share nothing,
which is what fattens the Galerkin chain and prices the V-cycle. The
3-D fix, when wanted, is nesting the band levels, not more patch.

test_1022 restructured to the ruling: test 1 enshrines pure contact
with the structural patch alone; test 2 covers the fac_zone union
machinery and the loud retirement of the _fac_zone_cells spelling.

Underworld development team with AI support from Claude Code

* The 3-D ladder band: extruded prism-tets from the fault sheet, no remesh (#629)

place_thin_volume(mesher="ladder") in 3-D takes the fault surface's own
structured discretisation — a (grid, normals) pair — and offsets it
±width/2 into two prism layers split to tets (Dompierre subdivision;
quad-diagonal compatibility proven against the analytic skin count).
The mid-surface is a real vertex sheet, so the slit is coordinate-set
selection with no plane test, and a 2:1 subsampled grid's band shares
every vertex with the fine band through placement and split. Exact
where exactness is checkable: planar volume to 1e-12; an over-curved
extrusion is a refusal, never a reorder.

Measured on a curved fault (sinusoidal bulge, campaign rig): physics
identical to GAMG, 6-7 velocity iterations against 60-65. The corrected
diagnosis, recorded in the design note: vertex nesting alone does not
collapse the Galerkin chain — the unstructured FILL SHELL between band
skin and coarse background is the node majority (51% of finest P2
nodes, nesting 3%), and vertex nesting is not P2 nesting (10% inside
the band). Thinning the shell (clearance floor) brings level 1 to
native density. The residual warm-time gap is the rotated path's
per-solve transfer rebuild (the un-guarded rotated twin of #622) — the
prerequisite fix for wall-clock claims on the contact chain.

Geometry tests appended to test_0855 (volume, curvature refusal,
nesting, input contract).

Underworld development team with AI support from Claude Code

* place_fault_ribbon: the one-call fault-ribbon production path (#629)

The user supplies the fault surface as their own structured point grid
— curved is fine — and one call thickens that grid into the resolved
band (extruded prisms, no remesh), embeds it, labels the mid-surface
(coordinate selection + rim erosion, now in-repo as
_label_mid_surface), splits it into a frictionless-ready fault whose
label is a boundary for add_fault_bc, and builds the nested 2:1 unsplit
bridge level for the multigrid hierarchy. One parametrisation
throughout: normals default to grid-derived and the mid level always
subsamples the same field, so the levels nest by construction and the
inconsistent-normals footgun cannot be expressed. Defaults follow the
measurements (clearance 0.3, the thin fill shell); split=False keeps
the labelled unsplit mesh for painted weak/TI models on the same
geometry. The 3-D ladder assembly also accepts a bare grid now.

The campaign rig, rebuilt on this path, reproduces its record
bit-for-bit (21,352 cells, 1,150 slit faces, 675/675 nesting, level
densities 103/84/151/93, velocity 6 iterations, slip 0.1523).
End-to-end wrapper test added to test_0855.

Underworld development team with AI support from Claude Code

* Correction + re-baseline: the rotated transfer cache exists; the warm lever is level economics (#629)

The design note's claim that the rotated/contact path rebuilds its
transfers every solve is retracted: it described the pre-fix state
from earlier in the campaign and was repeated without re-measuring. A
cProfile of the repeat solve shows the cache working end to end — the
geometry tier of _rotated_linear_cache carries the rotation and the
custom prolongations, the context carries the KSP/PC, and the operator
tier skips assembly and PCSetUp on an unchanged matrix; 46 of 48
seconds are inside the composite Krylov solve, with no build anywhere.

The warm cost structure is the Schur loop times the V-cycle price
(full factorization = one velocity solve per pressure iteration), and
the measured lever is tail depth: on the curved-ladder stack, dropping
the near-fine mid level reaches GAMG parity warm (24.0 vs 23.0 s) at 7
velocity iterations against GAMG's 65, with the remaining transfer
chain entirely native density — the Galerkin fat was the mid level
alone. The mid level's role is production proportions; at rig
proportions the tail of choice is [L0, L1] + finest. Physics identical
in every arm.

Underworld development team with AI support from Claude Code

* The AL penalty pays at gamma=1 on the ladder stack (#629, the #625 mechanism)

Measured on the parity-tuned [L0, L1] + finest curved-ladder stack,
warm repeats, leak machine-zero in every arm: gamma=1 shortens the
pressure loop 22 -> 17 with the velocity count unmoved (7), taking the
warm solve to 19.1 s against GAMG's 23.0 — FMG ahead outright at flat
viscosity. gamma=10 over-stiffens the velocity block (7 -> 12) and
reverses the gain. No Schur plumbing was needed: the penalty is
viscosity-scaled by construction, so penalty/mu is uniform and the 1/K
pressure-mass preconditioner remains spectrally correct.

Caveats recorded with the result: under penalty the recovered p is the
Lagrange multiplier (p_mech = p - lambda*mu*div_u for any
pressure-dependent rheology), and derived quantities drift with gamma
(peak slip 1% at gamma=1, 3% at gamma=10 at rig resolution — the same
bias family as #633's vertex-sampled dynamic topography). gamma=1 is
the sanctioned choice.

Underworld development team with AI support from Claude Code

* place_fault_ribbon honours the requested fault: the tip margin is extrapolated, never confiscated (#629)

A fault specified from a structural model has its extent as data; the
wrapper previously took the grid as the band and labelled the slip
surface inset_rings inside it, so the requested fault came out smaller
than specified. Inverted: the grid IS the fault, labelled on precisely
the supplied points, and the band is built on the sheet continued
margin_rings rings outward along its own end tangents (_extend_grid —
linear per ring, corners consistent, normals continued the same way
and renormalised; no curvature the data never asserted). The 2:1
bridge level subsamples the extended parametrisation, so nesting is
unchanged. margin_rings >= 1 is enforced: a split may never reach the
band rim, and the margin comes from invented surround.

Measured on the curved rig (29x29 fault, margin 2): 1,566 of the 1,568
requested triangles split — only the two corner triangles erode (the
splitter's no-interior-vertex refusal, a half-cell nick) — nesting
867/867, chain 95/80/95 nnz/row, velocity 8 iterations. Peak slip
0.1737 against 0.1523 under the old inset: the confiscated rings were
biasing the physics by 14%, and the honoured value matches the
full-patch embed reference (~0.174). GAMG parity exact (slip 0.1737,
leak 2e-17, 70 its vs 8).

Underworld development team with AI support from Claude Code

* The curved 2-D ladder: numpy rails for bent traces, nesting by shared parametrisation (#629)

The 2-D ladder was transfinite and straight-only; the S-fault rig
(San Andreas bend + through-line branch, designed with Louis) needs
bent traces. _ladder_curved_assembly_2d is the 3-D extrusion one
dimension down: the polyline resampled equispaced in arclength, offset
±width/2 along mitred vertex normals (constant width through turns,
sharp turns refused), three rails of shared vertices, alternating
diagonals, mixed-orientation triangles a refusal. No gmsh, no CAD.
Straight polylines keep the gmsh transfinite path, so the recorded
composed benchmark stays bit-identical (verified).

The nesting contract carries over from 3-D and is enforced by API
shape: coarser levels must SUBSAMPLE one fine parametrisation — the
assembly accepts precomputed (samples, reach) for exactly that, and
independently recomputed reach vectors were measured to break rail
coincidence (only the spine nested). With the shared parametrisation:
132/132 mid band vertices coincide on the rig's tanh S trace.

Underworld development team with AI support from Claude Code

* place_fault_ribbon_2d: the S-fault rig's fault-network prep, one call (#629)

The 2-D production path with the full contract set: each supplied
polyline IS a fault, its points becoming the band's spine vertices
verbatim (the curved ladder's (samples, reach) patches — one
parametrisation per trace, extended margin_rings by tangent
continuation); traces are placed sequentially as stop-short strands
(the junction ruling) and cut+split in ONE add_fault network call —
chained calls were measured to drop the earlier fault's pairing
records. The (samples, reach) tuple patch is wired through the 2-D
ladder hook with the domain gate shared between paths.

visualisation/glyphs.py brought over from feature/stress-glyphs
(PR #601) for the rig's standard figures: dCFF slip-vs-welded and
principal-stress trajectory nets over a fine grey mesh.

Validated on the S-fault rig (tanh S + through-line branch, the
geometry designed with Louis): 1,858 cells, native chain (24-27
nnz/row), sub-second warm solves, machine-zero leak on both strands,
first partitioning number branch/main = 0.41, locked welded state
reproducing uniform shear to 2.6e-5. Tests: curved-ladder nesting +
refusals, and the two-strand wrapper with honoured trace vertices.

Underworld development team with AI support from Claude Code

* uw-visualisation skill: grid-resampling dapples at element boundaries — render nodally

Louis's catch on the S-fault rig's dCFF panel: resampling a recovered
P1 field onto a regular pixel grid via uw.function.evaluate produces
artefacts across the elements — grid points that straddle a facet get
located into a neighbouring cell with slightly-off reference
coordinates. The rule added to the skill: render derived fields
NODALLY on the mesh's own triangulation (vertex evaluation is exact
for P1 whichever cell the locator picks; VTK interpolates within
elements), and on a split mesh never Delaunay the DOF cloud — it
re-triangulates across the slit.

Underworld development team with AI support from Claude Code

* Review fixes for #638: the honoured-paint rule becomes API; duplicate trace labels are refused

The fault-footprint mask is now returned by both wrappers instead of
living only in documentation: place_fault_ribbon reports
info["footprint"] and place_fault_ribbon_2d reports per-label
info["footprints"] — band cells whose nearest extended-parametrisation
sample is a USER point, so painted rheology and fac_zone keys can no
longer silently extend into the extrapolated tip margin (the mistake
measured twice in the campaign: whole-band paint put tip lobes about
two elements past the mapped tips). The test is geometry-free — the
parametrisation itself carries the user/extension distinction — so it
works for any curved strand.

place_fault_ribbon_2d also refuses duplicate trace labels at the API
boundary rather than failing downstream with an ambiguous-boundary
shape inside the add_fault network call.

Both fixes carry assertions in test_0855 (footprints strictly inside
the band, subset of it, per strand; the duplicate-label refusal).

Underworld development team with AI support from Claude Code

* CI fixes for #638: the gated coarse-solve semantics in test_1021; worst-local-h headroom in test_0861

test_1021 asserted the pre-#629 blanket rule (the rotated coarse solve
is always svd). The campaign gated it: svd only when the rotated
problem carries a VERIFIED rigid-rotation null mode, because blanket
svd was measured as most of ~0.8 s per V-cycle (#622). The bundle
fixtures pin the inner annulus boundary, so no rotation mode survives
and redundant/LU is the correct coarse solve there — the assertions
now say so. The svd arm keeps its regression protection through a new
test: rotated free-slip on BOTH boundaries leaves the genuine rotation
mode, the builder must verify and record it, and the coarse solve must
be svd (this is exactly where redundant/LU hits its zero pivot, #306).

test_0861's two setback placements failed only on CI: with the 0.6
default clearance the carve cavity extends ~(clearance+1)*h_local, and
at the worst local h a different gmsh version deals this box that
overruns the 0.25 setback margin. clearance=0.3 (the measured
production thin-shell choice) keeps the cavity inside the margin at
worst-case h; the tests pass locally under both settings.

test_0054's failure is the CI-flaky hang-watchdog family (development's
own latest CI failed sibling test_0053); no change here — rerun.

Underworld development team with AI support from Claude Code

* The environment-armed watchdog arms after import, and the reporter never dumps sources (#638 CI, test_0054)

Root cause, established with native thread samples of two live hangs:
faulthandler.dump_traceback_later(repeat=True) walks live frames from
its C thread without synchronisation, and fired against a
still-importing interpreter that walk loops forever (locally: the C
thread pinned in dump_traceback for the whole sample window, the main
thread starved mid-import) or reads garbage and dies at SIGSEGV (the
CI -11). The env-armed watchdog was arming during `import
underworld3.mpi` — inside the very import it then dumped over. The
failure is conditional, which made it look flaky: it needs the import
slow (cold bytecode caches, as on CI or straight after a build) and
piped child output (as the test runs), and this branch's larger import
graph pushed the import past CI's 1.0 s trigger where development
stays under it.

Fixes, each a real hardening: (1) mpi.py preloads
traceback/linecache/tokenize so the reporter thread never enters the
import system; (2) _stack_dump formats with lookup_lines=False — file
names, line numbers and functions carry the hang report, and the
source-text reads were reporter-side IO against modules mid-import;
(3) report() re-arms its own Timer only — re-issuing
dump_traceback_later cancels the C thread and cond-waits on it
mid-dump (the sampled deadlock triangle); (4) the structural fix: the
environment-armed watchdog arms at the END of `import underworld3`,
never during it, trading self-coverage of the import graph for safety
everywhere the tool exists for (documented in
_watch_from_environment).

Validation: the previously first-run-reproducing context (piped
children after a fresh build, 0.2 s interval) went from 12/15 frozen
to clean in the test's own scenario; the actual test passes 6/6
locally. Known latent issue, pre-existing and out of scope: a natural
process exit while a repeat dump is in flight can hang finalisation
(the test SIGKILLs and never sees it).

Underworld development team with AI support from Claude Code
lmoresi added a commit that referenced this pull request Sep 1, 2026
…ork (#663)

* The general 3-D outcrop cap: per-region collar, crease overlay, smooth walls

The zone outcrop no longer needs an axis-aligned wall. The cap over the
bowl is re-triangulated PER COPLANAR BOUNDARY REGION, each piece meshed
flat in its region's own plane, so the cap stays exactly on the faceted
surface and volume conservation survives. Where the band crosses a
crease the two sides segment the line differently (mesh vertices against
assembly nodes), so a 1-D overlay merges both node sets by arc
coordinate and keeps each elementary sub-segment for the side(s) whose
bowl covers it and the band does not; a one-side segment must be a whole
mesh edge, because a cavity-shell face matches it.

The frame rule (_outcrop_frame_3d) separates the two notions the third
dimension forces apart. Where the cavity may OPEN is by SMOOTH WALL —
coplanar regions grouped across low-dihedral creases — so a faceted
sphere's bowl legitimately spills onto facets beside the band's own
while a box's other walls stay refused. What the carve may DELETE is by
shape: band-covered vertices, single-region interiors, and interior
vertices of straight creases between touched regions; a vertex where
three or more regions meet is the domain's shape and is protected. A
protected vertex whose whole cell star drops is not stranded: it is a
collar node, and the gap fill's tets reference it — that is how a
curved boundary keeps its faceting through the surgery.

Wall labels are restored per removed face (the 2-D per-segment rule one
level up): each new wall triangle takes the labels of the removed face
it lies on, so a bowl spanning several walls restores each wall's own.
The Euler gate now demands CONSERVATION of the input's Euler number
rather than 1 — a spherical shell is S^2 x I, Euler 2, and refused
before for its topology, not for any defect. The same assumption in
place_sheet and remove_embedded is marked TODO(BUG).

_trace_wall_code and its refusals are deleted; the box path runs
through the general machinery and the box oracle tests are unchanged.
Driven on a rotated box (no wall axis-aligned), a band across the
Top/Front box edge (trace on both walls), and a spherical shell outer
surface (75 trace facets, 6 vertices removed): volume conserved to
1e-12 in each.

Underworld development team with AI support from Claude Code

* Tests: the general 3-D outcrop on a rotated box, across a box edge, on a sphere

Three configurations, each the failure mode of a different assumption
the box-framed cap made: a rotated box (no wall axis-aligned; the
single-region collar), a band across the Top/Front box edge (the crease
overlay conforms the two sides' differing segmentations, and each
wall's labels are restored on its own side), and a spherical shell
outer surface (every facet its own region, every vertex protected
faceting — the case the trace design exists for). Each runs its
negative control first — the identical census on an interior twin
counts no trace — and each ends in the P2 Poisson oracle, exact for a
quadratic through the zone. Volume conserved to 1e-12 throughout; the
shell's Euler number 2 comes through the conservation gate.

The parallel file gains the rotated-box outcrop at np>=2 with the same
mesh and patch as the serial test: the info dict is identical on every
rank and the trace count matches the serial value (28 facets at
np=1, 2 and 4), so the general path is partition-independent. No
boundary face is left without a wall label at any rank count.

The spherical control sits mid-gap of a 0.75-thick shell: a one-cell
chain from a victim corner spans h, so an interior zone needs
~(clearance + 1) * h to spare on both sides — thinner shells refuse
interior zones at this resolution by the carve's own clearance gate.

Underworld development team with AI support from Claude Code

* A relabel refusal must be collective

The collar-vs-bowl consistency check raised on the surgery rank inside
the relabel block, which is outside any try — a hang at np>=2. It now
sets the block's failure and flows through the allgather like every
other refusal there.

Underworld development team with AI support from Claude Code

* place_sheet and remove_embedded conserve the domain's Euler number

The same gate place_thin_volume already fixed: demanding global Euler
number 1 encodes a ball-topology domain, and a spherical shell —
S^2 x I, Euler 2 — was refused for its topology rather than for any
defect of the surgery. Both gates now compare against the input mesh's
own number. Pinned by a regression test: a sheet embeds mid-gap in a
spherical shell and its removal clears the label again, both passing
their volume and conservation gates.

Underworld development team with AI support from Claude Code

* The assembly volume gate sits above OCC's boolean-mass noise

The gate catches unmeshed solids — an O(1) relative defect — but its
reference, OCC's getMass on the clipped boolean, is only accurate to
~5e-7 relative when the domain tool carries many faces (measured on a
16.6k-facet adapted spherical boundary: the honest mesh volume exceeds
the reported CAD mass before any snap runs). At 1e-9 the gate refused
correct assemblies; it now allows 1e-6, far above the kernel noise and
far below any real missing-solid defect.

Underworld development team with AI support from Claude Code

* The boundary snap covers OCC's placement noise, masked to the facets

OCC's boolean leaves clipped nodes up to ~4e-7 off the tool's own
planes on O(1) geometry against a many-faceted tool; the 1e-9 snap
missed them, so a crease-crossing node of the band outline was not
recognised as on-crease and the collar meshed a 2-metre sliver beside
the crease (measured on a 1000 km megathrust against an adapted
spherical boundary; the 2-D fill then refused with moved nodes). The
snap tolerance now sits above that noise and below any layer mesh
size, and candidates are masked by distance to the boundary FACETS
first — the planes are infinite, and at this tolerance every point in
space is near some plane of a many-faceted tool.

Underworld development team with AI support from Claude Code

* The 3-D imprint collapse; on-crease outline edges bound the far collar

Two mechanisms a 1000 km outcrop band forced, plus the diagnostic that
found them. _collapse_boundary_imprints_3d is the 2-D imprint collapse
one dimension up: where the band outline grazes a boundary vertex
(measured: a 2.4 m gap the collar meshed as a sliver and the fill
refused as moved nodes), the outline is rerouted THROUGH the vertex —
the nearest outline node moves onto it, or the outline edge splits at
it with every incident tetrahedron bisected. A move or split must keep
every incident cell's volume healthy, else the vertex is skipped and
keeps its sliver (the status quo, not a defect); the band still tiles
the same faceted surface, so the domain's shape is untouched.

An outline edge lying ALONG a crease is no longer refused when the
cavity covers both sides: the band reaches the crease there, so the
edge bounds the collar piece on the FAR side of the crease, not the
owning band triangle's own region. The refusal remains for a band
bounded by a crease with no bowl beyond it.

A collar piece that fails to mesh now names its pinch — the thinnest
node-to-segment gap and the node kinds — which is what separated this
sliver class from the OCC placement noise the snap fix covers.

Known limit, measured and deliberately not papered over: refining a
SPHERICAL boundary projects new facet vertices onto the true sphere,
and the creases between the resulting nearly-coplanar sub-facets are
laterally fuzzy at (placement noise)/sin(theta) — wider than the
band's own feature spacing, which defeats the crease overlay. A
widened, fuzz-aware overlay tolerance was tried and withdrawn: it
traded the sliver for misclassified chains on the honest-crease tests.
Outcrops on adapt-refined spherical boundaries stay refused by the
fill's own gates until the crease representation is rethought.

Underworld development team with AI support from Claude Code

* place_sheet clips, carves and caps against the mesh's own boundary

The sheet path joins the general boundary machinery: one clip, one
frame, one collar (maintainer ruling 2026-08-18 — multiple placement
paths are themselves the defect). The Sutherland-Hodgman box clip and
the wall-code frame are deleted.

The discrete clip primitive cuts the authored triangulation against the
gathered boundary complex directly: per-component outward orientation
(a shell's inner surface signs opposite to its outer), signed-distance
classification, sequential cuts by the COPLANAR REGIONS' planes — a box
wall cuts as one plane however it is faceted, or the two triangles
sharing a cut edge key their cuts by different facets and the sheet
tears (measured: a duplicated node on the box top wall). Side cuts are
re-derived from the original sheet edge's endpoints and interned by
(edge, region), so neighbours whose sides are truncated differently
produce the bitwise-identical node (measured: ~1e-17 duplicate pairs on
the sphere without it). A crossing that can touch a locally concave
crease (an inner boundary) refuses loudly — the polyline cut is not
built — and every kept node is gated inside the domain on exit.

The carve takes the frame's two masks (open_deletable / open_near) like
the volume's carve, with the same protected-collar-node rescue; the
outcrop frame accepts a trace CHAIN of edges as the footprint alongside
a band of triangles; and the collar embeds the chain through its pieces
instead of cutting a hole — chain nodes at crease crossings land ON the
crease and enter the 1-D overlay as 'a' nodes, the runs between
crossings embed per region (_gmsh_fill_2d now takes several polylines),
free ends as gmsh's ordinary free-end embed. A trace edge along a
crease, or off the bowl, refuses with the reason. The overlay's local
loop variable is renamed crease_chain — it shadowed the new parameter,
which made the volume path read a stale trace.

The trace chain's edges carry <label>_trace in the result, the wall's
labels are restored per removed face (the volume path's rule), and the
counts are gated collectively. Box oracle unchanged (test_0854, 8/8;
area matches Sutherland-Hodgman bit-for-bit in the spike); the sphere
outcrop that could not run at all now embeds with volume conserved and
the P2 oracle exact (test_0860); trace counts identical at np=1/2/4
(ptest_0854).

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* The daylighting acceptance test: slip at the trace, gated on the split

The hybrid-seam study measured the defect this guards against: a split
surface terminating against something that cannot slip ends as a free
crack tip, slip pins to zero, and the composite is worse than either
pure representation (74.6% vs 100/105%). The outcrop is that join one
level up, so the acceptance test is kinematic — measured slip > 0 at
the trace against a pinned control — not mesh gates alone.

The placement half asserts today: every trace edge on the wall AND
bounding a labelled fault face, the wall's labels restored beside it.
The kinematic half attempts split_along_label_3d through the wall and
SKIPS at its daylighting refusal — the body below the gate is the
acceptance criterion, ready to run when feature/fault-split-node learns
to duplicate the trace chain.

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* The snap test's control sits above the widened tolerance

The boundary snap's tolerance was raised to 1e-6 to cover OCC's ~4e-7
boolean placement noise (34ea1928), but the test's untouched-control
point stayed at 2e-9 — inside the new tolerance, so it snapped and the
test failed. The control moves to 2e-5: above the tolerance, below any
real feature scale. A branch push does not trigger CI, which is how
this shipped red.

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* Faults reach daylight blind, under a damage zone — the acceptance test

Ruling (2026-08-18): split-node faults do not split through the surface.
They stop an element or two below, blind, with a damaged region above
carrying the deformation to the surface — the junction lessons applied:
a contact tip inside weak material is free, a tip at the weak zone's
edge is pinned, and an abutting composite is worse than either pure
form. This supersedes the split-through-the-wall acceptance this file
first carried; the gated body that waited on a daylighting split is
removed.

The kinematic acceptance turns the listric handover rule vertical,
against a FREE surface, and is validated by running (probe in
~/+Simulations/blind_fault_surface_expression/): a blind frictionless
split fault two cells under the surface, solved bare / damage-abutting /
damage-enclosing, orders strictly on near-tip slip (0.45 / 0.65 / 0.80)
and on surface localization (37% / 44% / 50%). The abutting case is the
deliberate negative control — the seam defect the overlap margin exists
to avoid. The structural test keeps the placement contract: the trace
chain labelled through to the wall, now as the surface LOCATOR for the
damage region rather than a split path.

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* place_sheet stops short on request: setback clips against the offset boundary

The ruling's other half: the job is still to figure out the
intersection so the fault can stop BEFORE it hits the surface — it just
never meshes or splits all the way there. place_sheet(..., setback=d)
clips the sheet against the boundary offset INWARD by d (each coplanar
region's plane shifted along its inward normal; on a curved boundary
the shifted faceted planes, within a sagitta of the true offset): the
placed sheet arrives BLIND, its rim strictly interior, so
split_along_label_3d takes it as it stands — the daylighting refusal
never fires. The would-be intersection with the true boundary is still
computed from the unclipped input and returned as
info['surface_trace'] — the locator for the damage region above.

The clip's output is made split-safe by construction: a cut corner
polygon (two side-rim originals plus cut nodes) admits no triangulation
without an all-rim face, which the split refuses however fine the sheet
(measured: 3 such faces at every density tried). The face's longest
interior edge is split at its midpoint — bisecting both sharing faces,
rim edges (the trace included) untouched, no child worse-shaped than
its parent. A centroid split was tried first and REJECTED: a centroid
inside a sliver corner face drove the gap fill to 1e-23 cell volumes
and a P2 error of 0.64 on the shell outcrop.

test_0861 gains the end-to-end workflow test: a through-running sheet
placed with setback=0.25 arrives with no trace on the wall, its
shallowest vertex exactly a setback below it (the box's offset plane is
exact), surface_trace on the true boundary, and the placed patch splits
with the Plus side carrying every placed face. Box oracle and the
outcrop suite unchanged (test_0854 8/8, test_0860 4/4, suite green,
np=2/4).

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* The concavity gate measures depth against the setback; the trace locator marches

Physics-first ruling (2026-08-19): choose the fault's blind depth from
the physics and adapt the resolution to meet it. Two library changes
make that workable on the meshes it needs — locally refined shells
whose boundaries snap to the true sphere.

The clip's concave refusal becomes a MEASURED gate. A snapped
multi-resolution boundary is genuinely non-convex where fine facets
meet coarse chords, but the concavity DEPTH there is the sagitta
mismatch — metres at Earth scale — while an inner boundary's is the
facet size. The sequential plane clip over-cuts by at most that depth,
so a crossing is allowed when the deepest concave crease among the
near facets stays under 20% of the setback, and refused otherwise;
setback zero (an outcrop, where cut nodes must land on the complex
exactly) still refuses any concavity at all, as before.

The surface-trace locator contours the boundary signed distance over
the sheet's own triangulation (marching triangles): every crossing
point interpolates on a sheet EDGE from that edge's two vertex
distances, so the two triangles sharing it produce the identical point
and the polyline chains exactly, with no tolerance welding, on ANY
boundary — concave, graded, snapped. A direct triangle-triangle
intersection was tried first and REJECTED by measurement: robust-
geometry endpoint mismatches fragmented a 300 km trace into 148
components. Locator accuracy is the linear interpolant's,
O(spacing^2/R) — metres, which is the locator contract.

Verified end to end at Earth scale (probe:
~/+Simulations/spherical_slab_outcrop/megathrust_blind_adapted.py): a
19,768-cell shell adapts to 288,525 cells (proportional grading
h <= 0.5*distance — required, or coarse deep-rim tets span to the wall
and the carve refuses; edge_split engine — NVB's volume proxy leaves
diameters ~3x coarser than the carve's reach rules read), and the
megathrust places blind at 15 km = 2.5 local elements with the trace
located. Suites unchanged: test_0854/0859/0860/0861 21/21, spike green.

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* The sheet's resolution is a mesh choice: place_sheet(size=) re-triangulates

The ruling (2026-08-19): the authored fault triangulation is DATA, at
whatever spacing the source provided — but the embedded fault's
resolution must match the mesh it cuts, or the verbatim embed forces
sliver cells around every mismatched sheet triangle. place_sheet gains
size= — the counterpart of place_thin_volume's size, closing the
asymmetry the unification left: the clipped sheet's rim is compressed
to its exact corners (a too-fine authored rim coarsens here), each
straight run is resampled at the target, and the interior is re-meshed
by gmsh in the sheet's own plane. Planar sheets only (a curved surface
needs a parametric remesh, refused with the reason), and not for an
outcropping sheet — its trace chain must stay on the boundary complex
verbatim, so blind (setback) and interior placements only.

The all-rim split-safety pass is factored out (_split_safe_triangulation)
and applied to the resampled triangulation too: a gmsh planar mesh of a
polygon produces corner faces with all three vertices on the rim just
as the clip's corner polygons did.

test_0861 gains the workflow test: a deliberately coarse 3x3 authored
sheet placed blind with size=0.1 refines to gmsh-quality faces (min
quality > 0.3 asserted), keeps the blind rim exactly on the offset
plane, and splits with the Plus side carrying every face. Suites
unchanged (0854/0859/0860/0861 22/22, spike green).

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* Point-to-sheet sweeps cost the near-band, not the domain (#613)

A reach-aware spatial hash (_reach_query: points binned per octave of
their own threshold) and a culled distance (_sheet_distance_within:
exact wherever it is below the per-point reach, sentinel above it)
replace the whole-domain per-triangle sweeps at the placement's nine
hot sites — the gather marks, the carve's victim distances, its
straddle sweep and centroid rule, in the sheet and volume paths alike.
Per-point reaches matter: the thresholds scale with the local cell
size, so a single cutoff cannot cull a graded mesh. Every caller
thresholds where it reads the distance, so decisions are identical —
verified: the placed mesh is bit-identical on the 452k-cell
middle-ground benchmark, and place_sheet drops from 555 s to 67 s
(the 446 s _sheet_distance share to under a second; the sewn rebuild
is now the placement's largest piece at 30 s).

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* edge_split adapt reads each pass's topology once (#610 tier 1)

Three repeated whole-mesh walks removed from the adapt loop, none
changing a single cell of the output (bit-identical child on the
452k-cell benchmark):

- cell_diameters(return_tables=True) hands its cell-edge and
  edge-length tables to bisect_longest_edges, which was re-deriving
  both in the same pass — the per-cell closure walk is the loop's
  dominant Python cost and was paid twice per pass (309 sweeps -> 155).
- cell_diameters' per-cell max is vectorised (a simplex has a fixed
  edge count, so the ragged list stacks).
- The MG level selection re-measured every retained generation's
  5th-percentile diameter at the end — 76 more topology walks — when
  the marking pass had just computed exactly those numbers; they are
  cached per-dm and passed as resolution_hint.

Adapt on the benchmark: 658 s -> 551 s. The remaining ledger is the
engine surgery recorded on #610 — per-pass clone + label writes inside
the split call (242 s), MG parent/prolongation maps built per pass
(107 s), independent-edge selection (54 s) — the plan-in-numpy /
apply-back-to-back and multi-edge-template items.

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* MG parent maps are built for the retained levels, not per pass (#610)

The subsampler was already discarding every per-pass parent map whose
span covered more than one pass — nearly all of them — so ~80 s of the
benchmark's 107 s of map building produced maps that were thrown away,
while the retained multi-pass levels got None and the any-degree
transfer fell back to the geometric builder. The engine loop now
records nothing; the subsampler derives parents for the 3-4 RETAINED
pairs from the composed vertex transfer (nested_cell_parents is
topological through the transfer, and a descendant's referenced coarse
vertices are corners of its ancestor at any depth), so the deferral is
also an upgrade: multi-pass levels now carry exact parents.

Measured on the 452k benchmark, bit-identical child: parent-map cost
86 s (x75) -> 7.8 s (x9); adapt 551 -> 473 s. The bisect interior was
also split with instrumentation: of its 242 s, the native transform's
setUp+apply is 187 s (~2.5 s per application — the per-pass C cost is
honest, the x76 pass count is the waste) and the independence pruning
54 s; clone and label writes are negligible. The remaining #610 levers
are therefore pass-count reduction (multi-edge templates, C-side) and
the plan-in-numpy replacement of the per-pass Python (~170 s).

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* adapt-on-top skill: the h_far-below-base-diameters trap

The metric's far clip must sit above the base mesh's measured cell
DIAMETERS, not its nominal gmsh cellSize: diameters run 1.2-2.5x the
target edge length, edge_split marks on diameter, and a clip below them
refines the entire domain once — wasted cells AND a measured
de-conditioning of the far field (median shape quality 0.372 -> 0.295),
which every later adapt and solve inherits. Found by eye in the viewer
(scattered one-level patches across the globe), confirmed by
measurement, fixed by h_far >= 1.05 * cell_diameters(base.dm).max().

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_01WDKP73LD3dCFQSNgEXbbw9

* cells_supporting reads a facet's support directly, in any dimension (#620)

The zone walk went through _cells_on_edge, an EDGE walk: correct for a
2-D facet (an edge), one level too low for a 3-D facet (a face), where
support-of-support lands on nothing and the zone comes back all-False,
silently. The method had only ever seen 2-D meshes. Facets now take
getSupport directly — the cells, in any dimension — and the edge walk
is kept for labelled edges (a trace chain).

Fixes #620.

Underworld development team with AI support from Claude Code

* add_conforming_sheet: the 3-D cut-at-the-finest-level, with the tail

The Mesh-level form of place_sheet, mirroring add_conforming_surface:
the sheet is cut at the finest level ONLY, and the child inherits this
mesh plus everything below it as its coarse multigrid tail — the coarse
levels carry neither the cut nor the label, and do not need to (the
Galerkin coarse operators are formed from the fine operator; measured
in add_conforming_surface's note). The adoption — wrap, bookkeeping,
and the "a cut earns a level only if it is genuinely finer" subsample —
is extracted from add_conforming_surface into _adopt_cut_child and
shared, so the two dimensions cannot drift.

The method takes explicit (points, triangles, name): a sheet is DATA —
a slab model, an authored parameter-space triangulation — whose
connectivity must be embedded verbatim, and FaultSurface re-derives its
triangulation so it cannot carry an authored one. setback (blind
faults, surface_trace in child._surface_info), size (resample to match
the mesh being cut) and clearance pass through to place_sheet.

test_0862 mirrors the 2-D contract of test_0844: the tail composition
and the replace-not-stack decision, the named boundary and its zone,
the untouched base, chaining, the duplicate-name refusal, and a solve
with a per-cell contrast across the sheet consuming the cut mesh as it
stands.

Underworld development team with AI support from Claude Code

* The fault-network place route inherits the adapt hierarchy

_build_3d(mesher="place") placed each sheet at DM level and wrapped the
result in a bare Mesh, so the adapt child's multigrid tail died at
placement and every solve on the composed mesh fell back to algebraic
multigrid — not by design, but because the handoff that
add_conforming_surface performs in 2-D was never written here. The
route now chains add_conforming_sheet, so each cut child inherits the
hierarchy; the split at the end still forfeits it (see add_fault), but
the intermediate cut mesh now carries the tail that split-aware
multigrid would need.

The recorded place-route pathology (solve 3850 s vs 125 s embed, on
edge_split children) predates this handoff and wants re-measuring on
top of it.

Underworld development team with AI support from Claude Code

* The place route's recorded pathology re-measured: cell count, not operator health (#621)

The 3850-vs-125 note said the composed chain's solve was pathological on
adapt children. Measured again with the tail handoff in place: 27x the
cells embed builds for the same nominal sizes, 41x the time — which is
proportionate under 3-D Stokes scaling, with comparable per-cell cost,
one nonlinear iteration, machine-zero leak and agreeing slip. The
over-build is the route's own sizing (base at cellSize=h_far with
refinement=1 puts the far field at h_far/2). Docstring restated; the
sizing fix is #621. Companion measurements: #622 (custom-P V-cycles
across a cut level cost 7x more than GAMG saves), #623 (fill volume
drift on steep gradings).

Underworld development team with AI support from Claude Code

* Skip the custom-P re-install when the hierarchy is already live (#622)

inject_custom_mg re-ran build + install on every solve: a duplicate
Jacobian assembly (done only to make the fieldsplit reachable), a PCMG
reset + Galerkin setup, and a from-scratch geometric transfer build —
measured at 55 s of every repeat Stokes solve on an 85k-cell cut child,
while the transfers depend only on the meshes and PETSc re-Galerkins
changed operator values at PCSetUp by itself. The guard checks the LIVE
PC (the managed block exists, is a PCMG, carries the hierarchy's level
count) against a marker written at install; any doubt re-installs — the
cost of a wrong False is one redundant install, the cost of a wrong
True would be a solve on a stale PC.

Measured (place_route_health benchmark): repeat-solve overhead 55 s -> 0;
a genuine solve (operator values changed) keeps mg:4its on the velocity
block against gamg:46 and now comes in ahead on wall time as well
(284.6 s vs 307.7 s with the fgmres outer).

Underworld development team with AI support from Claude Code

* The Stokes OUTER Krylov is fgmres: the flexible-outer rule reached the sub-blocks but never the top (#624)

SNES_Stokes_SaddlePt sets both fieldsplit sub-solves to fgmres but
never pushed an outer ksp_type, so every saddle-point solve ran PETSc's
default plain gmres around variable-iteration Krylov inner solves — a
non-constant preconditioner under which standard GMRES's recurrence
does not hold, the same reasoning as the velocity-block FGMRES note
(#147) one level up. Measured on a genuine 85k-cell contrast solve:
14,400 inner iterations / 543.9 s under gmres; 307.7 s under fgmres
(gamg velocity), 284.6 s (guarded custom-P). Pushed as a managed
option in both the constructor and the strategy setter, so an explicit
user ksp_type still wins.

test_0203's lower-bound test asserted a STRICT undercount on the first
solve's velocity count — an artifact of left-preconditioned gmres,
which burned one full preconditioner application before its first
monitor fired. Right-preconditioned fgmres forms the unpreconditioned
residual first, so the bound is now tight; the test asserts the
contract (second >= first, plus the honesty flag) instead of the
artifact.

The remaining genuine-solve bottleneck is the pressure sub-solve at its
200-iteration gasm cap, silently unconverged — #625.

Underworld development team with AI support from Claude Code

* custom_mg's rbf builder is the standard sparse local interpolator (#429), plus a zero-column repair for placed levels

The "rbf" transfer builder assembled and solved the GLOBAL dense
coarse-cloud kernel matrix and returned nnz/row == n_coarse — dense
Galerkin coarse operators, no conditioning path, a rescue that could
not scale (#429, agreed long ago and never landed; the interpolation
note recorded it as "Not rewritten"). It now delegates to the standard
kd-tree local linear-exact interpolator (kdtree.interpolation_matrix,
order=1) — the same polyharmonic kernel and reproduction guarantees,
sparse support, the interpolator the rest of the code uses.

A row-wise kNN builder guarantees nonzeros per ROW, never per COLUMN
(#424): on NON-NESTED level pairs — two independently placed meshes
(#626), a relaxed child — a coarse DOF outside every fine stencil gives
an empty column and a singular PtAP. The serial build loop now repairs
such columns by nearest-fine-DOF injection (weight 1, same component),
warns with the count, and still refuses if any column stays empty.
These are preconditioner transfers, not the discretisation: an injected
row costs iterations at worst, never correctness. First measured use:
the split-finest-over-unsplit-band ribbon hierarchy (6 of ~50k columns
repaired; converged, machine-zero leak, answer unchanged).

Suites: test_1015 / test_1016 / test_0753 — 28 passed, 1 pre-existing
conditional skip.

Underworld development team with AI support from Claude Code

* The rotated PCMG coarse solve is SVD only when rotation modes exist (#622)

The rotated custom-P path hardcoded coarse="svd" for the velocity
PCMG because a free-slip enclosure's Galerkin-coarsened velocity block
inherits the rigid-rotation null modes and redundant/LU zero-pivots on
them. But nsp being non-None cannot discriminate — it also carries the
constant-PRESSURE mode (enclosed domains), which lives outside the
velocity block — so a Dirichlet-walled problem with no rotation modes
still paid a DENSE SVD factorisation of a 3-D P2 coarse level.
_rotated_nullspace now records the count of VERIFIED rotation modes on
the solver, and the coarse solve is SVD only when that count is
nonzero; the fallback (count unknown) keeps SVD whenever nsp exists,
so free-slip shells are untouched.

Measured on the split-fault contact benchmark: the SVD was NOT the
dominant cost (the per-cycle weight is the level-smoothing footprint
of a localized-refinement hierarchy — the background rides at full
size on every band level — recorded on #622); the gate is correctness
of configuration, not the headline saving.

Suites: 0844 / 0845 / 1015 / 1016 / 0203 — 63 passed.

Underworld development team with AI support from Claude Code

* Mesh._cell_node_indices: which DOF rows belong to which cell

The multigrid transfer needs more than DOF coordinates. To evaluate a coarse
Lagrange basis inside a parent cell it must know WHICH rows of
_get_coords_for_basis are that cell's own nodes -- and the section that answers
that lives on a coordinate DM _get_coords_for_basis builds and then destroys.

_basis_coordinate_dm is extracted from _get_coords_for_basis (second occurrence
of the same construction) so both read the same layout, and _cell_node_indices
walks that DM's local section over each cell's transitive closure. Degree 3,
where an edge carries two DOFs, falls out of the offset/ndof expansion; a
discontinuous space hangs every DOF off its own cell and needs no special case.
Non-simplex meshes are refused explicitly: a tensor-product Q_k cell carries
(k+1)^dim nodes, so the total-degree monomial basis the transfer builds would
not be square against them.

Cached beside _coord_array and cleared with it -- both describe the same node
layout, and it is the DS re-creation, not node motion, that invalidates either.

Tested geometrically rather than against itself: every node a cell claims must
lie inside that cell, checked with barycentric coordinates built from the cell's
vertices, at P1/P2/P3 in 2D and 3D for both continuities. A wrong section offset
points at another cell's node, which is somewhere else in the mesh, so it cannot
pass. Also asserted: node counts equal C(k+d, d), continuous rows are all
covered, discontinuous rows partition cell-block-contiguously (the layout
_build_kd_tree_index_DS already assumes), and Q_k is refused.

Underworld development team with AI support from Claude Code

* Exact nested transfers for native refine() pairs, structurally-clean geometric transfers, and FAC patch smoothing (#629)

Item 1 of the #629 program, plus the fill mechanism it exposed. A level
pair that is a native refine() pair (tagged by _coarse_level_meshes via
hierarchy-slot sentinels) now gets the EXACT nested prolongation at any
polynomial degree: parent cells recovered topologically from the two DMs,
weights by the dual-basis identity W = B M^-1 under the parent's affine
pullback (the #425 construction; verified exact to 1e-15 at P1/P2 in 2D
and 3D). PETSc's DMCreateInterpolation general path was measured NOT to
be the embedding (row sums to 1.375, quadratic reproduction error 1e-2)
and is not used.

The larger density win turned out to sit beside it: the point-located
builders emit ~1e-16 junk weights that are STRUCTURAL nonzeros, and
Galerkin RAP fills by structure, compounding level over level. Dropping
them (_drop_structural_zeros) collapsed the split-contact benchmark's
Galerkin chain from 319/481/265/90 nnz/row to 138/119/173/90 — the item-1
acceptance band — and the warm full-tail contact solve from 97 s to 52 s
at bit-identical answers (slip 0.1509, leak 0). Full-tail iterations
reproduce the recorded 6-7 under the robust smoother (the 6-its dial row
was gmres/4; the script's fast row runs 12).

Item 2: FAC/MLAT patch-restricted smoothing. Each level's patch is read
off its own transfer (identity row + bit-coincident node = background,
per NODE so the rotated path's per-node Q cannot disturb it; slit
duplicates count as patch; halo = one coarse-cell layer through the
transfer graph), stashed on the hierarchy, and consumed by
_configure_pcmg: the level smoother PC becomes ASM with that single
subdomain. Three configuration findings are load-bearing:

* BASIC, not restricted, ASM: discarding the halo correction stalls the
  outer KSP at 80-375 iterations where basic runs 6 against a
  whole-level baseline of 4 (banded Poisson, 4-level tail).
* The subdomain solve is SOR, the patch twin of the whole-level
  smoother: PCASM's default ILU-0 takes a NUMERIC_ZEROPIVOT on the
  rotated Galerkin patch block (min |diag| 8e-5 near the constraint;
  PC_FAILED -11 before the first iteration).
* The rotated path must force the FAC levels' smoother setup before its
  options-DB cleanup: PCASM creates the sub-KSP lazily at first apply,
  after the keys are gone, so sub options silently never applied.

A uniform pair (patch = everything) declines to whole-level smoothing.
Measured on the split-contact ribbon: correct answers with rotation and
contact, but a net cost at THIS toy's proportions — the ribbon band is
66-82% of every refined level's DOFs, so there is no background to save
and the ASM gather/scatter is overhead (warm 60 vs 52 s). The FAC win
requires production proportions (thin patch in a large domain), which is
where the item-3 contrast test goes next. The UW_FAC_* / UW_CUSTOM_MG_*
environment knobs are TODO(MEASURE)-marked A/B affordances for that
campaign.

Tests: test_1017_nested_native_and_fac.py (exactness by degree-k
reproduction, tag discipline, split classification with a geometric
oracle, end-to-end ASM solve vs GAMG); test_1015/1016/0753/0846/1014/
1017/1018 suites green.

Underworld development team with AI support from Claude Code

* FAC strong patch solves restore contrast-independent V-cycles (#629 item 3 diagnosis)

The contrast sweep's FMG degradation (velocity 7 -> 10 -> 36 its over
1e0 -> 1e4, where constant V-cycles are the expectation) is DIAGNOSED
and FIXED at the smoother:

* Level ablation at 1e4: full/no_mid/two tails converge in 20/19/24
  its — the coarse corrections contribute nothing under zonal contrast;
  the weak-band modes are invisible to every coarse space, including
  the band-carrying mid level.
* Doubling the smoother (gmres/8 vs /4) exactly halves the count
  (20 -> 10): convergence is proportional to total fine-level smoothing
  work — the signature of smoother-limited band modes.
* FAC with a DIRECT subdomain solve (sub_pc lu) on the patch — which
  contains the weak zone — collapses the count to 2 its at 1e4 and
  3 at 1e6: V-cycle constancy across six decades of contrast, below
  the flat-viscosity count (7).

Two supporting changes: factorization sub-solvers get
sub_pc_factor_shift_type=nonzero (the rotated Galerkin patch block
carries near-zero pivots from the constraint-zeroed transfer rows —
unshifted LU takes NUMERIC_ZEROPIVOT even as an exact factorization),
and UW_MG_SMOOTH_ITS joins the TODO(MEASURE) campaign knobs.

Remaining engineering to make the strong-patch cycle cheap: shrink the
subdomain from the whole refined patch to the weak zone + halo, and
reuse factors across cycles; the wall-time gate at contrast stays the
pressure gasm cap (#625).

Underworld development team with AI support from Claude Code

* A physics-keyed strong-patch mode: UW_FAC_PATCH=slit takes the split-duplicated nodes, not the refinement patch (#629)

The refinement patch conflates the smooth refined bulk (well served by
ordinary multigrid) with the fault zone (the only set needing the strong
solve). The slit mode keys the subdomain on the split-duplicated nodes
with operator-sparsity overlap as the halo — proportional to the fault
trace, not the refinement. Validated on the 2-D line rig: constant 6-7
velocity iterations across six decades of viscosity contrast on a 5%%
subdomain, against a GAMG control that degrades to its iteration cap.

Underworld development team with AI support from Claude Code

* Slit patches detect coincident FINE pairs, and split into per-segment ASM blocks (#629)

Two corrections from the 2-D campaign. The slit detection keyed on
transfer identity rows onto a coarse node — empty for a cut mesh, whose
slit vertices are NEW points at edge crossings coinciding with no coarse
node, so the mode silently declined to whole-level smoothing (caught by
a k-sweep flat at plain-FMG counts with no ASM in the probe). The trace
is now the coincident FINE pairs — the split's plus/minus nodes at
bit-identical coordinates — with the transfer-dup rule kept as a union.

A patch entry may now be a LIST of (owned, subdomain) blocks and
_configure_pcmg installs one ASM subdomain per block; UW_FAC_SEGMENTS=k
splits the trace into k along-strike chunks (by the coordinate along the
trace's leading principal component) for the segmentation experiment.

Measured on the 2-D rig (contrast 1e4, shifted-LU blocks, overlap 1):
7 velocity iterations at k = 1, 2, 4, 8, 16 — block-wise segment solving
is FREE; across-fault stiffness is block-local and the along-fault mode
is smooth, so the ordinary coarse ladder carries it. k=16 at contrast
1e6: still 7. And with a strong lid (eta 1e4, jump OR smooth profile)
crossed by the 1e-4 gouge — 1e8 across the fault walls — still 7/7,
against GAMG 88/80 and patchless FMG 16/16: the extended co-dim-1 lid
jump is benign for the Galerkin ladder; the malignant sharp feature is
the thin cross-cutting gouge, which is exactly what the patch owns.

Underworld development team with AI support from Claude Code

* Zone-keyed strong patches: painted fault models need no split to be patch-solved (#629)

The patch SOLVE is rheology-agnostic — ASM blocks + shifted LU over a
row set, blind to the constitutive model — and only the DETECTION was
split-specific (coincident fine pairs). A painted weak or TI band has no
topology to detect and needs none: the modeler painted the cells. A
boolean cell mask on the solver (solver._fac_zone_cells) now keys the
finest level's patch to those cells' DOFs directly.

Measured on the 2-D rig, UNSPLIT painted weak band (the weak-fault
model, standard Stokes path): zone-patched FMG holds 9 its at contrast
1e4 and 7 at 1e6 against GAMG's 40-and-degrading — the same constancy
the split-contact runs show, with no split anywhere. The fault
representation (split / weak / TI) returns to being a physics choice;
the solver architecture is indifferent to it.

Underworld development team with AI support from Claude Code

* The finest patch always contains the STRUCTURAL patch: zone blocks union the non-identity rows (#629)

The patch smoother REPLACES whole-level smoothing, so it inherits every
row the coarse level cannot represent — the cut/split-inserted DOFs the
transfer's non-identity rows identify — whether or not the physics zone
covers them. A zone away from the cut left those rows smoothed nowhere:
the velocity sub-solve capped at 200 iterations on every application
(measured on a V-junction, on parallel strips, and on a single band
crossing the cut, while zones ALONG the cut worked by accident of
geometry; the overlap-0 slit stagnation was the same omission). The
zone-block builder now appends the uncovered structural rows as one
more ASM block.

Confirmed: V-junction 8 its at contrast 1e2 and 1e4, merged or
per-segment blocks; parallel strips 8 its — all previously stagnant,
and the junction itself is a NON-EVENT. The pressure sub-solve also
returns to 14 iterations from its 200 cap once the velocity PC is
whole: part of the recorded pressure-cap pathology (#625) is downstream
of an incomplete velocity preconditioner.

Underworld development team with AI support from Claude Code

* Design note: fault-patch multigrid — the measured skeleton and parallel rules for 3-D (#629)

The architecture (ordinary multigrid + physics-keyed strong patches),
the evidence, the configuration rules each learned from a failure —
led by the structural-patch rule — the scaling statement, and the
parallel design rules for the 3-D deployment.

Underworld development team with AI support from Claude Code

* The ribbon is part of the FMG (design ruling, #629)

The placed ribbon joins the multigrid design in three roles: the bridge
level structure between the standard mesh and the patch, the AUTHORED
damage-zone identifier (the placement zone label survives the split as
cell children — never a distance-mask staircase or a cells_supporting
zipper), and one mesh discipline for split, TI, and weak fault
representations alike. Junction default: split segments a short
distance apart, the intact gap is the linkage; painted cores are
opt-in physics. Open engineering: partition weighting for the band and
split-pair co-residency in one mechanism.

Underworld development team with AI support from Claude Code

* Pair co-residency is automatic under local-frame splitting (design correction, #629)

Louis's correction: split surgery runs in the local frame after
distribution, so a pair is born on its parent facet's rank and cannot
be separated — no partitioner constraint exists. The real rules: never
distribute a pre-split mesh (the slit is a zero-cost graph cut, so a
partitioner would preferentially separate the sides while the contact
coupling lives outside the graph — gate that pipeline), and keep the
star forest consistent where a fault crosses a rank seam along strike
(the known np>=3 line-cut item). Ribbon balance reduces to ordinary
cell-count weighting.

Underworld development team with AI support from Claude Code

* What the fault ribbon is for (design ruling, #629)

The finite-width ribbon is a modelling object in its own right, with
three sanctioned physics: a damage zone (when a damage evolution
equation is solved — damage is a field, not a paint), a nonlinear
plastic/yielding material (so failure patterns EMERGE in the resolved
band — the mechanism by which junctions form rather than being
authored), and a permeable zone for fluid flow. A hand-painted static
weak viscosity is none of these — neither gouge nor a fault. The fault
itself is always the split surface in the mesh; ribbon physics
complements the slip surface. With none of the three physics present,
the ribbon carries background rheology and is purely resolution. All
roles compose with the solver design unchanged.

Underworld development team with AI support from Claude Code

* The meaning of w follows from the fault representation (design clarification, #629)

A fault may legitimately be REPRESENTED as a weak / TI weak zone
carried by the ribbon — then w is a PHYSICAL parameter (the fault-zone
width), chosen appropriately and resolved by ~2 elements across. In a
split-node model, w is a mesh-bridging convenience with no physical
reading, and nothing rheological may be keyed to it. The recurring
campaign error was mixing the readings: split nodes plus band-wide
weakness with w chosen as a mesh number is neither model.

Underworld development team with AI support from Claude Code

* #629 productionizing: fac_zone API, #589 fixed at source, ladder band in-repo, composed benchmark test

The fault-zone patch key settles into API: set_custom_fmg(...,
fac_zone=mask | [masks]), validated against the finest mesh at
registration; the solver._fac_zone_cells attribute spelling is retired
loudly (setting it raises rather than declining silently — the #629
campaign's sharpest instrumentation lesson).

The #589 empty-stratum getIndices() segfault is fixed at source:
utilities/dm_labels.py provides label_stratum_indices() gated on
getStratumSize (safe on both the null-IS wrapper and values outside the
live set), and the dead `is None` guards in nvb/reconnect/fault_split —
petsc4py returns a non-None NULL-handle wrapper, so they never fired —
are routed through it. mesh.cells_labelled(name, value) is the
empty-safe cell-mask accessor for placement labels (the natural
fac_zone builder).

The transfinite ladder band (#595: three nodes across, rails + exact
centreline, mandatory for spine cuts) moves in-repo from the campaign
scripts as place_thin_volume(..., mesher="ladder").

tests/test_1022_composed_ribbon_fmg.py (tier B) enshrines the composed
2-D benchmark of record: native level densities, the 2-block finest
patch (zone + automatic structural union), iteration bounds, slip/leak
invariants. Negative control verified (FAC disabled fails the block
assertion). The benchmark reproduces bit-for-bit on the in-repo path;
the 3-D pure-contact composition measures 7 velocity iterations vs
GAMG 56 with identical physics (design note updated).

Underworld development team with AI support from Claude Code

* The patch-keying ruling: fac_zone is for volumetric fault zones only (#629)

A split-node fault is the efficient fault representation and runs no
zone patch: the structural patch is automatic where it matters, and
under pure contact the strong patch is redundant outright — measured in
3-D, velocity iterations are 7 with the zone patch, 7 with the
structural patch, and 7 with no finest patch at all (the cover gate
declines the band-shaped patch of the non-nested placed pair). The
fac_zone key is reserved for volumetric representations — weak / TI /
damage / gouge rheology, where the zone width is physics — and the
ribbon is never the key: it is resolution, not rheology.

The design note records the ruling under patch keying, plus the answer
to why 2-D and 3-D economics differ: the 2-D ladder's band levels nest
by construction (36 -> 72 rungs, an exact 2:1 transfinite pair), so
placed pairs behave like native refinement; the 3-D ribbon layers are
independent unstructured fills at unrelated sizes and share nothing,
which is what fattens the Galerkin chain and prices the V-cycle. The
3-D fix, when wanted, is nesting the band levels, not more patch.

test_1022 restructured to the ruling: test 1 enshrines pure contact
with the structural patch alone; test 2 covers the fac_zone union
machinery and the loud retirement of the _fac_zone_cells spelling.

Underworld development team with AI support from Claude Code

* The 3-D ladder band: extruded prism-tets from the fault sheet, no remesh (#629)

place_thin_volume(mesher="ladder") in 3-D takes the fault surface's own
structured discretisation — a (grid, normals) pair — and offsets it
±width/2 into two prism layers split to tets (Dompierre subdivision;
quad-diagonal compatibility proven against the analytic skin count).
The mid-surface is a real vertex sheet, so the slit is coordinate-set
selection with no plane test, and a 2:1 subsampled grid's band shares
every vertex with the fine band through placement and split. Exact
where exactness is checkable: planar volume to 1e-12; an over-curved
extrusion is a refusal, never a reorder.

Measured on a curved fault (sinusoidal bulge, campaign rig): physics
identical to GAMG, 6-7 velocity iterations against 60-65. The corrected
diagnosis, recorded in the design note: vertex nesting alone does not
collapse the Galerkin chain — the unstructured FILL SHELL between band
skin and coarse background is the node majority (51% of finest P2
nodes, nesting 3%), and vertex nesting is not P2 nesting (10% inside
the band). Thinning the shell (clearance floor) brings level 1 to
native density. The residual warm-time gap is the rotated path's
per-solve transfer rebuild (the un-guarded rotated twin of #622) — the
prerequisite fix for wall-clock claims on the contact chain.

Geometry tests appended to test_0855 (volume, curvature refusal,
nesting, input contract).

Underworld development team with AI support from Claude Code

* place_fault_ribbon: the one-call fault-ribbon production path (#629)

The user supplies the fault surface as their own structured point grid
— curved is fine — and one call thickens that grid into the resolved
band (extruded prisms, no remesh), embeds it, labels the mid-surface
(coordinate selection + rim erosion, now in-repo as
_label_mid_surface), splits it into a frictionless-ready fault whose
label is a boundary for add_fault_bc, and builds the nested 2:1 unsplit
bridge level for the multigrid hierarchy. One parametrisation
throughout: normals default to grid-derived and the mid level always
subsamples the same field, so the levels nest by construction and the
inconsistent-normals footgun cannot be expressed. Defaults follow the
measurements (clearance 0.3, the thin fill shell); split=False keeps
the labelled unsplit mesh for painted weak/TI models on the same
geometry. The 3-D ladder assembly also accepts a bare grid now.

The campaign rig, rebuilt on this path, reproduces its record
bit-for-bit (21,352 cells, 1,150 slit faces, 675/675 nesting, level
densities 103/84/151/93, velocity 6 iterations, slip 0.1523).
End-to-end wrapper test added to test_0855.

Underworld development team with AI support from Claude Code

* Correction + re-baseline: the rotated transfer cache exists; the warm lever is level economics (#629)

The design note's claim that the rotated/contact path rebuilds its
transfers every solve is retracted: it described the pre-fix state
from earlier in the campaign and was repeated without re-measuring. A
cProfile of the repeat solve shows the cache working end to end — the
geometry tier of _rotated_linear_cache carries the rotation and the
custom prolongations, the context carries the KSP/PC, and the operator
tier skips assembly and PCSetUp on an unchanged matrix; 46 of 48
seconds are inside the composite Krylov solve, with no build anywhere.

The warm cost structure is the Schur loop times the V-cycle price
(full factorization = one velocity solve per pressure iteration), and
the measured lever is tail depth: on the curved-ladder stack, dropping
the near-fine mid level reaches GAMG parity warm (24.0 vs 23.0 s) at 7
velocity iterations against GAMG's 65, with the remaining transfer
chain entirely native density — the Galerkin fat was the mid level
alone. The mid level's role is production proportions; at rig
proportions the tail of choice is [L0, L1] + finest. Physics identical
in every arm.

Underworld development team with AI support from Claude Code

* The AL penalty pays at gamma=1 on the ladder stack (#629, the #625 mechanism)

Measured on the parity-tuned [L0, L1] + finest curved-ladder stack,
warm repeats, leak machine-zero in every arm: gamma=1 shortens the
pressure loop 22 -> 17 with the velocity count unmoved (7), taking the
warm solve to 19.1 s against GAMG's 23.0 — FMG ahead outright at flat
viscosity. gamma=10 over-stiffens the velocity block (7 -> 12) and
reverses the gain. No Schur plumbing was needed: the penalty is
viscosity-scaled by construction, so penalty/mu is uniform and the 1/K
pressure-mass preconditioner remains spectrally correct.

Caveats recorded with the result: under penalty the recovered p is the
Lagrange multiplier (p_mech = p - lambda*mu*div_u for any
pressure-dependent rheology), and derived quantities drift with gamma
(peak slip 1% at gamma=1, 3% at gamma=10 at rig resolution — the same
bias family as #633's vertex-sampled dynamic topography). gamma=1 is
the sanctioned choice.

Underworld development team with AI support from Claude Code

* place_fault_ribbon honours the requested fault: the tip margin is extrapolated, never confiscated (#629)

A fault specified from a structural model has its extent as data; the
wrapper previously took the grid as the band and labelled the slip
surface inset_rings inside it, so the requested fault came out smaller
than specified. Inverted: the grid IS the fault, labelled on precisely
the supplied points, and the band is built on the sheet continued
margin_rings rings outward along its own end tangents (_extend_grid —
linear per ring, corners consistent, normals continued the same way
and renormalised; no curvature the data never asserted). The 2:1
bridge level subsamples the extended parametrisation, so nesting is
unchanged. margin_rings >= 1 is enforced: a split may never reach the
band rim, and the margin comes from invented surround.

Measured on the curved rig (29x29 fault, margin 2): 1,566 of the 1,568
requested triangles split — only the two corner triangles erode (the
splitter's no-interior-vertex refusal, a half-cell nick) — nesting
867/867, chain 95/80/95 nnz/row, velocity 8 iterations. Peak slip
0.1737 against 0.1523 under the old inset: the confiscated rings were
biasing the physics by 14%, and the honoured value matches the
full-patch embed reference (~0.174). GAMG parity exact (slip 0.1737,
leak 2e-17, 70 its vs 8).

Underworld development team with AI support from Claude Code

* The curved 2-D ladder: numpy rails for bent traces, nesting by shared parametrisation (#629)

The 2-D ladder was transfinite and straight-only; the S-fault rig
(San Andreas bend + through-line branch, designed with Louis) needs
bent traces. _ladder_curved_assembly_2d is the 3-D extrusion one
dimension down: the polyline resampled equispaced in arclength, offset
±width/2 along mitred vertex normals (constant width through turns,
sharp turns refused), three rails of shared vertices, alternating
diagonals, mixed-orientation triangles a refusal. No gmsh, no CAD.
Straight polylines keep the gmsh transfinite path, so the recorded
composed benchmark stays bit-identical (verified).

The nesting contract carries over from 3-D and is enforced by API
shape: coarser levels must SUBSAMPLE one fine parametrisation — the
assembly accepts precomputed (samples, reach) for exactly that, and
independently recomputed reach vectors were measured to break rail
coincidence (only the spine nested). With the shared parametrisation:
132/132 mid band vertices coincide on the rig's tanh S trace.

Underworld development team with AI support from Claude Code

* place_fault_ribbon_2d: the S-fault rig's fault-network prep, one call (#629)

The 2-D production path with the full contract set: each supplied
polyline IS a fault, its points becoming the band's spine vertices
verbatim (the curved ladder's (samples, reach) patches — one
parametrisation per trace, extended margin_rings by tangent
continuation); traces are placed sequentially as stop-short strands
(the junction ruling) and cut+split in ONE add_fault network call —
chained calls were measured to drop the earlier fault's pairing
records. The (samples, reach) tuple patch is wired through the 2-D
ladder hook with the domain gate shared between paths.

visualisation/glyphs.py brought over from feature/stress-glyphs
(PR #601) for the rig's standard figures: dCFF slip-vs-welded and
principal-stress trajectory nets over a fine grey mesh.

Validated on the S-fault rig (tanh S + through-line branch, the
geometry designed with Louis): 1,858 cells, native chain (24-27
nnz/row), sub-second warm solves, machine-zero leak on both strands,
first partitioning number branch/main = 0.41, locked welded state
reproducing uniform shear to 2.6e-5. Tests: curved-ladder nesting +
refusals, and the two-strand wrapper with honoured trace vertices.

Underworld development team with AI support from Claude Code

* uw-visualisation skill: grid-resampling dapples at element boundaries — render nodally

Louis's catch on the S-fault rig's dCFF panel: resampling a recovered
P1 field onto a regular pixel grid via uw.function.evaluate produces
artefacts across the elements — grid points that straddle a facet get
located into a neighbouring cell with slightly-off reference
coordinates. The rule added to the skill: render derived fields
NODALLY on the mesh's own triangulation (vertex evaluation is exact
for P1 whichever cell the locator picks; VTK interpolates within
elements), and on a split mesh never Delaunay the DOF cloud — it
re-triangulates across the slit.

Underworld development team with AI support from Claude Code

* Review fixes for #638: the honoured-paint rule becomes API; duplicate trace labels are refused

The fault-footprint mask is now returned by both wrappers instead of
living only in documentation: place_fault_ribbon reports
info["footprint"] and place_fault_ribbon_2d reports per-label
info["footprints"] — band cells whose nearest extended-parametrisation
sample is a USER point, so painted rheology and fac_zone keys can no
longer silently extend into the extrapolated tip margin (the mistake
measured twice in the campaign: whole-band paint put tip lobes about
two elements past the mapped tips). The test is geometry-free — the
parametrisation itself carries the user/extension distinction — so it
works for any curved strand.

place_fault_ribbon_2d also refuses duplicate trace labels at the API
boundary rather than failing downstream with an ambiguous-boundary
shape inside the add_fault network call.

Both fixes carry assertions in test_0855 (footprints strictly inside
the band, subset of it, per strand; the duplicate-label refusal).

Underworld development team with AI support from Claude Code

* CI fixes for #638: the gated coarse-solve semantics in test_1021; worst-local-h headroom in test_0861

test_1021 asserted the pre-#629 blanket rule (the rotated coarse solve
is always svd). The campaign gated it: svd only when the rotated
problem carries a VERIFIED rigid-rotation null mode, because blanket
svd was measured as most of ~0.8 s per V-cycle (#622). The bundle
fixtures pin the inner annulus boundary, so no rotation mode survives
and redundant/LU is the correct coarse solve there — the assertions
now say so. The svd arm keeps its regression protection through a new
test: rotated free-slip on BOTH boundaries leaves the genuine rotation
mode, the builder must verify and record it, and the coarse solve must
be svd (this is exactly where redundant/LU hits its zero pivot, #306).

test_0861's two setback placements failed only on CI: with the 0.6
default clearance the carve cavity extends ~(clearance+1)*h_local, and
at the worst local h a different gmsh version deals this box that
overruns the 0.25 setback margin. clearance=0.3 (the measured
production thin-shell choice) keeps the cavity inside the margin at
worst-case h; the tests pass locally under both settings.

test_0054's failure is the CI-flaky hang-watchdog family (development's
own latest CI failed sibling test_0053); no change here — rerun.

Underworld development team with AI support from Claude Code

* The environment-armed watchdog arms after import, and the reporter never dumps sources (#638 CI, test_0054)

Root cause, established with native thread samples of two live hangs:
faulthandler.dump_traceback_later(repeat=True) walks live frames from
its C thread without synchronisation, and fired against a
still-importing interpreter that walk loops forever (locally: the C
thread pinned in dump_traceback for the whole sample window, the main
thread starved mid-import) or reads garbage and dies at SIGSEGV (the
CI -11). The env-armed watchdog was arming during `import
underworld3.mpi` — inside the very import it then dumped over. The
failure is conditional, which made it look flaky: it needs the import
slow (cold bytecode caches, as on CI or straight after a build) and
piped child output (as the test runs), and this branch's larger import
graph pushed the import past CI's 1.0 s trigger where development
stays under it.

Fixes, each a real hardening: (1) mpi.py preloads
traceback/linecache/tokenize so the reporter thread never enters the
import system; (2) _stack_dump formats with lookup_lines=False — file
names, line numbers and functions carry the hang report, and the
source-text reads were reporter-side IO against modules mid-import;
(3) report() re-arms its own Timer only — re-issuing
dump_traceback_later cancels the C thread and cond-waits on it
mid-dump (the sampled deadlock triangle); (4) the structural fix: the
environment-armed watchdog arms at the END of `import underworld3`,
never during it, trading self-coverage of the import graph for safety
everywhere the tool exists for (documented in
_watch_from_environment).

Validation: the previously first-run-reproducing context (piped
children after a fresh build, 0.2 s interval) went from 12/15 frozen
to clean in the test's own scenario; the actual test passes 6/6
locally. Known latent issue, pre-existing and out of scope: a natural
process exit while a repeat dump is in flight can hang finalisation
(the test SIGKILLs and never sees it).

Underworld development team with AI support from Claude Code

* place_thin_volume: a 'network' mesher — fused ribbons with embedded spines

A whole network of 2-D polylines in ONE call: the ribbons are fused in
CAD (touching strands, junctions free) and every spine's interior points
and segments are embedded in the fused face before gmsh meshes it, so
the split cut walks exact vertices at any resolution (#595: nothing
snaps). This unifies what the sequential ladder (robust cuts, no
touching) and the plain fuse (touching, routed cuts that failed at fine
resolution and for open-gap stepovers) each did only half of.

Measured on the S-fault rig (kissing Y at gap w/2, shared-band
stepover, branch): coarse and fine split cuts leak ~1e-17 on all four
strands; the open-gap and fine cases that failed on the plain fuse
now pass.

Underworld development team with AI support from Claude Code

* test_0857: the network mesher embeds its spines and cuts along a kissing pair

A main line and a splay leaving its side at half the band width, placed
with mesher…
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.

1 participant