diff --git a/src/estimagic/parameters/check_constraints.py b/src/estimagic/parameters/check_constraints.py index 216516e0e..8464f20ae 100644 --- a/src/estimagic/parameters/check_constraints.py +++ b/src/estimagic/parameters/check_constraints.py @@ -39,28 +39,30 @@ def check_constraints_are_satisfied(flat_constraints, param_values, param_names) typ = constr["type"] subset = param_values[constr["index"]] + report = [] + _msg = partial(_get_message, constr, param_names) if typ == "covariance": cov = cov_params_to_matrix(subset) e, _ = np.linalg.eigh(cov) if not np.all(e > -1e-8): - raise InvalidParamsError(_msg()) + report.append(_msg()) elif typ == "sdcorr": cov = sdcorr_params_to_matrix(subset) e, _ = np.linalg.eigh(cov) if not np.all(e > -1e-8): - raise InvalidParamsError(_msg()) + report.append(_msg()) elif typ == "probability": if not np.isclose(subset.sum(), 1, rtol=0.01): explanation = "Probabilities do not sum to 1." - raise InvalidParamsError(_msg(explanation)) + report.append(_msg(explanation)) if np.any(subset < 0): explanation = "There are negative Probabilities." - raise InvalidParamsError(_msg(explanation)) + report.append(_msg(explanation)) if np.any(subset > 1): explanation = "There are probabilities larger than 1." - raise InvalidParamsError(_msg(explanation)) + report.append(_msg(explanation)) elif typ == "fixed": if "value" in constr and not np.allclose(subset, constr["value"]): explanation = ( @@ -68,27 +70,31 @@ def check_constraints_are_satisfied(flat_constraints, param_values, param_names) "was allowed in earlier versions of estimagic but is " "forbidden now. " ) - raise InvalidParamsError(_msg(explanation)) + report.append(_msg(explanation)) elif typ == "increasing": if np.any(np.diff(subset) < 0): - raise InvalidParamsError(_msg()) + report.append(_msg()) elif typ == "decreasing": if np.any(np.diff(subset) > 0): - InvalidParamsError(_msg()) + report.append(_msg()) elif typ == "linear": wsum = subset.dot(constr["weights"]) if "lower_bound" in constr and wsum < constr["lower_bound"]: explanation = "Lower bound of linear constraint is violated." - raise InvalidParamsError(_msg(explanation)) + report.append(_msg(explanation)) elif "upper_bound" in constr and wsum > constr["upper_bound"]: explanation = "Upper bound of linear constraint violated" - raise InvalidParamsError(_msg(explanation)) + report.append(_msg(explanation)) elif "value" in constr and not np.isclose(wsum, constr["value"]): explanation = "Equality condition of linear constraint violated" - raise InvalidParamsError(_msg(explanation)) + report.append(_msg(explanation)) elif typ == "equality": if len(set(subset.tolist())) > 1: - raise InvalidParamsError(_msg()) + report.append(_msg()) + + report = "\n".join(report) + if report != "": + raise InvalidParamsError(f"Violated constraint at start params:\n{report}") def _get_message(constraint, param_names, explanation=""): diff --git a/tests/parameters/test_check_constraints.py b/tests/parameters/test_check_constraints.py new file mode 100644 index 000000000..a8496d8ae --- /dev/null +++ b/tests/parameters/test_check_constraints.py @@ -0,0 +1,110 @@ +import pytest +import numpy as np +from estimagic.exceptions import InvalidParamsError +from estimagic.parameters.constraint_tools import check_constraints + + +def test_check_constraints_are_satisfied_type_equality(): + with pytest.raises(InvalidParamsError): + check_constraints( + params=np.array([1, 2, 3]), constraints={"type": "equality", "loc": [0, 1]} + ) + + +def test_check_constraints_are_satisfied_type_increasing(): + with pytest.raises(InvalidParamsError): + check_constraints( + params=np.array([1, 2, 3, 2, 4]), + constraints={"type": "increasing", "loc": [1, 2, 3]}, + ) + + +def test_check_constraints_are_satisfied_type_decreasing(): + with pytest.raises(InvalidParamsError): + check_constraints( + params=np.array([1, 2, 3, 2, 4]), + constraints={"type": "decreasing", "loc": [0, 1, 3]}, + ) + + +def test_check_constraints_are_satisfied_type_pairwise_equality(): + with pytest.raises(InvalidParamsError): + check_constraints( + params=np.array([1, 2, 3, 3, 4]), + constraints={"type": "pairwise_equality", "locs": [[0, 4], [3, 2]]}, + ) + + +def test_check_constraints_are_satisfied_type_probability(): + with pytest.raises(InvalidParamsError): + check_constraints( + params=np.array([0.10, 0.25, 0.50, 1, 0.7]), + constraints={"type": "probability", "loc": [0, 1, 2, 4]}, + ) + + +def test_check_constraints_are_satisfied_type_linear_lower_bound(): + with pytest.raises(InvalidParamsError): + check_constraints( + params=np.ones(5), + constraints={ + "type": "linear", + "loc": [0, 2, 3, 4], + "lower_bound": 1.1, + "weights": 0.25, + }, + ) + + +def test_check_constraints_are_satisfied_type_linear_upper_bound(): + with pytest.raises(InvalidParamsError): + check_constraints( + params=np.ones(5), + constraints={ + "type": "linear", + "loc": [0, 2, 3, 4], + "upper_bound": 0.9, + "weights": 0.25, + }, + ) + + +def test_check_constraints_are_satisfied_type_linear_value(): + with pytest.raises(InvalidParamsError): + check_constraints( + params=np.ones(5), + constraints={ + "type": "linear", + "loc": [0, 2, 3, 4], + "value": 2, + "weights": 0.25, + }, + ) + + +def test_check_constraints_are_satisfied_type_covariance(): + with pytest.raises(InvalidParamsError): + check_constraints( + params=[1, 1, 1, -1, 1, -1], + constraints={ + "type": "covariance", + # "loc": [0, 1, 2], + "selector": lambda params: params, + }, + ) + + +def test_check_constraints_are_satisfied_type_sdcorr(): + with pytest.raises(InvalidParamsError): + check_constraints( + params=[1, 1, 1, -1, 1, 1], + constraints={ + "type": "sdcorr", + # "loc": [0, 1, 2], + "selector": lambda params: params, + }, + ) + + +# to ignore as per email? +# def test_check_constraints_are_satisfied_type_nonlinear():