Skip to content

Commit

Permalink
Feat/add local binding constraints (#5)
Browse files Browse the repository at this point in the history
* Added create local binding constraint

* Added properties object to binding constraints

* Added default properties to binding constraints

* Added bindingconstraints.ini file

* Added bindingconstraints.ini file is managed from the Study and has the correct contents

* Can add constraint terms

* 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

* Added default value 0 for constraint weight

* Added the weight and offset combination in the ini file, as well as disabled interpolation in the ini tool

* 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

* Fixed pydantic serializer warnings

* Corrected number of decimal digits

* Refactored binding constraints to be tracked in the service instead of the study to allow correct handling of ini file

* Rewrite binding constraint to use the new default values and decorator

* Updated TimeSeriesFile to require named arguments after file type and path, updated use to match

* Binding constraint time series can be created

* Refactored binding constraint time series to use the correct object type

* Default time series with zeroes are created if none is provided

* Added test to verify custom time series content provided at creation works

* Removed unused dependency as per comment in pull request #5

* Updated properties setter to also update local properties as per PR #5

* Refactored storing the time series to separate functions to reduce complexity when reading the code

* Fix for mypy

* Converted to function and removed `@computed_field` and `@property` to avoid confusion.

* Removed unnecessary `@computed_field`s.

* 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
  • Loading branch information
Sigurd-Borge authored Sep 27, 2024
1 parent d1d31d3 commit 3643b47
Show file tree
Hide file tree
Showing 23 changed files with 625 additions and 76 deletions.
4 changes: 3 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -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
11 changes: 5 additions & 6 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
absl-py==1.4.0
numpy==1.24.4
protobuf==4.23.3
absl-py~=1.4.0
numpy~=1.26.4
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
14 changes: 6 additions & 8 deletions src/antares/model/area.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -294,12 +292,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

Expand Down Expand Up @@ -348,22 +346,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

Expand Down
109 changes: 99 additions & 10 deletions src/antares/model/binding_constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@
from pydantic import BaseModel, Field, model_validator
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

DEFAULT_GROUP = "default"


class BindingConstraintFrequency(EnumIgnoreCase):
HOURLY = "hourly"
Expand All @@ -45,6 +44,14 @@ class TermOperators(BaseModel):
weight: Optional[float] = None
offset: Optional[int] = None

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
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


class LinkData(BaseModel):
"""
Expand Down Expand Up @@ -84,14 +91,72 @@ 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(DefaultBindingConstraintProperties):
"""
Used to create the entries for the bindingconstraints.ini file
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
"""

constraint_name: str
constraint_id: str
terms: dict[str, ConstraintTerm] = {}

@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 not in [None, ""]}

def yield_binding_constraint_properties(self) -> BindingConstraintProperties:
excludes = {
"constraint_name",
"constraint_id",
"terms",
"list_ini_fields",
}
return BindingConstraintProperties.model_validate(self.model_dump(mode="json", exclude=excludes))


class BindingConstraint:
Expand All @@ -107,6 +172,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.model_validate(
self._create_local_property_args(self._properties)
)

@property
def name(self) -> str:
Expand All @@ -120,6 +188,27 @@ def id(self) -> str:
def properties(self) -> BindingConstraintProperties:
return self._properties

@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, Union[str, dict[str, ConstraintTerm]]]:
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

def get_terms(self) -> Dict[str, ConstraintTerm]:
return self._terms

Expand Down
3 changes: 1 addition & 2 deletions src/antares/model/hydro.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 1 addition & 3 deletions src/antares/model/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -78,7 +78,6 @@ class LinkProperties(DefaultLinkProperties):


class LinkPropertiesLocal(DefaultLinkProperties):
@computed_field # type: ignore[misc]
@property
def ini_fields(self) -> Mapping[str, str]:
return {
Expand Down Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions src/antares/model/renewable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 1 addition & 2 deletions src/antares/model/st_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
9 changes: 3 additions & 6 deletions src/antares/model/study.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -225,7 +224,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
Expand Down Expand Up @@ -265,11 +264,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)
Expand All @@ -278,7 +275,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)
2 changes: 0 additions & 2 deletions src/antares/model/thermal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 5 additions & 1 deletion src/antares/service/api_services/binding_constraint_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}

def create_binding_constraint(
self,
Expand Down Expand Up @@ -103,7 +104,10 @@ 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

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}"
Expand Down
2 changes: 2 additions & 0 deletions src/antares/service/base_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 3643b47

Please sign in to comment.