Skip to content

Commit

Permalink
refactor(areas): create user classes for Properties (#85)
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinBelthle authored Feb 14, 2025
1 parent 2bfdaef commit 809a57b
Show file tree
Hide file tree
Showing 17 changed files with 262 additions and 165 deletions.
89 changes: 30 additions & 59 deletions src/antares/craft/model/area.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@
//TO_DO to be completed as implementation progress
"""

from dataclasses import dataclass, field
from types import MappingProxyType
from typing import Any, Mapping, Optional
from typing import Any, Optional

import pandas as pd

from antares.craft.model.commons import FilterOption, sort_filter_values
from antares.craft.model.commons import FilterOption
from antares.craft.model.hydro import Hydro, HydroProperties
from antares.craft.model.renewable import RenewableCluster, RenewableClusterProperties
from antares.craft.model.st_storage import STStorage, STStorageProperties
Expand All @@ -33,8 +34,6 @@
BaseShortTermStorageService,
BaseThermalService,
)
from antares.craft.tools.alias_generators import to_space
from antares.craft.tools.all_optional_meta import all_optional_model
from antares.craft.tools.contents_tool import EnumIgnoreCase, transform_name_to_id
from pydantic import BaseModel, computed_field
from pydantic.alias_generators import to_camel
Expand All @@ -52,72 +51,44 @@ class AdequacyPatchMode(EnumIgnoreCase):
VIRTUAL = "virtual"


class DefaultAreaProperties(BaseModel, extra="forbid", populate_by_name=True):
"""
DTO for updating area properties
"""
@dataclass
class AreaPropertiesUpdate:
energy_cost_unsupplied: Optional[float] = None
energy_cost_spilled: Optional[float] = None
non_dispatch_power: Optional[bool] = None
dispatch_hydro_power: Optional[bool] = None
other_dispatch_power: Optional[bool] = None
filter_synthesis: Optional[set[FilterOption]] = None
filter_by_year: Optional[set[FilterOption]] = None
adequacy_patch_mode: Optional[AdequacyPatchMode] = None
spread_unsupplied_energy_cost: Optional[float] = None
spread_spilled_energy_cost: Optional[float] = None

energy_cost_unsupplied: float = 0.0
energy_cost_spilled: float = 0.0
non_dispatch_power: bool = True
dispatch_hydro_power: bool = True
other_dispatch_power: bool = True
filter_synthesis: set[FilterOption] = {
FilterOption.HOURLY,
FilterOption.DAILY,
FilterOption.WEEKLY,
FilterOption.MONTHLY,
FilterOption.ANNUAL,
}
filter_by_year: set[FilterOption] = {

def default_filtering() -> set[FilterOption]:
return {
FilterOption.HOURLY,
FilterOption.DAILY,
FilterOption.WEEKLY,
FilterOption.MONTHLY,
FilterOption.ANNUAL,
}
# version 830


@dataclass
class AreaProperties:
energy_cost_unsupplied: float = 0.0
energy_cost_spilled: float = 0.0
non_dispatch_power: bool = True
dispatch_hydro_power: bool = True
other_dispatch_power: bool = True
filter_synthesis: set[FilterOption] = field(default_factory=default_filtering)
filter_by_year: set[FilterOption] = field(default_factory=default_filtering)
adequacy_patch_mode: AdequacyPatchMode = AdequacyPatchMode.OUTSIDE
spread_unsupplied_energy_cost: float = 0.0
spread_spilled_energy_cost: float = 0.0


@all_optional_model
class AreaProperties(DefaultAreaProperties, alias_generator=to_camel):
pass


class AreaPropertiesLocal(DefaultAreaProperties, alias_generator=to_space):
@property
def nodal_optimization(self) -> Mapping[str, str]:
return {
"non-dispatchable-power": f"{self.non_dispatch_power}".lower(),
"dispatchable-hydro-power": f"{self.dispatch_hydro_power}".lower(),
"other-dispatchable-power": f"{self.other_dispatch_power}".lower(),
"spread-unsupplied-energy-cost": f"{self.spread_unsupplied_energy_cost:.6f}",
"spread-spilled-energy-cost": f"{self.spread_spilled_energy_cost:.6f}",
}

@property
def filtering(self) -> Mapping[str, str]:
return {
"filter-synthesis": ", ".join(filter_value for filter_value in sort_filter_values(self.filter_synthesis)),
"filter-year-by-year": ", ".join(filter_value for filter_value in sort_filter_values(self.filter_by_year)),
}

def adequacy_patch(self) -> dict[str, dict[str, str]]:
return {"adequacy-patch": {"adequacy-patch-mode": self.adequacy_patch_mode.value}}

def yield_local_dict(self) -> dict[str, Mapping[str, str]]:
args = {"nodal optimization": self.nodal_optimization}
args.update({"filtering": self.filtering})
return args

def yield_area_properties(self) -> AreaProperties:
excludes = {"filtering", "nodal_optimization"}
return AreaProperties.model_validate(self.model_dump(mode="json", exclude=excludes))


class AreaUi(BaseModel, extra="forbid", populate_by_name=True, alias_generator=to_camel):
"""
DTO for updating area UI
Expand Down Expand Up @@ -323,7 +294,7 @@ def delete_st_storages(self, storages: list[STStorage]) -> None:
def delete_st_storage(self, storage: STStorage) -> None:
self.delete_st_storages([storage])

def update_properties(self, properties: AreaProperties) -> None:
def update_properties(self, properties: AreaPropertiesUpdate) -> None:
new_properties = self._area_service.update_area_properties(self.id, properties)
self._properties = new_properties

Expand Down
2 changes: 1 addition & 1 deletion src/antares/craft/service/api_services/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
from antares.craft.api_conf.request_wrapper import RequestWrapper
from antares.craft.exceptions.exceptions import APIError, StudyCreationError, StudyImportError, StudyMoveError
from antares.craft.model.study import Study
from antares.craft.service.api_services.area_api import AreaApiService
from antares.craft.service.api_services.link_api import LinkApiService
from antares.craft.service.api_services.services.area import AreaApiService
from antares.craft.service.api_services.services.binding_constraint import BindingConstraintApiService
from antares.craft.service.api_services.services.hydro import HydroApiService
from antares.craft.service.api_services.services.output import OutputApiService
Expand Down
53 changes: 53 additions & 0 deletions src/antares/craft/service/api_services/models/area.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Copyright (c) 2024, RTE (https://www.rte-france.com)
#
# See AUTHORS.txt
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# SPDX-License-Identifier: MPL-2.0
#
# This file is part of the Antares project.
from dataclasses import asdict
from typing import Union

from antares.craft.model.area import AdequacyPatchMode, AreaProperties, AreaPropertiesUpdate
from antares.craft.model.commons import FilterOption
from antares.craft.service.api_services.models.base_model import APIBaseModel
from antares.craft.tools.all_optional_meta import all_optional_model

AreaPropertiesType = Union[AreaProperties, AreaPropertiesUpdate]


@all_optional_model
class AreaPropertiesAPI(APIBaseModel):
energy_cost_unsupplied: float
energy_cost_spilled: float
non_dispatch_power: bool
dispatch_hydro_power: bool
other_dispatch_power: bool
filter_synthesis: set[FilterOption]
filter_by_year: set[FilterOption]
adequacy_patch_mode: AdequacyPatchMode
spread_unsupplied_energy_cost: float
spread_spilled_energy_cost: float

@staticmethod
def from_user_model(user_class: AreaPropertiesType) -> "AreaPropertiesAPI":
user_dict = asdict(user_class)
return AreaPropertiesAPI.model_validate(user_dict)

def to_user_model(self) -> AreaProperties:
return AreaProperties(
energy_cost_unsupplied=self.energy_cost_spilled,
energy_cost_spilled=self.energy_cost_spilled,
non_dispatch_power=self.non_dispatch_power,
dispatch_hydro_power=self.dispatch_hydro_power,
other_dispatch_power=self.other_dispatch_power,
filter_synthesis=self.filter_synthesis,
filter_by_year=self.filter_by_year,
adequacy_patch_mode=self.adequacy_patch_mode,
spread_unsupplied_energy_cost=self.spread_unsupplied_energy_cost,
spread_spilled_energy_cost=self.spread_spilled_energy_cost,
)
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@
ThermalCreationError,
ThermalDeletionError,
)
from antares.craft.model.area import Area, AreaProperties, AreaUi
from antares.craft.model.area import Area, AreaProperties, AreaPropertiesUpdate, AreaUi
from antares.craft.model.hydro import Hydro
from antares.craft.model.renewable import RenewableCluster, RenewableClusterProperties
from antares.craft.model.st_storage import STStorage, STStorageProperties
from antares.craft.model.thermal import ThermalCluster, ThermalClusterProperties
from antares.craft.service.api_services.models.area import AreaPropertiesAPI
from antares.craft.service.api_services.models.renewable import RenewableClusterPropertiesAPI
from antares.craft.service.api_services.models.st_storage import STStoragePropertiesAPI
from antares.craft.service.api_services.models.thermal import ThermalClusterPropertiesAPI
Expand Down Expand Up @@ -99,7 +100,10 @@ def create_area(

if properties:
url = f"{base_area_url}/{area_id}/properties/form"
body = properties.model_dump(mode="json", exclude_none=True)
api_model = AreaPropertiesAPI.from_user_model(properties)
# todo: change this exclude when AntaresWeb will work
exclude = {"spread_unsupplied_energy_cost", "spread_spilled_energy_cost"}
body = api_model.model_dump(mode="json", by_alias=True, exclude_none=True, exclude=exclude)
if body:
self._wrapper.put(url, json=body)
if ui:
Expand All @@ -122,7 +126,8 @@ def create_area(

url = f"{base_area_url}/{area_id}/properties/form"
response = self._wrapper.get(url)
area_properties = AreaProperties.model_validate(response.json())
api_properties = AreaPropertiesAPI.model_validate(response.json())
area_properties = api_properties.to_user_model()

# TODO: Ask AntaresWeb to do the same endpoint for only one area
url = f"{base_area_url}?type=AREA&ui=true"
Expand Down Expand Up @@ -354,14 +359,18 @@ def create_misc_gen(self, area_id: str, series: pd.DataFrame) -> None:
raise MatrixUploadError(area_id, "misc-gen", e.message) from e

@override
def update_area_properties(self, area_id: str, properties: AreaProperties) -> AreaProperties:
def update_area_properties(self, area_id: str, properties: AreaPropertiesUpdate) -> AreaProperties:
url = f"{self._base_url}/studies/{self.study_id}/areas/{area_id}/properties/form"
try:
body = properties.model_dump(mode="json", exclude_none=True)
api_model = AreaPropertiesAPI.from_user_model(properties)
# todo: change this exclude when AntaresWeb will work
exclude = {"spread_unsupplied_energy_cost", "spread_spilled_energy_cost"}
body = api_model.model_dump(mode="json", by_alias=True, exclude_none=True, exclude=exclude)

self._wrapper.put(url, json=body)
response = self._wrapper.get(url)
area_properties = AreaProperties.model_validate(response.json())
api_properties = AreaPropertiesAPI.model_validate(response.json())
area_properties = api_properties.to_user_model()

except APIError as e:
raise AreaPropertiesUpdateError(area_id, e.message) from e
Expand Down
4 changes: 2 additions & 2 deletions src/antares/craft/service/base_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from antares.craft.model.simulation import AntaresSimulationParameters, Job

if TYPE_CHECKING:
from antares.craft.model.area import Area, AreaProperties, AreaUi
from antares.craft.model.area import Area, AreaProperties, AreaPropertiesUpdate, AreaUi
from antares.craft.model.binding_constraint import (
BindingConstraint,
BindingConstraintProperties,
Expand Down Expand Up @@ -179,7 +179,7 @@ def create_misc_gen(self, area_id: str, series: pd.DataFrame) -> None:
pass

@abstractmethod
def update_area_properties(self, area_id: str, properties: "AreaProperties") -> "AreaProperties":
def update_area_properties(self, area_id: str, properties: "AreaPropertiesUpdate") -> "AreaProperties":
"""
Args:
area_id: concerned area
Expand Down
2 changes: 1 addition & 1 deletion src/antares/craft/service/local_services/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
from antares.craft.service.base_services import (
StudyServices,
)
from antares.craft.service.local_services.area_local import AreaLocalService
from antares.craft.service.local_services.link_local import LinkLocalService
from antares.craft.service.local_services.services.area import AreaLocalService
from antares.craft.service.local_services.services.binding_constraint import BindingConstraintLocalService
from antares.craft.service.local_services.services.hydro import HydroLocalService
from antares.craft.service.local_services.services.output import OutputLocalService
Expand Down
100 changes: 100 additions & 0 deletions src/antares/craft/service/local_services/models/area.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Copyright (c) 2024, RTE (https://www.rte-france.com)
#
# See AUTHORS.txt
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# SPDX-License-Identifier: MPL-2.0
#
# This file is part of the Antares project.

from typing import Any, Union

from antares.craft.model.area import AdequacyPatchMode, AreaProperties, AreaPropertiesUpdate, default_filtering
from antares.craft.model.commons import FilterOption
from antares.craft.service.local_services.models.base_model import LocalBaseModel
from antares.craft.tools.alias_generators import to_kebab
from pydantic import Field, field_validator

AreaPropertiesType = Union[AreaProperties, AreaPropertiesUpdate]


class OptimizationPropertiesLocal(LocalBaseModel, alias_generator=to_kebab):
non_dispatchable_power: bool = True
dispatchable_hydro_power: bool = True
other_dispatchable_power: bool = True
spread_unsupplied_energy_cost: float = 0.0
spread_spilled_energy_cost: float = 0.0


class FilteringPropertiesLocal(LocalBaseModel, alias_generator=to_kebab):
filter_synthesis: set[FilterOption] = Field(default_factory=default_filtering)
filter_year_by_year: set[FilterOption] = Field(default_factory=default_filtering)

@field_validator("filter_synthesis", "filter_year_by_year", mode="before")
def validate_accuracy_on_correlation(cls, v: Any) -> set[str]:
if isinstance(v, (list, set)):
return set(v)
if isinstance(v, str):
if v[0] == "[":
v = v[1:-1]
return set(v.replace(" ", "").split(","))
raise ValueError(f"Value {v} not supported for filtering")


class AdequacyPatchPropertiesLocal(LocalBaseModel, alias_generator=to_kebab):
adequacy_patch_mode: AdequacyPatchMode = AdequacyPatchMode.OUTSIDE


class AreaPropertiesLocal(LocalBaseModel):
nodal_optimization: OptimizationPropertiesLocal = Field(alias="nodal optimization")
filtering: FilteringPropertiesLocal
adequacy_patch: AdequacyPatchPropertiesLocal = Field(alias="adequacy-patch")
energy_cost_unsupplied: float = 0.0
energy_cost_spilled: float = 0.0

@staticmethod
def from_user_model(user_class: AreaPropertiesType) -> "AreaPropertiesLocal":
args = {
"adequacy_patch": {"adequacy_patch_mode": user_class.adequacy_patch_mode},
"filtering": {
"filter_synthesis": user_class.filter_synthesis,
"filter_year_by_year": user_class.filter_by_year,
},
"nodal_optimization": {
"non_dispatchable_power": user_class.non_dispatch_power,
"dispatchable_hydro_power": user_class.dispatch_hydro_power,
"other_dispatchable_power": user_class.other_dispatch_power,
"spread_unsupplied_energy_cost": user_class.spread_unsupplied_energy_cost,
"spread_spilled_energy_cost": user_class.spread_spilled_energy_cost,
},
"energy_cost_unsupplied": user_class.energy_cost_unsupplied,
"energy_cost_spilled": user_class.energy_cost_spilled,
}

return AreaPropertiesLocal.model_validate(args)

def to_user_model(self) -> AreaProperties:
return AreaProperties(
energy_cost_unsupplied=self.energy_cost_unsupplied,
energy_cost_spilled=self.energy_cost_spilled,
non_dispatch_power=self.nodal_optimization.non_dispatchable_power,
dispatch_hydro_power=self.nodal_optimization.dispatchable_hydro_power,
other_dispatch_power=self.nodal_optimization.other_dispatchable_power,
filter_synthesis=self.filtering.filter_synthesis,
filter_by_year=self.filtering.filter_year_by_year,
adequacy_patch_mode=self.adequacy_patch.adequacy_patch_mode,
spread_unsupplied_energy_cost=self.nodal_optimization.spread_unsupplied_energy_cost,
spread_spilled_energy_cost=self.nodal_optimization.spread_spilled_energy_cost,
)

def to_adequacy_ini(self) -> dict[str, dict[str, str]]:
return self.model_dump(mode="json", include={"adequacy_patch"}, by_alias=True)

def to_optimization_ini(self) -> dict[str, dict[str, str]]:
args = self.model_dump(mode="json", include={"nodal_optimization", "filtering"}, by_alias=True)
args["filtering"]["filter-synthesis"] = ", ".join(sorted(args["filtering"]["filter-synthesis"]))
args["filtering"]["filter-year-by-year"] = ", ".join(sorted(args["filtering"]["filter-year-by-year"]))
return args
Loading

0 comments on commit 809a57b

Please sign in to comment.