diff --git a/src/easyscience/fitting/minimizers/minimizer_base.py b/src/easyscience/fitting/minimizers/minimizer_base.py index fdc950ed..10006da0 100644 --- a/src/easyscience/fitting/minimizers/minimizer_base.py +++ b/src/easyscience/fitting/minimizers/minimizer_base.py @@ -4,6 +4,9 @@ from abc import ABCMeta from abc import abstractmethod +from inspect import Parameter as InspectParameter +from inspect import Signature +from inspect import _empty from typing import Callable from typing import Dict from typing import List @@ -166,6 +169,94 @@ def _prepare_parameters(self, parameters: dict[str, float]) -> dict[str, float]: parameters[parameter_name] = item.raw_value return parameters + def _generate_fit_function(self) -> Callable: + """ + Using the user supplied `fit_function`, wrap it in such a way we can update `Parameter` on + iterations. + + :return: a fit function which is compatible with bumps models + """ + # Original fit function + func = self._original_fit_function + # Get a list of `Parameters` + self._cached_pars = {} + self._cached_pars_vals = {} + for parameter in self._object.get_fit_parameters(): + key = parameter.unique_name + self._cached_pars[key] = parameter + self._cached_pars_vals[key] = (parameter.value, parameter.error) + + # Make a new fit function + def _fit_function(x: np.ndarray, **kwargs): + """ + Wrapped fit function which now has an EasyScience compatible form + + :param x: array of data points to be calculated + :type x: np.ndarray + :param kwargs: key word arguments + :return: points calculated at `x` + :rtype: np.ndarray + """ + # Update the `Parameter` values and the callback if needed + # TODO THIS IS NOT THREAD SAFE :-( + # TODO clean when full move to new_variable + from easyscience.Objects.new_variable import Parameter + + for name, value in kwargs.items(): + par_name = name[1:] + if par_name in self._cached_pars.keys(): + # TODO clean when full move to new_variable + if isinstance(self._cached_pars[par_name], Parameter): + # This will take into account constraints + if self._cached_pars[par_name].value != value: + self._cached_pars[par_name].value = value + else: + # This will take into account constraints + if self._cached_pars[par_name].raw_value != value: + self._cached_pars[par_name].value = value + + # Since we are calling the parameter fset will be called. + # TODO Pre processing here + for constraint in self.fit_constraints(): + constraint() + return_data = func(x) + # TODO Loading or manipulating data here + return return_data + + _fit_function.__signature__ = self._create_signature(self._cached_pars) + return _fit_function + + @staticmethod + def _create_signature(parameters: Dict[int, Parameter]) -> Signature: + """ + Wrap the function signature. + This is done as lmfit wants the function to be in the form: + f = (x, a=1, b=2)... + Where we need to be generic. Note that this won't hold for much outside of this scope. + """ + wrapped_parameters = [] + wrapped_parameters.append(InspectParameter('x', InspectParameter.POSITIONAL_OR_KEYWORD, annotation=_empty)) + + ## TODO clean when full move to new_variable + from easyscience.Objects.new_variable import Parameter as NewParameter + + for name, parameter in parameters.items(): + ## TODO clean when full move to new_variable + if isinstance(parameter, NewParameter): + default_value = parameter.value + else: + default_value = parameter.raw_value + + wrapped_parameters.append( + InspectParameter( + MINIMIZER_PARAMETER_PREFIX + str(name), + InspectParameter.POSITIONAL_OR_KEYWORD, + annotation=_empty, + default=default_value, + ) + ) + return Signature(wrapped_parameters) + @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_bumps.py b/src/easyscience/fitting/minimizers/minimizer_bumps.py index 3881372d..52ee4dcd 100644 --- a/src/easyscience/fitting/minimizers/minimizer_bumps.py +++ b/src/easyscience/fitting/minimizers/minimizer_bumps.py @@ -3,7 +3,6 @@ # © 2021-2023 Contributors to the EasyScience project List[str]: @@ -204,90 +202,6 @@ def _make_func(x, y, weights): return _outer(self) - def _generate_fit_function(self) -> Callable: - """ - Using the user supplied `fit_function`, wrap it in such a way we can update `Parameter` on - iterations. - - :return: a fit function which is compatible with bumps models - :rtype: Callable - """ - # Original fit function - func = self._original_fit_function - # Get a list of `Parameters` - self._cached_pars_vals = {} - for parameter in self._object.get_fit_parameters(): - key = parameter.unique_name - self._cached_pars[key] = parameter - self._cached_pars_vals[key] = (parameter.value, parameter.error) - - # Make a new fit function - def _fit_function(x: np.ndarray, **kwargs): - """ - Wrapped fit function which now has a bumps compatible form - - :param x: array of data points to be calculated - :type x: np.ndarray - :param kwargs: key word arguments - :return: points calculated at `x` - :rtype: np.ndarray - """ - # Update the `Parameter` values and the callback if needed - ## TODO clean when full move to new_variable - from easyscience.Objects.new_variable import Parameter - - for name, value in kwargs.items(): - par_name = name[1:] - if par_name in self._cached_pars.keys(): - ## TODO clean when full move to new_variable - if isinstance(self._cached_pars[par_name], Parameter): - if self._cached_pars[par_name].value != value: - self._cached_pars[par_name].value = value - else: - if self._cached_pars[par_name].raw_value != value: - self._cached_pars[par_name].value = value - - # update_fun = self._cached_pars[par_name]._callback.fset - # if update_fun: - # update_fun(value) - # TODO Pre processing here - for constraint in self.fit_constraints(): - constraint() - return_data = func(x) - # TODO Loading or manipulating data here - return return_data - - # Fake the function signature. - # This is done as lmfit wants the function to be in the form: - # f = (x, a=1, b=2)... - # Where we need to be generic. Note that this won't hold for much outside of this scope. - - ## TODO clean when full move to new_variable - from easyscience.Objects.new_variable import Parameter - - if isinstance(parameter, Parameter): - default_value = parameter.value - else: - default_value = parameter.raw_value - - self._cached_pars_order = tuple(self._cached_pars.keys()) - params = [ - inspect.Parameter('x', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=inspect._empty), - *[ - inspect.Parameter( - MINIMIZER_PARAMETER_PREFIX + str(name), - inspect.Parameter.POSITIONAL_OR_KEYWORD, - annotation=inspect._empty, - default=default_value, - ) - for name in self._cached_pars_order - ], - ] - # Sign the function - _fit_function.__signature__ = inspect.Signature(params) - self._fit_function = _fit_function - return _fit_function - def _set_parameter_fit_result(self, fit_result, stack_status: bool): """ Update parameters to their final values and assign a std error to them. diff --git a/src/easyscience/fitting/minimizers/minimizer_dfo.py b/src/easyscience/fitting/minimizers/minimizer_dfo.py index e6e91bc7..a57f5d24 100644 --- a/src/easyscience/fitting/minimizers/minimizer_dfo.py +++ b/src/easyscience/fitting/minimizers/minimizer_dfo.py @@ -174,64 +174,6 @@ def _residuals(pars_values: List[float]) -> np.ndarray: return _outer(self) - def _generate_fit_function(self) -> Callable: - """ - Using the user supplied `fit_function`, wrap it in such a way we can update `Parameter` on - iterations. - - :return: a fit function which is compatible with bumps models - :rtype: Callable - """ - # Original fit function - func = self._original_fit_function - # Get a list of `Parameters` - self._cached_pars = {} - self._cached_pars_vals = {} - for parameter in self._object.get_fit_parameters(): - key = parameter.unique_name - self._cached_pars[key] = parameter - self._cached_pars_vals[key] = (parameter.value, parameter.error) - - # Make a new fit function - def _fit_function(x: np.ndarray, **kwargs): - """ - Wrapped fit function which now has an EasyScience compatible form - - :param x: array of data points to be calculated - :type x: np.ndarray - :param kwargs: key word arguments - :return: points calculated at `x` - :rtype: np.ndarray - """ - # Update the `Parameter` values and the callback if needed - # TODO THIS IS NOT THREAD SAFE :-( - # TODO clean when full move to new_variable - from easyscience.Objects.new_variable import Parameter - - for name, value in kwargs.items(): - par_name = name[1:] - if par_name in self._cached_pars.keys(): - # TODO clean when full move to new_variable - if isinstance(self._cached_pars[par_name], Parameter): - # This will take into account constraints - if self._cached_pars[par_name].value != value: - self._cached_pars[par_name].value = value - else: - # This will take into account constraints - if self._cached_pars[par_name].raw_value != value: - self._cached_pars[par_name].value = value - - # Since we are calling the parameter fset will be called. - # TODO Pre processing here - for constraint in self.fit_constraints(): - constraint() - return_data = func(x) - # TODO Loading or manipulating data here - return return_data - - self._fit_function = _fit_function - return _fit_function - def _set_parameter_fit_result(self, fit_result, stack_status, ci: float = 0.95) -> None: """ Update parameters to their final values and assign a std error to them. diff --git a/src/easyscience/fitting/minimizers/minimizer_lmfit.py b/src/easyscience/fitting/minimizers/minimizer_lmfit.py index c099fdad..4f82a711 100644 --- a/src/easyscience/fitting/minimizers/minimizer_lmfit.py +++ b/src/easyscience/fitting/minimizers/minimizer_lmfit.py @@ -2,11 +2,8 @@ # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project LMModel: - """ - Generate a lmfit model from the supplied `fit_function` and parameters in the base object. - - :return: Callable lmfit model - :rtype: LMModel - """ - # Generate the fitting function - fit_func = self._generate_fit_function() - self._fit_function = fit_func - - if pars is None: - pars = self._cached_pars - # Create the model - model = LMModel( - fit_func, - independent_vars=['x'], - param_names=[MINIMIZER_PARAMETER_PREFIX + str(key) for key in pars.keys()], - ) - # Assign values from the `Parameter` to the model - for name, item in pars.items(): - if isinstance(item, LMParameter): - value = item.value - else: - ## TODO clean when full move to new_variable - from easyscience.Objects.new_variable import Parameter - - if isinstance(item, Parameter): - value = item.value - else: - value = item.raw_value - - model.set_param_hint(MINIMIZER_PARAMETER_PREFIX + str(name), value=value, min=item.min, max=item.max) - - # Cache the model for later reference - self._cached_model = model - return model - - def _generate_fit_function(self) -> Callable: - """ - Using the user supplied `fit_function`, wrap it in such a way we can update `Parameter` on - iterations. - - :return: a fit function which is compatible with lmfit models - :rtype: Callable - """ - # Get a list of `Parameters` - self._cached_pars = {} - self._cached_pars_vals = {} - for parameter in self._object.get_fit_parameters(): - key = parameter.unique_name - self._cached_pars[key] = parameter - self._cached_pars_vals[key] = (parameter.value, parameter.error) - - # Make a lm fit function - def lm_fit_function(x: np.ndarray, **kwargs): - """ - Fit function with a lmfit compatible signature. - - :param x: array of data points to be calculated - :type x: np.ndarray - :param kwargs: key word arguments - :return: points, `f(x)`, calculated at `x` - :rtype: np.ndarray - """ - # Update the `Parameter` values and the callback if needed - # TODO THIS IS NOT THREAD SAFE :-( - for name, value in kwargs.items(): - par_name = name[1:] - if par_name in self._cached_pars.keys(): - # This will take into account constraints - - ## TODO clean when full move to new_variable - from easyscience.Objects.new_variable import Parameter - - if isinstance(self._cached_pars[par_name], Parameter): - if self._cached_pars[par_name].value != value: - self._cached_pars[par_name].value = value - else: - if self._cached_pars[par_name].raw_value != value: - self._cached_pars[par_name].value = value - - # Since we are calling the parameter fset will be called. - # TODO Pre processing here - for constraint in self.fit_constraints(): - constraint() - return_data = self._original_fit_function(x) - # TODO Loading or manipulating data here - return return_data - - lm_fit_function.__signature__ = self._wrap_to_lm_signature(self._cached_pars) - return lm_fit_function + def available_methods(self) -> List[str]: + return [ + 'least_squares', + 'leastsq', + 'differential_evolution', + 'basinhopping', + 'ampgo', + 'nelder', + 'lbfgsb', + 'powell', + 'cg', + 'newton', + 'cobyla', + 'bfgs', + ] def fit( self, @@ -257,6 +177,45 @@ def convert_to_par_object(parameter: Parameter) -> LMParameter: brute_step=None, ) + def _make_model(self, pars: Optional[LMParameters] = None) -> LMModel: + """ + Generate a lmfit model from the supplied `fit_function` and parameters in the base object. + + :return: Callable lmfit model + :rtype: LMModel + """ + # Generate the fitting function + fit_func = self._generate_fit_function() + + self._fit_function = fit_func + + if pars is None: + pars = self._cached_pars + # Create the model + model = LMModel( + fit_func, + independent_vars=['x'], + param_names=[MINIMIZER_PARAMETER_PREFIX + str(key) for key in pars.keys()], + ) + # Assign values from the `Parameter` to the model + for name, item in pars.items(): + if isinstance(item, LMParameter): + value = item.value + else: + ## TODO clean when full move to new_variable + from easyscience.Objects.new_variable import Parameter + + if isinstance(item, Parameter): + value = item.value + else: + value = item.raw_value + + model.set_param_hint(MINIMIZER_PARAMETER_PREFIX + str(name), value=value, min=item.min, max=item.max) + + # Cache the model for later reference + self._cached_model = model + return model + def _set_parameter_fit_result(self, fit_result: ModelResult, stack_status: bool): """ Update parameters to their final values and assign a std error to them. @@ -313,50 +272,3 @@ def _gen_fit_results(self, fit_results: ModelResult, **kwargs) -> FitResults: results.engine_result = fit_results # results.check_sanity() return results - - def available_methods(self) -> List[str]: - return [ - 'least_squares', - 'leastsq', - 'differential_evolution', - 'basinhopping', - 'ampgo', - 'nelder', - 'lbfgsb', - 'powell', - 'cg', - 'newton', - 'cobyla', - 'bfgs', - ] - - @staticmethod - def _wrap_to_lm_signature(parameters: Dict[int, Parameter]) -> Signature: - """ - Wrap the function signature. - This is done as lmfit wants the function to be in the form: - f = (x, a=1, b=2)... - Where we need to be generic. Note that this won't hold for much outside of this scope. - """ - wrapped_parameters = [] - wrapped_parameters.append(InspectParameter('x', InspectParameter.POSITIONAL_OR_KEYWORD, annotation=_empty)) - - ## TODO clean when full move to new_variable - from easyscience.Objects.new_variable import Parameter as NewParameter - - for name, parameter in parameters.items(): - ## TODO clean when full move to new_variable - if isinstance(parameter, NewParameter): - default_value = parameter.value - else: - default_value = parameter.raw_value - - wrapped_parameters.append( - InspectParameter( - MINIMIZER_PARAMETER_PREFIX + str(name), - InspectParameter.POSITIONAL_OR_KEYWORD, - annotation=_empty, - default=default_value, - ) - ) - return Signature(wrapped_parameters) diff --git a/tests/unit_tests/Fitting/minimizers/test_minimizer_base.py b/tests/unit_tests/Fitting/minimizers/test_minimizer_base.py index dd6a29de..c036d4f9 100644 --- a/tests/unit_tests/Fitting/minimizers/test_minimizer_base.py +++ b/tests/unit_tests/Fitting/minimizers/test_minimizer_base.py @@ -2,8 +2,13 @@ from unittest.mock import MagicMock +from inspect import Parameter as InspectParameter +from inspect import Signature +from inspect import _empty + from easyscience.fitting.minimizers.minimizer_base import MinimizerBase from easyscience.fitting.minimizers.utils import FitError +from easyscience.Objects.new_variable import Parameter class TestMinimizerBase(): @pytest.fixture @@ -23,6 +28,7 @@ def test_init_exception(self): # When Then MinimizerBase.__abstractmethods__ = set() MinimizerBase.available_methods = MagicMock(return_value=['method']) + # Expect with pytest.raises(FitError): MinimizerBase( @@ -112,4 +118,54 @@ def test_prepare_parameters(self, minimizer: MinimizerBase): 'pa': 1, 'pb': 2, 'pc': 5 - } \ No newline at end of file + } + + def test_generate_fit_function(self, minimizer: MinimizerBase) -> None: + # When + minimizer._original_fit_function = MagicMock(return_value='fit_function_result') + + mock_fit_constraint = MagicMock() + minimizer.fit_constraints = MagicMock(return_value=[mock_fit_constraint]) + + minimizer._object = MagicMock() + mock_parm_1 = MagicMock(Parameter) + mock_parm_1.unique_name = 'mock_parm_1' + mock_parm_1.value = 1.0 + mock_parm_1.error = 0.1 + mock_parm_2 = MagicMock(Parameter) + mock_parm_2.unique_name = 'mock_parm_2' + mock_parm_2.value = 2.0 + mock_parm_2.error = 0.2 + minimizer._object.get_fit_parameters = MagicMock(return_value=[mock_parm_1, mock_parm_2]) + + # Then + fit_function = minimizer._generate_fit_function() + fit_function_result = fit_function([10.0]) + + # Expect + assert 'fit_function_result' == fit_function_result + mock_fit_constraint.assert_called_once_with() + minimizer._original_fit_function.assert_called_once_with([10.0]) + assert minimizer._cached_pars['mock_parm_1'] == mock_parm_1 + assert minimizer._cached_pars['mock_parm_2'] == mock_parm_2 + assert str(fit_function.__signature__) == '(x, pmock_parm_1=1.0, pmock_parm_2=2.0)' + + def test_create_signature(self, minimizer: MinimizerBase) -> None: + # When + mock_parm_1 = MagicMock(Parameter) + mock_parm_1.value = 1.0 + mock_parm_2 = MagicMock(Parameter) + mock_parm_2.value = 2.0 + pars = {1: mock_parm_1, 2: mock_parm_2} + + # Then + signature = minimizer._create_signature(pars) + + # Expect + wrapped_parameters = [ + InspectParameter('x', InspectParameter.POSITIONAL_OR_KEYWORD, annotation=_empty), + InspectParameter('p1', InspectParameter.POSITIONAL_OR_KEYWORD, annotation=_empty, default=1.0), + InspectParameter('p2', InspectParameter.POSITIONAL_OR_KEYWORD, annotation=_empty, default=2.0) + ] + expected_signature = Signature(wrapped_parameters) + assert signature == expected_signature \ No newline at end of file diff --git a/tests/unit_tests/Fitting/minimizers/test_minimizer_bumps.py b/tests/unit_tests/Fitting/minimizers/test_minimizer_bumps.py index 50b460d8..558b9b60 100644 --- a/tests/unit_tests/Fitting/minimizers/test_minimizer_bumps.py +++ b/tests/unit_tests/Fitting/minimizers/test_minimizer_bumps.py @@ -22,7 +22,6 @@ def minimizer(self) -> Bumps: def test_init(self, minimizer: Bumps) -> None: assert minimizer._p_0 == {} - assert minimizer._cached_pars_order == () assert minimizer.wrapping == 'bumps' def test_init_exception(self) -> None: @@ -71,36 +70,6 @@ def test_fit(self, minimizer: Bumps, monkeypatch) -> None: mock_model_function.assert_called_once_with(1.0, 2.0, 1.4142135623730951) mock_FitProblem.assert_called_once_with(mock_model) - def test_generate_fit_function(self, minimizer: Bumps) -> None: - # When - minimizer._original_fit_function = MagicMock(return_value='fit_function_result') - - mock_fit_constraint = MagicMock() - minimizer.fit_constraints = MagicMock(return_value=[mock_fit_constraint]) - - minimizer._object = MagicMock() - mock_parm_1 = MagicMock(Parameter) - mock_parm_1.unique_name = 'mock_parm_1' - mock_parm_1.value = 1.0 - mock_parm_1.error = 0.1 - mock_parm_2 = MagicMock(Parameter) - mock_parm_2.unique_name = 'mock_parm_2' - mock_parm_2.value = 2.0 - mock_parm_2.error = 0.2 - minimizer._object.get_fit_parameters = MagicMock(return_value=[mock_parm_1, mock_parm_2]) - - # Then - fit_function = minimizer._generate_fit_function() - fit_function_result = fit_function([10.0]) - - # Expect - assert 'fit_function_result' == fit_function_result - mock_fit_constraint.assert_called_once_with() - minimizer._original_fit_function.assert_called_once_with([10.0]) - assert minimizer._cached_pars['mock_parm_1'] == mock_parm_1 - assert minimizer._cached_pars['mock_parm_2'] == mock_parm_2 - assert minimizer._cached_pars_order == ('mock_parm_1', 'mock_parm_2') - assert str(fit_function.__signature__) == '(x, pmock_parm_1=2.0, pmock_parm_2=2.0)' def test_make_model(self, minimizer: Bumps, monkeypatch) -> None: # When diff --git a/tests/unit_tests/Fitting/minimizers/test_minimizer_lmfit.py b/tests/unit_tests/Fitting/minimizers/test_minimizer_lmfit.py index d992e91b..b8b42f28 100644 --- a/tests/unit_tests/Fitting/minimizers/test_minimizer_lmfit.py +++ b/tests/unit_tests/Fitting/minimizers/test_minimizer_lmfit.py @@ -1,8 +1,5 @@ import pytest -from inspect import Parameter as InspectParameter -from inspect import Signature -from inspect import _empty from unittest.mock import MagicMock import easyscience.fitting.minimizers.minimizer_lmfit @@ -87,57 +84,6 @@ def test_make_model_no_pars(self, minimizer: LMFit, monkeypatch) -> None: assert mock_lm_model.set_param_hint.call_count == 2 assert model == mock_lm_model - def test_generate_fit_function_signatur(self, minimizer: LMFit) -> None: - # When - mock_parm_1 = MagicMock(Parameter) - mock_parm_1.value = 1.0 - mock_parm_1.error = 0.1 - mock_parm_2 = MagicMock(Parameter) - mock_parm_2.value = 2.0 - mock_parm_2.error = 0.2 - mock_obj = MagicMock(BaseObj) - mock_obj.get_fit_parameters = MagicMock(return_value=[mock_parm_1, mock_parm_2]) - minimizer._object = mock_obj - - mock_wrap_to_lm_signature = MagicMock(return_value='signature') - minimizer._wrap_to_lm_signature = mock_wrap_to_lm_signature - minimizer._original_fit_function = MagicMock(return_value='fit_function_return') - - # Then - fit_function = minimizer._generate_fit_function() - - # Expect - assert fit_function.__signature__ == 'signature' - - def test_generate_fit_function_lm_fit_function(self, minimizer: LMFit) -> None: - # When - mock_parm_1 = MagicMock(Parameter) - mock_parm_1.value = 1.0 - mock_parm_1.error = 0.1 - mock_parm_2 = MagicMock(Parameter) - mock_parm_2.value = 2.0 - mock_parm_2.error = 0.2 - mock_obj = MagicMock(BaseObj) - mock_obj.get_fit_parameters = MagicMock(return_value=[mock_parm_1, mock_parm_2]) - minimizer._object = mock_obj - - mock_wrap_to_lm_signature = MagicMock(return_value='signature') - minimizer._wrap_to_lm_signature = mock_wrap_to_lm_signature - - minimizer._original_fit_function = MagicMock(return_value='fit_function_return') - - mock_constraint = MagicMock() - minimizer.fit_constraints = MagicMock(return_value=[mock_constraint]) - - fit_function = minimizer._generate_fit_function() - - # Then - result = fit_function(1) - - # Expect - result == 'fit_function_return' - mock_constraint.assert_called_once_with() - def test_fit(self, minimizer: LMFit) -> None: # When from easyscience import global_object @@ -361,22 +307,3 @@ def test_gen_fit_results(self, minimizer: LMFit, monkeypatch) -> None: assert str(domain_fit_results.minimizer_engine) == "" assert domain_fit_results.fit_args is None - def test_wrap_to_lm_signature(self, minimizer: LMFit) -> None: - # When - mock_parm_1 = MagicMock(Parameter) - mock_parm_1.value = 1.0 - mock_parm_2 = MagicMock(Parameter) - mock_parm_2.value = 2.0 - pars = {1: mock_parm_1, 2: mock_parm_2} - - # Then - signature = minimizer._wrap_to_lm_signature(pars) - - # Expect - wrapped_parameters = [ - InspectParameter('x', InspectParameter.POSITIONAL_OR_KEYWORD, annotation=_empty), - InspectParameter('p1', InspectParameter.POSITIONAL_OR_KEYWORD, annotation=_empty, default=1.0), - InspectParameter('p2', InspectParameter.POSITIONAL_OR_KEYWORD, annotation=_empty, default=2.0) - ] - expected_signature = Signature(wrapped_parameters) - assert signature == expected_signature