diff --git a/.travis.yml b/.travis.yml index 861dbb4..19142c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,6 @@ language: python matrix: include: - - python: "3.5" - env: TOXENV=py35-pytest - python: "3.6" env: TOXENV=py36-pytest - python: "3.7" diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ce48e9..60d23ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v0.9.0 + +* 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 * Add `csaps` function that can be used as the main API diff --git a/README.md b/README.md index ccbb7ec..171f8fe 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ ## Installation -Python 3.5 or above is supported. +Python 3.6 or above is supported. ``` pip install -U csaps diff --git a/csaps/__init__.py b/csaps/__init__.py index 89758a8..adc8bd9 100644 --- a/csaps/__init__.py +++ b/csaps/__init__.py @@ -13,6 +13,7 @@ ) from csaps._sspumv import ( SplinePPForm, + CubicSmoothingSpline, UnivariateCubicSmoothingSpline, MultivariateCubicSmoothingSpline, ) @@ -38,6 +39,7 @@ 'ISmoothingSpline', 'SplinePPForm', 'NdGridSplinePPForm', + 'CubicSmoothingSpline', 'UnivariateCubicSmoothingSpline', 'MultivariateCubicSmoothingSpline', 'NdGridCubicSmoothingSpline', diff --git a/csaps/_base.py b/csaps/_base.py index 25bc3f9..f6e8b21 100644 --- a/csaps/_base.py +++ b/csaps/_base.py @@ -10,7 +10,7 @@ import numpy as np -from csaps._types import TData, TProps, TSmooth, TXi, TSpline +from ._types import TData, TProps, TSmooth, TXi, TSpline class SplinePPFormBase(abc.ABC, ty.Generic[TData, TProps]): @@ -87,17 +87,15 @@ def evaluate(self, xi: TData) -> np.ndarray: Interpolated/smoothed data """ - def __repr__(self): + def __repr__(self): # pragma: no cover return ( - '{}\n' - ' breaks: {}\n' - ' coeffs: {} shape\n' - ' pieces: {}\n' - ' order: {}\n' - ' ndim: {}\n' - ).format( - type(self).__name__, self.breaks, self.coeffs.shape, - self.pieces, self.order, self.ndim) + f'{type(self).__name__}\n' + f' breaks: {self.breaks}\n' + f' coeffs: {self.coeffs.shape} shape\n' + f' pieces: {self.pieces}\n' + f' order: {self.order}\n' + f' ndim: {self.ndim}\n' + ) class ISmoothingSpline(abc.ABC, ty.Generic[TSpline, TSmooth, TXi]): diff --git a/csaps/_utils.py b/csaps/_reshape.py similarity index 92% rename from csaps/_utils.py rename to csaps/_reshape.py index c5c71e0..893a160 100644 --- a/csaps/_utils.py +++ b/csaps/_reshape.py @@ -64,8 +64,8 @@ def to_2d(arr: np.ndarray, axis: int) -> np.ndarray: arr = np.asarray(arr) axis = arr.ndim + axis if axis < 0 else axis - if axis >= arr.ndim: - raise ValueError('axis {} is out of array axes {}'.format(axis, arr.ndim)) + 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) @@ -135,8 +135,8 @@ def from_2d(arr: np.ndarray, shape: ty.Sequence[int], axis: int) -> np.ndarray: ndim = len(shape) axis = ndim + axis if axis < 0 else axis - if axis >= ndim: - raise ValueError('axis {} is out of N-D array axes {}'.format(axis, ndim)) + if axis >= ndim: # pragma: no cover + raise ValueError(f'axis {axis} is out of N-D array axes {ndim}') new_shape = list(shape) new_shape.pop(axis) diff --git a/csaps/_shortcut.py b/csaps/_shortcut.py index d42c7d5..fee3b53 100644 --- a/csaps/_shortcut.py +++ b/csaps/_shortcut.py @@ -10,10 +10,10 @@ import numpy as np -from csaps._base import ISmoothingSpline -from csaps._sspumv import UnivariateCubicSmoothingSpline -from csaps._sspndg import ndgrid_prepare_data_sites, NdGridCubicSmoothingSpline -from csaps._types import ( +from ._base import ISmoothingSpline +from ._sspumv import CubicSmoothingSpline +from ._sspndg import ndgrid_prepare_data_sites, NdGridCubicSmoothingSpline +from ._types import ( UnivariateDataType, UnivariateVectorizedDataType, NdGridDataType, @@ -25,11 +25,16 @@ _WeightsDataType = Optional[Union[UnivariateDataType, NdGridDataType]] _SmoothDataType = Optional[Union[float, Sequence[Optional[float]]]] -AutoSmoothingResult = NamedTuple('AutoSmoothingResult', [ - ('values', _YDataType), - ('smooth', _SmoothDataType), -]) -"""The result for auto smoothing for `csaps` function""" + +class AutoSmoothingResult(NamedTuple): + """The result for auto smoothing for `csaps` function""" + + values: _YDataType + """Smoothed data values""" + + smooth: _SmoothDataType + """The calculated smoothing parameter""" + _ReturnType = Union[ _YDataType, @@ -41,6 +46,7 @@ def csaps(xdata: _XDataType, ydata: _YDataType, xidata: _XiDataType = None, + *, weights: _WeightsDataType = None, smooth: _SmoothDataType = None, axis: Optional[int] = None) -> _ReturnType: @@ -105,7 +111,7 @@ def csaps(xdata: _XDataType, ssp_obj : ISmoothingSpline Smoothing spline object if ``xidata`` was not set: - - :class:`UnivariateCubicSmoothingSpline` instance for univariate/multivariate data + - :class:`CubicSmoothingSpline` instance for univariate/multivariate data - :class:`NdGridCubicSmoothingSpline` instance for nd-gridded data Examples @@ -134,7 +140,7 @@ def csaps(xdata: _XDataType, See Also -------- - UnivariateCubicSmoothingSpline + CubicSmoothingSpline NdGridCubicSmoothingSpline """ @@ -151,7 +157,7 @@ def csaps(xdata: _XDataType, if umv: axis = -1 if axis is None else axis - sp = UnivariateCubicSmoothingSpline(xdata, ydata, weights, smooth, axis) + sp = CubicSmoothingSpline(xdata, ydata, weights, smooth, axis) else: sp = NdGridCubicSmoothingSpline(xdata, ydata, weights, smooth) diff --git a/csaps/_sspndg.py b/csaps/_sspndg.py index e339931..a91fe80 100644 --- a/csaps/_sspndg.py +++ b/csaps/_sspndg.py @@ -10,24 +10,23 @@ import numpy as np -from csaps._base import SplinePPFormBase, ISmoothingSpline -from csaps._types import UnivariateDataType, NdGridDataType -from csaps._sspumv import SplinePPForm, UnivariateCubicSmoothingSpline +from ._base import SplinePPFormBase, ISmoothingSpline +from ._types import UnivariateDataType, NdGridDataType +from ._sspumv import SplinePPForm, CubicSmoothingSpline def ndgrid_prepare_data_sites(data, name) -> ty.Tuple[np.ndarray, ...]: if not isinstance(data, c_abc.Sequence): - raise TypeError("'{}' must be a sequence of the vectors.".format(name)) + raise TypeError(f"'{name}' must be a sequence of the vectors.") data = list(data) for i, di in enumerate(data): di = np.array(di, dtype=np.float64) if di.ndim > 1: - raise ValueError("All '{}' elements must be a vector.".format(name)) + raise ValueError(f"All '{name}' elements must be a vector.") if di.size < 2: - raise ValueError( - "'{}' must contain at least 2 data points.".format(name)) + raise ValueError(f"'{name}' must contain at least 2 data points.") data[i] = di return tuple(data) @@ -165,13 +164,11 @@ def _prepare_data(cls, xdata, ydata, weights, smooth): data_ndim = len(xdata) if ydata.ndim != data_ndim: - raise ValueError( - 'ydata must have dimension {} according to xdata'.format(data_ndim)) + raise ValueError(f'ydata must have dimension {data_ndim} according to xdata') for yd, xs in zip(ydata.shape, map(len, xdata)): if yd != xs: - raise ValueError( - 'ydata ({}) and xdata ({}) dimension size mismatch'.format(yd, xs)) + raise ValueError(f'ydata ({yd}) and xdata ({xs}) dimension size mismatch') if not weights: weights = [None] * data_ndim @@ -179,14 +176,12 @@ def _prepare_data(cls, xdata, ydata, weights, smooth): weights = ndgrid_prepare_data_sites(weights, 'weights') if len(weights) != data_ndim: - raise ValueError( - 'weights ({}) and xdata ({}) dimensions mismatch'.format(len(weights), data_ndim)) + raise ValueError(f'weights ({len(weights)}) and xdata ({data_ndim}) dimensions mismatch') for w, x in zip(weights, xdata): if w is not None: if w.size != x.size: - raise ValueError( - 'weights ({}) and xdata ({}) dimension size mismatch'.format(w, x)) + raise ValueError(f'weights ({w}) and xdata ({x}) dimension size mismatch') if not smooth: smooth = [None] * data_ndim @@ -198,8 +193,8 @@ def _prepare_data(cls, xdata, ydata, weights, smooth): if len(smooth) != data_ndim: raise ValueError( - 'Number of smoothing parameter values must be equal ' - 'number of dimensions ({})'.format(data_ndim)) + f'Number of smoothing parameter values must ' + f'be equal number of dimensions ({data_ndim})') return xdata, ydata, weights, smooth @@ -208,9 +203,8 @@ def __call__(self, xi: NdGridDataType) -> np.ndarray: """ xi = ndgrid_prepare_data_sites(xi, 'xi') - if len(xi) != self._ndim: - raise ValueError( - 'xi ({}) and xdata ({}) dimensions mismatch'.format(len(xi), self._ndim)) + if len(xi) != self._ndim: # pragma: no cover + raise ValueError(f'xi ({len(xi)}) and xdata ({self._ndim}) dimensions mismatch') return self._spline.evaluate(xi) @@ -224,8 +218,8 @@ def _make_spline(self, smooth: ty.List[ty.Optional[float]]) -> ty.Tuple[NdGridSp shape_i = (np.prod(sizey[:-1]), sizey[-1]) ydata_i = ydata.reshape(shape_i, order='F') - s = UnivariateCubicSmoothingSpline( - self._xdata[i], ydata_i, self._weights[i], smooth[i]) + s = CubicSmoothingSpline( + self._xdata[i], ydata_i, weights=self._weights[i], smooth=smooth[i]) _smooth.append(s.smooth) sizey[-1] = s.spline.pieces * s.spline.order diff --git a/csaps/_sspumv.py b/csaps/_sspumv.py index b5db301..0e1581a 100644 --- a/csaps/_sspumv.py +++ b/csaps/_sspumv.py @@ -6,14 +6,15 @@ """ import typing as ty +import warnings import numpy as np import scipy.sparse as sp import scipy.sparse.linalg as la -from csaps._base import SplinePPFormBase, ISmoothingSpline -from csaps._types import UnivariateDataType, UnivariateVectorizedDataType, MultivariateDataType -from csaps._utils import from_2d, to_2d +from ._base import SplinePPFormBase, ISmoothingSpline +from ._types import UnivariateDataType, UnivariateVectorizedDataType, MultivariateDataType +from ._reshape import from_2d, to_2d class SplinePPForm(SplinePPFormBase[np.ndarray, int]): @@ -113,8 +114,10 @@ def evaluate(self, xi: np.ndarray) -> np.ndarray: return values -class UnivariateCubicSmoothingSpline(ISmoothingSpline[SplinePPForm, float, UnivariateDataType]): - """Univariate cubic smoothing spline +class CubicSmoothingSpline(ISmoothingSpline[SplinePPForm, float, UnivariateDataType]): + """Cubic smoothing spline + + The cubic spline implementation for univariate/multivariate data. Parameters ---------- @@ -161,7 +164,7 @@ def __call__(self, xi: UnivariateDataType) -> np.ndarray: """ xi = ty.cast(np.ndarray, np.asarray(xi, dtype=np.float64)) - if xi.ndim > 1: + if xi.ndim > 1: # pragma: no cover raise ValueError('"xi" data must be a 1-d array.') return self._spline.evaluate(xi) @@ -202,8 +205,8 @@ def _prepare_data(xdata, ydata, weights, axis): if yshape[axis] != xdata.size: raise ValueError( - '"ydata" data must be a 1-D or N-D array with shape[{}] that is equal to "xdata" size ({})'.format( - axis, xdata.size)) + f'"ydata" data must be a 1-D or N-D array with shape[{axis}] ' + f'that is equal to "xdata" size ({xdata.size})') # Reshape ydata N-D array to 2-D NxM array where N is the data # dimension and M is the number of data points. @@ -214,8 +217,7 @@ def _prepare_data(xdata, ydata, weights, axis): else: weights = np.asarray(weights, dtype=np.float64) if weights.size != xdata.size: - raise ValueError( - 'Weights vector size must be equal of xdata size') + raise ValueError('Weights vector size must be equal of xdata size') return xdata, ydata, weights, yshape @@ -237,9 +239,8 @@ def _make_spline(self, smooth: ty.Optional[float]) -> ty.Tuple[SplinePPForm, flo pcount = self._xdata.size dx = np.diff(self._xdata) - if not all(dx > 0): - raise ValueError( - 'Items of xdata vector must satisfy the condition: x1 < x2 < ... < xN') + if not all(dx > 0): # pragma: no cover + raise ValueError('Items of xdata vector must satisfy the condition: x1 < x2 < ... < xN') dy = np.diff(self._ydata, axis=1) dy_dx = dy / dx @@ -310,6 +311,41 @@ def _make_spline(self, smooth: ty.Optional[float]) -> ty.Tuple[SplinePPForm, flo return spline, p +class UnivariateCubicSmoothingSpline(ISmoothingSpline[SplinePPForm, float, UnivariateDataType]): + __doc__ = CubicSmoothingSpline.__doc__ + + def __init__(self, + xdata: UnivariateDataType, + ydata: UnivariateVectorizedDataType, + weights: ty.Optional[UnivariateDataType] = None, + smooth: ty.Optional[float] = None, + axis: int = -1) -> None: + with warnings.catch_warnings(): + warnings.simplefilter('always', DeprecationWarning) + warnings.warn( + "'UnivariateCubicSmoothingSpline' class is deprecated " + "and will be removed in the future version. " + "Use 'CubicSmoothingSpline' class instead.", stacklevel=2) + + self._cssp = CubicSmoothingSpline( + xdata, ydata, weights=weights, smooth=smooth, axis=axis) + + @property + def smooth(self) -> float: + return self._cssp.smooth + + @property + def spline(self) -> SplinePPForm: + return self._cssp.spline + + def __call__(self, xi: UnivariateDataType) -> np.ndarray: + return self._cssp(xi) + + +# For case isinstance(CubicSmoothingSpline(...), UnivariateCubicSmoothingSpline) +UnivariateCubicSmoothingSpline.register(CubicSmoothingSpline) + + class MultivariateCubicSmoothingSpline(ISmoothingSpline[SplinePPForm, float, UnivariateDataType]): """Multivariate parametrized cubic smoothing spline @@ -379,6 +415,13 @@ def __init__(self, smooth: ty.Optional[float] = None, axis: int = -1): + with warnings.catch_warnings(): + warnings.simplefilter('always', DeprecationWarning) + warnings.warn( + "'MultivariateCubicSmoothingSpline' class is deprecated " + "and will be removed in the future version. " + "Use 'CubicSmoothingSpline' class instead.", stacklevel=2) + ydata = ty.cast(np.ndarray, np.asarray(ydata, dtype=np.float64)) if tdata is None: @@ -386,14 +429,13 @@ def __init__(self, tdata = ty.cast(np.ndarray, np.asarray(tdata, dtype=np.float64)) - if tdata.size != ydata.shape[-1]: - raise ValueError('"tdata" size must be equal to "ydata" shape[{}] size ({})'.format( - axis, ydata.shape[axis])) + if tdata.size != ydata.shape[-1]: # pragma: no cover + raise ValueError(f'"tdata" size must be equal to "ydata" shape[{axis}] size ({ydata.shape[axis]})') self._tdata = tdata # Use vectorization for compute spline for every dimension from t - self._univariate_spline = UnivariateCubicSmoothingSpline( + self._univariate_spline = CubicSmoothingSpline( xdata=tdata, ydata=ydata, weights=weights, diff --git a/csaps/_version.py b/csaps/_version.py index 0c6d7b8..d452437 100644 --- a/csaps/_version.py +++ b/csaps/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -__version__ = '0.8.0' +__version__ = '0.9.0' diff --git a/docs/api.rst b/docs/api.rst index f85dfe5..14e8fc6 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -15,8 +15,7 @@ Summary AutoSmoothingResult ISmoothingSpline - UnivariateCubicSmoothingSpline - MultivariateCubicSmoothingSpline + CubicSmoothingSpline NdGridCubicSmoothingSpline SplinePPFormBase @@ -39,13 +38,7 @@ Main API Object-Oriented API ------------------- -.. autoclass:: UnivariateCubicSmoothingSpline - :members: - :special-members: __call__ - ----- - -.. autoclass:: MultivariateCubicSmoothingSpline +.. autoclass:: CubicSmoothingSpline :members: :special-members: __call__ diff --git a/docs/index.rst b/docs/index.rst index 1e784a0..63a33a7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -41,7 +41,7 @@ You can install and update csaps using pip: The module depends only on NumPy and SciPy. -Python 3.5 or above is supported. +Python 3.6 or above is supported. Content ------- diff --git a/setup.py b/setup.py index 5518fed..e3c8830 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ def _get_long_description(): name='csaps', version=_get_version(), packages=['csaps'], - python_requires='>=3.5, <4', + python_requires='>=3.6, <4', install_requires=[ 'numpy >=0.12.1, <1.20.0', 'scipy >=0.19.1, <1.6.0', @@ -41,6 +41,7 @@ def _get_long_description(): ], 'tests': [ 'pytest', + 'coverage', ], }, package_data={"csaps": ["py.typed"]}, @@ -67,7 +68,6 @@ def _get_long_description(): 'Programming Language :: Python', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', diff --git a/tests/test_multivariate.py b/tests/test_multivariate.py index f01f49a..ded12bc 100644 --- a/tests/test_multivariate.py +++ b/tests/test_multivariate.py @@ -14,4 +14,8 @@ def test_auto_tdata(): t = [0., 3.74165739, 8.10055633, 12.68313203] sp = csaps.MultivariateCubicSmoothingSpline(data) + + assert isinstance(sp.spline, csaps.SplinePPForm) + assert 0 < sp.smooth < 1 + assert sp(t).shape == (3, 4) np.testing.assert_allclose(sp.t, t) diff --git a/tests/test_gridded.py b/tests/test_ndgrid.py similarity index 76% rename from tests/test_gridded.py rename to tests/test_ndgrid.py index 412f840..075fb9a 100644 --- a/tests/test_gridded.py +++ b/tests/test_ndgrid.py @@ -14,7 +14,9 @@ ([[1, 2, 3], [1, 2, 3]], np.ones((3, 3)), [1, 2, 3], None), ([[1, 2, 3], [1, 2, 3]], np.ones((3, 3)), [[1, 2, 3]], None), ([[1, 2, 3], [1, 2, 3]], np.ones((3, 3)), [[1, 2], [1, 2]], None), - ([[1, 2, 3], [1, 2, 3]], np.ones((3, 3)), None, [0.5, 0.4, 0.2]) + ([[1, 2, 3], [1, 2, 3]], np.ones((3, 3)), None, [0.5, 0.4, 0.2]), + (np.array([[1, 2, 3], [4, 5, 6]]), np.ones((3, 3)), None, None), + ([np.arange(6).reshape(2, 3), np.arange(6).reshape(2, 3)], np.ones((6, 6)), None, None), ]) def test_invalid_data(x, y, w, p): with pytest.raises((ValueError, TypeError)): @@ -33,4 +35,9 @@ def test_surface(): noisy = ydata + (np.random.randn(*ydata.shape) * 0.75) sp = csaps.NdGridCubicSmoothingSpline(xdata, noisy) - _ = sp(xdata) + noisy_s = sp(xdata) + + assert isinstance(sp.smooth, tuple) + assert len(sp.smooth) == 2 + assert isinstance(sp.spline, csaps.NdGridSplinePPForm) + assert noisy_s.shape == noisy.shape diff --git a/tests/test_shortcut.py b/tests/test_shortcut.py index f7539bd..e1c4559 100644 --- a/tests/test_shortcut.py +++ b/tests/test_shortcut.py @@ -3,7 +3,7 @@ import pytest import numpy as np -from csaps import csaps, AutoSmoothingResult, UnivariateCubicSmoothingSpline, NdGridCubicSmoothingSpline +from csaps import csaps, AutoSmoothingResult, CubicSmoothingSpline, NdGridCubicSmoothingSpline @pytest.fixture(scope='module') @@ -34,7 +34,7 @@ def data(curve, surface, request): if request.param == 'univariate': x, y = curve xi = np.linspace(x[0], x[-1], 150) - return x, y, xi, 0.85, UnivariateCubicSmoothingSpline + return x, y, xi, 0.85, CubicSmoothingSpline elif request.param == 'ndgrid': x, y = surface @@ -46,9 +46,15 @@ def data(curve, surface, request): 'univariate', 'ndgrid', ], indirect=True) -def test_shortcut_output(data): +@pytest.mark.parametrize('tolist', [True, False]) +def test_shortcut_output(data, tolist): x, y, xi, smooth, sp_cls = data + if tolist and isinstance(x, np.ndarray): + x = x.tolist() + y = y.tolist() + xi = xi.tolist() + yi = csaps(x, y, xi, smooth=smooth) assert isinstance(yi, np.ndarray) diff --git a/tests/test_univariate.py b/tests/test_univariate.py index 306c4f0..947fcde 100644 --- a/tests/test_univariate.py +++ b/tests/test_univariate.py @@ -23,7 +23,7 @@ ]) def test_invalid_data(x, y, w): with pytest.raises(ValueError): - csaps.UnivariateCubicSmoothingSpline(x, y, w) + csaps.UnivariateCubicSmoothingSpline(x, y, weights=w) @pytest.mark.parametrize('y', [ @@ -160,6 +160,7 @@ def test_auto_smooth(): xi = np.linspace(x[0], x[-1], 120) yi = sp(xi) + assert isinstance(sp.spline, csaps.SplinePPForm) np.testing.assert_almost_equal(sp.smooth, 0.996566686) desired_yi = [ @@ -238,7 +239,7 @@ def test_weighted(w, yid): y = [2., 4., 5., 7.] xi = np.linspace(1., 6., 10) - sp = csaps.UnivariateCubicSmoothingSpline(x, y, w) + sp = csaps.UnivariateCubicSmoothingSpline(x, y, weights=w) yi = sp(xi) np.testing.assert_allclose(yi, yid) diff --git a/tox.ini b/tox.ini index 7d9429d..091605d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{35,36,37,38}-pytest-coverage, flake8 +envlist = py{36,37,38}-pytest-coverage, flake8 [testenv] deps = pytest