Repository: scipy/scipy

Issue:
BUG: constraint function is overwritten when equal bounds are used in SLSQP
### Describe your issue.

@mdhaber, just after I merged #13096 I realised that the constraints functions are replaced in the user supplied list. This is not a good idea, as we're overwriting user information. I'll make a fix now.

### Reproducing Code Example

```python
import numpy as np
from scipy.optimize import minimize

def f (x):
    return x[0]**3 + x[1]**2 + x[2]*x[3]

cfun = lambda x : x[0] + x[1] + x[2] + x[3] - 40

constraints = [{
    'type'  : 'ineq',
    'fun'   : cfun
}]

bounds = [(0, 20)] * 4
bounds[1] = (5, 5)
res_A = minimize (
    f,
    x0          = [1, 2, 3, 4],
    method      = 'SLSQP',
    bounds      = bounds,
    constraints = constraints,
)

assert constraints[0]['fun'] == cfun
```


### Error message

```shell
-
```


### SciPy/NumPy/Python version information

Master


Discussion:

Resolving patch:
diff --git a/scipy/optimize/_minimize.py b/scipy/optimize/_minimize.py
--- a/scipy/optimize/_minimize.py
+++ b/scipy/optimize/_minimize.py
@@ -634,6 +634,10 @@ def minimize(fun, x0, args=(), method=None, jac=None, hess=None,
                     callback = _remove_from_func(callback, i_fixed, x_fixed)
                 if callable(jac):
                     jac = _remove_from_func(jac, i_fixed, x_fixed, remove=1)
+
+                # make a copy of the constraints so the user's version doesn't
+                # get changed. (Shallow copy is ok)
+                constraints = [con.copy() for con in constraints]
                 for con in constraints:  # yes, guaranteed to be a list
                     con['fun'] = _remove_from_func(con['fun'], i_fixed,
                                                    x_fixed, min_dim=1,


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
@@ -2413,7 +2413,7 @@ def test_equal_bounds(method, kwds, bound_type, constraints, callback):
 
     assert res.success
     assert_allclose(res.fun, expected.fun, rtol=1e-6)
-    assert_allclose(res.x, expected.x, rtol=1e-6)
+    assert_allclose(res.x, expected.x, rtol=3e-6)
 
     if fd_needed or kwds['jac'] is False:
         expected.jac[i_eb] = np.nan
@@ -2421,6 +2421,29 @@ def test_equal_bounds(method, kwds, bound_type, constraints, callback):
     assert_allclose(res.jac[i_eb], expected.jac[i_eb], rtol=1e-6)
 
 
+def test_eb_constraints():
+    # make sure constraint functions aren't overwritten when equal bounds
+    # are employed, and a parameter is factored out. GH14859
+    def f(x):
+        return x[0]**3 + x[1]**2 + x[2]*x[3]
+
+    def cfun(x):
+        return x[0] + x[1] + x[2] + x[3] - 40
+
+    constraints = [{'type': 'ineq', 'fun': cfun}]
+
+    bounds = [(0, 20)] * 4
+    bounds[1] = (5, 5)
+    optimize.minimize(
+        f,
+        x0=[1, 2, 3, 4],
+        method='SLSQP',
+        bounds=bounds,
+        constraints=constraints,
+    )
+    assert constraints[0]['fun'] == cfun
+
+
 def test_show_options():
     solver_methods = {
         'minimize': MINIMIZE_METHODS,