Skip to content

Commit b2ff9a6

Browse files
authored
Merge pull request #23 from EconForge/albop/interp
Albop/interp: merging as is.
2 parents ee206fa + fcc8507 commit b2ff9a6

13 files changed

+1277
-17
lines changed

.travis.yml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
language: python
22

33
python:
4-
- "2.7"
5-
- "3.5"
4+
- "3.6"
65

76
sudo: false
87

@@ -11,11 +10,7 @@ install:
1110
# conda line below will keep everything up-to-date. We do this
1211
# conditionally because it saves us some downloading if the version is
1312
# the same.
14-
- if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then
15-
wget http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh;
16-
else
17-
wget http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh;
18-
fi
13+
- wget http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh;
1914
- bash miniconda.sh -b -p $HOME/miniconda
2015
- export PATH="$HOME/miniconda/bin:$PATH"
2116
- hash -r

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,28 @@ In the near future:
4949
- JIT classes for all interpolation objects
5050

5151

52+
## jitted, non-uniform multilinear interpolation
53+
54+
There is a simple `interp` function with a flexible API which does multinear on uniform or non uniform cartesian grids.
55+
56+
```
57+
### 1d grid
58+
from interpolation import interp
59+
60+
x = np.linspace(0,1,100)**2 # non-uniform points
61+
y = np.linspace(0,1,100) # values
62+
63+
# interpolate at one point:
64+
interp(x,y,0.5)
65+
66+
# or at many points:
67+
u = np.linspace(0,1,1000) # points
68+
interp(x,y,u)
69+
```
70+
71+
72+
73+
5274

5375

5476
## smolyak interpolation

examples/example_mlinterp.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import numpy as np
2+
3+
from numba import generated_jit
4+
import ast
5+
6+
C = ((0.1,0.2),(0.1,0.2))
7+
l = (0.1,0.5)
8+
9+
from interpolation.multilinear.fungen import extract_row, tensor_reduction
10+
11+
tensor_reduction(C,l)
12+
13+
ll = np.row_stack([1-np.array(l),l])
14+
ll
15+
np.einsum('ij,i,j', np.array(C), ll[0,:], ll[1,:])
16+
17+
A = np.random.random((5,5))
18+
extract_row(A, 1, (2,2))
19+
20+
from interpolation.multilinear.fungen import get_coeffs
21+
get_coeffs(A, (1,2))
22+
23+
##########
24+
# interp #
25+
##########
26+
27+
### 1d interpolation
28+
29+
from interpolation import interp
30+
31+
x = np.linspace(0,1,100)**2 # non-uniform points
32+
y = np.linspace(0,1,100) # values
33+
34+
# interpolate at one point:
35+
interp(x,y,0.5)
36+
37+
# or at many points:
38+
u = np.linspace(0,1,1000) # points
39+
interp(x,y,u)
40+
41+
# one can iterate at low cost since the function is jitable:
42+
from numba import njit
43+
@njit
44+
def vec_eval(u):
45+
N = u.shape[0]
46+
out = np.zeros(N)
47+
for n in range(N):
48+
out[n] = interp(x,y,u)
49+
return out
50+
51+
print( abs(vec_eval(u) - interp(x,y,u)).max())
52+
53+
54+
### 2d interpolation (same for higher orders)
55+
56+
57+
from interpolation import interp
58+
59+
x1 = np.linspace(0,1,100)**2 # non-uniform points
60+
x2 = np.linspace(0,1,100)**2 # non-uniform points
61+
y = np.array([[np.sqrt(u1**2 + u2**2) for u2 in x2] for u1 in x1])
62+
# (y[i,j] = sqrt(x1[i]**2+x2[j]**2)
63+
64+
65+
# interpolate at one point:
66+
67+
interp(x1,x2,y,0.5,0.2)
68+
interp(x1,x2,y,(0.5,0.2))
69+
70+
# or at many points: (each line corresponds to one observation)
71+
points = np.random.random((1000,2))
72+
interp(x1,x2,y,points)
73+
74+
from numba import njit
75+
@njit
76+
def vec_eval(p):
77+
N = p.shape[0]
78+
out = np.zeros(N)
79+
for n in range(N):
80+
z1 = p[n,0]
81+
z2 = p[n,1]
82+
out[n] = interp(x1,x2,y,z1,z2)
83+
return out
84+
85+
print( abs(vec_eval(points) - interp(x1,x2,y,points)).max())
86+
87+
88+
# in the special case where the points at which one wants to interpolate
89+
# form a cartesian grid, one can use another call style:
90+
91+
z1 = np.linspace(0,1,100)
92+
z2 = np.linspace(0,1,100)
93+
out = interp(x1,x2,y,z1,z2)
94+
# out[i,j] contains f(z1[i],z2[j])
95+
96+
97+
98+
############
99+
# mlinterp #
100+
############
101+
102+
# same as interp but with less flexible and more general API
103+
104+
from interpolation import mlinterp
105+
106+
x1 = np.linspace(0,1,100)**2 # non-uniform points for first dimensoin
107+
x2 = (0,1,100) # uniform points for second dimension
108+
grid = (x1,x2)
109+
y = np.array([[np.sqrt(u1**2 + u2**2) for u2 in x2] for u1 in x1])
110+
111+
112+
points = np.random.random((1000,2))
113+
114+
# vectorized call:
115+
mlinterp(grid, y, points)
116+
117+
# non-vectorized call (note third argument must be a tuple of floats of right size)
118+
mlinterp(grid, y, (0.4, 0.2))
119+
120+
# arbitrary dimension
121+
122+
d = 4
123+
K = 100
124+
N = 10000
125+
grid = (np.linspace(0,1,K),)*d
126+
y = np.random.random((K,)*d)
127+
z = np.random.random((N,d))
128+
129+
mlinterp(grid,y,z)

interpolation/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .multilinear.mlinterp import interp, mlinterp

interpolation/multilinear/__init__.py

Whitespace-only changes.

interpolation/multilinear/fungen.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import numba
2+
import numpy as np
3+
from numba import float64, int64
4+
from numba import generated_jit, njit
5+
import ast
6+
7+
from numba.extending import overload
8+
from numba.types.containers import Tuple, UniTuple
9+
10+
11+
####################
12+
# Dimension helper #
13+
####################
14+
15+
t_coord = numba.typeof((2.3,2.4,1)) # type of an evenly spaced dimension
16+
t_array = numba.typeof(np.array([4.0, 3.9])) # type of an unevenly spaced dimension
17+
18+
# returns the index of a 1d point along a 1d dimension
19+
@generated_jit(nopython=True)
20+
def get_index(gc, x):
21+
if gc == t_coord:
22+
# regular coordinate
23+
def fun(gc,x):
24+
δ = (gc[1]-gc[0])/(gc[2]-1)
25+
d = x-gc[0]
26+
ii = d // δ
27+
r = d-ii*δ
28+
i = int(ii)
29+
λ = r/δ
30+
return (i, λ)
31+
return fun
32+
else:
33+
# irregular coordinate
34+
def fun(gc,x):
35+
i = int(np.searchsorted(gc, x))-1
36+
λ = (x-gc[i])/(gc[i+1]-gc[i])
37+
return (i, λ)
38+
return fun
39+
40+
# returns number of dimension of a dimension
41+
@generated_jit(nopython=True)
42+
def get_size(gc):
43+
if gc == t_coord:
44+
# regular coordinate
45+
def fun(gc):
46+
return gc[2]
47+
return fun
48+
else:
49+
# irregular coordinate
50+
def fun(gc):
51+
return len(gc)
52+
return fun
53+
54+
#####################
55+
# Generator helpers #
56+
#####################
57+
58+
# the next functions replace the use of generators, with the difference that the
59+
# output is a tuple, which dimension is known by the jit guy.
60+
61+
# example:
62+
# ```
63+
# def f(x): x**2
64+
# fmap(f, (1,2,3)) -> (1,3,9)
65+
# def g(x,y): x**2 + y
66+
# fmap(g, (1,2,3), 0.1) -> (1.1,3.1,9.1) # (g(1,0.1), g(2,0.1), g(3,0.1))
67+
# def g(x,y): x**2 + y
68+
# fmap(g, (1,2,3), (0.1,0.2,0.3)) -> (1.1,3.0.12,9.3)
69+
# ```
70+
71+
72+
def fmap():
73+
pass
74+
75+
@overload(fmap)
76+
def _map(*args):
77+
78+
if len(args)==2 and isinstance(args[1], (Tuple, UniTuple)):
79+
k = len(args[1])
80+
s = "def __map(f, t): return ({}, )".format(str.join(', ',['f(t[{}])'.format(i) for i in range(k)]))
81+
elif len(args)==3 and isinstance(args[1], (Tuple, UniTuple)):
82+
k = len(args[1])
83+
if isinstance(args[2], (Tuple, UniTuple)):
84+
if len(args[2]) != k:
85+
# we don't know what to do in this case
86+
return None
87+
s = "def __map(f, t1, t2): return ({}, )".format(str.join(', ',['f(t1[{}], t2[{}])'.format(i,i) for i in range(k)]))
88+
else:
89+
s = "def __map(f, t1, x): return ({}, )".format(str.join(', ',['f(t1[{}], x)'.format(i,i) for i in range(k)]))
90+
else:
91+
return None
92+
d = {}
93+
eval(compile(ast.parse(s),'<string>','exec'), d)
94+
return d['__map']
95+
96+
97+
# not that `fmap` does nothing in python mode...
98+
# an alternative would be
99+
#
100+
# @njit
101+
# def _fmap():
102+
# pass
103+
#
104+
# @overload(_fmap)
105+
# ...
106+
#
107+
# @njit
108+
# def fmap(*args):
109+
# return _fmap(*args)
110+
#
111+
# but this seems to come with a performance cost.
112+
# It it is also possible to overload `map` but we would risk
113+
# a future conflict with the map api.
114+
115+
116+
#
117+
# @njit
118+
# def fmap(*args):
119+
# return _fmap(*args)
120+
#
121+
# funzip(((1,2), (2,3), (4,3))) -> ((1,2,4),(2,3,3))
122+
123+
@generated_jit(nopython=True)
124+
def funzip(t):
125+
k = t.count
126+
assert(len(set([e.count for e in t.types]))==1)
127+
l = t.types[0].count
128+
def print_tuple(t): return "({},)".format(str.join(", ", t))
129+
tab = [ [ 't[{}][{}]'.format(i,j) for i in range(k)] for j in range(l) ]
130+
s = "def funzip(t): return {}".format(print_tuple( [print_tuple(e) for e in tab] ))
131+
d = {}
132+
eval(compile(ast.parse(s),'<string>','exec'), d)
133+
return d['funzip']
134+
135+
136+
#####
137+
# array subscribing:
138+
# when X is a 2d array and I=(i,j) a 2d index, `get_coeffs(X,I)`
139+
# extracts `X[i:i+1,j:j+1]` but represents it as a tuple of tuple, so that
140+
# the number of its elements can be inferred by the compiler
141+
#####
142+
143+
144+
@generated_jit(nopython=True)
145+
def get_coeffs(X,I):
146+
if X.ndim>len(I):
147+
print("not implemented yet")
148+
else:
149+
from itertools import product
150+
d = len(I)
151+
mat = np.array( ["X[{}]".format(str.join(',', e)) for e in product(*[(f"I[{j}]", f"I[{j}]+1") for j in range(d)])] ).reshape((2,)*d)
152+
mattotup = lambda mat: mat if isinstance(mat,str) else "({})".format(str.join(',',[mattotup(e) for e in mat]))
153+
t = mattotup(mat)
154+
s = "def get_coeffs(X,I): return {}".format(t)
155+
dd = {}
156+
eval(compile(ast.parse(s),'<string>','exec'), dd)
157+
return dd['get_coeffs']
158+
return fun
159+
160+
# tensor_reduction(C,l)
161+
# (in 2d) computes the equivalent of np.einsum('ij,i,j->', C, [1-l[0],l[0]], [1-l[1],l[1]])`
162+
# but where l is a tuple and C a tuple of tuples.
163+
164+
# this one is a temporary implementation (should be merged with old gen_splines* code)
165+
def gen_tensor_reduction(X, symbs, inds=[]):
166+
if len(symbs) == 0:
167+
return '{}[{}]'.format(X, str.join('][',[str(e) for e in inds]))
168+
else:
169+
h = symbs[0]
170+
q = symbs[1:]
171+
exprs = [ '{}*({})'.format(h if i==1 else '(1-{})'.format(h),gen_tensor_reduction(X, q,inds + [i])) for i in range(2)]
172+
return str.join( ' + ', exprs )
173+
174+
175+
@generated_jit(nopython=True)
176+
def tensor_reduction(C,l):
177+
ex = gen_tensor_reduction('C', ['l[{}]'.format(i) for i in range(len(l.types))])
178+
dd = dict()
179+
s = "def tensor_reduction(C,l): return {}".format(ex)
180+
eval(compile(ast.parse(s),'<string>','exec'), dd)
181+
return dd['tensor_reduction']
182+
183+
@generated_jit(nopython=True)
184+
def extract_row(a, n, tup):
185+
d = len(tup.types)
186+
dd = {}
187+
s = "def extract_row(a, n, tup): return ({},)".format(str.join(', ', [f"a[n,{i}]" for i in range(d)]))
188+
eval(compile(ast.parse(s),'<string>','exec'), dd)
189+
return dd['extract_row']

0 commit comments

Comments
 (0)