Package: ndsplines
This is a Python package for multivariate B-splines with performant NumPy and C
(via Cython) implementations. For a mathematical overview of tensor product
B-splines, see theSplinespage of the documentation.The primary goal of this package is to provide a unified API for tensor product
splines of arbitrary input and output dimension. For a list of related packages
see theComparisonspage.InstallationInstall ndsplines with pip:$ pip install ndsplinesWheels are provided for a range of Python versions and platforms, so no
compilation is required to get the better-performing Cython-based implementation
in many cases.If no matching wheel is found, pip will install build dependencies and attempt
to compile the Cython-based extension module. If this is not desired, set the
environment variableNDSPLINES_NUMPY_ONLY=1, e.g.:$ NDSPLINES_NUMPY_ONLY=1 pip install ndsplinesUsageThe easiest way to usendsplinesis to use one of themake_*functions:make_interp_spline,make_interp_spline_from_tidy, ormake_lsq_spline, which return anNDSplineobject which can be used to
evaluate the spline. For example, suppose we have data over a two-dimensional
mesh.importndsplinesimportnumpyasnp# generate grid of independent variablesx=np.array([-1,-7/8,-3/4,-1/2,-1/4,-1/8,0,1/8,1/4,1/2,3/4,7/8,1])*np.piy=np.array([-1,-1/2,0,1/2,1])meshx,meshy=np.meshgrid(x,y,indexing='ij')gridxy=np.stack((meshx,meshy),axis=-1)# evaluate a function to interpolate over input gridmeshf=np.sin(meshx)*(meshy-3/8)**2+2We can then usemake_interp_splineto create an interpolating spline and
evaluate it over a denser mesh.# create the interpolating splineinterp=ndsplines.make_interp_spline(gridxy,meshf)# generate denser grid of independent variables to interpolatesparse_dense=2**7xx=np.concatenate([np.linspace(x[i],x[i+1],sparse_dense)foriinrange(x.size-1)])yy=np.concatenate([np.linspace(y[i],y[i+1],sparse_dense)foriinrange(y.size-1)])gridxxyy=np.stack(np.meshgrid(xx,yy,indexing='ij'),axis=-1)# evaluate spline over denser gridmeshff=interp(gridxxyy)Generally, we construct data so that the firstndimaxes index the
independent variables and the remaining axes index output. This is
a generalization of using rows to index time and columns to index output
variables for time-series data.We can also create an interpolating spline from atidy dataformat:tidy_data=np.dstack((gridxy,meshf)).reshape((-1,3))tidy_interp=ndsplines.make_interp_spline_from_tidy(tidy_data,[0,1],# columns to use as independent variable data[2]# columns to use as dependent variable data)print("\nCoefficients all same?",np.all(tidy_interp.coefficients==interp.coefficients))print("Knots all same?",np.all([np.all(k0==k1)fork0,k1inzip(tidy_interp.knots,interp.knots)]))Note however, that the tidy dataset must be over a structured rectangular grid
equivalent to the N-dimensional tensor product representation. Also note that
Pandas dataframes can be used, in which case lists of column names can be used
instead of lists of column indices.To see examples for creating least-squares regression splines
withmake_lsq_spline, see the1D exampleand2D example.Derivatives of constructed splines can be evaluated in two ways: (1) by using
thenusparameter while calling the interpolator or (2) by creating a new spline
with thederivativemethod. In this codeblock, we show both ways of
evaluating derivatives in each direction.# two ways to evaluate derivatives x-direction: create a derivative spline or call with nus:deriv_interp=interp.derivative(0)deriv1=deriv_interp(gridxxy)deriv2=interp(gridxy,nus=np.array([1,0]))# two ways to evaluate derivative - y directionderiv_interp=interp.derivative(1)deriv1=deriv_interp(gridxy)deriv2=interp(gridxxyy,nus=np.array([0,1]))TheNDSplineclass also has anantiderivativemethod for creating a
spline representative of the anti-derivative in the specified direction.# Calculus demonstrationinterp1=deriv_interp.antiderivative(0)coeff_diff=interp1.coefficients-interp.coefficientsprint("\nAntiderivative of derivative:\n","Coefficients differ by constant?",np.allclose(interp1.coefficients+2.0,interp.coefficients))print("Knots all same?",np.all([np.all(k0==k1)fork0,k1inzip(interp1.knots,interp.knots)]))antideriv_interp=interp.antiderivative(0)interp2=antideriv_interp.derivative(0)print("\nDerivative of antiderivative:\n","Coefficients the same?",np.allclose(interp2.coefficients,interp.coefficients))print("Knots all same?",np.all([np.all(k0==k1)fork0,k1inzip(interp2.knots,interp.knots)]))ContributingPlease feel free to share any thoughts or opinions about the design and
implementation of this software byopening an issue on GitHub. Constructive feedback is
welcomed and appreciated.Bug fix pull requests are always welcome. For feature additions, breaking
changes, etc. check if there is an open issue discussing the change and
reference it in the pull request. If there isn’t one, it is recommended to open
one with your rationale for the change before spending significant time
preparing the pull request.Ideally, new/changed functionality should come with tests and documentation. If
you are new to contributing, it is perfectly fine to open a work-in-progress
pull request and have it iteratively reviewed.TestingTo test, install the package with thetestextras and usepytest:$ pip install .[test]
$ pytestDocumentationDocumentation is based on Sphinx and built and served by Read the Docs. To
build locally, install thedocsrequirements:$ pip install .[docs]
$ cd docs
$ make html