Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
9d16872
The general 3-D outcrop cap: per-region collar, crease overlay, smoot…
lmoresi Aug 16, 2026
1ffa392
Tests: the general 3-D outcrop on a rotated box, across a box edge, o…
lmoresi Aug 16, 2026
adf9889
A relabel refusal must be collective
lmoresi Aug 16, 2026
41eb60a
place_sheet and remove_embedded conserve the domain's Euler number
lmoresi Aug 17, 2026
807a912
The assembly volume gate sits above OCC's boolean-mass noise
lmoresi Aug 17, 2026
34ea192
The boundary snap covers OCC's placement noise, masked to the facets
lmoresi Aug 17, 2026
db68864
The 3-D imprint collapse; on-crease outline edges bound the far collar
lmoresi Aug 17, 2026
111ae09
place_sheet clips, carves and caps against the mesh's own boundary
lmoresi Aug 18, 2026
2661679
The daylighting acceptance test: slip at the trace, gated on the split
lmoresi Aug 18, 2026
24cbd56
The snap test's control sits above the widened tolerance
lmoresi Aug 18, 2026
aa79ec5
Faults reach daylight blind, under a damage zone — the acceptance test
lmoresi Aug 18, 2026
ae7f9f6
place_sheet stops short on request: setback clips against the offset …
lmoresi Aug 18, 2026
de6f5ba
The concavity gate measures depth against the setback; the trace loca…
lmoresi Aug 19, 2026
925b34f
The sheet's resolution is a mesh choice: place_sheet(size=) re-triang…
lmoresi Aug 19, 2026
06e969a
Point-to-sheet sweeps cost the near-band, not the domain (#613)
lmoresi Aug 19, 2026
b76bb85
edge_split adapt reads each pass's topology once (#610 tier 1)
lmoresi Aug 19, 2026
176239b
MG parent maps are built for the retained levels, not per pass (#610)
lmoresi Aug 19, 2026
60bfdb5
adapt-on-top skill: the h_far-below-base-diameters trap
lmoresi Aug 19, 2026
4b8b0cd
cells_supporting reads a facet's support directly, in any dimension (…
lmoresi Aug 20, 2026
927a560
add_conforming_sheet: the 3-D cut-at-the-finest-level, with the tail
lmoresi Aug 20, 2026
f8183b2
The fault-network place route inherits the adapt hierarchy
lmoresi Aug 20, 2026
5f07bc3
The place route's recorded pathology re-measured: cell count, not ope…
lmoresi Aug 20, 2026
4b895d5
Skip the custom-P re-install when the hierarchy is already live (#622)
lmoresi Aug 20, 2026
1afaa78
The Stokes OUTER Krylov is fgmres: the flexible-outer rule reached th…
lmoresi Aug 20, 2026
ce41af6
custom_mg's rbf builder is the standard sparse local interpolator (#4…
lmoresi Aug 20, 2026
0fb7805
The rotated PCMG coarse solve is SVD only when rotation modes exist (…
lmoresi Aug 21, 2026
fec283b
Mesh._cell_node_indices: which DOF rows belong to which cell
lmoresi Jul 27, 2026
ec21348
Exact nested transfers for native refine() pairs, structurally-clean …
lmoresi Aug 21, 2026
836bc47
FAC strong patch solves restore contrast-independent V-cycles (#629 i…
lmoresi Aug 21, 2026
910c636
A physics-keyed strong-patch mode: UW_FAC_PATCH=slit takes the split-…
lmoresi Aug 21, 2026
49d01f0
Slit patches detect coincident FINE pairs, and split into per-segment…
lmoresi Aug 22, 2026
d4b86ea
Zone-keyed strong patches: painted fault models need no split to be p…
lmoresi Aug 22, 2026
3044580
The finest patch always contains the STRUCTURAL patch: zone blocks un…
lmoresi Aug 22, 2026
5df597d
Design note: fault-patch multigrid — the measured skeleton and parall…
lmoresi Aug 22, 2026
1c96fa3
The ribbon is part of the FMG (design ruling, #629)
lmoresi Aug 22, 2026
b902579
Pair co-residency is automatic under local-frame splitting (design co…
lmoresi Aug 22, 2026
e701085
What the fault ribbon is for (design ruling, #629)
lmoresi Aug 22, 2026
ce57895
The meaning of w follows from the fault representation (design clarif…
lmoresi Aug 22, 2026
996b67e
#629 productionizing: fac_zone API, #589 fixed at source, ladder band…
lmoresi Aug 22, 2026
fcb8d02
The patch-keying ruling: fac_zone is for volumetric fault zones only …
lmoresi Aug 23, 2026
8b4f325
The 3-D ladder band: extruded prism-tets from the fault sheet, no rem…
lmoresi Aug 23, 2026
c0fa290
place_fault_ribbon: the one-call fault-ribbon production path (#629)
lmoresi Aug 23, 2026
ecfc978
Correction + re-baseline: the rotated transfer cache exists; the warm…
lmoresi Aug 23, 2026
986ca81
The AL penalty pays at gamma=1 on the ladder stack (#629, the #625 me…
lmoresi Aug 23, 2026
9bb5a89
place_fault_ribbon honours the requested fault: the tip margin is ext…
lmoresi Aug 23, 2026
a04f861
The curved 2-D ladder: numpy rails for bent traces, nesting by shared…
lmoresi Aug 24, 2026
253bc3b
place_fault_ribbon_2d: the S-fault rig's fault-network prep, one call…
lmoresi Aug 24, 2026
0ea8953
uw-visualisation skill: grid-resampling dapples at element boundaries…
lmoresi Aug 24, 2026
0732680
Merge remote-tracking branch 'origin/development' into feature/fault-…
lmoresi Aug 24, 2026
ffc5bc8
Review fixes for #638: the honoured-paint rule becomes API; duplicate…
lmoresi Aug 24, 2026
f31b9eb
CI fixes for #638: the gated coarse-solve semantics in test_1021; wor…
lmoresi Aug 24, 2026
4b5ad9c
The environment-armed watchdog arms after import, and the reporter ne…
lmoresi Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/skills/adapt-on-top-faults/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ Measured guidance, all at matched cell count:
| FMG under rotated free-slip on a plain (non-adapt) refined mesh | the rotated KSP resolves hierarchies via `custom_mg.build_transfers`: an adapt child's mesh-owned tail is picked up AUTOMATICALLY (#467 fixed the old silent GAMG fallback), but the native `dm_hierarchy` is still not read — on a plain refined mesh, `set_custom_fmg(..., field_id=0)`. |
| NVB at np>1 raises NotImplementedError | native `_nvb_transform` extension not built (needs the custom-PETSc/amr env). Both `nvb` and `edge_split` are otherwise fully parallel, 2-D and 3-D. |
| high stress appears in the matrix beside the fault | elements STRADDLING the weak-zone margin: one end sees high strain rate, the other high viscosity. The FE forms `mean(η)·mean(ε̇)`; the honest cell average is `mean(η ε̇)`, and the difference is `−2 Cov(η, ε̇)` across the cell. Zero for any cell wholly in or wholly out. See the band-width section below. |
| scattered 1-level refinement across the WHOLE domain; far-field quality drops | the metric's far clip ``h_far`` sits below the base mesh's cell DIAMETERS (gmsh ``cellSize`` is a target edge length; diameters run 1.2–2.5x it — measured 0.108–0.223 for cellSize 0.18+ref 1). ``edge_split`` marks on diameter, so 100% of the domain refines once and the unrequested bisection DE-CONDITIONS the grid (far-field median q 0.372 -> 0.295, measured). Set ``h_far >= 1.05 * cell_diameters(base.dm).max()``. |
| refinement band narrower than the fault's INFLUENCE | measured: η still 0.07 at d=0.06 while the mesh has already coarsened 4×, so the artefact peaks on the transition flank, not on the fault. **85 % of it sits at d>0.01.** Size the flat core from the *influence* width, not the fault. |
| `uw.function.evaluate` fails "Total components 8 != 6" | cached-interpolation mismatch on a mesh already carrying several solver variables. Sample the field numerically from `surface.unsigned_distance` instead. |
| bare SIGSEGV, no traceback, after building a Mesh from a raw DM | `uw.discretisation.Mesh(dm, ...)` TAKES THE DM OVER. Read geometry from `child.dm`, never the handle you passed in. |
Expand Down
9 changes: 9 additions & 0 deletions .claude/skills/uw-visualisation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ pl.screenshot(out); pl.close()
high-order DOFs → blocky. **Attach `T.data[:,0]` directly** to the DOF-cloud
mesh from `meshVariable_to_pv_mesh_object` (it is correct for annulus/box/disc
— do NOT avoid it). `clim` MUST be passed (default `clim=""` trips `np.any`).
5. Resampling ANY field (even P1) onto a regular pixel grid via
`uw.function.evaluate` **dapples at element boundaries** — grid points that
straddle a facet get located into a neighbouring cell with slightly-off
reference coords (Louis: "artefacts across the elements", S-fault rig).
Render derived fields NODALLY on the mesh's own triangulation instead:
evaluate at `mesh_to_pv_mesh(mesh).points` (exact at vertices for P1,
whichever cell the locator picks), attach as point_data, let VTK
interpolate WITHIN elements. On a SPLIT mesh never Delaunay the DOF cloud
(it re-triangulates across the slit) — use the mesh's own cells.

## Seeing the MESH (adaptation / moving mesh)

Expand Down
197 changes: 197 additions & 0 deletions docs/advanced/stress-visualisation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# Visualising the Stress Tensor

Scalar fields get colormaps and velocity gets arrows; the stress tensor
needs its own glyph. `underworld3.visualisation` provides
principal-stress glyphs — sampled at seed points, the way velocity
arrows sample the velocity — and stress trajectories, the curvilinear
net traced by the principal directions. Both work from the same
recovered stress fields you already checkpoint.

## Principal-stress glyphs

At each seed point we diagonalise the stress and draw one bar per
principal axis: bar length proportional to the principal-value
magnitude, blue for compressive ($\lambda < 0$), red for tensile. In
2-D this is the classical stress cross; in 3-D each seed carries three
orthogonal bars, best drawn on one or two section planes rather than a
filled volume.

```python
import underworld3 as uw
import underworld3.visualisation as vis
import sympy

# Recovered stress components (P1 projections of the deviatoric
# stress) plus pressure give the full stress. Project once after the
# solve; do not pass raw solver derivative expressions to a plot.
# A scalar variable's .sym is a 1x1 Matrix — index it before
# assembling the tensor.
stress = sympy.Matrix([[Txx.sym[0] - P.sym[0], Txy.sym[0]],
[Txy.sym[0], Tyy.sym[0] - P.sym[0]]])

pl = vis.plot_stress_glyphs(mesh, stress, num_seeds=24,
save_png=True, dir_fname="stress_glyphs.png")
```

Seeds default to a regular grid over the mesh bounding box, filtered
to points inside the mesh (an annulus seeds no glyphs in its hole).
Pass `seeds` explicitly to sample section planes in 3-D or to avoid
regions — in fault models, keep seeds out of the weak zones, where the
recovered stress mixes materials across the interface.

```{figure} figures/stress_glyphs_thrust.png
:alt: Two-panel figure for a thrust-ramp model. Top panel, principal stress crosses on a 26 by 13 seed grid over a 2 by 1 section with two grey parabolic ramps rising from a basal decollement, drawn over a faint peach strain-rate wash in which the quiet wedges riding each ramp glow white and the blind ramp tips smudge darker. Far-field crosses are blue-horizontal (compression); in the wedges the crosses turn red and near-vertical (tension), largest just above the blind tips. Bottom panel, stress trajectories over the same wash: dark sigma-1 lines run horizontally and arch smoothly over each ramp tip, pale sigma-3 lines rise near-vertically between them, crossing at right angles everywhere.
:name: fig-stress-glyphs-thrust

Principal-stress crosses (top) and stress trajectories (bottom) for a
blind-thrust model, over a faint strain-rate wash (log scale,
background-percentile limits, low opacity — the second invariant of
the strain rate from the same recovered stress). The colour
convention matches the RdBu_r field convention used across the
documentation: blue compressive, red tensile. The wash ties the
stress geometry to the deformation it drives: the near-rigid thrust
wedges glow white while the blind tips concentrate both quantities.
```

### The pressure gauge matters for colours, not directions

For incompressible models the full stress is
$\sigma = \tau - p\,I$ and the pressure datum is a gauge choice.
Shifting that datum shifts every principal value equally, so it can
flip bars between red and blue — but it cannot rotate the principal
directions or reorder the principal values. State the gauge in your
caption (the examples here demean the pressure), or add the
lithostatic reference before plotting if absolute compression
matters.

The same argument answers a common question about map-view regime
colouring in the style of the World Stress Map (red normal, green
strike-slip, blue thrust): the regime classification is
gauge-invariant, but in a 2-D incompressible plane-strain model it is
also *degenerate* — the out-of-plane deviatoric stress is zero while
the in-plane deviatoric principals are $\pm s$, so the out-of-plane
stress is always the intermediate principal stress and every map-view
point classifies as strike-slip. Regime colouring only carries
information in 3-D models.

## Stress trajectories (2-D)

Trajectories integrate the principal *direction* field into curves —
the classical stress-trajectory diagrams of structural geology. A
principal direction is defined only modulo 180°, so ordinary
streamline tools cannot draw this field: the integrator in
`direction_trajectories` sign-aligns each evaluated eigenvector with
the previous heading, and places lines evenly (Jobard–Lehmann
occupancy) so the figure stays legible. The two families cross at
right angles wherever both are drawn — a built-in correctness check.

The direction callable is yours to build, which keeps the integrator
independent of how the stress is stored. From nodal arrays:

```python
import numpy as np

def make_direction(interpolate_stress, family="compressive"):
# interpolate_stress(p) -> (sxx, syy, sxy) at point p, or None
def direction_at(p):
values = interpolate_stress(p)
if values is None:
return None
sxx, syy, sxy = values
mean_dev = 0.5 * (sxx - syy)
radius = np.hypot(mean_dev, sxy)
if radius < 1.0e-12: # isotropic point: direction undefined
return None
angle = 0.5 * np.arctan2(sxy, mean_dev) # most-tensile axis
if family == "compressive":
angle += 0.5 * np.pi
return np.array([np.cos(angle), np.sin(angle)])
return direction_at

lines = vis.direction_trajectories(
direction_at, candidate_seeds, inside,
step=0.008, separation=0.04,
)
trajectory_lines = vis.trajectories_to_pv_lines(lines)
```

Draw the compressive family dark and the tensile family pale, as in
the figure above. In 3-D the analogue of a trajectory is a surface;
we do not attempt those — draw glyphs on section planes instead.

## Two more regimes, same recipe

The same two panels for the extensional and strike-slip companions of
the thrust model — nested listric normal faults above a detachment,
and en-echelon segments in dextral simple shear. Nothing changes in
the code except the checkpoint being loaded.

```{figure} figures/stress_glyphs_listric.png
:alt: Two-panel figure for three nested listric normal faults soling into a basal detachment, over a faint peach strain-rate wash with white quiet triangles in the footwall beneath each sole. Top panel, principal stress crosses: red-horizontal tension dominates the upper plate and deepens between the fault traces; blue compression concentrates near the detachment, with strongly rotated mixed crosses hugging each curved fault. Below the detachment the crosses return to uniform red-horizontal. Bottom panel, trajectories: dark sigma-1 lines hang near-vertically in the extending upper plate, bending to meet each listric sole, while pale sigma-3 lines run horizontally; beneath the detachment the net is an undisturbed rectangular grid.
:name: fig-stress-glyphs-listric

Nested listric normal faults (extension): horizontal tension aloft,
compression concentrating under the soles, and an undisturbed
trajectory grid beneath the detachment — the faults decouple the two
plates.
```

```{figure} figures/stress_glyphs_enechelon.png
:alt: Two-panel map-view figure for three en-echelon fault segments in dextral simple shear, over a faint strain-rate wash with bright white lobes at the relay steps between segment tips. Top panel, principal stress crosses at 45 degrees far from the faults, shrinking and rotating through the relay zones, with a blue compressive bridge linking overlapping tips. Bottom panel, trajectories: the dark sigma-1 family sweeps diagonally across the box and kinks sharply as it hands across each relay step; the pale sigma-3 family crosses it orthogonally.
:name: fig-stress-glyphs-enechelon

En-echelon segments in dextral simple shear (map view): the conjugate
cross pattern of the far field collapses through the relay steps, and
the trajectory families kink as stress hands across from segment to
segment.
```

## 3-D glyphs

`principal_stress_glyphs` accepts `(n, 3, 3)` tensors and returns
three bars per seed. Seed one or two planes through the feature of
interest:

```python
u = np.linspace(0.05, 0.95, 13)
gx, gz = np.meshgrid(u, u)
plane = np.column_stack([gx.ravel(),
np.full(gx.size, 0.5), # y = centre plane
gz.ravel()])
pl = vis.plot_stress_glyphs(mesh, stress, seeds=plane)
```

```{figure} figures/stress_glyphs_sinker3d.png
:alt: A unit cube drawn in outline with a grey sphere of radius 0.16 near the centre, slightly above mid-height. Three-bar principal stress glyphs are drawn on a vertical section plane and a horizontal section plane through the sphere, via the one-call plot_stress_glyphs with the cube edges and sphere added to the returned plotter. Above the sphere the bars are red and near-vertical (tension as material is pulled down behind the sinker); below and beside it they are blue (compression), fanning outward on the horizontal plane beneath the sphere. Bar length decays with distance from the sphere.
:name: fig-stress-glyphs-sinker

Three-bar principal-stress glyphs on two section planes through a
Stokes sinker: a tensile (red) column above the sinking sphere, a
compressive (blue) fan below and around it. Rendered with
`plot_stress_glyphs(..., show=False)` so the cube outline and sphere
could be added to the returned plotter before the screenshot.
```

## Building blocks

The plot function is a convenience wrapper; every step is available
separately for custom figures:

| Function | Purpose |
|---|---|
| `tensor_fn_to_pv_points(pv_mesh, uw_fn)` | Evaluate a `dim`×`dim` sympy tensor at points |
| `principal_stress_glyphs(coords, stress, scale)` | Bar segments + `"tensile"` cell array |
| `direction_trajectories(direction_at, seeds, inside, step, separation)` | Evenly spaced mod-180° trajectories |
| `trajectories_to_pv_lines(lines)` | Bundle polylines for `add_mesh` |
| `plot_stress_glyphs(mesh, stress, ...)` | One-call cross plot |

The figures on this page come from checkpointed models (the three
fault-interaction examples and a Stokes sinker): the solve writes the
mesh, the velocity, the pressure, and the recovered stress components
with `mesh.write_timestep`, and the glyph plots load them back with
`read_timestep` — no re-solving to restyle a figure. The faint
background in the 2-D panels is the strain-rate second invariant from
the same recovered stress ($\dot\varepsilon = \tau/2$ at matrix
viscosity 1), drawn first at low opacity with log scaling and
background-percentile colour limits, so the glyphs carry the figure
and the wash only whispers where deformation concentrates.
Loading
Loading