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 7fc3d366..15254e03 100644 --- a/src/antares/service/local_services/binding_constraint_local.py +++ b/src/antares/service/local_services/binding_constraint_local.py @@ -8,8 +8,6 @@ ConstraintTerm, BindingConstraint, ConstraintMatrixName, - BindingConstraintFrequency, - BindingConstraintOperator, ) from antares.service.base_services import BaseBindingConstraintService @@ -29,19 +27,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 7d5f59c6..ef4fd2e5 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -9,7 +9,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 ( @@ -28,6 +33,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: @@ -1089,3 +1095,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