diff --git a/src/easyscience/fitting/minimizers/minimizer_bumps.py b/src/easyscience/fitting/minimizers/minimizer_bumps.py index ac093cc6..3315d2eb 100644 --- a/src/easyscience/fitting/minimizers/minimizer_bumps.py +++ b/src/easyscience/fitting/minimizers/minimizer_bumps.py @@ -98,6 +98,11 @@ def fit( :type method: str :return: Fit results :rtype: ModelResult + + For standard least squares, the weights should be 1/sigma, where + sigma is the standard deviation of the measurement. For + unweighted least squares, these should be 1. + """ method_dict = self._get_method_kwargs(method) @@ -191,6 +196,8 @@ def _make_model(self, parameters: Optional[List[BumpsParameter]] = None) -> Call Generate a bumps model from the supplied `fit_function` and parameters in the base object. Note that this makes a callable as it needs to be initialized with *x*, *y*, *weights* + Weights are converted to dy (standard deviation of y). + :return: Callable to make a bumps Curve model :rtype: Callable """ @@ -205,7 +212,7 @@ def _make_func(x, y, weights): else: for par in parameters: bumps_pars[MINIMIZER_PARAMETER_PREFIX + par.unique_name] = obj.convert_to_par_object(par) - return Curve(fit_func, x, y, dy=weights, **bumps_pars) + return Curve(fit_func, x, y, dy=1 / weights, **bumps_pars) return _make_func diff --git a/src/easyscience/fitting/minimizers/minimizer_dfo.py b/src/easyscience/fitting/minimizers/minimizer_dfo.py index 8f23d88d..dacef618 100644 --- a/src/easyscience/fitting/minimizers/minimizer_dfo.py +++ b/src/easyscience/fitting/minimizers/minimizer_dfo.py @@ -74,7 +74,7 @@ def fit( :type x: np.ndarray :param y: measured points :type y: np.ndarray - :param weights: Weights for supplied measured points + :param weights: Weights for supplied measured points. :type weights: np.ndarray :param model: Optional Model which is being fitted to :type model: lmModel @@ -85,6 +85,10 @@ def fit( :type method: str :return: Fit results :rtype: ModelResult + + For standard least squares, the weights should be 1/sigma, where + sigma is the standard deviation of the measurement. For + unweighted least squares, these should be 1. """ x, y, weights = np.asarray(x), np.asarray(y), np.asarray(weights) @@ -163,7 +167,7 @@ def _make_func(x, y, weights): def _residuals(pars_values: List[float]) -> np.ndarray: for idx, par_name in enumerate(dfo_pars.keys()): dfo_pars[par_name] = pars_values[idx] - return (y - fit_func(x, **dfo_pars)) / weights + return (y - fit_func(x, **dfo_pars)) * weights return _residuals @@ -267,7 +271,11 @@ def _dfo_fit( return results @staticmethod - def _prepare_kwargs(tolerance: Optional[float] = None, max_evaluations: Optional[int] = None, **kwargs) -> dict[str:str]: + def _prepare_kwargs( + tolerance: Optional[float] = None, + max_evaluations: Optional[int] = None, + **kwargs, + ) -> dict[str:str]: if max_evaluations is not None: kwargs['maxfun'] = max_evaluations # max number of function evaluations if tolerance is not None: diff --git a/src/easyscience/fitting/minimizers/minimizer_lmfit.py b/src/easyscience/fitting/minimizers/minimizer_lmfit.py index 4a10993c..2c63cea3 100644 --- a/src/easyscience/fitting/minimizers/minimizer_lmfit.py +++ b/src/easyscience/fitting/minimizers/minimizer_lmfit.py @@ -111,6 +111,11 @@ def fit( :param kwargs: Additional arguments for the fitting function. :return: Fit results :rtype: ModelResult + + For standard least squares, the weights should be 1/sigma, where + sigma is the standard deviation of the measurement. For + unweighted least squares, these should be 1. + """ x, y, weights = np.asarray(x), np.asarray(y), np.asarray(weights) @@ -230,7 +235,12 @@ def _make_model(self, pars: Optional[LMParameters] = None) -> LMModel: else: value = item.value - model.set_param_hint(MINIMIZER_PARAMETER_PREFIX + str(name), value=value, min=item.min, max=item.max) + 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 diff --git a/tests/integration_tests/fitting/test_fitter.py b/tests/integration_tests/fitting/test_fitter.py index bfa3f237..d5c9b255 100644 --- a/tests/integration_tests/fitting/test_fitter.py +++ b/tests/integration_tests/fitting/test_fitter.py @@ -12,6 +12,7 @@ from easyscience.fitting.minimizers import FitError from easyscience.base_classes import ModelBase + # Model and container of parameters for tests class AbsSin(ObjBase): phase: Parameter @@ -36,20 +37,21 @@ def __init__(self, offset_val: float, phase_val: float): super().__init__("sin2D", offset=offset, phase=phase) def __call__(self, x): - X = x[:, :, 0] # x is a 2D array + X = x[:, :, 0] # x is a 2D array Y = x[:, :, 1] - return np.abs( - np.sin(self.phase.value * X + self.offset.value) - ) * np.abs(np.sin(self.phase.value * Y + self.offset.value)) + return np.abs(np.sin(self.phase.value * X + self.offset.value)) * np.abs( + np.sin(self.phase.value * Y + self.offset.value) + ) class AbsSin2DL(AbsSin2D): def __call__(self, x): - X = x[:, 0] # x is a 1D array + X = x[:, 0] # x is a 1D array Y = x[:, 1] - return np.abs( - np.sin(self.phase.value * X + self.offset.value) - ) * np.abs(np.sin(self.phase.value * Y + self.offset.value)) + return np.abs(np.sin(self.phase.value * X + self.offset.value)) * np.abs( + np.sin(self.phase.value * Y + self.offset.value) + ) + class StraightLine(ModelBase): def __init__(self, slope: float, intercept: float): @@ -60,7 +62,7 @@ def __init__(self, slope: float, intercept: float): @property def slope(self) -> Parameter: return self._slope - + @slope.setter def slope(self, value: float) -> None: self._slope.value = value @@ -100,7 +102,15 @@ def check_fit_results(result, sp_sin, ref_sin, x, **kwargs): assert result.residual == pytest.approx(sp_sin(x) - y_calc_ref, abs=1e-2) -@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) +@pytest.mark.parametrize( + "fit_engine", + [ + None, + AvailableMinimizers.LMFit, + AvailableMinimizers.Bumps, + AvailableMinimizers.DFO, + ], +) def test_basic_fit(fit_engine: AvailableMinimizers): ref_sin = AbsSin(0.2, np.pi) sp_sin = AbsSin(0.354, 3.05) @@ -122,12 +132,22 @@ def test_basic_fit(fit_engine: AvailableMinimizers): result = f.fit(x=x, y=y, weights=weights) if fit_engine is not None: - assert result.minimizer_engine.package == fit_engine.name.lower() # Special case where minimizer matches package + assert ( + result.minimizer_engine.package == fit_engine.name.lower() + ) # Special case where minimizer matches package assert sp_sin.phase.value == pytest.approx(ref_sin.phase.value, rel=1e-3) assert sp_sin.offset.value == pytest.approx(ref_sin.offset.value, rel=1e-3) -@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) +@pytest.mark.parametrize( + "fit_engine", + [ + None, + AvailableMinimizers.LMFit, + AvailableMinimizers.Bumps, + AvailableMinimizers.DFO, + ], +) def test_fit_result(fit_engine): ref_sin = AbsSin(0.2, np.pi) sp_sin = AbsSin(0.354, 3.05) @@ -160,7 +180,15 @@ def test_fit_result(fit_engine): check_fit_results(result, sp_sin, ref_sin, x, sp_ref1=sp_ref1, sp_ref2=sp_ref2) -@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) +@pytest.mark.parametrize( + "fit_engine", + [ + None, + AvailableMinimizers.LMFit, + AvailableMinimizers.Bumps, + AvailableMinimizers.DFO, + ], +) def test_basic_max_evaluations(fit_engine): ref_sin = AbsSin(0.2, np.pi) sp_sin = AbsSin(0.354, 3.05) @@ -189,7 +217,15 @@ def test_basic_max_evaluations(fit_engine): assert "Objective has been called MAXFUN times" in str(e) -@pytest.mark.parametrize("fit_engine,tolerance", [(None, 10), (AvailableMinimizers.LMFit, 10), (AvailableMinimizers.Bumps, 10), (AvailableMinimizers.DFO, 0.1)]) +@pytest.mark.parametrize( + "fit_engine,tolerance", + [ + (None, 10), + (AvailableMinimizers.LMFit, 10), + (AvailableMinimizers.Bumps, 10), + (AvailableMinimizers.DFO, 0.1), + ], +) def test_basic_tolerance(fit_engine, tolerance): ref_sin = AbsSin(0.2, np.pi) sp_sin = AbsSin(0.354, 3.05) @@ -232,7 +268,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) @@ -252,7 +288,10 @@ def test_bumps_methods(fit_method): check_fit_results(result, sp_sin, ref_sin, x) -@pytest.mark.parametrize("fit_engine", [AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) +@pytest.mark.parametrize( + "fit_engine", + [AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO], +) def test_dependent_parameter(fit_engine): ref_sin = AbsSin(np.pi * 0.45, 0.45 * np.pi * 0.5) sp_sin = AbsSin(1, 0.5) @@ -263,7 +302,9 @@ def test_dependent_parameter(fit_engine): f = Fitter(sp_sin, sp_sin) - sp_sin.offset.make_dependent_on(dependency_expression='2*phase', dependency_map={"phase": sp_sin.phase}) + sp_sin.offset.make_dependent_on( + dependency_expression="2*phase", dependency_map={"phase": sp_sin.phase} + ) if fit_engine is not None: try: @@ -274,13 +315,20 @@ def test_dependent_parameter(fit_engine): result = f.fit(x, y, weights=weights) check_fit_results(result, sp_sin, ref_sin, x) -@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) + +@pytest.mark.parametrize( + "fit_engine", + [ + None, + AvailableMinimizers.LMFit, + AvailableMinimizers.Bumps, + AvailableMinimizers.DFO, + ], +) def test_2D_vectorized(fit_engine): x = np.linspace(0, 5, 200) mm = AbsSin2D(0.3, 1.6) - m2 = AbsSin2D( - 0.1, 1.8 - ) # The fit is quite sensitive to the initial values :-( + m2 = AbsSin2D(0.1, 1.8) # The fit is quite sensitive to the initial values :-( X, Y = np.meshgrid(x, x) XY = np.stack((X, Y), axis=2) weights = np.ones_like(mm(XY)) @@ -306,13 +354,19 @@ def test_2D_vectorized(fit_engine): assert result.residual == pytest.approx(mm(XY) - y_calc_ref, abs=1e-2) -@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) +@pytest.mark.parametrize( + "fit_engine", + [ + None, + AvailableMinimizers.LMFit, + AvailableMinimizers.Bumps, + AvailableMinimizers.DFO, + ], +) def test_2D_non_vectorized(fit_engine): x = np.linspace(0, 5, 200) mm = AbsSin2DL(0.3, 1.6) - m2 = AbsSin2DL( - 0.1, 1.8 - ) # The fit is quite sensitive to the initial values :-( + m2 = AbsSin2DL(0.1, 1.8) # The fit is quite sensitive to the initial values :-( X, Y = np.meshgrid(x, x) XY = np.stack((X, Y), axis=2) weights = np.ones_like(mm(XY.reshape(-1, 2))) @@ -323,7 +377,9 @@ def test_2D_non_vectorized(fit_engine): except AttributeError: pytest.skip(msg=f"{fit_engine} is not installed") try: - result = ff.fit(x=XY, y=mm(XY.reshape(-1, 2)), weights=weights, vectorized=False) + result = ff.fit( + x=XY, y=mm(XY.reshape(-1, 2)), weights=weights, vectorized=False + ) except FitError as e: if "Unable to allocate" in str(e): pytest.skip(msg="MemoryError - Matrix too large") @@ -339,7 +395,16 @@ def test_2D_non_vectorized(fit_engine): mm(XY.reshape(-1, 2)) - y_calc_ref, abs=1e-2 ) -@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) + +@pytest.mark.parametrize( + "fit_engine", + [ + None, + AvailableMinimizers.LMFit, + AvailableMinimizers.Bumps, + AvailableMinimizers.DFO, + ], +) def test_fixed_parameter_does_not_change(fit_engine): # WHEN ref_sin = AbsSin(0.2, np.pi) @@ -365,12 +430,13 @@ def test_fixed_parameter_does_not_change(fit_engine): result = f.fit(x=x, y=y, weights=weights) - # EXPECT + # EXPECT # Offset should remain unchanged assert sp_sin.offset.value == pytest.approx(fixed_offset_before, abs=1e-12) # Phase should be optimized assert sp_sin.phase.value != pytest.approx(ref_sin.phase.value, rel=1e-3) + def test_fitter_new_model_base_integration(): # WHEN ground_truth = StraightLine(slope=2.0, intercept=1.0) @@ -388,4 +454,63 @@ def test_fitter_new_model_base_integration(): # EXPECT assert model.slope.value == pytest.approx(ground_truth.slope.value, rel=1e-3) - assert model.intercept.value == pytest.approx(ground_truth.intercept.value, rel=1e-3) \ No newline at end of file + assert model.intercept.value == pytest.approx( + ground_truth.intercept.value, rel=1e-3 + ) + + +@pytest.mark.parametrize( + "fit_engine", + [ + None, + AvailableMinimizers.LMFit, + AvailableMinimizers.Bumps, + AvailableMinimizers.DFO, + ], +) +def test_fitter_variable_weights(fit_engine): + # WHEN + ref_sin = AbsSin(0.2, np.pi) + sp_sin = AbsSin(0.354, 3.05) + + x = np.linspace(0, 5, 200) + y_true = ref_sin(x) + + # Introduce bias in second half of data + y = y_true.copy() + y[100:] += 0.5 # Artificial distortion + + # Case 1: High weight on distorted region + weights_high = np.ones_like(x) + weights_high[100:] = 10.0 + + # Case 2: Low weight on distorted region + weights_low = np.ones_like(x) + weights_low[100:] = 0.1 + + sp_sin.offset.fixed = False + sp_sin.phase.fixed = False + + def run_fit(weights): + model = AbsSin(0.354, 3.05) + model.offset.fixed = False + model.phase.fixed = False + + f = Fitter(model, model) + if fit_engine is not None: + try: + f.switch_minimizer(fit_engine) + except AttributeError: + pytest.skip(msg=f"{fit_engine} is not installed") + + f.fit(x=x, y=y, weights=weights) + return model.offset.value, model.phase.value + + offset_high, phase_high = run_fit(weights_high) + offset_low, phase_low = run_fit(weights_low) + + # The fit should shift more toward the distorted region + # when it has higher weight + assert abs(offset_high - ref_sin.offset.value) > abs( + offset_low - ref_sin.offset.value + ) diff --git a/tests/unit_tests/fitting/minimizers/test_minimizer_dfo.py b/tests/unit_tests/fitting/minimizers/test_minimizer_dfo.py index 6d6b8f8b..439afea0 100644 --- a/tests/unit_tests/fitting/minimizers/test_minimizer_dfo.py +++ b/tests/unit_tests/fitting/minimizers/test_minimizer_dfo.py @@ -10,90 +10,104 @@ from easyscience.fitting.minimizers.utils import FitError -class TestDFOFit(): +class TestDFOFit: @pytest.fixture def minimizer(self) -> DFO: minimizer = DFO( - obj='obj', - fit_function='fit_function', - minimizer_enum=MagicMock(package='dfo', method='leastsq') + obj="obj", + fit_function="fit_function", + minimizer_enum=MagicMock(package="dfo", method="leastsq"), ) return minimizer def test_init(self, minimizer: DFO) -> None: assert minimizer._p_0 == {} - assert minimizer.package == 'dfo' + assert minimizer.package == "dfo" def test_init_exception(self) -> None: with pytest.raises(FitError): DFO( - obj='obj', - fit_function='fit_function', - minimizer_enum=MagicMock(package='dfo', method='not_leastsq') + obj="obj", + fit_function="fit_function", + minimizer_enum=MagicMock(package="dfo", method="not_leastsq"), ) def test_supported_methods(self, minimizer: DFO) -> None: # When Then Expect - assert minimizer.supported_methods() == ['leastsq'] + assert minimizer.supported_methods() == ["leastsq"] def test_supported_methods(self, minimizer: DFO) -> None: # When Then Expect - assert minimizer.supported_methods() == ['leastsq'] + assert minimizer.supported_methods() == ["leastsq"] def test_fit(self, minimizer: DFO) -> None: # When from easyscience import global_object + global_object.stack.enabled = False mock_model = MagicMock() mock_model_function = MagicMock(return_value=mock_model) minimizer._make_model = MagicMock(return_value=mock_model_function) - minimizer._dfo_fit = MagicMock(return_value='fit') + minimizer._dfo_fit = MagicMock(return_value="fit") minimizer._set_parameter_fit_result = MagicMock() - minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') + minimizer._gen_fit_results = MagicMock(return_value="gen_fit_results") cached_par = MagicMock() cached_par.value = 1 - cached_pars = {'mock_parm_1': cached_par} + cached_pars = {"mock_parm_1": cached_par} minimizer._cached_pars = cached_pars # Then result = minimizer.fit(x=1.0, y=2.0, weights=1) # Expect - assert result == 'gen_fit_results' + assert result == "gen_fit_results" minimizer._dfo_fit.assert_called_once_with(cached_pars, mock_model) minimizer._make_model.assert_called_once_with(parameters=None) - minimizer._set_parameter_fit_result.assert_called_once_with('fit', False) - minimizer._gen_fit_results.assert_called_once_with('fit', 1) + minimizer._set_parameter_fit_result.assert_called_once_with("fit", False) + minimizer._gen_fit_results.assert_called_once_with("fit", 1) mock_model_function.assert_called_once_with(1.0, 2.0, 1) def test_generate_fit_function(self, minimizer: DFO) -> None: # When - minimizer._original_fit_function = MagicMock(return_value='fit_function_result') + minimizer._original_fit_function = MagicMock(return_value="fit_function_result") minimizer._object = MagicMock() mock_parm_1 = MagicMock() - mock_parm_1.unique_name = 'mock_parm_1' + mock_parm_1.unique_name = "mock_parm_1" mock_parm_1.value = 1.0 mock_parm_1.error = 0.1 mock_parm_2 = MagicMock() - mock_parm_2.unique_name = 'mock_parm_2' + 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]) + 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 + assert "fit_function_result" == fit_function_result 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 - - @pytest.mark.parametrize("weights", [np.array([1, 2, 3, 4]), np.array([[1, 2, 3], [4, 5, 6]]), np.repeat(np.nan,3), np.zeros(3), np.repeat(np.inf,3), -np.ones(3)], ids=["wrong_length", "multidimensional", "NaNs", "zeros", "Infs", "negative"]) + assert minimizer._cached_pars["mock_parm_1"] == mock_parm_1 + assert minimizer._cached_pars["mock_parm_2"] == mock_parm_2 + + @pytest.mark.parametrize( + "weights", + [ + np.array([1, 2, 3, 4]), + np.array([[1, 2, 3], [4, 5, 6]]), + np.repeat(np.nan, 3), + np.zeros(3), + np.repeat(np.inf, 3), + -np.ones(3), + ], + ids=["wrong_length", "multidimensional", "NaNs", "zeros", "Infs", "negative"], + ) def test_fit_weight_exceptions(self, minimizer: DFO, weights) -> None: # When Then Expect with pytest.raises(ValueError): @@ -105,87 +119,115 @@ def test_make_model(self, minimizer: DFO) -> None: minimizer._generate_fit_function = MagicMock(return_value=mock_fit_function) mock_parm_1 = MagicMock() - mock_parm_1.unique_name = 'mock_parm_1' + mock_parm_1.unique_name = "mock_parm_1" mock_parm_1.value = 1000.0 mock_parm_2 = MagicMock() - mock_parm_2.unique_name = 'mock_parm_2' + mock_parm_2.unique_name = "mock_parm_2" mock_parm_2.value = 2000.0 # Then model = minimizer._make_model(parameters=[mock_parm_1, mock_parm_2]) - residuals_for_model = model(x=np.array([1, 2]), y=np.array([10, 20]), weights=np.array([100, 200])) + residuals_for_model = model( + x=np.array([1, 2]), + y=np.array([10, 20]), + weights=np.array([1 / 100, 1 / 200]), + ) # Expect minimizer._generate_fit_function.assert_called_once_with() - assert all(np.array([-0.01, -0.01]) == residuals_for_model(np.array([1111, 2222]))) + assert all( + np.array([-0.01, -0.01]) == residuals_for_model(np.array([1111, 2222])) + ) assert all(mock_fit_function.call_args[0][0] == np.array([1, 2])) - assert mock_fit_function.call_args[1] == {'pmock_parm_1': 1111, 'pmock_parm_2': 2222} + assert mock_fit_function.call_args[1] == { + "pmock_parm_1": 1111, + "pmock_parm_2": 2222, + } def test_set_parameter_fit_result_no_stack_status(self, minimizer: DFO): # When minimizer._cached_pars = { - 'a': MagicMock(), - 'b': MagicMock(), + "a": MagicMock(), + "b": MagicMock(), } - minimizer._cached_pars['a'].value = 'a' - minimizer._cached_pars['b'].value = 'b' + minimizer._cached_pars["a"].value = "a" + minimizer._cached_pars["b"].value = "b" mock_fit_result = MagicMock() mock_fit_result.x = [1.0, 2.0] - mock_fit_result.jacobian = 'jacobian' - mock_fit_result.resid = 'resid' + mock_fit_result.jacobian = "jacobian" + mock_fit_result.resid = "resid" - minimizer._error_from_jacobian = MagicMock(return_value=np.array([[0.1, 0.0], [0.0, 0.2]])) + minimizer._error_from_jacobian = MagicMock( + return_value=np.array([[0.1, 0.0], [0.0, 0.2]]) + ) # Then minimizer._set_parameter_fit_result(mock_fit_result, False) # Expect - assert minimizer._cached_pars['a'].value == 1.0 - assert minimizer._cached_pars['a'].error == 0.1 - assert minimizer._cached_pars['b'].value == 2.0 - assert minimizer._cached_pars['b'].error == 0.2 - minimizer._error_from_jacobian.assert_called_once_with('jacobian', 'resid', 0.95) + assert minimizer._cached_pars["a"].value == 1.0 + assert minimizer._cached_pars["a"].error == 0.1 + assert minimizer._cached_pars["b"].value == 2.0 + assert minimizer._cached_pars["b"].error == 0.2 + minimizer._error_from_jacobian.assert_called_once_with( + "jacobian", "resid", 0.95 + ) def test_gen_fit_results(self, minimizer: DFO, monkeypatch): # When mock_domain_fit_results = MagicMock() mock_FitResults = MagicMock(return_value=mock_domain_fit_results) - monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_dfo, "FitResults", mock_FitResults) + monkeypatch.setattr( + easyscience.fitting.minimizers.minimizer_dfo, "FitResults", mock_FitResults + ) mock_fit_result = MagicMock() mock_fit_result.flag = False mock_cached_model = MagicMock() - mock_cached_model.x = 'x' - mock_cached_model.y = 'y' + mock_cached_model.x = "x" + mock_cached_model.y = "y" minimizer._cached_model = mock_cached_model mock_cached_par_1 = MagicMock() - mock_cached_par_1.value = 'par_value_1' + mock_cached_par_1.value = "par_value_1" mock_cached_par_2 = MagicMock() - mock_cached_par_2.value = 'par_value_2' - minimizer._cached_pars = {'par_1': mock_cached_par_1, 'par_2': mock_cached_par_2} + mock_cached_par_2.value = "par_value_2" + minimizer._cached_pars = { + "par_1": mock_cached_par_1, + "par_2": mock_cached_par_2, + } - minimizer._p_0 = 'p_0' - minimizer.evaluate = MagicMock(return_value='evaluate') + minimizer._p_0 = "p_0" + minimizer.evaluate = MagicMock(return_value="evaluate") # Then - domain_fit_results = minimizer._gen_fit_results(mock_fit_result, 'weights', **{'kwargs_set_key': 'kwargs_set_val'}) + domain_fit_results = minimizer._gen_fit_results( + mock_fit_result, "weights", **{"kwargs_set_key": "kwargs_set_val"} + ) # Expect assert domain_fit_results == mock_domain_fit_results - assert domain_fit_results.kwargs_set_key == 'kwargs_set_val' - assert domain_fit_results.success == True - assert domain_fit_results.y_obs == 'y' - assert domain_fit_results.x == 'x' - assert domain_fit_results.p == {'ppar_1': 'par_value_1', 'ppar_2': 'par_value_2'} - assert domain_fit_results.p0 == 'p_0' - assert domain_fit_results.y_calc == 'evaluate' - assert domain_fit_results.y_err == 'weights' - assert str(domain_fit_results.minimizer_engine) == "" + assert domain_fit_results.kwargs_set_key == "kwargs_set_val" + assert domain_fit_results.success == True + assert domain_fit_results.y_obs == "y" + assert domain_fit_results.x == "x" + assert domain_fit_results.p == { + "ppar_1": "par_value_1", + "ppar_2": "par_value_2", + } + assert domain_fit_results.p0 == "p_0" + assert domain_fit_results.y_calc == "evaluate" + assert domain_fit_results.y_err == "weights" + assert ( + str(domain_fit_results.minimizer_engine) + == "" + ) assert domain_fit_results.fit_args is None - minimizer.evaluate.assert_called_once_with('x', minimizer_parameters={'ppar_1': 'par_value_1', 'ppar_2': 'par_value_2'}) + minimizer.evaluate.assert_called_once_with( + "x", minimizer_parameters={"ppar_1": "par_value_1", "ppar_2": "par_value_2"} + ) def test_dfo_fit(self, minimizer: DFO, monkeypatch): # When @@ -199,26 +241,28 @@ def test_dfo_fit(self, minimizer: DFO, monkeypatch): mock_parm_2.max = 20.0 pars = {1: mock_parm_1, 2: mock_parm_2} - kwargs = {'kwargs_set_key': 'kwargs_set_val'} + kwargs = {"kwargs_set_key": "kwargs_set_val"} mock_dfols = MagicMock() mock_results = MagicMock() - mock_results.msg = 'Success' + mock_results.msg = "Success" mock_dfols.solve = MagicMock(return_value=mock_results) - monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_dfo, "dfols", mock_dfols) + monkeypatch.setattr( + easyscience.fitting.minimizers.minimizer_dfo, "dfols", mock_dfols + ) # Then - results = minimizer._dfo_fit(pars, 'model', **kwargs) - + results = minimizer._dfo_fit(pars, "model", **kwargs) + # Expect assert results == mock_results - assert mock_dfols.solve.call_args[0][0] == 'model' - assert all(mock_dfols.solve.call_args[0][1] == np.array([1., 2.])) - assert all(mock_dfols.solve.call_args[1]['bounds'][0] == np.array([0.1, 0.2])) - assert all(mock_dfols.solve.call_args[1]['bounds'][1] == np.array([10., 20.])) - assert mock_dfols.solve.call_args[1]['scaling_within_bounds'] is True - assert mock_dfols.solve.call_args[1]['kwargs_set_key'] == 'kwargs_set_val' + assert mock_dfols.solve.call_args[0][0] == "model" + assert all(mock_dfols.solve.call_args[0][1] == np.array([1.0, 2.0])) + assert all(mock_dfols.solve.call_args[1]["bounds"][0] == np.array([0.1, 0.2])) + assert all(mock_dfols.solve.call_args[1]["bounds"][1] == np.array([10.0, 20.0])) + assert mock_dfols.solve.call_args[1]["scaling_within_bounds"] is True + assert mock_dfols.solve.call_args[1]["kwargs_set_key"] == "kwargs_set_val" def test_dfo_fit_no_scaling(self, minimizer: DFO, monkeypatch): # When @@ -232,40 +276,46 @@ def test_dfo_fit_no_scaling(self, minimizer: DFO, monkeypatch): mock_parm_2.max = 20.0 pars = {1: mock_parm_1, 2: mock_parm_2} - kwargs = {'kwargs_set_key': 'kwargs_set_val'} + kwargs = {"kwargs_set_key": "kwargs_set_val"} mock_dfols = MagicMock() mock_results = MagicMock() - mock_results.msg = 'Success' + mock_results.msg = "Success" mock_dfols.solve = MagicMock(return_value=mock_results) - monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_dfo, "dfols", mock_dfols) + monkeypatch.setattr( + easyscience.fitting.minimizers.minimizer_dfo, "dfols", mock_dfols + ) # Then - results = minimizer._dfo_fit(pars, 'model', **kwargs) - + results = minimizer._dfo_fit(pars, "model", **kwargs) + # Expect assert results == mock_results - assert mock_dfols.solve.call_args[0][0] == 'model' - assert all(mock_dfols.solve.call_args[0][1] == np.array([1., 2.])) - assert all(mock_dfols.solve.call_args[1]['bounds'][0] == np.array([-np.inf, 0.2])) - assert all(mock_dfols.solve.call_args[1]['bounds'][1] == np.array([10., 20.])) - assert not 'scaling_within_bounds' in list(mock_dfols.solve.call_args[1].keys()) - assert 'kwargs_set_key' in list(mock_dfols.solve.call_args[1].keys()) - assert mock_dfols.solve.call_args[1]['kwargs_set_key'] == 'kwargs_set_val' + assert mock_dfols.solve.call_args[0][0] == "model" + assert all(mock_dfols.solve.call_args[0][1] == np.array([1.0, 2.0])) + assert all( + mock_dfols.solve.call_args[1]["bounds"][0] == np.array([-np.inf, 0.2]) + ) + assert all(mock_dfols.solve.call_args[1]["bounds"][1] == np.array([10.0, 20.0])) + assert not "scaling_within_bounds" in list(mock_dfols.solve.call_args[1].keys()) + assert "kwargs_set_key" in list(mock_dfols.solve.call_args[1].keys()) + assert mock_dfols.solve.call_args[1]["kwargs_set_key"] == "kwargs_set_val" def test_dfo_fit_exception(self, minimizer: DFO, monkeypatch): # When pars = {1: MagicMock(Parameter)} - kwargs = {'kwargs_set_key': 'kwargs_set_val'} + kwargs = {"kwargs_set_key": "kwargs_set_val"} mock_dfols = MagicMock() mock_results = MagicMock() - mock_results.msg = 'Failed' + mock_results.msg = "Failed" mock_dfols.solve = MagicMock(return_value=mock_results) - monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_dfo, "dfols", mock_dfols) + monkeypatch.setattr( + easyscience.fitting.minimizers.minimizer_dfo, "dfols", mock_dfols + ) # Then Expect with pytest.raises(FitError): - minimizer._dfo_fit(pars, 'model', **kwargs) \ No newline at end of file + minimizer._dfo_fit(pars, "model", **kwargs)