Skip to content

Teaching example: listric faults over a common detachment — the two-representations comparison - #527

Open
lmoresi wants to merge 5 commits into
developmentfrom
docs/listric-teaching-example
Open

Teaching example: listric faults over a common detachment — the two-representations comparison#527
lmoresi wants to merge 5 commits into
developmentfrom
docs/listric-teaching-example

Conversation

@lmoresi

@lmoresi lmoresi commented Aug 12, 2026

Copy link
Copy Markdown
Member

The classic continental-rift geometry (two listric normal faults soling into a common detachment) built BOTH ways on the same base mesh — finite-width ribbons via place_thin_volume and zero-thickness surfaces via place_along_lines — solved under identical extension and rendered side by side.

The comparison is the lesson: junctions decide the tectonics. The ribbons sole INTO the detachment (junctions are just cells of the union, resolved in the CAD stage) and reproduce the linked hanging-wall block structure with the detachment active between the junctions. The zero-thickness version needs the gap convention at the sole, localises on each fault individually, and under-connects the system — the detachment barely engages. The page closes with the when-to-use-which table and notes both representations share the placement family's lifecycle and (3-D) outcrop support.

New page docs/advanced/listric-fault-extension.md + two committed figures + toctree entry; generating scripts in ~/+Simulations/listric_extension/ (named subdir with README, per the working conventions).

Underworld development team with AI support from Claude Code

…ns teaching example

The classic rift geometry built both ways on the same base mesh, with the
junction treatment as the lesson: ribbons sole into the detachment
natively (junctions are cells of the union) and reproduce the linked
block tectonics; zero-thickness surfaces with the junction gap localise
individually but under-connect the system — the detachment barely
engages. Scripts in ~/+Simulations/listric_extension/; figures committed.

Underworld development team with AI support from Claude Code
Copilot AI lite review requested due to automatic review settings August 12, 2026 02:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new advanced documentation lesson comparing Underworld3’s two fault-placement representations (finite-width ribbons via place_thin_volume vs zero-thickness surfaces via place_along_lines) using the classic “two listric faults soling into a common detachment” rift geometry, emphasizing how junction handling controls system connectivity and resulting kinematics.

Changes:

  • Added a new advanced page walking through the geometry, setup, and side-by-side interpretation of the two representations.
  • Added the page to the advanced docs toctree so it builds and is discoverable.

Reviewed changes

Copilot reviewed 2 out of 4 changed files in this pull request and generated 1 comment.

File Description
docs/advanced/listric-fault-extension.md New teaching page describing the two-representations comparison, including code snippets, figures, and a comparison table.
docs/advanced/index.md Adds the new page to the advanced documentation toctree.

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

Comment on lines +18 to +24
```python
DETACH_Y = 0.4 # the common detachment depth
LAM = 0.18 # flattening length

def listric_trace(x0, y_lo, n=30):
y = np.linspace(1.0 - TOP_GAP, y_lo, n)
x = x0 + LAM * np.log((1.0 - DETACH_Y) / (y - DETACH_Y))
…n to come

Terminology and scope per the maintainer's direction: the zero-thickness
fault MEANS the split mesh (true discontinuity + contact), and the useful
comparison for this geometry is ribbons vs split mesh — arriving with the
fault-contact machinery. The conforming weak band is named for what it is
and dropped from the page (its junctions under-connect; not a useful model
of a detachment system). The ribbon presentation stands alone, with the
glancing-junction note.

Underworld development team with AI support from Claude Code
@lmoresi

lmoresi commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Adversarial review — PR #527: listric faults over a common detachment (teaching example)

Reviewed at head c48d8465 against origin/development. We rebuilt the PR head in a
fresh worktree (.claude/worktrees/r527-review, amr-dev env verified via uw.__file__)
and re-ran the generating script end-to-end.

Verdict: request changes. The page is sound and its claims reproduce, but the
committed figure carries a real rendering defect (a negative "second invariant"),
the generating scripts are not in the repo while the just-merged #502 sets exactly
that precedent in the same docs directory, and the PR body describes a page that the
head commit no longer contains.


1. MAJOR — the strain-rate panel plots negative values of a nonnegative invariant

The committed docs/advanced/figures/listric_ribbon.png shows the edot colorbar
running -6.66 → 19.2. A second invariant sqrt(trace(E²)/2) cannot be negative.
We probed the actual pipeline (scalar_fn_to_pv_points / uw.function.evaluate on the
composite sqrt expression, PR-head build):

e2 min/max: -6.66 / 19.21   n negative: 215 of 2610 (8%)

This is the known composite-evaluate trap — evaluate() interpolates the composite
rather than evaluating the sqrt pointwise, and undershoots hard near the weak-zone
junction singularities. Consequences in a teaching figure: the colorbar asserts an
impossible range, and mapping Oranges from -6.66 compresses the localisation
contrast (the background sits mid-tone instead of at the low end).

Fix per the house recipe (recover components, invariants in numpy): evaluate the
strain-tensor components to point arrays and form sqrt(exx² + 2exy² + eyy²)/√2 in
numpy; or at minimum clim=[0, max]. The figure must be regenerated either way.

2. MAJOR — figure is not reproducible from the repository; #502 sets the opposite precedent

The PR commits the PNG but the generating scripts live only in
~/+Simulations/listric_extension/ (the page even points readers at that
user-machine path: "Scripts: ~/+Simulations/listric_extension/"). Hours before this
review, #502 merged to development and established the pattern in the same
directory
: docs/advanced/figures/fault-examples/ ships the generating .py
scripts, the PNGs, cached probe .npz, and a .gitignore. This page should match:
commit listric_common.py + listric_ribbon.py under e.g.
docs/advanced/figures/listric/ and point the page there. As written, the page's
snippet also references names it never defines (TOP_GAP, y_lo, np) — fine if
the full script is a click away in-repo, broken otherwise.

3. MODERATE — the PR body describes the previous version of the page

The head commit ("reshape ... ribbons now, split-mesh comparison to come") removed
the two-representations comparison, the second figure, the gap-convention discussion
and the when-to-use table — but the PR body still advertises all of them ("two
committed figures", "rendered side by side", "closes with the when-to-use-which
table"). The diff contains one figure and no comparison. Rewrite the body before
merge; it is the merge-commit record.

4. MODERATE — overtaken by #502; no cross-links

Development merged the split-node fault + fault-contact machinery (#502) after this
branch was cut, adding docs/advanced/split-node-faults.md, fault-networks.md,
fault-mechanics-examples.md to the same toctree. The page's closing section says
the split-mesh comparison "arrives with the fault-contact machinery" — it has now
arrived. At minimum, cross-link {doc} references to those three pages (the docs
rules require linking related documentation; the page currently links nothing).
We verified the merge is clean (git merge-tree: no conflicts, including the
adjacent toctree insertions in docs/advanced/index.md).

Forward note for the promised comparison: the listric trace is a sampled curve
the split-mesh version must use the smooth/averaged normal, not per-facet normals,
or the zig-zag will contaminate the contact conditions.

5. MINOR

  • RdBu_r (our signed-field diverging map) is used for the log-viscosity zones
    panel; viscosity is positive — use a sequential map there. Vx on RdBu_r is correct.
  • Panels occupy a small fraction of a mostly-white 1400×1800 canvas; tighten the
    camera per the visualisation recipe before regenerating.
  • The ~/+Simulations README still describes the dropped two-way comparison
    (listric_lines.py, gap convention) — stale once scripts move into the repo.

CI

  • test job: FAILURE, but it is runner infrastructure, not the PR — UCX
    ud_verbs/mana_0 failed to open, MPI_Init_thread aborted during the import
    smoke test
    , before any pytest ran. Development is green on the identical base
    commit. Docs-only diff cannot cause this. Rerun the job.
  • Style gate (deprecated-pattern scan): pass.

Verified good (live, PR-head build)

  • Script runs clean against the PR head: 587 zone cells, min angle 4.7° — the
    page's "≈5°" glancing-junction claim is real and reproduces.
  • Our regenerated figure is identical in content to the committed PNG (same fields,
    same colorbar ranges) — the PNG is genuinely the script's output.
  • API current: place_thin_volume 2-D path exists with the exact signature used;
    the info['min_angle'] key is real in the 2-D return; Split-node faults: zero-thickness fault contacts in 2-D and 3-D, parallel, with interface constitutive laws #502 does not touch
    place_surface.py; branch base was development tip pre-Split-node faults: zero-thickness fault contacts in 2-D and 3-D, parallel, with interface constitutive laws #502.
  • Page snippet parameters match the real script (DETACH_Y, LAM, trace formula, ±0.5
    pull, v_y=0 base, free top, η=10⁻³).
  • Viscosity is P0 discontinuous, assigned per cell off the label, and rendered as
    cell data — all per doctrine; the page's justification paragraph is correct.
  • Docs placement: page under docs/advanced/, figure under docs/advanced/figures/,
    toctree entry added, MyST {figure} directive well-formed, no stray files outside
    docs/. No argparse in the scripts.

— review run in .claude/worktrees/r527-review; probes in the session scratchpad
(listric_run/), user's simulation directory left untouched.

Split-node faults landed on development (#502), so the second half of
this teaching example can be built. Both representations now run from
the SAME raw traces, the SAME uniform base mesh and the SAME drive,
with the interface law matched to the band (eta_f = eta_band / w).

What the comparison shows:

- the kinematics agree: same blocks, same subsidence pattern, peak
  vertical velocity -0.648 (ribbon) against -0.635 (split);
- the strain rate cannot agree, and that is the point — the ribbon
  spends deformation as strain inside a band, the split spends it as a
  jump no strain measure can see;
- peak slip agrees to 7-20% on the listric faults, but the detachment
  slips half as much in the split model.

That halving is the JUNCTION, not the law. Controls: removing the
interface law entirely moves slip by 2-4%; doubling the ligament costs
the sole 16%. A listric sole is a glancing junction, so the offset
form's pull-back is ligament*h/sin(theta) — 0.10 of trace here, three
times what the same setting costs at an orthogonal crossing.

Figures: the junction close-up (both constructions at one scale), the
three-field comparison, and the slip profiles.

Solver note recorded on the page: the split runs warn that an inner
velocity KSP hits its cap; checked against the direct-LU route (0.2%)
and against a 1000x tighter tolerance (unchanged).

Underworld development team with AI support from Claude Code
@lmoresi

lmoresi commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Adversarial review — the completed comparison

We built the second half against our own case. What survives, and what we would not yet claim.

Holds up

  • The matching is not a fudge. eta_f = eta_band / w is the zero-thickness limit of the band, and the control shows it barely matters: removing the law entirely (eta_f = 0) moves peak slip by 2–4%. Both representations are already weak, so the comparison is not being carried by a tuned parameter.
  • The junction attribution has a control. Ligament 1h → 2h costs the sole 16% and F1 12%, while the law costs 2–4%. The claim "it is the gap" is measured, not inferred from the picture.
  • The pairing is exact. Normal leak 6e-17 on every fault — the no-opening constraint is doing what it says.
  • The inner-KSP warning was chased, not waved through. An exactly repeating 200-iteration count is a cap, so we checked against the direct-LU route (_rotated_use_lu): agreement to 0.2%, and unchanged under a 1000x tighter tolerance. The outer Krylov is flexible; the warning is preconditioner quality on this mesh. Recorded on the page rather than hidden.

Does not, and is stated on the page or here

  1. Not mesh-converged. One h. Since the ligament scales with h, our own attribution predicts that refining should move the split toward the ribbon — the prediction is untested at the time of writing, and it is the obvious way this conclusion could be overstated. A refinement sweep is running.
  2. Opening is not separated from the junction. The ribbon's band can thin across its width; the split's bilateral constraint forbids it. In extension that is not a small distinction, and the eta_f = 0 control does not probe it — it varies the tangential law only. Some part of the sole deficit may be this rather than the gap.
  3. The ribbon slip probe is contaminated near a junction. It differences the velocity at ±0.75w from the centreline; within about w of a junction those probes land inside the intersecting ribbon. That is the origin of the spikes in the sole panel, and the tabulated ribbon peak (0.317) is taken 0.06 from the junction. Away from junctions the ratio is still ~2, so the conclusion survives, but the headline number is not a clean measurement.
  4. min_angle 0.9° in the ribbon union — the glancing junction's sliver. This solve was untroubled by it; a stiffer rheology may not be.
  5. Display artefact, no bearing on results: the P1 projection of the ribbon's strain rate undershoots to −4.2 at the band (Gibbs at a sharp field). clim hides it; the invariant itself is non-negative.

CI: the four failures on this branch (test_0851_fault_network_3d x3, test_0851_std_reduction_method) are inherited from development — identical set on bc53cfc, which contains none of this change — and a trunk fix for the fault-network three is already in flight. This PR adds no failures. Note also that three different test files now claim number 0851.

Underworld development team with AI support from Claude Code

…e claim

The first version of this page said the detachment's halved slip IS the
junction gap. A refinement sweep shows that is too strong, and the
follow-up controls refute two further explanations, so the page now
carries the whole parameter study instead of the tempting conclusion.

Sole peak slip, one knob at a time (ribbon reference 0.317):

  interface law removed (eta_f = 0)        0.154 -> 0.161   +5%
  ligament 1h -> 2h                        0.154 -> 0.130   -16%
  refine h 0.035 -> 0.0233 -> 0.0175       0.154 -> 0.187   +21%, converging ~0.19
  glue dial 0.05 -> 0.01 -> 0.002          0.181 -> 0.210   +16%, saturating
  glue plug made LARGER at fixed strength  0.181 -> 0.167   -8%
  best case (finest h, dial at ceiling)    0.216            68% of the ribbon

The gap is the biggest single lever, but closing it converges to ~0.19,
not to 0.317, and the ribbon is h-independent over the same range
(0.3173 -> 0.3155) — a clean control, since its width is physical.

Two explanations tested and DISCARDED, both recorded on the page:

- band stretching (a weak sub-horizontal layer extending along its own
  length, read by our probe as slip): shrinking w 4x at fixed
  eta_f = eta_band/w moves the sole 4.5%. Not it — and the near
  w-independence at fixed eta_f is itself a good result, confirming
  eta_f as the similarity parameter between the formulations.
- the plug shrinking away under refinement leaving a strong corner:
  pinning the plug LARGER makes transfer WORSE (-8%). A junction plug
  is a hinge, not a bridge.

A ~30% residual survives every control. Stated as unexplained, with a
warning to calibrate transfer rather than assume it, and the practical
guidance sharpened: ribbons for junction-dominated systems (a detachment
fed by splays is exactly that), split nodes for slip on individual
faults, where the two agree to 86-97%.

Control scripts kept with the study in ~/+Simulations/listric_extension/
(probe_h.py, probe_w.py, probe_dial.py).

Underworld development team with AI support from Claude Code
@lmoresi

lmoresi commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Correction: the junction is most of it, not all of it

The refinement sweep we said was running has landed, and it does not support the claim we made two comments ago. Correcting it here, and on the page (653930f).

What we claimed: the detachment's halved slip is the junction gap.

What the sweep says: the gap is the largest single lever, but closing it recovers only about half the deficit.

h pull-back ribbon sole split sole ratio
0.0350 0.102 0.3173 0.1542 0.49
0.0233 0.068 0.3166 0.1811 0.57
0.0175 0.051 0.3155 0.1866 0.59

The split rises in the predicted direction and converges to ≈0.19 — not to 0.317. The ribbon is flat over the same range (0.6%), which is the control we wanted: its width is a physical parameter, so it should be h-independent, and it is.

Two follow-up explanations, both refuted.

  1. Band stretching — a weak sub-horizontal layer in horizontal extension can extend along its own length, and our probe would read that as slip. Shrinking w by 4x at fixed eta_f = eta_band/w: sole 0.3173 → 0.3173 → 0.3030. A 4.5% effect, not a 100% one. Discarded. (Worth keeping, though: near-independence of w at fixed eta_f says eta_f really is the similarity parameter between the two formulations.)
  2. The plug shrinking away under refinement, leaving a strong corner — since radius = max(2.5h, 1.2*pull) scales with h. Weakening the dial does help and then saturates (0.181 → 0.204 → 0.210 at dial 0.05 → 0.01 → 0.002, the inviscid-plug ceiling). But pinning the plug larger at fixed strength makes it worse: 0.181 → 0.167. A bigger weak region lets the corner deform instead of passing slip along the sole. A junction plug is a hinge, not a bridge. Discarded.

Best case — finest mesh, glue at the ceiling — is 0.216 against the ribbon's 0.3155: 68%. A ~30% residual survives every control we have, and we are stating it as unexplained rather than attributing it to the last thing we tried.

This is a better result for the page than the original: the two formulations agree on what a fault is (the listric faults reach 86–97% at the ceiling) and disagree on what a junction is, structurally rather than through mistuning. The sole is the only fault that must receive slip through two junctions, and it is the one that stays short. The practical guidance sharpens accordingly — ribbons for junction-dominated systems, split nodes for slip on individual faults — with a warning to calibrate transfer rather than assume it.

Control scripts are kept with the study (probe_h.py, probe_w.py, probe_dial.py).

Underworld development team with AI support from Claude Code

lmoresi added a commit that referenced this pull request Aug 13, 2026
Some Azure runners advertise an InfiniBand verbs device (mana_0) that UCX
cannot open; MPI_Init aborts inside the import smoke test before any
underworld3 code runs. Four hits in a week across #466/#526/#527 (issues
#461/#528), each fixed by re-rolling the runner lottery. A single-node CI
runner needs only shared memory and TCP, so pin the transports at the
workflow level - it covers the smoke test and every pytest batch alike.

Underworld development team with AI support from Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants