Repository: scipy/scipy

Issue:
Version 1.2.0 introduces `too many indices for array` error in `optimize.newton()`
<!-- 
Thank you for taking the time to report a SciPy issue.
Please describe the issue in detail, and for bug reports
fill in the fields below. You can delete the sections that 
don't apply to your issue. You can view the final output
by clicking the preview button above.
-->

Since version 1.2.0, `optimize.newton` fails when passing an array with a single entry and when passing `fprime2`

### Reproducing code example:
<!-- 
If you place your code between the triple backticks below, 
it will be marked as a code block automatically 
-->


```python
from scipy.optimize import newton
import numpy as np

def f(x):
    return x**2

def fprime(x):
    return 2 * x

def fprime2(x):
    return 2

def test():
    x0 = np.array([-2], dtype=np.float32)

    optimum = newton(f, x0=x0)  # OK
    print(optimum)
    optimum = newton(f, x0=x0, fprime=fprime)  # OK
    print(optimum)
    optimum = newton(f, x0=x0, fprime=fprime, fprime2=fprime2)  # Error
    print(optimum)

test()
```

### Error message:
<!-- If any, paste the *full* error message inside a code block
as above (starting from line Traceback)
-->

```
[-1.95439407e-08]
[-7.4505806e-09]
Traceback (most recent call last):
  File "lolx.py", line 23, in <module>
    test()
  File "lolx.py", line 20, in test
    optimum = newton(f, x0=x0, fprime=fprime, fprime2=fprime2)  # Error
  File "/home/nico/.virtualenvs/pygbm/lib/python3.7/site-packages/scipy/optimize/zeros.py", line 263, in newton
    full_output)
  File "/home/nico/.virtualenvs/pygbm/lib/python3.7/site-packages/scipy/optimize/zeros.py", line 380, in _array_newton
    dp = dp / (1.0 - 0.5 * dp * fder2[nz_der] / fder[nz_der])
IndexError: too many indices for array

```

### Scipy/Numpy/Python version information:

Works in 1.1.0
```
1.1.0 1.15.4 sys.version_info(major=3, minor=7, micro=1, releaselevel='final', serial=0)
```

Fails in 1.2.0
```
1.2.0 1.15.4 sys.version_info(major=3, minor=7, micro=1, releaselevel='final', serial=0)
```


Discussion:
@NicolasHug thanks for the report.

@mikofski would you mind having a look at this one?
Thanks @NicolasHug  for reporting this! It's an interesting case, b/c the 2nd derivative returns a Python scalar 
```python
def fprime2(x):
    return 2
```
which is different than the function and 1st derivative, which both return NumPy arrays
```python
def f(x):
    return x**2

def fprime(x):
    return 2 * x
```
because the initial guess, `x`, is a NumPy array
```python
def test():
    x0 = np.array([-2], dtype=np.float32)
```
So the same error would have occurred if `fprime` returned an scalar.

When evaluating `fprime2`, the return _is_ converted to a NumPy array on L380
https://github.com/scipy/scipy/blob/47976a8f15f175daac6c76e4fbb7c24830d051c4/scipy/optimize/zeros.py#L380
but the size is not preserved, and so that's the bug. I think I have a fix. I'll submit a PR shortly.

Resolving patch:
diff --git a/scipy/optimize/zeros.py b/scipy/optimize/zeros.py
--- a/scipy/optimize/zeros.py
+++ b/scipy/optimize/zeros.py
@@ -99,9 +99,10 @@ def newton(func, x0, fprime=None, args=(), tol=1.48e-8, maxiter=50,
     derivative `fprime2` of `func` is also provided, then Halley's method is
     used.
 
-    If `x0` is a sequence, then `newton` returns an array, and `func` must be
-    vectorized and return a sequence or array of the same shape as its first
-    argument.
+    If `x0` is a sequence with more than one item, then `newton` returns an
+    array, and `func` must be vectorized and return a sequence or array of the
+    same shape as its first argument. If `fprime` or `fprime2` is given then
+    its return must also have the same shape.
 
     Parameters
     ----------
@@ -259,7 +260,7 @@ class of similar problems can be solved together.
         raise ValueError("tol too small (%g <= 0)" % tol)
     if maxiter < 1:
         raise ValueError("maxiter must be greater than 0")
-    if not np.isscalar(x0):
+    if np.size(x0) > 1:
         return _array_newton(func, x0, fprime, args, tol, maxiter, fprime2,
                              full_output)
 
@@ -350,7 +351,7 @@ def _array_newton(func, x0, fprime, args, tol, maxiter, fprime2, full_output):
     A vectorized version of Newton, Halley, and secant methods for arrays.
 
     Do not use this method directly. This method is called from `newton`
-    when ``np.isscalar(x0)`` is True. For docstring, see `newton`.
+    when ``np.size(x0) > 1`` is ``True``. For docstring, see `newton`.
     """
     try:
         p = np.asarray(x0, dtype=float)


Test patch:
diff --git a/scipy/optimize/tests/test_zeros.py b/scipy/optimize/tests/test_zeros.py
--- a/scipy/optimize/tests/test_zeros.py
+++ b/scipy/optimize/tests/test_zeros.py
@@ -619,3 +619,37 @@ def fpp(x):
     # Check that it now succeeds.
     rt, r = newton(f, x0, fprime=fp, fprime2=fpp, full_output=True)
     assert(r.converged)
+
+
+def test_gh_9608_preserve_array_shape():
+    """
+    Test that shape is preserved for array inputs even if fprime or fprime2 is
+    scalar
+    """
+    def f(x):
+        return x**2
+
+    def fp(x):
+        return 2 * x
+
+    def fpp(x):
+        return 2
+
+    x0 = np.array([-2], dtype=np.float32)
+    rt, r = newton(f, x0, fprime=fp, fprime2=fpp, full_output=True)
+    assert(r.converged)
+
+    x0_array = np.array([-2, -3], dtype=np.float32)
+    # This next invocation should fail
+    with pytest.raises(IndexError):
+        result = zeros.newton(
+            f, x0_array, fprime=fp, fprime2=fpp, full_output=True
+        )
+
+    def fpp_array(x):
+        return 2*np.ones(np.shape(x), dtype=np.float32)
+
+    result = zeros.newton(
+        f, x0_array, fprime=fp, fprime2=fpp_array, full_output=True
+    )
+    assert result.converged.all()