Repository: scipy/scipy

Issue:
LSQUnivariateSpline does not check for FITPACK conditions
Hi,

It looks like LSQUnivariateSpline is broken as it returns nans for any input 

``` python
xs=np.arange(100)*1.
ys=np.arange(100)*1.
knots=np.linspace(0,99,10)
bbox=(-1,101)
i=scipy.interpolate.LSQUnivariateSpline(xs,ys,knots,bbox=bbox)
print i(3.), i(10.),i(60.),i.get_coeffs()

> nan nan nan [nan ... nan]
```

I don't see any reason for those results other than a bug. 

(verified with scipy 0.12)



Discussion:
This works:

```
knots=np.linspace(0,99,10)[1:-1]
i=scipy.interpolate.LSQUnivariateSpline(xs,ys,knots,bbox=bbox)
```

A knot selection with a knot coincident with the first or last data point probably fails to satisfy one of the conditions FITPACK requires for the knot set, meaning the check currently in LSQ\* is not enough.

It turns out that it is related to bbox parameter. Because if I don't specify the bbox, the default bbox=[x[0],x[-1]] is used and then the error about "Schoenberg-Whitney conditions" appears.

The check in LSQ\* actually does not check the whole set required conditions, they are more involved. FITPACK has a routine [fpchec](https://github.com/scipy/scipy/blob/master/scipy/interpolate/fitpack/fpchec.f) that checks them.

This limitation of FITPACK's spline fitting routine is rather annoying, since a least-squares spline could certainly also be defined under looser conditions.

So how should we move forward? Implement full checking of the conditions in LSQ*? Or wrap fpcheck?

Wrapping fpcheck sounds safer. Rewriting the conditions in Python sounds error-prone.


Resolving patch:
diff --git a/scipy/interpolate/fitpack2.py b/scipy/interpolate/fitpack2.py
--- a/scipy/interpolate/fitpack2.py
+++ b/scipy/interpolate/fitpack2.py
@@ -538,6 +538,21 @@ def __init__(self, x, y, w=None, bbox=[None]*2, k=3, ext=0):
             raise ValueError("Unknown extrapolation mode %s." % ext)
 
 
+_fpchec_error_string = """The input parameters have been rejected by fpchec. \
+This means that at least one of the following conditions is violated:
+
+1) k+1 <= n-k-1 <= m
+2) t(1) <= t(2) <= ... <= t(k+1)
+   t(n-k) <= t(n-k+1) <= ... <= t(n)
+3) t(k+1) < t(k+2) < ... < t(n-k)
+4) t(k+1) <= x(i) <= t(n-k)
+5) The conditions specified by Schoenberg and Whitney must hold
+   for at least one subset of data points, i.e., there must be a
+   subset of data points y(j) such that
+       t(j) < y(j) < t(j+k+1), j=1,2,...,n-k-1
+"""
+
+
 class LSQUnivariateSpline(UnivariateSpline):
     """
     One-dimensional spline with explicit internal knots.
@@ -563,7 +578,7 @@ class LSQUnivariateSpline(UnivariateSpline):
     k : int, optional
         Degree of the smoothing spline.  Must be 1 <= `k` <= 5.
     ext : int or str, optional
-        Controls the extrapolation mode for elements 
+        Controls the extrapolation mode for elements
         not in the interval defined by the knot sequence.
 
         * if ext=0 or 'extrapolate', return the extrapolated value.
@@ -626,10 +641,11 @@ def __init__(self, x, y, t, w=None, bbox=[None]*2, k=3, ext=0):
                        the approximation interval.
                        By default, bbox=[x[0],x[-1]]
           k=3        - degree of the univariate spline.
-          ext        - Controls the extrapolation mode for elements 
+          ext        - Controls the extrapolation mode for elements
                        not in the interval defined by the knot sequence.
 
-                       * if ext=0 or 'extrapolate', return the extrapolated value.
+                       * if ext=0 or 'extrapolate', return the extrapolated
+                         value.
                        * if ext=1 or 'zeros', return 0
                        * if ext=2 or 'raise', raise a ValueError.
 
@@ -642,13 +658,15 @@ def __init__(self, x, y, t, w=None, bbox=[None]*2, k=3, ext=0):
             xb = x[0]
         if xe is None:
             xe = x[-1]
-        t = concatenate(([xb]*(k+1),t,[xe]*(k+1)))
+        t = concatenate(([xb]*(k+1), t, [xe]*(k+1)))
         n = len(t)
-        if not alltrue(t[k+1:n-k]-t[k:n-k-1] > 0,axis=0):
+        if not alltrue(t[k+1:n-k]-t[k:n-k-1] > 0, axis=0):
             raise ValueError('Interior knots t must satisfy '
-                            'Schoenberg-Whitney conditions')
-        data = dfitpack.fpcurfm1(x,y,k,t,w=w,xb=xb,xe=xe)
-        self._data = data[:-3] + (None,None,data[-1])
+                             'Schoenberg-Whitney conditions')
+        if not dfitpack.fpchec(x, t, k) == 0:
+            raise ValueError(_fpchec_error_string)
+        data = dfitpack.fpcurfm1(x, y, k, t, w=w, xb=xb, xe=xe)
+        self._data = data[:-3] + (None, None, data[-1])
         self._reset_class()
 
         try:


Test patch:
diff --git a/scipy/interpolate/tests/test_fitpack2.py b/scipy/interpolate/tests/test_fitpack2.py
--- a/scipy/interpolate/tests/test_fitpack2.py
+++ b/scipy/interpolate/tests/test_fitpack2.py
@@ -114,6 +114,14 @@ def test_out_of_range_regression(self):
             assert_raises(ValueError, UnivariateSpline,
                     **dict(x=x, y=y, ext=ext))
 
+    def test_lsq_fpchec(self):
+        xs = np.arange(100) * 1.
+        ys = np.arange(100) * 1.
+        knots = np.linspace(0, 99, 10)
+        bbox = (-1, 101)
+        assert_raises(ValueError, LSQUnivariateSpline, xs, ys, knots,
+                      bbox=bbox)
+
     def test_derivative_and_antiderivative(self):
         # Thin wrappers to splder/splantider, so light smoke test only.
         x = np.linspace(0, 1, 70)**3