diff --git a/docs/docs/tutorials/progress-callback.ipynb b/docs/docs/tutorials/progress-callback.ipynb index 43a75680..f782f1b9 100644 --- a/docs/docs/tutorials/progress-callback.ipynb +++ b/docs/docs/tutorials/progress-callback.ipynb @@ -47,9 +47,9 @@ "import matplotlib.pyplot as plt\n", "import numpy as np\n", "\n", - "from easyscience import AvailableMinimizers\n", "from easyscience import EasyList\n", "from easyscience import Fitter\n", + "from easyscience import Minimizers\n", "from easyscience import ModelBase\n", "from easyscience import Parameter" ] @@ -264,7 +264,7 @@ }, "outputs": [], "source": [ - "def fit_with(backend: AvailableMinimizers):\n", + "def fit_with(backend: Minimizers):\n", " model = make_initial_model()\n", " fitter = Fitter(model, model)\n", " fitter.switch_minimizer(backend)\n", @@ -277,7 +277,7 @@ "\n", "results = {}\n", "histories = {}\n", - "for backend in (AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO):\n", + "for backend in (Minimizers.LMFit, Minimizers.Bumps, Minimizers.DFO):\n", " res, hist = fit_with(backend)\n", " results[backend.name] = res\n", " histories[backend.name] = hist\n", @@ -370,7 +370,7 @@ "else:\n", " model = make_initial_model()\n", " fitter = Fitter(model, model)\n", - " fitter.switch_minimizer(AvailableMinimizers.LMFit)\n", + " fitter.switch_minimizer(Minimizers.LMFit)\n", " fitter.max_evaluations = 108\n", "\n", " pbar = tqdm(total=fitter.max_evaluations, desc='LMFit', unit='eval')\n", diff --git a/docs/docs/user-guide/external-calculators.md b/docs/docs/user-guide/external-calculators.md index 58cb5214..5aed758c 100644 --- a/docs/docs/user-guide/external-calculators.md +++ b/docs/docs/user-guide/external-calculators.md @@ -291,7 +291,9 @@ It is polled during the optimizer loop and once more after it finishes, so it must have no side effects: ```python -fitter.switch_minimizer('Bumps') +from easyscience import Minimizers + +fitter.switch_minimizer(Minimizers.Bumps) result = fitter.fit( x=x, y=y, weights=np.ones_like(x), abort_test=lambda: stop_requested ) diff --git a/src/easyscience/__init__.py b/src/easyscience/__init__.py index 2cfab610..ac27f372 100644 --- a/src/easyscience/__init__.py +++ b/src/easyscience/__init__.py @@ -11,20 +11,20 @@ from .base_classes import EasyList # noqa: E402 from .base_classes import ModelBase # noqa: E402 -from .fitting import AvailableMinimizers # noqa: E402 from .fitting import Fitter # noqa: E402 +from .fitting.available_minimizers import Minimizers # noqa: E402 from .variable import DescriptorNumber # noqa: E402 from .variable import Parameter # noqa: E402 __version__ = version('easyscience') __all__ = [ - '__version__', - 'global_object', - 'ModelBase', - 'EasyList', - 'AvailableMinimizers', - 'Fitter', - 'DescriptorNumber', - 'Parameter', + __version__, + global_object, + ModelBase, + EasyList, + Minimizers, + Fitter, + DescriptorNumber, + Parameter, ] diff --git a/src/easyscience/fitting/__init__.py b/src/easyscience/fitting/__init__.py index 29e6ddef..a3956527 100644 --- a/src/easyscience/fitting/__init__.py +++ b/src/easyscience/fitting/__init__.py @@ -1,7 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -from .available_minimizers import AvailableMinimizers from .fitter import Fitter from .minimizers.utils import FitResults from .sampler import Sampler diff --git a/src/easyscience/fitting/available_minimizers.py b/src/easyscience/fitting/available_minimizers.py index 311bf35e..daf42be1 100644 --- a/src/easyscience/fitting/available_minimizers.py +++ b/src/easyscience/fitting/available_minimizers.py @@ -48,7 +48,7 @@ class AvailableMinimizer: enum_id: int -class AvailableMinimizers(AvailableMinimizer, Enum): +class Minimizers(AvailableMinimizer, Enum): if lmfit_engine_available: LMFit = 'lm', 'leastsq', 11 LMFit_leastsq = 'lm', 'leastsq', 12 @@ -66,39 +66,3 @@ class AvailableMinimizers(AvailableMinimizer, Enum): if dfo_engine_available: DFO = 'dfo', 'leastsq', 31 DFO_leastsq = 'dfo', 'leastsq', 32 - - -# Temporary solution to convert string to enum -def from_string_to_enum(minimizer_name: str) -> AvailableMinimizers: - if minimizer_name == 'LMFit': - minmizer_enum = AvailableMinimizers.LMFit - elif minimizer_name == 'LMFit_leastsq': - minmizer_enum = AvailableMinimizers.LMFit_leastsq - elif minimizer_name == 'LMFit_powell': - minmizer_enum = AvailableMinimizers.LMFit_powell - elif minimizer_name == 'LMFit_cobyla': - minmizer_enum = AvailableMinimizers.LMFit_cobyla - elif minimizer_name == 'LMFit_differential_evolution': - minmizer_enum = AvailableMinimizers.LMFit_differential_evolution - elif minimizer_name == 'LMFit_scipy_least_squares': - minmizer_enum = AvailableMinimizers.LMFit_scipy_least_squares - - elif minimizer_name == 'Bumps': - minmizer_enum = AvailableMinimizers.Bumps - elif minimizer_name == 'Bumps_simplex': - minmizer_enum = AvailableMinimizers.Bumps_simplex - elif minimizer_name == 'Bumps_newton': - minmizer_enum = AvailableMinimizers.Bumps_newton - elif minimizer_name == 'Bumps_lm': - minmizer_enum = AvailableMinimizers.Bumps_lm - - elif minimizer_name == 'DFO': - minmizer_enum = AvailableMinimizers.DFO - elif minimizer_name == 'DFO_leastsq': - minmizer_enum = AvailableMinimizers.DFO_leastsq - else: - raise ValueError( - f'Invalid minimizer name: {minimizer_name}. The following minimizers are available: {[minimize.name for minimize in AvailableMinimizers]}' # noqa: E501 - ) - - return minmizer_enum diff --git a/src/easyscience/fitting/fitter.py b/src/easyscience/fitting/fitter.py index 4d4f98f1..bdccfd1e 100644 --- a/src/easyscience/fitting/fitter.py +++ b/src/easyscience/fitting/fitter.py @@ -4,21 +4,17 @@ import functools from typing import Callable from typing import List -from typing import Union import numpy as np -from easyscience import global_object - -from .available_minimizers import AvailableMinimizers -from .available_minimizers import from_string_to_enum +from .available_minimizers import Minimizers from .minimizers import FitResults from .minimizers import MinimizerBase from .minimizers.factory import factory from .reshaping import inject_x from .reshaping import reshape_dataset -DEFAULT_MINIMIZER = AvailableMinimizers.LMFit_leastsq +DEFAULT_MINIMIZER = Minimizers.LMFit_leastsq class Fitter: @@ -35,7 +31,7 @@ def __init__(self, fit_object, fit_function: Callable): self._max_evaluations: int = None self._minimizer: MinimizerBase = None # set in _update_minimizer - self._enum_current_minimizer: AvailableMinimizers = None # set in _update_minimizer + self._enum_current_minimizer: Minimizers = None # set in _update_minimizer self._update_minimizer(DEFAULT_MINIMIZER) def make_model(self, pars=None) -> Callable: @@ -60,42 +56,29 @@ def initialize(self, fit_object: 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_enum: Union[AvailableMinimizers, str] = DEFAULT_MINIMIZER) -> None: - """ - Create the required minimizer. - - Parameters - ---------- - minimizer_enum : Union[AvailableMinimizers, str], default=DEFAULT_MINIMIZER - The enum of the minimization engine to create. By default, - DEFAULT_MINIMIZER. - """ - if isinstance(minimizer_enum, str): - global_object.log.getLogger('fitting').warning( - 'minimizer should be set with enum %s', minimizer_enum - ) - minimizer_enum = from_string_to_enum(minimizer_enum) - self._update_minimizer(minimizer_enum) - - def switch_minimizer(self, minimizer_enum: Union[AvailableMinimizers, str]) -> None: + def switch_minimizer(self, minimizer_enum: Minimizers) -> None: """ Switch minimizer and initialize. Parameters ---------- - minimizer_enum : Union[AvailableMinimizers, str] + minimizer_enum : Minimizers The enum of the minimizer to create and instantiate. + + Raises + ------ + TypeError + If ``minimizer_enum`` is not a ``Minimizers`` + member. """ - if isinstance(minimizer_enum, str): - global_object.log.getLogger('fitting').warning( - 'minimizer should be set with enum %s', minimizer_enum + if not isinstance(minimizer_enum, Minimizers): + raise TypeError( + f'minimizer must be a Minimizers member, got {minimizer_enum!r}; ' + f'use Minimizers[name] to convert a name' ) - minimizer_enum = from_string_to_enum(minimizer_enum) - self._update_minimizer(minimizer_enum) - def _update_minimizer(self, minimizer_enum: AvailableMinimizers) -> None: + def _update_minimizer(self, minimizer_enum: Minimizers) -> None: self._minimizer = factory( minimizer_enum=minimizer_enum, fit_object=self._fit_object, @@ -104,16 +87,16 @@ def _update_minimizer(self, minimizer_enum: AvailableMinimizers) -> None: self._enum_current_minimizer = minimizer_enum @property - def available_minimizers(self) -> List[str]: + def available_minimizers(self) -> List[Minimizers]: """ - Get a list of the names of available fitting minimizers. + Get a list of the available fitting minimizers. Returns ------- - List[str] + List[Minimizers] List of available fitting minimizers. """ - return [minimize.name for minimize in AvailableMinimizers] + return list(Minimizers) @property def minimizer(self) -> MinimizerBase: diff --git a/src/easyscience/fitting/minimizers/factory.py b/src/easyscience/fitting/minimizers/factory.py index 335d288c..ea41e1e2 100644 --- a/src/easyscience/fitting/minimizers/factory.py +++ b/src/easyscience/fitting/minimizers/factory.py @@ -4,7 +4,7 @@ from typing import Callable from .. import available_minimizers -from ..available_minimizers import AvailableMinimizers +from ..available_minimizers import Minimizers from .minimizer_base import MinimizerBase if available_minimizers.lmfit_engine_available: @@ -15,9 +15,7 @@ from .minimizer_bumps import Bumps -def factory( - minimizer_enum: AvailableMinimizers, fit_object, fit_function: Callable -) -> MinimizerBase: +def factory(minimizer_enum: Minimizers, fit_object, fit_function: Callable) -> MinimizerBase: if minimizer_enum.package == 'lm': minimizer = LMFit(obj=fit_object, fit_function=fit_function, minimizer_enum=minimizer_enum) diff --git a/src/easyscience/fitting/minimizers/minimizer_base.py b/src/easyscience/fitting/minimizers/minimizer_base.py index 36d8efee..66054f7a 100644 --- a/src/easyscience/fitting/minimizers/minimizer_base.py +++ b/src/easyscience/fitting/minimizers/minimizer_base.py @@ -10,7 +10,7 @@ from easyscience.variable import Parameter -from ..available_minimizers import AvailableMinimizers +from ..available_minimizers import Minimizers from ..engine_base import EngineBase from .utils import FitError from .utils import FitResults @@ -26,7 +26,7 @@ def __init__( self, obj, #: ModelBase, fit_function: Callable, - minimizer_enum: AvailableMinimizers, + minimizer_enum: Minimizers, ): # todo after constraint changes, add type hint: obj: ModelBase # noqa: E501 if minimizer_enum.method not in self.supported_methods(): raise FitError(f'Method {minimizer_enum.method} not available in {self.__class__}') @@ -36,7 +36,7 @@ def __init__( self._cached_model = None @property - def enum(self) -> AvailableMinimizers: + def enum(self) -> Minimizers: return self._minimizer_enum @property diff --git a/src/easyscience/fitting/minimizers/minimizer_bumps.py b/src/easyscience/fitting/minimizers/minimizer_bumps.py index a27b624d..3b7c6915 100644 --- a/src/easyscience/fitting/minimizers/minimizer_bumps.py +++ b/src/easyscience/fitting/minimizers/minimizer_bumps.py @@ -17,7 +17,7 @@ from easyscience.variable import Parameter -from ..available_minimizers import AvailableMinimizers +from ..available_minimizers import Minimizers from ..engine_base import PARAMETER_PREFIX from .bumps_utils import BumpsProgressMonitor from .bumps_utils import EvalCounter @@ -49,7 +49,7 @@ def __init__( self, obj: object, fit_function: Callable, - minimizer_enum: AvailableMinimizers | None = None, + minimizer_enum: Minimizers | None = None, ): """ Initialize the fitting engine. @@ -60,7 +60,7 @@ def __init__( Object containing the ``Parameter`` instances to fit. fit_function : Callable Callable returning model y values for the supplied x values. - minimizer_enum : AvailableMinimizers | None, default=None + minimizer_enum : Minimizers | None, default=None Selected BUMPS minimizer configuration. By default, None. """ super().__init__(obj=obj, fit_function=fit_function, minimizer_enum=minimizer_enum) diff --git a/src/easyscience/fitting/minimizers/minimizer_dfo.py b/src/easyscience/fitting/minimizers/minimizer_dfo.py index d6f8bb45..a9afea72 100644 --- a/src/easyscience/fitting/minimizers/minimizer_dfo.py +++ b/src/easyscience/fitting/minimizers/minimizer_dfo.py @@ -12,7 +12,7 @@ from easyscience.variable import Parameter -from ..available_minimizers import AvailableMinimizers +from ..available_minimizers import Minimizers from ..engine_base import PARAMETER_PREFIX from .minimizer_base import MinimizerBase from .utils import FitError @@ -46,7 +46,7 @@ def __init__( self, obj: object, #: ModelBase, fit_function: Callable, - minimizer_enum: AvailableMinimizers | None = None, + minimizer_enum: Minimizers | None = None, ): # todo after constraint changes, add type hint: obj: ModelBase # noqa: E501 """ Initialize the fitting engine. @@ -57,7 +57,7 @@ def __init__( Object containing the ``Parameter`` instances to fit. fit_function : Callable Callable returning model y values for the supplied x values. - minimizer_enum : AvailableMinimizers | None, default=None + minimizer_enum : Minimizers | None, default=None Selected DFO minimizer configuration. By default, None. """ super().__init__(obj=obj, fit_function=fit_function, minimizer_enum=minimizer_enum) diff --git a/src/easyscience/fitting/minimizers/minimizer_lmfit.py b/src/easyscience/fitting/minimizers/minimizer_lmfit.py index 9d8f829e..57dfcfa1 100644 --- a/src/easyscience/fitting/minimizers/minimizer_lmfit.py +++ b/src/easyscience/fitting/minimizers/minimizer_lmfit.py @@ -13,7 +13,7 @@ from easyscience.variable import Parameter -from ..available_minimizers import AvailableMinimizers +from ..available_minimizers import Minimizers from ..engine_base import PARAMETER_PREFIX from .minimizer_base import MinimizerBase from .utils import FitError @@ -34,7 +34,7 @@ def __init__( self, obj: object, #: ModelBase, fit_function: Callable, - minimizer_enum: AvailableMinimizers | None = None, + minimizer_enum: Minimizers | None = None, ): # todo after constraint changes, add type hint: obj: ModelBase # noqa: E501 """ Initialize the minimizer. @@ -45,7 +45,7 @@ def __init__( Object containing the ``Parameter`` instances to fit. fit_function : Callable Callable returning model y values for the supplied x values. - minimizer_enum : AvailableMinimizers | None, default=None + minimizer_enum : Minimizers | None, default=None Selected LMFit minimizer configuration. By default, None. """ super().__init__(obj=obj, fit_function=fit_function, minimizer_enum=minimizer_enum) diff --git a/tests/integration/fitting/test_fitter.py b/tests/integration/fitting/test_fitter.py index ce466993..bbc347e0 100644 --- a/tests/integration/fitting/test_fitter.py +++ b/tests/integration/fitting/test_fitter.py @@ -4,8 +4,8 @@ import numpy as np import pytest -from easyscience import AvailableMinimizers from easyscience import Fitter +from easyscience import Minimizers from easyscience import Parameter from easyscience.base_classes import ModelBase from easyscience.fitting.minimizers import FitError @@ -115,12 +115,12 @@ def check_fit_results(result, sp_sin, ref_sin, x, expect_error=True, **kwargs): 'fit_engine', [ None, - AvailableMinimizers.LMFit, - AvailableMinimizers.Bumps, - AvailableMinimizers.DFO, + Minimizers.LMFit, + Minimizers.Bumps, + Minimizers.DFO, ], ) -def test_basic_fit(fit_engine: AvailableMinimizers): +def test_basic_fit(fit_engine: Minimizers): ref_sin = AbsSin(0.2, np.pi) sp_sin = AbsSin(0.354, 3.05) @@ -133,10 +133,7 @@ def test_basic_fit(fit_engine: AvailableMinimizers): f = Fitter(sp_sin, sp_sin) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) result = f.fit(x=x, y=y, weights=weights) @@ -153,9 +150,9 @@ def test_basic_fit(fit_engine: AvailableMinimizers): 'fit_engine', [ None, - AvailableMinimizers.LMFit, - AvailableMinimizers.Bumps, - AvailableMinimizers.DFO, + Minimizers.LMFit, + Minimizers.Bumps, + Minimizers.DFO, ], ) def test_fit_result(fit_engine): @@ -181,10 +178,7 @@ def test_fit_result(fit_engine): f = Fitter(sp_sin, sp_sin) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) result = f.fit(x, y, weights=weights) check_fit_results(result, sp_sin, ref_sin, x, sp_ref1=sp_ref1, sp_ref2=sp_ref2) @@ -195,9 +189,9 @@ def test_fit_result(fit_engine): 'fit_engine', [ None, - AvailableMinimizers.LMFit, - AvailableMinimizers.Bumps, - AvailableMinimizers.DFO, + Minimizers.LMFit, + Minimizers.Bumps, + Minimizers.DFO, ], ) def test_basic_max_evaluations(fit_engine): @@ -213,10 +207,7 @@ def test_basic_max_evaluations(fit_engine): f = Fitter(sp_sin, sp_sin) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) f.max_evaluations = 3 result = f.fit(x=x, y=y, weights=weights) # Result should not be the same as the reference @@ -229,9 +220,9 @@ def test_basic_max_evaluations(fit_engine): 'fit_engine', [ None, - AvailableMinimizers.LMFit, - AvailableMinimizers.Bumps, - AvailableMinimizers.DFO, + Minimizers.LMFit, + Minimizers.Bumps, + Minimizers.DFO, ], ) def test_max_evaluations_populates_fit_result_fields(fit_engine): @@ -248,10 +239,7 @@ def test_max_evaluations_populates_fit_result_fields(fit_engine): f = Fitter(sp_sin, sp_sin) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) f.max_evaluations = 3 result = f.fit(x=x, y=y, weights=weights) @@ -276,10 +264,7 @@ def test_bumps_max_evaluations_counts_objective_calls() -> None: sp_sin.phase.fixed = False f = Fitter(sp_sin, sp_sin) - try: - f.switch_minimizer(AvailableMinimizers.Bumps) - except AttributeError: - pytest.skip(reason=f'{AvailableMinimizers.Bumps} is not installed') + f.switch_minimizer(Minimizers.Bumps) f.max_evaluations = 3 result = f.fit(x=x, y=y, weights=weights) @@ -296,9 +281,9 @@ def test_bumps_max_evaluations_counts_objective_calls() -> None: 'fit_engine,tolerance', [ (None, 10), - (AvailableMinimizers.LMFit, 10), - (AvailableMinimizers.Bumps, 10), - (AvailableMinimizers.DFO, 0.1), + (Minimizers.LMFit, 10), + (Minimizers.Bumps, 10), + (Minimizers.DFO, 0.1), ], ) def test_basic_tolerance(fit_engine, tolerance): @@ -314,10 +299,7 @@ def test_basic_tolerance(fit_engine, tolerance): f = Fitter(sp_sin, sp_sin) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) f.tolerance = tolerance result = f.fit(x=x, y=y, weights=weights) # Result should not be the same as the reference @@ -361,7 +343,7 @@ def test_bumps_methods(fit_method): sp_sin.phase.fixed = False f = Fitter(sp_sin, sp_sin) - f.switch_minimizer('Bumps') + f.switch_minimizer(Minimizers.Bumps) assert fit_method in f._minimizer.supported_methods() result = f.fit(x, y, weights=weights, method=fit_method) check_fit_results(result, sp_sin, ref_sin, x) @@ -386,7 +368,7 @@ def test_bumps_fit_emits_no_fitness_deprecation_warning(): sp_sin.phase.fixed = False f = Fitter(sp_sin, sp_sin) - f.switch_minimizer('Bumps') + f.switch_minimizer(Minimizers.Bumps) with warnings.catch_warnings(record=True) as caught: warnings.simplefilter('always') @@ -399,7 +381,7 @@ def test_bumps_fit_emits_no_fitness_deprecation_warning(): @pytest.mark.fast @pytest.mark.parametrize( 'fit_engine', - [AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO], + [Minimizers.LMFit, Minimizers.Bumps, Minimizers.DFO], ) def test_dependent_parameter(fit_engine): ref_sin = AbsSin(np.pi * 0.45, 0.45 * np.pi * 0.5) @@ -416,18 +398,13 @@ def test_dependent_parameter(fit_engine): ) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) result = f.fit(x, y, weights=weights) # With `offset` dependent on `phase` only one parameter varies against # noiseless data, so lmfit's covariance is exactly [[0.]] and it reports # `errorbars=False` -> no uncertainties. - check_fit_results( - result, sp_sin, ref_sin, x, expect_error=fit_engine is not AvailableMinimizers.LMFit - ) + check_fit_results(result, sp_sin, ref_sin, x, expect_error=fit_engine is not Minimizers.LMFit) @pytest.mark.fast @@ -435,9 +412,9 @@ def test_dependent_parameter(fit_engine): 'fit_engine', [ None, - AvailableMinimizers.LMFit, - AvailableMinimizers.Bumps, - AvailableMinimizers.DFO, + Minimizers.LMFit, + Minimizers.Bumps, + Minimizers.DFO, ], ) def test_2D_vectorized(fit_engine): @@ -449,10 +426,7 @@ def test_2D_vectorized(fit_engine): weights = np.ones_like(mm(XY)) ff = Fitter(m2, m2) if fit_engine is not None: - try: - ff.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + ff.switch_minimizer(fit_engine) try: result = ff.fit(x=XY, y=mm(XY), weights=weights, vectorized=True) except FitError as e: @@ -474,9 +448,9 @@ def test_2D_vectorized(fit_engine): 'fit_engine', [ None, - AvailableMinimizers.LMFit, - AvailableMinimizers.Bumps, - AvailableMinimizers.DFO, + Minimizers.LMFit, + Minimizers.Bumps, + Minimizers.DFO, ], ) def test_2D_non_vectorized(fit_engine): @@ -488,10 +462,7 @@ def test_2D_non_vectorized(fit_engine): weights = np.ones_like(mm(XY.reshape(-1, 2))) ff = Fitter(m2, m2) if fit_engine is not None: - try: - ff.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + ff.switch_minimizer(fit_engine) try: result = ff.fit(x=XY, y=mm(XY.reshape(-1, 2)), weights=weights, vectorized=False) except FitError as e: @@ -513,9 +484,9 @@ def test_2D_non_vectorized(fit_engine): 'fit_engine', [ None, - AvailableMinimizers.LMFit, - AvailableMinimizers.Bumps, - AvailableMinimizers.DFO, + Minimizers.LMFit, + Minimizers.Bumps, + Minimizers.DFO, ], ) def test_fixed_parameter_does_not_change(fit_engine): @@ -536,10 +507,7 @@ def test_fixed_parameter_does_not_change(fit_engine): # THEN f = Fitter(sp_sin, sp_sin) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) result = f.fit(x=x, y=y, weights=weights) @@ -576,9 +544,9 @@ def test_fitter_new_model_base_integration(): 'fit_engine', [ None, - AvailableMinimizers.LMFit, - AvailableMinimizers.Bumps, - AvailableMinimizers.DFO, + Minimizers.LMFit, + Minimizers.Bumps, + Minimizers.DFO, ], ) def test_fitter_variable_weights(fit_engine): @@ -611,10 +579,7 @@ def run_fit(weights): f = Fitter(model, model) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) f.fit(x=x, y=y, weights=weights) return model.offset.value, model.phase.value @@ -645,9 +610,9 @@ def _analytic_wls(design: np.ndarray, y: np.ndarray, point_weights: np.ndarray): 'fit_engine', [ None, - AvailableMinimizers.LMFit, - AvailableMinimizers.Bumps, - AvailableMinimizers.DFO, + Minimizers.LMFit, + Minimizers.Bumps, + Minimizers.DFO, ], ) def test_weight_convention_matches_analytic_wls(fit_engine): @@ -685,10 +650,7 @@ def test_weight_convention_matches_analytic_wls(fit_engine): f = Fitter(model, model) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(reason=f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) result = f.fit(x=x, y=y, weights=1.0 / sigma) @@ -696,7 +658,7 @@ def test_weight_convention_matches_analytic_wls(fit_engine): assert model.slope.value == pytest.approx(beta[0], abs=tolerance) assert model.intercept.value == pytest.approx(beta[1], abs=tolerance) - if fit_engine in (None, AvailableMinimizers.LMFit): + if fit_engine in (None, Minimizers.LMFit): # lmfit scales the covariance by reduced chi-square (scale_covar=True) residual = y - design @ beta chi2 = float(np.sum((residual / sigma) ** 2)) diff --git a/tests/integration/fitting/test_multi_fitter.py b/tests/integration/fitting/test_multi_fitter.py index 927eff0b..4c575668 100644 --- a/tests/integration/fitting/test_multi_fitter.py +++ b/tests/integration/fitting/test_multi_fitter.py @@ -4,6 +4,7 @@ import numpy as np import pytest +from easyscience import Minimizers from easyscience import Parameter from easyscience.base_classes import ModelBase from easyscience.fitting.minimizers import FitError @@ -71,7 +72,10 @@ def __call__(self, x): ) -@pytest.mark.parametrize('fit_engine', [None, 'LMFit', 'Bumps', 'DFO']) +@pytest.mark.parametrize( + 'fit_engine', + [None, Minimizers.LMFit, Minimizers.Bumps, Minimizers.DFO], +) def test_multi_fit(fit_engine): ref_sin_1 = AbsSin(0.2, np.pi) sp_sin_1 = AbsSin(0.354, 3.05) @@ -98,10 +102,7 @@ def test_multi_fit(fit_engine): f = MultiFitter([sp_sin_1, sp_sin_2], [sp_sin_1, sp_sin_2]) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) results = f.fit(x=[x1, x2], y=[y1, y2], weights=[weights, weights]) X = [x1, x2] @@ -121,7 +122,10 @@ def test_multi_fit(fit_engine): assert result.residual == pytest.approx(F_real[idx](X[idx]) - F_ref[idx](X[idx]), abs=1e-2) -@pytest.mark.parametrize('fit_engine', [None, 'LMFit', 'Bumps', 'DFO']) +@pytest.mark.parametrize( + 'fit_engine', + [None, Minimizers.LMFit, Minimizers.Bumps, Minimizers.DFO], +) def test_multi_fit_propagates_iteration_metadata_and_message(fit_engine): """Verify that fit metadata and message are copied into each per-dataset result.""" ref_sin_1 = AbsSin(0.2, np.pi) @@ -148,10 +152,7 @@ def test_multi_fit_propagates_iteration_metadata_and_message(fit_engine): f = MultiFitter([sp_sin_1, sp_sin_2], [sp_sin_1, sp_sin_2]) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) results = f.fit(x=[x1, x2], y=[y1, y2], weights=[weights, weights]) for result in results: @@ -164,7 +165,10 @@ def test_multi_fit_propagates_iteration_metadata_and_message(fit_engine): assert isinstance(result.message, str) -@pytest.mark.parametrize('fit_engine', [None, 'LMFit', 'Bumps', 'DFO']) +@pytest.mark.parametrize( + 'fit_engine', + [None, Minimizers.LMFit, Minimizers.Bumps, Minimizers.DFO], +) def test_multi_fit2(fit_engine): ref_sin_1 = AbsSin(0.2, np.pi) sp_sin_1 = AbsSin(0.354, 3.05) @@ -203,10 +207,7 @@ def test_multi_fit2(fit_engine): 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_minimizer(fit_engine) - except AttributeError: - pytest.skip(f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) results = f.fit(x=[x1, x2, x3], y=[y1, y2, y3], weights=[weights, weights, weights]) X = [x1, x2, x3] @@ -229,7 +230,10 @@ def test_multi_fit2(fit_engine): assert result.residual == pytest.approx(F_ref[idx](X[idx]) - F_real[idx](X[idx]), abs=1e-2) -@pytest.mark.parametrize('fit_engine', [None, 'LMFit', 'Bumps', 'DFO']) +@pytest.mark.parametrize( + 'fit_engine', + [None, Minimizers.LMFit, Minimizers.Bumps, Minimizers.DFO], +) def test_multi_fit_1D_2D(fit_engine): # Generate fit and reference objects ref_sin1D = AbsSin(0.2, np.pi) @@ -259,10 +263,7 @@ def test_multi_fit_1D_2D(fit_engine): ff = MultiFitter([sp_sin1D, sp_sin2D], [sp_sin1D, sp_sin2D]) if fit_engine is not None: - try: - ff.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(f'{fit_engine} is not installed') + ff.switch_minimizer(fit_engine) sp_sin1D.offset.fixed = False sp_sin1D.phase.fixed = False @@ -270,10 +271,7 @@ def test_multi_fit_1D_2D(fit_engine): f = MultiFitter([sp_sin1D, sp_sin2D], [sp_sin1D, sp_sin2D]) if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(f'{fit_engine} is not installed') + f.switch_minimizer(fit_engine) try: results = f.fit( x=[x1D, x2D], y=[y1D, y2D], weights=[weights1D, weights2D], vectorized=True @@ -293,7 +291,7 @@ def test_multi_fit_1D_2D(fit_engine): sp_sin2D.get_fit_parameters() ) if ( - fit_engine != 'DFO' + fit_engine != Minimizers.DFO ): # DFO apparently does not fit well with even weights. Can't be bothered to fix assert result.chi2 == pytest.approx(0, abs=1.5e-3 * (len(result.x) - result.n_pars)) assert result.reduced_chi2 == pytest.approx(0, abs=1.5e-3) diff --git a/tests/integration/fitting/test_stateless_calculator.py b/tests/integration/fitting/test_stateless_calculator.py index 12eb0c81..304abe9a 100644 --- a/tests/integration/fitting/test_stateless_calculator.py +++ b/tests/integration/fitting/test_stateless_calculator.py @@ -13,6 +13,7 @@ import pytest from easy_test_models import CoefficientModel +from easyscience import Minimizers from easyscience import Parameter from easyscience import global_object from easyscience.fitting import Fitter @@ -76,7 +77,7 @@ def test_calculation_reads_current_values_and_leaves_model_untouched(): np.testing.assert_allclose(calculator.calculate(model, x), 3.0 * x + 1.0) -@pytest.mark.parametrize('fit_engine', ['LMFit', 'Bumps', 'DFO']) +@pytest.mark.parametrize('fit_engine', [Minimizers.LMFit, Minimizers.Bumps, Minimizers.DFO]) def test_fit_with_dependent_parameter(fit_engine): # When # The intercept is tied to the slope, so only the slope is fitted slope = Parameter(1.0, display_name='slope', min=-10, max=10) @@ -102,7 +103,7 @@ def test_fit_with_dependent_parameter(fit_engine): np.testing.assert_allclose(calculator.calculate(model, x), y, atol=0.1) -@pytest.mark.parametrize('fit_engine', ['LMFit', 'Bumps', 'DFO']) +@pytest.mark.parametrize('fit_engine', [Minimizers.LMFit, Minimizers.Bumps, Minimizers.DFO]) def test_multi_fit_with_shared_parameter(fit_engine): # When # Two models share their intercept shared = Parameter(0.0, display_name='shared', min=-10, max=10) diff --git a/tests/unit/fitting/minimizers/test_factory.py b/tests/unit/fitting/minimizers/test_factory.py index 8ee4df6c..cef4be4e 100644 --- a/tests/unit/fitting/minimizers/test_factory.py +++ b/tests/unit/fitting/minimizers/test_factory.py @@ -5,14 +5,13 @@ import pytest -from easyscience import AvailableMinimizers -from easyscience.fitting.available_minimizers import from_string_to_enum +from easyscience import Minimizers from easyscience.fitting.minimizers import MinimizerBase from easyscience.fitting.minimizers.factory import factory class TestFactory: - def pull_minminizer(self, minimizer: AvailableMinimizers) -> MinimizerBase: + def pull_minminizer(self, minimizer: Minimizers) -> MinimizerBase: mock_fit_object = MagicMock() mock_fit_function = MagicMock() minimizer = factory(minimizer, mock_fit_object, mock_fit_function) @@ -21,12 +20,12 @@ def pull_minminizer(self, minimizer: AvailableMinimizers) -> MinimizerBase: @pytest.mark.parametrize( 'minimizer_method,minimizer_enum', [ - ('leastsq', AvailableMinimizers.LMFit), - ('leastsq', AvailableMinimizers.LMFit_leastsq), - ('powell', AvailableMinimizers.LMFit_powell), - ('cobyla', AvailableMinimizers.LMFit_cobyla), - ('differential_evolution', AvailableMinimizers.LMFit_differential_evolution), - ('least_squares', AvailableMinimizers.LMFit_scipy_least_squares), + ('leastsq', Minimizers.LMFit), + ('leastsq', Minimizers.LMFit_leastsq), + ('powell', Minimizers.LMFit_powell), + ('cobyla', Minimizers.LMFit_cobyla), + ('differential_evolution', Minimizers.LMFit_differential_evolution), + ('least_squares', Minimizers.LMFit_scipy_least_squares), ], ) def test_factory_lm_fit(self, minimizer_method, minimizer_enum): @@ -37,10 +36,10 @@ def test_factory_lm_fit(self, minimizer_method, minimizer_enum): @pytest.mark.parametrize( 'minimizer_method,minimizer_enum', [ - ('amoeba', AvailableMinimizers.Bumps), - ('amoeba', AvailableMinimizers.Bumps_simplex), - ('newton', AvailableMinimizers.Bumps_newton), - ('lm', AvailableMinimizers.Bumps_lm), + ('amoeba', Minimizers.Bumps), + ('amoeba', Minimizers.Bumps_simplex), + ('newton', Minimizers.Bumps_newton), + ('lm', Minimizers.Bumps_lm), ], ) def test_factory_bumps_fit(self, minimizer_method, minimizer_enum): @@ -50,7 +49,7 @@ def test_factory_bumps_fit(self, minimizer_method, minimizer_enum): @pytest.mark.parametrize( 'minimizer_method,minimizer_enum', - [('leastsq', AvailableMinimizers.DFO), ('leastsq', AvailableMinimizers.DFO_leastsq)], + [('leastsq', Minimizers.DFO), ('leastsq', Minimizers.DFO_leastsq)], ) def test_factory_dfo_fit(self, minimizer_method, minimizer_enum): minimizer = self.pull_minminizer(minimizer_enum) @@ -58,53 +57,17 @@ def test_factory_dfo_fit(self, minimizer_method, minimizer_enum): assert minimizer.package == '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), - ('LMFit_differential_evolution', AvailableMinimizers.LMFit_differential_evolution), - ('LMFit_scipy_least_squares', AvailableMinimizers.LMFit_scipy_least_squares), - ], -) -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(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(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.LMFit_differential_evolution - assert AvailableMinimizers.LMFit_scipy_least_squares - 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) == 12 + assert Minimizers.LMFit + assert Minimizers.LMFit_leastsq + assert Minimizers.LMFit_powell + assert Minimizers.LMFit_cobyla + assert Minimizers.LMFit_differential_evolution + assert Minimizers.LMFit_scipy_least_squares + assert Minimizers.Bumps + assert Minimizers.Bumps_simplex + assert Minimizers.Bumps_newton + assert Minimizers.Bumps_lm + assert Minimizers.DFO + assert Minimizers.DFO_leastsq + assert len(Minimizers) == 12 diff --git a/tests/unit/fitting/test_fitter.py b/tests/unit/fitting/test_fitter.py index 64605c2a..9070ebc7 100644 --- a/tests/unit/fitting/test_fitter.py +++ b/tests/unit/fitting/test_fitter.py @@ -7,8 +7,8 @@ import pytest import easyscience.fitting.fitter -from easyscience import AvailableMinimizers from easyscience import Fitter +from easyscience import Minimizers class TestFitter: @@ -24,9 +24,9 @@ def test_constructor(self, fitter: Fitter): assert fitter._fit_object == self.mock_fit_object assert fitter._fit_function == self.mock_fit_function assert fitter._dependent_dims is None - assert fitter._enum_current_minimizer is None # == AvailableMinimizers.LMFit_leastsq + assert fitter._enum_current_minimizer is None # == Minimizers.LMFit_leastsq assert fitter._minimizer is None - fitter._update_minimizer.assert_called_once_with(AvailableMinimizers.LMFit_leastsq) + fitter._update_minimizer.assert_called_once_with(Minimizers.LMFit_leastsq) def test_make_model(self, fitter: Fitter): # When @@ -67,41 +67,31 @@ def test_initialize(self, fitter: Fitter): assert fitter._fit_function == mock_fit_function fitter._update_minimizer.count(2) - def test_create(self, fitter: Fitter, monkeypatch): + def test_switch_minimizer(self, fitter: Fitter): # When fitter._update_minimizer = MagicMock() - mock_string_to_enum = MagicMock(return_value=10) - monkeypatch.setattr(easyscience.fitting.fitter, 'from_string_to_enum', mock_string_to_enum) # Then - fitter.create('great-minimizer') + fitter.switch_minimizer(Minimizers.Bumps) # Expect - mock_string_to_enum.assert_called_once_with('great-minimizer') - fitter._update_minimizer.assert_called_once_with(10) + fitter._update_minimizer.assert_called_once_with(Minimizers.Bumps) - def test_switch_minimizer(self, fitter: Fitter, monkeypatch): + def test_switch_minimizer_rejects_string(self, fitter: Fitter): # When - mock_minimizer = MagicMock() - fitter._minimizer = mock_minimizer - mock_string_to_enum = MagicMock(return_value=10) - monkeypatch.setattr(easyscience.fitting.fitter, 'from_string_to_enum', mock_string_to_enum) - - # Then - fitter.switch_minimizer('great-minimizer') + fitter._update_minimizer = MagicMock() - # Expect - fitter._update_minimizer.count(2) - mock_string_to_enum.assert_called_once_with('great-minimizer') + # Then Expect + with pytest.raises(TypeError, match='Minimizers'): + fitter.switch_minimizer('Bumps') + fitter._update_minimizer.assert_not_called() 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) @@ -118,18 +108,18 @@ def test_available_minimizers(self, fitter: Fitter): # Then Expect assert minimizers == [ - 'LMFit', - 'LMFit_leastsq', - 'LMFit_powell', - 'LMFit_cobyla', - 'LMFit_differential_evolution', - 'LMFit_scipy_least_squares', - 'Bumps', - 'Bumps_simplex', - 'Bumps_newton', - 'Bumps_lm', - 'DFO', - 'DFO_leastsq', + Minimizers.LMFit, + Minimizers.LMFit_leastsq, + Minimizers.LMFit_powell, + Minimizers.LMFit_cobyla, + Minimizers.LMFit_differential_evolution, + Minimizers.LMFit_scipy_least_squares, + Minimizers.Bumps, + Minimizers.Bumps_simplex, + Minimizers.Bumps_newton, + Minimizers.Bumps_lm, + Minimizers.DFO, + Minimizers.DFO_leastsq, ] def test_minimizer(self, fitter: Fitter):