rbfgpu 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rbfgpu/__init__.py +77 -0
- rbfgpu/__version__.py +32 -0
- rbfgpu/_backend.py +121 -0
- rbfgpu/_distance.py +138 -0
- rbfgpu/_kernels.py +220 -0
- rbfgpu/_polynomial.py +104 -0
- rbfgpu/interpolator.py +497 -0
- rbfgpu/py.typed +0 -0
- rbfgpu-1.0.0.dist-info/METADATA +336 -0
- rbfgpu-1.0.0.dist-info/RECORD +13 -0
- rbfgpu-1.0.0.dist-info/WHEEL +5 -0
- rbfgpu-1.0.0.dist-info/licenses/LICENSE +21 -0
- rbfgpu-1.0.0.dist-info/top_level.txt +1 -0
rbfgpu/interpolator.py
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Radial Basis Function interpolation on the GPU (with a transparent CPU fallback).
|
|
3
|
+
|
|
4
|
+
This is a GPU-optimised alternative to ``scipy.interpolate.RBFInterpolator`` for
|
|
5
|
+
the regime where SciPy runs out of breath: a few thousand constraint points, for
|
|
6
|
+
which the O(n^2) Gram matrix and the O(n^3) solve dominate the runtime and fit
|
|
7
|
+
comfortably in device memory.
|
|
8
|
+
|
|
9
|
+
Theory
|
|
10
|
+
------
|
|
11
|
+
An RBF interpolant is a linear combination of radial basis functions centred at
|
|
12
|
+
the data points ``y`` plus a polynomial:
|
|
13
|
+
|
|
14
|
+
f(x) = sum_i a_i phi(||x - y_i||) + sum_j b_j p_j(x)
|
|
15
|
+
|
|
16
|
+
where ``phi`` is the RBF kernel (e.g. the thin plate spline ``r^2 log r``) and
|
|
17
|
+
``p_j`` are monomials of total degree <= ``degree``. The coefficients ``(a, b)``
|
|
18
|
+
solve the block system
|
|
19
|
+
|
|
20
|
+
[K + lambda I P] [a] = [d]
|
|
21
|
+
[P^T 0] [b] [0]
|
|
22
|
+
|
|
23
|
+
with ``K`` the RBF Gram matrix and ``lambda`` the smoothing parameter. The
|
|
24
|
+
polynomial side conditions ``P^T a = 0`` are what makes the system uniquely
|
|
25
|
+
solvable for conditionally positive definite kernels, and what allows the
|
|
26
|
+
interpolant to extrapolate.
|
|
27
|
+
|
|
28
|
+
Everything runs through XuPy, so the same object is built on CuPy arrays when a
|
|
29
|
+
GPU is available and on NumPy arrays otherwise.
|
|
30
|
+
|
|
31
|
+
References
|
|
32
|
+
----------
|
|
33
|
+
[1] Fasshauer, G., 2007. Meshfree Approximation Methods with Matlab.
|
|
34
|
+
[2] Wahba, G., 1990. Spline Models for Observational Data.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import math
|
|
40
|
+
import warnings
|
|
41
|
+
|
|
42
|
+
import numpy as np
|
|
43
|
+
from xupy import typings as _ot
|
|
44
|
+
|
|
45
|
+
from ._backend import LINALG_ERRORS, as_numpy, xp
|
|
46
|
+
from ._distance import default_epsilon, euclidean_distance
|
|
47
|
+
from ._kernels import Kernel, resolve_kernel
|
|
48
|
+
from ._polynomial import monomial_exponents, polynomial_matrix
|
|
49
|
+
|
|
50
|
+
__all__ = ["DEFAULT_MAX_CHUNK_BYTES", "DEFAULT_MIN_CHUNK_SIZE", "RBFInterpolator"]
|
|
51
|
+
|
|
52
|
+
#: Memory budget used to pick the evaluation chunk size (1 GiB).
|
|
53
|
+
DEFAULT_MAX_CHUNK_BYTES = 1024 * 1024**2
|
|
54
|
+
#: Floor for the estimated chunk size, so that tiny budgets still batch usefully.
|
|
55
|
+
DEFAULT_MIN_CHUNK_SIZE = 4096
|
|
56
|
+
|
|
57
|
+
#: Message for a system that cannot be solved, whatever the backend reported.
|
|
58
|
+
_SINGULAR_SYSTEM_HINT = (
|
|
59
|
+
"Failed to solve the RBF system: the matrix is singular. The constraint "
|
|
60
|
+
"points may be duplicated or collinear, or the kernel may be too wide or too "
|
|
61
|
+
"narrow for the data. Try removing duplicated points, increasing `smoothing`, "
|
|
62
|
+
"or tuning `epsilon`."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class RBFInterpolator:
|
|
67
|
+
"""
|
|
68
|
+
Radial Basis Function interpolator, GPU accelerated through XuPy.
|
|
69
|
+
|
|
70
|
+
The API mirrors ``scipy.interpolate.RBFInterpolator`` for the parts that are
|
|
71
|
+
implemented here (there is no ``neighbors`` support: every evaluation point
|
|
72
|
+
always uses all the constraint points). Kernels, ``epsilon`` and ``degree``
|
|
73
|
+
defaults follow SciPy, so the two agree to round-off.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
y : array, shape (npoints, ndims)
|
|
78
|
+
Constraint point coordinates. A 1-D array is accepted and read as
|
|
79
|
+
``npoints`` points in one dimension. Host arrays are moved onto the
|
|
80
|
+
active backend automatically.
|
|
81
|
+
d : array, shape (npoints, ...)
|
|
82
|
+
Data values at ``y``. The first axis must match ``y``; the trailing
|
|
83
|
+
shape is preserved in the output. Several outputs are solved for at
|
|
84
|
+
once, in a single factorisation.
|
|
85
|
+
kernel : str or callable, optional
|
|
86
|
+
RBF kernel: one of ``rbfgpu.available_kernels()``, or a callable mapping
|
|
87
|
+
an array of distances to an array of weights (then ``epsilon`` and
|
|
88
|
+
``degree`` must be given explicitly). Default: 'thin_plate_spline'.
|
|
89
|
+
epsilon : float, optional
|
|
90
|
+
Shape parameter: distances are multiplied by it before the kernel is
|
|
91
|
+
applied. Defaults to 1 for the scale invariant kernels ('linear',
|
|
92
|
+
'thin_plate_spline', 'cubic', 'quintic') and, for the others, to
|
|
93
|
+
:func:`rbfgpu.default_epsilon` (the square root of the mean nearest
|
|
94
|
+
neighbour distance of ``y``). SciPy refuses to guess and requires
|
|
95
|
+
``epsilon`` for those kernels; rbfgpu estimates it instead, so pass your
|
|
96
|
+
own value whenever the interpolation quality matters.
|
|
97
|
+
smoothing : float or array, shape (npoints,), optional
|
|
98
|
+
Tikhonov regularization. 0 (default) interpolates the data exactly;
|
|
99
|
+
large values approach a least squares polynomial fit of the given
|
|
100
|
+
``degree``.
|
|
101
|
+
degree : int, optional
|
|
102
|
+
Degree of the added polynomial. ``-1`` adds none. Defaults to the
|
|
103
|
+
minimum degree the kernel needs ('thin_plate_spline' and 'cubic': 1,
|
|
104
|
+
'quintic': 2, 'linear' and 'multiquadric': 0), or 0 for positive
|
|
105
|
+
definite kernels. A degree strictly between -1 and that minimum warns,
|
|
106
|
+
as in SciPy: the interpolant may not be uniquely solvable.
|
|
107
|
+
memory_efficient : bool, optional
|
|
108
|
+
If True (default), evaluation is chunked to keep the peak device memory
|
|
109
|
+
below ``max_chunk_bytes``.
|
|
110
|
+
max_chunk_bytes : int, optional
|
|
111
|
+
Memory budget, in bytes, for the temporaries allocated while evaluating
|
|
112
|
+
one chunk. Default: 1 GiB.
|
|
113
|
+
min_chunk_size : int, optional
|
|
114
|
+
Smallest chunk the automatic estimate may return. Default: 4096.
|
|
115
|
+
|
|
116
|
+
Examples
|
|
117
|
+
--------
|
|
118
|
+
>>> import numpy as np
|
|
119
|
+
>>> import rbfgpu
|
|
120
|
+
>>> from rbfgpu import RBFInterpolator
|
|
121
|
+
>>> rng = np.random.default_rng(0)
|
|
122
|
+
>>> y = rng.random((1000, 2)) # 1000 constraint points in 2-D
|
|
123
|
+
>>> d = np.sin(y[:, 0]) * np.cos(y[:, 1])
|
|
124
|
+
>>> rbf = RBFInterpolator(y, d) # thin plate spline, degree=1
|
|
125
|
+
>>> x = rng.random((10000, 2)) # 10000 evaluation points
|
|
126
|
+
>>> values = rbf(x)
|
|
127
|
+
>>> values.shape
|
|
128
|
+
(10000,)
|
|
129
|
+
|
|
130
|
+
The result lives on the interpolant's backend (CuPy on a GPU), so bring it to
|
|
131
|
+
the host before mixing it with NumPy arrays:
|
|
132
|
+
|
|
133
|
+
>>> residual = rbfgpu.as_numpy(rbf(y)) - d
|
|
134
|
+
>>> bool(np.max(np.abs(residual)) < 1e-9)
|
|
135
|
+
True
|
|
136
|
+
|
|
137
|
+
Notes
|
|
138
|
+
-----
|
|
139
|
+
Building the interpolant costs one O(npoints^2) distance/kernel evaluation
|
|
140
|
+
and one O(npoints^3) solve; evaluating it costs O(n_eval * npoints). With
|
|
141
|
+
``memory_efficient=True`` the evaluation is split into chunks, trading a
|
|
142
|
+
little speed for a bounded peak memory.
|
|
143
|
+
|
|
144
|
+
The ``epsilon`` default and the ``smoothing`` validation reduce device values
|
|
145
|
+
to Python floats, i.e. they synchronise the GPU once at build time. The
|
|
146
|
+
solve synchronises anyway, so no time is lost.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def __init__(
|
|
150
|
+
self,
|
|
151
|
+
y: _ot.ArrayLike,
|
|
152
|
+
d: _ot.ArrayLike,
|
|
153
|
+
kernel: _ot.Union[str, _ot.Callable] = "thin_plate_spline",
|
|
154
|
+
epsilon: _ot.Optional[float] = None,
|
|
155
|
+
smoothing: _ot.Union[float, _ot.ArrayLike] = 0.0,
|
|
156
|
+
degree: _ot.Optional[int] = None,
|
|
157
|
+
memory_efficient: bool = True,
|
|
158
|
+
max_chunk_bytes: int = DEFAULT_MAX_CHUNK_BYTES,
|
|
159
|
+
min_chunk_size: int = DEFAULT_MIN_CHUNK_SIZE,
|
|
160
|
+
):
|
|
161
|
+
self.y, npoints, ndim = self._as_points(y)
|
|
162
|
+
self.d, self.d_shape = self._as_values(d, npoints)
|
|
163
|
+
|
|
164
|
+
self._kernel: Kernel = resolve_kernel(kernel)
|
|
165
|
+
self.kernel = self._kernel.name
|
|
166
|
+
self.epsilon = self._resolve_epsilon(epsilon)
|
|
167
|
+
self.degree = self._resolve_degree(degree)
|
|
168
|
+
self._powers = monomial_exponents(ndim, self.degree)
|
|
169
|
+
self.n_poly = len(self._powers)
|
|
170
|
+
|
|
171
|
+
self.smoothing = self._as_smoothing(smoothing, npoints)
|
|
172
|
+
|
|
173
|
+
self.memory_efficient = bool(memory_efficient)
|
|
174
|
+
self.max_chunk_bytes = int(max_chunk_bytes)
|
|
175
|
+
self.min_chunk_size = int(min_chunk_size)
|
|
176
|
+
if self.max_chunk_bytes <= 0:
|
|
177
|
+
raise ValueError(
|
|
178
|
+
f"`max_chunk_bytes` must be positive, got {max_chunk_bytes}."
|
|
179
|
+
)
|
|
180
|
+
if self.min_chunk_size <= 0:
|
|
181
|
+
raise ValueError(
|
|
182
|
+
f"`min_chunk_size` must be positive, got {min_chunk_size}."
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
self.npoints = npoints
|
|
186
|
+
self.ndim = ndim
|
|
187
|
+
self.output_dim = self.d.shape[1]
|
|
188
|
+
|
|
189
|
+
self._build_system()
|
|
190
|
+
|
|
191
|
+
# ------------------------------------------------------------------
|
|
192
|
+
# Input validation
|
|
193
|
+
# ------------------------------------------------------------------
|
|
194
|
+
@staticmethod
|
|
195
|
+
def _as_points(y: _ot.ArrayLike):
|
|
196
|
+
"""Convert ``y`` to a float64 (npoints, ndims) array on the backend."""
|
|
197
|
+
y = xp.asarray(y, dtype=xp.float64)
|
|
198
|
+
if y.ndim == 1:
|
|
199
|
+
y = xp.reshape(y, (y.shape[0], 1))
|
|
200
|
+
if y.ndim != 2:
|
|
201
|
+
raise ValueError(
|
|
202
|
+
"`y` must be a 1-D or 2-D array of point coordinates, got an "
|
|
203
|
+
f"array with {y.ndim} dimensions."
|
|
204
|
+
)
|
|
205
|
+
npoints, ndim = y.shape
|
|
206
|
+
if npoints == 0:
|
|
207
|
+
raise ValueError("`y` must contain at least one point.")
|
|
208
|
+
return y, int(npoints), int(ndim)
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def _as_values(d: _ot.ArrayLike, npoints: int):
|
|
212
|
+
"""
|
|
213
|
+
Convert ``d`` to a float64 (npoints, output_dim) array on the backend.
|
|
214
|
+
|
|
215
|
+
Returns the flattened values and the trailing shape of the input, so
|
|
216
|
+
that ``__call__`` can restore it.
|
|
217
|
+
"""
|
|
218
|
+
d = xp.asarray(d, dtype=xp.float64)
|
|
219
|
+
if d.ndim == 0:
|
|
220
|
+
d = xp.reshape(d, (1,))
|
|
221
|
+
if d.shape[0] != npoints:
|
|
222
|
+
raise ValueError(
|
|
223
|
+
"`d` must have the same length along its first axis as `y` "
|
|
224
|
+
f"({npoints}), got {d.shape[0]}."
|
|
225
|
+
)
|
|
226
|
+
d_shape = tuple(int(size) for size in d.shape[1:])
|
|
227
|
+
return xp.reshape(d, (npoints, -1)), d_shape
|
|
228
|
+
|
|
229
|
+
def _resolve_epsilon(self, epsilon: _ot.Optional[float]) -> float:
|
|
230
|
+
"""Pick the shape parameter, following SciPy's defaulting rules."""
|
|
231
|
+
if self._kernel.min_degree is None and epsilon is None:
|
|
232
|
+
raise ValueError(
|
|
233
|
+
"`epsilon` must be given explicitly when `kernel` is a callable."
|
|
234
|
+
)
|
|
235
|
+
if epsilon is None:
|
|
236
|
+
# Scale invariant kernels only use epsilon to rescale the data, so
|
|
237
|
+
# any value works; the others need a data-driven default.
|
|
238
|
+
epsilon = 1.0 if self._kernel.scale_invariant else default_epsilon(self.y)
|
|
239
|
+
epsilon = float(epsilon)
|
|
240
|
+
if not math.isfinite(epsilon) or epsilon <= 0:
|
|
241
|
+
raise ValueError(f"`epsilon` must be a positive finite number, got {epsilon}.")
|
|
242
|
+
return epsilon
|
|
243
|
+
|
|
244
|
+
def _resolve_degree(self, degree: _ot.Optional[int]) -> int:
|
|
245
|
+
"""Pick the polynomial degree, following SciPy's defaulting rules."""
|
|
246
|
+
if self._kernel.min_degree is None and degree is None:
|
|
247
|
+
raise ValueError(
|
|
248
|
+
"`degree` must be given explicitly when `kernel` is a callable."
|
|
249
|
+
)
|
|
250
|
+
if degree is None:
|
|
251
|
+
return int(self._kernel.default_degree)
|
|
252
|
+
|
|
253
|
+
degree = int(degree)
|
|
254
|
+
if degree < -1:
|
|
255
|
+
raise ValueError(f"`degree` must be >= -1 (-1 means no polynomial), got {degree}.")
|
|
256
|
+
min_degree = self._kernel.min_degree
|
|
257
|
+
if min_degree is not None and -1 < degree < min_degree:
|
|
258
|
+
# Same rule as SciPy: -1 ("no polynomial at all") is an explicit,
|
|
259
|
+
# legitimate choice and must not warn; a degree in between is not.
|
|
260
|
+
warnings.warn(
|
|
261
|
+
f"`degree` should not be below {min_degree} except -1 when "
|
|
262
|
+
f"`kernel` is '{self._kernel.name}'. The interpolant may not be "
|
|
263
|
+
"uniquely solvable, and the smoothing parameter may have an "
|
|
264
|
+
"unintuitive effect.",
|
|
265
|
+
UserWarning,
|
|
266
|
+
stacklevel=3,
|
|
267
|
+
)
|
|
268
|
+
return degree
|
|
269
|
+
|
|
270
|
+
@staticmethod
|
|
271
|
+
def _as_smoothing(smoothing: _ot.Union[float, _ot.ArrayLike], npoints: int):
|
|
272
|
+
"""Broadcast a scalar ``smoothing`` or validate an array one."""
|
|
273
|
+
if np.ndim(smoothing) == 0:
|
|
274
|
+
smoothing = xp.full(npoints, float(smoothing), dtype=xp.float64)
|
|
275
|
+
else:
|
|
276
|
+
smoothing = xp.asarray(smoothing, dtype=xp.float64)
|
|
277
|
+
if smoothing.shape != (npoints,):
|
|
278
|
+
raise ValueError(
|
|
279
|
+
"`smoothing` must be a scalar or an array of shape "
|
|
280
|
+
f"({npoints},), got shape {tuple(smoothing.shape)}."
|
|
281
|
+
)
|
|
282
|
+
if bool(xp.any(smoothing < 0)):
|
|
283
|
+
raise ValueError("`smoothing` must be non-negative.")
|
|
284
|
+
return smoothing
|
|
285
|
+
|
|
286
|
+
# ------------------------------------------------------------------
|
|
287
|
+
# Build
|
|
288
|
+
# ------------------------------------------------------------------
|
|
289
|
+
def _build_system(self) -> None:
|
|
290
|
+
"""
|
|
291
|
+
Build and solve the RBF linear system.
|
|
292
|
+
|
|
293
|
+
Constructs the block matrix
|
|
294
|
+
|
|
295
|
+
[K + lambda I P] [a] = [d]
|
|
296
|
+
[P^T 0] [b] [0]
|
|
297
|
+
|
|
298
|
+
with ``K`` the RBF Gram matrix, ``P`` the polynomial matrix and
|
|
299
|
+
``lambda`` the smoothing, and stores the solution as ``coeffs_rbf``
|
|
300
|
+
(``a``) and ``coeffs_poly`` (``b``, or None when ``degree == -1``).
|
|
301
|
+
"""
|
|
302
|
+
n = self.npoints
|
|
303
|
+
n_poly = self.n_poly
|
|
304
|
+
n_sys = n + n_poly
|
|
305
|
+
|
|
306
|
+
A = xp.zeros((n_sys, n_sys), dtype=xp.float64)
|
|
307
|
+
|
|
308
|
+
# The kernel block is written straight into the system matrix, so that
|
|
309
|
+
# the (n, n) Gram matrix and the (n_sys, n_sys) block matrix are never
|
|
310
|
+
# resident on the device at the same time.
|
|
311
|
+
dist = euclidean_distance(self.y, self.y, self.epsilon)
|
|
312
|
+
A[:n, :n] = self._kernel.function(dist)
|
|
313
|
+
del dist
|
|
314
|
+
|
|
315
|
+
A[:n, :n] += xp.diag(self.smoothing)
|
|
316
|
+
|
|
317
|
+
if n_poly > 0:
|
|
318
|
+
P = polynomial_matrix(self.y, self._powers)
|
|
319
|
+
A[:n, n:] = P
|
|
320
|
+
A[n:, :n] = P.T
|
|
321
|
+
del P
|
|
322
|
+
|
|
323
|
+
b = xp.zeros((n_sys, self.output_dim), dtype=xp.float64)
|
|
324
|
+
b[:n] = self.d
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
coeffs = xp.linalg.solve(A, b)
|
|
328
|
+
except LINALG_ERRORS as error:
|
|
329
|
+
raise RuntimeError(f"{_SINGULAR_SYSTEM_HINT} ({error})") from error
|
|
330
|
+
del A, b
|
|
331
|
+
|
|
332
|
+
# LAPACK reports a singular factorisation by raising LinAlgError; cuSOLVER
|
|
333
|
+
# quietly returns NaNs instead. The solution is therefore checked
|
|
334
|
+
# explicitly: handing back NaN coefficients would corrupt every later
|
|
335
|
+
# evaluation without any visible cause. Costs one host synchronisation,
|
|
336
|
+
# which the solve has already paid for.
|
|
337
|
+
if not bool(xp.isfinite(coeffs).all()):
|
|
338
|
+
raise RuntimeError(
|
|
339
|
+
f"{_SINGULAR_SYSTEM_HINT} (the solver returned non-finite "
|
|
340
|
+
"coefficients)"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
self.coeffs_rbf = coeffs[:n]
|
|
344
|
+
self.coeffs_poly = coeffs[n:] if n_poly > 0 else None
|
|
345
|
+
|
|
346
|
+
# ------------------------------------------------------------------
|
|
347
|
+
# Evaluation
|
|
348
|
+
# ------------------------------------------------------------------
|
|
349
|
+
def _resolve_chunk_size(self, n_eval: int, chunk_size: _ot.Optional[int]) -> int:
|
|
350
|
+
"""
|
|
351
|
+
Resolve the evaluation chunk size.
|
|
352
|
+
|
|
353
|
+
Parameters
|
|
354
|
+
----------
|
|
355
|
+
n_eval : int
|
|
356
|
+
Number of evaluation points.
|
|
357
|
+
chunk_size : int or None
|
|
358
|
+
Explicit chunk size. If None, a size is estimated from the
|
|
359
|
+
configured memory budget (or the whole evaluation is done at once
|
|
360
|
+
when ``memory_efficient`` is False).
|
|
361
|
+
|
|
362
|
+
Returns
|
|
363
|
+
-------
|
|
364
|
+
int
|
|
365
|
+
Effective chunk size, always in [1, n_eval].
|
|
366
|
+
"""
|
|
367
|
+
if chunk_size is not None:
|
|
368
|
+
return max(1, min(n_eval, int(chunk_size)))
|
|
369
|
+
|
|
370
|
+
if not self.memory_efficient:
|
|
371
|
+
return n_eval
|
|
372
|
+
|
|
373
|
+
bytes_per_float = np.dtype(np.float64).itemsize
|
|
374
|
+
# Per evaluation point: the (npoints,) distance and kernel arrays and
|
|
375
|
+
# the matmul result (4 arrays of npoints floats, conservatively), plus
|
|
376
|
+
# the polynomial matrix and the output.
|
|
377
|
+
floats_per_eval = 4 * self.npoints + self.output_dim + self.n_poly + self.ndim
|
|
378
|
+
bytes_per_eval = bytes_per_float * floats_per_eval
|
|
379
|
+
estimated_chunk = self.max_chunk_bytes // max(bytes_per_eval, 1)
|
|
380
|
+
estimated_chunk = max(self.min_chunk_size, int(estimated_chunk))
|
|
381
|
+
return max(1, min(n_eval, estimated_chunk))
|
|
382
|
+
|
|
383
|
+
def __call__(self, x: _ot.ArrayLike, chunk_size: _ot.Optional[int] = None):
|
|
384
|
+
"""
|
|
385
|
+
Evaluate the interpolant at the points ``x``.
|
|
386
|
+
|
|
387
|
+
Parameters
|
|
388
|
+
----------
|
|
389
|
+
x : array, shape (npts, ndims)
|
|
390
|
+
Evaluation points. For a 1-D interpolation problem (``ndims == 1``)
|
|
391
|
+
a flat array of ``npts`` coordinates is accepted; for ``ndims > 1``
|
|
392
|
+
a flat array is read as a single point.
|
|
393
|
+
chunk_size : int, optional
|
|
394
|
+
Number of evaluation points per batch. Overrides the automatic
|
|
395
|
+
estimate; useful to bound the device memory on very large grids.
|
|
396
|
+
|
|
397
|
+
Returns
|
|
398
|
+
-------
|
|
399
|
+
f : array
|
|
400
|
+
Interpolant values at ``x``, with shape ``(npts,) + d.shape[1:]``,
|
|
401
|
+
on the same backend as the interpolant.
|
|
402
|
+
"""
|
|
403
|
+
x = self._as_evaluation_points(x)
|
|
404
|
+
n_eval = x.shape[0]
|
|
405
|
+
|
|
406
|
+
resolved_chunk_size = self._resolve_chunk_size(n_eval, chunk_size)
|
|
407
|
+
if resolved_chunk_size >= n_eval:
|
|
408
|
+
out = self._evaluate_batch(x)
|
|
409
|
+
else:
|
|
410
|
+
out = xp.empty((n_eval, self.output_dim), dtype=xp.float64)
|
|
411
|
+
for start in range(0, n_eval, resolved_chunk_size):
|
|
412
|
+
stop = min(start + resolved_chunk_size, n_eval)
|
|
413
|
+
out[start:stop] = self._evaluate_batch(x[start:stop])
|
|
414
|
+
|
|
415
|
+
if self.d_shape:
|
|
416
|
+
return xp.reshape(out, (n_eval,) + self.d_shape)
|
|
417
|
+
return out[:, 0]
|
|
418
|
+
|
|
419
|
+
def _as_evaluation_points(self, x: _ot.ArrayLike):
|
|
420
|
+
"""Convert ``x`` to a float64 (npts, ndims) array and check its shape."""
|
|
421
|
+
x = xp.asarray(x, dtype=xp.float64)
|
|
422
|
+
if x.ndim == 1:
|
|
423
|
+
if self.ndim == 1:
|
|
424
|
+
x = xp.reshape(x, (-1, 1))
|
|
425
|
+
else:
|
|
426
|
+
x = xp.reshape(x, (1, -1))
|
|
427
|
+
if x.ndim != 2:
|
|
428
|
+
raise ValueError(
|
|
429
|
+
"`x` must be a 2-D array of shape (npts, ndims), got an array "
|
|
430
|
+
f"with {x.ndim} dimensions."
|
|
431
|
+
)
|
|
432
|
+
if x.shape[1] != self.ndim:
|
|
433
|
+
raise ValueError(
|
|
434
|
+
f"Expected the second axis of `x` to have length {self.ndim}, "
|
|
435
|
+
f"got {x.shape[1]}."
|
|
436
|
+
)
|
|
437
|
+
return x
|
|
438
|
+
|
|
439
|
+
def _evaluate_batch(self, x):
|
|
440
|
+
"""Evaluate the interpolant on one batch of points."""
|
|
441
|
+
# RBF contribution: sum_i a_i phi(||x - y_i||)
|
|
442
|
+
dist = euclidean_distance(x, self.y, self.epsilon)
|
|
443
|
+
K = self._kernel.function(dist)
|
|
444
|
+
del dist
|
|
445
|
+
out = K @ self.coeffs_rbf
|
|
446
|
+
del K
|
|
447
|
+
|
|
448
|
+
# Polynomial contribution: sum_j b_j p_j(x)
|
|
449
|
+
if self.coeffs_poly is not None:
|
|
450
|
+
P = polynomial_matrix(x, self._powers)
|
|
451
|
+
out = out + P @ self.coeffs_poly
|
|
452
|
+
del P
|
|
453
|
+
|
|
454
|
+
return out
|
|
455
|
+
|
|
456
|
+
# ------------------------------------------------------------------
|
|
457
|
+
# Inspection / export
|
|
458
|
+
# ------------------------------------------------------------------
|
|
459
|
+
@property
|
|
460
|
+
def on_gpu(self) -> bool:
|
|
461
|
+
"""True when this interpolant's arrays live in GPU memory."""
|
|
462
|
+
return not isinstance(self.y, np.ndarray)
|
|
463
|
+
|
|
464
|
+
def to_cpu(self) -> dict:
|
|
465
|
+
"""
|
|
466
|
+
Export the solved interpolant as host (NumPy) arrays.
|
|
467
|
+
|
|
468
|
+
The returned state is everything needed to evaluate the interpolant
|
|
469
|
+
without this package, for instance to hand it over to a CPU-only process
|
|
470
|
+
or to store it on disk.
|
|
471
|
+
|
|
472
|
+
Returns
|
|
473
|
+
-------
|
|
474
|
+
state : dict
|
|
475
|
+
With keys ``y``, ``coeffs_rbf``, ``coeffs_poly`` (None when
|
|
476
|
+
``degree == -1``), ``powers``, ``degree``, ``kernel`` and
|
|
477
|
+
``epsilon``.
|
|
478
|
+
"""
|
|
479
|
+
return {
|
|
480
|
+
"y": as_numpy(self.y),
|
|
481
|
+
"coeffs_rbf": as_numpy(self.coeffs_rbf),
|
|
482
|
+
"coeffs_poly": (
|
|
483
|
+
None if self.coeffs_poly is None else as_numpy(self.coeffs_poly)
|
|
484
|
+
),
|
|
485
|
+
"powers": self._powers,
|
|
486
|
+
"degree": self.degree,
|
|
487
|
+
"kernel": self.kernel,
|
|
488
|
+
"epsilon": self.epsilon,
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
def __repr__(self) -> str:
|
|
492
|
+
return (
|
|
493
|
+
f"{type(self).__name__}(kernel={self.kernel!r}, npoints={self.npoints}, "
|
|
494
|
+
f"ndim={self.ndim}, output_dim={self.output_dim}, degree={self.degree}, "
|
|
495
|
+
f"epsilon={self.epsilon!r}, "
|
|
496
|
+
f"backend={'gpu' if self.on_gpu else 'cpu'!r})"
|
|
497
|
+
)
|
rbfgpu/py.typed
ADDED
|
File without changes
|