diff options
Diffstat (limited to 'tests/test_parameters.py')
-rw-r--r-- | tests/test_parameters.py | 889 |
1 files changed, 574 insertions, 315 deletions
diff --git a/tests/test_parameters.py b/tests/test_parameters.py index 67f6c41..37cad6f 100644 --- a/tests/test_parameters.py +++ b/tests/test_parameters.py @@ -1,319 +1,578 @@ +"""Tests for the Parameters class.""" + + from copy import copy, deepcopy import pickle -import unittest +import asteval import numpy as np -from numpy.testing import assert_, assert_almost_equal, assert_equal - -from lmfit import Model, Parameter, Parameters -from lmfit.printfuncs import params_html_table - - -class TestParameters(unittest.TestCase): - - def setUp(self): - self.params = Parameters() - self.params.add_many(('a', 1., True, None, None, None), - ('b', 2., True, None, None, None), - ('c', 3., True, None, None, '2. * a')) - - def test_expr_was_evaluated(self): - self.params.update_constraints() - assert_almost_equal(self.params['c'].value, - 2 * self.params['a'].value) - - def test_copy(self): - # check simple Parameters.copy() does not fail - # on non-trivial Parameters - p1 = Parameters() - p1.add('t', 2.0, min=0.0, max=5.0) - p1.add('x', 10.0) - 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) - - def test_copy_function(self): - # check copy(Parameters) does not fail - p1 = Parameters() - p1.add('t', 2.0, min=0.0, max=5.0) - p1.add('x', 10.0) - p1.add('y', expr='x*t + sqrt(t)/3.0') - - p2 = copy(p1) - 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 '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 p1['y'].value < 10 - - def test_deepcopy(self): - # check that a simple copy works - b = deepcopy(self.params) - assert_(self.params == b) - - # check that we can add a symbol to the interpreter - self.params['b'].expr = 'sin(1)' - self.params['b'].value = 10 - assert_almost_equal(self.params['b'].value, np.sin(1)) - assert_almost_equal(self.params._asteval.symtable['b'], np.sin(1)) - - # check that the symbols in the interpreter are still the same after - # deepcopying - b = deepcopy(self.params) - - unique_symbols_params = self.params._asteval.user_defined_symbols() - unique_symbols_b = self.params._asteval.user_defined_symbols() - assert_(unique_symbols_b == unique_symbols_params) - for unique_symbol in unique_symbols_b: - if self.params._asteval.symtable[unique_symbol] is np.nan: - continue - - assert_(self.params._asteval.symtable[unique_symbol] - == - b._asteval.symtable[unique_symbol]) - - def test_add_many_params(self): - # test that we can add many parameters, but only parameters are added. - a = Parameter('a', 1) - b = Parameter('b', 2) - - p = Parameters() - p.add_many(a, b) - - assert_(list(p.keys()) == ['a', 'b']) - - def test_expr_and_constraints_GH265(self): - # test that parameters are reevaluated if they have bounds and expr - # see GH265 - p = Parameters() - - p['a'] = Parameter('a', 10, True) - p['b'] = Parameter('b', 10, True, 0, 20) - - assert_equal(p['b'].min, 0) - assert_equal(p['b'].max, 20) - - p['a'].expr = '2 * b' - assert_almost_equal(p['a'].value, 20) - - p['b'].value = 15 - assert_almost_equal(p['b'].value, 15) - assert_almost_equal(p['a'].value, 30) - - p['b'].value = 30 - assert_almost_equal(p['b'].value, 20) - assert_almost_equal(p['a'].value, 40) - - def test_pickle_parameter(self): - # test that we can pickle a Parameter - p = Parameter('a', 10, True, 0, 1) - pkl = pickle.dumps(p) - - q = pickle.loads(pkl) - - assert_(p == q) - - def test_pickle_parameters(self): - # test that we can pickle a Parameters object - p = Parameters() - p.add('a', 10, True, 0, 100) - p.add('b', 10, True, 0, 100, 'a * sin(1)') - p.update_constraints() - p._asteval.symtable['abc'] = '2 * 3.142' - - pkl = pickle.dumps(p, -1) - q = pickle.loads(pkl) - - q.update_constraints() - assert_(p == q) - assert_(p is not q) - - # now test if the asteval machinery survived - assert_(q._asteval.symtable['abc'] == '2 * 3.142') - - # check that unpickling of Parameters is not affected by expr that - # refer to Parameter that are added later on. In the following - # example var_0.expr refers to var_1, which is a Parameter later - # on in the Parameters OrderedDict. - p = Parameters() - p.add('var_0', value=1) - p.add('var_1', value=2) - p['var_0'].expr = 'var_1' - pkl = pickle.dumps(p) - q = pickle.loads(pkl) - - def test_params_usersyms(self): - # test passing usersymes to Parameters() - def myfun(x): - return x**3 - - params = Parameters(usersyms={"myfun": myfun}) - params.add("a", value=2.3) - params.add("b", expr="myfun(a)") - - xx = np.linspace(0, 1, 10) - yy = 3 * xx + np.random.normal(scale=0.002, size=len(xx)) - - model = Model(lambda x, a: a * x) - result = model.fit(yy, params=params, x=xx) - assert_(np.isclose(result.params['a'].value, 3.0, rtol=0.025)) - assert_(result.nfev > 3) - assert_(result.nfev < 300) - - def test_set_symtable(self): - # test that we use Parameter.set(value=XXX) and have - # that new value be used in constraint expressions - pars = Parameters() - pars.add('x', value=1.0) - pars.add('y', expr='x + 1') - - assert_(np.isclose(pars['y'].value, 2.0)) - pars['x'].set(value=3.0) - assert_(np.isclose(pars['y'].value, 4.0)) - - def test_dumps_loads_parameters(self): - # test that we can dumps() and then loads() a Parameters - pars = Parameters() - pars.add('x', value=1.0) - pars.add('y', value=2.0) - pars['x'].expr = 'y / 2.0' - - dumps = pars.dumps() - - newpars = Parameters().loads(dumps) - newpars['y'].value = 100.0 - assert_(np.isclose(newpars['x'].value, 50.0)) - - def test_isclose(self): - assert_(np.isclose(1., 1+1e-5, atol=1e-4, rtol=0)) - assert_(not np.isclose(1., 1+1e-5, atol=1e-6, rtol=0)) - assert_(np.isclose(1e10, 1.00001e10, rtol=1e-5, atol=1e-8)) - assert_(not np.isclose(0, np.inf)) - assert_(not np.isclose(-np.inf, np.inf)) - assert_(np.isclose(np.inf, np.inf)) - assert_(not np.isclose(np.nan, np.nan)) - - def test_expr_with_bounds(self): - "test an expression with bounds, without value" - pars = Parameters() - pars.add('c1', value=0.2) - pars.add('c2', value=0.2) - pars.add('c3', value=0.2) - pars.add('csum', value=0.8) - # this should not raise TypeError: - pars.add('c4', expr='csum-c1-c2-c3', min=0, max=1) - assert_(np.isclose(pars['c4'].value, 0.2)) - - def test_invalid_expr_exceptions(self): - "test if an exception is raised for invalid expressions (GH486)""" - p1 = Parameters() - p1.add('t', 2.0, min=0.0, max=5.0) - 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 - 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 - with self.assertRaises(SyntaxError): - p1['y'].set(expr='t+') - assert len(p1['y']._expr_eval.error) > 0 - assert_almost_equal(p1['y'].value, 34.0) - - def test_eval(self): - # check that eval() works with usersyms and parameter values - def myfun(x): - return 2.0 * x - p = Parameters(usersyms={"myfun": myfun}) - p.add("a", value=4.0) - p.add("b", value=3.0) - assert_almost_equal(p.eval("myfun(2.0) * a"), 16) - assert_almost_equal(p.eval("b / myfun(3.0)"), 0.5) - - def test_params_html_table(self): - p1 = Parameters() - p1.add('t', 2.0, min=0.0, max=5.0) - p1.add('x', 0.0, ) - - html = params_html_table(p1) - self.assertIsInstance(html, str) - - def test_add_params_expr_outoforder(self): - params1 = Parameters() - params1.add("a", value=1.0) - - params2 = Parameters() - params2.add("b", value=1.0) - params2.add("c", value=2.0) - params2['b'].expr = 'c/2' - - params = params1 + params2 - 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 - - def test_add_with_symtable(self): - pars1 = Parameters() - pars1.add("a", value=1.0, vary=True) - - def half(x): - return 0.5*x - - pars2 = Parameters(usersyms={"half": half}) - pars2.add("b", value=3.0) - pars2.add("c", expr="half(b)") - - params = pars1 + pars2 - assert_almost_equal(params['c'].value, 1.5) - - params = pars2 + pars1 - assert_almost_equal(params['c'].value, 1.5) - - params = deepcopy(pars1) - params.update(pars2) - assert_almost_equal(params['c'].value, 1.5) +from numpy.testing import assert_allclose +import pytest + +import lmfit + + +@pytest.fixture +def parameters(): + """Initialize a Parameters class for tests.""" + pars = lmfit.Parameters() + pars.add(lmfit.Parameter(name='a', value=10.0, vary=True, min=-100.0, + max=100.0, expr=None, brute_step=5.0, + user_data=1)) + pars.add(lmfit.Parameter(name='b', value=0.0, vary=True, min=-250.0, + max=250.0, expr="2.0*a", brute_step=25.0, + user_data=2.5)) + exp_attr_values_A = ('a', 10.0, True, -100.0, 100.0, None, 5.0, 1) + exp_attr_values_B = ('b', 20.0, False, -250.0, 250.0, "2.0*a", 25.0, 2.5) + assert_parameter_attributes(pars['a'], exp_attr_values_A) + assert_parameter_attributes(pars['b'], exp_attr_values_B) + return pars, exp_attr_values_A, exp_attr_values_B + + +def assert_parameter_attributes(par, expected): + """Assert that parameter attributes have the expected values.""" + par_attr_values = (par.name, par._val, par.vary, par.min, par.max, + par._expr, par.brute_step, par.user_data) + assert par_attr_values == expected + + +def test_check_ast_errors(): + """Assert that an exception is raised upon AST errors.""" + pars = lmfit.Parameters() + + msg = r"at expr='<_ast.Module object at" + with pytest.raises(NameError, match=msg): + pars.add('par1', expr='2.0*par2') + + +def test_parameters_init(): + """Test for initialization of the Parameters class""" + ast_int = asteval.Interpreter() + pars = lmfit.Parameters(asteval=ast_int, usersyms={'test': np.sin}) + assert pars._asteval == ast_int + assert 'test' in pars._asteval.symtable + + +def test_parameters_copy(parameters): + """Tests for copying a Parameters class; all use the __deepcopy__ method.""" + pars, exp_attr_values_A, exp_attr_values_B = parameters + + copy_pars = copy(pars) + pars_copy = pars.copy() + pars__copy__ = pars.__copy__() + + pars['a'].set(value=100) + + for copied in [copy_pars, pars_copy, pars__copy__]: + assert isinstance(copied, lmfit.Parameters) + assert copied != pars + assert copied._asteval is not None + assert copied._asteval.symtable is not None + assert_parameter_attributes(copied['a'], exp_attr_values_A) + assert_parameter_attributes(copied['b'], exp_attr_values_B) + + +def test_parameters_deepcopy(parameters): + """Tests for deepcopy of a Parameters class.""" + pars, _, _ = parameters + + deepcopy_pars = deepcopy(pars) + assert isinstance(deepcopy_pars, lmfit.Parameters) + assert deepcopy_pars == pars + + # check that we can add a symbol to the interpreter + pars['b'].expr = 'sin(1)' + pars['b'].value = 10 + assert_allclose(pars['b'].value, np.sin(1)) + assert_allclose(pars._asteval.symtable['b'], np.sin(1)) + + # check that the symbols in the interpreter are still the same after + # deepcopying + pars, exp_attr_values_A, exp_attr_values_B = parameters + deepcopy_pars = deepcopy(pars) + + unique_symbols_pars = pars._asteval.user_defined_symbols() + unique_symbols_copied = deepcopy_pars._asteval.user_defined_symbols() + assert unique_symbols_copied == unique_symbols_pars + + for unique_symbol in unique_symbols_copied: + if pars._asteval.symtable[unique_symbol] is not np.nan: + assert (pars._asteval.symtable[unique_symbol] == + deepcopy_pars._asteval.symtable[unique_symbol]) + + +def test_parameters_update(parameters): + """Tests for updating a Parameters class.""" + pars, exp_attr_values_A, exp_attr_values_B = parameters + + msg = r"'test' is not a Parameters object" + with pytest.raises(ValueError, match=msg): + pars.update('test') + + pars2 = lmfit.Parameters() + pars2.add(lmfit.Parameter(name='c', value=7.0, vary=True, min=-70.0, + max=70.0, expr=None, brute_step=0.7, + user_data=7)) + exp_attr_values_C = ('c', 7.0, True, -70.0, 70.0, None, 0.7, 7) + + pars_updated = pars.update(pars2) + + assert_parameter_attributes(pars_updated['a'], exp_attr_values_A) + assert_parameter_attributes(pars_updated['b'], exp_attr_values_B) + assert_parameter_attributes(pars_updated['c'], exp_attr_values_C) + + +def test_parameters__setitem__(parameters): + """Tests for __setitem__ method of a Parameters class.""" + pars, _, exp_attr_values_B = parameters + + msg = r"'10' is not a valid Parameters name" + with pytest.raises(KeyError, match=msg): + pars.__setitem__('10', None) + + msg = r"'not_a_parameter' is not a Parameter" + with pytest.raises(ValueError, match=msg): + pars.__setitem__('a', 'not_a_parameter') + + par = lmfit.Parameter('b', value=10, min=-25.0, brute_step=1) + pars.__setitem__('b', par) + + exp_attr_values_B = ('b', 10, True, -25.0, np.inf, None, 1, None) + assert_parameter_attributes(pars['b'], exp_attr_values_B) + + +def test_parameters__add__(parameters): + """Test the __add__ magic method.""" + pars, exp_attr_values_A, exp_attr_values_B = parameters + + msg = r"'other' is not a Parameters object" + with pytest.raises(ValueError, match=msg): + _ = pars + 'other' + + pars2 = lmfit.Parameters() + pars2.add_many(('c', 1., True, None, None, None), + ('d', 2., True, None, None, None)) + exp_attr_values_C = ('c', 1, True, -np.inf, np.inf, None, None, None) + exp_attr_values_D = ('d', 2, True, -np.inf, np.inf, None, None, None) + + pars_added = pars + pars2 + + assert_parameter_attributes(pars_added['a'], exp_attr_values_A) + assert_parameter_attributes(pars_added['b'], exp_attr_values_B) + assert_parameter_attributes(pars_added['c'], exp_attr_values_C) + assert_parameter_attributes(pars_added['d'], exp_attr_values_D) + + +def test_parameters__iadd__(parameters): + """Test the __iadd__ magic method.""" + pars, exp_attr_values_A, exp_attr_values_B = parameters + + msg = r"'other' is not a Parameters object" + with pytest.raises(ValueError, match=msg): + pars += 'other' + + pars2 = lmfit.Parameters() + pars2.add_many(('c', 1., True, None, None, None), + ('d', 2., True, None, None, None)) + exp_attr_values_C = ('c', 1, True, -np.inf, np.inf, None, None, None) + exp_attr_values_D = ('d', 2, True, -np.inf, np.inf, None, None, None) + + pars += pars2 + + assert_parameter_attributes(pars['a'], exp_attr_values_A) + assert_parameter_attributes(pars['b'], exp_attr_values_B) + assert_parameter_attributes(pars['c'], exp_attr_values_C) + assert_parameter_attributes(pars['d'], exp_attr_values_D) + + +def test_parameters_add_with_symtable(): + """Regression test for GitHub Issue 607.""" + pars1 = lmfit.Parameters() + pars1.add('a', value=1.0) + + def half(x): + return 0.5*x + + pars2 = lmfit.Parameters(usersyms={"half": half}) + pars2.add("b", value=3.0) + pars2.add("c", expr="half(b)") + + params = pars1 + pars2 + assert_allclose(params['c'].value, 1.5) + + params = pars2 + pars1 + assert_allclose(params['c'].value, 1.5) + + params = deepcopy(pars1) + params.update(pars2) + assert_allclose(params['c'].value, 1.5) + + pars1 += pars2 + assert_allclose(params['c'].value, 1.5) + + +def test_parameters__array__(parameters): + """Test the __array__ magic method.""" + pars, _, _ = parameters + + assert_allclose(np.array(pars), np.array([10.0, 20.0])) + + +def test_parameters__reduce__(parameters): + """Test the __reduce__ magic method.""" + pars, _, _ = parameters + reduced = pars.__reduce__() + + assert isinstance(reduced[2], dict) + assert 'unique_symbols' in reduced[2].keys() + assert reduced[2]['unique_symbols']['b'] == 20 + assert 'params' in reduced[2].keys() + assert isinstance(reduced[2]['params'][0], lmfit.Parameter) + + +def test_parameters__setstate__(parameters): + """Test the __setstate__ magic method.""" + pars, exp_attr_values_A, exp_attr_values_B = parameters + reduced = pars.__reduce__() + + pars_setstate = lmfit.Parameters() + pars_setstate.__setstate__(reduced[2]) + + assert isinstance(pars_setstate, lmfit.Parameters) + assert_parameter_attributes(pars_setstate['a'], exp_attr_values_A) + assert_parameter_attributes(pars_setstate['b'], exp_attr_values_B) + + +def test_pickle_parameters(): + """Test that we can pickle a Parameters object.""" + p = lmfit.Parameters() + p.add('a', 10, True, 0, 100) + p.add('b', 10, True, 0, 100, 'a * sin(1)') + p.update_constraints() + p._asteval.symtable['abc'] = '2 * 3.142' + + pkl = pickle.dumps(p, -1) + q = pickle.loads(pkl) + + q.update_constraints() + assert p == q + assert p is not q + + # now test if the asteval machinery survived + assert q._asteval.symtable['abc'] == '2 * 3.142' + + # check that unpickling of Parameters is not affected by expr that + # refer to Parameter that are added later on. In the following + # example var_0.expr refers to var_1, which is a Parameter later + # on in the Parameters OrderedDict. + p = lmfit.Parameters() + p.add('var_0', value=1) + p.add('var_1', value=2) + p['var_0'].expr = 'var_1' + pkl = pickle.dumps(p) + q = pickle.loads(pkl) + + +def test_parameters_eval(parameters): + """Test the eval method.""" + pars, _, _ = parameters + evaluated = pars.eval('10.0*a+b') + assert_allclose(evaluated, 120) + + # check that eval() works with usersyms and parameter values + def myfun(x): + return 2.0 * x + + pars2 = lmfit.Parameters(usersyms={"myfun": myfun}) + pars2.add('a', value=4.0) + pars2.add('b', value=3.0) + assert_allclose(pars2.eval('myfun(2.0) * a'), 16) + assert_allclose(pars2.eval('b / myfun(3.0)'), 0.5) + + +def test_parameters_pretty_repr(parameters): + """Test the pretty_repr method.""" + pars, _, _ = parameters + output = pars.pretty_repr() + output_oneline = pars.pretty_repr(oneline=True) + + split_output = output.split('\n') + assert len(split_output) == 5 + assert 'Parameters' in split_output[0] + assert "Parameter 'a'" in split_output[1] + assert "Parameter 'b'" in split_output[2] + + oneliner = ("Parameters([('a', <Parameter 'a', value=10.0, " + "bounds=[-100.0:100.0], brute_step=5.0>), ('b', <Parameter " + "'b', value=20.0, bounds=[-250.0:250.0], expr='2.0*a', " + "brute_step=25.0>)])") + assert output_oneline == oneliner + + +def test_parameters_pretty_print(parameters, capsys): + """Test the pretty_print method.""" + pars, _, _ = parameters + + # oneliner + pars.pretty_print(oneline=True) + captured = capsys.readouterr() + oneliner = ("Parameters([('a', <Parameter 'a', value=10.0, " + "bounds=[-100.0:100.0], brute_step=5.0>), ('b', <Parameter " + "'b', value=20.0, bounds=[-250.0:250.0], expr='2.0*a', " + "brute_step=25.0>)])") + assert oneliner in captured.out + + # default + pars.pretty_print() + captured = capsys.readouterr() + captured_split = captured.out.split('\n') + assert len(captured_split) == 4 + header = ('Name Value Min Max Stderr Vary ' + 'Expr Brute_Step') + assert captured_split[0] == header + + # specify columnwidth + pars.pretty_print(colwidth=12) + captured = capsys.readouterr() + captured_split = captured.out.split('\n') + header = ('Name Value Min Max Stderr ' + ' Vary Expr Brute_Step') + assert captured_split[0] == header + + # specify columns + pars['a'].stderr = 0.01 + pars.pretty_print(columns=['value', 'min', 'max', 'stderr']) + captured = capsys.readouterr() + captured_split = captured.out.split('\n') + assert captured_split[0] == 'Name Value Min Max Stderr' + assert captured_split[1] == 'a 10 -100 100 0.01' + assert captured_split[2] == 'b 20 -250 250 None' + + # specify fmt + pars.pretty_print(fmt='e', columns=['value', 'min', 'max']) + captured = capsys.readouterr() + captured_split = captured.out.split('\n') + assert captured_split[0] == 'Name Value Min Max' + assert captured_split[1] == 'a 1.0000e+01 -1.0000e+02 1.0000e+02' + assert captured_split[2] == 'b 2.0000e+01 -2.5000e+02 2.5000e+02' + + # specify precision + pars.pretty_print(precision=2, fmt='e', columns=['value', 'min', 'max']) + captured = capsys.readouterr() + captured_split = captured.out.split('\n') + assert captured_split[0] == 'Name Value Min Max' + assert captured_split[1] == 'a 1.00e+01 -1.00e+02 1.00e+02' + assert captured_split[2] == 'b 2.00e+01 -2.50e+02 2.50e+02' + + +def test_parameters__repr_html_(parameters): + """Test _repr_html method to generate HTML table for Parameters class.""" + pars, _, _ = parameters + repr_html = pars._repr_html_() + + assert isinstance(repr_html, str) + assert '<table><tr><th> name </th><th> value </th>' in repr_html + + +def test_parameters_add(): + """Tests for adding a Parameter to the Parameters class.""" + pars = lmfit.Parameters() + pars_from_par = lmfit.Parameters() + + pars.add('a') + exp_attr_values_A = ('a', -np.inf, True, -np.inf, np.inf, None, None, None) + assert_parameter_attributes(pars['a'], exp_attr_values_A) + + pars_from_par.add(lmfit.Parameter('a')) + assert pars_from_par == pars + + pars.add('b', value=1, vary=False, min=-5.0, max=5.0, brute_step=0.1) + exp_attr_values_B = ('b', 1.0, False, -5.0, 5.0, None, 0.1, None) + assert_parameter_attributes(pars['b'], exp_attr_values_B) + + pars_from_par.add(lmfit.Parameter('b', value=1, vary=False, min=-5.0, + max=5.0, brute_step=0.1)) + assert pars_from_par == pars + + +def test_add_params_expr_outoforder(): + """Regression test for GitHub Issue 560.""" + params1 = lmfit.Parameters() + params1.add("a", value=1.0) + + params2 = lmfit.Parameters() + params2.add("b", value=1.0) + params2.add("c", value=2.0) + params2['b'].expr = 'c/2' + + params = params1 + params2 + assert 'b' in params + assert_allclose(params['b'].value, 1.0) + + +def test_parameters_add_many(): + """Tests for add_many method.""" + a = lmfit.Parameter('a', 1) + b = lmfit.Parameter('b', 2) + + par = lmfit.Parameters() + par.add_many(a, b) + + par_with_tuples = lmfit.Parameters() + par_with_tuples.add_many(('a', 1), ('b', 2)) + + assert list(par.keys()) == ['a', 'b'] + assert par == par_with_tuples + + +def test_parameters_valuesdict(parameters): + """Test for valuesdict method.""" + pars, _, _ = parameters + vals_dict = pars.valuesdict() + + assert isinstance(vals_dict, dict) + assert_allclose(vals_dict['a'], pars['a'].value) + assert_allclose(vals_dict['b'], pars['b'].value) + + +def test_dumps_loads_parameters(parameters): + """Test for dumps and loads methods for a Parameters class.""" + pars, _, _ = parameters + + dumps = pars.dumps() + assert isinstance(dumps, str) + newpars = lmfit.Parameters().loads(dumps) + assert newpars == pars + + newpars['a'].value = 100.0 + assert_allclose(newpars['b'].value, 200.0) + + +def test_dump_load_parameters(parameters): + """Test for dump and load methods for a Parameters class.""" + pars, _, _ = parameters + + with open('parameters.sav', 'w') as outfile: + pars.dump(outfile) + + with open('parameters.sav') as infile: + newpars = pars.load(infile) + + assert newpars == pars + newpars['a'].value = 100.0 + assert_allclose(newpars['b'].value, 200.0) + + +def test_dumps_loads_parameters_usersyms(): + """Test for dumps/loads methods for a Parameters class with usersyms.""" + def half(x): + return 0.5*x + + pars = lmfit.Parameters(usersyms={"half": half, 'my_func': np.sqrt}) + pars.add(lmfit.Parameter(name='a', value=9.0, min=-100.0, max=100.0)) + pars.add(lmfit.Parameter(name='b', value=100.0, min=-250.0, max=250.0)) + pars.add("c", expr="half(b) + my_func(a)") + + dumps = pars.dumps() + assert isinstance(dumps, str) + assert '"half": {' in dumps + assert '"my_func": {' in dumps + + newpars = lmfit.Parameters().loads(dumps) + assert 'half' in newpars._asteval.symtable + assert 'my_func' in newpars._asteval.symtable + assert_allclose(newpars['a'].value, 9.0) + assert_allclose(newpars['b'].value, 100.0) + + # within the py.test environment the encoding of the function 'half' does + # not work correctly as it is changed from <function half at 0x?????????>" + # to "<function test_dumps_loads_parameters_usersyms.<locals>.half at 0x?????????> + # This result in the "importer" to be set to None and the final "decode4js" + # does not do the correct thing. + # + # Of note, this is only an issue within the py.test framework and it DOES + # work correctly in a normal Python interpreter. Also, it isn't an issue + # when DILL is used, so in that case the two asserts below will pass. + if lmfit.jsonutils.HAS_DILL: + assert newpars == pars + assert_allclose(newpars['c'].value, 53.0) + + +def test_parameters_expr_and_constraints(): + """Regression tests for GitHub Issue #265. Test that parameters are re- + evaluated if they have bounds and expr. + + """ + p = lmfit.Parameters() + p.add(lmfit.Parameter('a', 10, True)) + p.add(lmfit.Parameter('b', 10, True, 0, 20)) + + assert_allclose(p['b'].min, 0) + assert_allclose(p['b'].max, 20) + + p['a'].expr = '2 * b' + assert_allclose(p['a'].value, 20) + + p['b'].value = 15 + assert_allclose(p['b'].value, 15) + assert_allclose(p['a'].value, 30) + + p['b'].value = 30 + assert_allclose(p['b'].value, 20) + assert_allclose(p['a'].value, 40) + + +def test_parameters_usersyms(): + """Test for passing usersyms to Parameters().""" + def myfun(x): + return x**3 + + params = lmfit.Parameters(usersyms={"myfun": myfun}) + params.add("a", value=2.3) + params.add("b", expr="myfun(a)") + + np.random.seed(2020) + xx = np.linspace(0, 1, 10) + yy = 3 * xx + np.random.normal(scale=0.002, size=xx.size) + + model = lmfit.Model(lambda x, a: a * x) + result = model.fit(yy, params=params, x=xx) + assert_allclose(result.params['a'].value, 3.0, rtol=1e-3) + assert (result.nfev > 3 and result.nfev < 300) + + +def test_parameters_expr_with_bounds(): + """Test Parameters using an expression with bounds, without value.""" + pars = lmfit.Parameters() + pars.add('c1', value=0.2) + pars.add('c2', value=0.2) + pars.add('c3', value=0.2) + pars.add('csum', value=0.8) + + # this should not raise TypeError: + pars.add('c4', expr='csum-c1-c2-c3', min=0, max=1) + assert_allclose(pars['c4'].value, 0.2) + + +def test_invalid_expr_exceptions(): + """Regression test for GitHub Issue #486: check that an exception is + raised for invalid expressions. + + """ + p1 = lmfit.Parameters() + p1.add('t', 2.0, min=0.0, max=5.0) + p1.add('x', 10.0) + + with pytest.raises(SyntaxError): + p1.add('y', expr='x*t + sqrt(t)/') + 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 + + with pytest.raises(SyntaxError): + p1['y'].set(expr='t+') + assert len(p1['y']._expr_eval.error) > 0 + assert_allclose(p1['y'].value, 34.0) |