Repository: scipy/scipy

Issue:
Back to old constraints format on scipy.optimze.minimize for custom optimizer
When using custom optimizer on scipy.optimize.minimize like that:
scipy.optimize.minimize(fun, x0, args=(), method='custom, jac=jac, hess=hess, bounds=bounds, constraints=([NonLinearConstraints, LinearConstraints]))
Scipy does not give to the custom optimizer the NonLinearConstraint and LinearConstraint scipy objects like in scipy version 1.1 but gives the old way of defining constraints and gives the same objects for linear and non linear. When going through the code source we can see that a function "new_to_old" is added. Is it possible to get back to the NonLinearConstraint and LinearConstraint objects for a custom optimizer ?


Discussion:
@LouiseCannoni thanks for posting!

@antonior92 @andyfaff @pv Thoughts on how this (bottom of [_minimize.py](https://github.com/scipy/scipy/blob/master/scipy/optimize/_minimize.py)) should be modified?
```
    if meth == 'trust-constr':
        for i, con in enumerate(constraints):
            if not isinstance(con, new_constraint_types):
                constraints[i] = old_constraint_to_new(i, con)
    else:
        # iterate over copy, changing original
        for i, con in enumerate(list(constraints)):
            if isinstance(con, new_constraint_types):
                old_constraints = new_constraint_to_old(con, x0)
                constraints[i] = old_constraints[0]
                constraints.extend(old_constraints[1:])  # appends 1 if present
```
(the logic for bounds would need to be changed, too.)

I don't think we discussed in gh-9112 which form of the constraints custom methods should get.

Maybe add an option to `minimize` that would bypass constraint conversion or ensure that the constraints are passed to the solver in one form or the other?
My preference is to pass unchanged bounds/constraints to the custom solver, and let it deal with them. Presumably if the user passes in their own bounds/constraints then the solver should be able to deal with them. Around L583:

```
   if meth == '_custom':
        return method(fun, x0, args=args, jac=jac, hess=hess, hessp=hessp,
                      bounds=bounds, constraints=constraints,
                      callback=callback, **options)

    if bounds is not None:
        bounds = standardize_bounds(bounds, x0, meth)

    if constraints is not None:
        constraints = standardize_constraints(constraints, x0, meth)

    if meth == 'nelder-mead':
```

It's either that, or make the custom solver take new-style everything.
> pass unchanged bounds/constraints to the custom solver,

OK, that sounds good to me. 

Resolving patch:
diff --git a/scipy/optimize/_minimize.py b/scipy/optimize/_minimize.py
--- a/scipy/optimize/_minimize.py
+++ b/scipy/optimize/_minimize.py
@@ -575,17 +575,21 @@ def minimize(fun, x0, args=(), method=None, jac=None, hess=None,
             options.setdefault('gtol', tol)
             options.setdefault('barrier_tol', tol)
 
+    if meth == '_custom':
+        # custom method called before bounds and constraints are 'standardised'
+        # custom method should be able to accept whatever bounds/constraints
+        # are provided to it.
+        return method(fun, x0, args=args, jac=jac, hess=hess, hessp=hessp,
+                      bounds=bounds, constraints=constraints,
+                      callback=callback, **options)
+
     if bounds is not None:
         bounds = standardize_bounds(bounds, x0, meth)
 
     if constraints is not None:
         constraints = standardize_constraints(constraints, x0, meth)
 
-    if meth == '_custom':
-        return method(fun, x0, args=args, jac=jac, hess=hess, hessp=hessp,
-                      bounds=bounds, constraints=constraints,
-                      callback=callback, **options)
-    elif meth == 'nelder-mead':
+    if meth == 'nelder-mead':
         return _minimize_neldermead(fun, x0, args, callback, **options)
     elif meth == 'powell':
         return _minimize_powell(fun, x0, args, callback, **options)


Test patch:
diff --git a/scipy/optimize/tests/test_optimize.py b/scipy/optimize/tests/test_optimize.py
--- a/scipy/optimize/tests/test_optimize.py
+++ b/scipy/optimize/tests/test_optimize.py
@@ -721,6 +721,21 @@ def custmin(fun, x0, args=(), maxfev=None, stepsize=0.1,
                                 options=dict(stepsize=0.05))
         assert_allclose(res.x, 1.0, rtol=1e-4, atol=1e-4)
 
+    def test_gh10771(self):
+        # check that minimize passes bounds and constraints to a custom
+        # minimizer without altering them.
+        bounds = [(-2, 2), (0, 3)]
+        constraints = 'constraints'
+
+        def custmin(fun, x0, **options):
+            assert options['bounds'] is bounds
+            assert options['constraints'] is constraints
+            return optimize.OptimizeResult()
+
+        x0 = [1, 1]
+        res = optimize.minimize(optimize.rosen, x0, method=custmin,
+                                bounds=bounds, constraints=constraints)
+
     def test_minimize_tol_parameter(self):
         # Check that the minimize() tol= argument does something
         def func(z):