Skip to content

Infrastructure for moving VertexOnlyMeshes - #5294

Open
achanbour wants to merge 33 commits into
mainfrom
achanbour/vom-mutation
Open

Infrastructure for moving VertexOnlyMeshes#5294
achanbour wants to merge 33 commits into
mainfrom
achanbour/vom-mutation

Conversation

@achanbour

Copy link
Copy Markdown
Contributor

This PR contains the bulk of the work for supporting mutable VOMs.

Broadly, this introduces 3 new features:

  1. A VertexOnlyMeshMutator that 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 VOM
  2. A conditional caching mechanism wherein FunctionSpace properties get recomputed when the topology version number of their underlying mesh changes
  3. A Function migration 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.

@achanbour
achanbour requested a review from connorjward July 29, 2026 10:23
Comment thread firedrake/function.py Outdated
Comment thread firedrake/function.py Outdated
Comment thread firedrake/function.py Outdated
Comment thread firedrake/function.py Outdated
Comment thread firedrake/function.py Outdated
Comment thread firedrake/vertexonly_mutation.py Outdated
Comment thread firedrake/vertexonly_mutation.py
Comment thread firedrake/vertexonly_mutation.py Outdated
Comment thread firedrake/vertexonly_mutation.py Outdated
Comment thread firedrake/function.py Outdated
Comment thread firedrake/function.py Outdated
# 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

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.

Also need to be careful here with MeshSequences

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread firedrake/functionspaceimpl.py Outdated

@connorjward connorjward 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.

This is a big change. Lots of feedback but this is really promising.

Comment thread firedrake/exceptions.py Outdated
Comment thread firedrake/mesh.py
Comment thread firedrake/mesh.py Outdated
Comment thread firedrake/mesh.py Outdated
Comment thread firedrake/mesh.py Outdated
# and migrate only ones in the intersection

# NOTE: This doesn't collect reference-cycle garbage identically on all ranks
gc.collect()

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.

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.

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.

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.

Comment thread firedrake/function.py Outdated
Comment thread firedrake/vertexonly_mutation.py Outdated
Comment thread firedrake/vertexonly_mutation.py Outdated
ref_coords_func = self.vom.reference_coordinates
ref_coords_func.dat.data[:] = new_refcoords

def rebuild_vom(self, absorbed_vom_indices=None):

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.

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:

  1. Partially mutate the VoM
  2. Hand this partially mutated VoM to another class that finishes the mutation

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So 1) should be in the VertexOnlyMeshMutator and 2) should be a method of the VertexOnlyMeshTopology?

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.

I don't think a separate class (VertexOnlyMeshMutator) is needed. All of this logic naturally lives on the VoM.

Comment thread firedrake/vertexonly_mutation.py Outdated
Comment thread firedrake/vertexonly_mutation.py Outdated
# 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

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.

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.

Comment thread firedrake/vertexonly_mutation.py Outdated
Comment thread firedrake/function.py Outdated
Comment thread firedrake/exceptions.py Outdated
Comment thread firedrake/function.py
Comment thread firedrake/function.py Outdated
Comment thread firedrake/function.py Outdated
Comment thread firedrake/function.py Outdated
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

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.

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'.

@achanbour
achanbour force-pushed the achanbour/vom-mutation branch from d0efc88 to 577eb25 Compare August 4, 2026 12:20
@achanbour

achanbour commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

@connorjward regarding your earlier comment about chaining migrations instead of composing the step SFs:
migrate_dg0_dat takes as one of its arguments the topological FS describing the new cell-node layout of the topology we're migrating into i.e., the latest version. But migrating from version k-2 to k requires us knowing the intermediate topology state (at version k-1) which is currently not preserved (all fields such as _cell_numbering, _vertex_numbering entity classes etc. are overwritten to reflect the last version k). All we have left from version k-1 is effectively the step SF k-2 -> k-1 hence why SF composition works nicely.

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?

@connorjward

Copy link
Copy Markdown
Contributor

But migrating from version k-2 to k requires us knowing the intermediate topology state (at version k-1) which is currently not preserved (all fields such as _cell_numbering, _vertex_numbering entity classes etc. are overwritten to reflect the last version k). All we have left from version k-1 is effectively the step SF k-2 -> k-1 hence why SF composition works nicely.

Ahh yes. Bloody mutable state... OK I see that my previous proposal is not the right way to design the abstraction. Crack on.

Comment thread firedrake/function.py

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)

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.

This is the place where we should check that we are actually DG0 and fail appropriately (NotImplementedError)

Comment thread firedrake/function.py
return self._dat

@dat.setter
def dat(self, value):

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.

We should have a comment explaining things here because this is an extremely strange pattern.

Comment thread firedrake/function.py
def dat(self, value):
if value is self._dat:
return
raise AttributeError("A Function's Dat cannot be replaced directly.")

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.

Suggested change
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

Comment thread firedrake/function.py

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.

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...

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.

3 participants