Repository: scipy/scipy

Issue:
`scipy.optimize.curve_fit`'s undocumented `return_full` keyword doesn't work with bounded problems
scipy's `curve_fit` has a useful, but undocumented, keyword `return_full`. When this keyword is set to `True` the function will return extra information that is useful for debugging and adding functionality to one's own code. However, the keyword doesn't work with bounded problems. This is because bounded problems are solved by `least_squares` and unbounded problems are solved by `leastsq` and `least_squares` returns an `OptimizeResult` which is different from what is returned by `leastsq`.


Discussion:
Thanks for the report. I guess it's possible to return the whole OptimizeResult if `full_output is True` and bounds are not None. 

or the output of `leastsq` could be packed into an `OptimizeResult` so that the return is the same in all cases.

> or the output of leastsq could be packed into an OptimizeResult so that the return is the same in all cases.

If we were starting from scratch, then yes. Otherwise no, for it breaks backwards compatibility for a not-good-enough reason.

It can be re-packed into OptimizeResult in curve_fit, however.

Yes I meant what @pv suggested. If that's still unacceptable than one could do it the other way around: repackage the `OptimizeResult` returned by `least_squares` into the same format as `leastsq`:

```
infodict = dict(nfev=res.nfev, fvec=res.fun, fjac=res.jac, ipvt=None, qtf=None)
ier = res.status
errmsg = res.message
```

Where `res` is the `OptimizeResult` returned by `least_squares` and the `None`'s would be replaced by appropriate values.


Resolving patch:
diff --git a/scipy/optimize/_minpack_py.py b/scipy/optimize/_minpack_py.py
--- a/scipy/optimize/_minpack_py.py
+++ b/scipy/optimize/_minpack_py.py
@@ -532,7 +532,7 @@ def _initialize_feasible(lb, ub):
 
 def curve_fit(f, xdata, ydata, p0=None, sigma=None, absolute_sigma=False,
               check_finite=True, bounds=(-np.inf, np.inf), method=None,
-              jac=None, **kwargs):
+              jac=None, *, full_output=False, **kwargs):
     """
     Use non-linear least squares to fit a function, f, to data.
 
@@ -613,7 +613,12 @@ def curve_fit(f, xdata, ydata, p0=None, sigma=None, absolute_sigma=False,
         a finite difference scheme, see `least_squares`.
 
         .. versionadded:: 0.18
-    kwargs
+    full_output : boolean, optional
+        If True, this function returns additioal information: `infodict`,
+        `mesg`, and `ier`.
+
+        .. versionadded:: 1.9
+    **kwargs
         Keyword arguments passed to `leastsq` for ``method='lm'`` or
         `least_squares` otherwise.
 
@@ -634,6 +639,45 @@ def curve_fit(f, xdata, ydata, p0=None, sigma=None, absolute_sigma=False,
         'lm' method returns a matrix filled with ``np.inf``, on the other hand
         'trf'  and 'dogbox' methods use Moore-Penrose pseudoinverse to compute
         the covariance matrix.
+    infodict : dict (returned only if `full_output` is True)
+        a dictionary of optional outputs with the keys:
+
+        ``nfev``
+            The number of function calls. Methods 'trf' and 'dogbox' do not
+            count function calls for numerical Jacobian approximation,
+            as opposed to 'lm' method.
+        ``fvec``
+            The function values evaluated at the solution.
+        ``fjac``
+            A permutation of the R matrix of a QR
+            factorization of the final approximate
+            Jacobian matrix, stored column wise.
+            Together with ipvt, the covariance of the
+            estimate can be approximated.
+            Method 'lm' only provides this information.
+        ``ipvt``
+            An integer array of length N which defines
+            a permutation matrix, p, such that
+            fjac*p = q*r, where r is upper triangular
+            with diagonal elements of nonincreasing
+            magnitude. Column j of p is column ipvt(j)
+            of the identity matrix.
+            Method 'lm' only provides this information.
+        ``qtf``
+            The vector (transpose(q) * fvec).
+            Method 'lm' only provides this information.
+
+        .. versionadded:: 1.9
+    mesg : str (returned only if `full_output` is True)
+        A string message giving information about the solution.
+
+        .. versionadded:: 1.9
+    ier : int (returnned only if `full_output` is True)
+        An integer flag. If it is equal to 1, 2, 3 or 4, the solution was
+        found. Otherwise, the solution was not found. In either case, the
+        optional output variable `mesg` gives more information.
+
+        .. versionadded:: 1.9
 
     Raises
     ------
@@ -784,8 +828,6 @@ def curve_fit(f, xdata, ydata, p0=None, sigma=None, absolute_sigma=False,
         if ydata.size != 1 and n > ydata.size:
             raise TypeError(f"The number of func parameters={n} must not"
                             f" exceed the number of data points={ydata.size}")
-        # Remove full_output from kwargs, otherwise we're passing it in twice.
-        return_full = kwargs.pop('full_output', False)
         res = leastsq(func, p0, Dfun=jac, full_output=1, **kwargs)
         popt, pcov, infodict, errmsg, ier = res
         ysize = len(infodict['fvec'])
@@ -803,6 +845,10 @@ def curve_fit(f, xdata, ydata, p0=None, sigma=None, absolute_sigma=False,
         if not res.success:
             raise RuntimeError("Optimal parameters not found: " + res.message)
 
+        infodict = dict(nfev=res.nfev, fvec=res.fun)
+        ier = res.status
+        errmsg = res.message
+
         ysize = len(res.fun)
         cost = 2 * res.cost  # res.cost is half sum of squares!
         popt = res.x
@@ -813,7 +859,6 @@ def curve_fit(f, xdata, ydata, p0=None, sigma=None, absolute_sigma=False,
         s = s[s > threshold]
         VT = VT[:s.size]
         pcov = np.dot(VT.T / s**2, VT)
-        return_full = False
 
     warn_cov = False
     if pcov is None:
@@ -833,7 +878,7 @@ def curve_fit(f, xdata, ydata, p0=None, sigma=None, absolute_sigma=False,
         warnings.warn('Covariance of the parameters could not be estimated',
                       category=OptimizeWarning)
 
-    if return_full:
+    if full_output:
         return popt, pcov, infodict, errmsg, ier
     else:
         return popt, pcov


Test patch:
diff --git a/scipy/optimize/tests/test_minpack.py b/scipy/optimize/tests/test_minpack.py
--- a/scipy/optimize/tests/test_minpack.py
+++ b/scipy/optimize/tests/test_minpack.py
@@ -576,6 +576,26 @@ def f(x, a, b):
 
         assert_raises(ValueError, curve_fit, f, xdata, ydata, method='unknown')
 
+    def test_full_output(self):
+        def f(x, a, b):
+            return a * np.exp(-b * x)
+
+        xdata = np.linspace(0, 1, 11)
+        ydata = f(xdata, 2., 2.)
+
+        for method in ['trf', 'dogbox', 'lm', None]:
+            popt, pcov, infodict, errmsg, ier = curve_fit(
+                f, xdata, ydata, method=method, full_output=True)
+            assert_allclose(popt, [2., 2.])
+            assert "nfev" in infodict
+            assert "fvec" in infodict
+            if method == 'lm' or method is None:
+                assert "fjac" in infodict
+                assert "ipvt" in infodict
+                assert "qtf" in infodict
+            assert isinstance(errmsg, str)
+            assert ier in (1, 2, 3, 4)
+
     def test_bounds(self):
         def f(x, a, b):
             return a * np.exp(-b*x)