diff --git a/src/underworld3/discretisation/discretisation_mesh_variables.py b/src/underworld3/discretisation/discretisation_mesh_variables.py index 7d7419cb6..27327c2b0 100644 --- a/src/underworld3/discretisation/discretisation_mesh_variables.py +++ b/src/underworld3/discretisation/discretisation_mesh_variables.py @@ -1229,11 +1229,20 @@ def _petsc_payload_name(data_file, data_name): if uw.mpi.rank == 0: try: with h5py.File(data_file, "r") as h5f: - topologies = h5f.get("topologies") - for topology in (topologies or {}): - dms = topologies[topology].get("dms") - if dms is not None and data_name in dms: - found = data_name + # The payload sat at the file root before #756 and under + # /restart/petsc after it. Look in both: this is only an + # existence check, and a file written by either version + # has to stay readable. Checking one place is what + # regressed #640 -- the writer still emits the per-field + # section and its vector, just one group deeper. + for group in ("topologies", "restart/petsc/topologies"): + topologies = h5f.get(group) + for topology in (topologies or {}): + dms = topologies[topology].get("dms") + if dms is not None and data_name in dms: + found = data_name + break + if found is not None: break except (OSError, KeyError): found = None @@ -1253,10 +1262,20 @@ def _guard_coincident_dofs(self, data_file, data_name, is_v1_1, 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( - data_file, data_name) - - if payload is not None and saved_rows == live_rows: + # Look for the section in either format. `_petsc_payload_name` is a + # structural lookup (topologies/*/dms/) and does not care which + # wrapper the file uses, and `read_timestep` calls + # `_read_native_payload` on a v1.1 file straight after this guard -- + # so treating v1.1 as payload-free refused the very files the native + # path was about to read correctly (#640 regressed by #756). + payload = self._petsc_payload_name(data_file, data_name) + + # v1.1 shares one compact geometry across the fields (#756), so there + # is no per-field saved cloud to count and `saved_rows` is 0 above by + # construction; the row comparison only means something for a legacy + # file. The payload carries the section, which is what resolves the + # cut, so in v1.1 its presence is the whole test. + if payload is not None and (is_v1_1 or saved_rows == live_rows): return # `_read_native_payload` will take it raise RuntimeError( diff --git a/tests/test_0864_split_checkpoint_roundtrip.py b/tests/test_0864_split_checkpoint_roundtrip.py index 7a18317ac..2e493ffc4 100644 --- a/tests/test_0864_split_checkpoint_roundtrip.py +++ b/tests/test_0864_split_checkpoint_roundtrip.py @@ -149,3 +149,45 @@ def test_unsplit_mesh_still_uses_the_coordinate_remap(tmp_path): coords = np.asarray(loaded.coords_nd) assert np.allclose(loaded.data[:, 0], 100.0 * coords[:, 0] + 10.0 * coords[:, 1], atol=1e-8) + + +@pytest.mark.level_1 +@pytest.mark.tier_b +def test_the_payload_is_found_wherever_the_writer_put_it(tmp_path): + """The existence check must not be tied to one file layout. + + #756 moved the reload payload from the file root into /restart/petsc. + The writer still emitted the per-field section and its vector, one + group deeper, but `_petsc_payload_name` looked only at the root, so it + reported "no PETSc-native payload" for a file written WITH + petsc_reload=True and the #640 guard refused the good path as well as + the bad. The round-trip test above catches the symptom; this pins the + cause, so a future relocation fails here with the reason rather than + as a confusing refusal. + """ + import h5py + + mesh = _split_mesh() + var = _stamped_variable(mesh) + _write(tmp_path, mesh, var, petsc_reload=True) + written = tmp_path / f"rt.mesh.{var.clean_name}.00000.h5" + + # The section and its global vector are both on disk, somewhere. + with h5py.File(written, "r") as h5f: + groups = [] + h5f.visititems( + lambda name, obj: groups.append(name) + if isinstance(obj, h5py.Group) else None) + assert any(g.endswith(f"dms/{var.clean_name}/section") for g in groups), groups + assert any(g.endswith(f"dms/{var.clean_name}/vecs/{var.clean_name}") + for g in groups), groups + + # ... and the guard's existence check finds it there. The method lives on + # _BaseMeshVariable; EnhancedMeshVariable is a proxy and does not inherit + # from it, so reach for the defining class rather than type(var). + from underworld3.discretisation.discretisation_mesh_variables import ( + _BaseMeshVariable, + ) + + assert _BaseMeshVariable._petsc_payload_name( + str(written), var.clean_name) == var.clean_name