Skip to content

Tidy up interpolation tsfc<->firedrake interface #233

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 3 commits into from
Nov 24, 2020

Conversation

ReubenHill
Copy link
Contributor

@ReubenHill ReubenHill commented Oct 9, 2020

See issue #232. Coupled with firedrakeproject/firedrake#1861. Please feel free to bikeshed this whilst it's WIP!

@ReubenHill
Copy link
Contributor Author

So my broad impression is that we want to make ExpressionKernelBuilder more like KernelBuilder which seems to require much less firedrake specific data. Is that a fair interpretation? Any suggestions on how best to proceed @wence- ? I feel that what I've done so far kind of side steps the issue with inputting coordinates. They are still there just hidden within the domain.

@wence-
Copy link
Contributor

wence- commented Oct 9, 2020

So my broad impression is that we want to make ExpressionKernelBuilder more like KernelBuilder which seems to require much less firedrake specific data. Is that a fair interpretation? Any suggestions on how best to proceed @wence- ? I feel that what I've done so far kind of side steps the issue with inputting coordinates. They are still there just hidden within the domain.

Suppose I give you just the generated kernel (the loopy thing). And I say "this kernel matches up with interpolating this expression". And then the only thing I permit you to ask of the expression is "what are your coefficients" and "what is your domain". What information do I need to construct the parloop that executes the kernel?

@ReubenHill
Copy link
Contributor Author

I would need to match the domain to a PyOP2 iteration set, match the coefficients to the par loop arguments (which we ought to do with something like coefficient_numbers that you get when you make a kernel from compiling a form), the access descriptors for the coefficients, and any special information about how the iterset should be traversed (I presume this is what oriented and needs_cell_sizes are for?)

Sound right or wrong?

Also this seems to be an answer to a different question - I am, as far as I'm aware, not in a situation where I have such a kernel - rather I'm trying to work out how you build one. The answer to your question seems one step ahead.

@wence-
Copy link
Contributor

wence- commented Oct 9, 2020

OK, I see. Basically, inside TSFC, if you want to disentangle the symbolics from the numerics you're only allowed to called UFL methods on the objects. IOW, you want to get to a situation where if you just had a fully symbolic expression, compile_expression_dual_evaluation would still work.

from ufl import *
from tsfc.finatinterface import create_element
V = VectorElement("P", triangle, 1)
mesh = Mesh(V)

V = FunctionSpace(mesh, FiniteElement("P", triangle, 2))
x, y = SpatialCoordinate(mesh)

expr = x*y - y**2 + x
element = create_element(V.ufl_element())
stuff = compile_expression_dual_evaluation(expr, element, coffee=False)

@ReubenHill
Copy link
Contributor Author

OK, I see. Basically, inside TSFC, if you want to disentangle the symbolics from the numerics you're only allowed to called UFL methods on the objects. IOW, you want to get to a situation where if you just had a fully symbolic expression, compile_expression_dual_evaluation would still work.

from ufl import *
from tsfc.finatinterface import create_element
V = VectorElement("P", triangle, 1)
mesh = Mesh(V)

V = FunctionSpace(mesh, FiniteElement("P", triangle, 2))
x, y = SpatialCoordinate(mesh)

expr = x*y - y**2 + x
element = create_element(V.ufl_element())
stuff = compile_expression_dual_evaluation(expr, element, coffee=False)

This example should fail I believe - a function of spatial coordinates surely requires the coordinates data.

@wence-
Copy link
Contributor

wence- commented Oct 20, 2020

This example should fail I believe - a function of spatial coordinates surely requires the coordinates data.

Only when executing, not when compiling.

@ReubenHill
Copy link
Contributor Author

ReubenHill commented Oct 20, 2020

This example should fail I believe - a function of spatial coordinates surely requires the coordinates data.

Only when executing, not when compiling.

So I can execute this

import ufl
from tsfc.finatinterface import create_element
from tsfc import compile_expression_dual_evaluation


def test_ufl_only_dependence():
    V = ufl.VectorElement("P", ufl.triangle, 1)
    mesh = ufl.Mesh(V)
    V = ufl.FunctionSpace(mesh, ufl.FiniteElement("P", ufl.triangle, 2))
    v = ufl.Coefficient(V)
    expr = ufl.inner(v, v)
    element = create_element(V.ufl_element())
    ast, oriented, needs_cell_sizes, coefficients, _ = compile_expression_dual_evaluation(expr, element, coffee=False)

and the ast has no code property. Am I aiming for similar behaviour with an expression of spatial coordinates?

@wence-
Copy link
Contributor

wence- commented Oct 20, 2020

and the ast has no code property. Am I aiming for similar behaviour with an expression of spatial coordinates?

I think we are talking at cross-purposes.

I claim that for generation of an interpolation kernel (which is what this is doing), I should need no global (Firedrake-level) information. But rather I should only need symbolic information (what the elements are, what the expression is, etc...).

I proposed one way of checking if this is the case right now is to feed purely symbolic objects (from UFL) into compile_expression_dual_evaluation. The kernel code that comes out should be identical to if I had fed in Firedrake objects.

@ReubenHill
Copy link
Contributor Author

and the ast has no code property. Am I aiming for similar behaviour with an expression of spatial coordinates?

I think we are talking at cross-purposes.

I claim that for generation of an interpolation kernel (which is what this is doing), I should need no global (Firedrake-level) information. But rather I should only need symbolic information (what the elements are, what the expression is, etc...).

I proposed one way of checking if this is the case right now is to feed purely symbolic objects (from UFL) into compile_expression_dual_evaluation. The kernel code that comes out should be identical to if I had fed in Firedrake objects.

Okay - in a discussion with @dham he conjectured that if my expression is one of spatial coordinates or non-trivial pullback, I should need global domain information from the domain.coordinates coefficient (or we misunderstood each other). Why ought this not to be the case? Do the FInAT elements contain enough information already?

Put another way, how do I deal with has_type(expression, GeometricQuantity) == true to get past these lines:

    # Collect required coefficients
    coefficients = extract_coefficients(expression)
    if has_type(expression, GeometricQuantity) or any(fem.needs_coordinate_mapping(c.ufl_element()) for c in coefficients):
        coefficients = [coordinates] + coefficients
    builder.set_coefficients(coefficients)

(permalink)?

Does a correctly refactored ExpressionKernelBuilder not actually require the coordinates coefficient?

@wence-
Copy link
Contributor

wence- commented Oct 20, 2020

Does a correctly refactored ExpressionKernelBuilder not actually require the coordinates coefficient?

It needs a symbolic equivalent of that coefficient. A Coefficient(coordinates.ufl_function_space()) should work fine.

You shouldn't global domain information (numbers) when you're compiling a kernel. You only need the symbolic information that tells you how numbers are to be interpreted.

builder.set_coordinates(mesh)

What does that function do?

def set_coordinates(self, domain):
"""Prepare the coordinate field.
:arg domain: :class:`ufl.Domain`
"""
# Create a fake coordinate coefficient for a domain.
f = Coefficient(FunctionSpace(domain, domain.ufl_coordinate_element()))
self.domain_coordinate[domain] = f
self.coordinates_arg = self._coefficient(f, "coords")

So it makes a symbolic representation of the coordinate field from the mesh's coordinate element. No Firedrake object required.

@wence-
Copy link
Contributor

wence- commented Oct 21, 2020

Sorry, edited my comments for tetchiness.

First step towards dealing with #232.

Use fake coordinates coefficient when required instead of data carrying
.coordinates firedrake Function.
Tests for non-dependence of compile_expression_dual_evaluation on
numerics by checking it works with purely symbolic expressions.
@ReubenHill ReubenHill force-pushed the ReubenHill/tsfc-interp-tidy branch from 728e728 to 649a451 Compare October 21, 2020 14:49
@ReubenHill
Copy link
Contributor Author

Okay so it looks like I've got everything compiling using UFL only. Now I need to work out how to get all the necessary coefficient processing information (which I need, do I need to split coefficients) out of compile_expression_dual_evaluation. I'm wary of doing much more until #234 has been reviewed since so much of what I will do will copy KernelBuilder.

So that caller can replace if necessary
@ReubenHill ReubenHill force-pushed the ReubenHill/tsfc-interp-tidy branch from 44f19bc to ed89971 Compare November 18, 2020 13:07
@ReubenHill ReubenHill requested a review from wence- November 19, 2020 14:32
@ReubenHill
Copy link
Contributor Author

This now works - I wonder if there's a better way of dealing with the coordinates coefficient but perhaps this is good enough for now?

@ReubenHill ReubenHill marked this pull request as ready for review November 23, 2020 17:11
@ReubenHill ReubenHill requested a review from ksagiyam November 24, 2020 09:38
@@ -267,7 +267,7 @@ def name_multiindex(multiindex, name):
return builder.construct_kernel(kernel_name, impero_c, index_names, quad_rule)


def compile_expression_dual_evaluation(expression, to_element, coordinates, *,
def compile_expression_dual_evaluation(expression, to_element, *,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

to_element should eventually become a UFL cofunction whenever they are implemented

@dham dham merged commit c220702 into master Nov 24, 2020
@dham dham deleted the ReubenHill/tsfc-interp-tidy branch November 24, 2020 11:52
"""Constructs an :class:`ExpressionKernel`.

:arg return_arg: loopy.GlobalArg for the return value
:arg impero_c: gem.ImperoC object that represents the kernel
:arg index_names: pre-assigned index names
:arg first_coefficient_fake_coords: If true, the kernel's first
coefficient is a constructed UFL coordinate field
Copy link
Contributor

Choose a reason for hiding this comment

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

TBH, this is a horrible variable name. I think what this means is that the kernel expects to be fed:

[mesh.coordinates] + extract_coefficients(expression)

Is that right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Didn't see this comment in time. Yes that is correct

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you have a suggested better interface and/or variable name to say and I can make a new PR

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 in various places in Firedrake we have similar variables to this e.g. for cell sizes. They are called something like needs_cell_sizes, maybe you just want to rename to needs_coords.

builder.domain_coordinate[domain] = coords_coefficient
builder.set_cell_sizes(domain)
coefficients = [coords_coefficient] + coefficients
first_coefficient_fake_coords = True
builder.set_coefficients(coefficients)

Copy link
Contributor

Choose a reason for hiding this comment

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

The way coordinate is taken care of in the above is identical to how we do in KernelBuilder.set_coordinate, so maybe one potential future change is to make set_coordinate method also available in ExpressionKernelBuilder.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I strongly considered doing this, but didn't want to make a "fake" version of that interface without recreating it more fully. Hopefully once your changes to that interface have landed this can be given another spin.

@@ -173,7 +175,8 @@ def construct_kernel(self, return_arg, impero_c, index_names):
loopy_kernel = generate_loopy(impero_c, args, self.scalar_type,
"expression_kernel", index_names)
return ExpressionKernel(loopy_kernel, self.oriented, self.cell_sizes,
self.coefficients, self.tabulations)
self.coefficients, first_coefficient_fake_coords,
Copy link
Contributor

Choose a reason for hiding this comment

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

Another thing I think we should think about next time is how to make ExpressionKernelBuilder more symmetric to KernelBuilder. I think instead of returning coefficients here, we should just call extract_coefficients(expr) again in Firedrake (this compares to form.coefficients() called in assemble.py) and split Functions if necessary when setting up ParLoop arguments (again, as is done in assemble.py).

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants