From a9f43e58f93f0324a298a8c2ee356119b1e464db Mon Sep 17 00:00:00 2001 From: Leo Collins Date: Wed, 4 Feb 2026 13:52:29 +0000 Subject: [PATCH 1/3] initial make the vom a cached_property --- firedrake/interpolation.py | 40 +++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/firedrake/interpolation.py b/firedrake/interpolation.py index f71a08445e..69e8f3808a 100644 --- a/firedrake/interpolation.py +++ b/firedrake/interpolation.py @@ -462,28 +462,15 @@ def __init__(self, expr: Interpolate): # scalar fiat/finat element self.dest_element = dest_element - def _get_symbolic_expressions(self) -> tuple[Interpolate, Interpolate]: - """Return the symbolic ``Interpolate`` expressions for point evaluation and - re-ordering into the input-ordering VertexOnlyMesh. - - Returns - ------- - tuple[Interpolate, Interpolate] - A tuple containing the point evaluation interpolation and the - input-ordering interpolation. - - Raises - ------ - DofNotDefinedError - If any DoFs in the target mesh cannot be defined in the source mesh. - """ + @cached_property + def vom(self): from firedrake.assemble import assemble # Immerse coordinates of target space point evaluation dofs in src_mesh target_space_vec = VectorFunctionSpace(self.target_mesh, self.dest_element) f_dest_node_coords = assemble(interpolate(self.target_mesh.coordinates, target_space_vec)) dest_node_coords = f_dest_node_coords.dat.data_ro.reshape(-1, self.target_mesh.geometric_dimension) try: - vom = VertexOnlyMesh( + source_vom = VertexOnlyMesh( self.source_mesh, dest_node_coords, redundant=False, @@ -495,7 +482,24 @@ def _get_symbolic_expressions(self) -> tuple[Interpolate, Interpolate]: f"source function space on domain {self.source_mesh}. " "This may be because the target mesh covers a larger domain than the " "source mesh. To disable this error, set allow_missing_dofs=True.") + return source_vom + + def _get_symbolic_expressions(self) -> tuple[Interpolate, Interpolate]: + """Return the symbolic ``Interpolate`` expressions for point evaluation and + re-ordering into the input-ordering VertexOnlyMesh. + + Returns + ------- + tuple[Interpolate, Interpolate] + A tuple containing the point evaluation interpolation and the + input-ordering interpolation. + + Raises + ------ + DofNotDefinedError + If any DoFs in the target mesh cannot be defined in the source mesh. + """ # Get the correct type of function space shape = self.target_space.ufl_function_space().value_shape if len(shape) == 0: @@ -507,11 +511,11 @@ def _get_symbolic_expressions(self) -> tuple[Interpolate, Interpolate]: fs_type = partial(TensorFunctionSpace, shape=shape, symmetry=symmetry) # Get expression for point evaluation at the dest_node_coords - P0DG_vom = fs_type(vom, "DG", 0) + P0DG_vom = fs_type(self.vom, "DG", 0) point_eval = interpolate(self.operand, P0DG_vom) # Interpolate into the input-ordering VOM - P0DG_vom_input_ordering = fs_type(vom.input_ordering, "DG", 0) + P0DG_vom_input_ordering = fs_type(self.vom.input_ordering, "DG", 0) arg = Argument(P0DG_vom, 0 if self.ufl_interpolate.is_adjoint else 1) point_eval_input_ordering = interpolate(arg, P0DG_vom_input_ordering) From 71ea78a49f8205996729ab2b55808cac8f7c774b Mon Sep 17 00:00:00 2001 From: Leo Collins Date: Wed, 4 Feb 2026 14:08:09 +0000 Subject: [PATCH 2/3] docstrings --- firedrake/interpolation.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/firedrake/interpolation.py b/firedrake/interpolation.py index 69e8f3808a..fd154b1d86 100644 --- a/firedrake/interpolation.py +++ b/firedrake/interpolation.py @@ -463,7 +463,20 @@ def __init__(self, expr: Interpolate): self.dest_element = dest_element @cached_property - def vom(self): + def vom(self) -> MeshGeometry: + """The VertexOnlyMesh consisting of the target space's dofs immersed in the + source space's mesh. + + Returns + ------- + MeshGeometry + The VertexOnlyMesh. + + Raises + ------ + DofNotDefinedError + If any DoFs in the target mesh cannot be defined in the source mesh. + """ from firedrake.assemble import assemble # Immerse coordinates of target space point evaluation dofs in src_mesh target_space_vec = VectorFunctionSpace(self.target_mesh, self.dest_element) @@ -484,7 +497,6 @@ def vom(self): "source mesh. To disable this error, set allow_missing_dofs=True.") return source_vom - def _get_symbolic_expressions(self) -> tuple[Interpolate, Interpolate]: """Return the symbolic ``Interpolate`` expressions for point evaluation and re-ordering into the input-ordering VertexOnlyMesh. @@ -494,11 +506,6 @@ def _get_symbolic_expressions(self) -> tuple[Interpolate, Interpolate]: tuple[Interpolate, Interpolate] A tuple containing the point evaluation interpolation and the input-ordering interpolation. - - Raises - ------ - DofNotDefinedError - If any DoFs in the target mesh cannot be defined in the source mesh. """ # Get the correct type of function space shape = self.target_space.ufl_function_space().value_shape From 252500984ceaed45c566125e6fcfdc77af24fd77 Mon Sep 17 00:00:00 2001 From: Leo Collins Date: Wed, 4 Feb 2026 15:21:08 +0000 Subject: [PATCH 3/3] smarter caching --- firedrake/interpolation.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/firedrake/interpolation.py b/firedrake/interpolation.py index fd154b1d86..0b23a4822a 100644 --- a/firedrake/interpolation.py +++ b/firedrake/interpolation.py @@ -7,6 +7,7 @@ from typing import Hashable, Literal, Callable, Iterable from dataclasses import asdict, dataclass from numbers import Number +from weakref import WeakValueDictionary from ufl.algorithms import extract_arguments, replace from ufl.domain import extract_unique_domain @@ -64,6 +65,8 @@ "Interpolator" ) +_vom_cache = WeakValueDictionary() + @dataclass(kw_only=True) class InterpolateOptions: @@ -462,9 +465,39 @@ def __init__(self, expr: Interpolate): # scalar fiat/finat element self.dest_element = dest_element + @staticmethod + def _vom_cache_key(target_space, source_mesh, allow_missing_dofs): + # The VOM used for cross-mesh interpolation depends on only these + return ( + target_space, + source_mesh, + allow_missing_dofs, + ) + @cached_property def vom(self) -> MeshGeometry: - """The VertexOnlyMesh consisting of the target space's dofs immersed in the + """Access the VertexOnlyMesh consisting of the target space's dofs immersed in the + source space's mesh. + + Returns + ------- + MeshGeometry + The VertexOnlyMesh. + """ + key = self._vom_cache_key( + self.target_space, + self.source_mesh, + self.allow_missing_dofs, + ) + try: + return _vom_cache[key] + except KeyError: + vom = self._create_vom() + _vom_cache[key] = vom + return vom + + def _create_vom(self) -> MeshGeometry: + """Create the VertexOnlyMesh consisting of the target space's dofs immersed in the source space's mesh. Returns