Repository: lmfit/lmfit-py

Issue:
Wrong documentation for Split Lorenztian model
The documentation is wrong for the Split Lorenztian model.

From the built-in models page, the fit function is:
split_lorentzian(x, amplitude, center, sigma, sigma_r) =
        amplitude * 2 / pi / (sigma + sigma_r) * \
        (1 / (sigma ** 2+ ((x - center) )**2) * (x < center)
         + 1 / (sigma_r ** 2+ ((x - center) )**2) * (x >= center))


The actual fit function from lineshapes.py is:
split_lorentzian(x, amplitude, center, sigma, sigma_r) =
        amplitude * 2 / pi / (sigma + sigma_r) * \
        (1 / (1 + ((x - center) / sigma)**2) * (x < center)
         + 1 / (1 + ((x - center) / sigma_r)**2) * (x >= center)).


Discussion:
@teajohns Oh, yes that definition and the docstring for the function is kind of a mess to read.  I will fix docstring and doc latex.  Thanks!

Resolving patch:
diff --git a/lmfit/lineshapes.py b/lmfit/lineshapes.py
--- a/lmfit/lineshapes.py
+++ b/lmfit/lineshapes.py
@@ -52,14 +52,17 @@ def split_lorentzian(x, amplitude=1.0, center=0.0, sigma=1.0, sigma_r=1.0):
     to ``amplitude``.
 
     split_lorentzian(x, amplitude, center, sigma, sigma_r) =
-        amplitude * 2 / pi / (sigma + sigma_r) * \
-        (1 / (1 + ((x - center) / sigma)**2) * (x < center)
-         + 1 / (1 + ((x - center) / sigma_r)**2) * (x >= center))
-
+        [2*amplitude / (pi* (sigma + sigma_r)] *
+         {  sigma**2   * (x<center)  / [sigma**2  + (x - center)**2]
+          + sigma_r**2 * (x>=center) / [sigma_r**2+ (x - center)**2] }
     """
-    return amplitude * 2 / pi / max(tiny, (sigma + sigma_r)) * \
-        (1 / (1 + ((x - center) / max(tiny, sigma))**2) * (x < center)
-         + 1 / (1 + ((x - center) / max(tiny, sigma_r))**2) * (x >= center))
+    s = max(tiny, sigma)
+    r = max(tiny, sigma_r)
+    s2 = s*s
+    r2 = r*r
+    xc2 = (x-center)**2
+    amp = 2*amplitude/(pi*(s+r))
+    return amp*(s2*(x < center)/(s2+xc2) + r2*(x >= center)/(r2+xc2))
 
 
 def voigt(x, amplitude=1.0, center=0.0, sigma=1.0, gamma=None):
diff --git a/lmfit/models.py b/lmfit/models.py
--- a/lmfit/models.py
+++ b/lmfit/models.py
@@ -390,7 +390,7 @@ class SplitLorentzianModel(Model):
 
     .. math::
 
-        f(x; A, \mu, \sigma, \sigma_r) = \frac{2 A}{\pi (\sigma+\sigma_r)} \big[\frac{1}{(x - \mu)^2 + \sigma^2} * H(\mu-x) + \frac{1}{(x - \mu)^2 + \sigma_r^2} * H(x-\mu)\big]
+        f(x; A, \mu, \sigma, \sigma_r) = \frac{2 A}{\pi (\sigma+\sigma_r)} \big[\frac{\sigma^2}{(x - \mu)^2 + \sigma^2} * H(\mu-x) + \frac{\sigma_r^2}{(x - \mu)^2 + \sigma_r^2} * H(x-\mu)\big]
 
     where the parameter ``amplitude`` corresponds to :math:`A`, ``center`` to
     :math:`\mu`, ``sigma`` to :math:`\sigma`, ``sigma_l`` to


Test patch:
diff --git a/tests/test_lineshapes_models.py b/tests/test_lineshapes_models.py
--- a/tests/test_lineshapes_models.py
+++ b/tests/test_lineshapes_models.py
@@ -4,6 +4,8 @@
 import sys
 
 import numpy as np
+from numpy.testing import assert_allclose
+
 import pytest
 from scipy.optimize import fsolve
 
@@ -195,6 +197,70 @@ def test_height_and_fwhm_expression_evalution_in_builtin_models():
         params.update_constraints()
 
 
+def test_guess_modelparams():
+    x = np.linspace(-10, 10, 501)
+
+    mod = models.ConstantModel()
+    y = 6.0 + x*0.005
+    pars = mod.guess(y)
+    assert_allclose(pars['c'].value, 6.0, rtol=0.01)
+
+    mod = models.ComplexConstantModel(prefix='f_')
+    y = 6.0 + x*0.005 + (4.0 - 0.02*x)*1j
+    pars = mod.guess(y)
+    assert_allclose(pars['f_re'].value, 6.0, rtol=0.01)
+    assert_allclose(pars['f_im'].value, 4.0, rtol=0.01)
+
+    mod = models.QuadraticModel(prefix='g_')
+    y = -0.2 + 3.0*x + 0.005*x**2
+    pars = mod.guess(y, x=x)
+    assert_allclose(pars['g_a'].value, 0.005, rtol=0.01)
+    assert_allclose(pars['g_b'].value, 3.0, rtol=0.01)
+    assert_allclose(pars['g_c'].value, -0.2, rtol=0.01)
+
+    mod = models.PolynomialModel(4, prefix='g_')
+    y = -0.2 + 3.0*x + 0.005*x**2 - 3.3e-6*x**3 + 1.e-9*x**4
+    pars = mod.guess(y, x=x)
+    assert_allclose(pars['g_c0'].value, -0.2, rtol=0.01)
+    assert_allclose(pars['g_c1'].value, 3.0, rtol=0.01)
+    assert_allclose(pars['g_c2'].value, 0.005, rtol=0.1)
+    assert_allclose(pars['g_c3'].value, -3.3e-6, rtol=0.1)
+    assert_allclose(pars['g_c4'].value, 1.e-9, rtol=0.1)
+
+    mod = models.GaussianModel(prefix='g_')
+    y = lineshapes.gaussian(x, amplitude=2.2, center=0.25, sigma=1.3)
+    y += np.random.normal(size=len(x), scale=0.004)
+    pars = mod.guess(y, x=x)
+    assert_allclose(pars['g_amplitude'].value, 3, rtol=2)
+    assert_allclose(pars['g_center'].value, 0.25, rtol=1)
+    assert_allclose(pars['g_sigma'].value, 1.3, rtol=1)
+
+    mod = models.LorentzianModel(prefix='l_')
+    pars = mod.guess(y, x=x)
+    assert_allclose(pars['l_amplitude'].value, 3, rtol=2)
+    assert_allclose(pars['l_center'].value, 0.25, rtol=1)
+    assert_allclose(pars['l_sigma'].value, 1.3, rtol=1)
+
+    mod = models.SplitLorentzianModel(prefix='s_')
+    pars = mod.guess(y, x=x)
+    assert_allclose(pars['s_amplitude'].value, 3, rtol=2)
+    assert_allclose(pars['s_center'].value, 0.25, rtol=1)
+    assert_allclose(pars['s_sigma'].value, 1.3, rtol=1)
+    assert_allclose(pars['s_sigma_r'].value, 1.3, rtol=1)
+
+    mod = models.VoigtModel(prefix='l_')
+    pars = mod.guess(y, x=x)
+    assert_allclose(pars['l_amplitude'].value, 3, rtol=2)
+    assert_allclose(pars['l_center'].value, 0.25, rtol=1)
+    assert_allclose(pars['l_sigma'].value, 1.3, rtol=1)
+
+    mod = models.SkewedVoigtModel(prefix='l_')
+    pars = mod.guess(y, x=x)
+    assert_allclose(pars['l_amplitude'].value, 3, rtol=2)
+    assert_allclose(pars['l_center'].value, 0.25, rtol=1)
+    assert_allclose(pars['l_sigma'].value, 1.3, rtol=1)
+
+
 def test_splitlorentzian_prefix():
     mod1 = models.SplitLorentzianModel()
     par1 = mod1.make_params(amplitude=1.0, center=0.0, sigma=0.9, sigma_r=1.3)
diff --git a/tests/test_model.py b/tests/test_model.py
--- a/tests/test_model.py
+++ b/tests/test_model.py
@@ -111,7 +111,7 @@ def test_fit_with_weights(self):
             val2 = out2.values[parname]
             if max_diff < abs(val1-val2):
                 max_diff = abs(val1-val2)
-        assert(max_diff > 1.e-8)
+        assert max_diff > 1.e-8
 
     def test_result_attributes(self):
         pars = self.model.make_params(**self.guess())
@@ -160,12 +160,12 @@ def test_result_report(self):
         pars = self.model.make_params(**self.guess())
         result = self.model.fit(self.data, pars, x=self.x)
         report = result.fit_report()
-        assert("[[Model]]" in report)
-        assert("[[Variables]]" in report)
-        assert("[[Fit Statistics]]" in report)
-        assert(" # function evals   =" in report)
-        assert(" Akaike " in report)
-        assert(" chi-square " in report)
+        assert "[[Model]]" in report
+        assert "[[Variables]]" in report
+        assert "[[Fit Statistics]]" in report
+        assert " # function evals   =" in report
+        assert " Akaike " in report
+        assert " chi-square " in report
 
     def test_data_alignment(self):
         _skip_if_no_pandas()
@@ -461,7 +461,7 @@ def test_eval_components(self):
         self.assertTrue(abs(result.params['bkg_c'].value - 1.0) < 0.25)
 
         comps = mod.eval_components(x=self.x)
-        assert('bkg_' in comps)
+        assert 'bkg_' in comps
 
     def test_composite_has_bestvalues(self):
         # test that a composite model has non-empty best_values
@@ -471,6 +471,42 @@ def test_composite_has_bestvalues(self):
         mod = model1 + model2
         pars = mod.make_params()
 
+        values1 = dict(amplitude=7.10, center=1.1, sigma=2.40)
+        values2 = dict(amplitude=12.2, center=2.5, sigma=0.5)
+        data = (gaussian(x=self.x, **values1) + gaussian(x=self.x, **values2)
+                + 0.1*self.noise)
+
+        pars['g1_sigma'].set(value=2)
+        pars['g1_center'].set(value=1, max=1.5)
+        pars['g1_amplitude'].set(value=3)
+        pars['g2_sigma'].set(value=1)
+        pars['g2_center'].set(value=2.6, min=2.0)
+        pars['g2_amplitude'].set(value=1)
+
+        result = mod.fit(data, params=pars, x=self.x)
+
+        self.assertTrue(len(result.best_values) == 6)
+
+        self.assertTrue(abs(result.params['g1_amplitude'].value - 7.1) < 0.5)
+        self.assertTrue(abs(result.params['g2_amplitude'].value - 12.2) < 0.5)
+        self.assertTrue(abs(result.params['g1_center'].value - 1.1) < 0.2)
+        self.assertTrue(abs(result.params['g2_center'].value - 2.5) < 0.2)
+
+        for name, par in pars.items():
+            assert len(repr(par)) > 5
+
+    def test_composite_plotting(self):
+        # test that a composite model has non-empty best_values
+        pytest.importorskip("matplotlib")
+        import matplotlib
+        matplotlib.use('Agg')
+
+        model1 = models.GaussianModel(prefix='g1_')
+        model2 = models.GaussianModel(prefix='g2_')
+
+        mod = model1 + model2
+        pars = mod.make_params()
+
         values1 = dict(amplitude=7.10, center=1.1, sigma=2.40)
         values2 = dict(amplitude=12.2, center=2.5, sigma=0.5)
         data = (gaussian(x=self.x, **values1) + gaussian(x=self.x, **values2)
@@ -484,13 +520,14 @@ def test_composite_has_bestvalues(self):
         pars['g2_amplitude'].set(1)
 
         result = mod.fit(data, params=pars, x=self.x)
+        fig, ax = result.plot(show_init=True)
 
-        self.assertTrue(len(result.best_values) == 6)
+        assert isinstance(fig, matplotlib.figure.Figure)
+        assert isinstance(ax, matplotlib.axes.GridSpec)
 
-        self.assertTrue(abs(result.params['g1_amplitude'].value - 7.1) < 0.5)
-        self.assertTrue(abs(result.params['g2_amplitude'].value - 12.2) < 0.5)
-        self.assertTrue(abs(result.params['g1_center'].value - 1.1) < 0.2)
-        self.assertTrue(abs(result.params['g2_center'].value - 2.5) < 0.2)
+        comps = result.eval_components(x=self.x)
+        assert len(comps) == 2
+        assert 'g1_' in comps
 
     def test_hints_in_composite_models(self):
         # test propagation of hints from base models to composite model
diff --git a/tests/test_parameters.py b/tests/test_parameters.py
--- a/tests/test_parameters.py
+++ b/tests/test_parameters.py
@@ -34,16 +34,16 @@ def test_copy(self):
         p1.add('y', expr='x*t + sqrt(t)/3.0')
 
         p2 = p1.copy()
-        assert(isinstance(p2, Parameters))
-        assert('t' in p2)
-        assert('y' in p2)
-        assert(p2['t'].max < 6.0)
-        assert(np.isinf(p2['x'].max) and p2['x'].max > 0)
-        assert(np.isinf(p2['x'].min) and p2['x'].min < 0)
-        assert('sqrt(t)' in p2['y'].expr)
-        assert(p2._asteval is not None)
-        assert(p2._asteval.symtable is not None)
-        assert((p2['y'].value > 20) and (p2['y'].value < 21))
+        assert isinstance(p2, Parameters)
+        assert 't' in p2
+        assert 'y' in p2
+        assert p2['t'].max < 6.0
+        assert np.isinf(p2['x'].max) and p2['x'].max > 0
+        assert np.isinf(p2['x'].min) and p2['x'].min < 0
+        assert 'sqrt(t)' in p2['y'].expr
+        assert p2._asteval is not None
+        assert p2._asteval.symtable is not None
+        assert (p2['y'].value > 20) and (p2['y'].value < 21)
 
     def test_copy_function(self):
         # check copy(Parameters) does not fail
@@ -53,26 +53,26 @@ def test_copy_function(self):
         p1.add('y', expr='x*t + sqrt(t)/3.0')
 
         p2 = copy(p1)
-        assert(isinstance(p2, Parameters))
+        assert isinstance(p2, Parameters)
 
         # change the 'x' value in the original
         p1['x'].value = 4.0
 
-        assert(p2['x'].value > 9.8)
-        assert(p2['x'].value < 10.2)
-        assert(np.isinf(p2['x'].max) and p2['x'].max > 0)
+        assert p2['x'].value > 9.8
+        assert p2['x'].value < 10.2
+        assert np.isinf(p2['x'].max) and p2['x'].max > 0
 
-        assert('t' in p2)
-        assert('y' in p2)
-        assert(p2['t'].max < 6.0)
+        assert 't' in p2
+        assert 'y' in p2
+        assert p2['t'].max < 6.0
 
-        assert(np.isinf(p2['x'].min) and p2['x'].min < 0)
-        assert('sqrt(t)' in p2['y'].expr)
-        assert(p2._asteval is not None)
-        assert(p2._asteval.symtable is not None)
-        assert((p2['y'].value > 20) and (p2['y'].value < 21))
+        assert np.isinf(p2['x'].min) and p2['x'].min < 0
+        assert 'sqrt(t)' in p2['y'].expr
+        assert p2._asteval is not None
+        assert p2._asteval.symtable is not None
+        assert (p2['y'].value > 20) and (p2['y'].value < 21)
 
-        assert(p1['y'].value < 10)
+        assert p1['y'].value < 10
 
     def test_deepcopy(self):
         # check that a simple copy works
@@ -239,14 +239,14 @@ def test_invalid_expr_exceptions(self):
         p1.add('x', 10.0)
         with self.assertRaises(SyntaxError):
             p1.add('y', expr='x*t + sqrt(t)/')
-        assert(len(p1['y']._expr_eval.error) > 0)
+        assert len(p1['y']._expr_eval.error) > 0
         p1.add('y', expr='x*t + sqrt(t)/3.0')
         p1['y'].set(expr='x*3.0 + t**2')
-        assert('x*3' in p1['y'].expr)
-        assert(len(p1['y']._expr_eval.error) == 0)
+        assert 'x*3' in p1['y'].expr
+        assert len(p1['y']._expr_eval.error) == 0
         with self.assertRaises(SyntaxError):
             p1['y'].set(expr='t+')
-        assert(len(p1['y']._expr_eval.error) > 0)
+        assert len(p1['y']._expr_eval.error) > 0
         assert_almost_equal(p1['y'].value, 34.0)
 
     def test_eval(self):
@@ -277,5 +277,25 @@ def test_add_params_expr_outoforder(self):
         params2['b'].expr = 'c/2'
 
         params = params1 + params2
-        assert('b' in params)
+        assert 'b' in params
         assert_almost_equal(params['b'].value, 1.0)
+
+    def test_params_prints(self):
+        params = Parameters()
+        params.add("a", value=1.0, vary=True)
+        params.add("b", value=8.5, min=0, vary=True)
+        params.add("c", expr='a + sqrt(b)')
+
+        repr_full = params.pretty_repr()
+        repr_one = params.pretty_repr(oneline=True)
+
+        out = []
+        for key, val in params.items():
+            out.append("%s: %s" % (key, repr(val)))
+        out = '\n'.join(out)
+
+        assert repr_full.count('\n') > 4
+        assert repr_one.count('\n') < 2
+        assert len(repr_full) > 150
+        assert len(repr_one) > 150
+        assert len(out) > 150