| """Module for RBF interpolation.""" |
| import warnings |
| from itertools import combinations_with_replacement |
|
|
| import numpy as np |
| from numpy.linalg import LinAlgError |
| from scipy.spatial import KDTree |
| from scipy.special import comb |
| from scipy.linalg.lapack import dgesv |
|
|
| from ._rbfinterp_pythran import (_build_system, |
| _build_evaluation_coefficients, |
| _polynomial_matrix) |
|
|
|
|
| __all__ = ["RBFInterpolator"] |
|
|
|
|
| |
| _AVAILABLE = { |
| "linear", |
| "thin_plate_spline", |
| "cubic", |
| "quintic", |
| "multiquadric", |
| "inverse_multiquadric", |
| "inverse_quadratic", |
| "gaussian" |
| } |
|
|
|
|
| |
| _SCALE_INVARIANT = {"linear", "thin_plate_spline", "cubic", "quintic"} |
|
|
|
|
| |
| |
| |
| |
| |
| _NAME_TO_MIN_DEGREE = { |
| "multiquadric": 0, |
| "linear": 0, |
| "thin_plate_spline": 1, |
| "cubic": 1, |
| "quintic": 2 |
| } |
|
|
|
|
| def _monomial_powers(ndim, degree): |
| """Return the powers for each monomial in a polynomial. |
| |
| Parameters |
| ---------- |
| ndim : int |
| Number of variables in the polynomial. |
| degree : int |
| Degree of the polynomial. |
| |
| Returns |
| ------- |
| (nmonos, ndim) int ndarray |
| Array where each row contains the powers for each variable in a |
| monomial. |
| |
| """ |
| nmonos = comb(degree + ndim, ndim, exact=True) |
| out = np.zeros((nmonos, ndim), dtype=np.dtype("long")) |
| count = 0 |
| for deg in range(degree + 1): |
| for mono in combinations_with_replacement(range(ndim), deg): |
| |
| |
| for var in mono: |
| out[count, var] += 1 |
|
|
| count += 1 |
|
|
| return out |
|
|
|
|
| def _build_and_solve_system(y, d, smoothing, kernel, epsilon, powers): |
| """Build and solve the RBF interpolation system of equations. |
| |
| Parameters |
| ---------- |
| y : (P, N) float ndarray |
| Data point coordinates. |
| d : (P, S) float ndarray |
| Data values at `y`. |
| smoothing : (P,) float ndarray |
| Smoothing parameter for each data point. |
| kernel : str |
| Name of the RBF. |
| epsilon : float |
| Shape parameter. |
| powers : (R, N) int ndarray |
| The exponents for each monomial in the polynomial. |
| |
| Returns |
| ------- |
| coeffs : (P + R, S) float ndarray |
| Coefficients for each RBF and monomial. |
| shift : (N,) float ndarray |
| Domain shift used to create the polynomial matrix. |
| scale : (N,) float ndarray |
| Domain scaling used to create the polynomial matrix. |
| |
| """ |
| lhs, rhs, shift, scale = _build_system( |
| y, d, smoothing, kernel, epsilon, powers |
| ) |
| _, _, coeffs, info = dgesv(lhs, rhs, overwrite_a=True, overwrite_b=True) |
| if info < 0: |
| raise ValueError(f"The {-info}-th argument had an illegal value.") |
| elif info > 0: |
| msg = "Singular matrix." |
| nmonos = powers.shape[0] |
| if nmonos > 0: |
| pmat = _polynomial_matrix((y - shift)/scale, powers) |
| rank = np.linalg.matrix_rank(pmat) |
| if rank < nmonos: |
| msg = ( |
| "Singular matrix. The matrix of monomials evaluated at " |
| "the data point coordinates does not have full column " |
| f"rank ({rank}/{nmonos})." |
| ) |
|
|
| raise LinAlgError(msg) |
|
|
| return shift, scale, coeffs |
|
|
|
|
| class RBFInterpolator: |
| """Radial basis function (RBF) interpolation in N dimensions. |
| |
| Parameters |
| ---------- |
| y : (npoints, ndims) array_like |
| 2-D array of data point coordinates. |
| d : (npoints, ...) array_like |
| N-D array of data values at `y`. The length of `d` along the first |
| axis must be equal to the length of `y`. Unlike some interpolators, the |
| interpolation axis cannot be changed. |
| neighbors : int, optional |
| If specified, the value of the interpolant at each evaluation point |
| will be computed using only this many nearest data points. All the data |
| points are used by default. |
| smoothing : float or (npoints, ) array_like, optional |
| Smoothing parameter. The interpolant perfectly fits the data when this |
| is set to 0. For large values, the interpolant approaches a least |
| squares fit of a polynomial with the specified degree. Default is 0. |
| kernel : str, optional |
| Type of RBF. This should be one of |
| |
| - 'linear' : ``-r`` |
| - 'thin_plate_spline' : ``r**2 * log(r)`` |
| - 'cubic' : ``r**3`` |
| - 'quintic' : ``-r**5`` |
| - 'multiquadric' : ``-sqrt(1 + r**2)`` |
| - 'inverse_multiquadric' : ``1/sqrt(1 + r**2)`` |
| - 'inverse_quadratic' : ``1/(1 + r**2)`` |
| - 'gaussian' : ``exp(-r**2)`` |
| |
| Default is 'thin_plate_spline'. |
| epsilon : float, optional |
| Shape parameter that scales the input to the RBF. If `kernel` is |
| 'linear', 'thin_plate_spline', 'cubic', or 'quintic', this defaults to |
| 1 and can be ignored because it has the same effect as scaling the |
| smoothing parameter. Otherwise, this must be specified. |
| degree : int, optional |
| Degree of the added polynomial. For some RBFs the interpolant may not |
| be well-posed if the polynomial degree is too small. Those RBFs and |
| their corresponding minimum degrees are |
| |
| - 'multiquadric' : 0 |
| - 'linear' : 0 |
| - 'thin_plate_spline' : 1 |
| - 'cubic' : 1 |
| - 'quintic' : 2 |
| |
| The default value is the minimum degree for `kernel` or 0 if there is |
| no minimum degree. Set this to -1 for no added polynomial. |
| |
| Notes |
| ----- |
| An RBF is a scalar valued function in N-dimensional space whose value at |
| :math:`x` can be expressed in terms of :math:`r=||x - c||`, where :math:`c` |
| is the center of the RBF. |
| |
| An RBF interpolant for the vector of data values :math:`d`, which are from |
| locations :math:`y`, is a linear combination of RBFs centered at :math:`y` |
| plus a polynomial with a specified degree. The RBF interpolant is written |
| as |
| |
| .. math:: |
| f(x) = K(x, y) a + P(x) b, |
| |
| where :math:`K(x, y)` is a matrix of RBFs with centers at :math:`y` |
| evaluated at the points :math:`x`, and :math:`P(x)` is a matrix of |
| monomials, which span polynomials with the specified degree, evaluated at |
| :math:`x`. The coefficients :math:`a` and :math:`b` are the solution to the |
| linear equations |
| |
| .. math:: |
| (K(y, y) + \\lambda I) a + P(y) b = d |
| |
| and |
| |
| .. math:: |
| P(y)^T a = 0, |
| |
| where :math:`\\lambda` is a non-negative smoothing parameter that controls |
| how well we want to fit the data. The data are fit exactly when the |
| smoothing parameter is 0. |
| |
| The above system is uniquely solvable if the following requirements are |
| met: |
| |
| - :math:`P(y)` must have full column rank. :math:`P(y)` always has full |
| column rank when `degree` is -1 or 0. When `degree` is 1, |
| :math:`P(y)` has full column rank if the data point locations are not |
| all collinear (N=2), coplanar (N=3), etc. |
| - If `kernel` is 'multiquadric', 'linear', 'thin_plate_spline', |
| 'cubic', or 'quintic', then `degree` must not be lower than the |
| minimum value listed above. |
| - If `smoothing` is 0, then each data point location must be distinct. |
| |
| When using an RBF that is not scale invariant ('multiquadric', |
| 'inverse_multiquadric', 'inverse_quadratic', or 'gaussian'), an appropriate |
| shape parameter must be chosen (e.g., through cross validation). Smaller |
| values for the shape parameter correspond to wider RBFs. The problem can |
| become ill-conditioned or singular when the shape parameter is too small. |
| |
| The memory required to solve for the RBF interpolation coefficients |
| increases quadratically with the number of data points, which can become |
| impractical when interpolating more than about a thousand data points. |
| To overcome memory limitations for large interpolation problems, the |
| `neighbors` argument can be specified to compute an RBF interpolant for |
| each evaluation point using only the nearest data points. |
| |
| .. versionadded:: 1.7.0 |
| |
| See Also |
| -------- |
| NearestNDInterpolator |
| LinearNDInterpolator |
| CloughTocher2DInterpolator |
| |
| References |
| ---------- |
| .. [1] Fasshauer, G., 2007. Meshfree Approximation Methods with Matlab. |
| World Scientific Publishing Co. |
| |
| .. [2] http://amadeus.math.iit.edu/~fass/603_ch3.pdf |
| |
| .. [3] Wahba, G., 1990. Spline Models for Observational Data. SIAM. |
| |
| .. [4] http://pages.stat.wisc.edu/~wahba/stat860public/lect/lect8/lect8.pdf |
| |
| Examples |
| -------- |
| Demonstrate interpolating scattered data to a grid in 2-D. |
| |
| >>> import numpy as np |
| >>> import matplotlib.pyplot as plt |
| >>> from scipy.interpolate import RBFInterpolator |
| >>> from scipy.stats.qmc import Halton |
| |
| >>> rng = np.random.default_rng() |
| >>> xobs = 2*Halton(2, seed=rng).random(100) - 1 |
| >>> yobs = np.sum(xobs, axis=1)*np.exp(-6*np.sum(xobs**2, axis=1)) |
| |
| >>> xgrid = np.mgrid[-1:1:50j, -1:1:50j] |
| >>> xflat = xgrid.reshape(2, -1).T |
| >>> yflat = RBFInterpolator(xobs, yobs)(xflat) |
| >>> ygrid = yflat.reshape(50, 50) |
| |
| >>> fig, ax = plt.subplots() |
| >>> ax.pcolormesh(*xgrid, ygrid, vmin=-0.25, vmax=0.25, shading='gouraud') |
| >>> p = ax.scatter(*xobs.T, c=yobs, s=50, ec='k', vmin=-0.25, vmax=0.25) |
| >>> fig.colorbar(p) |
| >>> plt.show() |
| |
| """ |
|
|
| def __init__(self, y, d, |
| neighbors=None, |
| smoothing=0.0, |
| kernel="thin_plate_spline", |
| epsilon=None, |
| degree=None): |
| y = np.asarray(y, dtype=float, order="C") |
| if y.ndim != 2: |
| raise ValueError("`y` must be a 2-dimensional array.") |
|
|
| ny, ndim = y.shape |
|
|
| d_dtype = complex if np.iscomplexobj(d) else float |
| d = np.asarray(d, dtype=d_dtype, order="C") |
| if d.shape[0] != ny: |
| raise ValueError( |
| f"Expected the first axis of `d` to have length {ny}." |
| ) |
|
|
| d_shape = d.shape[1:] |
| d = d.reshape((ny, -1)) |
| |
| |
| |
| d = d.view(float) |
|
|
| if np.isscalar(smoothing): |
| smoothing = np.full(ny, smoothing, dtype=float) |
| else: |
| smoothing = np.asarray(smoothing, dtype=float, order="C") |
| if smoothing.shape != (ny,): |
| raise ValueError( |
| "Expected `smoothing` to be a scalar or have shape " |
| f"({ny},)." |
| ) |
|
|
| kernel = kernel.lower() |
| if kernel not in _AVAILABLE: |
| raise ValueError(f"`kernel` must be one of {_AVAILABLE}.") |
|
|
| if epsilon is None: |
| if kernel in _SCALE_INVARIANT: |
| epsilon = 1.0 |
| else: |
| raise ValueError( |
| "`epsilon` must be specified if `kernel` is not one of " |
| f"{_SCALE_INVARIANT}." |
| ) |
| else: |
| epsilon = float(epsilon) |
|
|
| min_degree = _NAME_TO_MIN_DEGREE.get(kernel, -1) |
| if degree is None: |
| degree = max(min_degree, 0) |
| else: |
| degree = int(degree) |
| if degree < -1: |
| raise ValueError("`degree` must be at least -1.") |
| elif -1 < degree < min_degree: |
| warnings.warn( |
| f"`degree` should not be below {min_degree} except -1 " |
| f"when `kernel` is '{kernel}'." |
| f"The interpolant may not be uniquely " |
| f"solvable, and the smoothing parameter may have an " |
| f"unintuitive effect.", |
| UserWarning, stacklevel=2 |
| ) |
|
|
| if neighbors is None: |
| nobs = ny |
| else: |
| |
| |
| neighbors = int(min(neighbors, ny)) |
| nobs = neighbors |
|
|
| powers = _monomial_powers(ndim, degree) |
| |
| |
| |
| if powers.shape[0] > nobs: |
| raise ValueError( |
| f"At least {powers.shape[0]} data points are required when " |
| f"`degree` is {degree} and the number of dimensions is {ndim}." |
| ) |
|
|
| if neighbors is None: |
| shift, scale, coeffs = _build_and_solve_system( |
| y, d, smoothing, kernel, epsilon, powers |
| ) |
|
|
| |
| self._shift = shift |
| self._scale = scale |
| self._coeffs = coeffs |
|
|
| else: |
| self._tree = KDTree(y) |
|
|
| self.y = y |
| self.d = d |
| self.d_shape = d_shape |
| self.d_dtype = d_dtype |
| self.neighbors = neighbors |
| self.smoothing = smoothing |
| self.kernel = kernel |
| self.epsilon = epsilon |
| self.powers = powers |
|
|
| def _chunk_evaluator( |
| self, |
| x, |
| y, |
| shift, |
| scale, |
| coeffs, |
| memory_budget=1000000 |
| ): |
| """ |
| Evaluate the interpolation while controlling memory consumption. |
| We chunk the input if we need more memory than specified. |
| |
| Parameters |
| ---------- |
| x : (Q, N) float ndarray |
| array of points on which to evaluate |
| y: (P, N) float ndarray |
| array of points on which we know function values |
| shift: (N, ) ndarray |
| Domain shift used to create the polynomial matrix. |
| scale : (N,) float ndarray |
| Domain scaling used to create the polynomial matrix. |
| coeffs: (P+R, S) float ndarray |
| Coefficients in front of basis functions |
| memory_budget: int |
| Total amount of memory (in units of sizeof(float)) we wish |
| to devote for storing the array of coefficients for |
| interpolated points. If we need more memory than that, we |
| chunk the input. |
| |
| Returns |
| ------- |
| (Q, S) float ndarray |
| Interpolated array |
| """ |
| nx, ndim = x.shape |
| if self.neighbors is None: |
| nnei = len(y) |
| else: |
| nnei = self.neighbors |
| |
| chunksize = memory_budget // (self.powers.shape[0] + nnei) + 1 |
| if chunksize <= nx: |
| out = np.empty((nx, self.d.shape[1]), dtype=float) |
| for i in range(0, nx, chunksize): |
| vec = _build_evaluation_coefficients( |
| x[i:i + chunksize, :], |
| y, |
| self.kernel, |
| self.epsilon, |
| self.powers, |
| shift, |
| scale) |
| out[i:i + chunksize, :] = np.dot(vec, coeffs) |
| else: |
| vec = _build_evaluation_coefficients( |
| x, |
| y, |
| self.kernel, |
| self.epsilon, |
| self.powers, |
| shift, |
| scale) |
| out = np.dot(vec, coeffs) |
| return out |
|
|
| def __call__(self, x): |
| """Evaluate the interpolant at `x`. |
| |
| Parameters |
| ---------- |
| x : (Q, N) array_like |
| Evaluation point coordinates. |
| |
| Returns |
| ------- |
| (Q, ...) ndarray |
| Values of the interpolant at `x`. |
| |
| """ |
| x = np.asarray(x, dtype=float, order="C") |
| if x.ndim != 2: |
| raise ValueError("`x` must be a 2-dimensional array.") |
|
|
| nx, ndim = x.shape |
| if ndim != self.y.shape[1]: |
| raise ValueError("Expected the second axis of `x` to have length " |
| f"{self.y.shape[1]}.") |
|
|
| |
| |
| |
| |
| |
| memory_budget = max(x.size + self.y.size + self.d.size, 1000000) |
|
|
| if self.neighbors is None: |
| out = self._chunk_evaluator( |
| x, |
| self.y, |
| self._shift, |
| self._scale, |
| self._coeffs, |
| memory_budget=memory_budget) |
| else: |
| |
| |
| _, yindices = self._tree.query(x, self.neighbors) |
| if self.neighbors == 1: |
| |
| yindices = yindices[:, None] |
|
|
| |
| |
| |
| |
| yindices = np.sort(yindices, axis=1) |
| yindices, inv = np.unique(yindices, return_inverse=True, axis=0) |
| inv = np.reshape(inv, (-1,)) |
| |
| |
| |
| xindices = [[] for _ in range(len(yindices))] |
| for i, j in enumerate(inv): |
| xindices[j].append(i) |
|
|
| out = np.empty((nx, self.d.shape[1]), dtype=float) |
| for xidx, yidx in zip(xindices, yindices): |
| |
| |
| |
| xnbr = x[xidx] |
| ynbr = self.y[yidx] |
| dnbr = self.d[yidx] |
| snbr = self.smoothing[yidx] |
| shift, scale, coeffs = _build_and_solve_system( |
| ynbr, |
| dnbr, |
| snbr, |
| self.kernel, |
| self.epsilon, |
| self.powers, |
| ) |
| out[xidx] = self._chunk_evaluator( |
| xnbr, |
| ynbr, |
| shift, |
| scale, |
| coeffs, |
| memory_budget=memory_budget) |
|
|
| out = out.view(self.d_dtype) |
| out = out.reshape((nx, ) + self.d_shape) |
| return out |
|
|