Repository: scipy/scipy

Issue:
ENH: Allow to choose line-search parameters in related optimization algorithms
### Is your feature request related to a problem? Please describe.

While looking at #18481 I noticed it is not yet possible to choose `c1` and/or `c2` parameters of the line-search algorithm used within multiple optimization algorithms (CG, BFGS, NEWTON-CG) from the `scipy.optimize.minimize` API.

### Describe the solution you'd like.

Add `c1` and `c2` parameters with default values to `1e-4` and `0.9` to the signature of [`_minimize_newtoncg`](https://github.com/scipy/scipy/blob/5ebc7afe8949d097728ee5b93271a481f2fcb881/scipy/optimize/_optimize.py#L1980), [`_minimize_cg`](https://github.com/scipy/scipy/blob/5ebc7afe8949d097728ee5b93271a481f2fcb881/scipy/optimize/_optimize.py#L1720) and [`_minimize_bfgs`](https://github.com/scipy/scipy/blob/5ebc7afe8949d097728ee5b93271a481f2fcb881/scipy/optimize/_optimize.py#L1382) .

### Describe alternatives you've considered.

I am not mastering the whole `scipy.optimize` code base but I can't see any other straightforward one. 

### Additional context (e.g. screenshots, GIFs)

_No response_


Discussion:
This is already covered by #18977 for BFGS
I was not aware of this PR. Is there any reason for not exposing `c1` and `c2` for other algorithms than BFGS?
I don't think there are reasons against it. So far just no one looked into it.

Would you be interested in submitting a PR? #18977 can serve as a helpful example.

Resolving patch:
diff --git a/scipy/optimize/_linesearch.py b/scipy/optimize/_linesearch.py
--- a/scipy/optimize/_linesearch.py
+++ b/scipy/optimize/_linesearch.py
@@ -24,6 +24,12 @@ class LineSearchWarning(RuntimeWarning):
     pass
 
 
+def _check_c1_c2(c1, c2):
+    if not (0 < c1 < c2 < 1):
+        raise ValueError("'c1' and 'c2' do not satisfy"
+                         "'0 < c1 < c2 < 1'.")
+
+
 #------------------------------------------------------------------------------
 # Minpack's Wolfe line and scalar searches
 #------------------------------------------------------------------------------
@@ -131,6 +137,7 @@ def scalar_search_wolfe1(phi, derphi, phi0=None, old_phi0=None, derphi0=None,
     Uses routine DCSRCH from MINPACK.
 
     """
+    _check_c1_c2(c1, c2)
 
     if phi0 is None:
         phi0 = phi(0.)
@@ -378,6 +385,7 @@ def scalar_search_wolfe2(phi, derphi, phi0=None,
     1999, pp. 59-61.
 
     """
+    _check_c1_c2(c1, c2)
 
     if phi0 is None:
         phi0 = phi(0.)
diff --git a/scipy/optimize/_optimize.py b/scipy/optimize/_optimize.py
--- a/scipy/optimize/_optimize.py
+++ b/scipy/optimize/_optimize.py
@@ -1319,6 +1319,8 @@ def fmin_bfgs(f, x0, fprime=None, args=(), gtol=1e-5, norm=np.inf,
     Optimize the function, `f`, whose gradient is given by `fprime`
     using the quasi-Newton method of Broyden, Fletcher, Goldfarb,
     and Shanno (BFGS).
+    
+    Parameters `c1` and `c2` must satisfy ``0 < c1 < c2 < 1``.
 
     See Also
     --------
@@ -1421,9 +1423,12 @@ def _minimize_bfgs(fun, x0, args=(), jac=None, callback=None,
         less than ``xk * xrtol`` where ``xk`` is the current parameter vector.
     c1 : float, default: 1e-4
         Parameter for Armijo condition rule.
-    c2 : float, default: .9
+    c2 : float, default: 0.9
         Parameter for curvature condition rule.
 
+    Notes
+    -----
+    Parameters `c1` and `c2` must satisfy ``0 < c1 < c2 < 1``.
     """
     _check_unknown_options(unknown_options)
     retall = return_all
@@ -1555,7 +1560,7 @@ def _print_success_message_or_warn(warnflag, message, warntype=None):
 
 def fmin_cg(f, x0, fprime=None, args=(), gtol=1e-5, norm=np.inf,
             epsilon=_epsilon, maxiter=None, full_output=0, disp=1, retall=0,
-            callback=None):
+            callback=None, c1=1e-4, c2=0.4):
     """
     Minimize a function using a nonlinear conjugate gradient algorithm.
 
@@ -1600,6 +1605,10 @@ def fmin_cg(f, x0, fprime=None, args=(), gtol=1e-5, norm=np.inf,
     callback : callable, optional
         An optional user-supplied function, called after each iteration.
         Called as ``callback(xk)``, where ``xk`` is the current value of `x0`.
+    c1 : float, default: 1e-4
+        Parameter for Armijo condition rule.
+    c2 : float, default: 0.4
+        Parameter for curvature condition rule.
 
     Returns
     -------
@@ -1653,6 +1662,8 @@ def fmin_cg(f, x0, fprime=None, args=(), gtol=1e-5, norm=np.inf,
     5. The initial guess, `x0`, is reasonably close to `f` 's global
        minimizing point, `xopt`.
 
+    Parameters `c1` and `c2` must satisfy ``0 < c1 < c2 < 1``.
+
     References
     ----------
     .. [1] Wright & Nocedal, "Numerical Optimization", 1999, pp. 120-122.
@@ -1715,7 +1726,8 @@ def fmin_cg(f, x0, fprime=None, args=(), gtol=1e-5, norm=np.inf,
             'return_all': retall}
 
     callback = _wrap_callback(callback)
-    res = _minimize_cg(f, x0, args, fprime, callback=callback, **opts)
+    res = _minimize_cg(f, x0, args, fprime, callback=callback, c1=c1, c2=c2,
+                       **opts)
 
     if full_output:
         retlist = res['x'], res['fun'], res['nfev'], res['njev'], res['status']
@@ -1732,7 +1744,7 @@ def fmin_cg(f, x0, fprime=None, args=(), gtol=1e-5, norm=np.inf,
 def _minimize_cg(fun, x0, args=(), jac=None, callback=None,
                  gtol=1e-5, norm=np.inf, eps=_epsilon, maxiter=None,
                  disp=False, return_all=False, finite_diff_rel_step=None,
-                 **unknown_options):
+                 c1=1e-4, c2=0.4, **unknown_options):
     """
     Minimization of scalar function of one or more variables using the
     conjugate gradient algorithm.
@@ -1761,6 +1773,14 @@ def _minimize_cg(fun, x0, args=(), jac=None, callback=None,
         possibly adjusted to fit into the bounds. For ``jac='3-point'``
         the sign of `h` is ignored. If None (default) then step is selected
         automatically.
+    c1 : float, default: 1e-4
+        Parameter for Armijo condition rule.
+    c2 : float, default: 0.4
+        Parameter for curvature condition rule.
+
+    Notes
+    -----
+    Parameters `c1` and `c2` must satisfy ``0 < c1 < c2 < 1``.
     """
     _check_unknown_options(unknown_options)
 
@@ -1827,7 +1847,7 @@ def descent_condition(alpha, xkp1, fp1, gfkp1):
         try:
             alpha_k, fc, gc, old_fval, old_old_fval, gfkp1 = \
                      _line_search_wolfe12(f, myfprime, xk, pk, gfk, old_fval,
-                                          old_old_fval, c2=0.4, amin=1e-100, amax=1e100,
+                                          old_old_fval, c1=c1, c2=c2, amin=1e-100, amax=1e100,
                                           extra_condition=descent_condition)
         except _LineSearchError:
             # Line search failed to find a better solution.
@@ -1877,7 +1897,7 @@ def descent_condition(alpha, xkp1, fp1, gfkp1):
 
 def fmin_ncg(f, x0, fprime, fhess_p=None, fhess=None, args=(), avextol=1e-5,
              epsilon=_epsilon, maxiter=None, full_output=0, disp=1, retall=0,
-             callback=None):
+             callback=None, c1=1e-3, c2=0.9):
     """
     Unconstrained minimization of a function using the Newton-CG method.
 
@@ -1915,6 +1935,10 @@ def fmin_ncg(f, x0, fprime, fhess_p=None, fhess=None, args=(), avextol=1e-5,
         If True, print convergence message.
     retall : bool, optional
         If True, return a list of results at each iteration.
+    c1 : float, default: 1e-4
+        Parameter for Armijo condition rule.
+    c2 : float, default: 0.9
+        Parameter for curvature condition rule
 
     Returns
     -------
@@ -1961,6 +1985,8 @@ def fmin_ncg(f, x0, fprime, fhess_p=None, fhess=None, args=(), avextol=1e-5,
         or box constrained minimization. (Box constraints give
         lower and upper bounds for each variable separately.)
 
+    Parameters `c1` and `c2` must satisfy ``0 < c1 < c2 < 1``.
+
     References
     ----------
     Wright & Nocedal, 'Numerical Optimization', 1999, p. 140.
@@ -1974,7 +2000,7 @@ def fmin_ncg(f, x0, fprime, fhess_p=None, fhess=None, args=(), avextol=1e-5,
 
     callback = _wrap_callback(callback)
     res = _minimize_newtoncg(f, x0, args, fprime, fhess, fhess_p,
-                             callback=callback, **opts)
+                             callback=callback, c1=c1, c2=c2, **opts)
 
     if full_output:
         retlist = (res['x'], res['fun'], res['nfev'], res['njev'],
@@ -1991,7 +2017,7 @@ def fmin_ncg(f, x0, fprime, fhess_p=None, fhess=None, args=(), avextol=1e-5,
 
 def _minimize_newtoncg(fun, x0, args=(), jac=None, hess=None, hessp=None,
                        callback=None, xtol=1e-5, eps=_epsilon, maxiter=None,
-                       disp=False, return_all=False,
+                       disp=False, return_all=False, c1=1e-3, c2=0.9,
                        **unknown_options):
     """
     Minimization of scalar function of one or more variables using the
@@ -2013,6 +2039,14 @@ def _minimize_newtoncg(fun, x0, args=(), jac=None, hess=None, hessp=None,
     return_all : bool, optional
         Set to True to return a list of the best solution at each of the
         iterations.
+    c1 : float, default: 1e-4
+        Parameter for Armijo condition rule.
+    c2 : float, default: 0.9
+        Parameter for curvature condition rule.
+
+    Notes
+    -----
+    Parameters `c1` and `c2` must satisfy ``0 < c1 < c2 < 1``.
     """
     _check_unknown_options(unknown_options)
     if jac is None:
@@ -2146,7 +2180,7 @@ def terminate(warnflag, msg):
         try:
             alphak, fc, gc, old_fval, old_old_fval, gfkp1 = \
                      _line_search_wolfe12(f, fprime, xk, pk, gfk,
-                                          old_fval, old_old_fval)
+                                          old_fval, old_old_fval, c1=c1, c2=c2)
         except _LineSearchError:
             # Line search failed to find a better solution.
             msg = "Warning: " + _status_message['pr_loss']


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
@@ -256,6 +256,15 @@ def test_bfgs_c2(self):
         res_mod = optimize.minimize(optimize.rosen,
                                     x0, method='bfgs', options={'c2': 1e-2})
         assert res_default.nit > res_mod.nit
+    
+    @pytest.mark.parametrize(["c1", "c2"], [[0.5, 2],
+                                            [-0.1, 0.1],
+                                            [0.2, 0.1]])
+    def test_invalid_c1_c2(self, c1, c2):
+        with pytest.raises(ValueError, match="'c1' and 'c2'"):
+            x0 = [10.3, 20.7, 10.8, 1.9, -1.2]
+            optimize.minimize(optimize.rosen, x0, method='cg',
+                              options={'c1': c1, 'c2': c2})
 
     def test_powell(self):
         # Powell (direction set) optimization routine