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(area): move area command creation #2322

Open
wants to merge 11 commits into
base: dev
Choose a base branch
from
2 changes: 1 addition & 1 deletion antarest/study/business/allocation_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from typing_extensions import Annotated

from antarest.core.exceptions import AllocationDataNotFound, AreaNotFound
from antarest.study.business.area_management import AreaInfoDTO
from antarest.study.business.model.area_model import AreaInfoDTO
from antarest.study.business.utils import FormFieldsBaseModel, execute_or_add_commands
from antarest.study.model import Study
from antarest.study.storage.storage_service import StudyStorageService
Expand Down
275 changes: 22 additions & 253 deletions antarest/study/business/area_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,27 @@
#
# This file is part of the Antares project.

import enum
import logging
import re
import typing as t

from pydantic import Field

from antarest.core.exceptions import ConfigFileNotFound, DuplicateAreaName, LayerNotAllowedToBeDeleted, LayerNotFound
from antarest.core.model import JSON
from antarest.core.serialization import AntaresBaseModel
from antarest.study.business.all_optional_meta import all_optional_model, camel_case_model
from antarest.study.business.model.area_model import (
AreaCreationDTO,
AreaInfoDTO,
AreaOutput,
AreaType,
ClusterInfoDTO,
LayerInfoDTO,
UpdateAreaUi,
)
from antarest.study.business.utils import execute_or_add_commands
from antarest.study.model import Patch, PatchArea, PatchCluster, RawStudy, Study
from antarest.study.repository import StudyMetadataRepository
from antarest.study.storage.patch_service import PatchService
from antarest.study.storage.rawstudy.model.filesystem.config.area import (
AdequacyPathProperties,
AreaFolder,
OptimizationProperties,
ThermalAreasProperties,
UIProperties,
)
Expand All @@ -37,97 +39,15 @@
from antarest.study.storage.storage_service import StudyStorageService
from antarest.study.storage.variantstudy.model.command.create_area import CreateArea
from antarest.study.storage.variantstudy.model.command.icommand import ICommand
from antarest.study.storage.variantstudy.model.command.move_area import MoveArea
from antarest.study.storage.variantstudy.model.command.remove_area import RemoveArea
from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig

logger = logging.getLogger(__name__)


class AreaType(enum.Enum):
AREA = "AREA"
DISTRICT = "DISTRICT"


class AreaCreationDTO(AntaresBaseModel):
name: str
type: AreaType
metadata: t.Optional[PatchArea] = None
set: t.Optional[t.List[str]] = None


# review: is this class necessary?
class ClusterInfoDTO(PatchCluster):
id: str
name: str
enabled: bool = True
unitcount: int = 0
nominalcapacity: float = 0
group: t.Optional[str] = None
min_stable_power: t.Optional[float] = None
min_up_time: t.Optional[int] = None
min_down_time: t.Optional[int] = None
spinning: t.Optional[float] = None
marginal_cost: t.Optional[float] = None
spread_cost: t.Optional[float] = None
market_bid_cost: t.Optional[float] = None


class AreaInfoDTO(AreaCreationDTO):
id: str
thermals: t.Optional[t.List[ClusterInfoDTO]] = None


class LayerInfoDTO(AntaresBaseModel):
id: str
name: str
areas: t.List[str]


class UpdateAreaUi(AntaresBaseModel, extra="forbid", populate_by_name=True):
"""
DTO for updating area UI
Usage:
>>> from antarest.study.business.area_management import UpdateAreaUi
>>> from pprint import pprint
>>> obj = {
... "x": -673.75,
... "y": 301.5,
... "color_rgb": [230, 108, 44],
... "layerX": {"0": -230, "4": -230, "6": -95, "7": -230, "8": -230},
... "layerY": {"0": 136, "4": 136, "6": 39, "7": 136, "8": 136},
... "layerColor": {
... "0": "230, 108, 44",
... "4": "230, 108, 44",
... "6": "230, 108, 44",
... "7": "230, 108, 44",
... "8": "230, 108, 44",
... },
... }
>>> model = UpdateAreaUi(**obj)
>>> pprint(model.model_dump(by_alias=True), width=80)
{'colorRgb': [230, 108, 44],
'layerColor': {0: '230, 108, 44',
4: '230, 108, 44',
6: '230, 108, 44',
7: '230, 108, 44',
8: '230, 108, 44'},
'layerX': {0: -230, 4: -230, 6: -95, 7: -230, 8: -230},
'layerY': {0: 136, 4: 136, 6: 39, 7: 136, 8: 136},
'x': -673,
'y': 301}
"""

x: int = Field(title="X position")
y: int = Field(title="Y position")
color_rgb: t.Sequence[int] = Field(title="RGB color", alias="colorRgb")
layer_x: t.Mapping[int, int] = Field(default_factory=dict, title="X position of each layer", alias="layerX")
layer_y: t.Mapping[int, int] = Field(default_factory=dict, title="Y position of each layer", alias="layerY")
layer_color: t.Mapping[int, str] = Field(default_factory=dict, title="Color of each layer", alias="layerColor")
_ALL_AREAS_PATH = "input/areas"
_THERMAL_AREAS_PATH = "input/thermal/areas"


def _get_ui_info_map(file_study: FileStudy, area_ids: t.Sequence[str]) -> t.Dict[str, t.Any]:
Expand Down Expand Up @@ -171,101 +91,6 @@ def _get_area_layers(area_uis: t.Dict[str, t.Any], area: str) -> t.List[str]:
return []


_ALL_AREAS_PATH = "input/areas"
_THERMAL_AREAS_PATH = "input/thermal/areas"


# noinspection SpellCheckingInspection
class _BaseAreaDTO(
OptimizationProperties.FilteringSection,
OptimizationProperties.ModalOptimizationSection,
AdequacyPathProperties.AdequacyPathSection,
extra="forbid",
validate_assignment=True,
populate_by_name=True,
):
"""
Represents an area output.
Aggregates the fields of the `OptimizationProperties` and `AdequacyPathProperties` classes,
but without the `UIProperties` fields.
Add the fields extracted from the `/input/thermal/areas.ini` information:
- `average_unsupplied_energy_cost` is extracted from `unserverd_energy_cost`,
- `average_spilled_energy_cost` is extracted from `spilled_energy_cost`.
"""

average_unsupplied_energy_cost: float = Field(0.0, description="average unserverd energy cost (€/MWh)")
average_spilled_energy_cost: float = Field(0.0, description="average spilled energy cost (€/MWh)")


# noinspection SpellCheckingInspection
@all_optional_model
@camel_case_model
class AreaOutput(_BaseAreaDTO):
"""
DTO object use to get the area information using a flat structure.
"""

@classmethod
def from_model(
cls,
area_folder: AreaFolder,
*,
average_unsupplied_energy_cost: float,
average_spilled_energy_cost: float,
) -> "AreaOutput":
"""
Creates a `GetAreaDTO` object from configuration data.
Args:
area_folder: Configuration data read from the `/input/areas/<area>` information.
average_unsupplied_energy_cost: Unserverd energy cost (€/MWh).
average_spilled_energy_cost: Spilled energy cost (€/MWh).
Returns:
The `GetAreaDTO` object.
"""
obj = {
"average_unsupplied_energy_cost": average_unsupplied_energy_cost,
"average_spilled_energy_cost": average_spilled_energy_cost,
**area_folder.optimization.filtering.model_dump(mode="json", by_alias=False),
**area_folder.optimization.nodal_optimization.model_dump(mode="json", by_alias=False),
# adequacy_patch is only available if study version >= 830.
**(
area_folder.adequacy_patch.adequacy_patch.model_dump(mode="json", by_alias=False)
if area_folder.adequacy_patch
else {}
),
}
return cls(**obj)

def _to_optimization(self) -> OptimizationProperties:
obj = {name: getattr(self, name) for name in OptimizationProperties.FilteringSection.model_fields}
filtering_section = OptimizationProperties.FilteringSection(**obj)
obj = {name: getattr(self, name) for name in OptimizationProperties.ModalOptimizationSection.model_fields}
nodal_optimization_section = OptimizationProperties.ModalOptimizationSection(**obj)
args = {"filtering": filtering_section, "nodal_optimization": nodal_optimization_section}
return OptimizationProperties.model_validate(args)

def _to_adequacy_patch(self) -> t.Optional[AdequacyPathProperties]:
obj = {name: getattr(self, name) for name in AdequacyPathProperties.AdequacyPathSection.model_fields}
# If all fields are `None`, the object is empty.
if all(value is None for value in obj.values()):
return None
adequacy_path_section = AdequacyPathProperties.AdequacyPathSection(**obj)
return AdequacyPathProperties.model_validate({"adequacy_patch": adequacy_path_section})

@property
def area_folder(self) -> AreaFolder:
area_folder = AreaFolder(
optimization=self._to_optimization(),
adequacy_patch=self._to_adequacy_patch(),
# UI properties are not configurable in Table Mode
)
return area_folder


class AreaManager:
"""
Manages operations related to areas in a study, including retrieval, creation, and updates.
Expand Down Expand Up @@ -414,7 +239,7 @@ def update_areas_props(

@staticmethod
def get_table_schema() -> JSON:
return AreaOutput.schema()
return AreaOutput.model_json_schema()

def get_all_areas(self, study: RawStudy, area_type: t.Optional[AreaType] = None) -> t.List[AreaInfoDTO]:
"""
Expand Down Expand Up @@ -680,73 +505,17 @@ def update_area_metadata(
)

def update_area_ui(self, study: Study, area_id: str, area_ui: UpdateAreaUi, layer: str = "0") -> None:
obj = {
"x": area_ui.x,
"y": area_ui.y,
"color_r": area_ui.color_rgb[0],
"color_g": area_ui.color_rgb[1],
"color_b": area_ui.color_rgb[2],
}
file_study = self.storage_service.get_storage(study).get_raw(study)
commands = (
[
UpdateConfig(
target=f"input/areas/{area_id}/ui/ui/x",
data=obj["x"],
command_context=self.storage_service.variant_study_service.command_factory.command_context,
study_version=file_study.config.version,
),
UpdateConfig(
target=f"input/areas/{area_id}/ui/ui/y",
data=obj["y"],
command_context=self.storage_service.variant_study_service.command_factory.command_context,
study_version=file_study.config.version,
),
UpdateConfig(
target=f"input/areas/{area_id}/ui/ui/color_r",
data=obj["color_r"],
command_context=self.storage_service.variant_study_service.command_factory.command_context,
study_version=file_study.config.version,
),
UpdateConfig(
target=f"input/areas/{area_id}/ui/ui/color_g",
data=obj["color_g"],
command_context=self.storage_service.variant_study_service.command_factory.command_context,
study_version=file_study.config.version,
),
UpdateConfig(
target=f"input/areas/{area_id}/ui/ui/color_b",
data=obj["color_b"],
command_context=self.storage_service.variant_study_service.command_factory.command_context,
study_version=file_study.config.version,
),
]
if layer == "0"
else []
)
commands.extend(
[
UpdateConfig(
target=f"input/areas/{area_id}/ui/layerX/{layer}",
data=obj["x"],
command_context=self.storage_service.variant_study_service.command_factory.command_context,
study_version=file_study.config.version,
),
UpdateConfig(
target=f"input/areas/{area_id}/ui/layerY/{layer}",
data=obj["y"],
command_context=self.storage_service.variant_study_service.command_factory.command_context,
study_version=file_study.config.version,
),
UpdateConfig(
target=f"input/areas/{area_id}/ui/layerColor/{layer}",
data=f"{obj['color_r']},{obj['color_g']},{obj['color_b']}",
command_context=self.storage_service.variant_study_service.command_factory.command_context,
study_version=file_study.config.version,
),
]

command = MoveArea(
area_id=area_id,
area_ui=area_ui,
layer=layer,
command_context=self.storage_service.variant_study_service.command_factory.command_context,
study_version=file_study.config.version,
)
execute_or_add_commands(study, file_study, commands, self.storage_service)

execute_or_add_commands(study, file_study, [command], self.storage_service)

def update_thermal_cluster_metadata(
self,
Expand Down
2 changes: 1 addition & 1 deletion antarest/study/business/correlation_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from pydantic import ValidationInfo, field_validator

from antarest.core.exceptions import AreaNotFound
from antarest.study.business.area_management import AreaInfoDTO
from antarest.study.business.model.area_model import AreaInfoDTO
from antarest.study.business.utils import FormFieldsBaseModel, execute_or_add_commands
from antarest.study.model import Study
from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy
Expand Down
Loading
Loading