Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/add local binding constraints #5

Merged
merged 26 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
33eb7c7
Added create local binding constraint
Sigurd-Borge Sep 10, 2024
9505419
Added properties object to binding constraints
Sigurd-Borge Sep 10, 2024
6960a30
Added default properties to binding constraints
Sigurd-Borge Sep 11, 2024
3aecdea
Added bindingconstraints.ini file
Sigurd-Borge Sep 11, 2024
007ca33
Added bindingconstraints.ini file is managed from the Study and has t…
Sigurd-Borge Sep 12, 2024
999c29f
Can add constraint terms
Sigurd-Borge Sep 13, 2024
2614de9
Refactored bindingconstraints.ini to be handled by the bindingconstra…
Sigurd-Borge Sep 16, 2024
7f54ae6
Added default value 0 for constraint weight
Sigurd-Borge Sep 16, 2024
f319ddb
Added the weight and offset combination in the ini file, as well as d…
Sigurd-Borge Sep 16, 2024
cc3e16e
Upped numpy to 1.26.4, currently last version before 2.0.0.
Sigurd-Borge Sep 17, 2024
809002e
Fixed pydantic serializer warnings
Sigurd-Borge Sep 18, 2024
c906faa
Corrected number of decimal digits
Sigurd-Borge Sep 18, 2024
5bf47a9
Refactored binding constraints to be tracked in the service instead o…
Sigurd-Borge Sep 18, 2024
784e45c
Rewrite binding constraint to use the new default values and decorator
Sigurd-Borge Sep 23, 2024
d163cee
Updated TimeSeriesFile to require named arguments after file type and…
Sigurd-Borge Sep 25, 2024
1115467
Binding constraint time series can be created
Sigurd-Borge Sep 25, 2024
b63d99f
Refactored binding constraint time series to use the correct object type
Sigurd-Borge Sep 26, 2024
7d58483
Default time series with zeroes are created if none is provided
Sigurd-Borge Sep 26, 2024
6d4073c
Added test to verify custom time series content provided at creation …
Sigurd-Borge Sep 26, 2024
1158b01
Removed unused dependency as per comment in pull request #5
Sigurd-Borge Sep 27, 2024
72c8b6c
Updated properties setter to also update local properties as per PR #5
Sigurd-Borge Sep 27, 2024
50d8125
Refactored storing the time series to separate functions to reduce co…
Sigurd-Borge Sep 27, 2024
62ef247
Fix for mypy
Sigurd-Borge Sep 27, 2024
052a26a
Converted to function and removed `@computed_field` and `@property` t…
Sigurd-Borge Sep 27, 2024
a5ad4a3
Removed unnecessary `@computed_field`s.
Sigurd-Borge Sep 27, 2024
c52a322
Changed binding_constraints in the binding_constraint_services from p…
Sigurd-Borge Sep 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
MartinBelthle marked this conversation as resolved.
Show resolved Hide resolved
tox-uv~=1.11.3
12 changes: 6 additions & 6 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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
Sigurd-Borge marked this conversation as resolved.
Show resolved Hide resolved
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
12 changes: 6 additions & 6 deletions src/antares/model/area.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
105 changes: 94 additions & 11 deletions src/antares/model/binding_constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@
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.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,16 @@ class TermOperators(BaseModel):
weight: Optional[float] = None
offset: Optional[int] = None

@computed_field # type: ignore[misc]
@property
def weight_offset(self) -> str:
Sigurd-Borge marked this conversation as resolved.
Show resolved Hide resolved
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 +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:
MartinBelthle marked this conversation as resolved.
Show resolved Hide resolved
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] = {}

@computed_field # type: ignore[misc]
MartinBelthle marked this conversation as resolved.
Show resolved Hide resolved
@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 +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 {}
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:
Expand All @@ -120,6 +195,14 @@ def id(self) -> str:
def properties(self) -> BindingConstraintProperties:
return self._properties

@properties.setter
Sigurd-Borge marked this conversation as resolved.
Show resolved Hide resolved
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

Expand Down
8 changes: 3 additions & 5 deletions src/antares/model/study.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
MartinBelthle marked this conversation as resolved.
Show resolved Hide resolved
return constraint

def update_settings(self, settings: StudySettings) -> None:
new_settings = self._study_service.update_study_settings(settings)
Expand All @@ -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)
10 changes: 9 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: dict[str, BindingConstraint] = {}

def create_binding_constraint(
self,
Expand Down Expand Up @@ -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}"
Expand Down
5 changes: 5 additions & 0 deletions src/antares/service/base_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Sigurd-Borge marked this conversation as resolved.
Show resolved Hide resolved

@abstractmethod
def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> None:
"""
Expand Down
20 changes: 15 additions & 5 deletions src/antares/service/local_services/area_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand Down
Loading