Repository: scipy/scipy

Issue:
BUG: 1.10.1 scipy.interpolate.UnivariateSpline.roots() returns only 10 roots
### Describe your issue.

When calling the `UnivariateSpline.roots` function of `UnivariateSpline`, there are only 10 roots maximum that are returned with a warning message if there is more than 10 roots to evaluate. `UnivariateSpline.roots` doesn't accept any argument.

Worked well with 1.9.3 : all the roots were given even if the evaluated spline has more than 10 roots.

### Reproducing Code Example

```python
import numpy as np
import scipy.interpolate as itp

x = np.linspace(0 , 50 * np.pi, 1000)
y = np.cos(x)

spl = itp.UnivariateSpline(x, y, s=0)
roots = spl.roots()

print(roots)
```


### Error message

```shell
C:\Users\user\WP\WPy64-31090\python-3.10.9.amd64\lib\site-packages\scipy\interpolate\_fitpack_impl.py:724: RuntimeWarning: The number of zeros exceeds mest
  warnings.warn(RuntimeWarning("The number of zeros exceeds mest"))
```


### SciPy/NumPy/Python version and system information

```shell
1.10.1 1.23.5 sys.version_info(major=3, minor=10, micro=9, releaselevel='final', serial=0)
Build Dependencies:
  blas:
    detection method: pkgconfig
    found: true
    include directory: c:/opt/openblas/if_32/64/include
    lib directory: c:/opt/openblas/if_32/64/lib
    name: openblas
    openblas configuration: USE_64BITINT= DYNAMIC_ARCH=1 DYNAMIC_OLDER= NO_CBLAS=
      NO_LAPACK= NO_LAPACKE= NO_AFFINITY=1 USE_OPENMP= PRESCOTT MAX_THREADS=4
    pc file directory: c:/opt/openblas/if_32/64/lib/pkgconfig
    version: 0.3.18
  lapack:
    detection method: pkgconfig
    found: true
    include directory: c:/opt/openblas/if_32/64/include
    lib directory: c:/opt/openblas/if_32/64/lib
    name: openblas
    openblas configuration: USE_64BITINT= DYNAMIC_ARCH=1 DYNAMIC_OLDER= NO_CBLAS=
      NO_LAPACK= NO_LAPACKE= NO_AFFINITY=1 USE_OPENMP= PRESCOTT MAX_THREADS=4
    pc file directory: c:/opt/openblas/if_32/64/lib/pkgconfig
    version: 0.3.18
Compilers:
  c:
    commands: cc
    linker: ld.bfd
    name: gcc
    version: 10.3.0
  c++:
    commands: c++
    linker: ld.bfd
    name: gcc
    version: 10.3.0
  cython:
    commands: cython
    linker: cython
    name: cython
    version: 0.29.33
  fortran:
    commands: gfortran
    linker: ld.bfd
    name: gcc
    version: 10.3.0
  pythran:
    include directory: C:\Users\runneradmin\AppData\Local\Temp\pip-build-env-v_zt9njv\overlay\Lib\site-packages\pythran
    version: 0.12.1
Machine Information:
  build:
    cpu: x86_64
    endian: little
    family: x86_64
    system: windows
  cross-compiled: false
  host:
    cpu: x86_64
    endian: little
    family: x86_64
    system: windows
Python Information:
  path: C:\Users\runneradmin\AppData\Local\Temp\cibw-run-dskf9r9k\cp310-win_amd64\build\venv\Scripts\python.exe
  version: '3.10'
```



Discussion:
`UnivariateSpline.roots` may grow an optional `mest` argument, similar to that of `sproot` (which used to work and keeps working today):

```
In [7]: tck = itp.splrep(x, y)

In [8]: itp.sproot(tck, mest=120)
Out[8]: 
array([  1.57079632,   4.71238896,   7.85398161,  10.99557425,
        14.1371669 ,  17.27875954,  20.42035219,  23.56194484,
        26.70353748,  29.84513013,  32.98672279,  36.12831544,
        39.26990809,  42.41150075,  45.5530934 ,  48.69468606,
        51.83627872,  54.97787138,  58.11946404,  61.2610567 ,
        64.40264936,  67.54424202,  70.68583468,  73.82742735,
        76.96902001,  80.11061267,  83.25220533,  86.393798  ,
        89.53539066,  92.67698332,  95.81857598,  98.96016864,
       102.1017613 , 105.24335396, 108.38494662, 111.52653928,
       114.66813193, 117.80972459, 120.95131724, 124.09290989,
       127.23450255, 130.37609519, 133.51768784, 136.65928049,
       139.80087314, 142.94246578, 146.08405843, 149.22565107,
       152.36724372, 155.50883636])
```

A PR adding mest is welcome, just make sure the default is consistent with sproot. 

And an obligatory note: prefer CubicSpline or make_interp_spline to UnivariateSpline(..., s=0) and splrep to UnivariateSpline(..., nonzero s) 
I think it would be better if `UnivariateSpline.roots` keeps the same behavior as in the previous versions : no assumption on the number of roots when calling `UnivariateSpline.roots` without any argument if an optional one is added.  

I have the impression this is [this PR](https://github.com/scipy/scipy/pull/17091) which has changed how `UnivariateSpline.roots` behaves.

@ev-br are we sure about the name of the param? Or is there too much use use of these functions and `mest` would be clear to everyone already? `n_roots` would be a better name to me.
The limitation was always there, and there is no easy way around it, the way the underlying Fortran code is structured. See below however.

The issue here indeed came with gh-17091:

- the f2py wrapper has `mest = 3*(n-7)`, where `n` is the length of the knot array.  If I'm to guess, this is just the number of intervals times the max number of roots a cubic parabola can have on a single interval: 
https://github.com/scipy/scipy/blob/maintenance/1.9.x/scipy/interpolate/src/fitpack.pyf#L163
- gh-17091 rerouted the `UnivariateSpline.roots` call through `_fitpack_impl.sproot`, which has `mest=10` as a default, https://github.com/scipy/scipy/blob/maintenance/1.9.x/scipy/interpolate/_fitpack_impl.py#L668

That latter default is clearly artificial. The only reason for it I can think of is to limit memory consumption.

So the lightest touch approach to restore pre-1.10 behavior would be to add `mest = 3*(len(t) - 7)` to `UnivariateSpline.roots` calling into `_fitpack_impl.sproot`. 

A pull request is welcome.

Re name of the parameter : agreed, `mest` is very much 1980-ish name for it. Which it indeed is: https://github.com/scipy/scipy/blob/maintenance/1.9.x/scipy/interpolate/fitpack/sproot.f#L13  and https://github.com/scipy/scipy/blob/maintenance/1.9.x/scipy/interpolate/fitpack/sproot.f#L37 :-).

It's a technical parameter anyway, and I'm pretty sure the number of people who had even noticed its existence over last 20 years is ... very few. So IMO not worth any effort.


Resolving patch:
diff --git a/scipy/interpolate/_fitpack2.py b/scipy/interpolate/_fitpack2.py
--- a/scipy/interpolate/_fitpack2.py
+++ b/scipy/interpolate/_fitpack2.py
@@ -527,7 +527,9 @@ def roots(self):
         """
         k = self._data[5]
         if k == 3:
-            return _fitpack_impl.sproot(self._eval_args)
+            t = self._eval_args[0]
+            mest = 3 * (len(t) - 7)
+            return _fitpack_impl.sproot(self._eval_args, mest=mest)
         raise NotImplementedError('finding roots unsupported for '
                                   'non-cubic splines')
 


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
@@ -67,6 +67,12 @@ def test_roots(self):
         spl = UnivariateSpline(x, y, k=3)
         assert_almost_equal(spl.roots()[0], 1.050290639101332)
 
+    def test_roots_length(self): # for gh18335
+        x = np.linspace(0, 50 * np.pi, 1000)
+        y = np.cos(x)
+        spl = UnivariateSpline(x, y, s=0)
+        assert_equal(len(spl.roots()), 50)
+
     def test_derivatives(self):
         x = [1, 3, 5, 7, 9]
         y = [0, 4, 9, 12, 21]