Repository: sympy/sympy (Python)

Issue:
Implement interpolating splines 
SymPy has [`interpolating_poly`](http://docs.sympy.org/latest/modules/polys/reference.html#sympy.polys.specialpolys.interpolating_poly) but no interpolating spline method that I know of. Symbolic interpolating splines are sometimes requested on Stack Overflow by people who want to get a symbolc formula for a particular spline and either (a)  implement it themselves in some low level language, or (b) study the spline itself for some reason. A recent example is [here](https://stackoverflow.com/q/42602553). Another use case is the convenience of constructing piecewise linear functions (splines of degree 1): it's much easier to list the points it should hit than to enter a slope-intercept equation for each piece. 

By analogy with `interpolating_poly(n, x, X, Y)`, I propose that the method should be `interpolating_spline(n, x, X, Y)` where n is the degree of the spline, x is the symbol to use, X is a list of x-coordinates and Y is a list of Y-coordinates. The returned object should be either Piecewise or sum of Piecewise; more on that below.  The best place for this function is probably in [bsplines](http://docs.sympy.org/latest/modules/functions/special.html#b-splines).

My [Stack Overflow answer](https://stackoverflow.com/a/47878791) already has an implementation, which follows SciPy, Matlab, etc in choosing knot placements. So this is a start, but there are things to consider: 

- Input sanitization: there should be enough data points for a spline of given degree. Do we assume x to be sorted, or try to sort them ourselves? (Probably assume sorted, otherwise we can't support symbolic x-values since it doesn't make sense sorting them).  
- Extrapolation (should the spline be defined outside of the range of x-values, and if so, how);
- Form of output: currently it's a sum of Piecewise objects. It'd be better to flatten that into one Piecewise object somehow, with one piece for each `(x_k, x_{k+1})` interval. Some issues with open/closed intervals may transpire here, and might require surgical manipulations with the arguments of Piecewise. 

This issue is on my "to do" list but if anyone feels like working on it, I'm more than happy to stand aside and assist when needed.  


Discussion:
I would like to work on it. Can you please assign this task to me?
I don't think anyone makes formal assignments of tasks to people in this repo. If you'd like to work on it, that's great; and your comment is enough to let others know that someone is on the task.
Just made a pull request. Can you please review it and tell me possible improvements? :)
What boundary conditions does this spline have?



Patch:
diff --git a/sympy/functions/__init__.py b/sympy/functions/__init__.py
index 0c8fda287d89..9f4f17ea8199 100644
--- a/sympy/functions/__init__.py
+++ b/sympy/functions/__init__.py
@@ -33,7 +33,7 @@
         KroneckerDelta)
 from sympy.functions.special.singularity_functions import SingularityFunction
 from sympy.functions.special.delta_functions import DiracDelta, Heaviside
-from sympy.functions.special.bsplines import bspline_basis, bspline_basis_set
+from sympy.functions.special.bsplines import bspline_basis, bspline_basis_set, interpolating_spline
 from sympy.functions.special.bessel import (besselj, bessely, besseli, besselk,
         hankel1, hankel2, jn, yn, jn_zeros, hn1, hn2, airyai, airybi, airyaiprime, airybiprime)
 from sympy.functions.special.hyper import hyper, meijerg
diff --git a/sympy/functions/special/bsplines.py b/sympy/functions/special/bsplines.py
index 0a49d27d65df..a085e6d5dbe3 100644
--- a/sympy/functions/special/bsplines.py
+++ b/sympy/functions/special/bsplines.py
@@ -16,7 +16,7 @@ def _add_splines(c, b1, d, b2):
         new_args = []
         n_intervals = len(b1.args)
         if n_intervals != len(b2.args):
-            # Args of b1 and b2 are not equal. Just combining the Piecewise without any fancy optimisation
+            # Args of b1 and b2 are not equal. Just combining the Piecewise without any fancy optimization
             p1 = piecewise_fold(c*b1)
             p2 = piecewise_fold(d*b2)
 
@@ -111,7 +111,7 @@ def bspline_basis(d, knots, n, x):
     B-splines and their derivatives:
 
         >>> d = 1
-        >>> knots = [0,0,2,3,4]
+        >>> knots = [0, 0, 2, 3, 4]
         >>> bspline_basis(d, knots, 0, x)
         Piecewise((-x/2 + 1, (x >= 0) & (x <= 2)), (0, True))
 
@@ -202,3 +202,78 @@ def bspline_basis_set(d, knots, x):
     """
     n_splines = len(knots) - d - 1
     return [bspline_basis(d, knots, i, x) for i in range(n_splines)]
+
+
+def interpolating_spline(d, x, X, Y):
+    """Return spline of degree ``d`` interpolating for given
+    ``X`` and ``Y`` values.
+
+    This function returns a piecewise function such that each part is
+    a polynomial of degreeat most ``d`` .
+    The degree ``d`` must be at least 1. The values listed in X must
+    be strictly increasing.
+
+    Examples
+    ========
+
+    >>> from sympy import interpolating_spline
+    >>> from sympy.abc import x
+    >>> interpolating_spline(1, x, [1, 2, 4, 7], [3, 6, 5, 7])
+    Piecewise((3*x, (x >= 1) & (x <= 2)),
+            (-x/2 + 7, (x >= 2) & (x <= 4)),
+            (2*x/3 + 7/3, (x >= 4) & (x <= 7)))
+    >>> interpolating_spline(3, x, [-2, 0, 1, 3, 4], [4, 2, 1, 1, 3])
+    Piecewise((-x**3/36 - x**2/36 - 17*x/18 + 2, (x >= -2) & (x <= 1)),
+            (5*x**3/36 - 13*x**2/36 - 11*x/18 + 7/3, (x >= 1) & (x <= 4)))
+
+    See Also
+    ========
+
+    bsplines_basis_set, sympy.polys.specialpolys.interpolating_poly
+    """
+    from sympy import symbols, Number, Dummy, Rational
+    from sympy.solvers.solveset import linsolve
+    from sympy.matrices.dense import Matrix
+
+    # Input sanitization
+    d = sympify(d)
+    if not(d.is_Integer and d.is_positive):
+        raise ValueError("Spline degree must be a positive integer, not %s" % d)
+    if len(X) != len(Y):
+        raise ValueError('Number of X and Y coordinates passed not equal')
+    if len(X) < d + 1:
+        raise ValueError('Degree must be less than the number of control points')
+    if not all(a < b for a, b in zip(X, X[1:])):
+        raise ValueError('The x-coordinates must be strictly increasing')
+
+    # Evaluating knots value
+    if d.is_odd:
+        j = (d + 1) // 2
+        interior_knots = X[j:-j]
+    else:
+        j = d // 2
+        interior_knots = [Rational(a + b, 2) for a, b in zip(X[j:-j - 1], X[j + 1:-j])]
+
+    knots = [X[0]] * (d + 1) + list(interior_knots) + [X[-1]] * (d + 1)
+
+    basis = bspline_basis_set(d, knots, x)
+
+    A = [[b.subs(x, v) for b in basis] for v in X]
+
+    coeff = linsolve((Matrix(A), Matrix(Y)), symbols('c0:{}'.format(len(X)), cls=Dummy))
+    coeff = list(coeff)[0]
+    intervals = set([c for b in basis for (e, c) in b.args if c != True])
+
+    # Sorting the intervals
+    ival = [e.atoms(Number) for e in intervals]   # List containing end-points of each interval
+    ival = [list(sorted(e))[0] for e in ival]
+    com = zip(ival, intervals)
+    com = sorted(com, key=lambda x: x[0])
+    intervals = [y for x, y in com]
+
+    basis_dicts = [dict((c, e) for (e, c) in b.args) for b in basis]
+    spline = []
+    for i in intervals:
+        piece = sum([c*d.get(i, S.Zero) for (c, d) in zip(coeff, basis_dicts)], S.Zero)
+        spline.append((piece, i))
+    return(Piecewise(*spline))


Test patch:
diff --git a/sympy/functions/special/tests/test_bsplines.py b/sympy/functions/special/tests/test_bsplines.py
index f46b7a7a291d..e52b1bcc0d05 100644
--- a/sympy/functions/special/tests/test_bsplines.py
+++ b/sympy/functions/special/tests/test_bsplines.py
@@ -1,7 +1,7 @@
-from sympy.functions import bspline_basis_set
+from sympy.functions import bspline_basis_set, interpolating_spline
 from sympy.core.compatibility import range
 from sympy import Piecewise, Interval, And
-from sympy import symbols, Rational
+from sympy import symbols, Rational, sympify as S
 
 x, y = symbols('x,y')
 
@@ -100,3 +100,49 @@ def test_repeated_degree_2():
     assert splines[4] == Piecewise((x**2/2 - 2*x + 2, And(x <= 3, x >= 2)),
                                    (-3*x**2/2 + 10*x - 16, And(x <= 4, x >= 3)),
                                    (0, True))
+
+# Tests for interpolating_spline
+
+
+def test_10_points_degree_1():
+    d = 1
+    X = [-5, 2, 3, 4, 7, 9, 10, 30, 31, 34]
+    Y = [-10, -2, 2, 4, 7, 6, 20, 45, 19, 25]
+    spline = interpolating_spline(d, x, X, Y)
+
+    assert spline == Piecewise((8*x/7 - S(30)/7, (x >= -5) & (x <= 2)), (4*x - 10, (x >= 2) & (x <= 3)),
+                               (2*x - 4, (x >= 3) & (x <= 4)), (x, (x >= 4) & (x <= 7)),
+                               (-x/2 + S(21)/2, (x >= 7) & (x <= 9)), (14*x - 120, (x >= 9) & (x <= 10)),
+                               (5*x/4 + S(15)/2, (x >= 10) & (x <= 30)), (-26*x + 825, (x >= 30) & (x <= 31)),
+                               (2*x - 43, (x >= 31) & (x <= 34)))
+
+
+def test_3_points_degree_2():
+    d = 2
+    X = [-3, 10, 19]
+    Y = [3, -4, 30]
+    spline = interpolating_spline(d, x, X, Y)
+
+    assert spline == Piecewise((505*x**2/2574 - 4921*x/2574 - S(1931)/429, (x >= -3) & (x <= 19)))
+
+
+def test_5_points_degree_2():
+    d = 2
+    X = [-3, 2, 4, 5, 10]
+    Y = [-1, 2, 5, 10, 14]
+    spline = interpolating_spline(d, x, X, Y)
+
+    assert spline == Piecewise((4*x**2/329 + 1007*x/1645 + S(1196)/1645, (x >= -3) & (x <= 3)),
+                               (2701*x**2/1645 - 15079*x/1645 + S(5065)/329, (x >= 3) & (x <= S(9)/2)),
+                               (-1319*x**2/1645 + 21101*x/1645 - S(11216)/329, (x >= S(9)/2) & (x <= 10)))
+
+
+def test_6_points_degree_3():
+    d = 3
+    X = [-1, 0, 2, 3, 9, 12]
+    Y = [-4, 3, 3, 7, 9, 20]
+    spline = interpolating_spline(d, x, X, Y)
+
+    assert spline == Piecewise((6058*x**3/5301 - 18427*x**2/5301 + 12622*x/5301 + 3, (x >= -1) & (x <= 2)),
+                               (-8327*x**3/5301 + 67883*x**2/5301 - 159998*x/5301 + S(43661)/1767, (x >= 2) & (x <= 3)),
+                               (5414*x**3/47709 - 1386*x**2/589 + 4267*x/279 - S(12232)/589, (x >= 3) & (x <= 12)))