Skip to content
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

dsl: Implementation of new interpolation methods #1932

Closed
wants to merge 25 commits into from

Conversation

guaacoelho
Copy link

@guaacoelho guaacoelho commented Jun 8, 2022

Hi Guys @FabioLuporini @mloubout @navjotk @georgebisbas ,

I've been working with @nogueirapeterson to include new interpolation techniques on Devito. We implement the cubic and sinc interpolation methods. Thus, we create two new classes: CubicInterpolator and SincInterpolator, which contains, respectively, the cubic and sinc interpolation methods.

The cubic interpolator was implemented using the cubic convolution technique (10.1109/TASSP.1981.1163711) and the sinc interpolator was implemented using Kaiser windowed sinc functions (https://doi.org/10.1190/1.1451454).

We hope you appreciate this contribution and help us to improve this implementation with your great review!

@guaacoelho guaacoelho changed the title Interpolation methods dsl: Implementation of new interpolation methods Jun 8, 2022
@mloubout mloubout added the API api (symbolics, types, ...) label Jun 8, 2022
Copy link
Contributor

@mloubout mloubout left a comment

Choose a reason for hiding this comment

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

Great! Thanks a lot. I'll have a thorough walk-through.

So SincInterpolation is not testable, right. It would be good to make it testable in some way. Also, there is a hardcoded path to lookuptable.h that will break the rest of the code.

Copy link
Contributor

@mloubout mloubout left a comment

Choose a reason for hiding this comment

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

Ok made a first pass. I think it's quite nice and provides some pretty cool features. I left some comments and I'm happy to discuss if not quite clear.

mathematically equivalent expanded form `coord/spacing -
origin/spacing`. This particular form is problematic when a sparse
point is in close proximity of the grid origin, since due to a larger
machine precision error it may cause a +-1 error in the computation of
Copy link
Contributor

Choose a reason for hiding this comment

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

That +-1 used to be an error and we made changes to the API to make sure it would not happen anymore. Can you point us to a small MFE with the breaking change.

Copy link
Author

Choose a reason for hiding this comment

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

Actually this was my mistake, I took the same comments from position_map function and forgot to update after the rebase. So there is no error in here

return tuple(product(range(-1, 3), repeat=self.grid.dim))
elif self.sinc:
r = self.interpolator.r
return tuple(product(range(-r + 1, r+1), repeat=self.grid.dim))
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this case true for all r? I.e for cubic you have r=2 which is range(-1, 3) and for the linear case r=1 whci his range(0, 2)

Copy link
Author

Choose a reason for hiding this comment

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

You're right! range(-r + 1, r+1) works for all r.

I'll change this

Copy link
Member

Choose a reason for hiding this comment

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

So in the end we won't have this conditional, I hope?

Copy link
Author

Choose a reason for hiding this comment

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

I updated the PR with this fix.

Function, TimeFunction)


def unit_box(name='a', shape=(11, 11), grid=None):
Copy link
Contributor

Choose a reason for hiding this comment

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

Redefinition of existing fixture.
same below

((101, 101), [(3.4, 95.), (3., 92)]),
((101, 101, 101), [(3.4, 95.), (3., 92), (10., 10.)])
])
def test_interpolate(shape, coords, npoints=20):
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this all be gathered under a common test class?

Also I don't think it needs a new file, test_interpolation.py exists for this

# print(p.coordinates.data)
op(a=a)

# Precisão 3D nao bate em 10-6 mas sim em 10-5
Copy link
Contributor

Choose a reason for hiding this comment

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

There is leftover Portugues throughout


idx_subs = []
for i, idx in enumerate(index_matrix):
# Introduce ConditionalDimension so that we don't go OOB
Copy link
Contributor

Choose a reason for hiding this comment

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

This should only be necessary if r > variables.space_order

Copy link
Author

Choose a reason for hiding this comment

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

So if r <= variables.space_order there is no need to use ConditionalDimension in this case?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes

Copy link
Author

Choose a reason for hiding this comment

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

I adjust this part of the code and updated here.

You'll see that I added a conditionalif hasattr(x, 'space_order') else False. I needed to do this because in some cases variables does not have the space_order attribute

lhs = self.sfunction.subs(self_subs)
last = [Inc(lhs, rhs)] if increment else [Eq(lhs, rhs)]

# Creates the symbolic equation that calls the populate function
Copy link
Contributor

Choose a reason for hiding this comment

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

What's this?

Copy link
Author

Choose a reason for hiding this comment

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

To implement the sinc interpolation I used a look up table that holds the values of the coefficients. Before it could be accessed, it was necessary to initialize this table with the values. This symbolic Function populate() creates a call to the function responsible for initialize the look up table.

This function is defined inside look up table.h, that I apparently forgot to add here. I'm attaching the lookuptable.h in this comment, this way you can see how it works

lookuptable.zip

dim_pos = self.sfunction.grid.dimensions

# Checks if the data is 3D or 2D
if len(dim_pos) == 3:
Copy link
Contributor

Choose a reason for hiding this comment

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

this can be setup at __init_ with self._eqs = self.bicubic if self.sfunction.grid.dim = 2 else ...

Copy link
Member

Choose a reason for hiding this comment

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

...also lets try to setup as much at init as possible?

idx_subs,
dim_pos=dim_pos)]
else:
eqs = [np.sum(v) for v in self._bicubic_equations(_expr,
Copy link
Contributor

Choose a reason for hiding this comment

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

why doesn't ..._equations directly return the sum?
Also why np.sum? Isn't it still partially symbolic?

Copy link
Author

Choose a reason for hiding this comment

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

Not returning directly the sum, the return of ..._equations can be used for both injection and interpolation. In interpolation it sum the values and in injection just assign the values for its respective grid point.

About the use of numpy: Yeah, it is still a symbolic. I wanted to generate a symbolic equation that sum all the individual values, that's why i used np.sum. Is there some problem in using it?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok makes sense for sum. But should not use numpy as it is meant for numerical values. Should use Devito.Add(*v)

Copy link
Member

Choose a reason for hiding this comment

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

Does this actually work? Numpy summing over sympy symbols?

Copy link
Author

Choose a reason for hiding this comment

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

Yes, it works summing over sympy symbols. But I will take a look at Devito.Add(*v) to use it

Copy link
Author

Choose a reason for hiding this comment

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

I updated the code and it is using Add(*v) now

field_offset=field_offset)

rhs = Symbol(name='sum', dtype=self.sfunction.dtype)
summands = [Eq(rhs, 0., implicit_dims=self.sfunction.dimensions)]
Copy link
Contributor

Choose a reason for hiding this comment

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

everything till here is the same as in LinearInterpolation right? and after the bicubic_equation too. So may be nice to gather those under a PolynomialInterpolation with order=1 or order=3 that specialize on the equations only since it's the only thing that changes

Copy link
Author

Choose a reason for hiding this comment

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

I added a PolynomialInterpolator class that works as a parent class. It's not exactly what you just said, but I think it solves the code reuse problem

class CubicInterpolator(GenericInterpolator):
"""
Concrete implementation of GenericInterpolator implementing a cubic interpolation
scheme.
Copy link
Contributor

Choose a reason for hiding this comment

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

blank line

scheme.
Parameters
----------
sfunction: The SparseFunction that this Interpolator operates on.
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 not legal numpydoc format

Copy link
Contributor

Choose a reason for hiding this comment

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


return eqs

def _interpolation_indices(self, variables, offset=0, field_offset=0):
Copy link
Contributor

Choose a reason for hiding this comment

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

isn't there anything we could factorize in a common superclass?

Copy link
Author

Choose a reason for hiding this comment

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

isn't there anything we could factorize in a common superclass?

Yes, I think it has. I'm working on it

Copy link
Author

Choose a reason for hiding this comment

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

I updated the code by adding the superclass you suggested


def __init__(self, sfunction):
self.sfunction = sfunction
self.r = 4
Copy link
Contributor

Choose a reason for hiding this comment

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

This should probably be a class property

Copy link
Member

Choose a reason for hiding this comment

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

Is it a problem to let the user choose?

Copy link
Author

Choose a reason for hiding this comment

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

Is it a problem to let the user choose?

The Bessel function was implemented inside the .h file considering r = 4. So it would be a problem to allow the user to choose the value of r

Copy link
Contributor

@FabioLuporini FabioLuporini left a comment

Choose a reason for hiding this comment

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

Thank you for this contribution. Looking great! I just left a bunch of comments

@@ -0,0 +1,269 @@
import numpy as np
Copy link
Contributor

Choose a reason for hiding this comment

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

IMHO this file should be squashed with test_interpolation, and as much code and existing tests as possible should be reused, through pytest.parametrize for example

((101, 101, 101), [(3.4, 95.), (3., 92), (10., 10.)])
])
def test_interpolate(shape, coords, npoints=20):
"""Test generic point interpolation testing the x-coordinate of an
Copy link
Contributor

Choose a reason for hiding this comment

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

Opening triple quotes want to be on line above for consistency with the rest of the codebase, i.e.

"""
Test generic...
"""

----------
expr : expr-like
Input expression to interpolate.
position: List
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be list (all lowercase)

for ii in range(4)]
return eqs

def _tricubic_equations(self, expr, idx_subs, dim_pos=None):
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this (and _bicubic_equations) be combined into some _ncubic_equations for N-dimensional interpolation?

Copy link
Author

Choose a reason for hiding this comment

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

Yes, it could. I changed the code and made this change you suggested

idx_subs: dict
Structure responsible for mapping the order of substitution of dimensions
in expr.
idx_2d: Integer
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be int I think?


return eqs

def _sinc_equations3D(self, expr, idx_subs, dim_pos=None):
Copy link
Contributor

Choose a reason for hiding this comment

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

Again, can these be consolidated into _sinc_equationsND()? Seems like a decent amount of lines are the same and would be nice to have a generalization to N-D

@@ -0,0 +1,269 @@
import numpy as np
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be worth having a test to check the convergence of the cubic interpolator? Not sure what convergence would be expected for the sinc one

Copy link
Contributor

Choose a reason for hiding this comment

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

Does this seem like a decent idea @mloubout ?

Copy link
Contributor

Choose a reason for hiding this comment

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

It isn't as I pointed out above

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe I'm just being stupid and missing it, but where?

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry completely missed what you were talking about only saw the numpy.

Convergence may be tricky, it's usually easier to just do an analytical check where you set the field to polynomial values and check the interpolation is exact.

Copy link
Member

@navjotk navjotk left a comment

Choose a reason for hiding this comment

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

Left some minor comments.

Also have two higher-level questions:

  1. Is SincInterpolator the best name for what this is? (Sinc is only a part of the interpolation function)
  2. How does this approach compare to using the machinery provided in PrecomputedInterpolator, i.e. the Sinc class could inherit from that, precompute the coefficients (which I think you're doing in some way anyway), and hand them off to PrecomputedInterpolator for the rest?

option = 1, ponto imediatamente vizinho ao ponto de intepolação.
Onde 0 < |arg| < 1
option = 2, pontos extremos da vizinhança.
Onde 1 < |arg| < 2
Copy link
Member

Choose a reason for hiding this comment

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

Am I seeing an old version? We probably want our documentation in English :)

dim_pos = self.sfunction.grid.dimensions

# Checks if the data is 3D or 2D
if len(dim_pos) == 3:
Copy link
Member

Choose a reason for hiding this comment

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

...also lets try to setup as much at init as possible?

idx_subs,
dim_pos=dim_pos)]
else:
eqs = [np.sum(v) for v in self._bicubic_equations(_expr,
Copy link
Member

Choose a reason for hiding this comment

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

Does this actually work? Numpy summing over sympy symbols?

eqs = [v for v in self._tricubic_equations(_expr,
idx_subs,
dim_pos=dim_pos)]
else:
Copy link
Member

Choose a reason for hiding this comment

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

Can we do this with inheritance? Or in a way that generalises beyond 2/3 dimensions?


def __init__(self, sfunction):
self.sfunction = sfunction
self.r = 4
Copy link
Member

Choose a reason for hiding this comment

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

Is it a problem to let the user choose?

@@ -133,7 +134,8 @@ class Operator(Callable):
"""

_default_headers = [('_POSIX_C_SOURCE', '200809L')]
_default_includes = ['stdlib.h', 'math.h', 'sys/time.h']
_default_includes = ['stdlib.h', 'math.h', 'sys/time.h',
f'{os.path.expanduser("~")}/lookuptable.h']
Copy link
Member

Choose a reason for hiding this comment

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

I need help understanding what this header file is doing. Also, is this a static header file that should become a standard part of Devito? Alternately, can this be generated using the JIT infra?

Copy link
Author

Choose a reason for hiding this comment

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

To implement the sinc interpolation I used a look up table that holds the values of the coefficients. So this header contains the look up table and the methods responsible for initialize and get the values from it.

Access to these functions is defined from the definition of symbolic functions during the generation of symbolic equations.

I updated the PR adding the lookuptable.h file, this way you can take a look if you want to.

Copy link
Author

Choose a reason for hiding this comment

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

About using JIT to generate the code, I don't know how it could be using JIT but I will take a look.

And yes, the static header file should become a standard part of Devito.

return tuple(product(range(-1, 3), repeat=self.grid.dim))
elif self.sinc:
r = self.interpolator.r
return tuple(product(range(-r + 1, r+1), repeat=self.grid.dim))
Copy link
Member

Choose a reason for hiding this comment

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

So in the end we won't have this conditional, I hope?

@mloubout
Copy link
Contributor

The .h file shouldn't really be necessary. Bessel function are defined in math.h so should be able to make a symbolic expression directly calling it rather than the lookup table

@navjotk
Copy link
Member

navjotk commented Jun 14, 2022

Also, I want to understand if this is significantly different from using PrecomputedInterpolator (i.e. this class could extend and use the functionality there)

@guaacoelho
Copy link
Author

The .h file shouldn't really be necessary. Bessel function are defined in math.h so should be able to make a symbolic expression directly calling it rather than the lookup table

@mloubout Before the decision of create this '.h' file it was made a search for some library that could already has the implementation of the Bessel function that we needed (the zero-order modified Bessel function of the first kind).

I found some implementations of the bessel functions, but none of the zero-order modified Bessel function of the first kind.

@guaacoelho
Copy link
Author

guaacoelho commented Jun 14, 2022

Also, I want to understand if this is significantly different from using PrecomputedInterpolator (i.e. this class could extend and use the functionality there)

@navjotk At the same way that linear interpolation, both cubic and sinc interpolation also can be done by using PrecomputedInterpolator, if the right coefficients are used. So I can say that the main difference is that this new classes removes the responsibility of obtaining the interpolation coefficients from the user.

@navjotk
Copy link
Member

navjotk commented Jun 23, 2022

@guaacoelho But LinearInterpolator does not do any pre computation. If this class is doing pre computation of coefficients, it could store them in the properties provided by PrecomputedInterpolator and use the rest of the infrastructure there. i.e. This class precomputes the coefficients and hands them off to PrecomputedInterpolator, not the user.

Copy link
Contributor

@georgebisbas georgebisbas left a comment

Choose a reason for hiding this comment

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

Thanks for this contribution!

@@ -1,5 +1,6 @@
from abc import ABC, abstractmethod

# Polly interpolator é a que contem as duas interpolações
Copy link
Contributor

Choose a reason for hiding this comment

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

Translate and avoid comment in this area


def interpolate(self, expr, offset=0, increment=False, self_subs={}):
"""
Generate equations interpolating an arbitrary expression into ``self``.

Copy link
Contributor

Choose a reason for hiding this comment

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

add line back

variables = list(retrieve_function_carriers(_expr))

# Need to get origin of the field in case it is staggered
# TODO: handle each variable staggereing spearately
Copy link
Contributor

Choose a reason for hiding this comment

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

typo

@@ -133,7 +134,8 @@ class Operator(Callable):
"""

_default_headers = [('_POSIX_C_SOURCE', '200809L')]
_default_includes = ['stdlib.h', 'math.h', 'sys/time.h']
_default_includes = ['stdlib.h', 'math.h', 'sys/time.h',
f'{os.path.expanduser("~/devito")}/lookuptable.h']
Copy link
Contributor

Choose a reason for hiding this comment

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

Ι think this .h is not something we encourage

@mloubout
Copy link
Contributor

mloubout commented Aug 3, 2023

FYI sparse interpolation is beeing revamp in #2128 which should help updae this PR.

@mloubout
Copy link
Contributor

sinc interpolation added in #2342 , closing.

Feel free to open a new PR for cubic with the new API.

@mloubout mloubout closed this Apr 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
API api (symbolics, types, ...)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants