Uh oh!
There was an error while loading. Please reload this page.
fix(swarm): parallel read_timestep restores each particle once (rank-0 routed read) + honest global-add docstring - #329
Conversation
…rdinates The docstring promised that passing the same array on every rank produces one copy of each point in the correct partition. That is mechanically untrue: the method inserts the full array on every rank with no locality filtering and no deduplication, and migration is a scatter that routes each inserted particle to its owner — rank-identical input at np ranks yields np copies of every point (issue #324). The docstring now states the method is a low-level primitive and names the two correct usage patterns: migrate=False where each rank deliberately keeps a full copy (the global-evaluation / mesh-transfer pattern), or pre-partitioned / rank-0-only input followed by migration. add_particles_with_coordinates is cross-referenced as the safe rank-identical-input method. Underworld development team with AI support from Claude Code
Swarm.read_timestep read the full coordinate dataset on every rank and called add_particles_with_global_coordinates(migrate=True). Migration is a scatter with no deduplication, so a parallel restore produced one copy of the saved swarm per rank: reproduced at np2 as 1944 particles restored from a 972-particle checkpoint (2.0x), silently corrupting integration and statistics after every parallel restart. The migrate=True path now reads the dataset on rank 0 only, stages empty (0, dim) arrays on the other ranks, and lets Swarm.migrate route each point to the rank that owns it — the same rank-0 routed-read design that commit 9fb198d applied to SwarmVariable.read_timestep. The serial path and the migrate=False escape hatch (full per-rank copy, points outside the mesh preserved) behave exactly as before. Also adds a TODO(BUG) on Swarm.save: it returns on non-zero ranks while rank 0 is still appending metadata, so an immediate reopen can hit HDF5 file locking (BlockingIOError errno 35) — found while reproducing this defect. Closes#324. Underworld development team with AI support from Claude Code
New tests/parallel/test_0757_swarm_read_timestep_mpi.py (inside CI's tests/parallel/test_075* glob) asserts, at np2 and np4: restored global particle count equals the saved count, gathered and sorted coordinates match the checkpoint to 1e-12, and a SwarmVariable read back on top of the restored swarm reproduces its analytic per-particle values. Written first and shown to fail (2.0x duplication) against the unfixed code. test_0003_save_load.py::test_swarm_save_and_load now asserts the restored particle count — it previously exercised the read path without checking anything. Underworld development team with AI support from Claude Code
test_deformed_spherical_shell_boundary_area_parallel calls the raw Mesh._deform_mesh primitive on a mesh that already carries a variable, which the live-mesh guard from PR #326 now rejects. The failure exists on development independently of the #324 fix; marked with TODO(BUG) so the migration to mesh.deform() is not lost. Underworld development team with AI support from Claude Code
There was a problem hiding this comment.
Pull request overview
This pull request fixes a parallel restart defect in Swarm.read_timestep where each MPI rank previously restored the full saved coordinate set and then migrated, resulting in an np-fold duplication of particles. The fix switches to a rank-0-only read (staging empty arrays on other ranks) and then uses migration to route particles to their owning ranks, and it corrects the misleading contract described in add_particles_with_global_coordinates’s docstring.
Changes:
- Fix
Swarm.read_timestep(migrate=True)to read coordinates on rank 0 only and migrate from a single inserted copy, preventingnp-fold duplication in parallel restores. - Update
add_particles_with_global_coordinatesdocstring to accurately describe its non-deduplicating semantics and point users to safer alternatives. - Add/strengthen regression tests: a new MPI test that would have failed before the fix, and a serial save/load test that now asserts restored particle count.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/underworld3/swarm.py | Implements rank-0 routed read for swarm coordinate restore; updates docstring contract; adds a TODO note about a known save/metadata race. |
tests/parallel/test_0757_swarm_read_timestep_mpi.py | New MPI regression tests verifying restored swarm count/coordinates match saved state and that swarm variables read correctly post-restore. |
tests/test_0003_save_load.py | Strengthens serial save/load test by asserting restored swarm particle count matches the saved count. |
tests/parallel/test_0765_internal_boundary_integral_mpi.py | Adds an explanatory TODO comment about an unrelated pre-existing failure due to _deform_mesh live-mesh guard behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
lmoresi
commented
Jul 6, 2026
Design discussion (maintainer review): the rank-0-routed read fixes the duplication but adopts a filesystem-dependent trade-off that deserves to be explicit rather than inherited. The primitive was never broken — The strategy space:
Which is 'best' depends on the filesystem; none is universally correct. The regression tests added here assert outcomes (count / coordinates / values at np2 and np4), not strategy — so the implementation can be swapped without touching them. Proposed follow-up per maintainer steer: switch this PR's restore path to strategy 2 (the filtering sibling — smaller diff, no comms), document the trade-off at the call site, and reconsider Underworld development team with AI support from Claude Code |
… steer) Per the design discussion on PR #329: replace the rank-0 routed read with the keep-local model. Every rank reads the coordinate dataset (plain read-only h5py) and add_particles_with_coordinates keeps only the points the rank owns via points_in_domain — no communication, no migrate call, no rank-0 memory hotspot. The cost is np-fold read amplification, which scalable/striped parallel filesystems absorb; the trade-off is documented at the call site. Rank-0-routed reading remains the pattern in SwarmVariable.read_timestep pending its own reconsideration. The migrate=False escape hatch (full per-rank copy via the global method, points outside the mesh preserved) and the serial path are unchanged. The np2/np4 regression tests assert outcomes, not strategy, and pass unchanged. Underworld development team with AI support from Claude Code
lmoresi
commented
Jul 6, 2026
Strategy swap done per the design comment (commit b2da15f): The Verification after the swap (single
Underworld development team with AI support from Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
…tub banner (WE-11) (#335) * docs(design): swarm modernization design doc (FO-01) Campaign dimension-5 deliverable (2026-07 quality campaign): the design blueprint for modernizing the swarm subsystem, building on docs/reviews/2026-07/SWARM-SUBSYSTEM-REVIEW.md and the FO-01 worklist row. Covers, each with current behaviour / problem / target design / migration path + tests, all verified against development @ 3184a40 (code reading plus runtime probes): - self-validating canonical cache (SWARM-10): generation+size token - migration trigger matrix (SWARM-03/18): post-#313 state machine and gaps - shared array-view refactor for both variable families (SWARM-14) - rank-local RBF seam behaviour (SWARM-15) and relation to #314 - _get_map stale-cache trap (SWARM-23): delete the dead trio - KDTree copy-in-__cinit__ (SWARM-20): probe-quantified inconsistency - checkpoint/restore fidelity audit (post-#329 keep-local, #333, #330) - the fossil contract (#313) and how the new cache preserves it Includes a Track-0 finding-status table (fixed findings cited as history, not open problems), non-goals, a phasing plan (FO-02 early items), a test strategy with tier assignments, and ten numbered maintainer questions. Linked into the developer design-documents toctree. Underworld development team with AI support from Claude Code * docs(subsystems): honest banner on the swarm-system stub (WE-11) The 26-line stub claimed the swarm subsystem was 'well documented', 'priority low', citing a nonexistent swarm/ module of 4,484 lines (SWARM-24) - actively misdirecting reviewers away from the subsystem the 2026-07 audit found most in need of attention. Replace the misleading content with a warning banner pointing at the audit (docs/reviews/2026-07/SWARM-SUBSYSTEM-REVIEW.md, with a caveat that many findings are since fixed) and at the modernization design document as the current authorities, until the FO-01 refactor delivers the real subsystem documentation. Underworld development team with AI support from Claude Code
…og, value-first call-site sweep (WE-01..03,05,06,08,09,10) (#338) * docs(WE-01): adopt the one-governing-doc-per-topic authority map Repoint CLAUDE.md's Data Access 'Authoritative Reference' from the stale UW3_Style_and_Patterns_Guide.md to subsystems/data-access.md (the guide it crowned teaches patterns the code deprecates at runtime — DOC-04), and record the Style Charter §10 authority table in docs/developer/index.md as the master authority index. The Charter is added to the Getting Started toctree (removes a baseline 'not included in any toctree' warning). Finding: DOC-04 (docs/reviews/2026-07/DOCS-STANDARDS-COHERENCE.md). Underworld development team with AI support from Claude Code * docs(WE-02): de-drift the Style Guide's four stale normative sections Rewrites the sections DOC-01 verified as contradicting the settled standards: - Docstring format: the 'Markdown Docstrings for pdoc/pdoc3' section is replaced by the NumPy/Sphinx RST standard (worked example with :math: and Parameters/Returns/Examples/Notes; conversion tracked in docs/plans/docstring-conversion-plan.md), per Style Charter section 6. - Doc file format: Quarto .qmd prescription (zero .qmd files exist in the repo) replaced by MyST .md/Sphinx guidance matching CLAUDE.md; migration table row updated. - Data access examples: 'Preferred' coordinate examples now use the real, runnable API — mesh.X.coords (read), mesh.deform() (coordinate changes), and the swarm.coords getter/setter for particle positions. The previous 'Preferred' example swarm.data += displacement raises AttributeError (getter-only property — SWARM-13 evidence); mesh.data warns at runtime. The private-attribute migration advice (swarm._particle_coordinates, mesh._deform_mesh presented as the NEW pattern) is deleted. - Front matter: the 21-line Quarto YAML header is replaced by a minimal MyST title block, and the guide now states that the UW3 Style Charter is the normative contract and wins on conflict. All replacement examples verified against current source: Swarm.coords setter (swarm.py), Mesh.deform (discretisation_mesh.py:3133), uw.synchronised_array_update / NDArray_With_Callback.delay_callbacks_global. Findings: DOC-01, SWARM-13 (style-guide part). Underworld development team with AI support from Claude Code * docs(WE-03): regenerate the docstring review queue; add the sweep to the release checklist The queue (last generated 2026-01-13, cdf5bb2) misrepresented the codebase both ways: it flagged now-complete items (solve, SNES_Scalar) as missing and contained zero entries for the June 2026 API (DOC-02). Regenerated over src/underworld3/**/*.py + **/*.pyx at the current tip. Two bugs in scripts/docstring_sweep.py's regex-based Cython parser made the regenerated queue lie about .pyx docstrings and are fixed as part of making the regeneration meaningful: - the indent group '(\s*)' with re.MULTILINE consumed preceding blank lines, shifting the computed definition line so the docstring search started ON the def/class line and always missed; - the docstring search started at the definition line rather than after the (possibly multi-line) signature, so long signatures hid their docstrings; - raw-string docstrings (r""", the norm in the solver .pyx) were not recognised. DOC-02 cross-validation on the regenerated queue now passes: solve / SNES_Scalar in the solver pyx are no longer flagged 'none'; the queue contains the June API (add_nitsche_bc, add_rotated_freeslip_bc, boundary_flux, set_custom_fmg, consistent_jacobian: 13 mentions) and flags the DOC-05 targets (Swarm.advection x2, read_timestep, write_proxy) as undocumented. Also adds the sweep to the quarterly release checklist (guides/release-process.md) so the queue cannot go stale unnoticed again. Findings: DOC-02 (docs/reviews/2026-07/DOCS-STANDARDS-COHERENCE.md). Underworld development team with AI support from Claude Code * docs(WE-05): backfill the changelog for May - early July 2026; add the changelog sweep to the release checklist The changelog (the quarterly CIG/stakeholder record) ended in April 2026 while ~117 first-parent commits landed May through early July (DOC-03). Backfilled at the existing conceptual granularity — 14 grouped entries, grouped by subsystem rather than by PR, matching the established format (### Title (Month Year), bold lead sentence, hyphen bullets, inline PR references): - New '2026 Q3 (July - September)' section: the July 2026 quality campaign (#309-#313, #317, #322-#326, #329, #334 as grouped entries), rotated strong free-slip / boundary traction / dynamic topography (#293, #294, #298, #306), generalized geometric multigrid via custom prolongation (#290, #297), consistent Jacobian tangent (#258), swarm correctness (#216, #313, #323, #329), numpy 2 support (#301, #305). - Extended '2026 Q2' section with the May-June entries: mesh adaptation movers (#190, #209, #213, #228, #259, #264, #266), moving-mesh field transfer / deform() (#246, #249, #251), semi-Lagrangian accuracy controls (#164, #183, #185-#189, #208, #220), snapshot/checkpoint toolkit (#146, #195, #196, #198), Stokes_Constrained (#224, #229, #240, #265), local-h Nitsche + boundary-slip surfaces (#225, #241, #275), units interoperability (#277, #278, #283, #284), memory/evaluation/solver infrastructure (#161, #177-#179, #181, #182, #222, #237, #250, ...). Every entry is backed by a merged commit on development (verified against git log --first-parent aed517f..3184a40). Also adds a quarterly-changelog sweep step beside the docstring sweep in the release checklist (guides/release-process.md) per DOC-03's proposed fix. Findings: DOC-03 (docs/reviews/2026-07/DOCS-STANDARDS-COHERENCE.md). Underworld development team with AI support from Claude Code * docs(WE-06): status headers on the unmarked design docs (per-doc git verification) Adds one-to-three-line Status markers to the 13 design docs that lacked one, following the directory's existing conventions (**Status**: line under the title; status: key inside existing YAML frontmatter for the three frontmatter-only docs), and corrects the stale 'Design Phase' marker on MATHEMATICAL_MIXIN_DESIGN.md (the mixin ships in utilities/mathematical_mixin.py). Every stamp was verified against git history (git log --follow dates) and the current source tree before writing: - Implemented: jacobian-consistent-tangent (PR #258, c63cd70), fmg-checkpoint-hierarchy (3cd73cd), petsc-dmplex-checkpoint-reload-plan (PR #146, write_timestep(petsc_reload=True) in tree), fault-refinement-simplification (smooth_mesh_interior / metric_density_from_gradient / fault_comb_metric all in tree), MATHEMATICAL_MIXIN_DESIGN. - Current reference/contract: mesh-adaptation-formulation, ND_UNITS_BOUNDARY_CONTRACT (PR #278, e0ece9a). - Investigation records (preserved via PR #245, 34a9dd4; production geometric-MG is custom prolongation, PR #290): snesfas-feasibility, snesfas-vanka-feasibility-study. - Design notes / prototypes with honest gaps: in_memory_checkpoint_design (not implemented, per its own trailing Status section), submesh-solver-architecture (extract_region/extract_surface exist; coarsened_companion does not). - Historical: ARCHITECTURE_ANALYSIS (persistence.py layout superseded), COORDINATE_MIGRATION_GUIDE (transition shipped), WHY_UNITS_NOT_DIMENSIONALITY (decision record). The audit's ~16 estimate over-counted: re-derived at this tip, 13 docs were unmarked plus one marked-but-stale (DOC-07). Findings: DOC-07 (docs/reviews/2026-07/DOCS-STANDARDS-COHERENCE.md). Underworld development team with AI support from Claude Code * docs(WE-08): convert units.py public docstrings Google -> NumPy style Docstring-only conversion of the 18 public module-level functions that carried Google-style Args:/Returns:/Raises:/Examples: labels (check_units_consistency, get_dimensionality, get_units, non_dimensionalise, show_nondimensional_form, simplify_units, create_quantity, convert_units, to_base_units, to_reduced_units, to_compact, get_scaling_coefficients, set_scaling_coefficients, validate_expression_units, assert_dimensionality, validate_coordinates_dimensionality, enforce_units_consistency, require_units_if_active, convert_angle_to_degrees) to the NumPy/Sphinx standard (Style Charter section 6). dimensionalise was already NumPy style; one-line docstrings and private helpers are untouched. No code, signature, or behaviour changes (verified: every diff hunk is inside a docstring; ast.parse clean). Finding: API-12 (docs/reviews/2026-07/API-CONSISTENCY-REVIEW.md). Underworld development team with AI support from Claude Code * docs(WE-09): sweep call sites of the newer BC methods to value-first (conds, boundary, ...) order Wave C (#334) made the ORIGINAL value-first order canonical for add_nitsche_bc / add_rotated_freeslip_bc / add_constraint_bc (maintainer decisions D2/D3; Style Charter section 6) with deprecation shims for the legacy boundary-first and g= spellings. This sweep updates every call site of those THREE methods to the canonical order so nothing in the repository exercises the shims — 74 sites total: - tests/: 63 call sites across 12 files (test_1017, test_1018, test_1060, test_1061, test_1062, test_1064, test_1065 x2 serial; parallel test_1017, test_1062, test_1063, test_1064). tests/test_0641_wave_c_api_shims.py is deliberately untouched — its legacy-order calls ARE the deprecation contract. - docs/: 7 sites (curved-boundary-conditions.md x4, CONSTRAINED_FREESLIP_MULTIPLIER.md call + signature line, examples/submesh_investigation/test_region_ds_nitsche.py). - .claude/skills/: 3 sites (adapt-on-top-faults x2, free-surface-convection x1). - CLAUDE.md: 1 signature reference (free-slip BC preference section). The ~1,370 legacy-trio (add_dirichlet_bc/add_natural_bc/add_essential_bc) sites already conform and are untouched per the D2 decision. The audit review documents under docs/reviews/2026-07/ record the pre-decision state as evidence and are not swept. Discovered while verifying the swept tests run warning-free: the Wave C zero-datum guard in add_rotated_freeslip_bc rejects FLOAT zero (sympy.sympify(0.0) != 0 is structurally True), so the canonical add_rotated_freeslip_bc(0.0, boundary) raises NotImplementedError while conds=0 works. Filed as issue #336 with a TODO(BUG) marker at the guard (comment-only src touch); the swept call sites use the working integer form add_rotated_freeslip_bc(0, boundary). No fix applied here (Charter section 9 scope discipline). Findings: API-01/API-02 sweep (WE-09, REMEDIATION-WORKLIST.md). Underworld development team with AI support from Claude Code
Defect
Closes#324. Follows the investigation recorded on that issue (2026-07 audit,
SWARM findings family;
docs/reviews/2026-07/REMEDIATION-WORKLIST.md).Swarm.read_timestepread the full coordinate dataset on every rank and thencalled
add_particles_with_global_coordinates(..., migrate=True). Migration isa scatter with no duplication-prevention code — each rank's copy of every point is routed to
the owning rank — so a parallel restore produced one copy of the saved swarm
per rank.
Reproduction (before the fix, np2, box mesh,
fill_param=2):The failure is silent in production: no error, just np-fold particle counts and
correspondingly wrong integration/statistics after every parallel restart.
The root cause of the pattern was a false promise in the
add_particles_with_global_coordinatesdocstring ("If the same array is passedon every rank, this produces one copy of each point in the correct partition"),
which is mechanically untrue.
Fix
Swarm.read_timestep(migrate=True)now reads the coordinate dataset on rank 0only, stages empty
(0, dim)arrays on the other ranks, and letsSwarm.migrate()route each point to its owner — the same rank-0 routed-readdesign commit 9fb198d introduced for
SwarmVariable.read_timestep. Theserial path and the
migrate=Falseescape hatch (full per-rank copy, pointsoutside the mesh preserved) behave exactly as before.
add_particles_with_global_coordinatesdocstring now states the realcontract: a low-level primitive with no locality filtering and no de-duplication;
correct usage is
migrate=Falsefull-copy, or pre-partitioned / rank-0-onlyinput followed by migration.
add_particles_with_coordinatesiscross-referenced as the safe rank-identical-input method. Its legitimate
migrate=Falsecallers (evaluation swarms, mesh transfer, global evaluation)are untouched.
Tests
tests/parallel/test_0757_swarm_read_timestep_mpi.py(inside CI'stests/parallel/test_075*glob): restored global count == saved count,gathered/sorted coordinates match to 1e-12, and a
SwarmVariableread backon top of the restored swarm reproduces its analytic values. Written first
and shown to fail (2.0x duplication) against the unfixed code.
tests/test_0003_save_load.py::test_swarm_save_and_loadnow asserts therestored particle count (it previously checked nothing).
Gate results:
pytest tests/ -m "level_1 and tier_a" -q: identical before andafter the fix — 328 passed, 10 skipped, 4 xfailed, 1 xpassed both runs.
tests/test_0003_save_load.py: 6 passed.test_0755,test_0756,test_0765,test_0766):np2 20 passed / np4 21 passed, with ONE pre-existing failure in
test_0765_internal_boundary_integral_mpi.py— a direct_deform_meshcall rejected by the live-mesh guard from PR fix: medium Track-0 misc — parallel BoxInternalBoundary, SL theta restore, viewer crash, projection double-count, units-boundary honesty (BF-10a/12/13/15/18, D9) #326, present on
developmentindependently of this change (flagged with a TODO(BUG)in the test).
Also flagged (TODO(BUG), out of scope here):
Swarm.savereturns on non-zeroranks while rank 0 is still appending metadata, so an immediate reopen can hit
HDF5 file locking (BlockingIOError errno 35) — found while reproducing #324.
Underworld development team with AI support from Claude Code