Infrastructure for moving VertexOnlyMeshes - #5294
Conversation
…o achanbour/mutable-fs-on-vom
…tractMeshTopology
| # LRU cache for expressions assembled onto this function | ||
| self._expression_cache = cachetools.LRUCache(maxsize=50) | ||
|
|
||
| self._mesh_topo = self._function_space.topological.mesh() # the MeshTopology object |
There was a problem hiding this comment.
Also need to be careful here with MeshSequences
There was a problem hiding this comment.
If self._function_space.topological.mesh() returns a MeshSequence then the versioning mechanism still works as I just added a _topology_version property inside the MeshSequence class:
@property
def _topology_version(self):
return tuple(mesh._topology_version for mesh in self._meshes)
The migration itself, however, wouldn't work because it currently checks that the Function being updated is one defined on a VOM:
def _rebuild_function(self, current_version):
# Check if the mesh on which the function is defined is a VertexOnlyMesh
if not isinstance(self._mesh_topology, VertexOnlyMeshTopology):
raise TopologyVersionMismatchError(
"The mesh topology has changed since this Function was created, \
and migration is currently only supported for Functions defined on VertexOnlyMeshes. \
Please re-create this Function on the updated mesh."
)
I think this is fine for now? It would seem wrong to only migrate the VOM component for a MeshSequence and leave everything else stale.
connorjward
left a comment
There was a problem hiding this comment.
This is a big change. Lots of feedback but this is really promising.
| # and migrate only ones in the intersection | ||
|
|
||
| # NOTE: This doesn't collect reference-cycle garbage identically on all ranks | ||
| gc.collect() |
There was a problem hiding this comment.
I would advocate for using this function (just in pyop3 currently but could come in early) instead here. Just temporarily disable the GC for the duration of this function.
There was a problem hiding this comment.
But if you don't do things eagerly then this might not be an issue, by definition if you are explicitly asking for a function to be migrated then you must hold another ref to it.
| ref_coords_func = self.vom.reference_coordinates | ||
| ref_coords_func.dat.data[:] = new_refcoords | ||
|
|
||
| def rebuild_vom(self, absorbed_vom_indices=None): |
There was a problem hiding this comment.
I much prefer the pattern where this a method of the VertexOnlyMesh.
Rebuild the VertexOnlyMesh using the state already stored on the VOM.
I think this demonstrates what I mean. The current pattern is:
- Partially mutate the VoM
- Hand this partially mutated VoM to another class that finishes the mutation
There was a problem hiding this comment.
So 1) should be in the VertexOnlyMeshMutator and 2) should be a method of the VertexOnlyMeshTopology?
There was a problem hiding this comment.
I don't think a separate class (VertexOnlyMeshMutator) is needed. All of this logic naturally lives on the VoM.
| # cell_numbering and vertex_numbering are PETSc ISes - translation tables between PETSc numbering of mesh entities and that of Firedrake's | ||
| # For each plex point, they store two integers: the dof count and an offset (which happens to be the Firedrake's number of that plex point) | ||
| topology._cell_numbering, _ = topology.create_section(entity_dofs) | ||
| topology._vertex_numbering = topology._cell_numbering # holds for VOM only |
There was a problem hiding this comment.
All this crud has to be here because the __init__ method does a whole lot of magic. I think if we made topology._vertex_numbering into a topology_cached_property then a lot of this could just go away. It is appealing for us to just tweak the swarm, increment the topology count, and then have everything else recompute as needed.
| f"Failed to migrate the Function across multiple topology changes: \ | ||
| the intermediate topology mapping from version {v-1} to {v} could not be found." | ||
| ) | ||
| chained_sf = step_sf.compose(chained_sf) # V -> V-2 |
There was a problem hiding this comment.
I am suggesting chaining the migrations as distinct steps with per-migration SFs, instead of building a composed SF that does it in one shot. Consider the k-2 to k example you showed. I'm basically saying to do
step_sf1 = self._mesh_topology._topology_step_sfs.get(...)
self._data = migrate_dg0_dat(self._data, FS_topo, step_sf1)
step_sf2 = self._mesh_topology._topology_step_sfs.get(...)
self._data = migrate_dg0_dat(self._data, FS_topo, step_sf2)instead of building the composed thing.
Also you're building the composed SF every time you migrate a function. If you have lots of functions with different topology versions that could get expensive.
And lastly I think it is reasonable to think about cases where the migration is done via a non-SF method. For example if you have some sort of mesh adaptivity you might refine a cell into some smaller cells. To transfer the data you'd have to do an interpolation. We can stack interpolation operations but I don't know how we would compose them into a single 'super interpolation'.
d0efc88 to
577eb25
Compare
|
@connorjward regarding your earlier comment about chaining migrations instead of composing the step SFs: For transferring data through interpolation, it would make sense to save a snapshot of the topological FS at each intermediate version, in which case stacking the migration/interpolation operations would work. At first impression, it doesn't sound like we're saving much more than simply doing the SF composition. Also, for the DG0 VOM case I'm dealing with now, composing SFs seems the most natural thing to do. What do you think? |
Ahh yes. Bloody mutable state... OK I see that my previous proposal is not the right way to design the abstraction. Crack on. |
|
|
||
| def _migrate_to_current_topology_version(self) -> None: | ||
| """Migrate this coordinateless function's data to the current topology version.""" | ||
| _migrate_dg0_coefficient(self, self._function_space) |
There was a problem hiding this comment.
This is the place where we should check that we are actually DG0 and fail appropriately (NotImplementedError)
| return self._dat | ||
|
|
||
| @dat.setter | ||
| def dat(self, value): |
There was a problem hiding this comment.
We should have a comment explaining things here because this is an extremely strange pattern.
| def dat(self, value): | ||
| if value is self._dat: | ||
| return | ||
| raise AttributeError("A Function's Dat cannot be replaced directly.") |
There was a problem hiding this comment.
| raise AttributeError("A Function's Dat cannot be replaced directly.") | |
| raise AttributeError("The 'dat' of a function cannot be changed") |
small thing but I think this is a bit clearer
There was a problem hiding this comment.
It is not a requirement for this PR, but you can probably now see that Function and Cofunction share an awful lot of code. It is somewhere on my TODO list to build a parent FunctionSpaceData (when the old version of that class dies) class for the shared functionality. One day...
This PR contains the bulk of the work for supporting mutable VOMs.
Broadly, this introduces 3 new features:
VertexOnlyMeshMutatorthat handles two update regimes: one that updates the local (reference-space) state of the VOM and one that does a full (topological) rebuild of the VOMFunctionSpaceproperties get recomputed when the topology version number of their underlying mesh changesFunctionmigration mechanism (both lazy and eager) which ensures that Function values refresh accordingly with the VOM's re-ordering.While I have tried to make 2 and 3 as general as possible, I have only written and tested them to work for DG0-style Function Spaces defined on VOMs.