csaps-1.3.3/CHANGELOG.md0000644000000000000000000001023000000000000011323 0ustar00# Changelog ## v1.3.3 (07.09.2025) * Fix spsolve warning (set diag sparse matrix type to CSR to fix the warning) * Add extrapolation section to tutorial documentation * Formatting code, type-hints (internal) ## v1.3.2 (15.04.2025) * Remove `docs` extra dependencies from the package * Refresh the documentation with Furo theme ## v1.3.1 (14.04.2025) * Update readme and docs ## v1.3.0 (14.04.2025) * Bump minimal Python version to 3.10 * Fix type annotations * Fix checking types by mypy ## v1.2.1 (10.04.2025) * Update dependencies * Update the package classifiers ## v1.2.0 (30.06.2024) * Bump minimal Python version to 3.9 * Use ruff as the code linter and formatter * Update dependencies ## 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 csaps-1.3.3/CONTRIBUTORS.txt0000644000000000000000000000003700000000000012214 0ustar00Eugene Prilepin Shamus Husheer csaps-1.3.3/LICENSE0000644000000000000000000000206000000000000010521 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. csaps-1.3.3/README.md0000644000000000000000000001045600000000000011003 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 ``` or Poetry: ``` poetry add csaps ``` The module depends only on NumPy and SciPy. Python 3.10 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. ## Development We use Poetry to manage the project: ``` git clone https://github.com/espdev/csaps.git cd csaps poetry install -E docs ``` Also, install pre-commit hooks: ``` poetry run pre-commit install ``` ## Testing and Linting We use pytest for testing and ruff/mypy for linting. Use `poethepoet` to run tests and linters: ``` poetry run poe test poetry run poe check ``` ## 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.3.3/csaps/__init__.py0000644000000000000000000000142300000000000012740 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, SequenceUnivariateDataType, UnivariateDataType from csaps._version import __version__ __all__ = [ # Shortcut 'csaps', 'AutoSmoothingResult', # Classes 'ISplinePPForm', 'ISmoothingSpline', 'SplinePPForm', 'NdGridSplinePPForm', 'CubicSmoothingSpline', 'NdGridCubicSmoothingSpline', # Type-hints 'UnivariateDataType', 'MultivariateDataType', 'SequenceUnivariateDataType', '__version__', ] csaps-1.3.3/csaps/_base.py0000644000000000000000000000445700000000000012264 0ustar00""" The base classes and interfaces """ from typing import Generic import abc import numpy as np from ._types import FloatNDArrayType, 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, 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 : Union[int, Tuple[int, ...]] The spline order """ @property @abc.abstractmethod def pieces(self) -> TProps: """Returns the spline pieces data Returns ------- pieces : Union[int, 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: TNu | None = None, extrapolate: TExtrapolate | None = None, ) -> FloatNDArrayType: """Evaluates spline on the data sites""" csaps-1.3.3/csaps/_reshape.py0000644000000000000000000001241000000000000012765 0ustar00import 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: int = arr.shape[0] order: int = arr.shape[1] // pieces shape: tuple[int, ...] strides: tuple[int, ...] 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: 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) csaps-1.3.3/csaps/_shortcut.py0000644000000000000000000001647500000000000013230 0ustar00""" The module provided `csaps` shortcut function for smoothing data """ from typing import NamedTuple, Sequence, overload import numpy as np from ._base import ISmoothingSpline from ._sspndg import NdGridCubicSmoothingSpline from ._sspumv import CubicSmoothingSpline from ._types import MultivariateDataType, SequenceUnivariateDataType, UnivariateDataType class AutoSmoothingResult(NamedTuple): """The result for auto smoothing for `csaps` function""" values: MultivariateDataType """Smoothed data values""" smooth: float | Sequence[float | None] """The calculated smoothing parameter""" # ************************************** # csaps signatures # @overload def csaps( xdata: UnivariateDataType, ydata: MultivariateDataType, *, weights: UnivariateDataType | None = None, smooth: float | None = None, axis: int | None = None, normalizedsmooth: bool = False, ) -> ISmoothingSpline: # pragma: no cover ... @overload def csaps( xdata: UnivariateDataType, ydata: MultivariateDataType, xidata: UnivariateDataType, *, weights: UnivariateDataType | None = None, axis: int | None = None, normalizedsmooth: bool = False, ) -> AutoSmoothingResult: # pragma: no cover ... @overload def csaps( xdata: UnivariateDataType, ydata: MultivariateDataType, xidata: UnivariateDataType, *, smooth: float, weights: UnivariateDataType | None = None, axis: int | None = None, normalizedsmooth: bool = False, ) -> MultivariateDataType: # pragma: no cover ... @overload def csaps( xdata: SequenceUnivariateDataType, ydata: MultivariateDataType, *, weights: SequenceUnivariateDataType | None = None, smooth: Sequence[float | None] | None = None, axis: int | None = None, normalizedsmooth: bool = False, ) -> ISmoothingSpline: # pragma: no cover ... @overload def csaps( xdata: SequenceUnivariateDataType, ydata: MultivariateDataType, xidata: SequenceUnivariateDataType, *, weights: SequenceUnivariateDataType | None = None, axis: int | None = None, normalizedsmooth: bool = False, ) -> AutoSmoothingResult: # pragma: no cover ... @overload def csaps( xdata: SequenceUnivariateDataType, ydata: MultivariateDataType, xidata: SequenceUnivariateDataType, *, smooth: Sequence[float | None], weights: SequenceUnivariateDataType | None = None, axis: int | None = None, normalizedsmooth: bool = False, ) -> MultivariateDataType: # pragma: no cover ... # ************************************** # csaps implementation def csaps( xdata, ydata, xidata=None, *, weights=None, smooth=None, axis=None, normalizedsmooth=False, ): """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 """ umv = True if isinstance(xdata, Sequence): if len(xdata) and isinstance(xdata[0], (np.ndarray, Sequence)): umv = False if umv: axis = -1 if axis is None else axis sp = CubicSmoothingSpline( xdata=xdata, ydata=ydata, weights=weights, smooth=smooth, axis=axis, normalizedsmooth=normalizedsmooth, ) else: sp = NdGridCubicSmoothingSpline( xdata=xdata, ydata=ydata, weights=weights, smooth=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 csaps-1.3.3/csaps/_sspndg.py0000644000000000000000000002560100000000000012642 0ustar00""" ND-Gridded cubic smoothing spline implementation """ from typing import Optional, Sequence, cast 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 Float1DArrayTupe, FloatNDArrayType, SequenceUnivariateDataType def ndgrid_prepare_data_vectors( data: SequenceUnivariateDataType, name: str, min_size: int = 2, ) -> tuple[Float1DArrayTupe, ...]: if not isinstance(data, Sequence): raise TypeError(f"'{name}' must be a sequence of 1-d array-like (vectors) or scalars.") data_: list[Float1DArrayTupe] = [] 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_.append(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__( # type: ignore[override] self, x: SequenceUnivariateDataType, nu: Optional[tuple[int, ...]] = None, extrapolate: Optional[bool] = None, ) -> np.ndarray: """Evaluate the spline for given data Parameters ---------- x : Sequence of 1-d array-like The sequence 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, ...], SequenceUnivariateDataType, 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: SequenceUnivariateDataType, ydata: np.ndarray, weights: SequenceUnivariateDataType | None = None, smooth: float | Sequence[float | None] | None = None, normalizedsmooth: bool = False, ) -> None: x, y, w, s = self._prepare_data(xdata, ydata, weights, smooth) coeffs, self._smooth = self._make_spline(x, y, w, s, normalizedsmooth) self._spline = cast(NdGridSplinePPForm, NdGridSplinePPForm.construct_fast(coeffs, x)) def __call__( self, x: SequenceUnivariateDataType, nu: tuple[int, ...] | None = None, extrapolate: bool | None = None, ) -> FloatNDArrayType: """Evaluate the spline for given data Parameters ---------- x : Sequence of 1-d array-like The sequence 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, Sequence): smooth = [float(smooth)] * data_ndim else: smooth = list(smooth) if len(smooth) != data_ndim: raise ValueError(f'Number of smoothing parameter values must 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)) csaps-1.3.3/csaps/_sspumv.py0000644000000000000000000002400000000000000012671 0ustar00""" Univariate/multivariate cubic smoothing spline implementation """ from typing import Literal, cast from functools import partial 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 FloatNDArrayType, MultivariateDataType, UnivariateDataType diags_csr = partial(sp.diags, format='csr') vpad = partial(np.pad, pad_width=[(1, 1), (0, 0)], mode='constant') 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) -> str: # 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, bool | Literal['periodic'], ] ): """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, it 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: UnivariateDataType | None = None, smooth: float | None = None, axis: int = -1, normalizedsmooth: bool = False, ) -> None: x, y, w, shape, axis = self._prepare_data(xdata, ydata, weights, axis) coeffs, self._smooth = self._make_spline(x, y, w, smooth, shape, normalizedsmooth) self._spline = cast(SplinePPForm, SplinePPForm.construct_fast(coeffs, x, axis=axis)) def __call__( self, x: UnivariateDataType, nu: int | None = None, extrapolate: bool | Literal['periodic'] | None = None, ) -> FloatNDArrayType: """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: float | None) -> 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, format='csr') 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 = diags_csr(diags_qtw, [0, 1, 2], (pcount - 2, pcount)) @ diags_csr(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] d1 = np.diff(vpad(u), axis=0) / dx d2 = np.diff(vpad(d1), axis=0) diags_w_recip = 1.0 / w w = diags_csr(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 csaps-1.3.3/csaps/_types.py0000644000000000000000000000144500000000000012510 0ustar00""" Type-hints and type vars """ from typing import Annotated, Literal, Sequence, TypeVar, Union from typing_extensions import TypeAlias import numpy as np from numpy.typing import NDArray FloatDType: TypeAlias = Union[np.float32, np.float64] FloatNDArrayType: TypeAlias = NDArray[FloatDType] Float1DArrayTupe: TypeAlias = Annotated[FloatNDArrayType, Literal['N']] UnivariateDataType: TypeAlias = Union[Float1DArrayTupe, Sequence[float]] MultivariateDataType: TypeAlias = Union[FloatNDArrayType, Sequence[Union[float, UnivariateDataType]]] SequenceUnivariateDataType: TypeAlias = Sequence[UnivariateDataType] TData = TypeVar('TData') TProps = TypeVar('TProps') TSmooth = TypeVar('TSmooth') TXi = TypeVar('TXi') TNu = TypeVar('TNu') TExtrapolate = TypeVar('TExtrapolate') TSpline = TypeVar('TSpline') csaps-1.3.3/csaps/_version.py0000644000000000000000000000026600000000000013031 0ustar00from importlib.metadata import PackageNotFoundError, version try: __version__ = version('csaps') except PackageNotFoundError: # pragma: no cover __version__ = '0.0.0.dev0' csaps-1.3.3/csaps/py.typed0000644000000000000000000000000000000000000012314 0ustar00csaps-1.3.3/pyproject.toml0000644000000000000000000000630100000000000012432 0ustar00[tool.poetry] name = "csaps" version = "1.3.3" 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.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", ] include = [ "LICENSE", "CHANGELOG.md", "CONTRIBUTORS.txt", ] packages = [ {include = "csaps"} ] [tool.poetry.dependencies] python = ">=3.10" 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 = "^78.1.0" pytest = "^8.3.5" pytest-cov = "^6.1.1" ruff = "^0.11.5" mypy = "^1.15.0" scipy-stubs = "*" pre-commit = "^4.2.0" poethepoet = "^0.33.1" [tool.poetry.group.docs.dependencies] sphinx = "^7.1.2" docutils = "^0.20.0" furo = "^2024.8.6" numpydoc = "^1.6.0" m2r2 = "^0.3.2" matplotlib = "^3.7.4" [build-system] requires = ["poetry-core", "setuptools"] build-backend = "poetry.core.masonry.api" [tool.ruff] target-version = "py310" 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 "I", # Sort imports "T201", # print found "T203", # pprint found ] ignore = [] [tool.ruff.lint.flake8-quotes] docstring-quotes = "double" inline-quotes = "single" [tool.ruff.lint.isort] force-to-top = ["typing", "typing_extensions", "pytest"] force-sort-within-sections = true [tool.ruff.format] quote-style = "single" [tool.mypy] python_version = "3.10" [tool.poe.tasks] test = "pytest -v " test-cov = "pytest --cov=csaps" test-ci = "pytest -v --color=yes --cov=csaps --cov-report=term --cov-report=lcov:coverage.info" check-format-pre-commit = "ruff format --check" check-lint-pre-commit = "ruff check" check-types-pre-commit = "mypy" check-format = "ruff format . --check" check-lint = "ruff check ." check-types = "mypy -p csaps" check = ["check-format", "check-lint", "check-types"] fix-format = "ruff format ." fix-lint = "ruff check --fix ." fix = ["fix-format", "fix-lint"] docs = "sphinx-build docs/ docs/_build/ --builder html" csaps-1.3.3/PKG-INFO0000644000000000000000000001340600000000000010617 0ustar00Metadata-Version: 2.3 Name: csaps Version: 1.3.3 Summary: Cubic spline approximation (smoothing) License: MIT Keywords: cubic,spline,approximation,smoothing,interpolation,csaps Author: Evgeny Prilepin Author-email: esp.home@gmail.com Requires-Python: >=3.10 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.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 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: Homepage, https://github.com/espdev/csaps 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 ``` or Poetry: ``` poetry add csaps ``` The module depends only on NumPy and SciPy. Python 3.10 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. ## Development We use Poetry to manage the project: ``` git clone https://github.com/espdev/csaps.git cd csaps poetry install -E docs ``` Also, install pre-commit hooks: ``` poetry run pre-commit install ``` ## Testing and Linting We use pytest for testing and ruff/mypy for linting. Use `poethepoet` to run tests and linters: ``` poetry run poe test poetry run poe check ``` ## 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/)