Repository: lmfit/lmfit-py

Issue:
BUG: lmfit.conf_interval does not work with len(p_names) = 1
#### Description
The lmfit confidence calculation fails when len(p_names) < 2. This is due to bug in the counting of the free variables: Instead counting all parameters where vary is True, it counts only the one which are in p_names.

BUG: lmfit.conf_interval does not work with len(p_names) = 1
#### Description
The lmfit confidence calculation fails when len(p_names) < 2. This is due to bug in the counting of the free variables: Instead counting all parameters where vary is True, it counts only the one which are in p_names.



Discussion:
@Tillsten Oh, thanks.  We should fix that. And maybe add a test of `conf_intervals` with 1 variable.
Yeah, it almost a one-liner. I am just on a machine where I don't have a good dev environment.
@Tillsten Oh, thanks.  We should fix that. And maybe add a test of `conf_intervals` with 1 variable.
Yeah, it almost a one-liner. I am just on a machine where I don't have a good dev environment.

Resolving patch:
diff --git a/lmfit/confidence.py b/lmfit/confidence.py
--- a/lmfit/confidence.py
+++ b/lmfit/confidence.py
@@ -182,15 +182,11 @@ def __init__(self, minimizer, result, p_names=None, prob_func=None,
 
         # check that there are at least 2 true variables!
         # check that all stderrs are sensible (including not None or NaN)
-        nvars = 0
+
         for par in self.fit_params:
-            if par.vary:
-                nvars += 1
-            try:
-                if not (par.vary and par.stderr > 0.0):
-                    raise MinimizerException(CONF_ERR_STDERR)
-            except TypeError:
+            if par.vary and (par.stderr is None or par.stderr is np.nan):
                 raise MinimizerException(CONF_ERR_STDERR)
+        nvars = len([p for p in self.params.values() if p.vary])
         if nvars < 2:
             raise MinimizerException(CONF_ERR_NVARS)
 
@@ -255,7 +251,6 @@ def calc_ci(self, para, direction):
             try:
                 val = brentq(calc_prob, a_limit,
                              limit, rtol=.5e-4, args=prob)
-
             except ValueError:
                 self.reset_vals()
                 try:
@@ -293,16 +288,35 @@ def find_limit(self, para, direction):
         old_prob = 0
         limit = start_val
         i = 0
+        bound_reached = False
+        max_prob = max(self.probs)
 
-        while old_prob < max(self.probs):
+        while old_prob < max_prob:
             i = i + 1
             limit += step * direction
+            if limit > para.max:
+                limit = para.max
+                bound_reached = True
+            elif limit < para.min:
+                limit = para.min
+                bound_reached = True
 
             new_prob = self.calc_prob(para, limit)
-            rel_change = (new_prob - old_prob) / max(new_prob, old_prob, 1.e-12)
+            rel_change = (new_prob - old_prob) / max(new_prob, old_prob, 1e-12)
             old_prob = new_prob
+            if self.verbose:
+                msg = "P({}={}) = {}, max. prob={}"
+                print(msg.format(para.name, limit, new_prob, max_prob))
 
             # check for convergence
+            if bound_reached:
+                if new_prob < max(self.probs):
+                    errmsg = ("Bound reached with "
+                              "prob({}={}) = {} < max(sigmas)"
+                              ).format(para.name, limit, new_prob)
+                    warn(errmsg)
+                    break
+
             if i > self.maxiter:
                 errmsg = "maxiter={} reached ".format(self.maxiter)
                 errmsg += ("and prob({}={}) = {} < "


Test patch:
diff --git a/tests/test_confidence.py b/tests/test_confidence.py
--- a/tests/test_confidence.py
+++ b/tests/test_confidence.py
@@ -12,7 +12,7 @@ def data():
     """Generate synthetic data."""
     x = np.linspace(0.3, 10, 100)
     np.random.seed(0)
-    y = 1.0/(0.1*x) + 2.0 + 0.1*np.random.randn(x.size)
+    y = 1.0 / (0.1 * x) + 2.0 + 0.1 * np.random.randn(x.size)
     return (x, y)
 
 
@@ -26,7 +26,7 @@ def pars():
 
 def residual(params, x, data):
     """Define objective function for the minimization."""
-    return data - 1.0/(params['a']*x) + params['b']
+    return data - 1.0 / (params['a'] * x) + params['b']
 
 
 @pytest.mark.parametrize("verbose", [False, True])
@@ -54,6 +54,40 @@ def test_confidence_leastsq(data, pars, verbose, capsys):
         assert 'Calculating CI for' in captured.out
 
 
+def test_confidence_pnames(data, pars):
+    """Test if pnames works as expected."""
+    minimizer = lmfit.Minimizer(residual, pars, fcn_args=(data))
+    out = minimizer.leastsq()
+
+    assert_paramval(out.params['a'], 0.1, tol=0.1)
+    assert_paramval(out.params['b'], -2.0, tol=0.1)
+
+    ci = lmfit.conf_interval(minimizer, out, p_names=['a'])
+    assert 'a' in ci
+    assert 'b' not in ci
+
+
+def test_confidence_bounds_reached(data, pars):
+    """Check if conf_interval handles bounds correctly"""
+
+    # Should work
+    pars['a'].max = 0.2
+    minimizer = lmfit.Minimizer(residual, pars, fcn_args=(data))
+    out = minimizer.leastsq()
+    out.params['a'].stderr = 1
+    lmfit.conf_interval(minimizer, out, verbose=True)
+
+    # Should warn
+    pars['b'].max = 2.03
+    pars['b'].min = 1.97
+    minimizer = lmfit.Minimizer(residual, pars, fcn_args=(data))
+    out = minimizer.leastsq()
+    out.params['b'].stderr = 0.005
+    out.params['a'].stderr = 0.01
+    with pytest.warns(UserWarning, match="Bound reached"):
+        lmfit.conf_interval(minimizer, out, verbose=True)
+
+
 def test_confidence_sigma_vs_prob(data, pars):
     """Calculate confidence by specifying sigma or probability."""
     minimizer = lmfit.Minimizer(residual, pars, fcn_args=(data))
@@ -61,8 +95,9 @@ def test_confidence_sigma_vs_prob(data, pars):
 
     ci_sigmas = lmfit.conf_interval(minimizer, out, sigmas=[1, 2, 3])
     ci_1sigma = lmfit.conf_interval(minimizer, out, sigmas=[1])
-    ci_probs = lmfit.conf_interval(minimizer, out, sigmas=[0.68269, 0.9545,
-                                                           0.9973])
+    ci_probs = lmfit.conf_interval(minimizer,
+                                   out,
+                                   sigmas=[0.68269, 0.9545, 0.9973])
 
     assert_allclose(ci_sigmas['a'][0][1], ci_probs['a'][0][1], rtol=0.01)
     assert_allclose(ci_sigmas['b'][2][1], ci_probs['b'][2][1], rtol=0.01)
@@ -72,7 +107,9 @@ def test_confidence_sigma_vs_prob(data, pars):
 
 def test_confidence_exceptions(data, pars):
     """Make sure the proper exceptions are raised when needed."""
-    minimizer = lmfit.Minimizer(residual, pars, calc_covar=False,
+    minimizer = lmfit.Minimizer(residual,
+                                pars,
+                                calc_covar=False,
                                 fcn_args=data)
     out = minimizer.minimize(method='nelder')
     out_lsq = minimizer.minimize(params=out.params, method='leastsq')