Skip to content

Read split-mesh checkpoints through the section, not the coordinates (#640) - #714

Open
lmoresi wants to merge 1 commit into
developmentfrom
bugfix/checkpoint-cut-dofs
Open

Read split-mesh checkpoints through the section, not the coordinates (#640)#714
lmoresi wants to merge 1 commit into
developmentfrom
bugfix/checkpoint-cut-dofs

Conversation

@lmoresi

@lmoresi lmoresi commented Sep 9, 2026

Copy link
Copy Markdown
Member

Closes #640.

The defect

A cut duplicates a node at exactly the same coordinate, one copy per side of the fault. read_timestep matches saved values to live DOFs by nearest neighbour, which cannot choose between them — both sides were handed whichever saved point the tree returned first, and part of the slip discontinuity was smeared into the first element ring.

Reproduced without a solve, by stamping each side of every cut pair with a value that differs by construction:

split mesh: 780 DOFs, 15 coincident groups
  read_timestep  (coordinate remap): 15/15 groups wrong — ALL collapsed onto one saved value
  read_checkpoint (PETSc native)   :  0/15 groups wrong

That matches the field symptom in #640: near-fault stress recovered from a reloaded checkpoint ran ~200x the in-memory answer.

The fix

The PETSc-native payload written by write_timestep(..., petsc_reload=True) carries the section, so it restores the two sides exactly. When the saved cloud contains coincident coordinates, read_timestep reads through that payload instead. Without one — or when the saved and live DOF counts differ, a cross-mesh remap that no coordinate can disambiguate at a cut — it raises rather than return a quietly wrong field. allow_ambiguous_duplicates=True restores the old behaviour, which stays valid for far-field quantities.

The parallel twist

The ambiguity is measured on the saved cloud, not on the live mesh, and that distinction is load-bearing: the partitioner routinely puts the two sides of a cut node on different ranks, so a rank-local duplicate test sees nothing to guard against — measured at np=2, 15 coincident groups in serial, 0 seen rank-locally. The guard would never fire where it is needed most. The file is a single global object, so one rank-0 read answers it for every rank.

Verified across write and read rank counts, every combination of np=1, 2, 3: all 15 cut pairs keep their two distinct values.

Tests

  • tests/test_0864_split_checkpoint_roundtrip.py — 5 serial cases: the round trip, the refusal without a payload, the escape hatch, and an unsplit mesh showing the remap path is untouched.
  • tests/parallel/ptest_0864_split_checkpoint_parallel.py — 4 cases at np=2 and np=3, including a full-coverage stamp that reaches the pairs straddling the partition.

Regression: test_0003/0005/0010/0102/0114 (109 passed), the fault suite test_0845-0848 (30 passed), and the parallel read_timestep tests (4 passed at np=2).

One trap worth recording

write_timestep reuses <name>.mesh.00000.h5 when it already exists (meshUpdates=False). Writing successive runs into one output directory therefore reads a stale mesh file against fresh variable files, which silently corrupts values at a cut — it cost an hour here posing as a writer defect. Worth a guard later; it is not one this PR adds.

Underworld development team with AI support from Claude Code

…640)

A cut duplicates a node at exactly the same coordinate, one copy per
side of the fault. `read_timestep` matches saved values to live DOFs by
nearest neighbour, which cannot choose between them: both sides were
handed whichever saved point the tree returned first, and part of the
slip discontinuity was smeared into the first element ring. Measured on
a 2-D fault box, every one of the 15 coincident groups collapsed onto a
single value; the near-fault stress recovered from a reloaded
checkpoint ran ~200x the in-memory answer, which is how this was found.

The PETSc-native payload written by `write_timestep(..., petsc_reload=
True)` carries the section, so it restores the two sides exactly. When
the saved cloud contains coincident coordinates, `read_timestep` now
reads through that payload instead. Without one — or when the saved and
live DOF counts differ, a cross-mesh remap no coordinate can
disambiguate at a cut — it raises rather than return a quietly wrong
field. `allow_ambiguous_duplicates=True` restores the old behaviour for
far-field quantities.

The ambiguity is measured on the SAVED cloud, not on the live mesh, and
that distinction is load-bearing in parallel: the partitioner routinely
puts the two sides of a cut node on different ranks, so a rank-local
duplicate test sees nothing to guard against (np=2: 15 coincident groups
in serial, 0 seen rank-locally) and the guard would never fire where it
is needed most. The file is a single global object, so one rank-0 read
answers it for every rank.

Verified across write and read rank counts (np=1, 2, 3 in every
combination): all 15 cut pairs keep their two distinct values.

Tests: test_0864 (serial, 5 cases: the round trip, the refusal, the
escape hatch, and an unsplit mesh to show the remap path is untouched)
and ptest_0864 (parallel, 4 cases including a full-coverage stamp that
reaches pairs straddling the partition).

Underworld development team with AI support from Claude Code
Copilot AI lite review requested due to automatic review settings September 9, 2026 17:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new guard appears to treat v1.1 snapshot wrappers as lacking a PETSc-native payload (raising on split meshes even though sidecar checkpoints exist), and one new parallel premise test assertion is currently non-testing/tautological.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes split-mesh checkpoint reload corruption (#640) by detecting when the coordinate-based nearest-neighbour remap is ambiguous (coincident duplicate DOF coordinates from add_fault) and, when possible, reloading through the PETSc-native section payload written by Mesh.write_timestep(..., petsc_reload=True); otherwise it raises unless explicitly overridden.

Changes:

  • Add coincident-DOF detection and a guarded fallback to read_checkpoint() inside MeshVariable.read_timestep(), plus an allow_ambiguous_duplicates escape hatch.
  • Add serial and parallel regression tests that stamp the two sides of cut-node duplicates to make collapses exact/visible without a solve.
  • Extend read_timestep() docstring and error messaging to describe split-mesh semantics and required writer settings.
File summaries
File Description
src/underworld3/discretisation/discretisation_mesh_variables.py Adds duplicate-coordinate guard + PETSc-native payload reload path to make split-mesh reload unambiguous.
tests/test_0864_split_checkpoint_roundtrip.py Serial regression coverage for round-trip correctness, refusal without payload, and escape hatch behavior.
tests/parallel/ptest_0864_split_checkpoint_parallel.py Parallel regression coverage ensuring cut-node distinctions survive reload across rank counts / partitions.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 5
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

if self._lvec is None:
self._set_vec(available=True)
live_rows = self._gvec.getSize() // self.num_components
payload = None if is_v1_1 else self._petsc_payload_name(
Comment on lines +1202 to +1205
"""The DM group in ``data_file`` holding ``data_name``'s section.

``None`` when the file was written without ``petsc_reload=True``.
"""
# the cut is in the topology whatever the partition did with it ...
assert global_dofs == serial_dofs
# ... but a rank need not see both copies of any given node
assert uw.mpi.comm.allreduce(local_pairs, op=MPI.SUM) >= 0
Comment on lines +152 to +158
def _side_stamped(mesh, name):
"""Stamp each side of a cut differently wherever a rank sees both.

This reaches the pairs an arbitrary stamp misses — including the ones
whose two copies straddle the partition, which is where the remaining
defect lives.
"""
Comment on lines +173 to +179
def test_every_cut_pair_survives_a_parallel_write(tmp_path):
"""Full-coverage version of the reload check — the writer's turn.

The stamp above leaves a few pairs holding the same value by chance;
this one reaches every pair a rank can see both copies of, so it
covers the ones that straddle the partition too.
"""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants