Repository: scipy/scipy

Issue:
Optimization functions should count approx_grad function evaluations (Trac #1786)
_Original ticket http://projects.scipy.org/scipy/ticket/1786 on 2012-12-10 by @pv, assigned to @dlax._

The function evaluations from `approx_grad` are not counted in optimization routines.

Since several of these support limiting the function evaluation count (e.g. L-BFGS-B), it would make sense to include the gradient approximation into the limit.



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
@@ -69,7 +69,7 @@ def minimize(fun, x0, args=(), method='BFGS', jac=None, hess=None,
 
     jac : bool or callable, optional
         Jacobian of objective function. Only for CG, BFGS, Newton-CG,
-        dogleg, trust-ncg.
+        L-BFGS-B, TNC, SLSQP, dogleg, trust-ncg.
         If `jac` is a Boolean and is True, `fun` is assumed to return the
         value of Jacobian along with the objective function. If False, the
         Jacobian will be estimated numerically.
diff --git a/scipy/optimize/lbfgsb.py b/scipy/optimize/lbfgsb.py
--- a/scipy/optimize/lbfgsb.py
+++ b/scipy/optimize/lbfgsb.py
@@ -38,7 +38,8 @@
 import numpy as np
 from numpy import array, asarray, float64, int32, zeros
 from . import _lbfgsb
-from .optimize import approx_fprime, MemoizeJac, Result, _check_unknown_options
+from .optimize import (approx_fprime, MemoizeJac, Result,
+                       _check_unknown_options, wrap_function)
 
 __all__ = ['fmin_l_bfgs_b']
 
@@ -253,6 +254,7 @@ def _minimize_lbfgsb(fun, x0, args=(), jac=None, bounds=None,
         else:
             iprint = disp
 
+    n_function_evals, fun = wrap_function(fun, ())
     if jac is None:
         def func_and_grad(x):
             f = fun(x, *args)
@@ -294,7 +296,6 @@ def func_and_grad(x):
 
     task[:] = 'START'
 
-    n_function_evals = 0
     n_iterations = 0
 
     while 1:
@@ -304,12 +305,11 @@ def func_and_grad(x):
                        isave, dsave)
         task_str = task.tostring()
         if task_str.startswith(b'FG'):
-            if n_function_evals > maxfun:
+            if n_function_evals[0] > maxfun:
                 task[:] = ('STOP: TOTAL NO. of f AND g EVALUATIONS '
                            'EXCEEDS LIMIT')
             else:
                 # minimization routine wants f and g at the current x
-                n_function_evals += 1
                 # Overwrite f and g:
                 f, g = func_and_grad(x)
         elif task_str.startswith(b'NEW_X'):
@@ -326,14 +326,14 @@ def func_and_grad(x):
     task_str = task.tostring().strip(b'\x00').strip()
     if task_str.startswith(b'CONV'):
         warnflag = 0
-    elif n_function_evals > maxfun:
+    elif n_function_evals[0] > maxfun:
         warnflag = 1
     elif n_iterations > maxiter:
         warnflag = 1
     else:
         warnflag = 2
 
-    return Result(fun=f, jac=g, nfev=n_function_evals, nit=n_iterations,
+    return Result(fun=f, jac=g, nfev=n_function_evals[0], nit=n_iterations,
                   status=warnflag, message=task_str, x=x,
                   success=(warnflag == 0))
 


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
@@ -392,11 +392,18 @@ def fun(x):
     def test_minimize_l_bfgs_b(self):
         """ Minimize with L-BFGS-B method """
         opts = {'disp': False, 'maxiter': self.maxiter}
-        x = optimize.minimize(self.func, self.startparams,
+        r = optimize.minimize(self.func, self.startparams,
                               method='L-BFGS-B', jac=self.grad,
-                              options=opts)['x']
-        assert_allclose(self.func(x), self.func(self.solution),
+                              options=opts)
+        assert_allclose(self.func(r.x), self.func(self.solution),
                         atol=1e-6)
+        # approximate jacobian
+        ra = optimize.minimize(self.func, self.startparams,
+                               method='L-BFGS-B', options=opts)
+        assert_allclose(self.func(ra.x), self.func(self.solution),
+                        atol=1e-6)
+        # check that function evaluations in approximate jacobian are counted
+        assert_(ra.nfev > r.nfev)
 
     def test_minimize_l_bfgs_b_ftol(self):
         # Check that the `ftol` parameter in l_bfgs_b works as expected