Repository navigation
Release v3.2.0 - #805
Merged
Merged
Release v3.2.0#805
Conversation
…xactness tests The recovered review gates move into _validity_and_orientation_gates(), called by place_sheet AND place_thin_volume (the embed shares the sewing skeleton, so it shares the defect classes). check_faces is promoted from serial-only to every rank count: it is issue #520's oracle, the plain distributed box passes it cleanly, and with the vertex SF attached before the interpolate the rebuilt mesh passes it too — so the gate that found the defect now stands guard in every parallel test that places anything. check_geometry stays serial-only (measured false-positives on healthy distributed meshes on this stack). New tests: P2 Poisson must reproduce a quadratic on the placed-sheet mesh (test_0854) and through the embedded zone (test_0855) to solver precision — the end-to-end assembly oracle that was missing when the mixed-handedness defect shipped: a mesh can pass conformity, Euler, exact volume and every abs()-based gate while assembling a wrong operator; a contained exact solution cannot be faked. Measured on the fixed build: max error 1e-11 (sheet) and 9e-16 (volume), serial and np=2,4; plain Stokes on the placed mesh at np=2 is 12.1 s against the base box's 7.6 s — issue #520's acceptance was 2x, the defect measured 3594 s. Underworld development team with AI support from Claude Code
…terface-orientation Fix #520: seam cone reconciliation + the recovered orientation fix — parallel FE on placed meshes works
…n test The interface-cone fix (#521, closing #520) verifies independently on this branch: CheckFaces green at np=2 on the rebuilt mesh, placed-mesh plain Stokes 24 s (was 3594 s), and — the acceptance that motivated the whole ladder — the COMPOSED chain place_sheet -> split_fault -> frictionless contact at np=2 converges in 18 s with the serial answer (peak slip 0.1461 vs 0.1462) and machine-zero no-opening leak. ptest_0852 locks that chain as a regression gate: the solve IS the gate, because both #518-era defects passed every topological check. Underworld development team with AI support from Claude Code
build(mesher="place") runs the native chain — static base, edge_split graded adaptation along the network, each prepared rim triangulated at h (interior grid; centreline points for ligament-trimmed strips; edge flips kill all-rim triangles at corners — the split's precondition), place_sheet per piece (clearance 0.8, the measured working window on edge_split children), then split. Non-cumulative and parallel-capable (ptest_0852). "embed" stays the network default: on graded bases the placed composition builds and converges but the solve is pathological (3850 s vs 125 s embed at finer h) — recorded as the open operator-health item; on uniform bases place is healthy (22 s incl solve at np=2). Underworld development team with AI support from Claude Code
…d (lifecycle half 1) (#522) The design ruling this serves: a fault is ADDED to the running distributed mesh and a spent one DELETED, with no redistribution ever. Removal is the inverse placement: the object's labels seed the carve (cells = a thin volume, faces = a sheet; a zone's companion _skin label goes with it), the cavity is refilled by a PLAIN gmsh fill at the background scale — erasing the layer-scale grading — and the labels vanish with their points (gated: globally empty afterwards). Every OTHER embedded surface is held and count-gated exactly as a placement holds it; the interface readers gain an exclude= parameter so a removal does not hold its own object against itself. Parallel via the same gather-first surgery; the object's geometry is allgathered from its labels first, so removal works whatever the current distribution. Composing the lifecycle surfaced three robustness fixes, all measured: - The sheet carve gains the volume carve's shell-growth: a sheet placed into a REFILLED region can pinch against the new connectivity where the same sheet in the original mesh did not. The growth is now one shared helper (_closed_shell_3d) used by every 3-D carve — the flagged unification debt, paid. - The sheet carve gains the centroid rule: the plane-straddle test covers triangle interiors, and a refill's locally-small cells can straddle a rim EDGE with every vertex outside reach (gmsh: "a segment and a facet intersect"). Any cell whose centre is within reach of the bounded sheet is dropped. - Surgery-rank catches broadened to Exception: a raw gmsh error is a plain Exception, and an uncaught raise on the surgery rank is a HANG for its peers — every failure must become a collective refusal. - The removal carve does NOT use the centroid rule (nothing is being embedded, there are no straddlers to catch); with it, a sheet removal reached the domain wall that the sheet's own placement cleared. Round trip measured at np=1..4: add zone -> FE-exact (9e-16) -> remove -> FE-exact (4e-11), cell count back within ~8% of base -> add sheet in the SAME region -> remove again -> FE-exact. Cell counts after a refill are partition-sensitive (gmsh fill vs shell node ordering) and are not part of the contract; the invariants are. tests/test_0856 (5 serial, number claimed in the ledger) and ptest_0856 (np=2,4); suites 0853-0855 green against the widened sheet carve. Underworld development team with AI support from Claude Code
… count (#523) The lifecycle ruling's second half, first instalment. The uninterp + SF-first + DMPlexInterpolate rebuild is generalised to any dimension (_rebuild_sewn: cone size dim+1, the handedness flip reads signed area in 2-D, join recovery over edges alone) and BOTH 2-D serial paths now go through it — reconnect.rebuild_cavities leaves the placement family. The handedness census (_cell_volumes_signed6) is dimension-generic too: signed 2*area in 2-D, same structural point (abs() is blind to inversion). place_along_lines runs distributed via _place_one_parallel: mark by distance, gather the region to one rank, carve and fill there, rebuild collectively, label by the chain's new ids, gate globally (area conserved, inversion, the validity battery with check_faces at every rank count, held surfaces compared per rank — the mesh outside the region is untouched, so per-rank equality is exactly the right gate). Scope, stated not fudged: INTERIOR surfaces — a wall-touching end needs the end-settling machinery (wall-vertex slides, facet splits) whose collective form is not built, and refuses collectively with the reason; the serial path keeps the full feature set. Wall proximity is judged against TRUE wall segments (support 1 AND unshared — a seam edge also has local support 1, the 3-D lesson one dimension down). Measured: same facet count as serial and P2 FE exactness (x^2+y^2 to 1e-11) at np=1..4. ptest_0853 added (FE oracle + collective wall refusal); all serial suites and the 3-D ptests green. Next instalment: the 2-D ribbon and 2-D removal on the same skeleton. Underworld development team with AI support from Claude Code
…oth dimensions (#524) The lifecycle ruling's remaining instalments. _place_thin_volume_2d is rewritten in the collective form (rank-0 assembly + broadcast, mark by skin distance, gather, target carve with ring growth and orphan promotion, holes fill, one rebuild, cell+skin labels by join, collective gates incl. the validity battery) — serial is the same path at size 1, and the 2-D serial refusal is gone. remove_embedded gains its 2-D branch on the same skeleton (label-seeded ring carve, PLAIN background-scale fill); the face soup and the interface readers generalise (edges as the 2-D facets; the exclusion that stops a removal holding its own object against itself). The composition is now the tested contract in BOTH dimensions: zone -> FE-exact -> remove -> FE-exact -> place a line in the same region -> remove again, at np=1..4, with zero area drift — formalised as test_the_two_dimensional_lifecycle_composes (serial) and the two distributed lifecycle tests in ptest_0856 (2-D and 3-D, each stage held to the P2 quadratic oracle). Suites 0853-0856 and all four ptest files green at np=2 and 4. Underworld development team with AI support from Claude Code
… bowl cavity + remeshed cap) (#525) The science case this machinery exists for: plate-boundary faults intersect the free surface. Three pieces, spike-proven first (outcrop_sheet_spike.py): 1. THE SPECIFY-LONG CONTRACT (ruling 2026-08-11): sheets may extend PAST the domain. _clip_sheet_to_box Sutherland-Hodgman-clips each triangle against the box planes — cut points snap exactly onto the plane and are computed in canonical edge order so shared edges dedupe bitwise — and _outcrop_chain extracts the trace as one ordered polyline on ONE wall (box-edge and multi-wall outcrops refuse with the reason). 2. THE BOWL CAVITY: with an outcrop, wall vertices inside the cap become victims and _closed_shell_3d returns the dropped cells' wall faces as CAP faces (any other wall contact still refuses); the manifold check runs on shell UNION cap. The cap over the cavity is PRE-MESHED by the 2-D fill in the wall plane's own coordinates — rim ring verbatim, outcrop chain embedded free-end — because a geo surface bounded by the discrete rim RESAMPLES it (measured: 84 renumbered nodes, volume 1.31, Euler -46; MeshOnlyEmpty does not prevent it). The 3-D fill takes the surface loop [shell + discrete cap] with the sheet embedded, outcrop nodes shared by tag between cap and sheet. 3. EXPLICIT RELABELLING: the cap's new wall faces carry exactly the (label, value) pairs the replaced faces carried, written with their full closures — joins cannot recover new points, and an unlabelled boundary patch silently loses its Dirichlet conditions. Measured, serial and np=2..4: identical facet counts, P2 quadratic FE-exact (2e-11..5e-11) WITH a Dirichlet condition gripping the remeshed Top, full Top-label coverage on the new boundary faces. Interior sheets unchanged (suites 0853-0856, 44 serial + placement ptests green). The outcropping thin-volume BAND is the remaining instalment (plan carries the design: cap-with-hole + interior skin as one closed loop). Underworld development team with AI support from Claude Code
* Outcropping zones: the fault zone leaves its band on the surface The thin-volume form of the outcrop, completing "get all the implementation done". The assembly is clipped against the domain box in OCC (specify-long: patches may protrude; clipped faces snap exactly onto the wall plane), and a clipped face left in ONE wall plane becomes the zone's BAND. The gap's boundary is then ONE closed surface of three discrete pieces — the interior shell, the pre-meshed CAP (an annulus in the wall plane: rim ring verbatim, the band outline as its HOLE, via the 2-D fill), and the interior part of the skin, rim-matched to the cap's hole. The band's triangles become boundary faces carrying BOTH the zone's skin label and the wall's own labels; the cap's annulus is relabelled the same way, full closures, counts gated. The volume carve gains open_wall (victims may lie in the open wall's plane; the shell may open there via the shared _closed_shell_3d); box-edge bands and multi-wall contact refuse with the reason. Measured serial and np=2,4: identical zone counts, P2 quadratic FE-exact (5e-11..7e-11) with Dirichlet gripping the remeshed Top, full Top coverage. Interior zones unchanged: suites 0853-0856 (45 serial) and the placement ptests green. Band contract test added to test_0855. Underworld development team with AI support from Claude Code * docs: the outcropping-fault section (specify-long, the cap, the labels) Underworld development team with AI support from Claude Code
Split-node faults: zero-thickness fault contacts in 2-D and 3-D, parallel, with interface constitutive laws
Replace the hard-coded .pixi environment path test with validation of the active hpc environment, project root, Conda prefix, and Python executable so detached environments on scratch are accepted safely. Prioritize the Pixi C++ runtime during Gadi configure and petsc4py builds for NumPy 2 compatibility, derive build concurrency from PBS_NCPUS instead of oversubscribing with 40 jobs, and update versioned PETSc architecture diagnostics. Also replace legacy which calls and resolve ShellCheck declaration warnings.
* Update adapt vs ref24 cost script for current APIs * Restore ref24 comparison free-slip and warm-start timing
* Update anisotropic convection demo for current APIs * Restore free-slip upper boundary in anisotropic convection demo
…ndent pinning, explicit-GAMG opt-out, the #492 CI disarm (#530) * Make the pinned-band emptiness test collective, and create the label on every rank label_interface_band raised rank-locally when its own subdomain held no band cell. A rank the surface never enters legitimately has an empty local band — only a GLOBALLY empty band is a user error. With the rank-local raise, a corner-confined surface at np=4 left three ranks raising while the fourth entered the collective mover and hung to the 300 s timeout (measured 2026-08-06, review of PR #488). Now the pinned count is allreduced, the error is raised on every rank only when the band is globally empty, and the DMLabel is created on every rank — including band-less ones — so downstream consumers see a symmetric label. Underworld development team with AI support from Claude Code * Make the pinned band partition-independent: synchronise pinned vertices by coordinate The straddle test and the halo-ring growth both walk rank-local cells, and cells are partitioned disjointly, so a shared vertex whose cut (or ring) cell lives on the neighbour rank was pinned there but not on the owner. The mover then moved the owned copy and the neighbour's pinned copy followed through the SF: measured at np=4 (review of PR #488, 2026-08-06), two pinned leaves moved 4.2e-3 and 1.9e-3 while np=2/3 passed on partition luck. The pinned set is now synchronised across ranks by rounded coordinate after the core band and after each halo ring. The parallel test gains two guards for the band-less rank np=4 creates: getStratumIS on a strata-less DMLabel is a segfault (#291), and max() of a zero-size moved array raises rank-locally and desyncs the collectives that follow. Underworld development team with AI support from Claude Code * An explicit GAMG choice opts out of the custom-P multigrid pickup, without crashing build_transfers now stands down — returning (None, None), never bare None — when the solver carries an explicit preconditioner="gamg" or a latched _pc_user_override (the user wrote their own pc_type into petsc_options). Before this gate, the opportunistic mesh-owned pickup silently clobbered the explicit choice back to the custom-P PCMG at solve time: both arms of test_0842's fmg-vs-gamg comparison ran pc_type=mg, so a user could not opt out and the comparison was vacuous. Both call sites (auto_inject_custom_mg and the rotated free-slip _build_rotated_custom_Pl) guard the unpack so a "no hierarchy" answer degrades to the default preconditioner instead of a TypeError mid-solve — a bare return inside this gate is exactly what the #488 x #471 merge once shipped. Underworld development team with AI support from Claude Code * Disarm the #492 trap in test_0842 and make the fmg-vs-gamg comparison real Both MeshVariables are created before any solver runs: creating one after a solve rebuilds mesh.dm under the custom-MG coarse/fine links (issue #492), and that dangling reference is what segfaulted Linux CI downstream of this test. Each arm now asserts the PC type it actually ran (mg vs gamg), so the comparison can never silently become FMG-vs-itself again. The tolerance pair (ksp_rtol 1e-9, nodal bound 1e-7) is chosen to catch the once-shipped true-residual stall signature; the geometric bundle now owns its fgmres outer pairing (#514/#515), so the fmg arm's rtol is enforced in the true residual norm. Underworld development team with AI support from Claude Code * CI crashes print a Python stack: PYTHONFAULTHANDLER=1 in scripts/test.sh Underworld development team with AI support from Claude Code
…p, and a gmsh finalize cascade (#533) * Fix inverted inside test for parallel edges so crossing patches are detected on every LAPACK build crossing_segment missed genuine X crossings whenever a polygon edge was EXACTLY parallel to the plane-plane intersection line: the parallel-edge branch of _line_clip_interval returned "outside" for num < 0, but for an interior point num = nrm.(A - p0) is negative. CI's OpenBLAS returns exact zeros for axis-aligned plane bases, so den == 0.0 hit the inverted branch and the 0851 fault-network tests refused with a bogus near-miss (and the BoxInternalPatch crossing pre-check let a raw tetgen PLC error escape); macOS Accelerate left ~1e-17 jitter in den and took the correct division branch by luck, which is why the tests only failed on CI. Adds a deterministic regression test with axis-aligned patches (den is exactly zero on every platform): a genuine crossing must be found, and a parallel-edge non-crossing must still return None. Underworld development team with AI support from Claude Code * Finalize the gmsh session even when the 3-D mesher fails, so one bad mesh cannot poison every later one BoxInternalPatch called gmsh.finalize() only on success; an escaped mesher exception (e.g. a tetgen PLC error on a crossing patch set) left the session initialized with the failed model, and the NEXT mesh constructor in the same process then wrote an invalid .msh that PETSc rejects with error 79 ("expecting $Nodes"). This is exactly how the CI crossing-detection miss in test_0851_fault_network_3d took down the unrelated test_0851_std_reduction_method mesh build two tests later. generate/write now run under try/finally. TODO(BUG) notes the remaining fault-session follow-up: patch sets the crossing pre-check cannot classify (coplanar overlap, edge touching) can still reach tetgen raw. Underworld development team with AI support from Claude Code
…, and 56 orphaned test files rejoined to CI (#504) (#532) * EnhancedMeshVariable.clone was uncallable either way — forward name/varsymbol (#498) The enhanced wrapper declared clone(self) and forwarded nothing, while the base MeshVariable.clone requires (name, varsymbol). Passing the arguments every in-tree caller uses raised TypeError, and the zero-argument form the wrapper offered would have failed inside the base — so clone() could not be called at all, and six shipped examples aborted on it. clone(name, varsymbol) now mirrors the base signature and returns an EnhancedMeshVariable (same mesh/shape/type/degree/continuity, independent data), so chained clones and enhanced features keep working. Regression test covers each documented argument, clone-of-clone, data independence, and discontinuous spaces; all four failed with the reported TypeError before the fix. Fixes #498 Underworld development team with AI support from Claude Code * timing.print_table: accept the legacy keywords 18 examples still pass (#499) The move to the PETSc-log backend cut print_table down to (filename, format), but 18 shipped examples still call it with the old display_fraction / group_by / output_file keywords and aborted with TypeError on the first timing report. output_file is restored as a working alias for filename (same behaviour: the table is written to that file; passing both is an error). The PETSc log backend cannot honour display_fraction (no row culling) or group_by (events, not lines/routines), so those are accepted with a visible FutureWarning rather than raising or being silently swallowed. Regression tests cover the exact example call patterns, that output_file really writes, the filename/output_file conflict, and that the current (filename, format) interface is unchanged; four of them failed with the reported TypeError before the fix. Fixes #499 Underworld development team with AI support from Claude Code * test.sh: run the rotated free-slip suite and other orphaned batches in CI (#504) The serial batch globs skipped test_1018_rotated_freeslip.py entirely, so the 19-test rotated free-slip suite (including the #497 regression guard) never ran in CI. The one-off audit the issue asked for found 56 orphaned files in total. Batches now cover test_101*/test_102* (rotated free-slip, stokes MG, boundary flux), test_105* (VE/VEP, ddt, yield), test_005[1-9]*/test_006[0-1]* (mesh/deform/callback utilities), test_02* (solver smoke), test_1120*/ test_1450*, and the six named (un-numbered) test files - every added batch verified green locally with the exact test.sh pytest invocation. test_106* and test_107* stay out deliberately: they carry level_2/level_3 + slow + tier_b/tier_c suites and need a triage decision before gating CI (noted in the script). Also fixed in passing: the inline comment on the test_1110 line swallowed its "|| status=1", so annulus adv-diff failures could never fail the script. Fixes #504 Underworld development team with AI support from Claude Code
…le-field FMG unlock, and multiplier_schur_pc verified (#478 #483 #484 #486) (#534) * Make every preconditioner fallback readable: solver.pc_fallbacks records what degraded, where, and why (#484) Ten of the twelve places a solver silently degrades its preconditioner (the single-field FMG gate, missing hierarchy, transfer-build failures, guard skips, forced Galerkin, the rotated-path substitutions) left no queryable state — a warning at best, nothing at worst — so neither a user nor a test could ask "did I get what I asked for?". Every site now writes through one recorder (_record_pc_fallback) into a public read-only property, solver.pc_fallbacks, with a fixed reason vocabulary: unavailable / declined / build_failed / check_skipped / forced. The record is cleared whenever the preconditioner options re-resolve (the same staleness rule as _pc_resolved), and solve-time sites (custom_mg, rotated_bc) re-record each solve. Warnings are unchanged where they exist; the silent "auto" declines gain a record but deliberately NO new warning. rotated_bc mirrors its two degraded arms into the same record; its own ctx keys stay authoritative. Behaviourally inert: no option value, route, or default changes. tests/test_1022_pc_fallback_observability.py: every probe proven to fire and proven silent on a clean solve (empty record is the global negative control). Underworld development team with AI support from Claude Code * Honour user-set snes_rtol/ksp_atol on the saddle-point solvers, and derive tolerance margins through one documented mechanism (#483) The Stokes solve re-pushed snes_rtol and ksp_atol before every solve (_reassert_outer_tolerances), silently discarding a value the user set explicitly — the worst reachability middle ground: documented as settable, actually owned. The re-push now runs through the same recorded-ownership latch that made snes_max_it reachable (ruling D18), generalised to a dict (_resolve_owned_option/_push_owned_option, with _resolve_snes_max_it kept as a named delegate): the framework keeps asserting the tolerance-derived values until the user sets a key, after which their value is honoured across solves. A user who never touches the keys sees byte-identical behaviour (proved by the no-override arms). Second half: Stokes and Stokes_Constrained derived different option keys from `tolerance` through two unrelated hand-rolled setters. Both now apply a per-class table (_TOLERANCE_DERIVED_KEYS, the base table keeping its historical _INNER_RTOL_MARGIN name as an alias) through one base method (_derive_tolerance_margins), at SET time only. The difference in table contents is deliberate and now documented: the base class derives the inner fieldsplit margins; Constrained derives the outer ksp_rtol and the Eisenstat-Walker pins, because EW pinning owns its outer accuracy. Constrained.__init__'s duplicate EW writes route through the same helper. Both `tolerance` docstrings now state the ownership table (OWNED vs DERIVED-at-set-time keys, and the EW caveat on ksp_rtol). tests/test_1023_saddle_tolerance_ownership.py reads the LIVE PETSc objects after two solves (the second solve is the one that used to clobber), with no-override negative controls on both classes. Stakeholder tests that write these keys (0820, 1013, 1014x2) pass. Underworld development team with AI support from Claude Code * Honour explicit geometric multigrid on single-field solvers via injection-free custom-P transfers (#478) preconditioner="fmg" on a scalar/vector solver was declined to GAMG with a warning, because the NATIVE geometric-FMG path needs DMCreateInjection, which PETSc cannot reliably build for a single-field discretisation on a refined DMPlex (err62, #276). That gate locked geometric MG out of every scalar/vector solver even though the robust custom-P route (own prolongations + Galerkin RAP, no injection anywhere) was already in utilities.custom_mg. The explicit request is now honoured: the gate flags the reroute (_pc_single_field_geo_requested) and custom_mg.build_transfers grows a THIRD hierarchy source — requested-native, wrapping the mesh's own dm_hierarchy tail in _DMLevelView adapters — resolved after solver-set and mesh-owned so build_transfers stays the single "which hierarchy" owner (#471, composing with #530's explicit-gamg opt-out, which is untouched). Installation rides the existing auto_inject_custom_mg solve hooks and the shared multigrid_options bundle (#468/#515), so the options DB deliberately keeps GAMG as the safe degrade base until the live PC is configured at first solve. Build failure degrades to that base through the recorded barycentric -> RBF -> default ladder (#484). Contract line, stated in both docstrings: preconditioner="fmg" is a PREFERENCE (degrades readably); set_custom_fmg is a DEMAND (raises). "auto" is deliberately unchanged — flipping the single-field default is its own validation campaign per #478 — it keeps GAMG and records the decline as the migration probe. Found en route: a gmsh-imported BASE hierarchy level carries section-only coordinates (PetscContainer, no PetscFE), and DMCreateInterpolation from such a source silently returns a ZERO matrix; _DMLevelView therefore works on a clone and installs a P1 Lagrange coordinate FE when missing. tests/test_1020 becomes the unlock test (live PC "mg" over every hierarchy level on the exact #276 err62 geometries; degrade arm proven by monkeypatched builders; auto-unchanged arm; Stokes velocity block untouched; DB-vs-live honesty arm) and tests/parallel/ptest_1020_fmg_single_field_parallel.py proves route parity at np=2. Underworld development team with AI support from Claude Code * multiplier_schur_pc: prove where it is live, instrument where it is inert, correct the drifted docstrings (#486) The issue measured four identical convergence rows across two decades of viscosity contrast and asked "verify or remove". Tracing PETSc's fieldsplit.c settles it without ambiguity: the flag swaps only the Pmat (h,h) block, and under Stokes_Constrained's own defaults (selfp + diag_use_amat) selfp assembles the Schur preconditioner from AMAT sub-blocks — the swapped block is provably never read, which is exactly what the issue measured. The flag IS read under pc_fieldsplit_schur_precondition='a11' (Sp = the grouped [p,h] Pmat block) and under a monolithic direct factorisation of the Pmat. Verdict: keep + instrument, not remove. Instrumentation: at the hh_pc selection in _setup_solver, an explicit opt-in that cannot reach the PC (not a11, diag_use_amat set, not a direct solve) records reason='declined' in pc_fallbacks AND warns — an explicit opt-in silently doing nothing is exactly the #477 class. Docstrings: multiplier_schur_pc now states its two live regimes and the inert one; the Constrained saddle_preconditioner claim that selfp uses "the 1/viscosity mass from constitutive_model.K" was drifted (selfp reads the Amat; the 1/mu mass participates only under a11) — corrected, with a TODO(BUG) on whether selfp should see Pmat blocks at all (a design question, deliberately not changed here). tests/test_1024_multiplier_schur_pc.py: matrix-level oracle (assembled Schur pre differs flag-on/off under a11 — immune to "both converge in 4 iterations"), the inertness codified as an exact-equality control pinning the PETSc semantics, regime-aware probe silence, no-flag silence, and the setter re-registration contract. Underworld development team with AI support from Claude Code * Review response: a resolution-cached hierarchy yields to a new explicit choice; the inertness probe reads the flag's value Two defects from the #534 adversarial review, both violations of the bundle's own observability contract. (1) The explicit-fmg install cached its hierarchy in solver._custom_mg, which auto_inject re-installs unconditionally — so a later preconditioner="gamg" was unreachable with an EMPTY pc_fallbacks record. The cache is now marked auto_cached and the preconditioner setter drops it; a set_custom_fmg registration is a demand and is kept. Regression test with a verified negative control (fails with the marker disabled). (2) The multiplier_schur_pc inertness probe read hasName("pc_fieldsplit_diag_use_amat") — the key's presence — so diag_use_amat=false (the flag LIVE, Schur pre measured differing by rel-Frobenius 0.30) still warned "no effect" and recorded a decline. It now reads the bool value; regression test pins the flag=false silence. Underworld development team with AI support from Claude Code
…scales on K (#463) (#535) * A yielding TI-VEP fault looked unyielded in every diagnostic: .viscosity now reports the yield-limited eta_1 the stress uses (#463) TransverseIsotropicVEPFlowModel.viscosity computed the yield-limited fault-plane viscosity and then returned the un-yielded bulk eta_0 — the computed value was a dead local. The flux was always correct (the rank-4 tensor applies the yield via _eta_for_tensor), but renders, projections and anything else reading .viscosity saw no yielding at all. The property now delegates to _eta_for_tensor with the ACTIVE integrator mode, so it returns exactly the coefficient the stress tensor is built from: under yield, the persistent _eta1_yield_eff container from #493 (a wrapped atom, so the Picard freezing contract holds for anything that bakes the property). plastic_fraction had the same drift squared (bulk-vs-fault comparison that Max()ed to zero, plus a misplaced ternary) and now reports 1 - eta_1_eff/eta_1_ve on the weak-plane channel. K stays shear_viscosity_0, deliberately. The two Nitsche boundary kernels used .viscosity as their PENALTY scale; that consumer wants the stiffness scale, so they now read .K — which is identical to their old behaviour for every model (isotropic: K == viscosity; TI: K == eta_0 == the old .viscosity return) and immune to the weak-plane value collapsing at yield. New test (validated fail-before on the pre-fix build): test_ti_vep_viscosity_property_reports_yield_limited_eta1 — property is the container _eta_for_tensor bakes, frozen under d/dL with a Newton- unwrap positive control, and reports the unlimited VE fault-plane value when yield is disabled (negative control). Underworld development team with AI support from Claude Code * Isotropic VEP now bakes its effective viscosity as a wrapped atom, like every other yielding model (the tidy deferred on #493) ViscoElasticPlasticFlowModel._build_c_tensor baked the UNWRAPPED .sym contents of the effective viscosity into the c-tensor. The tangent happened to be frozen anyway (the strain-rate content sat inside wrapped atoms one level down), but the class was the one yielding model not following the wrapped-atom freezing contract from #457/#493. The viscosity property now stores the combined (yield-limited, floored) coefficient inside a persistent _vep_eff_viscosity container and returns it — the ViscoPlasticFlowModel._plastic_eff_viscosity pattern — and the _build_c_tensor override is deleted so the base ViscousFlowModel build bakes the wrapped atom (taking two stray debug prints with it). The property now always returns a UWexpression, so _object_viewer no longer breaks on a yielding VEP model. Proof nothing changes: the new frozenness pin (test_vep_c_tensor_coefficients_are_frozen, with a Newton-unwrap positive control) passes on both sides of the change, and a 6-step shear-box loading sequence through yield reruns identically to 3.2e-10 in v, p and the tau_xy trace (solver tolerance 1e-6; the TI-VEP twin of the same check is bitwise identical). Underworld development team with AI support from Claude Code
…shVariable rebuild (#492) (#536) * Release, don't destroy, the old DM and vectors in the MeshVariable rebuild (#492) Creating a variable on a mesh that already has fields rebuilds mesh.dm; the old DM and vectors were eagerly destroy()-ed. petsc4py destroy() zeroes the handle of the wrapper object itself, and mesh.dm / var.vec hand out that same wrapper, so any user-captured handle was blinded -- the next call on it dereferenced a NULL handle (SIGSEGV on optimized PETSc, issue #492). The freed vector pages also left user-held numpy .data views silently reading and writing freed memory, the delayed heap-corruption crash behind the PR #488 Linux CI segfault. Drop the references instead and let PETSc refcounting free each object with its last holder: captured handles stay valid (stale), and memory behaviour is unchanged when nobody holds them -- measured RSS over repeated rebuild+solve cycles is identical with and without the eager destroy (solver-side holders are clones of mesh.dm, never mesh.dm). The rebuild's restore loop now also clears _data_cache/_array_cache alongside _canonical_data (matching the mesh.adapt path) so UW3 never hands back a view of the released vectors. mesh._lvec release composes with the existing lazy rebuild in update_lvec(). Underworld development team with AI support from Claude Code * Regression tests for the DM-rebuild handle contract (#492) test_0858_dm_rebuild_lifecycle.py: the issue reproducer (a captured mesh.dm handle stays valid and the mesh solves on the rebuilt DM), refcount hygiene with a 20-cycle RSS bound (7.1 MB measured, 50 MB bound), .data view refresh after rebuild, and the test_0842-shaped adapt-child arm/solve/teardown sequence from the CI detonation story. The pre-fix SIGSEGV (design-probe subprocess exit -11) is cited in the docstring rather than executed; the observable asserts fail cleanly on the unfixed build (held handle zeroed) -- verified before the fix. ptest_0011_dm_rebuild_held_handles.py + mpi_runner.sh entries: the same contract on every rank; verified at np2 and np4. Underworld development team with AI support from Claude Code * docs: data-access contract for views and handles across a DM rebuild (#492) State the rule explicitly in the governing document: raw numpy views of variable data do not survive creating another variable on the same mesh and must be re-read (UW3 invalidates every cache it hands out; views captured by user code cannot be reached). Captured PETSc handles (mesh.dm, var.vec) get the gentler post-#492 contract: valid but stale. Underworld development team with AI support from Claude Code * Review hardenings: an allocator-proof view assert, and the FMG chain on the adapt child Two test robustness items from the #536 review. np.shares_memory on the refreshed view was allocator-dependent - the released buffer is freed before its identically-sized replacement allocates, so glibc block recycling could fail the assert spuriously on exactly the platform it guards; object identity plus value correctness assert the same contract without dereferencing the dangling view. And the adapt-child arm now solves its first pass under preconditioner="fmg" so the custom-P coarse chain from the original detonation sequence is genuinely constructed - under "auto" a single-field solver declines to GAMG and the chain was never built. Underworld development team with AI support from Claude Code
Some Azure runners advertise an InfiniBand verbs device (mana_0) that UCX cannot open; MPI_Init aborts inside the import smoke test before any underworld3 code runs. Four hits in a week across #466/#526/#527 (issues #461/#528), each fixed by re-rolling the runner lottery. A single-node CI runner needs only shared memory and TCP, so pin the transports at the workflow level - it covers the smoke test and every pytest batch alike. Underworld development team with AI support from Claude Code
…current; plasticity-solvers rewritten (#454) (#489) * skills: parallel adapt engines, band pinning, and FMG on adapt children adapt-on-top-faults - engines section: nvb vs edge_split, both parallel in 2-D and 3-D and bit-confluent; edge_split has no conforming closure so refinement cannot escape the marked region, and marks on the DIAMETER (the volume proxy reported the target met while the mesh was 3.2x coarser across the feature). - repair=True: gates on reducing the largest angle, NOT on Delaunay. Delaunay maximises the minimum angle while P1 depends on the maximum, and flipping a gmsh mesh toward Delaunay raised the 99th-percentile max angle. Worth it on a poor base (156 -> 115 degrees, slivers 3.84% -> 0.00%), marginal on a clean one, and it gives up bit-confluence, so it is opt-in. - relax on a mesh refined onto an interface makes things WORSE (+77% leak); pin_bands is the fix. - new section on sizing the band and representing the fault margin: the -2 Cov(eta, edot) leak metric, why a within-cell marking rule loses to the plain distance size field, why the optimal band width depends on which objective you pick, and what a step-edged margin buys and costs. - gotchas: Mesh(dm) takes the DM over (bare SIGSEGV if you keep using the old handle); Mesh(dm) without boundaries= loses the boundary enum; evaluate() "Total components 8 != 6" on a variable-heavy mesh. adaptive-meshing - PIN THE INTERFACE section for relax(pin_bands=...), including the signed-vs-unsigned distance rule and the pinned_labels merge trap. - cross-reference to nonlinear-solver for the FMG setup. nonlinear-solver - new section: FMG on an adapt-on-top child. The child carries its own graded custom-P tail and solvers pick it up automatically; the base must have refinement>=1; a base-only tail triples the V-cycle count; V-cycle counts are insensitive to element quality (a pass, not a failed measurement) so use GAMG as the quality probe; relax can trip #424 into the dense RBF fallback; repair invalidates the any-degree transfer but not the vertex prolongation. - cross-references to adapt-on-top-faults and adaptive-meshing. Underworld development team with AI support from Claude Code * skills: bring the two adapt/solver skills current with development adapt-on-top-faults: the rotated free-slip path now picks up an adapt child's mesh-owned MG tail automatically (custom_mg.build_transfers, the #467 fix) — the "FUNDAMENTAL, not a quick fix" gotcha row was describing a bug that is gone. Point the band-sizing section at the interface-alignment primitives that now exist (place_sheet / place_thin_volume / remove_embedded, #517-#526). nonlinear-solver: three stale claims corrected — rotated free-slip is no longer the exception to the automatic tail pickup (#467); the adapt tail is one MG level per DOUBLING of h, not per refinement generation (mg_coarsening_ratio=2.0, #515); refinement=0 still yields a hierarchy that starts at the base, so "no coarse grid" overstated it. Three capabilities that landed since the branch: preconditioner="gamg" is respected on adapt children (#530), single-field FMG (#478/#534), and solver.pc_fallbacks as the observability hook (#534). The supersession note no longer names the removed in-SNES ramp API. Underworld development team with AI support from Claude Code * skills: rewrite plasticity-solvers around what the yield campaigns measured Fixes #454. The skill taught the retired yield-homotopy doctrine in ~8 places, headlined by an enable method that no longer exists in src/ and an in-SNES delta-ramp that is separately proven to diverge. The doctrine rested on a unit-scaling error: re-measured on the corrected problem, the delta-march never succeeded where a direct hard-Min solve failed, and the ruling was to regularise the problem, not the solver. Rewritten around the evidence: Newton with the automatic Picard entry (Picard is an entry requirement, not an accelerator), rescue on failure OR stagnation, grid sequencing as the validated warm start, the per-model tangent table, and the #475 yield_mode / yield_smoother / yield_anchor substrate presented as a modelling choice with the multi-solve-only discipline for any delta march. yield_continuation is described honestly per open issue #473 (the cold-start guarantee does not hold on a Piecewise yield stress; the step control is effectively one-shot). Floors updated to the post-#475 semantics (viscosity_min_rounding). Kept: the Newton-confirmation check, the VEP-indefinite ruling, the SNESFAS ruling, the Picard-folklore footnote. Underworld development team with AI support from Claude Code * The nonlinear-solver recipe demotes the delta-march to a rescue The recipe's step 2 still presented solve(homotopy=True) as the default entry point ("one call - automatic"), which is the retracted doctrine in its post-API form: the evidence that recommended a delta-march first rested on a unit-scaling error, and the driver's cold-start guarantee is broken (issue #473). The step now escalates honestly - grid sequencing first, the delta-continuation as rescue of last resort with the #473 caveat stated - and the Layer-2 status note carries the same demotion. The delta-discipline itself (constant per solve, never in-SNES) is unchanged; plasticity-solvers holds the ruling and evidence. Underworld development team with AI support from Claude Code
…de coordinates, and the consistent line mass (#459) (#537) * 2D boundary flux: give every trace node its own coordinate — degree-3 recovery was silently wrong (#459) A degree-3 trace carries TWO interpolation nodes per boundary edge point, but the node gather emitted one entry per DMPlex point with the point's single (midpoint) coordinate: one of the two edge-interior reactions was never read, the coordinate keys collapsed, and the de-smear proceeded down the P2 path — returning 0.57-0.74 of an exact unit flux from 17 of the 25 trace nodes, with no error raised. The fix keys by interpolation node, not by point: * _boundary_field_nodes emits (point, slot, coord) per NODE; the true edge-interior node coordinates come from interpolating the mesh coordinate field into a matching-degree space (_trace_interior_coords, the same createDefault/node_endpoints=False construction used for every UW3 field FE, so the per-point slot ordering matches the field section by construction). The build is collective; whether it is needed is agreed by allreduce so empty ranks participate. * boundary_flux reads the reaction per (point, slot) — the second P3 edge dof is no longer dropped; vector reactions read node-major. * _desmear derives the trace order structurally from the field section (no more midpoint-key sniffing) and assembles a general Lagrange line mass from the measured node parameters; P1/P2 keep their exact hardcoded matrices byte-for-byte. * mass="auto" now selects the consistent solve for 2D traces of degree >= 3: PETSc places the interior nodes asymmetrically (Gauss-Jacobi), where row-sum lumping is only O(h) pointwise (measured 1.6e-2 flat error at res 8 for a linear flux, vs 5.8e-4 consistent). P1/P2 keep lumped, which is exact for linear flux on their symmetric layouts. * The de-smear now REFUSES a key collapse exactly (len(nodeR) vs len(xs), both 2D and 3D branches) instead of silently overwriting — the cheap detection the issue asked for, kept as a guard. Measured on 8x8 unit-flux conduction (T = 1 - y, exact at every degree): degree 1: 8.0e-5, degree 2: 1.2e-4 (both unchanged, bit-identical), degree 3: 4.3e-1 before -> 3.1e-4 after, 25/25 nodes reporting. The 3D path is untouched: P3 traces there were already rejected legibly (facet/edge dof checks) and still are; the #469 mass="p1" mode is unchanged. Underworld development team with AI support from Claude Code * Tests for #459: degree-sweep oracle, interior-node placement, collapse negative control, np2 arm * test_boundary_flux_degree_sweep_2d: T = 1 - y unit flux at trace degrees 1/2/3 — every wall node exact, every trace node reporting (fail-before validated: degree 3 read 0.57-0.74 of the unit flux from 17 of 25 nodes on the unfixed build; degrees 1-2 passed). * test_boundary_flux_p3_interior_node_placement: T = xy gives a flux linear in x, so a slot swap or midpoint-collapsed node placement displaces the recovery by ~2e-2 — 36x the passing mid-wall error. The constant-flux oracle alone cannot see a swap (both edge-interior reactions are equal under uniform flux). * test_boundary_flux_p3_collapse_guard: negative control — force the pre-fix collapse (both edge-interior nodes keyed by the edge midpoint) and the de-smear must refuse with the #459 RuntimeError, proving the guard fires rather than silently overwriting. * parallel/test_1065: degree-3 unit-flux arm at np >= 2 — the per-node coordinate build is collective, so ranks owning none of the flux boundary must still participate (passes np2 and np4). Underworld development team with AI support from Claude Code
place_thin_volume gains assembly={"fuse","fragment"}, defaulting to
"fuse". Where two zones overlap, fragment cuts along the boundary of the
overlap; for a tangential merge — a ribbon soling into another, the
listric case — that boundary is a lens closing at the convergence angle,
and the mesher resolves it as a chain of slivers. Measured on the sole
geometry now in test_0855: minimum angle 0 degrees and 22 cells under 5,
against 37 degrees and none for the fused union.
Nothing downstream needs those internal boundaries. The zone carries one
label, and a cell's fault properties are read from the Surface objects by
proximity, not from the CAD piece it was meshed in — so the seams are
conditioning cost with no physics on them.
The same swap applies in 3-D (_occ_assembly_3d) and is the default there
too. The CAD area/volume gate is unaffected: it is computed from the
faces that survive the boolean, which are the union either way, and
matches the meshed area to 1e-16 relative under both.
The fragment branch is kept and tested as the negative control: a test
that only asserted the fused mesh is clean could not tell whether the
geometry still exercises the defect.
Underworld development team with AI support from Claude Code
The plan for this work lived outside the repository. The architecture belongs in docs/developer/design/ where collaborators can read it: the two representations and what separates them, the tip-pinning mechanism behind the seam defect, the fuse ruling landed here, and the geometric slice criterion that is proposed and not yet built. Marked throughout as to what is implemented and what is not, and it records two things that are decided but not obvious: the CAD area gate did not need the change the plan expected, and the question of what owns a cell where two zones overlap is still open — contacts refuse to overlap and pay a gap, zones overlap freely and resolve ownership by nearest surface vertex, which is neither continuous nor independent of how the traces were sampled. Underworld development team with AI support from Claude Code
… with a constants-aware key (#417, supersedes #418) (#543) * Reuse the rotated free-slip solver workspace across solves (#417) Repeated transient rotated free-slip Stokes solves rebuilt the rotation Q, the PtAP'd operator, the fieldsplit Schur KSP/PC and the GAMG hierarchy on every timestep — an allocator high-water problem (RSS growth to OOM, issue #417) and roughly half the per-step cost of the Zhong A1 production runs. Cache the workspace on the solver between solves, re-derived from the original PR #418 (bec76bb) at the seam of the rewritten unified Newton loop: - geometry tier (Q/Qt, constrained rows, fault pair blocks, custom-FMG prolongation) reused while the boundary/fault registration and DM are unchanged; - structure tier (Ahat, Schur pmat, nullspaces, KSP/PC) reused as objects with values refreshed in place — the loop's own between-iteration pattern; - an iteration-0 fast path skips Jacobian assembly / ptap / PCSetUp entirely when the operator coefficient state counters match (RHS-only timesteps); - the cache is forfeited for direct-LU, prescribed-datum and fault interface-law solves, and torn down by _reset() and the _build() full rebuild before the SNES/DM are destroyed; - expose the existing time= argument through the Stokes.solve wrapper; an explicit time vetoes the fast path (petsc_t bypasses every counter). Unlike the original one-shot linear path, a wrong fast-path verdict here cannot return a stale solution: the loop measures the true residual at every iterate and reassembles from iteration 1 on. Regression: RHS-only reuse (same Q/Ahat/KSP handles), viscosity-field invalidation with in-place refresh, and the time= veto, in test_1018_rotated_freeslip. Production evidence on the original mechanism: 310 guarded Zhong A1 steps, flat RSS (PR #418 thread). Underworld development team with AI support from Claude Code * Close the rotated-workspace blind spot: rampable constants join the key, the verdict goes collective The PR #418 review's unresolved finding: the reuse key was built from MeshVariable._state counters, which are BLIND to rampable UWexpression constants — the #416 contract lets a constant change value with no state bump, so a 2x viscosity ramp between solves reported "unchanged" and (on the original one-shot path) returned a bit-identical stale solution while the matrix-probe safety net was disabled by the very verdict it was meant to check. The reworked loop already made a stale verdict non-fatal (the true residual is measured every iterate and iteration 1 onward always reassembles), but the verdict itself must still be honest: - the operator key now includes the packed constants[] values the kernels will actually assemble with, plus the JIT bundle key (covers an in-place kernel rewire). Measured on the ramp probe: the naive key reported workspace_reused=True for a 2x constant ramp; this key reports False and reassembles. Over-invalidation on RHS-only constant changes is accepted — reassembly is the safe default; - if the constants manifest or the coefficient enumeration cannot be read, the fast path is forfeited outright — correctness first; - the fast path additionally requires a self-measured linear hint (last solve converged in <= 1 increment): for a nonlinear model the cached operator is the previous solve's tangent, and the skip would only trade an assembly for a wasted increment; - the match verdict is allgathered and must be unanimous before it gates any collective PETSc call — state counters follow rank-local writes, and a rank-divergent verdict is a deadlock; - on a detected change the stored signature is poisoned before the in-place refresh, so an exception mid-refresh cannot leave a stale key that later matches half-updated values. Regression: test_rotated_workspace_constant_ramp_invalidates (fail-before validated on the naive key: the reused flag lies there) with its own armed- fast-path negative control and a fresh-solver control at the ramped viscosity; test_rotated_workspace_deform_invalidates re-proves the mesh.deform teardown on the reworked cache. Underworld development team with AI support from Claude Code * Fault contact composes with the rotated workspace cache: pair blocks cache, interface laws opt out solve_with_fault drives the same rotated Newton loop, so the cross-solve workspace decision had to be made explicitly for the fault machinery: - FRICTIONLESS pair blocks are geometry (coincident-node pairing + fault normals live in Q, keyed by the fault registration in the geometry signature) — they cache. A warm repeat reuses the rotation; a cold re-solve rides the iteration-0 fast path; both match a fresh-solver control on the same mesh. - INTERFACE-LAW solvers (viscous / Coulomb / rate-state) opt out entirely: the interface tangent is reassembled per iterate at the current slip rates and the reaction-fed normal stress is Picard-lagged solver state — neither is keyable registration state, so cache_allowed excludes them at the top of solve_rotated_freeslip. Regression: test_fault_repeat_solve_composes_with_workspace_reuse covers both arms, with a fresh-solver control for the cached arm and the absent- cache assertion for the opt-out. Underworld development team with AI support from Claude Code * Review response: persist the operator key only for values Ahat holds; null before destroy Two minors from the #543 review. (m2) The persisted operator_sig now requires that this solve either reassembled Ahat or rode a key-matched fast path - a poisoned solve exiting at iteration 0 without assembling can no longer store a fresh key against unrefreshed values (the window was bounded by the always-reassemble net; now it is closed). (m4) Both teardown sites null the cache attribute before destroying, so an exception mid-destroy leaves objects unreachable rather than arming a double-destroy. Underworld development team with AI support from Claude Code --------- Co-authored-by: Tyagi <gthyagi@gmail.com>
Four settings on one mesh, measured. Keeping the weak zone under the contacts reproduces the pure-zone answer to 99% and solves about three times faster; stripping the zone back to the handover buys a further factor of two and loses a quarter of the slip on the fault that receives through the merge. Those are the same mechanism. With the zone intact a contact tip ends inside weak material and is never pinned; stripped, it ends at the weak material's edge, which is the abutting case measured at 74.6% on the straight-fault control. So the criterion only has to be geometrically right for the stripped variant. With the zone kept, slicing less is slower, slicing more is faster, and slicing past what the split machinery supports is an explicit refusal — never a wrong answer. Also records a negative result, so it is not re-derived: the three-times speedup is NOT contrast-driven conditioning relief. It is flat across three decades of viscosity contrast and the zone-only cost barely moves, because transverse isotropy had already removed that penalty. The reason is still open, and the rotated path's iteration counters read zero, which is what has to be fixed to settle it. Underworld development team with AI support from Claude Code
Two defects, both of which made a converged rotated solve report that it had not converged. The direct path pins one pressure DOF to fix the gauge — the row is replaced by the identity and the rhs zeroed — but the residual kept counting that row. The loop therefore measured itself against a floor it had itself defined as unreachable, and ran to max_it. Measured on a fault contact problem: the velocity residual reached 6e-12 at the FIRST increment and the entire remaining |F| was the pinned DOF alone, bit-identical for eight further iterations that changed nothing (the velocity residual was exactly 0.0 from the second on). The pin is now decided once, before the residual closure is built, and excluded from the residual for the same reason normal_rows are: it is not an equation the solve is trying to satisfy, so it is not evidence about convergence. Separately, the loop never published a reason on the SNES. Every SNES call inside it evaluates one residual or tangent, so the SNES's own reason describes a linear step and stays 0 for the solve as a whole — and petsc_generic_snes_solvers decides convergence from `snes.getConvergedReason() > 0`. Every rotated solve was therefore reported as unconverged, including ones that converged at rel 1e-8. The loop now sets FNORM_RELATIVE / SNORM_RELATIVE on a clean exit and DIVERGED_MAX_IT / DIVERGED_LINE_SEARCH otherwise. The regression test covers both paths, because the pin exists only in the direct one; it fails on both before this change. Underworld development team with AI support from Claude Code
The linear (n=1) plane-strain solution for a fault terminating inside a viscous medium, from the Appendix of Barr & Houseman 1996 (GJI 125, 473-490). Implementation follows that of @gthyagi, who has been using this solution for fault benchmarking. Why it is worth having: every fault result we have is currently measured against another discretisation — a gmsh-union ribbon control — so there is no absolute standard. This is one, and it has a fault in it: an internal boundary carrying zero shear traction, continuous normal velocity and continuous normal stress, with the tip inside the medium. The structure is the interesting part. In polar coordinates about the tip, the stream function separates into a Fourier series in m = q/2: whole-integer m is continuous deformation, half-integer m IS the fault discontinuity, and boundedness at r=0 admits only one negative index, m = -1/2. That single mode carries the whole singularity, which is why slip goes as sqrt(r) and stress as 1/sqrt(r). The exponents are a property of the fault's own Fourier mode rather than an assumption, and the test asserting the slip is carried entirely by that mode says so. The tests verify the field IS a Stokes solution rather than comparing it to a stored answer: div u = 0, the momentum balance vanishes identically in both components, zero shear traction on both faces of the fault, normal velocity continuous across it, and slip = 2 U0 sqrt(r/R0) — the paper's own normalisation. If those hold together the transcription is the solution, whatever a solver later does with it. That mattered here. The half-integer sine terms of u_theta appear with one sign in the paper's boundary datum (A8b) and the opposite sign in its solution (A9b). Incompressibility settles it: for u_r = A sqrt(R) f and u_theta = sqrt(R) g, div u = 0 forces g' = -(3/2) A f, which is (A8b)'s sign. Verified by flipping it — the Stokes test fails with a divergence of (0.75 cos(t/2) + 2.25 cos(3t/2))/sqrt(r). Plane strain only. The paper's thin-viscous-sheet solution (A10-A13) has non-zero in-plane divergence and is a different equation set from our incompressible Stokes, so it is not a benchmark for this solver. Not yet wired to a UW3 solve: the analytic domain is a disc with the fault running from the centre to the perimeter, and 2-D faults cannot reach a boundary yet (#549). The solution and its verification stand on their own until that lands. Underworld development team with AI support from Claude Code
A guard for the test globs, and 22 session scripts out of the test tree (#721)
…quote $PYTEST **The bands were never a cost decision.** `test_06*` was disabled as "potentially problematic" and stayed that way; `test_106*`/`test_107*` were held back pending "a triage/deselect decision" that nobody made. Measured 2026-09-12: those three bands hold 243 tests, of which only 26 are level_3 or tier_c. 55 are level_1 AND tier_a — hardened, fast, and exactly what CI exists to run. One slow test (test_1064) was quarantining its neighbours because they shared a number. They run now. The only failure among them was a test-side defect, fixed separately in #728. **This is affordable today.** The comment justifying the 120-minute cap measured the serial phase at ~55 min on 2026-08-15, before xdist. The five successful runs before 2026-09-12 spent 34, 35, 43, 44 and 48 minutes in "Run tests". Sharding across parallel jobs stays the durable fix if this tightens again — referenced in the workflow as "the CI-runtime issue" but never filed, now #730. **Tier C reports rather than gates.** A tier C failure demands an EXPLANATION, not a revert (Charter §8, TESTING-RELIABILITY-SYSTEM.md): these are characterisations that can fail because the code got BETTER. They are excluded from the batches that set $status and run in their own pass at the end, which is read but cannot fail the build. **$PYTEST is now an array.** It was a string expanded UNQUOTED at all 17 call sites, which made adding any argument containing a space unsafe: bash splits `-m 'not tier_c'` into the words `'not` and `tier_c'`, and pytest answers that by silently collecting NOTHING and exiting 0 — a green run that tested nothing: $ pytest -m "'not" "level_2'" tests/test_1070_free_surface_plume.py no tests collected in 0.00s # exit 0 The first version of this change dodged it through PYTEST_ADDOPTS, which pytest shlex-splits. Quoting the variable removes the hazard instead of routing around it, so the next argument someone adds is safe too. Verified by dry run that the marker arrives as a single argument, `ARG[not tier_c]`, in both the xdist and in-process branches. **Also drops a line that now runs twice.** `test_1072` was pulled forward out of test_107* by name because that band was not batched. The band runs as a whole now, so the named line is redundant; it is still covered, once. Verified by dry run with a stub pytest: every gating batch carries the exclusion as one argument, the previously-deferred bands appear, test_1072 appears once, and the tier C pass runs without the exclusion. Underworld development team with AI support from Claude Code
…re DEFERRED Rebased onto #723 so the two changes land consistently, which immediately surfaced an integration defect the guard itself caught: `globs_run_by` matched the literal `$PYTEST`, and this PR rewrites every call site to the array form `"${PYTEST[@]}"`. The guard therefore found NO globs and declared all 302 test files dark. It now matches both spellings. **DEFERRED is empty, and that is the point.** Every band that sat there — the test_06NN regression suite and test_106*/test_107* — is batched now. Nothing is excluded from CI by its number any more; a test that should not gate says so with `@pytest.mark.tier_c`, which is a property of the test rather than of where it falls in the numbering. **Dependencies, which the PR did not declare.** Copilot was right that this cannot land alone: - #728 fixes `test_1060_nitsche_freeslip`, whose bound is still `<1e-4` against an observed 1.0081e-4. Enabling `test_106*` without it makes this batch fail. - #729 puts the `tier_c` mark on the `test_1070` comparison and updates the tier definitions in `tests/pytest.ini` and the docs. Without it this PR enables `test_107*` while that comparison still gates, and the reporting pass contradicts a marker contract that still reads "development only, not for automation". Merge order is #728, #729, #723, then this. The PYTEST_ADDOPTS point in the same review was already addressed: the array landed in an amended commit the review had not seen. It also removes the objection behind it — an inherited PYTEST_ADDOPTS from the caller is no longer discarded, because the script no longer sets it. Verified: dry run shows the marker arriving as a single `ARG[not tier_c]`, the tier C pass running without it, and the guard passing with 302 reachable and 0 deferred. Underworld development team with AI support from Claude Code
Every session loads this file verbatim before it loads anything else, and it had grown to more than twice the size of the Style Charter it defers to. That is the wrong way round: the file whose job is to point at the governing documents was the bulkiest thing in the context, and the 2026-09 audit found it competing for attention with the rules it exists to route to. Most of the bulk was duplication of documents that already govern their topic. `docs/developer/guides/branching-strategy.md` already carries the branch roles, the API-versus-implementation discipline, the worktree lifecycle, the branch policy and a section addressed to AI assistants; CLAUDE.md restated 4.3 KB of it. The on-demand documentation list restated the authority map in `docs/developer/index.md`. Both are now one pointer each. The build constraints move to `guides/development-setup.md`, which was a stub that still told developers to run `pixi run underworld-build` — a command the tooling replaced with `./uw build`. It now carries the rebuild rule, the editable-install prohibition and its recovery recipe, the PETSc non-relocatability warning, and how worktree environments work. CLAUDE.md keeps the four hard rules as one line each. Nothing normative was dropped. What remains is what a session needs before it knows where to look: the Charter mandate, session bootstrap and the external planning protocol, the hard constraints, where files go, the rulings that are non-obvious and easy to get wrong (rotated free-slip over Nitsche, the data access contract, unwrap-before-atoms, the ambiguous `model`), the test markers, and the pointer to the authority map. Every path this file names was checked to resolve. Underworld development team with AI support from Claude Code
… a gate
The first real CI run of the tier C pass worked — two characterisations failed,
were reported, and the build stayed green:
2 failed, 29 passed, 3227 deselected ... in 2504.48s (0:41:44)
⚠️ A tier C characterisation changed. This does NOT fail the build.
✅ All tests passed!
But it cost **41m44s** and took the job from ~45 minutes to **1h40m** against a
120-minute cap, leaving 20 minutes of headroom. My "affordable today" estimate
covered the newly-batched bands (~22 min) and missed that the reporting pass
re-runs the slow level_3 characterisations — `test_1064` is `slow` and is most
of that 42 minutes.
Trimming the pass to `tier_c and not slow` would have fixed the clock by
dropping `test_1064` — the very test whose two-month absence concealed the
Nitsche penalty regression in #734. So the COST moves off the pull-request path
instead of the COVERAGE being cut: `--tier-c-report` is now a standalone mode
that runs only the characterisations and always exits 0, and the workflow asks
for it on a push to development rather than on every pull request.
A report does not need to be per-commit. It still runs on every merge, which is
where someone reads it.
Verified: the default run contains no tier C pass, `--tier-c-report` runs only
the characterisations and exits 0, the gating batches still carry
`ARG[not tier_c]` as one argument, and the coverage guard passes with 302
reachable, 0 deferred.
Underworld development team with AI support from Claude Code
…efinition fails on the day Maintainer ruling (2026-09-14): a test asserts against a hard baseline, or it is not a test. A test loose enough to absorb a change in a default does not fail when the default changes — it drifts inside its own margin and breaks later, somewhere else, for reasons that are hard to trace back. #734 is the case study. #692 redefined `mesh.cell_size()`; nothing failed; the Nitsche penalty `gamma*mu/h` moved 43%; the Zhong spherical-shell benchmark slid from 0.2% to 2.4% — still inside its 5% tolerance — and only broke months later on one platform's triangulation, three layers from the cause. **The test that would have caught it on the day.** `cell_size` had two tests and between them they missed the case that changed: one pins an exact value (sqrt(2)/8) on a regular QUAD box, where `cell_size` and `get_min_radius` happen to agree; the other covers simplices but checks the implementation against an independent reading of the SAME DEFINITION, so it stays true if the definition moves on both sides. Added: - `cell_size` on congruent right-isosceles cells is exactly 2h/3, to 1e-12. A redefinition fails immediately, at the quantity that changed. - The two measures agree on TENSOR cells (ratio 1) and differ on uniform SIMPLICES by exactly sqrt(2) — `cell_size` 2h/3 against `get_min_radius` sqrt(2)h/3. The Nitsche penalty scales with that ratio, so pinning it means neither measure can move silently. Negative-controlled against the real 1.745x shift: both fail, and both name the quantity rather than a downstream symptom. **The docstring was false.** `add_nitsche_bc` said the two measures "coincide on a uniform mesh". True on a regular quad box — presumably where it was checked — and false on a uniform simplex mesh, which is what every free-slip and fault model uses. Corrected in both the implementation comment and the user-facing parameter docs. **Four method-comparison assertions removed rather than relocated.** #729 moved them to tier C; that was the wrong fix. In every case the hard baseline was already present and the ratio was discarding it: - `test_1070` computed an absolute error against the PRESCRIBED datum and then asserted `errors["strong"] < 0.5 * errors["penalty"]`. Now asserts the error directly. The ratio also hid which side moved — strong went 1.06e-2 to 1.73e-2 between platforms while penalty barely changed. - `test_0066` and `test_0773` had their baselines asserted in sibling tests already, so the comparisons added nothing actionable. - `test_1060` asserted that the weak constraints must STAY inaccurate (`leak > 1e-5`) — an assertion that fails when the code improves. Its exact half, an essential BC holding v.n to machine precision, is a hard baseline and is now a tier B contract; the weak-path leaks are printed, not gated. Charter §8 leads with the baseline requirement and says why: it must be tight enough to fail the moment a default changes. TESTING-RELIABILITY-SYSTEM.md records that C1 is rare by design — reach for it only after showing no baseline exists. 55 passed, 1 skipped across the five files. Underworld development team with AI support from Claude Code
Stop excluding tests by band, let tier C report instead of gate, and quote $PYTEST
…eline Hard baselines instead of drift detectors, and pin cell_size so a redefinition fails on the day
CLAUDE.md is a router, not a manual: 22.9 KB to 8.3 KB
…fuse (#753) The JIT derives both the module name and the C symbol prefix from a hash of the generated source, so ranks that generate different text build disjoint artefacts and the rank-0-compiles/others-load protocol breaks. Agreement was REQUIRED, and a mismatch raised. It fires in practice. The lowering in generate_c_source is not deterministic across ranks (#752), and a Stokes solve with a power-law transversely isotropic viscosity under the consistent tangent trips it in roughly half of np=2 runs - measured 2 of 4 on feature/discrete-adjoint. The abort lands in the forward solve. Two measurements say adopting one rank's source is a sound repair rather than a way of ignoring a wrong answer: * the sources differ ONLY in the order of factors in commutative products. Diffing both ranks' output on a failing run: 12 of 225 lines differ, every one with an identical token multiset and identical length. Every rank's source is a correct kernel for the same equation. * the solver's own symbolic blocks - constitutive tensor, flux, and every Jacobian block - hash IDENTICALLY across ranks on the runs that abort. What differs is produced inside generate_c_source, not handed to it. So the disagreement is over which of several correct spellings to compile, and adopting one is enough. Rank 0's is taken and every rank rehashes from it, which restores the invariant that actually matters: one source, one hash, one module. Measured on the reproducing branch: the repair fired in 4 of 6 runs and all 6 succeeded, against a baseline of 2 aborts in 4. This is a REPAIR, not a fix. It is said out loud - one warning naming the issue - rather than papering over the upstream non-determinism, which is still a bug and still worth finding. The common path is untouched: ranks that already agree keep their own objects and pay one allgather of a 16-character string. Two dead ends recorded so they are not retried. The C printers do NOT use StrPrinter._print_Mul - C89CodePrinter delegates to CodePrinter._print_Mul, which has no stored-order fast path and does sort, and forcing that sort produces byte-identical C while a run still aborts. And it is not commutative ordering at all: recursively sorting every Mul/Add argument by default_sort_key still leaves the ranks differing. The repair is extracted as _agree_source_across_ranks so it can be tested directly. Reproducing a real disagreement means reproducing a non-deterministic bug at about one run in two, which is not a test - it would pass half the time with the repair removed. The tests build the disagreement themselves, so they keep working once the lowering is made deterministic. Underworld development team with AI support from Claude Code
Proposal for what v3.2.0 announces and at what level, built from the 186 PRs merged since v3.1.0. 21 features rated against EU/NASA TRL: 9 Supported, 10 Preview, 2 In development. Evidence is the 2026-09-24 validation runs: ordinary tests at 4 ranks (587/590), the MPI suite (157/158) and the 16 genuine ptest_ files run for the first time (99 pass, 1 hang). Not the release manifest. For review by @lmoresi and the Steering Committee before any manifest change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pre-read for the Steering Committee, red/amber/green TRL banding Feature table: free surface has a design note and no runnable example, not the reverse — the free_surface/ scripts are FSSA, which the project rejected. Mesh adaptation is held at Preview by validation, not a missing demo. Seven doc links pointed at pages that never use the feature's API. Collective robustness and code generation are itemised rather than rated, since neither is a capability a user invokes: 21 entries -> 19.
A rank-divergent test hangs the run instead of failing. Adds --timeout=600 to both mpirun lines; step 6b now runs 2 and 4 ranks.
Claims follow the bands in docs/developer/planning/v3.2.0-feature-table.md. Gate passes all 20 at levels 1,2,3: 683 tests, no downgrades. fmg moves preview -> supported. v3.1.0 held it at preview pending "a dedicated FMG tier_a benchmark"; test_1020_fmg_single_field_lockout.py now meets that condition. mesh-adaptation drops tests/test_0760_mesh_ot_adapt.py, which no longer exists and was being silently ignored.
…dmap Release notes assembled from the gate output (11 Supported, 9 Preview) plus hand-written Highlights, Bug Fixes, New Features, Improvements, Breaking Changes and Contributors. Extends the committee pre-read into a full meeting pack: GitHub activity and issue-response KPIs, the five DOI-registered publications from this quarter, progress against the nine features agreed in June, and the full TRL 1-9 scale. Corrects the swarm proxy speedup, quoted as "17-80x" from the PR title. The benchmark measures two operations, not a range: 77x particle-to-node and 17x node-to-particle.
…otes Committee sign-off now precedes external adoption, inverting the EU ordering deliberately — sign-off is what authorises outside groups to depend on a capability. Noted in the feature table. Band labels in the committee pack now use the release notes' own terms (Supported, Preview, In development) instead of a parallel colour vocabulary.
constitutive-models and transport-histories each bundled a mature capability with a new one, so both announced at their weakest part. Split per Steering Committee, 2026-09-29: constitutive-models preview -> supported transverse-isotropy new, preview (accuracy anchor +1.8%) transport-histories preview -> supported integration-point-variables new, preview (no user-facing docs) Also swaps TRL 7 and 8 — committee sign-off now precedes external adoption — and corrects the Gamma_P1 breaking change, which said a still-present deprecated property had been removed. Gate: 22 entries, 699 tests, no downgrades.
🔍 Architectural Review ChecklistThank you for submitting an architectural review! Reviewers should validate: Design & Architecture
Implementation
Testing & Validation
Documentation
Review Process: See CODE-REVIEW-PROCESS.md Approval: This PR merge = Review formally approved |
|
✅ Test Suite: success |
Title and summary only, the key is unchanged, so the gate and its history are unaffected.
|
✅ Test Suite: success |
ci(container): build HPC images for Gadi, Kaiju and Setonix from shared recipes
|
✅ Test Suite: success |
lmoresi
approved these changes
Oct 13, 2026
lmoresi
left a comment
Member
There was a problem hiding this comment.
Has been reviewed by the team and by the steering committee
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Underworld3 v3.2.0
Highlights
Free surface in three dimensions on a spherical shell. Surface height relaxes toward the
equilibrium topography implied by the stress field, using an exponential integrator that is
L-stable and cannot overshoot. The scheme is now dimension-general. Preview: the method and its
validation are written up in full, but there is no runnable example yet.
The viscoelastic-plastic regressions reported at v3.1.0 are fixed. All seven tests suspended
for that release now pass, without the tests being altered — the fixes came from Stokes uu_G3 transposed tangent + TI un-frozen Picard: fix issue #457 at source #493 (Stokes
uu_G3transposed tangent and TI un-frozen Picard, closing TransverseIsotropicFlowModel: consistent (Newton) tangent inconsistent with the residual when anisotropy is active #457 at source), TI-VEP viscosity reports the yield-limited weak-plane value; Nitsche scales on K (#463) #535 (TI-VEPyield-limited weak-plane viscosity) and yield_anchor: which side of exact Min the soft-min yield law sits on #475 (
yield_anchor), closing test_1052 VEP stability tests fail under PETSc 3.25 (variable-dt pure-VE blow-up), pass in level_1 gate #390.Full multigrid preconditioning is now Supported, the only capability promoted since v3.1.0.
It keeps the Stokes velocity solve at a steady ~5 iterations as the mesh distorts, where the
algebraic alternative runs a volatile 64–131. The gain is predictability rather than raw speed.
Faults as real discontinuities in the mesh, rather than smeared weak zones. A fault is
specified once — a trace, its place in a hierarchy, the properties it carries — then realised
either as a split-node cut with node-pair contact laws or as a volumetric weak plane. The largest
single body of work this quarter. Preview.
Particle proxy updates are dramatically faster. The
IndexSwarmVariableproxy update wasvectorised (perf(swarm): vectorize IndexSwarmVariable proxy update (17-80x speedup) #374): 77× for particle-to-node and 17× for node-to-particle, measured on a 60×60
mesh with 130k particles. It shows up directly in any multi-material model carrying material
indices on a swarm.
A materials registry that separates what a material is from where it is, so one rheology can
be attached to particles or to regions without being rewritten.
A rebuilt container that cannot drift from the development environment. Two-stage pixi build,
multi-architecture (amd64 and arm64), resolved from
pixi.lock— the drift between container andBinder that broke the v3.1.0 container build is no longer possible. Automated Gadi/HPC images
alongside it.
Fifteen fixes to collective participation under MPI, covering the failure mode where one rank
takes a shortcut and every other rank waits forever in the matching collective. Includes the
empty-rank swarm-save deadlock (fix: swarm empty-rank save deadlock (MPIO dtype) and estimate_dt reshape crash #680).
Supported (validated)
preconditionerproperty).Preview (present, unguaranteed)
Bug Fixes
DMLocatePointssilently dropped onquadrilateral and hexahedral meshes (fix(evaluate): recover boundary-face query points dropped by DMLocatePoints on quad/hex meshes (#390) #391, Point-location fallback ladder: NaN sentinel + RBF rung for deformed quad/hex meshes #392)
read_timestepnow keeps one coordinate frame across sessions — DG0 fields no longer collapse onreload (fix(checkpoint): read_timestep uses one coordinate frame across sessions — DG0 fields no longer collapse (#269) #398, read_timestep mis-reads DG0 cell fields as a constant (spatial mean) for gmsh/units/multi-var checkpoints #269)
estimate_dtno longer crasheson reshape (fix: swarm empty-rank save deadlock (MPIO dtype) and estimate_dt reshape crash #680)
meshes (Fix #520: seam cone reconciliation + the recovered orientation fix — parallel FE on placed meshes works #521, place_sheet parallel rebuild: face/edge cone orientations unreconciled across the point SF — parallel FE on placed meshes assembles a wrong operator #520)
GenericFluxModelflux storage preserves symbolic field dependencies (Fix GenericFluxModel flux storage — preserve symbolic field dependencies #455)flux_jacobianis settable, for custom Jacobian tangents (fix(constitutive_model): make flux_jacobian settable for custom Jacobian tangents #362)New Features
counterpart and a reaction-based shell projection (Add generic spherical-shell geoid and self-gravity postprocessing #591, Add cylindrical annulus geoid postprocessing #648, Add reaction-based spherical-shell geoid projection #686)
dmfield_evaluateexposed from PETSc (Add dmfield_evaluate from PETSc #436)Improvements
IndexSwarmVariableproxy updates vectorised — 77× particle-to-node, 17× node-to-particle (perf(swarm): vectorize IndexSwarmVariable proxy update (17-80x speedup) #374)node_redistributionalongside an engine-lessadapt()(feat(meshing): purposeful adapt/redistribution naming — node_redistribution + engine-less adapt() #372)Breaking Changes
Null_Boundaryfake natural BC, and the label it was built on, are removed (Remove the Null_Boundary fake natural BC and the label it was built on #503)synchronised_array_updatenow performs a collective dirty-variable flush, replacing rank-localevent replay (synchronised_array_update: collective dirty-variable flush replaces rank-local event replay (#379 item 2) #383, Callback-array parallel safety: remaining exposures after #376 (swarm.points callback, delayed-callback replay, central guard) #379)
SemiLagrangianno longer accepts the unusedswarm_degree,swarm_continuousor_workVararguments (SemiLagrangian: remove the unused swarm_degree / swarm_continuous and _workVar (#704) #705, SemiLagrangian: swarm_degree / swarm_continuous are accepted but never used #704)
mesh.Gamma_P1, and the mesh factoriesno longer recommend it.
Gamma_P1itself remains, deprecated — useGammain integrands and BCs,or
boundary_normalfor a per-boundary P1 normal field (Take slip normals per boundary, not from the deprecated Gamma_P1 (#538) #659, Stop the mesh factories recommending a deprecated, broken API (#538) #654, Gamma_P1 returns wrong boundary normals on a box: left/bottom rotated 90 deg, right/top position-dependent #538)Contributors
Thanks to everyone who contributed to this release:
461 commits since v3.1.0, touching 136 files under
src/.Process notes. No
v3.2.0rc1— validation was already complete and clean, so an RC would havepublished a candidate nobody needed. Steps 6a, 6b and 7 all ran on this branch: serial levels 1,2,3
clean, MPI at 2 and 4 ranks clean, gate 22 entries / 699 tests / no downgrades.
One known failure:
test_1063_constrained_freeslip_parallel[ti]moves its accuracy anchor +1.8%(1% tolerance), identically at 2 and 4 ranks, so not a partition effect.
transverse-isotropyisclaimed
previewfor that reason; issue to follow.