diff --git a/src/antares/craft/exceptions/exceptions.py b/src/antares/craft/exceptions/exceptions.py index ec14968a..4e243ab6 100644 --- a/src/antares/craft/exceptions/exceptions.py +++ b/src/antares/craft/exceptions/exceptions.py @@ -238,6 +238,12 @@ def __init__(self, study_name: str, message: str) -> None: super().__init__(self.message) +class StudySettingsReadError(Exception): + def __init__(self, study_name: str, message: str) -> None: + self.message = f"Could not read settings for study {study_name}: " + message + super().__init__(self.message) + + class StudyDeletionError(Exception): def __init__(self, study_id: str, message: str) -> None: self.message = f"Could not delete the study {study_id}: " + message diff --git a/src/antares/craft/model/settings/adequacy_patch.py b/src/antares/craft/model/settings/adequacy_patch.py index 6159c1d1..6a9d3234 100644 --- a/src/antares/craft/model/settings/adequacy_patch.py +++ b/src/antares/craft/model/settings/adequacy_patch.py @@ -9,12 +9,9 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. - +from dataclasses import dataclass from enum import Enum - -from antares.craft.tools.all_optional_meta import all_optional_model -from pydantic import BaseModel, ConfigDict, Field -from pydantic.alias_generators import to_camel +from typing import Optional class PriceTakingOrder(Enum): @@ -22,46 +19,14 @@ class PriceTakingOrder(Enum): LOAD = "Load" -class DefaultAdequacyPatchParameters(BaseModel, populate_by_name=True, alias_generator=to_camel): - model_config = ConfigDict(use_enum_values=True) - - # version 830 - enable_adequacy_patch: bool = False - ntc_from_physical_areas_out_to_physical_areas_in_adequacy_patch: bool = True - ntc_between_physical_areas_out_adequacy_patch: bool = True - # version 850 - price_taking_order: PriceTakingOrder = Field(default=PriceTakingOrder.DENS, validate_default=True) - include_hurdle_cost_csr: bool = False - check_csr_cost_function: bool = False - enable_first_step: bool = False - threshold_initiate_curtailment_sharing_rule: int = 0 - threshold_display_local_matching_rule_violations: int = 0 - threshold_csr_variable_bounds_relaxation: int = 3 - - -@all_optional_model -class AdequacyPatchParameters(DefaultAdequacyPatchParameters): - pass - - -class AdequacyPatchParametersLocal(DefaultAdequacyPatchParameters): - @property - def ini_fields(self) -> dict: - return { - "adequacy patch": { - "include-adq-patch": str(self.enable_adequacy_patch).lower(), - "set-to-null-ntc-from-physical-out-to-physical-in-for-first-step": str( - self.ntc_from_physical_areas_out_to_physical_areas_in_adequacy_patch - ).lower(), - "set-to-null-ntc-between-physical-out-for-first-step": str( - self.ntc_between_physical_areas_out_adequacy_patch - ).lower(), - "enable-first-step": str(self.enable_first_step).lower(), - "price-taking-order": self.price_taking_order, - "include-hurdle-cost-csr": str(self.include_hurdle_cost_csr).lower(), - "check-csr-cost-function": str(self.check_csr_cost_function).lower(), - "threshold-initiate-curtailment-sharing-rule": f"{self.threshold_initiate_curtailment_sharing_rule:.6f}", - "threshold-display-local-matching-rule-violations": f"{self.threshold_display_local_matching_rule_violations:.6f}", - "threshold-csr-variable-bounds-relaxation": f"{self.threshold_csr_variable_bounds_relaxation}", - } - } +@dataclass +class AdequacyPatchParameters: + include_adq_patch: Optional[bool] = None + set_to_null_ntc_from_physical_out_to_physical_in_for_first_step: Optional[bool] = None + set_to_null_ntc_between_physical_out_for_first_step: Optional[bool] = None + price_taking_order: Optional[PriceTakingOrder] = None + include_hurdle_cost_csr: Optional[bool] = None + check_csr_cost_function: Optional[bool] = None + threshold_initiate_curtailment_sharing_rule: Optional[int] = None + threshold_display_local_matching_rule_violations: Optional[int] = None + threshold_csr_variable_bounds_relaxation: Optional[int] = None diff --git a/src/antares/craft/model/settings/advanced_parameters.py b/src/antares/craft/model/settings/advanced_parameters.py index b145ef1f..682cec66 100644 --- a/src/antares/craft/model/settings/advanced_parameters.py +++ b/src/antares/craft/model/settings/advanced_parameters.py @@ -9,16 +9,11 @@ # 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 Any, Optional +from typing import Optional from antares.craft.model.settings.general import OutputChoices -from antares.craft.tools.alias_generators import to_kebab -from antares.craft.tools.all_optional_meta import all_optional_model -from pydantic import BaseModel, ConfigDict, Field, model_validator -from pydantic.alias_generators import to_camel -from typing_extensions import Self class InitialReservoirLevel(Enum): @@ -47,10 +42,6 @@ class SheddingPolicy(Enum): MINIMIZE_DURATION = "minimize duration" -class ReserveManagement(Enum): - GLOBAL = "global" - - class UnitCommitmentMode(Enum): FAST = "fast" ACCURATE = "accurate" @@ -70,81 +61,25 @@ class RenewableGenerationModeling(Enum): CLUSTERS = "clusters" -class DefaultAdvancedParameters(BaseModel, alias_generator=to_camel): - model_config = ConfigDict(use_enum_values=True) - - # Advanced parameters +@dataclass +class AdvancedParameters: + initial_reservoir_levels: Optional[InitialReservoirLevel] = None + hydro_heuristic_policy: Optional[HydroHeuristicPolicy] = None + hydro_pricing_mode: Optional[HydroPricingMode] = None + power_fluctuations: Optional[PowerFluctuation] = None + shedding_policy: Optional[SheddingPolicy] = None + unit_commitment_mode: Optional[UnitCommitmentMode] = None + number_of_cores_mode: Optional[SimulationCore] = None + renewable_generation_modelling: Optional[RenewableGenerationModeling] = None accuracy_on_correlation: Optional[set[OutputChoices]] = None - # Other preferences - initial_reservoir_levels: InitialReservoirLevel = Field( - default=InitialReservoirLevel.COLD_START, validate_default=True - ) - hydro_heuristic_policy: HydroHeuristicPolicy = Field( - default=HydroHeuristicPolicy.ACCOMMODATE_RULES_CURVES, validate_default=True - ) - hydro_pricing_mode: HydroPricingMode = Field(default=HydroPricingMode.FAST, validate_default=True) - power_fluctuations: PowerFluctuation = Field(default=PowerFluctuation.FREE_MODULATIONS, validate_default=True) - shedding_policy: SheddingPolicy = Field(default=SheddingPolicy.SHAVE_PEAKS, validate_default=True) - unit_commitment_mode: UnitCommitmentMode = Field(default=UnitCommitmentMode.FAST, validate_default=True) - number_of_cores_mode: SimulationCore = Field(default=SimulationCore.MEDIUM, validate_default=True) - renewable_generation_modelling: RenewableGenerationModeling = Field( - default=RenewableGenerationModeling.AGGREGATED, validate_default=True - ) - # Seeds - seed_tsgen_wind: int = 5489 - seed_tsgen_load: int = 1005489 - seed_tsgen_hydro: int = 2005489 - seed_tsgen_thermal: int = 3005489 - seed_tsgen_solar: int = 4005489 - seed_tsnumbers: int = 5005489 - seed_unsupplied_energy_costs: int = 6005489 - seed_spilled_energy_costs: int = 7005489 - seed_thermal_costs: int = 8005489 - seed_hydro_costs: int = 9005489 - seed_initial_reservoir_levels: int = 10005489 - - -@all_optional_model -class AdvancedParameters(DefaultAdvancedParameters): - @model_validator(mode="before") - def change_accuracy_on_correlation(cls, data: Any) -> Self: - if "accuracyOnCorrelation" in data.keys(): - data["accuracyOnCorrelation"] = ( - {OutputChoices(list_item) for list_item in data["accuracyOnCorrelation"].replace(" ", "").split(",")} - if data["accuracyOnCorrelation"] - else None - ) - return data - - -class AdvancedParametersLocal(DefaultAdvancedParameters, alias_generator=to_kebab): - @property - def ini_fields(self) -> dict: - return { - "other preferences": { - "initial-reservoir-levels": self.initial_reservoir_levels, - "hydro-heuristic-policy": self.hydro_heuristic_policy, - "hydro-pricing-mode": self.hydro_pricing_mode, - "power-fluctuations": self.power_fluctuations, - "shedding-policy": self.shedding_policy, - "unit-commitment-mode": self.unit_commitment_mode, - "number-of-cores-mode": self.number_of_cores_mode, - "renewable-generation-modelling": self.renewable_generation_modelling, - }, - "advanced parameters": { - "accuracy-on-correlation": self.accuracy_on_correlation if self.accuracy_on_correlation else "", - }, - "seeds - Mersenne Twister": { - "seed-tsgen-wind": str(self.seed_tsgen_wind), - "seed-tsgen-load": str(self.seed_tsgen_load), - "seed-tsgen-hydro": str(self.seed_tsgen_hydro), - "seed-tsgen-thermal": str(self.seed_tsgen_thermal), - "seed-tsgen-solar": str(self.seed_tsgen_solar), - "seed-tsnumbers": str(self.seed_tsnumbers), - "seed-unsupplied-energy-costs": str(self.seed_unsupplied_energy_costs), - "seed-spilled-energy-costs": str(self.seed_spilled_energy_costs), - "seed-thermal-costs": str(self.seed_thermal_costs), - "seed-hydro-costs": str(self.seed_hydro_costs), - "seed-initial-reservoir-levels": str(self.seed_initial_reservoir_levels), - }, - } + + +@dataclass +class SeedParameters: + seed_tsgen_thermal: Optional[int] = None + seed_tsnumbers: Optional[int] = None + seed_unsupplied_energy_costs: Optional[int] = None + seed_spilled_energy_costs: Optional[int] = None + seed_thermal_costs: Optional[int] = None + seed_hydro_costs: Optional[int] = None + seed_initial_reservoir_levels: Optional[int] = None diff --git a/src/antares/craft/model/settings/general.py b/src/antares/craft/model/settings/general.py index de4e991b..ea5bcc5b 100644 --- a/src/antares/craft/model/settings/general.py +++ b/src/antares/craft/model/settings/general.py @@ -9,44 +9,40 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. +from dataclasses import dataclass +from typing import Optional -import typing as t - -from antares.craft.tools.all_optional_meta import all_optional_model from antares.craft.tools.contents_tool import EnumIgnoreCase -from pydantic import BaseModel, ConfigDict, Field, field_validator -from pydantic.alias_generators import to_camel class Mode(EnumIgnoreCase): - ECONOMY = "economy" - ADEQUACY = "adequacy" - DRAFT = "draft" + ECONOMY = "Economy" + ADEQUACY = "Adequacy" class Month(EnumIgnoreCase): - JANUARY = "january" - FEBRUARY = "february" - MARCH = "march" - APRIL = "april" - MAY = "may" - JUNE = "june" - JULY = "july" - AUGUST = "august" - SEPTEMBER = "september" - OCTOBER = "october" - NOVEMBER = "november" - DECEMBER = "december" + JANUARY = "January" + FEBRUARY = "February" + MARCH = "March" + APRIL = "April" + MAY = "May" + JUNE = "June" + JULY = "July" + AUGUST = "August" + SEPTEMBER = "September" + OCTOBER = "October" + NOVEMBER = "November" + DECEMBER = "December" class WeekDay(EnumIgnoreCase): - MONDAY = "monday" - TUESDAY = "tuesday" - WEDNESDAY = "wednesday" - THURSDAY = "thursday" - FRIDAY = "friday" - SATURDAY = "saturday" - SUNDAY = "sunday" + MONDAY = "Monday" + TUESDAY = "Tuesday" + WEDNESDAY = "Wednesday" + THURSDAY = "Thursday" + FRIDAY = "Friday" + SATURDAY = "Saturday" + SUNDAY = "Sunday" class BuildingMode(EnumIgnoreCase): @@ -71,74 +67,22 @@ class OutputFormat(EnumIgnoreCase): ZIP = "zip-files" -class DefaultGeneralParameters(BaseModel, extra="forbid", populate_by_name=True, alias_generator=to_camel): - model_config = ConfigDict(use_enum_values=True) - - mode: Mode = Field(default=Mode.ECONOMY, validate_default=True) - horizon: str = "" - # Calendar parameters - nb_years: int = 1 - first_day: int = 1 - last_day: int = 365 - first_january: WeekDay = Field(default=WeekDay.MONDAY, validate_default=True) - first_month: Month = Field(default=Month.JANUARY, validate_default=True) - first_week_day: WeekDay = Field(default=WeekDay.MONDAY, validate_default=True) - leap_year: bool = False - # Additional parameters - year_by_year: bool = False - building_mode: BuildingMode = Field( - default=BuildingMode.AUTOMATIC, validate_default=True - ) # ? derated and custom-scenario - selection_mode: bool = False # ? user-playlist - thematic_trimming: bool = False - geographic_trimming: bool = False - active_rules_scenario: str = "default ruleset" # only one option available currently - read_only: bool = False - # Output parameters - simulation_synthesis: bool = True # ? output/synthesis - mc_scenario: bool = False # ? output/storenewset - result_format: OutputFormat = Field(default=OutputFormat.TXT, exclude=True) - - @field_validator("horizon", mode="before") - def transform_horizon_to_str(cls, val: t.Union[str, int, None]) -> t.Optional[str]: - # horizon can be an int. - return str(val) if val else val # type: ignore - - -@all_optional_model -class GeneralParameters(DefaultGeneralParameters): - pass - - -class GeneralParametersLocal(DefaultGeneralParameters): - @property - def ini_fields(self) -> dict: - return { - "general": { - "mode": str(self.mode).title(), - "horizon": self.horizon, - "nbyears": str(self.nb_years), - "simulation.start": str(self.first_day), - "simulation.end": str(self.last_day), - "january.1st": str(self.first_january).title(), - "first-month-in-year": str(self.first_month).title(), - "first.weekday": str(self.first_week_day).title(), - "leapyear": str(self.leap_year).lower(), - "year-by-year": str(self.year_by_year).lower(), - "derated": str(self.building_mode == BuildingMode.DERATED).lower(), - "custom-scenario": str(self.building_mode == BuildingMode.CUSTOM).lower(), - "user-playlist": str(self.selection_mode).lower(), - "thematic-trimming": str(self.thematic_trimming).lower(), - "geographic-trimming": str(self.geographic_trimming).lower(), - "readonly": str(self.read_only).lower(), - }, - "input": {}, - "output": { - "synthesis": str(self.simulation_synthesis).lower(), - "storenewset": str(self.mc_scenario).lower(), - "result-format": self.result_format.value, - }, - } - - def yield_properties(self) -> GeneralParameters: - return GeneralParameters.model_validate(self.model_dump(exclude_none=True)) +@dataclass +class GeneralParameters: + mode: Optional[Mode] = None + horizon: Optional[str] = None + nb_years: Optional[int] = None + simulation_start: Optional[int] = None + simulation_end: Optional[int] = None + january_first: Optional[WeekDay] = None + first_month_in_year: Optional[Month] = None + first_week_day: Optional[WeekDay] = None + leap_year: Optional[bool] = None + year_by_year: Optional[bool] = None + simulation_synthesis: Optional[bool] = None + building_mode: Optional[BuildingMode] = None + user_playlist: Optional[bool] = None + thematic_trimming: Optional[bool] = None + geographic_trimming: Optional[bool] = None + store_new_set: Optional[bool] = None + nb_timeseries_thermal: Optional[int] = None diff --git a/src/antares/craft/model/settings/optimization.py b/src/antares/craft/model/settings/optimization.py index 8154786f..ef519911 100644 --- a/src/antares/craft/model/settings/optimization.py +++ b/src/antares/craft/model/settings/optimization.py @@ -9,17 +9,9 @@ # 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 Union - -from antares.craft.tools.all_optional_meta import all_optional_model -from pydantic import BaseModel, ConfigDict, Field -from pydantic.alias_generators import to_camel - - -class LegacyTransmissionCapacities(Enum): - INFINITE = "infinite" +from typing import Optional class OptimizationTransmissionCapacities(Enum): @@ -43,60 +35,24 @@ class SimplexOptimizationRange(Enum): class ExportMPS(Enum): - FALSE = False NONE = "none" OPTIM1 = "optim1" OPTIM2 = "optim2" BOTH_OPTIMS = "both-optims" - TRUE = True - - -class DefaultOptimizationParameters(BaseModel, alias_generator=to_camel): - model_config = ConfigDict(use_enum_values=True) - - simplex_optimization_range: SimplexOptimizationRange = Field( - default=SimplexOptimizationRange.WEEK, validate_default=True - ) - transmission_capacities: Union[bool, Union[LegacyTransmissionCapacities, OptimizationTransmissionCapacities]] = ( - Field(default=OptimizationTransmissionCapacities.LOCAL_VALUES, validate_default=True) - ) - binding_constraints: bool = True - hurdle_costs: bool = True - thermal_clusters_min_stable_power: bool = True - thermal_clusters_min_ud_time: bool = True - day_ahead_reserve: bool = True - strategic_reserve: bool = True - spinning_reserve: bool = True - primary_reserve: bool = True - export_mps: ExportMPS = Field(default=ExportMPS.NONE, validate_default=True) - include_exportstructure: bool = False - unfeasible_problem_behavior: UnfeasibleProblemBehavior = Field( - default=UnfeasibleProblemBehavior.ERROR_VERBOSE, validate_default=True - ) - - -@all_optional_model -class OptimizationParameters(DefaultOptimizationParameters): - pass -class OptimizationParametersLocal(DefaultOptimizationParameters): - @property - def ini_fields(self) -> dict: - return { - "optimization": { - "simplex-range": self.simplex_optimization_range, - "transmission-capacities": self.transmission_capacities, - "include-constraints": str(self.binding_constraints).lower(), - "include-hurdlecosts": str(self.hurdle_costs).lower(), - "include-tc-minstablepower": str(self.thermal_clusters_min_stable_power).lower(), - "include-tc-min-ud-time": str(self.thermal_clusters_min_ud_time).lower(), - "include-dayahead": str(self.day_ahead_reserve).lower(), - "include-strategicreserve": str(self.primary_reserve).lower(), - "include-spinningreserve": str(self.spinning_reserve).lower(), - "include-primaryreserve": str(self.primary_reserve).lower(), - "include-exportmps": self.export_mps, - "include-exportstructure": str(self.include_exportstructure).lower(), - "include-unfeasible-problem-behavior": self.unfeasible_problem_behavior, - } - } +@dataclass +class OptimizationParameters: + simplex_range: Optional[SimplexOptimizationRange] = None + transmission_capacities: Optional[OptimizationTransmissionCapacities] = None + include_constraints: Optional[bool] = None + include_hurdlecosts: Optional[bool] = None + include_tc_minstablepower: Optional[bool] = None + include_tc_min_ud_time: Optional[bool] = None + include_dayahead: Optional[bool] = None + include_strategicreserve: Optional[bool] = None + include_spinningreserve: Optional[bool] = None + include_primaryreserve: Optional[bool] = None + include_exportmps: Optional[ExportMPS] = None + include_exportstructure: Optional[bool] = None + include_unfeasible_problem_behavior: Optional[UnfeasibleProblemBehavior] = None diff --git a/src/antares/craft/model/settings/playlist_parameters.py b/src/antares/craft/model/settings/playlist_parameters.py index fa00e767..38bf77a0 100644 --- a/src/antares/craft/model/settings/playlist_parameters.py +++ b/src/antares/craft/model/settings/playlist_parameters.py @@ -9,83 +9,10 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. +from dataclasses import dataclass -from typing import Any -from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator, model_serializer, model_validator - - -class PlaylistData(BaseModel): +@dataclass +class PlaylistParameters: status: bool = True weight: float = 1.0 - - -class PlaylistParameters(BaseModel): - """ - Parameters for playlists. - - Attributes: - playlist (list[PlaylistData]): A list of years (in **PlaylistData** format) in the playlist - """ - - model_config = ConfigDict(validate_assignment=True) - - playlist: list[PlaylistData] = Field(default=[], exclude=True) - - @field_validator("playlist", mode="before") - @classmethod - def playlist_is_list(cls, value: Any) -> list[PlaylistData]: - if isinstance(value, PlaylistParameters): - ret_val = [PlaylistData.model_validate(year) for year in value.mc_years.values()] - elif isinstance(value, list): - ret_val = [PlaylistData.model_validate(year) for year in value] - else: - raise ValueError("Not a valid playlist.") - return ret_val - - @model_validator(mode="before") - @classmethod - def handle_dict_validation(cls, data: Any) -> Any: - return_value = data - if isinstance(data, dict): - if "playlist" in data.keys() and isinstance(data["playlist"], dict): - try: - playlist = [PlaylistData.model_validate(year) for year in data["playlist"]["mc_years"].values()] - except KeyError: - try: - playlist = [PlaylistData.model_validate(year) for year in data["playlist"].values()] - except ValidationError: - raise ValueError("Not a valid playlist dictionary.") - return_value = {"playlist": playlist} - elif "mc_years" in data.keys(): - playlist = [PlaylistData.model_validate(year) for year in data["mc_years"].values()] - return_value = {"playlist": playlist} - return return_value - - # Custom serializer is necessary to preserve compatibility with AntaREST - @model_serializer - def ser_mod(self) -> dict[int, PlaylistData]: - return self.mc_years - - @property - def playlist_reset(self) -> bool: - return sum([year.status for year in self.playlist]) > (len(self.playlist) / 2) - - @property - def mc_years(self) -> dict[int, PlaylistData]: - return {year + 1: self.playlist[year] for year in range(len(self.playlist))} - - @property - def ini_fields(self) -> dict: - playlist_years = repr( - [str(year) for year, year_obj in enumerate(self.playlist) if year_obj.status ^ self.playlist_reset] - ) - - playlist_year_dict = {"playlist_year " + ("-" if self.playlist_reset else "+"): playlist_years} - - return { - "playlist": { - "playlist_reset": str(self.playlist_reset).lower(), - } - | playlist_year_dict - } diff --git a/src/antares/craft/model/settings/study_settings.py b/src/antares/craft/model/settings/study_settings.py index d243f942..2cb48355 100644 --- a/src/antares/craft/model/settings/study_settings.py +++ b/src/antares/craft/model/settings/study_settings.py @@ -9,64 +9,23 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. +from dataclasses import dataclass from typing import Optional -from antares.craft.model.settings.adequacy_patch import AdequacyPatchParametersLocal, DefaultAdequacyPatchParameters -from antares.craft.model.settings.advanced_parameters import AdvancedParametersLocal, DefaultAdvancedParameters -from antares.craft.model.settings.general import DefaultGeneralParameters, GeneralParametersLocal -from antares.craft.model.settings.optimization import DefaultOptimizationParameters, OptimizationParametersLocal +from antares.craft.model.settings.adequacy_patch import AdequacyPatchParameters +from antares.craft.model.settings.advanced_parameters import AdvancedParameters, SeedParameters +from antares.craft.model.settings.general import GeneralParameters +from antares.craft.model.settings.optimization import OptimizationParameters from antares.craft.model.settings.playlist_parameters import PlaylistParameters -from antares.craft.model.settings.thematic_trimming import ( - DefaultThematicTrimmingParameters, - ThematicTrimmingParametersLocal, -) -from antares.craft.model.settings.time_series import DefaultTimeSeriesParameters, TimeSeriesParametersLocal -from antares.craft.tools.all_optional_meta import all_optional_model -from antares.craft.tools.ini_tool import get_ini_fields_for_ini -from pydantic import BaseModel, model_serializer - - -class DefaultStudySettings(BaseModel): - general_parameters: DefaultGeneralParameters = DefaultGeneralParameters() - # These parameters are listed under the [variables selection] section in the .ini file. - # They are required if thematic-trimming is set to true. - # https://antares-simulator.readthedocs.io/en/latest/user-guide/solver/04-parameters/#variables-selection-parameters - time_series_parameters: DefaultTimeSeriesParameters = DefaultTimeSeriesParameters() - # These parameters are listed under the [general] section in the .ini file. - # https://antares-simulator.readthedocs.io/en/latest/user-guide/ts-generator/04-parameters/ - optimization_parameters: DefaultOptimizationParameters = DefaultOptimizationParameters() - adequacy_patch_parameters: DefaultAdequacyPatchParameters = DefaultAdequacyPatchParameters() - advanced_parameters: DefaultAdvancedParameters = DefaultAdvancedParameters() - playlist_parameters: Optional[PlaylistParameters] = None - thematic_trimming_parameters: Optional[DefaultThematicTrimmingParameters] = None - - -@all_optional_model -class StudySettings(DefaultStudySettings): - pass - - -class StudySettingsLocal(DefaultStudySettings): - general_parameters: GeneralParametersLocal = GeneralParametersLocal() - time_series_parameters: TimeSeriesParametersLocal = TimeSeriesParametersLocal() - optimization_parameters: OptimizationParametersLocal = OptimizationParametersLocal() - adequacy_patch_parameters: AdequacyPatchParametersLocal = AdequacyPatchParametersLocal() - advanced_parameters: AdvancedParametersLocal = AdvancedParametersLocal() - thematic_trimming_parameters: Optional[ThematicTrimmingParametersLocal] = None - - @model_serializer - def serialize(self) -> dict: - output_dict = get_ini_fields_for_ini(self) - return self._sort_fields_last(output_dict) - - @staticmethod - def _sort_fields_last(output_dict: dict) -> dict: - new_general = {key: value for key, value in output_dict["general"].items() if key != "readonly"} | { - "readonly": output_dict["general"]["readonly"] - } - new_output = {key: value for key, value in output_dict["output"].items() if key != "result-format"} | { - "result-format": output_dict["output"]["result-format"] - } - output_dict["general"] = new_general - output_dict["output"] = new_output - return output_dict +from antares.craft.model.settings.thematic_trimming import ThematicTrimmingParameters + + +@dataclass +class StudySettings: + general_parameters: Optional[GeneralParameters] = None + optimization_parameters: Optional[OptimizationParameters] = None + advanced_parameters: Optional[AdvancedParameters] = None + seed_parameters: Optional[SeedParameters] = None + adequacy_patch_parameters: Optional[AdequacyPatchParameters] = None + playlist_parameters: Optional[dict[int, PlaylistParameters]] = None + thematic_trimming_parameters: Optional[ThematicTrimmingParameters] = None diff --git a/src/antares/craft/model/settings/thematic_trimming.py b/src/antares/craft/model/settings/thematic_trimming.py index 8d1460fd..7b8f3270 100644 --- a/src/antares/craft/model/settings/thematic_trimming.py +++ b/src/antares/craft/model/settings/thematic_trimming.py @@ -9,239 +9,103 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. -from enum import Enum +from dataclasses import dataclass +from typing import Optional -from antares.craft.tools.all_optional_meta import all_optional_model -from pydantic import BaseModel -from pydantic.alias_generators import to_camel - -class DefaultThematicTrimmingParameters(BaseModel, alias_generator=to_camel): - """ - This class manages the configuration of result filtering in a simulation. - - This table allows the user to enable or disable specific variables before running a simulation. - """ - - ov_cost: bool = True - op_cost: bool = True - mrg_price: bool = True - co2_emis: bool = True - dtg_by_plant: bool = True - balance: bool = True - row_bal: bool = True - psp: bool = True - misc_ndg: bool = True - load: bool = True - h_ror: bool = True - wind: bool = True - solar: bool = True - nuclear: bool = True - lignite: bool = True - coal: bool = True - gas: bool = True - oil: bool = True - mix_fuel: bool = True - misc_dtg: bool = True - h_stor: bool = True - h_pump: bool = True - h_lev: bool = True - h_infl: bool = True - h_ovfl: bool = True - h_val: bool = True - h_cost: bool = True - unsp_enrg: bool = True - spil_enrg: bool = True - lold: bool = True - lolp: bool = True - avl_dtg: bool = True - dtg_mrg: bool = True - max_mrg: bool = True - np_cost: bool = True - np_cost_by_plant: bool = True - nodu: bool = True - nodu_by_plant: bool = True - flow_lin: bool = True - ucap_lin: bool = True - loop_flow: bool = True - flow_quad: bool = True - cong_fee_alg: bool = True - cong_fee_abs: bool = True - marg_cost: bool = True - cong_prob_plus: bool = True - cong_prob_minus: bool = True - hurdle_cost: bool = True - # since v8.1 - res_generation_by_plant: bool = True - misc_dtg_2: bool = True - misc_dtg_3: bool = True - misc_dtg_4: bool = True - wind_offshore: bool = True - wind_onshore: bool = True - solar_concrt: bool = True - solar_pv: bool = True - solar_rooft: bool = True - renw_1: bool = True - renw_2: bool = True - renw_3: bool = True - renw_4: bool = True - # since v8.3 - dens: bool = True - profit_by_plant: bool = True - # since v8.6 - sts_inj_by_plant: bool = True - sts_withdrawal_by_plant: bool = True - sts_lvl_by_plant: bool = True - psp_open_injection: bool = True - psp_open_withdrawal: bool = True - psp_open_level: bool = True - psp_closed_injection: bool = True - psp_closed_withdrawal: bool = True - psp_closed_level: bool = True - pondage_injection: bool = True - pondage_withdrawal: bool = True - pondage_level: bool = True - battery_injection: bool = True - battery_withdrawal: bool = True - battery_level: bool = True - other1_injection: bool = True - other1_withdrawal: bool = True - other1_level: bool = True - other2_injection: bool = True - other2_withdrawal: bool = True - other2_level: bool = True - other3_injection: bool = True - other3_withdrawal: bool = True - other3_level: bool = True - other4_injection: bool = True - other4_withdrawal: bool = True - other4_level: bool = True - other5_injection: bool = True - other5_withdrawal: bool = True - other5_level: bool = True - # since v8.8 - sts_cashflow_by_cluster: bool = True - - @property - def selected_vars_reset(self) -> bool: - return sum([getattr(self, field) for field in self.model_fields]) > (len(self.model_fields) / 2) - - -@all_optional_model -class ThematicTrimmingParameters(DefaultThematicTrimmingParameters): - pass - - -class ThematicTrimmingParametersLocal(DefaultThematicTrimmingParameters, populate_by_name=True): - @property - def ini_fields(self) -> dict: - variable_list = repr( - [ - getattr(ThematicVars, to_camel(variable)).value - for variable in self.model_fields - if getattr(self, variable) ^ self.selected_vars_reset - ] - ) - thematic_trimming_dict = {"select_var " + ("-" if self.selected_vars_reset else "+"): variable_list} - - return { - "variables selection": {"selected_vars_reset": str(self.selected_vars_reset).lower()} - | thematic_trimming_dict - } - - -class ThematicVars(Enum): - balance = "BALANCE" - dens = "DENS" - load = "LOAD" - lold = "LOLD" - lolp = "LOLP" - miscNdg = "MISC. NDG" - mrgPrice = "MRG. PRICE" - opCost = "OP. COST" - ovCost = "OV. COST" - rowBal = "ROW BAL." - spilEnrg = "SPIL. ENRG" - unspEnrg = "UNSP. ENRG" - hCost = "H. COST" - hInfl = "H. INFL" - hLev = "H. LEV" - hOvfl = "H. OVFL" - hPump = "H. PUMP" - hRor = "H. ROR" - hStor = "H. STOR" - hVal = "H. VAL" - psp = "PSP" - renw1 = "RENW. 1" - renw2 = "RENW. 2" - renw3 = "RENW. 3" - renw4 = "RENW. 4" - resGenerationByPlant = "RES generation by plant" - solar = "SOLAR" - solarConcrt = "SOLAR CONCRT." - solarPv = "SOLAR PV" - solarRooft = "SOLAR ROOFT" - wind = "WIND" - windOffshore = "WIND OFFSHORE" - windOnshore = "WIND ONSHORE" - batteryInjection = "BATTERY_INJECTION" - batteryLevel = "BATTERY_LEVEL" - batteryWithdrawal = "BATTERY_WITHDRAWAL" - other1Injection = "OTHER1_INJECTION" - other1Level = "OTHER1_LEVEL" - other1Withdrawal = "OTHER1_WITHDRAWAL" - other2Injection = "OTHER2_INJECTION" - other2Level = "OTHER2_LEVEL" - other2Withdrawal = "OTHER2_WITHDRAWAL" - other3Injection = "OTHER3_INJECTION" - other3Level = "OTHER3_LEVEL" - other3Withdrawal = "OTHER3_WITHDRAWAL" - other4Injection = "OTHER4_INJECTION" - other4Level = "OTHER4_LEVEL" - other4Withdrawal = "OTHER4_WITHDRAWAL" - other5Injection = "OTHER5_INJECTION" - other5Level = "OTHER5_LEVEL" - other5Withdrawal = "OTHER5_WITHDRAWAL" - pondageInjection = "PONDAGE_INJECTION" - pondageLevel = "PONDAGE_LEVEL" - pondageWithdrawal = "PONDAGE_WITHDRAWAL" - pspClosedInjection = "PSP_CLOSED_INJECTION" - pspClosedLevel = "PSP_CLOSED_LEVEL" - pspClosedWithdrawal = "PSP_CLOSED_WITHDRAWAL" - pspOpenInjection = "PSP_OPEN_INJECTION" - pspOpenLevel = "PSP_OPEN_LEVEL" - pspOpenWithdrawal = "PSP_OPEN_WITHDRAWAL" - stsCashflowByCluster = "STS CASHFLOW BY CLUSTER" - stsInjByPlant = "STS inj by plant" - stsLvlByPlant = "STS lvl by plant" - stsWithdrawalByPlant = "STS withdrawal by plant" - avlDtg = "AVL DTG" - co2Emis = "CO2 EMIS." - coal = "COAL" - dtgByPlant = "DTG by plant" - dtgMrg = "DTG MRG" - gas = "GAS" - lignite = "LIGNITE" - maxMrg = "MAX MRG" - miscDtg = "MISC. DTG" - miscDtg2 = "MISC. DTG 2" - miscDtg3 = "MISC. DTG 3" - miscDtg4 = "MISC. DTG 4" - mixFuel = "MIX. FUEL" - nodu = "NODU" - noduByPlant = "NODU by plant" - npCost = "NP COST" - npCostByPlant = "NP Cost by plant" - nuclear = "NUCLEAR" - oil = "OIL" - profitByPlant = "Profit by plant" - congFeeAbs = "CONG. FEE (ABS.)" - congFeeAlg = "CONG. FEE (ALG.)" - congProbMinus = "CONG. PROB -" - congProbPlus = "CONG. PROB +" - flowLin = "FLOW LIN." - flowQuad = "FLOW QUAD." - hurdleCost = "HURDLE COST" - loopFlow = "LOOP FLOW" - margCost = "MARG. COST" - ucapLin = "UCAP LIN." +@dataclass +class ThematicTrimmingParameters: + ov_cost: Optional[bool] = None + op_cost: Optional[bool] = None + mrg_price: Optional[bool] = None + co2_emis: Optional[bool] = None + dtg_by_plant: Optional[bool] = None + balance: Optional[bool] = None + row_bal: Optional[bool] = None + psp: Optional[bool] = None + misc_ndg: Optional[bool] = None + load: Optional[bool] = None + h_ror: Optional[bool] = None + wind: Optional[bool] = None + solar: Optional[bool] = None + nuclear: Optional[bool] = None + lignite: Optional[bool] = None + coal: Optional[bool] = None + gas: Optional[bool] = None + oil: Optional[bool] = None + mix_fuel: Optional[bool] = None + misc_dtg: Optional[bool] = None + h_stor: Optional[bool] = None + h_pump: Optional[bool] = None + h_lev: Optional[bool] = None + h_infl: Optional[bool] = None + h_ovfl: Optional[bool] = None + h_val: Optional[bool] = None + h_cost: Optional[bool] = None + unsp_enrg: Optional[bool] = None + spil_enrg: Optional[bool] = None + lold: Optional[bool] = None + lolp: Optional[bool] = None + avl_dtg: Optional[bool] = None + dtg_mrg: Optional[bool] = None + max_mrg: Optional[bool] = None + np_cost: Optional[bool] = None + np_cost_by_plant: Optional[bool] = None + nodu: Optional[bool] = None + nodu_by_plant: Optional[bool] = None + flow_lin: Optional[bool] = None + ucap_lin: Optional[bool] = None + loop_flow: Optional[bool] = None + flow_quad: Optional[bool] = None + cong_fee_alg: Optional[bool] = None + cong_fee_abs: Optional[bool] = None + marg_cost: Optional[bool] = None + cong_prob_plus: Optional[bool] = None + cong_prob_minus: Optional[bool] = None + hurdle_cost: Optional[bool] = None + res_generation_by_plant: Optional[bool] = None + misc_dtg_2: Optional[bool] = None + misc_dtg_3: Optional[bool] = None + misc_dtg_4: Optional[bool] = None + wind_offshore: Optional[bool] = None + wind_onshore: Optional[bool] = None + solar_concrt: Optional[bool] = None + solar_pv: Optional[bool] = None + solar_rooft: Optional[bool] = None + renw_1: Optional[bool] = None + renw_2: Optional[bool] = None + renw_3: Optional[bool] = None + renw_4: Optional[bool] = None + dens: Optional[bool] = None + profit_by_plant: Optional[bool] = None + sts_inj_by_plant: Optional[bool] = None + sts_withdrawal_by_plant: Optional[bool] = None + sts_lvl_by_plant: Optional[bool] = None + psp_open_injection: Optional[bool] = None + psp_open_withdrawal: Optional[bool] = None + psp_open_level: Optional[bool] = None + psp_closed_injection: Optional[bool] = None + psp_closed_withdrawal: Optional[bool] = None + psp_closed_level: Optional[bool] = None + pondage_injection: Optional[bool] = None + pondage_withdrawal: Optional[bool] = None + pondage_level: Optional[bool] = None + battery_injection: Optional[bool] = None + battery_withdrawal: Optional[bool] = None + battery_level: Optional[bool] = None + other1_injection: Optional[bool] = None + other1_withdrawal: Optional[bool] = None + other1_level: Optional[bool] = None + other2_injection: Optional[bool] = None + other2_withdrawal: Optional[bool] = None + other2_level: Optional[bool] = None + other3_injection: Optional[bool] = None + other3_withdrawal: Optional[bool] = None + other3_level: Optional[bool] = None + other4_injection: Optional[bool] = None + other4_withdrawal: Optional[bool] = None + other4_level: Optional[bool] = None + other5_injection: Optional[bool] = None + other5_withdrawal: Optional[bool] = None + other5_level: Optional[bool] = None + sts_cashflow_by_cluster: Optional[bool] = None diff --git a/src/antares/craft/model/settings/time_series.py b/src/antares/craft/model/settings/time_series.py deleted file mode 100644 index 19381518..00000000 --- a/src/antares/craft/model/settings/time_series.py +++ /dev/null @@ -1,104 +0,0 @@ -# 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 enum import Enum -from typing import Optional - -from antares.craft.tools.all_optional_meta import all_optional_model -from antares.craft.tools.model_tools import filter_out_empty_model_fields -from pydantic import BaseModel, Field -from pydantic.alias_generators import to_camel - - -class SeasonCorrelation(Enum): - MONTHLY = "monthly" - ANNUAL = "annual" - - -class _DefaultParameters(BaseModel, alias_generator=to_camel): - stochastic_ts_status: bool = False - number: int = 1 - refresh: bool = False - refresh_interval: int = 100 - season_correlation: SeasonCorrelation = SeasonCorrelation.ANNUAL - store_in_input: bool = False - store_in_output: bool = False - intra_modal: bool = False - inter_modal: bool = False - - -@all_optional_model -class _Parameters(_DefaultParameters): - pass - - -class _ParametersLocal(_DefaultParameters, populate_by_name=True): - field_name: str = Field(exclude=True) - - -class DefaultTimeSeriesParameters(BaseModel, alias_generator=to_camel): - load: _DefaultParameters = _DefaultParameters() - hydro: _DefaultParameters = _DefaultParameters() - thermal: _DefaultParameters = _DefaultParameters() - wind: _DefaultParameters = _DefaultParameters() - solar: _DefaultParameters = _DefaultParameters() - renewables: Optional[_DefaultParameters] = None - ntc: Optional[_DefaultParameters] = None - - -@all_optional_model -class TimeSeriesParameters(DefaultTimeSeriesParameters): - pass - - -class TimeSeriesParametersLocal(DefaultTimeSeriesParameters): - load: _ParametersLocal = _ParametersLocal(field_name="load") - hydro: _ParametersLocal = _ParametersLocal(field_name="hydro") - thermal: _ParametersLocal = _ParametersLocal(field_name="thermal") - wind: _ParametersLocal = _ParametersLocal(field_name="wind") - solar: _ParametersLocal = _ParametersLocal(field_name="solar") - renewables: Optional[_ParametersLocal] = None - ntc: Optional[_ParametersLocal] = None - - @property - def ini_fields(self) -> dict: - fields_to_check = filter_out_empty_model_fields(self) - general_dict = {} - general_dict["generate"] = ", ".join(self._make_list_from_fields("stochastic_ts_status")) - general_dict |= { - "nbtimeseries" + field_to_add: str(getattr(self, field_to_add).number) for field_to_add in fields_to_check - } - general_dict["refreshtimeseries"] = ", ".join(self._make_list_from_fields("refresh")) - general_dict["intra-modal"] = ", ".join(self._make_list_from_fields("intra_modal")) - general_dict["inter-modal"] = ", ".join(self._make_list_from_fields("inter_modal")) - general_dict |= { - "refreshinterval" + field_to_add: str(getattr(self, field_to_add).refresh_interval) - for field_to_add in fields_to_check - } - input_dict = {"import": ", ".join(self._make_list_from_fields("store_in_input"))} - output_dict = {"archives": ", ".join(self._make_list_from_fields("store_in_output"))} - return {"general": general_dict, "input": input_dict, "output": output_dict} - - def _make_list_from_fields(self, field_to_check: str) -> list: - fields_to_check = filter_out_empty_model_fields(self) - return [ - field_to_add for field_to_add in fields_to_check if getattr(getattr(self, field_to_add), field_to_check) - ] - - -def correlation_defaults(season_correlation: SeasonCorrelation) -> dict[str, dict[str, str]]: - general_section = {"general": {"mode": season_correlation.value}} - annual_section: dict[str, dict] = {"annual": {}} if season_correlation.value == "annual" else {} - extra_sections: dict[str, dict] = ( - {f"{num}": {} for num in range(12)} if season_correlation.value == "annual" else {} - ) - return general_section | annual_section | extra_sections diff --git a/src/antares/craft/model/study.py b/src/antares/craft/model/study.py index 8eb7ce03..83dd2992 100644 --- a/src/antares/craft/model/study.py +++ b/src/antares/craft/model/study.py @@ -38,11 +38,11 @@ ) from antares.craft.model.link import Link, LinkProperties, LinkUi from antares.craft.model.output import Output -from antares.craft.model.settings.study_settings import DefaultStudySettings, StudySettings, StudySettingsLocal -from antares.craft.model.settings.time_series import correlation_defaults +from antares.craft.model.settings.study_settings import StudySettings from antares.craft.model.simulation import AntaresSimulationParameters, Job -from antares.craft.service.api_services.study_api import _returns_study_settings +from antares.craft.service.api_services.services.settings import read_study_settings_api from antares.craft.service.base_services import BaseStudyService +from antares.craft.service.local_services.services.settings import edit_study_settings, read_study_settings_local from antares.craft.service.service_factory import ServiceFactory from antares.craft.tools.ini_tool import IniFile, InitializationFilesTypes @@ -82,8 +82,12 @@ def create_study_api( url = f"{base_url}/studies?name={study_name}&version={version}" response = wrapper.post(url) study_id = response.json() - study_settings = _returns_study_settings(base_url, study_id, wrapper, False, settings) + # Settings part + study_settings = None if settings else read_study_settings_api(base_url, study_id, wrapper) study = Study(study_name, version, ServiceFactory(api_config, study_id), study_settings) + if settings: + study.update_settings(settings) + # Move part if parent_path: study.move(parent_path) url = f"{base_url}/studies/{study_id}" @@ -123,7 +127,7 @@ def create_study_local( study_name: str, version: str, parent_directory: str, - settings: StudySettingsLocal = StudySettingsLocal(), + settings: StudySettings = StudySettings(), ) -> "Study": """ Create a directory structure for the study with empty files. @@ -169,20 +173,16 @@ def create_study_local( with open(desktop_ini_path, "w") as desktop_ini_file: desktop_ini_file.write(desktop_ini_content) - local_settings = StudySettingsLocal.model_validate(settings) - local_settings_file = IniFile(study_directory, InitializationFilesTypes.GENERAL) - local_settings_file.ini_dict = local_settings.model_dump(exclude_none=True, by_alias=True) - local_settings_file.write_ini_file() - # Create various .ini files for the study - _create_correlation_ini_files(local_settings, study_directory) + _create_correlation_ini_files(study_directory) logging.info(f"Study successfully created: {study_name}") + new_settings = edit_study_settings(study_directory, settings, update=False) return Study( name=study_name, version=version, service_factory=ServiceFactory(config=local_config, study_name=study_name), - settings=local_settings, + settings=new_settings, path=study_directory, ) @@ -210,11 +210,14 @@ def _directory_not_exists(local_path: Path) -> None: local_config = LocalConfiguration(study_directory.parent, study_directory.name) + settings = read_study_settings_local(study_directory) + return Study( name=study_params["caption"], version=study_params["version"], service_factory=ServiceFactory(config=local_config, study_name=study_params["caption"]), path=study_directory, + settings=settings, ) @@ -229,7 +232,7 @@ def read_study_api(api_config: APIconf, study_id: str) -> "Study": path = json_study.pop("folder") pure_path = PurePath(path) if path else PurePath(".") - study_settings = _returns_study_settings(base_url, study_id, wrapper, False, None) + study_settings = read_study_settings_api(base_url, study_id, wrapper) study = Study( study_name, study_version, ServiceFactory(api_config, study_id, study_name), study_settings, pure_path ) @@ -262,7 +265,7 @@ def __init__( name: str, version: str, service_factory: ServiceFactory, - settings: Union[StudySettings, StudySettingsLocal, None] = None, + settings: Union[StudySettings, None] = None, path: PurePath = PurePath("."), ): self.name = name @@ -273,7 +276,7 @@ def __init__( self._link_service = service_factory.create_link_service() self._run_service = service_factory.create_run_service() self._binding_constraints_service = service_factory.create_binding_constraints_service() - self._settings = DefaultStudySettings.model_validate(settings if settings is not None else StudySettings()) + self._settings = settings or StudySettings() self._areas: dict[str, Area] = dict() self._links: dict[str, Link] = dict() self._binding_constraints: dict[str, BindingConstraint] = dict() @@ -303,7 +306,7 @@ def get_areas(self) -> MappingProxyType[str, Area]: def get_links(self) -> MappingProxyType[str, Link]: return MappingProxyType(self._links) - def get_settings(self) -> DefaultStudySettings: + def get_settings(self) -> StudySettings: return self._settings def get_binding_constraints(self) -> MappingProxyType[str, BindingConstraint]: @@ -387,9 +390,8 @@ def read_binding_constraints(self) -> list[BindingConstraint]: return constraints def update_settings(self, settings: StudySettings) -> None: - new_settings = self._study_service.update_study_settings(settings) - if new_settings: - self._settings = new_settings + self._study_service.update_study_settings(settings) + self._settings = settings def delete_binding_constraint(self, constraint: BindingConstraint) -> None: self._study_service.delete_binding_constraint(constraint) @@ -505,23 +507,20 @@ def _create_directory_structure(study_path: Path) -> None: (study_path / subdirectory).mkdir(parents=True, exist_ok=True) -def _create_correlation_ini_files(local_settings: StudySettingsLocal, study_directory: Path) -> None: - fields_to_check = ["hydro", "load", "solar", "wind"] +def _create_correlation_ini_files(study_directory: Path) -> None: correlation_inis_to_create = [ - ( - field + "_correlation", - getattr(InitializationFilesTypes, field.upper() + "_CORRELATION_INI"), - field, - ) - for field in fields_to_check + getattr(InitializationFilesTypes, field.upper() + "_CORRELATION_INI") + for field in ["hydro", "load", "solar", "wind"] ] - for correlation, file_type, field in correlation_inis_to_create: + ini_content = {"general": {"mode": "annual"}, "annual": {}} + for k in range(12): + ini_content[str(k)] = {} + + for file_type in correlation_inis_to_create: ini_file = IniFile( study_directory, file_type, - ini_contents=correlation_defaults( - season_correlation=getattr(local_settings.time_series_parameters, field).season_correlation, - ), + ini_contents=ini_content, ) ini_file.write_ini_file() diff --git a/src/antares/craft/service/api_services/models/__init__.py b/src/antares/craft/service/api_services/models/__init__.py new file mode 100644 index 00000000..058c6b22 --- /dev/null +++ b/src/antares/craft/service/api_services/models/__init__.py @@ -0,0 +1,11 @@ +# 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. diff --git a/src/antares/craft/service/api_services/models/settings.py b/src/antares/craft/service/api_services/models/settings.py new file mode 100644 index 00000000..77a5cfa7 --- /dev/null +++ b/src/antares/craft/service/api_services/models/settings.py @@ -0,0 +1,356 @@ +# 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 Optional + +from antares.craft.model.settings.adequacy_patch import AdequacyPatchParameters, PriceTakingOrder +from antares.craft.model.settings.advanced_parameters import ( + AdvancedParameters, + HydroHeuristicPolicy, + HydroPricingMode, + InitialReservoirLevel, + PowerFluctuation, + RenewableGenerationModeling, + SeedParameters, + SheddingPolicy, + SimulationCore, + UnitCommitmentMode, +) +from antares.craft.model.settings.general import ( + BuildingMode, + GeneralParameters, + Mode, + Month, + OutputChoices, + OutputFormat, + WeekDay, +) +from antares.craft.model.settings.optimization import ( + ExportMPS, + OptimizationParameters, + OptimizationTransmissionCapacities, + SimplexOptimizationRange, + UnfeasibleProblemBehavior, +) +from antares.craft.model.settings.thematic_trimming import ThematicTrimmingParameters +from antares.craft.tools.all_optional_meta import all_optional_model +from pydantic import BaseModel, Field +from pydantic.alias_generators import to_camel + + +@all_optional_model +class AdequacyPatchParametersAPI(BaseModel, alias_generator=to_camel): + enable_adequacy_patch: Optional[bool] = None + ntc_from_physical_areas_out_to_physical_areas_in_adequacy_patch: bool = True + ntc_between_physical_areas_out_adequacy_patch: bool = True + price_taking_order: PriceTakingOrder = Field(default=PriceTakingOrder.DENS, validate_default=True) + include_hurdle_cost_csr: bool = False + check_csr_cost_function: bool = False + enable_first_step: bool = False + threshold_initiate_curtailment_sharing_rule: int = 0 + threshold_display_local_matching_rule_violations: int = 0 + threshold_csr_variable_bounds_relaxation: int = 3 + + @staticmethod + def from_user_model(user_class: AdequacyPatchParameters) -> "AdequacyPatchParametersAPI": + user_dict = asdict(user_class) + user_dict["enable_adequacy_patch"] = user_dict.pop("include_adq_patch") + user_dict["ntc_from_physical_areas_out_to_physical_areas_in_adequacy_patch"] = user_dict.pop( + "set_to_null_ntc_from_physical_out_to_physical_in_for_first_step" + ) + user_dict["ntc_between_physical_areas_out_adequacy_patch"] = user_dict.pop( + "set_to_null_ntc_between_physical_out_for_first_step" + ) + return AdequacyPatchParametersAPI.model_validate(user_dict) + + def to_user_model(self) -> AdequacyPatchParameters: + return AdequacyPatchParameters( + include_adq_patch=self.enable_adequacy_patch, + set_to_null_ntc_from_physical_out_to_physical_in_for_first_step=self.ntc_from_physical_areas_out_to_physical_areas_in_adequacy_patch, + set_to_null_ntc_between_physical_out_for_first_step=self.ntc_between_physical_areas_out_adequacy_patch, + price_taking_order=self.price_taking_order, + include_hurdle_cost_csr=self.include_hurdle_cost_csr, + check_csr_cost_function=self.check_csr_cost_function, + threshold_initiate_curtailment_sharing_rule=self.threshold_initiate_curtailment_sharing_rule, + threshold_display_local_matching_rule_violations=self.threshold_display_local_matching_rule_violations, + threshold_csr_variable_bounds_relaxation=self.threshold_csr_variable_bounds_relaxation, + ) + + +@all_optional_model +class AdvancedAndSeedParametersAPI(BaseModel, alias_generator=to_camel): + accuracy_on_correlation: set[OutputChoices] + initial_reservoir_levels: InitialReservoirLevel + hydro_heuristic_policy: HydroHeuristicPolicy + hydro_pricing_mode: HydroPricingMode + power_fluctuations: PowerFluctuation + shedding_policy: SheddingPolicy + unit_commitment_mode: UnitCommitmentMode + number_of_cores_mode: SimulationCore + renewable_generation_modelling: RenewableGenerationModeling + seed_tsgen_wind: int + seed_tsgen_load: int + seed_tsgen_hydro: int + seed_tsgen_thermal: int + seed_tsgen_solar: int + seed_tsnumbers: int + seed_unsupplied_energy_costs: int + seed_spilled_energy_costs: int + seed_thermal_costs: int + seed_hydro_costs: int + seed_initial_reservoir_levels: int + + @staticmethod + def _get_advanced_user_parameters_fields() -> set[str]: + return { + "seed_tsgen_wind", + "seed_tsgen_load", + "seed_tsgen_hydro", + "seed_tsgen_thermal", + "seed_tsgen_solar", + "seed_tsnumbers", + "seed_unsupplied_energy_costs", + "seed_spilled_energy_costs", + "seed_thermal_costs", + "seed_hydro_costs", + "seed_initial_reservoir_levels", + } + + @staticmethod + def from_user_model( + advanced_parameters: Optional[AdvancedParameters] = None, seed_parameters: Optional[SeedParameters] = None + ) -> "AdvancedAndSeedParametersAPI": + advanced_parameters_dict = asdict(advanced_parameters) if advanced_parameters else {} + seed_parameters_dict = asdict(seed_parameters) if seed_parameters else {} + api_dict = advanced_parameters_dict | seed_parameters_dict + return AdvancedAndSeedParametersAPI.model_validate(api_dict) + + def to_user_advanced_parameters_model(self) -> AdvancedParameters: + excluded_fields = self._get_advanced_user_parameters_fields() + return AdvancedParameters(**self.model_dump(mode="json", exclude=excluded_fields)) + + def to_user_seed_parameters_model(self) -> SeedParameters: + included_fields = set(asdict(SeedParameters()).keys()) + return SeedParameters(**self.model_dump(mode="json", include=included_fields)) + + +@all_optional_model +class GeneralParametersAPI(BaseModel, extra="forbid", populate_by_name=True, alias_generator=to_camel): + mode: Mode = Field(default=Mode.ECONOMY, validate_default=True) + horizon: str + nb_years: int + first_day: int + last_day: int + first_january: WeekDay + first_month: Month + first_week_day: WeekDay + leap_year: bool + year_by_year: bool + building_mode: BuildingMode + selection_mode: bool + thematic_trimming: bool + geographic_trimming: bool + active_rules_scenario: str + read_only: bool + simulation_synthesis: bool + mc_scenario: bool + result_format: OutputFormat + + @staticmethod + def from_user_model(user_class: GeneralParameters) -> "GeneralParametersAPI": + user_dict = asdict(user_class) + user_dict["first_day"] = user_dict.pop("simulation_start") + user_dict["last_day"] = user_dict.pop("simulation_end") + user_dict["first_january"] = user_dict.pop("january_first") + user_dict["first_month"] = user_dict.pop("first_month_in_year") + user_dict["selection_mode"] = user_dict.pop("user_playlist") + user_dict["mc_scenario"] = user_dict.pop("store_new_set") + user_dict.pop("nb_timeseries_thermal") + return GeneralParametersAPI.model_validate(user_dict) + + def to_user_model(self, nb_ts_thermal: int) -> GeneralParameters: + return GeneralParameters( + mode=self.mode, + horizon=self.horizon, + nb_years=self.nb_years, + simulation_start=self.first_day, + simulation_end=self.last_day, + january_first=self.first_january, + first_month_in_year=self.first_month, + first_week_day=self.first_week_day, + leap_year=self.leap_year, + year_by_year=self.year_by_year, + simulation_synthesis=self.simulation_synthesis, + building_mode=self.building_mode, + user_playlist=self.selection_mode, + thematic_trimming=self.thematic_trimming, + geographic_trimming=self.geographic_trimming, + store_new_set=self.mc_scenario, + nb_timeseries_thermal=nb_ts_thermal, + ) + + +@all_optional_model +class OptimizationParametersAPI(BaseModel, alias_generator=to_camel): + simplex_optimization_range: SimplexOptimizationRange + transmission_capacities: OptimizationTransmissionCapacities + binding_constraints: bool + hurdle_costs: bool + thermal_clusters_min_stable_power: bool + thermal_clusters_min_ud_time: bool + day_ahead_reserve: bool + strategic_reserve: bool + spinning_reserve: bool + primary_reserve: bool + export_mps: ExportMPS + include_exportstructure: bool + unfeasible_problem_behavior: UnfeasibleProblemBehavior + + @staticmethod + def from_user_model(user_class: OptimizationParameters) -> "OptimizationParametersAPI": + user_dict = asdict(user_class) + user_dict["simplex_optimization_range"] = user_dict.pop("simplex_range") + user_dict["binding_constraints"] = user_dict.pop("include_constraints") + user_dict["hurdle_costs"] = user_dict.pop("include_hurdle_costs") + user_dict["thermal_clusters_min_stable_power"] = user_dict.pop("include_tc_minstablepower") + user_dict["thermal_clusters_min_ud_time"] = user_dict.pop("include_tc_min_ud_time") + user_dict["day_ahead_reserve"] = user_dict.pop("include_dayahead") + user_dict["strategic_reserve"] = user_dict.pop("include_strategicreserve") + user_dict["spinning_reserve"] = user_dict.pop("include_spinningreserve") + user_dict["primary_reserve"] = user_dict.pop("include_primaryreserve") + user_dict["export_mps"] = user_dict.pop("include_exportmps") + user_dict["include_exportstructure"] = user_dict.pop("include_exportstructure") + user_dict["unfeasible_problem_behavior"] = user_dict.pop("include_unfeasible_problem_behavior") + return OptimizationParametersAPI.model_validate(user_dict) + + def to_user_model(self) -> OptimizationParameters: + return OptimizationParameters( + simplex_range=self.simplex_optimization_range, + transmission_capacities=self.transmission_capacities, + include_constraints=self.binding_constraints, + include_hurdlecosts=self.hurdle_costs, + include_tc_minstablepower=self.thermal_clusters_min_stable_power, + include_tc_min_ud_time=self.thermal_clusters_min_ud_time, + include_dayahead=self.day_ahead_reserve, + include_strategicreserve=self.strategic_reserve, + include_spinningreserve=self.spinning_reserve, + include_primaryreserve=self.primary_reserve, + include_exportmps=self.export_mps, + include_exportstructure=self.include_exportstructure, + include_unfeasible_problem_behavior=self.unfeasible_problem_behavior, + ) + + +@all_optional_model +class ThematicTrimmingParametersAPI(BaseModel, alias_generator=to_camel): + ov_cost: bool + op_cost: bool + mrg_price: bool + co2_emis: bool + dtg_by_plant: bool + balance: bool + row_bal: bool + psp: bool + misc_ndg: bool + load: bool + h_ror: bool + wind: bool + solar: bool + nuclear: bool + lignite: bool + coal: bool + gas: bool + oil: bool + mix_fuel: bool + misc_dtg: bool + h_stor: bool + h_pump: bool + h_lev: bool + h_infl: bool + h_ovfl: bool + h_val: bool + h_cost: bool + unsp_enrg: bool + spil_enrg: bool + lold: bool + lolp: bool + avl_dtg: bool + dtg_mrg: bool + max_mrg: bool + np_cost: bool + np_cost_by_plant: bool + nodu: bool + nodu_by_plant: bool + flow_lin: bool + ucap_lin: bool + loop_flow: bool + flow_quad: bool + cong_fee_alg: bool + cong_fee_abs: bool + marg_cost: bool + cong_prob_plus: bool + cong_prob_minus: bool + hurdle_cost: bool + res_generation_by_plant: bool + misc_dtg_2: bool + misc_dtg_3: bool + misc_dtg_4: bool + wind_offshore: bool + wind_onshore: bool + solar_concrt: bool + solar_pv: bool + solar_rooft: bool + renw_1: bool + renw_2: bool + renw_3: bool + renw_4: bool + dens: bool + profit_by_plant: bool + sts_inj_by_plant: bool + sts_withdrawal_by_plant: bool + sts_lvl_by_plant: bool + psp_open_injection: bool + psp_open_withdrawal: bool + psp_open_level: bool + psp_closed_injection: bool + psp_closed_withdrawal: bool + psp_closed_level: bool + pondage_injection: bool + pondage_withdrawal: bool + pondage_level: bool + battery_injection: bool + battery_withdrawal: bool + battery_level: bool + other1_injection: bool + other1_withdrawal: bool + other1_level: bool + other2_injection: bool + other2_withdrawal: bool + other2_level: bool + other3_injection: bool + other3_withdrawal: bool + other3_level: bool + other4_injection: bool + other4_withdrawal: bool + other4_level: bool + other5_injection: bool + other5_withdrawal: bool + other5_level: bool + sts_cashflow_by_cluster: bool + + @staticmethod + def from_user_model(user_class: ThematicTrimmingParameters) -> "ThematicTrimmingParametersAPI": + user_dict = asdict(user_class) + return ThematicTrimmingParametersAPI.model_validate(user_dict) + + def to_user_model(self) -> ThematicTrimmingParameters: + return ThematicTrimmingParameters(**self.model_dump(mode="json")) diff --git a/src/antares/craft/service/api_services/services/__init__.py b/src/antares/craft/service/api_services/services/__init__.py new file mode 100644 index 00000000..058c6b22 --- /dev/null +++ b/src/antares/craft/service/api_services/services/__init__.py @@ -0,0 +1,11 @@ +# 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. diff --git a/src/antares/craft/service/api_services/services/settings.py b/src/antares/craft/service/api_services/services/settings.py new file mode 100644 index 00000000..0d5eb0c3 --- /dev/null +++ b/src/antares/craft/service/api_services/services/settings.py @@ -0,0 +1,136 @@ +# 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 antares.craft.api_conf.request_wrapper import RequestWrapper +from antares.craft.exceptions.exceptions import APIError, StudySettingsReadError +from antares.craft.model.settings.playlist_parameters import PlaylistParameters +from antares.craft.model.settings.study_settings import StudySettings +from antares.craft.service.api_services.models.settings import ( + AdequacyPatchParametersAPI, + AdvancedAndSeedParametersAPI, + GeneralParametersAPI, + OptimizationParametersAPI, + ThematicTrimmingParametersAPI, +) + + +def edit_study_settings(base_url: str, study_id: str, wrapper: RequestWrapper, settings: StudySettings) -> None: + settings_base_url = f"{base_url}/studies/{study_id}/config" + + # thematic trimming + if settings.thematic_trimming_parameters: + thematic_trimming_url = f"{settings_base_url}/thematictrimming/form" + api_model = ThematicTrimmingParametersAPI.from_user_model(settings.thematic_trimming_parameters) + body = api_model.model_dump(mode="json", exclude_unset=True, by_alias=True) + wrapper.put(thematic_trimming_url, json=body) + + # playlist + if settings.playlist_parameters: + playlist_url = f"{settings_base_url}/playlist/form" + body = {} + for key, value in settings.playlist_parameters.items(): + body[str(key)] = asdict(value) + wrapper.put(playlist_url, json=body) + + # optimization + if settings.optimization_parameters: + optimization_url = f"{settings_base_url}/optimization/form" + optimization_api_model = OptimizationParametersAPI.from_user_model(settings.optimization_parameters) + body = optimization_api_model.model_dump(mode="json", exclude_unset=True, by_alias=True) + wrapper.put(optimization_url, json=body) + + # general and timeseries + if settings.general_parameters: + general_url = f"{settings_base_url}/general/form" + general_api_model = GeneralParametersAPI.from_user_model(settings.general_parameters) + body = general_api_model.model_dump(mode="json", exclude_unset=True, by_alias=True) + wrapper.put(general_url, json=body) + + if nb_ts_thermal := settings.general_parameters.nb_timeseries_thermal: + timeseries_url = f"{base_url}/studies/{study_id}/timeseries/config" + wrapper.put(timeseries_url, json={"thermal": {"number": nb_ts_thermal}}) + + # advanced and seed parameters + if settings.advanced_parameters or settings.seed_parameters: + advanced_parameters_url = f"{settings_base_url}/advancedparameters/form" + advanced_api_model = AdvancedAndSeedParametersAPI.from_user_model( + settings.advanced_parameters, settings.seed_parameters + ) + body = advanced_api_model.model_dump(mode="json", exclude_unset=True, by_alias=True) + wrapper.put(advanced_parameters_url, json=body) + + # adequacy patch + if settings.adequacy_patch_parameters: + adequacy_patch_url = f"{settings_base_url}/adequacypatch/form" + adequacy_patch_api_model = AdequacyPatchParametersAPI.from_user_model(settings.adequacy_patch_parameters) + body = adequacy_patch_api_model.model_dump(mode="json", exclude_unset=True, by_alias=True) + wrapper.put(adequacy_patch_url, json=body) + + +def read_study_settings_api(base_url: str, study_id: str, wrapper: RequestWrapper) -> StudySettings: + settings_base_url = f"{base_url}/studies/{study_id}/config" + try: + # thematic trimming + thematic_trimming_url = f"{settings_base_url}/thematictrimming/form" + response = wrapper.get(thematic_trimming_url) + thematic_trimming_api_model = ThematicTrimmingParametersAPI.model_validate(response.json()) + thematic_trimming_parameters = thematic_trimming_api_model.to_user_model() + + # playlist + playlist_url = f"{settings_base_url}/playlist/form" + response = wrapper.get(playlist_url) + json_response = response.json() + user_playlist = {} + for key, value in json_response.items(): + user_playlist[int(key)] = PlaylistParameters(**value) + + # optimization + optimization_url = f"{settings_base_url}/optimization/form" + response = wrapper.get(optimization_url) + optimization_api_model = OptimizationParametersAPI.model_validate(response.json()) + optimization_parameters = optimization_api_model.to_user_model() + + # general and timeseries + general_url = f"{settings_base_url}/general/form" + response = wrapper.get(general_url) + general_api_model = GeneralParametersAPI.model_validate(response.json()) + timeseries_url = f"{base_url}/studies/{study_id}/timeseries/config" + response = wrapper.get(timeseries_url) + nb_ts_thermal = response.json()["thermal"]["number"] + general_parameters = general_api_model.to_user_model(nb_ts_thermal) + + # advanced and seed parameters + advanced_parameters_url = f"{settings_base_url}/advancedparameters/form" + response = wrapper.get(advanced_parameters_url) + advanced_parameters_api_model = AdvancedAndSeedParametersAPI.model_validate(response.json()) + seed_parameters = advanced_parameters_api_model.to_user_seed_parameters_model() + advanced_parameters = advanced_parameters_api_model.to_user_advanced_parameters_model() + + # adequacy patch + adequacy_patch_url = f"{settings_base_url}/adequacypatch/form" + response = wrapper.get(adequacy_patch_url) + adequacy_patch_api_model = AdequacyPatchParametersAPI.model_validate(response.json()) + adequacy_patch_parameters = adequacy_patch_api_model.to_user_model() + + except APIError as e: + raise StudySettingsReadError(study_id, e.message) from e + + return StudySettings( + general_parameters=general_parameters, + optimization_parameters=optimization_parameters, + seed_parameters=seed_parameters, + advanced_parameters=advanced_parameters, + adequacy_patch_parameters=adequacy_patch_parameters, + playlist_parameters=user_playlist, + thematic_trimming_parameters=thematic_trimming_parameters, + ) diff --git a/src/antares/craft/service/api_services/study_api.py b/src/antares/craft/service/api_services/study_api.py index 2a29ef43..39bd44a5 100644 --- a/src/antares/craft/service/api_services/study_api.py +++ b/src/antares/craft/service/api_services/study_api.py @@ -31,14 +31,8 @@ ) from antares.craft.model.binding_constraint import BindingConstraint from antares.craft.model.output import Output -from antares.craft.model.settings.adequacy_patch import AdequacyPatchParameters -from antares.craft.model.settings.advanced_parameters import AdvancedParameters -from antares.craft.model.settings.general import GeneralParameters -from antares.craft.model.settings.optimization import OptimizationParameters -from antares.craft.model.settings.playlist_parameters import PlaylistData, PlaylistParameters from antares.craft.model.settings.study_settings import StudySettings -from antares.craft.model.settings.thematic_trimming import ThematicTrimmingParameters -from antares.craft.model.settings.time_series import TimeSeriesParameters +from antares.craft.service.api_services.services.settings import edit_study_settings from antares.craft.service.api_services.utils import wait_task_completion from antares.craft.service.base_services import BaseOutputService, BaseStudyService @@ -46,43 +40,6 @@ from antares.craft.model.study import Study -def _returns_study_settings( - base_url: str, study_id: str, wrapper: RequestWrapper, update: bool, settings: Optional[StudySettings] -) -> Optional[StudySettings]: - settings_base_url = f"{base_url}/studies/{study_id}/config" - mapping = { - "general_parameters": ("general", GeneralParameters), - "thematic_trimming_parameters": ("thematictrimming", ThematicTrimmingParameters), - "time_series_parameters": ("timeseries", TimeSeriesParameters), - "adequacy_patch_parameters": ("adequacypatch", AdequacyPatchParameters), - "advanced_parameters": ("advancedparameters", AdvancedParameters), - "optimization_parameters": ("optimization", OptimizationParameters), - "playlist_parameters": ("playlist", PlaylistParameters), - } - if settings: - json_settings = settings.model_dump(mode="json", by_alias=True, exclude_none=True) - if not json_settings and update: - return None - - for key, value in json_settings.items(): - url = f"{settings_base_url}/{mapping[key][0]}/form" - wrapper.put(url, json=value) - - json_settings = {} - for settings_type, settings_tuple in mapping.items(): - settings_class = settings_tuple[1] - url = f"{settings_base_url}/{settings_tuple[0]}/form" - response = wrapper.get(url) - if settings_type == "playlist_parameters": - mc_years = [PlaylistData.model_validate(year) for year in response.json().values()] - settings_property = settings_class(playlist=mc_years) if mc_years else None - else: - settings_property = settings_class.model_validate(response.json()) # type: ignore - json_settings[settings_type] = settings_property - - return StudySettings.model_validate(json_settings) - - class StudyApiService(BaseStudyService): def __init__(self, config: APIconf, study_id: str): super().__init__() @@ -107,12 +64,11 @@ def output_service(self) -> Optional[BaseOutputService]: def set_output_service(self, output_service: BaseOutputService) -> None: self._output_service = output_service - def update_study_settings(self, settings: StudySettings) -> Optional[StudySettings]: + def update_study_settings(self, settings: StudySettings) -> None: try: - new_settings = _returns_study_settings(self._base_url, self.study_id, self._wrapper, True, settings) + edit_study_settings(self._base_url, self.study_id, self._wrapper, settings) except APIError as e: raise StudySettingsUpdateError(self.study_id, e.message) from e - return new_settings def delete_binding_constraint(self, constraint: BindingConstraint) -> None: url = f"{self._base_url}/studies/{self.study_id}/bindingconstraints/{constraint.id}" diff --git a/src/antares/craft/service/base_services.py b/src/antares/craft/service/base_services.py index d74ed61f..591f0c31 100644 --- a/src/antares/craft/service/base_services.py +++ b/src/antares/craft/service/base_services.py @@ -564,7 +564,7 @@ def config(self) -> BaseConfiguration: pass @abstractmethod - def update_study_settings(self, settings: StudySettings) -> Optional[StudySettings]: + def update_study_settings(self, settings: StudySettings) -> None: """ Args: settings: new study settings. Only registered fields will be updated. diff --git a/src/antares/craft/service/local_services/models/__init__.py b/src/antares/craft/service/local_services/models/__init__.py new file mode 100644 index 00000000..058c6b22 --- /dev/null +++ b/src/antares/craft/service/local_services/models/__init__.py @@ -0,0 +1,11 @@ +# 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. diff --git a/src/antares/craft/service/local_services/models/settings.py b/src/antares/craft/service/local_services/models/settings.py new file mode 100644 index 00000000..5f68b083 --- /dev/null +++ b/src/antares/craft/service/local_services/models/settings.py @@ -0,0 +1,258 @@ +# 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 ast + +from dataclasses import asdict +from typing import Any, Sequence, Set + +from antares.craft.model.settings.adequacy_patch import AdequacyPatchParameters, PriceTakingOrder +from antares.craft.model.settings.advanced_parameters import ( + AdvancedParameters, + HydroHeuristicPolicy, + HydroPricingMode, + InitialReservoirLevel, + PowerFluctuation, + RenewableGenerationModeling, + SeedParameters, + SheddingPolicy, + SimulationCore, + UnitCommitmentMode, +) +from antares.craft.model.settings.general import BuildingMode, GeneralParameters, Mode, Month, OutputChoices, WeekDay +from antares.craft.model.settings.optimization import ( + OptimizationParameters, + OptimizationTransmissionCapacities, + SimplexOptimizationRange, + UnfeasibleProblemBehavior, +) +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 + + +class AdequacyPatchParametersLocal(LocalBaseModel, alias_generator=to_kebab): + include_adq_patch: bool = False + set_to_null_ntc_from_physical_out_to_physical_in_for_first_step: bool = True + set_to_null_ntc_between_physical_out_for_first_step: bool = True + price_taking_order: PriceTakingOrder = PriceTakingOrder.DENS + include_hurdle_cost_csr: bool = False + check_csr_cost_function: bool = False + threshold_initiate_curtailment_sharing_rule: int = 0 + threshold_display_local_matching_rule_violations: int = 0 + threshold_csr_variable_bounds_relaxation: int = 3 + enable_first_step: bool = False + + @staticmethod + def from_user_model(user_class: AdequacyPatchParameters) -> "AdequacyPatchParametersLocal": + user_dict = asdict(user_class) + return AdequacyPatchParametersLocal.model_validate(user_dict) + + def to_user_model(self) -> AdequacyPatchParameters: + local_dict = self.model_dump(mode="json", by_alias=False, exclude={"enable_first_step"}) + return AdequacyPatchParameters(**local_dict) + + +class OtherPreferencesLocal(LocalBaseModel, alias_generator=to_kebab): + initial_reservoir_levels: InitialReservoirLevel = InitialReservoirLevel.COLD_START + hydro_heuristic_policy: HydroHeuristicPolicy = HydroHeuristicPolicy.ACCOMMODATE_RULES_CURVES + hydro_pricing_mode: HydroPricingMode = HydroPricingMode.FAST + power_fluctuations: PowerFluctuation = PowerFluctuation.FREE_MODULATIONS + shedding_policy: SheddingPolicy = SheddingPolicy.SHAVE_PEAKS + shedding_strategy: Any = "shave margins" + unit_commitment_mode: UnitCommitmentMode = UnitCommitmentMode.FAST + number_of_cores_mode: SimulationCore = SimulationCore.MEDIUM + renewable_generation_modelling: RenewableGenerationModeling = RenewableGenerationModeling.CLUSTERS + day_ahead_reserve_management: Any = "global" + + +class AdvancedParametersLocal(LocalBaseModel, alias_generator=to_kebab): + accuracy_on_correlation: set[OutputChoices] = set() + adequacy_block_size: int = 100 + + @field_validator("accuracy_on_correlation", mode="before") + def validate_accuracy_on_correlation(cls, v: Any) -> Sequence[str]: + """Ensure the ID is lower case.""" + if v is None: + return [] + return ast.literal_eval(v) + + +class SeedParametersLocal(LocalBaseModel, alias_generator=to_kebab): + seed_tsgen_wind: int = 5489 + seed_tsgen_load: int = 1005489 + seed_tsgen_hydro: int = 2005489 + seed_tsgen_thermal: int = 3005489 + seed_tsgen_solar: int = 4005489 + seed_tsnumbers: int = 5005489 + seed_unsupplied_energy_costs: int = 6005489 + seed_spilled_energy_costs: int = 7005489 + seed_thermal_costs: int = 8005489 + seed_hydro_costs: int = 9005489 + seed_initial_reservoir_levels: int = 10005489 + + +class AdvancedAndSeedParametersLocal(LocalBaseModel): + other_preferences: OtherPreferencesLocal = Field(alias="other preferences") + advanced_parameters: AdvancedParametersLocal = Field(alias="advanced parameters") + seeds: SeedParametersLocal = Field(alias="seeds - Mersenne Twister") + + @staticmethod + def from_user_model( + advanced_parameters: AdvancedParameters, seed_parameters: SeedParameters + ) -> "AdvancedAndSeedParametersLocal": + other_preferences_local_dict = asdict(advanced_parameters) + advanced_local_dict = { + "advanced_parameters": { + "accuracy_on_correlation": other_preferences_local_dict.pop("accuracy_on_correlation") + } + } + seed_local_dict = {"seeds": asdict(seed_parameters)} + + local_dict = {"other_preferences": other_preferences_local_dict} | advanced_local_dict | seed_local_dict + return AdvancedAndSeedParametersLocal.model_validate(local_dict) + + def to_seed_parameters_model(self) -> SeedParameters: + seed_values = self.seeds.model_dump(mode="json", by_alias=False, include=set(asdict(SeedParameters()).keys())) + return SeedParameters(**seed_values) + + def to_advanced_parameters_model(self) -> AdvancedParameters: + includes = set(asdict(AdvancedParameters()).keys()) + advanced_values = self.advanced_parameters.model_dump(mode="json", by_alias=False, include=includes) + other_preferences_values = self.other_preferences.model_dump(mode="json", by_alias=False, include=includes) + merged_values = advanced_values | other_preferences_values + return AdvancedParameters(**merged_values) + + +class GeneralSectionLocal(LocalBaseModel): + mode: Mode = Mode.ECONOMY + horizon: str = "" + nb_years: int = Field(default=1, alias="nbyears") + simulation_start: int = Field(default=1, alias="simulation.start") + simulation_end: int = Field(default=365, alias="simulation.end") + january_first: WeekDay = Field(default=WeekDay.MONDAY, alias="january.1st") + first_month_in_year: Month = Field(default=Month.JANUARY, alias="first-month-in-year") + first_week_day: WeekDay = Field(default=WeekDay.MONDAY, alias="first.weekday") + leap_year: bool = Field(default=False, alias="leapyear") + year_by_year: bool = Field(default=False, alias="year-by-year") + building_mode: BuildingMode = BuildingMode.AUTOMATIC + user_playlist: bool = Field(default=False, alias="user-playlist") + thematic_trimming: bool = Field(default=False, alias="thematic-trimming") + geographic_trimming: bool = Field(default=False, alias="geographic-trimming") + generate: bool = False + nb_timeseries_load: int = Field(default=1, alias="nbtimeseriesload") + nb_timeseries_hydro: int = Field(default=1, alias="nbtimeserieshydro") + nb_timeseries_wind: int = Field(default=1, alias="nbtimeserieswind") + nb_timeseries_thermal: int = Field(default=1, alias="nbtimeseriesthermal") + nb_timeseries_solar: int = Field(default=1, alias="nbtimeseriessolar") + refresh_timeseries: bool = Field(default=False, alias="refreshtimeseries") + intra_modal: bool = Field(default=False, alias="intra-modal") + inter_modal: bool = Field(default=False, alias="inter-modal") + refresh_interval_load: int = Field(default=100, alias="refreshintervalload") + refresh_interval_hydro: int = Field(default=100, alias="refreshintervalhydro") + refresh_interval_wind: int = Field(default=100, alias="refreshintervalwind") + refresh_interval_thermal: int = Field(default=100, alias="refreshintervalthermal") + refresh_interval_solar: int = Field(default=100, alias="refreshintervalsolar") + read_only: bool = Field(default=False, alias="readonly") + + +class OutputSectionLocal(LocalBaseModel): + synthesis: bool = True + store_new_set: bool = Field(default=False, alias="storenewset") + archives: Any = "" + + +class GeneralParametersLocal(LocalBaseModel): + general: GeneralSectionLocal + input: dict = {"import": ""} + output: OutputSectionLocal + + @staticmethod + def from_user_model(user_class: GeneralParameters) -> "GeneralParametersLocal": + user_dict = asdict(user_class) + + output_dict = { + "output": { + "store_new_set": user_dict.pop("store_new_set"), + "synthesis": user_dict.pop("simulation_synthesis"), + } + } + general_dict = {"general": user_dict} + local_dict = general_dict | output_dict + + return GeneralParametersLocal.model_validate(local_dict) + + @staticmethod + def get_excluded_fields_for_user_class() -> Set[str]: + return { + "generate", + "nb_timeseries_load", + "nb_timeseries_hydro", + "nb_timeseries_wind", + "nb_timeseries_solar", + "refresh_timeseries", + "intra_modal", + "inter_modal", + "refresh_interval_load", + "refresh_interval_hydro", + "refresh_interval_wind", + "refresh_interval_thermal", + "refresh_interval_solar", + "read_only", + } + + def to_user_model(self) -> GeneralParameters: + local_dict = self.general.model_dump( + mode="json", by_alias=False, exclude=self.get_excluded_fields_for_user_class() + ) + local_dict.update(self.output.model_dump(mode="json", by_alias=False, exclude={"archives"})) + local_dict["simulation_synthesis"] = local_dict.pop("synthesis") + return GeneralParameters(**local_dict) + + +class OptimizationParametersLocal(LocalBaseModel, alias_generator=to_kebab): + simplex_range: SimplexOptimizationRange = SimplexOptimizationRange.WEEK + transmission_capacities: OptimizationTransmissionCapacities = OptimizationTransmissionCapacities.LOCAL_VALUES + include_constraints: bool = True + include_hurdlecosts: bool = True + include_tc_minstablepower: bool = True + include_tc_min_ud_time: bool = True + include_dayahead: bool = True + include_strategicreserve: bool = True + include_spinningreserve: bool = True + include_primaryreserve: bool = True + include_exportmps: bool = False + include_exportstructure: bool = False + include_unfeasible_problem_behavior: UnfeasibleProblemBehavior = UnfeasibleProblemBehavior.ERROR_VERBOSE + + @staticmethod + def from_user_model(user_class: OptimizationParameters) -> "OptimizationParametersLocal": + user_dict = asdict(user_class) + return OptimizationParametersLocal.model_validate(user_dict) + + def to_user_model(self) -> OptimizationParameters: + local_dict = self.model_dump(mode="json", by_alias=False) + return OptimizationParameters(**local_dict) diff --git a/src/antares/craft/service/local_services/services/__init__.py b/src/antares/craft/service/local_services/services/__init__.py new file mode 100644 index 00000000..058c6b22 --- /dev/null +++ b/src/antares/craft/service/local_services/services/__init__.py @@ -0,0 +1,11 @@ +# 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. diff --git a/src/antares/craft/service/local_services/services/settings.py b/src/antares/craft/service/local_services/services/settings.py new file mode 100644 index 00000000..69ef20f9 --- /dev/null +++ b/src/antares/craft/service/local_services/services/settings.py @@ -0,0 +1,168 @@ +# 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 pathlib import Path + +from antares.craft.model.settings.adequacy_patch import AdequacyPatchParameters +from antares.craft.model.settings.advanced_parameters import ( + AdvancedParameters, + SeedParameters, +) +from antares.craft.model.settings.general import BuildingMode, GeneralParameters +from antares.craft.model.settings.optimization import ( + OptimizationParameters, +) +from antares.craft.model.settings.study_settings import StudySettings +from antares.craft.service.local_services.models.settings import ( + AdequacyPatchParametersLocal, + AdvancedAndSeedParametersLocal, + AdvancedParametersLocal, + GeneralParametersLocal, + OptimizationParametersLocal, + OtherPreferencesLocal, + SeedParametersLocal, +) +from antares.craft.tools.ini_tool import IniFile, InitializationFilesTypes + + +def read_study_settings_local(study_directory: Path) -> StudySettings: + general_data_ini = IniFile(study_directory, InitializationFilesTypes.GENERAL) + ini_content = general_data_ini.ini_dict + + # general + general_params_ini = {"general": ini_content["general"]} + if general_params_ini.pop("derated", None): + general_params_ini["building_mode"] = BuildingMode.DERATED.value + if general_params_ini.pop("custom-scenario", None): + general_params_ini["building_mode"] = BuildingMode.CUSTOM.value + else: + general_params_ini["building_mode"] = BuildingMode.AUTOMATIC.value + + excluded_keys = GeneralParametersLocal.get_excluded_fields_for_user_class() + for key in excluded_keys: + general_params_ini.pop(key, None) + + output_parameters_ini = {"output": ini_content["output"]} + local_general_ini = general_params_ini | output_parameters_ini + general_parameters_local = GeneralParametersLocal.model_validate(local_general_ini) + general_parameters = general_parameters_local.to_user_model() + + # optimization + optimization_ini = ini_content["optimization"] + optimization_ini.pop("link-type", None) + optimization_parameters_local = OptimizationParametersLocal.model_validate(optimization_ini) + optimization_parameters = optimization_parameters_local.to_user_model() + + # adequacy_patch + adequacy_ini = ini_content["adequacy patch"] + adequacy_parameters_local = AdequacyPatchParametersLocal.model_validate(adequacy_ini) + adequacy_patch_parameters = adequacy_parameters_local.to_user_model() + + # seed and advanced + seed_local_parameters = SeedParametersLocal.model_validate(ini_content["seeds - Mersenne Twister"]) + advanced_local_parameters = AdvancedParametersLocal.model_validate(ini_content["advanced parameters"]) + other_preferences_local_parameters = OtherPreferencesLocal.model_validate(ini_content["other preferences"]) + args = { + "other_preferences": other_preferences_local_parameters, + "seeds": seed_local_parameters, + "advanced_parameters": advanced_local_parameters, + } + seed_and_advanced_local_parameters = AdvancedAndSeedParametersLocal.model_validate(args) + seed_parameters = seed_and_advanced_local_parameters.to_seed_parameters_model() + advanced_parameters = seed_and_advanced_local_parameters.to_advanced_parameters_model() + + # playlist + playlist_parameters = None + if "playlist" in ini_content: + playlist_parameters = None + # todo + + # thematic trimming + thematic_trimming_parameters = None + if "variables selection" in ini_content: + thematic_trimming_parameters = None + # todo + + return StudySettings( + general_parameters=general_parameters, + optimization_parameters=optimization_parameters, + seed_parameters=seed_parameters, + advanced_parameters=advanced_parameters, + adequacy_patch_parameters=adequacy_patch_parameters, + playlist_parameters=playlist_parameters, + thematic_trimming_parameters=thematic_trimming_parameters, + ) + + +def edit_study_settings(study_directory: Path, settings: StudySettings, update: bool) -> StudySettings: + general_data_ini = IniFile(study_directory, InitializationFilesTypes.GENERAL) + ini_content = general_data_ini.ini_dict if update else {} + + # general + general_parameters = settings.general_parameters or GeneralParameters() + general_local_parameters = GeneralParametersLocal.from_user_model(general_parameters) + + json_content = general_local_parameters.model_dump(mode="json", by_alias=True, exclude_unset=update) + if "general" in json_content and "building_mode" in json_content["general"]: + general_values = json_content["general"] + del general_values["building_mode"] + building_mode = general_local_parameters.general.building_mode + general_values["derated"] = building_mode == BuildingMode.DERATED + general_values["custom-scenario"] = building_mode == BuildingMode.CUSTOM + + ini_content.update(json_content) + new_general_parameters = general_local_parameters.to_user_model() + + # optimization + optimization_parameters = settings.optimization_parameters or OptimizationParameters() + optimization_local_parameters = OptimizationParametersLocal.from_user_model(optimization_parameters) + ini_content.update( + {"optimization": optimization_local_parameters.model_dump(mode="json", by_alias=True, exclude_unset=update)} + ) + new_optimization_parameters = optimization_local_parameters.to_user_model() + + # adequacy_patch + adequacy_parameters = settings.adequacy_patch_parameters or AdequacyPatchParameters() + adequacy_local_parameters = AdequacyPatchParametersLocal.from_user_model(adequacy_parameters) + ini_content.update( + {"adequacy patch": adequacy_local_parameters.model_dump(mode="json", by_alias=True, exclude_unset=update)} + ) + new_adequacy_parameters = adequacy_local_parameters.to_user_model() + + # seed and advanced + seed_parameters = settings.seed_parameters or SeedParameters() + advanced_parameters = settings.advanced_parameters or AdvancedParameters() + advanced_parameters_local = AdvancedAndSeedParametersLocal.from_user_model(advanced_parameters, seed_parameters) + ini_content.update(advanced_parameters_local.model_dump(mode="json", by_alias=True, exclude_unset=update)) + new_seed_parameters = advanced_parameters_local.to_seed_parameters_model() + new_advanced_parameters = advanced_parameters_local.to_advanced_parameters_model() + + # playlist + # todo + + # thematic trimming + # todo + + # writing + general_data_ini.ini_dict = ini_content + general_data_ini.write_ini_file() + + # returning new_settings + return StudySettings( + general_parameters=new_general_parameters, + optimization_parameters=new_optimization_parameters, + adequacy_patch_parameters=new_adequacy_parameters, + seed_parameters=new_seed_parameters, + advanced_parameters=new_advanced_parameters, + playlist_parameters=None, + thematic_trimming_parameters=None, + ) diff --git a/src/antares/craft/service/local_services/study_local.py b/src/antares/craft/service/local_services/study_local.py index d4ae9a7a..847c3933 100644 --- a/src/antares/craft/service/local_services/study_local.py +++ b/src/antares/craft/service/local_services/study_local.py @@ -17,6 +17,7 @@ from antares.craft.model.output import Output from antares.craft.model.settings.study_settings import StudySettings from antares.craft.service.base_services import BaseOutputService, BaseStudyService +from antares.craft.service.local_services.services.settings import edit_study_settings if TYPE_CHECKING: from antares.craft.model.study import Study @@ -44,8 +45,8 @@ def output_service(self) -> Optional[BaseOutputService]: def set_output_service(self, output_service: BaseOutputService) -> None: self._output_service = output_service - def update_study_settings(self, settings: StudySettings) -> Optional[StudySettings]: - raise NotImplementedError + def update_study_settings(self, settings: StudySettings) -> None: + edit_study_settings(self.config.study_path, settings, update=True) def delete_binding_constraint(self, constraint: BindingConstraint) -> None: raise NotImplementedError diff --git a/src/antares/craft/tools/contents_tool.py b/src/antares/craft/tools/contents_tool.py index 98963681..41964338 100644 --- a/src/antares/craft/tools/contents_tool.py +++ b/src/antares/craft/tools/contents_tool.py @@ -10,11 +10,9 @@ # # This file is part of the Antares project. -import json import re from enum import Enum -from pathlib import Path from typing import Any, Dict, Optional from antares.craft.tools.custom_raw_config_parser import CustomRawConfigParser @@ -32,50 +30,6 @@ def transform_name_to_id(name: str) -> str: return _sub_invalid_chars(" ", name).strip().lower() -def retrieve_file_content(file_to_retrieve: str) -> Dict[str, Any]: - module_path = Path(__file__).resolve().parent - - path_resources = module_path.parent.parent / "resources" - path_to_file = path_resources / file_to_retrieve - - with open(path_to_file, "r") as read_content: - return json.load(read_content) - - -def transform_ui_data_to_text(data_from_json: Dict[str, Any]) -> str: - """ - Args: - data_from_json: ini data to be inserted - - Returns: - str to be written in .ini file - """ - ini_content = "" - for key, value in data_from_json.items(): - if isinstance(value, dict): - section_header = f"[{key}]" - ini_content += f"{section_header}\n" - for inner_key, inner_value in value.items(): - if isinstance(inner_value, list): - inner_value_str = " , ".join(map(str, inner_value)) - ini_content += f"{inner_key} = {inner_value_str}\n" - else: - ini_content += f"{inner_key} = {inner_value}\n" - else: - ini_content += f"{key} = {value}\n" - - return ini_content - - -def extract_content(key: str, file_to_retrieve: str) -> str: - ini_data = retrieve_file_content(file_to_retrieve) - data_for_file = ini_data.get(key) - if data_for_file is not None: - return transform_ui_data_to_text(data_for_file) - else: - raise KeyError(f"Key '{key}' not defined in {file_to_retrieve}") - - class EnumIgnoreCase(Enum): @classmethod def _missing_(cls, value: object) -> Optional["EnumIgnoreCase"]: diff --git a/tests/antares/integration/test_local_client.py b/tests/antares/integration/test_local_client.py index 8087f3ce..34a71ec3 100644 --- a/tests/antares/integration/test_local_client.py +++ b/tests/antares/integration/test_local_client.py @@ -20,9 +20,9 @@ from antares.craft.model.commons import FilterOption from antares.craft.model.link import Link, LinkProperties, LinkUi from antares.craft.model.renewable import RenewableClusterGroup, RenewableClusterProperties -from antares.craft.model.settings.general import GeneralParametersLocal, Mode -from antares.craft.model.settings.playlist_parameters import PlaylistParameters -from antares.craft.model.settings.study_settings import StudySettingsLocal +from antares.craft.model.settings.advanced_parameters import AdvancedParameters, UnitCommitmentMode +from antares.craft.model.settings.general import GeneralParameters +from antares.craft.model.settings.study_settings import StudySettings from antares.craft.model.st_storage import STStorageGroup, STStorageProperties from antares.craft.model.study import Study, create_study_local from antares.craft.model.thermal import ThermalCluster, ThermalClusterGroup, ThermalClusterProperties @@ -248,13 +248,9 @@ def test_local_study(self, tmp_path, unknown_area): } # test study creation with settings - settings = StudySettingsLocal() - settings.general_parameters = GeneralParametersLocal(mode="Adequacy") - settings.general_parameters.year_by_year = False - settings.playlist_parameters = PlaylistParameters() - settings.playlist_parameters.playlist = [{"status": False, "weight": 1}] + settings = StudySettings() + settings.general_parameters = GeneralParameters(nb_years=4) + settings.advanced_parameters = AdvancedParameters(unit_commitment_mode=UnitCommitmentMode.MILP) new_study = create_study_local("second_study", "880", tmp_path, settings) - settings = new_study.get_settings() - assert settings.general_parameters.mode == Mode.ADEQUACY.value - assert not settings.general_parameters.year_by_year - assert settings.playlist_parameters.model_dump() == {1: {"status": False, "weight": 1}} + assert new_study.get_settings().general_parameters.nb_years == 4 + assert new_study.get_settings().advanced_parameters.unit_commitment_mode == UnitCommitmentMode.MILP.value diff --git a/tests/antares/model/settings/conftest.py b/tests/antares/model/settings/conftest.py deleted file mode 100644 index 8b5255e6..00000000 --- a/tests/antares/model/settings/conftest.py +++ /dev/null @@ -1,25 +0,0 @@ -# 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 pytest - -from antares.craft.model.settings.playlist_parameters import PlaylistData, PlaylistParameters - - -@pytest.fixture -def test_playlist() -> PlaylistParameters: - return PlaylistParameters(playlist=[PlaylistData(status=False, weight=2.1)]) - - -@pytest.fixture -def test_playlist_model_dump(test_playlist) -> dict[str, str]: - return test_playlist.model_dump() diff --git a/tests/antares/model/settings/test_playlist_parameters.py b/tests/antares/model/settings/test_playlist_parameters.py deleted file mode 100644 index d709e931..00000000 --- a/tests/antares/model/settings/test_playlist_parameters.py +++ /dev/null @@ -1,88 +0,0 @@ -# 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 pytest - -from antares.craft.model.settings.playlist_parameters import PlaylistData, PlaylistParameters - - -class TestCreatePlaylistParameters: - def test_create_playlist_parameters_with_list(self): - created_params = PlaylistParameters(playlist=[PlaylistData(status=False, weight=2.1)]) - expected_model_dump = {1: {"status": False, "weight": 2.1}} - assert created_params - assert isinstance(created_params, PlaylistParameters) - assert created_params.model_dump() == expected_model_dump - - def test_create_playlist_parameters_with_dict(self): - created_params = PlaylistParameters( - playlist={"playlist_reset": True, "mc_years": {1: {"status": True, "weight": 3}}} - ) - expected_model_dump = {1: {"status": True, "weight": 3}} - assert created_params - assert isinstance(created_params, PlaylistParameters) - assert created_params.model_dump() == expected_model_dump - - def test_create_playlist_with_small_dict(self, test_playlist_model_dump): - # Given - created_params = PlaylistParameters(playlist={1: {"status": False, "weight": 2.1}}) - - # Then - assert created_params - assert isinstance(created_params, PlaylistParameters) - assert created_params.model_dump() == test_playlist_model_dump - - def test_create_playlist_parameters_with_object(self, test_playlist, test_playlist_model_dump): - created_params = PlaylistParameters(playlist=test_playlist) - - assert created_params - assert isinstance(created_params, PlaylistParameters) - assert created_params.model_dump() == test_playlist_model_dump - - def test_creating_wrong_dictionary_errors(self): - # Given - wrong_playlist_parameters_dict = {"playlist_reset": True, "mcyears": {1: {"status": True, "weight": 3}}} - - # Then - with pytest.raises(ValueError, match="Not a valid playlist dictionary."): - PlaylistParameters(playlist=wrong_playlist_parameters_dict) - - -class TestValidatePlaylistParameters: - def test_playlist_parameters_validate_dict(self): - created_params = PlaylistParameters.model_validate( - {"playlist_reset": True, "mc_years": {1: {"status": False, "weight": 2}}} - ) - expected_model_dump = {1: {"status": False, "weight": 2}} - - assert created_params - assert isinstance(created_params, PlaylistParameters) - assert created_params.model_dump() == expected_model_dump - - def test_playlist_parameters_validate_object(self, test_playlist, test_playlist_model_dump): - created_params = PlaylistParameters.model_validate(test_playlist) - - assert created_params - assert isinstance(created_params, PlaylistParameters) - assert created_params.model_dump() == test_playlist_model_dump - - def test_validating_wrong_dictionary_gives_empty_playlist(self): - # Given - wrong_playlist_parameters_dict = {"playlist_reset": True, "mcyears": {1: {"status": True, "weight": 3}}} - expected_model_dump = {} - - # When - created_params = PlaylistParameters.model_validate(wrong_playlist_parameters_dict) - - # Then - assert created_params - assert isinstance(created_params, PlaylistParameters) - assert created_params.model_dump() == expected_model_dump diff --git a/tests/antares/services/api_services/test_study_api.py b/tests/antares/services/api_services/test_study_api.py index 8488b82e..b4d31102 100644 --- a/tests/antares/services/api_services/test_study_api.py +++ b/tests/antares/services/api_services/test_study_api.py @@ -78,6 +78,8 @@ def test_create_study_test_ok(self) -> None: mocker.post(expected_url, json=self.study_id, status_code=200) config_urls = re.compile(f"https://antares.com/api/v1/studies/{self.study_id}/config/.*") mocker.get(config_urls, json={}, status_code=200) + ts_settings_url = f"https://antares.com/api/v1/studies/{self.study_id}/timeseries/config" + mocker.get(ts_settings_url, json={"thermal": {"number": 1}}, status_code=200) expected_url_path = f"https://antares.com/api/v1/studies/{self.study_id}" mocker.get( expected_url_path, @@ -231,6 +233,7 @@ def test_read_study_api(self): } config_urls = re.compile(f"https://antares.com/api/v1/studies/{self.study_id}/config/.*") + ts_settings_url = f"https://antares.com/api/v1/studies/{self.study_id}/timeseries/config" base_url = "https://antares.com/api/v1" url = f"{base_url}/studies/{self.study_id}" @@ -245,6 +248,7 @@ def test_read_study_api(self): with requests_mock.Mocker() as mocker: mocker.get(url, json=json_study) mocker.get(config_urls, json={}) + mocker.get(ts_settings_url, json={"thermal": {"number": 1}}, status_code=200) mocker.get(area_url, json=json_ui) mocker.get(area_props_url, json={}) mocker.get(renewable_url, json=[]) @@ -289,6 +293,8 @@ def test_create_variant_success(self): config_urls = re.compile(f"{base_url}/studies/{variant_id}/config/.*") mocker.get(config_urls, json={}, status_code=200) + ts_settings_url = f"https://antares.com/api/v1/studies/{variant_id}/timeseries/config" + mocker.get(ts_settings_url, json={"thermal": {"number": 1}}, status_code=200) areas_url = f"{base_url}/studies/{variant_id}/areas?ui=true" mocker.get(areas_url, json={}, status_code=200) @@ -737,6 +743,7 @@ def test_import_study_success(self, tmp_path): output_url = f"{url}/outputs" constraints_url = f"{base_url}/studies/{self.study_id}/bindingconstraints" config_urls = re.compile(f"{base_url}/studies/{self.study_id}/config/.*") + ts_settings_url = f"https://antares.com/api/v1/studies/{self.study_id}/timeseries/config" url_import = f"{base_url}/studies/_import" url_move = f"{base_url}/studies/{self.study_id}/move?folder_dest={new_path}" @@ -747,15 +754,13 @@ def test_import_study_success(self, tmp_path): mocker.get(url, json=json_study) mocker.get(config_urls, json={}) + mocker.get(ts_settings_url, json={"thermal": {"number": 1}}, status_code=200) mocker.get(area_url, json={}) mocker.get(area_props_url, json={}) mocker.get(renewable_url, json=[]) mocker.get(thermal_url, json=[]) mocker.get(storage_url, json=[]) - mocker.get( - output_url, - json=[], - ) + mocker.get(output_url, json=[]) mocker.get(constraints_url, json=[]) mocker.put(url_move) diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index 285f30ad..9f1c95b2 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -50,40 +50,19 @@ TransmissionCapacities, ) from antares.craft.model.settings.adequacy_patch import ( - DefaultAdequacyPatchParameters, - PriceTakingOrder, + AdequacyPatchParameters, ) from antares.craft.model.settings.advanced_parameters import ( - AdvancedParametersLocal, - HydroHeuristicPolicy, - HydroPricingMode, - InitialReservoirLevel, - PowerFluctuation, - RenewableGenerationModeling, - SheddingPolicy, - SimulationCore, - UnitCommitmentMode, + AdvancedParameters, + SeedParameters, ) from antares.craft.model.settings.general import ( - BuildingMode, - GeneralParametersLocal, - Mode, - Month, - WeekDay, + GeneralParameters, ) from antares.craft.model.settings.optimization import ( - ExportMPS, - OptimizationParametersLocal, - OptimizationTransmissionCapacities, - SimplexOptimizationRange, - UnfeasibleProblemBehavior, -) -from antares.craft.model.settings.playlist_parameters import PlaylistData, PlaylistParameters -from antares.craft.model.settings.study_settings import DefaultStudySettings, StudySettingsLocal -from antares.craft.model.settings.thematic_trimming import ( - DefaultThematicTrimmingParameters, - ThematicTrimmingParametersLocal, + OptimizationParameters, ) +from antares.craft.model.settings.study_settings import StudySettings from antares.craft.model.study import create_study_local from antares.craft.tools.ini_tool import InitializationFilesTypes @@ -213,92 +192,71 @@ def test_local_study_has_settings(self, local_study): local_study_settings = local_study.get_settings() # Then assert local_study.get_settings() - assert isinstance(local_study_settings, DefaultStudySettings) + assert isinstance(local_study_settings, StudySettings) def test_local_study_has_correct_default_general_properties(self, local_study): - # Given - # https://antares-simulator.readthedocs.io/en/latest/user-guide/solver/04-parameters/ - expected_general_properties = GeneralParametersLocal.model_validate( - { - "mode": Mode.ECONOMY, + expected_general_properties = GeneralParameters( + **{ + "mode": "Economy", "horizon": "", "nb_years": 1, - "first_day": 1, - "last_day": 365, - "first_january": WeekDay.MONDAY, - "first_month": Month.JANUARY, - "first_week_day": WeekDay.MONDAY, + "simulation_start": 1, + "simulation_end": 365, + "january_first": "Monday", + "first_month_in_year": "January", + "first_week_day": "Monday", "leap_year": False, "year_by_year": False, - "building_mode": BuildingMode.AUTOMATIC, - "selection_mode": False, + "building_mode": "automatic", "thematic_trimming": False, "geographic_trimming": False, - "active_rules_scenario": "default ruleset", - "read_only": False, "simulation_synthesis": True, - "mc_scenario": False, + "user_playlist": False, + "store_new_set": False, + "nb_timeseries_thermal": 1, } ) - # When - expected_study_settings = StudySettingsLocal(general_properties=expected_general_properties) - # Then assert local_study.get_settings().general_parameters == expected_general_properties - assert local_study.get_settings() == expected_study_settings def test_local_study_has_correct_default_adequacy_patch_properties(self, local_study): - # Given - expected_adequacy_patch_properties = DefaultAdequacyPatchParameters.model_validate( - { - "enable_adequacy_patch": False, - "ntc_from_physical_areas_out_to_physical_areas_in_adequacy_patch": True, - "ntc_between_physical_areas_out_adequacy_patch": True, - "price_taking_order": PriceTakingOrder.DENS, + expected_adequacy_patch_properties = AdequacyPatchParameters( + **{ + "include_adq_patch": False, + "set_to_null_ntc_from_physical_out_to_physical_in_for_first_step": True, + "set_to_null_ntc_between_physical_out_for_first_step": True, + "price_taking_order": "DENS", "include_hurdle_cost_csr": False, "check_csr_cost_function": False, - "enable_first_step": False, "threshold_initiate_curtailment_sharing_rule": 0, "threshold_display_local_matching_rule_violations": 0, "threshold_csr_variable_bounds_relaxation": 3, } ) - expected_study_settings = StudySettingsLocal( - adequacy_patch_properties=DefaultAdequacyPatchParameters.model_validate( - expected_adequacy_patch_properties.model_dump(exclude_none=True) - ) - ) - # When - actual_adequacy_patch_properties = DefaultAdequacyPatchParameters.model_validate( - local_study.get_settings().adequacy_patch_parameters.model_dump(exclude_none=True) - ) - actual_study_settings = StudySettingsLocal.model_validate( - local_study.get_settings().model_dump(exclude_none=True) + assert local_study.get_settings().adequacy_patch_parameters == expected_adequacy_patch_properties + + def test_local_study_has_correct_advanced_parameters(self, local_study): + expected_advanced_parameters = AdvancedParameters( + **{ + "accuracy_on_correlation": [], + "initial_reservoir_levels": "cold start", + "hydro_heuristic_policy": "accommodate rule curves", + "hydro_pricing_mode": "fast", + "power_fluctuations": "free modulations", + "shedding_policy": "shave peaks", + "unit_commitment_mode": "fast", + "number_of_cores_mode": "medium", + "renewable_generation_modelling": "clusters", + } ) - # Then - assert actual_adequacy_patch_properties == expected_adequacy_patch_properties - assert actual_study_settings == expected_study_settings + assert local_study.get_settings().advanced_parameters == expected_advanced_parameters - def test_local_study_has_correct_advanced_parameters(self, local_study): - # Given - expected_advanced_parameters = AdvancedParametersLocal.model_validate( - { - "accuracy_on_correlation": "", - "initial_reservoir_levels": InitialReservoirLevel.COLD_START, - "hydro_heuristic_policy": HydroHeuristicPolicy.ACCOMMODATE_RULES_CURVES, - "hydro_pricing_mode": HydroPricingMode.FAST, - "power_fluctuations": PowerFluctuation.FREE_MODULATIONS, - "shedding_policy": SheddingPolicy.SHAVE_PEAKS, - "unit_commitment_mode": UnitCommitmentMode.FAST, - "number_of_cores_mode": SimulationCore.MEDIUM, - "renewable_generation_modelling": RenewableGenerationModeling.AGGREGATED, - "seed_tsgen_wind": 5489, - "seed_tsgen_load": 1005489, - "seed_tsgen_hydro": 2005489, + def test_local_study_has_correct_seed_parameters(self, local_study): + expected_seed_parameters = SeedParameters( + **{ "seed_tsgen_thermal": 3005489, - "seed_tsgen_solar": 4005489, "seed_tsnumbers": 5005489, "seed_unsupplied_energy_costs": 6005489, "seed_spilled_energy_costs": 7005489, @@ -307,200 +265,33 @@ def test_local_study_has_correct_advanced_parameters(self, local_study): "seed_initial_reservoir_levels": 10005489, } ) - expected_study_settings = StudySettingsLocal(advanced_parameters=expected_advanced_parameters) - # When - actual_advanced_parameters = AdvancedParametersLocal.model_validate( - local_study.get_settings().advanced_parameters.model_dump(exclude_none=True) - ) - actual_study_settings = StudySettingsLocal.model_validate( - local_study.get_settings().model_dump(exclude_none=True) - ) - - # Then - assert actual_advanced_parameters == expected_advanced_parameters - assert actual_study_settings == expected_study_settings + assert local_study.get_settings().seed_parameters == expected_seed_parameters def test_local_study_has_correct_optimization_parameters(self, local_study): - # Given - expected_optimization_parameters = OptimizationParametersLocal.model_validate( - { - "simplex_optimization_range": SimplexOptimizationRange.WEEK, - "transmission_capacities": OptimizationTransmissionCapacities.LOCAL_VALUES, - "binding_constraints": True, - "hurdle_costs": True, - "thermal_clusters_min_stable_power": True, - "thermal_clusters_min_ud_time": True, - "day_ahead_reserve": True, - "strategic_reserve": True, - "spinning_reserve": True, - "primary_reserve": True, - "export_mps": ExportMPS.NONE, + expected_optimization_parameters = OptimizationParameters( + **{ + "simplex_range": "week", + "transmission_capacities": "local-values", + "include_constraints": True, + "include_hurdlecosts": True, + "include_tc_minstablepower": True, + "include_tc_min_ud_time": True, + "include_dayahead": True, + "include_strategicreserve": True, + "include_spinningreserve": True, + "include_primaryreserve": True, + "include_exportmps": False, "include_exportstructure": False, - "unfeasible_problem_behavior": UnfeasibleProblemBehavior.ERROR_VERBOSE, - } - ) - expected_study_settings = StudySettingsLocal(optimization_parameters=expected_optimization_parameters) - - # When - actual_optimization_parameters = OptimizationParametersLocal.model_validate( - local_study.get_settings().optimization_parameters.model_dump(exclude_none=True) - ) - actual_study_settings = StudySettingsLocal.model_validate( - local_study.get_settings().model_dump(exclude_none=True) - ) - - # Then - assert actual_optimization_parameters == expected_optimization_parameters - assert actual_study_settings == expected_study_settings - - def test_local_study_with_playlist_has_correct_defaults(self, tmp_path): - # Given - nb_years = 2 - playlist_study = create_study_local( - "test_study", - "880", - str(tmp_path.absolute()), - StudySettingsLocal( - general_parameters=GeneralParametersLocal(nb_years=nb_years, selection_mode=True), - playlist_parameters=PlaylistParameters(playlist=[PlaylistData()] * nb_years), - ), - ) - - # When - expected_playlist_parameters_dict = {1: {"status": True, "weight": 1.0}, 2: {"status": True, "weight": 1.0}} - expected_playlist_parameters = PlaylistParameters(playlist=expected_playlist_parameters_dict) - - actual_playlist_parameters_dict = playlist_study.get_settings().playlist_parameters.model_dump() - actual_playlist_parameters = playlist_study.get_settings().playlist_parameters - - # Then - assert actual_playlist_parameters_dict == expected_playlist_parameters_dict - assert actual_playlist_parameters == expected_playlist_parameters - - def test_local_study_has_correct_thematic_trimming_parameters(self, tmp_path): - # Given - expected_thematic_trimming_parameters = ThematicTrimmingParametersLocal.model_validate( - { - "ov_cost": True, - "op_cost": True, - "mrg_price": True, - "co2_emis": True, - "dtg_by_plant": True, - "balance": True, - "row_bal": True, - "psp": True, - "misc_ndg": True, - "load": True, - "h_ror": True, - "wind": True, - "solar": True, - "nuclear": True, - "lignite": True, - "coal": True, - "gas": True, - "oil": True, - "mix_fuel": True, - "misc_dtg": True, - "h_stor": True, - "h_pump": True, - "h_lev": True, - "h_infl": True, - "h_ovfl": True, - "h_val": True, - "h_cost": True, - "unsp_enrg": True, - "spil_enrg": True, - "lold": True, - "lolp": True, - "avl_dtg": True, - "dtg_mrg": True, - "max_mrg": True, - "np_cost": True, - "np_cost_by_plant": True, - "nodu": True, - "nodu_by_plant": True, - "flow_lin": True, - "ucap_lin": True, - "loop_flow": True, - "flow_quad": True, - "cong_fee_alg": True, - "cong_fee_abs": True, - "marg_cost": True, - "cong_prob_plus": True, - "cong_prob_minus": True, - "hurdle_cost": True, - "res_generation_by_plant": True, - "misc_dtg_2": True, - "misc_dtg_3": True, - "misc_dtg_4": True, - "wind_offshore": True, - "wind_onshore": True, - "solar_concrt": True, - "solar_pv": True, - "solar_rooft": True, - "renw_1": True, - "renw_2": True, - "renw_3": True, - "renw_4": True, - "dens": True, - "profit_by_plant": True, - "sts_inj_by_plant": True, - "sts_withdrawal_by_plant": True, - "sts_lvl_by_plant": True, - "psp_open_injection": True, - "psp_open_withdrawal": True, - "psp_open_level": True, - "psp_closed_injection": True, - "psp_closed_withdrawal": True, - "psp_closed_level": True, - "pondage_injection": True, - "pondage_withdrawal": True, - "pondage_level": True, - "battery_injection": True, - "battery_withdrawal": True, - "battery_level": True, - "other1_injection": True, - "other1_withdrawal": True, - "other1_level": True, - "other2_injection": True, - "other2_withdrawal": True, - "other2_level": True, - "other3_injection": True, - "other3_withdrawal": True, - "other3_level": True, - "other4_injection": True, - "other4_withdrawal": True, - "other4_level": True, - "other5_injection": True, - "other5_withdrawal": True, - "other5_level": True, - "sts_cashflow_by_cluster": True, + "include_unfeasible_problem_behavior": "error-verbose", } ) - expected_study_settings = StudySettingsLocal( - general_parameters=GeneralParametersLocal(thematic_trimming=True), - thematic_trimming_parameters=expected_thematic_trimming_parameters, - ) - thematic_trimming_study = create_study_local( - "test_study", - "880", - str(tmp_path.absolute()), - StudySettingsLocal( - general_parameters=GeneralParametersLocal(thematic_trimming=True), - thematic_trimming_parameters=ThematicTrimmingParametersLocal(), - ), - ) - # When - actual_thematic_trimming_parameters = DefaultThematicTrimmingParameters.model_validate( - thematic_trimming_study.get_settings().thematic_trimming_parameters - ) - actual_study_settings = DefaultStudySettings.model_validate(thematic_trimming_study.get_settings()) + assert local_study.get_settings().optimization_parameters == expected_optimization_parameters - # Then - assert actual_thematic_trimming_parameters == expected_thematic_trimming_parameters - assert actual_study_settings == expected_study_settings + def test_local_study_has_correct_playlist_and_thematic_parameters(self, local_study): + assert local_study.get_settings().playlist_parameters is None + assert local_study.get_settings().thematic_trimming_parameters is None def test_generaldata_ini_exists(self, local_study): # Given @@ -520,299 +311,63 @@ def test_generaldata_ini_has_correct_default_values(self, local_study): january.1st = Monday first-month-in-year = January first.weekday = Monday -leapyear = false -year-by-year = false -derated = false -custom-scenario = false -user-playlist = false -thematic-trimming = false -geographic-trimming = false -generate = +leapyear = False +year-by-year = False +user-playlist = False +thematic-trimming = False +geographic-trimming = False +generate = False nbtimeseriesload = 1 nbtimeserieshydro = 1 -nbtimeseriesthermal = 1 nbtimeserieswind = 1 -nbtimeseriessolar = 1 -refreshtimeseries = -intra-modal = -inter-modal = -refreshintervalload = 100 -refreshintervalhydro = 100 -refreshintervalthermal = 100 -refreshintervalwind = 100 -refreshintervalsolar = 100 -readonly = false - -[input] -import = - -[output] -synthesis = true -storenewset = false -archives = -result-format = txt-files - -[optimization] -simplex-range = week -transmission-capacities = local-values -include-constraints = true -include-hurdlecosts = true -include-tc-minstablepower = true -include-tc-min-ud-time = true -include-dayahead = true -include-strategicreserve = true -include-spinningreserve = true -include-primaryreserve = true -include-exportmps = none -include-exportstructure = false -include-unfeasible-problem-behavior = error-verbose - -[adequacy patch] -include-adq-patch = false -set-to-null-ntc-from-physical-out-to-physical-in-for-first-step = true -set-to-null-ntc-between-physical-out-for-first-step = true -enable-first-step = false -price-taking-order = DENS -include-hurdle-cost-csr = false -check-csr-cost-function = false -threshold-initiate-curtailment-sharing-rule = 0.000000 -threshold-display-local-matching-rule-violations = 0.000000 -threshold-csr-variable-bounds-relaxation = 3 - -[other preferences] -initial-reservoir-levels = cold start -hydro-heuristic-policy = accommodate rule curves -hydro-pricing-mode = fast -power-fluctuations = free modulations -shedding-policy = shave peaks -unit-commitment-mode = fast -number-of-cores-mode = medium -renewable-generation-modelling = aggregated - -[advanced parameters] -accuracy-on-correlation = - -[seeds - Mersenne Twister] -seed-tsgen-wind = 5489 -seed-tsgen-load = 1005489 -seed-tsgen-hydro = 2005489 -seed-tsgen-thermal = 3005489 -seed-tsgen-solar = 4005489 -seed-tsnumbers = 5005489 -seed-unsupplied-energy-costs = 6005489 -seed-spilled-energy-costs = 7005489 -seed-thermal-costs = 8005489 -seed-hydro-costs = 9005489 -seed-initial-reservoir-levels = 10005489 - -""" - - # When - actual_generaldata_ini_file = local_study.service.config.study_path / InitializationFilesTypes.GENERAL.value - actual_file_content = actual_generaldata_ini_file.read_text() - - # Then - assert actual_file_content == expected_file_content - - def test_generaldata_ini_with_thematic_trimming_has_negative_sign(self, tmp_path): - # Given - study_name = "test study" - study_version = "880" - general_parameters = GeneralParametersLocal(thematic_trimming=True) - thematic_trimming_parameters = ThematicTrimmingParametersLocal(op_cost=False) - study_path = str(tmp_path.absolute()) - expected_file_content = """[general] -mode = Economy -horizon = -nbyears = 1 -simulation.start = 1 -simulation.end = 365 -january.1st = Monday -first-month-in-year = January -first.weekday = Monday -leapyear = false -year-by-year = false -derated = false -custom-scenario = false -user-playlist = false -thematic-trimming = true -geographic-trimming = false -generate = -nbtimeseriesload = 1 -nbtimeserieshydro = 1 nbtimeseriesthermal = 1 -nbtimeserieswind = 1 nbtimeseriessolar = 1 -refreshtimeseries = -intra-modal = -inter-modal = +refreshtimeseries = False +intra-modal = False +inter-modal = False refreshintervalload = 100 refreshintervalhydro = 100 -refreshintervalthermal = 100 refreshintervalwind = 100 -refreshintervalsolar = 100 -readonly = false - -[input] -import = - -[output] -synthesis = true -storenewset = false -archives = -result-format = txt-files - -[optimization] -simplex-range = week -transmission-capacities = local-values -include-constraints = true -include-hurdlecosts = true -include-tc-minstablepower = true -include-tc-min-ud-time = true -include-dayahead = true -include-strategicreserve = true -include-spinningreserve = true -include-primaryreserve = true -include-exportmps = none -include-exportstructure = false -include-unfeasible-problem-behavior = error-verbose - -[adequacy patch] -include-adq-patch = false -set-to-null-ntc-from-physical-out-to-physical-in-for-first-step = true -set-to-null-ntc-between-physical-out-for-first-step = true -enable-first-step = false -price-taking-order = DENS -include-hurdle-cost-csr = false -check-csr-cost-function = false -threshold-initiate-curtailment-sharing-rule = 0.000000 -threshold-display-local-matching-rule-violations = 0.000000 -threshold-csr-variable-bounds-relaxation = 3 - -[other preferences] -initial-reservoir-levels = cold start -hydro-heuristic-policy = accommodate rule curves -hydro-pricing-mode = fast -power-fluctuations = free modulations -shedding-policy = shave peaks -unit-commitment-mode = fast -number-of-cores-mode = medium -renewable-generation-modelling = aggregated - -[advanced parameters] -accuracy-on-correlation = - -[seeds - Mersenne Twister] -seed-tsgen-wind = 5489 -seed-tsgen-load = 1005489 -seed-tsgen-hydro = 2005489 -seed-tsgen-thermal = 3005489 -seed-tsgen-solar = 4005489 -seed-tsnumbers = 5005489 -seed-unsupplied-energy-costs = 6005489 -seed-spilled-energy-costs = 7005489 -seed-thermal-costs = 8005489 -seed-hydro-costs = 9005489 -seed-initial-reservoir-levels = 10005489 - -[variables selection] -selected_vars_reset = true -select_var - = OP. COST - -""" - - # When - new_study = create_study_local( - study_name, - study_version, - study_path, - StudySettingsLocal( - general_parameters=general_parameters, thematic_trimming_parameters=thematic_trimming_parameters - ), - ) - actual_generaldata_ini_file = new_study.service.config.study_path / InitializationFilesTypes.GENERAL.value - actual_file_content = actual_generaldata_ini_file.read_text() - - # Then - assert actual_file_content == expected_file_content - - def test_generaldata_ini_with_thematic_trimming_has_positive_sign(self, tmp_path): - # Given - study_name = "test study" - study_version = "880" - general_parameters = GeneralParametersLocal(thematic_trimming=True) - thematic_trimming_parameters = ThematicTrimmingParametersLocal(op_cost=False) - thematic_trimming_parameters = { - key: not value for key, value in thematic_trimming_parameters.model_dump().items() - } - study_path = str(tmp_path.absolute()) - expected_file_content = """[general] -mode = Economy -horizon = -nbyears = 1 -simulation.start = 1 -simulation.end = 365 -january.1st = Monday -first-month-in-year = January -first.weekday = Monday -leapyear = false -year-by-year = false -derated = false -custom-scenario = false -user-playlist = false -thematic-trimming = true -geographic-trimming = false -generate = -nbtimeseriesload = 1 -nbtimeserieshydro = 1 -nbtimeseriesthermal = 1 -nbtimeserieswind = 1 -nbtimeseriessolar = 1 -refreshtimeseries = -intra-modal = -inter-modal = -refreshintervalload = 100 -refreshintervalhydro = 100 refreshintervalthermal = 100 -refreshintervalwind = 100 refreshintervalsolar = 100 -readonly = false +readonly = False +derated = False +custom-scenario = False [input] import = [output] -synthesis = true -storenewset = false +synthesis = True +storenewset = False archives = -result-format = txt-files [optimization] simplex-range = week transmission-capacities = local-values -include-constraints = true -include-hurdlecosts = true -include-tc-minstablepower = true -include-tc-min-ud-time = true -include-dayahead = true -include-strategicreserve = true -include-spinningreserve = true -include-primaryreserve = true -include-exportmps = none -include-exportstructure = false +include-constraints = True +include-hurdlecosts = True +include-tc-minstablepower = True +include-tc-min-ud-time = True +include-dayahead = True +include-strategicreserve = True +include-spinningreserve = True +include-primaryreserve = True +include-exportmps = False +include-exportstructure = False include-unfeasible-problem-behavior = error-verbose [adequacy patch] -include-adq-patch = false -set-to-null-ntc-from-physical-out-to-physical-in-for-first-step = true -set-to-null-ntc-between-physical-out-for-first-step = true -enable-first-step = false +include-adq-patch = False +set-to-null-ntc-from-physical-out-to-physical-in-for-first-step = True +set-to-null-ntc-between-physical-out-for-first-step = True price-taking-order = DENS -include-hurdle-cost-csr = false -check-csr-cost-function = false -threshold-initiate-curtailment-sharing-rule = 0.000000 -threshold-display-local-matching-rule-violations = 0.000000 +include-hurdle-cost-csr = False +check-csr-cost-function = False +threshold-initiate-curtailment-sharing-rule = 0 +threshold-display-local-matching-rule-violations = 0 threshold-csr-variable-bounds-relaxation = 3 +enable-first-step = False [other preferences] initial-reservoir-levels = cold start @@ -820,12 +375,15 @@ def test_generaldata_ini_with_thematic_trimming_has_positive_sign(self, tmp_path hydro-pricing-mode = fast power-fluctuations = free modulations shedding-policy = shave peaks +shedding-strategy = shave margins unit-commitment-mode = fast number-of-cores-mode = medium -renewable-generation-modelling = aggregated +renewable-generation-modelling = clusters +day-ahead-reserve-management = global [advanced parameters] -accuracy-on-correlation = +accuracy-on-correlation = [] +adequacy-block-size = 100 [seeds - Mersenne Twister] seed-tsgen-wind = 5489 @@ -840,425 +398,15 @@ def test_generaldata_ini_with_thematic_trimming_has_positive_sign(self, tmp_path seed-hydro-costs = 9005489 seed-initial-reservoir-levels = 10005489 -[variables selection] -selected_vars_reset = false -select_var + = OP. COST - """ # When - new_study = create_study_local( - study_name, - study_version, - study_path, - StudySettingsLocal( - general_parameters=general_parameters, thematic_trimming_parameters=thematic_trimming_parameters - ), - ) - actual_generaldata_ini_file = new_study.service.config.study_path / InitializationFilesTypes.GENERAL.value + actual_generaldata_ini_file = local_study.service.config.study_path / InitializationFilesTypes.GENERAL.value actual_file_content = actual_generaldata_ini_file.read_text() # Then assert actual_file_content == expected_file_content - def test_generaldata_ini_with_thematic_trimming_two_variables(self, tmp_path): - # Given - study_name = "test study" - study_version = "880" - general_parameters = GeneralParametersLocal(thematic_trimming=True) - thematic_trimming_parameters = ThematicTrimmingParametersLocal(op_cost=False, ov_cost=False) - # Invert selection - thematic_trimming_parameters = { - key: not value for key, value in thematic_trimming_parameters.model_dump().items() - } - - study_path = str(tmp_path.absolute()) - expected_file_content = """[general] -mode = Economy -horizon = -nbyears = 1 -simulation.start = 1 -simulation.end = 365 -january.1st = Monday -first-month-in-year = January -first.weekday = Monday -leapyear = false -year-by-year = false -derated = false -custom-scenario = false -user-playlist = false -thematic-trimming = true -geographic-trimming = false -generate = -nbtimeseriesload = 1 -nbtimeserieshydro = 1 -nbtimeseriesthermal = 1 -nbtimeserieswind = 1 -nbtimeseriessolar = 1 -refreshtimeseries = -intra-modal = -inter-modal = -refreshintervalload = 100 -refreshintervalhydro = 100 -refreshintervalthermal = 100 -refreshintervalwind = 100 -refreshintervalsolar = 100 -readonly = false - -[input] -import = - -[output] -synthesis = true -storenewset = false -archives = -result-format = txt-files - -[optimization] -simplex-range = week -transmission-capacities = local-values -include-constraints = true -include-hurdlecosts = true -include-tc-minstablepower = true -include-tc-min-ud-time = true -include-dayahead = true -include-strategicreserve = true -include-spinningreserve = true -include-primaryreserve = true -include-exportmps = none -include-exportstructure = false -include-unfeasible-problem-behavior = error-verbose - -[adequacy patch] -include-adq-patch = false -set-to-null-ntc-from-physical-out-to-physical-in-for-first-step = true -set-to-null-ntc-between-physical-out-for-first-step = true -enable-first-step = false -price-taking-order = DENS -include-hurdle-cost-csr = false -check-csr-cost-function = false -threshold-initiate-curtailment-sharing-rule = 0.000000 -threshold-display-local-matching-rule-violations = 0.000000 -threshold-csr-variable-bounds-relaxation = 3 - -[other preferences] -initial-reservoir-levels = cold start -hydro-heuristic-policy = accommodate rule curves -hydro-pricing-mode = fast -power-fluctuations = free modulations -shedding-policy = shave peaks -unit-commitment-mode = fast -number-of-cores-mode = medium -renewable-generation-modelling = aggregated - -[advanced parameters] -accuracy-on-correlation = - -[seeds - Mersenne Twister] -seed-tsgen-wind = 5489 -seed-tsgen-load = 1005489 -seed-tsgen-hydro = 2005489 -seed-tsgen-thermal = 3005489 -seed-tsgen-solar = 4005489 -seed-tsnumbers = 5005489 -seed-unsupplied-energy-costs = 6005489 -seed-spilled-energy-costs = 7005489 -seed-thermal-costs = 8005489 -seed-hydro-costs = 9005489 -seed-initial-reservoir-levels = 10005489 - -[variables selection] -selected_vars_reset = false -""" - - expected_file_content += "select_var + = OV. COST\nselect_var + = OP. COST\n\n" - - # When - new_study = create_study_local( - study_name, - study_version, - study_path, - StudySettingsLocal( - general_parameters=general_parameters, thematic_trimming_parameters=thematic_trimming_parameters - ), - ) - - actual_generaldata_ini_file_path = new_study.service.config.study_path / InitializationFilesTypes.GENERAL.value - actual_file_content = actual_generaldata_ini_file_path.read_text() - - # Then - assert actual_file_content == expected_file_content - - def test_generaldata_ini_with_playlist_has_negative_sign(self, tmp_path): - # Given - study_name = "test study" - study_version = "880" - general_parameters = GeneralParametersLocal(selection_mode=True, thematic_trimming=True) - playlist_parameters = PlaylistParameters(playlist=[PlaylistData(), PlaylistData(), PlaylistData(status=False)]) - thematic_trimming_parameters = ThematicTrimmingParametersLocal(op_cost=False) - thematic_trimming_parameters = ThematicTrimmingParametersLocal.model_validate( - {key: not value for key, value in thematic_trimming_parameters.model_dump().items()} - ) - study_path = str(tmp_path.absolute()) - expected_file_content = """[general] -mode = Economy -horizon = -nbyears = 1 -simulation.start = 1 -simulation.end = 365 -january.1st = Monday -first-month-in-year = January -first.weekday = Monday -leapyear = false -year-by-year = false -derated = false -custom-scenario = false -user-playlist = true -thematic-trimming = true -geographic-trimming = false -generate = -nbtimeseriesload = 1 -nbtimeserieshydro = 1 -nbtimeseriesthermal = 1 -nbtimeserieswind = 1 -nbtimeseriessolar = 1 -refreshtimeseries = -intra-modal = -inter-modal = -refreshintervalload = 100 -refreshintervalhydro = 100 -refreshintervalthermal = 100 -refreshintervalwind = 100 -refreshintervalsolar = 100 -readonly = false - -[input] -import = - -[output] -synthesis = true -storenewset = false -archives = -result-format = txt-files - -[optimization] -simplex-range = week -transmission-capacities = local-values -include-constraints = true -include-hurdlecosts = true -include-tc-minstablepower = true -include-tc-min-ud-time = true -include-dayahead = true -include-strategicreserve = true -include-spinningreserve = true -include-primaryreserve = true -include-exportmps = none -include-exportstructure = false -include-unfeasible-problem-behavior = error-verbose - -[adequacy patch] -include-adq-patch = false -set-to-null-ntc-from-physical-out-to-physical-in-for-first-step = true -set-to-null-ntc-between-physical-out-for-first-step = true -enable-first-step = false -price-taking-order = DENS -include-hurdle-cost-csr = false -check-csr-cost-function = false -threshold-initiate-curtailment-sharing-rule = 0.000000 -threshold-display-local-matching-rule-violations = 0.000000 -threshold-csr-variable-bounds-relaxation = 3 - -[other preferences] -initial-reservoir-levels = cold start -hydro-heuristic-policy = accommodate rule curves -hydro-pricing-mode = fast -power-fluctuations = free modulations -shedding-policy = shave peaks -unit-commitment-mode = fast -number-of-cores-mode = medium -renewable-generation-modelling = aggregated - -[advanced parameters] -accuracy-on-correlation = - -[seeds - Mersenne Twister] -seed-tsgen-wind = 5489 -seed-tsgen-load = 1005489 -seed-tsgen-hydro = 2005489 -seed-tsgen-thermal = 3005489 -seed-tsgen-solar = 4005489 -seed-tsnumbers = 5005489 -seed-unsupplied-energy-costs = 6005489 -seed-spilled-energy-costs = 7005489 -seed-thermal-costs = 8005489 -seed-hydro-costs = 9005489 -seed-initial-reservoir-levels = 10005489 - -[playlist] -playlist_reset = true -playlist_year - = 2 - -[variables selection] -selected_vars_reset = false -select_var + = OP. COST - -""" - - # When - new_study = create_study_local( - study_name, - study_version, - study_path, - StudySettingsLocal( - general_parameters=general_parameters, - playlist_parameters=playlist_parameters, - thematic_trimming_parameters=thematic_trimming_parameters, - ), - ) - - actual_general_parameters_file_path = ( - new_study.service.config.study_path / InitializationFilesTypes.GENERAL.value - ) - actual_file_content = actual_general_parameters_file_path.read_text() - - # Then - assert actual_file_content == expected_file_content - - def test_generaldata_ini_with_playlist_has_positive_sign(self, tmp_path): - # Given - study_name = "test study" - study_version = "880" - general_parameters = GeneralParametersLocal(selection_mode=True, thematic_trimming=True) - playlist_parameters = PlaylistParameters( - playlist=[PlaylistData(status=False), PlaylistData(status=False), PlaylistData()] - ) - thematic_trimming_parameters = ThematicTrimmingParametersLocal(op_cost=False) - thematic_trimming_parameters = ThematicTrimmingParametersLocal.model_validate( - {key: not value for key, value in thematic_trimming_parameters.model_dump().items()} - ) - study_path = str(tmp_path.absolute()) - expected_file_content = """[general] -mode = Economy -horizon = -nbyears = 1 -simulation.start = 1 -simulation.end = 365 -january.1st = Monday -first-month-in-year = January -first.weekday = Monday -leapyear = false -year-by-year = false -derated = false -custom-scenario = false -user-playlist = true -thematic-trimming = true -geographic-trimming = false -generate = -nbtimeseriesload = 1 -nbtimeserieshydro = 1 -nbtimeseriesthermal = 1 -nbtimeserieswind = 1 -nbtimeseriessolar = 1 -refreshtimeseries = -intra-modal = -inter-modal = -refreshintervalload = 100 -refreshintervalhydro = 100 -refreshintervalthermal = 100 -refreshintervalwind = 100 -refreshintervalsolar = 100 -readonly = false - -[input] -import = - -[output] -synthesis = true -storenewset = false -archives = -result-format = txt-files - -[optimization] -simplex-range = week -transmission-capacities = local-values -include-constraints = true -include-hurdlecosts = true -include-tc-minstablepower = true -include-tc-min-ud-time = true -include-dayahead = true -include-strategicreserve = true -include-spinningreserve = true -include-primaryreserve = true -include-exportmps = none -include-exportstructure = false -include-unfeasible-problem-behavior = error-verbose - -[adequacy patch] -include-adq-patch = false -set-to-null-ntc-from-physical-out-to-physical-in-for-first-step = true -set-to-null-ntc-between-physical-out-for-first-step = true -enable-first-step = false -price-taking-order = DENS -include-hurdle-cost-csr = false -check-csr-cost-function = false -threshold-initiate-curtailment-sharing-rule = 0.000000 -threshold-display-local-matching-rule-violations = 0.000000 -threshold-csr-variable-bounds-relaxation = 3 - -[other preferences] -initial-reservoir-levels = cold start -hydro-heuristic-policy = accommodate rule curves -hydro-pricing-mode = fast -power-fluctuations = free modulations -shedding-policy = shave peaks -unit-commitment-mode = fast -number-of-cores-mode = medium -renewable-generation-modelling = aggregated - -[advanced parameters] -accuracy-on-correlation = - -[seeds - Mersenne Twister] -seed-tsgen-wind = 5489 -seed-tsgen-load = 1005489 -seed-tsgen-hydro = 2005489 -seed-tsgen-thermal = 3005489 -seed-tsgen-solar = 4005489 -seed-tsnumbers = 5005489 -seed-unsupplied-energy-costs = 6005489 -seed-spilled-energy-costs = 7005489 -seed-thermal-costs = 8005489 -seed-hydro-costs = 9005489 -seed-initial-reservoir-levels = 10005489 - -[playlist] -playlist_reset = false -playlist_year + = 2 - -[variables selection] -selected_vars_reset = false -select_var + = OP. COST - -""" - - # When - new_study = create_study_local( - study_name, - study_version, - study_path, - StudySettingsLocal( - general_parameters=general_parameters, - playlist_parameters=playlist_parameters, - thematic_trimming_parameters=thematic_trimming_parameters, - ), - ) - - actual_general_ini_file_path = new_study.service.config.study_path / InitializationFilesTypes.GENERAL.value - actual_file_content = actual_general_ini_file_path.read_text() - - # Then - assert actual_file_content == expected_file_content - class TestCreateArea: def test_initialization_when_creating_area(self, tmp_path, local_study): diff --git a/tests/integration/test_web_client.py b/tests/integration/test_web_client.py index 41c26c16..1230ff89 100644 --- a/tests/integration/test_web_client.py +++ b/tests/integration/test_web_client.py @@ -491,21 +491,18 @@ def test_creation_lifecycle(self, antares_web: AntaresWebDesktop, tmp_path): # test study creation with settings settings = StudySettings() - settings.general_parameters = GeneralParameters(mode="Adequacy") + settings.general_parameters = GeneralParameters(mode=Mode.ADEQUACY) settings.general_parameters.year_by_year = False - settings.playlist_parameters = PlaylistParameters() - settings.playlist_parameters.playlist = [{"status": False, "weight": 1}] + settings.playlist_parameters = {1: PlaylistParameters(status=False, weight=1)} new_study = create_study_api("second_study", "880", api_config, settings) settings = new_study.get_settings() assert settings.general_parameters.mode == Mode.ADEQUACY.value assert not settings.general_parameters.year_by_year - assert settings.playlist_parameters.model_dump() == {1: {"status": False, "weight": 1}} + assert settings.playlist_parameters == {1: PlaylistParameters(status=False, weight=1)} # tests update settings new_settings = StudySettings() - # Really important note. To instance such object with value you must respect camel case. - # Another way to do so is to instance the object and then fill its values - new_settings.general_parameters = GeneralParameters(nbYears=4) + new_settings.general_parameters = GeneralParameters(nb_years=4) new_settings.advanced_parameters = AdvancedParameters() new_settings.advanced_parameters.unit_commitment_mode = UnitCommitmentMode.MILP new_study.update_settings(new_settings)