From f75943d56e19e33c03a735558bb454325016c68b Mon Sep 17 00:00:00 2001 From: Martin Roelfs Date: Tue, 23 Oct 2018 10:21:20 +0200 Subject: [PATCH 01/10] Made minimizers picklable. Added tests, and made FitResults eq-comparable. --- symfit/core/fit_results.py | 28 +++++++ symfit/core/minimizers.py | 25 +++++- tests/test_minimizers.py | 157 ++++++++++++++++++++++++++++++++++++- 3 files changed, 203 insertions(+), 7 deletions(-) diff --git a/symfit/core/fit_results.py b/symfit/core/fit_results.py index 55f3813f..443abf43 100644 --- a/symfit/core/fit_results.py +++ b/symfit/core/fit_results.py @@ -121,3 +121,31 @@ def covariance(self, param_1, param_2): param_1_number = self.model.params.index(param_1) param_2_number = self.model.params.index(param_2) return self.covariance_matrix[param_1_number, param_2_number] + + @staticmethod + def _array_safe_dict_eq(one_dict, other_dict): + """ + Dicts containing arrays are hard to compare. This function uses + numpy.allclose to compare arrays, and does normal comparison for all + other types. + + :param one_dict: + :param other_dict: + :return: bool + """ + for key in one_dict: + try: + assert one_dict[key] == other_dict[key] + except ValueError as err: + # When dealing with arrays, we need to use numpy for comparison + if isinstance(one_dict[key], dict): + assert FitResults._array_safe_dict_eq(one_dict[key], other_dict[key]) + else: + assert np.allclose(one_dict[key], other_dict[key]) + except AssertionError: + print('key', key) + return False + else: return True + + def __eq__(self, other): + return FitResults._array_safe_dict_eq(self.__dict__, other.__dict__) \ No newline at end of file diff --git a/symfit/core/minimizers.py b/symfit/core/minimizers.py index c8be7175..fe0d9501 100644 --- a/symfit/core/minimizers.py +++ b/symfit/core/minimizers.py @@ -32,6 +32,8 @@ def __init__(self, objective, parameters): self.parameters = parameters self._fixed_params = [p for p in parameters if p.fixed] self.objective = partial(objective, **{p.name: p.value for p in self._fixed_params}) + # Mapping which we use to track the original, to be used upon pickling + self._pickle_kwargs = {'parameters': parameters, 'objective': objective} self.params = [p for p in parameters if not p.fixed] @abc.abstractmethod @@ -56,6 +58,13 @@ def initial_guesses(self): def initial_guesses(self, vals): self._initial_guesses = vals + def __getstate__(self): + return {key: value for key, value in self.__dict__.items() + if not key.startswith('wrapped_')} + + def __setstate__(self, state): + self.__dict__.update(state) + self.__init__(**self._pickle_kwargs) class BoundedMinimizer(BaseMinimizer): """ @@ -73,6 +82,10 @@ class ConstrainedMinimizer(BaseMinimizer): def __init__(self, *args, **kwargs): constraints = kwargs.pop('constraints') super(ConstrainedMinimizer, self).__init__(*args, **kwargs) + # Remember the vanilla constraints for pickling + self._pickle_kwargs['constraints'] = constraints + if constraints is None: + constraints = [] self.constraints = [ partial(constraint, **{p.name: p.value for p in self._fixed_params}) for constraint in constraints @@ -84,11 +97,12 @@ class GradientMinimizer(BaseMinimizer): """ @keywordonly(jacobian=None) def __init__(self, *args, **kwargs): - jacobian = kwargs.pop('jacobian') + self.jacobian = kwargs.pop('jacobian') super(GradientMinimizer, self).__init__(*args, **kwargs) + self._pickle_kwargs['jacobian'] = self.jacobian - if jacobian is not None: - jac_with_fixed_params = partial(jacobian, **{p.name: p.value for p in self._fixed_params}) + if self.jacobian is not None: + jac_with_fixed_params = partial(self.jacobian, **{p.name: p.value for p in self._fixed_params}) self.wrapped_jacobian = self.resize_jac(jac_with_fixed_params) else: self.jacobian = None @@ -143,6 +157,7 @@ def __init__(self, *args, **kwargs): minimizers = kwargs.pop('minimizers') super(ChainedMinimizer, self).__init__(*args, **kwargs) self.minimizers = minimizers + self._pickle_kwargs['minimizers'] = self.minimizers self.__signature__ = self._make_signature() def execute(self, **minimizer_kwargs): @@ -306,7 +321,7 @@ def _pack_output(self, ans): covariance_matrix=None, infodic=infodic, mesg=ans.message, - ier=ans.nit if hasattr(ans, 'nit') else float('nan'), + ier=ans.nit if hasattr(ans, 'nit') else None, objective_value=ans.fun, ) @@ -327,6 +342,7 @@ def method_name(cls): """ return cls.__name__ + class ScipyGradientMinimize(ScipyMinimize, GradientMinimizer): """ Base class for :func:`scipy.optimize.minimize`'s gradient-minimizers. @@ -374,6 +390,7 @@ def scipy_constraints(self, constraints): cons = tuple(cons) return cons + class BFGS(ScipyGradientMinimize): """ Wrapper around :func:`scipy.optimize.minimize`'s BFGS algorithm. diff --git a/tests/test_minimizers.py b/tests/test_minimizers.py index 069ad464..c0a6e11d 100644 --- a/tests/test_minimizers.py +++ b/tests/test_minimizers.py @@ -5,13 +5,49 @@ import numpy as np from scipy.optimize import minimize +import pickle from symfit import ( Variable, Parameter, Eq, Ge, Le, Lt, Gt, Ne, parameters, ModelError, Fit, - Model, FitResults, variables + Model, FitResults, variables, CallableNumericalModel, Constraint ) -from symfit.core.objectives import MinimizeModel -from symfit.core.minimizers import BFGS, Powell +from symfit.core.minimizers import * +from symfit.core.support import partial + +# Defined at the global level because local functions can't be pickled. +def f(x, a, b): + return a * x + b + +def chi_squared(x, y, a, b, sum=True): + if sum: + return np.sum((y - f(x, a, b)) ** 2) + else: + return (y - f(x, a, b)) ** 2 + +def worker(fit_obj): + return fit_obj.execute() + + +def subclasses(object, all_subs=None): + """ + Recursively create a set of subclasses of ``object``. Returns only + the leaves in the subclass tree. + + :param object: Class + :param all_subs: set of subclasses so far. Will be build internally. + :return: All leaves of the subclass tree. + """ + if all_subs is None: + all_subs = set() + object_subs = set(object.__subclasses__()) + all_subs.update(object_subs) + for sub in object_subs: + sub_subs = subclasses(sub, all_subs=all_subs) + # Only keep the leaves of the tree + if sub_subs and object in all_subs: + all_subs.remove(object) + all_subs.update(sub_subs) + return all_subs class TestMinimize(unittest.TestCase): @@ -86,6 +122,121 @@ def test_powell(self): fit_result = fit.execute() self.assertAlmostEqual(fit_result.value(b), 1.0) + def test_pickle(self): + """ + Test the picklability of the different minimizers. + """ + # Create test data + xdata = np.linspace(0, 100, 2) # From 0 to 100 in 100 steps + a_vec = np.random.normal(15.0, scale=2.0, size=xdata.shape) + b_vec = np.random.normal(100, scale=2.0, size=xdata.shape) + ydata = a_vec * xdata + b_vec # Point scattered around the line 5 * x + 105 + + # Normal symbolic fit + a = Parameter('a', value=0, min=0.0, max=1000) + b = Parameter('b', value=0, min=0.0, max=1000) + + # Make a set of all ScipyMinimizers, and add a chained minimizer. + scipy_minimizers = subclasses(ScipyMinimize) + chained_minimizer = partial(ChainedMinimizer, + minimizers=[DifferentialEvolution, BFGS]) + scipy_minimizers.add(chained_minimizer) + constrained_minimizers = subclasses(ScipyConstrainedMinimize) + # Test for all of them if they can be pickled. + for minimizer in scipy_minimizers: + if minimizer is MINPACK: + fit = minimizer( + partial(chi_squared, x=xdata, y=ydata, sum=False), + [a, b] + ) + elif minimizer in constrained_minimizers: + # For constraint minimizers we also add a constraint, just to be + # sure constraints are treated well. + dummy_model = CallableNumericalModel({}, independent_vars=[], params=[a, b]) + fit = minimizer( + partial(chi_squared, x=xdata, y=ydata), + [a, b], + constraints=[Constraint(Ge(b, a), model=dummy_model)] + ) + elif isinstance(minimizer, partial) and issubclass(minimizer.func, ChainedMinimizer): + init_minimizers = [] + for sub_minimizer in minimizer.keywords['minimizers']: + init_minimizers.append(sub_minimizer( + partial(chi_squared, x=xdata, y=ydata), + [a, b] + )) + minimizer.keywords['minimizers'] = init_minimizers + fit = minimizer(partial(chi_squared, x=xdata, y=ydata), [a, b]) + else: + fit = minimizer(partial(chi_squared, x=xdata, y=ydata), [a, b]) + + dump = pickle.dumps(fit) + pickled_fit = pickle.loads(dump) + problematic_attr = [ + 'objective', '_objective', 'wrapped_objective', + '_constraints', 'constraints', 'wrapped_constraints', + 'local_minimizer', 'minimizers' + ] + + for key, value in fit.__dict__.items(): + new_value = pickled_fit.__dict__[key] + try: + self.assertEqual(value, new_value) + except AssertionError as err: + if key in problematic_attr: + # These attr are new instances, and therefore do not + # pass an equality test. All we can do is see if they + # are at least the same type. + if isinstance(value, list): + for val1, val2 in zip(value, new_value): + self.assertTrue(isinstance(val1, val2.__class__)) + else: + self.assertTrue(isinstance(new_value, value.__class__)) + else: + raise err + self.assertEqual(fit.__dict__.keys(), pickled_fit.__dict__.keys()) + + # Test if we converge to the same result. + np.random.seed(2) + res_before = fit.execute() + np.random.seed(2) + res_after = pickled_fit.execute() + self.assertEqual(res_before, res_after) + + def test_multiprocessing(self): + """ + To make sure pickling truly works, try multiprocessing. No news is good + news. + """ + import multiprocessing as mp + + np.random.seed(2) + x = np.arange(100, dtype=float) + y = x + 0.25 * x * np.random.rand(100) + a_values = np.arange(12) + 1 + np.random.shuffle(a_values) + + def gen_fit_objs(x, y, a, minimizer): + for a_i in a: + a_par = Parameter('a', 5, min=0.0, max=20) + b_par = Parameter('b', 1, min=0.0, max=2) + x_var = Variable('x') + y_var = Variable('y') + + model = CallableNumericalModel({y_var: f}, [x_var], [a_par, b_par]) + + fit = Fit(model, x, a_i * y + 1, minimizer=minimizer) + yield fit + + minimizers = subclasses(ScipyMinimize) + chained_minimizer = (DifferentialEvolution, BFGS) + minimizers.add(chained_minimizer) + + all_results = {} + pool = mp.Pool() + for minimizer in minimizers: + results = pool.map(worker, gen_fit_objs(x, y, a_values, minimizer)) + all_results[minimizer] = [res.params['a'] for res in results] if __name__ == '__main__': From c9b9c8164f8b8fa994d0a10cc50fee638d8e2819 Mon Sep 17 00:00:00 2001 From: Martin Roelfs Date: Tue, 23 Oct 2018 10:24:02 +0200 Subject: [PATCH 02/10] Test models signature after pickling --- tests/test_model.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_model.py b/tests/test_model.py index 6ce0ae78..508ae814 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -230,6 +230,8 @@ def test_pickle(self): new_model.model_dict = model.model_dict new_model.dependent_vars = model.dependent_vars new_model.sigmas = model.sigmas + # Compare signatures + self.assertEqual(model.__signature__, new_model.__signature__) # Trigger the cached vars. model.vars new_model.vars From fe02143998e012fa1515480e9df5e940818e3426 Mon Sep 17 00:00:00 2001 From: Martin Roelfs Date: Tue, 23 Oct 2018 10:25:01 +0200 Subject: [PATCH 03/10] Fixed Constraint's __reduce__ method --- symfit/core/fit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/symfit/core/fit.py b/symfit/core/fit.py index c5e96de0..5f5b4ced 100644 --- a/symfit/core/fit.py +++ b/symfit/core/fit.py @@ -754,7 +754,7 @@ def _make_signature(self): def __reduce__(self): return ( self.__class__, - (self.constraint_type(list(self.values())[0]), self.model) + (self.constraint_type(list(self.values())[0], 0), self.model) ) class TakesData(object): From 5be244630e5dd354cda4f22dd8666b7f479b057c Mon Sep 17 00:00:00 2001 From: Martin Roelfs Date: Tue, 23 Oct 2018 10:48:51 +0200 Subject: [PATCH 04/10] Tests for pickling of objectives --- tests/test_objectives.py | 54 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/test_objectives.py diff --git a/tests/test_objectives.py b/tests/test_objectives.py new file mode 100644 index 00000000..1b70e514 --- /dev/null +++ b/tests/test_objectives.py @@ -0,0 +1,54 @@ +from __future__ import division, print_function +import unittest +import warnings +import pickle + +import numpy as np + +from symfit import ( + Variable, Parameter, Eq, Ge, Le, Lt, Gt, Ne, parameters, ModelError, Fit, + Model, FitResults, variables, CallableNumericalModel, Constraint +) +from symfit.core.objectives import ( + VectorLeastSquares, LeastSquares, LogLikelihood, MinimizeModel +) +from symfit.core.fit_results import FitResults + +class TestObjectives(unittest.TestCase): + @classmethod + def setUpClass(cls): + np.random.seed(0) + + def test_pickle(self): + """ + Test the picklability of the built-in objectives. + """ + # Create test data + xdata = np.linspace(0, 100, 25) # From 0 to 100 in 100 steps + a_vec = np.random.normal(15.0, scale=2.0, size=xdata.shape) + b_vec = np.random.normal(100, scale=2.0, size=xdata.shape) + ydata = a_vec * xdata + b_vec # Point scattered around the line 5 * x + 105 + + # Normal symbolic fit + a = Parameter('a', value=0, min=0.0, max=1000) + b = Parameter('b', value=0, min=0.0, max=1000) + x, y = variables('x, y') + model = Model({y: a * x + b}) + + for objective in [VectorLeastSquares, LeastSquares, LogLikelihood, MinimizeModel]: + obj = objective(model, data={'x': xdata, 'y': ydata}) + new_obj = pickle.loads(pickle.dumps(obj)) + self.assertTrue(FitResults._array_safe_dict_eq(obj.__dict__, + new_obj.__dict__)) + + +if __name__ == '__main__': + try: + unittest.main(warnings='ignore') + # Note that unittest will catch and handle exceptions raised by tests. + # So this line will *only* deal with exceptions raised by the line + # above. + except TypeError: + # In Py2, unittest.main doesn't take a warnings argument + warnings.simplefilter('ignore') + unittest.main() From 56d4465a49497c5540a6a36719ac1a29659431a8 Mon Sep 17 00:00:00 2001 From: Martin Roelfs Date: Tue, 23 Oct 2018 11:55:19 +0200 Subject: [PATCH 05/10] Fixed model equality check --- symfit/core/fit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/symfit/core/fit.py b/symfit/core/fit.py index 5f5b4ced..639b7a9b 100644 --- a/symfit/core/fit.py +++ b/symfit/core/fit.py @@ -108,7 +108,7 @@ def __eq__(self, other): if var_1 != var_2: return False else: - if not self[var_1].expand() - other[var_2].expand() == 0: + if not self[var_1].expand() == other[var_2].expand(): return False else: return True From ec3158e2df365ebeee83a6d28509ef6f34dc15a3 Mon Sep 17 00:00:00 2001 From: Martin Roelfs Date: Tue, 23 Oct 2018 11:55:45 +0200 Subject: [PATCH 06/10] Fixed mistake in test --- symfit/core/fit_results.py | 1 - tests/test_minimize.py | 2 -- tests/test_minimizers.py | 10 +++++----- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/symfit/core/fit_results.py b/symfit/core/fit_results.py index 443abf43..d44ee557 100644 --- a/symfit/core/fit_results.py +++ b/symfit/core/fit_results.py @@ -143,7 +143,6 @@ def _array_safe_dict_eq(one_dict, other_dict): else: assert np.allclose(one_dict[key], other_dict[key]) except AssertionError: - print('key', key) return False else: return True diff --git a/tests/test_minimize.py b/tests/test_minimize.py index 128f3b03..f65de22d 100644 --- a/tests/test_minimize.py +++ b/tests/test_minimize.py @@ -167,12 +167,10 @@ def test_basinhopping(self): x0 = [1.] np.random.seed(555) res = basinhopping(func, x0, minimizer_kwargs={"method": "BFGS"}, niter=200) - print(res) np.random.seed(555) x, = parameters('x') fit = BasinHopping(func, [x]) fit_result = fit.execute(minimizer_kwargs={"method": "BFGS", 'jac': False}, niter=200) - print(fit_result) self.assertEqual(res.x, fit_result.value(x)) self.assertEqual(res.fun, fit_result.objective_value) diff --git a/tests/test_minimizers.py b/tests/test_minimizers.py index c0a6e11d..ee432d99 100644 --- a/tests/test_minimizers.py +++ b/tests/test_minimizers.py @@ -138,9 +138,9 @@ def test_pickle(self): # Make a set of all ScipyMinimizers, and add a chained minimizer. scipy_minimizers = subclasses(ScipyMinimize) - chained_minimizer = partial(ChainedMinimizer, - minimizers=[DifferentialEvolution, BFGS]) - scipy_minimizers.add(chained_minimizer) + # chained_minimizer = partial(ChainedMinimizer, + # minimizers=[DifferentialEvolution, BFGS]) + # scipy_minimizers.add(chained_minimizer) constrained_minimizers = subclasses(ScipyConstrainedMinimize) # Test for all of them if they can be pickled. for minimizer in scipy_minimizers: @@ -173,8 +173,8 @@ def test_pickle(self): dump = pickle.dumps(fit) pickled_fit = pickle.loads(dump) problematic_attr = [ - 'objective', '_objective', 'wrapped_objective', - '_constraints', 'constraints', 'wrapped_constraints', + 'objective', '_pickle_kwargs', 'wrapped_objective', + 'constraints', 'wrapped_constraints', 'local_minimizer', 'minimizers' ] From b25af8f60cc6ae5d366c0bc110b46042d7920cc4 Mon Sep 17 00:00:00 2001 From: Martin Roelfs Date: Tue, 23 Oct 2018 14:31:31 +0200 Subject: [PATCH 07/10] Improved py27 compatibility of tests --- tests/test_minimizers.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/test_minimizers.py b/tests/test_minimizers.py index ee432d99..ced32256 100644 --- a/tests/test_minimizers.py +++ b/tests/test_minimizers.py @@ -4,8 +4,8 @@ import warnings import numpy as np -from scipy.optimize import minimize import pickle +import multiprocessing as mp from symfit import ( Variable, Parameter, Eq, Ge, Le, Lt, Gt, Ne, parameters, ModelError, Fit, @@ -138,9 +138,9 @@ def test_pickle(self): # Make a set of all ScipyMinimizers, and add a chained minimizer. scipy_minimizers = subclasses(ScipyMinimize) - # chained_minimizer = partial(ChainedMinimizer, - # minimizers=[DifferentialEvolution, BFGS]) - # scipy_minimizers.add(chained_minimizer) + chained_minimizer = partial(ChainedMinimizer, + minimizers=[DifferentialEvolution, BFGS]) + scipy_minimizers.add(chained_minimizer) constrained_minimizers = subclasses(ScipyConstrainedMinimize) # Test for all of them if they can be pickled. for minimizer in scipy_minimizers: @@ -187,14 +187,17 @@ def test_pickle(self): # These attr are new instances, and therefore do not # pass an equality test. All we can do is see if they # are at least the same type. - if isinstance(value, list): + if isinstance(value, (list, tuple)): for val1, val2 in zip(value, new_value): self.assertTrue(isinstance(val1, val2.__class__)) + elif key == '_pickle_kwargs': + FitResults._array_safe_dict_eq(value, new_value) else: self.assertTrue(isinstance(new_value, value.__class__)) else: raise err - self.assertEqual(fit.__dict__.keys(), pickled_fit.__dict__.keys()) + self.assertEqual(set(fit.__dict__.keys()), + set(pickled_fit.__dict__.keys())) # Test if we converge to the same result. np.random.seed(2) @@ -208,12 +211,10 @@ def test_multiprocessing(self): To make sure pickling truly works, try multiprocessing. No news is good news. """ - import multiprocessing as mp - np.random.seed(2) x = np.arange(100, dtype=float) y = x + 0.25 * x * np.random.rand(100) - a_values = np.arange(12) + 1 + a_values = np.arange(3) + 1 np.random.shuffle(a_values) def gen_fit_objs(x, y, a, minimizer): From 62d2c487dc300b16ab74e39b7e9705ab654db797 Mon Sep 17 00:00:00 2001 From: Martin Roelfs Date: Tue, 23 Oct 2018 14:44:59 +0200 Subject: [PATCH 08/10] Made __getstate__ py27 compatible for ChainedMinimizers --- symfit/core/minimizers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/symfit/core/minimizers.py b/symfit/core/minimizers.py index fe0d9501..64080cbd 100644 --- a/symfit/core/minimizers.py +++ b/symfit/core/minimizers.py @@ -239,6 +239,10 @@ def _make_signature(self): ) return inspect_sig.Signature(parameters=reversed(parameters)) + def __getstate__(self): + state = super(ChainedMinimizer, self).__getstate__() + del state['__signature__'] + return state class ScipyMinimize(object): """ From dfd9b7bb0c96d2d23620fa9169db61f07cc44079 Mon Sep 17 00:00:00 2001 From: Martin Roelfs Date: Tue, 23 Oct 2018 15:02:01 +0200 Subject: [PATCH 09/10] Improved subclasses function as suggested --- tests/test_minimizers.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/tests/test_minimizers.py b/tests/test_minimizers.py index ced32256..b28e12f7 100644 --- a/tests/test_minimizers.py +++ b/tests/test_minimizers.py @@ -27,29 +27,24 @@ def chi_squared(x, y, a, b, sum=True): def worker(fit_obj): return fit_obj.execute() - -def subclasses(object, all_subs=None): +def subclasses(base, leaves_only=True): """ - Recursively create a set of subclasses of ``object``. Returns only - the leaves in the subclass tree. + Recursively create a set of subclasses of ``object``. :param object: Class - :param all_subs: set of subclasses so far. Will be build internally. - :return: All leaves of the subclass tree. + :param leaves_only: If ``True``, return only the leaves of the subclass tree + :return: (All leaves of) the subclass tree. """ - if all_subs is None: + base_subs = set(base.__subclasses__()) + if not base_subs or not leaves_only: + all_subs = {base} + else: all_subs = set() - object_subs = set(object.__subclasses__()) - all_subs.update(object_subs) - for sub in object_subs: - sub_subs = subclasses(sub, all_subs=all_subs) - # Only keep the leaves of the tree - if sub_subs and object in all_subs: - all_subs.remove(object) + for sub in list(base_subs): + sub_subs = subclasses(sub, leaves_only=leaves_only) all_subs.update(sub_subs) return all_subs - class TestMinimize(unittest.TestCase): @classmethod def setUpClass(cls): From d8e52829f7bad7e930fc9dbaafd86a0e7bf088d6 Mon Sep 17 00:00:00 2001 From: Martin Roelfs Date: Wed, 24 Oct 2018 12:01:36 +0200 Subject: [PATCH 10/10] Added additional safeguards to the checking for pickling of constraints. --- tests/test_minimizers.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_minimizers.py b/tests/test_minimizers.py index b28e12f7..0ee06346 100644 --- a/tests/test_minimizers.py +++ b/tests/test_minimizers.py @@ -185,6 +185,24 @@ def test_pickle(self): if isinstance(value, (list, tuple)): for val1, val2 in zip(value, new_value): self.assertTrue(isinstance(val1, val2.__class__)) + if key == 'constraints': + self.assertEqual(val1.func.constraint_type, + val2.func.constraint_type) + self.assertEqual( + list(val1.func.model_dict.values())[0], + list(val2.func.model_dict.values())[0] + ) + self.assertEqual(val1.func.independent_vars, + val2.func.independent_vars) + self.assertEqual(val1.func.params, + val2.func.params) + self.assertEqual(val1.func.__signature__, + val2.func.__signature__) + elif key == 'wrapped_constraints': + self.assertEqual(val1['type'], + val2['type']) + self.assertEqual(set(val1.keys()), + set(val2.keys())) elif key == '_pickle_kwargs': FitResults._array_safe_dict_eq(value, new_value) else: