lmfit / lmfit-py

Non-Linear Least Squares Minimization, with flexible Parameter settings, based on scipy.optimize, and with many additional classes and methods for curve fitting.
https://lmfit.github.io/lmfit-py/
Other
1.06k stars 274 forks source link

ModelResult dumps does not work with parameters containing certain numerical datatypes #968

Closed cberk17 closed 4 weeks ago

cberk17 commented 4 weeks ago

First Time Issue Code

Yes, I read the instructions and I am sure this is a GitHub Issue.

Description

ModelResult.dumps does not work for some numeric data types in its parameters. If the parameters have non-supported datatypes such as np.float32 or np.int32 the model result cannot be serialized to json. Although, both the Parameters object and the ModelResult object allow for keywords to be passed to json.dumps which should allow for a custom JSON encoder to be used, the keywords passed into ModelResult.dumps are not propagated to the Parameters.dumps method.

A Minimal, Complete, and Verifiable example
import json
import numpy as np
from lmfit.parameter import Parameters
from lmfit.model import Model

class CustomJSONEncoder(json.JSONEncoder):
    """A JSON encoder that serializes scalar numpy types."""

    def default(self, obj):
        if isinstance(obj, np.float32):
            return float(obj)
        if isinstance(obj, np.integer):
            return int(obj)
        return super().default(obj)

numpy_parameters = Parameters()
numpy_parameters.add("slope", np.int32(6))
numpy_parameters.add("baseline", np.float32(5))
# The below line will fail
# numpy_parameters.dumps()
# However, this can be handled through use of a custom encoder
numpy_parameters.dumps(cls=CustomJSONEncoder)

# This cannot be handled when these parameters are used to fit a model
def linear_model(x, baseline, slope):
    return slope * x + baseline

model = Model(linear_model)
model_result = model.fit(
    np.arange(1, 2, 0.1),
    x=np.arange(1, 2, 0.1),
    params=numpy_parameters,
)

# This below will fail as well which is expected
# model_result.dumps()
# However, this will also fail due to the encoder not being used on the parameters
model_result.dumps(cls=CustomJSONEncoder)
Fit report:

N/A

Error message:
TypeError                                 Traceback (most recent call last)
Cell In[1], line 39
     30 model_result = model.fit(
     31     np.arange(1, 2, 0.1),
     32     x=np.arange(1, 2, 0.1),
     33     params=numpy_parameters,
     34 )
     36 # This will fail which is expected
     37 # model_result.dumps()
     38 # However, this will also fail due to the encoder not being used on the parameters
---> 39 model_result.dumps(cls=CustomJSONEncoder)

File ~/repos/lmfit-py/.venv/lib/python3.9/site-packages/lmfit/model.py:1921, in ModelResult.dumps(self, **kws)
   1917 out = {'__class__': 'lmfit.ModelResult', '__version__': '2',
   1918        'model': encode4js(self.model._get_state())}
   1920 for attr in ('params', 'init_params'):
-> 1921     out[attr] = getattr(self, attr).dumps()
   1923 for attr in ('aborted', 'aic', 'best_values', 'bic', 'chisqr',
   1924              'ci_out', 'col_deriv', 'covar', 'errorbars', 'flatchain',
   1925              'ier', 'init_values', 'lmdif_message', 'message',
   (...)
   1928              'calc_covar', 'success', 'userargs', 'userkws', 'values',
   1929              'var_names', 'weights', 'user_options'):
   1930     try:

File ~/repos/lmfit-py/.venv/lib/python3.9/site-packages/lmfit/parameter.py:595, in Parameters.dumps(self, **kws)
    592 params = [p.__getstate__() for p in self.values()]
    593 unique_symbols = {key: encode4js(deepcopy(self._asteval.symtable[key]))
    594                   for key in self._asteval.user_defined_symbols()}
--> 595 return json.dumps({'unique_symbols': unique_symbols,
    596                    'params': params}, **kws)

File ~/.pyenv/versions/3.9.11/lib/python3.9/json/__init__.py:231, in dumps(obj, skipkeys, ensure_ascii, check_circular, allow_nan, cls, indent, separators, default, sort_keys, **kw)
    226 # cached encoder
    227 if (not skipkeys and ensure_ascii and
    228     check_circular and allow_nan and
    229     cls is None and indent is None and separators is None and
    230     default is None and not sort_keys and not kw):
--> 231     return _default_encoder.encode(obj)
    232 if cls is None:
    233     cls = JSONEncoder

File ~/.pyenv/versions/3.9.11/lib/python3.9/json/encoder.py:199, in JSONEncoder.encode(self, o)
    195         return encode_basestring(o)
    196 # This doesn't pass the iterator directly to ''.join() because the
    197 # exceptions aren't as detailed.  The list call should be roughly
    198 # equivalent to the PySequence_Fast that ''.join() would do.
--> 199 chunks = self.iterencode(o, _one_shot=True)
    200 if not isinstance(chunks, (list, tuple)):
    201     chunks = list(chunks)

File ~/.pyenv/versions/3.9.11/lib/python3.9/json/encoder.py:257, in JSONEncoder.iterencode(self, o, _one_shot)
    252 else:
    253     _iterencode = _make_iterencode(
    254         markers, self.default, _encoder, self.indent, floatstr,
    255         self.key_separator, self.item_separator, self.sort_keys,
    256         self.skipkeys, _one_shot)
--> 257 return _iterencode(o, 0)

File ~/.pyenv/versions/3.9.11/lib/python3.9/json/encoder.py:179, in JSONEncoder.default(self, o)
    160 def default(self, o):
    161     """Implement this method in a subclass such that it returns
    162     a serializable object for ``o``, or calls the base implementation
    163     (to raise a ``TypeError``).
   (...)
    177 
    178     """
--> 179     raise TypeError(f'Object of type {o.__class__.__name__} '
    180                     f'is not JSON serializable')

TypeError: Object of type int32 is not JSON serializable
Version information

Python: 3.9.11 (main, Aug 26 2024, 10:22:08) [Clang 15.0.0 (clang-1500.3.9.4)]

lmfit: 0.0.post2858+g776e14d, scipy: 1.13.1, numpy: 2.0.2,asteval: 1.0.2, uncertainties: 3.2.2

Link(s)

N/A

newville commented 4 weeks ago

@cberk17 That sounds like reasonable behavior to me.

Parameter values must be double precision floating point numbers: Python floats. No other data types will ever work.
Perhaps you reviewing the meaning of "unsupported" would be helpful?

As you read, you could could have asked a question about how and why lmfit works. You ignored that advice and insisted that your unsupported usage was a problem that we need to solve. I would encourage you to take a different approach the next time you are looking for support or help with open source software.