From d8df7e901361405a267f36dff2721a63a8ad16b7 Mon Sep 17 00:00:00 2001 From: MartinBelthle Date: Thu, 19 Dec 2024 11:49:03 +0100 Subject: [PATCH] fix(xpansion): fix several issues related to weights and constraints (#2273) --- .../study/business/xpansion_management.py | 44 +++++++------ .../storage/business/test_xpansion_manager.py | 61 +++++++++++++++++-- 2 files changed, 81 insertions(+), 24 deletions(-) diff --git a/antarest/study/business/xpansion_management.py b/antarest/study/business/xpansion_management.py index c86dd5cab7..3f19e7c8f3 100644 --- a/antarest/study/business/xpansion_management.py +++ b/antarest/study/business/xpansion_management.py @@ -203,7 +203,7 @@ def from_config(cls, config_obj: JSON) -> "GetXpansionSettings": try: return cls(**config_obj) except ValidationError: - return cls.construct(**config_obj) + return cls.model_construct(**config_obj) @all_optional_model @@ -270,11 +270,6 @@ def __init__(self) -> None: super().__init__(http.HTTPStatus.BAD_REQUEST) -class WrongTypeFormat(HTTPException): - def __init__(self, message: str) -> None: - super().__init__(http.HTTPStatus.BAD_REQUEST, message) - - class WrongLinkFormatError(HTTPException): def __init__(self, message: str) -> None: super().__init__(http.HTTPStatus.BAD_REQUEST, message) @@ -295,11 +290,6 @@ def __init__(self, message: str) -> None: super().__init__(http.HTTPStatus.NOT_FOUND, message) -class ConstraintsNotFoundError(HTTPException): - def __init__(self, message: str) -> None: - super().__init__(http.HTTPStatus.NOT_FOUND, message) - - class FileCurrentlyUsedInSettings(HTTPException): def __init__(self, message: str) -> None: super().__init__(http.HTTPStatus.CONFLICT, message) @@ -337,7 +327,10 @@ def create_xpansion_configuration(self, study: Study, zipped_config: t.Optional[ xpansion_settings = XpansionSettings() settings_obj = xpansion_settings.model_dump( - mode="json", by_alias=True, exclude_none=True, exclude={"sensitivity_config"} + mode="json", + by_alias=True, + exclude_none=True, + exclude={"sensitivity_config", "yearly_weights", "additional_constraints"}, ) if xpansion_settings.sensitivity_config: sensitivity_obj = xpansion_settings.sensitivity_config.model_dump( @@ -389,22 +382,33 @@ def update_xpansion_settings( file_study = self.study_storage_service.get_storage(study).get_raw(study) - # Specific handling of the additional constraints file: - # - If the file name is `None`, it means that the user does not want to select an additional constraints file. - # - If the file name is empty, it means that the user wants to deselect the additional constraints file, - # but he does not want to delete it from the expansion configuration folder. - # - If the file name is not empty, it means that the user wants to select an additional constraints file. + # Specific handling for yearly_weights and additional_constraints: + # - If the attributes are given, it means that the user wants to select a file. # It is therefore necessary to check that the file exists. - constraints_file = new_xpansion_settings.additional_constraints - if constraints_file: + # - Else, it means the user want to deselect the additional constraints file, + # but he does not want to delete it from the expansion configuration folder. + excludes = {"sensitivity_config"} + if constraints_file := new_xpansion_settings.additional_constraints: try: constraints_url = ["user", "expansion", "constraints", constraints_file] file_study.tree.get(constraints_url) except ChildNotFoundError: msg = f"Additional constraints file '{constraints_file}' does not exist" raise XpansionFileNotFoundError(msg) from None + else: + excludes.add("additional_constraints") + + if weights_file := new_xpansion_settings.yearly_weights: + try: + weights_url = ["user", "expansion", "weights", weights_file] + file_study.tree.get(weights_url) + except ChildNotFoundError: + msg = f"Additional weights file '{weights_file}' does not exist" + raise XpansionFileNotFoundError(msg) from None + else: + excludes.add("yearly_weights") - config_obj = updated_settings.model_dump(mode="json", by_alias=True, exclude={"sensitivity_config"}) + config_obj = updated_settings.model_dump(mode="json", by_alias=True, exclude=excludes) file_study.tree.save(config_obj, ["user", "expansion", "settings"]) if new_xpansion_settings.sensitivity_config: diff --git a/tests/storage/business/test_xpansion_manager.py b/tests/storage/business/test_xpansion_manager.py index 5b1f8d1abd..325ddf7e0c 100644 --- a/tests/storage/business/test_xpansion_manager.py +++ b/tests/storage/business/test_xpansion_manager.py @@ -127,8 +127,6 @@ def set_up_xpansion_manager(tmp_path: Path) -> t.Tuple[FileStudy, RawStudy, Xpan "log_level": 0, "separation_parameter": 0.5, "batch_size": 96, - "yearly-weights": "", - "additional-constraints": "", "timelimit": int(1e12), }, "weights": {}, @@ -228,8 +226,6 @@ def test_update_xpansion_settings(tmp_path: Path) -> None: "max_iteration": 123, "uc_type": UcType.EXPANSION_FAST, "master": Master.INTEGER, - "yearly-weights": "", - "additional-constraints": "", "relaxed_optimality_gap": "1.2%", # percentage "relative_gap": 1e-12, "batch_size": 4, @@ -463,6 +459,63 @@ def test_update_constraints(tmp_path: Path) -> None: assert actual_settings.additional_constraints == "" +@pytest.mark.unit_test +def test_update_constraints_via_the_front(tmp_path: Path) -> None: + empty_study, study, xpansion_manager = set_up_xpansion_manager(tmp_path) + empty_study.tree.save({"user": {"expansion": {"constraints": {"constraints.txt": b"0"}}}}) + + # asserts we can update a field without writing the field additional constraint in the file + front_settings = UpdateXpansionSettings(master="relaxed") + xpansion_manager.update_xpansion_settings(study, front_settings) + json_content = empty_study.tree.get(["user", "expansion", "settings"]) + assert "additional-constraints" not in json_content + assert json_content["master"] == "relaxed" + + # asserts the front-end can fill additional constraints + new_constraint = {"additional-constraints": "constraints.txt"} + front_settings = UpdateXpansionSettings.model_validate(new_constraint) + actual_settings = xpansion_manager.update_xpansion_settings(study, front_settings) + assert actual_settings.additional_constraints == "constraints.txt" + json_content = empty_study.tree.get(["user", "expansion", "settings"]) + assert json_content["additional-constraints"] == "constraints.txt" + + # asserts the front-end can unselect this constraint by not filling it + front_settings = UpdateXpansionSettings() + actual_settings = xpansion_manager.update_xpansion_settings(study, front_settings) + assert actual_settings.additional_constraints == "" + json_content = empty_study.tree.get(["user", "expansion", "settings"]) + assert "additional-constraints" not in json_content + + +@pytest.mark.unit_test +def test_update_weights_via_the_front(tmp_path: Path) -> None: + # Same test as the one for constraints + empty_study, study, xpansion_manager = set_up_xpansion_manager(tmp_path) + empty_study.tree.save({"user": {"expansion": {"weights": {"weights.txt": b"0"}}}}) + + # asserts we can update a field without writing the field yearly-weights in the file + front_settings = UpdateXpansionSettings(master="relaxed") + xpansion_manager.update_xpansion_settings(study, front_settings) + json_content = empty_study.tree.get(["user", "expansion", "settings"]) + assert "yearly-weights" not in json_content + assert json_content["master"] == "relaxed" + + # asserts the front-end can fill yearly weights + new_constraint = {"yearly-weights": "weights.txt"} + front_settings = UpdateXpansionSettings.model_validate(new_constraint) + actual_settings = xpansion_manager.update_xpansion_settings(study, front_settings) + assert actual_settings.yearly_weights == "weights.txt" + json_content = empty_study.tree.get(["user", "expansion", "settings"]) + assert json_content["yearly-weights"] == "weights.txt" + + # asserts the front-end can unselect this weight by not filling it + front_settings = UpdateXpansionSettings() + actual_settings = xpansion_manager.update_xpansion_settings(study, front_settings) + assert actual_settings.yearly_weights == "" + json_content = empty_study.tree.get(["user", "expansion", "settings"]) + assert "yearly-weights" not in json_content + + @pytest.mark.unit_test def test_add_resources(tmp_path: Path) -> None: empty_study, study, xpansion_manager = set_up_xpansion_manager(tmp_path)