Skip to content

Commit 2c5f295

Browse files
committed
Fix _gvec not synced after solver runs, causing zero checkpoint data
After PETSc solves, solution data was copied only to _lvec (local vector) but _gvec (global vector) was never updated. Since write_timestep and all checkpoint methods write from _gvec, every saved field was zeros. Fix adds _sync_lvec_to_gvec() to _BaseMeshVariable using the correct subdm scatter pattern, and calls it: - In all 3 base solver finalizations (SNES_Scalar, SNES_Vector, SNES_Stokes_SaddlePt) - In all write/checkpoint methods (belt-and-suspenders) The operation is idempotent so the double coverage is safe. Underworld development team with AI support from Claude Code
1 parent 546f48c commit 2c5f295

3 files changed

Lines changed: 40 additions & 4 deletions

File tree

src/underworld3/cython/petsc_generic_snes_solvers.pyx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1669,9 +1669,11 @@ class SNES_Scalar(SolverBaseClass):
16691669
self.u.vec.array[:] = lvec.array[:]
16701670
self.mesh._stale_lvec = True
16711671

1672-
# Invalidate cached data views - PETSc buffer may have changed
1673-
# Handle both EnhancedMeshVariable (has _base_var) and direct _MeshVariable
1672+
# Sync _gvec so downstream consumers (write, stats) see the result
16741673
target_var = getattr(self.u, "_base_var", self.u)
1674+
target_var._sync_lvec_to_gvec()
1675+
1676+
# Invalidate cached data views - PETSc buffer may have changed
16751677
if hasattr(target_var, "_canonical_data"):
16761678
target_var._canonical_data = None
16771679

@@ -2438,9 +2440,11 @@ class SNES_Vector(SolverBaseClass):
24382440
self.u.vec.array[:] = lvec.array[:]
24392441
self.mesh._stale_lvec = True
24402442

2441-
# Invalidate cached data views - PETSc buffer may have changed
2442-
# Handle both EnhancedMeshVariable (has _base_var) and direct _MeshVariable
2443+
# Sync _gvec so downstream consumers (write, stats) see the result
24432444
target_var = getattr(self.u, "_base_var", self.u)
2445+
target_var._sync_lvec_to_gvec()
2446+
2447+
# Invalidate cached data views - PETSc buffer may have changed
24442448
if hasattr(target_var, "_canonical_data"):
24452449
target_var._canonical_data = None
24462450

@@ -3842,6 +3846,12 @@ class SNES_Stokes_SaddlePt(SolverBaseClass):
38423846
var.vec.array[:] = clvec.getSubVector(pressure_is).array[:]
38433847
self.mesh._stale_lvec = True
38443848

3849+
# Sync _gvec so downstream consumers (write, stats) see the result
3850+
for name, var in self.fields.items():
3851+
target_var = getattr(var, "_base_var", var)
3852+
target_var._sync_lvec_to_gvec()
3853+
if hasattr(target_var, "_canonical_data"):
3854+
target_var._canonical_data = None
38453855

38463856
self.dm.restoreGlobalVec(clvec)
38473857
self.dm.restoreGlobalVec(gvec)

src/underworld3/discretisation/discretisation_mesh.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1851,6 +1851,7 @@ def petsc_save_checkpoint(
18511851
### Empty meshVars will save just the mesh
18521852
if meshVars != None:
18531853
for var in meshVars:
1854+
var._sync_lvec_to_gvec()
18541855
viewer(var._gvec)
18551856

18561857
viewer.destroy()
@@ -1903,6 +1904,7 @@ def write_checkpoint(
19031904

19041905
if meshVars is not None:
19051906
for var in meshVars:
1907+
var._sync_lvec_to_gvec()
19061908
iset, subdm = self.dm.createSubDM(var.field_id)
19071909
subdm.setName(var.clean_name)
19081910
self.dm.globalVectorView(viewer, subdm, var._gvec)
@@ -1912,6 +1914,7 @@ def write_checkpoint(
19121914
if swarmVars is not None:
19131915
for svar in swarmVars:
19141916
var = svar._meshVar
1917+
var._sync_lvec_to_gvec()
19151918
iset, subdm = self.dm.createSubDM(var.field_id)
19161919
subdm.setName(var.clean_name)
19171920
self.dm.globalVectorView(viewer, subdm, var._gvec)

src/underworld3/discretisation/discretisation_mesh_variables.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,9 @@ def save(
977977
# Keep vector available for future access
978978
pass
979979

980+
# Ensure global vector is up-to-date before writing.
981+
self._sync_lvec_to_gvec()
982+
980983
viewer = PETSc.ViewerHDF5().create(filename, "a", comm=PETSc.COMM_WORLD)
981984
if index:
982985
raise RuntimeError("Recording `index` not currently supported")
@@ -1078,6 +1081,9 @@ def write(
10781081
dmnew.localToGlobal(lvec, gvec, addv=False)
10791082
gvec.setName("coordinates")
10801083

1084+
# Ensure global vector is up-to-date before writing.
1085+
self._sync_lvec_to_gvec()
1086+
10811087
viewer = PETSc.ViewerHDF5().create(filename, "w", comm=PETSc.COMM_WORLD)
10821088
viewer(self._gvec)
10831089
viewer(gvec)
@@ -1601,6 +1607,23 @@ def vec(self) -> PETSc.Vec:
16011607

16021608
return self._lvec
16031609

1610+
def _sync_lvec_to_gvec(self):
1611+
"""Ensure the global vector reflects the current local vector.
1612+
1613+
After PETSc solves (Stokes, Projection, etc.), data lives only in
1614+
``_lvec``. Any operation that reads ``_gvec`` (writes, norms, etc.)
1615+
must call this first. The scatter uses the variable's sub-DM so it
1616+
works for any field order (P1, P2, DG-0, …).
1617+
1618+
The operation is idempotent — calling it when ``_gvec`` is already
1619+
up-to-date is harmless.
1620+
"""
1621+
if self._lvec is None or self._gvec is None:
1622+
return
1623+
indexset, subdm = self.mesh.dm.createSubDM(self.field_id)
1624+
subdm.localToGlobal(self._lvec, self._gvec, addv=False)
1625+
indexset.destroy()
1626+
16041627
@property
16051628
def old_data(self) -> numpy.ndarray:
16061629
"""

0 commit comments

Comments
 (0)