From 33eb7c7a71a3396da0072bf7fc64068da5c3829d Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Tue, 10 Sep 2024 11:43:49 +0200 Subject: [PATCH 01/26] Added create local binding constraint --- .../local_services/binding_constraint_local.py | 7 ++++++- tests/antares/services/local_services/test_study.py | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/antares/service/local_services/binding_constraint_local.py b/src/antares/service/local_services/binding_constraint_local.py index aef1ec6c..56fb3a3e 100644 --- a/src/antares/service/local_services/binding_constraint_local.py +++ b/src/antares/service/local_services/binding_constraint_local.py @@ -39,7 +39,12 @@ def create_binding_constraint( equal_term_matrix: Optional[pd.DataFrame] = None, greater_term_matrix: Optional[pd.DataFrame] = None, ) -> BindingConstraint: - raise NotImplementedError + return BindingConstraint( + name=name, + binding_constraint_service=self, + properties=properties, + terms=terms, + ) def add_constraint_terms(self, constraint: BindingConstraint, terms: List[ConstraintTerm]) -> List[ConstraintTerm]: raise NotImplementedError diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index f3c2f371..18ef857a 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -21,6 +21,7 @@ from antares.config.local_configuration import LocalConfiguration from antares.exceptions.exceptions import CustomError, LinkCreationError from antares.model.area import AreaProperties, AreaUi, AreaUiLocal, AreaPropertiesLocal, Area +from antares.model.binding_constraint import BindingConstraint from antares.model.commons import FilterOption from antares.model.hydro import Hydro from antares.model.link import ( @@ -1074,3 +1075,13 @@ def test_created_link_with_custom_ui_values(self, tmp_path, local_study_w_areas) assert actual_properties == LinkPropertiesLocal.model_validate(created_properties).yield_link_properties() created_ui = expected_ui.model_dump(mode="json", exclude_none=True) assert actual_ui == LinkUiLocal.model_validate(created_ui).yield_link_ui() + + +class TestCreateBindingconstraint: + def test_can_be_created(self, local_study_with_hydro): + # When + binding_constraint_name = "test constraint" + binding_constraint = local_study_with_hydro.create_binding_constraint(name=binding_constraint_name) + + # Then + assert isinstance(binding_constraint, BindingConstraint) From 9505419533a3d8fbbc7e00c8d61fd940404dbc66 Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Tue, 10 Sep 2024 15:47:33 +0200 Subject: [PATCH 02/26] Added properties object to binding constraints --- .../service/local_services/binding_constraint_local.py | 1 + src/antares/tools/ini_tool.py | 1 + tests/antares/services/local_services/conftest.py | 6 ++++++ tests/antares/services/local_services/test_study.py | 7 +++++++ 4 files changed, 15 insertions(+) diff --git a/src/antares/service/local_services/binding_constraint_local.py b/src/antares/service/local_services/binding_constraint_local.py index 56fb3a3e..0b110512 100644 --- a/src/antares/service/local_services/binding_constraint_local.py +++ b/src/antares/service/local_services/binding_constraint_local.py @@ -39,6 +39,7 @@ def create_binding_constraint( equal_term_matrix: Optional[pd.DataFrame] = None, greater_term_matrix: Optional[pd.DataFrame] = None, ) -> BindingConstraint: + properties = properties if properties is not None else BindingConstraintProperties(enabled=True) return BindingConstraint( name=name, binding_constraint_service=self, diff --git a/src/antares/tools/ini_tool.py b/src/antares/tools/ini_tool.py index 6109a3da..05eb48f4 100644 --- a/src/antares/tools/ini_tool.py +++ b/src/antares/tools/ini_tool.py @@ -32,6 +32,7 @@ class IniFileTypes(Enum): AREA_OPTIMIZATION_INI = "input/areas/{area_name}/optimization.ini" AREA_UI_INI = "input/areas/{area_name}/ui.ini" AREA_ADEQUACY_PATCH_INI = "input/areas/{area_name}/adequacy_patch.ini" + BINDING_CONSTRAINTS_INI = "input/bindingconstraints/bindingconstraints.ini" HYDRO_INI = "input/hydro/hydro.ini" LINK_PROPERTIES_INI = "input/links/{area_name}/properties.ini" LOAD_CORRELATION_INI = "input/load/prepro/correlation.ini" diff --git a/tests/antares/services/local_services/conftest.py b/tests/antares/services/local_services/conftest.py index 6147e5db..9adec163 100644 --- a/tests/antares/services/local_services/conftest.py +++ b/tests/antares/services/local_services/conftest.py @@ -228,3 +228,9 @@ def fr_wind(area_fr) -> Wind: @pytest.fixture def fr_load(area_fr) -> Load: return area_fr.create_load(None) + + +@pytest.fixture +def local_study_with_constraint(local_study_with_hydro) -> Study: + local_study_with_hydro.create_binding_constraint(name="test constraint") + return local_study_with_hydro diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index 18ef857a..b3b164a8 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -1085,3 +1085,10 @@ def test_can_be_created(self, local_study_with_hydro): # Then assert isinstance(binding_constraint, BindingConstraint) + + def test_constraints_have_default_properties(self, local_study_with_constraint): + # Given + constraint = local_study_with_constraint.get_binding_constraints()["test constraint"] + + # Then + assert constraint.properties.model_dump(exclude_none=True) From 6960a30b8242513235eb185765a98ea7ca8a3ade Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Wed, 11 Sep 2024 15:15:09 +0200 Subject: [PATCH 03/26] Added default properties to binding constraints --- .../binding_constraint_local.py | 16 ++++++++++++- .../services/local_services/conftest.py | 24 +++++++++++++++++++ .../services/local_services/test_study.py | 3 +++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/antares/service/local_services/binding_constraint_local.py b/src/antares/service/local_services/binding_constraint_local.py index 0b110512..c526cc8c 100644 --- a/src/antares/service/local_services/binding_constraint_local.py +++ b/src/antares/service/local_services/binding_constraint_local.py @@ -20,6 +20,8 @@ ConstraintTerm, BindingConstraint, ConstraintMatrixName, + BindingConstraintFrequency, + BindingConstraintOperator, ) from antares.service.base_services import BaseBindingConstraintService @@ -39,7 +41,19 @@ def create_binding_constraint( equal_term_matrix: Optional[pd.DataFrame] = None, greater_term_matrix: Optional[pd.DataFrame] = None, ) -> BindingConstraint: - properties = properties if properties is not None else BindingConstraintProperties(enabled=True) + properties = ( + properties + if properties is not None + else BindingConstraintProperties( + enabled=True, + time_step=BindingConstraintFrequency.HOURLY, + operator=BindingConstraintOperator.LESS, + comments=None, + filter_year_by_year="hourly", + filter_synthesis="hourly", + group=None, + ) + ) return BindingConstraint( name=name, binding_constraint_service=self, diff --git a/tests/antares/services/local_services/conftest.py b/tests/antares/services/local_services/conftest.py index 9adec163..4537ffe0 100644 --- a/tests/antares/services/local_services/conftest.py +++ b/tests/antares/services/local_services/conftest.py @@ -14,6 +14,12 @@ from antares.config.local_configuration import LocalConfiguration from antares.model.area import Area +from antares.model.binding_constraint import ( + BindingConstraint, + BindingConstraintProperties, + BindingConstraintFrequency, + BindingConstraintOperator, +) from antares.model.hydro import HydroProperties from antares.model.load import Load from antares.model.renewable import RenewableClusterProperties, TimeSeriesInterpretation, RenewableClusterGroup @@ -234,3 +240,21 @@ def fr_load(area_fr) -> Load: def local_study_with_constraint(local_study_with_hydro) -> Study: local_study_with_hydro.create_binding_constraint(name="test constraint") return local_study_with_hydro + + +@pytest.fixture +def test_constraint(local_study_with_constraint) -> BindingConstraint: + return local_study_with_constraint.get_binding_constraints()["test constraint"] + + +@pytest.fixture +def default_constraint_properties() -> BindingConstraintProperties: + return BindingConstraintProperties( + enabled=True, + time_step=BindingConstraintFrequency.HOURLY, + operator=BindingConstraintOperator.LESS, + comments=None, + filter_year_by_year="hourly", + filter_synthesis="hourly", + group=None, + ) diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index b3b164a8..144096f1 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -1092,3 +1092,6 @@ def test_constraints_have_default_properties(self, local_study_with_constraint): # Then assert constraint.properties.model_dump(exclude_none=True) + + def test_constraints_have_correct_default_properties(self, test_constraint, default_constraint_properties): + assert test_constraint.properties == default_constraint_properties From 3aecdeab24cedb8df64306bd1b02dd252b8d4a6f Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Wed, 11 Sep 2024 17:08:22 +0200 Subject: [PATCH 04/26] Added bindingconstraints.ini file --- src/antares/model/study.py | 4 ++++ tests/antares/services/local_services/test_study.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/antares/model/study.py b/src/antares/model/study.py index d136c593..c8e5f88b 100644 --- a/src/antares/model/study.py +++ b/src/antares/model/study.py @@ -29,6 +29,7 @@ from antares.model.settings import StudySettings from antares.service.api_services.study_api import _returns_study_settings from antares.service.base_services import BaseStudyService +from antares.service.local_services.study_local import StudyLocalService from antares.service.service_factory import ServiceFactory from antares.tools.ini_tool import IniFile, IniFileTypes @@ -269,6 +270,9 @@ def create_binding_constraint( name, properties, terms, less_term_matrix, equal_term_matrix, greater_term_matrix ) self._binding_constraints[constraint.id] = constraint + if isinstance(self.service, StudyLocalService): + binding_constraints_ini = IniFile(self.service.config.study_path, IniFileTypes.BINDING_CONSTRAINTS_INI) + binding_constraints_ini.write_ini_file() return constraint def update_settings(self, settings: StudySettings) -> None: diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index 144096f1..2195339c 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -1095,3 +1095,13 @@ def test_constraints_have_default_properties(self, local_study_with_constraint): def test_constraints_have_correct_default_properties(self, test_constraint, default_constraint_properties): assert test_constraint.properties == default_constraint_properties + + def test_creating_constraints_creates_ini(self, local_study_with_constraint): + # Given + expected_ini_file_path = ( + local_study_with_constraint.service.config.study_path / "input/bindingconstraints/bindingconstraints.ini" + ) + + # Then + assert expected_ini_file_path.exists() + assert expected_ini_file_path.is_file() From 007ca33cbd4b1a3d6f8ce56966cf967105242a32 Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Thu, 12 Sep 2024 16:22:36 +0200 Subject: [PATCH 05/26] Added bindingconstraints.ini file is managed from the Study and has the correct contents --- src/antares/model/binding_constraint.py | 73 ++++++++++++++++- src/antares/model/study.py | 9 ++- .../binding_constraint_local.py | 15 ---- .../services/local_services/test_study.py | 80 ++++++++++++++++++- 4 files changed, 159 insertions(+), 18 deletions(-) diff --git a/src/antares/model/binding_constraint.py b/src/antares/model/binding_constraint.py index ee7f0468..087d02bc 100644 --- a/src/antares/model/binding_constraint.py +++ b/src/antares/model/binding_constraint.py @@ -14,10 +14,11 @@ from typing import Optional, Union, List, Any, Dict import pandas as pd -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, Field, model_validator, computed_field from pydantic.alias_generators import to_camel from antares.tools.contents_tool import EnumIgnoreCase, transform_name_to_id +from antares.tools.ini_tool import check_if_none DEFAULT_GROUP = "default" @@ -94,6 +95,65 @@ class BindingConstraintProperties(BaseModel, extra="forbid", populate_by_name=Tr group: Optional[str] = None +class BindingConstraintPropertiesLocal(BaseModel): + """ + Used to create the entries for the bindingconstraints.ini file + + Args: + constraint_name: The constraint name + constraint_id: The constraint id + properties (BindingConstraintProperties): The BindingConstraintProperties to set + """ + + def __init__( + self, + constraint_name: str, + constraint_id: str, + properties: Optional[BindingConstraintProperties] = None, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + properties = properties if properties is not None else BindingConstraintProperties() + self._constraint_name = constraint_name + self._constraint_id = constraint_id + self._enabled = check_if_none(properties.enabled, True) + self._time_step = check_if_none(properties.time_step, BindingConstraintFrequency.HOURLY) + self._operator = check_if_none(properties.operator, BindingConstraintOperator.LESS) + self._comments = properties.comments + self._filter_year_by_year = check_if_none(properties.filter_year_by_year, "hourly") + self._filter_synthesis = check_if_none(properties.filter_synthesis, "hourly") + self._group = properties.group + + @computed_field # type: ignore[misc] + @property + def list_ini_fields(self) -> dict[str, str]: + ini_dict = { + "name": self._constraint_name, + "id": self._constraint_id, + "enabled": f"{self._enabled}".lower(), + "type": self._time_step.value, + "operator": self._operator.value, + "comments": self._comments, + "filter-year-by-year": self._filter_year_by_year, + "filter-synthesis": self._filter_synthesis, + "group": self._group, + } + return {key: value for key, value in ini_dict.items() if value is not None} + + @computed_field # type: ignore[misc] + @property + def yield_binding_constraint_properties(self) -> BindingConstraintProperties: + return BindingConstraintProperties( + enabled=self._enabled, + time_step=self._time_step, + operator=self._operator, + comments=self._comments, + filter_year_by_year=self._filter_year_by_year, + filter_synthesis=self._filter_synthesis, + group=self._group, + ) + + class BindingConstraint: def __init__( # type: ignore # TODO: Find a way to avoid circular imports self, @@ -107,6 +167,9 @@ def __init__( # type: ignore # TODO: Find a way to avoid circular imports self._id = transform_name_to_id(name) self._properties = properties or BindingConstraintProperties() self._terms = {term.id: term for term in terms} if terms else {} + self._local_properties = BindingConstraintPropertiesLocal( + constraint_name=self._name, constraint_id=self._id, properties=properties + ) @property def name(self) -> str: @@ -120,6 +183,14 @@ def id(self) -> str: def properties(self) -> BindingConstraintProperties: return self._properties + @properties.setter + def properties(self, new_properties: BindingConstraintProperties) -> None: + self._properties = new_properties + + @property + def local_properties(self) -> BindingConstraintPropertiesLocal: + return self._local_properties + def get_terms(self) -> Dict[str, ConstraintTerm]: return self._terms diff --git a/src/antares/model/study.py b/src/antares/model/study.py index c8e5f88b..de9a0f59 100644 --- a/src/antares/model/study.py +++ b/src/antares/model/study.py @@ -269,10 +269,17 @@ def create_binding_constraint( constraint = self._binding_constraints_service.create_binding_constraint( name, properties, terms, less_term_matrix, equal_term_matrix, greater_term_matrix ) - self._binding_constraints[constraint.id] = constraint if isinstance(self.service, StudyLocalService): + constraint.properties = constraint.local_properties.yield_binding_constraint_properties + binding_constraints_ini_content = { + idx: idx_constraint.local_properties.list_ini_fields + for idx, idx_constraint in enumerate((self._binding_constraints | {constraint.id: constraint}).values()) + } + binding_constraints_ini = IniFile(self.service.config.study_path, IniFileTypes.BINDING_CONSTRAINTS_INI) + binding_constraints_ini.ini_dict = binding_constraints_ini_content binding_constraints_ini.write_ini_file() + self._binding_constraints[constraint.id] = constraint return constraint def update_settings(self, settings: StudySettings) -> None: diff --git a/src/antares/service/local_services/binding_constraint_local.py b/src/antares/service/local_services/binding_constraint_local.py index c526cc8c..56fb3a3e 100644 --- a/src/antares/service/local_services/binding_constraint_local.py +++ b/src/antares/service/local_services/binding_constraint_local.py @@ -20,8 +20,6 @@ ConstraintTerm, BindingConstraint, ConstraintMatrixName, - BindingConstraintFrequency, - BindingConstraintOperator, ) from antares.service.base_services import BaseBindingConstraintService @@ -41,19 +39,6 @@ def create_binding_constraint( equal_term_matrix: Optional[pd.DataFrame] = None, greater_term_matrix: Optional[pd.DataFrame] = None, ) -> BindingConstraint: - properties = ( - properties - if properties is not None - else BindingConstraintProperties( - enabled=True, - time_step=BindingConstraintFrequency.HOURLY, - operator=BindingConstraintOperator.LESS, - comments=None, - filter_year_by_year="hourly", - filter_synthesis="hourly", - group=None, - ) - ) return BindingConstraint( name=name, binding_constraint_service=self, diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index 2195339c..f3d55d97 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -21,7 +21,12 @@ from antares.config.local_configuration import LocalConfiguration from antares.exceptions.exceptions import CustomError, LinkCreationError from antares.model.area import AreaProperties, AreaUi, AreaUiLocal, AreaPropertiesLocal, Area -from antares.model.binding_constraint import BindingConstraint +from antares.model.binding_constraint import ( + BindingConstraint, + BindingConstraintProperties, + BindingConstraintFrequency, + BindingConstraintOperator, +) from antares.model.commons import FilterOption from antares.model.hydro import Hydro from antares.model.link import ( @@ -40,6 +45,7 @@ from antares.service.local_services.renewable_local import RenewableLocalService from antares.service.local_services.st_storage_local import ShortTermStorageLocalService from antares.service.local_services.thermal_local import ThermalLocalService +from antares.tools.ini_tool import IniFileTypes class TestCreateStudy: @@ -1105,3 +1111,75 @@ def test_creating_constraints_creates_ini(self, local_study_with_constraint): # Then assert expected_ini_file_path.exists() assert expected_ini_file_path.is_file() + + def test_constraints_ini_have_correct_default_content( + self, local_study_with_constraint, test_constraint, default_constraint_properties + ): + # Given + expected_ini_contents = """[0] +name = test constraint +id = test constraint +enabled = true +type = hourly +operator = less +filter-year-by-year = hourly +filter-synthesis = hourly + +""" + + # When + actual_ini_path = ( + local_study_with_constraint.service.config.study_path / IniFileTypes.BINDING_CONSTRAINTS_INI.value + ) + with actual_ini_path.open("r") as file: + actual_ini_content = file.read() + + # Then + assert default_constraint_properties == test_constraint.properties + assert actual_ini_content == expected_ini_contents + + def test_constraints_and_ini_have_custom_properties(self, local_study_with_constraint): + # Given + custom_constraint_properties = BindingConstraintProperties( + enabled=False, + time_step=BindingConstraintFrequency.WEEKLY, + operator=BindingConstraintOperator.BOTH, + comments="test comment", + filter_year_by_year="yearly", + filter_synthesis="monthly", + group="test group", + ) + expected_ini_content = """[0] +name = test constraint +id = test constraint +enabled = true +type = hourly +operator = less +filter-year-by-year = hourly +filter-synthesis = hourly + +[1] +name = test constraint two +id = test constraint two +enabled = false +type = weekly +operator = both +comments = test comment +filter-year-by-year = yearly +filter-synthesis = monthly +group = test group + +""" + + # When + local_study_with_constraint.create_binding_constraint( + name="test constraint two", properties=custom_constraint_properties + ) + actual_file_path = ( + local_study_with_constraint.service.config.study_path / IniFileTypes.BINDING_CONSTRAINTS_INI.value + ) + with actual_file_path.open("r") as file: + actual_ini_content = file.read() + + # Then + assert actual_ini_content == expected_ini_content From 999c29f99dfd767c8cc1b3a8726d0f367fecba5c Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Fri, 13 Sep 2024 15:30:46 +0200 Subject: [PATCH 06/26] Can add constraint terms --- .../service/local_services/binding_constraint_local.py | 3 ++- tests/antares/services/local_services/test_study.py | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/antares/service/local_services/binding_constraint_local.py b/src/antares/service/local_services/binding_constraint_local.py index 56fb3a3e..4c5fd58c 100644 --- a/src/antares/service/local_services/binding_constraint_local.py +++ b/src/antares/service/local_services/binding_constraint_local.py @@ -47,7 +47,8 @@ def create_binding_constraint( ) def add_constraint_terms(self, constraint: BindingConstraint, terms: List[ConstraintTerm]) -> List[ConstraintTerm]: - raise NotImplementedError + new_terms = [term for term in terms if term.id not in constraint.get_terms()] + return new_terms def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> None: raise NotImplementedError diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index f3d55d97..1d9a7b6b 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -26,6 +26,7 @@ BindingConstraintProperties, BindingConstraintFrequency, BindingConstraintOperator, + ConstraintTerm, ) from antares.model.commons import FilterOption from antares.model.hydro import Hydro @@ -1183,3 +1184,8 @@ def test_constraints_and_ini_have_custom_properties(self, local_study_with_const # Then assert actual_ini_content == expected_ini_content + + def test_constraint_can_add_term(self, test_constraint): + new_term = [ConstraintTerm(data={"area1": "fr", "area2": "at"})] + test_constraint.add_terms(new_term) + assert test_constraint.get_terms() From 2614de9922346e1f4d1321376827b5d160d1e448 Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Mon, 16 Sep 2024 12:53:37 +0200 Subject: [PATCH 07/26] Refactored bindingconstraints.ini to be handled by the bindingconstraint service potentially duplicating the bindingconstraints in memory. Also fixed a default value and a documentation gap --- src/antares/model/binding_constraint.py | 16 ++++++++-- src/antares/model/study.py | 11 ------- .../binding_constraint_local.py | 31 ++++++++++++++++--- .../services/local_services/conftest.py | 2 +- .../services/local_services/test_study.py | 2 ++ 5 files changed, 42 insertions(+), 20 deletions(-) diff --git a/src/antares/model/binding_constraint.py b/src/antares/model/binding_constraint.py index 087d02bc..fe81e5fd 100644 --- a/src/antares/model/binding_constraint.py +++ b/src/antares/model/binding_constraint.py @@ -110,6 +110,7 @@ def __init__( constraint_name: str, constraint_id: str, properties: Optional[BindingConstraintProperties] = None, + terms: Optional[dict[str, ConstraintTerm]] = None, **kwargs: Any, ) -> None: super().__init__(**kwargs) @@ -122,7 +123,16 @@ def __init__( self._comments = properties.comments self._filter_year_by_year = check_if_none(properties.filter_year_by_year, "hourly") self._filter_synthesis = check_if_none(properties.filter_synthesis, "hourly") - self._group = properties.group + self._group = check_if_none(properties.group, DEFAULT_GROUP) + self._terms = check_if_none(terms, {}) + + @property + def terms(self) -> dict[str, ConstraintTerm]: + return self._terms + + @terms.setter + def terms(self, new_terms: dict[str, ConstraintTerm]) -> None: + self._terms = new_terms @computed_field # type: ignore[misc] @property @@ -137,7 +147,7 @@ def list_ini_fields(self) -> dict[str, str]: "filter-year-by-year": self._filter_year_by_year, "filter-synthesis": self._filter_synthesis, "group": self._group, - } + } | {term_id: term.weight for term_id, term in self._terms.items()} return {key: value for key, value in ini_dict.items() if value is not None} @computed_field # type: ignore[misc] @@ -168,7 +178,7 @@ def __init__( # type: ignore # TODO: Find a way to avoid circular imports self._properties = properties or BindingConstraintProperties() self._terms = {term.id: term for term in terms} if terms else {} self._local_properties = BindingConstraintPropertiesLocal( - constraint_name=self._name, constraint_id=self._id, properties=properties + constraint_name=self._name, constraint_id=self._id, properties=properties, terms=self._terms ) @property diff --git a/src/antares/model/study.py b/src/antares/model/study.py index de9a0f59..d136c593 100644 --- a/src/antares/model/study.py +++ b/src/antares/model/study.py @@ -29,7 +29,6 @@ from antares.model.settings import StudySettings from antares.service.api_services.study_api import _returns_study_settings from antares.service.base_services import BaseStudyService -from antares.service.local_services.study_local import StudyLocalService from antares.service.service_factory import ServiceFactory from antares.tools.ini_tool import IniFile, IniFileTypes @@ -269,16 +268,6 @@ def create_binding_constraint( constraint = self._binding_constraints_service.create_binding_constraint( name, properties, terms, less_term_matrix, equal_term_matrix, greater_term_matrix ) - if isinstance(self.service, StudyLocalService): - constraint.properties = constraint.local_properties.yield_binding_constraint_properties - binding_constraints_ini_content = { - idx: idx_constraint.local_properties.list_ini_fields - for idx, idx_constraint in enumerate((self._binding_constraints | {constraint.id: constraint}).values()) - } - - binding_constraints_ini = IniFile(self.service.config.study_path, IniFileTypes.BINDING_CONSTRAINTS_INI) - binding_constraints_ini.ini_dict = binding_constraints_ini_content - binding_constraints_ini.write_ini_file() self._binding_constraints[constraint.id] = constraint return constraint diff --git a/src/antares/service/local_services/binding_constraint_local.py b/src/antares/service/local_services/binding_constraint_local.py index 4c5fd58c..7850cce2 100644 --- a/src/antares/service/local_services/binding_constraint_local.py +++ b/src/antares/service/local_services/binding_constraint_local.py @@ -22,6 +22,7 @@ ConstraintMatrixName, ) from antares.service.base_services import BaseBindingConstraintService +from antares.tools.ini_tool import IniFile, IniFileTypes class BindingConstraintLocalService(BaseBindingConstraintService): @@ -29,26 +30,46 @@ def __init__(self, config: LocalConfiguration, study_name: str, **kwargs: Any) - super().__init__(**kwargs) self.config = config self.study_name = study_name + self.binding_constraints: dict[str, BindingConstraint] = {} + self.ini_file = IniFile(self.config.study_path, IniFileTypes.BINDING_CONSTRAINTS_INI) def create_binding_constraint( self, name: str, properties: Optional[BindingConstraintProperties] = None, - terms: Optional[List[ConstraintTerm]] = None, + terms: Optional[list[ConstraintTerm]] = None, less_term_matrix: Optional[pd.DataFrame] = None, equal_term_matrix: Optional[pd.DataFrame] = None, greater_term_matrix: Optional[pd.DataFrame] = None, ) -> BindingConstraint: - return BindingConstraint( + constraint = BindingConstraint( name=name, binding_constraint_service=self, properties=properties, terms=terms, ) + constraint.properties = constraint.local_properties.yield_binding_constraint_properties + self.binding_constraints[constraint.id] = constraint - def add_constraint_terms(self, constraint: BindingConstraint, terms: List[ConstraintTerm]) -> List[ConstraintTerm]: - new_terms = [term for term in terms if term.id not in constraint.get_terms()] - return new_terms + self._write_binding_constraint_ini() + + return constraint + + def _write_binding_constraint_ini(self) -> None: + binding_constraints_ini_content = { + idx: idx_constraint.local_properties.list_ini_fields + for idx, idx_constraint in enumerate(self.binding_constraints.values()) + } + self.ini_file.ini_dict = binding_constraints_ini_content + self.ini_file.write_ini_file() + + def add_constraint_terms(self, constraint: BindingConstraint, terms: list[ConstraintTerm]) -> list[ConstraintTerm]: + new_terms = constraint.local_properties.terms | { + term.id: term for term in terms if term.id not in constraint.get_terms() + } + constraint.local_properties.terms = new_terms + self._write_binding_constraint_ini() + return list(new_terms.values()) def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> None: raise NotImplementedError diff --git a/tests/antares/services/local_services/conftest.py b/tests/antares/services/local_services/conftest.py index 4537ffe0..7b69a422 100644 --- a/tests/antares/services/local_services/conftest.py +++ b/tests/antares/services/local_services/conftest.py @@ -256,5 +256,5 @@ def default_constraint_properties() -> BindingConstraintProperties: comments=None, filter_year_by_year="hourly", filter_synthesis="hourly", - group=None, + group="default", ) diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index 1d9a7b6b..996a9df6 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -1125,6 +1125,7 @@ def test_constraints_ini_have_correct_default_content( operator = less filter-year-by-year = hourly filter-synthesis = hourly +group = default """ @@ -1158,6 +1159,7 @@ def test_constraints_and_ini_have_custom_properties(self, local_study_with_const operator = less filter-year-by-year = hourly filter-synthesis = hourly +group = default [1] name = test constraint two From 7f54ae634b24eae9061378b67aca45ae1b5f0464 Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Mon, 16 Sep 2024 15:25:47 +0200 Subject: [PATCH 08/26] Added default value 0 for constraint weight --- .../binding_constraint_local.py | 2 ++ .../services/local_services/test_study.py | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/antares/service/local_services/binding_constraint_local.py b/src/antares/service/local_services/binding_constraint_local.py index 7850cce2..d936895c 100644 --- a/src/antares/service/local_services/binding_constraint_local.py +++ b/src/antares/service/local_services/binding_constraint_local.py @@ -64,6 +64,8 @@ def _write_binding_constraint_ini(self) -> None: self.ini_file.write_ini_file() def add_constraint_terms(self, constraint: BindingConstraint, terms: list[ConstraintTerm]) -> list[ConstraintTerm]: + for term in terms: + term.weight = term.weight if term.weight is not None else 0 new_terms = constraint.local_properties.terms | { term.id: term for term in terms if term.id not in constraint.get_terms() } diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index 996a9df6..f4805a52 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -1191,3 +1191,25 @@ def test_constraint_can_add_term(self, test_constraint): new_term = [ConstraintTerm(data={"area1": "fr", "area2": "at"})] test_constraint.add_terms(new_term) assert test_constraint.get_terms() + + def test_constraint_term_and_ini_have_correct_defaults(self, local_study_with_constraint, test_constraint): + # Given + expected_ini_contents = """[0] +name = test constraint +id = test constraint +enabled = true +type = hourly +operator = less +filter-year-by-year = hourly +filter-synthesis = hourly +group = default +at%fr = 0 + +""" + # When + new_term = [ConstraintTerm(data={"area1": "fr", "area2": "at"})] + test_constraint.add_terms(new_term) + with local_study_with_constraint._binding_constraints_service.ini_file.ini_path.open("r") as file: + actual_ini_content = file.read() + + assert actual_ini_content == expected_ini_contents From f319ddb9e431942828a81dc8f2905e1fc5ae1f19 Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Mon, 16 Sep 2024 16:12:19 +0200 Subject: [PATCH 09/26] Added the weight and offset combination in the ini file, as well as disabled interpolation in the ini tool --- src/antares/model/binding_constraint.py | 11 ++++++++- .../binding_constraint_local.py | 2 -- src/antares/tools/contents_tool.py | 2 +- src/antares/tools/ini_tool.py | 10 ++++---- .../services/local_services/test_study.py | 24 +++++++++++++++++++ 5 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/antares/model/binding_constraint.py b/src/antares/model/binding_constraint.py index fe81e5fd..abca0b09 100644 --- a/src/antares/model/binding_constraint.py +++ b/src/antares/model/binding_constraint.py @@ -46,6 +46,15 @@ class TermOperators(BaseModel): weight: Optional[float] = None offset: Optional[int] = None + @computed_field # type: ignore[misc] + @property + def weight_offset(self) -> str: + if self.offset is not None: + weight_offset = f"{(self.weight if self.weight is not None else 0):.1f}%{self.offset}" + else: + weight_offset = f"{self.weight if self.weight is not None else 0}" + return weight_offset + class LinkData(BaseModel): """ @@ -147,7 +156,7 @@ def list_ini_fields(self) -> dict[str, str]: "filter-year-by-year": self._filter_year_by_year, "filter-synthesis": self._filter_synthesis, "group": self._group, - } | {term_id: term.weight for term_id, term in self._terms.items()} + } | {term_id: term.weight_offset for term_id, term in self._terms.items()} return {key: value for key, value in ini_dict.items() if value is not None} @computed_field # type: ignore[misc] diff --git a/src/antares/service/local_services/binding_constraint_local.py b/src/antares/service/local_services/binding_constraint_local.py index d936895c..7850cce2 100644 --- a/src/antares/service/local_services/binding_constraint_local.py +++ b/src/antares/service/local_services/binding_constraint_local.py @@ -64,8 +64,6 @@ def _write_binding_constraint_ini(self) -> None: self.ini_file.write_ini_file() def add_constraint_terms(self, constraint: BindingConstraint, terms: list[ConstraintTerm]) -> list[ConstraintTerm]: - for term in terms: - term.weight = term.weight if term.weight is not None else 0 new_terms = constraint.local_properties.terms | { term.id: term for term in terms if term.id not in constraint.get_terms() } diff --git a/src/antares/tools/contents_tool.py b/src/antares/tools/contents_tool.py index c2c3cd91..a3d78184 100644 --- a/src/antares/tools/contents_tool.py +++ b/src/antares/tools/contents_tool.py @@ -118,7 +118,7 @@ def to_craft(self) -> Dict[str, Any]: # TODO maybe put sorting functions together def sort_ini_sections(ini_to_sort: configparser.ConfigParser) -> configparser.ConfigParser: - sorted_ini = configparser.ConfigParser() + sorted_ini = configparser.ConfigParser(interpolation=None) for section in sorted(ini_to_sort.sections()): sorted_ini[section] = ini_to_sort[section] return sorted_ini diff --git a/src/antares/tools/ini_tool.py b/src/antares/tools/ini_tool.py index 05eb48f4..eac41b1f 100644 --- a/src/antares/tools/ini_tool.py +++ b/src/antares/tools/ini_tool.py @@ -69,7 +69,7 @@ def __init__( elif isinstance(ini_contents, ConfigParser): self._ini_contents = ini_contents else: - self._ini_contents = ConfigParser() + self._ini_contents = ConfigParser(interpolation=None) if self._full_path.is_file(): self.update_from_ini_file() else: @@ -82,7 +82,7 @@ def ini_dict(self) -> dict: @ini_dict.setter def ini_dict(self, new_ini_dict: dict[str, dict[str, str]]) -> None: - self._ini_contents = ConfigParser() + self._ini_contents = ConfigParser(interpolation=None) self._ini_contents.read_dict(new_ini_dict) @property @@ -118,7 +118,7 @@ def update_from_ini_file(self) -> None: if not self._full_path.is_file(): raise FileNotFoundError(f"No such file: {self._full_path}") - parsed_ini = ConfigParser() + parsed_ini = ConfigParser(interpolation=None) with self._full_path.open() as file: parsed_ini.read_file(file) @@ -139,14 +139,14 @@ def write_ini_file( @staticmethod def _sort_ini_sections(ini_to_sort: ConfigParser) -> ConfigParser: - sorted_ini = ConfigParser() + sorted_ini = ConfigParser(interpolation=None) for section in sorted(ini_to_sort.sections()): sorted_ini[section] = ini_to_sort[section] return sorted_ini @staticmethod def _sort_ini_section_content(ini_to_sort: ConfigParser) -> ConfigParser: - sorted_ini = ConfigParser() + sorted_ini = ConfigParser(interpolation=None) for section in ini_to_sort.sections(): sorted_ini[section] = {key: value for (key, value) in sorted(list(ini_to_sort[section].items()))} return sorted_ini diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index f4805a52..0425402a 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -1213,3 +1213,27 @@ def test_constraint_term_and_ini_have_correct_defaults(self, local_study_with_co actual_ini_content = file.read() assert actual_ini_content == expected_ini_contents + + def test_constraint_term_with_offset_and_ini_have_correct_values( + self, local_study_with_constraint, test_constraint + ): + # Given + expected_ini_contents = """[0] +name = test constraint +id = test constraint +enabled = true +type = hourly +operator = less +filter-year-by-year = hourly +filter-synthesis = hourly +group = default +at%fr = 0.0%1 + +""" + # When + new_term = [ConstraintTerm(offset=1, data={"area1": "fr", "area2": "at"})] + test_constraint.add_terms(new_term) + with local_study_with_constraint._binding_constraints_service.ini_file.ini_path.open("r") as file: + actual_ini_content = file.read() + + assert actual_ini_content == expected_ini_contents From cc3e16eb869872c7401aadaca91f7df1d54a5fc9 Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Tue, 17 Sep 2024 14:37:40 +0200 Subject: [PATCH 10/26] Upped numpy to 1.26.4, currently last version before 2.0.0. Added tox and tox-uv to allow for fast testing with multiple python versions. Changed exact dependencies to allow patch dependencies in requirements.txt and requirements-dev.txt --- requirements-dev.txt | 4 +++- requirements.txt | 12 ++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index be739fe1..89752773 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,8 @@ -r requirements.txt mypy~=1.10.0 -ruff==0.4.7 +ruff~=0.4.7 pytest-cov~=5.0.0 requests-mock~=1.12.1 types-requests~=2.27.1 +tox~=4.18.1 +tox-uv~=1.11.3 diff --git a/requirements.txt b/requirements.txt index 4bb6fbbe..674d91b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,11 @@ -absl-py==1.4.0 -numpy==1.24.4 -protobuf==4.23.3 +absl-py~=1.4.0 +numpy~=1.26.4 +protobuf~=4.23.3 requests~=2.31.0 -pandas ~=2.2.2 -pandas-stubs ~=2.2.2 +pandas~=2.2.2 +pandas-stubs~=2.2.2 pytest~=7.2.1 python-dateutil~=2.9.0 -pydantic==2.7.1 +pydantic~=2.7.1 configparser~=5.0.2 click~=8.1.7 From 809002e1fa4884e04bd69b60b8b1108d81d59b55 Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Wed, 18 Sep 2024 18:43:34 +0200 Subject: [PATCH 11/26] Fixed pydantic serializer warnings --- .../antares/services/api_services/test_link_api.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/antares/services/api_services/test_link_api.py b/tests/antares/services/api_services/test_link_api.py index 88781a47..ddc5bed8 100644 --- a/tests/antares/services/api_services/test_link_api.py +++ b/tests/antares/services/api_services/test_link_api.py @@ -11,6 +11,9 @@ # This file is part of the Antares project. +import pytest +import requests_mock + from antares.api_conf.api_conf import APIconf from antares.exceptions.exceptions import LinkUiUpdateError, LinkPropertiesUpdateError from antares.model.area import Area @@ -18,9 +21,6 @@ from antares.model.link import LinkProperties, LinkUi, Link from antares.model.study import Study from antares.service.service_factory import ServiceFactory -import requests_mock - -import pytest class TestCreateAPI: @@ -37,8 +37,8 @@ class TestCreateAPI: def test_update_links_properties_success(self): with requests_mock.Mocker() as mocker: properties = LinkProperties() - properties.filter_synthesis = [FilterOption.DAILY] - properties.filter_year_by_year = [FilterOption.DAILY] + properties.filter_synthesis = {FilterOption.DAILY} + properties.filter_year_by_year = {FilterOption.DAILY} ui = LinkUi() raw_url = ( f"https://antares.com/api/v1/studies/{self.study_id}/raw?path=input/links/" @@ -56,8 +56,8 @@ def test_update_links_properties_success(self): def test_update_links_properties_fails(self): with requests_mock.Mocker() as mocker: properties = LinkProperties() - properties.filter_synthesis = [FilterOption.DAILY] - properties.filter_year_by_year = [FilterOption.DAILY] + properties.filter_synthesis = {FilterOption.DAILY} + properties.filter_year_by_year = {FilterOption.DAILY} raw_url = ( f"https://antares.com/api/v1/studies/{self.study_id}/raw?path=input/links/" f"{self.area_from.id}/properties/{self.area_to.id}" From c906faab172c5cde4c7ed8a2f3d628fa237e30a0 Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Wed, 18 Sep 2024 18:44:26 +0200 Subject: [PATCH 12/26] Corrected number of decimal digits --- src/antares/model/binding_constraint.py | 3 ++- tests/antares/services/local_services/test_study.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/antares/model/binding_constraint.py b/src/antares/model/binding_constraint.py index abca0b09..2e3f1864 100644 --- a/src/antares/model/binding_constraint.py +++ b/src/antares/model/binding_constraint.py @@ -50,7 +50,8 @@ class TermOperators(BaseModel): @property def weight_offset(self) -> str: if self.offset is not None: - weight_offset = f"{(self.weight if self.weight is not None else 0):.1f}%{self.offset}" + # Rounded the weight to 6 decimals to be in line with other floats in the ini files + weight_offset = f"{(self.weight if self.weight is not None else 0):.6f}%{self.offset}" else: weight_offset = f"{self.weight if self.weight is not None else 0}" return weight_offset diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index 0425402a..9862bdf5 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -1227,7 +1227,7 @@ def test_constraint_term_with_offset_and_ini_have_correct_values( filter-year-by-year = hourly filter-synthesis = hourly group = default -at%fr = 0.0%1 +at%fr = 0.000000%1 """ # When From 5bf47a950457709ab2e56c88c4049e690da54daa Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Wed, 18 Sep 2024 18:46:18 +0200 Subject: [PATCH 13/26] Refactored binding constraints to be tracked in the service instead of the study to allow correct handling of ini file --- src/antares/model/study.py | 8 +++----- .../service/api_services/binding_constraint_api.py | 10 +++++++++- src/antares/service/base_services.py | 5 +++++ .../service/local_services/binding_constraint_local.py | 10 +++++++--- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/antares/model/study.py b/src/antares/model/study.py index d136c593..6604031f 100644 --- a/src/antares/model/study.py +++ b/src/antares/model/study.py @@ -225,7 +225,7 @@ def get_settings(self) -> StudySettings: return self._settings def get_binding_constraints(self) -> MappingProxyType[str, BindingConstraint]: - return MappingProxyType(self._binding_constraints) + return MappingProxyType(self._binding_constraints_service.binding_constraints) def create_area( self, area_name: str, *, properties: Optional[AreaProperties] = None, ui: Optional[AreaUi] = None @@ -265,11 +265,9 @@ def create_binding_constraint( equal_term_matrix: Optional[pd.DataFrame] = None, greater_term_matrix: Optional[pd.DataFrame] = None, ) -> BindingConstraint: - constraint = self._binding_constraints_service.create_binding_constraint( + return self._binding_constraints_service.create_binding_constraint( name, properties, terms, less_term_matrix, equal_term_matrix, greater_term_matrix ) - self._binding_constraints[constraint.id] = constraint - return constraint def update_settings(self, settings: StudySettings) -> None: new_settings = self._study_service.update_study_settings(settings) @@ -278,7 +276,7 @@ def update_settings(self, settings: StudySettings) -> None: def delete_binding_constraint(self, constraint: BindingConstraint) -> None: self._study_service.delete_binding_constraint(constraint) - self._binding_constraints.pop(constraint.id) + self._binding_constraints_service.binding_constraints.pop(constraint.id) def delete(self, children: bool = False) -> None: self._study_service.delete(children) diff --git a/src/antares/service/api_services/binding_constraint_api.py b/src/antares/service/api_services/binding_constraint_api.py index ca0d7770..6d1c4243 100644 --- a/src/antares/service/api_services/binding_constraint_api.py +++ b/src/antares/service/api_services/binding_constraint_api.py @@ -43,6 +43,7 @@ def __init__(self, config: APIconf, study_id: str) -> None: self.study_id = study_id self._wrapper = RequestWrapper(self.api_config.set_up_api_conf()) self._base_url = f"{self.api_config.get_host()}/api/v1" + self._binding_constraints: dict[str, BindingConstraint] = {} def create_binding_constraint( self, @@ -103,7 +104,14 @@ def create_binding_constraint( except APIError as e: raise BindingConstraintCreationError(name, e.message) from e - return BindingConstraint(name, self, bc_properties, bc_terms) + constraint = BindingConstraint(name, self, bc_properties, bc_terms) + self._binding_constraints[constraint.id] = constraint + + return constraint + + @property + def binding_constraints(self) -> dict[str, BindingConstraint]: + return self._binding_constraints def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> None: url = f"{self._base_url}/studies/{self.study_id}/bindingconstraints/{constraint_id}/term/{term_id}" diff --git a/src/antares/service/base_services.py b/src/antares/service/base_services.py index f372d6aa..291908de 100644 --- a/src/antares/service/base_services.py +++ b/src/antares/service/base_services.py @@ -391,6 +391,11 @@ def add_constraint_terms(self, constraint: BindingConstraint, terms: List[Constr """ pass + @property + @abstractmethod + def binding_constraints(self) -> dict[str, BindingConstraint]: + pass + @abstractmethod def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> None: """ diff --git a/src/antares/service/local_services/binding_constraint_local.py b/src/antares/service/local_services/binding_constraint_local.py index 7850cce2..991792b4 100644 --- a/src/antares/service/local_services/binding_constraint_local.py +++ b/src/antares/service/local_services/binding_constraint_local.py @@ -30,7 +30,7 @@ def __init__(self, config: LocalConfiguration, study_name: str, **kwargs: Any) - super().__init__(**kwargs) self.config = config self.study_name = study_name - self.binding_constraints: dict[str, BindingConstraint] = {} + self._binding_constraints: dict[str, BindingConstraint] = {} self.ini_file = IniFile(self.config.study_path, IniFileTypes.BINDING_CONSTRAINTS_INI) def create_binding_constraint( @@ -49,7 +49,7 @@ def create_binding_constraint( terms=terms, ) constraint.properties = constraint.local_properties.yield_binding_constraint_properties - self.binding_constraints[constraint.id] = constraint + self._binding_constraints[constraint.id] = constraint self._write_binding_constraint_ini() @@ -58,11 +58,15 @@ def create_binding_constraint( def _write_binding_constraint_ini(self) -> None: binding_constraints_ini_content = { idx: idx_constraint.local_properties.list_ini_fields - for idx, idx_constraint in enumerate(self.binding_constraints.values()) + for idx, idx_constraint in enumerate(self._binding_constraints.values()) } self.ini_file.ini_dict = binding_constraints_ini_content self.ini_file.write_ini_file() + @property + def binding_constraints(self) -> dict[str, BindingConstraint]: + return self._binding_constraints + def add_constraint_terms(self, constraint: BindingConstraint, terms: list[ConstraintTerm]) -> list[ConstraintTerm]: new_terms = constraint.local_properties.terms | { term.id: term for term in terms if term.id not in constraint.get_terms() From 784e45c4d48cce70514f4db67926328a8c415a7a Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Mon, 23 Sep 2024 16:01:52 +0200 Subject: [PATCH 14/26] Rewrite binding constraint to use the new default values and decorator --- src/antares/model/binding_constraint.py | 124 ++++++++---------- .../binding_constraint_local.py | 4 +- .../services/local_services/conftest.py | 2 +- 3 files changed, 61 insertions(+), 69 deletions(-) diff --git a/src/antares/model/binding_constraint.py b/src/antares/model/binding_constraint.py index 2e3f1864..ea1aa852 100644 --- a/src/antares/model/binding_constraint.py +++ b/src/antares/model/binding_constraint.py @@ -17,10 +17,8 @@ from pydantic import BaseModel, Field, model_validator, computed_field from pydantic.alias_generators import to_camel +from antares.tools.all_optional_meta import all_optional_model from antares.tools.contents_tool import EnumIgnoreCase, transform_name_to_id -from antares.tools.ini_tool import check_if_none - -DEFAULT_GROUP = "default" class BindingConstraintFrequency(EnumIgnoreCase): @@ -95,83 +93,73 @@ def generate_id(cls, data: Union[Dict[str, str], LinkData, ClusterData]) -> str: return ".".join((data.area.lower(), data.cluster.lower())) -class BindingConstraintProperties(BaseModel, extra="forbid", populate_by_name=True, alias_generator=to_camel): - enabled: Optional[bool] = None - time_step: Optional[BindingConstraintFrequency] = None - operator: Optional[BindingConstraintOperator] = None - comments: Optional[str] = None - filter_year_by_year: Optional[str] = None - filter_synthesis: Optional[str] = None - group: Optional[str] = None +class DefaultBindingConstraintProperties(BaseModel, extra="forbid", populate_by_name=True, alias_generator=to_camel): + """Default properties for binding constraints + + Attributes: + enabled (bool): True + time_step (BindingConstraintFrequency): BindingConstraintFrequency.HOURLY + operator (BindingConstraintOperator): BindingConstraintOperator.LESS + comments (str): None + filter_year_by_year (str): "hourly" + filter_synthesis (str): "hourly" + group (str): "default" + + """ + + enabled: bool = True + time_step: BindingConstraintFrequency = BindingConstraintFrequency.HOURLY + operator: BindingConstraintOperator = BindingConstraintOperator.LESS + comments: str = "" + filter_year_by_year: str = "hourly" + filter_synthesis: str = "hourly" + group: str = "default" + +@all_optional_model +class BindingConstraintProperties(DefaultBindingConstraintProperties): + pass -class BindingConstraintPropertiesLocal(BaseModel): + +class BindingConstraintPropertiesLocal(DefaultBindingConstraintProperties): """ Used to create the entries for the bindingconstraints.ini file - Args: + Attributes: constraint_name: The constraint name constraint_id: The constraint id properties (BindingConstraintProperties): The BindingConstraintProperties to set + terms (dict[str, ConstraintTerm]]): The terms applying to the binding constraint """ - def __init__( - self, - constraint_name: str, - constraint_id: str, - properties: Optional[BindingConstraintProperties] = None, - terms: Optional[dict[str, ConstraintTerm]] = None, - **kwargs: Any, - ) -> None: - super().__init__(**kwargs) - properties = properties if properties is not None else BindingConstraintProperties() - self._constraint_name = constraint_name - self._constraint_id = constraint_id - self._enabled = check_if_none(properties.enabled, True) - self._time_step = check_if_none(properties.time_step, BindingConstraintFrequency.HOURLY) - self._operator = check_if_none(properties.operator, BindingConstraintOperator.LESS) - self._comments = properties.comments - self._filter_year_by_year = check_if_none(properties.filter_year_by_year, "hourly") - self._filter_synthesis = check_if_none(properties.filter_synthesis, "hourly") - self._group = check_if_none(properties.group, DEFAULT_GROUP) - self._terms = check_if_none(terms, {}) - - @property - def terms(self) -> dict[str, ConstraintTerm]: - return self._terms - - @terms.setter - def terms(self, new_terms: dict[str, ConstraintTerm]) -> None: - self._terms = new_terms + constraint_name: str + constraint_id: str + terms: dict[str, ConstraintTerm] = {} @computed_field # type: ignore[misc] @property def list_ini_fields(self) -> dict[str, str]: ini_dict = { - "name": self._constraint_name, - "id": self._constraint_id, - "enabled": f"{self._enabled}".lower(), - "type": self._time_step.value, - "operator": self._operator.value, - "comments": self._comments, - "filter-year-by-year": self._filter_year_by_year, - "filter-synthesis": self._filter_synthesis, - "group": self._group, - } | {term_id: term.weight_offset for term_id, term in self._terms.items()} - return {key: value for key, value in ini_dict.items() if value is not None} + "name": self.constraint_name, + "id": self.constraint_id, + "enabled": f"{self.enabled}".lower(), + "type": self.time_step.value, + "operator": self.operator.value, + "comments": self.comments, + "filter-year-by-year": self.filter_year_by_year, + "filter-synthesis": self.filter_synthesis, + "group": self.group, + } | {term_id: term.weight_offset for term_id, term in self.terms.items()} + return {key: value for key, value in ini_dict.items() if value not in [None, ""]} - @computed_field # type: ignore[misc] - @property def yield_binding_constraint_properties(self) -> BindingConstraintProperties: - return BindingConstraintProperties( - enabled=self._enabled, - time_step=self._time_step, - operator=self._operator, - comments=self._comments, - filter_year_by_year=self._filter_year_by_year, - filter_synthesis=self._filter_synthesis, - group=self._group, - ) + excludes = { + "constraint_name", + "constraint_id", + "terms", + "list_ini_fields", + } + return BindingConstraintProperties.model_validate(self.model_dump(mode="json", exclude=excludes)) class BindingConstraint: @@ -187,9 +175,13 @@ def __init__( # type: ignore # TODO: Find a way to avoid circular imports self._id = transform_name_to_id(name) self._properties = properties or BindingConstraintProperties() self._terms = {term.id: term for term in terms} if terms else {} - self._local_properties = BindingConstraintPropertiesLocal( - constraint_name=self._name, constraint_id=self._id, properties=properties, terms=self._terms - ) + local_property_args = { + "constraint_name": self._name, + "constraint_id": self._id, + "terms": self._terms, + **self._properties.model_dump(mode="json", exclude_none=True), + } + self._local_properties = BindingConstraintPropertiesLocal.model_validate(local_property_args) @property def name(self) -> str: diff --git a/src/antares/service/local_services/binding_constraint_local.py b/src/antares/service/local_services/binding_constraint_local.py index 991792b4..c5e1ad5b 100644 --- a/src/antares/service/local_services/binding_constraint_local.py +++ b/src/antares/service/local_services/binding_constraint_local.py @@ -10,7 +10,7 @@ # # This file is part of the Antares project. -from typing import Optional, List, Any +from typing import Optional, Any import pandas as pd @@ -48,7 +48,7 @@ def create_binding_constraint( properties=properties, terms=terms, ) - constraint.properties = constraint.local_properties.yield_binding_constraint_properties + constraint.properties = constraint.local_properties.yield_binding_constraint_properties() self._binding_constraints[constraint.id] = constraint self._write_binding_constraint_ini() diff --git a/tests/antares/services/local_services/conftest.py b/tests/antares/services/local_services/conftest.py index 7b69a422..1570e33f 100644 --- a/tests/antares/services/local_services/conftest.py +++ b/tests/antares/services/local_services/conftest.py @@ -253,7 +253,7 @@ def default_constraint_properties() -> BindingConstraintProperties: enabled=True, time_step=BindingConstraintFrequency.HOURLY, operator=BindingConstraintOperator.LESS, - comments=None, + comments="", filter_year_by_year="hourly", filter_synthesis="hourly", group="default", From d163cee633b893a873802cff59346b42adbddc49 Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Wed, 25 Sep 2024 18:41:10 +0200 Subject: [PATCH 15/26] Updated TimeSeriesFile to require named arguments after file type and path, updated use to match --- src/antares/model/area.py | 12 +++++------ .../service/local_services/area_local.py | 20 ++++++++++++++----- src/antares/tools/prepro_folder.py | 12 +++++++---- src/antares/tools/time_series_tool.py | 5 +++-- tests/antares/tools/conftest.py | 2 +- tests/antares/tools/test_time_series_tool.py | 4 ++-- 6 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/antares/model/area.py b/src/antares/model/area.py index 047ff1e2..f1652e0a 100644 --- a/src/antares/model/area.py +++ b/src/antares/model/area.py @@ -294,12 +294,12 @@ def create_thermal_cluster_with_matrices( def create_renewable_cluster( self, renewable_name: str, properties: Optional[RenewableClusterProperties], series: Optional[pd.DataFrame] ) -> RenewableCluster: - renewable = self._area_service.create_renewable_cluster(self.id, renewable_name, properties, series) + renewable = self._area_service.create_renewable_cluster(self.id, renewable_name, properties, series=series) self._renewables[renewable.id] = renewable return renewable def create_load(self, series: Optional[pd.DataFrame]) -> Load: - load = self._area_service.create_load(self, series) + load = self._area_service.create_load(self, series=series) self._load = load return load @@ -348,22 +348,22 @@ def update_ui(self, ui: AreaUi) -> None: self._ui = new_ui def create_wind(self, series: Optional[pd.DataFrame]) -> Wind: - wind = self._area_service.create_wind(self, series) + wind = self._area_service.create_wind(self, series=series) self._wind = wind return wind def create_reserves(self, series: Optional[pd.DataFrame]) -> Reserves: - reserves = self._area_service.create_reserves(self, series) + reserves = self._area_service.create_reserves(self, series=series) self._reserves = reserves return reserves def create_solar(self, series: Optional[pd.DataFrame]) -> Solar: - solar = self._area_service.create_solar(self, series) + solar = self._area_service.create_solar(self, series=series) self._solar = solar return solar def create_misc_gen(self, series: Optional[pd.DataFrame]) -> MiscGen: - misc_gen = self._area_service.create_misc_gen(self, series) + misc_gen = self._area_service.create_misc_gen(self, series=series) self._misc_gen = misc_gen return misc_gen diff --git a/src/antares/service/local_services/area_local.py b/src/antares/service/local_services/area_local.py index f711a757..b2029857 100644 --- a/src/antares/service/local_services/area_local.py +++ b/src/antares/service/local_services/area_local.py @@ -123,7 +123,9 @@ def create_renewable_cluster( def create_load(self, area: Area, series: Optional[pd.DataFrame]) -> Load: series = series if series is not None else pd.DataFrame([]) - local_file = TimeSeriesFile(TimeSeriesFileType.LOAD, self.config.study_path, area.id, series) + local_file = TimeSeriesFile( + TimeSeriesFileType.LOAD, self.config.study_path, area_id=area.id, time_series=series + ) return Load(time_series=series, local_file=local_file, study_path=self.config.study_path, area_id=area.id) def create_st_storage( @@ -146,22 +148,30 @@ def create_st_storage( def create_wind(self, area: Area, series: Optional[pd.DataFrame]) -> Wind: series = series if series is not None else pd.DataFrame([]) - local_file = TimeSeriesFile(TimeSeriesFileType.WIND, self.config.study_path, area.id, series) + local_file = TimeSeriesFile( + TimeSeriesFileType.WIND, self.config.study_path, area_id=area.id, time_series=series + ) return Wind(time_series=series, local_file=local_file, study_path=self.config.study_path, area_id=area.id) def create_reserves(self, area: Area, series: Optional[pd.DataFrame]) -> Reserves: series = series if series is not None else pd.DataFrame([]) - local_file = TimeSeriesFile(TimeSeriesFileType.RESERVES, self.config.study_path, area.id, series) + local_file = TimeSeriesFile( + TimeSeriesFileType.RESERVES, self.config.study_path, area_id=area.id, time_series=series + ) return Reserves(series, local_file) def create_solar(self, area: Area, series: Optional[pd.DataFrame]) -> Solar: series = series if series is not None else pd.DataFrame([]) - local_file = TimeSeriesFile(TimeSeriesFileType.SOLAR, self.config.study_path, area.id, series) + local_file = TimeSeriesFile( + TimeSeriesFileType.SOLAR, self.config.study_path, area_id=area.id, time_series=series + ) return Solar(time_series=series, local_file=local_file, study_path=self.config.study_path, area_id=area.id) def create_misc_gen(self, area: Area, series: Optional[pd.DataFrame]) -> MiscGen: series = series if series is not None else pd.DataFrame([]) - local_file = TimeSeriesFile(TimeSeriesFileType.MISC_GEN, self.config.study_path, area.id, series) + local_file = TimeSeriesFile( + TimeSeriesFileType.MISC_GEN, self.config.study_path, area_id=area.id, time_series=series + ) return MiscGen(series, local_file) def create_hydro( diff --git a/src/antares/tools/prepro_folder.py b/src/antares/tools/prepro_folder.py index 742390b2..8896f452 100644 --- a/src/antares/tools/prepro_folder.py +++ b/src/antares/tools/prepro_folder.py @@ -46,13 +46,17 @@ def __init__(self, folder: str, study_path: Path, area_id: str) -> None: self._settings = IniFile(study_path, settings, area_id) self._conversion = TimeSeries( ConversionFile().data, - TimeSeriesFile(conversion, study_path, area_id, ConversionFile().data), + TimeSeriesFile(conversion, study_path, area_id=area_id, time_series=ConversionFile().data), + ) + self._data = TimeSeries( + DataFile().data, TimeSeriesFile(data, study_path, area_id=area_id, time_series=DataFile().data) + ) + self._k = TimeSeries( + pd.DataFrame([]), TimeSeriesFile(k, study_path, area_id=area_id, time_series=pd.DataFrame([])) ) - self._data = TimeSeries(DataFile().data, TimeSeriesFile(data, study_path, area_id, DataFile().data)) - self._k = TimeSeries(pd.DataFrame([]), TimeSeriesFile(k, study_path, area_id, pd.DataFrame([]))) self._translation = TimeSeries( pd.DataFrame([]), - TimeSeriesFile(translation, study_path, area_id, pd.DataFrame([])), + TimeSeriesFile(translation, study_path, area_id=area_id, time_series=pd.DataFrame([])), ) @property diff --git a/src/antares/tools/time_series_tool.py b/src/antares/tools/time_series_tool.py index 35bf1bb9..30540dfd 100644 --- a/src/antares/tools/time_series_tool.py +++ b/src/antares/tools/time_series_tool.py @@ -24,13 +24,13 @@ class TimeSeriesFileType(Enum): This DTO contains the relative paths to different timeseries files used in the generation of an Antares study, starting from the base folder of the study. - Files where the path contains {area_id} have to be used with .format(area_id=) where is replaced - with the area's id to access the correct path. + Files where the path contains {area_id} or {constraint_id} have to be used with `.format` to access the correct path. Example: TimeSeriesFileType.SOLAR.value.format(area_id="test_area") """ + BINDING_CONSTRAINT_LESS = "input/bindingconstraints/{constraint_id}_lt.txt" LOAD = "input/load/series/load_{area_id}.txt" LOAD_CONVERSION = "input/load/prepro/{area_id}/conversion.txt" LOAD_DATA = "input/load/prepro/{area_id}/data.txt" @@ -70,6 +70,7 @@ def __init__( self, ts_file_type: TimeSeriesFileType, study_path: Path, + *, area_id: Optional[str] = None, time_series: Optional[pd.DataFrame] = None, ) -> None: diff --git a/tests/antares/tools/conftest.py b/tests/antares/tools/conftest.py index fbe2d374..3807bd0b 100644 --- a/tests/antares/tools/conftest.py +++ b/tests/antares/tools/conftest.py @@ -24,4 +24,4 @@ def time_series_data(): @pytest.fixture def time_series_file(tmp_path, time_series_data): - return TimeSeriesFile(TimeSeriesFileType.RESERVES, tmp_path, "test", time_series_data) + return TimeSeriesFile(TimeSeriesFileType.RESERVES, tmp_path, area_id="test", time_series=time_series_data) diff --git a/tests/antares/tools/test_time_series_tool.py b/tests/antares/tools/test_time_series_tool.py index 5f4d8b21..e2f64f6d 100644 --- a/tests/antares/tools/test_time_series_tool.py +++ b/tests/antares/tools/test_time_series_tool.py @@ -112,7 +112,7 @@ def test_file_exists_time_series_provided_gives_error(self, tmp_path, time_serie with pytest.raises( ValueError, match=f"File {tmp_path / file_name} already exists and a time series was provided." ): - TimeSeriesFile(TimeSeriesFileType.RESERVES, tmp_path, "test", time_series.time_series) + TimeSeriesFile(TimeSeriesFileType.RESERVES, tmp_path, area_id="test", time_series=time_series.time_series) def test_file_exists_no_time_series_provided(self, tmp_path, time_series_data): # Given @@ -122,7 +122,7 @@ def test_file_exists_no_time_series_provided(self, tmp_path, time_series_data): # When file_name.parent.mkdir(exist_ok=True, parents=True) time_series.time_series.to_csv(file_name, sep="\t", header=False, index=False, encoding="utf-8") - time_series_file = TimeSeriesFile(TimeSeriesFileType.RESERVES, tmp_path, "test") + time_series_file = TimeSeriesFile(TimeSeriesFileType.RESERVES, tmp_path, area_id="test") # Then assert time_series_file.time_series.equals(time_series_data) From 11154675fd223a6b5589092b0f890d2276e7b476 Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Wed, 25 Sep 2024 23:10:56 +0200 Subject: [PATCH 16/26] Binding constraint time series can be created --- .../binding_constraint_local.py | 39 ++++++++++- src/antares/tools/time_series_tool.py | 10 ++- .../services/local_services/test_study.py | 64 +++++++++++++++++++ 3 files changed, 111 insertions(+), 2 deletions(-) diff --git a/src/antares/service/local_services/binding_constraint_local.py b/src/antares/service/local_services/binding_constraint_local.py index c5e1ad5b..cf5fcc96 100644 --- a/src/antares/service/local_services/binding_constraint_local.py +++ b/src/antares/service/local_services/binding_constraint_local.py @@ -20,9 +20,11 @@ ConstraintTerm, BindingConstraint, ConstraintMatrixName, + BindingConstraintOperator, ) from antares.service.base_services import BaseBindingConstraintService from antares.tools.ini_tool import IniFile, IniFileTypes +from antares.tools.time_series_tool import TimeSeriesFile, TimeSeriesFileType class BindingConstraintLocalService(BaseBindingConstraintService): @@ -32,6 +34,7 @@ def __init__(self, config: LocalConfiguration, study_name: str, **kwargs: Any) - self.study_name = study_name self._binding_constraints: dict[str, BindingConstraint] = {} self.ini_file = IniFile(self.config.study_path, IniFileTypes.BINDING_CONSTRAINTS_INI) + self._time_series: dict[str, TimeSeriesFile] = {} def create_binding_constraint( self, @@ -49,10 +52,40 @@ def create_binding_constraint( terms=terms, ) constraint.properties = constraint.local_properties.yield_binding_constraint_properties() - self._binding_constraints[constraint.id] = constraint + # Add binding constraints + self._binding_constraints[constraint.id] = constraint self._write_binding_constraint_ini() + # Add constraint time series + if ( + constraint.properties.operator in (BindingConstraintOperator.LESS, BindingConstraintOperator.BOTH) + and less_term_matrix is not None + ): + self._time_series[f"{name}_lt"] = TimeSeriesFile( + TimeSeriesFileType.BINDING_CONSTRAINT_LESS, + self.config.study_path, + constraint_id=constraint.id.lower(), + time_series=less_term_matrix, + ) + if constraint.properties.operator == BindingConstraintOperator.EQUAL and equal_term_matrix is not None: + self._time_series[f"{name}_eq"] = TimeSeriesFile( + TimeSeriesFileType.BINDING_CONSTRAINT_EQUAL, + self.config.study_path, + constraint_id=constraint.id.lower(), + time_series=equal_term_matrix, + ) + if ( + constraint.properties.operator in (BindingConstraintOperator.GREATER, BindingConstraintOperator.BOTH) + and greater_term_matrix is not None + ): + self._time_series[f"{name}_gt"] = TimeSeriesFile( + TimeSeriesFileType.BINDING_CONSTRAINT_GREATER, + self.config.study_path, + constraint_id=constraint.id.lower(), + time_series=greater_term_matrix, + ) + return constraint def _write_binding_constraint_ini(self) -> None: @@ -67,6 +100,10 @@ def _write_binding_constraint_ini(self) -> None: def binding_constraints(self) -> dict[str, BindingConstraint]: return self._binding_constraints + @property + def time_series(self) -> dict[str, TimeSeriesFile]: + return self._time_series + def add_constraint_terms(self, constraint: BindingConstraint, terms: list[ConstraintTerm]) -> list[ConstraintTerm]: new_terms = constraint.local_properties.terms | { term.id: term for term in terms if term.id not in constraint.get_terms() diff --git a/src/antares/tools/time_series_tool.py b/src/antares/tools/time_series_tool.py index 30540dfd..74682a67 100644 --- a/src/antares/tools/time_series_tool.py +++ b/src/antares/tools/time_series_tool.py @@ -30,6 +30,8 @@ class TimeSeriesFileType(Enum): TimeSeriesFileType.SOLAR.value.format(area_id="test_area") """ + BINDING_CONSTRAINT_EQUAL = "input/bindingconstraints/{constraint_id}_eq.txt" + BINDING_CONSTRAINT_GREATER = "input/bindingconstraints/{constraint_id}_gt.txt" BINDING_CONSTRAINT_LESS = "input/bindingconstraints/{constraint_id}_lt.txt" LOAD = "input/load/series/load_{area_id}.txt" LOAD_CONVERSION = "input/load/prepro/{area_id}/conversion.txt" @@ -60,6 +62,7 @@ class TimeSeriesFile: ts_file_type: Type of time series file using the class TimeSeriesFileType. study_path: `Path` to the study directory. area_id: Area ID for file paths that use the area's id in their path + constraint_id: Constraint ID for file paths that use the binding constraint's id in their path time_series: The actual timeseries as a pandas DataFrame. Raises: @@ -72,13 +75,18 @@ def __init__( study_path: Path, *, area_id: Optional[str] = None, + constraint_id: Optional[str] = None, time_series: Optional[pd.DataFrame] = None, ) -> None: if "{area_id}" in ts_file_type.value and area_id is None: raise ValueError("area_id is required for this file type.") + if "{constraint_id}" in ts_file_type.value and constraint_id is None: + raise ValueError("constraint_id is required for this file type.") self.file_path = study_path / ( - ts_file_type.value if not area_id else ts_file_type.value.format(area_id=area_id) + ts_file_type.value + if not (area_id or constraint_id) + else ts_file_type.value.format(area_id=area_id, constraint_id=constraint_id) ) if self.file_path.is_file() and time_series is not None: diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index 9862bdf5..acaa2efc 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -16,6 +16,8 @@ from configparser import ConfigParser from pathlib import Path +import numpy as np +import pandas as pd import pytest from antares.config.local_configuration import LocalConfiguration @@ -1237,3 +1239,65 @@ def test_constraint_term_with_offset_and_ini_have_correct_values( actual_ini_content = file.read() assert actual_ini_content == expected_ini_contents + + def test_binding_constraint_with_timeseries_stores_ts_file(self, local_study_with_hydro): + # Given + ts_matrix = pd.DataFrame(np.zeros([365 * 24, 2])) + + # When + constraints = { + "lesser": + # Less than timeseries + local_study_with_hydro.create_binding_constraint( + name="test constraint - less", + properties=BindingConstraintProperties( + operator=BindingConstraintOperator.LESS, + ), + less_term_matrix=ts_matrix, + ), + "equal": + # Equal timeseries + local_study_with_hydro.create_binding_constraint( + name="test constraint - equal", + properties=BindingConstraintProperties( + operator=BindingConstraintOperator.EQUAL, + ), + equal_term_matrix=ts_matrix, + ), + "greater": + # Greater than timeseries + local_study_with_hydro.create_binding_constraint( + name="test constraint - greater", + properties=BindingConstraintProperties( + operator=BindingConstraintOperator.GREATER, + ), + greater_term_matrix=ts_matrix, + ), + "both": + # Greater than timeseries + local_study_with_hydro.create_binding_constraint( + name="test constraint - both", + properties=BindingConstraintProperties( + operator=BindingConstraintOperator.BOTH, + ), + less_term_matrix=ts_matrix, + greater_term_matrix=ts_matrix, + ), + } + + # Then + assert local_study_with_hydro._binding_constraints_service.time_series[ + f"{constraints['lesser'].id.lower()}_lt" + ].file_path.is_file() + assert local_study_with_hydro._binding_constraints_service.time_series[ + f"{constraints['equal'].id.lower()}_eq" + ].file_path.is_file() + assert local_study_with_hydro._binding_constraints_service.time_series[ + f"{constraints['greater'].id.lower()}_gt" + ].file_path.is_file() + assert local_study_with_hydro._binding_constraints_service.time_series[ + f"{constraints['both'].id.lower()}_lt" + ].file_path.is_file() + assert local_study_with_hydro._binding_constraints_service.time_series[ + f"{constraints['both'].id.lower()}_gt" + ].file_path.is_file() From b63d99fb9ab9d2ad960b2672f11656da224fbb5c Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Thu, 26 Sep 2024 10:05:11 +0200 Subject: [PATCH 17/26] Refactored binding constraint time series to use the correct object type --- .../binding_constraint_local.py | 45 +++++++++++-------- .../services/local_services/test_study.py | 10 ++--- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/antares/service/local_services/binding_constraint_local.py b/src/antares/service/local_services/binding_constraint_local.py index cf5fcc96..0b30bfb9 100644 --- a/src/antares/service/local_services/binding_constraint_local.py +++ b/src/antares/service/local_services/binding_constraint_local.py @@ -24,7 +24,7 @@ ) from antares.service.base_services import BaseBindingConstraintService from antares.tools.ini_tool import IniFile, IniFileTypes -from antares.tools.time_series_tool import TimeSeriesFile, TimeSeriesFileType +from antares.tools.time_series_tool import TimeSeriesFile, TimeSeriesFileType, TimeSeries class BindingConstraintLocalService(BaseBindingConstraintService): @@ -34,7 +34,7 @@ def __init__(self, config: LocalConfiguration, study_name: str, **kwargs: Any) - self.study_name = study_name self._binding_constraints: dict[str, BindingConstraint] = {} self.ini_file = IniFile(self.config.study_path, IniFileTypes.BINDING_CONSTRAINTS_INI) - self._time_series: dict[str, TimeSeriesFile] = {} + self._time_series: dict[str, TimeSeries] = {} def create_binding_constraint( self, @@ -62,28 +62,37 @@ def create_binding_constraint( constraint.properties.operator in (BindingConstraintOperator.LESS, BindingConstraintOperator.BOTH) and less_term_matrix is not None ): - self._time_series[f"{name}_lt"] = TimeSeriesFile( - TimeSeriesFileType.BINDING_CONSTRAINT_LESS, - self.config.study_path, - constraint_id=constraint.id.lower(), - time_series=less_term_matrix, + self._time_series[f"{name}_lt"] = TimeSeries( + less_term_matrix, + TimeSeriesFile( + TimeSeriesFileType.BINDING_CONSTRAINT_LESS, + self.config.study_path, + constraint_id=constraint.id.lower(), + time_series=less_term_matrix, + ), ) if constraint.properties.operator == BindingConstraintOperator.EQUAL and equal_term_matrix is not None: - self._time_series[f"{name}_eq"] = TimeSeriesFile( - TimeSeriesFileType.BINDING_CONSTRAINT_EQUAL, - self.config.study_path, - constraint_id=constraint.id.lower(), - time_series=equal_term_matrix, + self._time_series[f"{name}_eq"] = TimeSeries( + equal_term_matrix, + TimeSeriesFile( + TimeSeriesFileType.BINDING_CONSTRAINT_EQUAL, + self.config.study_path, + constraint_id=constraint.id.lower(), + time_series=equal_term_matrix, + ), ) if ( constraint.properties.operator in (BindingConstraintOperator.GREATER, BindingConstraintOperator.BOTH) and greater_term_matrix is not None ): - self._time_series[f"{name}_gt"] = TimeSeriesFile( - TimeSeriesFileType.BINDING_CONSTRAINT_GREATER, - self.config.study_path, - constraint_id=constraint.id.lower(), - time_series=greater_term_matrix, + self._time_series[f"{name}_gt"] = TimeSeries( + greater_term_matrix, + TimeSeriesFile( + TimeSeriesFileType.BINDING_CONSTRAINT_GREATER, + self.config.study_path, + constraint_id=constraint.id.lower(), + time_series=greater_term_matrix, + ), ) return constraint @@ -101,7 +110,7 @@ def binding_constraints(self) -> dict[str, BindingConstraint]: return self._binding_constraints @property - def time_series(self) -> dict[str, TimeSeriesFile]: + def time_series(self) -> dict[str, TimeSeries]: return self._time_series def add_constraint_terms(self, constraint: BindingConstraint, terms: list[ConstraintTerm]) -> list[ConstraintTerm]: diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index acaa2efc..872e48df 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -1288,16 +1288,16 @@ def test_binding_constraint_with_timeseries_stores_ts_file(self, local_study_wit # Then assert local_study_with_hydro._binding_constraints_service.time_series[ f"{constraints['lesser'].id.lower()}_lt" - ].file_path.is_file() + ].local_file.file_path.is_file() assert local_study_with_hydro._binding_constraints_service.time_series[ f"{constraints['equal'].id.lower()}_eq" - ].file_path.is_file() + ].local_file.file_path.is_file() assert local_study_with_hydro._binding_constraints_service.time_series[ f"{constraints['greater'].id.lower()}_gt" - ].file_path.is_file() + ].local_file.file_path.is_file() assert local_study_with_hydro._binding_constraints_service.time_series[ f"{constraints['both'].id.lower()}_lt" - ].file_path.is_file() + ].local_file.file_path.is_file() assert local_study_with_hydro._binding_constraints_service.time_series[ f"{constraints['both'].id.lower()}_gt" - ].file_path.is_file() + ].local_file.file_path.is_file() From 7d584830010137af4e26d7343cef17b787e74176 Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Thu, 26 Sep 2024 15:38:29 +0200 Subject: [PATCH 18/26] Default time series with zeroes are created if none is provided --- .../binding_constraint_local.py | 41 +++++++----- .../services/local_services/test_study.py | 65 +++++++++++++++++++ 2 files changed, 91 insertions(+), 15 deletions(-) diff --git a/src/antares/service/local_services/binding_constraint_local.py b/src/antares/service/local_services/binding_constraint_local.py index 0b30bfb9..6946e8c9 100644 --- a/src/antares/service/local_services/binding_constraint_local.py +++ b/src/antares/service/local_services/binding_constraint_local.py @@ -12,6 +12,7 @@ from typing import Optional, Any +import numpy as np import pandas as pd from antares.config.local_configuration import LocalConfiguration @@ -21,6 +22,7 @@ BindingConstraint, ConstraintMatrixName, BindingConstraintOperator, + BindingConstraintFrequency, ) from antares.service.base_services import BaseBindingConstraintService from antares.tools.ini_tool import IniFile, IniFileTypes @@ -58,40 +60,49 @@ def create_binding_constraint( self._write_binding_constraint_ini() # Add constraint time series - if ( - constraint.properties.operator in (BindingConstraintOperator.LESS, BindingConstraintOperator.BOTH) - and less_term_matrix is not None - ): + time_series_length = ( + (365 * 24 + 24) if constraint.properties.time_step == BindingConstraintFrequency.HOURLY else 366 + ) + + if constraint.properties.operator in (BindingConstraintOperator.LESS, BindingConstraintOperator.BOTH): + time_series = ( + less_term_matrix if less_term_matrix is not None else pd.DataFrame(np.zeros([time_series_length, 1])) + ) self._time_series[f"{name}_lt"] = TimeSeries( - less_term_matrix, + time_series, TimeSeriesFile( TimeSeriesFileType.BINDING_CONSTRAINT_LESS, self.config.study_path, constraint_id=constraint.id.lower(), - time_series=less_term_matrix, + time_series=time_series, ), ) - if constraint.properties.operator == BindingConstraintOperator.EQUAL and equal_term_matrix is not None: + if constraint.properties.operator == BindingConstraintOperator.EQUAL: + time_series = ( + equal_term_matrix if equal_term_matrix is not None else pd.DataFrame(np.zeros([time_series_length, 1])) + ) self._time_series[f"{name}_eq"] = TimeSeries( - equal_term_matrix, + time_series, TimeSeriesFile( TimeSeriesFileType.BINDING_CONSTRAINT_EQUAL, self.config.study_path, constraint_id=constraint.id.lower(), - time_series=equal_term_matrix, + time_series=time_series, ), ) - if ( - constraint.properties.operator in (BindingConstraintOperator.GREATER, BindingConstraintOperator.BOTH) - and greater_term_matrix is not None - ): + if constraint.properties.operator in (BindingConstraintOperator.GREATER, BindingConstraintOperator.BOTH): + time_series = ( + greater_term_matrix + if greater_term_matrix is not None + else pd.DataFrame(np.zeros([time_series_length, 1])) + ) self._time_series[f"{name}_gt"] = TimeSeries( - greater_term_matrix, + time_series, TimeSeriesFile( TimeSeriesFileType.BINDING_CONSTRAINT_GREATER, self.config.study_path, constraint_id=constraint.id.lower(), - time_series=greater_term_matrix, + time_series=time_series, ), ) diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index 872e48df..75722dce 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -49,6 +49,7 @@ from antares.service.local_services.st_storage_local import ShortTermStorageLocalService from antares.service.local_services.thermal_local import ThermalLocalService from antares.tools.ini_tool import IniFileTypes +from antares.tools.time_series_tool import TimeSeriesFileType class TestCreateStudy: @@ -1301,3 +1302,67 @@ def test_binding_constraint_with_timeseries_stores_ts_file(self, local_study_wit assert local_study_with_hydro._binding_constraints_service.time_series[ f"{constraints['both'].id.lower()}_gt" ].local_file.file_path.is_file() + + def test_binding_constraints_have_correct_default_time_series(self, test_constraint, local_study_with_constraint): + # Given + expected_time_series_hourly = pd.DataFrame(np.zeros([365 * 24 + 24, 1])) + expected_time_series_daily_weekly = pd.DataFrame(np.zeros([365 + 1, 1])) + local_study_with_constraint.create_binding_constraint( + name="test greater", + properties=BindingConstraintProperties( + operator=BindingConstraintOperator.GREATER, time_step=BindingConstraintFrequency.WEEKLY + ), + ) + local_study_with_constraint.create_binding_constraint( + name="test equal", + properties=BindingConstraintProperties( + operator=BindingConstraintOperator.EQUAL, time_step=BindingConstraintFrequency.DAILY + ), + ) + local_study_with_constraint.create_binding_constraint( + name="test both", + properties=BindingConstraintProperties( + operator=BindingConstraintOperator.BOTH, time_step=BindingConstraintFrequency.HOURLY + ), + ) + expected_pre_created_ts_file = ( + local_study_with_constraint.service.config.study_path + / TimeSeriesFileType.BINDING_CONSTRAINT_LESS.value.format(constraint_id=test_constraint.id) + ) + + # When + with local_study_with_constraint._binding_constraints_service.time_series[ + f"{test_constraint.id}_lt" + ].local_file.file_path.open("r") as pre_created_file: + actual_time_series_pre_created = pd.read_csv(pre_created_file, header=None) + with local_study_with_constraint._binding_constraints_service.time_series[ + "test greater_gt" + ].local_file.file_path.open("r") as greater_file: + actual_time_series_greater = pd.read_csv(greater_file, header=None) + with local_study_with_constraint._binding_constraints_service.time_series[ + "test equal_eq" + ].local_file.file_path.open("r") as equal_file: + actual_time_series_equal = pd.read_csv(equal_file, header=None) + with local_study_with_constraint._binding_constraints_service.time_series[ + "test both_gt" + ].local_file.file_path.open("r") as both_greater_file: + actual_time_series_both_greater = pd.read_csv(both_greater_file, header=None) + with local_study_with_constraint._binding_constraints_service.time_series[ + "test both_lt" + ].local_file.file_path.open("r") as both_lesser_file: + actual_time_series_both_lesser = pd.read_csv(both_lesser_file, header=None) + + # Then + # Verify that file names are created correctly + assert ( + local_study_with_constraint._binding_constraints_service.time_series[ + f"{test_constraint.id}_lt" + ].local_file.file_path + == expected_pre_created_ts_file + ) + # Verify that default file contents are the correct and expected + assert actual_time_series_pre_created.equals(expected_time_series_hourly) + assert actual_time_series_greater.equals(expected_time_series_daily_weekly) + assert actual_time_series_equal.equals(expected_time_series_daily_weekly) + assert actual_time_series_both_greater.equals(expected_time_series_hourly) + assert actual_time_series_both_lesser.equals(expected_time_series_hourly) From 6d4073caf375cad30b8525f736d01bfc23ebbe49 Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Thu, 26 Sep 2024 16:07:40 +0200 Subject: [PATCH 19/26] Added test to verify custom time series content provided at creation works --- .../services/local_services/test_study.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index 75722dce..27467586 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -1366,3 +1366,30 @@ def test_binding_constraints_have_correct_default_time_series(self, test_constra assert actual_time_series_equal.equals(expected_time_series_daily_weekly) assert actual_time_series_both_greater.equals(expected_time_series_hourly) assert actual_time_series_both_lesser.equals(expected_time_series_hourly) + + def test_submitted_time_series_is_saved(self, local_study_with_constraint): + # Given + expected_time_series = pd.DataFrame(np.ones([3, 1])) + local_study_with_constraint.create_binding_constraint( + name="test time series", + properties=BindingConstraintProperties( + operator=BindingConstraintOperator.GREATER, time_step=BindingConstraintFrequency.HOURLY + ), + greater_term_matrix=expected_time_series, + ) + expected_file_contents = """1.0 +1.0 +1.0 +""" + + # When + with local_study_with_constraint._binding_constraints_service.time_series[ + "test time series_gt" + ].local_file.file_path.open("r") as time_series_file: + actual_time_series = pd.read_csv(time_series_file, header=None) + time_series_file.seek(0) + actual_file_contents = time_series_file.read() + + # Then + assert actual_time_series.equals(expected_time_series) + assert actual_file_contents == expected_file_contents From 1158b012cf8e8c7701e947e2dc2aa74ddd847c53 Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Fri, 27 Sep 2024 09:25:15 +0200 Subject: [PATCH 20/26] Removed unused dependency as per comment in pull request #5 --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 674d91b5..8c6696a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ absl-py~=1.4.0 numpy~=1.26.4 -protobuf~=4.23.3 requests~=2.31.0 pandas~=2.2.2 pandas-stubs~=2.2.2 From 72c8b6ca43606d51228c65c3d1c0e824c93635c5 Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Fri, 27 Sep 2024 10:55:49 +0200 Subject: [PATCH 21/26] Updated properties setter to also update local properties as per PR #5 --- src/antares/model/binding_constraint.py | 21 ++++++++++++------- .../services/local_services/test_study.py | 17 +++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/antares/model/binding_constraint.py b/src/antares/model/binding_constraint.py index ea1aa852..841bd20b 100644 --- a/src/antares/model/binding_constraint.py +++ b/src/antares/model/binding_constraint.py @@ -175,13 +175,9 @@ def __init__( # type: ignore # TODO: Find a way to avoid circular imports self._id = transform_name_to_id(name) self._properties = properties or BindingConstraintProperties() self._terms = {term.id: term for term in terms} if terms else {} - local_property_args = { - "constraint_name": self._name, - "constraint_id": self._id, - "terms": self._terms, - **self._properties.model_dump(mode="json", exclude_none=True), - } - self._local_properties = BindingConstraintPropertiesLocal.model_validate(local_property_args) + self._local_properties = BindingConstraintPropertiesLocal.model_validate( + self._create_local_property_args(self._properties) + ) @property def name(self) -> str: @@ -197,8 +193,19 @@ def properties(self) -> BindingConstraintProperties: @properties.setter def properties(self, new_properties: BindingConstraintProperties) -> None: + self._local_properties = BindingConstraintPropertiesLocal.model_validate( + self._create_local_property_args(new_properties) + ) self._properties = new_properties + def _create_local_property_args(self, properties: BindingConstraintProperties) -> dict[str, str]: + return { + "constraint_name": self._name, + "constraint_id": self._id, + "terms": self._terms, + **properties.model_dump(mode="json", exclude_none=True), + } + @property def local_properties(self) -> BindingConstraintPropertiesLocal: return self._local_properties diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index 27467586..15be72cc 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -29,6 +29,7 @@ BindingConstraintFrequency, BindingConstraintOperator, ConstraintTerm, + BindingConstraintPropertiesLocal, ) from antares.model.commons import FilterOption from antares.model.hydro import Hydro @@ -1393,3 +1394,19 @@ def test_submitted_time_series_is_saved(self, local_study_with_constraint): # Then assert actual_time_series.equals(expected_time_series) assert actual_file_contents == expected_file_contents + + def test_updating_binding_constraint_properties_updates_local(self, local_study_with_constraint, test_constraint): + # Given + new_properties = BindingConstraintProperties(comments="testing update") + local_property_args = { + "constraint_name": test_constraint.name, + "constraint_id": test_constraint.id, + "terms": test_constraint._terms, + **new_properties.model_dump(mode="json", exclude_none=True), + } + + # When + test_constraint.properties = new_properties + + # Then + assert test_constraint.local_properties == BindingConstraintPropertiesLocal.model_validate(local_property_args) From 50d8125013d90aabf1139fef4b9a0b61a8801c6c Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Fri, 27 Sep 2024 12:37:45 +0200 Subject: [PATCH 22/26] Refactored storing the time series to separate functions to reduce complexity when reading the code --- .../binding_constraint_local.py | 76 +++++++++---------- 1 file changed, 34 insertions(+), 42 deletions(-) diff --git a/src/antares/service/local_services/binding_constraint_local.py b/src/antares/service/local_services/binding_constraint_local.py index 6946e8c9..2b7149e8 100644 --- a/src/antares/service/local_services/binding_constraint_local.py +++ b/src/antares/service/local_services/binding_constraint_local.py @@ -60,53 +60,45 @@ def create_binding_constraint( self._write_binding_constraint_ini() # Add constraint time series - time_series_length = ( - (365 * 24 + 24) if constraint.properties.time_step == BindingConstraintFrequency.HOURLY else 366 - ) + self._store_time_series(constraint, less_term_matrix, equal_term_matrix, greater_term_matrix) + + return constraint + def _store_time_series( + self, + constraint: BindingConstraint, + less_term_matrix: Optional[pd.DataFrame], + equal_term_matrix: Optional[pd.DataFrame], + greater_term_matrix: Optional[pd.DataFrame], + ) -> None: + time_series = [] + time_series_ids = [] + file_types = [] + # Lesser or greater can happen together when operator is both if constraint.properties.operator in (BindingConstraintOperator.LESS, BindingConstraintOperator.BOTH): - time_series = ( - less_term_matrix if less_term_matrix is not None else pd.DataFrame(np.zeros([time_series_length, 1])) - ) - self._time_series[f"{name}_lt"] = TimeSeries( - time_series, - TimeSeriesFile( - TimeSeriesFileType.BINDING_CONSTRAINT_LESS, - self.config.study_path, - constraint_id=constraint.id.lower(), - time_series=time_series, - ), - ) - if constraint.properties.operator == BindingConstraintOperator.EQUAL: - time_series = ( - equal_term_matrix if equal_term_matrix is not None else pd.DataFrame(np.zeros([time_series_length, 1])) - ) - self._time_series[f"{name}_eq"] = TimeSeries( - time_series, - TimeSeriesFile( - TimeSeriesFileType.BINDING_CONSTRAINT_EQUAL, - self.config.study_path, - constraint_id=constraint.id.lower(), - time_series=time_series, - ), - ) + time_series += [self._check_if_empty_ts(constraint.properties.time_step, less_term_matrix)] + time_series_ids += [f"{constraint.id.lower()}_lt"] + file_types += [TimeSeriesFileType.BINDING_CONSTRAINT_LESS] if constraint.properties.operator in (BindingConstraintOperator.GREATER, BindingConstraintOperator.BOTH): - time_series = ( - greater_term_matrix - if greater_term_matrix is not None - else pd.DataFrame(np.zeros([time_series_length, 1])) - ) - self._time_series[f"{name}_gt"] = TimeSeries( - time_series, - TimeSeriesFile( - TimeSeriesFileType.BINDING_CONSTRAINT_GREATER, - self.config.study_path, - constraint_id=constraint.id.lower(), - time_series=time_series, - ), + time_series += [self._check_if_empty_ts(constraint.properties.time_step, greater_term_matrix)] + time_series_ids += [f"{constraint.id.lower()}_gt"] + file_types += [TimeSeriesFileType.BINDING_CONSTRAINT_GREATER] + # Equal is always exclusive + if constraint.properties.operator == BindingConstraintOperator.EQUAL: + time_series = [self._check_if_empty_ts(constraint.properties.time_step, equal_term_matrix)] + time_series_ids = [f"{constraint.id.lower()}_eq"] + file_types = [TimeSeriesFileType.BINDING_CONSTRAINT_EQUAL] + + for ts, ts_id, file_type in zip(time_series, time_series_ids, file_types): + self._time_series[ts_id] = TimeSeries( + ts, + TimeSeriesFile(file_type, self.config.study_path, constraint_id=constraint.id.lower(), time_series=ts), ) - return constraint + @staticmethod + def _check_if_empty_ts(time_step: BindingConstraintFrequency, time_series: Optional[pd.DataFrame]) -> pd.DataFrame: + time_series_length = (365 * 24 + 24) if time_step == BindingConstraintFrequency.HOURLY else 366 + return time_series if time_series is not None else pd.DataFrame(np.zeros([time_series_length, 1])) def _write_binding_constraint_ini(self) -> None: binding_constraints_ini_content = { From 62ef247f56895948f27c9b3ca1194c787f319ecd Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Fri, 27 Sep 2024 12:38:16 +0200 Subject: [PATCH 23/26] Fix for mypy --- src/antares/model/binding_constraint.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/antares/model/binding_constraint.py b/src/antares/model/binding_constraint.py index 841bd20b..bfb0ac2c 100644 --- a/src/antares/model/binding_constraint.py +++ b/src/antares/model/binding_constraint.py @@ -198,7 +198,9 @@ def properties(self, new_properties: BindingConstraintProperties) -> None: ) self._properties = new_properties - def _create_local_property_args(self, properties: BindingConstraintProperties) -> dict[str, str]: + def _create_local_property_args( + self, properties: BindingConstraintProperties + ) -> dict[str, Union[str, dict[str, ConstraintTerm]]]: return { "constraint_name": self._name, "constraint_id": self._id, From 052a26a67054e97a96be1553d2058aec5fb354ff Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Fri, 27 Sep 2024 14:08:09 +0200 Subject: [PATCH 24/26] Converted to function and removed `@computed_field` and `@property` to avoid confusion. --- src/antares/model/binding_constraint.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/antares/model/binding_constraint.py b/src/antares/model/binding_constraint.py index bfb0ac2c..b5dbc66c 100644 --- a/src/antares/model/binding_constraint.py +++ b/src/antares/model/binding_constraint.py @@ -44,8 +44,6 @@ class TermOperators(BaseModel): weight: Optional[float] = None offset: Optional[int] = None - @computed_field # type: ignore[misc] - @property def weight_offset(self) -> str: if self.offset is not None: # Rounded the weight to 6 decimals to be in line with other floats in the ini files @@ -149,7 +147,7 @@ def list_ini_fields(self) -> dict[str, str]: "filter-year-by-year": self.filter_year_by_year, "filter-synthesis": self.filter_synthesis, "group": self.group, - } | {term_id: term.weight_offset for term_id, term in self.terms.items()} + } | {term_id: term.weight_offset() for term_id, term in self.terms.items()} return {key: value for key, value in ini_dict.items() if value not in [None, ""]} def yield_binding_constraint_properties(self) -> BindingConstraintProperties: From a5ad4a37c1929b06a671bd1ffca1ce9ac08434d1 Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Fri, 27 Sep 2024 14:21:05 +0200 Subject: [PATCH 25/26] Removed unnecessary `@computed_field`s. --- src/antares/model/area.py | 2 -- src/antares/model/binding_constraint.py | 3 +-- src/antares/model/hydro.py | 3 +-- src/antares/model/link.py | 4 +--- src/antares/model/renewable.py | 2 -- src/antares/model/st_storage.py | 3 +-- src/antares/model/thermal.py | 2 -- 7 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/antares/model/area.py b/src/antares/model/area.py index f1652e0a..6ab1e4db 100644 --- a/src/antares/model/area.py +++ b/src/antares/model/area.py @@ -89,7 +89,6 @@ def config_alias_generator(field_name: str) -> str: class AreaPropertiesLocal(DefaultAreaProperties, alias_generator=config_alias_generator): - @computed_field # type: ignore[misc] @property def nodal_optimization(self) -> Mapping[str, str]: return { @@ -102,7 +101,6 @@ def nodal_optimization(self) -> Mapping[str, str]: "average-spilled-energy-cost": f"{self.energy_cost_spilled:.6f}", } - @computed_field # type: ignore[misc] @property def filtering(self) -> Mapping[str, str]: return { diff --git a/src/antares/model/binding_constraint.py b/src/antares/model/binding_constraint.py index b5dbc66c..47f30bd0 100644 --- a/src/antares/model/binding_constraint.py +++ b/src/antares/model/binding_constraint.py @@ -14,7 +14,7 @@ from typing import Optional, Union, List, Any, Dict import pandas as pd -from pydantic import BaseModel, Field, model_validator, computed_field +from pydantic import BaseModel, Field, model_validator from pydantic.alias_generators import to_camel from antares.tools.all_optional_meta import all_optional_model @@ -134,7 +134,6 @@ class BindingConstraintPropertiesLocal(DefaultBindingConstraintProperties): constraint_id: str terms: dict[str, ConstraintTerm] = {} - @computed_field # type: ignore[misc] @property def list_ini_fields(self) -> dict[str, str]: ini_dict = { diff --git a/src/antares/model/hydro.py b/src/antares/model/hydro.py index 2a98add8..811c2e8f 100644 --- a/src/antares/model/hydro.py +++ b/src/antares/model/hydro.py @@ -14,7 +14,7 @@ from typing import Optional, Dict import pandas as pd -from pydantic import BaseModel, computed_field +from pydantic import BaseModel from pydantic.alias_generators import to_camel from antares.tools.all_optional_meta import all_optional_model @@ -64,7 +64,6 @@ class HydroProperties(DefaultHydroProperties): class HydroPropertiesLocal(DefaultHydroProperties): area_id: str - @computed_field # type: ignore[misc] @property def hydro_ini_fields(self) -> dict[str, dict[str, str]]: return { diff --git a/src/antares/model/link.py b/src/antares/model/link.py index 43ad2938..95f0f983 100644 --- a/src/antares/model/link.py +++ b/src/antares/model/link.py @@ -13,7 +13,7 @@ from enum import Enum from typing import Optional, Set, Mapping -from pydantic import BaseModel, computed_field +from pydantic import BaseModel from antares.model.area import Area from antares.model.commons import FilterOption, sort_filter_values @@ -78,7 +78,6 @@ class LinkProperties(DefaultLinkProperties): class LinkPropertiesLocal(DefaultLinkProperties): - @computed_field # type: ignore[misc] @property def ini_fields(self) -> Mapping[str, str]: return { @@ -117,7 +116,6 @@ class LinkUi(DefaultLinkUi): class LinkUiLocal(DefaultLinkUi): - @computed_field # type: ignore[misc] @property def ini_fields(self) -> Mapping[str, str]: # todo: can be replaced with alias i believe diff --git a/src/antares/model/renewable.py b/src/antares/model/renewable.py index 54c218bb..d4cd3ebc 100644 --- a/src/antares/model/renewable.py +++ b/src/antares/model/renewable.py @@ -14,7 +14,6 @@ from typing import Optional import pandas as pd -from pydantic import computed_field from antares.model.cluster import ClusterProperties from antares.tools.all_optional_meta import all_optional_model @@ -72,7 +71,6 @@ class RenewableClusterProperties(DefaultRenewableClusterProperties): class RenewableClusterPropertiesLocal(DefaultRenewableClusterProperties): renewable_name: str - @computed_field # type: ignore[misc] @property def ini_fields(self) -> dict[str, dict[str, str]]: return { diff --git a/src/antares/model/st_storage.py b/src/antares/model/st_storage.py index 058359b0..25f6c2f9 100644 --- a/src/antares/model/st_storage.py +++ b/src/antares/model/st_storage.py @@ -14,7 +14,7 @@ from typing import Optional import pandas as pd -from pydantic import BaseModel, computed_field +from pydantic import BaseModel from pydantic.alias_generators import to_camel from antares.tools.all_optional_meta import all_optional_model @@ -68,7 +68,6 @@ class STStorageProperties(DefaultSTStorageProperties): class STStoragePropertiesLocal(DefaultSTStorageProperties): st_storage_name: str - @computed_field # type: ignore[misc] @property def list_ini_fields(self) -> dict[str, dict[str, str]]: return { diff --git a/src/antares/model/thermal.py b/src/antares/model/thermal.py index 91588163..ed0cba52 100644 --- a/src/antares/model/thermal.py +++ b/src/antares/model/thermal.py @@ -14,7 +14,6 @@ from typing import Optional import pandas as pd -from pydantic import computed_field from antares.model.cluster import ClusterProperties from antares.tools.all_optional_meta import all_optional_model @@ -115,7 +114,6 @@ class ThermalClusterProperties(DefaultThermalProperties): class ThermalClusterPropertiesLocal(DefaultThermalProperties): thermal_name: str - @computed_field # type: ignore[misc] @property def list_ini_fields(self) -> dict[str, dict[str, str]]: return { From c52a32220a4c2498d8aee04abff8c014f77e906d Mon Sep 17 00:00:00 2001 From: Sigurd Borge Date: Fri, 27 Sep 2024 16:40:09 +0200 Subject: [PATCH 26/26] Changed binding_constraints in the binding_constraint_services from property to attribute and removed unused binding_constraint attribute from `Study` as mentioned in discussion in PR #5 --- src/antares/model/study.py | 1 - .../service/api_services/binding_constraint_api.py | 8 ++------ src/antares/service/base_services.py | 7 ++----- .../service/local_services/binding_constraint_local.py | 10 +++------- 4 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/antares/model/study.py b/src/antares/model/study.py index 6604031f..e6a7a2b2 100644 --- a/src/antares/model/study.py +++ b/src/antares/model/study.py @@ -206,7 +206,6 @@ def __init__( self._settings = settings or StudySettings() self._areas: Dict[str, Area] = dict() self._links: Dict[str, Link] = dict() - self._binding_constraints: Dict[str, BindingConstraint] = dict() for argument in kwargs: if argument == "ini_files": self._ini_files: dict[str, IniFile] = kwargs[argument] or dict() diff --git a/src/antares/service/api_services/binding_constraint_api.py b/src/antares/service/api_services/binding_constraint_api.py index 6d1c4243..a2abac9b 100644 --- a/src/antares/service/api_services/binding_constraint_api.py +++ b/src/antares/service/api_services/binding_constraint_api.py @@ -43,7 +43,7 @@ def __init__(self, config: APIconf, study_id: str) -> None: self.study_id = study_id self._wrapper = RequestWrapper(self.api_config.set_up_api_conf()) self._base_url = f"{self.api_config.get_host()}/api/v1" - self._binding_constraints: dict[str, BindingConstraint] = {} + self.binding_constraints = {} def create_binding_constraint( self, @@ -105,14 +105,10 @@ def create_binding_constraint( raise BindingConstraintCreationError(name, e.message) from e constraint = BindingConstraint(name, self, bc_properties, bc_terms) - self._binding_constraints[constraint.id] = constraint + self.binding_constraints[constraint.id] = constraint return constraint - @property - def binding_constraints(self) -> dict[str, BindingConstraint]: - return self._binding_constraints - def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> None: url = f"{self._base_url}/studies/{self.study_id}/bindingconstraints/{constraint_id}/term/{term_id}" try: diff --git a/src/antares/service/base_services.py b/src/antares/service/base_services.py index 291908de..4d83f674 100644 --- a/src/antares/service/base_services.py +++ b/src/antares/service/base_services.py @@ -355,6 +355,8 @@ def get_thermal_matrix(self, thermal_cluster: ThermalCluster, ts_name: ThermalCl class BaseBindingConstraintService(ABC): + binding_constraints: dict[str, BindingConstraint] + @abstractmethod def create_binding_constraint( self, @@ -391,11 +393,6 @@ def add_constraint_terms(self, constraint: BindingConstraint, terms: List[Constr """ pass - @property - @abstractmethod - def binding_constraints(self) -> dict[str, BindingConstraint]: - pass - @abstractmethod def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> None: """ diff --git a/src/antares/service/local_services/binding_constraint_local.py b/src/antares/service/local_services/binding_constraint_local.py index 2b7149e8..c6b67aec 100644 --- a/src/antares/service/local_services/binding_constraint_local.py +++ b/src/antares/service/local_services/binding_constraint_local.py @@ -34,9 +34,9 @@ def __init__(self, config: LocalConfiguration, study_name: str, **kwargs: Any) - super().__init__(**kwargs) self.config = config self.study_name = study_name - self._binding_constraints: dict[str, BindingConstraint] = {} self.ini_file = IniFile(self.config.study_path, IniFileTypes.BINDING_CONSTRAINTS_INI) self._time_series: dict[str, TimeSeries] = {} + self.binding_constraints = {} def create_binding_constraint( self, @@ -56,7 +56,7 @@ def create_binding_constraint( constraint.properties = constraint.local_properties.yield_binding_constraint_properties() # Add binding constraints - self._binding_constraints[constraint.id] = constraint + self.binding_constraints[constraint.id] = constraint self._write_binding_constraint_ini() # Add constraint time series @@ -103,15 +103,11 @@ def _check_if_empty_ts(time_step: BindingConstraintFrequency, time_series: Optio def _write_binding_constraint_ini(self) -> None: binding_constraints_ini_content = { idx: idx_constraint.local_properties.list_ini_fields - for idx, idx_constraint in enumerate(self._binding_constraints.values()) + for idx, idx_constraint in enumerate(self.binding_constraints.values()) } self.ini_file.ini_dict = binding_constraints_ini_content self.ini_file.write_ini_file() - @property - def binding_constraints(self) -> dict[str, BindingConstraint]: - return self._binding_constraints - @property def time_series(self) -> dict[str, TimeSeries]: return self._time_series