././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1633431805.587115 csaps-1.2.0/CHANGELOG.md0000644000000000000000000000711014127030376011364 0ustar00# Changelog ## v1.1.0 (05.10.2021) * Introduced optional `normalizedsmooth` argument to reduce dependence on xdata and weights [#47](https://github.com/espdev/csaps/pull/47) * Update numpy and scipy dependency ranges ## v1.0.4 (04.05.2021) * Bump numpy dependency version ## v1.0.3 (01.01.2021) * Bump scipy dependency version * Bump sphinx dependency version and use m2r2 sphinx extension instead of m2r * Add Python 3.9 to classifiers list and to Travis CI * Set development status classifier to "5 - Production/Stable" * Happy New Year! ## v1.0.2 (19.07.2020) * Fix using 'nu' argument when n-d grid spline evaluating [#32](https://github.com/espdev/csaps/pull/32) ## v1.0.1 (19.07.2020) * Fix n-d grid spline evaluating performance regression [#31](https://github.com/espdev/csaps/pull/31) ## v1.0.0 (11.07.2020) * Use `PPoly` and `NdPPoly` base classes from SciPy interpolate module for `SplinePPForm` and `NdGridSplinePPForm` respectively. * Remove deprecated classes `UnivariateCubicSmoothingSpline` and `MultivariateCubicSmoothingSpline` * Update the documentation **Notes** In this release the spline representation (the array of spline coefficients) has been changed according to `PPoly`/`NdPPoly`. See SciPy [PPoly](https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.PPoly.html) and [NdPPoly](https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.NdPPoly.html) documentation for details. ## v0.11.0 (28.03.2020) * Internal re-design `SplinePPForm` and `NdGridSplinePPForm` classes [#17](https://github.com/espdev/csaps/issues/17): - Remove `shape` and `axis` properties and reshaping data in these classes - `NdGridSplinePPForm` coefficients array for 1D grid now is 1-d instead of 2-d * Refactoring the code and decrease memory consumption * Add `overload` type-hints for `csaps` function signatures ## v0.10.1 (19.03.2020) * Fix call of `numpy.pad` function for numpy <1.17 [#15](https://github.com/espdev/csaps/issues/15) ## v0.10.0 (18.02.2020) * Significant performance improvements for make/evaluate splines and memory consumption optimization * Change format for storing spline coefficients (reshape coeffs array) to improve performance * Add shape property to `SplinePPForm`/`NdGridSplinePPForm` and axis property to `SplinePPForm` * Fix issues with the smoothing factor in nd-grid case: inverted ordering and unnable to use 0.0 value * Update documentation ## v0.9.0 (21.01.2020) * Drop support of Python 3.5 * `weights`, `smooth` and `axis` arguments in `csaps` function are keyword-only now * `UnivariateCubicSmoothingSpline` and `MultivariateCubicSmoothingSpline` classes are deprecated and will be removed in 1.0.0 version. Use `CubicSmoothingSpline` instead. ## v0.8.0 (13.01.2020) * Add `csaps` function that can be used as the main API * Refactor the internal structure of the package * Add the [documentation](https://csaps.readthedocs.io) **Attention** This is the last version that supports Python 3.5. The next versions will support Python 3.6 or above. ## v0.7.0 (19.09.2019) * Add Generic-based type-hints and mypy-compatibility ## v0.6.1 (13.09.2019) * A slight refactoring and extra data copies removing ## v0.6.0 (12.09.2019) * Add "axis" parameter for univariate/multivariate cases ## v0.5.0 (10.06.2019) * Reorganize the project to package-based structure * Add the interface class for all smoothing spline classes ## v0.4.2 (07.09.2019) * FIX: "smooth" value is 0.0 was not used ## v0.4.1 (30.05.2019) * First PyPI release ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1645175151.659025 csaps-1.2.0/CONTRIBUTORS.txt0000644000000000000000000000004114203660560012244 0ustar00Eugene Prilepin Shamus Husheer ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1719750703.817928 csaps-1.2.0/csaps/__init__.py0000644000000000000000000000142714640250060012774 0ustar00""" Cubic spline approximation (smoothing) """ from csaps._base import ISmoothingSpline, ISplinePPForm from csaps._shortcut import AutoSmoothingResult, csaps from csaps._sspndg import NdGridCubicSmoothingSpline, NdGridSplinePPForm from csaps._sspumv import CubicSmoothingSpline, SplinePPForm from csaps._types import MultivariateDataType, NdGridDataType, UnivariateDataType from csaps._version import __version__ __all__ = [ # Shortcut 'csaps', 'AutoSmoothingResult', # Classes 'ISplinePPForm', 'ISmoothingSpline', 'SplinePPForm', 'NdGridSplinePPForm', 'CubicSmoothingSpline', 'NdGridCubicSmoothingSpline', # Type-hints 'UnivariateDataType', 'MultivariateDataType', 'NdGridDataType', '__version__', ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1719750703.8184576 csaps-1.2.0/csaps/_base.py0000644000000000000000000000457414640250060012314 0ustar00""" The base classes and interfaces """ from typing import Generic, Optional, Tuple import abc import numpy as np from ._types import TData, TExtrapolate, TNu, TProps, TSmooth, TSpline, TXi class ISplinePPForm(abc.ABC, Generic[TData, TProps]): """The interface class for spline representation in PP-form""" __module__ = 'csaps' @property @abc.abstractmethod def breaks(self) -> TData: """Returns the breaks for the spline Returns ------- breaks : Union[np.ndarray, ty.Tuple[np.ndarray, ...]] Breaks data """ @property @abc.abstractmethod def coeffs(self) -> np.ndarray: """Returns the spline coefficients Returns ------- coeffs : np.ndarray Coefficients n-d array """ @property @abc.abstractmethod def order(self) -> TProps: """Returns the spline order Returns ------- order : ty.Union[int, ty.Tuple[int, ...]] The spline order """ @property @abc.abstractmethod def pieces(self) -> TProps: """Returns the spline pieces data Returns ------- pieces : ty.Union[int, ty.Tuple[int, ...]] The spline pieces data """ @property @abc.abstractmethod def ndim(self) -> int: """Returns the spline dimension count Returns ------- ndim : int The spline dimension count """ @property @abc.abstractmethod def shape(self) -> Tuple[int, ...]: """Returns the source data shape Returns ------- shape : tuple of int The source data shape """ class ISmoothingSpline(abc.ABC, Generic[TSpline, TSmooth, TXi, TNu, TExtrapolate]): """The interface class for smooting splines""" __module__ = 'csaps' @property @abc.abstractmethod def smooth(self) -> TSmooth: """Returns smoothing factor(s)""" @property @abc.abstractmethod def spline(self) -> TSpline: """Returns spline representation in PP-form""" @abc.abstractmethod def __call__(self, xi: TXi, nu: Optional[TNu] = None, extrapolate: Optional[TExtrapolate] = None) -> np.ndarray: """Evaluates spline on the data sites""" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1719750703.8195078 csaps-1.2.0/csaps/_reshape.py0000644000000000000000000001266214640250060013026 0ustar00import typing as ty import functools from itertools import chain import operator import numpy as np from numpy.lib.stride_tricks import as_strided def prod(x): """Product of a list/tuple of numbers; ~40x faster vs np.prod for Python tuples""" if len(x) == 0: return 1 return functools.reduce(operator.mul, x) def to_2d(arr: np.ndarray, axis: int) -> np.ndarray: """Transforms the shape of N-D array to 2-D NxM array The function transforms N-D array to 2-D NxM array along given axis, where N is dimension and M is the nember of elements. The function does not create a copy. Parameters ---------- arr : np.array N-D array axis : int Axis that will be used for transform array shape Returns ------- arr2d : np.ndarray 2-D NxM array view Raises ------ ValueError : axis is out of array axes See Also -------- from_2d Examples -------- .. code-block:: python >>> shape = (2, 3, 4) >>> arr = np.arange(1, np.prod(shape)+1).reshape(shape) >>> arr_2d = to_2d(arr, axis=1) >>> print(arr) [[[ 1 2 3 4] [ 5 6 7 8] [ 9 10 11 12]] [[13 14 15 16] [17 18 19 20] [21 22 23 24]]] >>> print(arr_2d) [[ 1 5 9] [ 2 6 10] [ 3 7 11] [ 4 8 12] [13 17 21] [14 18 22] [15 19 23] [16 20 24]] """ arr = np.asarray(arr) axis = arr.ndim + axis if axis < 0 else axis if axis >= arr.ndim: # pragma: no cover raise ValueError(f'axis {axis} is out of array axes {arr.ndim}') tr_axes = list(range(arr.ndim)) tr_axes.pop(axis) tr_axes.append(axis) new_shape = (np.prod(arr.shape) // arr.shape[axis], arr.shape[axis]) return arr.transpose(tr_axes).reshape(new_shape) def umv_coeffs_to_canonical(arr: np.ndarray, pieces: int): """ Parameters ---------- arr : array The 2-d array with shape (n, m) where: n -- the number of spline dimensions (1 for univariate) m -- order * pieces pieces : int The number of pieces Returns ------- arr_view : array view The 2-d or 3-d array view with shape (k, p) or (k, p, n) where: k -- spline order p -- the number of spline pieces n -- the number of spline dimensions (multivariate case) """ ndim = arr.shape[0] order = arr.shape[1] // pieces if ndim == 1: shape = (order, pieces) strides = (arr.strides[1] * pieces, arr.strides[1]) else: shape = (order, pieces, ndim) strides = (arr.strides[1] * pieces, arr.strides[1], arr.strides[0]) return as_strided(arr, shape=shape, strides=strides) def umv_coeffs_to_flatten(arr: np.ndarray): """ Parameters ---------- arr : array The 2-d or 3-d array with shape (k, m) or (k, m, n) where: k -- the spline order m -- the number of spline pieces n -- the number of spline dimensions (multivariate case) Returns ------- arr_view : array view The array 2-d view with shape (1, k * m) or (n, k * m) """ if arr.ndim == 2: arr_view = arr.ravel()[np.newaxis] elif arr.ndim == 3: shape = (arr.shape[2], prod(arr.shape[:2])) strides = arr.strides[:-3:-1] arr_view = as_strided(arr, shape=shape, strides=strides) else: # pragma: no cover raise ValueError(f'The array ndim must be 2 or 3, but given array has ndim={arr.ndim}.') return arr_view def ndg_coeffs_to_canonical(arr: np.ndarray, pieces: ty.Tuple[int, ...]) -> np.ndarray: """Returns array canonical view for given n-d grid coeffs flatten array Creates n-d array canonical view with shape (k0, ..., kn, p0, ..., pn) for given array with shape (m0, ..., mn) and pieces (p0, ..., pn). Parameters ---------- arr : array The input array with shape (m0, ..., mn) pieces : tuple The number of pieces (p0, ..., pn) Returns ------- arr_view : array view The canonical view for given array with shape (k0, ..., kn, p0, ..., pn) """ if arr.ndim > len(pieces): return arr shape = tuple(sz // p for sz, p in zip(arr.shape, pieces)) + pieces strides = tuple(st * p for st, p in zip(arr.strides, pieces)) + arr.strides return as_strided(arr, shape=shape, strides=strides) def ndg_coeffs_to_flatten(arr: np.ndarray): """Creates flatten array view for n-d grid coeffs canonical array For example for input array (4, 4, 20, 30) will be created the flatten view (80, 120) Parameters ---------- arr : array The input array with shape (k0, ..., kn, p0, ..., pn) where: ``k0, ..., kn`` -- spline orders ``p0, ..., pn`` -- spline pieces Returns ------- arr_view : array view Flatten view of array with shape (m0, ..., mn) """ if arr.ndim == 2: return arr ndim = arr.ndim // 2 axes = tuple(chain.from_iterable(zip(range(ndim), range(ndim, arr.ndim)))) shape = tuple(prod(arr.shape[i::ndim]) for i in range(ndim)) return arr.transpose(axes).reshape(shape) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1719747936.6578047 csaps-1.2.0/csaps/_shortcut.py0000644000000000000000000001763414640242541013263 0ustar00""" The module provided `csaps` shortcut function for smoothing data """ from typing import NamedTuple, Optional, Sequence, Union, overload from collections import abc as c_abc from ._base import ISmoothingSpline from ._sspndg import NdGridCubicSmoothingSpline, ndgrid_prepare_data_vectors from ._sspumv import CubicSmoothingSpline from ._types import MultivariateDataType, NdGridDataType, UnivariateDataType class AutoSmoothingResult(NamedTuple): """The result for auto smoothing for `csaps` function""" values: MultivariateDataType """Smoothed data values""" smooth: Union[float, Sequence[Optional[float]]] """The calculated smoothing parameter""" # ************************************** # csaps signatures # @overload def csaps( xdata: UnivariateDataType, ydata: MultivariateDataType, *, weights: Optional[UnivariateDataType] = None, smooth: Optional[float] = None, axis: Optional[int] = None, normalizedsmooth: bool = False, ) -> ISmoothingSpline: # pragma: no cover ... @overload def csaps( xdata: UnivariateDataType, ydata: MultivariateDataType, xidata: UnivariateDataType, *, weights: Optional[UnivariateDataType] = None, axis: Optional[int] = None, normalizedsmooth: bool = False, ) -> AutoSmoothingResult: # pragma: no cover ... @overload def csaps( xdata: UnivariateDataType, ydata: MultivariateDataType, xidata: UnivariateDataType, *, smooth: float, weights: Optional[UnivariateDataType] = None, axis: Optional[int] = None, normalizedsmooth: bool = False, ) -> MultivariateDataType: # pragma: no cover ... @overload def csaps( xdata: NdGridDataType, ydata: MultivariateDataType, *, weights: Optional[NdGridDataType] = None, smooth: Optional[Sequence[float]] = None, axis: Optional[int] = None, normalizedsmooth: bool = False, ) -> ISmoothingSpline: # pragma: no cover ... @overload def csaps( xdata: NdGridDataType, ydata: MultivariateDataType, xidata: NdGridDataType, *, weights: Optional[NdGridDataType] = None, axis: Optional[int] = None, normalizedsmooth: bool = False, ) -> AutoSmoothingResult: # pragma: no cover ... @overload def csaps( xdata: NdGridDataType, ydata: MultivariateDataType, xidata: NdGridDataType, *, smooth: Sequence[float], weights: Optional[NdGridDataType] = None, axis: Optional[int] = None, normalizedsmooth: bool = False, ) -> MultivariateDataType: # pragma: no cover ... # # csaps signatures # ************************************** def csaps( xdata: Union[UnivariateDataType, NdGridDataType], ydata: MultivariateDataType, xidata: Optional[Union[UnivariateDataType, NdGridDataType]] = None, *, weights: Optional[Union[UnivariateDataType, NdGridDataType]] = None, smooth: Optional[Union[float, Sequence[float]]] = None, axis: Optional[int] = None, normalizedsmooth: bool = False, ) -> Union[MultivariateDataType, ISmoothingSpline, AutoSmoothingResult]: """Smooths the univariate/multivariate/gridded data or computes the corresponding splines This function might be used as the main API for smoothing any data. Parameters ---------- xdata : np.ndarray, array-like The data sites ``x1 < x2 < ... < xN``: - 1-D data vector/sequence (array-like) for univariate/multivariate ``ydata`` case - The sequence of 1-D data vectors for nd-gridded ``ydata`` case ydata : np.ndarray, array-like The data values: - 1-D data vector/sequence (array-like) for univariate data case - N-D array/array-like for multivariate data case - N-D array for nd-gridded data case xidata : [*Optional*] np.ndarray, array-like, Sequence[array-like] The data sites for output smoothed data: - 1-D data vector/sequence (array-like) for univariate/multivariate ``ydata`` case - The sequence of 1-D data vectors for nd-gridded ``ydata`` case If this argument was not set, the function will return computed spline for given data in :class:`ISmoothingSpline` object. weights : [*Optional*] np.ndarray, array-like, Sequence[array-like] The weights data vectors: - 1-D data vector/sequence (array-like) for univariate/multivariate ``ydata`` case - The sequence of 1-D data vectors for nd-gridded ``ydata`` case smooth : [*Optional*] float, Sequence[float] The smoothing factor value(s): - float value in the range ``[0, 1]`` for univariate/multivariate ``ydata`` case - the sequence of float in the range ``[0, 1]`` or None for nd-gridded ``ydata`` case If this argument was not set or None or sequence with None-items, the function will return named tuple :class:`AutoSmoothingResult` with computed smoothed data values and smoothing factor value(s). axis : [*Optional*] int The ``ydata`` axis. Axis along which ``ydata`` is assumed to be varying. If this argument was not set the last axis will be used (``axis == -1``). .. note:: Currently, `axis` will be ignored for nd-gridded ``ydata`` case. normalizedsmooth : [*Optional*] bool If True, the smooth parameter is normalized such that results are invariant to xdata range and less sensitive to nonuniformity of weights and xdata clumping .. versionadded:: 1.1.0 Returns ------- yidata : np.ndarray Smoothed data values if ``xidata`` and ``smooth`` were set. autosmoothing_result : AutoSmoothingResult The named tuple object with two fileds: - 'values' -- smoothed data values - 'smooth' -- computed smoothing factor This result will be returned if ``xidata`` was set and ``smooth`` was not set. ssp_obj : ISmoothingSpline Smoothing spline object if ``xidata`` was not set: - :class:`CubicSmoothingSpline` instance for univariate/multivariate data - :class:`NdGridCubicSmoothingSpline` instance for nd-gridded data Examples -------- Univariate data smoothing .. code-block:: python import numpy as np from csaps import csaps x = np.linspace(-5., 5., 25) y = np.exp(-(x/2.5)**2) + (np.random.rand(25) - 0.2) * 0.3 xi = np.linspace(-5., 5., 150) # Smooth data with smoothing factor 0.85 yi = csaps(x, y, xi, smooth=0.85) # Smooth data and compute smoothing factor automatically yi, smooth = csaps(x, y, xi) # Do not evaluate the spline, only compute it sp = csaps(x, y, smooth=0.98) See Also -------- CubicSmoothingSpline NdGridCubicSmoothingSpline """ if isinstance(xdata, c_abc.Sequence): try: ndgrid_prepare_data_vectors(xdata, 'xdata') except ValueError: umv = True else: umv = False else: umv = True if umv: axis = -1 if axis is None else axis sp = CubicSmoothingSpline( xdata, ydata, weights=weights, smooth=smooth, axis=axis, normalizedsmooth=normalizedsmooth, ) else: sp = NdGridCubicSmoothingSpline( xdata, ydata, weights, smooth, normalizedsmooth=normalizedsmooth, ) if xidata is None: return sp yidata = sp(xidata) auto_smooth = smooth is None if isinstance(smooth, Sequence): auto_smooth = any(sm is None for sm in smooth) if auto_smooth: return AutoSmoothingResult(yidata, sp.smooth) else: return yidata ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1719750703.8205647 csaps-1.2.0/csaps/_sspndg.py0000644000000000000000000002634214640250060012675 0ustar00""" ND-Gridded cubic smoothing spline implementation """ from typing import Optional, Sequence, Tuple, Union import collections.abc as c_abc from numbers import Number import numpy as np from scipy.interpolate import NdPPoly, PPoly from ._base import ISmoothingSpline, ISplinePPForm from ._reshape import ( ndg_coeffs_to_canonical, ndg_coeffs_to_flatten, prod, umv_coeffs_to_canonical, umv_coeffs_to_flatten, ) from ._sspumv import CubicSmoothingSpline from ._types import NdGridDataType, UnivariateDataType def ndgrid_prepare_data_vectors(data, name, min_size: int = 2) -> Tuple[np.ndarray, ...]: if not isinstance(data, c_abc.Sequence): raise TypeError(f"'{name}' must be a sequence of 1-d array-like (vectors) or scalars.") data = list(data) for axis, d in enumerate(data): d = np.asarray(d, dtype=np.float64) if d.ndim > 1: raise ValueError(f"All '{name}' elements must be a vector for axis {axis}.") if d.size < min_size: raise ValueError(f"'{name}' must contain at least {min_size} data points for axis {axis}.") data[axis] = d return tuple(data) class NdGridSplinePPForm(ISplinePPForm[Tuple[np.ndarray, ...], Tuple[int, ...]], NdPPoly): """N-D grid spline representation in PP-form N-D grid spline is represented in piecewise tensor product polynomial form. Notes ----- Inherited from :py:class:`scipy.interpolate.NdPPoly` """ __module__ = 'csaps' @property def breaks(self) -> Tuple[np.ndarray, ...]: return self.x @property def coeffs(self) -> np.ndarray: return self.c @property def order(self) -> Tuple[int, ...]: return self.c.shape[: self.c.ndim // 2] @property def pieces(self) -> Tuple[int, ...]: return self.c.shape[self.c.ndim // 2 :] @property def ndim(self) -> int: return len(self.x) @property def shape(self) -> Tuple[int, ...]: return tuple(len(xi) for xi in self.x) def __call__( self, x: Sequence[UnivariateDataType], nu: Optional[Tuple[int, ...]] = None, extrapolate: Optional[bool] = None, ) -> np.ndarray: """Evaluate the spline for given data Parameters ---------- x : tuple of 1-d array-like The tuple of point values for each dimension to evaluate the spline at. nu : [*Optional*] tuple of int Orders of derivatives to evaluate. Each must be non-negative. extrapolate : [*Optional*] bool Whether to extrapolate to out-of-bounds points based on first and last intervals, or to return NaNs. Returns ------- y : array-like Interpolated values. Shape is determined by replacing the interpolation axis in the original array with the shape of x. """ x = ndgrid_prepare_data_vectors(x, 'x', min_size=1) if len(x) != self.ndim: raise ValueError(f"'x' sequence must have length {self.ndim} according to 'breaks'") if nu is None: nu = (0,) * len(x) if extrapolate is None: extrapolate = True shape = tuple(x.size for x in x) coeffs = ndg_coeffs_to_flatten(self.coeffs) coeffs_shape = coeffs.shape ndim_m1 = self.ndim - 1 permuted_axes = (ndim_m1, *range(ndim_m1)) for i in reversed(range(self.ndim)): umv_ndim = prod(coeffs_shape[:ndim_m1]) c_shape = (umv_ndim, self.pieces[i] * self.order[i]) if c_shape != coeffs_shape: coeffs = coeffs.reshape(c_shape) coeffs_cnl = umv_coeffs_to_canonical(coeffs, self.pieces[i]) spline = PPoly.construct_fast(coeffs_cnl, self.breaks[i], axis=1) coeffs = spline(x[i], nu=nu[i], extrapolate=extrapolate) shape_r = (*coeffs_shape[:ndim_m1], shape[i]) coeffs = coeffs.reshape(shape_r).transpose(permuted_axes) coeffs_shape = coeffs.shape return coeffs.reshape(shape) def __repr__(self): # pragma: no cover return ( f'{type(self).__name__}\n' f' breaks: {self.breaks}\n' f' coeffs shape: {self.coeffs.shape}\n' f' data shape: {self.shape}\n' f' pieces: {self.pieces}\n' f' order: {self.order}\n' f' ndim: {self.ndim}\n' ) class NdGridCubicSmoothingSpline( ISmoothingSpline[ NdGridSplinePPForm, Tuple[float, ...], NdGridDataType, Tuple[int, ...], bool, ] ): """N-D grid cubic smoothing spline Class implements N-D grid data smoothing (piecewise tensor product polynomial). Parameters ---------- xdata : list, tuple, Sequence[vector-like] X data site vectors for each dimensions. These vectors determine ND-grid. For example:: # 2D grid x = [ np.linspace(0, 5, 21), np.linspace(0, 6, 25), ] ydata : np.ndarray Y data ND-array with shape equal ``xdata`` vector sizes weights : [*Optional*] list, tuple, Sequence[vector-like] Weights data vector(s) for all dimensions or each dimension with size(s) equal to ``xdata`` sizes smooth : [*Optional*] float, Sequence[float] The smoothing parameter (or a sequence of parameters for each dimension) in range ``[0, 1]`` where: - 0: The smoothing spline is the least-squares straight line fit - 1: The cubic spline interpolant with natural condition normalizedsmooth : [*Optional*] bool If True, the smooth parameter is normalized such that results are invariant to xdata range and less sensitive to nonuniformity of weights and xdata clumping .. versionadded:: 1.1.0 """ __module__ = 'csaps' def __init__( self, xdata: NdGridDataType, ydata: np.ndarray, weights: Optional[Union[UnivariateDataType, NdGridDataType]] = None, smooth: Optional[Union[float, Sequence[Optional[float]]]] = None, normalizedsmooth: bool = False, ) -> None: x, y, w, s = self._prepare_data(xdata, ydata, weights, smooth) coeffs, smooth = self._make_spline(x, y, w, s, normalizedsmooth) self._spline = NdGridSplinePPForm.construct_fast(coeffs, x) self._smooth = smooth def __call__( self, x: Union[NdGridDataType, Sequence[Number]], nu: Optional[Tuple[int, ...]] = None, extrapolate: Optional[bool] = None, ) -> np.ndarray: """Evaluate the spline for given data Parameters ---------- x : tuple of 1-d array-like The tuple of point values for each dimension to evaluate the spline at. nu : [*Optional*] tuple of int Orders of derivatives to evaluate. Each must be non-negative. extrapolate : [*Optional*] bool Whether to extrapolate to out-of-bounds points based on first and last intervals, or to return NaNs. Returns ------- y : array-like Interpolated values. Shape is determined by replacing the interpolation axis in the original array with the shape of x. """ return self._spline(x, nu=nu, extrapolate=extrapolate) @property def smooth(self) -> Tuple[float, ...]: """Returns a tuple of smoothing parameters for each axis Returns ------- smooth : Tuple[float, ...] The smoothing parameter in the range ``[0, 1]`` for each axis """ return self._smooth @property def spline(self) -> NdGridSplinePPForm: """Returns the spline description in 'NdGridSplinePPForm' instance Returns ------- spline : NdGridSplinePPForm The spline description in :class:`NdGridSplinePPForm` instance """ return self._spline @classmethod def _prepare_data(cls, xdata, ydata, weights, smooth): xdata = ndgrid_prepare_data_vectors(xdata, 'xdata') ydata = np.asarray(ydata) data_ndim = len(xdata) if ydata.ndim != data_ndim: raise ValueError(f"'ydata' must have dimension {data_ndim} according to 'xdata'") for axis, (yd, xs) in enumerate(zip(ydata.shape, map(len, xdata))): if yd != xs: raise ValueError(f"'ydata' ({yd}) and xdata ({xs}) sizes mismatch for axis {axis}") if not weights: weights = [None] * data_ndim else: weights = ndgrid_prepare_data_vectors(weights, 'weights') if len(weights) != data_ndim: raise ValueError(f"'weights' ({len(weights)}) and 'xdata' ({data_ndim}) dimensions mismatch") for axis, (w, x) in enumerate(zip(weights, xdata)): if w is not None: if w.size != x.size: raise ValueError(f"'weights' ({w.size}) and 'xdata' ({x.size}) sizes mismatch for axis {axis}") if smooth is None: smooth = [None] * data_ndim if not isinstance(smooth, c_abc.Sequence): smooth = [float(smooth)] * data_ndim else: smooth = list(smooth) if len(smooth) != data_ndim: raise ValueError( 'Number of smoothing parameter values must ' f'be equal number of dimensions ({data_ndim})' ) return xdata, ydata, weights, smooth @staticmethod def _make_spline(xdata, ydata, weights, smooth, normalizedsmooth): ndim = len(xdata) if ndim == 1: s = CubicSmoothingSpline( xdata[0], ydata, weights=weights[0], smooth=smooth[0], normalizedsmooth=normalizedsmooth, ) return s.spline.coeffs, (s.smooth,) shape = ydata.shape coeffs = ydata coeffs_shape = list(shape) smooths = [] permute_axes = (ndim - 1, *range(ndim - 1)) # computing coordinatewise smoothing spline for i in reversed(range(ndim)): if ndim > 2: coeffs = coeffs.reshape(prod(coeffs.shape[:-1]), coeffs.shape[-1]) s = CubicSmoothingSpline( xdata[i], coeffs, weights=weights[i], smooth=smooth[i], normalizedsmooth=normalizedsmooth, ) smooths.append(s.smooth) coeffs = umv_coeffs_to_flatten(s.spline.coeffs) if ndim > 2: coeffs_shape[-1] = s.spline.pieces * s.spline.order coeffs = coeffs.reshape(coeffs_shape) coeffs = coeffs.transpose(permute_axes) coeffs_shape = list(coeffs.shape) pieces = tuple(int(size - 1) for size in shape) coeffs = ndg_coeffs_to_canonical(coeffs.squeeze(), pieces) return coeffs, tuple(reversed(smooths)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1719750703.8210914 csaps-1.2.0/csaps/_sspumv.py0000644000000000000000000002431714640250060012734 0ustar00""" Univariate/multivariate cubic smoothing spline implementation """ from typing import List, Optional, Tuple, Union import functools import numpy as np from scipy.interpolate import PPoly import scipy.sparse as sp import scipy.sparse.linalg as la from ._base import ISmoothingSpline, ISplinePPForm from ._reshape import prod, to_2d from ._types import MultivariateDataType, UnivariateDataType class SplinePPForm(ISplinePPForm[np.ndarray, int], PPoly): """The base class for univariate/multivariate spline in piecewise polynomial form Piecewise polynomial in terms of coefficients and breakpoints. Notes ----- Inherited from :py:class:`scipy.interpolate.PPoly` """ __module__ = 'csaps' @property def breaks(self) -> np.ndarray: return self.x @property def coeffs(self) -> np.ndarray: return self.c @property def order(self) -> int: return self.c.shape[0] @property def pieces(self) -> int: return self.c.shape[1] @property def ndim(self) -> int: """Returns the number of spline dimensions The number of dimensions is product of shape without ``shape[self.axis]``. """ shape = list(self.shape) shape.pop(self.axis) return prod(shape) @property def shape(self) -> Tuple[int, ...]: """Returns the source data shape""" shape: List[int] = list(self.c.shape[2:]) shape.insert(self.axis, self.c.shape[1] + 1) return tuple(shape) def __repr__(self): # pragma: no cover return ( f'{type(self).__name__}\n' f' breaks: {self.breaks}\n' f' coeffs shape: {self.coeffs.shape}\n' f' data shape: {self.shape}\n' f' axis: {self.axis}\n' f' pieces: {self.pieces}\n' f' order: {self.order}\n' f' ndim: {self.ndim}\n' ) class CubicSmoothingSpline(ISmoothingSpline[SplinePPForm, float, UnivariateDataType, int, Union[bool, str]]): """Cubic smoothing spline The cubic spline implementation for univariate/multivariate data. Parameters ---------- xdata : np.ndarray, sequence, vector-like X input 1-D data vector (data sites: ``x1 < x2 < ... < xN``) ydata : np.ndarray, vector-like, sequence[vector-like] Y input 1-D data vector or ND-array with shape[axis] equal of `xdata` size) weights : [*Optional*] np.ndarray, list Weights 1-D vector with size equal of ``xdata`` size smooth : [*Optional*] float Smoothing parameter in range [0, 1] where: - 0: The smoothing spline is the least-squares straight line fit - 1: The cubic spline interpolant with natural condition axis : [*Optional*] int Axis along which ``ydata`` is assumed to be varying. Meaning that for x[i] the corresponding values are np.take(ydata, i, axis=axis). By default is -1 (the last axis). normalizedsmooth : [*Optional*] bool If True, the smooth parameter is normalized such that results are invariant to xdata range and less sensitive to nonuniformity of weights and xdata clumping .. versionadded:: 1.1.0 """ __module__ = 'csaps' def __init__( self, xdata: UnivariateDataType, ydata: MultivariateDataType, weights: Optional[UnivariateDataType] = None, smooth: Optional[float] = None, axis: int = -1, normalizedsmooth: bool = False, ): x, y, w, shape, axis = self._prepare_data(xdata, ydata, weights, axis) coeffs, smooth = self._make_spline(x, y, w, smooth, shape, normalizedsmooth) spline = SplinePPForm.construct_fast(coeffs, x, axis=axis) self._smooth = smooth self._spline = spline def __call__( self, x: UnivariateDataType, nu: Optional[int] = None, extrapolate: Optional[Union[bool, str]] = None, ) -> np.ndarray: """Evaluate the spline for given data Parameters ---------- x : 1-d array-like Points to evaluate the spline at. nu : [*Optional*] int Order of derivative to evaluate. Must be non-negative. extrapolate : [*Optional*] bool or 'periodic' If bool, determines whether to extrapolate to out-of-bounds points based on first and last intervals, or to return NaNs. If 'periodic', periodic extrapolation is used. Default is True. Notes ----- Derivatives are evaluated piecewise for each polynomial segment, even if the polynomial is not differentiable at the breakpoints. The polynomial intervals are considered half-open, ``[a, b)``, except for the last interval which is closed ``[a, b]``. """ if nu is None: nu = 0 return self._spline(x, nu=nu, extrapolate=extrapolate) @property def smooth(self) -> float: """Returns the smoothing factor Returns ------- smooth : float Smoothing factor in the range [0, 1] """ return self._smooth @property def spline(self) -> SplinePPForm: """Returns the spline description in `SplinePPForm` instance Returns ------- spline : SplinePPForm The spline representation in :class:`SplinePPForm` instance """ return self._spline @staticmethod def _prepare_data(xdata, ydata, weights, axis): xdata = np.asarray(xdata, dtype=np.float64) ydata = np.asarray(ydata, dtype=np.float64) if xdata.ndim > 1: raise ValueError("'xdata' must be a vector") if xdata.size < 2: raise ValueError("'xdata' must contain at least 2 data points.") axis = ydata.ndim + axis if axis < 0 else axis if ydata.shape[axis] != xdata.size: raise ValueError( f"'ydata' data must be a 1-D or N-D array with shape[{axis}] " f"that is equal to 'xdata' size ({xdata.size})" ) # Rolling axis for using its shape while constructing coeffs array shape = np.rollaxis(ydata, axis).shape # Reshape ydata N-D array to 2-D NxM array where N is the data # dimension and M is the number of data points. ydata = to_2d(ydata, axis) if weights is None: weights = np.ones_like(xdata) else: weights = np.asarray(weights, dtype=np.float64) if weights.size != xdata.size: raise ValueError('Weights vector size must be equal of xdata size') return xdata, ydata, weights, shape, axis @staticmethod def _compute_smooth(a, b): """ The calculation of the smoothing spline requires the solution of a linear system whose coefficient matrix has the form p*A + (1-p)*B, with the matrices A and B depending on the data sites x. The default value of p makes p*trace(A) equal (1 - p)*trace(B). """ def trace(m: sp.dia_matrix): return m.diagonal().sum() return 1.0 / (1.0 + trace(a) / (6.0 * trace(b))) @staticmethod def _normalize_smooth(x: np.ndarray, w: np.ndarray, smooth: Optional[float]): """ See the explanation here: https://github.com/espdev/csaps/pull/47 """ span = np.ptp(x) eff_x = 1 + (span**2) / np.sum(np.diff(x) ** 2) eff_w = np.sum(w) ** 2 / np.sum(w**2) k = 80 * (span**3) * (x.size**-2) * (eff_x**-0.5) * (eff_w**-0.5) s = 0.5 if smooth is None else smooth p = s / (s + (1 - s) * k) return p @staticmethod def _make_spline(x, y, w, smooth, shape, normalizedsmooth): pcount = x.size dx = np.diff(x) if not all(dx > 0): # pragma: no cover raise ValueError("Items of 'xdata' vector must satisfy the condition: x1 < x2 < ... < xN") dy = np.diff(y, axis=1) dy_dx = dy / dx if pcount == 2: # The corner case for the data with 2 points (1 breaks interval) # In this case we have 2-ordered spline and linear interpolation in fact yi = y[:, 0][:, np.newaxis] c_shape = (2, pcount - 1) + shape[1:] c = np.vstack((dy_dx, yi)).reshape(c_shape) p = 1.0 return c, p # Create diagonal sparse matrices diags_r = np.vstack((dx[1:], 2 * (dx[1:] + dx[:-1]), dx[:-1])) r = sp.spdiags(diags_r, [-1, 0, 1], pcount - 2, pcount - 2) dx_recip = 1.0 / dx diags_qtw = np.vstack((dx_recip[:-1], -(dx_recip[1:] + dx_recip[:-1]), dx_recip[1:])) diags_sqrw_recip = 1.0 / np.sqrt(w) qtw = sp.diags(diags_qtw, [0, 1, 2], (pcount - 2, pcount)) @ sp.diags(diags_sqrw_recip, 0, (pcount, pcount)) qtw = qtw @ qtw.T p = smooth if normalizedsmooth: p = CubicSmoothingSpline._normalize_smooth(x, w, smooth) elif smooth is None: p = CubicSmoothingSpline._compute_smooth(r, qtw) pp = 6.0 * (1.0 - p) # Solve linear system for the 2nd derivatives a = pp * qtw + p * r b = np.diff(dy_dx, axis=1).T u = la.spsolve(a, b) if u.ndim < 2: u = u[np.newaxis] if y.shape[0] == 1: u = u.T dx = dx[:, np.newaxis] vpad = functools.partial(np.pad, pad_width=[(1, 1), (0, 0)], mode='constant') d1 = np.diff(vpad(u), axis=0) / dx d2 = np.diff(vpad(d1), axis=0) diags_w_recip = 1.0 / w w = sp.diags(diags_w_recip, 0, (pcount, pcount)) yi = y.T - (pp * w) @ d2 pu = vpad(p * u) c1 = np.diff(pu, axis=0) / dx c2 = 3.0 * pu[:-1, :] c3 = np.diff(yi, axis=0) / dx - dx * (2.0 * pu[:-1, :] + pu[1:, :]) c4 = yi[:-1, :] c_shape = (4, pcount - 1) + shape[1:] c = np.vstack((c1, c2, c3, c4)).reshape(c_shape) return c, p ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1719747936.656654 csaps-1.2.0/csaps/_types.py0000644000000000000000000000140214640242541012536 0ustar00""" Type-hints and type vars """ from typing import Sequence, Tuple, TypeVar, Union from collections import abc from numbers import Number from typing_extensions import TypeAlias import numpy as np UnivariateDataType: TypeAlias = Union[np.ndarray, Sequence[Number]] MultivariateDataType: TypeAlias = Union[np.ndarray, abc.Sequence] NdGridDataType: TypeAlias = Sequence[UnivariateDataType] TData = TypeVar('TData', np.ndarray, Sequence[np.ndarray]) TProps = TypeVar('TProps', int, Tuple[int, ...]) TSmooth = TypeVar('TSmooth', float, Tuple[float, ...]) TXi = TypeVar('TXi', UnivariateDataType, NdGridDataType) TNu = TypeVar('TNu', int, Tuple[int, ...]) TExtrapolate = TypeVar('TExtrapolate', bool, Union[bool, str]) TSpline = TypeVar('TSpline') ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1719747936.656654 csaps-1.2.0/csaps/_version.py0000644000000000000000000000027414640242541013065 0ustar00from importlib.metadata import PackageNotFoundError, version try: __version__ = version('csaps') except PackageNotFoundError: # pragma: no cover __version__ = '0.0.0.dev0' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1623967905.7971945 csaps-1.2.0/csaps/py.typed0000644000000000000000000000000014062744242012354 0ustar00././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1623967905.793195 csaps-1.2.0/LICENSE0000644000000000000000000000210514062744242010561 0ustar00MIT License Copyright (c) 2017 Eugene Prilepin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1719750703.8247902 csaps-1.2.0/pyproject.toml0000644000000000000000000000510614640250060012464 0ustar00[tool.poetry] name = "csaps" version = "1.2.0" description = "Cubic spline approximation (smoothing)" authors = ["Evgeny Prilepin "] license = "MIT" readme = "README.md" homepage = "https://github.com/espdev/csaps" repository = "https://github.com/espdev/csaps" documentation = "https://csaps.readthedocs.io" keywords = ["cubic", "spline", "approximation", "smoothing", "interpolation", "csaps"] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Mathematics", "Topic :: Software Development :: Libraries", "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", ] include = [ "LICENSE", "CHANGELOG.md", "CONTRIBUTORS.txt", ] packages = [ {include = "csaps"} ] [tool.poetry.dependencies] python = ">=3.9" typing-extensions = "*" numpy = [ {version = "*", python = "<3.12"}, {version = ">=1.26.2", python = ">=3.12"}, ] scipy = [ {version = "*", python = "<3.12"}, {version = ">=1.11.4", python = ">=3.12"}, ] [tool.poetry.group.dev.dependencies] setuptools = "^69.0.2" pytest = "^7.4.3" pytest-cov = "^4.1.0" m2r2 = "^0.3.2" docutils = "^0.20.0" sphinx = "^7.1.2" numpydoc = "^1.6.0" matplotlib = "^3.7.4" ruff = "^0.5.0" mypy = "^1.7.1" pre-commit = "^3.7.1" [build-system] requires = ["poetry-core", "setuptools"] build-backend = "poetry.core.masonry.api" [tool.ruff] target-version = "py39" line-length = 120 exclude = [ ".ruff_cache", ".venv", ] [tool.ruff.lint] select = [ "E", # All pycodestyle errors "W", # All pycodestyle warnings "F", # All Pyflakes errors "A", # All flake8-builtins "Q", # Quotes "T201", # print found "T203", # pprint found ] ignore = [] [tool.ruff.lint.flake8-quotes] docstring-quotes = "double" inline-quotes = "single" [tool.ruff.format] quote-style = "single" [tool.mypy] python_version = "3.9" [[tool.mypy.overrides]] module = [ "numpy", "scipy" ] ignore_missing_imports = true ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1719750703.816873 csaps-1.2.0/README.md0000644000000000000000000001016414640250060011027 0ustar00

csaps

PyPI version Supported Python versions GitHub Actions (Tests) Documentation Status Coverage Status License

**csaps** is a Python package for univariate, multivariate and n-dimensional grid data approximation using cubic smoothing splines. The package can be useful in practical engineering tasks for data approximation and smoothing. ## Installing Use pip for installing: ``` pip install -U csaps ``` The module depends only on NumPy and SciPy. Python 3.9 or above is supported. ## Simple Examples Here is a couple of examples of smoothing data. An univariate data smoothing: ```python import numpy as np import matplotlib.pyplot as plt from csaps import csaps np.random.seed(1234) x = np.linspace(-5., 5., 25) y = np.exp(-(x/2.5)**2) + (np.random.rand(25) - 0.2) * 0.3 xs = np.linspace(x[0], x[-1], 150) ys = csaps(x, y, xs, smooth=0.85) plt.plot(x, y, 'o', xs, ys, '-') plt.show() ```

univariate

A surface data smoothing: ```python import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D from csaps import csaps np.random.seed(1234) xdata = [np.linspace(-3, 3, 41), np.linspace(-3.5, 3.5, 31)] i, j = np.meshgrid(*xdata, indexing='ij') ydata = (3 * (1 - j)**2. * np.exp(-(j**2) - (i + 1)**2) - 10 * (j / 5 - j**3 - i**5) * np.exp(-j**2 - i**2) - 1 / 3 * np.exp(-(j + 1)**2 - i**2)) ydata = ydata + (np.random.randn(*ydata.shape) * 0.75) ydata_s = csaps(xdata, ydata, xdata, smooth=0.988) fig = plt.figure(figsize=(7, 4.5)) ax = fig.add_subplot(111, projection='3d') ax.set_facecolor('none') c = [s['color'] for s in plt.rcParams['axes.prop_cycle']] ax.plot_wireframe(j, i, ydata, linewidths=0.5, color=c[0], alpha=0.5) ax.scatter(j, i, ydata, s=10, c=c[0], alpha=0.5) ax.plot_surface(j, i, ydata_s, color=c[1], linewidth=0, alpha=1.0) ax.view_init(elev=9., azim=290) plt.show() ```

surface

## Documentation More examples of usage and the full documentation can be found at https://csaps.readthedocs.io. ## Testing We use pytest for testing. ``` cd /path/to/csaps/project/directory pip install -e .[tests] pytest ``` ## Algorithm and Implementation **csaps** Python package is inspired by MATLAB [CSAPS](https://www.mathworks.com/help/curvefit/csaps.html) function that is an implementation of Fortran routine SMOOTH from [PGS](http://pages.cs.wisc.edu/~deboor/pgs/) (originally written by Carl de Boor). Also the algothithm implementation in other languages: * [csaps-rs](https://github.com/espdev/csaps-rs) Rust ndarray/sprs based implementation * [csaps-cpp](https://github.com/espdev/csaps-cpp) C++11 Eigen based implementation (incomplete) ## References C. de Boor, A Practical Guide to Splines, Springer-Verlag, 1978. ## License [MIT](https://choosealicense.com/licenses/mit/) csaps-1.2.0/PKG-INFO0000644000000000000000000001270500000000000010614 0ustar00Metadata-Version: 2.1 Name: csaps Version: 1.2.0 Summary: Cubic spline approximation (smoothing) Home-page: https://github.com/espdev/csaps License: MIT Keywords: cubic,spline,approximation,smoothing,interpolation,csaps Author: Evgeny Prilepin Author-email: esp.home@gmail.com Requires-Python: >=3.9 Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Scientific/Engineering Classifier: Topic :: Scientific/Engineering :: Mathematics Classifier: Topic :: Software Development :: Libraries Requires-Dist: numpy (>=1.26.2) ; python_version >= "3.12" Requires-Dist: numpy ; python_version < "3.12" Requires-Dist: scipy (>=1.11.4) ; python_version >= "3.12" Requires-Dist: scipy ; python_version < "3.12" Requires-Dist: typing-extensions Project-URL: Documentation, https://csaps.readthedocs.io Project-URL: Repository, https://github.com/espdev/csaps Description-Content-Type: text/markdown

csaps

PyPI version Supported Python versions GitHub Actions (Tests) Documentation Status Coverage Status License

**csaps** is a Python package for univariate, multivariate and n-dimensional grid data approximation using cubic smoothing splines. The package can be useful in practical engineering tasks for data approximation and smoothing. ## Installing Use pip for installing: ``` pip install -U csaps ``` The module depends only on NumPy and SciPy. Python 3.9 or above is supported. ## Simple Examples Here is a couple of examples of smoothing data. An univariate data smoothing: ```python import numpy as np import matplotlib.pyplot as plt from csaps import csaps np.random.seed(1234) x = np.linspace(-5., 5., 25) y = np.exp(-(x/2.5)**2) + (np.random.rand(25) - 0.2) * 0.3 xs = np.linspace(x[0], x[-1], 150) ys = csaps(x, y, xs, smooth=0.85) plt.plot(x, y, 'o', xs, ys, '-') plt.show() ```

univariate

A surface data smoothing: ```python import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D from csaps import csaps np.random.seed(1234) xdata = [np.linspace(-3, 3, 41), np.linspace(-3.5, 3.5, 31)] i, j = np.meshgrid(*xdata, indexing='ij') ydata = (3 * (1 - j)**2. * np.exp(-(j**2) - (i + 1)**2) - 10 * (j / 5 - j**3 - i**5) * np.exp(-j**2 - i**2) - 1 / 3 * np.exp(-(j + 1)**2 - i**2)) ydata = ydata + (np.random.randn(*ydata.shape) * 0.75) ydata_s = csaps(xdata, ydata, xdata, smooth=0.988) fig = plt.figure(figsize=(7, 4.5)) ax = fig.add_subplot(111, projection='3d') ax.set_facecolor('none') c = [s['color'] for s in plt.rcParams['axes.prop_cycle']] ax.plot_wireframe(j, i, ydata, linewidths=0.5, color=c[0], alpha=0.5) ax.scatter(j, i, ydata, s=10, c=c[0], alpha=0.5) ax.plot_surface(j, i, ydata_s, color=c[1], linewidth=0, alpha=1.0) ax.view_init(elev=9., azim=290) plt.show() ```

surface

## Documentation More examples of usage and the full documentation can be found at https://csaps.readthedocs.io. ## Testing We use pytest for testing. ``` cd /path/to/csaps/project/directory pip install -e .[tests] pytest ``` ## Algorithm and Implementation **csaps** Python package is inspired by MATLAB [CSAPS](https://www.mathworks.com/help/curvefit/csaps.html) function that is an implementation of Fortran routine SMOOTH from [PGS](http://pages.cs.wisc.edu/~deboor/pgs/) (originally written by Carl de Boor). Also the algothithm implementation in other languages: * [csaps-rs](https://github.com/espdev/csaps-rs) Rust ndarray/sprs based implementation * [csaps-cpp](https://github.com/espdev/csaps-cpp) C++11 Eigen based implementation (incomplete) ## References C. de Boor, A Practical Guide to Splines, Springer-Verlag, 1978. ## License [MIT](https://choosealicense.com/licenses/mit/)