Repository: lmfit/lmfit-py

Issue:
saved models fail to restore when using Voigt or other models that use complex constraints
Saving a model result with a Voigt Function works, but the saved model cannot be restored.

The problem can be reproduced with

``` python
import numpy as np
import matplotlib.pyplot as plt
from lmfit.model import save_modelresult, load_modelresult
from lmfit.models import  VoigtModel
from lmfit.lineshapes import gaussian, lorentzian

x = np.linspace(0, 20, 501)
y = gaussian(x, 1.1, 8.5, 2) + lorentzian(x, 1.7, 8.5, 1.5)
y = y + np.random.normal(size=len(x), scale=0.025)

model = VoigtModel()
pars = model.guess(y, x=x)
result = model.fit(y, pars, x=x)
save_modelresult(result, 'voigt_modelresult.sav')

r2 = load_modelresult('voigt_modelresult.sav')
```

which will give

```
TypeError
   <_ast.Module object at 0x1a263afb00>
                                                               ^^^
'{'__class__': 'Callable', '__name__': 'wofz', 'pyversion': '3.7', 'value': None, 'importer': 'scipy.special'}' is not callable!!
Traceback (most recent call last):
  File "d1.py", line 31, in <module>
    r2 = load_modelresult('voigt_modelresult.sav')
  File "/Users/Newville/Codes/lmfit-py/lmfit/model.py", line 1261, in load_modelresult
    mresult = modres.load(fh, funcdefs=funcdefs)
  File "/Users/Newville/Codes/lmfit-py/lmfit/model.py", line 1720, in load
    return self.loads(fp.read(), funcdefs=funcdefs, **kws)
  File "/Users/Newville/Codes/lmfit-py/lmfit/model.py", line 1676, in loads
    self.params.__setstate__(state)
  File "/Users/Newville/Codes/lmfit-py/lmfit/parameter.py", line 205, in __setstate__
    self.add_many(*state['params'])
  File "/Users/Newville/Codes/lmfit-py/lmfit/parameter.py", line 393, in add_many
    self.__setitem__(para.name, para)
  File "/Users/Newville/Codes/lmfit-py/lmfit/parameter.py", line 131, in __setitem__
    self._asteval.symtable[key] = par.value
  File "/Users/Newville/Codes/lmfit-py/lmfit/parameter.py", line 820, in value
    return self._getval()
  File "/Users/Newville/Codes/lmfit-py/lmfit/parameter.py", line 802, in _getval
    check_ast_errors(self._expr_eval)
  File "/Users/Newville/Codes/lmfit-py/lmfit/parameter.py", line 24, in check_ast_errors
    expr_eval.raise_exception(None)
  File "/Users/Newville/anaconda3/lib/python3.7/site-packages/asteval-0.9.13-py3.7.egg/asteval/asteval.py", line 261, in raise_exception
TypeError: at expr='<_ast.Module object at 0x1a263afb00>'
```




Discussion:

Resolving patch:
diff --git a/lmfit/parameter.py b/lmfit/parameter.py
--- a/lmfit/parameter.py
+++ b/lmfit/parameter.py
@@ -4,6 +4,7 @@
 from collections import OrderedDict
 from copy import deepcopy
 import json
+import importlib
 
 from asteval import Interpreter, get_ast_names, valid_symbol_name
 from numpy import arcsin, array, cos, inf, isclose, nan, sin, sqrt
@@ -183,8 +184,22 @@ def __setstate__(self, state):
         # an Error. Another way of doing this would be to remove all the expr
         # from the Parameter instances before they get added, then to restore
         # them.
-        self._asteval.symtable.update(state['unique_symbols'])
-
+        # self._asteval.symtable.update(state['unique_symbols'])
+
+        symtab = self._asteval.symtable
+        for key, val in state['unique_symbols'].items():
+            if key not in symtab:
+                if isinstance(val, dict):
+                    value = val.get('__name__', None)
+                    symname = val.get('__name__', None)
+                    importer = val.get('importer', None)
+                    if value is None and symname is not None and importer is not None:
+                        _mod = importlib.import_module(importer)
+                        value = getattr(_mod, symname, None)
+                    if value is not None:
+                        symtab[key] = value
+                else:
+                    symtab[key] = val
         # then add all the parameters
         self.add_many(*state['params'])
 


Test patch:
diff --git a/tests/test_saveload.py b/tests/test_saveload.py
--- a/tests/test_saveload.py
+++ b/tests/test_saveload.py
@@ -10,7 +10,9 @@
 from lmfit import Parameters
 from lmfit.model import (load_model, load_modelresult, save_model,
                          save_modelresult, Model, ModelResult)
-from lmfit.models import ExponentialModel, GaussianModel
+from lmfit.models import ExponentialModel, GaussianModel, VoigtModel
+from lmfit.lineshapes import gaussian, lorentzian
+
 from lmfit_testutils import assert_between, assert_param_between
 
 y, x = np.loadtxt(os.path.join(os.path.dirname(__file__), '..',
@@ -208,3 +210,31 @@ def mfunc(x, a, b):
     assert_param_between(result2.params['b'], 0.20, 0.25)
     assert_param_between(result3.params['a'], 0.48, 0.52)
     assert_param_between(result3.params['b'], 0.20, 0.25)
+
+
+def test_saveload_usersyms():
+    """Test save/load of modelresult with non-trivial user symbols,
+    this example uses a VoigtModel, wheree `wofz()` is used in a
+    constraint expression"""
+    x = np.linspace(0, 20, 501)
+    y = gaussian(x, 1.1, 8.5, 2) + lorentzian(x, 1.7, 8.5, 1.5)
+    np.random.seed(20)
+    y = y + np.random.normal(size=len(x), scale=0.025)
+
+    model = VoigtModel()
+    pars = model.guess(y, x=x)
+    result = model.fit(y, pars, x=x)
+
+    savefile = 'tmpvoigt_modelresult.sav'
+    save_modelresult(result, savefile)
+
+    assert_param_between(result.params['sigma'], 0.7, 2.1)
+    assert_param_between(result.params['center'], 8.4, 8.6)
+    assert_param_between(result.params['height'], 0.2, 1.0)
+
+    time.sleep(0.25)
+    result2 = load_modelresult(savefile)
+
+    assert_param_between(result2.params['sigma'], 0.7, 2.1)
+    assert_param_between(result2.params['center'], 8.4, 8.6)
+    assert_param_between(result2.params['height'], 0.2, 1.0)