From 9e3526b960599012d34651f085f2e122823b055b Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Fri, 7 Jun 2024 14:23:30 +0200 Subject: [PATCH 01/24] factory seems to be working --- src/easyscience/Fitting/fitter.py | 104 +++++++++++------- src/easyscience/Fitting/minimizers/factory.py | 30 +++++ 2 files changed, 96 insertions(+), 38 deletions(-) create mode 100644 src/easyscience/Fitting/minimizers/factory.py diff --git a/src/easyscience/Fitting/fitter.py b/src/easyscience/Fitting/fitter.py index dc950eec..4e71ec92 100644 --- a/src/easyscience/Fitting/fitter.py +++ b/src/easyscience/Fitting/fitter.py @@ -7,7 +7,8 @@ # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project list: + return self.__engine_obj.fit_constraints() + + def add_fit_constraint(self, constraint) -> None: + self.__engine_obj.add_fit_constraint(constraint) + + def remove_fit_constraint(self, index: int) -> None: + self.__engine_obj.remove_fit_constraint(index) + + def make_model(self, pars=None) -> Callable: + return self.__engine_obj.make_model(pars) + + def evaluate(self, pars=None) -> np.ndarray: + return self.__engine_obj.evaluate(pars) + + def convert_to_pars_obj(self, pars) -> object: + return self.__engine_obj.convert_to_pars_obj(pars) + + def available_methods(self) -> list: + return self.__engine_obj.available_methods() + def _fit_function_wrapper(self, real_x=None, flatten: bool = True) -> Callable: """ Simple fit function which injects the real X (independent) values into the @@ -107,12 +132,14 @@ def create(self, engine_name: str = default_fitting_engine): :param engine_name: The label of the optimization engine to create. :return: None """ - engines = self.available_engines - if engine_name in engines: - self._current_engine = self._engines[engines.index(engine_name)] - self._is_initialized = False - else: - raise AttributeError(f"The supplied optimizer engine '{engine_name}' is unknown.") + self._current_engine = minimizer_class_factory(from_string(engine_name)) + + # engines = self.available_engines + # if engine_name in engines: + # self._current_engine = self._engines[engines.index(engine_name)] + # self._is_initialized = False + # else: + # raise AttributeError(f"The supplied optimizer engine '{engine_name}' is unknown.") def switch_engine(self, engine_name: str): """ @@ -121,8 +148,8 @@ def switch_engine(self, engine_name: str): :return: None """ # There isn't any state to carry over - if not self._is_initialized: - raise ReferenceError('The fitting engine must be initialized before switching') + # if not self._is_initialized: + # raise ReferenceError('The fitting engine must be initialized before switching') # Constrains are not carried over. Do it manually. constraints = self.__engine_obj._constraints self.create(engine_name) @@ -137,9 +164,10 @@ def available_engines(self) -> List[str]: :return: List of available fitting engines :rtype: List[str] """ - if minimizers.engines is None: - raise ImportError('There are no available fitting engines. Install `lmfit` and/or `bumps`') - return [engine.name for engine in minimizers.engines] + # if minimizers.engines is None: + # raise ImportError('There are no available fitting engines. Install `lmfit` and/or `bumps`') + # return [engine.name for engine in minimizers.engines] + return [minimize.name for minimize in Minimizers] @property def can_fit(self) -> bool: @@ -207,23 +235,23 @@ def fit_object(self, fit_object): self._fit_object = fit_object self.__initialize() - def __pass_through_generator(self, name: str): - """ - Attach the attributes of the calculator template to the current fitter instance. - :param name: Attribute name to attach - :return: Wrapped calculator interface object. - """ - obj = self - - def inner(*args, **kwargs): - if not obj.can_fit: - raise ReferenceError('The fitting engine must first be initialized') - func = getattr(obj.engine, name, None) - if func is None: - raise ValueError('The fitting engine does not have the attribute "{}"'.format(name)) - return func(*args, **kwargs) - - return inner + # def __pass_through_generator(self, name: str): + # """ + # Attach the attributes of the calculator template to the current fitter instance. + # :param name: Attribute name to attach + # :return: Wrapped calculator interface object. + # """ + # obj = self + + # def inner(*args, **kwargs): + # if not obj.can_fit: + # raise ReferenceError('The fitting engine must first be initialized') + # func = getattr(obj.engine, name, None) + # if func is None: + # raise ValueError('The fitting engine does not have the attribute "{}"'.format(name)) + # return func(*args, **kwargs) + + # return inner @property def fit(self) -> Callable: diff --git a/src/easyscience/Fitting/minimizers/factory.py b/src/easyscience/Fitting/minimizers/factory.py new file mode 100644 index 00000000..0d3dec72 --- /dev/null +++ b/src/easyscience/Fitting/minimizers/factory.py @@ -0,0 +1,30 @@ +from enum import Enum + +from .minimizer_bumps import Bumps +from .minimizer_dfo import DFO +from .minimizer_lmfit import LMFit + + +class Minimizers(Enum): + Bumps = 1 + DFO = 2 + LMFit = 3 + + +def from_string(engine_name: str) -> Minimizers: + if engine_name == 'bumps': + engine_enum = Minimizers.Bumps + if engine_name == 'lmfit': + engine_enum = Minimizers.LMFit + if engine_name == 'dfo_ls': + engine_enum = Minimizers.DFO + return engine_enum + + +def minimizer_class_factory(minimizer: Minimizers): + if minimizer == Minimizers.Bumps: + return Bumps + elif minimizer == Minimizers.DFO: + return DFO + elif minimizer == Minimizers.LMFit: + return LMFit From 53129a061d46d403eb3ff85248941e3aee54b782 Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Fri, 7 Jun 2024 15:27:32 +0200 Subject: [PATCH 02/24] seems to be working --- src/easyscience/Fitting/fitter.py | 178 +++++++++++++++++------------- src/easyscience/__init__.py | 2 +- 2 files changed, 105 insertions(+), 75 deletions(-) diff --git a/src/easyscience/Fitting/fitter.py b/src/easyscience/Fitting/fitter.py index 4e71ec92..a3e63ec0 100644 --- a/src/easyscience/Fitting/fitter.py +++ b/src/easyscience/Fitting/fitter.py @@ -17,7 +17,7 @@ import numpy as np # import easyscience.Fitting.minimizers as minimizers -from easyscience import default_fitting_engine +from easyscience import DEFAULT_FITTING_ENGINE from .minimizers import FitResults from .minimizers import MinimizerBase @@ -34,56 +34,60 @@ class Fitter: Wrapper to the fitting engines """ - def __init__(self, fit_object=None, fit_function: Optional[Callable] = None): + def __init__(self, fit_object, fit_function: Callable): self._fit_object = fit_object self._fit_function = fit_function self._dependent_dims = None - can_initialize = False - # We can only proceed if both obj and func are not None - if (fit_object is not None) & (fit_function is not None): - can_initialize = True - else: - if (fit_object is not None) or (fit_function is not None): - raise AttributeError + # can_initialize = False + # # We can only proceed if both obj and func are not None + # if (fit_object is not None) & (fit_function is not None): + # can_initialize = True + # else: + # if (fit_object is not None) or (fit_function is not None): + # raise AttributeError # self._engines: List[_C] = minimizers.engines - self._current_engine: _C = None - self.__engine_obj: _M = None - self._is_initialized: bool = False - self.create() - - # fit_methods = [ - # x - # for x, y in MinimizerBase.__dict__.items() - # if (isinstance(y, FunctionType) and not x.startswith('_')) and x != 'fit' - # ] - # for method_name in fit_methods: - # setattr(self, method_name, self.__pass_through_generator(method_name)) - - if can_initialize: - self.__initialize() + self._minimizer: MinimizerBase # _minimizer is set in the create method + # self._initialize() + self.create(DEFAULT_FITTING_ENGINE) + + # self._current_engine: _C = None + # self.__engine_obj: _M = None + # self._is_initialized: bool = False + # self.create() + + # fit_methods = [ + # x + # for x, y in MinimizerBase.__dict__.items() + # if (isinstance(y, FunctionType) and not x.startswith('_')) and x != 'fit' + # ] + # for method_name in fit_methods: + # setattr(self, method_name, self.__pass_through_generator(method_name)) + + # if can_initialize: + # self.__initialize() def fit_constraints(self) -> list: - return self.__engine_obj.fit_constraints() + return self._minimizer.fit_constraints() def add_fit_constraint(self, constraint) -> None: - self.__engine_obj.add_fit_constraint(constraint) + self._minimizer.add_fit_constraint(constraint) def remove_fit_constraint(self, index: int) -> None: - self.__engine_obj.remove_fit_constraint(index) + self._minimizer.remove_fit_constraint(index) def make_model(self, pars=None) -> Callable: - return self.__engine_obj.make_model(pars) + return self._minimizer.make_model(pars) def evaluate(self, pars=None) -> np.ndarray: - return self.__engine_obj.evaluate(pars) + return self._minimizer.evaluate(pars) def convert_to_pars_obj(self, pars) -> object: - return self.__engine_obj.convert_to_pars_obj(pars) + return self._minimizer.convert_to_pars_obj(pars) def available_methods(self) -> list: - return self.__engine_obj.available_methods() + return self._minimizer.available_methods() def _fit_function_wrapper(self, real_x=None, flatten: bool = True) -> Callable: """ @@ -116,23 +120,31 @@ def initialize(self, fit_object, fit_function: Callable): """ self._fit_object = fit_object self._fit_function = fit_function - self.__initialize() + # self.__initialize() + # self._initialize() + self.create(DEFAULT_FITTING_ENGINE) - def __initialize(self): - """ - The real initialization. Setting the optimizer object properly - :return: None - """ - self.__engine_obj = self._current_engine(self._fit_object, self.fit_function) - self._is_initialized = True + # def _initialize(self): + # # def __initialize(self): + # """ + # The real initialization. Setting the optimizer object properly + # :return: None + # """ + # minimizer_class = minimizer_class_factory(from_string(DEFAULT_FITTING_ENGINE)) + # self._minimizer = minimizer_class(self._fit_object, self.fit_function) + + # self.__engine_obj = self._current_engine(self._fit_object, self.fit_function) + # self._is_initialized = True - def create(self, engine_name: str = default_fitting_engine): + def create(self, engine_name: str = DEFAULT_FITTING_ENGINE): """ Create a backend optimization engine. :param engine_name: The label of the optimization engine to create. :return: None """ - self._current_engine = minimizer_class_factory(from_string(engine_name)) + # self._current_engine = minimizer_class_factory(from_string(engine_name)) + minimizer_class = minimizer_class_factory(from_string(engine_name)) + self._minimizer = minimizer_class(self._fit_object, self.fit_function) # engines = self.available_engines # if engine_name in engines: @@ -151,10 +163,15 @@ def switch_engine(self, engine_name: str): # if not self._is_initialized: # raise ReferenceError('The fitting engine must be initialized before switching') # Constrains are not carried over. Do it manually. - constraints = self.__engine_obj._constraints - self.create(engine_name) - self.__initialize() - self.__engine_obj._constraints = constraints + # constraints = self.__engine_obj._constraints + # self.create(engine_name) + # self.__initialize() + # self.__engine_obj._constraints = constraints + + constraints = self._minimizer._constraints + minimizer_class = minimizer_class_factory(from_string(engine_name)) + self._minimizer = minimizer_class(self._fit_object, self.fit_function) + self._minimizer._constraints = constraints @property def available_engines(self) -> List[str]: @@ -169,40 +186,43 @@ def available_engines(self) -> List[str]: # return [engine.name for engine in minimizers.engines] return [minimize.name for minimize in Minimizers] - @property - def can_fit(self) -> bool: - """ - Can a fit be performed. i.e has the object been created properly + # @property + # def can_fit(self) -> bool: + # """ + # Can a fit be performed. i.e has the object been created properly - :return: Can a fit be performed - :rtype: bool - """ - return self._is_initialized + # :return: Can a fit be performed + # :rtype: bool + # """ + # return self._is_initialized - @property - def current_engine(self) -> _C: - """ - Get the class object of the current fitting engine. + # @property + # def current_engine(self) -> _C: + # """ + # Get the class object of the current fitting engine. - :return: Class of the current fitting engine (based on the `FittingTemplate` class) - :rtype: _T - """ - return self._current_engine + # :return: Class of the current fitting engine (based on the `FittingTemplate` class) + # :rtype: _T + # """ + # return self._current_engine @property - def engine(self) -> _M: + def minimizer(self) -> MinimizerBase: + # def engine(self) -> MinimizerBase: + # def engine(self) -> _M: """ Get the current fitting engine object. :return: :rtype: _M """ - return self.__engine_obj + # return self.__engine_obj + return self._minimizer @property def fit_function(self) -> Callable: """ - The raw fit function that the optimizer will call (no wrapping) + The raw fit function that the optimizer will call (no wrapping) :return: Raw fit function """ return self._fit_function @@ -215,7 +235,9 @@ def fit_function(self, fit_function: Callable): :return: None """ self._fit_function = fit_function - self.__initialize() + # self.__initialize() + # self._initialize() + self.create(self._minimizer.name) @property def fit_object(self): @@ -233,7 +255,10 @@ def fit_object(self, fit_object): :return: None """ self._fit_object = fit_object - self.__initialize() + # self._initialize() + self.create(self._minimizer.name) + + # self.__initialize() # def __pass_through_generator(self, name: str): # """ @@ -261,7 +286,8 @@ def fit(self) -> Callable: re-constitute the independent variables and once the fit is completed, reshape the inputs to those expected. """ - @functools.wraps(self.engine.fit) + @functools.wraps(self.minimizer.fit) + # @functools.wraps(self.engine.fit) def inner_fit_callable( x: np.ndarray, y: np.ndarray, @@ -276,9 +302,9 @@ def inner_fit_callable( - FIT = Wrapping the fit function and performing the fit - POST = Reshaping the outputs so it is coherent with the inputs. """ - # Check to see if we can perform a fit - if not self.can_fit: - raise ReferenceError('The fitting engine must first be initialized') + # # Check to see if we can perform a fit + # if not self.can_fit: + # raise ReferenceError('The fitting engine must first be initialized') # Precompute - Reshape all independents into the correct dimensionality x_fit, x_new, y_new, weights, dims, kwargs = self._precompute_reshaping(x, y, weights, vectorized, kwargs) @@ -289,16 +315,20 @@ def inner_fit_callable( fit_fun_wrap = self._fit_function_wrapper(x_new, flatten=True) # This should be wrapped. # We change the fit function, so have to reset constraints - constraints = self.__engine_obj._constraints + constraints = self._minimizer._constraints + # constraints = self.__engine_obj._constraints self.fit_function = fit_fun_wrap - self.__engine_obj._constraints = constraints - f_res = self.engine.fit(x_fit, y_new, weights=weights, **kwargs) + self._minimizer._constraints = constraints + # self.__engine_obj._constraints = constraints + # f_res = self.engine.fit(x_fit, y_new, weights=weights, **kwargs) + f_res = self.minimizer.fit(x_fit, y_new, weights=weights, **kwargs) # Postcompute fit_result = self._post_compute_reshaping(f_res, x, y, weights) # Reset the function and constrains self.fit_function = fit_fun - self.__engine_obj._constraints = constraints + # self.__engine_obj._constraints = constraints + self._minimizer._constraints = constraints return fit_result return inner_fit_callable diff --git a/src/easyscience/__init__.py b/src/easyscience/__init__.py index 91cfb3ac..d4512cbf 100644 --- a/src/easyscience/__init__.py +++ b/src/easyscience/__init__.py @@ -10,7 +10,7 @@ from easyscience.__version__ import __version__ as __version__ from easyscience.Objects.Borg import Borg -default_fitting_engine = 'lmfit' +DEFAULT_FITTING_ENGINE = 'lmfit' ureg = pint.UnitRegistry() borg = Borg() From 8719e97448bf9e05d0d5157cbe295329beb9c5c0 Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Mon, 10 Jun 2024 07:15:00 +0200 Subject: [PATCH 03/24] seems to be working --- examples_old/example1_dream.py | 2 +- examples_old/example5_broken.py | 2 +- examples_old/example6_broken.py | 2 +- examples_old/example_dataset2pt2.py | 2 +- examples_old/example_dataset2pt2_broken.py | 2 +- src/easyscience/Fitting/fitter.py | 54 ++++++++++--------- .../Fitting/minimizers/__init__.py | 52 +++++++++--------- src/easyscience/__init__.py | 2 +- .../integration_tests/Fitting/test_fitter.py | 12 ++--- .../Fitting/test_multi_fitter.py | 8 +-- tests/integration_tests/test_undoRedo.py | 2 +- 11 files changed, 73 insertions(+), 67 deletions(-) diff --git a/examples_old/example1_dream.py b/examples_old/example1_dream.py index f485d058..bc80a125 100644 --- a/examples_old/example1_dream.py +++ b/examples_old/example1_dream.py @@ -19,7 +19,7 @@ def fit_fun(x): f = Fitter() f.initialize(b, fit_fun) -f.switch_engine("bumps") +f.switch_minimizer("bumps") x = np.array([1, 2, 3]) y = np.array([2, 4, 6]) - 1 diff --git a/examples_old/example5_broken.py b/examples_old/example5_broken.py index 17abd5d9..c88451d3 100644 --- a/examples_old/example5_broken.py +++ b/examples_old/example5_broken.py @@ -357,7 +357,7 @@ def __repr__(self): a = line.c # Now lets change fitting engine -f.switch_engine("bumps") +f.switch_minimizer("bumps") # Reset the values so we don't cheat line.m = 1 line.c = 0 diff --git a/examples_old/example6_broken.py b/examples_old/example6_broken.py index 0c2b9c0d..88c13a65 100644 --- a/examples_old/example6_broken.py +++ b/examples_old/example6_broken.py @@ -436,7 +436,7 @@ def __repr__(self): print(hybrid) # Now lets change fitting engine -f.switch_engine("bumps") +f.switch_minimizer("bumps") # Reset the values so we don't cheat hybrid.m = 1 hybrid.c = 0 diff --git a/examples_old/example_dataset2pt2.py b/examples_old/example_dataset2pt2.py index c33c9a91..20d5950d 100644 --- a/examples_old/example_dataset2pt2.py +++ b/examples_old/example_dataset2pt2.py @@ -43,7 +43,7 @@ def fit_fun(x, *args, **kwargs): b.m = m_starting_point b.c = c_starting_point - f.switch_engine(minimizer) + f.switch_minimizer(minimizer) f_res = d['y'].easyscience.fit(f, vectorize=True) print(f_res.p) diff --git a/examples_old/example_dataset2pt2_broken.py b/examples_old/example_dataset2pt2_broken.py index b56ea7ae..ecce0408 100644 --- a/examples_old/example_dataset2pt2_broken.py +++ b/examples_old/example_dataset2pt2_broken.py @@ -43,7 +43,7 @@ def fit_fun(x, *args, **kwargs): b.m = m_starting_point b.c = c_starting_point - f.switch_engine(minimizer) + f.switch_minimizer(minimizer) f_res = d['y'].easyscience.fit(f, vectorize=True) print(f_res.p) diff --git a/src/easyscience/Fitting/fitter.py b/src/easyscience/Fitting/fitter.py index a3e63ec0..294400bd 100644 --- a/src/easyscience/Fitting/fitter.py +++ b/src/easyscience/Fitting/fitter.py @@ -6,18 +6,17 @@ # SPDX-FileCopyrightText: 2023 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project List[str]: + def available_minimizers(self) -> List[str]: """ - Get a list of the names of available fitting engines + Get a list of the names of available fitting minimizers - :return: List of available fitting engines + :return: List of available fitting minimizers :rtype: List[str] """ # if minimizers.engines is None: @@ -211,7 +217,7 @@ def minimizer(self) -> MinimizerBase: # def engine(self) -> MinimizerBase: # def engine(self) -> _M: """ - Get the current fitting engine object. + Get the current fitting minimizer object. :return: :rtype: _M diff --git a/src/easyscience/Fitting/minimizers/__init__.py b/src/easyscience/Fitting/minimizers/__init__.py index 6afab1c7..c1bfb38c 100644 --- a/src/easyscience/Fitting/minimizers/__init__.py +++ b/src/easyscience/Fitting/minimizers/__init__.py @@ -5,33 +5,33 @@ __author__ = 'github.com/wardsimon' __version__ = '0.1.0' -import warnings +# import warnings -from .minimizer_base import MinimizerBase # noqa: E402 +from .minimizer_base import MinimizerBase # noqa: F401, E402 from .utils import FitError # noqa: F401, E402 from .utils import FitResults # noqa: F401, E402 -imported = -1 -try: - from .minimizer_lmfit import LMFit # noqa: F401, E402 - - imported += 1 -except ImportError: - # TODO make this a proper message (use logging?) - warnings.warn('lmfit has not been installed.', ImportWarning, stacklevel=2) -try: - from .minimizer_bumps import Bumps # noqa: F401, E402 - - imported += 1 -except ImportError: - # TODO make this a proper message (use logging?) - warnings.warn('bumps has not been installed.', ImportWarning, stacklevel=2) -try: - from .minimizer_dfo import DFO # noqa: F401, E402 - - imported += 1 -except ImportError: - # TODO make this a proper message (use logging?) - warnings.warn('dfo-ls has not been installed.', ImportWarning, stacklevel=2) - -engines: list = MinimizerBase._engines +# imported = -1 +# try: +# from .minimizer_lmfit import LMFit # noqa: F401, E402 + +# imported += 1 +# except ImportError: +# # TODO make this a proper message (use logging?) +# warnings.warn('lmfit has not been installed.', ImportWarning, stacklevel=2) +# try: +# from .minimizer_bumps import Bumps # noqa: F401, E402 + +# imported += 1 +# except ImportError: +# # TODO make this a proper message (use logging?) +# warnings.warn('bumps has not been installed.', ImportWarning, stacklevel=2) +# try: +# from .minimizer_dfo import DFO # noqa: F401, E402 + +# imported += 1 +# except ImportError: +# # TODO make this a proper message (use logging?) +# warnings.warn('dfo-ls has not been installed.', ImportWarning, stacklevel=2) + +# engines: list = MinimizerBase._engines diff --git a/src/easyscience/__init__.py b/src/easyscience/__init__.py index d4512cbf..f2c9c968 100644 --- a/src/easyscience/__init__.py +++ b/src/easyscience/__init__.py @@ -10,7 +10,7 @@ from easyscience.__version__ import __version__ as __version__ from easyscience.Objects.Borg import Borg -DEFAULT_FITTING_ENGINE = 'lmfit' +DEFAULT_MINIMIZER = 'lmfit' ureg = pint.UnitRegistry() borg = Borg() diff --git a/tests/integration_tests/Fitting/test_fitter.py b/tests/integration_tests/Fitting/test_fitter.py index 0180ef36..be30eafd 100644 --- a/tests/integration_tests/Fitting/test_fitter.py +++ b/tests/integration_tests/Fitting/test_fitter.py @@ -92,7 +92,7 @@ def test_basic_fit(fit_engine, with_errors): f = Fitter(sp_sin, sp_sin) if fit_engine is not None: try: - f.switch_engine(fit_engine) + f.switch_minimizer(fit_engine) except AttributeError: pytest.skip(msg=f"{fit_engine} is not installed") args = [x, y] @@ -131,7 +131,7 @@ def test_fit_result(fit_engine): if fit_engine is not None: try: - f.switch_engine(fit_engine) + f.switch_minimizer(fit_engine) except AttributeError: pytest.skip(msg=f"{fit_engine} is not installed") @@ -169,7 +169,7 @@ def test_bumps_methods(fit_method): sp_sin.phase.fixed = False f = Fitter(sp_sin, sp_sin) - f.switch_engine("bumps") + f.switch_minimizer("bumps") assert fit_method in f.available_methods() result = f.fit(x, y, method=fit_method) check_fit_results(result, sp_sin, ref_sin, x) @@ -193,7 +193,7 @@ def test_fit_constraints(fit_engine): if fit_engine is not None: try: - f.switch_engine(fit_engine) + f.switch_minimizer(fit_engine) except AttributeError: pytest.skip(msg=f"{fit_engine} is not installed") @@ -217,7 +217,7 @@ def test_2D_vectorized(fit_engine, with_errors): ff = Fitter(m2, m2) if fit_engine is not None: try: - ff.switch_engine(fit_engine) + ff.switch_minimizer(fit_engine) except AttributeError: pytest.skip(msg=f"{fit_engine} is not installed") try: @@ -253,7 +253,7 @@ def test_2D_non_vectorized(fit_engine, with_errors): ff = Fitter(m2, m2) if fit_engine is not None: try: - ff.switch_engine(fit_engine) + ff.switch_minimizer(fit_engine) except AttributeError: pytest.skip(msg=f"{fit_engine} is not installed") try: diff --git a/tests/integration_tests/Fitting/test_multi_fitter.py b/tests/integration_tests/Fitting/test_multi_fitter.py index 5ec8de9e..435c7611 100644 --- a/tests/integration_tests/Fitting/test_multi_fitter.py +++ b/tests/integration_tests/Fitting/test_multi_fitter.py @@ -88,7 +88,7 @@ def test_multi_fit(fit_engine, with_errors): f = MultiFitter([sp_sin_1, sp_sin_2], [sp_sin_1, sp_sin_2]) if fit_engine is not None: try: - f.switch_engine(fit_engine) + f.switch_minimizer(fit_engine) except AttributeError: pytest.skip(msg=f"{fit_engine} is not installed") @@ -163,7 +163,7 @@ def test_multi_fit2(fit_engine, with_errors): f = MultiFitter([sp_sin_1, sp_line, sp_sin_2], [sp_sin_1, sp_line, sp_sin_2]) if fit_engine is not None: try: - f.switch_engine(fit_engine) + f.switch_minimizer(fit_engine) except AttributeError: pytest.skip(msg=f"{fit_engine} is not installed") @@ -232,7 +232,7 @@ def test_multi_fit_1D_2D(fit_engine, with_errors): ff = MultiFitter([sp_sin1D, sp_sin2D], [sp_sin1D, sp_sin2D]) if fit_engine is not None: try: - ff.switch_engine(fit_engine) + ff.switch_minimizer(fit_engine) except AttributeError: pytest.skip(msg=f"{fit_engine} is not installed") @@ -243,7 +243,7 @@ def test_multi_fit_1D_2D(fit_engine, with_errors): f = MultiFitter([sp_sin1D, sp_sin2D], [sp_sin1D, sp_sin2D]) if fit_engine is not None: try: - f.switch_engine(fit_engine) + f.switch_minimizer(fit_engine) except AttributeError: pytest.skip(msg=f"{fit_engine} is not installed") try: diff --git a/tests/integration_tests/test_undoRedo.py b/tests/integration_tests/test_undoRedo.py index 5db3a36a..bf89b54b 100644 --- a/tests/integration_tests/test_undoRedo.py +++ b/tests/integration_tests/test_undoRedo.py @@ -269,7 +269,7 @@ def __call__(self, x: np.ndarray) -> np.ndarray: f = Fitter(l2, l2) try: - f.switch_engine(fit_engine) + f.switch_minimizer(fit_engine) except AttributeError: pytest.skip(msg=f"{fit_engine} is not installed") From d48cd4fc5bb013856e9ca615571691c21b44de2b Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Mon, 10 Jun 2024 07:19:21 +0200 Subject: [PATCH 04/24] code cleaning --- src/easyscience/Fitting/fitter.py | 128 +----------------------------- 1 file changed, 3 insertions(+), 125 deletions(-) diff --git a/src/easyscience/Fitting/fitter.py b/src/easyscience/Fitting/fitter.py index 294400bd..c7ccca96 100644 --- a/src/easyscience/Fitting/fitter.py +++ b/src/easyscience/Fitting/fitter.py @@ -6,16 +6,12 @@ # SPDX-FileCopyrightText: 2023 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project list: return self._minimizer.fit_constraints() @@ -119,22 +86,8 @@ def initialize(self, fit_object, fit_function: Callable): """ self._fit_object = fit_object self._fit_function = fit_function - # self.__initialize() - # self._initialize() self._update_minimizer(DEFAULT_MINIMIZER) - # def _initialize(self): - # # def __initialize(self): - # """ - # The real initialization. Setting the optimizer object properly - # :return: None - # """ - # minimizer_class = minimizer_class_factory(from_string(DEFAULT_FITTING_ENGINE)) - # self._minimizer = minimizer_class(self._fit_object, self.fit_function) - - # self.__engine_obj = self._current_engine(self._fit_object, self.fit_function) - # self._is_initialized = True - def create(self, minimizer_name: str = DEFAULT_MINIMIZER): """ Create a backend minimization engine. @@ -142,17 +95,6 @@ def create(self, minimizer_name: str = DEFAULT_MINIMIZER): :return: None """ self._update_minimizer(minimizer_name) - # self._current_engine = minimizer_class_factory(from_string(minimizer_name)) - - # minimizer_class = minimizer_class_factory(from_string(minimizer_name)) - # self._minimizer = minimizer_class(self._fit_object, self.fit_function) - - # engines = self.available_engines - # if engine_name in engines: - # self._current_engine = self._engines[engines.index(engine_name)] - # self._is_initialized = False - # else: - # raise AttributeError(f"The supplied optimizer engine '{engine_name}' is unknown.") def switch_minimizer(self, minimizer_name: str): """ @@ -160,19 +102,8 @@ def switch_minimizer(self, minimizer_name: str): :param minimizer_name: The label of the minimization engine to create and instantiate. :return: None """ - # There isn't any state to carry over - # if not self._is_initialized: - # raise ReferenceError('The fitting engine must be initialized before switching') - # Constrains are not carried over. Do it manually. - # constraints = self.__engine_obj._constraints - # self.create(engine_name) - # self.__initialize() - # self.__engine_obj._constraints = constraints - constraints = self._minimizer._constraints self._update_minimizer(minimizer_name) - # minimizer_class = minimizer_class_factory(from_string(engine_name)) - # self._minimizer = minimizer_class(self._fit_object, self.fit_function) self._minimizer._constraints = constraints def _update_minimizer(self, minimizer_name: str): @@ -187,42 +118,16 @@ def available_minimizers(self) -> List[str]: :return: List of available fitting minimizers :rtype: List[str] """ - # if minimizers.engines is None: - # raise ImportError('There are no available fitting engines. Install `lmfit` and/or `bumps`') - # return [engine.name for engine in minimizers.engines] return [minimize.name for minimize in Minimizers] - # @property - # def can_fit(self) -> bool: - # """ - # Can a fit be performed. i.e has the object been created properly - - # :return: Can a fit be performed - # :rtype: bool - # """ - # return self._is_initialized - - # @property - # def current_engine(self) -> _C: - # """ - # Get the class object of the current fitting engine. - - # :return: Class of the current fitting engine (based on the `FittingTemplate` class) - # :rtype: _T - # """ - # return self._current_engine - @property def minimizer(self) -> MinimizerBase: - # def engine(self) -> MinimizerBase: - # def engine(self) -> _M: """ Get the current fitting minimizer object. :return: - :rtype: _M + :rtype: MinimizerBase """ - # return self.__engine_obj return self._minimizer @property @@ -241,9 +146,7 @@ def fit_function(self, fit_function: Callable): :return: None """ self._fit_function = fit_function - # self.__initialize() - # self._initialize() - self.create(self._minimizer.name) + self._update_minimizer(self._minimizer.name) @property def fit_object(self): @@ -261,28 +164,7 @@ def fit_object(self, fit_object): :return: None """ self._fit_object = fit_object - # self._initialize() - self.create(self._minimizer.name) - - # self.__initialize() - - # def __pass_through_generator(self, name: str): - # """ - # Attach the attributes of the calculator template to the current fitter instance. - # :param name: Attribute name to attach - # :return: Wrapped calculator interface object. - # """ - # obj = self - - # def inner(*args, **kwargs): - # if not obj.can_fit: - # raise ReferenceError('The fitting engine must first be initialized') - # func = getattr(obj.engine, name, None) - # if func is None: - # raise ValueError('The fitting engine does not have the attribute "{}"'.format(name)) - # return func(*args, **kwargs) - - # return inner + self._update_minimizer(self._minimizer.name) @property def fit(self) -> Callable: @@ -322,18 +204,14 @@ def inner_fit_callable( # We change the fit function, so have to reset constraints constraints = self._minimizer._constraints - # constraints = self.__engine_obj._constraints self.fit_function = fit_fun_wrap self._minimizer._constraints = constraints - # self.__engine_obj._constraints = constraints - # f_res = self.engine.fit(x_fit, y_new, weights=weights, **kwargs) f_res = self.minimizer.fit(x_fit, y_new, weights=weights, **kwargs) # Postcompute fit_result = self._post_compute_reshaping(f_res, x, y, weights) # Reset the function and constrains self.fit_function = fit_fun - # self.__engine_obj._constraints = constraints self._minimizer._constraints = constraints return fit_result From 46cdbae921f887b5c50145d46913897c1f1eaf62 Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Tue, 11 Jun 2024 14:45:17 +0200 Subject: [PATCH 05/24] move code parts --- src/easyscience/Fitting/fitter.py | 42 +++++++++++++++---------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/easyscience/Fitting/fitter.py b/src/easyscience/Fitting/fitter.py index c7ccca96..4ec7df66 100644 --- a/src/easyscience/Fitting/fitter.py +++ b/src/easyscience/Fitting/fitter.py @@ -55,27 +55,6 @@ def convert_to_pars_obj(self, pars) -> object: def available_methods(self) -> list: return self._minimizer.available_methods() - def _fit_function_wrapper(self, real_x=None, flatten: bool = True) -> Callable: - """ - Simple fit function which injects the real X (independent) values into the - optimizer function. This will also flatten the results if needed. - :param real_x: Independent x parameters to be injected - :param flatten: Should the result be a flat 1D array? - :return: Wrapped optimizer function. - """ - fun = self._fit_function - - @functools.wraps(fun) - def wrapped_fit_function(x, **kwargs): - if real_x is not None: - x = real_x - dependent = fun(x, **kwargs) - if flatten: - dependent = dependent.flatten() - return dependent - - return wrapped_fit_function - def initialize(self, fit_object, fit_function: Callable): """ Set the model and callable in the calculator interface. @@ -166,6 +145,27 @@ def fit_object(self, fit_object): self._fit_object = fit_object self._update_minimizer(self._minimizer.name) + def _fit_function_wrapper(self, real_x=None, flatten: bool = True) -> Callable: + """ + Simple fit function which injects the real X (independent) values into the + optimizer function. This will also flatten the results if needed. + :param real_x: Independent x parameters to be injected + :param flatten: Should the result be a flat 1D array? + :return: Wrapped optimizer function. + """ + fun = self._fit_function + + @functools.wraps(fun) + def wrapped_fit_function(x, **kwargs): + if real_x is not None: + x = real_x + dependent = fun(x, **kwargs) + if flatten: + dependent = dependent.flatten() + return dependent + + return wrapped_fit_function + @property def fit(self) -> Callable: """ From 0bba27e5c4bd80c606db4f5a5045e086b2f8e9c9 Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Wed, 12 Jun 2024 12:52:04 +0200 Subject: [PATCH 06/24] code cleaning --- src/easyscience/Fitting/fitter.py | 15 ++-------- .../Fitting/minimizers/__init__.py | 30 ------------------- .../Fitting/minimizers/minimizer_base.py | 3 -- .../Fitting/minimizers/minimizer_bumps.py | 3 -- .../Fitting/minimizers/minimizer_dfo.py | 3 -- .../Fitting/minimizers/minimizer_lmfit.py | 3 -- src/easyscience/Fitting/multi_fitter.py | 3 -- src/easyscience/__init__.py | 2 -- 8 files changed, 3 insertions(+), 59 deletions(-) diff --git a/src/easyscience/Fitting/fitter.py b/src/easyscience/Fitting/fitter.py index 4ec7df66..d0bfd508 100644 --- a/src/easyscience/Fitting/fitter.py +++ b/src/easyscience/Fitting/fitter.py @@ -1,25 +1,21 @@ -__author__ = 'github.com/wardsimon' -__version__ = '0.0.1' - -import functools - # SPDX-FileCopyrightText: 2023 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project Callable: """ @functools.wraps(self.minimizer.fit) - # @functools.wraps(self.engine.fit) def inner_fit_callable( x: np.ndarray, y: np.ndarray, @@ -190,10 +185,6 @@ def inner_fit_callable( - FIT = Wrapping the fit function and performing the fit - POST = Reshaping the outputs so it is coherent with the inputs. """ - # # Check to see if we can perform a fit - # if not self.can_fit: - # raise ReferenceError('The fitting engine must first be initialized') - # Precompute - Reshape all independents into the correct dimensionality x_fit, x_new, y_new, weights, dims, kwargs = self._precompute_reshaping(x, y, weights, vectorized, kwargs) self._dependent_dims = dims diff --git a/src/easyscience/Fitting/minimizers/__init__.py b/src/easyscience/Fitting/minimizers/__init__.py index c1bfb38c..2d5198d7 100644 --- a/src/easyscience/Fitting/minimizers/__init__.py +++ b/src/easyscience/Fitting/minimizers/__init__.py @@ -2,36 +2,6 @@ # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project Date: Wed, 12 Jun 2024 13:05:29 +0200 Subject: [PATCH 07/24] pin pint dependency --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 76304217..a67cb1f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,8 @@ dependencies = [ "numpy", "pint", "uncertainties", - "xarray" + "xarray", + "pint==0.23" # Only to ensure that unit is reported as dimensionless rather than empty string ] [project.optional-dependencies] From b9f3471855e1cabe3452602e5562a1541d37e741 Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Wed, 12 Jun 2024 13:15:19 +0200 Subject: [PATCH 08/24] code comment --- src/easyscience/Fitting/fitter.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/easyscience/Fitting/fitter.py b/src/easyscience/Fitting/fitter.py index d0bfd508..59798067 100644 --- a/src/easyscience/Fitting/fitter.py +++ b/src/easyscience/Fitting/fitter.py @@ -19,7 +19,7 @@ class Fitter: """ - Fitter is a class which provides a common interface to the supported minimizers. + Fitter is a class which makes it possible to undertake fitting utilizing one of the supported minimizers. """ def __init__(self, fit_object, fit_function: Callable): @@ -51,37 +51,34 @@ def convert_to_pars_obj(self, pars) -> object: def available_methods(self) -> list: return self._minimizer.available_methods() - def initialize(self, fit_object, fit_function: Callable): + def initialize(self, fit_object, fit_function: Callable) -> None: """ Set the model and callable in the calculator interface. :param fit_object: The EasyScience model object :param fit_function: The function to be optimized against. - :return: None """ self._fit_object = fit_object self._fit_function = fit_function self._update_minimizer(DEFAULT_MINIMIZER) - def create(self, minimizer_name: str = DEFAULT_MINIMIZER): + def create(self, minimizer_name: str = DEFAULT_MINIMIZER) -> None: """ - Create a backend minimization engine. + Create the required minimizer. :param minimizer_name: The label of the minimization engine to create. - :return: None """ self._update_minimizer(minimizer_name) - def switch_minimizer(self, minimizer_name: str): + def switch_minimizer(self, minimizer_name: str) -> None: """ - Switch backend minimization engine and initialize. - :param minimizer_name: The label of the minimization engine to create and instantiate. - :return: None + Switch minimizer and initialize. + :param minimizer_name: The label of the minimizer to create and instantiate. """ constraints = self._minimizer._constraints self._update_minimizer(minimizer_name) self._minimizer._constraints = constraints - def _update_minimizer(self, minimizer_name: str): + def _update_minimizer(self, minimizer_name: str) -> None: minimizer_class = minimizer_class_factory(from_string(minimizer_name)) self._minimizer = minimizer_class(self._fit_object, self.fit_function) @@ -114,7 +111,7 @@ def fit_function(self) -> Callable: return self._fit_function @fit_function.setter - def fit_function(self, fit_function: Callable): + def fit_function(self, fit_function: Callable) -> None: """ Set the raw fit function to a new one. :param fit_function: New fit function @@ -132,7 +129,7 @@ def fit_object(self): return self._fit_object @fit_object.setter - def fit_object(self, fit_object): + def fit_object(self, fit_object) -> None: """ Set the EasyScience object which wil be used as a model :param fit_object: New EasyScience object From 6afc21b2ecf288854307de755eec564b88478e56 Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Fri, 14 Jun 2024 11:22:01 +0200 Subject: [PATCH 09/24] seems to be working --- src/easyscience/Fitting/fitter.py | 27 +++-- src/easyscience/Fitting/minimizers/factory.py | 110 ++++++++++++++---- .../Fitting/minimizers/minimizer_base.py | 27 ++--- .../Fitting/minimizers/minimizer_bumps.py | 9 +- .../Fitting/minimizers/minimizer_dfo.py | 11 +- .../Fitting/minimizers/minimizer_lmfit.py | 4 +- .../integration_tests/Fitting/test_fitter.py | 2 +- 7 files changed, 138 insertions(+), 52 deletions(-) diff --git a/src/easyscience/Fitting/fitter.py b/src/easyscience/Fitting/fitter.py index 59798067..0bd0c752 100644 --- a/src/easyscience/Fitting/fitter.py +++ b/src/easyscience/Fitting/fitter.py @@ -10,11 +10,11 @@ from .minimizers import FitResults from .minimizers import MinimizerBase -from .minimizers.factory import Minimizers +from .minimizers.factory import AvailableMinimizers from .minimizers.factory import from_string from .minimizers.factory import minimizer_class_factory -DEFAULT_MINIMIZER = 'lmfit' +DEFAULT_MINIMIZER = 'lmfit-leastsq' class Fitter: @@ -27,8 +27,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._minimizer: MinimizerBase # _minimizer is set in the create method - self.create(DEFAULT_MINIMIZER) + self._update_minimizer(self._name_current_minimizer) def fit_constraints(self) -> list: return self._minimizer.fit_constraints() @@ -49,6 +50,13 @@ def convert_to_pars_obj(self, pars) -> object: return self._minimizer.convert_to_pars_obj(pars) def available_methods(self) -> list: + """ + Return the available fitting methods for minimizer engine contaning the current minimizer. + This should only be used to inspect potential methods. + The supported minizers are the ones in the minimizers.factory.AvailableMinimizers enum. + + :return: List of available fitting methods + """ return self._minimizer.available_methods() def initialize(self, fit_object, fit_function: Callable) -> None: @@ -79,8 +87,11 @@ def switch_minimizer(self, minimizer_name: str) -> None: self._minimizer._constraints = constraints def _update_minimizer(self, minimizer_name: str) -> None: - minimizer_class = minimizer_class_factory(from_string(minimizer_name)) - self._minimizer = minimizer_class(self._fit_object, self.fit_function) + minimizer_enum = from_string(minimizer_name) + self._minimizer = minimizer_class_factory( + minimizer_enum=minimizer_enum, fit_object=self._fit_object, fit_function=self.fit_function + ) + self._name_current_minimizer = minimizer_name @property def available_minimizers(self) -> List[str]: @@ -90,7 +101,7 @@ def available_minimizers(self) -> List[str]: :return: List of available fitting minimizers :rtype: List[str] """ - return [minimize.name for minimize in Minimizers] + return [minimize.name for minimize in AvailableMinimizers] @property def minimizer(self) -> MinimizerBase: @@ -118,7 +129,7 @@ def fit_function(self, fit_function: Callable) -> None: :return: None """ self._fit_function = fit_function - self._update_minimizer(self._minimizer.name) + self._update_minimizer(self._name_current_minimizer) @property def fit_object(self): @@ -136,7 +147,7 @@ def fit_object(self, fit_object) -> None: :return: None """ self._fit_object = fit_object - self._update_minimizer(self._minimizer.name) + self._update_minimizer(self._name_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 0d3dec72..fe66c82a 100644 --- a/src/easyscience/Fitting/minimizers/factory.py +++ b/src/easyscience/Fitting/minimizers/factory.py @@ -1,30 +1,100 @@ +import warnings from enum import Enum +from enum import auto +from typing import Callable -from .minimizer_bumps import Bumps -from .minimizer_dfo import DFO -from .minimizer_lmfit import LMFit +from .minimizer_base import MinimizerBase +lmfit_minimizer_imported = False +try: + from .minimizer_lmfit import LMFit -class Minimizers(Enum): - Bumps = 1 - DFO = 2 - LMFit = 3 + lmfit_minimizer_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_minimizer_imported = False +try: + from .minimizer_bumps import Bumps -def from_string(engine_name: str) -> Minimizers: - if engine_name == 'bumps': - engine_enum = Minimizers.Bumps + bumps_minimizer_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) + +dfo_minimizer_imported = False +try: + from .minimizer_dfo import DFO + + dfo_minimizer_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_minimizer_imported: + LMFit = auto() + LMFit_leastsq = auto() + LMFit_powell = auto() + LMFit_cobyla = auto() + + if bumps_minimizer_imported: + Bumps = auto() + Bumps_simplex = auto() + Bumps_newton = auto() + Bumps_lm = auto() + + if dfo_minimizer_imported: + DFO = auto() + + +def from_string(engine_name: str) -> AvailableMinimizers: if engine_name == 'lmfit': - engine_enum = Minimizers.LMFit - if engine_name == 'dfo_ls': - engine_enum = Minimizers.DFO + engine_enum = AvailableMinimizers.LMFit + elif engine_name == 'lmfit-leastsq': + engine_enum = AvailableMinimizers.LMFit_leastsq + elif engine_name == 'lmfit-powell': + engine_enum = AvailableMinimizers.LMFit_powell + elif engine_name == 'lmfit-cobyla': + engine_enum = AvailableMinimizers.LMFit_cobyla + + elif engine_name == 'bumps': + engine_enum = AvailableMinimizers.Bumps + elif engine_name == 'bumps-simplex': + engine_enum = AvailableMinimizers.Bumps_simplex + elif engine_name == 'bumps-newton': + engine_enum = AvailableMinimizers.Bumps_newton + elif engine_name == 'bumps-lm': + engine_enum = AvailableMinimizers.Bumps_lm + + elif engine_name == 'dfo_ls': + engine_enum = AvailableMinimizers.DFO + return engine_enum -def minimizer_class_factory(minimizer: Minimizers): - if minimizer == Minimizers.Bumps: - return Bumps - elif minimizer == Minimizers.DFO: - return DFO - elif minimizer == Minimizers.LMFit: - return LMFit +def minimizer_class_factory(minimizer_enum: AvailableMinimizers, fit_object, fit_function: Callable) -> MinimizerBase: + if minimizer_enum == AvailableMinimizers.LMFit: + minimizer = LMFit(obj=fit_object, fit_function=fit_function, method='leastsq') + elif minimizer_enum == AvailableMinimizers.LMFit_leastsq: + minimizer = LMFit(obj=fit_object, fit_function=fit_function, method='leastsq') + elif minimizer_enum == AvailableMinimizers.LMFit_powell: + 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.Bumps: + minimizer = Bumps(obj=fit_object, fit_function=fit_function, method='amoeba') + elif minimizer_enum == AvailableMinimizers.Bumps_simplex: + minimizer = Bumps(obj=fit_object, fit_function=fit_function, method='amoeba') + elif minimizer_enum == AvailableMinimizers.Bumps_newton: + minimizer = Bumps(obj=fit_object, fit_function=fit_function, method='newton') + elif minimizer_enum == AvailableMinimizers.Bumps_lm: + minimizer = Bumps(obj=fit_object, fit_function=fit_function, method='lm') + + elif minimizer_enum == AvailableMinimizers.DFO: + minimizer = DFO(obj=fit_object, fit_function=fit_function) + + return minimizer diff --git a/src/easyscience/Fitting/minimizers/minimizer_base.py b/src/easyscience/Fitting/minimizers/minimizer_base.py index 210bdf81..498ac0eb 100644 --- a/src/easyscience/Fitting/minimizers/minimizer_base.py +++ b/src/easyscience/Fitting/minimizers/minimizer_base.py @@ -19,21 +19,22 @@ class MinimizerBase(metaclass=ABCMeta): This template class is the basis for all fitting engines in `EasyScience`. """ - _engines = [] - property_type = None - name: str = '' - - def __init_subclass__(cls, is_abstract: bool = False, **kwargs): - super().__init_subclass__(**kwargs) - if not is_abstract: - # Deal with the issue of people not reading the schema. - if not hasattr(cls, 'name'): - setattr(cls, 'name', cls.__class__.__name__) - cls._engines.append(cls) - - def __init__(self, obj, fit_function: Callable): + # _engines = [] + # property_type = None + # name: str = '' + + # def __init_subclass__(cls, is_abstract: bool = False, **kwargs): + # super().__init_subclass__(**kwargs) + # if not is_abstract: + # # Deal with the issue of people not reading the schema. + # if not hasattr(cls, 'name'): + # setattr(cls, 'name', cls.__class__.__name__) + # cls._engines.append(cls) + + def __init__(self, obj, fit_function: Callable, method: Optional[str] = None): self._object = obj self._original_fit_function = fit_function + self._method = method self._cached_pars = {} self._cached_pars_vals = {} self._cached_model = None diff --git a/src/easyscience/Fitting/minimizers/minimizer_bumps.py b/src/easyscience/Fitting/minimizers/minimizer_bumps.py index 3931a4fb..f3d1205d 100644 --- a/src/easyscience/Fitting/minimizers/minimizer_bumps.py +++ b/src/easyscience/Fitting/minimizers/minimizer_bumps.py @@ -26,10 +26,10 @@ class Bumps(MinimizerBase): # noqa: S101 It allows for the Bumps fitting engine to use parameters declared in an `EasyScience.Objects.Base.BaseObj`. """ - property_type = BumpsParameter + # property_type = BumpsParameter name = 'bumps' - def __init__(self, obj, fit_function: Callable): + def __init__(self, obj, fit_function: Callable, method: Optional[str] = None): """ Initialize the fitting engine with a `BaseObj` and an arbitrary fitting function. @@ -40,7 +40,7 @@ def __init__(self, obj, fit_function: Callable): keyword/value pairs :type fit_function: Callable """ - super().__init__(obj, fit_function) + super().__init__(obj=obj, fit_function=fit_function, method=method) self._cached_pars_order = () self._p_0 = {} @@ -167,8 +167,9 @@ 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.available_methods(): default_method['method'] = method diff --git a/src/easyscience/Fitting/minimizers/minimizer_dfo.py b/src/easyscience/Fitting/minimizers/minimizer_dfo.py index d7c428ac..c1ce91e9 100644 --- a/src/easyscience/Fitting/minimizers/minimizer_dfo.py +++ b/src/easyscience/Fitting/minimizers/minimizer_dfo.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project Callable: @@ -150,8 +150,9 @@ 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.available_methods(): default_method['method'] = method diff --git a/src/easyscience/Fitting/minimizers/minimizer_lmfit.py b/src/easyscience/Fitting/minimizers/minimizer_lmfit.py index e758dba7..6b67ac6c 100644 --- a/src/easyscience/Fitting/minimizers/minimizer_lmfit.py +++ b/src/easyscience/Fitting/minimizers/minimizer_lmfit.py @@ -25,7 +25,7 @@ class LMFit(MinimizerBase): # noqa: S101 It allows for the lmfit fitting engine to use parameters declared in an `EasyScience.Objects.Base.BaseObj`. """ - property_type = LMParameter + # property_type = LMParameter name = 'lmfit' def make_model(self, pars: Optional[LMParameters] = None) -> LMModel: @@ -157,6 +157,8 @@ def fit( :rtype: ModelResult """ default_method = {} + if self._method is not None: + default_method = {'method': self._method} if method is not None and method in self.available_methods(): default_method['method'] = method diff --git a/tests/integration_tests/Fitting/test_fitter.py b/tests/integration_tests/Fitting/test_fitter.py index be30eafd..47957fcd 100644 --- a/tests/integration_tests/Fitting/test_fitter.py +++ b/tests/integration_tests/Fitting/test_fitter.py @@ -156,7 +156,7 @@ def test_lmfit_methods(fit_method): check_fit_results(result, sp_sin, ref_sin, x) -@pytest.mark.xfail(reason="known bumps issue") +#@pytest.mark.xfail(reason="known bumps issue") @pytest.mark.parametrize("fit_method", ["newton", "lm"]) def test_bumps_methods(fit_method): ref_sin = AbsSin(0.2, np.pi) From a2aa43f3600e1dff1d264c412ac24f14813a0e76 Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Fri, 14 Jun 2024 12:06:36 +0200 Subject: [PATCH 10/24] fitting_engine to minimizer_engine --- src/easyscience/Datasets/xarray.py | 2 +- src/easyscience/Fitting/minimizers/minimizer_bumps.py | 4 ++-- src/easyscience/Fitting/minimizers/minimizer_dfo.py | 4 ++-- src/easyscience/Fitting/minimizers/minimizer_lmfit.py | 4 ++-- src/easyscience/Fitting/minimizers/utils.py | 4 ++-- src/easyscience/Fitting/multi_fitter.py | 2 +- tests/integration_tests/Fitting/test_fitter.py | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/easyscience/Datasets/xarray.py b/src/easyscience/Datasets/xarray.py index 36633213..9c56e027 100644 --- a/src/easyscience/Datasets/xarray.py +++ b/src/easyscience/Datasets/xarray.py @@ -803,7 +803,7 @@ def check_sanity_multiple(fit_results: FitResults, originals: List[xr.DataArray] current_results = fit_results.__class__() # Fill out the basic stuff.... current_results.engine_result = fit_results.engine_result - current_results.fitting_engine = fit_results.fitting_engine + current_results.minimizer_engine = fit_results.minimizer_engine current_results.success = fit_results.success current_results.p = fit_results.p current_results.p0 = fit_results.p0 diff --git a/src/easyscience/Fitting/minimizers/minimizer_bumps.py b/src/easyscience/Fitting/minimizers/minimizer_bumps.py index f3d1205d..cb90df0d 100644 --- a/src/easyscience/Fitting/minimizers/minimizer_bumps.py +++ b/src/easyscience/Fitting/minimizers/minimizer_bumps.py @@ -27,7 +27,7 @@ class Bumps(MinimizerBase): # noqa: S101 """ # property_type = BumpsParameter - name = 'bumps' + wrapping = 'bumps' def __init__(self, obj, fit_function: Callable, method: Optional[str] = None): """ @@ -291,7 +291,7 @@ def _gen_fit_results(self, fit_results, **kwargs) -> FitResults: results.y_err = self._cached_model.dy # results.residual = results.y_obs - results.y_calc # results.goodness_of_fit = np.sum(results.residual**2) - results.fitting_engine = self.__class__ + results.minimizer_engine = self.__class__ results.fit_args = None results.engine_result = fit_results # results.check_sanity() diff --git a/src/easyscience/Fitting/minimizers/minimizer_dfo.py b/src/easyscience/Fitting/minimizers/minimizer_dfo.py index c1ce91e9..7f5b6d8f 100644 --- a/src/easyscience/Fitting/minimizers/minimizer_dfo.py +++ b/src/easyscience/Fitting/minimizers/minimizer_dfo.py @@ -22,7 +22,7 @@ class DFO(MinimizerBase): # noqa: S101 """ # property_type = Number - name = 'dfo_ls' + wrapping = 'dfo_ls' def __init__(self, obj, fit_function: Callable, method: Optional[str] = None): """ @@ -250,7 +250,7 @@ def _gen_fit_results(self, fit_results, weights, **kwargs) -> FitResults: # results.residual = results.y_obs - results.y_calc # results.goodness_of_fit = fit_results.f - results.fitting_engine = self.__class__ + results.minimizer_engine = self.__class__ results.fit_args = None # results.check_sanity() diff --git a/src/easyscience/Fitting/minimizers/minimizer_lmfit.py b/src/easyscience/Fitting/minimizers/minimizer_lmfit.py index 6b67ac6c..1928ada3 100644 --- a/src/easyscience/Fitting/minimizers/minimizer_lmfit.py +++ b/src/easyscience/Fitting/minimizers/minimizer_lmfit.py @@ -26,7 +26,7 @@ class LMFit(MinimizerBase): # noqa: S101 """ # property_type = LMParameter - name = 'lmfit' + wrapping = 'lmfit' def make_model(self, pars: Optional[LMParameters] = None) -> LMModel: """ @@ -276,7 +276,7 @@ def _gen_fit_results(self, fit_results: ModelResult, **kwargs) -> FitResults: # results.goodness_of_fit = fit_results.chisqr results.y_calc = fit_results.best_fit results.y_err = 1 / fit_results.weights - results.fitting_engine = self.__class__ + results.minimizer_engine = self.__class__ results.fit_args = None results.engine_result = fit_results diff --git a/src/easyscience/Fitting/minimizers/utils.py b/src/easyscience/Fitting/minimizers/utils.py index ff56e87a..59e0792e 100644 --- a/src/easyscience/Fitting/minimizers/utils.py +++ b/src/easyscience/Fitting/minimizers/utils.py @@ -8,7 +8,7 @@ class FitResults: __slots__ = [ 'success', - 'fitting_engine', + 'minimizer_engine', 'fit_args', 'p', 'p0', @@ -23,7 +23,7 @@ class FitResults: def __init__(self): self.success = False - self.fitting_engine = None + self.minimizer_engine = None self.fit_args = {} self.p = {} self.p0 = {} diff --git a/src/easyscience/Fitting/multi_fitter.py b/src/easyscience/Fitting/multi_fitter.py index aaf3e8b1..010259b1 100644 --- a/src/easyscience/Fitting/multi_fitter.py +++ b/src/easyscience/Fitting/multi_fitter.py @@ -121,7 +121,7 @@ def _post_compute_reshaping( # Fill out the new result obj (see EasyScience.Fitting.Fitting_template.FitResults) current_results.success = fit_result_obj.success - current_results.fitting_engine = fit_result_obj.fitting_engine + current_results.minimizer_engine = fit_result_obj.minimizer_engine current_results.p = fit_result_obj.p current_results.p0 = fit_result_obj.p0 current_results.x = this_x diff --git a/tests/integration_tests/Fitting/test_fitter.py b/tests/integration_tests/Fitting/test_fitter.py index 47957fcd..2a82e753 100644 --- a/tests/integration_tests/Fitting/test_fitter.py +++ b/tests/integration_tests/Fitting/test_fitter.py @@ -102,7 +102,7 @@ def test_basic_fit(fit_engine, with_errors): result = f.fit(*args, **kwargs) if fit_engine is not None: - assert result.fitting_engine.name == fit_engine + assert result.minimizer_engine.wrapping == fit_engine 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) From b9951604f82d88225ad2f1c8bd52f13d929659e8 Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Fri, 14 Jun 2024 14:03:31 +0200 Subject: [PATCH 11/24] module renamed from Fitting to fitting --- docs/src/fitting/constraints.rst | 20 +++--- examples_old/dataset_examples.ipynb | 2 +- examples_old/dimer_example.ipynb | 4 +- src/easyscience/Datasets/xarray.py | 10 +-- src/easyscience/Fitting/__init__.py | 3 +- src/easyscience/Objects/Inferface.py | 2 +- src/easyscience/Objects/Variable.py | 2 +- src/easyscience/Objects/virtual.py | 71 ++++++++----------- .../integration_tests/Fitting/test_fitter.py | 6 +- .../Fitting/test_multi_fitter.py | 6 +- tests/integration_tests/test_undoRedo.py | 3 +- tests/unit_tests/Fitting/test_constraints.py | 4 +- tests/unit_tests/Objects/test_Groups.py | 2 +- 13 files changed, 63 insertions(+), 72 deletions(-) diff --git a/docs/src/fitting/constraints.rst b/docs/src/fitting/constraints.rst index 78b4e1ff..d92c87c2 100644 --- a/docs/src/fitting/constraints.rst +++ b/docs/src/fitting/constraints.rst @@ -19,7 +19,7 @@ Constraints on Parameters Constraints on Fitting ^^^^^^^^^^^^^^^^^^^^^^ -:class:`easyscience.Fitting.Fitting.Fitter` has the ability to evaluate user supplied constraints which effect the value of both fixed and non-fixed parameters. A good example of one such use case would be the ratio between two parameters, where you would create a :class:`easyscience.Fitting.Constraints.ObjConstraint`. +:class:`easyscience.fitting.Fitter` has the ability to evaluate user supplied constraints which effect the value of both fixed and non-fixed parameters. A good example of one such use case would be the ratio between two parameters, where you would create a :class:`easyscience.fitting.Constraints.ObjConstraint`. Using constraints ----------------- @@ -28,7 +28,7 @@ A constraint can be used in one of three ways; Assignment to a parameter, assign .. code-block:: python - from easyscience.Fitting.Constraints import NumericConstraint + from easyscience.fitting.Constraints import NumericConstraint from easyscience.Objects.Base import Parameter # Create an `a < 1` constraint a = Parameter('a', 0.5) @@ -41,7 +41,7 @@ A constraint can be used in one of three ways; Assignment to a parameter, assign Constraint Reference -------------------- -.. minigallery:: easyscience.Fitting.Constraints.NumericConstraint +.. minigallery:: easyscience.fitting.Constraints.NumericConstraint :add-heading: Examples using `Constraints` Built-in constraints @@ -49,27 +49,27 @@ Built-in constraints These are the built in constraints which you can use -.. autoclass:: easyscience.Fitting.Constraints.SelfConstraint +.. autoclass:: easyscience.fitting.Constraints.SelfConstraint :members: +enabled -.. autoclass:: easyscience.Fitting.Constraints.NumericConstraint +.. autoclass:: easyscience.fitting.Constraints.NumericConstraint :members: +enabled -.. autoclass:: easyscience.Fitting.Constraints.ObjConstraint +.. autoclass:: easyscience.fitting.Constraints.ObjConstraint :members: +enabled -.. autoclass:: easyscience.Fitting.Constraints.FunctionalConstraint +.. autoclass:: easyscience.fitting.Constraints.FunctionalConstraint :members: +enabled -.. autoclass:: easyscience.Fitting.Constraints.MultiObjConstraint +.. autoclass:: easyscience.fitting.Constraints.MultiObjConstraint :members: +enabled User created constraints ^^^^^^^^^^^^^^^^^^^^^^^^ -You can also make your own constraints by subclassing the :class:`easyscience.Fitting.Constraints.ConstraintBase` class. For this at a minimum the abstract methods ``_parse_operator`` and ``__repr__`` need to be written. +You can also make your own constraints by subclassing the :class:`easyscience.fitting.Constraints.ConstraintBase` class. For this at a minimum the abstract methods ``_parse_operator`` and ``__repr__`` need to be written. -.. autoclass:: easyscience.Fitting.Constraints.ConstraintBase +.. autoclass:: easyscience.fitting.Constraints.ConstraintBase :members: :private-members: :special-members: __repr__ \ No newline at end of file diff --git a/examples_old/dataset_examples.ipynb b/examples_old/dataset_examples.ipynb index 65b0a003..96b0cd19 100644 --- a/examples_old/dataset_examples.ipynb +++ b/examples_old/dataset_examples.ipynb @@ -9,7 +9,7 @@ "import numpy as np\n", "from easyscience.Datasets.xarray import xr\n", "from easyscience.Objects.Base import Parameter, BaseObj\n", - "from easyscience.Fitting.Fitting import Fitter" + "from easyscience.fitting import Fitter" ] }, { diff --git a/examples_old/dimer_example.ipynb b/examples_old/dimer_example.ipynb index bb708115..fc18f014 100644 --- a/examples_old/dimer_example.ipynb +++ b/examples_old/dimer_example.ipynb @@ -18,7 +18,7 @@ "import numpy as np\n", "from easyscience.Datasets.xarray import xr\n", "from easyscience.Objects.Base import Parameter, BaseObj\n", - "from easyscience.Fitting.Fitting import Fitter" + "from easyscience.fitting import Fitter" ] }, { @@ -914,7 +914,7 @@ "metadata": {}, "outputs": [], "source": [ - "from easyscience.Fitting.Fitting import Fitter\n", + "from easyscience.fitting import Fitter\n", "f = Fitter()\n", "f.initialize(sl, sl.dispersion)" ] diff --git a/src/easyscience/Datasets/xarray.py b/src/easyscience/Datasets/xarray.py index 9c56e027..02b8fb4b 100644 --- a/src/easyscience/Datasets/xarray.py +++ b/src/easyscience/Datasets/xarray.py @@ -371,13 +371,13 @@ def fit( **kwargs, ) -> List[FitResults]: """ - Perform a fit on one or more DataArrays. This fit utilises a given fitter from `EasyScience.Fitting.Fitter`, though + Perform a fit on one or more DataArrays. This fit utilises a given fitter from `EasyScience.fitting.Fitter`, though there are a few differences to a standard EasyScience fit. In particular, key-word arguments to control the optimisation algorithm go in the `fit_kwargs` dictionary, fit function key-word arguments go in the `fn_kwargs` and given key-word arguments control the `xarray.apply_ufunc` function. :param fitter: Fitting object which controls the fitting - :type fitter: EasyScience.Fitting.Fitter + :type fitter: EasyScience.fitting.Fitter :param args: Arguments to go to the fit function :type args: Any :param dask: Dask control string. See `xarray.apply_ufunc` documentation @@ -681,13 +681,13 @@ def fit( **kwargs, ) -> FitResults: """ - Perform a fit on the given DataArray. This fit utilises a given fitter from `EasyScience.Fitting.Fitter`, though + Perform a fit on the given DataArray. This fit utilises a given fitter from `EasyScience.fitting.Fitter`, though there are a few differences to a standard EasyScience fit. In particular, key-word arguments to control the optimisation algorithm go in the `fit_kwargs` dictionary, fit function key-word arguments go in the `fn_kwargs` and given key-word arguments control the `xarray.apply_ufunc` function. :param fitter: Fitting object which controls the fitting - :type fitter: EasyScience.Fitting.Fitter + :type fitter: EasyScience.fitting.Fitter :param args: Arguments to go to the fit function :type args: Any :param dask: Dask control string. See `xarray.apply_ufunc` documentation @@ -732,7 +732,7 @@ def local_fit_func(x, *args, **kwargs): # Set the new callable to the fitter and initialize fitter.initialize(fitter.fit_object, local_fit_func) - # Make EasyScience.Fitting.Fitter compatible `x` + # Make EasyScience.fitting.Fitter compatible `x` x_for_fit = xr.concat(bdims, dim='fit_dim') x_for_fit = x_for_fit.stack(all_x=[d.name for d in bdims]) try: diff --git a/src/easyscience/Fitting/__init__.py b/src/easyscience/Fitting/__init__.py index 4c4f3ac1..698b5831 100644 --- a/src/easyscience/Fitting/__init__.py +++ b/src/easyscience/Fitting/__init__.py @@ -1,3 +1,4 @@ from .fitter import Fitter # noqa: F401, E402 from .minimizers.minimizer_base import FitResults # noqa: F401, E402 -from .multi_fitter import MultiFitter # noqa: F401, E402 +# Causes circular import +# from .multi_fitter import MultiFitter # noqa: F401, E402 diff --git a/src/easyscience/Objects/Inferface.py b/src/easyscience/Objects/Inferface.py index 2ff89dd0..ae1c3781 100644 --- a/src/easyscience/Objects/Inferface.py +++ b/src/easyscience/Objects/Inferface.py @@ -65,7 +65,7 @@ def switch(self, new_interface: str, fitter: Optional[Type[Fitter]] = None): :param new_interface: name of new interface to be created :type new_interface: str :param fitter: Fitting interface which contains the fitting object which may have bindings which will be updated. - :type fitter: EasyScience.Fitting.Fitting.Fitter + :type fitter: EasyScience.fitting.Fitter :return: None :rtype: noneType """ diff --git a/src/easyscience/Objects/Variable.py b/src/easyscience/Objects/Variable.py index b9816abb..f4018646 100644 --- a/src/easyscience/Objects/Variable.py +++ b/src/easyscience/Objects/Variable.py @@ -30,7 +30,7 @@ from easyscience import borg from easyscience import pint from easyscience import ureg -from easyscience.Fitting.Constraints import SelfConstraint +from easyscience.fitting.Constraints import SelfConstraint from easyscience.Objects.core import ComponentSerializer from easyscience.Utils.classTools import addProp from easyscience.Utils.Exceptions import CoreSetException diff --git a/src/easyscience/Objects/virtual.py b/src/easyscience/Objects/virtual.py index c938c5d8..e6991e27 100644 --- a/src/easyscience/Objects/virtual.py +++ b/src/easyscience/Objects/virtual.py @@ -4,8 +4,8 @@ from __future__ import annotations -__author__ = "github.com/wardsimon" -__version__ = "0.0.1" +__author__ = 'github.com/wardsimon' +__version__ = '0.0.1' import inspect import weakref @@ -15,7 +15,7 @@ from typing import MutableSequence from easyscience import borg -from easyscience.Fitting.Constraints import ObjConstraint +from easyscience.fitting.Constraints import ObjConstraint if TYPE_CHECKING: from easyscience.Utils.typing import BV @@ -31,8 +31,8 @@ def _remover(a_obj_id: str, v_obj_id: str): a_obj = borg.map.get_item_by_key(int(a_obj_id)) except ValueError: return - if a_obj._constraints["virtual"].get(v_obj_id, False): - del a_obj._constraints["virtual"][v_obj_id] + if a_obj._constraints['virtual'].get(v_obj_id, False): + del a_obj._constraints['virtual'][v_obj_id] def realizer(obj: BV): @@ -41,8 +41,8 @@ def realizer(obj: BV): :param obj: Virtual object which has the property `component` """ - if getattr(obj, "_is_virtual", False): - klass = getattr(obj, "__non_virtual_class__") + if getattr(obj, '_is_virtual', False): + klass = getattr(obj, '__non_virtual_class__') import easyscience.Objects.Variable as ec_var args = [] @@ -78,7 +78,7 @@ def component_realizer(obj: BV, component: str, recursive: bool = True): if not isinstance(obj, Iterable) or not issubclass(obj.__class__, MutableSequence): old_component = obj._kwargs[component] new_components = realizer(obj._kwargs[component]) - if hasattr(new_components, "enabled"): + if hasattr(new_components, 'enabled'): new_components.enabled = True else: old_component = obj[component] @@ -94,14 +94,9 @@ def component_realizer(obj: BV, component: str, recursive: bool = True): else: value = key key = value._borg.map.convert_id_to_key(value) - if ( - getattr(value, "__old_class__", value.__class__) - in ec_var.__dict__.values() - ): + if getattr(value, '__old_class__', value.__class__) in ec_var.__dict__.values(): continue - component._borg.map.prune_vertex_from_edge( - component, component._kwargs[key] - ) + component._borg.map.prune_vertex_from_edge(component, component._kwargs[key]) component._borg.map.add_edge(component, old_component._kwargs[key]) component._kwargs[key] = old_component._kwargs[key] done_mapping = False @@ -125,15 +120,13 @@ def virtualizer(obj: BV) -> BV: :rtype: """ # First check if we're already a virtual object - if getattr(obj, "_is_virtual", False): + if getattr(obj, '_is_virtual', False): new_obj = deepcopy(obj) old_obj = obj._borg.map.get_item_by_key(obj._derived_from) - constraint = ObjConstraint(new_obj, "", old_obj) + constraint = ObjConstraint(new_obj, '', old_obj) constraint.external = True - old_obj._constraints["virtual"][ - str(obj._borg.map.convert_id(new_obj).int) - ] = constraint - new_obj._constraints["builtin"] = dict() + old_obj._constraints['virtual'][str(obj._borg.map.convert_id(new_obj).int)] = constraint + new_obj._constraints['builtin'] = dict() # setattr(new_obj, "__previous_set", getattr(olobj, "__previous_set", None)) weakref.finalize( new_obj, @@ -144,42 +137,40 @@ def virtualizer(obj: BV) -> BV: return new_obj # The supplied class - klass = getattr(obj, "__old_class__", obj.__class__) + klass = getattr(obj, '__old_class__', obj.__class__) virtual_options = { - "_is_virtual": True, - "is_virtual": property(fget=lambda self: self._is_virtual), - "_derived_from": property(fget=lambda self: self._borg.map.convert_id(obj).int), - "__non_virtual_class__": klass, - "realize": realizer, - "relalize_component": component_realizer, + '_is_virtual': True, + 'is_virtual': property(fget=lambda self: self._is_virtual), + '_derived_from': property(fget=lambda self: self._borg.map.convert_id(obj).int), + '__non_virtual_class__': klass, + 'realize': realizer, + 'relalize_component': component_realizer, } import easyscience.Objects.Variable as ec_var if klass in ec_var.__dict__.values(): # is_variable check - virtual_options["fixed"] = property( + virtual_options['fixed'] = property( fget=lambda self: self._fixed, - fset=lambda self, value: raise_( - AttributeError("Virtual parameters cannot be fixed") - ), + fset=lambda self, value: raise_(AttributeError('Virtual parameters cannot be fixed')), ) # Generate a new class - cls = type("Virtual" + klass.__name__, (klass,), virtual_options) + cls = type('Virtual' + klass.__name__, (klass,), virtual_options) # Determine what to do next. args = [] # If `obj` is a parameter or descriptor etc, then simple mods. - if hasattr(obj, "_constructor"): + if hasattr(obj, '_constructor'): # All Variables are based on the Descriptor. d = obj.encode_data() - if hasattr(d, "fixed"): - d["fixed"] = True + if hasattr(d, 'fixed'): + d['fixed'] = True v_p = cls(**d) v_p._enabled = False - constraint = ObjConstraint(v_p, "", obj) + constraint = ObjConstraint(v_p, '', obj) constraint.external = True - obj._constraints["virtual"][str(cls._borg.map.convert_id(v_p).int)] = constraint - v_p._constraints["builtin"] = dict() - setattr(v_p, "__previous_set", getattr(obj, "__previous_set", None)) + obj._constraints['virtual'][str(cls._borg.map.convert_id(v_p).int)] = constraint + v_p._constraints['builtin'] = dict() + setattr(v_p, '__previous_set', getattr(obj, '__previous_set', None)) weakref.finalize( v_p, _remover, diff --git a/tests/integration_tests/Fitting/test_fitter.py b/tests/integration_tests/Fitting/test_fitter.py index 2a82e753..46cf16a9 100644 --- a/tests/integration_tests/Fitting/test_fitter.py +++ b/tests/integration_tests/Fitting/test_fitter.py @@ -8,9 +8,9 @@ import pytest import numpy as np -from easyscience.Fitting.Constraints import ObjConstraint -from easyscience.Fitting.fitter import Fitter -from easyscience.Fitting.minimizers import FitError +from easyscience.fitting.Constraints import ObjConstraint +from easyscience.fitting.fitter import Fitter +from easyscience.fitting.minimizers import FitError from easyscience.Objects.ObjectClasses import BaseObj from easyscience.Objects.ObjectClasses import Parameter diff --git a/tests/integration_tests/Fitting/test_multi_fitter.py b/tests/integration_tests/Fitting/test_multi_fitter.py index 435c7611..41ee81ca 100644 --- a/tests/integration_tests/Fitting/test_multi_fitter.py +++ b/tests/integration_tests/Fitting/test_multi_fitter.py @@ -8,9 +8,9 @@ import pytest import numpy as np -from easyscience.Fitting.Constraints import ObjConstraint -from easyscience.Fitting.multi_fitter import MultiFitter -from easyscience.Fitting.minimizers import FitError +from easyscience.fitting.Constraints import ObjConstraint +from easyscience.fitting.multi_fitter import MultiFitter +from easyscience.fitting.minimizers import FitError from easyscience.Objects.ObjectClasses import BaseObj from easyscience.Objects.ObjectClasses import Parameter diff --git a/tests/integration_tests/test_undoRedo.py b/tests/integration_tests/test_undoRedo.py index bf89b54b..f89e3378 100644 --- a/tests/integration_tests/test_undoRedo.py +++ b/tests/integration_tests/test_undoRedo.py @@ -14,6 +14,7 @@ from easyscience.Objects.ObjectClasses import BaseObj from easyscience.Objects.Variable import Descriptor from easyscience.Objects.Variable import Parameter +from easyscience.fitting import Fitter def createSingleObjs(idx): @@ -265,8 +266,6 @@ def __call__(self, x: np.ndarray) -> np.ndarray: y = l1(x) + 0.125 * (dy - 0.5) - from easyscience.Fitting import Fitter - f = Fitter(l2, l2) try: f.switch_minimizer(fit_engine) diff --git a/tests/unit_tests/Fitting/test_constraints.py b/tests/unit_tests/Fitting/test_constraints.py index 60f37fa0..040b706a 100644 --- a/tests/unit_tests/Fitting/test_constraints.py +++ b/tests/unit_tests/Fitting/test_constraints.py @@ -10,8 +10,8 @@ import pytest -from easyscience.Fitting.Constraints import NumericConstraint -from easyscience.Fitting.Constraints import ObjConstraint +from easyscience.fitting.Constraints import NumericConstraint +from easyscience.fitting.Constraints import ObjConstraint from easyscience.Objects.Variable import Parameter diff --git a/tests/unit_tests/Objects/test_Groups.py b/tests/unit_tests/Objects/test_Groups.py index e0b9757c..5e483bee 100644 --- a/tests/unit_tests/Objects/test_Groups.py +++ b/tests/unit_tests/Objects/test_Groups.py @@ -394,7 +394,7 @@ def test_baseCollection_constraints(cls): p1 = Parameter("p1", 1) p2 = Parameter("p2", 2) - from easyscience.Fitting.Constraints import ObjConstraint + from easyscience.fitting.Constraints import ObjConstraint p2.user_constraints["testing"] = ObjConstraint(p2, "2*", p1) From e957287559980f1fb0da12a357f00f1ad37f0883 Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Fri, 14 Jun 2024 14:17:22 +0200 Subject: [PATCH 12/24] force folder rename --- src/easyscience/{Fitting => fitting1}/Constraints.py | 0 src/easyscience/{Fitting => fitting1}/__init__.py | 0 src/easyscience/{Fitting => fitting1}/fitter.py | 0 src/easyscience/{Fitting => fitting1}/minimizers/__init__.py | 0 src/easyscience/{Fitting => fitting1}/minimizers/factory.py | 0 .../{Fitting => fitting1}/minimizers/minimizer_base.py | 0 .../{Fitting => fitting1}/minimizers/minimizer_bumps.py | 0 src/easyscience/{Fitting => fitting1}/minimizers/minimizer_dfo.py | 0 .../{Fitting => fitting1}/minimizers/minimizer_lmfit.py | 0 src/easyscience/{Fitting => fitting1}/minimizers/utils.py | 0 src/easyscience/{Fitting => fitting1}/multi_fitter.py | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename src/easyscience/{Fitting => fitting1}/Constraints.py (100%) rename src/easyscience/{Fitting => fitting1}/__init__.py (100%) rename src/easyscience/{Fitting => fitting1}/fitter.py (100%) rename src/easyscience/{Fitting => fitting1}/minimizers/__init__.py (100%) rename src/easyscience/{Fitting => fitting1}/minimizers/factory.py (100%) rename src/easyscience/{Fitting => fitting1}/minimizers/minimizer_base.py (100%) rename src/easyscience/{Fitting => fitting1}/minimizers/minimizer_bumps.py (100%) rename src/easyscience/{Fitting => fitting1}/minimizers/minimizer_dfo.py (100%) rename src/easyscience/{Fitting => fitting1}/minimizers/minimizer_lmfit.py (100%) rename src/easyscience/{Fitting => fitting1}/minimizers/utils.py (100%) rename src/easyscience/{Fitting => fitting1}/multi_fitter.py (100%) diff --git a/src/easyscience/Fitting/Constraints.py b/src/easyscience/fitting1/Constraints.py similarity index 100% rename from src/easyscience/Fitting/Constraints.py rename to src/easyscience/fitting1/Constraints.py diff --git a/src/easyscience/Fitting/__init__.py b/src/easyscience/fitting1/__init__.py similarity index 100% rename from src/easyscience/Fitting/__init__.py rename to src/easyscience/fitting1/__init__.py diff --git a/src/easyscience/Fitting/fitter.py b/src/easyscience/fitting1/fitter.py similarity index 100% rename from src/easyscience/Fitting/fitter.py rename to src/easyscience/fitting1/fitter.py diff --git a/src/easyscience/Fitting/minimizers/__init__.py b/src/easyscience/fitting1/minimizers/__init__.py similarity index 100% rename from src/easyscience/Fitting/minimizers/__init__.py rename to src/easyscience/fitting1/minimizers/__init__.py diff --git a/src/easyscience/Fitting/minimizers/factory.py b/src/easyscience/fitting1/minimizers/factory.py similarity index 100% rename from src/easyscience/Fitting/minimizers/factory.py rename to src/easyscience/fitting1/minimizers/factory.py diff --git a/src/easyscience/Fitting/minimizers/minimizer_base.py b/src/easyscience/fitting1/minimizers/minimizer_base.py similarity index 100% rename from src/easyscience/Fitting/minimizers/minimizer_base.py rename to src/easyscience/fitting1/minimizers/minimizer_base.py diff --git a/src/easyscience/Fitting/minimizers/minimizer_bumps.py b/src/easyscience/fitting1/minimizers/minimizer_bumps.py similarity index 100% rename from src/easyscience/Fitting/minimizers/minimizer_bumps.py rename to src/easyscience/fitting1/minimizers/minimizer_bumps.py diff --git a/src/easyscience/Fitting/minimizers/minimizer_dfo.py b/src/easyscience/fitting1/minimizers/minimizer_dfo.py similarity index 100% rename from src/easyscience/Fitting/minimizers/minimizer_dfo.py rename to src/easyscience/fitting1/minimizers/minimizer_dfo.py diff --git a/src/easyscience/Fitting/minimizers/minimizer_lmfit.py b/src/easyscience/fitting1/minimizers/minimizer_lmfit.py similarity index 100% rename from src/easyscience/Fitting/minimizers/minimizer_lmfit.py rename to src/easyscience/fitting1/minimizers/minimizer_lmfit.py diff --git a/src/easyscience/Fitting/minimizers/utils.py b/src/easyscience/fitting1/minimizers/utils.py similarity index 100% rename from src/easyscience/Fitting/minimizers/utils.py rename to src/easyscience/fitting1/minimizers/utils.py diff --git a/src/easyscience/Fitting/multi_fitter.py b/src/easyscience/fitting1/multi_fitter.py similarity index 100% rename from src/easyscience/Fitting/multi_fitter.py rename to src/easyscience/fitting1/multi_fitter.py From d85932b15d5d9c0a0ac3360764f36efddab7078a Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Fri, 14 Jun 2024 14:17:50 +0200 Subject: [PATCH 13/24] force folder rename --- src/easyscience/{fitting1 => fitting}/Constraints.py | 0 src/easyscience/{fitting1 => fitting}/__init__.py | 0 src/easyscience/{fitting1 => fitting}/fitter.py | 0 src/easyscience/{fitting1 => fitting}/minimizers/__init__.py | 0 src/easyscience/{fitting1 => fitting}/minimizers/factory.py | 0 .../{fitting1 => fitting}/minimizers/minimizer_base.py | 0 .../{fitting1 => fitting}/minimizers/minimizer_bumps.py | 0 src/easyscience/{fitting1 => fitting}/minimizers/minimizer_dfo.py | 0 .../{fitting1 => fitting}/minimizers/minimizer_lmfit.py | 0 src/easyscience/{fitting1 => fitting}/minimizers/utils.py | 0 src/easyscience/{fitting1 => fitting}/multi_fitter.py | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename src/easyscience/{fitting1 => fitting}/Constraints.py (100%) rename src/easyscience/{fitting1 => fitting}/__init__.py (100%) rename src/easyscience/{fitting1 => fitting}/fitter.py (100%) rename src/easyscience/{fitting1 => fitting}/minimizers/__init__.py (100%) rename src/easyscience/{fitting1 => fitting}/minimizers/factory.py (100%) rename src/easyscience/{fitting1 => fitting}/minimizers/minimizer_base.py (100%) rename src/easyscience/{fitting1 => fitting}/minimizers/minimizer_bumps.py (100%) rename src/easyscience/{fitting1 => fitting}/minimizers/minimizer_dfo.py (100%) rename src/easyscience/{fitting1 => fitting}/minimizers/minimizer_lmfit.py (100%) rename src/easyscience/{fitting1 => fitting}/minimizers/utils.py (100%) rename src/easyscience/{fitting1 => fitting}/multi_fitter.py (100%) diff --git a/src/easyscience/fitting1/Constraints.py b/src/easyscience/fitting/Constraints.py similarity index 100% rename from src/easyscience/fitting1/Constraints.py rename to src/easyscience/fitting/Constraints.py diff --git a/src/easyscience/fitting1/__init__.py b/src/easyscience/fitting/__init__.py similarity index 100% rename from src/easyscience/fitting1/__init__.py rename to src/easyscience/fitting/__init__.py diff --git a/src/easyscience/fitting1/fitter.py b/src/easyscience/fitting/fitter.py similarity index 100% rename from src/easyscience/fitting1/fitter.py rename to src/easyscience/fitting/fitter.py diff --git a/src/easyscience/fitting1/minimizers/__init__.py b/src/easyscience/fitting/minimizers/__init__.py similarity index 100% rename from src/easyscience/fitting1/minimizers/__init__.py rename to src/easyscience/fitting/minimizers/__init__.py diff --git a/src/easyscience/fitting1/minimizers/factory.py b/src/easyscience/fitting/minimizers/factory.py similarity index 100% rename from src/easyscience/fitting1/minimizers/factory.py rename to src/easyscience/fitting/minimizers/factory.py diff --git a/src/easyscience/fitting1/minimizers/minimizer_base.py b/src/easyscience/fitting/minimizers/minimizer_base.py similarity index 100% rename from src/easyscience/fitting1/minimizers/minimizer_base.py rename to src/easyscience/fitting/minimizers/minimizer_base.py diff --git a/src/easyscience/fitting1/minimizers/minimizer_bumps.py b/src/easyscience/fitting/minimizers/minimizer_bumps.py similarity index 100% rename from src/easyscience/fitting1/minimizers/minimizer_bumps.py rename to src/easyscience/fitting/minimizers/minimizer_bumps.py diff --git a/src/easyscience/fitting1/minimizers/minimizer_dfo.py b/src/easyscience/fitting/minimizers/minimizer_dfo.py similarity index 100% rename from src/easyscience/fitting1/minimizers/minimizer_dfo.py rename to src/easyscience/fitting/minimizers/minimizer_dfo.py diff --git a/src/easyscience/fitting1/minimizers/minimizer_lmfit.py b/src/easyscience/fitting/minimizers/minimizer_lmfit.py similarity index 100% rename from src/easyscience/fitting1/minimizers/minimizer_lmfit.py rename to src/easyscience/fitting/minimizers/minimizer_lmfit.py diff --git a/src/easyscience/fitting1/minimizers/utils.py b/src/easyscience/fitting/minimizers/utils.py similarity index 100% rename from src/easyscience/fitting1/minimizers/utils.py rename to src/easyscience/fitting/minimizers/utils.py diff --git a/src/easyscience/fitting1/multi_fitter.py b/src/easyscience/fitting/multi_fitter.py similarity index 100% rename from src/easyscience/fitting1/multi_fitter.py rename to src/easyscience/fitting/multi_fitter.py From 0180f01ffbb236683312958d23104f118c19ba97 Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Fri, 14 Jun 2024 14:25:46 +0200 Subject: [PATCH 14/24] more Fitting to fitting --- src/easyscience/Datasets/xarray.py | 2 +- src/easyscience/fitting/Constraints.py | 16 ++++++++-------- src/easyscience/fitting/multi_fitter.py | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/easyscience/Datasets/xarray.py b/src/easyscience/Datasets/xarray.py index 02b8fb4b..b8b27c76 100644 --- a/src/easyscience/Datasets/xarray.py +++ b/src/easyscience/Datasets/xarray.py @@ -20,7 +20,7 @@ import xarray as xr from easyscience import ureg -from easyscience.Fitting import FitResults +from easyscience.fitting import FitResults T_ = TypeVar('T_') diff --git a/src/easyscience/fitting/Constraints.py b/src/easyscience/fitting/Constraints.py index c2405c32..9d947386 100644 --- a/src/easyscience/fitting/Constraints.py +++ b/src/easyscience/fitting/Constraints.py @@ -187,7 +187,7 @@ def __init__(self, dependent_obj: V, operator: str, value: Number): :example: .. code-block:: python - from easyscience.Fitting.Constraints import NumericConstraint + from easyscience.fitting.Constraints import NumericConstraint from easyscience.Objects.Base import Parameter # Create an `a < 1` constraint a = Parameter('a', 0.2) @@ -243,7 +243,7 @@ def __init__(self, dependent_obj: V, operator: str, value: str): :example: .. code-block:: python - from easyscience.Fitting.Constraints import SelfConstraint + from easyscience.fitting.Constraints import SelfConstraint from easyscience.Objects.Base import Parameter # Create an `a < a.max` constraint a = Parameter('a', 0.2, max=1) @@ -297,7 +297,7 @@ def __init__(self, dependent_obj: V, operator: str, independent_obj: V): :example: .. code-block:: python - from easyscience.Fitting.Constraints import ObjConstraint + from easyscience.fitting.Constraints import ObjConstraint from easyscience.Objects.Base import Parameter # Create an `a = 2 * b` constraint a = Parameter('a', 0.2) @@ -331,7 +331,7 @@ def __repr__(self) -> str: class MultiObjConstraint(ConstraintBase): """ - A `MultiObjConstraint` is similar to :class:`EasyScience.Fitting.Constraints.ObjConstraint` except that it relates to + A `MultiObjConstraint` is similar to :class:`EasyScience.fitting.Constraints.ObjConstraint` except that it relates to multiple independent objects. """ @@ -343,7 +343,7 @@ def __init__( value: Number, ): """ - A `MultiObjConstraint` is similar to :class:`EasyScience.Fitting.Constraints.ObjConstraint` except that it relates + A `MultiObjConstraint` is similar to :class:`EasyScience.fitting.Constraints.ObjConstraint` except that it relates to one or more independent objects. E.g. @@ -360,7 +360,7 @@ def __init__( .. code-block:: python - from easyscience.Fitting.Constraints import MultiObjConstraint + from easyscience.fitting.Constraints import MultiObjConstraint from easyscience.Objects.Base import Parameter # Create an `a + b = 1` constraint a = Parameter('a', 0.2) @@ -376,7 +376,7 @@ def __init__( .. code-block:: python - from easyscience.Fitting.Constraints import MultiObjConstraint + from easyscience.fitting.Constraints import MultiObjConstraint from easyscience.Objects.Base import Parameter # Create an `a + b - 2c = 0` constraint a = Parameter('a', 0.5) @@ -442,7 +442,7 @@ def __init__( .. code-block:: python import numpy as np - from easyscience.Fitting.Constraints import FunctionalConstraint + from easyscience.fitting.Constraints import FunctionalConstraint from easyscience.Objects.Base import Parameter a = Parameter('a', 0.2, max=1) diff --git a/src/easyscience/fitting/multi_fitter.py b/src/easyscience/fitting/multi_fitter.py index 010259b1..6f404ca4 100644 --- a/src/easyscience/fitting/multi_fitter.py +++ b/src/easyscience/fitting/multi_fitter.py @@ -119,7 +119,7 @@ def _post_compute_reshaping( current_results = cls() ep = sp + int(np.array(self._dependent_dims[idx]).prod()) - # Fill out the new result obj (see EasyScience.Fitting.Fitting_template.FitResults) + # Fill out the new result obj (see EasyScience.fitting.Fitting_template.FitResults) current_results.success = fit_result_obj.success current_results.minimizer_engine = fit_result_obj.minimizer_engine current_results.p = fit_result_obj.p From 8ea0efc74387b7fcc8346c628a49e3783289a116 Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Mon, 17 Jun 2024 06:31:32 +0200 Subject: [PATCH 15/24] better naming --- src/easyscience/fitting/fitter.py | 10 ++-- src/easyscience/fitting/minimizers/factory.py | 60 +++++++++---------- .../fitting/minimizers/minimizer_base.py | 12 ---- .../fitting/minimizers/minimizer_bumps.py | 1 - .../fitting/minimizers/minimizer_dfo.py | 1 - .../fitting/minimizers/minimizer_lmfit.py | 1 - 6 files changed, 34 insertions(+), 51 deletions(-) diff --git a/src/easyscience/fitting/fitter.py b/src/easyscience/fitting/fitter.py index 0bd0c752..54974fe4 100644 --- a/src/easyscience/fitting/fitter.py +++ b/src/easyscience/fitting/fitter.py @@ -10,9 +10,7 @@ from .minimizers import FitResults from .minimizers import MinimizerBase -from .minimizers.factory import AvailableMinimizers -from .minimizers.factory import from_string -from .minimizers.factory import minimizer_class_factory +from .minimizers import factory as minmizer_factory DEFAULT_MINIMIZER = 'lmfit-leastsq' @@ -87,8 +85,8 @@ def switch_minimizer(self, minimizer_name: str) -> None: self._minimizer._constraints = constraints def _update_minimizer(self, minimizer_name: str) -> None: - minimizer_enum = from_string(minimizer_name) - self._minimizer = minimizer_class_factory( + minimizer_enum = minmizer_factory.from_string_to_enum(minimizer_name) + self._minimizer = minmizer_factory.factory( minimizer_enum=minimizer_enum, fit_object=self._fit_object, fit_function=self.fit_function ) self._name_current_minimizer = minimizer_name @@ -101,7 +99,7 @@ def available_minimizers(self) -> List[str]: :return: List of available fitting minimizers :rtype: List[str] """ - return [minimize.name for minimize in AvailableMinimizers] + return [minimize.name for minimize in minmizer_factory.AvailableMinimizers] @property def minimizer(self) -> MinimizerBase: diff --git a/src/easyscience/fitting/minimizers/factory.py b/src/easyscience/fitting/minimizers/factory.py index fe66c82a..af43fd3f 100644 --- a/src/easyscience/fitting/minimizers/factory.py +++ b/src/easyscience/fitting/minimizers/factory.py @@ -5,77 +5,77 @@ from .minimizer_base import MinimizerBase -lmfit_minimizer_imported = False +lmfit_engine_imported = False try: from .minimizer_lmfit import LMFit - lmfit_minimizer_imported = True + 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_minimizer_imported = False +bumps_engine_imported = False try: from .minimizer_bumps import Bumps - bumps_minimizer_imported = True + 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) -dfo_minimizer_imported = False +dfo_engine_imported = False try: from .minimizer_dfo import DFO - dfo_minimizer_imported = True + 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_minimizer_imported: + if lmfit_engine_imported: LMFit = auto() LMFit_leastsq = auto() LMFit_powell = auto() LMFit_cobyla = auto() - if bumps_minimizer_imported: + if bumps_engine_imported: Bumps = auto() Bumps_simplex = auto() Bumps_newton = auto() Bumps_lm = auto() - if dfo_minimizer_imported: + if dfo_engine_imported: DFO = auto() -def from_string(engine_name: str) -> AvailableMinimizers: - if engine_name == 'lmfit': - engine_enum = AvailableMinimizers.LMFit - elif engine_name == 'lmfit-leastsq': - engine_enum = AvailableMinimizers.LMFit_leastsq - elif engine_name == 'lmfit-powell': - engine_enum = AvailableMinimizers.LMFit_powell - elif engine_name == 'lmfit-cobyla': - engine_enum = AvailableMinimizers.LMFit_cobyla +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 engine_name == 'bumps': - engine_enum = AvailableMinimizers.Bumps - elif engine_name == 'bumps-simplex': - engine_enum = AvailableMinimizers.Bumps_simplex - elif engine_name == 'bumps-newton': - engine_enum = AvailableMinimizers.Bumps_newton - elif engine_name == 'bumps-lm': - engine_enum = AvailableMinimizers.Bumps_lm + 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 engine_name == 'dfo_ls': - engine_enum = AvailableMinimizers.DFO + elif minimizer_name == 'dfo_ls': + minmizer_enum = AvailableMinimizers.DFO - return engine_enum + return minmizer_enum -def minimizer_class_factory(minimizer_enum: AvailableMinimizers, fit_object, fit_function: Callable) -> MinimizerBase: +def factory(minimizer_enum: AvailableMinimizers, fit_object, fit_function: Callable) -> MinimizerBase: if minimizer_enum == AvailableMinimizers.LMFit: minimizer = LMFit(obj=fit_object, fit_function=fit_function, method='leastsq') elif minimizer_enum == AvailableMinimizers.LMFit_leastsq: diff --git a/src/easyscience/fitting/minimizers/minimizer_base.py b/src/easyscience/fitting/minimizers/minimizer_base.py index 498ac0eb..952f74f6 100644 --- a/src/easyscience/fitting/minimizers/minimizer_base.py +++ b/src/easyscience/fitting/minimizers/minimizer_base.py @@ -19,18 +19,6 @@ class MinimizerBase(metaclass=ABCMeta): This template class is the basis for all fitting engines in `EasyScience`. """ - # _engines = [] - # property_type = None - # name: str = '' - - # def __init_subclass__(cls, is_abstract: bool = False, **kwargs): - # super().__init_subclass__(**kwargs) - # if not is_abstract: - # # Deal with the issue of people not reading the schema. - # if not hasattr(cls, 'name'): - # setattr(cls, 'name', cls.__class__.__name__) - # cls._engines.append(cls) - def __init__(self, obj, fit_function: Callable, method: Optional[str] = None): self._object = obj self._original_fit_function = fit_function diff --git a/src/easyscience/fitting/minimizers/minimizer_bumps.py b/src/easyscience/fitting/minimizers/minimizer_bumps.py index cb90df0d..0c0862d5 100644 --- a/src/easyscience/fitting/minimizers/minimizer_bumps.py +++ b/src/easyscience/fitting/minimizers/minimizer_bumps.py @@ -26,7 +26,6 @@ class Bumps(MinimizerBase): # noqa: S101 It allows for the Bumps fitting engine to use parameters declared in an `EasyScience.Objects.Base.BaseObj`. """ - # property_type = BumpsParameter wrapping = 'bumps' def __init__(self, obj, fit_function: Callable, method: Optional[str] = None): diff --git a/src/easyscience/fitting/minimizers/minimizer_dfo.py b/src/easyscience/fitting/minimizers/minimizer_dfo.py index 7f5b6d8f..5b73192a 100644 --- a/src/easyscience/fitting/minimizers/minimizer_dfo.py +++ b/src/easyscience/fitting/minimizers/minimizer_dfo.py @@ -21,7 +21,6 @@ class DFO(MinimizerBase): # noqa: S101 This is a wrapper to Derivative Free Optimisation for Least Square: https://numericalalgorithmsgroup.github.io/dfols/ """ - # property_type = Number wrapping = 'dfo_ls' def __init__(self, obj, fit_function: Callable, method: Optional[str] = None): diff --git a/src/easyscience/fitting/minimizers/minimizer_lmfit.py b/src/easyscience/fitting/minimizers/minimizer_lmfit.py index 1928ada3..7f5e5910 100644 --- a/src/easyscience/fitting/minimizers/minimizer_lmfit.py +++ b/src/easyscience/fitting/minimizers/minimizer_lmfit.py @@ -25,7 +25,6 @@ class LMFit(MinimizerBase): # noqa: S101 It allows for the lmfit fitting engine to use parameters declared in an `EasyScience.Objects.Base.BaseObj`. """ - # property_type = LMParameter wrapping = 'lmfit' def make_model(self, pars: Optional[LMParameters] = None) -> LMModel: From 55f9c7d7a181bb603ad25a0f213df47f18ba6025 Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Mon, 17 Jun 2024 06:49:45 +0200 Subject: [PATCH 16/24] code cleaning --- src/easyscience/fitting/minimizers/factory.py | 9 +++++++-- src/easyscience/fitting/minimizers/minimizer_base.py | 10 ---------- src/easyscience/fitting/minimizers/minimizer_dfo.py | 2 +- tests/integration_tests/Fitting/test_fitter.py | 10 +++++----- tests/integration_tests/Fitting/test_multi_fitter.py | 6 +++--- tests/integration_tests/test_undoRedo.py | 2 +- 6 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/easyscience/fitting/minimizers/factory.py b/src/easyscience/fitting/minimizers/factory.py index af43fd3f..f8ff85d5 100644 --- a/src/easyscience/fitting/minimizers/factory.py +++ b/src/easyscience/fitting/minimizers/factory.py @@ -48,6 +48,7 @@ class AvailableMinimizers(Enum): if dfo_engine_imported: DFO = auto() + DFO_leastsq = auto() def from_string_to_enum(minimizer_name: str) -> AvailableMinimizers: @@ -69,8 +70,10 @@ def from_string_to_enum(minimizer_name: str) -> AvailableMinimizers: elif minimizer_name == 'bumps-lm': minmizer_enum = AvailableMinimizers.Bumps_lm - elif minimizer_name == 'dfo_ls': + elif minimizer_name == 'dfo': minmizer_enum = AvailableMinimizers.DFO + elif minimizer_name == 'dfo-leastsq': + minmizer_enum = AvailableMinimizers.DFO_leastsq return minmizer_enum @@ -95,6 +98,8 @@ def factory(minimizer_enum: AvailableMinimizers, fit_object, fit_function: Calla minimizer = Bumps(obj=fit_object, fit_function=fit_function, method='lm') elif minimizer_enum == AvailableMinimizers.DFO: - minimizer = DFO(obj=fit_object, fit_function=fit_function) + minimizer = DFO(obj=fit_object, fit_function=fit_function, method='leastsq') + elif minimizer_enum == AvailableMinimizers.DFO_leastsq: + minimizer = DFO(obj=fit_object, fit_function=fit_function, method='leastsq') return minimizer diff --git a/src/easyscience/fitting/minimizers/minimizer_base.py b/src/easyscience/fitting/minimizers/minimizer_base.py index 952f74f6..94d727cc 100644 --- a/src/easyscience/fitting/minimizers/minimizer_base.py +++ b/src/easyscience/fitting/minimizers/minimizer_base.py @@ -5,7 +5,6 @@ from abc import ABCMeta from abc import abstractmethod from typing import Callable -from typing import List from typing import Optional from typing import Union @@ -155,15 +154,6 @@ def _gen_fit_results(self, fit_results, **kwargs) -> 'FitResults': :rtype: FitResults """ - @abstractmethod - def available_methods(self) -> List[str]: - """ - Generate a list of available methods - - :return: List of available methods for minimization - :rtype: List[str] - """ - @staticmethod def _error_from_jacobian(jacobian: np.ndarray, residuals: np.ndarray, confidence: float = 0.95) -> np.ndarray: from scipy import stats diff --git a/src/easyscience/fitting/minimizers/minimizer_dfo.py b/src/easyscience/fitting/minimizers/minimizer_dfo.py index 5b73192a..ae3ba93f 100644 --- a/src/easyscience/fitting/minimizers/minimizer_dfo.py +++ b/src/easyscience/fitting/minimizers/minimizer_dfo.py @@ -21,7 +21,7 @@ class DFO(MinimizerBase): # noqa: S101 This is a wrapper to Derivative Free Optimisation for Least Square: https://numericalalgorithmsgroup.github.io/dfols/ """ - wrapping = 'dfo_ls' + wrapping = 'dfo' def __init__(self, obj, fit_function: Callable, method: Optional[str] = None): """ diff --git a/tests/integration_tests/Fitting/test_fitter.py b/tests/integration_tests/Fitting/test_fitter.py index 46cf16a9..82edd8c3 100644 --- a/tests/integration_tests/Fitting/test_fitter.py +++ b/tests/integration_tests/Fitting/test_fitter.py @@ -78,7 +78,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_ls"]) +@pytest.mark.parametrize("fit_engine", [None, "lmfit", "bumps", "dfo"]) def test_basic_fit(fit_engine, with_errors): ref_sin = AbsSin(0.2, np.pi) sp_sin = AbsSin(0.354, 3.05) @@ -107,7 +107,7 @@ def test_basic_fit(fit_engine, with_errors): 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_ls"]) +@pytest.mark.parametrize("fit_engine", [None, "lmfit", "bumps", "dfo"]) def test_fit_result(fit_engine): ref_sin = AbsSin(0.2, np.pi) sp_sin = AbsSin(0.354, 3.05) @@ -175,7 +175,7 @@ def test_bumps_methods(fit_method): check_fit_results(result, sp_sin, ref_sin, x) -@pytest.mark.parametrize("fit_engine", ["lmfit", "bumps", "dfo_ls"]) +@pytest.mark.parametrize("fit_engine", ["lmfit", "bumps", "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 +205,7 @@ def test_fit_constraints(fit_engine): @pytest.mark.parametrize("with_errors", [False, True]) -@pytest.mark.parametrize("fit_engine", [None, "lmfit", "bumps", "dfo_ls"]) +@pytest.mark.parametrize("fit_engine", [None, "lmfit", "bumps", "dfo"]) def test_2D_vectorized(fit_engine, with_errors): x = np.linspace(0, 5, 200) mm = AbsSin2D(0.3, 1.6) @@ -241,7 +241,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_ls"]) +@pytest.mark.parametrize("fit_engine", [None, "lmfit", "bumps", "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 41ee81ca..1d6af1ff 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_ls"]) +@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_ls"]) +@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_ls"]) +@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/test_undoRedo.py b/tests/integration_tests/test_undoRedo.py index f89e3378..fe8f0366 100644 --- a/tests/integration_tests/test_undoRedo.py +++ b/tests/integration_tests/test_undoRedo.py @@ -230,7 +230,7 @@ def test_UndoRedoMacros(): assert item.raw_value == old_value + offset -@pytest.mark.parametrize("fit_engine", ["lmfit", "bumps", "dfo_ls"]) +@pytest.mark.parametrize("fit_engine", ["lmfit", "bumps", "dfo"]) def test_fittingUndoRedo(fit_engine): m_value = 6 c_value = 2 From 06a8540809130c612878ee1fff15ec27e8f27c3f Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Mon, 17 Jun 2024 11:02:27 +0200 Subject: [PATCH 17/24] unit tests for fitter --- src/easyscience/fitting/fitter.py | 26 +-- .../fitting/minimizers/minimizer_base.py | 3 + .../integration_tests/Fitting/test_fitter.py | 4 +- tests/unit_tests/Fitting/test_fitter.py | 210 ++++++++++++++++++ 4 files changed, 223 insertions(+), 20 deletions(-) create mode 100644 tests/unit_tests/Fitting/test_fitter.py diff --git a/src/easyscience/fitting/fitter.py b/src/easyscience/fitting/fitter.py index 54974fe4..2f0d18c2 100644 --- a/src/easyscience/fitting/fitter.py +++ b/src/easyscience/fitting/fitter.py @@ -10,7 +10,9 @@ from .minimizers import FitResults from .minimizers import MinimizerBase -from .minimizers import factory as minmizer_factory +from .minimizers.factory import AvailableMinimizers +from .minimizers.factory import factory +from .minimizers.factory import from_string_to_enum DEFAULT_MINIMIZER = 'lmfit-leastsq' @@ -47,16 +49,6 @@ def evaluate(self, pars=None) -> np.ndarray: def convert_to_pars_obj(self, pars) -> object: return self._minimizer.convert_to_pars_obj(pars) - def available_methods(self) -> list: - """ - Return the available fitting methods for minimizer engine contaning the current minimizer. - This should only be used to inspect potential methods. - The supported minizers are the ones in the minimizers.factory.AvailableMinimizers enum. - - :return: List of available fitting methods - """ - return self._minimizer.available_methods() - def initialize(self, fit_object, fit_function: Callable) -> None: """ Set the model and callable in the calculator interface. @@ -80,15 +72,13 @@ def switch_minimizer(self, minimizer_name: str) -> None: Switch minimizer and initialize. :param minimizer_name: The label of the minimizer to create and instantiate. """ - constraints = self._minimizer._constraints + constraints = self._minimizer.fit_constraints() self._update_minimizer(minimizer_name) - self._minimizer._constraints = constraints + self._minimizer.set_fit_constraint(constraints) def _update_minimizer(self, minimizer_name: str) -> None: - minimizer_enum = minmizer_factory.from_string_to_enum(minimizer_name) - self._minimizer = minmizer_factory.factory( - minimizer_enum=minimizer_enum, fit_object=self._fit_object, fit_function=self.fit_function - ) + minimizer_enum = from_string_to_enum(minimizer_name) + self._minimizer = factory(minimizer_enum=minimizer_enum, fit_object=self._fit_object, fit_function=self.fit_function) self._name_current_minimizer = minimizer_name @property @@ -99,7 +89,7 @@ def available_minimizers(self) -> List[str]: :return: List of available fitting minimizers :rtype: List[str] """ - return [minimize.name for minimize in minmizer_factory.AvailableMinimizers] + return [minimize.name for minimize in AvailableMinimizers] @property def minimizer(self) -> MinimizerBase: diff --git a/src/easyscience/fitting/minimizers/minimizer_base.py b/src/easyscience/fitting/minimizers/minimizer_base.py index 94d727cc..578be9d3 100644 --- a/src/easyscience/fitting/minimizers/minimizer_base.py +++ b/src/easyscience/fitting/minimizers/minimizer_base.py @@ -36,6 +36,9 @@ def all_constraints(self) -> list: def fit_constraints(self) -> list: return self._constraints + def set_fit_constraint(self, constraints): + self._constraints = constraints + def add_fit_constraint(self, constraint): self._constraints.append(constraint) diff --git a/tests/integration_tests/Fitting/test_fitter.py b/tests/integration_tests/Fitting/test_fitter.py index 82edd8c3..2449fccb 100644 --- a/tests/integration_tests/Fitting/test_fitter.py +++ b/tests/integration_tests/Fitting/test_fitter.py @@ -151,7 +151,7 @@ def test_lmfit_methods(fit_method): sp_sin.phase.fixed = False f = Fitter(sp_sin, sp_sin) - assert fit_method in f.available_methods() + 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) @@ -170,7 +170,7 @@ def test_bumps_methods(fit_method): f = Fitter(sp_sin, sp_sin) f.switch_minimizer("bumps") - assert fit_method in f.available_methods() + 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) diff --git a/tests/unit_tests/Fitting/test_fitter.py b/tests/unit_tests/Fitting/test_fitter.py new file mode 100644 index 00000000..50cb1e84 --- /dev/null +++ b/tests/unit_tests/Fitting/test_fitter.py @@ -0,0 +1,210 @@ +from unittest.mock import MagicMock + +import pytest +from easyscience.fitting.fitter import Fitter +import easyscience.fitting.fitter + + +class TestFitter(): + @pytest.fixture + def fitter(self, monkeypatch): + monkeypatch.setattr(Fitter, '_update_minimizer', MagicMock()) + self.mock_fit_object = MagicMock() + self.mock_fit_function = MagicMock() + return Fitter(self.mock_fit_object, self.mock_fit_function) + + def test_constructor(self, fitter: Fitter): + # When Then Expect + 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') + + def test_fit_constraints(self, fitter: Fitter): + # When + mock_minimizer = MagicMock() + mock_minimizer.fit_constraints = MagicMock(return_value='constraints') + fitter._minimizer = mock_minimizer + + # Then + constraints = fitter.fit_constraints() + + # Expect + assert constraints == 'constraints' + + def test_add_fit_constraint(self, fitter: Fitter): + # When + mock_minimizer = MagicMock() + mock_minimizer.add_fit_constraint = MagicMock() + fitter._minimizer = mock_minimizer + + # Then + fitter.add_fit_constraint('constraints') + + # Expect + mock_minimizer.add_fit_constraint.assert_called_once_with('constraints') + + def test_remove_fit_constraint(self, fitter: Fitter): + # When + mock_minimizer = MagicMock() + mock_minimizer.remove_fit_constraint = MagicMock() + fitter._minimizer = mock_minimizer + + # Then + fitter.remove_fit_constraint(10) + + # Expect + mock_minimizer.remove_fit_constraint.assert_called_once_with(10) + + def test_make_model(self, fitter: Fitter): + # When + mock_minimizer = MagicMock() + mock_minimizer.make_model = MagicMock(return_value='model') + fitter._minimizer = mock_minimizer + + # Then + model = fitter.make_model('pars') + + # Expect + assert model == 'model' + mock_minimizer.make_model.assert_called_once_with('pars') + + def test_evaluate(self, fitter: Fitter): + # When + mock_minimizer = MagicMock() + mock_minimizer.evaluate = MagicMock(return_value='result') + fitter._minimizer = mock_minimizer + + # Then + result = fitter.evaluate('pars') + + # Expect + assert result == 'result' + mock_minimizer.evaluate.assert_called_once_with('pars') + + def test_convert_to_pars_obj(self, fitter: Fitter): + # When + mock_minimizer = MagicMock() + mock_minimizer.convert_to_pars_obj = MagicMock(return_value='obj') + fitter._minimizer = mock_minimizer + + # Then + obj = fitter.convert_to_pars_obj('pars') + + # Expect + assert obj == 'obj' + mock_minimizer.convert_to_pars_obj.assert_called_once_with('pars') + + def test_initialize(self, fitter: Fitter): + # When + mock_fit_object = MagicMock() + mock_fit_function = MagicMock() + + # Then + fitter.initialize(mock_fit_object, mock_fit_function) + + # Expect + assert fitter._fit_object == mock_fit_object + assert fitter._fit_function == mock_fit_function + fitter._update_minimizer.count(2) + + def test_create(self, fitter: Fitter): + # When + fitter._update_minimizer = MagicMock() + + # Then + fitter.create('great-minimizer') + + # Expect + fitter._update_minimizer.assert_called_once_with('great-minimizer') + + def test_switch_minimizer(self, fitter: Fitter): + # When + mock_minimizer = MagicMock() + mock_minimizer.fit_constraints = MagicMock(return_value='constraints') + mock_minimizer.set_fit_constraint = MagicMock() + fitter._minimizer = mock_minimizer + + # Then + fitter.switch_minimizer('great-minimizer') + + # Expect + fitter._update_minimizer.count(2) + mock_minimizer.set_fit_constraint.assert_called_once_with('constraints') + mock_minimizer.fit_constraints.assert_called_once() + + def test_update_minimizer(self, monkeypatch): + # When + mock_fit_object = MagicMock() + mock_fit_function = MagicMock() + + mock_string_to_enum = MagicMock(return_value=10) + mock_factory = MagicMock(return_value='minimizer') + monkeypatch.setattr(easyscience.fitting.fitter, 'from_string_to_enum', mock_string_to_enum) + monkeypatch.setattr(easyscience.fitting.fitter, 'factory', mock_factory) + fitter = Fitter(mock_fit_object, mock_fit_function) + + # Then + fitter._update_minimizer('great-minimizer') + + # Expect + assert fitter._name_current_minimizer == 'great-minimizer' + assert fitter._minimizer == 'minimizer' + + def test_available_minimizers(self, fitter: Fitter): + # When + minimizers = fitter.available_minimizers + + # Then Expect + assert minimizers == [ + 'LMFit', 'LMFit_leastsq', 'LMFit_powell', 'LMFit_cobyla', + 'Bumps', 'Bumps_simplex', 'Bumps_newton', 'Bumps_lm', + 'DFO', 'DFO_leastsq' + ] + + def test_minimizer(self, fitter: Fitter): + # When + fitter._minimizer = 'minimizer' + + # Then + minimizer = fitter.minimizer + + # Expect + assert minimizer == 'minimizer' + + def test_fit_function(self, fitter: Fitter): + # When Then + fit_function = fitter.fit_function + + # Expect + assert fit_function == self.mock_fit_function + + def test_set_fit_function(self, fitter: Fitter): + # When + fitter._name_current_minimizer = 'current_minimizer' + + # Then + fitter.fit_function = 'new-fit-function' + + # Expect + assert fitter._fit_function == 'new-fit-function' + fitter._update_minimizer.assert_called_with('current_minimizer') + + def test_fit_object(self, fitter: Fitter): + # When Then + fit_object = fitter.fit_object + + # Expect + assert fit_object == self.mock_fit_object + + def test_set_fit_object(self, fitter: Fitter): + # When + fitter._name_current_minimizer = 'current_minimizer' + + # Then + fitter.fit_object = 'new-fit-object' + + # Expect + assert fitter.fit_object == 'new-fit-object' + fitter._update_minimizer.assert_called_with('current_minimizer') From a0521f4425b7546637fd64edc8fbea60e549fb1a Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Mon, 17 Jun 2024 14:26:16 +0200 Subject: [PATCH 18/24] remove unused arguments --- src/easyscience/fitting/fitter.py | 23 +++++++++++------------ src/easyscience/fitting/multi_fitter.py | 8 +++----- tests/unit_tests/Fitting/test_fitter.py | 21 +++++++++++++++++++++ 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/easyscience/fitting/fitter.py b/src/easyscience/fitting/fitter.py index 2f0d18c2..042cbeba 100644 --- a/src/easyscience/fitting/fitter.py +++ b/src/easyscience/fitting/fitter.py @@ -182,7 +182,7 @@ def inner_fit_callable( - POST = Reshaping the outputs so it is coherent with the inputs. """ # Precompute - Reshape all independents into the correct dimensionality - x_fit, x_new, y_new, weights, dims, kwargs = self._precompute_reshaping(x, y, weights, vectorized, kwargs) + x_fit, x_new, y_new, weights, dims = self._precompute_reshaping(x, y, weights, vectorized) self._dependent_dims = dims # Fit @@ -190,16 +190,16 @@ def inner_fit_callable( fit_fun_wrap = self._fit_function_wrapper(x_new, flatten=True) # This should be wrapped. # We change the fit function, so have to reset constraints - constraints = self._minimizer._constraints + constraints = self._minimizer.fit_constraints() self.fit_function = fit_fun_wrap - self._minimizer._constraints = constraints + self._minimizer.set_fit_constraint(constraints) f_res = self.minimizer.fit(x_fit, y_new, weights=weights, **kwargs) # Postcompute - fit_result = self._post_compute_reshaping(f_res, x, y, weights) + fit_result = self._post_compute_reshaping(f_res, x, y) # Reset the function and constrains self.fit_function = fit_fun - self._minimizer._constraints = constraints + self._minimizer.set_fit_constraint(constraints) return fit_result return inner_fit_callable @@ -210,7 +210,6 @@ def _precompute_reshaping( y: np.ndarray, weights: Optional[np.ndarray], vectorized: bool, - kwargs, ): """ Check the dimensions of the inputs and reshape if necessary. @@ -252,10 +251,10 @@ def _precompute_reshaping( weights = np.array(weights).flatten() # Make a 'dummy' x array for the fit function x_for_fit = np.array(range(y_new.size)) - return x_for_fit, x_new, y_new, weights, x_shape, kwargs + return x_for_fit, x_new, y_new, weights, x_shape @staticmethod - def _post_compute_reshaping(fit_result: FitResults, x: np.ndarray, y: np.ndarray, weights: np.ndarray) -> FitResults: + def _post_compute_reshaping(fit_result: FitResults, x: np.ndarray, y: np.ndarray) -> FitResults: """ Reshape the output of the fitter into the correct dimensions. :param fit_result: Output from the fitter @@ -263,8 +262,8 @@ def _post_compute_reshaping(fit_result: FitResults, x: np.ndarray, y: np.ndarray :param y: Input y dependent :return: Reshaped Fit Results """ - setattr(fit_result, 'x', x) - setattr(fit_result, 'y_obs', y) - setattr(fit_result, 'y_calc', np.reshape(fit_result.y_calc, y.shape)) - setattr(fit_result, 'y_err', np.reshape(fit_result.y_err, y.shape)) + fit_result.x = x + fit_result.y_obs = y + fit_result.y_calc = np.reshape(fit_result.y_calc, y.shape) + fit_result.y_err = np.reshape(fit_result.y_err, y.shape) return fit_result diff --git a/src/easyscience/fitting/multi_fitter.py b/src/easyscience/fitting/multi_fitter.py index 6f404ca4..c812ff0e 100644 --- a/src/easyscience/fitting/multi_fitter.py +++ b/src/easyscience/fitting/multi_fitter.py @@ -65,7 +65,6 @@ def _precompute_reshaping( y: List[np.ndarray], weights: Optional[List[np.ndarray]], vectorized: bool, - kwargs, ): """ Convert an array of X's and Y's to an acceptable shape for fitting. @@ -77,13 +76,13 @@ def _precompute_reshaping( """ if weights is None: weights = [None] * len(x) - _, _x_new, _y_new, _weights, _dims, kwargs = Fitter._precompute_reshaping(x[0], y[0], weights[0], vectorized, kwargs) + _, _x_new, _y_new, _weights, _dims = Fitter._precompute_reshaping(x[0], y[0], weights[0], vectorized) x_new = [_x_new] y_new = [_y_new] w_new = [_weights] dims = [_dims] for _x, _y, _w in zip(x[1::], y[1::], weights[1::]): - _, _x_new, _y_new, _weights, _dims, _ = Fitter._precompute_reshaping(_x, _y, _w, vectorized, kwargs) + _, _x_new, _y_new, _weights, _dims = Fitter._precompute_reshaping(_x, _y, _w, vectorized) x_new.append(_x_new) y_new.append(_y_new) w_new.append(_weights) @@ -94,14 +93,13 @@ def _precompute_reshaping( else: w_new = np.hstack(w_new) x_fit = np.linspace(0, y_new.size - 1, y_new.size) - return x_fit, x_new, y_new, w_new, dims, kwargs + return x_fit, x_new, y_new, w_new, dims def _post_compute_reshaping( self, fit_result_obj: FitResults, x: List[np.ndarray], y: List[np.ndarray], - weights: List[np.ndarray], ) -> List[FitResults]: """ Take a fit results object and split it into n chuncks based on the size of the x, y inputs diff --git a/tests/unit_tests/Fitting/test_fitter.py b/tests/unit_tests/Fitting/test_fitter.py index 50cb1e84..4970e976 100644 --- a/tests/unit_tests/Fitting/test_fitter.py +++ b/tests/unit_tests/Fitting/test_fitter.py @@ -1,6 +1,7 @@ from unittest.mock import MagicMock import pytest +import numpy as np from easyscience.fitting.fitter import Fitter import easyscience.fitting.fitter @@ -208,3 +209,23 @@ def test_set_fit_object(self, fitter: Fitter): # Expect assert fitter.fit_object == 'new-fit-object' fitter._update_minimizer.assert_called_with('current_minimizer') + + def test_fit(self, fitter: Fitter): + # When + fitter._precompute_reshaping = MagicMock(return_value=('x_fit', 'x_new', 'y_new', 'weights', 'dims')) + fitter._fit_function_wrapper = MagicMock(return_value='wrapped_fit_function') + fitter._post_compute_reshaping = MagicMock(return_value='fit_result') + fitter._minimizer = MagicMock() + fitter._minimizer.fit = MagicMock(return_value='result') + + x = np.array([1, 2, 3]) + y = np.array([10, 20, 30]) + weights = np.array([0.1, 0.2, 0.3]) + + # Then + result = fitter.fit('x', 'y', 'weights', 'vectorized') + + # TODO + # def test_fit_function_wrapper() + # def test_precompute_reshaping() + # def test_post_compute_reshaping() \ No newline at end of file From 5b8f7c5ab5fd5b324dbd60482ee4f7d42437315d Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Mon, 17 Jun 2024 14:42:34 +0200 Subject: [PATCH 19/24] unit tests --- src/easyscience/fitting/fitter.py | 4 +-- tests/unit_tests/Fitting/test_fitter.py | 34 ++++++++++++++++++++----- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/easyscience/fitting/fitter.py b/src/easyscience/fitting/fitter.py index 042cbeba..3f4565eb 100644 --- a/src/easyscience/fitting/fitter.py +++ b/src/easyscience/fitting/fitter.py @@ -186,7 +186,7 @@ def inner_fit_callable( self._dependent_dims = dims # Fit - fit_fun = self._fit_function + fit_fun_org = self._fit_function fit_fun_wrap = self._fit_function_wrapper(x_new, flatten=True) # This should be wrapped. # We change the fit function, so have to reset constraints @@ -198,7 +198,7 @@ def inner_fit_callable( # Postcompute fit_result = self._post_compute_reshaping(f_res, x, y) # Reset the function and constrains - self.fit_function = fit_fun + self.fit_function = fit_fun_org self._minimizer.set_fit_constraint(constraints) return fit_result diff --git a/tests/unit_tests/Fitting/test_fitter.py b/tests/unit_tests/Fitting/test_fitter.py index 4970e976..4669400b 100644 --- a/tests/unit_tests/Fitting/test_fitter.py +++ b/tests/unit_tests/Fitting/test_fitter.py @@ -218,14 +218,34 @@ def test_fit(self, fitter: Fitter): fitter._minimizer = MagicMock() fitter._minimizer.fit = MagicMock(return_value='result') + # Then + result = fitter.fit('x', 'y', 'weights', 'vectorized') + + # Expect + fitter._precompute_reshaping.assert_called_once_with('x', 'y', 'weights', 'vectorized') + fitter._fit_function_wrapper.assert_called_once_with('x_new', flatten=True) + fitter._post_compute_reshaping.assert_called_once_with('result', 'x', 'y') + assert result == 'fit_result' + assert fitter._dependent_dims == 'dims' + assert fitter._fit_function == self.mock_fit_function + + def test_post_compute_reshaping(self, fitter: Fitter): + # When + fit_result = MagicMock() + fit_result.y_calc = np.array([[10], [20], [30]]) + fit_result.y_err = np.array([[40], [50], [60]]) x = np.array([1, 2, 3]) - y = np.array([10, 20, 30]) - weights = np.array([0.1, 0.2, 0.3]) + y = np.array([4, 5, 6]) # Then - result = fitter.fit('x', 'y', 'weights', 'vectorized') + result = fitter._post_compute_reshaping(fit_result, x, y) - # TODO - # def test_fit_function_wrapper() - # def test_precompute_reshaping() - # def test_post_compute_reshaping() \ No newline at end of file + # Expect + assert np.array_equal(result.y_calc, np.array([10, 20, 30])) + assert np.array_equal(result.y_err, np.array([40, 50, 60])) + assert np.array_equal(result.x, x) + assert np.array_equal(result.y_obs, y) + +# TODO +# def test_fit_function_wrapper() +# def test_precompute_reshaping() From 76e8fe2304c80eead68a1ec6d70de161d09d6e79 Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Mon, 17 Jun 2024 15:27:30 +0200 Subject: [PATCH 20/24] more tests --- src/easyscience/fitting/minimizers/factory.py | 1 + .../fitting/minimizers/minimizer_base.py | 2 + .../Fitting/minimizers/test_factory.py | 57 +++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 tests/unit_tests/Fitting/minimizers/test_factory.py diff --git a/src/easyscience/fitting/minimizers/factory.py b/src/easyscience/fitting/minimizers/factory.py index f8ff85d5..c48d8ea0 100644 --- a/src/easyscience/fitting/minimizers/factory.py +++ b/src/easyscience/fitting/minimizers/factory.py @@ -51,6 +51,7 @@ class AvailableMinimizers(Enum): 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 diff --git a/src/easyscience/fitting/minimizers/minimizer_base.py b/src/easyscience/fitting/minimizers/minimizer_base.py index 578be9d3..c6c6435c 100644 --- a/src/easyscience/fitting/minimizers/minimizer_base.py +++ b/src/easyscience/fitting/minimizers/minimizer_base.py @@ -18,6 +18,8 @@ class MinimizerBase(metaclass=ABCMeta): This template class is the basis for all fitting engines in `EasyScience`. """ + wrapping: str = None + def __init__(self, obj, fit_function: Callable, method: Optional[str] = None): self._object = obj self._original_fit_function = fit_function diff --git a/tests/unit_tests/Fitting/minimizers/test_factory.py b/tests/unit_tests/Fitting/minimizers/test_factory.py new file mode 100644 index 00000000..10a38a18 --- /dev/null +++ b/tests/unit_tests/Fitting/minimizers/test_factory.py @@ -0,0 +1,57 @@ +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.minimizers import MinimizerBase +from unittest.mock import MagicMock +import pytest + +class TestFactory: + def pull_minminizer(self, minimizer: AvailableMinimizers) -> MinimizerBase: + mock_fit_object = MagicMock() + mock_fit_function = MagicMock() + minimizer = factory(minimizer, mock_fit_object, mock_fit_function) + return minimizer + + @pytest.mark.parametrize('minimizer_method,minimizer_enum', [('leastsq', AvailableMinimizers.LMFit), ('leastsq', AvailableMinimizers.LMFit_leastsq), ('powell', AvailableMinimizers.LMFit_powell), ('cobyla', AvailableMinimizers.LMFit_cobyla)]) + def test_factory_lm_fit(self, minimizer_method, minimizer_enum): + minimizer = self.pull_minminizer(minimizer_enum) + assert minimizer._method == minimizer_method + assert minimizer.wrapping == 'lmfit' + + @pytest.mark.parametrize('minimizer_method,minimizer_enum', [('amoeba', AvailableMinimizers.Bumps), ('amoeba', AvailableMinimizers.Bumps_simplex), ('newton', AvailableMinimizers.Bumps_newton), ('lm', AvailableMinimizers.Bumps_lm)]) + def test_factory_bumps_fit(self, minimizer_method, minimizer_enum): + minimizer = self.pull_minminizer(minimizer_enum) + assert minimizer._method == minimizer_method + assert minimizer.wrapping == 'bumps' + + @pytest.mark.parametrize('minimizer_method,minimizer_enum', [('leastsq', AvailableMinimizers.DFO), ('leastsq', AvailableMinimizers.DFO_leastsq)]) + def test_factory_dfo_fit(self, minimizer_method, minimizer_enum): + minimizer = self.pull_minminizer(minimizer_enum) + assert minimizer._method == minimizer_method + 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), ]) +def test_from_string_to_enum_lmfit(self, 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)]) +def test_from_string_to_enum_bumps(self, 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)]) +def test_from_string_to_enum_dfo(self, minimizer_name, expected): + assert from_string_to_enum(minimizer_name) == expected + + +def test_available_minimizers(): + assert AvailableMinimizers.LMFit + assert AvailableMinimizers.LMFit_leastsq + assert AvailableMinimizers.LMFit_powell + assert AvailableMinimizers.LMFit_cobyla + 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 From cc9aa05b645a53306c1ba404ecb82190ddc42c50 Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Mon, 17 Jun 2024 15:34:46 +0200 Subject: [PATCH 21/24] pin numpy to 1.26 after release of 2.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a67cb1f8..9d76e95f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ "bumps", "DFO-LS", "lmfit", - "numpy", + "numpy==1.26", # Should be updated to numpy 2.0 "pint", "uncertainties", "xarray", From ea875b18fe4bd3ababe288390691140b3ec0e08c Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Mon, 17 Jun 2024 15:50:10 +0200 Subject: [PATCH 22/24] code comment and test fixes --- src/easyscience/fitting/fitter.py | 2 ++ tests/unit_tests/Fitting/minimizers/test_factory.py | 9 ++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/easyscience/fitting/fitter.py b/src/easyscience/fitting/fitter.py index 3f4565eb..07ca844e 100644 --- a/src/easyscience/fitting/fitter.py +++ b/src/easyscience/fitting/fitter.py @@ -49,6 +49,7 @@ def evaluate(self, pars=None) -> np.ndarray: def convert_to_pars_obj(self, pars) -> object: return self._minimizer.convert_to_pars_obj(pars) + # TODO: remove this method when we are ready to adjust the dependent products def initialize(self, fit_object, fit_function: Callable) -> None: """ Set the model and callable in the calculator interface. @@ -60,6 +61,7 @@ def initialize(self, fit_object, fit_function: Callable) -> None: self._fit_function = fit_function 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: """ Create the required minimizer. diff --git a/tests/unit_tests/Fitting/minimizers/test_factory.py b/tests/unit_tests/Fitting/minimizers/test_factory.py index 10a38a18..64754925 100644 --- a/tests/unit_tests/Fitting/minimizers/test_factory.py +++ b/tests/unit_tests/Fitting/minimizers/test_factory.py @@ -30,16 +30,19 @@ def test_factory_dfo_fit(self, minimizer_method, minimizer_enum): assert minimizer._method == minimizer_method 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), ]) -def test_from_string_to_enum_lmfit(self, minimizer_name, expected): +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)]) -def test_from_string_to_enum_bumps(self, minimizer_name, expected): +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)]) -def test_from_string_to_enum_dfo(self, minimizer_name, expected): +def test_from_string_to_enum_dfo(minimizer_name, expected): assert from_string_to_enum(minimizer_name) == expected From c38ffcedbe8ea1c121ec64e69492e6c91a389dcc Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Tue, 18 Jun 2024 06:52:15 +0200 Subject: [PATCH 23/24] code cleaning --- src/easyscience/fitting/minimizers/minimizer_base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/easyscience/fitting/minimizers/minimizer_base.py b/src/easyscience/fitting/minimizers/minimizer_base.py index c6c6435c..4c28c878 100644 --- a/src/easyscience/fitting/minimizers/minimizer_base.py +++ b/src/easyscience/fitting/minimizers/minimizer_base.py @@ -29,7 +29,6 @@ def __init__(self, obj, fit_function: Callable, method: Optional[str] = None): self._cached_model = None self._fit_function = None self._constraints = [] - self._dataset = None @property def all_constraints(self) -> list: From 23fa62cbf42b725281cd3a6d2255ea17025a12c2 Mon Sep 17 00:00:00 2001 From: Andreas Pedersen Date: Tue, 18 Jun 2024 09:46:42 +0200 Subject: [PATCH 24/24] typo --- src/easyscience/Objects/Inferface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/easyscience/Objects/Inferface.py b/src/easyscience/Objects/Inferface.py index ae1c3781..767715fc 100644 --- a/src/easyscience/Objects/Inferface.py +++ b/src/easyscience/Objects/Inferface.py @@ -19,7 +19,7 @@ _C = TypeVar('_C', bound=ABCMeta) _M = TypeVar('_M') if TYPE_CHECKING: - from easyscience.Fitting import Fitter + from easyscience.fitting import Fitter class InterfaceFactoryTemplate: