Skip to content

The add-on partitioned once, custom multigrid on empty ranks, and the rotated solve's switch and record - #890

Open
lmoresi wants to merge 84 commits into
feature/fault-split-multigridfrom
feature/fault-split-integration
Open

lmoresi wants to merge 84 commits into
feature/fault-split-multigridfrom
feature/fault-split-integration

Conversation

@lmoresi

@lmoresi lmoresi commented Oct 29, 2026

Copy link
Copy Markdown
Member

The rest of the fault-split work, stacked on #889 (split-level fault multigrid); retarget to development when that merges. Together with #882 (the rebuild leak) this is everything UWTN 2026-018 and 2026-022 describe.

What it adds

  • The add-on partitioned once (utilities/addon_partition.py, docs/developer/design/parallel-addon-distribution-2026-10.md): the finest mesh built on a static base by adapt, a cut, a placement or a split is partitioned for load once, with its add-on levels, and every transfer to it is partition-agnostic (recorded provenance, not location). A second partition keeps a split fault whole; faults are glued by name. Reviewed 2026-10-17 (record in docs/reviews/2026-10/).
  • Custom multigrid on ranks with no coarse cells (bfae3e6 and its parent): the custom-P hierarchy and the cut survive a rank that holds none of a coarse level (the kaiju np 32/52 death).
  • The rotated solve's Newton switch measured against the same reference as its convergence test (a warm start no longer stays in Picard to the end), and its record: the residual history and the line-search steps, read by the moving-fault drivers.
  • A thin sheet's prescribed normal velocity (ThinSheet.normal_velocity): the breathing of a tidally flexed shell.
  • The plex topology tables in Cython (_plex_topology.pyx).

Measured

UWTN 2026-022's fixture on this head: split 5 / welded 13 velocity iterations at k = 3, peak slip 0.1589860839 / 0.1589859512 (the note's figures); the note's "Using it" recipe runs as written. On the head before today's merges, the note's README numbers reproduced at np 1 (all of them) and np 4 (same hierarchies; iterations within one; slip within 2.4e-6).

Review

The parallel-addon lineage was reviewed on 2026-10-17. Today a pass on the whole of this PR's delta (the merged rotated solve, today's merges, the add-on and empty-rank code, the tests) found two must-fix items and several should-fix ones; the first comment lists them and what was done.

Suites (laptop, macOS, this head): tier A level 1 serial 1498 passed, 44 skipped, 0 failed; tests/parallel at np 2 180 passed, 15 skipped, 1 failed; at np 4 191 passed, 4 skipped, 1 failed; no hangs. The one failure is test_1063_constrained_freeslip_parallel[ti] (#826), as on development. Until #882 merges this PR's diff includes it; it drops out when #882 lands on development and #889 takes development in.

Issues

Left open by this PR, found in its review: #883 (the cold-start datum lift's convergence reference), #884 (the rotation-gauge projection measured from the second assembly), #885 (custom_mg.last_builder_stats is module-global; curved boundaries trigger the cross-partition rebuild), #886 (a rank-local exception decides build_transfers' retry), #887 (docs and messages behind the code), #888 (test reach: the cross-partition builder, an np 2 empty-rank case, test_1094's clamp).

Underworld development team with AI support from Claude Code

…a datum-only solve

ThinSheet.normal_velocity sets the datum of the sheet-tangency constraint, so a
sheet on a manifold can move along its normal (the radial motion of a tidally
flexed shell over an ocean) and its in-plane strain rate carries the curvature
term through the nodal normals. In the rotated nonlinear loop a cold start whose
only forcing is the prescribed datum had a zero residual at u = 0 and returned
converged in zero iterations before the lift was applied; the convergence test
now waits for the lift and takes its reference scale from the lifted iterate.

tests/test_1092: breathing (u = W rhat exactly, stress 6 eta W P) and the
degree-2 membrane closed form (tangential velocity 3/11 of the radial pattern's
surface gradient; 7.2, 3.4, 2.5 percent at h 0.3, 0.2, 0.15).

scripts/sessions/tidal_shell/tidal_shell.py: the eccentricity-tide driver
(degree 2, viscous or Maxwell sheet, collective diagnostics against the closed
form, Mollweide maps).

Underworld development team with AI support from Claude Code
… sphere, in-plane principal stresses

The maps were drawn from a rank-local evaluation of the whole grid (garbage outside rank 0's cells) and the largest principal stress picked up the zero normal eigenvalue where both in-plane stresses were compressive.

Underworld development team with AI support from Claude Code
… datum on fault pairs

When rigid rotations are a genuine null space (a drag-free sheet on a sphere, a
free-slip shell) the inner solve cannot reduce the residual's component along
them, so the outer nonlinear residual is now measured in that quotient space,
as it already was for the pressure gauge. A viscoelastic stress history on the
faceted surface leaves such a component (measured: a tidally flexed Maxwell
sheet floored at a relative residual of 1e-4 with every linear solve converged;
one iteration per step after the change). The projected-out magnitude is kept
on the solver as _rotated_rotation_gauge_residual.

A split-fault pair on a manifold takes its tangency through the pair block, so a
prescribed sheet normal velocity now rides the pair's mean-normal row (sqrt2 w,
signed against the analytic normal); before, the fault nodes stood still while
the sheet breathed (datum error 0.018 against an amplitude of 0.1).

Tidal driver: tiger-stripe faults (count, length, spacing, latitude, interface
law), per-step stress diagnostics on a collective grid through nodal stress
components (the stress expression's velocity gradients cannot cross ranks),
the objective-rate option, and the sweep plot script.

Underworld development team with AI support from Claude Code
…, shear invariant, speed) and the faulted-versus-unfaulted comparison script

Underworld development team with AI support from Claude Code
…re stress against a reference run

Underworld development team with AI support from Claude Code
…circle optimum) beside the stripe-parallel receivers

Underworld development team with AI support from Claude Code
…rden, driving-traction probes on virtual stripes, stripe azimuth, cycle plots and the Berne et al. figure analogue

CoulombFaultLaw takes a constant effective normal stress added to the reaction-fed one (compression positive), the clamped-but-modulated fault of Berne et al. 2024.

Underworld development team with AI support from Claude Code
Underworld development team with AI support from Claude Code
…ckness to the sheet's resultant units; clamping reported as a stress

Underworld development team with AI support from Claude Code
…et topology

On a sheet on a sphere the fault is an edge carried by a 3-vector; the
de-smearing mass picked the triangle's weights by the component count and gave
the mid-edge nodes an eighth of their mass, so the effective normal stress the
interface laws read (and the friction they produced) was off by node-dependent
factors of 5 to 8. The reaction is the membrane resultant, L sigma_nn, now to
0.4 percent on every pair of the breathing sphere (test_1092).

Underworld development team with AI support from Claude Code
…he prediction in the sweep plot

Amplitude (24/11) mu A wt/sqrt(1+wt^2) and lag atan(1/wt): measured at 128 steps
per period 0.0195 against 0.0193 and 32 against 28 degrees at De 0.3; the lag
excess is first order in the step (15 degrees at 32 steps per period).

Underworld development team with AI support from Claude Code
…tion coefficient

Underworld development team with AI support from Claude Code
… saved per step for the site analysis

Underworld development team with AI support from Claude Code
…pe-plane tractions (phase of the clamping, Coulomb asymmetry index); the overlay takes labelled runs

Underworld development team with AI support from Claude Code
… azimuth

Underworld development team with AI support from Claude Code
…ycle, the excess and the slip it drives

Underworld development team with AI support from Claude Code
…and polar grids, saved at cycle boundaries

Underworld development team with AI support from Claude Code
…ulted

Underworld development team with AI support from Claude Code
…rules

A fault placed as a ladder on a copy of the base's finest level gets only the
welded tail (A); in a yielding, convecting box its velocity sub-solve caps at
200 once the lid mobilises. Building C by hand, the same ladder at 4S and 2S
on the coarser base levels, each split along its own rungs, keeps the
Newton-phase velocity iterations at 29-38 with no caps (A: 71-200, 2 caps)
at A's cost per cycle.

New capability guide guides/faults-in-convection.md (skill link included):
the lean tail, the rotated loop's warm-start rules (first solve from rest;
newton_switch_rtol measured from each solve's own residual, so warm solves
need it at 1), rebuilding for a moving trench (shifted warm start, Picard
first, rebuild every few rungs), and the measured traps. The design note
records the A / C comparison and lists the ribbon replay as not yet done.

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_019aromUkeWWPzcL2Kb8365E
…le-integrated activity and net offset), saved at orbit boundaries

Underworld development team with AI support from Claude Code
… profiles (Berne et al. Fig. 4 style)

Underworld development team with AI support from Claude Code
… kinked chain of arcs lost part of its cut silently)

Underworld development team with AI support from Claude Code
…polar view

Underworld development team with AI support from Claude Code
…pe block

Underworld development team with AI support from Claude Code
…noise floor (the unfaulted shell's residual)

Underworld development team with AI support from Claude Code
…before the cycle's first step (it missed one step)

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
Three fixes found by the tumbling-fault Mohr sweep, brought onto this branch
for the box fault study:

- fault_contact: the 3-D fault normal traction is recovered pointwise (a
  linear fit of the P2-triangle consistent load on the facet vertices) rather
  than by dividing by P1 sub-lumped masses, which read zero at every vertex and
  4/3 of the value at every midpoint. The branch is keyed on the facet
  topology (mesh.dim), not the coordinate dimension, so the edge fault on a
  surface mesh keeps its lumped path.
- place_surface: the sheet cavity is closed in the three shapes that defeated
  it on adapted meshes (an enclosed kept cell, a pinched shell vertex, a flat
  fill tet), with a bounded refill loop.
- fault_split: a single split_fault keeps the mesh's multigrid tail, as the
  network path already did.

Tests: the pointwise 3-D traction and the multigrid-tail checks in
test_0848; the adapted-mesh placement cases as test_0867 (0864 was taken
on this branch).

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

Two owners kept every faulted rebuild alive: the default model's _meshes,
_variables and _solvers (strong dicts; now WeakValueDictionary, as _swarms
already was), and the coordinate objects' strong link to their mesh, pinned
through sympy's function caches (UWCoordinate, CoordinateSystem and its
accessors now hold the mesh weakly). test_0077 failed on both counts before
and passes after; full validation and the memory probe still to run.

Underworld development team with AI support from Claude Code

(cherry picked from commit 8ce92ea)
…h dies

Weak variable registration changed what a variable's lifetime means (a mesh
holds its variables weakly, so a variable the caller did not keep vanished:
22 tier-A failures). The registry holds variables strongly again and a
finalizer on each mesh drops its variables when it is freed. MeshVariable
no longer keeps its constructor arguments (_init_params held the mesh
strongly). test_0077 passes; one remaining failure, test_0864, is being
checked against the base.

Underworld development team with AI support from Claude Code

(cherry picked from commit dd7545c)
petsc4py defers destroying collected objects in parallel until a collective
PETSc.garbage_cleanup; nothing called it, so every dropped mesh's DMs,
matrices and vectors lived on (52.7 MB per rebuild at np 2 with every Python
object freed; 2217 live DMs after five rebuilds against 98 in serial).
Mesh.__init__, collective over its DM's communicator, now collects and
cleans up on that communicator. tests/parallel/test_0078 (written first:
39.6 MB per rebuild against a 10 MB bound) passes.

Underworld development team with AI support from Claude Code

(cherry picked from commit 8a955f5)
Each faulted-mesh rebuild kept the whole previous mesh: 53.7 MB per rebuild
in serial and 56.0 MB at np 2 on the moving-fault fixture, ~7 GB per node per
rebuild in kaiju production (an OOM kill after 24 rebuilds). Four owners:

- the default model's _meshes and _solvers registries (now weak; _variables
  stays the variables' owner and drops a mesh's variables when it is freed);
- sympy's caches holding coordinate symbols that held their mesh
  (UWCoordinate, CoordinateSystem and its accessors now hold it weakly);
- MeshVariable._init_params, the constructor arguments kept with the mesh;
- in parallel, petsc4py's deferred destroy list, never emptied: Mesh.__init__
  now collects and runs PETSc.garbage_cleanup on its DM's communicator.

After: 0.1 MB per rebuild in serial and at np 2. tests/test_0077 and
tests/parallel/test_0078, both written first and failing, pass. Tier A
level 1: 1495 passed, 1 failed (test_0864, failing identically on the base);
np 2 custom-MG / rotated / fault-split: 19 passed, 4 skipped. Numbers and
method in scripts/sessions/rebuild_leak/README.md.

Underworld development team with AI support from Claude Code

(cherry picked from commit 547c2e5)
…exes

Review findings on the leak fix:
- Model._part_objects (the step transcript's part -> object map) was a strong
  dict keyed by instance: every solver that solved inside model.step was kept,
  with its mesh. Now weak.
- A variable no longer kept its mesh (a helper returning only a variable lost
  the mesh). A variable now holds its mesh; the mesh owns its variables
  (mesh._vars a plain dict, the cycle collected with the mesh); the model's
  _variables is a weak index, so the finalizer purge (and the stale stats()
  temporaries it missed) is gone. Variable symbols hold the variable weakly,
  so sympy's caches pin neither.
- _primary_mesh_id outlived its mesh and could be taken by a reused address,
  differently per rank: the primary mesh is now a weak reference, a freed
  primary is replaced by the next mesh, and set_primary_mesh always sets it.
- The .msh path's cleanup now uses the communicator the reader uses.
test_0077 gains three tests (a variable keeps its mesh; a solver that solved
in a step with transcripts on is freed; the primary mesh is released and
replaced), each failing on 547c2e5 and passing here.

Underworld development team with AI support from Claude Code

(cherry picked from commit 56df136)
A mesh now owns its variables, so a model reset no longer releases one and
the tests' orphan never appeared (3 tier-A failures, premise only). The
orphan is made the way the statistics helpers make it: the variable taken off
mesh.vars and collected, its DM field left behind. The assertions on packing
around the orphan are unchanged.

Underworld development team with AI support from Claude Code

(cherry picked from commit ff782c4)
Underworld development team with AI support from Claude Code

(cherry picked from commit 5b4e365)
Underworld development team with AI support from Claude Code

(cherry picked from commit fb9b1ba)
Underworld development team with AI support from Claude Code

(cherry picked from commit b8624a4)
Underworld development team with AI support from Claude Code
WIP, hyperion shutdown 2026-10-27

Underworld development team with AI support from Claude Code
WIP, hyperion shutdown 2026-10-27

Underworld development team with AI support from Claude Code
# Conflicts:
#	src/underworld3/utilities/rotated_bc.py
#	tests/test_1018_rotated_freeslip.py
…' into HEAD

# Conflicts:
#	src/underworld3/utilities/rotated_bc.py
- A snapshot collects before it visits the registered meshes
  (Model._live_meshes). A dropped mesh with variables is a reference cycle
  and stays in the weak registry until the cyclic collector runs, at a
  different moment on each rank; write_snapshot's per-mesh collective
  checkpoint write then hung at np 2 (test_0079 hangs before, passes after;
  the in-memory snapshot raised "duplicate mesh name" in serial).
- A swarm holds its mesh. Found by id in the weak registry, a mesh nobody
  else held was freed under the swarm (swarm.mesh returned None).
- The base variable holds its user-facing wrapper, so the variable the model
  indexes lives as long as the mesh that owns the base. A dropped wrapper
  vanished from the registry, and units, scaling and the adjoint skipped it.
- MeshVariable no longer destroys its vectors in __del__: it is freed by the
  cyclic collector at per-rank times, where an explicit destroy of the
  parallel vector is a collective call by one rank. petsc4py defers it to
  the cleanup the next mesh construction runs.

The three serial tests in test_0077 fail before and pass after.

Underworld development team with AI support from Claude Code
It read /proc/self/statm and failed with FileNotFoundError on macOS. On
macOS the resident set does not track freed memory: with the fix, PETSc's
object table is unchanged between 4 and 8 rebuilds (284 DMs, 221 Mats, 298
Vecs on rank 0 at np 2) while the resident set rises 14-26 MB per rebuild,
so the bound is a Linux assertion.

Underworld development team with AI support from Claude Code
…ords out

The ownership rules review 3 added (the base variable holds its wrapper, a
swarm holds its mesh, collective traversals collect first, no parallel destroy
from __del__) go into docs/developer/subsystems/model-orchestration.md beside
the rest. The probes, run records and README under scripts/sessions/
rebuild_leak leave the repository (Louis, 2026-10-28: library and expertise
in underworld3, campaign drivers and run outputs outside it); they are kept
in ~/+Simulations/rebuild_leak, and the review record goes on the PR.

Underworld development team with AI support from Claude Code
… per snapshot

- A proxied swarm variable's remesh callback held the variable strongly; the
  proxy's mesh owns the proxy (through the base variable and its wrapper), so
  a swarm moved off a long-lived mesh left that mesh keeping the variable and
  the swarm's new mesh. The callback holds it weakly (test_0077, fails before).
- The variable statistics destroy their temporary's vectors where they discard
  it, a point every rank reaches; left to the collector they waited on
  petsc4py's deferred list until the next mesh, one vector per call.
- Each snapshot entry point collects once, on every rank, and passes the
  list down; _solvers_of and the metadata writer no longer collect again,
  which they did inside rank-0-only blocks.
- test_0077 is level 2 and tier B (two tests solve; all are new and proven);
  test_0079 uses the thread timeout and gains the in-memory snapshot case;
  the orchestration doc states the Swarm.__del__ exception; a stale comment
  in Swarm.__init__ and test_1058's docstring corrected.

Underworld development team with AI support from Claude Code
… the review fixes)

Six conflicts, resolved:
- constitutive_models.py (two hunks) and ddt.py (two): development's
  _etd_maxwell_time / _update_exp_values (#851: the step as its own constant,
  the declared vanishing floor, expm1, units) supersede this lineage's
  9e97670, which made the same repair (a viscosity varying in space keeps
  its elastic response); test_1093 from 9e97670 stays and checks it.
- solvers.py: development's #850 guard in _check_velocity_preconditioner, with
  this lineage's fault-contact marker kept (a faulted solve with no walls takes
  the same rotated loop).
- thin_sheet.py: the lineage's normal_velocity property, then the review's
  add_basal_drag(conds, ...).
- scripts/test.sh: the review's comment on the test_108*/test_109* batch.
- fault-split-multigrid.md: the lineage's text (it removed co-location: the
  add-on is partitioned once), not the review's co-location bullet.

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

# Conflicts:
#	scripts/sessions/fault_mg/fig_parallel_load.py
…gauge vectors

- guides/faults-in-convection.md (a capability guide, read as a skill) said
  solve(picard=N) raises on the rotated path and that the Newton switch reads
  the warm start's own residual, with newton_switch_rtol = 1.0 as the
  workaround; on this branch picard=N stages Picard then Newton and the switch
  reads the convergence reference (371d3bd). Corrected.
- Campaign drivers and probes leave the repository (Louis, 2026-10-28):
  scripts/sessions/parallel_addon, tidal_shell, box_fault_cyclic, and the last
  fault_mg file; the design note's probe references say where they live.
- test_1093 duplicated development's test_1053 (same fixture and checks) and
  is removed. New tests start at tier B: test_1098, test_1100's table test,
  and the two newest tests of test_1018, whose tiers now go on each test.
- The rotated solve destroys its rotation-gauge mode vectors (one leaked per
  mode per solve) and warns when the projected-out rigid-rotation component
  dominates a converged residual, as the pressure gauge does (a net torque on
  free-slip walls has no solution and reported converged in silence).
- FAULT_MULTIGRID_HIERARCHIES no longer lists co-location as to come.

Underworld development team with AI support from Claude Code
@lmoresi

lmoresi commented Oct 29, 2026

Copy link
Copy Markdown
Member Author

Adversarial review

The parallel-addon lineage had its own review on 2026-10-17 (docs/reviews/2026-10/). This pass (2026-10-29, read-only) covered the whole of this PR's delta: the merged rotated_bc.py (two lineages edited the same hunk of solve_rotated_freeslip), today's merges of #889 and #882 into this branch, the add-on and empty-rank code where the earlier review did not reach, and the tests. Every item below is fixed in this branch unless it names an issue.

Fixed

  • guides/faults-in-convection.md (a capability guide, read by sessions as a skill) said solve(picard=N) raises on the rotated path and that the Newton switch reads the warm start's own residual, with newton_switch_rtol = 1.0 as the workaround. On this branch picard=N stages Picard then Newton (test_1018::test_rotated_freeslip_picard_under_newton_stages_picard_then_newton) and the switch reads the convergence reference (371d3bd). Corrected.
  • Campaign drivers and probes (scripts/sessions/parallel_addon, tidal_shell, box_fault_cyclic, a fault_mg figure script) left the branch (Louis, 2026-10-15 and 2026-10-28); the design note says where the probes live.
  • The rotation-gauge projection leaked one Vec per mode per solve, and a forcing with a net torque on free-slip walls (no solution) reported converged in silence; the modes are destroyed, and a dominant projected-out component warns, as the pressure gauge does.
  • test_1093 duplicated development's test_1053 (same fixture and checks); removed.
  • New tests started at tier A (test_1098, test_1100's table test, two new tests in test_1018 inheriting its module tier); tier B, and test_1018's tiers go on each test.
  • FAULT_MULTIGRID_HIERARCHIES.md listed co-location as to come; it was done and retired.

Merge resolutions (c842c11)

Left open (filed)

Attacks that failed

  • Co-location is gone: colocate_split_level, fault_star_owner and _fault_star_cells have no reference in src or tests.
  • normal_velocity touches neither Split-level fault multigrid, the thin-sheet solver, and sheet extrusion #889's constitutive_model setter nor the pressure refresh; a datum forfeits the cross-solve cache, so a changed datum cannot ride a stale Q. On fault pairs the datum row matches the stored factor.
  • Parallel uniformity of the rotated loop: every new branch reads collective norms or dots; _fitted_normal_traction reaches _collective_raise once on every rank, early return included.
  • The info keys the note's recipe and the tests read (vel_its_last, pres_its_last, converged, velocity_pc) are present; rnorm_history and line_search_alpha have consistent lengths on all three increment branches.
  • _plex_topology.pyx: the extern PetscInt is the header's type; empty strata reshape cleanly; the kd-tree index order matches the old closure slice.
  • redistribute_by_assignment: all three callers take the new 4-tuple.
  • CI reach: check_test_coverage.py reports every test file reachable; fault_fixtures.py is not collected.

Merge conditions

@github-actions

Copy link
Copy Markdown

🔍 Architectural Review Checklist

Thank you for submitting an architectural review! Reviewers should validate:

Design & Architecture

  • Design rationale is clear and well-justified
  • Trade-offs are documented with alternatives considered
  • System architecture is comprehensible
  • Integration points are clearly identified

Implementation

  • Implementation matches documented design
  • Code quality meets project standards
  • Breaking changes are identified and justified
  • Backward compatibility is properly addressed

Testing & Validation

  • Testing strategy is adequate for the changes
  • Test coverage is sufficient
  • Edge cases are properly covered
  • Performance impact has been assessed

Documentation

  • Known limitations are clearly documented
  • Benefits are quantified with metrics
  • User-facing changes are documented
  • Migration guide provided (if needed)

Review Process: See CODE-REVIEW-PROCESS.md

Approval: This PR merge = Review formally approved

@github-actions

Copy link
Copy Markdown

✅ Test Suite: success

…base level

The prose placed the 2S and 4S ladders one and two levels below the finest.
The guide's own code (host = base_levels[len - k] for k = chain..1) and its
cell counts (lean chain 642, 2924, 10950, 11612) put 2S on the finest base
level beside the S ladder and 4S one level down; the coarser levels stay
welded.

Underworld development team with AI support from Claude Code
@github-actions

Copy link
Copy Markdown

✅ Test Suite: success

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.

1 participant