Repository: scipy/scipy

Issue:
scipy.optimize.basinhopping: accept_test returning numpy.bool_ raises an error
Dear Scipy team

When using scipy.optimize.basinhopping, if the accept_test function returns an object of type numpy.bool_, an error occurs. Is this an intentional feature? If not, I think it can be removed allowing accept_test to return numpy.bool_ object.

Thanks
Ajay



Discussion:
Indeed, it seems that this is not handled. According to the [code](https://github.com/scipy/scipy/blob/master/scipy/optimize/_basinhopping.py#L124), this function should return an instance of `bool` or a string with a "special value". And it appears that `numpy.bool_` is not an instance of bool. Perhaps this could be better handled without those `instance` calls.

How about:

```
if testres == 'force_accept':
    ...
elif testres is True:
    ...
else:
    ...
```

Andrew Nelson wrote:

> How about:
> 
> |if testres == 'force_accept': ... elif testres is True: ... else: ... |

Not so much better. `np.bool_(True) is not True`.

As a general rule, a function should return a value of the same type 
(i.e. bool here). If you want another behavior, use an exception.


Resolving patch:
diff --git a/scipy/optimize/_basinhopping.py b/scipy/optimize/_basinhopping.py
--- a/scipy/optimize/_basinhopping.py
+++ b/scipy/optimize/_basinhopping.py
@@ -45,12 +45,13 @@ class BasinHoppingRunner(object):
         This function displaces the coordinates randomly.  Signature should
         be ``x_new = step_taking(x)``.  Note that `x` may be modified in-place.
     accept_tests : list of callables
-        To each test is passed the kwargs `f_new`, `x_new`, `f_old` and
+        Each test is passed the kwargs `f_new`, `x_new`, `f_old` and
         `x_old`.  These tests will be used to judge whether or not to accept
         the step.  The acceptable return values are True, False, or ``"force
-        accept"``.  If the latter, then this will override any other tests in
-        order to accept the step.  This can be used, for example, to forcefully
-        escape from a local minimum that ``basinhopping`` is trapped in.
+        accept"``.  If any of the tests return False then the step is rejected.
+        If the latter, then this will override any other tests in order to
+        accept the step. This can be used, for example, to forcefully escape
+        from a local minimum that ``basinhopping`` is trapped in.
     disp : bool, optional
         Display status messages.
 
@@ -116,7 +117,7 @@ def _monte_carlo_step(self):
         if hasattr(minres, "nhev"):
             self.res.nhev += minres.nhev
 
-        # accept the move based on self.accept_tests. If any test is false,
+        # accept the move based on self.accept_tests. If any test is False,
         # than reject the step.  If any test returns the special value, the
         # string 'force accept', accept the step regardless.  This can be used
         # to forcefully escape from a local minimum if normal basin hopping
@@ -125,19 +126,11 @@ def _monte_carlo_step(self):
         for test in self.accept_tests:
             testres = test(f_new=energy_after_quench, x_new=x_after_quench,
                            f_old=self.energy, x_old=self.x)
-            if isinstance(testres, bool):
-                if not testres:
-                    accept = False
-            elif isinstance(testres, str):
-                if testres == "force accept":
-                    accept = True
-                    break
-                else:
-                    raise ValueError("accept test must return bool or string "
-                                     "'force accept'. Type is", type(testres))
-            else:
-                raise ValueError("accept test must return bool or string "
-                                 "'force accept'. Type is", type(testres))
+            if testres == 'force accept':
+                accept = True
+                break
+            elif not testres:
+                accept = False
 
         # Report the result of the acceptance test to the take step class.
         # This is for adaptive step taking
@@ -345,10 +338,11 @@ def basinhopping(func, x0, niter=100, T=1.0, stepsize=0.5,
         Define a test which will be used to judge whether or not to accept the
         step.  This will be used in addition to the Metropolis test based on
         "temperature" ``T``.  The acceptable return values are True,
-        False, or ``"force accept"``.  If the latter, then this will
-        override any other tests in order to accept the step.  This can be
-        used, for example, to forcefully escape from a local minimum that
-        ``basinhopping`` is trapped in.
+        False, or ``"force accept"``. If any of the tests return False
+        then the step is rejected. If the latter, then this will override any
+        other tests in order to accept the step. This can be used, for example,
+        to forcefully escape from a local minimum that ``basinhopping`` is
+        trapped in.
     callback : callable, ``callback(x, f, accept)``, optional
         A callback function which will be called for all minima found.  ``x``
         and ``f`` are the coordinates and function value of the trial minimum,


Test patch:
diff --git a/scipy/optimize/tests/test__basinhopping.py b/scipy/optimize/tests/test__basinhopping.py
--- a/scipy/optimize/tests/test__basinhopping.py
+++ b/scipy/optimize/tests/test__basinhopping.py
@@ -76,14 +76,14 @@ class MyAcceptTest(object):
     def __init__(self):
         self.been_called = False
         self.ncalls = 0
+        self.testres = [False, 'force accept', True, np.bool_(True),
+                        np.bool_(False), [], {}, 0, 1]
 
     def __call__(self, **kwargs):
         self.been_called = True
         self.ncalls += 1
-        if self.ncalls == 1:
-            return False
-        elif self.ncalls == 2:
-            return 'force accept'
+        if self.ncalls - 1 < len(self.testres):
+            return self.testres[self.ncalls - 1]
         else:
             return True
 
@@ -136,19 +136,6 @@ def test_TypeError(self):
         # if accept_test is passed, it must be callable
         assert_raises(TypeError, basinhopping, func2d, self.x0[i],
                           accept_test=1)
-        # accept_test must return bool or string "force_accept"
-
-        def bad_accept_test1(*args, **kwargs):
-            return 1
-
-        def bad_accept_test2(*args, **kwargs):
-            return "not force_accept"
-        assert_raises(ValueError, basinhopping, func2d, self.x0[i],
-                          minimizer_kwargs=self.kwargs,
-                          accept_test=bad_accept_test1)
-        assert_raises(ValueError, basinhopping, func2d, self.x0[i],
-                          minimizer_kwargs=self.kwargs,
-                          accept_test=bad_accept_test2)
 
     def test_1d_grad(self):
         # test 1d minimizations with gradient