Repository: scipy/scipy

Issue:
minimize_scalar doesn't seem to honor "disp" option
The docs for `minimize_scalar` say

```
options : dict, optional
    A dictionary of solver options.
        maxiter : int
            Maximum number of iterations to perform.
        disp : bool
            Set to True to print convergence messages.
```

but

```
In [11]: optimize.minimize_scalar(lambda x: x ** 2, method="brent", options={"disp": True})
/usr/lib/python3.5/site-packages/ipykernel/__main__.py:1: OptimizeWarning: Unknown solver options: disp
  if __name__ == '__main__':
Out[11]: 
     fun: 0.0
    nfev: 5
     nit: 4
 success: True
       x: 0.0

In [12]: optimize.minimize_scalar(lambda x: x ** 2, method="golden", options={"disp": True})
/usr/lib/python3.5/site-packages/ipykernel/__main__.py:1: OptimizeWarning: Unknown solver options: disp
  if __name__ == '__main__':
Out[12]: 
  fun: 0.0
 nfev: 818
    x: 1.5717277788484873e-162

In [13]: optimize.minimize_scalar(lambda x: x ** 2, method="bounded", bounds=(-1, 1), options={"maxiter": 2})
Out[13]: 
     fun: 0.055728090000841161
 message: 'Maximum number of function calls reached.'
    nfev: 2
  status: 1
 success: False
       x: 0.23606797749978958
```

Note that there's no warning but no output either in the last case.



Discussion:
I've just come across this. Reproduced using:
```
minimize_scalar(lambda x: x**2 + 3, options={'disp': True})
```

Function signatures missing the disp option:
```
def _minimize_scalar_brent(func, brack=None, args=(),
                           xtol=1.48e-8, maxiter=500,
                           **unknown_options):
```
```
def _minimize_scalar_golden(func, brack=None, args=(),
                            xtol=_epsilon, maxiter=5000, **unknown_options):
```

Is this by design or is the logging just missing? The docs for `minimize_scalar` state that the disp option is always valid.
@pv could you confirm whether this needs needs implementing? Happy to open PR
Bumping this. Is there a plan to adjust this? 
Hi,
I bumped into this issue. Is there any solution?
version = '1.7.0'
Still a problem in 2021, version 1.7.1, any update?
Let me put a `1.7.2` milestone on this issue in case that helps it get a bit more visibility--I see it has been open for 5 years
I've had a look at `minimize_scalar`. Out of the three methods only one offers the `disp` option. `bounded` is the only one who does so.
The three options are:

> 0 : no message printing. 1 : non-convergence notification messages only. 2 : print a message on convergence too. 3 : print iteration results.

Dealing with 0 and 3 is fairly straightforward. 1 and 2 is slightly more involved. THere is an [`_endprint`](https://github.com/scipy/scipy/blob/47bb6febaa10658c72962b9615d5d5aa2513fa3a/scipy/optimize/optimize.py#L3116) utility function, but whether that can be used will need a little looking into.

Resolving patch:
diff --git a/scipy/optimize/_minimize.py b/scipy/optimize/_minimize.py
--- a/scipy/optimize/_minimize.py
+++ b/scipy/optimize/_minimize.py
@@ -788,6 +788,11 @@ def minimize_scalar(fun, bracket=None, bounds=None, args=(),
         else:
             options.setdefault('xtol', tol)
 
+    # replace boolean "disp" option, if specified, by an integer value.
+    disp = options.get('disp')
+    if isinstance(disp, bool):
+        options['disp'] = 2 * int(disp)
+
     if meth == '_custom':
         return method(fun, args=args, bracket=bracket, bounds=bounds, **options)
     elif meth == 'brent':
@@ -796,11 +801,6 @@ def minimize_scalar(fun, bracket=None, bounds=None, args=(),
         if bounds is None:
             raise ValueError('The `bounds` parameter is mandatory for '
                              'method `bounded`.')
-        # replace boolean "disp" option, if specified, by an integer value, as
-        # expected by _minimize_scalar_bounded()
-        disp = options.get('disp')
-        if isinstance(disp, bool):
-            options['disp'] = 2 * int(disp)
         return _minimize_scalar_bounded(fun, bounds, args, **options)
     elif meth == 'golden':
         return _minimize_scalar_golden(fun, bracket, args, **options)
diff --git a/scipy/optimize/optimize.py b/scipy/optimize/optimize.py
--- a/scipy/optimize/optimize.py
+++ b/scipy/optimize/optimize.py
@@ -2161,7 +2161,7 @@ def _minimize_scalar_bounded(func, bounds, args=(),
 class Brent:
     #need to rethink design of __init__
     def __init__(self, func, args=(), tol=1.48e-8, maxiter=500,
-                 full_output=0):
+                 full_output=0, disp=0):
         self.func = func
         self.args = args
         self.tol = tol
@@ -2172,6 +2172,7 @@ def __init__(self, func, args=(), tol=1.48e-8, maxiter=500,
         self.fval = None
         self.iter = 0
         self.funcalls = 0
+        self.disp = disp
 
     # need to rethink design of set_bracket (new options, etc.)
     def set_bracket(self, brack=None):
@@ -2228,6 +2229,12 @@ def optimize(self):
         deltax = 0.0
         funcalls += 1
         iter = 0
+
+        if self.disp > 2:
+            print(" ")
+            print(f"{'Func-count':^12} {'x':^12} {'f(x)': ^12}")
+            print(f"{funcalls:^12g} {x:^12.6g} {fx:^12.6g}")
+
         while (iter < self.maxiter):
             tol1 = self.tol * np.abs(x) + _mintol
             tol2 = 2.0 * tol1
@@ -2306,6 +2313,9 @@ def optimize(self):
                 fw = fx
                 fx = fu
 
+            if self.disp > 2:
+                print(f"{funcalls:^12g} {x:^12.6g} {fx:^12.6g}")
+
             iter += 1
         #################################
         #END CORE ALGORITHM
@@ -2342,7 +2352,7 @@ def brent(func, args=(), brack=None, tol=1.48e-8, full_output=0, maxiter=500):
         `bracket`). Providing the pair (xa,xb) does not always mean
         the obtained solution will satisfy xa<=x<=xb.
     tol : float, optional
-        Stop if between iteration change is less than `tol`.
+        Relative error in solution `xopt` acceptable for convergence.
     full_output : bool, optional
         If True, return all output args (xmin, fval, iter,
         funcalls).
@@ -2402,8 +2412,8 @@ def brent(func, args=(), brack=None, tol=1.48e-8, full_output=0, maxiter=500):
         return res['x']
 
 
-def _minimize_scalar_brent(func, brack=None, args=(),
-                           xtol=1.48e-8, maxiter=500,
+def _minimize_scalar_brent(func, brack=None, args=(), xtol=1.48e-8,
+                           maxiter=500, disp=0,
                            **unknown_options):
     """
     Options
@@ -2412,7 +2422,12 @@ def _minimize_scalar_brent(func, brack=None, args=(),
         Maximum number of iterations to perform.
     xtol : float
         Relative error in solution `xopt` acceptable for convergence.
-
+    disp: int, optional
+        If non-zero, print messages.
+            0 : no message printing.
+            1 : non-convergence notification messages only.
+            2 : print a message on convergence too.
+            3 : print iteration results.
     Notes
     -----
     Uses inverse parabolic interpolation when possible to speed up
@@ -2425,13 +2440,24 @@ def _minimize_scalar_brent(func, brack=None, args=(),
         raise ValueError('tolerance should be >= 0, got %r' % tol)
 
     brent = Brent(func=func, args=args, tol=tol,
-                  full_output=True, maxiter=maxiter)
+                  full_output=True, maxiter=maxiter, disp=disp)
     brent.set_bracket(brack)
     brent.optimize()
     x, fval, nit, nfev = brent.get_result(full_output=True)
 
     success = nit < maxiter and not (np.isnan(x) or np.isnan(fval))
 
+    # for 'disp' purposes
+    if success and disp > 1:
+        print("\nOptimization terminated successfully;\n"
+              "The returned value satisfies the termination criteria\n"
+              "(using xtol = ", xtol, ")")
+    elif not success and disp:
+        if nit >= maxiter:
+            print("\nMaximum number of iterations exceeded")
+        if np.isnan(x) or np.isnan(fval):
+            print("\n{}".format(_status_message['nan']))
+
     return OptimizeResult(fun=fval, x=x, nit=nit, nfev=nfev,
                           success=success)
 
@@ -2504,15 +2530,21 @@ def golden(func, args=(), brack=None, tol=_epsilon,
 
 
 def _minimize_scalar_golden(func, brack=None, args=(),
-                            xtol=_epsilon, maxiter=5000, **unknown_options):
+                            xtol=_epsilon, maxiter=5000, disp=0,
+                            **unknown_options):
     """
     Options
     -------
-    maxiter : int
-        Maximum number of iterations to perform.
     xtol : float
         Relative error in solution `xopt` acceptable for convergence.
-
+    maxiter : int
+        Maximum number of iterations to perform.
+    disp: int, optional
+        If non-zero, print messages.
+            0 : no message printing.
+            1 : non-convergence notification messages only.
+            2 : print a message on convergence too.
+            3 : print iteration results.
     """
     _check_unknown_options(unknown_options)
     tol = xtol
@@ -2550,6 +2582,11 @@ def _minimize_scalar_golden(func, brack=None, args=(),
     f2 = func(*((x2,) + args))
     funcalls += 2
     nit = 0
+
+    if disp > 2:
+        print(" ")
+        print(f"{'Func-count':^12} {'x':^12} {'f(x)': ^12}")
+
     for i in range(maxiter):
         if np.abs(x3 - x0) <= tol * (np.abs(x1) + np.abs(x2)):
             break
@@ -2566,7 +2603,16 @@ def _minimize_scalar_golden(func, brack=None, args=(),
             f2 = f1
             f1 = func(*((x1,) + args))
         funcalls += 1
+        if disp > 2:
+            if (f1 < f2):
+                xmin, fval = x1, f1
+            else:
+                xmin, fval = x2, f2
+            print(f"{funcalls:^12g} {xmin:^12.6g} {fval:^12.6g}")
+
         nit += 1
+    # end of iteration loop
+
     if (f1 < f2):
         xmin = x1
         fval = f1
@@ -2576,6 +2622,17 @@ def _minimize_scalar_golden(func, brack=None, args=(),
 
     success = nit < maxiter and not (np.isnan(fval) or np.isnan(xmin))
 
+    # for 'disp' purposes
+    if success and disp > 1:
+        print("\nOptimization terminated successfully;\n"
+              "The returned value satisfies the termination criteria\n"
+              "(using xtol = ", xtol, ")")
+    elif not success and disp:
+        if nit >= maxiter:
+            print("\nMaximum number of iterations exceeded")
+        if np.isnan(xmin) or np.isnan(fval):
+            print("\n{}".format(_status_message['nan']))
+
     return OptimizeResult(fun=fval, nfev=funcalls, x=xmin, nit=nit,
                           success=success)
 


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
@@ -1580,6 +1580,12 @@ def test_minimize_scalar_coerce_args_param(self):
         # Regression test for gh-3503
         optimize.minimize_scalar(self.fun, args=1.5)
 
+    @pytest.mark.parametrize('method', ['brent', 'bounded', 'golden'])
+    def test_disp(self, method):
+        # test that all minimize_scalar methods accept a disp option.
+        for disp in [0, 1, 2, 3]:
+            optimize.minimize_scalar(self.fun, options={"disp": disp})
+
     @pytest.mark.parametrize('method', ['brent', 'bounded', 'golden'])
     def test_nan_values(self, method):
         # Check nan values result to failed exit status