From a5d6a44124592f1c8c1c75066cd29dfe4ef033d4 Mon Sep 17 00:00:00 2001 From: MartinBelthle Date: Wed, 12 Feb 2025 11:28:49 +0100 Subject: [PATCH] refactor(thermal, renewable): create user classes (#79) --- src/antares/craft/model/area.py | 25 +-- src/antares/craft/model/cluster.py | 27 ++- src/antares/craft/model/renewable.py | 42 +---- src/antares/craft/model/thermal.py | 105 ++++------- .../craft/service/api_services/area_api.py | 137 +++++--------- .../service/api_services/models/__init__.py | 6 + .../service/api_services/models/renewable.py | 47 +++++ .../service/api_services/models/settings.py | 15 +- .../service/api_services/models/thermal.py | 109 ++++++++++++ .../renewable.py} | 14 +- .../{thermal_api.py => services/thermal.py} | 19 +- src/antares/craft/service/base_services.py | 49 ++--- .../service/local_services/area_local.py | 64 ++++--- .../service/local_services/models/__init__.py | 18 ++ .../local_services/models/renewable.py | 48 +++++ .../service/local_services/models/settings.py | 19 +- .../service/local_services/models/thermal.py | 108 +++++++++++ .../renewable.py} | 32 +--- .../{thermal_local.py => services/thermal.py} | 24 +-- src/antares/craft/service/service_factory.py | 8 +- tests/antares/delete/test_delete_api.py | 4 +- .../services/api_services/test_area_api.py | 59 +++---- .../api_services/test_renewable_api.py | 11 +- .../services/api_services/test_thermal_api.py | 16 +- .../services/local_services/test_area.py | 33 ++-- .../test_create_thermal_cluster.py | 167 +++++++----------- tests/integration/test_web_client.py | 31 ++-- 27 files changed, 694 insertions(+), 543 deletions(-) create mode 100644 src/antares/craft/service/api_services/models/renewable.py create mode 100644 src/antares/craft/service/api_services/models/thermal.py rename src/antares/craft/service/api_services/{renewable_api.py => services/renewable.py} (86%) rename src/antares/craft/service/api_services/{thermal_api.py => services/thermal.py} (84%) create mode 100644 src/antares/craft/service/local_services/models/renewable.py create mode 100644 src/antares/craft/service/local_services/models/thermal.py rename src/antares/craft/service/local_services/{renewable_local.py => services/renewable.py} (60%) rename src/antares/craft/service/local_services/{thermal_local.py => services/thermal.py} (77%) diff --git a/src/antares/craft/model/area.py b/src/antares/craft/model/area.py index cba3448d..968a9e08 100644 --- a/src/antares/craft/model/area.py +++ b/src/antares/craft/model/area.py @@ -256,24 +256,17 @@ def ui(self) -> AreaUi: return self._ui def create_thermal_cluster( - self, thermal_name: str, properties: Optional[ThermalClusterProperties] = None - ) -> ThermalCluster: - thermal = self._area_service.create_thermal_cluster(self.id, thermal_name, properties) - self._thermals[thermal.id] = thermal - return thermal - - def create_thermal_cluster_with_matrices( self, - cluster_name: str, - parameters: ThermalClusterProperties, - prepro: Optional[pd.DataFrame], - modulation: Optional[pd.DataFrame], - series: Optional[pd.DataFrame], - CO2Cost: Optional[pd.DataFrame], - fuelCost: Optional[pd.DataFrame], + thermal_name: str, + properties: Optional[ThermalClusterProperties] = None, + prepro: Optional[pd.DataFrame] = None, + modulation: Optional[pd.DataFrame] = None, + series: Optional[pd.DataFrame] = None, + co2_cost: Optional[pd.DataFrame] = None, + fuel_cost: Optional[pd.DataFrame] = None, ) -> ThermalCluster: - thermal = self._area_service.create_thermal_cluster_with_matrices( - self.id, cluster_name, parameters, prepro, modulation, series, CO2Cost, fuelCost + thermal = self._area_service.create_thermal_cluster( + self.id, thermal_name, properties, prepro, modulation, series, co2_cost, fuel_cost ) self._thermals[thermal.id] = thermal return thermal diff --git a/src/antares/craft/model/cluster.py b/src/antares/craft/model/cluster.py index bab9649e..c133f350 100644 --- a/src/antares/craft/model/cluster.py +++ b/src/antares/craft/model/cluster.py @@ -9,34 +9,31 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. - +from dataclasses import dataclass from typing import Optional -from pydantic import BaseModel -from pydantic.alias_generators import to_camel - -class ClusterProperties(BaseModel, extra="forbid", populate_by_name=True, alias_generator=to_camel): +@dataclass +class ClusterProperties: """ Common properties for thermal and renewable clusters """ - # Activity status: - # - True: the plant may generate. - # - False: not yet commissioned, moth-balled, etc. enabled: bool = True - unit_count: int = 1 nominal_capacity: float = 0 @property - def installed_capacity(self) -> Optional[float]: - if self.unit_count is None or self.nominal_capacity is None: - return None + def installed_capacity(self) -> float: return self.unit_count * self.nominal_capacity @property - def enabled_capacity(self) -> Optional[float]: - if self.enabled is None or self.installed_capacity is None: - return None + def enabled_capacity(self) -> float: return self.enabled * self.installed_capacity + + +@dataclass +class ClusterPropertiesUpdate: + enabled: Optional[bool] = None + unit_count: Optional[int] = None + nominal_capacity: Optional[float] = None diff --git a/src/antares/craft/model/renewable.py b/src/antares/craft/model/renewable.py index 6e050f7c..e4f18481 100644 --- a/src/antares/craft/model/renewable.py +++ b/src/antares/craft/model/renewable.py @@ -9,15 +9,14 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. - +from dataclasses import dataclass from enum import Enum from typing import Optional import pandas as pd -from antares.craft.model.cluster import ClusterProperties +from antares.craft.model.cluster import ClusterProperties, ClusterPropertiesUpdate from antares.craft.service.base_services import BaseRenewableService -from antares.craft.tools.all_optional_meta import all_optional_model from antares.craft.tools.contents_tool import transform_name_to_id @@ -55,39 +54,16 @@ class TimeSeriesInterpretation(Enum): PRODUCTION_FACTOR = "production-factor" -class DefaultRenewableClusterProperties(ClusterProperties): - """ - Properties of a renewable cluster read from the configuration files. - """ - +@dataclass +class RenewableClusterProperties(ClusterProperties): group: RenewableClusterGroup = RenewableClusterGroup.OTHER1 ts_interpretation: TimeSeriesInterpretation = TimeSeriesInterpretation.POWER_GENERATION -@all_optional_model -class RenewableClusterProperties(DefaultRenewableClusterProperties): - pass - - -class RenewableClusterPropertiesLocal(DefaultRenewableClusterProperties): - renewable_name: str - - @property - def ini_fields(self) -> dict[str, dict[str, str]]: - return { - self.renewable_name: { - "name": self.renewable_name, - "group": self.group.value, - "enabled": f"{self.enabled}".lower(), - "nominalcapacity": f"{self.nominal_capacity:.6f}", - "unitcount": f"{self.unit_count}", - "ts-interpretation": self.ts_interpretation.value, - } - } - - def yield_renewable_cluster_properties(self) -> RenewableClusterProperties: - excludes = {"renewable_name", "ini_fields"} - return RenewableClusterProperties.model_validate(self.model_dump(mode="json", exclude=excludes)) +@dataclass +class RenewableClusterPropertiesUpdate(ClusterPropertiesUpdate): + group: Optional[RenewableClusterGroup] = None + ts_interpretation: Optional[TimeSeriesInterpretation] = None class RenewableCluster: @@ -122,7 +98,7 @@ def id(self) -> str: def properties(self) -> RenewableClusterProperties: return self._properties - def update_properties(self, properties: RenewableClusterProperties) -> None: + def update_properties(self, properties: RenewableClusterPropertiesUpdate) -> None: new_properties = self._renewable_service.update_renewable_properties(self, properties) self._properties = new_properties diff --git a/src/antares/craft/model/thermal.py b/src/antares/craft/model/thermal.py index 106e262b..825a6ffd 100644 --- a/src/antares/craft/model/thermal.py +++ b/src/antares/craft/model/thermal.py @@ -9,15 +9,14 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. - +from dataclasses import dataclass from enum import Enum from typing import Optional import pandas as pd -from antares.craft.model.cluster import ClusterProperties +from antares.craft.model.cluster import ClusterProperties, ClusterPropertiesUpdate from antares.craft.service.base_services import BaseThermalService -from antares.craft.tools.all_optional_meta import all_optional_model from antares.craft.tools.contents_tool import transform_name_to_id @@ -65,12 +64,8 @@ class ThermalCostGeneration(Enum): USE_COST_TIME_SERIES = "useCostTimeseries" -class DefaultThermalProperties(ClusterProperties): - """ - Thermal cluster configuration model. - This model describes the configuration parameters for a thermal cluster. - """ - +@dataclass +class ThermalClusterProperties(ClusterProperties): group: ThermalClusterGroup = ThermalClusterGroup.OTHER1 gen_ts: LocalTSGenerationBehavior = LocalTSGenerationBehavior.USE_GLOBAL min_stable_power: float = 0 @@ -88,7 +83,6 @@ class DefaultThermalProperties(ClusterProperties): startup_cost: float = 0 market_bid_cost: float = 0 co2: float = 0 - # version 860 nh3: float = 0 so2: float = 0 nox: float = 0 @@ -101,66 +95,45 @@ class DefaultThermalProperties(ClusterProperties): op3: float = 0 op4: float = 0 op5: float = 0 - # version 870 cost_generation: ThermalCostGeneration = ThermalCostGeneration.SET_MANUALLY efficiency: float = 100 variable_o_m_cost: float = 0 -@all_optional_model -class ThermalClusterProperties(DefaultThermalProperties): - pass - - -class ThermalClusterPropertiesLocal(DefaultThermalProperties): - thermal_name: str - - @property - def list_ini_fields(self) -> dict[str, dict[str, str]]: - return { - f"{self.thermal_name}": { - "group": self.group.value, - "name": self.thermal_name, - "enabled": f"{self.enabled}", - "unitcount": f"{self.unit_count}", - "nominalcapacity": f"{self.nominal_capacity:.6f}", - "gen-ts": self.gen_ts.value, - "min-stable-power": f"{self.min_stable_power:.6f}", - "min-up-time": f"{self.min_up_time}", - "min-down-time": f"{self.min_down_time}", - "must-run": f"{self.must_run}", - "spinning": f"{self.spinning:.6f}", - "volatility.forced": f"{self.volatility_forced:.6f}", - "volatility.planned": f"{self.volatility_planned:.6f}", - "law.forced": self.law_forced.value, - "law.planned": self.law_planned.value, - "marginal-cost": f"{self.marginal_cost:.6f}", - "spread-cost": f"{self.spread_cost:.6f}", - "fixed-cost": f"{self.fixed_cost:.6f}", - "startup-cost": f"{self.startup_cost:.6f}", - "market-bid-cost": f"{self.market_bid_cost:.6f}", - "co2": f"{self.co2:.6f}", - "nh3": f"{self.nh3:.6f}", - "so2": f"{self.so2:.6f}", - "nox": f"{self.nox:.6f}", - "pm2_5": f"{self.pm2_5:.6f}", - "pm5": f"{self.pm5:.6f}", - "pm10": f"{self.pm10:.6f}", - "nmvoc": f"{self.nmvoc:.6f}", - "op1": f"{self.op1:.6f}", - "op2": f"{self.op2:.6f}", - "op3": f"{self.op3:.6f}", - "op4": f"{self.op4:.6f}", - "op5": f"{self.op5:.6f}", - "costgeneration": self.cost_generation.value, - "efficiency": f"{self.efficiency:.6f}", - "variableomcost": f"{self.variable_o_m_cost:.6f}", - } - } - - def yield_thermal_cluster_properties(self) -> ThermalClusterProperties: - excludes = {"thermal_name", "list_ini_fields"} - return ThermalClusterProperties.model_validate(self.model_dump(mode="json", exclude=excludes)) +@dataclass +class ThermalClusterPropertiesUpdate(ClusterPropertiesUpdate): + group: Optional[ThermalClusterGroup] = None + gen_ts: Optional[LocalTSGenerationBehavior] = None + min_stable_power: Optional[float] = None + min_up_time: Optional[int] = None + min_down_time: Optional[int] = None + must_run: Optional[bool] = None + spinning: Optional[float] = None + volatility_forced: Optional[float] = None + volatility_planned: Optional[float] = None + law_forced: Optional[LawOption] = None + law_planned: Optional[LawOption] = None + marginal_cost: Optional[float] = None + spread_cost: Optional[float] = None + fixed_cost: Optional[float] = None + startup_cost: Optional[float] = None + market_bid_cost: Optional[float] = None + co2: Optional[float] = None + nh3: Optional[float] = None + so2: Optional[float] = None + nox: Optional[float] = None + pm2_5: Optional[float] = None + pm5: Optional[float] = None + pm10: Optional[float] = None + nmvoc: Optional[float] = None + op1: Optional[float] = None + op2: Optional[float] = None + op3: Optional[float] = None + op4: Optional[float] = None + op5: Optional[float] = None + cost_generation: Optional[ThermalCostGeneration] = None + efficiency: Optional[float] = None + variable_o_m_cost: Optional[float] = None class ThermalClusterMatrixName(Enum): @@ -203,7 +176,7 @@ def id(self) -> str: def properties(self) -> ThermalClusterProperties: return self._properties - def update_properties(self, properties: ThermalClusterProperties) -> None: + def update_properties(self, properties: ThermalClusterPropertiesUpdate) -> None: new_properties = self._thermal_service.update_thermal_properties(self, properties) self._properties = new_properties diff --git a/src/antares/craft/service/api_services/area_api.py b/src/antares/craft/service/api_services/area_api.py index 372fff58..96c37adb 100644 --- a/src/antares/craft/service/api_services/area_api.py +++ b/src/antares/craft/service/api_services/area_api.py @@ -38,6 +38,8 @@ 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.renewable import RenewableClusterPropertiesAPI +from antares.craft.service.api_services.models.thermal import ThermalClusterPropertiesAPI from antares.craft.service.api_services.utils import get_matrix, upload_series from antares.craft.service.base_services import ( BaseAreaService, @@ -148,63 +150,27 @@ def create_area( @override def create_thermal_cluster( - self, area_id: str, thermal_name: str, properties: Optional[ThermalClusterProperties] = None - ) -> ThermalCluster: - """ - Args: - area_id: the area id of the thermal cluster - thermal_name: the name of the thermal cluster - properties: the properties of the thermal cluster. If not provided, AntaresWeb will use its own default values. - - Returns: - The created thermal cluster - - Raises: - MissingTokenError if api_token is missing - ThermalCreationError if an HTTP Exception occurs - """ - - try: - url = f"{self._base_url}/studies/{self.study_id}/areas/{area_id}/clusters/thermal" - body = {"name": thermal_name.lower()} - if properties: - camel_properties = properties.model_dump(mode="json", by_alias=True, exclude_none=True) - body = {**body, **camel_properties} - response = self._wrapper.post(url, json=body) - json_response = response.json() - name = json_response["name"] - del json_response["name"] - del json_response["id"] - properties = ThermalClusterProperties.model_validate(json_response) - - except APIError as e: - raise ThermalCreationError(thermal_name, area_id, e.message) from e - - return ThermalCluster(self.thermal_service, area_id, name, properties) - - @override - def create_thermal_cluster_with_matrices( self, area_id: str, cluster_name: str, - parameters: ThermalClusterProperties, + properties: Optional[ThermalClusterProperties] = None, prepro: Optional[pd.DataFrame] = None, modulation: Optional[pd.DataFrame] = None, series: Optional[pd.DataFrame] = None, - CO2Cost: Optional[pd.DataFrame] = None, - fuelCost: Optional[pd.DataFrame] = None, + co2_cost: Optional[pd.DataFrame] = None, + fuel_cost: Optional[pd.DataFrame] = None, ) -> ThermalCluster: """ Args: area_id: the area id of the thermal cluster cluster_name: the name of the thermal cluster - parameters: the properties of the thermal cluster. + properties: the properties of the thermal cluster. prepro: prepro matrix as a pandas DataFrame. modulation: modulation matrix as a pandas DataFrame. series: matrix for series at input/thermal/series/series.txt (optional). - CO2Cost: matrix for CO2Cost at input/thermal/series/CO2Cost.txt (optional). - fuelCost: matrix for CO2Cost at input/thermal/series/fuelCost.txt (optional). + co2_cost: matrix for CO2Cost at input/thermal/series/CO2Cost.txt (optional). + fuel_cost: matrix for CO2Cost at input/thermal/series/fuelCost.txt (optional). Returns: The created thermal cluster with matrices. @@ -213,64 +179,41 @@ def create_thermal_cluster_with_matrices( MissingTokenError if api_token is missing ThermalCreationError if an HTTP Exception occurs """ - try: - url = f"{self._base_url}/studies/{self.study_id}/commands" - body = { - "action": "create_cluster", - "args": {"area_id": area_id, "cluster_name": cluster_name, "parameters": {}}, - } - args = body.get("args") - - if not isinstance(args, dict): - raise TypeError("body['args'] must be a dictionary") - - if parameters: - camel_properties = parameters.model_dump(mode="json", by_alias=True, exclude_none=True) - args["parameters"].update(camel_properties) + url = f"{self._base_url}/studies/{self.study_id}/areas/{area_id}/clusters/thermal" + body = {"name": cluster_name.lower()} + if properties: + api_properties = ThermalClusterPropertiesAPI.from_user_model(properties) + camel_properties = api_properties.model_dump(mode="json", by_alias=True, exclude_none=True) + body = {**body, **camel_properties} + response = self._wrapper.post(url, json=body) + json_response = response.json() + name = json_response.pop("name") + thermal_id = json_response.pop("id") + created_api_properties = ThermalClusterPropertiesAPI.model_validate(json_response) + properties = created_api_properties.to_user_model() + # Upload matrices if prepro is not None: - args["prepro"] = prepro.to_numpy().tolist() + matrix_path = f"input/thermal/prepro/{area_id}/{thermal_id}/data" + upload_series(self._base_url, self.study_id, self._wrapper, prepro, matrix_path) if modulation is not None: - args["modulation"] = modulation.to_numpy().tolist() - - payload = [body] - response = self._wrapper.post(url, json=payload) - response.raise_for_status() - - if series is not None or CO2Cost is not None or fuelCost is not None: - self._create_thermal_series(area_id, cluster_name, series, CO2Cost, fuelCost) + matrix_path = f"input/thermal/prepro/{area_id}/{thermal_id}/modulation" + upload_series(self._base_url, self.study_id, self._wrapper, modulation, matrix_path) + if series is not None: + matrix_path = f"input/thermal/series/{area_id}/{thermal_id}/series" + upload_series(self._base_url, self.study_id, self._wrapper, series, matrix_path) + if co2_cost is not None: + matrix_path = f"input/thermal/series/{area_id}/{thermal_id}/CO2Cost" + upload_series(self._base_url, self.study_id, self._wrapper, co2_cost, matrix_path) + if fuel_cost is not None: + matrix_path = f"input/thermal/series/{area_id}/{thermal_id}/fuelCost" + upload_series(self._base_url, self.study_id, self._wrapper, fuel_cost, matrix_path) except APIError as e: raise ThermalCreationError(cluster_name, area_id, e.message) from e - return ThermalCluster(self.thermal_service, area_id, cluster_name, parameters) - - def _create_thermal_series( - self, - area_id: str, - cluster_name: str, - series: Optional[pd.DataFrame], - CO2Cost: Optional[pd.DataFrame], - fuelCost: Optional[pd.DataFrame], - ) -> None: - command_body = [] - if series is not None: - series_path = f"input/thermal/series/{area_id}/{cluster_name.lower()}/series" - command_body.append(prepare_args_replace_matrix(series, series_path)) - - if CO2Cost is not None: - co2_cost_path = f"input/thermal/series/{area_id}/{cluster_name.lower()}/CO2Cost" - command_body.append(prepare_args_replace_matrix(CO2Cost, co2_cost_path)) - - if fuelCost is not None: - fuel_cost_path = f"input/thermal/series/{area_id}/{cluster_name.lower()}/fuelCost" - command_body.append(prepare_args_replace_matrix(fuelCost, fuel_cost_path)) - - if command_body: - json_payload = command_body - - self._replace_matrix_request(json_payload) + return ThermalCluster(self.thermal_service, area_id, name, properties) def _replace_matrix_request(self, json_payload: Union[dict[str, Any], list[dict[str, Any]]]) -> None: """ @@ -288,8 +231,8 @@ def create_renewable_cluster( self, area_id: str, renewable_name: str, - properties: Optional[RenewableClusterProperties], - series: Optional[pd.DataFrame], + properties: Optional[RenewableClusterProperties] = None, + series: Optional[pd.DataFrame] = None, ) -> RenewableCluster: """ Args: @@ -309,14 +252,16 @@ def create_renewable_cluster( url = f"{self._base_url}/studies/{self.study_id}/areas/{area_id}/clusters/renewable" body = {"name": renewable_name.lower()} if properties: - camel_properties = properties.model_dump(mode="json", by_alias=True, exclude_none=True) + api_model = RenewableClusterPropertiesAPI.from_user_model(properties) + camel_properties = api_model.model_dump(mode="json", by_alias=True, exclude_none=True) body = {**body, **camel_properties} response = self._wrapper.post(url, json=body) json_response = response.json() name = json_response["name"] del json_response["name"] del json_response["id"] - properties = RenewableClusterProperties.model_validate(json_response) + api_properties = RenewableClusterPropertiesAPI.model_validate(json_response) + properties = api_properties.to_user_model() if series is not None: series_path = f"input/renewables/series/{area_id}/{renewable_name.lower()}/series" diff --git a/src/antares/craft/service/api_services/models/__init__.py b/src/antares/craft/service/api_services/models/__init__.py index 058c6b22..492f5696 100644 --- a/src/antares/craft/service/api_services/models/__init__.py +++ b/src/antares/craft/service/api_services/models/__init__.py @@ -9,3 +9,9 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. +from pydantic import BaseModel, ConfigDict +from pydantic.alias_generators import to_camel + + +class APIBaseModel(BaseModel): + model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel, extra="forbid") diff --git a/src/antares/craft/service/api_services/models/renewable.py b/src/antares/craft/service/api_services/models/renewable.py new file mode 100644 index 00000000..7696298c --- /dev/null +++ b/src/antares/craft/service/api_services/models/renewable.py @@ -0,0 +1,47 @@ +# 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.renewable import ( + RenewableClusterGroup, + RenewableClusterProperties, + RenewableClusterPropertiesUpdate, + TimeSeriesInterpretation, +) +from antares.craft.service.api_services.models import APIBaseModel +from antares.craft.tools.all_optional_meta import all_optional_model + +RenewablePropertiesType = Union[RenewableClusterProperties, RenewableClusterPropertiesUpdate] + + +@all_optional_model +class RenewableClusterPropertiesAPI(APIBaseModel): + group: RenewableClusterGroup + ts_interpretation: TimeSeriesInterpretation + enabled: bool + unit_count: int + nominal_capacity: float + + @staticmethod + def from_user_model(user_class: RenewablePropertiesType) -> "RenewableClusterPropertiesAPI": + user_dict = asdict(user_class) + return RenewableClusterPropertiesAPI.model_validate(user_dict) + + def to_user_model(self) -> RenewableClusterProperties: + return RenewableClusterProperties( + enabled=self.enabled, + unit_count=self.unit_count, + nominal_capacity=self.nominal_capacity, + group=self.group, + ts_interpretation=self.ts_interpretation, + ) diff --git a/src/antares/craft/service/api_services/models/settings.py b/src/antares/craft/service/api_services/models/settings.py index bc644899..83c0d276 100644 --- a/src/antares/craft/service/api_services/models/settings.py +++ b/src/antares/craft/service/api_services/models/settings.py @@ -52,15 +52,15 @@ UnfeasibleProblemBehavior, ) from antares.craft.model.settings.thematic_trimming import ThematicTrimmingParameters, ThematicTrimmingParametersUpdate +from antares.craft.service.api_services.models import APIBaseModel from antares.craft.tools.all_optional_meta import all_optional_model -from pydantic import BaseModel, Field, field_validator -from pydantic.alias_generators import to_camel +from pydantic import Field, field_validator AdequacyPatchParametersType = Union[AdequacyPatchParameters, AdequacyPatchParametersUpdate] @all_optional_model -class AdequacyPatchParametersAPI(BaseModel, alias_generator=to_camel, populate_by_name=True): +class AdequacyPatchParametersAPI(APIBaseModel): enable_adequacy_patch: bool ntc_from_physical_areas_out_to_physical_areas_in_adequacy_patch: bool ntc_between_physical_areas_out_adequacy_patch: bool @@ -103,7 +103,7 @@ def to_user_model(self) -> AdequacyPatchParameters: @all_optional_model -class AdvancedAndSeedParametersAPI(BaseModel, alias_generator=to_camel, populate_by_name=True): +class AdvancedAndSeedParametersAPI(APIBaseModel): accuracy_on_correlation: set[OutputChoices] initial_reservoir_levels: InitialReservoirLevel hydro_heuristic_policy: HydroHeuristicPolicy @@ -113,6 +113,7 @@ class AdvancedAndSeedParametersAPI(BaseModel, alias_generator=to_camel, populate unit_commitment_mode: UnitCommitmentMode number_of_cores_mode: SimulationCore renewable_generation_modelling: RenewableGenerationModeling + day_ahead_reserve_management: Any seed_tsgen_wind: int seed_tsgen_load: int seed_tsgen_hydro: int @@ -172,7 +173,7 @@ def to_user_seed_parameters_model(self) -> SeedParameters: @all_optional_model -class GeneralParametersAPI(BaseModel, extra="forbid", populate_by_name=True, alias_generator=to_camel): +class GeneralParametersAPI(APIBaseModel): mode: Mode = Field(default=Mode.ECONOMY, validate_default=True) horizon: str nb_years: int @@ -231,7 +232,7 @@ def to_user_model(self, nb_ts_thermal: int) -> GeneralParameters: @all_optional_model -class OptimizationParametersAPI(BaseModel, alias_generator=to_camel, populate_by_name=True): +class OptimizationParametersAPI(APIBaseModel): simplex_optimization_range: SimplexOptimizationRange transmission_capacities: OptimizationTransmissionCapacities binding_constraints: bool @@ -285,7 +286,7 @@ def to_user_model(self) -> OptimizationParameters: @all_optional_model -class ThematicTrimmingParametersAPI(BaseModel, alias_generator=to_camel, populate_by_name=True): +class ThematicTrimmingParametersAPI(APIBaseModel): ov_cost: bool op_cost: bool mrg_price: bool diff --git a/src/antares/craft/service/api_services/models/thermal.py b/src/antares/craft/service/api_services/models/thermal.py new file mode 100644 index 00000000..9e718e92 --- /dev/null +++ b/src/antares/craft/service/api_services/models/thermal.py @@ -0,0 +1,109 @@ +# 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.thermal import ( + LawOption, + LocalTSGenerationBehavior, + ThermalClusterGroup, + ThermalClusterProperties, + ThermalClusterPropertiesUpdate, + ThermalCostGeneration, +) +from antares.craft.service.api_services.models import APIBaseModel +from antares.craft.tools.all_optional_meta import all_optional_model + +ThermalPropertiesType = Union[ThermalClusterProperties, ThermalClusterPropertiesUpdate] + + +@all_optional_model +class ThermalClusterPropertiesAPI(APIBaseModel): + enabled: bool + unit_count: int + nominal_capacity: float + group: ThermalClusterGroup + gen_ts: LocalTSGenerationBehavior + min_stable_power: float + min_up_time: int = 1 + min_down_time: int = 1 + must_run: bool = False + spinning: float + volatility_forced: float + volatility_planned: float + law_forced: LawOption + law_planned: LawOption + marginal_cost: float + spread_cost: float + fixed_cost: float + startup_cost: float + market_bid_cost: float + co2: float + nh3: float + so2: float + nox: float + pm2_5: float + pm5: float + pm10: float + nmvoc: float + op1: float + op2: float + op3: float + op4: float + op5: float + cost_generation: ThermalCostGeneration + efficiency: float + variable_o_m_cost: float + + @staticmethod + def from_user_model(user_class: ThermalPropertiesType) -> "ThermalClusterPropertiesAPI": + user_dict = asdict(user_class) + return ThermalClusterPropertiesAPI.model_validate(user_dict) + + def to_user_model(self) -> ThermalClusterProperties: + return ThermalClusterProperties( + enabled=self.enabled, + unit_count=self.unit_count, + nominal_capacity=self.nominal_capacity, + group=self.group, + gen_ts=self.gen_ts, + min_stable_power=self.min_stable_power, + min_up_time=self.min_up_time, + min_down_time=self.min_down_time, + must_run=self.must_run, + spinning=self.spinning, + volatility_forced=self.volatility_forced, + volatility_planned=self.volatility_planned, + law_forced=self.law_forced, + law_planned=self.law_planned, + marginal_cost=self.marginal_cost, + spread_cost=self.spread_cost, + fixed_cost=self.fixed_cost, + startup_cost=self.startup_cost, + market_bid_cost=self.market_bid_cost, + co2=self.co2, + nh3=self.nh3, + so2=self.so2, + nox=self.nox, + pm2_5=self.pm2_5, + pm5=self.pm5, + pm10=self.pm10, + nmvoc=self.nmvoc, + op1=self.op1, + op2=self.op2, + op3=self.op3, + op4=self.op4, + op5=self.op5, + cost_generation=self.cost_generation, + efficiency=self.efficiency, + variable_o_m_cost=self.variable_o_m_cost, + ) diff --git a/src/antares/craft/service/api_services/renewable_api.py b/src/antares/craft/service/api_services/services/renewable.py similarity index 86% rename from src/antares/craft/service/api_services/renewable_api.py rename to src/antares/craft/service/api_services/services/renewable.py index ad6b3f0f..e45bb5a4 100644 --- a/src/antares/craft/service/api_services/renewable_api.py +++ b/src/antares/craft/service/api_services/services/renewable.py @@ -23,7 +23,8 @@ RenewableMatrixUpdateError, RenewablePropertiesUpdateError, ) -from antares.craft.model.renewable import RenewableCluster, RenewableClusterProperties +from antares.craft.model.renewable import RenewableCluster, RenewableClusterProperties, RenewableClusterPropertiesUpdate +from antares.craft.service.api_services.models.renewable import RenewableClusterPropertiesAPI from antares.craft.service.api_services.utils import get_matrix, upload_series from antares.craft.service.base_services import BaseRenewableService from typing_extensions import override @@ -39,11 +40,12 @@ def __init__(self, config: APIconf, study_id: str): @override def update_renewable_properties( - self, renewable_cluster: RenewableCluster, properties: RenewableClusterProperties + self, renewable_cluster: RenewableCluster, properties: RenewableClusterPropertiesUpdate ) -> RenewableClusterProperties: url = f"{self._base_url}/studies/{self.study_id}/areas/{renewable_cluster.area_id}/clusters/renewable/{renewable_cluster.id}" try: - body = properties.model_dump(mode="json", by_alias=True, exclude_none=True) + api_model = RenewableClusterPropertiesAPI.from_user_model(properties) + body = api_model.model_dump(mode="json", by_alias=True, exclude_none=True) if not body: return renewable_cluster.properties @@ -51,7 +53,8 @@ def update_renewable_properties( json_response = response.json() del json_response["id"] del json_response["name"] - new_properties = RenewableClusterProperties.model_validate(json_response) + new_api_properties = RenewableClusterPropertiesAPI.model_validate(json_response) + new_properties = new_api_properties.to_user_model() except APIError as e: raise RenewablePropertiesUpdateError(renewable_cluster.id, renewable_cluster.area_id, e.message) from e @@ -108,7 +111,8 @@ def read_renewables( renewable_id = renewable.pop("id") renewable_name = renewable.pop("name") - renewable_props = RenewableClusterProperties(**renewable) + api_props = RenewableClusterPropertiesAPI.model_validate(renewable) + renewable_props = api_props.to_user_model() renewable_cluster = RenewableCluster(self, renewable_id, renewable_name, renewable_props) renewables.append(renewable_cluster) diff --git a/src/antares/craft/service/api_services/thermal_api.py b/src/antares/craft/service/api_services/services/thermal.py similarity index 84% rename from src/antares/craft/service/api_services/thermal_api.py rename to src/antares/craft/service/api_services/services/thermal.py index 17d6f837..af2c6671 100644 --- a/src/antares/craft/service/api_services/thermal_api.py +++ b/src/antares/craft/service/api_services/services/thermal.py @@ -23,7 +23,13 @@ ThermalMatrixUpdateError, ThermalPropertiesUpdateError, ) -from antares.craft.model.thermal import ThermalCluster, ThermalClusterMatrixName, ThermalClusterProperties +from antares.craft.model.thermal import ( + ThermalCluster, + ThermalClusterMatrixName, + ThermalClusterProperties, + ThermalClusterPropertiesUpdate, +) +from antares.craft.service.api_services.models.thermal import ThermalClusterPropertiesAPI from antares.craft.service.api_services.utils import get_matrix, upload_series from antares.craft.service.base_services import BaseThermalService from typing_extensions import override @@ -39,11 +45,12 @@ def __init__(self, config: APIconf, study_id: str): @override def update_thermal_properties( - self, thermal_cluster: ThermalCluster, properties: ThermalClusterProperties + self, thermal_cluster: ThermalCluster, properties: ThermalClusterPropertiesUpdate ) -> ThermalClusterProperties: url = f"{self._base_url}/studies/{self.study_id}/areas/{thermal_cluster.area_id}/clusters/thermal/{thermal_cluster.id}" try: - body = properties.model_dump(mode="json", by_alias=True, exclude_none=True) + api_model = ThermalClusterPropertiesAPI.from_user_model(properties) + body = api_model.model_dump(mode="json", by_alias=True, exclude_none=True) if not body: return thermal_cluster.properties @@ -51,7 +58,8 @@ def update_thermal_properties( json_response = response.json() del json_response["id"] del json_response["name"] - new_properties = ThermalClusterProperties.model_validate(json_response) + new_api_properties = ThermalClusterPropertiesAPI.model_validate(json_response) + new_properties = new_api_properties.to_user_model() except APIError as e: raise ThermalPropertiesUpdateError(thermal_cluster.id, thermal_cluster.area_id, e.message) from e @@ -105,7 +113,8 @@ def read_thermal_clusters( thermal_id = thermal.pop("id") thermal_name = thermal.pop("name") - thermal_props = ThermalClusterProperties(**thermal) + api_props = ThermalClusterPropertiesAPI.model_validate(thermal) + thermal_props = api_props.to_user_model() thermal_cluster = ThermalCluster(self, thermal_id, thermal_name, thermal_props) thermals.append(thermal_cluster) diff --git a/src/antares/craft/service/base_services.py b/src/antares/craft/service/base_services.py index 9324b88a..5ae6c668 100644 --- a/src/antares/craft/service/base_services.py +++ b/src/antares/craft/service/base_services.py @@ -31,13 +31,18 @@ from antares.craft.model.hydro import Hydro, HydroMatrixName, HydroProperties from antares.craft.model.link import Link, LinkProperties, LinkUi from antares.craft.model.output import AggregationEntry, Output - from antares.craft.model.renewable import RenewableCluster, RenewableClusterProperties + from antares.craft.model.renewable import ( + RenewableCluster, + RenewableClusterProperties, + RenewableClusterPropertiesUpdate, + ) from antares.craft.model.st_storage import STStorage, STStorageMatrixName, STStorageProperties from antares.craft.model.study import Study from antares.craft.model.thermal import ( ThermalCluster, ThermalClusterMatrixName, ThermalClusterProperties, + ThermalClusterPropertiesUpdate, ) @@ -50,43 +55,27 @@ def create_area( @abstractmethod def create_thermal_cluster( - self, area_id: str, thermal_name: str, properties: Optional["ThermalClusterProperties"] = None - ) -> "ThermalCluster": - """ - Args: - area_id: the area id in which to create the thermal cluster - thermal_name: the name of the thermal cluster - properties: the properties of the thermal cluster. If not provided, AntaresWeb will use its own default values. - - Returns: - The created ThermalCluster - """ - pass - - @abstractmethod - def create_thermal_cluster_with_matrices( self, area_id: str, cluster_name: str, - parameters: "ThermalClusterProperties", - prepro: Optional[pd.DataFrame], - modulation: Optional[pd.DataFrame], - series: Optional[pd.DataFrame], - CO2Cost: Optional[pd.DataFrame], - fuelCost: Optional[pd.DataFrame], + properties: Optional["ThermalClusterProperties"] = None, + prepro: Optional[pd.DataFrame] = None, + modulation: Optional[pd.DataFrame] = None, + series: Optional[pd.DataFrame] = None, + co2_cost: Optional[pd.DataFrame] = None, + fuel_cost: Optional[pd.DataFrame] = None, ) -> "ThermalCluster": """ - Args: area_id: area id in which to create the thermal cluster cluster_name: thermal cluster nam - parameters: properties of the thermal cluster. + properties: properties of the thermal cluster. prepro: matrix corresponding to prepro/data.txt modulation: matrix corresponding to prepro/modulation.txt series: matrix for series/series.txt - CO2Cost: matrix for series/CO2Cost.txt - fuelCost: matrix for series/fuelCost.txt + co2_cost: matrix for series/CO2Cost.txt + fuel_cost: matrix for series/fuelCost.txt Returns: Thermal cluster created @@ -98,8 +87,8 @@ def create_renewable_cluster( self, area_id: str, renewable_name: str, - properties: Optional["RenewableClusterProperties"], - series: Optional[pd.DataFrame], + properties: Optional["RenewableClusterProperties"] = None, + series: Optional[pd.DataFrame] = None, ) -> "RenewableCluster": """ Args: @@ -418,7 +407,7 @@ def create_capacity_indirect(self, series: pd.DataFrame, area_from: str, area_to class BaseThermalService(ABC): @abstractmethod def update_thermal_properties( - self, thermal_cluster: "ThermalCluster", properties: "ThermalClusterProperties" + self, thermal_cluster: "ThermalCluster", properties: "ThermalClusterPropertiesUpdate" ) -> "ThermalClusterProperties": """ Args: @@ -626,7 +615,7 @@ def generate_thermal_timeseries(self) -> None: class BaseRenewableService(ABC): @abstractmethod def update_renewable_properties( - self, renewable_cluster: "RenewableCluster", properties: "RenewableClusterProperties" + self, renewable_cluster: "RenewableCluster", properties: "RenewableClusterPropertiesUpdate" ) -> "RenewableClusterProperties": """ Args: diff --git a/src/antares/craft/service/local_services/area_local.py b/src/antares/craft/service/local_services/area_local.py index 745f9758..cbfc8ebe 100644 --- a/src/antares/craft/service/local_services/area_local.py +++ b/src/antares/craft/service/local_services/area_local.py @@ -28,9 +28,9 @@ AreaUiLocal, ) from antares.craft.model.hydro import Hydro, HydroMatrixName, HydroProperties, HydroPropertiesLocal -from antares.craft.model.renewable import RenewableCluster, RenewableClusterProperties, RenewableClusterPropertiesLocal +from antares.craft.model.renewable import RenewableCluster, RenewableClusterProperties from antares.craft.model.st_storage import STStorage, STStorageProperties, STStoragePropertiesLocal -from antares.craft.model.thermal import ThermalCluster, ThermalClusterProperties, ThermalClusterPropertiesLocal +from antares.craft.model.thermal import ThermalCluster, ThermalClusterProperties from antares.craft.service.base_services import ( BaseAreaService, BaseHydroService, @@ -38,6 +38,8 @@ BaseShortTermStorageService, BaseThermalService, ) +from antares.craft.service.local_services.models.renewable import RenewableClusterPropertiesLocal +from antares.craft.service.local_services.models.thermal import ThermalClusterPropertiesLocal from antares.craft.tools.contents_tool import transform_name_to_id from antares.craft.tools.ini_tool import IniFile, InitializationFilesTypes from antares.craft.tools.matrix_tool import read_timeseries @@ -88,11 +90,13 @@ def create_thermal_cluster( area_id: str, thermal_name: str, properties: Optional[ThermalClusterProperties] = None, + prepro: Optional[pd.DataFrame] = None, + modulation: Optional[pd.DataFrame] = None, + series: Optional[pd.DataFrame] = None, + co2_cost: Optional[pd.DataFrame] = None, + fuel_cost: Optional[pd.DataFrame] = None, ) -> ThermalCluster: - properties = properties or ThermalClusterProperties() - args = {"thermal_name": thermal_name, **properties.model_dump(mode="json", exclude_none=True)} - local_thermal_properties = ThermalClusterPropertiesLocal.model_validate(args) - + # Creating files list_ini = IniFile(self.config.study_path, InitializationFilesTypes.THERMAL_LIST_INI, area_id=area_id) IniFile( self.config.study_path, @@ -104,8 +108,13 @@ def create_thermal_cluster( IniFile( self.config.study_path, InitializationFilesTypes.THERMAL_SERIES, area_id=area_id, cluster_id=thermal_name ) + + # Writing properties try: - list_ini.add_section(local_thermal_properties.list_ini_fields) + properties = properties or ThermalClusterProperties() + local_properties = ThermalClusterPropertiesLocal.from_user_model(properties) + new_section_content = {"name": thermal_name, **local_properties.model_dump(mode="json", by_alias=True)} + list_ini.add_section({thermal_name: new_section_content}) except DuplicateSectionError: raise ThermalCreationError( thermal_name, @@ -114,23 +123,19 @@ def create_thermal_cluster( ) list_ini.write_ini_file(sort_sections=True) - return ThermalCluster( - self.thermal_service, area_id, thermal_name, local_thermal_properties.yield_thermal_cluster_properties() - ) + # Upload matrices + if prepro: + self._write_timeseries(prepro, TimeSeriesFileType.THERMAL_DATA, area_id) + if modulation: + self._write_timeseries(modulation, TimeSeriesFileType.THERMAL_MODULATION, area_id) + if series: + self._write_timeseries(series, TimeSeriesFileType.THERMAL_SERIES, area_id) + if co2_cost: + self._write_timeseries(co2_cost, TimeSeriesFileType.THERMAL_CO2, area_id) + if fuel_cost: + self._write_timeseries(fuel_cost, TimeSeriesFileType.THERMAL_FUEL, area_id) - @override - def create_thermal_cluster_with_matrices( - self, - area_id: str, - cluster_name: str, - parameters: ThermalClusterProperties, - prepro: Optional[pd.DataFrame], - modulation: Optional[pd.DataFrame], - series: Optional[pd.DataFrame], - CO2Cost: Optional[pd.DataFrame], - fuelCost: Optional[pd.DataFrame], - ) -> ThermalCluster: - raise NotImplementedError + return ThermalCluster(self.thermal_service, area_id, thermal_name, properties) @override def create_renewable_cluster( @@ -141,16 +146,17 @@ def create_renewable_cluster( series: Optional[pd.DataFrame] = None, ) -> RenewableCluster: properties = properties or RenewableClusterProperties() - args = {"renewable_name": renewable_name, **properties.model_dump(mode="json", exclude_none=True)} - local_properties = RenewableClusterPropertiesLocal.model_validate(args) + local_properties = RenewableClusterPropertiesLocal.from_user_model(properties) + new_section_content = {"name": renewable_name, **local_properties.model_dump(mode="json", by_alias=True)} list_ini = IniFile(self.config.study_path, InitializationFilesTypes.RENEWABLES_LIST_INI, area_id=area_id) - list_ini.add_section(local_properties.ini_fields) + list_ini.add_section({renewable_name: new_section_content}) list_ini.write_ini_file() - return RenewableCluster( - self.renewable_service, area_id, renewable_name, local_properties.yield_renewable_cluster_properties() - ) + if series: + self._write_timeseries(series, TimeSeriesFileType.RENEWABLE_DATA_SERIES, area_id) + + return RenewableCluster(self.renewable_service, area_id, renewable_name, properties) @override def create_load(self, area_id: str, series: pd.DataFrame) -> None: diff --git a/src/antares/craft/service/local_services/models/__init__.py b/src/antares/craft/service/local_services/models/__init__.py index 058c6b22..59d6384a 100644 --- a/src/antares/craft/service/local_services/models/__init__.py +++ b/src/antares/craft/service/local_services/models/__init__.py @@ -9,3 +9,21 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. +from typing import Any + +from pydantic import BaseModel, ConfigDict, field_validator +from pydantic_core import PydanticUseDefault + + +class LocalBaseModel(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + @field_validator("*", mode="before") + @classmethod + def _usedefault_for_none(cls, value: Any) -> Any: + """ + Will use the default value for the field if the value is None and the annotation doesn't allow for a None input. + """ + if value is None: + raise PydanticUseDefault() + return value diff --git a/src/antares/craft/service/local_services/models/renewable.py b/src/antares/craft/service/local_services/models/renewable.py new file mode 100644 index 00000000..61dbcfa0 --- /dev/null +++ b/src/antares/craft/service/local_services/models/renewable.py @@ -0,0 +1,48 @@ +# 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.renewable import ( + RenewableClusterGroup, + RenewableClusterProperties, + RenewableClusterPropertiesUpdate, + TimeSeriesInterpretation, +) +from antares.craft.service.local_services.models import LocalBaseModel +from pydantic import Field + +RenewablePropertiesType = Union[RenewableClusterProperties, RenewableClusterPropertiesUpdate] + + +class RenewableClusterPropertiesLocal(LocalBaseModel): + enabled: bool = True + unit_count: int = Field(default=1, alias="unitcount") + nominal_capacity: float = Field(default=0, alias="nominalcapacity") + group: RenewableClusterGroup = RenewableClusterGroup.OTHER1 + ts_interpretation: TimeSeriesInterpretation = Field( + default=TimeSeriesInterpretation.POWER_GENERATION, alias="ts-interpretation" + ) + + @staticmethod + def from_user_model(user_class: RenewablePropertiesType) -> "RenewableClusterPropertiesLocal": + user_dict = asdict(user_class) + return RenewableClusterPropertiesLocal.model_validate(user_dict) + + def to_user_model(self) -> RenewableClusterProperties: + return RenewableClusterProperties( + enabled=self.enabled, + unit_count=self.unit_count, + nominal_capacity=self.nominal_capacity, + group=self.group, + ts_interpretation=self.ts_interpretation, + ) diff --git a/src/antares/craft/service/local_services/models/settings.py b/src/antares/craft/service/local_services/models/settings.py index f2b7f4f6..6653d960 100644 --- a/src/antares/craft/service/local_services/models/settings.py +++ b/src/antares/craft/service/local_services/models/settings.py @@ -51,24 +51,9 @@ SimplexOptimizationRange, UnfeasibleProblemBehavior, ) +from antares.craft.service.local_services.models import LocalBaseModel from antares.craft.tools.alias_generators import to_kebab -from pydantic import BaseModel, ConfigDict, Field, field_validator -from pydantic_core import PydanticUseDefault - - -class LocalBaseModel(BaseModel): - model_config = ConfigDict(populate_by_name=True) - - @field_validator("*", mode="before") - @classmethod - def _usedefault_for_none(cls, value: Any) -> Any: - """ - Will use the default value for the field if the value is None and the annotation doesn't allow for a None input. - """ - if value is None: - raise PydanticUseDefault() - return value - +from pydantic import Field, field_validator AdequacyPatchParametersType = Union[AdequacyPatchParameters, AdequacyPatchParametersUpdate] diff --git a/src/antares/craft/service/local_services/models/thermal.py b/src/antares/craft/service/local_services/models/thermal.py new file mode 100644 index 00000000..57ed8c39 --- /dev/null +++ b/src/antares/craft/service/local_services/models/thermal.py @@ -0,0 +1,108 @@ +# 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.thermal import ( + LawOption, + LocalTSGenerationBehavior, + ThermalClusterGroup, + ThermalClusterProperties, + ThermalClusterPropertiesUpdate, + ThermalCostGeneration, +) +from antares.craft.service.local_services.models import LocalBaseModel +from pydantic import Field + +ThermalPropertiesType = Union[ThermalClusterProperties, ThermalClusterPropertiesUpdate] + + +class ThermalClusterPropertiesLocal(LocalBaseModel): + enabled: bool = True + unit_count: int = Field(default=1, alias="unitcount") + nominal_capacity: float = Field(default=0, alias="nominalcapacity") + group: ThermalClusterGroup = ThermalClusterGroup.OTHER1 + gen_ts: LocalTSGenerationBehavior = Field(default=LocalTSGenerationBehavior.USE_GLOBAL, alias="gen-ts") + min_stable_power: float = Field(default=0, alias="min-stable-power") + min_up_time: int = Field(default=1, alias="min-up-time") + min_down_time: int = Field(default=1, alias="min-down-time") + must_run: bool = Field(default=False, alias="must-run") + spinning: float = 0 + volatility_forced: float = Field(default=0, alias="volatility.forced") + volatility_planned: float = Field(default=0, alias="volatility.planned") + law_forced: LawOption = Field(default=LawOption.UNIFORM, alias="law.forced") + law_planned: LawOption = Field(default=LawOption.UNIFORM, alias="law.planned") + marginal_cost: float = Field(default=0, alias="marginal-cost") + spread_cost: float = Field(default=0, alias="spread-cost") + fixed_cost: float = Field(default=0, alias="fixed-cost") + startup_cost: float = Field(default=0, alias="startup-cost") + market_bid_cost: float = Field(default=0, alias="market-bid-cost") + co2: float = 0 + nh3: float = 0 + so2: float = 0 + nox: float = 0 + pm2_5: float = 0 + pm5: float = 0 + pm10: float = 0 + nmvoc: float = 0 + op1: float = 0 + op2: float = 0 + op3: float = 0 + op4: float = 0 + op5: float = 0 + cost_generation: ThermalCostGeneration = Field(default=ThermalCostGeneration.SET_MANUALLY, alias="costgeneration") + efficiency: float = 100 + variable_o_m_cost: float = Field(default=0, alias="variableomcost") + + @staticmethod + def from_user_model(user_class: ThermalPropertiesType) -> "ThermalClusterPropertiesLocal": + user_dict = asdict(user_class) + return ThermalClusterPropertiesLocal.model_validate(user_dict) + + def to_user_model(self) -> ThermalClusterProperties: + return ThermalClusterProperties( + enabled=self.enabled, + unit_count=self.unit_count, + nominal_capacity=self.nominal_capacity, + group=self.group, + gen_ts=self.gen_ts, + min_stable_power=self.min_stable_power, + min_up_time=self.min_up_time, + min_down_time=self.min_down_time, + must_run=self.must_run, + spinning=self.spinning, + volatility_forced=self.volatility_forced, + volatility_planned=self.volatility_planned, + law_forced=self.law_forced, + law_planned=self.law_planned, + marginal_cost=self.marginal_cost, + spread_cost=self.spread_cost, + fixed_cost=self.fixed_cost, + startup_cost=self.startup_cost, + market_bid_cost=self.market_bid_cost, + co2=self.co2, + nh3=self.nh3, + so2=self.so2, + nox=self.nox, + pm2_5=self.pm2_5, + pm5=self.pm5, + pm10=self.pm10, + nmvoc=self.nmvoc, + op1=self.op1, + op2=self.op2, + op3=self.op3, + op4=self.op4, + op5=self.op5, + cost_generation=self.cost_generation, + efficiency=self.efficiency, + variable_o_m_cost=self.variable_o_m_cost, + ) diff --git a/src/antares/craft/service/local_services/renewable_local.py b/src/antares/craft/service/local_services/services/renewable.py similarity index 60% rename from src/antares/craft/service/local_services/renewable_local.py rename to src/antares/craft/service/local_services/services/renewable.py index 1a4074ee..261bb245 100644 --- a/src/antares/craft/service/local_services/renewable_local.py +++ b/src/antares/craft/service/local_services/services/renewable.py @@ -11,13 +11,18 @@ # This file is part of the Antares project. -from typing import Any, get_type_hints +from typing import Any import pandas as pd from antares.craft.config.local_configuration import LocalConfiguration -from antares.craft.model.renewable import RenewableCluster, RenewableClusterProperties, RenewableClusterPropertiesLocal +from antares.craft.model.renewable import ( + RenewableCluster, + RenewableClusterProperties, + RenewableClusterPropertiesUpdate, +) from antares.craft.service.base_services import BaseRenewableService +from antares.craft.service.local_services.models.renewable import RenewableClusterPropertiesLocal from antares.craft.tools.ini_tool import IniFile, InitializationFilesTypes from antares.craft.tools.matrix_tool import read_timeseries from antares.craft.tools.time_series_tool import TimeSeriesFileType @@ -32,7 +37,7 @@ def __init__(self, config: LocalConfiguration, study_name: str, **kwargs: Any) - @override def update_renewable_properties( - self, renewable_cluster: RenewableCluster, properties: RenewableClusterProperties + self, renewable_cluster: RenewableCluster, properties: RenewableClusterPropertiesUpdate ) -> RenewableClusterProperties: raise NotImplementedError @@ -42,25 +47,6 @@ def get_renewable_matrix(self, cluster_id: str, area_id: str) -> pd.DataFrame: TimeSeriesFileType.RENEWABLE_DATA_SERIES, self.config.study_path, area_id=area_id, cluster_id=cluster_id ) - def _extract_renewable_properties(self, renewable_data: dict[str, Any]) -> RenewableClusterProperties: - # get_type_hints will yield a dict with every local property as key and its type as the value - property_types = get_type_hints(RenewableClusterPropertiesLocal) - - # the name key is called "name" in renewable_data but "renewable_name" in the properties, that's why we map it - property_mapping = {"name": "renewable_name"} - - # for each property in renewable_data, we will type it according to property_types while making sure it's not None - # because it's Optional. If it's "name" then we get its mapping from the property_mapping dict - parsed_data = { - property_mapping.get(property, property): property_types[property_mapping.get(property, property)](value) - if value is not None - else None - for property, value in renewable_data.items() - if property_mapping.get(property, property) in property_types - } - - return RenewableClusterPropertiesLocal(**parsed_data).yield_renewable_cluster_properties() - @override def read_renewables(self, area_id: str) -> list[RenewableCluster]: renewable_dict = IniFile( @@ -75,7 +61,7 @@ def read_renewables(self, area_id: str) -> list[RenewableCluster]: renewable_service=self, area_id=area_id, name=renewable_data["name"], - properties=self._extract_renewable_properties(renewable_data), + properties=RenewableClusterPropertiesLocal.model_validate(renewable_data).to_user_model(), ) for renewable_data in renewable_dict.values() ] diff --git a/src/antares/craft/service/local_services/thermal_local.py b/src/antares/craft/service/local_services/services/thermal.py similarity index 77% rename from src/antares/craft/service/local_services/thermal_local.py rename to src/antares/craft/service/local_services/services/thermal.py index d8ded906..23d4eb91 100644 --- a/src/antares/craft/service/local_services/thermal_local.py +++ b/src/antares/craft/service/local_services/services/thermal.py @@ -10,7 +10,7 @@ # # This file is part of the Antares project. -from typing import Any, get_type_hints +from typing import Any import pandas as pd @@ -19,9 +19,10 @@ ThermalCluster, ThermalClusterMatrixName, ThermalClusterProperties, - ThermalClusterPropertiesLocal, + ThermalClusterPropertiesUpdate, ) from antares.craft.service.base_services import BaseThermalService +from antares.craft.service.local_services.models.thermal import ThermalClusterPropertiesLocal from antares.craft.tools.ini_tool import IniFile, InitializationFilesTypes from antares.craft.tools.matrix_tool import read_timeseries from antares.craft.tools.time_series_tool import TimeSeriesFileType @@ -36,7 +37,7 @@ def __init__(self, config: LocalConfiguration, study_name: str, **kwargs: Any) - @override def update_thermal_properties( - self, thermal_cluster: ThermalCluster, properties: ThermalClusterProperties + self, thermal_cluster: ThermalCluster, properties: ThermalClusterPropertiesUpdate ) -> ThermalClusterProperties: raise NotImplementedError @@ -60,21 +61,6 @@ def get_thermal_matrix(self, thermal_cluster: ThermalCluster, ts_name: ThermalCl cluster_id=thermal_cluster.id, ) - def _extract_thermal_properties(self, thermal_data: dict[str, Any]) -> ThermalClusterProperties: - property_types = get_type_hints(ThermalClusterPropertiesLocal) - - property_mapping = {"name": "thermal_name"} - - parsed_data = { - property_mapping.get(property, property): property_types[property_mapping.get(property, property)](value) - if value is not None - else None - for property, value in thermal_data.items() - if property_mapping.get(property, property) in property_types - } - - return ThermalClusterPropertiesLocal(**parsed_data).yield_thermal_cluster_properties() - @override def read_thermal_clusters(self, area_id: str) -> list[ThermalCluster]: thermal_dict = IniFile( @@ -88,7 +74,7 @@ def read_thermal_clusters(self, area_id: str) -> list[ThermalCluster]: thermal_service=self, area_id=area_id, name=thermal_data["name"], - properties=self._extract_thermal_properties(thermal_data), + properties=ThermalClusterPropertiesLocal.model_validate(thermal_data).to_user_model(), ) for thermal_data in thermal_dict.values() ] diff --git a/src/antares/craft/service/service_factory.py b/src/antares/craft/service/service_factory.py index 461c453b..2083095d 100644 --- a/src/antares/craft/service/service_factory.py +++ b/src/antares/craft/service/service_factory.py @@ -17,13 +17,13 @@ from antares.craft.service.api_services.binding_constraint_api import BindingConstraintApiService from antares.craft.service.api_services.hydro_api import HydroApiService from antares.craft.service.api_services.link_api import LinkApiService -from antares.craft.service.api_services.renewable_api import RenewableApiService from antares.craft.service.api_services.services.output import OutputApiService +from antares.craft.service.api_services.services.renewable import RenewableApiService from antares.craft.service.api_services.services.run import RunApiService from antares.craft.service.api_services.services.settings import StudySettingsAPIService +from antares.craft.service.api_services.services.thermal import ThermalApiService from antares.craft.service.api_services.st_storage_api import ShortTermStorageApiService from antares.craft.service.api_services.study_api import StudyApiService -from antares.craft.service.api_services.thermal_api import ThermalApiService from antares.craft.service.base_services import ( BaseAreaService, BaseBindingConstraintService, @@ -41,13 +41,13 @@ from antares.craft.service.local_services.binding_constraint_local import BindingConstraintLocalService from antares.craft.service.local_services.hydro_local import HydroLocalService from antares.craft.service.local_services.link_local import LinkLocalService -from antares.craft.service.local_services.renewable_local import RenewableLocalService from antares.craft.service.local_services.services.output import OutputLocalService +from antares.craft.service.local_services.services.renewable import RenewableLocalService from antares.craft.service.local_services.services.run import RunLocalService from antares.craft.service.local_services.services.settings import StudySettingsLocalService +from antares.craft.service.local_services.services.thermal import ThermalLocalService from antares.craft.service.local_services.st_storage_local import ShortTermStorageLocalService from antares.craft.service.local_services.study_local import StudyLocalService -from antares.craft.service.local_services.thermal_local import ThermalLocalService ERROR_MESSAGE = "Unsupported configuration type: " diff --git a/tests/antares/delete/test_delete_api.py b/tests/antares/delete/test_delete_api.py index a75400bc..0db24d96 100644 --- a/tests/antares/delete/test_delete_api.py +++ b/tests/antares/delete/test_delete_api.py @@ -33,11 +33,11 @@ from antares.craft.service.api_services.binding_constraint_api import BindingConstraintApiService from antares.craft.service.api_services.hydro_api import HydroApiService from antares.craft.service.api_services.link_api import LinkApiService -from antares.craft.service.api_services.renewable_api import RenewableApiService from antares.craft.service.api_services.services.output import OutputApiService +from antares.craft.service.api_services.services.renewable import RenewableApiService +from antares.craft.service.api_services.services.thermal import ThermalApiService from antares.craft.service.api_services.st_storage_api import ShortTermStorageApiService from antares.craft.service.api_services.study_api import StudyApiService -from antares.craft.service.api_services.thermal_api import ThermalApiService class TestDeleteAPI: diff --git a/tests/antares/services/api_services/test_area_api.py b/tests/antares/services/api_services/test_area_api.py index 8f96a26e..d5d1ce87 100644 --- a/tests/antares/services/api_services/test_area_api.py +++ b/tests/antares/services/api_services/test_area_api.py @@ -9,6 +9,7 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. + import pytest import requests_mock @@ -31,6 +32,8 @@ from antares.craft.model.study import Study from antares.craft.model.thermal import ThermalCluster, ThermalClusterProperties from antares.craft.service.api_services.area_api import AreaApiService +from antares.craft.service.api_services.models.renewable import RenewableClusterPropertiesAPI +from antares.craft.service.api_services.models.thermal import ThermalClusterPropertiesAPI from antares.craft.service.service_factory import ServiceFactory @@ -53,7 +56,7 @@ class TestCreateAPI: hydro_service, ) antares_web_description_msg = "Mocked Server KO" - matrix = pd.DataFrame(data=[[0]]) + matrix = pd.DataFrame(data=[[1]]) study = Study("TestStudy", "880", ServiceFactory(api, study_id)) def test_update_area_properties_success(self): @@ -109,7 +112,7 @@ def test_update_area_ui_fails(self): def test_create_thermal_success(self): with requests_mock.Mocker() as mocker: url = f"https://antares.com/api/v1/studies/{self.study_id}/areas/{self.area.id}/clusters/thermal" - json_response = ThermalClusterProperties().model_dump(mode="json", by_alias=True) + json_response = ThermalClusterPropertiesAPI().model_dump(mode="json", by_alias=True) thermal_name = "thermal_cluster" mocker.post(url, json={"name": thermal_name, "id": thermal_name, **json_response}, status_code=201) thermal = self.area.create_thermal_cluster(thermal_name=thermal_name) @@ -130,7 +133,7 @@ def test_create_thermal_fails(self): def test_create_renewable_success(self): with requests_mock.Mocker() as mocker: url = f"https://antares.com/api/v1/studies/{self.study_id}/areas/{self.area.id}/clusters/renewable" - json_response = RenewableClusterProperties().model_dump(mode="json", by_alias=True) + json_response = RenewableClusterPropertiesAPI().model_dump(mode="json", by_alias=True) renewable_name = "renewable_cluster" mocker.post(url, json={"name": renewable_name, "id": renewable_name, **json_response}, status_code=201) @@ -176,36 +179,26 @@ def test_create_st_storage_fails(self): self.area.create_st_storage(st_storage_name=st_storage_name) def test_create_thermal_cluster_with_matrices(self): - expected_url = f"https://antares.com/api/v1/studies/{self.study_id}/commands" - matrix_test = pd.DataFrame(data=np.ones((8760, 1))) - json_for_post = ( - [ - { - "action": "create_cluster", - "args": { - "area_id": "fr", - "cluster_name": "cluster 1", - "parameters": {}, - "prepro": matrix_test.to_dict(orient="split"), - "modulation": matrix_test.to_dict(orient="split"), - }, - } - ], - ) + base_url = f"{self.api.api_host}/api/v1" with requests_mock.Mocker() as mocker: - mocker.post(expected_url, json=json_for_post, status_code=200) - - thermal_cluster = self.area.create_thermal_cluster_with_matrices( - cluster_name="cluster_test", - parameters=ThermalClusterProperties(), - prepro=matrix_test, - modulation=matrix_test, - series=matrix_test, - CO2Cost=matrix_test, - fuelCost=matrix_test, + url = f"{base_url}/studies/{self.study_id}/areas/{self.area.id}/clusters/thermal" + cluster_name = "cluster_test" + creation_response = {"name": cluster_name, "id": cluster_name, "group": "Nuclear"} + mocker.post(url, json=creation_response, status_code=200) + + raw_url = f"{base_url}/studies/{self.study_id}/raw" + mocker.post(raw_url, json={}, status_code=200) + + thermal_cluster = self.area.create_thermal_cluster( + thermal_name=cluster_name, + properties=ThermalClusterProperties(), + prepro=self.matrix, + series=self.matrix, + fuel_cost=self.matrix, ) - # to assert two http requests to "commands" - assert len(mocker.request_history) == 2 + # Asserts 4 commands were created + # 1 for the properties and 1 for each matrix we filled + assert len(mocker.request_history) == 4 assert isinstance(thermal_cluster, ThermalCluster) def test_create_hydro_success(self): @@ -319,9 +312,9 @@ def test_read_areas_success(self): storage_id = storage_.pop("id") storage_name = storage_.pop("name") - thermal_props = ThermalClusterProperties(**thermal_) + thermal_props = ThermalClusterPropertiesAPI(**thermal_).to_user_model() thermal_cluster = ThermalCluster(self.area_api.thermal_service, thermal_id, thermal_name, thermal_props) - renewable_props = RenewableClusterProperties(**renewable_) + renewable_props = RenewableClusterPropertiesAPI(**renewable_).to_user_model() renewable_cluster = RenewableCluster( self.area_api.renewable_service, renewable_id, renewable_name, renewable_props ) diff --git a/tests/antares/services/api_services/test_renewable_api.py b/tests/antares/services/api_services/test_renewable_api.py index 7c03ccc1..8105950e 100644 --- a/tests/antares/services/api_services/test_renewable_api.py +++ b/tests/antares/services/api_services/test_renewable_api.py @@ -23,9 +23,10 @@ RenewablePropertiesUpdateError, ) from antares.craft.model.area import Area -from antares.craft.model.renewable import RenewableCluster, RenewableClusterProperties +from antares.craft.model.renewable import RenewableCluster, RenewableClusterProperties, RenewableClusterPropertiesUpdate from antares.craft.service.api_services.area_api import AreaApiService -from antares.craft.service.api_services.renewable_api import RenewableApiService +from antares.craft.service.api_services.models.renewable import RenewableClusterPropertiesAPI +from antares.craft.service.api_services.services.renewable import RenewableApiService from antares.craft.service.service_factory import ServiceFactory @@ -46,12 +47,12 @@ class TestCreateAPI: def test_update_renewable_properties_success(self): with requests_mock.Mocker() as mocker: - properties = RenewableClusterProperties(enabled=False) + properties = RenewableClusterPropertiesUpdate(enabled=False) url = ( f"https://antares.com/api/v1/studies/{self.study_id}/areas/{self.renewable.area_id}/" f"clusters/renewable/{self.renewable.id}" ) - mocker.patch(url, json={"id": "id", "name": "name", **properties.model_dump()}, status_code=200) + mocker.patch(url, json={"id": "id", "name": "name", "enabled": False}, status_code=200) self.renewable.update_properties(properties=properties) def test_update_renewable_properties_fails(self): @@ -150,7 +151,7 @@ def test_read_renewables(self): renewable_id = json_renewable[0].pop("id") renewable_name = json_renewable[0].pop("name") - renewable_props = RenewableClusterProperties(**json_renewable[0]) + renewable_props = RenewableClusterPropertiesAPI(**json_renewable[0]).to_user_model() expected_renewable = RenewableCluster( area_api.renewable_service, renewable_id, renewable_name, renewable_props ) diff --git a/tests/antares/services/api_services/test_thermal_api.py b/tests/antares/services/api_services/test_thermal_api.py index 4e837a6e..f46bb223 100644 --- a/tests/antares/services/api_services/test_thermal_api.py +++ b/tests/antares/services/api_services/test_thermal_api.py @@ -24,9 +24,15 @@ ) from antares.craft.model.area import Area from antares.craft.model.study import Study -from antares.craft.model.thermal import ThermalCluster, ThermalClusterMatrixName, ThermalClusterProperties +from antares.craft.model.thermal import ( + ThermalCluster, + ThermalClusterMatrixName, + ThermalClusterProperties, + ThermalClusterPropertiesUpdate, +) from antares.craft.service.api_services.area_api import AreaApiService -from antares.craft.service.api_services.thermal_api import ThermalApiService +from antares.craft.service.api_services.models.thermal import ThermalClusterPropertiesAPI +from antares.craft.service.api_services.services.thermal import ThermalApiService from antares.craft.service.service_factory import ServiceFactory @@ -60,12 +66,12 @@ class TestCreateAPI: def test_update_thermal_properties_success(self): with requests_mock.Mocker() as mocker: - properties = ThermalClusterProperties(co2=4) + properties = ThermalClusterPropertiesUpdate(co2=4) url = ( f"https://antares.com/api/v1/studies/{self.study_id}/" f"areas/{self.thermal.area_id}/clusters/thermal/{self.thermal.id}" ) - mocker.patch(url, json={"id": "id", "name": "name", **properties.model_dump()}, status_code=200) + mocker.patch(url, json={"id": "id", "name": "name", "co2": 4}, status_code=200) self.thermal.update_properties(properties=properties) def test_update_thermal_properties_fails(self): @@ -167,7 +173,7 @@ def test_read_thermals(self): thermal_id = json_thermal[0].pop("id") thermal_name = json_thermal[0].pop("name") - thermal_props = ThermalClusterProperties(**json_thermal[0]) + thermal_props = ThermalClusterPropertiesAPI(**json_thermal[0]).to_user_model() expected_thermal = ThermalCluster(area_api.thermal_service, thermal_id, thermal_name, thermal_props) assert len(actual_thermal_list) == 1 diff --git a/tests/antares/services/local_services/test_area.py b/tests/antares/services/local_services/test_area.py index aefcd96c..529ec662 100644 --- a/tests/antares/services/local_services/test_area.py +++ b/tests/antares/services/local_services/test_area.py @@ -27,7 +27,6 @@ RenewableCluster, RenewableClusterGroup, RenewableClusterProperties, - RenewableClusterPropertiesLocal, TimeSeriesInterpretation, ) from antares.craft.model.st_storage import STStorage, STStorageGroup, STStorageProperties, STStoragePropertiesLocal @@ -53,11 +52,8 @@ def test_can_create_renewables_cluster(self, local_study_w_thermal): ) def test_renewable_cluster_has_properties(self, local_study_with_renewable): - assert ( - local_study_with_renewable.get_areas()["fr"] - .get_renewables()["renewable cluster"] - .properties.model_dump(exclude_none=True) - ) + renewable_cluster = local_study_with_renewable.get_areas()["fr"].get_renewables()["renewable cluster"] + assert renewable_cluster.properties == RenewableClusterProperties() def test_renewable_cluster_has_correct_default_properties( self, local_study_with_renewable, default_renewable_cluster_properties @@ -80,10 +76,10 @@ def test_renewable_list_ini_has_correct_default_values( # Given expected_renewables_list_ini_content = """[renewable cluster] name = renewable cluster -group = Other RES 1 -enabled = true -nominalcapacity = 0.000000 +enabled = True unitcount = 1 +nominalcapacity = 0.0 +group = Other RES 1 ts-interpretation = power-generation """ @@ -100,17 +96,16 @@ def test_renewable_list_ini_has_correct_default_values( def test_renewable_cluster_and_ini_have_custom_properties(self, local_study_w_thermal): # Given - props = RenewableClusterProperties( + renewable_properties = RenewableClusterProperties( group=RenewableClusterGroup.WIND_OFF_SHORE, ts_interpretation=TimeSeriesInterpretation.PRODUCTION_FACTOR ) - args = {"renewable_name": "renewable cluster", **props.model_dump(mode="json", exclude_none=True)} - custom_properties = RenewableClusterPropertiesLocal.model_validate(args) + renewable_name = "renewable cluster" expected_renewables_list_ini_content = """[renewable cluster] name = renewable cluster -group = Wind Offshore -enabled = true -nominalcapacity = 0.000000 +enabled = True unitcount = 1 +nominalcapacity = 0.0 +group = Wind Offshore ts-interpretation = production-factor """ @@ -119,17 +114,13 @@ def test_renewable_cluster_and_ini_have_custom_properties(self, local_study_w_th ) # When - local_study_w_thermal.get_areas()["fr"].create_renewable_cluster( - renewable_name=custom_properties.renewable_name, - properties=custom_properties.yield_renewable_cluster_properties(), - series=None, - ) + local_study_w_thermal.get_areas()["fr"].create_renewable_cluster(renewable_name, renewable_properties, None) with actual_renewable_list_ini.ini_path.open() as renewables_list_ini_file: actual_renewable_list_ini_content = renewables_list_ini_file.read() assert ( local_study_w_thermal.get_areas()["fr"].get_renewables()["renewable cluster"].properties - == custom_properties.yield_renewable_cluster_properties() + == renewable_properties ) assert actual_renewable_list_ini_content == expected_renewables_list_ini_content diff --git a/tests/antares/services/local_services/test_create_thermal_cluster.py b/tests/antares/services/local_services/test_create_thermal_cluster.py index 9f4dfe5c..c87a81b8 100644 --- a/tests/antares/services/local_services/test_create_thermal_cluster.py +++ b/tests/antares/services/local_services/test_create_thermal_cluster.py @@ -22,9 +22,9 @@ ThermalCluster, ThermalClusterGroup, ThermalClusterProperties, - ThermalClusterPropertiesLocal, ThermalCostGeneration, ) +from antares.craft.service.local_services.models.thermal import ThermalClusterPropertiesLocal from antares.craft.tools.ini_tool import IniFile, InitializationFilesTypes @@ -50,11 +50,8 @@ def test_duplicate_name_errors(self, local_study_w_thermal): local_study_w_thermal.get_areas()[area_name].create_thermal_cluster(thermal_name) def test_has_default_properties(self, local_study_w_thermal): - assert ( - local_study_w_thermal.get_areas()["fr"] - .get_thermals()["test thermal cluster"] - .properties.model_dump(exclude_none=True) - ) + thermal_cluster = local_study_w_thermal.get_areas()["fr"].get_thermals()["test thermal cluster"] + assert thermal_cluster.properties == ThermalClusterProperties() def test_has_correct_default_properties(self, local_study_w_thermal, default_thermal_cluster_properties): # Given @@ -84,42 +81,42 @@ def test_required_ini_files_exist(self, tmp_path, local_study_w_thermal): def test_list_ini_has_default_properties(self, tmp_path, local_study_w_thermal, actual_thermal_list_ini): # Given expected_list_ini_contents = """[test thermal cluster] -group = Other 1 name = test thermal cluster enabled = True unitcount = 1 -nominalcapacity = 0.000000 +nominalcapacity = 0.0 +group = Other 1 gen-ts = use global -min-stable-power = 0.000000 +min-stable-power = 0.0 min-up-time = 1 min-down-time = 1 must-run = False -spinning = 0.000000 -volatility.forced = 0.000000 -volatility.planned = 0.000000 +spinning = 0.0 +volatility.forced = 0.0 +volatility.planned = 0.0 law.forced = uniform law.planned = uniform -marginal-cost = 0.000000 -spread-cost = 0.000000 -fixed-cost = 0.000000 -startup-cost = 0.000000 -market-bid-cost = 0.000000 -co2 = 0.000000 -nh3 = 0.000000 -so2 = 0.000000 -nox = 0.000000 -pm2_5 = 0.000000 -pm5 = 0.000000 -pm10 = 0.000000 -nmvoc = 0.000000 -op1 = 0.000000 -op2 = 0.000000 -op3 = 0.000000 -op4 = 0.000000 -op5 = 0.000000 +marginal-cost = 0.0 +spread-cost = 0.0 +fixed-cost = 0.0 +startup-cost = 0.0 +market-bid-cost = 0.0 +co2 = 0.0 +nh3 = 0.0 +so2 = 0.0 +nox = 0.0 +pm2_5 = 0.0 +pm5 = 0.0 +pm10 = 0.0 +nmvoc = 0.0 +op1 = 0.0 +op2 = 0.0 +op3 = 0.0 +op4 = 0.0 +op5 = 0.0 costgeneration = SetManually -efficiency = 100.000000 -variableomcost = 0.000000 +efficiency = 100.0 +variableomcost = 0.0 """ expected_list_ini = ConfigParser() @@ -135,42 +132,42 @@ def test_list_ini_has_default_properties(self, tmp_path, local_study_w_thermal, def test_list_ini_has_custom_properties(self, tmp_path, local_study_w_areas): # Given expected_list_ini_contents = """[test thermal cluster] -group = Nuclear name = test thermal cluster enabled = False unitcount = 12 -nominalcapacity = 3.900000 +nominalcapacity = 3.9 +group = Nuclear gen-ts = force no generation -min-stable-power = 3.100000 +min-stable-power = 3.1 min-up-time = 3 min-down-time = 2 must-run = True -spinning = 2.300000 -volatility.forced = 3.500000 -volatility.planned = 3.700000 +spinning = 2.3 +volatility.forced = 3.5 +volatility.planned = 3.7 law.forced = geometric law.planned = geometric -marginal-cost = 2.900000 -spread-cost = 4.200000 -fixed-cost = 3.600000 -startup-cost = 0.700000 -market-bid-cost = 0.800000 -co2 = 1.000000 -nh3 = 2.000000 -so2 = 3.000000 -nox = 4.000000 -pm2_5 = 5.000000 -pm5 = 6.000000 -pm10 = 7.000000 -nmvoc = 8.000000 -op1 = 9.000000 -op2 = 10.000000 -op3 = 11.000000 -op4 = 12.000000 -op5 = 13.000000 +marginal-cost = 2.9 +spread-cost = 4.2 +fixed-cost = 3.6 +startup-cost = 0.7 +market-bid-cost = 0.8 +co2 = 1.0 +nh3 = 2.0 +so2 = 3.0 +nox = 4.0 +pm2_5 = 5.0 +pm5 = 6.0 +pm10 = 7.0 +nmvoc = 8.0 +op1 = 9.0 +op2 = 10.0 +op3 = 11.0 +op4 = 12.0 +op5 = 13.0 costgeneration = useCostTimeseries -efficiency = 123.400000 -variableomcost = 5.000000 +efficiency = 123.4 +variableomcost = 5.0 """ expected_list_ini = ConfigParser() @@ -230,50 +227,22 @@ def test_list_ini_has_custom_properties(self, tmp_path, local_study_w_areas): def test_list_ini_has_multiple_clusters( self, local_study_w_thermal, actual_thermal_list_ini, default_thermal_cluster_properties ): - # Given + # Asserts we can create 2 clusters local_study_w_thermal.get_areas()["fr"].create_thermal_cluster("test thermal cluster two") - args = default_thermal_cluster_properties.model_dump(mode="json", exclude_none=True) - args["thermal_name"] = "test thermal cluster" - expected_list_ini_dict = ThermalClusterPropertiesLocal.model_validate(args).list_ini_fields - args["thermal_name"] = "test thermal cluster two" - expected_list_ini_dict.update(ThermalClusterPropertiesLocal.model_validate(args).list_ini_fields) - - expected_list_ini = ConfigParser() - expected_list_ini.read_dict(expected_list_ini_dict) - - # When - actual_thermal_list_ini.update_from_ini_file() - - # Then - assert actual_thermal_list_ini.parsed_ini.sections() == expected_list_ini.sections() - assert actual_thermal_list_ini.parsed_ini == expected_list_ini - - def test_clusters_are_alphabetical_in_list_ini( - self, local_study_w_thermal, actual_thermal_list_ini, default_thermal_cluster_properties - ): - # Given - first_cluster_alphabetically = "a is before b and t" - second_cluster_alphabetically = "b is after a" - - args = default_thermal_cluster_properties.model_dump(mode="json", exclude_none=True) - args["thermal_name"] = first_cluster_alphabetically - expected_list_ini_dict = ThermalClusterPropertiesLocal.model_validate(args).list_ini_fields - args["thermal_name"] = second_cluster_alphabetically - expected_list_ini_dict.update(ThermalClusterPropertiesLocal.model_validate(args).list_ini_fields) - args["thermal_name"] = "test thermal cluster" - expected_list_ini_dict.update(ThermalClusterPropertiesLocal.model_validate(args).list_ini_fields) - expected_list_ini = ConfigParser() - expected_list_ini.read_dict(expected_list_ini_dict) + ini_file = IniFile( + local_study_w_thermal.service.config.study_path, InitializationFilesTypes.THERMAL_LIST_INI, area_id="fr" + ) - # When - local_study_w_thermal.get_areas()["fr"].create_thermal_cluster(second_cluster_alphabetically) - local_study_w_thermal.get_areas()["fr"].create_thermal_cluster(first_cluster_alphabetically) - actual_thermal_list_ini.update_from_ini_file() + new_content = ini_file.ini_dict + assert len(new_content.keys()) == 2 + expected_sections = ["test thermal cluster", "test thermal cluster two"] + for key in expected_sections: + assert key in new_content + created_properties = ThermalClusterPropertiesLocal(**new_content[key]).to_user_model() + assert created_properties == default_thermal_cluster_properties - # Then - assert actual_thermal_list_ini.ini_dict.keys() == expected_list_ini_dict.keys() - assert actual_thermal_list_ini.parsed_ini.sections() == expected_list_ini.sections() - assert actual_thermal_list_ini.parsed_ini == expected_list_ini + # Asserts the section are ordered in alphabetical order + assert ini_file.parsed_ini.sections() == expected_sections def test_create_thermal_initialization_files(self, local_study_w_areas): study_path = Path(local_study_w_areas.path) diff --git a/tests/integration/test_web_client.py b/tests/integration/test_web_client.py index 6de44280..ae15c425 100644 --- a/tests/integration/test_web_client.py +++ b/tests/integration/test_web_client.py @@ -37,7 +37,12 @@ LinkData, ) from antares.craft.model.link import LinkProperties, LinkStyle, LinkUi -from antares.craft.model.renewable import RenewableClusterGroup, RenewableClusterProperties, TimeSeriesInterpretation +from antares.craft.model.renewable import ( + RenewableClusterGroup, + RenewableClusterProperties, + RenewableClusterPropertiesUpdate, + TimeSeriesInterpretation, +) from antares.craft.model.settings.advanced_parameters import ( AdvancedParametersUpdate, RenewableGenerationModeling, @@ -49,7 +54,7 @@ from antares.craft.model.simulation import AntaresSimulationParameters, Job, JobStatus from antares.craft.model.st_storage import STStorageGroup, STStorageMatrixName, STStorageProperties from antares.craft.model.study import create_study_api, create_variant_api, import_study_api, read_study_api -from antares.craft.model.thermal import ThermalClusterGroup, ThermalClusterProperties +from antares.craft.model.thermal import ThermalClusterGroup, ThermalClusterProperties, ThermalClusterPropertiesUpdate from tests.integration.antares_web_desktop import AntaresWebDesktop @@ -174,8 +179,8 @@ def test_creation_lifecycle(self, antares_web: AntaresWebDesktop, tmp_path): prepro_modulation_matrix = pd.DataFrame(data=np.ones((8760, 6))) modulation_matrix = pd.DataFrame(data=np.ones((8760, 4))) series_matrix = pd.DataFrame(data=np.ones((8760, 6))) - CO2Cost_matrix = pd.DataFrame(data=np.ones((8760, 1))) - fuelCost_matrix = pd.DataFrame(data=np.ones((8760, 1))) + co2_cost_matrix = pd.DataFrame(data=np.ones((8760, 1))) + fuel_cost_matrix = pd.DataFrame(data=np.ones((8760, 1))) # creating parameters and capacities for this link and testing them link_be_fr.create_parameters(series_matrix) @@ -190,14 +195,14 @@ def test_creation_lifecycle(self, antares_web: AntaresWebDesktop, tmp_path): series_matrix.equals(indirect_matrix) # Case that succeeds - thermal_value_be = area_fr.create_thermal_cluster_with_matrices( - cluster_name=thermal_name, - parameters=thermal_properties, + thermal_value_be = area_fr.create_thermal_cluster( + thermal_name=thermal_name, + properties=thermal_properties, prepro=prepro_modulation_matrix, modulation=modulation_matrix, series=series_matrix, - CO2Cost=CO2Cost_matrix, - fuelCost=fuelCost_matrix, + co2_cost=co2_cost_matrix, + fuel_cost=fuel_cost_matrix, ) prepro = thermal_value_be.get_prepro_data_matrix() @@ -210,8 +215,8 @@ def test_creation_lifecycle(self, antares_web: AntaresWebDesktop, tmp_path): assert prepro.equals(prepro_modulation_matrix) assert modulation.equals(modulation_matrix) assert series.equals(series_matrix) - assert CO2.equals(CO2Cost_matrix) - assert fuel.equals(fuelCost_matrix) + assert CO2.equals(co2_cost_matrix) + assert fuel.equals(fuel_cost_matrix) # test renewable cluster creation with default values renewable_name = "cluster_test %?" @@ -416,7 +421,7 @@ def test_creation_lifecycle(self, antares_web: AntaresWebDesktop, tmp_path): assert link_be_fr.ui.link_style == LinkStyle.PLAIN # tests thermal properties update - new_props = ThermalClusterProperties() + new_props = ThermalClusterPropertiesUpdate() new_props.group = ThermalClusterGroup.NUCLEAR thermal_fr.update_properties(new_props) assert thermal_fr.properties.group == ThermalClusterGroup.NUCLEAR @@ -431,7 +436,7 @@ def test_creation_lifecycle(self, antares_web: AntaresWebDesktop, tmp_path): assert test_link_de_fr.id == link_de_fr.id # tests renewable properties update - new_props = RenewableClusterProperties() + new_props = RenewableClusterPropertiesUpdate() new_props.ts_interpretation = TimeSeriesInterpretation.POWER_GENERATION renewable_onshore.update_properties(new_props) assert renewable_onshore.properties.ts_interpretation == TimeSeriesInterpretation.POWER_GENERATION