-
Notifications
You must be signed in to change notification settings - Fork 166
Clement interpolant #4244
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Clement interpolant #4244
Conversation
firedrake/interpolation.py
Outdated
For arguments, see :class:`.Interpolator`. | ||
""" | ||
|
||
def __new__(cls, expr, V, **kwargs): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this check go to __init__
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason I put it here is that the class inherits from Interpolator
, which defines __new__
to account for the different implementations in SameMeshInterpolator
and CrossMeshInterpolator
. I could overwrite __new__
to just pass
and then put this logic into __init__
if that'd be preferred - just let me know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
__new__
should only be overloaded (and very rarely!) to dispatch to different class constructors. It isn't needed here so I wouldn't define a __new__
at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well I figured it'd make sense for ClementInterpolator
to inherit from Interpolator
, so something needs to be done to account for its implementation there, right? Or did I miss something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you not just leave __new__
unimplemented, and move the check to __init__
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I do that I get
# Projecting into P0 space and then applying Clement interpolation should recover
# the original function
x_P0 = assemble(project(expr, P0))
interpolator = ClementInterpolator(TestFunction(P0), P1)
> x_P1 = interpolator.interpolate(x_P0)
tests/firedrake/regression/test_interpolate.py:604:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
petsc4py/PETSc/Log.pyx:188: in petsc4py.PETSc.Log.EventDecorator.decorator.wrapped_func
???
petsc4py/PETSc/Log.pyx:189: in petsc4py.PETSc.Log.EventDecorator.decorator.wrapped_func
???
firedrake/interpolation.py:418: in interpolate
interp = self._interpolate_future(*function, adjoint=adjoint,
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <firedrake.interpolation.SameMeshInterpolator object at 0x72d277a4b7d0>
transpose = None, adjoint = False, default_missing_val = None
function = (Coefficient(WithGeometry(FunctionSpace(<firedrake.mesh.MeshTopology object at 0x72d277bb3440>, TensorElement(FiniteEl...ape=(3, 3), symmetry={}), name=None), Mesh(VectorElement(FiniteElement('Lagrange', tetrahedron, 1), dim=3), 45)), 69),)
def _interpolate_future(self, *function, transpose=None, adjoint=False, default_missing_val=None):
"""Define the :class:`Interpolate` object corresponding to the interpolation operation of interest.
Parameters
----------
*function: firedrake.function.Function or firedrake.cofunction.Cofunction
If the expression being interpolated contains an argument,
then the function value to interpolate.
transpose : bool
Deprecated, use adjoint instead.
adjoint: bool
Set to true to apply the adjoint of the interpolation
operator.
default_missing_val: bool
For interpolation across meshes: the
optional value to assign to DoFs in the target mesh that are
outside the source mesh. If this is not set then the values are
either (a) unchanged if some ``output`` is specified to the
:meth:`interpolate` method or (b) set to zero. This does not affect
adjoint interpolation. Ignored if interpolating within the same
mesh or onto a :func:`.VertexOnlyMesh`.
Returns
-------
firedrake.interpolation.Interpolate or ufl.action.Action or ufl.adjoint.Adjoint
The symbolic object representing the interpolation operation.
Notes
-----
This method is the default future behaviour of interpolation. In a future release, the
``Interpolator.interpolate`` method will be replaced by this method.
"""
> V = self.V
E AttributeError: 'SameMeshInterpolator' object has no attribute 'V'
firedrake/interpolation.py:336: AttributeError
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh god. I bet that type(ClementInterpolator(TestFunction(P0), P1))
returns SameMeshInterpolator
because of the __new__
magic in Interpolator
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that this level of the code really needs cleaning up, but for now I think you can just have a minimal __new__
:
def __new__(cls, *args, **kwargs):
return object.__new__(ClementInterpolator)
with a comment explaining why it is necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks - done in db99911.
firedrake/interpolation.py
Outdated
if rank != len(self.V.value_shape): | ||
raise ValueError(f"Rank-{rank} input inconsistent with target space.") | ||
mesh = self.V.mesh() | ||
dim = mesh.topological_dimension() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This dim
should be the number of copies of the element V.block_size
. I think a VectorFunctionSpace
and TensorFunctionSpace
could be indexed with a double index (flat_dof_id, component_id)
. So rank
could possibly be unrestricted and there wouldn't have to be special cases if you treat the rank-0 as the general case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in 7ec5197.
|
||
@pytest.mark.parametrize("rank", range(3)) | ||
@pytest.mark.parametrize("dim", range(1, 4)) | ||
def test_clement_interpolator_simplex(dim, rank): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should test a vector with 3 components in a 2D mesh.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion - refactoring for this helped to address some limitations in the implementation. Addressed in 7ec5197.
firedrake/interpolation.py
Outdated
domain = "{[i]: 0 <= i < patch.dofs}" | ||
instructions = "patch[i] = patch[i] + vol[0]" | ||
keys = {"vol": (volume, op2.READ), "patch": (patch_volume, op2.RW)} | ||
parloops.par_loop((domain, instructions), dX, keys) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The patch volume here could become a cached_property so it is not recomputed every time. Maybe we could attach this property to the mesh, so it can be reused more widely across different Interpolators
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. I've addressed this in 34ceb67, using the cell_sizes
cached property as a template.
I added
Is there somewhere else that I should be putting this reference? |
Co-authored-by: Pablo Brubeck <[email protected]>
if subset: | ||
raise NotImplementedError("subset not implemented") | ||
if freeze_expr: | ||
raise NotImplementedError("freeze_expr not implemented") | ||
if access != op2.WRITE: | ||
raise NotImplementedError("access other than op2.WRITE not implemented") | ||
if bcs: | ||
raise NotImplementedError("bcs not implemented") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sort of thing makes me wonder whether this is the best place to implement this functionality. Might it be better to simply have a free function called interpolate_clement
that does the parloop?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As in instead of having the Interpolator
at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. I think we need @dham to weigh in here. Is this the right abstraction?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't look great. Interpolator
objects should be dying out of the public API of Firedrake. They are really just the implementation backend to interpolate
. Having users directly instantiate Interpolator
objects will, for example, break differentiability.
I think the actually correct way to do this is to inherit from AbstractExternalOperator
so that you get all the right symbolic properties for free. The syntax would then be assemble(interpolate_clement(source, target))
. If you haven't yet implemented the operator (i.e. the matrix) or the adjoint then these will raise NotImplementedError
and at least the right interface will be there for the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be nice to have assemble(interpolate(source, target, variant="clement"))
but that might be tricky/unwieldy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we'd have to think harder about whether that is actually the Right Thing. For a finite element space with a known dual basis, there is a single canonical interpolation operation. Other interpolation operations are meaningfully different: they are just other projections that don't directly arise from the definition of the space.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The variant option at least requires heavier surgery because interpolate
is defined in UFL.
|
||
@pytest.mark.parametrize("tdim,shape", [(1, tuple()), (2, tuple()), (3, tuple()), | ||
(1, (1,)), (2, (2,)), (2, (3,)), (3, (3,)), | ||
(1, (1, 1)), (2, (2, 2)), (3, (3, 3))], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(1, (1, 1)), (2, (2, 2)), (3, (3, 3))], | |
(1, (1, 2)), (2, (2, 3)), (3, (2, 3))], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We would have to create expr with the right shape. Maybe something like:
P1 = fs_constructor(mesh, "CG", 1)
x = SpatialCoordinate(mesh)
expr = as_tensor(np.reshape([dot(Constant(tuple(range(i+1, i+1+tdim))), x) for i in range(P1.block_size)], shape))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in cd20a5e and simplified the notation while I was at it.
The most recent test fails don't seem to be related to the changes in this PR. |
from firedrake.functionspace import FunctionSpace | ||
DG0 = FunctionSpace(self, "Discontinuous Lagrange", 0) | ||
volume = Function(DG0) | ||
return volume.interpolate(ufl.CellVolume(self)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this actually mathematically correct for higher order meshes? Shouldn't it be a projection?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As long as CellVolume
is correctly integrated then this should be fine. Recall that CellVolume
is intrinsically a DG0 function on any mesh.
I've been meaning to upstream some functionality from Animate that might be useful for the wider Firedrake community, so am starting with this. Let me know if it's of interest!
This PR adds functionality for the Clément interpolant, which transfers fields from P1 space to P0 space via local volume averaging. We've found this particularly useful for gradient/Hessian recovery in P1 space (can upstream those later).
I've included tests on simplices in 1D, 2D, and 3D, and for scalar-, vector-, and matrix-valued functions.
A limitation of the approach is that the local averaging breaks down on the boundary, but it does a good job in the domain interior. I also have an implementation of local facet averaging on the boundary that I can provide, if it'd be of use.