Skip to content

Conversation

@leo-collins
Copy link
Contributor

@leo-collins leo-collins commented Sep 26, 2025

Simplifies interpolation code and introduces new features. So far:

  • Implemented assembly of cross-mesh interpolation operators, both forward and adjoint.
  • We can now matfree adjoint interpolate cross-mesh and VomOntoVom. The renumbering malarkey is completely removed.
  • Removed interp_data dictionary in favour of the InterpolateOptions dataclass. We get type hinting, better IDE support, single source of truth for these options.
  • Interpolator becomes an internal object. I removed the __new__ method and dispatch instead with a get_interpolator function.
  • Removed frozen_interpolator logic

@leo-collins leo-collins changed the title Leo/refactor interpolate refactor interpolation Sep 26, 2025
@connorjward
Copy link
Contributor

@leo-collins this looks like it includes changes from the other ongoing interpolation PRs. This will be much easier to review if you point this PR at that branch instead of main. Then we are only seeing the new changes.

@leo-collins leo-collins force-pushed the leo/refactor_interpolate branch from deb0a8c to 4097207 Compare September 26, 2025 13:19
@pbrubeck pbrubeck changed the base branch from main to pbrubeck/interp-adjoint-explicit September 26, 2025 13:33
@pbrubeck
Copy link
Contributor

  • Interpolator becomes an internal object. I removed the __new__ method and dispatch instead with a _get_interpolator function.

This is fine, but we might want to preserve a user-facing interface for reusable interpolators.

@pbrubeck pbrubeck force-pushed the pbrubeck/interp-adjoint-explicit branch 7 times, most recently from 210bae0 to 8d631f8 Compare September 30, 2025 21:56
Parameters
----------
expr : ufl.Interpolate | ufl.ZeroBaseForm
The symbolic interpolation expression, or a zero form. Zero forms
Copy link
Contributor

Choose a reason for hiding this comment

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

"zero form" is too close to 0-form, which is not necesarily equal to 0.

Suggested change
The symbolic interpolation expression, or a zero form. Zero forms
The symbolic interpolation expression, or a ZeroBaseForm. ZeroBaseForms


# NOTE: The par_loop is always over the target mesh cells.
target_mesh = as_domain(V)
target_mesh = V.mesh()
Copy link
Contributor

Choose a reason for hiding this comment

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

Now we might want to reconsider leaving as_domain here...

Copy link
Contributor

Choose a reason for hiding this comment

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

Are we sure this has been properly resolved?

Copy link
Contributor

Choose a reason for hiding this comment

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

It'll be useful to have some assertions here

if access == op2.INC:
copyin += (tensor.zero,)
Copy link
Contributor

Choose a reason for hiding this comment

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

For the 1-form adjoint on a mixed space, we might be adding different sub-interpolates onto the same tensor. The zeroing should be handled outside the for loop of line 633

Copy link
Contributor Author

Choose a reason for hiding this comment

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

tensor is already the indexed sub-tensor in this case. Can you write a test for this?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'll do that

@leo-collins leo-collins force-pushed the leo/refactor_interpolate branch from 89550eb to 9fd2344 Compare November 10, 2025 14:21
@leo-collins leo-collins marked this pull request as ready for review November 10, 2025 14:29
Copy link
Contributor

@connorjward connorjward left a comment

Choose a reason for hiding this comment

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

I haven't reviewed this in great detail but this seems really good!

"""
from firedrake.assemble import assemble
# If assembling the operator, we need the concrete permutation matrix
matfree = False if self.rank == 2 else self.matfree
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems bizarre. A user could pass a matfree option that gets ignored.

Copy link
Contributor Author

@leo-collins leo-collins Nov 13, 2025

Choose a reason for hiding this comment

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

We can't assemble the cross-mesh interpolation matrix if matfree=True. By doing this the user can do assemble(interpolate(TrialFunction(V), U)) and get the operator without having to pass an additional matfree=False.

Copy link
Contributor

Choose a reason for hiding this comment

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

In which case I think we should change (and document) the interface to

def interpolate(expr, ..., *, matfree=None):
  if matfree is None:
    if some_condition:
      matfree = False
    else:
      matfree = True

because currently the user could run

assemble(interpolate(TrialFunction(V), U, matfree=True))

and the option would be silently ignored.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The same could be said about the allow_missing_dofs and default_missing_val parameters if the user is doing same-mesh interpolation. I think it's fine if the docstring says something like "this parameter only applies if interpolating between a VertexOnlyMesh and its input_ordering".

Copy link
Contributor

Choose a reason for hiding this comment

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

Well the right answer there is to disallow those options for same-mesh interpolation. I don't want to force you to make big changes here though so I'd be happy provided that this behaviour is made clear in the docstring and you open an issue about it.

for indices, form in firedrake.formmanipulation.split_form(expr):
if isinstance(form, ufl.ZeroBaseForm):
# Get sub-interpolators and sub-bcs for each block
Isub: dict[tuple[int, int], tuple[Interpolator, list[DirichletBC]]] = {}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a great idea. Big fan.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not a fan. Why can we not have the BCs passed on to the Interpolator?

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess that's just a matter of taste. We don't generate code for the boundary conditions so stashing things on the Interpolator is less important.

I was specifically praising the use of type hints to make an extremely complex data structure clear.

Copy link
Contributor

Choose a reason for hiding this comment

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

We can't change the BCs on matrices on repeated assembly calls, or can we?

Copy link
Contributor

Choose a reason for hiding this comment

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

Also the tuple[int, int] could in some cases just be tuple[int]

Copy link
Contributor

Choose a reason for hiding this comment

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

We can't change the BCs on matrices on repeated assembly calls, or can we?

Good point. I'm not sure. We switch the lgmaps out at runtime so feasibly it's OK to do.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My thought was that BCs should be passed to assemble, like we do for all other forms

Copy link
Contributor

@connorjward connorjward left a comment

Choose a reason for hiding this comment

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

.

Copy link
Member

@dham dham left a comment

Choose a reason for hiding this comment

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

subject to checking that adjoint interpolation on mixed spaces is really correct, this is great. thanks.

sub_tensor.assign(sum(self[i]._interpolate(*function, adjoint=adjoint, **kwargs)
for i in self if i[0] == k))
return output
Isub[indices] = (get_interpolator(form), sub_bcs)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we not do this instead?

Suggested change
Isub[indices] = (get_interpolator(form), sub_bcs)
Isub[indices] = get_interpolator(form, bcs=sub_bcs)

Copy link
Contributor

Choose a reason for hiding this comment

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

Or if that is not your intended design, I suggest we enable something like this

Suggested change
Isub[indices] = (get_interpolator(form), sub_bcs)
Isub[indices] = get_assembler(form, bcs=sub_bcs)

Copy link
Contributor

@pbrubeck pbrubeck Nov 18, 2025

Choose a reason for hiding this comment

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

A third option is to do something like

Suggested change
Isub[indices] = (get_interpolator(form), sub_bcs)
Isub[indices] = partial(get_interpolator(form).assemble, bcs=sub_bcs)


operand, = expr.ufl_operands
target_mesh = expr.target_space.mesh()
source_mesh = extract_unique_domain(operand) or target_mesh
Copy link
Contributor

Choose a reason for hiding this comment

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

We should also consider the possibility of the operand having a MeshSequence. Right now extract_unique_domain will raise an Exception, but MixedMeshInterpolator should be able to handle the case where operand is an Argument of a MixedFunctionSpace. The case where operand is a more general Expr on multiple meshes might involve more complicated code generation.

This should be done in a separate PR.

coarse_space_bcs = tuple(coarse_space_bcs)
if G_callback is None:
interp_petscmat = chop(Interpolator(dminus(trial), V, bcs=bcs + coarse_space_bcs).callable().handle)
interp_petscmat = chop(assemble(interpolate(dminus(trial), V), bcs=bcs + coarse_space_bcs).mat())
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not assemble(...).petscmat?

self.target_space = dual_arg.function_space().dual()
"""The primal space we are interpolating into."""
self.target_mesh = self.target_space.mesh()
self.target_mesh = self.target_space.mesh().unique()
Copy link
Contributor

Choose a reason for hiding this comment

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

I think MixedInterpolator will be fine with MeshSequence, so this call should go in the subclasses

dual_arg, operand = expr.argument_slots()
V = dual_arg.arguments()[0].function_space()

V = dual_arg.function_space().dual()
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
V = dual_arg.function_space().dual()
assert isinstance(dual_arg, Cofunction | Coargument)
V = dual_arg.function_space().dual()

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.

5 participants