Repository: scipy/scipy
Pull request: BUG: Fix issue with shgo not correctly passing jac to minimizer
Resolves [gh-12963](https://github.com/scipy/scipy/issues/12963) where `_minimize_slsqp()` could receive too many arguments.

Problem statement:
shgo is not correctly passing jac to minimizer
I am getting  the "_minimize_slsqp() got multiple values for argument 'jac' " error for my toy example below. Is this a bug in shgo or am I doing something wrong? Please advise if there is a workaround.

```
from scipy.optimize import shgo

def objective(x):
    return 3*x[0]*x[0] + 2*x[0] + 5

def objective_jac(x):
    return 6*x[0] + 2

if __name__ == "__main__":

    bounds = [(-100, 100)]

    result = shgo(objective, bounds, options={'jac':objective_jac})
    print(result)
```

Hints from issue discussion:
This appears to be a mistake in the documentation. The `jac` parameter should be passed in the `minimizer_kwargs` dictionary rather than int `options`.

@Stefan-Endres would you like to make a fix? I think the `hess` parameter is also in the wrong place.
Thank you for tagging me @pmla.

You are right that passing to `minimizer_kwargs` works and the documentation is wrong. I will make a quick fix to update the `minimizer_kwargs` with `jac` and `hess` after it has been constructed in the `__init__` function this should solve the problem.


The original intention with passing it to the `options` was to allow both global and local use of the Jacobian (for example global use is when computing Lipschitz constants in the global sampling step to stop sampling well explored areas) without the need to define it twice. So this is a bug rather than a mistake in the documentation as it should only be needed for the user to define it once in a dictionary (defined in the `options` dict and then passed to `minimizer_kwargs` if and only if the local solver supports it).

Commit message: MAINT: optimize.shgo: simplify changes

Patch to review:
diff --git a/scipy/optimize/_shgo.py b/scipy/optimize/_shgo.py
index 346cb57e520e..a4589e312b96 100644
--- a/scipy/optimize/_shgo.py
+++ b/scipy/optimize/_shgo.py
@@ -8,6 +8,7 @@
 import warnings
 from scipy import spatial
 from scipy.optimize import OptimizeResult, minimize, Bounds
+from scipy.optimize._optimize import MemoizeJac
 from scipy.optimize._constraints import new_bounds_to_old
 from ._optimize import _wrap_scalar_function
 from scipy.optimize._shgo_lib.triangulation import Complex
@@ -682,7 +683,14 @@ def init_options(self, options):
         None
 
         """
+        # Ensure that 'jac' and 'hess' are passed directly to `minimize` as
+        # keywords, not as part of its 'options' dictionary
+        self.minimizer_kwargs['jac'] = options.pop('jac', None)
+        self.minimizer_kwargs['hess'] = options.pop('hess', None)
+
+        # Update 'options' dict passed to optimize.minimize
         self.minimizer_kwargs['options'].update(options)
+
         # Default settings:
         self.minimize_every_iter = options.get('minimize_every_iter', False)
 
diff --git a/scipy/optimize/tests/test__shgo.py b/scipy/optimize/tests/test__shgo.py
index fd276c868ada..41cdaa93e554 100644
--- a/scipy/optimize/tests/test__shgo.py
+++ b/scipy/optimize/tests/test__shgo.py
@@ -3,7 +3,7 @@
 from numpy.testing import assert_allclose
 import pytest
 from pytest import raises as assert_raises, warns
-from scipy.optimize import shgo, Bounds
+from scipy.optimize import shgo, Bounds, minimize
 from scipy.optimize._shgo import SHGO
 
 
@@ -779,3 +779,20 @@ def test_6_1_lower_known_f_min(self):
                   'sampling_method': 'sobol'
                   }
         warns(UserWarning, shgo, *args, **kwargs)
+
+    def test_21_2_jac_options(self):
+        """shgo used to raise an error when passing `options` with 'jac'
+        # see gh-12829. check that this is resolved
+        """
+        def objective(x):
+            return 3 * x[0] * x[0] + 2 * x[0] + 5
+
+        def gradient(x):
+            return 6 * x[0] + 2
+
+        bounds = [(-100, 100)]
+        res = shgo(objective, bounds, options={'jac': gradient})
+        ref = minimize(objective, x0=[0], bounds=bounds, jac=gradient)
+        assert res.success
+        numpy.testing.assert_allclose(res.fun, ref.fun)
+        numpy.testing.assert_allclose(res.x, ref.x)


Code review comments:
diff_hunk: @@ -682,7 +683,14 @@ def init_options(self, options):
         None
 
         """
+        # Ensure that 'jac' and 'hess' are passed directly to `minimize` as
+        # keywords, not as part of its 'options' dictionary
+        self.minimizer_kwargs['jac'] = options.pop('jac', None)
+        self.minimizer_kwargs['hess'] = options.pop('hess', None)
path: scipy/optimize/_shgo.py
text: @user1:
```suggestion
        if 'jac' in options:
            self.minimizer_kwargs['jac'] = options.pop('jac')
        if 'hass' in options:
            self.minimizer_kwargs['hess'] = options.pop('hess')
```

diff_hunk: @@ -682,7 +683,14 @@ def init_options(self, options):
         None
 
         """
+        # Ensure that 'jac' and 'hess' are passed directly to `minimize` as
+        # keywords, not as part of its 'options' dictionary
+        self.minimizer_kwargs['jac'] = options.pop('jac', None)
+        self.minimizer_kwargs['hess'] = options.pop('hess', None)
path: scipy/optimize/_shgo.py
text: @user1:
I tried to simplify this, but I see that this could replace `minimizer_kwargs['jac']` with `None` if the user passes the `jac` to `minimizer_kwargs` but not to `shgo`'s `options`. But is there any good reason that the user would want the local minimizer to have the gradient but not let `shgo` use it? If `jac` is found in `minimizer_kwargs`, shouldn't it go to `sgho`, too?

diff_hunk: @@ -8,6 +8,7 @@
 import warnings
 from scipy import spatial
 from scipy.optimize import OptimizeResult, minimize, Bounds
+from scipy.optimize._optimize import MemoizeJac
path: scipy/optimize/_shgo.py
text: @user1:
Oops, forgot to remove this. This will probably cause another lint failure.
```suggestion
```