diff --git a/src/easyscience/fitting/__init__.py b/src/easyscience/fitting/__init__.py index 1018c0c7..f34b8b81 100644 --- a/src/easyscience/fitting/__init__.py +++ b/src/easyscience/fitting/__init__.py @@ -1,7 +1,8 @@ +from .available_minimizers import AvailableMinimizers from .fitter import Fitter from .minimizers.utils import FitResults # Causes circular import # from .multi_fitter import MultiFitter # noqa: F401, E402 -all = [Fitter, FitResults] +all = [AvailableMinimizers, Fitter, FitResults] diff --git a/src/easyscience/fitting/available_minimizers.py b/src/easyscience/fitting/available_minimizers.py new file mode 100644 index 00000000..c432de85 --- /dev/null +++ b/src/easyscience/fitting/available_minimizers.py @@ -0,0 +1,85 @@ +import warnings +from enum import Enum +from enum import auto + +import pkg_resources + +installed_packages = {pkg.key for pkg in pkg_resources.working_set} + +# Change to importlib.metadata when Python 3.10 is the minimum version +# import importlib.metadata +# installed_packages = [x.name for x in importlib.metadata.distributions()] + +lmfit_engine_available = False +if 'lmfit' in installed_packages: + lmfit_engine_available = True +else: + # TODO make this a proper message (use logging?) + warnings.warn('LMFit minimization is not available. Probably lmfit has not been installed.', ImportWarning, stacklevel=2) + +bumps_engine_available = False +if 'bumps' in installed_packages: + bumps_engine_available = True +else: + # TODO make this a proper message (use logging?) + warnings.warn('Bumps minimization is not available. Probably bumps has not been installed.', ImportWarning, stacklevel=2) + +dfo_engine_available = False +if 'dfo-ls' in installed_packages: + dfo_engine_available = True +else: + # TODO make this a proper message (use logging?) + warnings.warn('DFO minimization is not available. Probably dfols has not been installed.', ImportWarning, stacklevel=2) + + +class AvailableMinimizers(Enum): + if lmfit_engine_available: + LMFit = auto() + LMFit_leastsq = auto() + LMFit_powell = auto() + LMFit_cobyla = auto() + LMFit_differential_evolution = auto() + + if bumps_engine_available: + Bumps = auto() + Bumps_simplex = auto() + Bumps_newton = auto() + Bumps_lm = auto() + + if dfo_engine_available: + 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': + minmizer_enum = AvailableMinimizers.LMFit + elif minimizer_name == 'LMFit_leastsq': + minmizer_enum = AvailableMinimizers.LMFit_leastsq + elif minimizer_name == 'LMFit_powell': + minmizer_enum = AvailableMinimizers.LMFit_powell + elif minimizer_name == 'LMFit_cobyla': + minmizer_enum = AvailableMinimizers.LMFit_cobyla + elif minimizer_name == 'LMFit_differential_evolution': + minmizer_enum = AvailableMinimizers.LMFit_differential_evolution + + elif minimizer_name == 'Bumps': + minmizer_enum = AvailableMinimizers.Bumps + elif minimizer_name == 'Bumps_simplex': + minmizer_enum = AvailableMinimizers.Bumps_simplex + elif minimizer_name == 'Bumps_newton': + minmizer_enum = AvailableMinimizers.Bumps_newton + elif minimizer_name == 'Bumps_lm': + minmizer_enum = AvailableMinimizers.Bumps_lm + + elif minimizer_name == 'DFO': + minmizer_enum = AvailableMinimizers.DFO + 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/fitter.py b/src/easyscience/fitting/fitter.py index 7af26cf4..3d96fa75 100644 --- a/src/easyscience/fitting/fitter.py +++ b/src/easyscience/fitting/fitter.py @@ -9,11 +9,11 @@ import numpy as np +from .available_minimizers import AvailableMinimizers +from .available_minimizers import from_string_to_enum from .minimizers import FitResults from .minimizers import MinimizerBase -from .minimizers.factory import AvailableMinimizers from .minimizers.factory import factory -from .minimizers.factory import from_string_to_enum DEFAULT_MINIMIZER = AvailableMinimizers.LMFit_leastsq diff --git a/src/easyscience/fitting/minimizers/factory.py b/src/easyscience/fitting/minimizers/factory.py index 503cc729..026a6c9c 100644 --- a/src/easyscience/fitting/minimizers/factory.py +++ b/src/easyscience/fitting/minimizers/factory.py @@ -1,83 +1,15 @@ -import warnings -from enum import Enum -from enum import auto from typing import Callable +from .. import available_minimizers +from ..available_minimizers import AvailableMinimizers from .minimizer_base import MinimizerBase -lmfit_engine_imported = False -try: +if available_minimizers.lmfit_engine_available: from .minimizer_lmfit import LMFit - - lmfit_engine_imported = True -except ImportError: - # TODO make this a proper message (use logging?) - warnings.warn('LMFit minimization is not available. Probably lmfit has not been installed.', ImportWarning, stacklevel=2) - -bumps_engine_imported = False -try: - from .minimizer_bumps import Bumps - - bumps_engine_imported = True -except ImportError: - # TODO make this a proper message (use logging?) - warnings.warn('Bumps minimization is not available. Probably bumps has not been installed.', ImportWarning, stacklevel=2) - -dfo_engine_imported = False -try: +if available_minimizers.dfo_engine_available: from .minimizer_dfo import DFO - - dfo_engine_imported = True -except ImportError: - # TODO make this a proper message (use logging?) - warnings.warn('DFO minimization is not available. Probably dfols has not been installed.', ImportWarning, stacklevel=2) - - -class AvailableMinimizers(Enum): - if lmfit_engine_imported: - LMFit = auto() - LMFit_leastsq = auto() - LMFit_powell = auto() - LMFit_cobyla = auto() - - if bumps_engine_imported: - Bumps = auto() - Bumps_simplex = auto() - Bumps_newton = auto() - Bumps_lm = auto() - - if dfo_engine_imported: - 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': - minmizer_enum = AvailableMinimizers.LMFit - elif minimizer_name == 'LMFit_leastsq': - minmizer_enum = AvailableMinimizers.LMFit_leastsq - elif minimizer_name == 'LMFit_powell': - minmizer_enum = AvailableMinimizers.LMFit_powell - elif minimizer_name == 'LMFit_cobyla': - minmizer_enum = AvailableMinimizers.LMFit_cobyla - - elif minimizer_name == 'Bumps': - minmizer_enum = AvailableMinimizers.Bumps - elif minimizer_name == 'Bumps_simplex': - minmizer_enum = AvailableMinimizers.Bumps_simplex - elif minimizer_name == 'Bumps_newton': - minmizer_enum = AvailableMinimizers.Bumps_newton - elif minimizer_name == 'Bumps_lm': - minmizer_enum = AvailableMinimizers.Bumps_lm - - elif minimizer_name == 'DFO': - minmizer_enum = AvailableMinimizers.DFO - 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 +if available_minimizers.bumps_engine_available: + from .minimizer_bumps import Bumps def factory(minimizer_enum: AvailableMinimizers, fit_object, fit_function: Callable) -> MinimizerBase: @@ -89,6 +21,8 @@ def factory(minimizer_enum: AvailableMinimizers, fit_object, fit_function: Calla minimizer = LMFit(obj=fit_object, fit_function=fit_function, method='powell') elif minimizer_enum == AvailableMinimizers.LMFit_cobyla: minimizer = LMFit(obj=fit_object, fit_function=fit_function, method='cobyla') + elif minimizer_enum == AvailableMinimizers.LMFit_differential_evolution: + minimizer = LMFit(obj=fit_object, fit_function=fit_function, method='differential_evolution') elif minimizer_enum == AvailableMinimizers.Bumps: minimizer = Bumps(obj=fit_object, fit_function=fit_function, method='amoeba') diff --git a/src/easyscience/fitting/minimizers/minimizer_base.py b/src/easyscience/fitting/minimizers/minimizer_base.py index 59ef4133..124c377b 100644 --- a/src/easyscience/fitting/minimizers/minimizer_base.py +++ b/src/easyscience/fitting/minimizers/minimizer_base.py @@ -16,8 +16,8 @@ import numpy as np -#causes circular import when Parameter is imported -#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 ..Constraints import ObjConstraint @@ -95,7 +95,7 @@ def fit( :return: Fit results """ - def evaluate(self, x: np.ndarray, minimizer_parameters: dict[str, float] = None, **kwargs) -> np.ndarray: + def evaluate(self, x: np.ndarray, minimizer_parameters: Optional[dict[str, float]] = None, **kwargs) -> np.ndarray: """ Evaluate the fit function for values of x. Parameters used are either the latest or user supplied. If the parameters are user supplied, it must be in a dictionary of {'parameter_name': parameter_value,...}. @@ -122,6 +122,17 @@ def evaluate(self, x: np.ndarray, minimizer_parameters: dict[str, float] = None, return self._fit_function(x, **minimizer_parameters, **kwargs) + def _get_method_dict(self, passed_method: Optional[str] = None) -> dict[str, str]: + if passed_method is not None: + if passed_method not in self.supported_methods(): + raise FitError(f'Method {passed_method} not available in {self.__class__}') + return {'method': passed_method} + + if self._method is not None: + return {'method': self._method} + + return {} + @abstractmethod def convert_to_pars_obj(self, par_list: Optional[Union[list]] = None): """ diff --git a/src/easyscience/fitting/minimizers/minimizer_bumps.py b/src/easyscience/fitting/minimizers/minimizer_bumps.py index 1840dddc..f571d717 100644 --- a/src/easyscience/fitting/minimizers/minimizer_bumps.py +++ b/src/easyscience/fitting/minimizers/minimizer_bumps.py @@ -62,7 +62,7 @@ def all_methods() -> List[str]: @staticmethod def supported_methods() -> List[str]: # only a small subset - methods = ['scipy.leastsq','amoeba', 'newton', 'lm'] + methods = ['scipy.leastsq', 'amoeba', 'newton', 'lm'] return methods def fit( @@ -96,11 +96,7 @@ def fit( :return: Fit results :rtype: ModelResult """ - default_method = {} - if self._method is not None: - default_method = {'method': self._method} - if method is not None and method in self.supported_methods(): - default_method['method'] = method + method_dict = self._get_method_dict(method) if weights is None: weights = np.sqrt(np.abs(y)) @@ -135,7 +131,7 @@ def fit( global_object.stack.enabled = False try: - model_results = bumps_fit(problem, **default_method, **minimizer_kwargs, **kwargs) + model_results = bumps_fit(problem, **method_dict, **minimizer_kwargs, **kwargs) self._set_parameter_fit_result(model_results, stack_status) results = self._gen_fit_results(model_results) except Exception as e: diff --git a/src/easyscience/fitting/minimizers/minimizer_dfo.py b/src/easyscience/fitting/minimizers/minimizer_dfo.py index fa65e481..042674ff 100644 --- a/src/easyscience/fitting/minimizers/minimizer_dfo.py +++ b/src/easyscience/fitting/minimizers/minimizer_dfo.py @@ -87,12 +87,6 @@ def fit( :return: Fit results :rtype: ModelResult """ - default_method = {} - if self._method is not None: - default_method = {'method': self._method} - if method is not None and method in self.supported_methods(): - default_method['method'] = method - if weights is None: weights = np.sqrt(np.abs(y)) diff --git a/src/easyscience/fitting/minimizers/minimizer_lmfit.py b/src/easyscience/fitting/minimizers/minimizer_lmfit.py index 59164047..514a9c23 100644 --- a/src/easyscience/fitting/minimizers/minimizer_lmfit.py +++ b/src/easyscience/fitting/minimizers/minimizer_lmfit.py @@ -71,6 +71,7 @@ def supported_methods() -> List[str]: return [ 'least_squares', 'leastsq', + 'differential_evolution', 'powell', 'cobyla', ] @@ -108,14 +109,7 @@ def fit( :return: Fit results :rtype: ModelResult """ - default_method = {} - if self._method is not None: - default_method = {'method': self._method} - if method is not None: - if method in self.supported_methods(): - default_method['method'] = method - else: - raise FitError(f'Method {method} not available in {self.__class__}') + method_dict = self._get_method_dict(method) if weights is None: weights = 1 / np.sqrt(np.abs(y)) @@ -139,7 +133,7 @@ def fit( if model is None: model = self._make_model() - model_results = model.fit(y, x=x, weights=weights, **default_method, **minimizer_kwargs, **kwargs) + model_results = model.fit(y, x=x, weights=weights, **method_dict, **minimizer_kwargs, **kwargs) self._set_parameter_fit_result(model_results, stack_status) results = self._gen_fit_results(model_results) except Exception as e: diff --git a/tests/integration_tests/Fitting/test_fitter.py b/tests/integration_tests/Fitting/test_fitter.py index 1dbcb6ed..757c3424 100644 --- a/tests/integration_tests/Fitting/test_fitter.py +++ b/tests/integration_tests/Fitting/test_fitter.py @@ -11,7 +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.fitting.available_minimizers import AvailableMinimizers from easyscience.Objects.ObjectClasses import BaseObj from easyscience.Objects.new_variable import Parameter diff --git a/tests/integration_tests/Fitting/test_fitter_legacy_parameter.py b/tests/integration_tests/Fitting/test_fitter_legacy_parameter.py index acafae5b..1f43d530 100644 --- a/tests/integration_tests/Fitting/test_fitter_legacy_parameter.py +++ b/tests/integration_tests/Fitting/test_fitter_legacy_parameter.py @@ -11,7 +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.fitting.available_minimizers import AvailableMinimizers from easyscience.Objects.ObjectClasses import BaseObj from easyscience.Objects.ObjectClasses import Parameter diff --git a/tests/unit_tests/Fitting/minimizers/test_factory.py b/tests/unit_tests/Fitting/minimizers/test_factory.py index 9dfccc94..8a0095a3 100644 --- a/tests/unit_tests/Fitting/minimizers/test_factory.py +++ b/tests/unit_tests/Fitting/minimizers/test_factory.py @@ -1,6 +1,6 @@ from easyscience.fitting.minimizers.factory import factory -from easyscience.fitting.minimizers.factory import from_string_to_enum -from easyscience.fitting.minimizers.factory import AvailableMinimizers +from easyscience.fitting.available_minimizers import from_string_to_enum +from easyscience.fitting.available_minimizers import AvailableMinimizers from easyscience.fitting.minimizers import MinimizerBase from unittest.mock import MagicMock import pytest @@ -51,10 +51,11 @@ def test_available_minimizers(): assert AvailableMinimizers.LMFit_leastsq assert AvailableMinimizers.LMFit_powell assert AvailableMinimizers.LMFit_cobyla + assert AvailableMinimizers.LMFit_differential_evolution assert AvailableMinimizers.Bumps assert AvailableMinimizers.Bumps_simplex assert AvailableMinimizers.Bumps_newton assert AvailableMinimizers.Bumps_lm assert AvailableMinimizers.DFO assert AvailableMinimizers.DFO_leastsq - assert len(AvailableMinimizers) == 10 \ No newline at end of file + assert len(AvailableMinimizers) == 11 \ No newline at end of file diff --git a/tests/unit_tests/Fitting/minimizers/test_minimizer_base.py b/tests/unit_tests/Fitting/minimizers/test_minimizer_base.py index 3f483dd5..3618489f 100644 --- a/tests/unit_tests/Fitting/minimizers/test_minimizer_base.py +++ b/tests/unit_tests/Fitting/minimizers/test_minimizer_base.py @@ -168,4 +168,40 @@ def test_create_signature(self, minimizer: MinimizerBase) -> None: InspectParameter('p2', InspectParameter.POSITIONAL_OR_KEYWORD, annotation=_empty, default=2.0) ] expected_signature = Signature(wrapped_parameters) - assert signature == expected_signature \ No newline at end of file + assert signature == expected_signature + + def test_get_method_dict(self, minimizer: MinimizerBase) -> None: + # When Then + result = minimizer._get_method_dict() + + # Expect + assert result == {'method': 'method'} + + def test_get_method_dict_no_self(self, minimizer: MinimizerBase) -> None: + # When + minimizer._method = None + + # Then + result = minimizer._get_method_dict() + + # Expect + assert result == {} + + def test_get_method_dict_supported_method(self, minimizer: MinimizerBase) -> None: + # When + minimizer.supported_methods = MagicMock(return_value=['supported_method']) + + # Then + result = minimizer._get_method_dict('supported_method') + + # Expect + assert result == {'method': 'supported_method'} + + def test_get_method_dict_not_supported_method(self, minimizer: MinimizerBase) -> None: + # When + minimizer.supported_methods = MagicMock(return_value=['supported_method']) + + # Then Expect + with pytest.raises(FitError): + result = minimizer._get_method_dict('not_supported_method') + diff --git a/tests/unit_tests/Fitting/minimizers/test_minimizer_lmfit.py b/tests/unit_tests/Fitting/minimizers/test_minimizer_lmfit.py index 0a6c8db9..cf0dc6cc 100644 --- a/tests/unit_tests/Fitting/minimizers/test_minimizer_lmfit.py +++ b/tests/unit_tests/Fitting/minimizers/test_minimizer_lmfit.py @@ -3,11 +3,10 @@ from unittest.mock import MagicMock import easyscience.fitting.minimizers.minimizer_lmfit - +from easyscience.fitting import AvailableMinimizers from easyscience.fitting.minimizers.minimizer_lmfit import LMFit from easyscience.Objects.new_variable import Parameter from lmfit import Parameter as LMParameter -from easyscience.Objects.ObjectClasses import BaseObj from easyscience.fitting.minimizers.utils import FitError diff --git a/tests/unit_tests/Fitting/test_fitter.py b/tests/unit_tests/Fitting/test_fitter.py index e3974e99..2095fb99 100644 --- a/tests/unit_tests/Fitting/test_fitter.py +++ b/tests/unit_tests/Fitting/test_fitter.py @@ -4,7 +4,7 @@ import numpy as np import easyscience.fitting.fitter from easyscience.fitting.fitter import Fitter -from easyscience.fitting.minimizers.factory import AvailableMinimizers +from easyscience.fitting.available_minimizers import AvailableMinimizers class TestFitter(): @@ -166,7 +166,7 @@ def test_available_minimizers(self, fitter: Fitter): # Then Expect assert minimizers == [ - 'LMFit', 'LMFit_leastsq', 'LMFit_powell', 'LMFit_cobyla', + 'LMFit', 'LMFit_leastsq', 'LMFit_powell', 'LMFit_cobyla', 'LMFit_differential_evolution', 'Bumps', 'Bumps_simplex', 'Bumps_newton', 'Bumps_lm', 'DFO', 'DFO_leastsq' ]