Source code for petres.interpolators.spatial.radial_basis

from __future__ import annotations

from typing import Any
from typing import Literal
import numpy as np
from numpy.typing import DTypeLike, NDArray
from scipy.interpolate import RBFInterpolator

from ..base import BaseInterpolator


[docs] class RadialBasisFunctionInterpolator(BaseInterpolator): """Interpolate scalar values with SciPy radial basis functions. This interpolator wraps :class:`scipy.interpolate.RBFInterpolator` and validates input shapes and hyperparameters before fitting. Parameters ---------- kernel : {'linear', 'thin_plate_spline', 'cubic', 'quintic', 'multiquadric', 'inverse_multiquadric', 'inverse_quadratic', 'gaussian'}, default 'thin_plate_spline' Radial kernel passed to ``scipy.interpolate.RBFInterpolator``. epsilon : float or None, optional Shape parameter for some kernels. Must be > 0 when provided. smoothing : float, default 0.0 Non-negative smoothing factor. ``0.0`` yields exact interpolation. neighbors : int or None, optional If provided, use only the k nearest samples per query; must be > 0. degree : int or None, optional Degree of appended polynomial term. ``None`` uses SciPy defaults. dtype : numpy.typing.DTypeLike, default numpy.float64 Storage dtype for cached arrays and outputs. Notes ----- The implementation expects scalar targets with shape ``(n_samples,)``. """
[docs] def __init__( self, kernel: Literal[ "linear", "thin_plate_spline", "cubic", "quintic", "multiquadric", "inverse_multiquadric", "inverse_quadratic", "gaussian", ] = "thin_plate_spline", epsilon: float | None = None, smoothing: float = 0.0, neighbors: int | None = None, degree: int | None = None, dtype: DTypeLike = np.float64, ) -> None: """Initialize interpolation hyperparameters. Raises ------ ValueError If ``epsilon`` is not positive, ``smoothing`` is negative, ``neighbors`` is non-positive, or ``degree`` is negative. """ super().__init__() if epsilon is not None and epsilon <= 0: raise ValueError(f"`epsilon` must be > 0 when provided. Got {epsilon}.") if smoothing < 0: raise ValueError(f"`smoothing` must be >= 0. Got {smoothing}.") if neighbors is not None and neighbors <= 0: raise ValueError(f"`neighbors` must be a positive int or None. Got {neighbors}.") if degree is not None and degree < 0: raise ValueError(f"`degree` must be >= 0 or None. Got {degree}.") self.kernel = kernel self.epsilon = float(epsilon) if epsilon is not None else None self.smoothing = float(smoothing) self.neighbors = int(neighbors) if neighbors is not None else None self.degree = int(degree) if degree is not None else None self.dtype = np.dtype(dtype) self._rbf: RBFInterpolator | None = None
def _fit_impl(self, coordinates: NDArray[np.float64], values: NDArray[np.float64]) -> None: """Fit the underlying RBF interpolator from validated arrays. Parameters ---------- coordinates : numpy.typing.NDArray[numpy.float64] Training coordinates with shape ``(n_samples, dim)``. values : numpy.typing.NDArray[numpy.float64] Scalar training targets with shape ``(n_samples,)``. """ X = np.asarray(coordinates, dtype=self.dtype) y = np.asarray(values, dtype=self.dtype) if X.ndim != 2: raise ValueError(f"`coordinates` must be 2D of shape (n_samples, dim). Got shape {X.shape}.") if y.ndim != 1: raise ValueError(f"`values` must be 1D of shape (n_samples,). Got shape {y.shape}.") if X.shape[0] != y.shape[0]: raise ValueError( f"Number of samples mismatch: coordinates has {X.shape[0]}, values has {y.shape[0]}." ) if X.shape[0] == 0: raise ValueError("Cannot fit with zero samples.") if self.neighbors is not None and self.neighbors > X.shape[0]: raise ValueError( f"`neighbors`={self.neighbors} cannot be greater than n_samples={X.shape[0]}." ) kwargs: dict[str, object] = { "kernel": self.kernel, "smoothing": self.smoothing, "neighbors": self.neighbors, "degree": self.degree, } if self.epsilon is not None: kwargs["epsilon"] = self.epsilon try: self._rbf = RBFInterpolator(X, y, **kwargs) except Exception as exc: raise ValueError(f"Failed to construct RBF interpolator: {exc}") from exc self._is_fitted = True def _predict_impl(self, coordinates: NDArray[np.float64]) -> NDArray[np.float64]: """Predict interpolated values at query coordinates. Parameters ---------- coordinates : numpy.typing.NDArray[numpy.float64] Query coordinates with shape ``(n_points, dim)``. Returns ------- numpy.typing.NDArray[numpy.float64] Interpolated values with shape ``(n_points,)`` and configured ``dtype``. """ self._check_fitted() assert self._rbf is not None Q = np.asarray(coordinates, dtype=self.dtype) if Q.ndim != 2: raise ValueError(f"`coordinates` must be 2D of shape (n_points, dim). Got shape {Q.shape}.") if Q.shape[0] == 0: return np.asarray([], dtype=self.dtype) values = self._rbf(Q) return np.asarray(values, dtype=self.dtype)