Skip to content

Commit f529be7

Browse files
authored
Merge pull request #31 from espdev/fix-ndgrid-evaluate-performance-regression
Fix n-d grid evaluate performance regression
2 parents 35fd5b4 + 6551942 commit f529be7

6 files changed

Lines changed: 238 additions & 36 deletions

File tree

csaps/_reshape.py

Lines changed: 128 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
# -*- coding: utf-8 -*-
22

3+
import functools
4+
import operator
5+
from itertools import chain
36
import typing as ty
7+
48
import numpy as np
9+
from numpy.lib.stride_tricks import as_strided
10+
11+
12+
def prod(x):
13+
"""Product of a list/tuple of numbers; ~40x faster vs np.prod for Python tuples"""
14+
if len(x) == 0:
15+
return 1
16+
return functools.reduce(operator.mul, x)
517

618

719
def to_2d(arr: np.ndarray, axis: int) -> np.ndarray:
@@ -76,26 +88,130 @@ def to_2d(arr: np.ndarray, axis: int) -> np.ndarray:
7688
return arr.transpose(tr_axes).reshape(new_shape)
7789

7890

79-
def block_view(arr: np.ndarray, block: ty.Tuple[int]) -> np.ndarray:
80-
"""Returns array block view for given n-d array
91+
def umv_coeffs_to_canonical(arr: np.ndarray, pieces: int):
92+
"""
93+
94+
Parameters
95+
----------
96+
arr : array
97+
The 2-d array with shape (n, m) where:
98+
99+
n -- the number of spline dimensions (1 for univariate)
100+
m -- order * pieces
101+
102+
pieces : int
103+
The number of pieces
104+
105+
Returns
106+
-------
107+
arr_view : array view
108+
The 2-d or 3-d array view with shape (k, p) or (k, p, n) where:
109+
110+
k -- spline order
111+
p -- the number of spline pieces
112+
n -- the number of spline dimensions (multivariate case)
113+
114+
"""
115+
116+
ndim = arr.shape[0]
117+
order = arr.shape[1] // pieces
118+
119+
if ndim == 1:
120+
shape = (order, pieces)
121+
strides = (arr.strides[1] * pieces, arr.strides[1])
122+
else:
123+
shape = (order, pieces, ndim)
124+
strides = (arr.strides[1] * pieces, arr.strides[1], arr.strides[0])
81125

82-
Creates n-d array block view with shape (k0, ..., kn, b0, ..., bn) for given
83-
array with shape (m0, ..., mn) and block (b0, ..., bn).
126+
return as_strided(arr, shape=shape, strides=strides)
127+
128+
129+
def umv_coeffs_to_flatten(arr: np.ndarray):
130+
"""
84131
85132
Parameters
86133
----------
87-
arr : array-like
134+
arr : array
135+
The 2-d or 3-d array with shape (k, m) or (k, m, n) where:
136+
137+
k -- the spline order
138+
m -- the number of spline pieces
139+
n -- the number of spline dimensions (multivariate case)
140+
141+
Returns
142+
-------
143+
arr_view : array view
144+
The array 2-d view with shape (1, k * m) or (n, k * m)
145+
146+
"""
147+
148+
if arr.ndim == 2:
149+
arr_view = arr.ravel()[np.newaxis]
150+
elif arr.ndim == 3:
151+
shape = (arr.shape[2], prod(arr.shape[:2]))
152+
strides = arr.strides[:-3:-1]
153+
arr_view = as_strided(arr, shape=shape, strides=strides)
154+
else: # pragma: no cover
155+
raise ValueError(
156+
f"The array ndim must be 2 or 3, but given array has ndim={arr.ndim}.")
157+
158+
return arr_view
159+
160+
161+
def ndg_coeffs_to_canonical(arr: np.ndarray, pieces: ty.Tuple[int]) -> np.ndarray:
162+
"""Returns array canonical view for given n-d grid coeffs flatten array
163+
164+
Creates n-d array canonical view with shape (k0, ..., kn, p0, ..., pn) for given
165+
array with shape (m0, ..., mn) and pieces (p0, ..., pn).
166+
167+
Parameters
168+
----------
169+
arr : array
88170
The input array with shape (m0, ..., mn)
89-
block : tuple
90-
The block tuple (b0, ..., bn)
171+
pieces : tuple
172+
The number of pieces (p0, ..., pn)
91173
92174
Returns
93175
-------
94-
a_view : array-like
95-
The block view for given array (k0, ..., kn, b0, ..., bn)
176+
arr_view : array view
177+
The canonical view for given array with shape (k0, ..., kn, p0, ..., pn)
96178
97179
"""
98-
shape = tuple(size // blk for size, blk in zip(arr.shape, block)) + block
99-
strides = tuple(stride * blk for stride, blk in zip(arr.strides, block)) + arr.strides
100180

101-
return np.lib.stride_tricks.as_strided(arr, shape=shape, strides=strides)
181+
if arr.ndim > len(pieces):
182+
return arr
183+
184+
shape = tuple(sz // p for sz, p in zip(arr.shape, pieces)) + pieces
185+
strides = tuple(st * p for st, p in zip(arr.strides, pieces)) + arr.strides
186+
187+
return as_strided(arr, shape=shape, strides=strides)
188+
189+
190+
def ndg_coeffs_to_flatten(arr: np.ndarray):
191+
"""Creates flatten array view for n-d grid coeffs canonical array
192+
193+
For example for input array (4, 4, 20, 30) will be created the flatten view (80, 120)
194+
195+
Parameters
196+
----------
197+
arr : array
198+
The input array with shape (k0, ..., kn, p0, ..., pn) where:
199+
200+
``k0, ..., kn`` -- spline orders
201+
``p0, ..., pn`` -- spline pieces
202+
203+
Returns
204+
-------
205+
arr_view : array view
206+
Flatten view of array with shape (m0, ..., mn)
207+
208+
"""
209+
210+
if arr.ndim == 2:
211+
return arr
212+
213+
ndim = arr.ndim // 2
214+
axes = tuple(chain.from_iterable(zip(range(ndim), range(ndim, arr.ndim))))
215+
shape = tuple(prod(arr.shape[i::ndim]) for i in range(ndim))
216+
217+
return arr.transpose(axes).reshape(shape)

csaps/_sspndg.py

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,18 @@
1010
from typing import Tuple, Sequence, Optional, Union
1111

1212
import numpy as np
13-
from scipy.interpolate import NdPPoly
13+
from scipy.interpolate import PPoly, NdPPoly
1414

1515
from ._base import ISplinePPForm, ISmoothingSpline
1616
from ._types import UnivariateDataType, NdGridDataType
17-
from ._sspumv import SplinePPForm, CubicSmoothingSpline
18-
from ._reshape import block_view
17+
from ._sspumv import CubicSmoothingSpline
18+
from ._reshape import (
19+
prod,
20+
umv_coeffs_to_canonical,
21+
umv_coeffs_to_flatten,
22+
ndg_coeffs_to_canonical,
23+
ndg_coeffs_to_flatten,
24+
)
1925

2026

2127
def ndgrid_prepare_data_vectors(data, name, min_size: int = 2) -> Tuple[np.ndarray, ...]:
@@ -35,14 +41,6 @@ def ndgrid_prepare_data_vectors(data, name, min_size: int = 2) -> Tuple[np.ndarr
3541
return tuple(data)
3642

3743

38-
def _flatten_coeffs(spline: SplinePPForm):
39-
shape = list(spline.shape)
40-
shape.pop(spline.axis)
41-
c_shape = (spline.order * spline.pieces, int(np.prod(shape)))
42-
43-
return spline.c.reshape(c_shape).T
44-
45-
4644
class NdGridSplinePPForm(ISplinePPForm[Tuple[np.ndarray, ...], Tuple[int, ...]],
4745
NdPPoly):
4846
"""N-D grid spline representation in PP-form
@@ -115,9 +113,29 @@ def __call__(self,
115113
raise ValueError(
116114
f"'x' sequence must have length {self.ndim} according to 'breaks'")
117115

118-
x = tuple(np.meshgrid(*x, indexing='ij'))
116+
shape = tuple(x.size for x in x)
117+
118+
coeffs = ndg_coeffs_to_flatten(self.coeffs)
119+
coeffs_shape = coeffs.shape
120+
121+
ndim_m1 = self.ndim - 1
122+
permuted_axes = (ndim_m1, *range(ndim_m1))
123+
124+
for i in reversed(range(self.ndim)):
125+
umv_ndim = prod(coeffs_shape[:ndim_m1])
126+
c_shape = (umv_ndim, self.pieces[i] * self.order[i])
127+
if c_shape != coeffs_shape:
128+
coeffs = coeffs.reshape(c_shape)
129+
130+
coeffs_cnl = umv_coeffs_to_canonical(coeffs, self.pieces[i])
131+
coeffs = PPoly.construct_fast(coeffs_cnl, self.breaks[i],
132+
extrapolate=extrapolate, axis=1)(x[i])
133+
134+
shape_r = (*coeffs_shape[:ndim_m1], shape[i])
135+
coeffs = coeffs.reshape(shape_r).transpose(permuted_axes)
136+
coeffs_shape = coeffs.shape
119137

120-
return super().__call__(x, nu, extrapolate)
138+
return coeffs.reshape(shape)
121139

122140
def __repr__(self): # pragma: no cover
123141
return (
@@ -298,13 +316,13 @@ def _make_spline(xdata, ydata, weights, smooth):
298316
# computing coordinatewise smoothing spline
299317
for i in reversed(range(ndim)):
300318
if ndim > 2:
301-
coeffs = coeffs.reshape(np.prod(coeffs.shape[:-1]), coeffs.shape[-1])
319+
coeffs = coeffs.reshape(prod(coeffs.shape[:-1]), coeffs.shape[-1])
302320

303321
s = CubicSmoothingSpline(
304322
xdata[i], coeffs, weights=weights[i], smooth=smooth[i])
305323

306324
smooths.append(s.smooth)
307-
coeffs = _flatten_coeffs(s.spline)
325+
coeffs = umv_coeffs_to_flatten(s.spline.coeffs)
308326

309327
if ndim > 2:
310328
coeffs_shape[-1] = s.spline.pieces * s.spline.order
@@ -313,7 +331,7 @@ def _make_spline(xdata, ydata, weights, smooth):
313331
coeffs = coeffs.transpose(permute_axes)
314332
coeffs_shape = list(coeffs.shape)
315333

316-
block = tuple(int(size - 1) for size in shape)
317-
coeffs = block_view(coeffs.squeeze(), block)
334+
pieces = tuple(int(size - 1) for size in shape)
335+
coeffs = ndg_coeffs_to_canonical(coeffs.squeeze(), pieces)
318336

319337
return coeffs, tuple(reversed(smooths))

csaps/_sspumv.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"""
77

88
import functools
9-
import operator
109
from typing import Optional, Union, Tuple, List
1110

1211
import numpy as np
@@ -17,7 +16,7 @@
1716

1817
from ._base import ISplinePPForm, ISmoothingSpline
1918
from ._types import UnivariateDataType, MultivariateDataType
20-
from ._reshape import to_2d
19+
from ._reshape import to_2d, prod
2120

2221

2322
class SplinePPForm(ISplinePPForm[np.ndarray, int], PPoly):
@@ -59,9 +58,7 @@ def ndim(self) -> int:
5958
shape = list(self.shape)
6059
shape.pop(self.axis)
6160

62-
if len(shape) == 0:
63-
return 1
64-
return functools.reduce(operator.mul, shape)
61+
return prod(shape)
6562

6663
@property
6764
def shape(self) -> Tuple[int]:

csaps/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# -*- coding: utf-8 -*-
22

3-
__version__ = '1.0.0'
3+
__version__ = '1.0.1.dev'

tests/test_ndg.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def test_nd_2pt_array(shape: tuple):
174174
(3, 4, 5, 6),
175175
(3, 2, 2, 6, 2),
176176
(3, 4, 5, 6, 7),
177-
], ids=['1d_o2', '1d_o4', '2d_o2', '2d_o4', '3d_o2', '3d_o4', '4d_o2', '4d_o4', '5d_o2', '5d_o4'])
177+
])
178178
def test_nd_array(shape: tuple):
179179
xdata = [np.arange(s) for s in shape]
180180
ydata = np.arange(0, np.prod(shape)).reshape(shape)

tests/test_reshape.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import pytest
4+
import numpy as np
5+
6+
from csaps._reshape import ( # noqa
7+
umv_coeffs_to_flatten,
8+
umv_coeffs_to_canonical,
9+
ndg_coeffs_to_flatten,
10+
ndg_coeffs_to_canonical,
11+
)
12+
13+
14+
@pytest.mark.parametrize('shape_canonical, shape_flatten, pieces', [
15+
((2, 1), (1, 2), 1),
16+
((3, 6), (1, 18), 6),
17+
((4, 3), (1, 12), 3),
18+
((4, 30), (1, 120), 30),
19+
((4, 5, 2), (2, 20), 5),
20+
((4, 6, 3), (3, 24), 6),
21+
((4, 120, 53), (53, 480), 120),
22+
])
23+
def test_umv_coeffs_reshape(shape_canonical: tuple, shape_flatten: tuple, pieces: int):
24+
np.random.seed(1234)
25+
arr_canonical_expected = np.random.randint(0, 99, size=shape_canonical)
26+
27+
arr_flatten = umv_coeffs_to_flatten(arr_canonical_expected)
28+
assert arr_flatten.shape == shape_flatten
29+
30+
arr_canonical_actual = umv_coeffs_to_canonical(arr_flatten, pieces)
31+
np.testing.assert_array_equal(arr_canonical_actual, arr_canonical_expected)
32+
33+
34+
@pytest.mark.parametrize('shape_canonical, shape_flatten, pieces', [
35+
# 1-d 2-ordered
36+
((2, 3), (2, 3), (3,)),
37+
((2, 4), (2, 4), (4,)),
38+
((2, 5), (2, 5), (5,)),
39+
40+
# 1-d 3-ordered
41+
((3, 3), (3, 3), (3,)),
42+
((3, 4), (3, 4), (4,)),
43+
((3, 5), (3, 5), (5,)),
44+
45+
# 1-d 4-ordered
46+
((4, 3), (4, 3), (3,)),
47+
((4, 4), (4, 4), (4,)),
48+
((4, 5), (4, 5), (5,)),
49+
50+
# 2-d {2,4}-ordered
51+
((2, 4, 3, 4), (6, 16), (3, 4)),
52+
((4, 2, 3, 3), (12, 6), (3, 3)),
53+
((4, 2, 4, 3), (16, 6), (4, 3)),
54+
((2, 4, 4, 4), (8, 16), (4, 4)),
55+
56+
# 2-d {4,4}-ordered
57+
((4, 4, 3, 3), (12, 12), (3, 3)),
58+
59+
# 3-d {4,4,4}-ordered
60+
((4, 4, 4, 3, 3, 3), (12, 12, 12), (3, 3, 3)),
61+
((4, 4, 4, 3, 5, 7), (12, 20, 28), (3, 5, 7)),
62+
])
63+
def test_ndg_coeffs_reshape(shape_canonical: tuple, shape_flatten: tuple, pieces: tuple):
64+
np.random.seed(1234)
65+
arr_canonical_expected = np.random.randint(0, 99, size=shape_canonical)
66+
67+
arr_flatten = ndg_coeffs_to_flatten(arr_canonical_expected)
68+
assert arr_flatten.shape == shape_flatten
69+
70+
arr_canonical_actual = ndg_coeffs_to_canonical(arr_flatten, pieces)
71+
np.testing.assert_array_equal(arr_canonical_actual, arr_canonical_expected)

0 commit comments

Comments
 (0)