Skip to content

Split-level fault multigrid, the thin-sheet solver, and sheet extrusion - #889

Open
lmoresi wants to merge 60 commits into
developmentfrom
feature/fault-split-multigrid
Open

lmoresi wants to merge 60 commits into
developmentfrom
feature/fault-split-multigrid

Conversation

@lmoresi

@lmoresi lmoresi commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Split-level fault multigrid, a thin-sheet (plane-stress) solver on flat and spherical meshes, and the extrusion of a sheet into a thick layer with its faults. This is the method UWTN 2026-022 describes; it is the base the parallel add-on work (the next PR, stacked on this one) builds on.

What it adds

  • Split-level multigrid for faults (utilities/fault_mg.py, docs/developer/subsystems/fault-split-multigrid.md): mesh.add_fault splits the finest level and, by default, every add-on level of the hierarchy (split_levels=False keeps the welded tail). Transfers are located by cell, side-aware across the slit; each level takes its own rotation frame (pairs and walls); coarse unknowns no fine unknown reaches are dropped rather than repaired by injection.
  • Rotated free-slip walls on every coarse level: under rotated free-slip, coarse levels take the per-node wall frame and lose their normal rows (a Cartesian coarse level under a rotated finest met zero pivots in the smoother).
  • ThinSheet (systems/thin_sheet.py, docs/developer/subsystems/thin-sheet.md): the vertically averaged sheet, viscous, Maxwell viscoelastic or yielding, with split-node faults, on a flat mesh or a SphericalManifold; objective (Jaumann) stress transport with an exactly orthogonal Cayley step on the sheet.
  • uw.meshing.extrude_sheet: the sheet as a thin 3-D layer, faults carried through, so the same model runs thin or thick.

Measured

UWTN 2026-022's fixture (box in simple shear, straight fault, library_c.py), velocity iterations, split / welded: 5 / 8, 5 / 13, 4 / 18 at k = 2, 3, 4 (serial); 6 / 8, 5 / 13, 5 / 18 at np 4; peak slip equal to six figures between the two hierarchies and between np 1 and np 4.

Rotated free-slip without a fault (the coarse-level wall rotation reaches every custom-FMG rotated solve), velocity iterations:

development (bfd07b7) this branch
adapted annulus, 2948 cells, 3 levels, np 1 18 3
adapted annulus, np 4 197 4
adapted spherical shell, 40074 cells, np 1 17 12

Same answers: the annulus to nine digits, the shell to 2.4e-7 (solve tolerance 1e-6). Adapted toward the outer wall with base.adapt, rotated free-slip on both walls, buoyancy forcing. A plain Annulus(refinement=...) or SphericalShell(refinement=...) solves on GAMG on both builds and is unaffected (identical iterations and answer; the plain shell's velocity solve sits at its 200-iteration cap on both).

Review

Three independent read-only passes on the branch (parallel rank asymmetry; the manifold solves and solver contracts; the tests), then a re-review of the fixes and of the merge with development. The findings and what was done are in the first comment. Fixed in this branch, among them: a rank-local failure in the replay of the fault onto the add-on levels hung the other ranks (new parallel test, hangs before); ThinSheet's L overwrote a user's same-named expression; the sheet's pressure was read from the shifted history; test_1091 never ran and check_test_coverage failed; a welded-beats-split assertion; two 13- and 7-minute tests gated CI. objective_order defaults to 1 for every solver as on development (Louis, 2026-10-28); the sheet selects the Cayley step itself.

Suites (laptop, macOS): tier A level 1 serial 1497 passed, 44 skipped, 1 xfailed, 0 failed; tests/parallel at np 2 168 passed, 11 skipped, 1 failed; at np 4 176 passed, 3 skipped, 1 failed; no hangs. The one failure is test_1063_constrained_freeslip_parallel[ti] (#826), which fails identically on development. The fault and sheet tests by name (test_1075-1080, 1082, 1083, 1085, 1087, 1091, test_0016, 0018, 0022, the parallel 1086 and 1089 at np 2 and 4) pass; test_1081 and test_1084 are tier C.

Issues

Left open by this PR, found in the review: #869-#872 (the four TODO(BUG) markers it adds), #873 (an empty coarse level on a rank), #874 (rank-local raises in the cell-located transfer; the line cut's orientation vote), #875 (the sheet's L and c_b inlined in its residual), #876 (ThinSheet.solve takes no picard), #877 (units-model rotated solve with cdim 3, present on development), #878 (no parallel test of the sheet or of extrude_sheet), #879 (the sheet's history projection is not in the transcript; the facet-normal field after a deform), #880 (test hardening).

Underworld development team with AI support from Claude Code

lmoresi added 30 commits October 8, 2026 17:20
SNES_Vector built its finite element with mesh.dim components while the
unknown MeshVariable, the residual and the Jacobian blocks were already
sized by the embedding dimension, so on a SphericalManifold (dim 2, cdim 3)
the field and the FE disagreed and the solve failed on a Vec-size mismatch.
The FE, the PC block size and the default unknown now use cdim. The same
component count applies in maths/vector_calculus (a tangential field on a
manifold is still a cdim-component row) and in the JIT's truncation of
sympy.vector objects. On a volume mesh cdim == dim and nothing changes.

Measured: a thin-sheet manufactured solution (toroidal l=2) on a unit
SphericalManifold at h=0.2, P2, relative nodal L2 error 4e-3.

Underworld development team with AI support from Claude Code
…cous)

uw.systems.ThinSheet solves -div_s(L sigma) + c u = f for the sheet
velocity with the plane-stress closure sigma = tau + tr(tau) P, which
eliminates the pressure: a vector problem on SNES_Vector, not a saddle
point. Any deviatoric constitutive law applies; the solver's inner
_Unknowns hands the model the in-plane strain rate P sym(grad u) P through
Unknowns.E (P = I on a flat mesh), and ViscousFlowModel.grad_u now reads
Unknowns.E as the VEP model already did (identical on a volume mesh).

Tests (closed forms): uniaxial stretch with free edges gives the Trouton
stress 3 eta edot and lateral contraction edot/2; a rigid motion carries no
stress; a uniform load against basal drag gives u = f/c; a manufactured
solution converges at third order with P2.

Design: docs/developer/design/THIN_SHEET_VISCOELASTIC_DESIGN.md.

Underworld development team with AI support from Claude Code
…d with Stokes

The viscoelastic stress history (its transport choice, the three per-step
hooks, the history factory and the `tau` read) moves from SNES_Stokes into
_StressHistoryMixin, a pure move apart from sizing the stored tensor by the
embedding dimension (identical on a volume mesh) and a no-op DEVSS hook
that Stokes overrides. ThinSheet.solve(timestep=) runs the same sequence
around its momentum solve, so any constitutive model with a stress history
works in the sheet unchanged.

Tests (closed forms): the exponential integrator reproduces the Maxwell
uniaxial response 3 eta edot (1 - exp(-t mu/eta)) at every step; backward
Euler reproduces its own recursion; Elsasser stress diffusion into an
elastic sheet over a viscous substrate (D = mu L / c) approaches the erfc
front at first order in the timestep. Stokes regression (1050/1051/1052/
0070) unchanged. scripts/sessions/thin_sheet_elsasser.py draws the profiles.

Underworld development team with AI support from Claude Code
The rotated strong-constraint machinery (add_rotated_freeslip_bc,
add_fault_bc, the constraint-mechanism guard, the reaction recoveries and
the solve dispatch) lived on the saddle-point class. It moves to
SolverBaseClass as _solve_rotated_if_registered, called by the Stokes solve
and now by SNES_Vector.solve, so a thin sheet takes a split-node fault the
way Stokes does. SNES_Vector registers its one field under the name the
rotated path addresses it by; rotated_bc gates its pressure sites on the
presence of a pressure field and solves the rotated operator of a vector
solver with one KSP (custom-prolongation FMG when a hierarchy exists, else
the shared GAMG bundle at the solver's block size).

Tests (thin viscous sheet, plane-stress elasticity with nu = 1/2): a
frictionless fault along or across a uniaxial stretch carries no shear
traction and leaves the exact solution untouched (nodal 1e-9); a viscous
interface approaches the welded sheet at first order in 1/eta_f; a
frictionless fault under remote shear slips as the Griffith mode-II crack,
(4 tau / E') sqrt(a^2 - x^2) with E' = 3 eta, within 8% (2 x 2 box,
a = 0.2, h = 1/25). Stokes regression on the moved code: 22 rotated/fault
tests and 17 viscoelastic tests unchanged.

Underworld development team with AI support from Claude Code
…rojector

A sheet on a SphericalManifold carries its velocity in the embedding space.
Tangency u.n = 0 is a rotated strong constraint at every node (rotated_bc's
new SHEET_TANGENCY spec, enumerated over all velocity nodes with the
analytic surface normal), so the solve takes the same path as rotated
free-slip and the split-node fault pairs. rotated_bc's component and
coordinate strides are the embedding dimension (identical on a volume
mesh) and the rigid-rotation gauge offers the three Killing fields when the
embedding dimension is three.

The in-plane projector uses the cell's facet normal, not the analytic one:
the assembler differentiates along the flat facets, and with the analytic
normal a rigid rotation kept a spurious strain of order h (0.7% of the
velocity at h = 0.25). With the facet normal the rotation is a discrete
solution (residual 2e-14) and is returned to 2e-10.

SphericalManifold cleared nothing after setting dm_plex_gmsh_spacedim, so
every later gmsh mesh in the process loaded with three coordinates (a 2-D
box after a sphere failed on a 3:2 vector-length mismatch); the option is
now cleared once consumed.

Tests: rigid rotation exact against basal drag (Killing field); toroidal
manufactured solution at second order with tangency at machine precision.

Underworld development team with AI support from Claude Code
A fault polyline given with three coordinates is read as great-circle arcs
about the origin: line_cut's chord primitives become segment objects (a
chord in the plane, an arc on the sphere) with the same cut algorithm on
top; the containing-cell test, the tip placement and the signed cell area
take their surface-mesh forms (the area signed against the outward
direction, the mesh's winding convention read off the majority).
reconnect orients a surface cell by its normal against the centroid, and
the chain orientation lives in the embedding dimension.

The contact frame of a coincident pair on a surface mesh is the in-surface
conormal of the fault edge (PETSc's FVM normal is undefined for an edge in
three dimensions), the trace tangent, and the surface normal — orthonormal-
ised, because the conormal is perpendicular to the facet while the surface
normal at the node is the analytic one: an O(h) misfit made the pair
rotation non-orthogonal and released a normal mode the sheet has no
stiffness against (a Killing field came back 5e-2 off, the same from LU).
The pair block pins the jump along the conormal and both sides' tangency
(the jump and the mean along the surface normal); the slip row stays free;
a paired node leaves the sheet-tangency wall set. Strides in fault_contact
are the embedding dimension and the interface assembler has one slip
component on a surface.

Tests: a rigid rotation (zero surface strain, so no traction on the fault)
is reproduced exactly across a frictionless arc with zero slip; a short arc
under the shear of the toroidal flow slips as the plane-stress Griffith
crack within 12% at a/R = 0.25. The 2-D fault suite (line cut, split,
contact, API, surfaces, placement, sheet faults) is unchanged: 110 passed.

Underworld development team with AI support from Claude Code
…g dimension

A stress history on a surface mesh is a 3x3 tensor. The symmetric-tensor
data layout and Voigt list of a MeshVariable, the history projection's
component count, the Eulerian history's velocity check, the velocity-trace
variables and the objective-rate term were all sized by the topological
dimension; they now use the embedding dimension (identical on a volume
mesh). ThinSheet carries its history on the grid on a manifold (the
Eulerian streamline-upwind transport): the semi-Lagrangian trace-back
leaves the faceted surface and is not validated there.

Test: with the grid history a rigid rotation stays the exact solution of
the Maxwell sheet on the sphere step after step (3e-10), with the carried
stress at round-off and tangency at machine precision. Flat viscoelastic
sheet and Stokes shear-box tests unchanged.

Underworld development team with AI support from Claude Code
…the facet topology

The interface assembler chose its quadrature by the component count, which
became the embedding dimension on a surface mesh and so picked the
triangle rule for edge facets (six nodes against three). It now follows
the facet topology; the component count still sizes the strides. Test: a
viscous interface law on a great-circle arc under the toroidal shear
reduces the slip at first order in 1/eta_f.

Underworld development team with AI support from Claude Code
ThinSheet.stress_transport refuses anything but the Eulerian (streamline-
upwind) history on a manifold mesh, where the semi-Lagrangian trace-back
leaves the faceted surface; the default is already the grid history.
Test: the refusal, and that the default reads "eulerian".

Underworld development team with AI support from Claude Code
…ts, histories

A subsystem page for uw.systems.ThinSheet — the plane-stress closure, the
closed forms the tests pin, the facet-normal projector against the analytic
tangency normal on a sphere, great-circle faults and their pair frame, the
grid-carried history on a manifold, and the known limits — linked from the
developer index. The design record notes the facet-normal finding.

Underworld development team with AI support from Claude Code
…ng dimension

write_timestep(petsc_reload=True) reshaped the coordinate vector by the
topological dimension and failed on a surface mesh (a reshape ValueError,
seen in the sphere fault-cycle run); it now uses the embedding dimension
(identical on a volume mesh). Test: a vector field on a SphericalManifold
round-trips exactly through the in-place reload.

Two further manifold defects found while testing, marked TODO(BUG) and not
fixed here: the default DMPlex-migration reload segfaults inside PETSc on a
manifold, and the XDMF writer labels triangles in three-space as
hexahedra.

Underworld development team with AI support from Claude Code
…erence, uplift rate

scripts/sessions/thin_sheet_fault_cycle.py runs a Coulomb fault in a Maxwell
sheet dragged by a shearing substrate, flat or on the sphere. Snapshots show
the Coulomb failure change on fault-parallel receivers against a reference
sheet with the fault welded, on the same split mesh and solved alongside
(a reference on the unsplit mesh differed by interpolation at the cut's
moved vertices), and the implied isostatic uplift rate from the thinning
rate. The fault traction and slip rate are recorded every step;
checkpoints are reloadable.

Underworld development team with AI support from Claude Code
The TODO(BUG) on read_checkpoint now records what was observed against the
current reload design (#674, #640): the reconstructed-mesh path segfaults on
a manifold when called on the mesh that wrote the file, where a volume mesh
round-trips exactly through the same call; the in-place path is exact on the
manifold; and the path's intended use, a mesh rebuilt from its checkpoint,
cannot yet be tried on a manifold because the h5 mesh round-trip drops the
coordinate dimension first.

Underworld development team with AI support from Claude Code
…e-back reads across a fault

With a sliding Coulomb fault in a Maxwell sheet, the semi-Lagrangian
history produced a period-2-step oscillation of the near-tip slip rate
(sign reversal at 77% of steps, amplitude doubling at half the timestep,
unchanged by a 1e-11 solver tolerance) and patchy stress along the trace.
Its trace-back samples the history by point evaluation, which within
about h/2 of a split-node slit can read the other side of the fault. The
grid history (Eulerian SUPG) removes both: reversals at 2% of steps, no
amplitude, the same yield time and mean slip rate, and a sixth of the
Newton work. ThinSheet now uses it by default on every mesh; a flat mesh
may still choose another. TODO(BUG) on SemiLagrangian.update_pre_solve: a
Stokes viscoelastic model with a split fault uses that history by default.

Thin-sheet tests 1075-1079: 19 passed with the new default.

Underworld development team with AI support from Claude Code
…rt and order options

-uw_tolerance, -uw_eta_v (a viscous term eta_v V added to the Coulomb law,
registered through fault_contact's private _register_law with a
TODO(FEATURE) for a public route), -uw_transport and -uw_order /
-uw_integrator: the options the sawtooth diagnosis used.

Underworld development team with AI support from Claude Code
The Eulerian SUPG history (its advection term, its stabilisation scale and
its transport-step residual) and the Eulerian advection-diffusion solver
(its velocity check and its diffusive flux) summed or sized over the
topological dimension. On a sphere surface mesh the velocity and the surface
gradient have three components, so the z term of the advection was dropped;
they now run over the embedding dimension (identical on a volume mesh).

Tests on a SphericalManifold, a rigid rotation about the x axis carrying
z to -y in a quarter turn: through the grid stress history (relative error
1.126 before the fix, 2.27e-3 after) and through uw.systems.AdvDiffusion
(a shape error before the fix, 2.27e-3 after). The stress-transport, SUPG
advection API and thin-sheet suites pass (61).

The Eulerian Navier-Stokes solver has the same topological sizing in several
places; it is not used on a surface mesh here and is left as it is.

Underworld development team with AI support from Claude Code
TODO(BUG): its velocity-shaped fields, SUPG terms and speed estimates are
sized by mesh.dim, which on a surface mesh drops a velocity component — the
defect just fixed in EulerianSUPG and the Eulerian AdvDiffusion. Left
unfixed; the solver is not used on a surface mesh yet.

Underworld development team with AI support from Claude Code
…lane

The history is transported component by component in the embedding space,
so a tensor carried along the curved surface keeps its Cartesian components
while the tangent plane under it turns. It acquired normal components (31%
of the stress by t = 3, measured on an unfaulted sheet driven by a
divergence-free toroidal flow), which the plane-stress closure reads as a
trace and so as a pressure. On a manifold the sheet now projects every
stored level onto the tangent plane, P sigma* P with the analytic normal at
each node, after the transport and after the commit: tangent to round-off.

That sheet still thickens at both poles (uplift rate 0.15). This is the
advection of the stress, a large-strain effect quadratic in the flow at a
Weissenberg number of about two: with the transport switched off it falls to
6e-6, and a Jaumann rate does not change it. The test records this and
asserts the tangency.

Underworld development team with AI support from Claude Code
…ing)

Work in progress, set aside to merge development: the objective step as a
truncated expansion of exp(dt K) sigma exp(dt K^T) or the Cayley rotation
(objective_order), the spin of a sheet element on a curved surface, the
carried stress frozen in the Picard tangent, the history projected only
after the commit (placement under measurement), and the design note on
one model for thin and thick sheets. Tests and measurements follow.

Underworld development team with AI support from Claude Code
The graph route converted sympy.vector Vector and Dyadic objects with the
topological dimension, which drops the third component on a surface mesh
in three-space. The expanded route was corrected the same way earlier
(aeb0293); the graph route, merged since, carried the old slice.

Underworld development team with AI support from Claude Code
…linearised in Newton

Completes 978b540. A viscoelastic model's carried stress is taken through
the step at objective_order: q truncates exp(dt K) sigma* exp(dt K^T) at
total order q in dt (K = W Jaumann, L upper-convected; q = 1 is the existing
explicit source and stays the default for upper-convected), or "cayley"
rotates it orthogonally (the Jaumann default). The rotation reads the
solver's spin, which on a curved sheet includes the turning of the normal.

The carried stress is frozen in the Picard tangent: differentiating the
Cayley rotation symbolically took tens of minutes per setup. A model now
declares tangent_surrogates, differentiable stand-ins for its frozen atoms
(here the first-order step); in Newton mode the solver swaps them into its
own flux before differentiating, so a solver's closure stays in the
tangent. Measured on the sphere, 4 steps: Picard 9 -> 14 iterations a
step, rising; Newton with the stand-in 5, steady, 31 s against 40-51 s.

Measured rigid quarter-turn on the sphere (h = 0.15, 40 steps): the history
projected onto the tangent plane only after the commit, Cayley 8.4e-3,
independent of dt (8.5e-3 at 80 steps) and 2.5e-3 at h = 0.075 (order
1.77 in space); projecting before the solve as well cost 4.0e-2.

Tests (test_1080): the order's defaults and refusals; the Cayley step is the
orthogonal rotation of the stress; a rigid quarter-turn of a flat sheet
carries the stress to 2.9e-3; the Newton tangent converges to the frozen
tangent's stress. Regression across stress transport, log-conformation,
VE stability, Newton cold start, yield, the JIT, rotated constraints,
faults and the thin-sheet suites: 157 passed.

Underworld development team with AI support from Claude Code
TODO(BUG): a model's flux_jacobian replaces the whole F1 in the Newton
tangent, so with one set the Stokes uu tangent loses the augmented-Lagrangian
penalty and the DEVSS term, which both depend on the velocity gradient; a
thin sheet's closure would be lost the same way. To be unified with the
thin/thick alignment (solvers apply their own closure to the model's
flux_jacobian; tangent_surrogates then retires).

Underworld development team with AI support from Claude Code
…ly (slow test)

The objective step on a curved sheet end to end: grid transport of the
Cartesian components, the spin with the turning of the normal, the Cayley
rotation, and the tangent-plane projection after the rotation. A quarter
turn about x in 20 steps at h = 0.15 carries the stress to 9.4e-3 (measured;
8.4e-3 at 40 steps, 8.5e-3 at 80, 2.5e-3 at h = 0.075), tangent to round-off.
level_3: 13 minutes.

Underworld development team with AI support from Claude Code
… forcing

The Newton tangent flux. A constitutive model's flux_jacobian is a
deviatoric stress, like its flux; each solver now builds its momentum flux
through one function (_flux_closure: Stokes and Navier-Stokes add the
mechanical pressure with the penalty and DEVSS, the thin sheet the
plane-stress trace term times the thickness) and applies it to the tangent
flux too. Before, the tangent flux replaced the whole F1 (a Stokes Newton
solve lost its pressure, penalty and DEVSS terms and diverged), and on vector
and scalar solvers it was silently ignored: a 2-D Array was flattened with
list(), which yields rows, and the error swallowed by a bare except.
tangent_surrogates becomes the model's private _tangent_surrogates.

Invariants. Every second invariant a constitutive model forms goes through
Unknowns.second_invariant (the plain contraction by default, the same
expression as before); the thin sheet adds the out-of-plane component
M_nn = -tr M, so a plastic sheet in uniaxial stretch yields at the von Mises
sqrt(3) tau_y instead of sqrt(18/5) tau_y.

The sheet's API follows Stokes: ThinSheet(mesh, v, p, degree, p_continuous),
with p = -tr(tau) as a diagnostic refreshed after each solve; bodyforce per
unit volume (the sheet integrates it through its thickness);
add_basal_drag(c, boundary, reference_velocity) and add_basal_traction on
both solvers (natural conditions on Stokes). uw.maths.tensor gains
resolved_traction and coulomb_failure_stress on any solver's stress.

SNES_Vector copies its fields to the coarse levels before the field
decomposition, which recurses into them (PETSc error 62 on any refined mesh
since the decomposition was added for the rotated solves).

Tests: test_1082 (tangent flux keeps the closure, and is live), test_1075
(forcing per unit volume with drag and traction, the pressure diagnostic,
the von Mises yield, a refined mesh); test_108* batched in scripts/test.sh.

Underworld development team with AI support from Claude Code
… second order

An unfaulted Maxwell sheet on the unit sphere dragged by the toroidal force
4A rhat x grad_s(z^2): the first-order flow is divergence-free, and the sheet
thins only through the Jaumann rate turning shear stress into normal stress
differences (the Weissenberg effect). The steady second-order perturbation,
derived in sympy (scripts/sessions/thin_sheet_quadratic_closed_form.py):

    -div_s u / A^2 = -3 (65 z^4 - 126 z^2 + 29) / 533
                   = (24/91) P2(z) - (24/287) P4(z),

with no P0 and no odd degrees. ETD-1 relaxes the corotational term with the
effective time lambda' = alpha dt / (1 - alpha) in steady state, and the
thinning is linear in lambda, so the discrete steady state is the closed form
times lambda'/lambda. Measured at A = 0.1: P2 within 0.65 % (h 0.2, dt 0.2),
0.18 % (h 0.12, dt 0.1), 0.09 % (h 0.08); P4 converges at second order in h
(14 %, 5.6 %, 2.75 %); the dt dependence is entirely lambda'.

test_1084 (level 3, 7 min): h 0.2, dt 0.2, 20 steps; P2 to 2 %, P4 to 25 %,
P0 and the odd degrees below 2e-3 of P2.

Underworld development team with AI support from Claude Code
uw.meshing.extrude_sheet / Mesh.extrude(L, layers) build a layer of
tetrahedra centred on a sheet mesh (flat, or radially from a spherical
manifold). The sheet's boundaries become the layer's walls under the same
names, its faces are Base and Surface, and a split-node fault of the sheet
becomes a vertical fault through the whole layer: the slit is duplicated at
every level, so the fault is open at the surface and the base, its tips are
shared vertical edges, and the side labels and minus-to-plus pairing are
rebuilt by joining mapped vertex tuples. (The 3-D splitter cannot make this
fault: it keeps a patch rim welded and needs about nine cells of depth.)
Each prism is cut into three tetrahedra by the vertex order, with a fault's
minus copy taking its plus copy's place, so both sides triangulate alike.
Cells are oriented as PETSc expects; with the other sign every volume
integral flipped against the facet ones (the volume came out negative and a
basal drag pushed).

An unfaulted layer gets a geometric-multigrid tail that coarsens through the
thickness first and then laterally through the sheet's own levels. A faulted
layer gets none yet: custom multigrid on the 3-D rotated pair operator fails
on its first application (marked TODO(BUG)); algebraic multigrid converges.

So one model function runs thin or thick by changing the mesh and the solver
class. test_1083: the layer's geometry and pairing; a dragged plate, thin
against thick, mid-plane velocity within 0.72 %; the same plate with a
frictionless fault, 0.53 % away from the slit and the slip within 1.6 %
(L = 0.04, c L / eta = 0.04). Serial only for now.

Underworld development team with AI support from Claude Code
…heet

The same model, a sheet on the unit-sphere manifold and the shell extruded
radially from it, dragged by a toroidal base: the difference is first order
in L, as the thin-sheet limit predicts (the base has 1 - L/R of the
mid-surface's area, and the drag-driven shear c L / eta scales with L too):
3.32 % at L = 0.04, 1.35 % at L = 0.02. In test_1083.

extrude_sheet carries over only the sheet's labels that mark facets (a
manifold labels the whole surface "Surface", which the layer's own surface
face then collided with), and refuses a real wall whose name the base or
the surface would take.

Underworld development team with AI support from Claude Code
…lit sheet

The layer's multigrid tail was extruded from the split sheet itself when the
split mesh owned no tail (a plain refined base hands none down), so the
one-layer coarse level carried the slit: coincident nodes, which the
barycentric and RBF transfer builders cannot place, and the first cycle
returned NaN (DIVERGED_PC_FAILED). It was first read as a failure of
geometric multigrid on the rotated 3-D pair operator and the faulted layer
was left on algebraic multigrid. The coarse levels now come from the split
sheet's ancestors: the conforming cut it was split from (same nodes, no
slit) for the through-thickness levels, the first ancestor with a refinement
hierarchy for the lateral ones. Measured: the faulted layer's velocity block
takes 14 iterations per solve against about 180 under algebraic multigrid.

test_1083's faulted case asserts the layer runs custom FMG and converges.

Underworld development team with AI support from Claude Code
build_rotation takes the operator's row layout from the solver's Jacobian.
_build() leaves the SNES set up only on some paths: with essential
conditions registered it is, without them (a drag-only layer) the matrix is
never created, and build_rotation dereferenced a null Mat - a segmentation
fault. The solver's own rotated solve sets the SNES up first, so production
solves were unaffected; a direct call on a built solver was not. Now it sets
the SNES up when the Jacobian does not exist yet.

test_1085 (crashed with SIGSEGV before the guard).

Underworld development team with AI support from Claude Code
…of the parallel path

Underworld development team with AI support from Claude Code
mesh.add_fault carries a split-node fault through every add-on level of the
inherited multigrid tail (the background is never cut), so the coarse spaces
contain the discontinuity and the velocity iterations fall with refinement:
6, 5, 4 at k = 2, 3, 4 add-on levels against 13, 18, 23 with the welded tail,
the slip identical to six figures. split_levels=False keeps the welded tail.

utilities/fault_mg.py: the side-aware cell-located Lagrange transfer (fine
cell centroid located in the coarse mesh, dual-basis weights, barycentric
clamp; batched; serial CSR and an MPIAIJ builder that locates rank-locally
and asks the other ranks for the nodes it cannot), the per-level pair
rotation built from the level's own pairing, and the composition that
removes each level's opening rows and drops unreached coarse unknowns
instead of repairing them by injection (which duplicated columns and made
the Galerkin operators singular in parallel). Every pair touching a split
level takes this transfer, so the welded tail under a split finest now runs
at np > 1 too (it returned NaN before).

fault_split: 3-D sheet faults through add_fault (placed and split, placement
options threaded through for bisection-refined meshes), the fault recipe
recorded for replay on the levels, and a pairing that rides redistributions
as a pair of DM labels: the migration star forest PETSc returns for an
initial distribution does not index the stratified numbering.

meshing/extrude: the layer's tail extrudes the split sheet and its split
levels, and the extrusion runs in parallel (built on one rank, distributed,
fault stars made rank-interior, pairing collected): 261 of 261 pairs
coincident at np = 2, the faulted plate solving with its slip unchanged.

Tests: test_1087 (tail structure, iterations against the welded tail,
viscous fault, moving fault, the transfer's side awareness and exactness)
and tests/parallel/test_1089 (np = 2 and 4; the thick twin extruded with
its pairing). Subsystem page docs/developer/subsystems/fault-split-multigrid.md.

Underworld development team with AI support from Claude Code
#756)

Conflicts resolved with both sides kept where they meet:

- fault_split: development's region-based redistribution (groups) carries
  the pairing through as labels when asked; add_fault takes cut / exclude /
  blind / across_seams and the 3-D placement options; the seam-normal
  override lives in the mesh-level pair helper, so coarse levels see it.
- Mesh.add_fault: development's options plus split_levels; the level replay
  applies to cut faults only (a placed band's spines have no counterpart on
  the levels), runs finest-down, and stops with a warning at a level the
  faults cannot be carried onto (a one-cell junction ligament closes at the
  coarser spacing), leaving that level and those beneath welded.
- custom_mg: the cell-located transfer and the orphan drop apply only when a
  COARSE level is split; a hierarchy whose only split level is the finest
  keeps development's builders, orphan repair and FAC patch.
- rotated_bc: the same zeroRows-by-global-row fix on both sides, one copy.
- Thin-sheet conflicts against development (solver Cython warm-up, the
  checkpoint read, field projection): development's code with the
  thin-sheet additions.

Merged build: tests 1086, 1087, 1089 (np 2), 1083, 0845-0847, 1017, 1022,
0865 (np 2) and 0866 pass.

Underworld development team with AI support from Claude Code
Repeated work has come from investigating without looking at what is in
flight: the fault multigrid session of 2026-10-13 re-diagnosed parallel
fault defects that PR #672, open for weeks, had already fixed, and then paid
a seven-file merge. The bootstrap now requires fetching development and
reading the open and recently merged PRs on the subsystem before an
investigation, a branch or a design decision, naming them in the opening
report, basing the work on current development, and raising a related PR
that has sat open for more than about a week as a finding at the start.
The branching guide says the same for feature branches.

Underworld development team with AI support from Claude Code
…oad measurement

Each level's split gathers its own fault star onto the rank holding most of
it, which differs from level to level, so a transfer between stars on
different ranks located most of the fault band remotely (measured: 48 % of
the finest nodes at np = 2, 58 % at np = 8). After the replay every level's
star is moved onto the rank holding the finest's star, the pairing carried
as labels; the misses fall to the band's rim (2 to 6 %). The parallel
transfer records its hit rate per level on the hierarchy.

scripts/sessions/fault_mg: parallel_load.py (owned cells and star per rank on
every level, misses, stage times), and the figure scripts for the wiki page
and the technical note (strategy schematics, the transfer rule, the pipeline,
the rendered hierarchy, iterations against refinement, the moving-fault cost,
the parallel load).

Underworld development team with AI support from Claude Code
… load case

Underworld development team with AI support from Claude Code
…ir is retired

The nearest-node injection that repaired a coarse unknown with no fine image
put a weight-1 entry on a fine row that already carried one: two identical
columns, a singular Galerkin coarse operator. Measured on the composed
ladder rig (test_1022's stack with two mid ribbons): four orphans on the
welded-to-finest transfer, NaN on the first cycle, with a split finest and
with an unsplit TI finest alike; and 512 duplicated columns at np = 2 on the
welded tail under a split fault. Every custom-P hierarchy now drops such
unknowns at install (restrict_unreached), on the standard and the rotated
path; the coarse levels' FAC patch rows are cleared by the renumbering, the
finest's kept. The repair and its zero-column asserts are removed.

scripts/sessions/fault_mg/ladder_c.py: the ladder measured — the mid
ribbons split, welded or absent at two depths, and the TI realisation on
the same nested ladders (recorded on the wiki, Fault multigrid).

Serial: 77 tests pass across the custom-P, adapt, ribbon and fault files;
parallel: the fault, rotated-row, seam-split and single-field FMG tests at
np = 2.

Underworld development team with AI support from Claude Code
…ch, the real ladder levels, and the parallel imbalance decomposed by level

Underworld development team with AI support from Claude Code
… contact and TI, and the finer base

Underworld development team with AI support from Claude Code
…e wall rotation

Placement on a curved wall (utilities/place_surface.py):
- the domain tool winds every boundary loop counter-clockwise; a hole loop wound
  against the exterior was added as area and the ribbon met nothing (annulus at
  cell sizes 0.04 and 0.08, "meshed to no triangles");
- the 2-D outcrop frame groups the wall's chords into smooth walls (the 3-D
  rule) and the splice keeps the curve's own vertices, so a dipping band's
  clearance cavity is not refused as contact away from the outcrop and the cap
  does not chord across the sagitta the area guard refuses;
- the structured ladder outcrops: rungs inside the domain to one rung short of
  the wall, closed by a small unstructured cap clipped against the domain. The
  spine keeps its rung vertices for the split; the cap is the TI bridge's home.

Custom multigrid under rotated free-slip (utilities/fault_mg.py, rotated_bc.py):
- every coarse level takes the wall rotation on its own nodes (level_wall_rotation,
  composed with a split level's pair rotation by level_rotation) and loses its
  normal rows. Cartesian coarse levels under a rotated finest kept wall-normal
  unknowns the fine operator reaches only through vanishing sines; the inode SOR
  smoother's nodal block inversion met a zero pivot and the velocity solve failed
  before its first iteration;
- the orphan rule's reach test is relative (UNREACHED_REL) and runs after the
  finest's constrained rows are zeroed;
- the finest's FAC patch rows survive the per-level restriction.

tests/test_1091_place_outcrop_curved_wall.py covers the placer on the annulus.
scripts/sessions/fault_mg/zg95_{probe,annulus,profile}.py: the Zhong & Gurnis
(1995) analogue (annulus, temperature-dependent lid, dipping trench that moves
with the overriding plate, topography and surface velocity), its probe and its
profile (meshing 5 s against a 60 s solve, the solve being Schur Krylov work).

Underworld development team with AI support from Claude Code
Underworld development team with AI support from Claude Code
On the developed temperature field of the annulus run at four ranks the inode
SOR's 2x2 block inversion met a zero pivot on the base's two finest levels with
every scalar diagonal healthy; without the grouping the same solve converged in
nine iterations to the two-rank answer. A rotated wall node keeps one tangential
row beside the two-row nodes. The finest operator is untouched. The driver gains
a level inspection (ZG95_INSPECT) and the parallel reductions.

Underworld development team with AI support from Claude Code
…river; the jigsaw renderer

Underworld development team with AI support from Claude Code
Underworld development team with AI support from Claude Code
Underworld development team with AI support from Claude Code
…ited)

Underworld development team with AI support from Claude Code
Underworld development team with AI support from Claude Code
Underworld development team with AI support from Claude Code
Underworld development team with AI support from Claude Code
…rench, hand-over note

Driver: the yield indicator is evaluated on the solve mesh into a P1 variable and
transferred to the base collectively (a rank-local "is this the base" test hung
the 8-rank fault phase); failures in the diagnostic are decided on every rank;
the figure's temperature panel is a collective polar-grid evaluation instead of a
serial-only branch; the add-on build line reports global counts; each rebuilt
add-on warm-starts from the previous velocity and pressure; the trench follows
the median overriding-plate velocity over a window beyond the fault; --verbose,
--newton_switch, --trench_off/--trench_span options; the surface and trace are
saved at checkpoints too.

Renderer: the trace falls back to the nearest earlier surface file; the unrolled
section has a vertical exaggeration of 2.

CAMPAIGN.md records the runs, results, solver state and open items for the
session taking the work over.

Underworld development team with AI support from Claude Code
One conflict, in systems/solvers.py: this branch moves Stokes' four
stress-history methods (_stress_history_prepare, _advance, _post_solve,
_create_stress_history_ddt) into _StressHistoryMixin; development changed
two comments inside them since f414da6 and nothing else. The methods stay
in the mixin, with development's two comments.

Underworld development team with AI support from Claude Code
…reach

Library:
- split_tail_levels exchanges each level's verdict, so every rank stops the
  replay at the same level; a rank-local failure of the split hung the ranks
  that went on (new test in tests/parallel/test_1089: hangs before, passes after).
- build_rotation's manifold pairing pre-pass raises collectively, like the
  pair-block assembly after it; an empty pair list no longer recomputes.
- custom_mg.install keeps the FAC patch rows of every coarse level that
  restrict_unreached left whole; only renumbered levels lose theirs.
- ThinSheet: L and c_b come from the solvers' unique-name factory (a user's
  "L" was reset to 1 by building a sheet, and two sheets shared a thickness);
  the plane-stress pressure is read from the committed stress after a
  viscoelastic solve, not from the shifted history, and p is filled on first
  access after a solve; a transversely isotropic model is refused on a
  manifold (its strain rate is not projected).
- objective_order defaults to 1 for every solver again (Louis, 2026-10-28);
  ThinSheet selects "cayley" for a first-order Jaumann history when no order
  was given. Stokes VE results and order=2 + jaumann are as on development.
- add_basal_drag / add_basal_traction take their value as conds (Charter
  section 6), and Stokes' versions non-dimensionalise the reference velocity
  and traction as the sheet's do (_nondimensional_row moved to solvers.py).
- extrude_sheet is in meshing.__all__.

Tests: test_1091 is reached (test_109* glob; check_test_coverage failed); the
welded-beats-split iteration assertion in test_1087 is gone (Charter section
8), the slip agreement stays; test_1081 and test_1084 (13 and 7 min) are tier
C, reported not gated; test_1083's thin-against-thick agreements are tier C;
test_1086 runs both tails at np >= 2 and no longer asserts a property true by
construction.

Docs: thin-sheet.md's limits paragraph (parallel extrusion, split tails) and
the co-location and ghost-cell descriptions corrected.

Underworld development team with AI support from Claude Code
…es (#869-#872)

Underworld development team with AI support from Claude Code
…itudes in test_1075

- test_1080: the model's default objective_order is 1 for every rate (Louis,
  2026-10-28); the default test now also checks that a ThinSheet selects the
  Cayley step for a first-order Jaumann history given no order, and keeps a
  given order; the Cayley test asks for the step by name.
- test_1075: the von Mises sheet test compares magnitudes. On development a
  viscoplastic stress evaluated in a model without units comes back tagged
  pascal (issue #881), and the comparison with a plain number raised.
- extrude: the boundary-value sentinels (666, 1001) are named where the base
  and surface values are chosen.

Underworld development team with AI support from Claude Code
…ted; sheet fixes

- The merge with development took #846's removal of the base refusal of a
  rotated constraint; only the Stokes saddle point routes a rotated adjoint
  (solve_rotated_adjoint), and the vector solvers' adjoint_solve transposes
  the unrotated operator. A manifold ThinSheet (its tangency is a rotated
  constraint) recorded "supported" and adjoint_solve ignored the constraint.
  Solvers declare _adjoint_routes_rotated_constraints (True on the saddle
  point); the base refuses otherwise. test_0018 gains the manifold sheet.
- ThinSheet refuses a transversely isotropic model on a manifold before it
  is installed (a caught refusal left the model in place), including a
  composite's constituents.
- The sheet's plane-stress pressure adds the solvent's Newtonian stress (the
  history carries the Maxwell element only).
- test_1089's replay test injects its rank-local failure after the split's
  last collective, the case the verdict exchange handles; injected before,
  the split itself hangs (issue #874), which no exchange after it can rescue.
  Passes with the exchange; with each rank deciding alone it hangs at np 2.

Underworld development team with AI support from Claude Code
…epository

Louis, 2026-10-28: library, tests and docs in underworld3; campaign drivers
outside it. scripts/sessions/fault_mg/ (28 files: the ZG95 annulus drivers,
the exploration stages, UWTN 2026-022's figure scripts) and the three
scripts/sessions/thin_sheet_*.py go; they are kept with the campaign. The
design note, the subsystem doc and test_1084 no longer point at them.

Underworld development team with AI support from Claude Code
Copilot AI balanced review requested due to automatic review settings October 29, 2026 00:12
@lmoresi

lmoresi commented Oct 29, 2026

Copy link
Copy Markdown
Member Author

Adversarial review

Three independent read-only passes on f414da6..488004d (2026-10-28): parallel rank asymmetry in the multigrid machinery; the manifold solves and the solver contracts; the tests. Then the branch was merged with development (bfd07b7), the findings were fixed, and a fourth pass reviewed the merge and the fixes. Every item below is fixed in this branch unless it names an issue.

Fixed

Parallel:

  • fault_mg.split_tail_levels stopped the replay of a fault on one rank when that rank's split failed after its last collective; the others kept splitting and the ranks held different hierarchies. The verdict is exchanged per level. tests/parallel/test_1089 gains the case: passes with the exchange, hangs at np 2 with each rank deciding alone. (A rank-local raise inside the split, before its own collectives, still hangs there: Fault-split multigrid: rank-local raises before collectives in the cell-located transfer and the line cut's orientation vote #874.)
  • build_rotation's manifold pairing pre-pass raised rank-locally before the collective Q assembly; it now goes through _collective_raise like the pair-block assembly. An empty pair list no longer recomputes.

Solver behaviour for users without faults:

  • custom_mg.install dropped the FAC patch rows of every coarse level; it now drops only those of levels restrict_unreached renumbered.
  • Every rotated free-slip custom-FMG solve now rotates its coarse levels (f42ca30). Measured against development on an adapted annulus (custom FMG, 2948 cells, 3 levels): 18 → 3 velocity iterations serial, 197 → 4 at np 4, same answer to nine digits. A plain Annulus(refinement=...) solves on GAMG on both and is unaffected.
  • objective_order defaulted to "cayley" for every Jaumann model, Stokes included (VE results moved at O(dt²); order 2 + Jaumann raised). Default 1 again (Louis, 2026-10-28); ThinSheet selects Cayley for a first-order Jaumann history given no order.
  • The merge took development's removal (The discrete adjoint of a rotated free-slip solve (re-opens #751, which never reached development) #846) of the base refusal of a rotated adjoint; only the Stokes saddle point routes one. A manifold ThinSheet (its tangency is a rotated constraint) recorded "supported" and adjoint_solve would transpose the unrotated operator. Solvers declare _adjoint_routes_rotated_constraints; the base refuses otherwise. test_0018 gains the case.

ThinSheet:

  • L and c_b were built with the name-shared uw.expression: building a sheet reset a user's L to 1, and two sheets shared a thickness. They come from the solvers' unique-name factory.
  • The plane-stress pressure read the flux after the history had shifted; it reads the committed stress, adds the solvent's Newtonian part, and is filled on first access after a solve.
  • A transversely isotropic model on a manifold (its strain rate is not projected) is refused before it is installed, including a composite's constituents.
  • add_basal_drag / add_basal_traction take conds (Charter §6); Stokes' versions non-dimensionalise their reference velocity and traction as the sheet's do.

Tests and CI:

  • test_1091 matched no batch glob and check_test_coverage.py failed (CI red). test_109* added.
  • test_1087 asserted that the welded tail takes at least 6 more iterations than the split one (Charter §8); removed, the slip agreement kept.
  • test_1081 (13 min) and test_1084 (7 min) gated every PR; tier C, reported not gated (Louis, 2026-10-28). test_1083's thin-against-thick agreements are characterisations: tier C.
  • test_1086 no longer reached the code it was written for (the add-on levels split by default) and asserted a property true by construction; it now runs both tails at np ≥ 2.
  • test_1080 and test_1075 follow the objective-order default and compare magnitudes (evaluate() of a viscoplastic stress returns a pascal-tagged array in a model with no units #881).

Docs and code hygiene: thin-sheet.md's limits (parallel extrusion, split tails), the co-location and ghost-cell descriptions, extrude_sheet in meshing.__all__, the four TODO(BUG) markers now name #869-#872, the boundary sentinels in extrude named. scripts/sessions/ files left the branch (Louis, 2026-10-28).

Left open (filed)

#869-#872 (the markers), #873 (an empty coarse level on a rank; not reproduced at np 4 and 6 on a long fault across a small add-on), #874 (rank-local raises in the cell-located transfer and inside the split; the line cut's orientation vote), #875 (L and c_b inlined in the residual), #876 (ThinSheet.solve takes no picard), #877 (units-model rotated solve with cdim 3 fails in _rigid_rotation_modes, present on development), #878 (no parallel test of the sheet or extrude_sheet), #879 (history projection not in the transcript; facet normals after a deform), #880 (test hardening: pinned serial values, loose bounds, unrecorded margins, four bug-fix commits without a regression test), #881 (a viscoplastic stress evaluated without units comes back in pascal; development).

Not changed (nits): direct mpi4py on the DM's communicator in extrude; the boundary argument of the sheet's basal methods is accepted and unused (documented); rotated_bc._build_rotated_custom_Pl still writes truncated patch rows back onto the shared hierarchy; _nondimensional_row's TODO about SNES_Stokes.bodyforce.

Attacks that failed

Merge conditions

  • tier A level 1 serial and tests/parallel at np 2 and 4 as in the PR body.
  • CI green.

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

This branch has not been deployed

No deployments
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