Uh oh!
There was an error while loading. Please reload this page.
On-disk snapshot toolkit v1.1 (stacked on #195, #196) - #198
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a persistent, on-disk backend for the snapshot toolkit via Model.save_state(file=...) / Model.load_state(path), layering an inspectable HDF5 “wrapper + bulk sidecars” format on top of PETSc DMPlex checkpoint primitives. This complements the in-memory snapshot token path by enabling durable restarts and selective reads while keeping snapshot/restore semantics (including solver-internal state via dataclass snapshots).
Changes:
- Implement v1.1 disk snapshot format (
.snap.h5wrapper +.snap.bulk/companion dir) including mesh/meshvar bulk, per-rank swarm sidecars, and/python_statedataclass serialization. - Unify APIs via
Model.save_state(...)/Model.load_state(...), and makeMeshVariable.read_timestep(...)dispatch format-aware (legacy vs v1.1 wrapper). - Add extensive serial + MPI test coverage plus user-facing docs and demo scripts.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_0010_snapshot_disk_format.py | Validates disk snapshot wrapper/bulk layout, inspectability, roundtrip, sidecars, and read_timestep dispatch. |
| tests/test_0009_model_tracker.py | Tests Model.tracker snapshot-managed semantics and git-stash behavior. |
| tests/test_0008_snapshot_realsolver.py | Real-solver confidence tests for snapshot restore/continuation guarantees. |
| tests/test_0007_snapshot_inmemory.py | In-memory snapshot suite expanded/maintained for meshes, swarms, DDt state, and continuation. |
| tests/run_snapshot_backstepping_demo.py | Time-series demo script illustrating adaptive-Δt back-stepping using snapshots. |
| tests/run_snapshot_backstepping_spatial.py | Spatial visualization demo companion for snapshot back-stepping. |
| tests/parallel/ptest_0010_snapshot_disk.py | MPI test for disk snapshots (wrapper + per-rank sidecars + exact reconstruction). |
| tests/parallel/ptest_0007_snapshot_inmemory.py | MPI test for in-memory snapshots (exact reconstruction + continuation). |
| tests/parallel/mpi_runner.sh | Adds snapshot ptests to the MPI runner script. |
| src/underworld3/systems/ddt.py | Adds Snapshottable state dataclasses + .state adapters and model registration for DDt flavors. |
| src/underworld3/swarm.py | Adds swarm population generation counter and snapshot payload/apply support. |
| src/underworld3/model.py | Adds _state_bearers, Model.tracker, and unified save_state/load_state API. |
| src/underworld3/discretisation/discretisation_mesh.py | Adds mesh snapshot payload/apply support for in-memory restore. |
| src/underworld3/discretisation/discretisation_mesh_variables.py | Adds v1.1 wrapper detection/bridge in read_timestep. |
| src/underworld3/checkpoint/tracker.py | Implements ModelTracker + TrackerState Snapshottable dataclass. |
| src/underworld3/checkpoint/state.py | Defines the SnapshottableState base and Snapshottable protocol. |
| src/underworld3/checkpoint/snapshot.py | In-memory snapshot orchestration (token capture/restore). |
| src/underworld3/checkpoint/disk_snapshot.py | Disk snapshot writer/reader, inspectability layer, sidecars, and python-state serialization. |
| src/underworld3/checkpoint/backend.py | Defines the snapshot backend protocol and in-memory backend implementation. |
| src/underworld3/checkpoint/init.py | Exposes snapshot toolkit public API surface. |
| src/underworld3/init.py | Imports underworld3.checkpoint at package import time. |
| docs/developer/guides/state-as-dataclass.md | Documents the state-as-dataclass contract for solver-internal state. |
| docs/developer/design/in_memory_checkpoint_design.md | Design note covering snapshot/restore motivation, semantics, and roadmap. |
| docs/advanced/snapshot-restore.md | User guide for save/load state (in-memory + on-disk) and tracker usage. |
| docs/advanced/index.md | Adds snapshot/restore to advanced docs index/toctree. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| DOFs, plus swarm positions and user swarm-variable data with | ||
| rebuild-on-restore semantics. Solver-internal Python state, on-disk | ||
| backend, schema versioning, mesh-DM rebuild, and cross-process restore | ||
| are scheduled for follow-up PRs per the design note. |
| """Unitary in-memory (and, later, on-disk) snapshot toolkit. | ||
| The first true unitary checkpoint in Underworld3 — captures enough state | ||
| that a Model can be put back exactly as it was, suitable for backtrack on | ||
| failure, multi-stage time integration, adaptive-Δt retry, and crash | ||
| recovery. | ||
| Distinct from the existing per-variable ``write_timestep`` / | ||
| ``read_timestep`` path, which serves visualisation and partial restart. | ||
| That path stays in service of its existing role. | ||
| See ``docs/developer/design/in_memory_checkpoint_design.md`` for the | ||
| design rationale, scope, and roadmap. In v1 (this code), only an | ||
| in-memory backend is implemented and only mesh + mesh-variable state is | ||
| captured. Subsequent PRs add swarm coverage, solver-internal Python | ||
| state (DDt history, parameter mutation history), an on-disk full-state | ||
| backend, and schema versioning across UW3 releases. |
| ├── /metadata (attrs: uw3_version, schema_version, | ||
| │ created_at, step, sim_time, dt, dim, | ||
| │ mesh_type, coordinate_system, | ||
| │ mpi_ranks_at_write, variables_summary, ...) | ||
| ├── /mesh (phase 2 — DMPlex topology + coords + labels) | ||
| ├── /variables (phase 2 — one subgroup per mesh-variable) | ||
| ├── /swarms (phase 3 — possibly @external_file refs) | ||
| └── /python_state (phase 3 — Snapshottable dataclasses as attrs) |
| f"current {DISK_SNAPSHOT_SCHEMA_VERSION}; on-disk schema " | ||
| f"migration will land with phase 6 (not yet implemented)" |
| output_base_name = os.path.join(outputPath, data_filename) | ||
| data_file = output_base_name + f".mesh.{data_name}.{index:05}.h5" | ||
| legacy_file = output_base_name + f".mesh.{data_name}.{index:05}.h5" | ||
| if not os.path.isfile(os.path.abspath(data_file)): | ||
| raise RuntimeError(f"{os.path.abspath(data_file)} does not exist") | ||
| is_v1_1 = ( | ||
| os.path.isfile(data_filename) | ||
| and not data_filename.endswith( | ||
| f".mesh.{data_name}.{index:05}.h5" | ||
| ) | ||
| and _is_snapshot_wrapper(data_filename) | ||
| ) | ||
| import h5py | ||
| import numpy as np | ||
| if is_v1_1: | ||
| data_file = data_filename | ||
| else: | ||
| data_file = legacy_file | ||
| if not os.path.isfile(os.path.abspath(data_file)): |
| # and restore would silently no-op. `state` is therefore a | ||
| # reserved name and cannot be a user-managed quantity. | ||
| cls_attr = getattr(type(self), name, None) | ||
| if hasattr(cls_attr, "__set__") or hasattr(cls_attr, "__get__"): |
| ```text | ||
| my_run.snap.h5 (~tens of KB; metadata, group structure) | ||
| my_run.snap.bulk/ (per-mesh + per-swarm sidecars) | ||
| {mesh}.mesh.00000.h5 | ||
| {mesh}.{var}.00000.h5 (one per mesh-variable) | ||
| {swarm}.swarm.h5 (one per swarm) | ||
| ``` |
| """Write a complete on-disk snapshot of the model's mesh + mesh-variable | ||
| state (phase 2 scope; swarms and python_state land in phase 3). | ||
| Produces two artifacts: | ||
| - ``path`` — the wrapper HDF5 file with rich metadata and the group | ||
| structure inspectable via ``h5ls``. | ||
| - ``_bulk_dir_for(path)`` — companion directory containing the | ||
| PETSc HDF5 files (mesh DM + per-variable section/vec) produced | ||
| by #146's :meth:`Mesh.write_checkpoint`. |
First slice of the on-disk snapshot format (v1.1). Establishes the file structure and the inspectability bar; no PETSc bulk yet (that is phase 2). Stacked on the in-memory snapshot toolkit (#195) and the model tracker (#196) so it can serialise both later. What lands: - src/underworld3/checkpoint/disk_snapshot.py - DISK_SNAPSHOT_SCHEMA_VERSION = 1 - write_snapshot_skeleton(model, path): writes /metadata attrs + empty stub groups /mesh /variables /swarms /python_state (the structure phases 2+ will fill in). - read_snapshot_metadata(path): reads /metadata back as a plain dict, decodes JSON-encoded list fields for convenience, validates schema version. - inspect_snapshot(path): human-readable summary suitable for print(...) at a notebook prompt. - src/underworld3/checkpoint/__init__.py: exports. - tests/test_0010_snapshot_disk_format.py (7, tier_a level_1): - top-level group structure matches the spec - h5py-readable /metadata attrs cover identity, schema, tracker conventions, geometry, MPI rank count, and inventories of meshes / swarms / state-bearer classes / variables — the proxy for "an external user running h5ls/h5dump sees useful info" - read/write roundtrip - rejection of non-snapshot files and wrong-schema files with clear errors (not obscure h5py noise) - inspect_snapshot includes the key facts - skeleton groups carry `filled_by` attrs so phases 2/3 readers and external inspectors can tell whether content is populated yet. Design notes encoded: - UW3-controlled rich-metadata wrapper around PETSc bulk; pure PETSc HDF5 dumps fail the inspectability bar so are rejected as the format. - List-typed metadata stored as JSON strings in scalar attrs so h5py / h5ls handle them cleanly; read API exposes them as plain Python lists alongside the *_json originals. - Swarm storage left as a phase-3 decision: the metadata wrapper is designed to support `@external_file` on /swarms/swarm_X/ when individual swarms grow too bulky for a single file. No commitment to inline vs split until phase 3 has real swarm sizes in hand. Stacked on feature/model-tracker; PRs to development after #195 and #196 land. Underworld development team with AI support from Claude Code (https://claude.com/claude-code)
…t roundtrip Builds on phase 1's metadata wrapper to actually carry mesh + mesh- variable state to disk and read it back. Delegates the heavy lifting to #146's `Mesh.write_checkpoint` / `MeshVariable.read_checkpoint` PETSc-DMPlex primitives — phase 2's job is layout, dispatch, and tying the wrapper to the bulk data via a simple convention. Layout (final v1.1 shape): /path/to/run.snap.h5 wrapper (h5py-inspectable) /path/to/run.snap.bulk/ companion directory (one per snap) {mesh_safe}.mesh.00000.h5 {mesh_safe}.{var_clean}.00000.h5 Wrapper carries /meshes/{mesh_safe}/ with @name, @mesh_file, and /meshes/{mesh_safe}/variables/{var_safe}/ with @name, @components, @degree, @continuous, @external_file. The bulk-dir path is derived from the wrapper path by convention (`.h5` → `.bulk`), so no external_file attr is needed for the standard placement. Move them together; a clear FileNotFoundError fires if bulk is missing on read. Phase 1 layout refactor folded in: - /mesh (singular) → /meshes (plural) — supports multi-mesh natively. - /variables removed from the top level — now nests under each mesh as /meshes/{name}/variables/{var}, matching the in-memory snapshot's mesh→vars structure. New API: - `write_snapshot(model, path)` — writes wrapper + bulk; covers every registered mesh and every allocated meshvar on each mesh. Lazy-allocated vars (_gvec is None) are skipped — same rule as the in-memory path. - `read_snapshot(model, path)` — loads var DOFs back into already- registered meshes by name. Mesh / variable mismatch raises a clear ValueError (mesh-rebuild on read is v1.2 scope). - `write_snapshot_skeleton` / `read_snapshot_metadata` / `inspect_snapshot` stay as phase-1 metadata-only entry points. Branch hygiene: merged origin/development (which now has #146) into this branch so the new code can actually call read_checkpoint. The merge was clean — #146 and the snapshot toolkit only overlap at different methods in `discretisation_mesh.py`, as the earlier analysis predicted. PR target will be development once #195/#196 land; the diff stays clean because the merged dev commits are already there. Tests (12 total, 5 new in phase 2, tier_a level_1): - write produces wrapper + bulk-dir with the expected file pattern - wrapper populated with the per-mesh + per-var metadata that makes inspectability self-sufficient - bit-exact write→scribble→read roundtrip on a 2D mesh with one scalar + one vector variable (np.array_equal, zero tolerance) - missing bulk-dir → clear FileNotFoundError - mismatched mesh on read → clear ValueError (not an obscure h5py trace) Regression: 64 tests pass (24 snapshot + 9 tracker + 12 disk-format + 19 core/regression). Phase 3 next: swarms (with the @external_file freedom kept open for bulky swarms) + /python_state for DDt + ModelTracker via dataclass- to-HDF5-attrs serialisation. Underworld development team with AI support from Claude Code (https://claude.com/claude-code)
Serialises every registered Snapshottable's .state dataclass into a
per-bearer group under /python_state, keyed by the same stable name
the in-memory snapshot uses (f"{type(obj).__name__}_{obj.instance_number}").
ModelTracker (always auto-registered) and DDt state therefore now
travel with the disk snapshot in addition to the mesh + meshvar
bulk from phase 2.
Generic field serialisation (no per-class code):
- None -> attr "__none__" sentinel
- bool/int/float/str-> scalar attr (preserves type via h5py)
- numpy.ndarray -> dataset
- list/tuple -> attr <name>__json (JSON, handles None)
- dict -> subgroup, recursive (used by TrackerState.managed)
- unhandleable -> attr <name>__skipped = "<type info>"
— restore keeps the *current* live value rather than clobbering
it with a placeholder, so a documented partial round-trip (e.g.
DDtSymbolicState.psi_star which is sympy and would need
srepr+sympify) doesn't break.
Restore uses the live obj.state as a type template + dataclasses.
replace(...): captured fields override; skipped fields keep their
current value. ValueError on state-bearer-not-registered keeps the
same-rank/same-model contract.
Tests (4 new, 16 total tier_a level_1):
- tracker time/step/dt + user-added quantity (scalar + numpy array)
round-trip exactly through disk
- /python_state group is h5py-inspectable: __bearer_class__,
__state_class__, instance_number; TrackerState.managed visible as
a subgroup with each managed key as an attr (so h5ls shows
'time', 'step', 'dt', 'my_q' directly)
- Symbolic DDt's primary BDF-control fields (dt_history,
history_initialised, n_solves_completed, dt) round-trip; psi_star
(sympy) is documented as skipped — restore keeps current value
- mismatched state-bearer set on read raises clearly
Phase 3b next: swarms in a per-swarm sidecar from day one
(per Louis's "break out swarms" direction — bulk is always a swarm
problem, so don't even try inline).
Regression: 68 tests pass (24 in-memory + 9 tracker + 16 disk-format
+ 19 core/regression).
Underworld development team with AI support from Claude Code
(https://claude.com/claude-code)Per Louis's direction ("break out the swarm information into a
separate file in the first instance — bulk is a problem with swarms,
always"), swarms always go to their own h5py-direct sidecar from day
one. No inline-vs-split toggle — sidecar is the only path.
Layout:
/path/to/run.snap.h5 wrapper
/path/to/run.snap.bulk/{swarm_safe}.swarm.h5 swarm sidecar (one
per swarm)
Sidecar structure (h5py-native, no PETSc — swarms aren't DMPlex
section/vec):
@num_particles_local, @dim, @mesh_name, @population_generation
/coordinates dataset, (n_local, dim)
/variables/{var_clean_name} dataset, (n_local, num_components)
@num_components, @dtype
The sidecar's top-level @attrs and group structure mean `h5ls -v`
on the sidecar alone tells you "this holds N particles in dim D on
mesh M with these variables" — same inspectability bar as the
wrapper.
Wrapper /swarms/{swarm_safe}/ carries metadata + the @external_file
pointer to the sidecar in the bulk dir.
Restore mirrors the in-memory Swarm.apply_snapshot_payload exactly:
clear local population via dm.removePoint loop, addNPoints at saved
coords, write var data back. Same rebuild-on-restore semantics — the
disk snapshot recovers from a particle-population mutation (added
particles between snapshot and restore) just like the in-memory path
does, proven by test_swarm_restore_recovers_after_particle_count_change.
Tests (5 new, 21 total tier_a level_1):
- swarm sidecar lands in bulk dir with predictable name; wrapper
records external_file ref + mesh_name + var inventory
- sidecar is self-inspectable via h5py (file-level attrs +
/coordinates + /variables with per-var attrs)
- whole swarm (coords + svar data) round-trips bit-exact through
write → scribble → read
- rebuild-on-restore parity with in-memory path: snapshot, mutate
population, restore → exact local population recovered
- PETSc-internal DMSwarm_* variables filtered at capture (same rule
as in-memory)
MPI: single-rank only in this phase. The current rank-0-only sidecar
write only captures rank 0's local particles in a parallel run.
Phase 6 will either use h5py-mpi parallel HDF5 or per-rank sidecars
to match #195's parallel exact-reconstruction guarantee.
73 tests pass (24 in-memory + 9 tracker + 21 disk-format + 19
core/regression).
Phase 4 next: format detection + dispatch in MeshVariable.read_timestep
so it reads BOTH the legacy per-variable layout AND the new v1.1
sidecar format via the KDTree bridge. Closes the compatibility
commitment from the design discussion.
Underworld development team with AI support from Claude Code
(https://claude.com/claude-code)Single user-facing entry point for all snapshot use cases. Same
methods serve in-memory ephemeral stash and on-disk persistent
snapshot — the dispatch is mechanical, the user has one API to
learn:
token = model.save_state() # in-memory, returns Snapshot
model.load_state(token) # restore from token
model.save_state(file="step42.snap.h5") # on-disk, returns path
model.load_state("step42.snap.h5") # restore from disk
# (also: load_state(file=…))
load_state dispatches on argument type — Snapshot → in-memory
restore; str/PathLike → disk restore. Type-mismatched source raises
TypeError with a clear message.
Renames replace the prior Model.snapshot() / Model.restore() pair
from #195. Pre-merge, no public users to migrate; getting the
user-facing API right now means there is never a disparate version
shipped. uw.checkpoint.{snapshot,restore,write_snapshot,read_snapshot,
read_snapshot_metadata,inspect_snapshot,write_snapshot_skeleton}
stay as power-user / lower-level entry points that save_state /
load_state delegate to.
Files updated (mechanical renames, except the doc rewrite):
- src/underworld3/model.py: save_state / load_state methods replace
snapshot / restore; load_state accepts positional Snapshot or
str/os.PathLike, with TypeError on anything else.
- tests/test_0007_snapshot_inmemory.py — 23 callers renamed; obsolete
test_snapshot_path_is_v1_1_scope deleted (v1.1 has landed).
- tests/test_0008_snapshot_realsolver.py — 3 tests renamed.
- tests/test_0009_model_tracker.py — 9 tests renamed.
- tests/test_0010_snapshot_disk_format.py — 21 tests: replace
uw.checkpoint.write_snapshot / read_snapshot with model.save_state
/ model.load_state at user-style call sites; keep
write_snapshot_skeleton + read_snapshot_metadata where the test is
specifically exercising the lower-level entry points.
- tests/parallel/ptest_0007_snapshot_inmemory.py — np-1/3/4 ptest.
- tests/run_snapshot_backstepping_{demo,spatial}.py — demo scripts.
- docs/advanced/snapshot-restore.md — rewritten API section to show
both modes; added "On-disk file layout" section and a "Choosing
between paths" comparison table covering write_timestep,
write_checkpoint, and save_state. Limitations section updated to
reflect that on-disk is now real (was "in-memory only").
Regression: 75 single-rank tests pass (was 76 — minus the deleted
obsolete v1.1-scope test); MPI ptest at -np 4 still PASS with the
parallel exact-reconstruction guarantee. Docs build clean with no
snapshot-related warnings; the new layout + choosing-between-paths
sections render.
Phase 4 (read_timestep format-aware dispatch for backward compat)
becomes a nice-to-have at this point — save_state / load_state is
the recommended surface, write_timestep / read_timestep keep their
existing role unchanged. Phase 6 (parallel HDF5 / per-rank sidecars
for on-disk MPI) is the remaining correctness item.
Underworld development team with AI support from Claude Code
(https://claude.com/claude-code)The selective-read entry point users already know (``var.read_timestep(...)``) now reads BOTH the legacy ``write_timestep`` per-variable HDF5 files AND v1.1 snapshot wrappers — same call, format detection is hidden inside the function. No user code has to learn a second API for the new format; existing scripts with ``var.read_timestep(...)`` calls keep working transparently against new files. This is the compat commitment from the design discussion: "the clean interface lies beneath the surface for this case" — meaning the format dispatch is hidden, not that read_timestep itself is hidden. read_timestep serves a different use case than save_state/load_state (selective per-variable, cross-resolution remap via KDTree, visualisation-style reads); both stay user-facing. Implementation: - ``uw.checkpoint.is_snapshot_wrapper(path)``: cheap format detector — checks for top-level /metadata + /meshes groups. - ``uw.checkpoint.extract_var_via_bridge(wrapper_path, var_name)``: given a v1.1 wrapper + variable name, returns (coords, values) numpy arrays — exactly what the legacy file's h5 read produces. Mechanism: load source mesh from .mesh.h5 sidecar, rebuild source variable with matching degree/components, load DOFs via #146's MeshVariable.read_checkpoint, read out var.coords and var.array. - MeshVariable.read_timestep: before its rank-0 (coord, value) read, dispatches on the file's format. v1.1 → bridge. Legacy → existing per-variable h5 read. Everything after — the source- swarm + query-swarm KDTree-routing machinery — is reused unchanged. Tests (2 new, 23 total in test_0010, 77 across the snapshot suite): - read_timestep against a v1.1 snapshot wrapper round-trips a variable bit-exact (KDTree query lands on captured DOF coords) - read_timestep against a legacy write_timestep file still uses the legacy code path (belt-and-braces no-regression check) Phase 6 (parallel on-disk MPI) remains as the production-readiness gate for the disk path. Underworld development team with AI support from Claude Code (https://claude.com/claude-code)
…n-disk
Closes the last production-readiness gate on the disk path. Swarm
sidecars are now per-rank files: each rank writes its own
{swarm_safe}.swarm.rank{R:04d}of{S:04d}.h5, the wrapper records the
naming pattern + rank count, and on restore each rank opens its
matching file. Same shape that #146 uses internally for mesh-var
collectives via PETSc, just expressed as per-rank h5py files rather
than a single parallel-HDF5 file (avoids the h5py-mpi build
dependency).
Contract: same-rank-count restart only. Rank-count mismatch on read
raises clearly with a pointer to mesh.write_timestep for the
flexible-restart path. Each sidecar carries its writer's
(mpi_rank, mpi_size_at_write) attrs so a wrong-rank-file load
also fails cleanly.
Wrapper layout addition:
- /swarms/@filled_by = "phase3b+phase6"
- /swarms/@mpi_size_at_write
- /swarms/{name}/@sidecar_pattern (template with {rank}/{size})
- /swarms/{name}/@num_particles_global (gathered across ranks via
MPI.SUM at write time)
Phase 6 implementation deliberately keeps the mesh-var collective
path #146 already provides — no changes to mesh-side bulk write/read.
Only the swarm-sidecar layer is rebuilt for per-rank operation.
Tests:
- 23 single-rank tests in test_0010 (unchanged count; updated the
two that asserted the old single-file naming).
- New ptest_0010_snapshot_disk.py exercises -np 1/3/4: wrapper +
per-rank sidecars present, particle count preserved, swarm round-
trip exact (gather + sort by per-particle gid), tracker state
restored, T mesh-var DOFs preserved (via partition-invariant
min/max scalars — gathered DOF tables include partition-boundary
duplicates that resist direct comparison).
- mpi_runner.sh registers the new ptest at -np 1 / 3 / 4.
Final tally: 77 single-rank tests green; parallel ptest_0007
(in-memory) and ptest_0010 (on-disk) both PASS at np 1/3/4.
Production verdict on the disk path: matches the in-memory path —
correct serial, parallel, and through real solvers. The full v1.1
plan from project_snapshot_v1_1_disk_format.md is now landed:
phases 1, 2, 3a, 3b, 4 (read_timestep dispatch), 5 (unified
save_state/load_state API), 6 (parallel sidecars).
Underworld development team with AI support from Claude Code
(https://claude.com/claude-code)dbbf52a to
eba7500CompareUh oh!
There was an error while loading. Please reload this page.
…ring Post-merge cleanup for the snapshot-toolkit work (PRs underworldcode#195, underworldcode#196, underworldcode#198) now landed on development. - CHANGES.md: new 2026-05-20 entry covering the toolkit and the underworldcode#184 Lagrangian typo fix. Documents save_state/load_state surface, Model.tracker, on-disk v1.1 format, format-aware read_timestep, the state-as-dataclass contract, and how the existing write_timestep / write_checkpoint paths remain unchanged (different use cases). - docs/developer/design/in_memory_checkpoint_design.md: API-shape code example updated to current names (save_state/load_state with file= kwarg) — the draft snapshot/restore verbs were renamed in phase 5; the design discussion itself is unchanged. - docs/developer/guides/state-as-dataclass.md: example code updated to the current save_state/load_state names; pointer to the user guide added. - docs/developer/index.md: added guides/state-as-dataclass to the Guides toctree (it was orphaned, generating a "not included in any toctree" warning since underworldcode#195 landed). docs-build succeeds with no snapshot-related warnings. Underworld development team with AI support from Claude Code (https://claude.com/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
Summary
Adds the on-disk arm of the snapshot toolkit —
Model.save_state(file=…)/Model.load_state(file=…). Pairs the in-memory work in #195 (the "git stash for timesteps") with a persistent format inspectable via standard h5 tools, parallel-correct, and built on top of #146's PETSc DMPlex primitives.Based on
feature/model-tracker(#196) →feature/in-memory-checkpoint(#195) →development. The PR will show a large diff until those land — once they merge, the diff narrows to just the snapshot-disk additions automatically. Targetingdevelopmentdirectly because that's the actual destination; readers focus on the new files undersrc/underworld3/checkpoint/disk_snapshot.pyandtests/test_0010*.What's here (six phases)
72681d1e4a43e0/python_state— Snapshottable dataclass round-trip8e3d04a83061beModel.save_state/load_stateAPIdf4f829MeshVariable.read_timestepformat-aware dispatch3bb201ddbbf52aUser-facing API
The whole disk path is exposed through the same two methods #196 introduced for in-memory:
File layout
h5ls -v my_run.snap.h5/metadatashows run name, schema version, sim time, step, dim, MPI rank count, and inventories — no UW3 needed.Tests
ptest_0007) and on-disk (ptest_0010). Exact reconstruction confirmed across cross-rank particle distribution; recovers from real cross-rank particle loss.test_0008_snapshot_realsolver) shows bit-exact discard guarantee through an AdvDiffusion solve.Design decisions captured
h5ls-without-UW3, so we wrap PETSc bulk in a UW3-controlled metadata layer.read_timestepstays user-facing and selective — different use case (variable subsets, cross-resolution remap) fromload_state's whole-model role. The format detection is hidden behind the call.write_timestepstays as the selective-output path; a futurevars=[…]filter onsave_statewould close the gap if needed.Test plan
pixi run -e amr-dev pytest tests/test_0007_snapshot_inmemory.py tests/test_0008_snapshot_realsolver.py tests/test_0009_model_tracker.py tests/test_0010_snapshot_disk_format.py(77 tests)cd tests/parallel && bash mpi_runner.sh(covers ptest_0007 + ptest_0010 at 1/3/4 ranks)pixi run -e amr-dev docs-build—advanced/snapshot-restore.htmlrenders with all sectionsAfter #195 and #196 merge
Retarget this PR's base if needed (it will be already if dev contains those merges) — the diff narrows automatically.
Underworld development team with AI support from Claude Code