diff --git a/src/easyscience/fitting/fitter.py b/src/easyscience/fitting/fitter.py index 07ca844e..7af26cf4 100644 --- a/src/easyscience/fitting/fitter.py +++ b/src/easyscience/fitting/fitter.py @@ -5,6 +5,7 @@ from typing import Callable from typing import List from typing import Optional +from typing import Union import numpy as np @@ -14,7 +15,7 @@ from .minimizers.factory import factory from .minimizers.factory import from_string_to_enum -DEFAULT_MINIMIZER = 'lmfit-leastsq' +DEFAULT_MINIMIZER = AvailableMinimizers.LMFit_leastsq class Fitter: @@ -27,9 +28,9 @@ def __init__(self, fit_object, fit_function: Callable): self._fit_function = fit_function self._dependent_dims = None - self._name_current_minimizer = DEFAULT_MINIMIZER + self._enum_current_minimizer = DEFAULT_MINIMIZER self._minimizer: MinimizerBase # _minimizer is set in the create method - self._update_minimizer(self._name_current_minimizer) + self._update_minimizer(self._enum_current_minimizer) def fit_constraints(self) -> list: return self._minimizer.fit_constraints() @@ -62,26 +63,32 @@ def initialize(self, fit_object, fit_function: Callable) -> None: self._update_minimizer(DEFAULT_MINIMIZER) # TODO: remove this method when we are ready to adjust the dependent products - def create(self, minimizer_name: str = DEFAULT_MINIMIZER) -> None: + def create(self, minimizer_enum: Union[AvailableMinimizers, str] = DEFAULT_MINIMIZER) -> None: """ Create the required minimizer. - :param minimizer_name: The label of the minimization engine to create. + :param minimizer_enum: The enum of the minimization engine to create. """ - self._update_minimizer(minimizer_name) + if isinstance(minimizer_enum, str): + print(f'minimizer should be set with enum {minimizer_enum}') + minimizer_enum = from_string_to_enum(minimizer_enum) + self._update_minimizer(minimizer_enum) - def switch_minimizer(self, minimizer_name: str) -> None: + def switch_minimizer(self, minimizer_enum: Union[AvailableMinimizers, str]) -> None: """ Switch minimizer and initialize. - :param minimizer_name: The label of the minimizer to create and instantiate. + :param minimizer_enum: The enum of the minimizer to create and instantiate. """ + if isinstance(minimizer_enum, str): + print(f'minimizer should be set with enum {minimizer_enum}') + minimizer_enum = from_string_to_enum(minimizer_enum) + constraints = self._minimizer.fit_constraints() - self._update_minimizer(minimizer_name) + self._update_minimizer(minimizer_enum) self._minimizer.set_fit_constraint(constraints) - def _update_minimizer(self, minimizer_name: str) -> None: - minimizer_enum = from_string_to_enum(minimizer_name) + def _update_minimizer(self, minimizer_enum: AvailableMinimizers) -> None: self._minimizer = factory(minimizer_enum=minimizer_enum, fit_object=self._fit_object, fit_function=self.fit_function) - self._name_current_minimizer = minimizer_name + self._enum_current_minimizer = minimizer_enum @property def available_minimizers(self) -> List[str]: @@ -119,7 +126,7 @@ def fit_function(self, fit_function: Callable) -> None: :return: None """ self._fit_function = fit_function - self._update_minimizer(self._name_current_minimizer) + self._update_minimizer(self._enum_current_minimizer) @property def fit_object(self): @@ -137,7 +144,7 @@ def fit_object(self, fit_object) -> None: :return: None """ self._fit_object = fit_object - self._update_minimizer(self._name_current_minimizer) + self._update_minimizer(self._enum_current_minimizer) def _fit_function_wrapper(self, real_x=None, flatten: bool = True) -> Callable: """ diff --git a/src/easyscience/fitting/minimizers/factory.py b/src/easyscience/fitting/minimizers/factory.py index c48d8ea0..503cc729 100644 --- a/src/easyscience/fitting/minimizers/factory.py +++ b/src/easyscience/fitting/minimizers/factory.py @@ -21,7 +21,7 @@ bumps_engine_imported = True except ImportError: # TODO make this a proper message (use logging?) - warnings.warn('Bummps minimization is not available. Probably bumps has not been installed.', ImportWarning, stacklevel=2) + warnings.warn('Bumps minimization is not available. Probably bumps has not been installed.', ImportWarning, stacklevel=2) dfo_engine_imported = False try: @@ -50,31 +50,32 @@ class AvailableMinimizers(Enum): DFO = auto() DFO_leastsq = auto() - # Temporary solution to convert string to enum def from_string_to_enum(minimizer_name: str) -> AvailableMinimizers: - if minimizer_name == 'lmfit': + if minimizer_name == 'LMFit': minmizer_enum = AvailableMinimizers.LMFit - elif minimizer_name == 'lmfit-leastsq': + elif minimizer_name == 'LMFit_leastsq': minmizer_enum = AvailableMinimizers.LMFit_leastsq - elif minimizer_name == 'lmfit-powell': + elif minimizer_name == 'LMFit_powell': minmizer_enum = AvailableMinimizers.LMFit_powell - elif minimizer_name == 'lmfit-cobyla': + elif minimizer_name == 'LMFit_cobyla': minmizer_enum = AvailableMinimizers.LMFit_cobyla - elif minimizer_name == 'bumps': + elif minimizer_name == 'Bumps': minmizer_enum = AvailableMinimizers.Bumps - elif minimizer_name == 'bumps-simplex': + elif minimizer_name == 'Bumps_simplex': minmizer_enum = AvailableMinimizers.Bumps_simplex - elif minimizer_name == 'bumps-newton': + elif minimizer_name == 'Bumps_newton': minmizer_enum = AvailableMinimizers.Bumps_newton - elif minimizer_name == 'bumps-lm': + elif minimizer_name == 'Bumps_lm': minmizer_enum = AvailableMinimizers.Bumps_lm - elif minimizer_name == 'dfo': + elif minimizer_name == 'DFO': minmizer_enum = AvailableMinimizers.DFO - elif minimizer_name == 'dfo-leastsq': + elif minimizer_name == 'DFO_leastsq': minmizer_enum = AvailableMinimizers.DFO_leastsq + else: + raise ValueError(f"Invalid minimizer name: {minimizer_name}. The following minimizers are available: {[minimize.name for minimize in AvailableMinimizers]}") # noqa: E501 return minmizer_enum diff --git a/src/easyscience/fitting/minimizers/minimizer_base.py b/src/easyscience/fitting/minimizers/minimizer_base.py index b73cdc38..fdc950ed 100644 --- a/src/easyscience/fitting/minimizers/minimizer_base.py +++ b/src/easyscience/fitting/minimizers/minimizer_base.py @@ -32,7 +32,10 @@ class MinimizerBase(metaclass=ABCMeta): wrapping: str = None def __init__( - self, obj, fit_function: Callable, method: Optional[str] = None + self, + obj, #: BaseObj, + fit_function: Callable, + method: Optional[str] = None, ): # todo after constraint changes, add type hint: obj: BaseObj # noqa: E501 if method not in self.available_methods(): raise FitError(f'Method {method} not available in {self.__class__}') diff --git a/src/easyscience/fitting/minimizers/minimizer_bumps.py b/src/easyscience/fitting/minimizers/minimizer_bumps.py index 807220d4..3881372d 100644 --- a/src/easyscience/fitting/minimizers/minimizer_bumps.py +++ b/src/easyscience/fitting/minimizers/minimizer_bumps.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project Callable: - """ - Generate a bumps model from the supplied `fit_function` and parameters in the base object. - Note that this makes a callable as it needs to be initialized with *x*, *y*, *weights* - - :return: Callable to make a bumps Curve model - :rtype: Callable - """ - fit_func = self._generate_fit_function() - - def outer(obj): - def make_func(x, y, weights): - par = {} - if not pars: - for name, item in obj._cached_pars.items(): - par[MINIMIZER_PARAMETER_PREFIX + str(name)] = obj.convert_to_par_object(item) - else: - for item in pars: - par[MINIMIZER_PARAMETER_PREFIX + item.unique_name] = obj.convert_to_par_object(item) - return Curve(fit_func, x, y, dy=weights, **par) - - return make_func - - return outer(self) - - def _generate_fit_function(self) -> Callable: - """ - Using the user supplied `fit_function`, wrap it in such a way we can update `Parameter` on - iterations. - - :return: a fit function which is compatible with bumps models - :rtype: Callable - """ - # Original fit function - func = self._original_fit_function - # Get a list of `Parameters` - self._cached_pars_vals = {} - for parameter in self._object.get_fit_parameters(): - key = parameter.unique_name - self._cached_pars[key] = parameter - self._cached_pars_vals[key] = (parameter.value, parameter.error) - - # Make a new fit function - def fit_function(x: np.ndarray, **kwargs): - """ - Wrapped fit function which now has a bumps compatible form - - :param x: array of data points to be calculated - :type x: np.ndarray - :param kwargs: key word arguments - :return: points calculated at `x` - :rtype: np.ndarray - """ - # Update the `Parameter` values and the callback if needed - for name, value in kwargs.items(): - par_name = name[1:] - if par_name in self._cached_pars.keys(): - ## TODO clean when full move to new_variable - from easyscience.Objects.new_variable import Parameter - - if isinstance(self._cached_pars[par_name], Parameter): - if self._cached_pars[par_name].value != value: - self._cached_pars[par_name].value = value - else: - if self._cached_pars[par_name].raw_value != value: - self._cached_pars[par_name].value = value - - # update_fun = self._cached_pars[par_name]._callback.fset - # if update_fun: - # update_fun(value) - # TODO Pre processing here - for constraint in self.fit_constraints(): - constraint() - return_data = func(x) - # TODO Loading or manipulating data here - return return_data - - # Fake the function signature. - # This is done as lmfit wants the function to be in the form: - # f = (x, a=1, b=2)... - # Where we need to be generic. Note that this won't hold for much outside of this scope. - - ## TODO clean when full move to new_variable - from easyscience.Objects.new_variable import Parameter - - if isinstance(parameter, Parameter): - default_value = parameter.value - else: - default_value = parameter.raw_value - - self._cached_pars_order = tuple(self._cached_pars.keys()) - params = [ - inspect.Parameter('x', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=inspect._empty), - *[ - inspect.Parameter( - MINIMIZER_PARAMETER_PREFIX + str(name), - inspect.Parameter.POSITIONAL_OR_KEYWORD, - annotation=inspect._empty, - default=default_value, - ) - for name in self._cached_pars_order - ], - ] - # Sign the function - fit_function.__signature__ = inspect.Signature(params) - self._fit_function = fit_function - return fit_function + def available_methods(self) -> List[str]: + return FIT_AVAILABLE_IDS_FILTERED def fit( self, @@ -203,8 +110,8 @@ def fit( minimizer_kwargs.update(engine_kwargs) if model is None: - model = self._make_model(pars=parameters) - model = model(x, y, weights) + model_function = self._make_model(parameters=parameters) + model = model_function(x, y, weights) self._cached_model = model ## TODO clean when full move to new_variable @@ -272,6 +179,115 @@ def convert_to_par_object(obj) -> BumpsParameter: fixed=obj.fixed, ) + def _make_model(self, parameters: Optional[List[BumpsParameter]] = None) -> Callable: + """ + Generate a bumps model from the supplied `fit_function` and parameters in the base object. + Note that this makes a callable as it needs to be initialized with *x*, *y*, *weights* + + :return: Callable to make a bumps Curve model + :rtype: Callable + """ + fit_func = self._generate_fit_function() + + def _outer(obj): + def _make_func(x, y, weights): + bumps_pars = {} + if not parameters: + for name, par in obj._cached_pars.items(): + bumps_pars[MINIMIZER_PARAMETER_PREFIX + str(name)] = obj.convert_to_par_object(par) + else: + for par in parameters: + bumps_pars[MINIMIZER_PARAMETER_PREFIX + par.unique_name] = obj.convert_to_par_object(par) + return Curve(fit_func, x, y, dy=weights, **bumps_pars) + + return _make_func + + return _outer(self) + + def _generate_fit_function(self) -> Callable: + """ + Using the user supplied `fit_function`, wrap it in such a way we can update `Parameter` on + iterations. + + :return: a fit function which is compatible with bumps models + :rtype: Callable + """ + # Original fit function + func = self._original_fit_function + # Get a list of `Parameters` + self._cached_pars_vals = {} + for parameter in self._object.get_fit_parameters(): + key = parameter.unique_name + self._cached_pars[key] = parameter + self._cached_pars_vals[key] = (parameter.value, parameter.error) + + # Make a new fit function + def _fit_function(x: np.ndarray, **kwargs): + """ + Wrapped fit function which now has a bumps compatible form + + :param x: array of data points to be calculated + :type x: np.ndarray + :param kwargs: key word arguments + :return: points calculated at `x` + :rtype: np.ndarray + """ + # Update the `Parameter` values and the callback if needed + ## TODO clean when full move to new_variable + from easyscience.Objects.new_variable import Parameter + + for name, value in kwargs.items(): + par_name = name[1:] + if par_name in self._cached_pars.keys(): + ## TODO clean when full move to new_variable + if isinstance(self._cached_pars[par_name], Parameter): + if self._cached_pars[par_name].value != value: + self._cached_pars[par_name].value = value + else: + if self._cached_pars[par_name].raw_value != value: + self._cached_pars[par_name].value = value + + # update_fun = self._cached_pars[par_name]._callback.fset + # if update_fun: + # update_fun(value) + # TODO Pre processing here + for constraint in self.fit_constraints(): + constraint() + return_data = func(x) + # TODO Loading or manipulating data here + return return_data + + # Fake the function signature. + # This is done as lmfit wants the function to be in the form: + # f = (x, a=1, b=2)... + # Where we need to be generic. Note that this won't hold for much outside of this scope. + + ## TODO clean when full move to new_variable + from easyscience.Objects.new_variable import Parameter + + if isinstance(parameter, Parameter): + default_value = parameter.value + else: + default_value = parameter.raw_value + + self._cached_pars_order = tuple(self._cached_pars.keys()) + params = [ + inspect.Parameter('x', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=inspect._empty), + *[ + inspect.Parameter( + MINIMIZER_PARAMETER_PREFIX + str(name), + inspect.Parameter.POSITIONAL_OR_KEYWORD, + annotation=inspect._empty, + default=default_value, + ) + for name in self._cached_pars_order + ], + ] + # Sign the function + _fit_function.__signature__ = inspect.Signature(params) + self._fit_function = _fit_function + return _fit_function + def _set_parameter_fit_result(self, fit_result, stack_status: bool): """ Update parameters to their final values and assign a std error to them. @@ -292,7 +308,7 @@ def _set_parameter_fit_result(self, fit_result, stack_status: bool): global_object.stack.beginMacro('Fitting routine') for index, name in enumerate(self._cached_model._pnames): - dict_name = name[1:] + dict_name = name[len(MINIMIZER_PARAMETER_PREFIX) :] pars[dict_name].value = fit_result.x[index] pars[dict_name].error = fit_result.dx[index] if stack_status: @@ -315,7 +331,7 @@ def _gen_fit_results(self, fit_results, **kwargs) -> FitResults: pars = self._cached_pars item = {} for index, name in enumerate(self._cached_model._pnames): - dict_name = name[1:] + dict_name = name[len(MINIMIZER_PARAMETER_PREFIX) :] ## TODO clean when full move to new_variable from easyscience.Objects.new_variable import Parameter @@ -338,6 +354,3 @@ def _gen_fit_results(self, fit_results, **kwargs) -> FitResults: results.engine_result = fit_results # results.check_sanity() return results - - def available_methods(self) -> List[str]: - return FIT_AVAILABLE_IDS diff --git a/src/easyscience/fitting/minimizers/minimizer_dfo.py b/src/easyscience/fitting/minimizers/minimizer_dfo.py index 7d683510..e6e91bc7 100644 --- a/src/easyscience/fitting/minimizers/minimizer_dfo.py +++ b/src/easyscience/fitting/minimizers/minimizer_dfo.py @@ -10,7 +10,8 @@ import dfols import numpy as np -from easyscience.Objects.ObjectClasses import BaseObj +# causes circular import when Parameter is imported +# from easyscience.Objects.ObjectClasses import BaseObj from easyscience.Objects.Variable import Parameter from .minimizer_base import MINIMIZER_PARAMETER_PREFIX @@ -26,7 +27,12 @@ class DFO(MinimizerBase): wrapping = 'dfo' - def __init__(self, obj: BaseObj, fit_function: Callable, method: Optional[str] = None): + def __init__( + self, + obj, #: BaseObj, + fit_function: Callable, + method: Optional[str] = None, + ): # todo after constraint changes, add type hint: obj: BaseObj # noqa: E501 """ Initialize the fitting engine with a `BaseObj` and an arbitrary fitting function. @@ -142,25 +148,25 @@ def _make_func(x, y, weights): ## TODO clean when full move to new_variable from easyscience.Objects.new_variable import Parameter as NewParameter - pars = {} + dfo_pars = {} if not parameters: for name, par in obj._cached_pars.items(): if isinstance(par, NewParameter): - pars[MINIMIZER_PARAMETER_PREFIX + str(name)] = par.value + dfo_pars[MINIMIZER_PARAMETER_PREFIX + str(name)] = par.value else: - pars[MINIMIZER_PARAMETER_PREFIX + str(name)] = par.raw_value + dfo_pars[MINIMIZER_PARAMETER_PREFIX + str(name)] = par.raw_value else: - for new_par in parameters: - if isinstance(new_par, NewParameter): - pars[MINIMIZER_PARAMETER_PREFIX + new_par.unique_name] = new_par.value + for par in parameters: + if isinstance(par, NewParameter): + dfo_pars[MINIMIZER_PARAMETER_PREFIX + par.unique_name] = par.value else: - pars[MINIMIZER_PARAMETER_PREFIX + new_par.unique_name] = new_par.raw_value + dfo_pars[MINIMIZER_PARAMETER_PREFIX + par.unique_name] = par.raw_value def _residuals(pars_values: List[float]) -> np.ndarray: - for idx, par_name in enumerate(pars.keys()): - pars[par_name] = pars_values[idx] - return (y - fit_func(x, **pars)) / weights + for idx, par_name in enumerate(dfo_pars.keys()): + dfo_pars[par_name] = pars_values[idx] + return (y - fit_func(x, **dfo_pars)) / weights return _residuals diff --git a/src/easyscience/fitting/minimizers/minimizer_lmfit.py b/src/easyscience/fitting/minimizers/minimizer_lmfit.py index 87c9d069..c099fdad 100644 --- a/src/easyscience/fitting/minimizers/minimizer_lmfit.py +++ b/src/easyscience/fitting/minimizers/minimizer_lmfit.py @@ -9,7 +9,6 @@ from typing import Dict from typing import List from typing import Optional -from typing import Union import numpy as np from lmfit import Model as LMModel @@ -17,8 +16,8 @@ from lmfit import Parameters as LMParameters from lmfit.model import ModelResult -from easyscience.Objects.new_variable import Parameter as NewParameter -from easyscience.Objects.ObjectClasses import BaseObj +# causes circular import when Parameter is imported +# from easyscience.Objects.ObjectClasses import BaseObj from easyscience.Objects.Variable import Parameter from .minimizer_base import MINIMIZER_PARAMETER_PREFIX @@ -35,7 +34,12 @@ class LMFit(MinimizerBase): # noqa: S101 wrapping = 'lmfit' - def __init__(self, obj: BaseObj, fit_function: Callable, method: Optional[str] = None): + def __init__( + self, + obj, #: BaseObj, + fit_function: Callable, + method: Optional[str] = None, + ): # todo after constraint changes, add type hint: obj: BaseObj # noqa: E501 """ Initialize the minimizer with the `BaseObj` and the `fit_function` to be used. @@ -214,7 +218,7 @@ def fit( raise FitError(e) return results - def convert_to_pars_obj(self, parameters: Optional[List[Union[Parameter, NewParameter]]] = None) -> LMParameters: + def convert_to_pars_obj(self, parameters: Optional[List[Parameter]] = None) -> LMParameters: """ Create an lmfit compatible container with the `Parameters` converted from the base object. @@ -228,14 +232,16 @@ def convert_to_pars_obj(self, parameters: Optional[List[Union[Parameter, NewPara return lm_parameters @staticmethod - def convert_to_par_object(parameter: Union[Parameter, NewParameter]) -> LMParameter: + def convert_to_par_object(parameter: Parameter) -> LMParameter: """ Convert an `EasyScience.Objects.Base.Parameter` object to a lmfit Parameter object. :return: lmfit Parameter compatible object. :rtype: LMParameter """ - ## TODO clean when full move to new_variable + ## TODO clean when full move to + from easyscience.Objects.new_variable import Parameter as NewParameter + if isinstance(parameter, NewParameter): value = parameter.value else: @@ -325,7 +331,7 @@ def available_methods(self) -> List[str]: ] @staticmethod - def _wrap_to_lm_signature(parameters: Dict[int, Union[Parameter, NewParameter]]) -> Signature: + def _wrap_to_lm_signature(parameters: Dict[int, Parameter]) -> Signature: """ Wrap the function signature. This is done as lmfit wants the function to be in the form: @@ -334,6 +340,10 @@ def _wrap_to_lm_signature(parameters: Dict[int, Union[Parameter, NewParameter]]) """ wrapped_parameters = [] wrapped_parameters.append(InspectParameter('x', InspectParameter.POSITIONAL_OR_KEYWORD, annotation=_empty)) + + ## TODO clean when full move to new_variable + from easyscience.Objects.new_variable import Parameter as NewParameter + for name, parameter in parameters.items(): ## TODO clean when full move to new_variable if isinstance(parameter, NewParameter): diff --git a/tests/integration_tests/Fitting/test_fitter.py b/tests/integration_tests/Fitting/test_fitter.py index 447eadf9..b6b8179f 100644 --- a/tests/integration_tests/Fitting/test_fitter.py +++ b/tests/integration_tests/Fitting/test_fitter.py @@ -11,6 +11,7 @@ from easyscience.fitting.Constraints import ObjConstraint from easyscience.fitting.fitter import Fitter from easyscience.fitting.minimizers import FitError +from easyscience.fitting.minimizers.factory import AvailableMinimizers from easyscience.Objects.ObjectClasses import BaseObj from easyscience.Objects.new_variable import Parameter @@ -78,8 +79,8 @@ def check_fit_results(result, sp_sin, ref_sin, x, **kwargs): @pytest.mark.parametrize("with_errors", [False, True]) -@pytest.mark.parametrize("fit_engine", [None, "lmfit", "bumps", "dfo"]) -def test_basic_fit(fit_engine, with_errors): +@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) +def test_basic_fit(fit_engine: AvailableMinimizers, with_errors): ref_sin = AbsSin(0.2, np.pi) sp_sin = AbsSin(0.354, 3.05) @@ -102,12 +103,12 @@ def test_basic_fit(fit_engine, with_errors): result = f.fit(*args, **kwargs) if fit_engine is not None: - assert result.minimizer_engine.wrapping == fit_engine + assert result.minimizer_engine.wrapping == fit_engine.name.lower() # Special case where minimizer matches wrapping assert sp_sin.phase.value == pytest.approx(ref_sin.phase.value, rel=1e-3) assert sp_sin.offset.value == pytest.approx(ref_sin.offset.value, rel=1e-3) -@pytest.mark.parametrize("fit_engine", [None, "lmfit", "bumps", "dfo"]) +@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) def test_fit_result(fit_engine): ref_sin = AbsSin(0.2, np.pi) sp_sin = AbsSin(0.354, 3.05) @@ -169,13 +170,13 @@ def test_bumps_methods(fit_method): sp_sin.phase.fixed = False f = Fitter(sp_sin, sp_sin) - f.switch_minimizer("bumps") + f.switch_minimizer("Bumps") assert fit_method in f._minimizer.available_methods() result = f.fit(x, y, method=fit_method) check_fit_results(result, sp_sin, ref_sin, x) -@pytest.mark.parametrize("fit_engine", ["lmfit", "bumps", "dfo"]) +@pytest.mark.parametrize("fit_engine", [AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) def test_fit_constraints(fit_engine): ref_sin = AbsSin(np.pi * 0.45, 0.45 * np.pi * 0.5) sp_sin = AbsSin(1, 0.5) @@ -205,7 +206,7 @@ def test_fit_constraints(fit_engine): @pytest.mark.parametrize("with_errors", [False, True]) -@pytest.mark.parametrize("fit_engine", [None, "lmfit", "bumps", "dfo"]) +@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) def test_2D_vectorized(fit_engine, with_errors): x = np.linspace(0, 5, 200) mm = AbsSin2D(0.3, 1.6) @@ -241,7 +242,7 @@ def test_2D_vectorized(fit_engine, with_errors): @pytest.mark.parametrize("with_errors", [False, True]) -@pytest.mark.parametrize("fit_engine", [None, "lmfit", "bumps", "dfo"]) +@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) def test_2D_non_vectorized(fit_engine, with_errors): x = np.linspace(0, 5, 200) mm = AbsSin2DL(0.3, 1.6) diff --git a/tests/integration_tests/Fitting/test_fitter_legacy_parameter.py b/tests/integration_tests/Fitting/test_fitter_legacy_parameter.py index cf7768fa..5dc040ff 100644 --- a/tests/integration_tests/Fitting/test_fitter_legacy_parameter.py +++ b/tests/integration_tests/Fitting/test_fitter_legacy_parameter.py @@ -11,6 +11,7 @@ from easyscience.fitting.Constraints import ObjConstraint from easyscience.fitting.fitter import Fitter from easyscience.fitting.minimizers import FitError +from easyscience.fitting.minimizers.factory import AvailableMinimizers from easyscience.Objects.ObjectClasses import BaseObj from easyscience.Objects.ObjectClasses import Parameter @@ -78,7 +79,7 @@ def check_fit_results(result, sp_sin, ref_sin, x, **kwargs): @pytest.mark.parametrize("with_errors", [False, True]) -@pytest.mark.parametrize("fit_engine", [None, "lmfit", "bumps", "dfo"]) +@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) def test_basic_fit(fit_engine, with_errors): ref_sin = AbsSin(0.2, np.pi) sp_sin = AbsSin(0.354, 3.05) @@ -102,12 +103,12 @@ def test_basic_fit(fit_engine, with_errors): result = f.fit(*args, **kwargs) if fit_engine is not None: - assert result.minimizer_engine.wrapping == fit_engine + assert result.minimizer_engine.wrapping == fit_engine.name.lower() assert sp_sin.phase.raw_value == pytest.approx(ref_sin.phase.raw_value, rel=1e-3) assert sp_sin.offset.raw_value == pytest.approx(ref_sin.offset.raw_value, rel=1e-3) -@pytest.mark.parametrize("fit_engine", [None, "lmfit", "bumps", "dfo"]) +@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) def test_fit_result(fit_engine): ref_sin = AbsSin(0.2, np.pi) sp_sin = AbsSin(0.354, 3.05) @@ -169,13 +170,13 @@ def test_bumps_methods(fit_method): sp_sin.phase.fixed = False f = Fitter(sp_sin, sp_sin) - f.switch_minimizer("bumps") + f.switch_minimizer("Bumps") assert fit_method in f._minimizer.available_methods() result = f.fit(x, y, method=fit_method) check_fit_results(result, sp_sin, ref_sin, x) -@pytest.mark.parametrize("fit_engine", ["lmfit", "bumps", "dfo"]) +@pytest.mark.parametrize("fit_engine", [AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) def test_fit_constraints(fit_engine): ref_sin = AbsSin(np.pi * 0.45, 0.45 * np.pi * 0.5) sp_sin = AbsSin(1, 0.5) @@ -205,7 +206,7 @@ def test_fit_constraints(fit_engine): @pytest.mark.parametrize("with_errors", [False, True]) -@pytest.mark.parametrize("fit_engine", [None, "lmfit", "bumps", "dfo"]) +@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) def test_2D_vectorized(fit_engine, with_errors): x = np.linspace(0, 5, 200) mm = AbsSin2D(0.3, 1.6) @@ -241,7 +242,7 @@ def test_2D_vectorized(fit_engine, with_errors): @pytest.mark.parametrize("with_errors", [False, True]) -@pytest.mark.parametrize("fit_engine", [None, "lmfit", "bumps", "dfo"]) +@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) def test_2D_non_vectorized(fit_engine, with_errors): x = np.linspace(0, 5, 200) mm = AbsSin2DL(0.3, 1.6) diff --git a/tests/integration_tests/Fitting/test_multi_fitter.py b/tests/integration_tests/Fitting/test_multi_fitter.py index 3ad1f4b5..66092dce 100644 --- a/tests/integration_tests/Fitting/test_multi_fitter.py +++ b/tests/integration_tests/Fitting/test_multi_fitter.py @@ -59,7 +59,7 @@ def __call__(self, x): @pytest.mark.parametrize("with_errors", [False, True]) -@pytest.mark.parametrize("fit_engine", [None, "lmfit", "bumps", "dfo"]) +@pytest.mark.parametrize("fit_engine", [None, "LMFit", "Bumps", "DFO"]) def test_multi_fit(fit_engine, with_errors): ref_sin_1 = AbsSin(0.2, np.pi) sp_sin_1 = AbsSin(0.354, 3.05) @@ -120,7 +120,7 @@ def test_multi_fit(fit_engine, with_errors): @pytest.mark.parametrize("with_errors", [False, True]) -@pytest.mark.parametrize("fit_engine", [None, "lmfit", "bumps", "dfo"]) +@pytest.mark.parametrize("fit_engine", [None, "LMFit", "Bumps", "DFO"]) def test_multi_fit2(fit_engine, with_errors): ref_sin_1 = AbsSin(0.2, np.pi) sp_sin_1 = AbsSin(0.354, 3.05) @@ -198,7 +198,7 @@ def test_multi_fit2(fit_engine, with_errors): @pytest.mark.parametrize("with_errors", [False, True]) -@pytest.mark.parametrize("fit_engine", [None, "lmfit", "bumps", "dfo"]) +@pytest.mark.parametrize("fit_engine", [None, "LMFit", "Bumps", "DFO"]) def test_multi_fit_1D_2D(fit_engine, with_errors): # Generate fit and reference objects ref_sin1D = AbsSin(0.2, np.pi) diff --git a/tests/integration_tests/Fitting/test_multi_fitter_legacy_parameter.py b/tests/integration_tests/Fitting/test_multi_fitter_legacy_parameter.py index 1d6af1ff..ed869302 100644 --- a/tests/integration_tests/Fitting/test_multi_fitter_legacy_parameter.py +++ b/tests/integration_tests/Fitting/test_multi_fitter_legacy_parameter.py @@ -59,7 +59,7 @@ def __call__(self, x): @pytest.mark.parametrize("with_errors", [False, True]) -@pytest.mark.parametrize("fit_engine", [None, "lmfit", "bumps", "dfo"]) +@pytest.mark.parametrize("fit_engine", [None, "LMFit", "Bumps", "DFO"]) def test_multi_fit(fit_engine, with_errors): ref_sin_1 = AbsSin(0.2, np.pi) sp_sin_1 = AbsSin(0.354, 3.05) @@ -120,7 +120,7 @@ def test_multi_fit(fit_engine, with_errors): @pytest.mark.parametrize("with_errors", [False, True]) -@pytest.mark.parametrize("fit_engine", [None, "lmfit", "bumps", "dfo"]) +@pytest.mark.parametrize("fit_engine", [None, "LMFit", "Bumps", "DFO"]) def test_multi_fit2(fit_engine, with_errors): ref_sin_1 = AbsSin(0.2, np.pi) sp_sin_1 = AbsSin(0.354, 3.05) @@ -198,7 +198,7 @@ def test_multi_fit2(fit_engine, with_errors): @pytest.mark.parametrize("with_errors", [False, True]) -@pytest.mark.parametrize("fit_engine", [None, "lmfit", "bumps", "dfo"]) +@pytest.mark.parametrize("fit_engine", [None, "LMFit", "Bumps", "DFO"]) def test_multi_fit_1D_2D(fit_engine, with_errors): # Generate fit and reference objects ref_sin1D = AbsSin(0.2, np.pi) diff --git a/tests/unit_tests/Fitting/minimizers/test_factory.py b/tests/unit_tests/Fitting/minimizers/test_factory.py index 64754925..9dfccc94 100644 --- a/tests/unit_tests/Fitting/minimizers/test_factory.py +++ b/tests/unit_tests/Fitting/minimizers/test_factory.py @@ -31,17 +31,17 @@ def test_factory_dfo_fit(self, minimizer_method, minimizer_enum): assert minimizer.wrapping == 'dfo' -@pytest.mark.parametrize('minimizer_name,expected', [('lmfit', AvailableMinimizers.LMFit), ('lmfit-leastsq', AvailableMinimizers.LMFit_leastsq), ('lmfit-powell', AvailableMinimizers.LMFit_powell), ('lmfit-cobyla', AvailableMinimizers.LMFit_cobyla), ]) +@pytest.mark.parametrize('minimizer_name,expected', [('LMFit', AvailableMinimizers.LMFit), ('LMFit_leastsq', AvailableMinimizers.LMFit_leastsq), ('LMFit_powell', AvailableMinimizers.LMFit_powell), ('LMFit_cobyla', AvailableMinimizers.LMFit_cobyla), ]) def test_from_string_to_enum_lmfit(minimizer_name, expected): assert from_string_to_enum(minimizer_name) == expected -@pytest.mark.parametrize('minimizer_name,expected', [('bumps', AvailableMinimizers.Bumps), ('bumps-simplex', AvailableMinimizers.Bumps_simplex), ('bumps-newton', AvailableMinimizers.Bumps_newton), ('bumps-lm', AvailableMinimizers.Bumps_lm)]) +@pytest.mark.parametrize('minimizer_name,expected', [('Bumps', AvailableMinimizers.Bumps), ('Bumps_simplex', AvailableMinimizers.Bumps_simplex), ('Bumps_newton', AvailableMinimizers.Bumps_newton), ('Bumps_lm', AvailableMinimizers.Bumps_lm)]) def test_from_string_to_enum_bumps(minimizer_name, expected): assert from_string_to_enum(minimizer_name) == expected -@pytest.mark.parametrize('minimizer_name,expected', [('dfo', AvailableMinimizers.DFO), ('dfo-leastsq', AvailableMinimizers.DFO_leastsq)]) +@pytest.mark.parametrize('minimizer_name,expected', [('DFO', AvailableMinimizers.DFO), ('DFO_leastsq', AvailableMinimizers.DFO_leastsq)]) def test_from_string_to_enum_dfo(minimizer_name, expected): assert from_string_to_enum(minimizer_name) == expected diff --git a/tests/unit_tests/Fitting/minimizers/test_minimizer_bumps.py b/tests/unit_tests/Fitting/minimizers/test_minimizer_bumps.py new file mode 100644 index 00000000..50b460d8 --- /dev/null +++ b/tests/unit_tests/Fitting/minimizers/test_minimizer_bumps.py @@ -0,0 +1,194 @@ +import pytest + +from unittest.mock import MagicMock +import numpy as np + +import easyscience.fitting.minimizers.minimizer_bumps +from easyscience.Objects.new_variable import Parameter + +from easyscience.fitting.minimizers.minimizer_bumps import Bumps +from easyscience.fitting.minimizers.utils import FitError + + +class TestBumpsFit(): + @pytest.fixture + def minimizer(self) -> Bumps: + minimizer = Bumps( + obj='obj', + fit_function='fit_function', + method='scipy.leastsq' + ) + return minimizer + + def test_init(self, minimizer: Bumps) -> None: + assert minimizer._p_0 == {} + assert minimizer._cached_pars_order == () + assert minimizer.wrapping == 'bumps' + + def test_init_exception(self) -> None: + with pytest.raises(FitError): + Bumps( + obj='obj', + fit_function='fit_function', + method='not_leastsq' + ) + + def test_available_methods(self, minimizer: Bumps) -> None: + # When Then Expect + assert minimizer.available_methods() == ['amoeba', 'de', 'dream', 'newton', 'scipy.leastsq', 'lm'] + + def test_fit(self, minimizer: Bumps, monkeypatch) -> None: + # When + from easyscience import global_object + global_object.stack.enabled = False + + mock_bumps_fit = MagicMock(return_value='fit') + monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_bumps, "bumps_fit", mock_bumps_fit) + + mock_FitProblem = MagicMock(return_value='fit_problem') + monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_bumps, "FitProblem", mock_FitProblem) + + mock_model = MagicMock() + mock_model_function = MagicMock(return_value=mock_model) + minimizer._make_model = MagicMock(return_value=mock_model_function) + minimizer._set_parameter_fit_result = MagicMock() + minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') + + cached_par = MagicMock() + cached_par.raw_value = 1 + cached_pars = {'mock_parm_1': cached_par} + minimizer._cached_pars = cached_pars + + # Then + result = minimizer.fit(x=1.0, y=2.0) + + # Expect + assert result == 'gen_fit_results' + mock_bumps_fit.assert_called_once_with('fit_problem', method='scipy.leastsq') + minimizer._make_model.assert_called_once_with(parameters=None) + minimizer._set_parameter_fit_result.assert_called_once_with('fit', False) + minimizer._gen_fit_results.assert_called_once_with('fit') + mock_model_function.assert_called_once_with(1.0, 2.0, 1.4142135623730951) + mock_FitProblem.assert_called_once_with(mock_model) + + def test_generate_fit_function(self, minimizer: Bumps) -> None: + # When + minimizer._original_fit_function = MagicMock(return_value='fit_function_result') + + mock_fit_constraint = MagicMock() + minimizer.fit_constraints = MagicMock(return_value=[mock_fit_constraint]) + + minimizer._object = MagicMock() + mock_parm_1 = MagicMock(Parameter) + mock_parm_1.unique_name = 'mock_parm_1' + mock_parm_1.value = 1.0 + mock_parm_1.error = 0.1 + mock_parm_2 = MagicMock(Parameter) + mock_parm_2.unique_name = 'mock_parm_2' + mock_parm_2.value = 2.0 + mock_parm_2.error = 0.2 + minimizer._object.get_fit_parameters = MagicMock(return_value=[mock_parm_1, mock_parm_2]) + + # Then + fit_function = minimizer._generate_fit_function() + fit_function_result = fit_function([10.0]) + + # Expect + assert 'fit_function_result' == fit_function_result + mock_fit_constraint.assert_called_once_with() + minimizer._original_fit_function.assert_called_once_with([10.0]) + assert minimizer._cached_pars['mock_parm_1'] == mock_parm_1 + assert minimizer._cached_pars['mock_parm_2'] == mock_parm_2 + assert minimizer._cached_pars_order == ('mock_parm_1', 'mock_parm_2') + assert str(fit_function.__signature__) == '(x, pmock_parm_1=2.0, pmock_parm_2=2.0)' + + def test_make_model(self, minimizer: Bumps, monkeypatch) -> None: + # When + mock_fit_function = MagicMock(return_value=np.array([11, 22])) + minimizer._generate_fit_function = MagicMock(return_value=mock_fit_function) + + mock_parm_1 = MagicMock() + mock_parm_1.unique_name = 'mock_parm_1' + minimizer.convert_to_par_object = MagicMock(return_value='converted_parm_1') + + mock_Curve = MagicMock(return_value='curve') + monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_bumps, "Curve", mock_Curve) + + # Then + model = minimizer._make_model(parameters=[mock_parm_1]) + curve_for_model = model(x=np.array([1, 2]), y=np.array([10, 20]), weights=np.array([100, 200])) + + # Expect + minimizer._generate_fit_function.assert_called_once_with() + assert mock_Curve.call_args[0][0] == mock_fit_function + assert all(mock_Curve.call_args[0][1] == np.array([1,2])) + assert all(mock_Curve.call_args[0][2] == np.array([10,20])) + assert curve_for_model == 'curve' + + def test_set_parameter_fit_result_no_stack_status(self, minimizer: Bumps): + # When + minimizer._cached_pars = { + 'a': MagicMock(), + 'b': MagicMock(), + } + minimizer._cached_pars['a'].value = 'a' + minimizer._cached_pars['b'].value = 'b' + + mock_cached_model = MagicMock() + mock_cached_model._pnames = ['pa', 'pb'] + minimizer._cached_model = mock_cached_model + + mock_fit_result = MagicMock() + mock_fit_result.x = [1.0, 2.0] + mock_fit_result.dx = [0.1, 0.2] + + # Then + minimizer._set_parameter_fit_result(mock_fit_result, False) + + # Expect + assert minimizer._cached_pars['a'].value == 1.0 + assert minimizer._cached_pars['a'].error == 0.1 + assert minimizer._cached_pars['b'].value == 2.0 + assert minimizer._cached_pars['b'].error == 0.2 + + def test_gen_fit_results(self, minimizer: Bumps, monkeypatch): + # When + mock_domain_fit_results = MagicMock() + mock_FitResults = MagicMock(return_value=mock_domain_fit_results) + monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_bumps, "FitResults", mock_FitResults) + + mock_fit_result = MagicMock() + mock_fit_result.success = True + + mock_cached_model = MagicMock() + mock_cached_model.x = 'x' + mock_cached_model.y = 'y' + mock_cached_model.dy = 'dy' + mock_cached_model._pnames = ['ppar_1', 'ppar_2'] + minimizer._cached_model = mock_cached_model + + mock_cached_par_1 = MagicMock() + mock_cached_par_1.raw_value = 'par_raw_value_1' + mock_cached_par_2 = MagicMock() + mock_cached_par_2.raw_value = 'par_raw_value_2' + minimizer._cached_pars = {'par_1': mock_cached_par_1, 'par_2': mock_cached_par_2} + + minimizer._p_0 = 'p_0' + minimizer.evaluate = MagicMock(return_value='evaluate') + + # Then + domain_fit_results = minimizer._gen_fit_results(mock_fit_result, **{'kwargs_set_key': 'kwargs_set_val'}) + + # Expect + assert domain_fit_results == mock_domain_fit_results + assert domain_fit_results.kwargs_set_key == 'kwargs_set_val' + assert domain_fit_results.success == True + assert domain_fit_results.y_obs == 'y' + assert domain_fit_results.x == 'x' + assert domain_fit_results.p == {'ppar_1': 'par_raw_value_1', 'ppar_2': 'par_raw_value_2'} + assert domain_fit_results.p0 == 'p_0' + assert domain_fit_results.y_calc == 'evaluate' + assert domain_fit_results.y_err == 'dy' + assert str(domain_fit_results.minimizer_engine) == "" + assert domain_fit_results.fit_args is None + minimizer.evaluate.assert_called_once_with('x', minimizer_parameters={'ppar_1': 'par_raw_value_1', 'ppar_2': 'par_raw_value_2'}) diff --git a/tests/unit_tests/Fitting/minimizers/test_minimizer_dfo.py b/tests/unit_tests/Fitting/minimizers/test_minimizer_dfo.py index 2b5940ea..676d9e8a 100644 --- a/tests/unit_tests/Fitting/minimizers/test_minimizer_dfo.py +++ b/tests/unit_tests/Fitting/minimizers/test_minimizer_dfo.py @@ -112,8 +112,8 @@ def test_make_model(self, minimizer: DFO) -> None: # Expect minimizer._generate_fit_function.assert_called_once_with() assert all(np.array([-0.01, -0.01]) == residuals_for_model(np.array([1111, 2222]))) - assert all(mock_fit_function.call_args_list[0][0][0] == np.array([1, 2])) - assert mock_fit_function.call_args_list[0][1] == {'pmock_parm_1': 1111, 'pmock_parm_2': 2222} + assert all(mock_fit_function.call_args[0][0] == np.array([1, 2])) + assert mock_fit_function.call_args[1] == {'pmock_parm_1': 1111, 'pmock_parm_2': 2222} def test_set_parameter_fit_result_no_stack_status(self, minimizer: DFO): # When @@ -179,6 +179,7 @@ def test_gen_fit_results(self, minimizer: DFO, monkeypatch): assert domain_fit_results.y_err == 'weights' assert str(domain_fit_results.minimizer_engine) == "" assert domain_fit_results.fit_args is None + minimizer.evaluate.assert_called_once_with('x', minimizer_parameters={'ppar_1': 'par_raw_value_1', 'ppar_2': 'par_raw_value_2'}) def test_dfo_fit(self, minimizer: DFO, monkeypatch): # When @@ -206,12 +207,12 @@ def test_dfo_fit(self, minimizer: DFO, monkeypatch): # Expect assert results == mock_results - assert mock_dfols.solve.call_args_list[0][0][0] == 'model' - assert all(mock_dfols.solve.call_args_list[0][0][1] == np.array([1., 2.])) - assert all(mock_dfols.solve.call_args_list[0][1]['bounds'][0] == np.array([0.1, 0.2])) - assert all(mock_dfols.solve.call_args_list[0][1]['bounds'][1] == np.array([10., 20.])) - assert mock_dfols.solve.call_args_list[0][1]['scaling_within_bounds'] is True - assert mock_dfols.solve.call_args_list[0][1]['kwargs_set_key'] == 'kwargs_set_val' + assert mock_dfols.solve.call_args[0][0] == 'model' + assert all(mock_dfols.solve.call_args[0][1] == np.array([1., 2.])) + assert all(mock_dfols.solve.call_args[1]['bounds'][0] == np.array([0.1, 0.2])) + assert all(mock_dfols.solve.call_args[1]['bounds'][1] == np.array([10., 20.])) + assert mock_dfols.solve.call_args[1]['scaling_within_bounds'] is True + assert mock_dfols.solve.call_args[1]['kwargs_set_key'] == 'kwargs_set_val' def test_dfo_fit_no_scaling(self, minimizer: DFO, monkeypatch): # When @@ -239,13 +240,13 @@ def test_dfo_fit_no_scaling(self, minimizer: DFO, monkeypatch): # Expect assert results == mock_results - assert mock_dfols.solve.call_args_list[0][0][0] == 'model' - assert all(mock_dfols.solve.call_args_list[0][0][1] == np.array([1., 2.])) - assert all(mock_dfols.solve.call_args_list[0][1]['bounds'][0] == np.array([-np.inf, 0.2])) - assert all(mock_dfols.solve.call_args_list[0][1]['bounds'][1] == np.array([10., 20.])) - assert not 'scaling_within_bounds' in list(mock_dfols.solve.call_args_list[0][1].keys()) - assert 'kwargs_set_key' in list(mock_dfols.solve.call_args_list[0][1].keys()) - assert mock_dfols.solve.call_args_list[0][1]['kwargs_set_key'] == 'kwargs_set_val' + assert mock_dfols.solve.call_args[0][0] == 'model' + assert all(mock_dfols.solve.call_args[0][1] == np.array([1., 2.])) + assert all(mock_dfols.solve.call_args[1]['bounds'][0] == np.array([-np.inf, 0.2])) + assert all(mock_dfols.solve.call_args[1]['bounds'][1] == np.array([10., 20.])) + assert not 'scaling_within_bounds' in list(mock_dfols.solve.call_args[1].keys()) + assert 'kwargs_set_key' in list(mock_dfols.solve.call_args[1].keys()) + assert mock_dfols.solve.call_args[1]['kwargs_set_key'] == 'kwargs_set_val' def test_dfo_fit_exception(self, minimizer: DFO, monkeypatch): # When diff --git a/tests/unit_tests/Fitting/test_fitter.py b/tests/unit_tests/Fitting/test_fitter.py index 4669400b..e3974e99 100644 --- a/tests/unit_tests/Fitting/test_fitter.py +++ b/tests/unit_tests/Fitting/test_fitter.py @@ -2,8 +2,9 @@ import pytest import numpy as np -from easyscience.fitting.fitter import Fitter import easyscience.fitting.fitter +from easyscience.fitting.fitter import Fitter +from easyscience.fitting.minimizers.factory import AvailableMinimizers class TestFitter(): @@ -19,8 +20,8 @@ def test_constructor(self, fitter: Fitter): assert fitter._fit_object == self.mock_fit_object assert fitter._fit_function == self.mock_fit_function assert fitter._dependent_dims is None - assert fitter._name_current_minimizer == 'lmfit-leastsq' - fitter._update_minimizer.assert_called_once_with('lmfit-leastsq') + assert fitter._enum_current_minimizer == AvailableMinimizers.LMFit_leastsq + fitter._update_minimizer.assert_called_once_with(AvailableMinimizers.LMFit_leastsq) def test_fit_constraints(self, fitter: Fitter): # When @@ -110,22 +111,27 @@ def test_initialize(self, fitter: Fitter): assert fitter._fit_function == mock_fit_function fitter._update_minimizer.count(2) - def test_create(self, fitter: Fitter): + def test_create(self, fitter: Fitter, monkeypatch): # When fitter._update_minimizer = MagicMock() + mock_string_to_enum = MagicMock(return_value=10) + monkeypatch.setattr(easyscience.fitting.fitter, 'from_string_to_enum', mock_string_to_enum) # Then fitter.create('great-minimizer') # Expect - fitter._update_minimizer.assert_called_once_with('great-minimizer') + mock_string_to_enum.assert_called_once_with('great-minimizer') + fitter._update_minimizer.assert_called_once_with(10) - def test_switch_minimizer(self, fitter: Fitter): + def test_switch_minimizer(self, fitter: Fitter, monkeypatch): # When mock_minimizer = MagicMock() mock_minimizer.fit_constraints = MagicMock(return_value='constraints') mock_minimizer.set_fit_constraint = MagicMock() fitter._minimizer = mock_minimizer + mock_string_to_enum = MagicMock(return_value=10) + monkeypatch.setattr(easyscience.fitting.fitter, 'from_string_to_enum', mock_string_to_enum) # Then fitter.switch_minimizer('great-minimizer') @@ -134,6 +140,7 @@ def test_switch_minimizer(self, fitter: Fitter): fitter._update_minimizer.count(2) mock_minimizer.set_fit_constraint.assert_called_once_with('constraints') mock_minimizer.fit_constraints.assert_called_once() + mock_string_to_enum.assert_called_once_with('great-minimizer') def test_update_minimizer(self, monkeypatch): # When @@ -150,7 +157,7 @@ def test_update_minimizer(self, monkeypatch): fitter._update_minimizer('great-minimizer') # Expect - assert fitter._name_current_minimizer == 'great-minimizer' + assert fitter._enum_current_minimizer == 'great-minimizer' assert fitter._minimizer == 'minimizer' def test_available_minimizers(self, fitter: Fitter): @@ -183,7 +190,7 @@ def test_fit_function(self, fitter: Fitter): def test_set_fit_function(self, fitter: Fitter): # When - fitter._name_current_minimizer = 'current_minimizer' + fitter._enum_current_minimizer = 'current_minimizer' # Then fitter.fit_function = 'new-fit-function' @@ -201,7 +208,7 @@ def test_fit_object(self, fitter: Fitter): def test_set_fit_object(self, fitter: Fitter): # When - fitter._name_current_minimizer = 'current_minimizer' + fitter._enum_current_minimizer = 'current_minimizer' # Then fitter.fit_object = 'new-fit-object' diff --git a/tests/unit_tests/global_object/test_undo_redo.py b/tests/unit_tests/global_object/test_undo_redo.py index a31f061e..fb067d10 100644 --- a/tests/unit_tests/global_object/test_undo_redo.py +++ b/tests/unit_tests/global_object/test_undo_redo.py @@ -230,7 +230,7 @@ def test_UndoRedoMacros(): assert item.raw_value == old_value + offset -@pytest.mark.parametrize("fit_engine", ["lmfit", "bumps", "dfo"]) +@pytest.mark.parametrize("fit_engine", ["LMFit", "Bumps", "DFO"]) def test_fittingUndoRedo(fit_engine): m_value = 6 c_value = 2