Fix mesh.X.coords stale after mesh deformation - #126
Conversation
_deform_mesh updates the DM coordinate vector and calls nuke_coords_and_rebuild, which may replace the internal coordinate buffer (via createCoordinateSpace). The self._coords NDArray view still pointed to the old buffer, so mesh.X.coords returned pre-deformation values. Fix: rebuild self._coords from the current DM coordinate vector after nuke_coords_and_rebuild, preserving the update callback. Closes #122 Underworld development team with AI support from Claude Code (https://claude.com/claude-code)
There was a problem hiding this comment.
Pull request overview
This PR fixes a correctness issue in Underworld3 mesh deformation where mesh.X.coords could continue to reference the pre-deformation PETSc coordinate buffer after _deform_mesh() triggers nuke_coords_and_rebuild() (which may internally replace the DM coordinate vector via createCoordinateSpace).
Changes:
- After
nuke_coords_and_rebuild(), rebuildself._coordsas a freshNDArray_With_Callbackview of the currentdm.getCoordinatesLocal()buffer. - Preserve the existing deformation callback(s) by re-attaching prior callbacks to the rebuilt
_coordswrapper.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| old_callbacks = getattr(self._coords, "_callbacks", []) | ||
| self._coords = underworld3.utilities.NDArray_With_Callback( | ||
| numpy.ndarray.view( | ||
| self.dm.getCoordinatesLocal().array.reshape(-1, self.cdim) | ||
| ), | ||
| owner=self, | ||
| ) |
There was a problem hiding this comment.
When rebuilding self._coords, only the callback functions are preserved. Any existing NDArray_With_Callback state (e.g. _callback_enabled from disable_callbacks() or _disable_inplace_operators) is reset to defaults, which can silently change behavior for callers that intentionally disabled callbacks. Consider copying these flags from the old self._coords onto the new instance (and copy the callback list rather than referencing it) so the wrapper rebuild is behavior-preserving beyond just the callback functions.
| old_callbacks = getattr(self._coords, "_callbacks", []) | |
| self._coords = underworld3.utilities.NDArray_With_Callback( | |
| numpy.ndarray.view( | |
| self.dm.getCoordinatesLocal().array.reshape(-1, self.cdim) | |
| ), | |
| owner=self, | |
| ) | |
| old_coords = self._coords | |
| sentinel = object() | |
| old_callbacks = list(getattr(old_coords, "_callbacks", [])) | |
| old_callback_enabled = getattr(old_coords, "_callback_enabled", sentinel) | |
| old_disable_inplace_operators = getattr( | |
| old_coords, "_disable_inplace_operators", sentinel | |
| ) | |
| self._coords = underworld3.utilities.NDArray_With_Callback( | |
| numpy.ndarray.view( | |
| self.dm.getCoordinatesLocal().array.reshape(-1, self.cdim) | |
| ), | |
| owner=self, | |
| ) | |
| if old_callback_enabled is not sentinel: | |
| self._coords._callback_enabled = old_callback_enabled | |
| if old_disable_inplace_operators is not sentinel: | |
| self._coords._disable_inplace_operators = ( | |
| old_disable_inplace_operators | |
| ) |
|
Test shows the fix works, thanks. |
Summary
mesh.X.coordsreturned pre-deformation coordinates after_deform_mesh()becauseself._coordsheld a numpy view of the old coordinate buffernuke_coords_and_rebuild(called by_deform_mesh) replaces the DM's coordinate vector viacreateCoordinateSpace, butself._coordswas never updated to view the new bufferNDArray_With_Callbackwrapper from the current DM coordinate vector afternuke_coords_and_rebuild, preserving the deformation callbackReproducer (from @NengLu)
Test plan
mesh.X.coordsmatchesmesh.dm.getCoordinatesLocal()after deformationCloses #122
Underworld development team with AI support from Claude Code