diff --git a/src/antares/model/area.py b/src/antares/model/area.py index 6f1f9221..047ff1e2 100644 --- a/src/antares/model/area.py +++ b/src/antares/model/area.py @@ -33,6 +33,7 @@ from antares.model.st_storage import STStorage, STStorageProperties from antares.model.thermal import ThermalCluster, ThermalClusterProperties from antares.model.wind import Wind +from antares.tools.all_optional_meta import all_optional_model from antares.tools.contents_tool import transform_name_to_id, EnumIgnoreCase @@ -48,111 +49,78 @@ class AdequacyPatchMode(EnumIgnoreCase): VIRTUAL = "virtual" -# todo: Warning, with filesystem, we want to avoid camel case and use link_aliasing. -class AreaProperties(BaseModel, extra="forbid", populate_by_name=True, alias_generator=to_camel): +class DefaultAreaProperties(BaseModel, extra="forbid", populate_by_name=True): """ DTO for updating area properties """ - energy_cost_unsupplied: Optional[float] = None - energy_cost_spilled: Optional[float] = None - non_dispatch_power: Optional[bool] = None - dispatch_hydro_power: Optional[bool] = None - other_dispatch_power: Optional[bool] = None - filter_synthesis: Optional[Set[FilterOption]] = None - filter_by_year: Optional[Set[FilterOption]] = None + energy_cost_unsupplied: float = 0.0 + energy_cost_spilled: float = 0.0 + non_dispatch_power: bool = True + dispatch_hydro_power: bool = True + other_dispatch_power: bool = True + filter_synthesis: Set[FilterOption] = { + FilterOption.HOURLY, + FilterOption.DAILY, + FilterOption.WEEKLY, + FilterOption.MONTHLY, + FilterOption.ANNUAL, + } + filter_by_year: Set[FilterOption] = { + FilterOption.HOURLY, + FilterOption.DAILY, + FilterOption.WEEKLY, + FilterOption.MONTHLY, + FilterOption.ANNUAL, + } # version 830 - adequacy_patch_mode: Optional[AdequacyPatchMode] = None - spread_unsupplied_energy_cost: Optional[float] = None - spread_spilled_energy_cost: Optional[float] = None + adequacy_patch_mode: AdequacyPatchMode = AdequacyPatchMode.OUTSIDE + spread_unsupplied_energy_cost: float = 0.0 + spread_spilled_energy_cost: float = 0.0 + + +@all_optional_model +class AreaProperties(DefaultAreaProperties, alias_generator=to_camel): + pass def config_alias_generator(field_name: str) -> str: return field_name.replace("_", " ") -# TODO update to use check_if_none -class AreaPropertiesLocal(BaseModel, alias_generator=config_alias_generator): - def __init__( - self, - input_area_properties: AreaProperties = AreaProperties(), - **kwargs: Optional[Any], - ): - super().__init__(**kwargs) - self._energy_cost_unsupplied = input_area_properties.energy_cost_unsupplied or 0.0 - self._energy_cost_spilled = input_area_properties.energy_cost_spilled or 0.0 - self._non_dispatch_power = ( - input_area_properties.non_dispatch_power if input_area_properties.non_dispatch_power is not None else True - ) - self._dispatch_hydro_power = ( - input_area_properties.dispatch_hydro_power - if input_area_properties.dispatch_hydro_power is not None - else True - ) - self._other_dispatch_power = ( - input_area_properties.other_dispatch_power - if input_area_properties.other_dispatch_power is not None - else True - ) - self._filter_synthesis = input_area_properties.filter_synthesis or { - FilterOption.HOURLY, - FilterOption.DAILY, - FilterOption.WEEKLY, - FilterOption.MONTHLY, - FilterOption.ANNUAL, - } - self._filter_by_year = input_area_properties.filter_by_year or { - FilterOption.HOURLY, - FilterOption.DAILY, - FilterOption.WEEKLY, - FilterOption.MONTHLY, - FilterOption.ANNUAL, - } - self._adequacy_patch_mode = ( - input_area_properties.adequacy_patch_mode - if input_area_properties.adequacy_patch_mode - else AdequacyPatchMode.OUTSIDE - ) - self._spread_spilled_energy_cost = input_area_properties.spread_spilled_energy_cost or 0.0 - self._spread_unsupplied_energy_cost = input_area_properties.spread_unsupplied_energy_cost or 0.0 - +class AreaPropertiesLocal(DefaultAreaProperties, alias_generator=config_alias_generator): @computed_field # type: ignore[misc] @property def nodal_optimization(self) -> Mapping[str, str]: return { - "non-dispatchable-power": f"{self._non_dispatch_power}".lower(), - "dispatchable-hydro-power": f"{self._dispatch_hydro_power}".lower(), - "other-dispatchable-power": f"{self._other_dispatch_power}".lower(), - "spread-unsupplied-energy-cost": f"{self._spread_unsupplied_energy_cost:.6f}", - "spread-spilled-energy-cost": f"{self._spread_spilled_energy_cost:.6f}", - "average-unsupplied-energy-cost": f"{self._energy_cost_unsupplied:.6f}", - "average-spilled-energy-cost": f"{self._energy_cost_spilled:.6f}", + "non-dispatchable-power": f"{self.non_dispatch_power}".lower(), + "dispatchable-hydro-power": f"{self.dispatch_hydro_power}".lower(), + "other-dispatchable-power": f"{self.other_dispatch_power}".lower(), + "spread-unsupplied-energy-cost": f"{self.spread_unsupplied_energy_cost:.6f}", + "spread-spilled-energy-cost": f"{self.spread_spilled_energy_cost:.6f}", + "average-unsupplied-energy-cost": f"{self.energy_cost_unsupplied:.6f}", + "average-spilled-energy-cost": f"{self.energy_cost_spilled:.6f}", } @computed_field # type: ignore[misc] @property def filtering(self) -> Mapping[str, str]: return { - "filter-synthesis": ", ".join(filter_value for filter_value in sort_filter_values(self._filter_synthesis)), - "filter-year-by-year": ", ".join(filter_value for filter_value in sort_filter_values(self._filter_by_year)), + "filter-synthesis": ", ".join(filter_value for filter_value in sort_filter_values(self.filter_synthesis)), + "filter-year-by-year": ", ".join(filter_value for filter_value in sort_filter_values(self.filter_by_year)), } - def adequacy_patch_mode(self) -> dict[str, dict[str, str]]: - return {"adequacy-patch": {"adequacy-patch-mode": self._adequacy_patch_mode.value}} + def adequacy_patch(self) -> dict[str, dict[str, str]]: + return {"adequacy-patch": {"adequacy-patch-mode": self.adequacy_patch_mode.value}} + + def yield_local_dict(self) -> dict[str, Mapping[str, str]]: + args = {"nodal optimization": self.nodal_optimization} + args.update({"filtering": self.filtering}) + return args def yield_area_properties(self) -> AreaProperties: - return AreaProperties( - energy_cost_unsupplied=self._energy_cost_unsupplied, - energy_cost_spilled=self._energy_cost_spilled, - non_dispatch_power=self._non_dispatch_power, - dispatch_hydro_power=self._dispatch_hydro_power, - other_dispatch_power=self._other_dispatch_power, - filter_synthesis=self._filter_synthesis, - filter_by_year=self._filter_by_year, - adequacy_patch_mode=self._adequacy_patch_mode, - spread_unsupplied_energy_cost=self._spread_unsupplied_energy_cost, - spread_spilled_energy_cost=self._spread_spilled_energy_cost, - ) + excludes = {"filtering", "nodal_optimization"} + return AreaProperties.model_validate(self.model_dump(mode="json", exclude=excludes)) class AreaUi(BaseModel, extra="forbid", populate_by_name=True, alias_generator=to_camel): diff --git a/src/antares/model/cluster.py b/src/antares/model/cluster.py index 8b54b5ed..bab9649e 100644 --- a/src/antares/model/cluster.py +++ b/src/antares/model/cluster.py @@ -24,10 +24,10 @@ class ClusterProperties(BaseModel, extra="forbid", populate_by_name=True, alias_ # Activity status: # - True: the plant may generate. # - False: not yet commissioned, moth-balled, etc. - enabled: Optional[bool] = None + enabled: bool = True - unit_count: Optional[int] = None - nominal_capacity: Optional[float] = None + unit_count: int = 1 + nominal_capacity: float = 0 @property def installed_capacity(self) -> Optional[float]: diff --git a/src/antares/model/hydro.py b/src/antares/model/hydro.py index ac4efed1..2a98add8 100644 --- a/src/antares/model/hydro.py +++ b/src/antares/model/hydro.py @@ -11,13 +11,13 @@ # This file is part of the Antares project. from enum import Enum -from typing import Optional, Dict, Any +from typing import Optional, Dict import pandas as pd from pydantic import BaseModel, computed_field from pydantic.alias_generators import to_camel -from antares.tools.ini_tool import check_if_none +from antares.tools.all_optional_meta import all_optional_model class HydroMatrixName(Enum): @@ -32,95 +32,62 @@ class HydroMatrixName(Enum): COMMON_CREDIT_MODULATIONS = "creditmodulations" -class HydroProperties(BaseModel, extra="forbid", populate_by_name=True, alias_generator=to_camel): +class DefaultHydroProperties(BaseModel, extra="forbid", populate_by_name=True, alias_generator=to_camel): """ Properties of hydro system read from the configuration files. All aliases match the name of the corresponding field in the INI files. """ - inter_daily_breakdown: Optional[float] = None - intra_daily_modulation: Optional[float] = None - inter_monthly_breakdown: Optional[float] = None - reservoir: Optional[bool] = None - reservoir_capacity: Optional[float] = None - follow_load: Optional[bool] = None - use_water: Optional[bool] = None - hard_bounds: Optional[bool] = None - initialize_reservoir_date: Optional[int] = None - use_heuristic: Optional[bool] = None - power_to_level: Optional[bool] = None - use_leeway: Optional[bool] = None - leeway_low: Optional[float] = None - leeway_up: Optional[float] = None - pumping_efficiency: Optional[float] = None - - -class HydroPropertiesLocal(BaseModel): - def __init__( - self, - area_id: str, - hydro_properties: Optional[HydroProperties] = None, - **kwargs: Optional[Any], - ): - super().__init__(**kwargs) - self._area_id = area_id - hydro_properties = hydro_properties or HydroProperties() - self._inter_daily_breakdown = check_if_none(hydro_properties.inter_daily_breakdown, 1) - self._intra_daily_modulation = check_if_none(hydro_properties.intra_daily_modulation, 24) - self._inter_monthly_breakdown = check_if_none(hydro_properties.inter_monthly_breakdown, 1) - self._reservoir = check_if_none(hydro_properties.reservoir, False) - self._reservoir_capacity = check_if_none(hydro_properties.reservoir_capacity, 0) - self._follow_load = check_if_none(hydro_properties.follow_load, True) - self._use_water = check_if_none(hydro_properties.use_water, False) - self._hard_bounds = check_if_none(hydro_properties.hard_bounds, False) - self._initialize_reservoir_date = check_if_none(hydro_properties.initialize_reservoir_date, 0) - self._use_heuristic = check_if_none(hydro_properties.use_heuristic, True) - self._power_to_level = check_if_none(hydro_properties.power_to_level, False) - self._use_leeway = check_if_none(hydro_properties.use_leeway, False) - self._leeway_low = check_if_none(hydro_properties.leeway_low, 1) - self._leeway_up = check_if_none(hydro_properties.leeway_up, 1) - self._pumping_efficiency = check_if_none(hydro_properties.pumping_efficiency, 1) + inter_daily_breakdown: float = 1 + intra_daily_modulation: float = 24 + inter_monthly_breakdown: float = 1 + reservoir: bool = False + reservoir_capacity: float = 0 + follow_load: bool = True + use_water: bool = False + hard_bounds: bool = False + initialize_reservoir_date: int = 0 + use_heuristic: bool = True + power_to_level: bool = False + use_leeway: bool = False + leeway_low: float = 1 + leeway_up: float = 1 + pumping_efficiency: float = 1 + + +@all_optional_model +class HydroProperties(DefaultHydroProperties): + pass + + +class HydroPropertiesLocal(DefaultHydroProperties): + area_id: str @computed_field # type: ignore[misc] @property def hydro_ini_fields(self) -> dict[str, dict[str, str]]: return { - "inter-daily-breakdown": {f"{self._area_id}": f"{self._inter_daily_breakdown:.6f}"}, - "intra-daily-modulation": {f"{self._area_id}": f"{self._intra_daily_modulation:.6f}"}, - "inter-monthly-breakdown": {f"{self._area_id}": f"{self._inter_monthly_breakdown:.6f}"}, - "reservoir": {f"{self._area_id}": f"{self._reservoir}".lower()}, - "reservoir capacity": {f"{self._area_id}": f"{self._reservoir_capacity:.6f}"}, - "follow load": {f"{self._area_id}": f"{self._follow_load}".lower()}, - "use water": {f"{self._area_id}": f"{self._use_water}".lower()}, - "hard bounds": {f"{self._area_id}": f"{self._hard_bounds}".lower()}, - "initialize reservoir date": {f"{self._area_id}": f"{self._initialize_reservoir_date}"}, - "use heuristic": {f"{self._area_id}": f"{self._use_heuristic}".lower()}, - "power to level": {f"{self._area_id}": f"{self._power_to_level}".lower()}, - "use leeway": {f"{self._area_id}": f"{self._use_leeway}".lower()}, - "leeway low": {f"{self._area_id}": f"{self._leeway_low:.6f}"}, - "leeway up": {f"{self._area_id}": f"{self._leeway_up:.6f}"}, - "pumping efficiency": {f"{self._area_id}": f"{self._pumping_efficiency:.6f}"}, + "inter-daily-breakdown": {f"{self.area_id}": f"{self.inter_daily_breakdown:.6f}"}, + "intra-daily-modulation": {f"{self.area_id}": f"{self.intra_daily_modulation:.6f}"}, + "inter-monthly-breakdown": {f"{self.area_id}": f"{self.inter_monthly_breakdown:.6f}"}, + "reservoir": {f"{self.area_id}": f"{self.reservoir}".lower()}, + "reservoir capacity": {f"{self.area_id}": f"{self.reservoir_capacity:.6f}"}, + "follow load": {f"{self.area_id}": f"{self.follow_load}".lower()}, + "use water": {f"{self.area_id}": f"{self.use_water}".lower()}, + "hard bounds": {f"{self.area_id}": f"{self.hard_bounds}".lower()}, + "initialize reservoir date": {f"{self.area_id}": f"{self.initialize_reservoir_date}"}, + "use heuristic": {f"{self.area_id}": f"{self.use_heuristic}".lower()}, + "power to level": {f"{self.area_id}": f"{self.power_to_level}".lower()}, + "use leeway": {f"{self.area_id}": f"{self.use_leeway}".lower()}, + "leeway low": {f"{self.area_id}": f"{self.leeway_low:.6f}"}, + "leeway up": {f"{self.area_id}": f"{self.leeway_up:.6f}"}, + "pumping efficiency": {f"{self.area_id}": f"{self.pumping_efficiency:.6f}"}, } def yield_hydro_properties(self) -> HydroProperties: - return HydroProperties( - inter_daily_breakdown=self._inter_daily_breakdown, - intra_daily_modulation=self._intra_daily_modulation, - inter_monthly_breakdown=self._inter_monthly_breakdown, - reservoir=self._reservoir, - reservoir_capacity=self._reservoir_capacity, - follow_load=self._follow_load, - use_water=self._use_water, - hard_bounds=self._hard_bounds, - initialize_reservoir_date=self._initialize_reservoir_date, - use_heuristic=self._use_heuristic, - power_to_level=self._power_to_level, - use_leeway=self._use_leeway, - leeway_low=self._leeway_low, - leeway_up=self._leeway_up, - pumping_efficiency=self._pumping_efficiency, - ) + excludes = {"area_id", "hydro_ini_fields"} + return HydroProperties.model_validate(self.model_dump(mode="json", exclude=excludes)) class Hydro: diff --git a/src/antares/model/link.py b/src/antares/model/link.py index 1a59d672..43ad2938 100644 --- a/src/antares/model/link.py +++ b/src/antares/model/link.py @@ -11,12 +11,13 @@ # This file is part of the Antares project. from enum import Enum -from typing import Optional, Set, Any, Mapping +from typing import Optional, Set, Mapping from pydantic import BaseModel, computed_field from antares.model.area import Area from antares.model.commons import FilterOption, sort_filter_values +from antares.tools.all_optional_meta import all_optional_model class TransmissionCapacities(Enum): @@ -44,127 +45,93 @@ def link_aliasing(string: str) -> str: return string.replace("_", "-") -class LinkProperties(BaseModel, extra="forbid", populate_by_name=True, alias_generator=link_aliasing): +class DefaultLinkProperties(BaseModel, extra="forbid", populate_by_name=True, alias_generator=link_aliasing): """ DTO for updating link properties """ - hurdles_cost: Optional[bool] = None - loop_flow: Optional[bool] = None - use_phase_shifter: Optional[bool] = None - transmission_capacities: Optional[TransmissionCapacities] = None - asset_type: Optional[AssetType] = None - display_comments: Optional[bool] = None - filter_synthesis: Optional[Set[FilterOption]] = None - filter_year_by_year: Optional[Set[FilterOption]] = None - - -# TODO update to use check_if_none -class LinkPropertiesLocal(BaseModel): - def __init__( - self, - link_properties: LinkProperties = LinkProperties(), - **kwargs: Optional[Any], - ): - super().__init__(**kwargs) - self._hurdles_cost = link_properties.hurdles_cost or False - self._loop_flow = link_properties.loop_flow or False - self._use_phase_shifter = link_properties.use_phase_shifter or False - self._transmission_capacities = ( - link_properties.transmission_capacities - if link_properties.transmission_capacities - else TransmissionCapacities.ENABLED - ) - self._asset_type = link_properties.asset_type if link_properties.asset_type else AssetType.AC - self._display_comments = link_properties.display_comments or True - self._filter_synthesis = link_properties.filter_synthesis or { - FilterOption.HOURLY, - FilterOption.DAILY, - FilterOption.WEEKLY, - FilterOption.MONTHLY, - FilterOption.ANNUAL, - } - self._filter_year_by_year = link_properties.filter_year_by_year or { - FilterOption.HOURLY, - FilterOption.DAILY, - FilterOption.WEEKLY, - FilterOption.MONTHLY, - FilterOption.ANNUAL, - } - + hurdles_cost: bool = False + loop_flow: bool = False + use_phase_shifter: bool = False + transmission_capacities: TransmissionCapacities = TransmissionCapacities.ENABLED + asset_type: AssetType = AssetType.AC + display_comments: bool = True + filter_synthesis: Set[FilterOption] = { + FilterOption.HOURLY, + FilterOption.DAILY, + FilterOption.WEEKLY, + FilterOption.MONTHLY, + FilterOption.ANNUAL, + } + filter_year_by_year: Set[FilterOption] = { + FilterOption.HOURLY, + FilterOption.DAILY, + FilterOption.WEEKLY, + FilterOption.MONTHLY, + FilterOption.ANNUAL, + } + + +@all_optional_model +class LinkProperties(DefaultLinkProperties): + pass + + +class LinkPropertiesLocal(DefaultLinkProperties): @computed_field # type: ignore[misc] @property def ini_fields(self) -> Mapping[str, str]: return { - "hurdles-cost": f"{self._hurdles_cost}".lower(), - "loop-flow": f"{self._loop_flow}".lower(), - "use-phase-shifter": f"{self._use_phase_shifter}".lower(), - "transmission-capacities": f"{self._transmission_capacities.value}", - "asset-type": f"{self._asset_type.value}", - "display-comments": f"{self._display_comments}".lower(), - "filter-synthesis": ", ".join(filter_value for filter_value in sort_filter_values(self._filter_synthesis)), + "hurdles-cost": f"{self.hurdles_cost}".lower(), + "loop-flow": f"{self.loop_flow}".lower(), + "use-phase-shifter": f"{self.use_phase_shifter}".lower(), + "transmission-capacities": f"{self.transmission_capacities.value}", + "asset-type": f"{self.asset_type.value}", + "display-comments": f"{self.display_comments}".lower(), + "filter-synthesis": ", ".join(filter_value for filter_value in sort_filter_values(self.filter_synthesis)), "filter-year-by-year": ", ".join( - filter_value for filter_value in sort_filter_values(self._filter_year_by_year) + filter_value for filter_value in sort_filter_values(self.filter_year_by_year) ), } def yield_link_properties(self) -> LinkProperties: - return LinkProperties( - hurdles_cost=self._hurdles_cost, - loop_flow=self._loop_flow, - use_phase_shifter=self._use_phase_shifter, - transmission_capacities=self._transmission_capacities, - asset_type=self._asset_type, - display_comments=self._display_comments, - filter_synthesis=self._filter_synthesis, - filter_year_by_year=self._filter_year_by_year, - ) - - -class LinkUi(BaseModel, extra="forbid", populate_by_name=True, alias_generator=link_aliasing): + excludes = {"ini_fields"} + return LinkProperties.model_validate(self.model_dump(mode="json", exclude=excludes)) + + +class DefaultLinkUi(BaseModel, extra="forbid", populate_by_name=True, alias_generator=link_aliasing): """ DTO for updating link UI """ - link_style: Optional[LinkStyle] = None - link_width: Optional[float] = None - colorr: Optional[int] = None - colorg: Optional[int] = None - colorb: Optional[int] = None + link_style: LinkStyle = LinkStyle.PLAIN + link_width: float = 1 + colorr: int = 112 + colorg: int = 112 + colorb: int = 112 -class LinkUiLocal(BaseModel): - def __init__( - self, - link_ui: LinkUi = LinkUi(), - **kwargs: Optional[Any], - ): - super().__init__(**kwargs) - self._link_style = link_ui.link_style if link_ui.link_style else LinkStyle.PLAIN - self._link_width = link_ui.link_width if link_ui.link_width is not None else 1 - self._colorr = link_ui.colorr if link_ui.colorr is not None else 112 - self._colorg = link_ui.colorg if link_ui.colorg is not None else 112 - self._colorb = link_ui.colorb if link_ui.colorb is not None else 112 +@all_optional_model +class LinkUi(DefaultLinkUi): + pass + +class LinkUiLocal(DefaultLinkUi): @computed_field # type: ignore[misc] @property def ini_fields(self) -> Mapping[str, str]: + # todo: can be replaced with alias i believe return { - "link-style": f"{self._link_style.value}", - "link-width": f"{self._link_width}", - "colorr": f"{self._colorr}", - "colorg": f"{self._colorg}", - "colorb": f"{self._colorb}", + "link-style": f"{self.link_style.value}", + "link-width": f"{self.link_width}", + "colorr": f"{self.colorr}", + "colorg": f"{self.colorg}", + "colorb": f"{self.colorb}", } def yield_link_ui(self) -> LinkUi: - return LinkUi( - link_style=self._link_style, - link_width=self._link_width, - colorr=self._colorr, - colorg=self._colorg, - colorb=self._colorb, - ) + excludes = {"ini_fields"} + return LinkUi.model_validate(self.model_dump(mode="json", exclude=excludes)) class Link: diff --git a/src/antares/model/renewable.py b/src/antares/model/renewable.py index c98ef8b1..54c218bb 100644 --- a/src/antares/model/renewable.py +++ b/src/antares/model/renewable.py @@ -11,12 +11,13 @@ # This file is part of the Antares project. from enum import Enum -from typing import Optional, Any +from typing import Optional import pandas as pd -from pydantic import BaseModel, computed_field +from pydantic import computed_field from antares.model.cluster import ClusterProperties +from antares.tools.all_optional_meta import all_optional_model from antares.tools.contents_tool import transform_name_to_id @@ -54,70 +55,40 @@ class TimeSeriesInterpretation(Enum): PRODUCTION_FACTOR = "production-factor" -class RenewableClusterProperties(ClusterProperties): +class DefaultRenewableClusterProperties(ClusterProperties): """ Properties of a renewable cluster read from the configuration files. """ - group: Optional[RenewableClusterGroup] = None - ts_interpretation: Optional[TimeSeriesInterpretation] = None + group: RenewableClusterGroup = RenewableClusterGroup.OTHER1 + ts_interpretation: TimeSeriesInterpretation = TimeSeriesInterpretation.POWER_GENERATION -# TODO update to use check_if_none -class RenewableClusterPropertiesLocal( - BaseModel, -): - def __init__( - self, - renewable_name: str, - renewable_cluster_properties: Optional[RenewableClusterProperties] = None, - **kwargs: Optional[Any], - ): - super().__init__(**kwargs) - renewable_cluster_properties = renewable_cluster_properties or RenewableClusterProperties() - self._renewable_name = renewable_name - self._enabled = ( - renewable_cluster_properties.enabled if renewable_cluster_properties.enabled is not None else True - ) - self._unit_count = ( - renewable_cluster_properties.unit_count if renewable_cluster_properties.unit_count is not None else 1 - ) - self._nominal_capacity = ( - renewable_cluster_properties.nominal_capacity - if renewable_cluster_properties.nominal_capacity is not None - else 0 - ) - self._group = renewable_cluster_properties.group or RenewableClusterGroup.OTHER1 - self._ts_interpretation = ( - renewable_cluster_properties.ts_interpretation or TimeSeriesInterpretation.POWER_GENERATION - ) +@all_optional_model +class RenewableClusterProperties(DefaultRenewableClusterProperties): + pass - @property - def renewable_name(self) -> str: - return self._renewable_name + +class RenewableClusterPropertiesLocal(DefaultRenewableClusterProperties): + renewable_name: str @computed_field # type: ignore[misc] @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, + 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: - return RenewableClusterProperties( - enabled=self._enabled, - unit_count=self._unit_count, - nominal_capacity=self._nominal_capacity, - group=self._group, - ts_interpretation=self._ts_interpretation, - ) + excludes = {"renewable_name", "ini_fields"} + return RenewableClusterProperties.model_validate(self.model_dump(mode="json", exclude=excludes)) class RenewableCluster: diff --git a/src/antares/model/st_storage.py b/src/antares/model/st_storage.py index 963e99d8..058359b0 100644 --- a/src/antares/model/st_storage.py +++ b/src/antares/model/st_storage.py @@ -11,14 +11,14 @@ # This file is part of the Antares project. from enum import Enum -from typing import Optional, Any +from typing import Optional import pandas as pd from pydantic import BaseModel, computed_field from pydantic.alias_generators import to_camel +from antares.tools.all_optional_meta import all_optional_model from antares.tools.contents_tool import transform_name_to_id -from antares.tools.ini_tool import check_if_none class STStorageGroup(Enum): @@ -42,75 +42,52 @@ class STStorageMatrixName(Enum): INFLOWS = "inflows" -class STStorageProperties(BaseModel, extra="forbid", populate_by_name=True, alias_generator=to_camel): +class DefaultSTStorageProperties(BaseModel, extra="forbid", populate_by_name=True, alias_generator=to_camel): """ Properties of a short-term storage system read from the configuration files. All aliases match the name of the corresponding field in the INI files. """ - group: Optional[STStorageGroup] = None - injection_nominal_capacity: Optional[float] = None - withdrawal_nominal_capacity: Optional[float] = None - reservoir_capacity: Optional[float] = None - efficiency: Optional[float] = None - initial_level: Optional[float] = None - initial_level_optim: Optional[bool] = None + group: STStorageGroup = STStorageGroup.OTHER1 + injection_nominal_capacity: float = 0 + withdrawal_nominal_capacity: float = 0 + reservoir_capacity: float = 0 + efficiency: float = 1 + initial_level: float = 0.5 + initial_level_optim: bool = False # v880 - enabled: Optional[bool] = None - - -class STStoragePropertiesLocal(BaseModel): - def __init__( - self, - st_storage_name: str, - st_storage_properties: Optional[STStorageProperties] = None, - **kwargs: Optional[Any], - ): - super().__init__(**kwargs) - st_storage_properties = st_storage_properties or STStorageProperties() - self._st_storage_name = st_storage_name - self._group = check_if_none(st_storage_properties.group, STStorageGroup.OTHER1) - self._injection_nominal_capacity = check_if_none(st_storage_properties.injection_nominal_capacity, 0) - self._withdrawal_nominal_capacity = check_if_none(st_storage_properties.withdrawal_nominal_capacity, 0) - self._reservoir_capacity = check_if_none(st_storage_properties.reservoir_capacity, 0) - self._efficiency = check_if_none(st_storage_properties.efficiency, 1) - self._initial_level = check_if_none(st_storage_properties.initial_level, 0.5) - self._initial_level_optim = check_if_none(st_storage_properties.initial_level_optim, False) - self._enabled = check_if_none(st_storage_properties.enabled, True) + enabled: bool = True + + +@all_optional_model +class STStorageProperties(DefaultSTStorageProperties): + pass - @property - def st_storage_name(self) -> str: - return self._st_storage_name + +class STStoragePropertiesLocal(DefaultSTStorageProperties): + st_storage_name: str @computed_field # type: ignore[misc] @property def list_ini_fields(self) -> dict[str, dict[str, str]]: return { - f"{self._st_storage_name}": { - "name": self._st_storage_name, - "group": self._group.value, - "injectionnominalcapacity": f"{self._injection_nominal_capacity:.6f}", - "withdrawalnominalcapacity": f"{self._withdrawal_nominal_capacity:.6f}", - "reservoircapacity": f"{self._reservoir_capacity:.6f}", - "efficiency": f"{self._efficiency:.6f}", - "initiallevel": f"{self._initial_level:.6f}", - "initialleveloptim": f"{self._initial_level_optim}".lower(), - "enabled": f"{self._enabled}".lower(), + f"{self.st_storage_name}": { + "name": self.st_storage_name, + "group": self.group.value, + "injectionnominalcapacity": f"{self.injection_nominal_capacity:.6f}", + "withdrawalnominalcapacity": f"{self.withdrawal_nominal_capacity:.6f}", + "reservoircapacity": f"{self.reservoir_capacity:.6f}", + "efficiency": f"{self.efficiency:.6f}", + "initiallevel": f"{self.initial_level:.6f}", + "initialleveloptim": f"{self.initial_level_optim}".lower(), + "enabled": f"{self.enabled}".lower(), } } def yield_st_storage_properties(self) -> STStorageProperties: - return STStorageProperties( - group=self._group, - injection_nominal_capacity=self._injection_nominal_capacity, - withdrawal_nominal_capacity=self._withdrawal_nominal_capacity, - reservoir_capacity=self._reservoir_capacity, - efficiency=self._efficiency, - initial_level=self._initial_level, - initial_level_optim=self._initial_level_optim, - enabled=self._enabled, - ) + excludes = {"st_storage_name", "list_ini_fields"} + return STStorageProperties.model_validate(self.model_dump(mode="json", exclude=excludes)) class STStorage: diff --git a/src/antares/model/thermal.py b/src/antares/model/thermal.py index eab6ea47..91588163 100644 --- a/src/antares/model/thermal.py +++ b/src/antares/model/thermal.py @@ -11,14 +11,14 @@ # This file is part of the Antares project. from enum import Enum -from typing import Optional, Any +from typing import Optional import pandas as pd -from pydantic import BaseModel, computed_field +from pydantic import computed_field from antares.model.cluster import ClusterProperties +from antares.tools.all_optional_meta import all_optional_model from antares.tools.contents_tool import transform_name_to_id -from antares.tools.ini_tool import check_if_none class LawOption(Enum): @@ -65,181 +65,103 @@ class ThermalCostGeneration(Enum): USE_COST_TIME_SERIES = "useCostTimeseries" -class ThermalClusterProperties(ClusterProperties): +class DefaultThermalProperties(ClusterProperties): """ Thermal cluster configuration model. This model describes the configuration parameters for a thermal cluster. """ - 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 + group: ThermalClusterGroup = ThermalClusterGroup.OTHER1 + gen_ts: LocalTSGenerationBehavior = LocalTSGenerationBehavior.USE_GLOBAL + min_stable_power: float = 0 + min_up_time: int = 1 + min_down_time: int = 1 + must_run: bool = False + spinning: float = 0 + volatility_forced: float = 0 + volatility_planned: float = 0 + law_forced: LawOption = LawOption.UNIFORM + law_planned: LawOption = LawOption.UNIFORM + marginal_cost: float = 0 + spread_cost: float = 0 + fixed_cost: float = 0 + startup_cost: float = 0 + market_bid_cost: float = 0 + co2: float = 0 # version 860 - 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 + 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 # version 870 - cost_generation: Optional[ThermalCostGeneration] = None - efficiency: Optional[float] = None - variable_o_m_cost: Optional[float] = None - - -class ThermalClusterPropertiesLocal(BaseModel): - def __init__( - self, - thermal_name: str, - thermal_cluster_properties: Optional[ThermalClusterProperties] = None, - **kwargs: Optional[Any], - ): - super().__init__(**kwargs) - thermal_cluster_properties = thermal_cluster_properties or ThermalClusterProperties() - self._thermal_name = thermal_name - self._enabled = check_if_none(thermal_cluster_properties.enabled, True) - self._unit_count = check_if_none(thermal_cluster_properties.unit_count, 1) - self._nominal_capacity = check_if_none(thermal_cluster_properties.nominal_capacity, 0) - self._group = ( - # The value OTHER1 matches AntaresWeb if a cluster is created via API without providing a group - thermal_cluster_properties.group or ThermalClusterGroup.OTHER1 - ) - self._gen_ts = check_if_none(thermal_cluster_properties.gen_ts, LocalTSGenerationBehavior.USE_GLOBAL) - self._min_stable_power = check_if_none(thermal_cluster_properties.min_stable_power, 0) - self._min_up_time = check_if_none(thermal_cluster_properties.min_up_time, 1) - self._min_down_time = check_if_none(thermal_cluster_properties.min_down_time, 1) - self._must_run = check_if_none(thermal_cluster_properties.must_run, False) - self._spinning = check_if_none(thermal_cluster_properties.spinning, 0) - self._volatility_forced = check_if_none(thermal_cluster_properties.volatility_forced, 0) - self._volatility_planned = check_if_none(thermal_cluster_properties.volatility_planned, 0) - self._law_forced = check_if_none(thermal_cluster_properties.law_forced, LawOption.UNIFORM) - self._law_planned = check_if_none(thermal_cluster_properties.law_planned, LawOption.UNIFORM) - self._marginal_cost = check_if_none(thermal_cluster_properties.marginal_cost, 0) - self._spread_cost = check_if_none(thermal_cluster_properties.spread_cost, 0) - self._fixed_cost = check_if_none(thermal_cluster_properties.fixed_cost, 0) - self._startup_cost = check_if_none(thermal_cluster_properties.startup_cost, 0) - self._market_bid_cost = check_if_none(thermal_cluster_properties.market_bid_cost, 0) - self._co2 = check_if_none(thermal_cluster_properties.co2, 0) - self._nh3 = check_if_none(thermal_cluster_properties.nh3, 0) - self._so2 = check_if_none(thermal_cluster_properties.so2, 0) - self._nox = check_if_none(thermal_cluster_properties.nox, 0) - self._pm2_5 = check_if_none(thermal_cluster_properties.pm2_5, 0) - self._pm5 = check_if_none(thermal_cluster_properties.pm5, 0) - self._pm10 = check_if_none(thermal_cluster_properties.pm10, 0) - self._nmvoc = check_if_none(thermal_cluster_properties.nmvoc, 0) - self._op1 = check_if_none(thermal_cluster_properties.op1, 0) - self._op2 = check_if_none(thermal_cluster_properties.op2, 0) - self._op3 = check_if_none(thermal_cluster_properties.op3, 0) - self._op4 = check_if_none(thermal_cluster_properties.op4, 0) - self._op5 = check_if_none(thermal_cluster_properties.op5, 0) - self._cost_generation = check_if_none( - thermal_cluster_properties.cost_generation, ThermalCostGeneration.SET_MANUALLY - ) - self._efficiency = check_if_none(thermal_cluster_properties.efficiency, 100) - self._variable_o_m_cost = check_if_none(thermal_cluster_properties.variable_o_m_cost, 0) + 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 @computed_field # type: ignore[misc] @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}", + 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: - return ThermalClusterProperties( - group=self._group, - enabled=self._enabled, - unit_count=self._unit_count, - nominal_capacity=self._nominal_capacity, - 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, - ) + excludes = {"thermal_name", "list_ini_fields"} + return ThermalClusterProperties.model_validate(self.model_dump(mode="json", exclude=excludes)) class ThermalClusterMatrixName(Enum): diff --git a/src/antares/service/local_services/area_local.py b/src/antares/service/local_services/area_local.py index 4ae4870a..f711a757 100644 --- a/src/antares/service/local_services/area_local.py +++ b/src/antares/service/local_services/area_local.py @@ -77,7 +77,9 @@ def create_thermal_cluster( thermal_name: str, properties: Optional[ThermalClusterProperties] = None, ) -> ThermalCluster: - local_thermal_properties = ThermalClusterPropertiesLocal(thermal_name, properties) + properties = properties or ThermalClusterProperties() + args = {"thermal_name": thermal_name, **properties.model_dump(mode="json", exclude_none=True)} + local_thermal_properties = ThermalClusterPropertiesLocal.model_validate(args) list_ini = IniFile(self.config.study_path, IniFileTypes.THERMAL_LIST_INI, area_name=area_id) list_ini.add_section(local_thermal_properties.list_ini_fields) @@ -107,7 +109,9 @@ def create_renewable_cluster( properties: Optional[RenewableClusterProperties] = None, series: Optional[pd.DataFrame] = None, ) -> RenewableCluster: - local_properties = RenewableClusterPropertiesLocal(renewable_name, properties) + properties = properties or RenewableClusterProperties() + args = {"renewable_name": renewable_name, **properties.model_dump(mode="json", exclude_none=True)} + local_properties = RenewableClusterPropertiesLocal.model_validate(args) list_ini = IniFile(self.config.study_path, IniFileTypes.RENEWABLES_LIST_INI, area_name=area_id) list_ini.add_section(local_properties.ini_fields) @@ -125,7 +129,9 @@ def create_load(self, area: Area, series: Optional[pd.DataFrame]) -> Load: def create_st_storage( self, area_id: str, st_storage_name: str, properties: Optional[STStorageProperties] = None ) -> STStorage: - local_st_storage_properties = STStoragePropertiesLocal(st_storage_name, properties) + properties = properties or STStorageProperties() + args = {"st_storage_name": st_storage_name, **properties.model_dump(mode="json", exclude_none=True)} + local_st_storage_properties = STStoragePropertiesLocal.model_validate(args) list_ini = IniFile(self.config.study_path, IniFileTypes.ST_STORAGE_LIST_INI, area_name=area_id) list_ini.add_section(local_st_storage_properties.list_ini_fields) @@ -164,7 +170,9 @@ def create_hydro( properties: Optional[HydroProperties] = None, matrices: Optional[Dict[HydroMatrixName, pd.DataFrame]] = None, ) -> Hydro: - local_hydro_properties = HydroPropertiesLocal(area_id, properties) + properties = properties or HydroProperties() + args = {"area_id": area_id, **properties.model_dump(mode="json", exclude_none=True)} + local_hydro_properties = HydroPropertiesLocal.model_validate(args) list_ini = IniFile(self.config.study_path, IniFileTypes.HYDRO_INI) list_ini.add_section(local_hydro_properties.hydro_ini_fields) @@ -226,14 +234,18 @@ def _line_exists_in_file(file_content: str, line_to_add: str) -> bool: with (self.config.study_path / IniFileTypes.AREAS_SETS_INI.value).open("w") as sets_ini: sets_ini_content.write(sets_ini) - local_properties = AreaPropertiesLocal(properties) if properties else AreaPropertiesLocal() + local_properties = ( + AreaPropertiesLocal.model_validate(properties.model_dump(mode="json", exclude_none=True)) + if properties + else AreaPropertiesLocal() + ) adequacy_patch_ini = IniFile(self.config.study_path, IniFileTypes.AREA_ADEQUACY_PATCH_INI, area_name) - adequacy_patch_ini.add_section(local_properties.adequacy_patch_mode()) + adequacy_patch_ini.add_section(local_properties.adequacy_patch()) adequacy_patch_ini.write_ini_file() optimization_ini = ConfigParser() - optimization_ini.read_dict(local_properties.model_dump(by_alias=True, exclude_none=True)) + optimization_ini.read_dict(local_properties.yield_local_dict()) with open(new_area_directory / "optimization.ini", "w") as optimization_ini_file: optimization_ini.write(optimization_ini_file) diff --git a/src/antares/service/local_services/link_local.py b/src/antares/service/local_services/link_local.py index f1cf08ce..b0f03601 100644 --- a/src/antares/service/local_services/link_local.py +++ b/src/antares/service/local_services/link_local.py @@ -65,8 +65,12 @@ def create_link( link_dir = self.config.study_path / "input/links" / area_from.name os.makedirs(link_dir, exist_ok=True) - local_properties = LinkPropertiesLocal(properties) if properties else LinkPropertiesLocal() - local_ui = LinkUiLocal(ui) if ui else LinkUiLocal() + local_properties = ( + LinkPropertiesLocal.model_validate(properties.model_dump(mode="json", exclude_none=True)) + if properties + else LinkPropertiesLocal() + ) + local_ui = LinkUiLocal.model_validate(ui.model_dump(mode="json", exclude_none=True)) if ui else LinkUiLocal() properties_ini_file = link_dir / "properties.ini" properties_ini = configparser.ConfigParser() diff --git a/src/antares/tools/all_optional_meta.py b/src/antares/tools/all_optional_meta.py new file mode 100644 index 00000000..6901566b --- /dev/null +++ b/src/antares/tools/all_optional_meta.py @@ -0,0 +1,37 @@ +# 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. + +import copy +import typing as t +from pydantic import BaseModel, create_model + +ModelClass = t.TypeVar("ModelClass", bound=BaseModel) + + +def all_optional_model(model: t.Type[ModelClass]) -> t.Type[ModelClass]: + """ + This decorator can be used to make all fields of a pydantic model optionals. + + Args: + model: The pydantic model to modify. + + Returns: + The modified model. + """ + kwargs = {} + for field_name, field_info in model.model_fields.items(): + new = copy.deepcopy(field_info) + new.default = None + new.annotation = t.Optional[field_info.annotation] # type: ignore + kwargs[field_name] = (new.annotation, new) + + return create_model(f"Partial{model.__name__}", __base__=model, __module__=model.__module__, **kwargs) # type: ignore diff --git a/src/antares/tools/ini_tool.py b/src/antares/tools/ini_tool.py index 5eccbfeb..6109a3da 100644 --- a/src/antares/tools/ini_tool.py +++ b/src/antares/tools/ini_tool.py @@ -149,7 +149,3 @@ def _sort_ini_section_content(ini_to_sort: ConfigParser) -> ConfigParser: for section in ini_to_sort.sections(): sorted_ini[section] = {key: value for (key, value) in sorted(list(ini_to_sort[section].items()))} return sorted_ini - - -def check_if_none(value_to_check: Any, default_value: Any) -> Any: - return value_to_check if value_to_check is not None else default_value diff --git a/tests/antares/services/local_services/test_area.py b/tests/antares/services/local_services/test_area.py index 67b59c70..38f93680 100644 --- a/tests/antares/services/local_services/test_area.py +++ b/tests/antares/services/local_services/test_area.py @@ -225,14 +225,11 @@ def test_list_ini_has_multiple_clusters( ): # Given local_study_w_thermal.get_areas()["fr"].create_thermal_cluster("test thermal cluster two") - expected_list_ini_dict = ThermalClusterPropertiesLocal( - thermal_name="test thermal cluster", thermal_cluster_properties=default_thermal_cluster_properties - ).list_ini_fields - expected_list_ini_dict.update( - ThermalClusterPropertiesLocal( - thermal_name="test thermal cluster two", thermal_cluster_properties=default_thermal_cluster_properties - ).list_ini_fields - ) + 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) @@ -251,20 +248,13 @@ def test_clusters_are_alphabetical_in_list_ini( first_cluster_alphabetically = "a is before b and t" second_cluster_alphabetically = "b is after a" - expected_list_ini_dict = ThermalClusterPropertiesLocal( - thermal_name=first_cluster_alphabetically, thermal_cluster_properties=default_thermal_cluster_properties - ).list_ini_fields - expected_list_ini_dict.update( - ThermalClusterPropertiesLocal( - thermal_name=second_cluster_alphabetically, - thermal_cluster_properties=default_thermal_cluster_properties, - ).list_ini_fields - ) - expected_list_ini_dict.update( - ThermalClusterPropertiesLocal( - thermal_name="test thermal cluster", thermal_cluster_properties=default_thermal_cluster_properties - ).list_ini_fields - ) + 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) @@ -341,12 +331,11 @@ def test_renewable_list_ini_has_correct_default_values( def test_renewable_cluster_and_ini_have_custom_properties(self, local_study_w_thermal, actual_renewable_list_ini): # Given - custom_properties = RenewableClusterPropertiesLocal( - "renewable cluster", - RenewableClusterProperties( - group=RenewableClusterGroup.WIND_OFF_SHORE, ts_interpretation=TimeSeriesInterpretation.PRODUCTION_FACTOR - ), + props = 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) expected_renewables_list_ini_content = """[renewable cluster] name = renewable cluster group = Wind Offshore @@ -432,10 +421,9 @@ def test_st_storage_list_ini_has_correct_default_values( def test_st_storage_and_ini_have_custom_properties(self, local_study_with_st_storage, actual_st_storage_list_ini): # Given - custom_properties = STStoragePropertiesLocal( - "short term storage", - STStorageProperties(group=STStorageGroup.BATTERY, reservoir_capacity=12.345), - ) + props = STStorageProperties(group=STStorageGroup.BATTERY, reservoir_capacity=12.345) + args = {"st_storage_name": "short term storage", **props.model_dump(mode="json", exclude_none=True)} + custom_properties = STStoragePropertiesLocal.model_validate(args) expected_st_storage_list_ini_content = """[short term storage] name = short term storage group = Battery diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index 7727f29f..f3c2f371 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -524,7 +524,7 @@ def test_created_area_has_ui(self, tmp_path, local_study): def test_areas_have_default_properties(self, tmp_path, local_study_w_areas): # Given expected_default_properties = { - "nodal_optimization": { + "nodal optimization": { "non-dispatchable-power": "true", "dispatchable-hydro-power": "true", "other-dispatchable-power": "true", @@ -541,7 +541,8 @@ def test_areas_have_default_properties(self, tmp_path, local_study_w_areas): # When actual_area_properties = local_study_w_areas.get_areas()["fr"].properties - actual_properties = AreaPropertiesLocal(actual_area_properties).model_dump(exclude_none=True) + created_properties = actual_area_properties.model_dump(mode="json", exclude_none=True) + actual_properties = AreaPropertiesLocal.model_validate(created_properties).yield_local_dict() assert expected_default_properties == actual_properties @@ -555,7 +556,7 @@ def test_areas_with_custom_properties(self, tmp_path, local_study): filter_by_year={FilterOption.ANNUAL, FilterOption.ANNUAL, FilterOption.HOURLY, FilterOption.WEEKLY}, ) expected_properties = { - "nodal_optimization": { + "nodal optimization": { "non-dispatchable-power": "true", "dispatchable-hydro-power": "false", "other-dispatchable-power": "true", @@ -572,8 +573,8 @@ def test_areas_with_custom_properties(self, tmp_path, local_study): # When created_area = local_study.create_area(area_name=area_to_create, properties=area_properties) - actual_properties = AreaPropertiesLocal(created_area.properties).model_dump(exclude_none=True) - + created_properties = created_area.properties.model_dump(mode="json", exclude_none=True) + actual_properties = AreaPropertiesLocal.model_validate(created_properties).yield_local_dict() assert expected_properties == actual_properties def test_areas_ini_has_correct_sections(self, actual_thermal_areas_ini): @@ -767,7 +768,7 @@ def test_created_link_has_default_local_properties(self, tmp_path, local_study_w """ expected_ini = ConfigParser() expected_ini.read_string(expected_ini_content) - default_properties = LinkPropertiesLocal(LinkProperties()).yield_link_properties() + default_properties = LinkPropertiesLocal().yield_link_properties() # When area_from, area_to = link_to_create.split("_") @@ -834,7 +835,8 @@ def test_created_link_has_custom_properties(self, tmp_path, local_study_w_areas) # Then assert actual_ini_content == expected_ini_content - assert link_created.properties == LinkPropertiesLocal(link_properties).yield_link_properties() + created_properties = link_properties.model_dump(mode="json", exclude_none=True) + assert link_created.properties == LinkPropertiesLocal.model_validate(created_properties).yield_link_properties() assert expected_ini == actual_ini def test_multiple_links_created_from_same_area(self, tmp_path, local_study_w_areas): @@ -1068,5 +1070,7 @@ def test_created_link_with_custom_ui_values(self, tmp_path, local_study_w_areas) assert isinstance(local_study_w_areas.get_links()[link_to_create].ui, LinkUi) assert actual_ini == expected_ini assert actual_ini_string == expected_ini_string - assert actual_properties == LinkPropertiesLocal(expected_properties).yield_link_properties() - assert actual_ui == LinkUiLocal(expected_ui).yield_link_ui() + created_properties = expected_properties.model_dump(mode="json", exclude_none=True) + assert actual_properties == LinkPropertiesLocal.model_validate(created_properties).yield_link_properties() + created_ui = expected_ui.model_dump(mode="json", exclude_none=True) + assert actual_ui == LinkUiLocal.model_validate(created_ui).yield_link_ui()