Repository: lmfit/lmfit-py

Issue:
analytic Jacobians not correct
see discussion at https://github.com/jjhelmus/leastsqbound-scipy/issues/6
and https://github.com/jjhelmus/leastsqbound-scipy/commit/e0ed1afbcd4c4b587e63366feffd4de8dffc1b0e

The fix should not be too hard, but slightly more complicated than for leastsqbound.



Discussion:

Resolving patch:
diff --git a/lmfit/minimizer.py b/lmfit/minimizer.py
--- a/lmfit/minimizer.py
+++ b/lmfit/minimizer.py
@@ -262,15 +262,26 @@ def __jacobian(self, fvars):
         analytical jacobian to be used with the Levenberg-Marquardt
 
         modified 02-01-2012 by Glenn Jones, Aberystwyth University
+        modified 06-29-2015 M Newville to apply gradient scaling
+               for bounded variables (thanks to JJ Helmus, N Mayorov)
         """
-        params = self.result.params
-        for name, val in zip(self.result.var_names, fvars):
-            params[name].value = params[name].from_internal(val)
+        pars  = self.result.params
+        grad_scale = ones_like(fvars)
+        for ivar, name in enumerate(self.result.var_names):
+            val = fvars[ivar]
+            pars[name].value = pars[name].from_internal(val)
+            grad_scale[ivar] = pars[name].scale_gradient(val)
 
         self.result.nfev = self.result.nfev + 1
-        self.result.params.update_constraints()
-        # computing the jacobian
-        return self.jacfcn(params, *self.userargs, **self.userkws)
+        pars.update_constraints()
+        # compute the jacobian for "internal" unbounded variables,
+        # the rescale for bounded "external" variables.
+        jac = self.jacfcn(pars, *self.userargs, **self.userkws)
+        if self.col_deriv:
+            jac = (jac.transpose()*grad_scale).transpose()
+        else:
+            jac = jac*grad_scale
+        return jac
 
     def penalty(self, fvars):
         """
@@ -508,14 +519,16 @@ def leastsq(self, params=None, **kws):
         result = self.prepare_fit(params=params)
         vars   = result.init_vals
         nvars = len(vars)
-        lskws = dict(full_output=1, xtol=1.e-7, ftol=1.e-7,
+        lskws = dict(full_output=1, xtol=1.e-7, ftol=1.e-7, col_deriv=False,
                      gtol=1.e-7, maxfev=2000*(nvars+1), Dfun=None)
 
         lskws.update(self.kws)
         lskws.update(kws)
 
+        self.col_deriv = False
         if lskws['Dfun'] is not None:
             self.jacfcn = lskws['Dfun']
+            self.col_deriv = lskws['col_deriv']
             lskws['Dfun'] = self.__jacobian
 
         # suppress runtime warnings during fit and error analysis


Test patch:
diff --git a/tests/test_bounded_jacobian.py b/tests/test_bounded_jacobian.py
new file mode 100644
--- /dev/null
+++ b/tests/test_bounded_jacobian.py
@@ -0,0 +1,43 @@
+from lmfit import Parameters, minimize, fit_report
+from lmfit_testutils import assert_paramval, assert_paramattr
+
+import numpy as np
+
+
+def test_bounded_jacobian():
+    pars = Parameters()
+    pars.add('x0', value=2.0)
+    pars.add('x1', value=2.0, min=1.5)
+
+    global jac_count
+
+    jac_count = 0
+
+    def resid(params):
+        x0 = params['x0'].value
+        x1 = params['x1'].value
+        return np.array([10 * (x1 - x0*x0), 1-x0])
+
+    def jac(params):
+        global jac_count
+        jac_count += 1
+        x0 = params['x0'].value
+        return np.array([[-20*x0, 10], [-1, 0]])
+    
+    out0 = minimize(resid, pars, Dfun=None)
+
+    assert_paramval(out0.params['x0'], 1.2243, tol=0.02)
+    assert_paramval(out0.params['x1'], 1.5000, tol=0.02)
+    assert(jac_count == 0)
+
+    out1 = minimize(resid, pars, Dfun=jac)
+
+    assert_paramval(out1.params['x0'], 1.2243, tol=0.02)
+    assert_paramval(out1.params['x1'], 1.5000, tol=0.02)
+    assert(jac_count > 5)
+
+    print(fit_report(out1, show_correl=True))
+
+
+if __name__ == '__main__':
+    test_bounded_jacobian()
diff --git a/tests/test_multidatasets.py b/tests/test_multidatasets.py
--- a/tests/test_multidatasets.py
+++ b/tests/test_multidatasets.py
@@ -2,7 +2,6 @@
 # example fitting to multiple (simulated) data sets
 #
 import numpy as np
-import matplotlib.pyplot as plt
 from lmfit import minimize, Parameters, fit_report
 from lmfit.lineshapes import gaussian
 
@@ -73,4 +72,3 @@ def test_multidatasets():
 
 if __name__ == '__main__':
     test_multidatasets()
-
diff --git a/tests/test_nose.py b/tests/test_nose.py
--- a/tests/test_nose.py
+++ b/tests/test_nose.py
@@ -128,7 +128,7 @@ def dfunc(pars, x, data=None):
         b = pars['b'].value
         c = pars['c'].value
         v = np.exp(-b*x)
-        return [v, -a*x*v, np.ones(len(x))]
+        return np.array([v, -a*x*v, np.ones(len(x))])
 
     def f(var, x):
         return var[0]* np.exp(-var[1] * x)+var[2]
diff --git a/tests/test_stepmodel.py b/tests/test_stepmodel.py
--- a/tests/test_stepmodel.py
+++ b/tests/test_stepmodel.py
@@ -3,8 +3,6 @@
 from lmfit.models import StepModel, ConstantModel
 from lmfit_testutils import assert_paramval, assert_paramattr
 
-import matplotlib.pyplot as plt
-
 def get_data():
     x  = np.linspace(0, 10, 201)
     dat = np.ones_like(x)
@@ -58,4 +56,3 @@ def test_stepmodel_erf():
 if __name__ == '__main__':
     # test_stepmodel_linear()
     test_stepmodel_erf()
-