diff --git a/src/antares/craft/model/settings/adequacy_patch.py b/src/antares/craft/model/settings/adequacy_patch.py index 6a9d3234..bdd06cdc 100644 --- a/src/antares/craft/model/settings/adequacy_patch.py +++ b/src/antares/craft/model/settings/adequacy_patch.py @@ -21,6 +21,19 @@ class PriceTakingOrder(Enum): @dataclass class AdequacyPatchParameters: + 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 + + +@dataclass +class AdequacyPatchParametersUpdate: 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 diff --git a/src/antares/craft/model/settings/advanced_parameters.py b/src/antares/craft/model/settings/advanced_parameters.py index 682cec66..7b2bec77 100644 --- a/src/antares/craft/model/settings/advanced_parameters.py +++ b/src/antares/craft/model/settings/advanced_parameters.py @@ -9,7 +9,7 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import Enum from typing import Optional @@ -63,6 +63,30 @@ class RenewableGenerationModeling(Enum): @dataclass class AdvancedParameters: + 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 + unit_commitment_mode: UnitCommitmentMode = UnitCommitmentMode.FAST + number_of_cores_mode: SimulationCore = SimulationCore.MEDIUM + renewable_generation_modelling: RenewableGenerationModeling = RenewableGenerationModeling.CLUSTERS + accuracy_on_correlation: set[OutputChoices] = field(default_factory=set) + + +@dataclass +class SeedParameters: + seed_tsgen_thermal: int = 3005489 + 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 + + +@dataclass +class AdvancedParametersUpdate: initial_reservoir_levels: Optional[InitialReservoirLevel] = None hydro_heuristic_policy: Optional[HydroHeuristicPolicy] = None hydro_pricing_mode: Optional[HydroPricingMode] = None @@ -75,7 +99,7 @@ class AdvancedParameters: @dataclass -class SeedParameters: +class SeedParametersUpdate: seed_tsgen_thermal: Optional[int] = None seed_tsnumbers: Optional[int] = None seed_unsupplied_energy_costs: Optional[int] = None diff --git a/src/antares/craft/model/settings/general.py b/src/antares/craft/model/settings/general.py index ea5bcc5b..bd3808a0 100644 --- a/src/antares/craft/model/settings/general.py +++ b/src/antares/craft/model/settings/general.py @@ -69,6 +69,27 @@ class OutputFormat(EnumIgnoreCase): @dataclass class GeneralParameters: + mode: Mode = Mode.ECONOMY + horizon: str = "" + nb_years: int = 1 + simulation_start: int = 1 + simulation_end: int = 365 + january_first: WeekDay = WeekDay.MONDAY + first_month_in_year: Month = Month.JANUARY + first_week_day: WeekDay = WeekDay.MONDAY + leap_year: bool = False + year_by_year: bool = False + simulation_synthesis: bool = True + building_mode: BuildingMode = BuildingMode.AUTOMATIC + user_playlist: bool = False + thematic_trimming: bool = False + geographic_trimming: bool = False + store_new_set: bool = False + nb_timeseries_thermal: int = 1 + + +@dataclass +class GeneralParametersUpdate: mode: Optional[Mode] = None horizon: Optional[str] = None nb_years: Optional[int] = None diff --git a/src/antares/craft/model/settings/optimization.py b/src/antares/craft/model/settings/optimization.py index ef519911..328e8711 100644 --- a/src/antares/craft/model/settings/optimization.py +++ b/src/antares/craft/model/settings/optimization.py @@ -35,14 +35,31 @@ class SimplexOptimizationRange(Enum): class ExportMPS(Enum): - NONE = "none" + TRUE = "True" + FALSE = "False" OPTIM1 = "optim1" OPTIM2 = "optim2" - BOTH_OPTIMS = "both-optims" @dataclass class OptimizationParameters: + 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: ExportMPS = ExportMPS.FALSE + include_exportstructure: bool = False + include_unfeasible_problem_behavior: UnfeasibleProblemBehavior = UnfeasibleProblemBehavior.ERROR_VERBOSE + + +@dataclass +class OptimizationParametersUpdate: simplex_range: Optional[SimplexOptimizationRange] = None transmission_capacities: Optional[OptimizationTransmissionCapacities] = None include_constraints: Optional[bool] = None diff --git a/src/antares/craft/model/settings/playlist_parameters.py b/src/antares/craft/model/settings/playlist_parameters.py index 38bf77a0..ae7e466c 100644 --- a/src/antares/craft/model/settings/playlist_parameters.py +++ b/src/antares/craft/model/settings/playlist_parameters.py @@ -14,5 +14,5 @@ @dataclass class PlaylistParameters: - status: bool = True - weight: float = 1.0 + status: bool + weight: float diff --git a/src/antares/craft/model/settings/study_settings.py b/src/antares/craft/model/settings/study_settings.py index 2cb48355..c3ac7055 100644 --- a/src/antares/craft/model/settings/study_settings.py +++ b/src/antares/craft/model/settings/study_settings.py @@ -9,23 +9,91 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. -from dataclasses import dataclass +from dataclasses import asdict, dataclass, field from typing import Optional -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.adequacy_patch import AdequacyPatchParameters, AdequacyPatchParametersUpdate +from antares.craft.model.settings.advanced_parameters import ( + AdvancedParameters, + AdvancedParametersUpdate, + SeedParameters, + SeedParametersUpdate, +) +from antares.craft.model.settings.general import GeneralParameters, GeneralParametersUpdate +from antares.craft.model.settings.optimization import OptimizationParameters, OptimizationParametersUpdate from antares.craft.model.settings.playlist_parameters import PlaylistParameters -from antares.craft.model.settings.thematic_trimming import ThematicTrimmingParameters +from antares.craft.model.settings.thematic_trimming import ThematicTrimmingParameters, ThematicTrimmingParametersUpdate @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 +class StudySettingsUpdate: + general_parameters: Optional[GeneralParametersUpdate] = None + optimization_parameters: Optional[OptimizationParametersUpdate] = None + advanced_parameters: Optional[AdvancedParametersUpdate] = None + seed_parameters: Optional[SeedParametersUpdate] = None + adequacy_patch_parameters: Optional[AdequacyPatchParametersUpdate] = None + thematic_trimming_parameters: Optional[ThematicTrimmingParametersUpdate] = None playlist_parameters: Optional[dict[int, PlaylistParameters]] = None - thematic_trimming_parameters: Optional[ThematicTrimmingParameters] = None + + +@dataclass +class StudySettings: + general_parameters: GeneralParameters = field(default_factory=GeneralParameters) + optimization_parameters: OptimizationParameters = field(default_factory=OptimizationParameters) + advanced_parameters: AdvancedParameters = field(default_factory=AdvancedParameters) + seed_parameters: SeedParameters = field(default_factory=SeedParameters) + adequacy_patch_parameters: AdequacyPatchParameters = field(default_factory=AdequacyPatchParameters) + thematic_trimming_parameters: ThematicTrimmingParameters = field(default_factory=ThematicTrimmingParameters) + playlist_parameters: dict[int, PlaylistParameters] = field(default_factory=dict) + + def from_update_settings(self, update_settings: StudySettingsUpdate) -> "StudySettings": + current_settings = asdict(self) + for key, values in asdict(update_settings).items(): + if values is not None: + for inner_key, inner_value in values.items(): + if inner_value is not None: + current_settings[key][inner_key] = inner_value + + general_parameters = GeneralParameters(**current_settings["general_parameters"]) + optimization_parameters = OptimizationParameters(**current_settings["optimization_parameters"]) + advanced_parameters = AdvancedParameters(**current_settings["advanced_parameters"]) + seed_parameters = SeedParameters(**current_settings["seed_parameters"]) + adequacy_patch_parameters = AdequacyPatchParameters(**current_settings["adequacy_patch_parameters"]) + thematic_trimming_parameters = ThematicTrimmingParameters(**current_settings["thematic_trimming_parameters"]) + playlist_parameters: dict[int, PlaylistParameters] = {} + for year in current_settings["playlist_parameters"]: + playlist_parameters[year] = PlaylistParameters(**current_settings["playlist_parameters"][year]) + + return StudySettings( + general_parameters, + optimization_parameters, + advanced_parameters, + seed_parameters, + adequacy_patch_parameters, + thematic_trimming_parameters, + playlist_parameters, + ) + + def to_update_settings(self) -> StudySettingsUpdate: + current_settings = asdict(self) + general_parameters = GeneralParametersUpdate(**current_settings["general_parameters"]) + optimization_parameters = OptimizationParametersUpdate(**current_settings["optimization_parameters"]) + advanced_parameters = AdvancedParametersUpdate(**current_settings["advanced_parameters"]) + seed_parameters = SeedParametersUpdate(**current_settings["seed_parameters"]) + adequacy_patch_parameters = AdequacyPatchParametersUpdate(**current_settings["adequacy_patch_parameters"]) + thematic_trimming_parameters = ThematicTrimmingParametersUpdate( + **current_settings["thematic_trimming_parameters"] + ) + playlist_parameters: dict[int, PlaylistParameters] = {} + for year in current_settings["playlist_parameters"]: + playlist_parameters[year] = PlaylistParameters(**current_settings["playlist_parameters"][year]) + + return StudySettingsUpdate( + general_parameters, + optimization_parameters, + advanced_parameters, + seed_parameters, + adequacy_patch_parameters, + thematic_trimming_parameters, + playlist_parameters, + ) diff --git a/src/antares/craft/model/settings/thematic_trimming.py b/src/antares/craft/model/settings/thematic_trimming.py index 7b8f3270..4293cbf8 100644 --- a/src/antares/craft/model/settings/thematic_trimming.py +++ b/src/antares/craft/model/settings/thematic_trimming.py @@ -15,6 +15,104 @@ @dataclass class ThematicTrimmingParameters: + ov_cost: bool = False + op_cost: bool = False + mrg_price: bool = False + co2_emis: bool = False + dtg_by_plant: bool = False + balance: bool = False + row_bal: bool = False + psp: bool = False + misc_ndg: bool = False + load: bool = False + h_ror: bool = False + wind: bool = False + solar: bool = False + nuclear: bool = False + lignite: bool = False + coal: bool = False + gas: bool = False + oil: bool = False + mix_fuel: bool = False + misc_dtg: bool = False + h_stor: bool = False + h_pump: bool = False + h_lev: bool = False + h_infl: bool = False + h_ovfl: bool = False + h_val: bool = False + h_cost: bool = False + unsp_enrg: bool = False + spil_enrg: bool = False + lold: bool = False + lolp: bool = False + avl_dtg: bool = False + dtg_mrg: bool = False + max_mrg: bool = False + np_cost: bool = False + np_cost_by_plant: bool = False + nodu: bool = False + nodu_by_plant: bool = False + flow_lin: bool = False + ucap_lin: bool = False + loop_flow: bool = False + flow_quad: bool = False + cong_fee_alg: bool = False + cong_fee_abs: bool = False + marg_cost: bool = False + cong_prob_plus: bool = False + cong_prob_minus: bool = False + hurdle_cost: bool = False + res_generation_by_plant: bool = False + misc_dtg_2: bool = False + misc_dtg_3: bool = False + misc_dtg_4: bool = False + wind_offshore: bool = False + wind_onshore: bool = False + solar_concrt: bool = False + solar_pv: bool = False + solar_rooft: bool = False + renw_1: bool = False + renw_2: bool = False + renw_3: bool = False + renw_4: bool = False + dens: bool = False + profit_by_plant: bool = False + sts_inj_by_plant: bool = False + sts_withdrawal_by_plant: bool = False + sts_lvl_by_plant: bool = False + psp_open_injection: bool = False + psp_open_withdrawal: bool = False + psp_open_level: bool = False + psp_closed_injection: bool = False + psp_closed_withdrawal: bool = False + psp_closed_level: bool = False + pondage_injection: bool = False + pondage_withdrawal: bool = False + pondage_level: bool = False + battery_injection: bool = False + battery_withdrawal: bool = False + battery_level: bool = False + other1_injection: bool = False + other1_withdrawal: bool = False + other1_level: bool = False + other2_injection: bool = False + other2_withdrawal: bool = False + other2_level: bool = False + other3_injection: bool = False + other3_withdrawal: bool = False + other3_level: bool = False + other4_injection: bool = False + other4_withdrawal: bool = False + other4_level: bool = False + other5_injection: bool = False + other5_withdrawal: bool = False + other5_level: bool = False + sts_cashflow_by_cluster: bool = False + + +@dataclass +class ThematicTrimmingParametersUpdate: ov_cost: Optional[bool] = None op_cost: Optional[bool] = None mrg_price: Optional[bool] = None diff --git a/src/antares/craft/model/study.py b/src/antares/craft/model/study.py index ecb4aea9..040deb41 100644 --- a/src/antares/craft/model/study.py +++ b/src/antares/craft/model/study.py @@ -38,7 +38,7 @@ ) from antares.craft.model.link import Link, LinkProperties, LinkUi from antares.craft.model.output import Output -from antares.craft.model.settings.study_settings import StudySettings +from antares.craft.model.settings.study_settings import StudySettings, StudySettingsUpdate from antares.craft.model.simulation import AntaresSimulationParameters, Job from antares.craft.service.base_services import BaseLinkService, BaseStudyService from antares.craft.service.local_services.services.settings import edit_study_settings @@ -58,7 +58,6 @@ def create_study_api( study_name: str, version: str, api_config: APIconf, - settings: Optional[StudySettings] = None, parent_path: Optional[Path] = None, ) -> "Study": """ @@ -66,7 +65,6 @@ def create_study_api( study_name: antares study name to be created version: antares version api_config: host and token config for API - settings: study settings. If not provided, AntaresWeb will use its default values. Raises: MissingTokenError if api_token is missing @@ -83,10 +81,7 @@ def create_study_api( study_id = response.json() # Settings part study = Study(study_name, version, ServiceFactory(api_config, study_id)) - if settings: - study.update_settings(settings) - else: - study.read_settings() + study.read_settings() # Move part if parent_path: study.move(parent_path) @@ -123,12 +118,7 @@ def import_study_api(api_config: APIconf, study_path: Path, destination_path: Op raise StudyImportError(study_path.name, e.message) from e -def create_study_local( - study_name: str, - version: str, - parent_directory: str, - settings: StudySettings = StudySettings(), -) -> "Study": +def create_study_local(study_name: str, version: str, parent_directory: Path) -> "Study": """ Create a directory structure for the study with empty files. @@ -136,12 +126,11 @@ def create_study_local( study_name: antares study name to be created version: antares version for study parent_directory: Local directory to store the study in. - settings: study settings. If not provided, AntaresCraft will use its default values. Raises: FileExistsError if the study already exists in the given location """ - local_config = LocalConfiguration(Path(parent_directory), study_name) + local_config = LocalConfiguration(parent_directory, study_name) study_directory = local_config.local_path / study_name @@ -184,7 +173,10 @@ def create_study_local( path=study_directory, ) # We need to create the file with default value - study._settings = edit_study_settings(study_directory, settings, False) + default_settings = StudySettings() + update_settings = default_settings.to_update_settings() + edit_study_settings(study_directory, update_settings, True) + study._settings = default_settings return study @@ -303,8 +295,9 @@ def read_settings(self) -> StudySettings: self._settings = study_settings return study_settings - def update_settings(self, settings: StudySettings) -> None: - self._settings = self._settings_service.edit_study_settings(settings) + def update_settings(self, settings: StudySettingsUpdate) -> None: + self._settings_service.edit_study_settings(settings) + self._settings = self._settings_service.read_study_settings() def get_areas(self) -> MappingProxyType[str, Area]: return MappingProxyType(dict(sorted(self._areas.items()))) diff --git a/src/antares/craft/service/api_services/models/settings.py b/src/antares/craft/service/api_services/models/settings.py index 77a5cfa7..bc644899 100644 --- a/src/antares/craft/service/api_services/models/settings.py +++ b/src/antares/craft/service/api_services/models/settings.py @@ -9,18 +9,26 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. +import ast + from dataclasses import asdict -from typing import Optional +from typing import Any, Optional, Sequence, Union, cast -from antares.craft.model.settings.adequacy_patch import AdequacyPatchParameters, PriceTakingOrder +from antares.craft.model.settings.adequacy_patch import ( + AdequacyPatchParameters, + AdequacyPatchParametersUpdate, + PriceTakingOrder, +) from antares.craft.model.settings.advanced_parameters import ( AdvancedParameters, + AdvancedParametersUpdate, HydroHeuristicPolicy, HydroPricingMode, InitialReservoirLevel, PowerFluctuation, RenewableGenerationModeling, SeedParameters, + SeedParametersUpdate, SheddingPolicy, SimulationCore, UnitCommitmentMode, @@ -28,6 +36,7 @@ from antares.craft.model.settings.general import ( BuildingMode, GeneralParameters, + GeneralParametersUpdate, Mode, Month, OutputChoices, @@ -37,31 +46,34 @@ from antares.craft.model.settings.optimization import ( ExportMPS, OptimizationParameters, + OptimizationParametersUpdate, OptimizationTransmissionCapacities, SimplexOptimizationRange, UnfeasibleProblemBehavior, ) -from antares.craft.model.settings.thematic_trimming import ThematicTrimmingParameters +from antares.craft.model.settings.thematic_trimming import ThematicTrimmingParameters, ThematicTrimmingParametersUpdate from antares.craft.tools.all_optional_meta import all_optional_model -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_validator from pydantic.alias_generators import to_camel +AdequacyPatchParametersType = Union[AdequacyPatchParameters, AdequacyPatchParametersUpdate] + @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 +class AdequacyPatchParametersAPI(BaseModel, alias_generator=to_camel, populate_by_name=True): + enable_adequacy_patch: bool + ntc_from_physical_areas_out_to_physical_areas_in_adequacy_patch: bool + ntc_between_physical_areas_out_adequacy_patch: bool + price_taking_order: PriceTakingOrder + include_hurdle_cost_csr: bool + check_csr_cost_function: bool + enable_first_step: bool + threshold_initiate_curtailment_sharing_rule: int + threshold_display_local_matching_rule_violations: int + threshold_csr_variable_bounds_relaxation: int @staticmethod - def from_user_model(user_class: AdequacyPatchParameters) -> "AdequacyPatchParametersAPI": + def from_user_model(user_class: AdequacyPatchParametersType) -> "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( @@ -86,8 +98,12 @@ def to_user_model(self) -> AdequacyPatchParameters: ) +AdvancedParametersType = Union[AdvancedParameters, AdvancedParametersUpdate] +SeedParametersType = Union[SeedParameters, SeedParametersUpdate] + + @all_optional_model -class AdvancedAndSeedParametersAPI(BaseModel, alias_generator=to_camel): +class AdvancedAndSeedParametersAPI(BaseModel, alias_generator=to_camel, populate_by_name=True): accuracy_on_correlation: set[OutputChoices] initial_reservoir_levels: InitialReservoirLevel hydro_heuristic_policy: HydroHeuristicPolicy @@ -109,25 +125,18 @@ class AdvancedAndSeedParametersAPI(BaseModel, alias_generator=to_camel): 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", - } + @field_validator("accuracy_on_correlation", mode="before") + def validate_accuracy_on_correlation(cls, v: Any) -> Union[Sequence[str], set[str]]: + if not v: + return set() + if isinstance(v, set): + return v + return cast(Sequence[str], ast.literal_eval(v)) @staticmethod def from_user_model( - advanced_parameters: Optional[AdvancedParameters] = None, seed_parameters: Optional[SeedParameters] = None + advanced_parameters: Optional[AdvancedParametersType] = None, + seed_parameters: Optional[SeedParametersType] = None, ) -> "AdvancedAndSeedParametersAPI": advanced_parameters_dict = asdict(advanced_parameters) if advanced_parameters else {} seed_parameters_dict = asdict(seed_parameters) if seed_parameters else {} @@ -135,12 +144,31 @@ def from_user_model( 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)) + return AdvancedParameters( + 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, + accuracy_on_correlation=self.accuracy_on_correlation, + ) def to_user_seed_parameters_model(self) -> SeedParameters: - included_fields = set(asdict(SeedParameters()).keys()) - return SeedParameters(**self.model_dump(mode="json", include=included_fields)) + return SeedParameters( + seed_tsgen_thermal=self.seed_tsgen_thermal, + seed_tsnumbers=self.seed_tsnumbers, + seed_unsupplied_energy_costs=self.seed_unsupplied_energy_costs, + seed_spilled_energy_costs=self.seed_spilled_energy_costs, + seed_thermal_costs=self.seed_thermal_costs, + seed_hydro_costs=self.seed_hydro_costs, + seed_initial_reservoir_levels=self.seed_initial_reservoir_levels, + ) + + +GeneralParametersType = Union[GeneralParameters, GeneralParametersUpdate] @all_optional_model @@ -166,7 +194,7 @@ class GeneralParametersAPI(BaseModel, extra="forbid", populate_by_name=True, ali result_format: OutputFormat @staticmethod - def from_user_model(user_class: GeneralParameters) -> "GeneralParametersAPI": + def from_user_model(user_class: GeneralParametersType) -> "GeneralParametersAPI": user_dict = asdict(user_class) user_dict["first_day"] = user_dict.pop("simulation_start") user_dict["last_day"] = user_dict.pop("simulation_end") @@ -199,8 +227,11 @@ def to_user_model(self, nb_ts_thermal: int) -> GeneralParameters: ) +OptimizationParametersType = Union[OptimizationParameters, OptimizationParametersUpdate] + + @all_optional_model -class OptimizationParametersAPI(BaseModel, alias_generator=to_camel): +class OptimizationParametersAPI(BaseModel, alias_generator=to_camel, populate_by_name=True): simplex_optimization_range: SimplexOptimizationRange transmission_capacities: OptimizationTransmissionCapacities binding_constraints: bool @@ -216,11 +247,11 @@ class OptimizationParametersAPI(BaseModel, alias_generator=to_camel): unfeasible_problem_behavior: UnfeasibleProblemBehavior @staticmethod - def from_user_model(user_class: OptimizationParameters) -> "OptimizationParametersAPI": + def from_user_model(user_class: OptimizationParametersType) -> "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["hurdle_costs"] = user_dict.pop("include_hurdlecosts") 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") @@ -250,8 +281,11 @@ def to_user_model(self) -> OptimizationParameters: ) +ThematicTrimmingParametersType = Union[ThematicTrimmingParameters, ThematicTrimmingParametersUpdate] + + @all_optional_model -class ThematicTrimmingParametersAPI(BaseModel, alias_generator=to_camel): +class ThematicTrimmingParametersAPI(BaseModel, alias_generator=to_camel, populate_by_name=True): ov_cost: bool op_cost: bool mrg_price: bool @@ -348,7 +382,7 @@ class ThematicTrimmingParametersAPI(BaseModel, alias_generator=to_camel): sts_cashflow_by_cluster: bool @staticmethod - def from_user_model(user_class: ThematicTrimmingParameters) -> "ThematicTrimmingParametersAPI": + def from_user_model(user_class: ThematicTrimmingParametersType) -> "ThematicTrimmingParametersAPI": user_dict = asdict(user_class) return ThematicTrimmingParametersAPI.model_validate(user_dict) diff --git a/src/antares/craft/service/api_services/services/settings.py b/src/antares/craft/service/api_services/services/settings.py index 925f3648..5146649e 100644 --- a/src/antares/craft/service/api_services/services/settings.py +++ b/src/antares/craft/service/api_services/services/settings.py @@ -14,8 +14,9 @@ from antares.craft.api_conf.api_conf import APIconf from antares.craft.api_conf.request_wrapper import RequestWrapper from antares.craft.exceptions.exceptions import APIError, StudySettingsReadError, StudySettingsUpdateError +from antares.craft.model.settings.optimization import ExportMPS from antares.craft.model.settings.playlist_parameters import PlaylistParameters -from antares.craft.model.settings.study_settings import StudySettings +from antares.craft.model.settings.study_settings import StudySettings, StudySettingsUpdate from antares.craft.service.api_services.models.settings import ( AdequacyPatchParametersAPI, AdvancedAndSeedParametersAPI, @@ -36,10 +37,9 @@ def __init__(self, config: APIconf, study_id: str): self._wrapper = RequestWrapper(self.config.set_up_api_conf()) @override - def edit_study_settings(self, settings: StudySettings) -> StudySettings: + def edit_study_settings(self, settings: StudySettingsUpdate) -> None: try: edit_study_settings(self._base_url, self.study_id, self._wrapper, settings) - return settings except APIError as e: raise StudySettingsUpdateError(self.study_id, e.message) from e @@ -51,14 +51,14 @@ def read_study_settings(self) -> StudySettings: raise StudySettingsReadError(self.study_id, e.message) from e -def edit_study_settings(base_url: str, study_id: str, wrapper: RequestWrapper, settings: StudySettings) -> None: +def edit_study_settings(base_url: str, study_id: str, wrapper: RequestWrapper, settings: StudySettingsUpdate) -> 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) + body = api_model.model_dump(mode="json", exclude_none=True, by_alias=True) wrapper.put(thematic_trimming_url, json=body) # playlist @@ -73,14 +73,16 @@ def edit_study_settings(base_url: str, study_id: str, wrapper: RequestWrapper, s 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) + body = optimization_api_model.model_dump(mode="json", exclude_none=True, by_alias=True) + if "includeExportstructure" in body: + raise APIError("AntaresWeb doesn't support editing the parameter include_exportstructure") 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) + body = general_api_model.model_dump(mode="json", exclude_none=True, by_alias=True) wrapper.put(general_url, json=body) if nb_ts_thermal := settings.general_parameters.nb_timeseries_thermal: @@ -93,14 +95,16 @@ def edit_study_settings(base_url: str, study_id: str, wrapper: RequestWrapper, s 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) + body = advanced_api_model.model_dump(mode="json", exclude_none=True, by_alias=True) + if "accuracyOnCorrelation" in body: + body["accuracyOnCorrelation"] = ", ".join(corr for corr in body["accuracyOnCorrelation"]) 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) + body = adequacy_patch_api_model.model_dump(mode="json", exclude_none=True, by_alias=True) wrapper.put(adequacy_patch_url, json=body) @@ -124,7 +128,13 @@ def read_study_settings_api(base_url: str, study_id: str, wrapper: RequestWrappe # optimization optimization_url = f"{settings_base_url}/optimization/form" response = wrapper.get(optimization_url) - optimization_api_model = OptimizationParametersAPI.model_validate(response.json()) + json_response = response.json() + export_mps_value = json_response.get("exportMps", None) + if export_mps_value is True: + json_response["exportMps"] = ExportMPS.TRUE + elif export_mps_value is False: + json_response["exportMps"] = ExportMPS.FALSE + optimization_api_model = OptimizationParametersAPI.model_validate(json_response) optimization_parameters = optimization_api_model.to_user_model() # general and timeseries diff --git a/src/antares/craft/service/base_services.py b/src/antares/craft/service/base_services.py index 838ae4cd..9324b88a 100644 --- a/src/antares/craft/service/base_services.py +++ b/src/antares/craft/service/base_services.py @@ -17,7 +17,7 @@ import pandas as pd from antares.craft.config.base_configuration import BaseConfiguration -from antares.craft.model.settings.study_settings import StudySettings +from antares.craft.model.settings.study_settings import StudySettings, StudySettingsUpdate from antares.craft.model.simulation import AntaresSimulationParameters, Job if TYPE_CHECKING: @@ -746,14 +746,12 @@ def aggregate_values( class BaseStudySettingsService(ABC): @abstractmethod - def edit_study_settings(self, settings: StudySettings) -> StudySettings: + def edit_study_settings(self, settings: StudySettingsUpdate) -> None: """ Edit the settings for a given study Args: - settings: the new Settings for the study - - Returns: the new Settings for the study + settings: the settings to update with their values """ pass diff --git a/src/antares/craft/service/local_services/models/settings.py b/src/antares/craft/service/local_services/models/settings.py index 04adaf86..f2b7f4f6 100644 --- a/src/antares/craft/service/local_services/models/settings.py +++ b/src/antares/craft/service/local_services/models/settings.py @@ -13,24 +13,40 @@ import ast from dataclasses import asdict -from typing import Any, Sequence, Set, cast +from typing import Any, Sequence, Set, Union, cast -from antares.craft.model.settings.adequacy_patch import AdequacyPatchParameters, PriceTakingOrder +from antares.craft.model.settings.adequacy_patch import ( + AdequacyPatchParameters, + AdequacyPatchParametersUpdate, + PriceTakingOrder, +) from antares.craft.model.settings.advanced_parameters import ( AdvancedParameters, + AdvancedParametersUpdate, HydroHeuristicPolicy, HydroPricingMode, InitialReservoirLevel, PowerFluctuation, RenewableGenerationModeling, SeedParameters, + SeedParametersUpdate, SheddingPolicy, SimulationCore, UnitCommitmentMode, ) -from antares.craft.model.settings.general import BuildingMode, GeneralParameters, Mode, Month, OutputChoices, WeekDay +from antares.craft.model.settings.general import ( + BuildingMode, + GeneralParameters, + GeneralParametersUpdate, + Mode, + Month, + OutputChoices, + WeekDay, +) from antares.craft.model.settings.optimization import ( + ExportMPS, OptimizationParameters, + OptimizationParametersUpdate, OptimizationTransmissionCapacities, SimplexOptimizationRange, UnfeasibleProblemBehavior, @@ -54,6 +70,9 @@ def _usedefault_for_none(cls, value: Any) -> Any: return value +AdequacyPatchParametersType = Union[AdequacyPatchParameters, AdequacyPatchParametersUpdate] + + 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 @@ -67,13 +86,26 @@ class AdequacyPatchParametersLocal(LocalBaseModel, alias_generator=to_kebab): enable_first_step: bool = False @staticmethod - def from_user_model(user_class: AdequacyPatchParameters) -> "AdequacyPatchParametersLocal": + def from_user_model(user_class: AdequacyPatchParametersType) -> "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) + return AdequacyPatchParameters( + include_adq_patch=self.include_adq_patch, + set_to_null_ntc_from_physical_out_to_physical_in_for_first_step=self.set_to_null_ntc_from_physical_out_to_physical_in_for_first_step, + set_to_null_ntc_between_physical_out_for_first_step=self.set_to_null_ntc_between_physical_out_for_first_step, + 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, + ) + + +AdvancedParametersType = Union[AdvancedParameters, AdvancedParametersUpdate] +SeedParametersType = Union[SeedParameters, SeedParametersUpdate] class OtherPreferencesLocal(LocalBaseModel, alias_generator=to_kebab): @@ -94,10 +126,11 @@ class AdvancedParametersLocal(LocalBaseModel, alias_generator=to_kebab): 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.""" + def validate_accuracy_on_correlation(cls, v: Any) -> Union[Sequence[str], set[str]]: if v is None: return [] + if isinstance(v, set): + return v return cast(Sequence[str], ast.literal_eval(v)) @@ -122,7 +155,7 @@ class AdvancedAndSeedParametersLocal(LocalBaseModel): @staticmethod def from_user_model( - advanced_parameters: AdvancedParameters, seed_parameters: SeedParameters + advanced_parameters: AdvancedParametersType, seed_parameters: SeedParametersType ) -> "AdvancedAndSeedParametersLocal": other_preferences_local_dict = asdict(advanced_parameters) advanced_local_dict = { @@ -136,15 +169,31 @@ def from_user_model( 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) + return SeedParameters( + seed_tsgen_thermal=self.seeds.seed_tsgen_thermal, + seed_tsnumbers=self.seeds.seed_tsnumbers, + seed_unsupplied_energy_costs=self.seeds.seed_unsupplied_energy_costs, + seed_spilled_energy_costs=self.seeds.seed_spilled_energy_costs, + seed_thermal_costs=self.seeds.seed_thermal_costs, + seed_hydro_costs=self.seeds.seed_hydro_costs, + seed_initial_reservoir_levels=self.seeds.seed_initial_reservoir_levels, + ) 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) + return AdvancedParameters( + initial_reservoir_levels=self.other_preferences.initial_reservoir_levels, + hydro_heuristic_policy=self.other_preferences.hydro_heuristic_policy, + hydro_pricing_mode=self.other_preferences.hydro_pricing_mode, + power_fluctuations=self.other_preferences.power_fluctuations, + shedding_policy=self.other_preferences.shedding_policy, + unit_commitment_mode=self.other_preferences.unit_commitment_mode, + number_of_cores_mode=self.other_preferences.number_of_cores_mode, + renewable_generation_modelling=self.other_preferences.renewable_generation_modelling, + accuracy_on_correlation=self.advanced_parameters.accuracy_on_correlation, + ) + + +GeneralParametersType = Union[GeneralParameters, GeneralParametersUpdate] class GeneralSectionLocal(LocalBaseModel): @@ -191,7 +240,7 @@ class GeneralParametersLocal(LocalBaseModel): output: OutputSectionLocal @staticmethod - def from_user_model(user_class: GeneralParameters) -> "GeneralParametersLocal": + def from_user_model(user_class: GeneralParametersType) -> "GeneralParametersLocal": user_dict = asdict(user_class) output_dict = { @@ -225,12 +274,28 @@ def get_excluded_fields_for_user_class() -> Set[str]: } 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() + return GeneralParameters( + mode=self.general.mode, + horizon=self.general.horizon, + nb_years=self.general.nb_years, + simulation_start=self.general.simulation_start, + simulation_end=self.general.simulation_end, + january_first=self.general.january_first, + first_month_in_year=self.general.first_month_in_year, + first_week_day=self.general.first_week_day, + leap_year=self.general.leap_year, + year_by_year=self.general.year_by_year, + simulation_synthesis=self.output.synthesis, + building_mode=self.general.building_mode, + user_playlist=self.general.user_playlist, + thematic_trimming=self.general.thematic_trimming, + geographic_trimming=self.general.geographic_trimming, + store_new_set=self.output.store_new_set, + nb_timeseries_thermal=self.general.nb_timeseries_thermal, ) - 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) + + +OptimizationParametersType = Union[OptimizationParameters, OptimizationParametersUpdate] class OptimizationParametersLocal(LocalBaseModel, alias_generator=to_kebab): @@ -244,15 +309,28 @@ class OptimizationParametersLocal(LocalBaseModel, alias_generator=to_kebab): include_strategicreserve: bool = True include_spinningreserve: bool = True include_primaryreserve: bool = True - include_exportmps: bool = False + include_exportmps: ExportMPS = ExportMPS.FALSE include_exportstructure: bool = False include_unfeasible_problem_behavior: UnfeasibleProblemBehavior = UnfeasibleProblemBehavior.ERROR_VERBOSE @staticmethod - def from_user_model(user_class: OptimizationParameters) -> "OptimizationParametersLocal": + def from_user_model(user_class: OptimizationParametersType) -> "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) + return OptimizationParameters( + simplex_range=self.simplex_range, + transmission_capacities=self.transmission_capacities, + include_constraints=self.include_constraints, + include_hurdlecosts=self.include_hurdlecosts, + include_tc_minstablepower=self.include_tc_minstablepower, + include_tc_min_ud_time=self.include_tc_min_ud_time, + include_dayahead=self.include_dayahead, + include_strategicreserve=self.include_strategicreserve, + include_spinningreserve=self.include_spinningreserve, + include_primaryreserve=self.include_primaryreserve, + include_exportmps=self.include_exportmps, + include_exportstructure=self.include_exportstructure, + include_unfeasible_problem_behavior=self.include_unfeasible_problem_behavior, + ) diff --git a/src/antares/craft/service/local_services/services/settings.py b/src/antares/craft/service/local_services/services/settings.py index d334c900..6d78a24f 100644 --- a/src/antares/craft/service/local_services/services/settings.py +++ b/src/antares/craft/service/local_services/services/settings.py @@ -14,16 +14,14 @@ from typing import Any from antares.craft.config.local_configuration import LocalConfiguration -from antares.craft.model.settings.adequacy_patch import AdequacyPatchParameters from antares.craft.model.settings.advanced_parameters import ( - AdvancedParameters, - SeedParameters, + AdvancedParametersUpdate, + SeedParametersUpdate, ) -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.model.settings.general import BuildingMode +from antares.craft.model.settings.playlist_parameters import PlaylistParameters +from antares.craft.model.settings.study_settings import StudySettings, StudySettingsUpdate +from antares.craft.model.settings.thematic_trimming import ThematicTrimmingParameters from antares.craft.service.base_services import BaseStudySettingsService from antares.craft.service.local_services.models.settings import ( AdequacyPatchParametersLocal, @@ -45,8 +43,8 @@ def __init__(self, config: LocalConfiguration, study_name: str, **kwargs: Any) - self.study_name = study_name @override - def edit_study_settings(self, settings: StudySettings) -> StudySettings: - return edit_study_settings(self.config.study_path, settings, update=True) + def edit_study_settings(self, settings: StudySettingsUpdate) -> None: + edit_study_settings(self.config.study_path, settings, creation=False) @override def read_study_settings(self) -> StudySettings: @@ -100,15 +98,15 @@ def read_study_settings(study_directory: Path) -> StudySettings: advanced_parameters = seed_and_advanced_local_parameters.to_advanced_parameters_model() # playlist - playlist_parameters = None + playlist_parameters: dict[int, PlaylistParameters] = {} if "playlist" in ini_content: - playlist_parameters = None + playlist_parameters = {} # todo # thematic trimming - thematic_trimming_parameters = None + thematic_trimming_parameters = ThematicTrimmingParameters() if "variables selection" in ini_content: - thematic_trimming_parameters = None + thematic_trimming_parameters = ThematicTrimmingParameters() # todo return StudySettings( @@ -122,48 +120,44 @@ def read_study_settings(study_directory: Path) -> StudySettings: ) -def edit_study_settings(study_directory: Path, settings: StudySettings, update: bool) -> StudySettings: +def edit_study_settings(study_directory: Path, settings: StudySettingsUpdate, creation: bool) -> None: general_data_ini = IniFile(study_directory, InitializationFilesTypes.GENERAL) - ini_content = general_data_ini.ini_dict if update else {} + update = not creation + ini_content = {} if creation else general_data_ini.ini_dict # general - general_parameters = settings.general_parameters or GeneralParameters() - general_local_parameters = GeneralParametersLocal.from_user_model(general_parameters) + if settings.general_parameters: + general_local_parameters = GeneralParametersLocal.from_user_model(settings.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 + 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() + ini_content.update(json_content) # 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() + if settings.optimization_parameters: + optimization_local_parameters = OptimizationParametersLocal.from_user_model(settings.optimization_parameters) + ini_content.update( + {"optimization": optimization_local_parameters.model_dump(mode="json", by_alias=True, exclude_unset=update)} + ) # 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() + if settings.adequacy_patch_parameters: + adequacy_local_parameters = AdequacyPatchParametersLocal.from_user_model(settings.adequacy_patch_parameters) + ini_content.update( + {"adequacy patch": adequacy_local_parameters.model_dump(mode="json", by_alias=True, exclude_unset=update)} + ) # seed and advanced - seed_parameters = settings.seed_parameters or SeedParameters() - advanced_parameters = settings.advanced_parameters or AdvancedParameters() + seed_parameters = settings.seed_parameters or SeedParametersUpdate() + advanced_parameters = settings.advanced_parameters or AdvancedParametersUpdate() 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 @@ -174,14 +168,3 @@ def edit_study_settings(study_directory: Path, settings: StudySettings, update: # 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/tests/antares/integration/test_local_client.py b/tests/antares/integration/test_local_client.py index 34a71ec3..d9b23db8 100644 --- a/tests/antares/integration/test_local_client.py +++ b/tests/antares/integration/test_local_client.py @@ -11,6 +11,8 @@ # This file is part of the Antares project. import pytest +from pathlib import Path + import numpy as np import pandas as pd @@ -20,9 +22,12 @@ 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.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.settings.advanced_parameters import ( + AdvancedParametersUpdate, + UnitCommitmentMode, +) +from antares.craft.model.settings.general import GeneralParametersUpdate +from antares.craft.model.settings.study_settings import StudySettingsUpdate 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 @@ -34,7 +39,7 @@ class TestLocalClient: Testing lifespan of a study in local mode. Creating a study, adding areas, links, clusters and so on. """ - def test_local_study(self, tmp_path, unknown_area): + def test_local_study(self, tmp_path: Path, unknown_area): study_name = "test study" study_version = "880" @@ -248,9 +253,10 @@ def test_local_study(self, tmp_path, unknown_area): } # test study creation with settings - 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 = StudySettingsUpdate() + settings.general_parameters = GeneralParametersUpdate(nb_years=4) + settings.advanced_parameters = AdvancedParametersUpdate(unit_commitment_mode=UnitCommitmentMode.MILP) + new_study = create_study_local("second_study", "880", tmp_path) + new_study.update_settings(settings) assert new_study.get_settings().general_parameters.nb_years == 4 - assert new_study.get_settings().advanced_parameters.unit_commitment_mode == UnitCommitmentMode.MILP.value + assert new_study.get_settings().advanced_parameters.unit_commitment_mode == UnitCommitmentMode.MILP diff --git a/tests/antares/services/api_services/test_study_api.py b/tests/antares/services/api_services/test_study_api.py index b4d31102..69b5ae18 100644 --- a/tests/antares/services/api_services/test_study_api.py +++ b/tests/antares/services/api_services/test_study_api.py @@ -50,8 +50,8 @@ from antares.craft.model.output import ( Output, ) -from antares.craft.model.settings.general import GeneralParameters -from antares.craft.model.settings.study_settings import StudySettings +from antares.craft.model.settings.general import GeneralParametersUpdate, Mode +from antares.craft.model.settings.study_settings import StudySettingsUpdate from antares.craft.model.simulation import AntaresSimulationParameters, Job, JobStatus, Solver from antares.craft.model.study import Study, create_study_api, create_variant_api, import_study_api, read_study_api from antares.craft.service.api_services.output_api import OutputApiService @@ -113,17 +113,19 @@ def test_create_study_fails(self): def test_update_study_settings_success(self): with requests_mock.Mocker() as mocker: - settings = StudySettings() - settings.general_parameters = GeneralParameters(mode="Adequacy") + settings = StudySettingsUpdate() + settings.general_parameters = GeneralParametersUpdate(mode=Mode.ADEQUACY) config_urls = re.compile(f"https://antares.com/api/v1/studies/{self.study_id}/config/.*") mocker.put(config_urls, status_code=200) - mocker.get(config_urls, json={}, status_code=200) + mocker.get(config_urls, status_code=200, json={}) + 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) self.study.update_settings(settings) def test_update_study_settings_fails(self): with requests_mock.Mocker() as mocker: - settings = StudySettings() - settings.general_parameters = GeneralParameters(mode="Adequacy") + settings = StudySettingsUpdate() + settings.general_parameters = GeneralParametersUpdate(mode=Mode.ADEQUACY) config_urls = re.compile(f"https://antares.com/api/v1/studies/{self.study_id}/config/.*") antares_web_description_msg = "Server KO" mocker.put(config_urls, json={"description": antares_web_description_msg}, status_code=404) diff --git a/tests/antares/services/local_services/conftest.py b/tests/antares/services/local_services/conftest.py index bf20aec6..c1e3065a 100644 --- a/tests/antares/services/local_services/conftest.py +++ b/tests/antares/services/local_services/conftest.py @@ -38,7 +38,7 @@ def local_study(tmp_path) -> Study: study_name = "studyTest" study_version = "880" - return create_study_local(study_name, study_version, str(tmp_path.absolute())) + return create_study_local(study_name, study_version, tmp_path.absolute()) @pytest.fixture diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index 9f1c95b2..07e1d4f3 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -51,18 +51,36 @@ ) 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, + WeekDay, ) from antares.craft.model.settings.optimization import ( + ExportMPS, OptimizationParameters, + OptimizationTransmissionCapacities, + SimplexOptimizationRange, + UnfeasibleProblemBehavior, ) from antares.craft.model.settings.study_settings import StudySettings +from antares.craft.model.settings.thematic_trimming import ThematicTrimmingParameters from antares.craft.model.study import create_study_local from antares.craft.tools.ini_tool import InitializationFilesTypes @@ -79,7 +97,7 @@ def test_create_study_success(self, tmp_path, caplog): expected_study_path = tmp_path / "studyTest" # When - create_study_local(study_name, version, str(tmp_path.absolute())) + create_study_local(study_name, version, tmp_path.absolute()) # Then assert os.path.exists(expected_study_path) @@ -127,7 +145,7 @@ def test_study_antares_content(self, monkeypatch, tmp_path): monkeypatch.setattr(time, "time", lambda: "123") # When - create_study_local(study_name, version, str(tmp_path.absolute())) + create_study_local(study_name, version, tmp_path.absolute()) with open(expected_study_antares_path, "r") as file: actual_content = file.read() @@ -142,7 +160,7 @@ def test_verify_study_already_exists_error(self, tmp_path): # When with pytest.raises(FileExistsError, match=f"Study {study_name} already exists"): - create_study_local(study_name, version, str(tmp_path.absolute())) + create_study_local(study_name, version, tmp_path.absolute()) def test_all_correlation_ini_files_exists(self, local_study): expected_ini_content = """[general] @@ -197,17 +215,17 @@ def test_local_study_has_settings(self, local_study): def test_local_study_has_correct_default_general_properties(self, local_study): expected_general_properties = GeneralParameters( **{ - "mode": "Economy", + "mode": Mode.ECONOMY, "horizon": "", "nb_years": 1, "simulation_start": 1, "simulation_end": 365, - "january_first": "Monday", - "first_month_in_year": "January", - "first_week_day": "Monday", + "january_first": WeekDay.MONDAY, + "first_month_in_year": Month.JANUARY, + "first_week_day": WeekDay.MONDAY, "leap_year": False, "year_by_year": False, - "building_mode": "automatic", + "building_mode": BuildingMode.AUTOMATIC, "thematic_trimming": False, "geographic_trimming": False, "simulation_synthesis": True, @@ -225,7 +243,7 @@ def test_local_study_has_correct_default_adequacy_patch_properties(self, local_s "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", + "price_taking_order": PriceTakingOrder.DENS, "include_hurdle_cost_csr": False, "check_csr_cost_function": False, "threshold_initiate_curtailment_sharing_rule": 0, @@ -239,15 +257,15 @@ def test_local_study_has_correct_default_adequacy_patch_properties(self, local_s 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", + "accuracy_on_correlation": set(), + "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.CLUSTERS, } ) @@ -271,8 +289,8 @@ def test_local_study_has_correct_seed_parameters(self, local_study): def test_local_study_has_correct_optimization_parameters(self, local_study): expected_optimization_parameters = OptimizationParameters( **{ - "simplex_range": "week", - "transmission_capacities": "local-values", + "simplex_range": SimplexOptimizationRange.WEEK, + "transmission_capacities": OptimizationTransmissionCapacities.LOCAL_VALUES, "include_constraints": True, "include_hurdlecosts": True, "include_tc_minstablepower": True, @@ -281,17 +299,17 @@ def test_local_study_has_correct_optimization_parameters(self, local_study): "include_strategicreserve": True, "include_spinningreserve": True, "include_primaryreserve": True, - "include_exportmps": False, + "include_exportmps": ExportMPS.FALSE, "include_exportstructure": False, - "include_unfeasible_problem_behavior": "error-verbose", + "include_unfeasible_problem_behavior": UnfeasibleProblemBehavior.ERROR_VERBOSE, } ) assert local_study.get_settings().optimization_parameters == expected_optimization_parameters 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 + assert local_study.get_settings().playlist_parameters == {} + assert local_study.get_settings().thematic_trimming_parameters == ThematicTrimmingParameters() def test_generaldata_ini_exists(self, local_study): # Given diff --git a/tests/integration/test_web_client.py b/tests/integration/test_web_client.py index 1230ff89..6de44280 100644 --- a/tests/integration/test_web_client.py +++ b/tests/integration/test_web_client.py @@ -25,6 +25,7 @@ ConstraintMatrixUpdateError, MatrixUploadError, STStorageMatrixUploadError, + StudySettingsUpdateError, ) from antares.craft.model.area import AdequacyPatchMode, AreaProperties, AreaUi, FilterOption from antares.craft.model.binding_constraint import ( @@ -37,9 +38,14 @@ ) from antares.craft.model.link import LinkProperties, LinkStyle, LinkUi from antares.craft.model.renewable import RenewableClusterGroup, RenewableClusterProperties, TimeSeriesInterpretation -from antares.craft.model.settings.advanced_parameters import AdvancedParameters, UnitCommitmentMode -from antares.craft.model.settings.general import GeneralParameters, Mode -from antares.craft.model.settings.study_settings import PlaylistParameters, StudySettings +from antares.craft.model.settings.advanced_parameters import ( + AdvancedParametersUpdate, + RenewableGenerationModeling, + UnitCommitmentMode, +) +from antares.craft.model.settings.general import GeneralParametersUpdate, Mode +from antares.craft.model.settings.optimization import ExportMPS, OptimizationParametersUpdate +from antares.craft.model.settings.study_settings import PlaylistParameters, StudySettings, StudySettingsUpdate from antares.craft.model.simulation import AntaresSimulationParameters, Job, JobStatus from antares.craft.model.st_storage import STStorageGroup, STStorageMatrixName, STStorageProperties from antares.craft.model.study import create_study_api, create_variant_api, import_study_api, read_study_api @@ -489,35 +495,40 @@ def test_creation_lifecycle(self, antares_web: AntaresWebDesktop, tmp_path): study.delete_area(area_de) assert area_de.id not in study.get_areas() - # test study creation with settings - settings = StudySettings() - settings.general_parameters = GeneralParameters(mode=Mode.ADEQUACY) - settings.general_parameters.year_by_year = False - 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 == {1: PlaylistParameters(status=False, weight=1)} + # test default settings at the study creation + new_study = create_study_api("second_study", "880", api_config) + actual_settings = new_study.get_settings() + default_settings = StudySettings() + assert actual_settings.general_parameters == default_settings.general_parameters + assert actual_settings.advanced_parameters == default_settings.advanced_parameters + assert actual_settings.adequacy_patch_parameters == default_settings.adequacy_patch_parameters + assert actual_settings.seed_parameters == default_settings.seed_parameters + assert actual_settings.playlist_parameters == {1: PlaylistParameters(status=False, weight=1)} # tests update settings - new_settings = StudySettings() - new_settings.general_parameters = GeneralParameters(nb_years=4) - new_settings.advanced_parameters = AdvancedParameters() - new_settings.advanced_parameters.unit_commitment_mode = UnitCommitmentMode.MILP + study_settings = StudySettingsUpdate() + study_settings.general_parameters = GeneralParametersUpdate(mode=Mode.ADEQUACY, year_by_year=True) + study_settings.playlist_parameters = {1: PlaylistParameters(status=True, weight=0.6)} + study_settings.optimization_parameters = OptimizationParametersUpdate(include_exportmps=ExportMPS.OPTIM1) + new_study.update_settings(study_settings) + updated_settings = new_study.get_settings() + assert updated_settings.general_parameters.mode == Mode.ADEQUACY + assert updated_settings.general_parameters.year_by_year + assert updated_settings.optimization_parameters.include_exportmps == ExportMPS.OPTIM1 + assert updated_settings.playlist_parameters == {1: PlaylistParameters(status=True, weight=0.6)} + + new_settings = StudySettingsUpdate() + new_settings.general_parameters = GeneralParametersUpdate(simulation_synthesis=False) + new_settings.advanced_parameters = AdvancedParametersUpdate(unit_commitment_mode=UnitCommitmentMode.MILP) + new_settings.optimization_parameters = OptimizationParametersUpdate(include_exportmps=ExportMPS.FALSE) new_study.update_settings(new_settings) - assert new_study.get_settings().general_parameters.mode == Mode.ADEQUACY.value - assert new_study.get_settings().general_parameters.nb_years == 4 - assert new_study.get_settings().advanced_parameters.unit_commitment_mode == UnitCommitmentMode.MILP.value - - old_settings = new_study.get_settings() - empty_settings = StudySettings() - new_study.update_settings(empty_settings) - assert old_settings == new_study.get_settings() - - series = pd.DataFrame(data=np.ones((365, 1))) + assert new_study.get_settings().general_parameters.mode == Mode.ADEQUACY + assert new_study.get_settings().general_parameters.simulation_synthesis is False + assert new_study.get_settings().optimization_parameters.include_exportmps == ExportMPS.FALSE + assert new_study.get_settings().advanced_parameters.unit_commitment_mode == UnitCommitmentMode.MILP # test each hydro matrices returns the good values + series = pd.DataFrame(data=np.ones((365, 1))) actual_reservoir_matrix = area_fr.hydro.get_reservoir() actual_maxpower_matrix = area_fr.hydro.get_maxpower() actual_inflow_matrix = area_fr.hydro.get_inflow_pattern() @@ -658,10 +669,13 @@ def test_creation_lifecycle(self, antares_web: AntaresWebDesktop, tmp_path): assert moved_study.path == study.path assert moved_study.name == study.name - new_settings_aggregated = StudySettings() - new_settings_aggregated.advanced_parameters = AdvancedParameters() - new_settings_aggregated.advanced_parameters.renewable_generation_modelling = "aggregated" - study_aggregated = create_study_api("test_aggregated", "880", api_config, new_settings_aggregated) + new_settings_aggregated = StudySettingsUpdate( + advanced_parameters=AdvancedParametersUpdate( + renewable_generation_modelling=RenewableGenerationModeling.AGGREGATED + ) + ) + study_aggregated = create_study_api("test_aggregated", "880", api_config) + study_aggregated.update_settings(new_settings_aggregated) study_aggregated.create_area("area_without_renewables") # read_study_api does not raise an error read_study_api(api_config, study_aggregated.service.study_id) @@ -687,3 +701,12 @@ def test_creation_lifecycle(self, antares_web: AntaresWebDesktop, tmp_path): assert imported_study.path == path_test / f"{imported_study.service.study_id}" assert list(imported_study.get_areas()) == list(study.get_areas()) + + # Asserts updating include_exportstructure parameter raises a clear Exception + update_settings = StudySettingsUpdate() + update_settings.optimization_parameters = OptimizationParametersUpdate(include_exportstructure=True) + with pytest.raises( + StudySettingsUpdateError, + match=f"Could not update settings for study {imported_study.service.study_id}: AntaresWeb doesn't support editing the parameter include_exportstructure", + ): + imported_study.update_settings(update_settings)