Skip to content

Commit

Permalink
feat(ts-gen)!: replace legacy endpoints with new ones (#2303)
Browse files Browse the repository at this point in the history
Co-authored-by: Samir Kamal <[email protected]>
  • Loading branch information
MartinBelthle and skamril authored Feb 4, 2025
1 parent b85fe72 commit 5e18c3e
Show file tree
Hide file tree
Showing 24 changed files with 284 additions and 657 deletions.
252 changes: 24 additions & 228 deletions antarest/study/business/timeseries_config_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,255 +10,51 @@
#
# This file is part of the Antares project.

import typing as t

from pydantic import StrictBool, StrictInt, field_validator, model_validator

from antarest.core.model import JSON
from antarest.core.serialization import AntaresBaseModel
from antarest.study.business.all_optional_meta import all_optional_model
from antarest.study.business.enum_ignore_case import EnumIgnoreCase
from antarest.study.business.utils import GENERAL_DATA_PATH, FormFieldsBaseModel, execute_or_add_commands
from antarest.study.model import STUDY_VERSION_8_1, STUDY_VERSION_8_2, Study
from antarest.study.storage.rawstudy.model.filesystem.config.model import EnrModelling
from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy
from antarest.study.business.utils import GENERAL_DATA_PATH, execute_or_add_commands
from antarest.study.model import Study
from antarest.study.storage.storage_service import StudyStorageService
from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig


class TSType(EnumIgnoreCase):
LOAD = "load"
HYDRO = "hydro"
THERMAL = "thermal"
WIND = "wind"
SOLAR = "solar"
RENEWABLES = "renewables"
NTC = "ntc"


class SeasonCorrelation(EnumIgnoreCase):
MONTHLY = "monthly"
ANNUAL = "annual"


@all_optional_model
class TSFormFieldsForType(FormFieldsBaseModel):
stochastic_ts_status: StrictBool
number: StrictInt
refresh: StrictBool
refresh_interval: StrictInt
season_correlation: SeasonCorrelation
store_in_input: StrictBool
store_in_output: StrictBool
intra_modal: StrictBool
inter_modal: StrictBool
class TimeSeriesTypeConfig(AntaresBaseModel, extra="forbid", validate_assignment=True, populate_by_name=True):
number: int


@all_optional_model
class TSFormFields(FormFieldsBaseModel):
load: TSFormFieldsForType
hydro: TSFormFieldsForType
thermal: TSFormFieldsForType
wind: TSFormFieldsForType
solar: TSFormFieldsForType
renewables: TSFormFieldsForType
ntc: TSFormFieldsForType

@model_validator(mode="before")
def check_type_validity(
cls, values: t.Dict[str, t.Optional[TSFormFieldsForType]]
) -> t.Dict[str, t.Optional[TSFormFieldsForType]]:
def has_type(ts_type: TSType) -> bool:
return values.get(ts_type.value, None) is not None

if has_type(TSType.RENEWABLES) and (has_type(TSType.WIND) or has_type(TSType.SOLAR)):
raise ValueError(
f"'{TSType.RENEWABLES}' type cannot be defined with '{TSType.WIND}' and '{TSType.SOLAR}' types"
)
return values

@field_validator("thermal")
def thermal_validation(cls, v: TSFormFieldsForType) -> TSFormFieldsForType:
if v.season_correlation is not None:
raise ValueError("season_correlation is not allowed for 'thermal' type")
return v


PATH_BY_TS_STR_FIELD = {
"stochastic_ts_status": f"{GENERAL_DATA_PATH}/general/generate",
"refresh": f"{GENERAL_DATA_PATH}/general/refreshtimeseries",
"intra_modal": f"{GENERAL_DATA_PATH}/general/intra-modal",
"inter_modal": f"{GENERAL_DATA_PATH}/general/inter-modal",
"store_in_input": f"{GENERAL_DATA_PATH}/input/import",
"store_in_output": f"{GENERAL_DATA_PATH}/output/archives",
}
class TimeSeriesConfigDTO(AntaresBaseModel, extra="forbid", validate_assignment=True, populate_by_name=True):
thermal: TimeSeriesTypeConfig


class TimeSeriesConfigManager:
def __init__(self, storage_service: StudyStorageService) -> None:
self.storage_service = storage_service

def get_field_values(self, study: Study) -> TSFormFields:
def get_values(self, study: Study) -> TimeSeriesConfigDTO:
"""
Get Time Series field values for the webapp form
Get Time-Series generation values
"""
file_study = self.storage_service.get_storage(study).get_raw(study)
general_data = file_study.tree.get(GENERAL_DATA_PATH.split("/"))

fields = {
ts_type.value: self.__get_form_fields_for_type(
file_study,
ts_type,
general_data,
)
for ts_type in TSType
}
url = GENERAL_DATA_PATH.split("/")
url.extend(["general", "nbtimeseriesthermal"])
nb_ts_gen_thermal = file_study.tree.get(url)

return TSFormFields.construct(**fields) # type: ignore
args = {"thermal": TimeSeriesTypeConfig(number=nb_ts_gen_thermal)}
return TimeSeriesConfigDTO.model_validate(args)

def set_field_values(self, study: Study, field_values: TSFormFields) -> None:
def set_values(self, study: Study, field_values: TimeSeriesConfigDTO) -> None:
"""
Set Time Series config from the webapp form
Set Time-Series generation values
"""
file_study = self.storage_service.get_storage(study).get_raw(study)

for ts_type, values in field_values:
if values is not None:
self.__set_field_values_for_type(study, file_study, TSType(ts_type), values)

def __set_field_values_for_type(
self,
study: Study,
file_study: FileStudy,
ts_type: TSType,
field_values: TSFormFieldsForType,
) -> None:
commands: t.List[UpdateConfig] = []
values = field_values.model_dump(mode="json")

for field, path in PATH_BY_TS_STR_FIELD.items():
field_val = values[field]
if field_val is not None:
commands.append(self.__set_ts_types_str(file_study, path, {ts_type: field_val}))

if field_values.number is not None:
commands.append(
UpdateConfig(
target=f"{GENERAL_DATA_PATH}/general/nbtimeseries{ts_type}",
data=field_values.number,
command_context=self.storage_service.variant_study_service.command_factory.command_context,
study_version=file_study.config.version,
)
)

if field_values.refresh_interval is not None:
commands.append(
UpdateConfig(
target=f"{GENERAL_DATA_PATH}/general/refreshinterval{ts_type}",
data=field_values.refresh_interval,
command_context=self.storage_service.variant_study_service.command_factory.command_context,
study_version=file_study.config.version,
)
)

if field_values.season_correlation is not None:
commands.append(
UpdateConfig(
target=f"input/{ts_type}/prepro/correlation/general/mode",
data=field_values.season_correlation.value,
command_context=self.storage_service.variant_study_service.command_factory.command_context,
study_version=file_study.config.version,
)
)

if len(commands) > 0:
execute_or_add_commands(study, file_study, commands, self.storage_service)

def __set_ts_types_str(self, file_study: FileStudy, path: str, values: t.Dict[TSType, bool]) -> UpdateConfig:
"""
Set string value with the format: "[ts_type_1], [ts_type_2]"
"""
path_arr = path.split("/")

try:
parent_target = file_study.tree.get(path_arr[:-1])
except:
parent_target = {}

target_value = parent_target.get(path_arr[-1], "")
current_values = [v.strip() for v in target_value.split(",")]
new_types = {
**{ts_type: True for ts_type in TSType if ts_type in current_values},
**values,
}

return UpdateConfig(
target=path,
data=", ".join([ts_type for ts_type in new_types if new_types[ts_type]]),
command_context=self.storage_service.variant_study_service.command_factory.command_context,
study_version=file_study.config.version,
)

@staticmethod
def __has_ts_type_in_str(value: str, ts_type: TSType) -> bool:
return ts_type in [v.strip() for v in value.split(",")]

@staticmethod
def __get_form_fields_for_type(
file_study: FileStudy,
ts_type: TSType,
general_data: JSON,
) -> t.Optional[TSFormFieldsForType]:
general = general_data.get("general", {})
input_ = general_data.get("input", {})
output = general_data.get("output", {})

config = file_study.config
study_version = config.version
has_renewables = (
study_version >= STUDY_VERSION_8_1 and EnrModelling(config.enr_modelling) == EnrModelling.CLUSTERS
)

if ts_type == TSType.RENEWABLES and not has_renewables:
return None

if ts_type in [TSType.WIND, TSType.SOLAR] and has_renewables:
return None

if ts_type == TSType.NTC and study_version < STUDY_VERSION_8_2:
return None

is_special_type = ts_type == TSType.RENEWABLES or ts_type == TSType.NTC
stochastic_ts_status = TimeSeriesConfigManager.__has_ts_type_in_str(general.get("generate", ""), ts_type)
intra_modal = TimeSeriesConfigManager.__has_ts_type_in_str(general.get("intra-modal", ""), ts_type)
inter_modal = TimeSeriesConfigManager.__has_ts_type_in_str(general.get("inter-modal", ""), ts_type)

if is_special_type:
return TSFormFieldsForType.construct(
stochastic_ts_status=stochastic_ts_status,
intra_modal=intra_modal,
inter_modal=inter_modal if ts_type == TSType.RENEWABLES else None,
)

return TSFormFieldsForType.construct(
stochastic_ts_status=stochastic_ts_status,
number=general.get(f"nbtimeseries{ts_type}", 1),
refresh=TimeSeriesConfigManager.__has_ts_type_in_str(general.get("refreshtimeseries", ""), ts_type),
refresh_interval=general.get(f"refreshinterval{ts_type}", 100),
season_correlation=None
if ts_type == TSType.THERMAL
else file_study.tree.get(
[
"input",
ts_type.value,
"prepro",
"correlation",
"general",
"mode",
]
if field_values.thermal:
url = f"{GENERAL_DATA_PATH}/general/nbtimeseriesthermal"
command = UpdateConfig(
target=url,
data=field_values.thermal.number,
command_context=self.storage_service.variant_study_service.command_factory.command_context,
study_version=file_study.config.version,
)
or SeasonCorrelation.ANNUAL,
store_in_input=TimeSeriesConfigManager.__has_ts_type_in_str(input_.get("import", ""), ts_type),
store_in_output=TimeSeriesConfigManager.__has_ts_type_in_str(output.get("archives", ""), ts_type),
intra_modal=intra_modal,
inter_modal=inter_modal,
)
execute_or_add_commands(study, file_study, [command], self.storage_service)
34 changes: 14 additions & 20 deletions antarest/study/web/study_data_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
from antarest.study.business.scenario_builder_management import Rulesets, ScenarioType
from antarest.study.business.table_mode_management import TableDataDTO, TableModeType
from antarest.study.business.thematic_trimming_field_infos import ThematicTrimmingFormFields
from antarest.study.business.timeseries_config_management import TSFormFields
from antarest.study.business.timeseries_config_management import TimeSeriesConfigDTO
from antarest.study.model import PatchArea, PatchCluster
from antarest.study.service import StudyService
from antarest.study.storage.rawstudy.model.filesystem.config.binding_constraint import (
Expand Down Expand Up @@ -965,43 +965,40 @@ def set_adequacy_patch_form_values(
study_service.adequacy_patch_manager.set_field_values(study, field_values)

@bp.get(
path="/studies/{uuid}/config/timeseries/form",
path="/studies/{uuid}/timeseries/config",
tags=[APITag.study_data],
summary="Get Time Series config values for form",
response_model=TSFormFields,
summary="Gets the TS Generation config",
response_model=TimeSeriesConfigDTO,
response_model_exclude_none=True,
)
def get_timeseries_form_values(
uuid: str,
current_user: JWTUser = Depends(auth.get_current_user),
) -> TSFormFields:
) -> TimeSeriesConfigDTO:
logger.info(
msg=f"Getting Time Series config for study {uuid}",
msg=f"Getting Time-Series generation config for study {uuid}",
extra={"user": current_user.id},
)
params = RequestParameters(user=current_user)
study = study_service.check_study_access(uuid, StudyPermissionType.READ, params)

return study_service.ts_config_manager.get_field_values(study)
return study_service.ts_config_manager.get_values(study)

@bp.put(
path="/studies/{uuid}/config/timeseries/form",
path="/studies/{uuid}/timeseries/config",
tags=[APITag.study_data],
summary="Set Time Series config with values from form",
summary="Sets the TS Generation config",
)
def set_timeseries_form_values(
uuid: str,
field_values: TSFormFields,
current_user: JWTUser = Depends(auth.get_current_user),
def set_ts_generation_config(
uuid: str, field_values: TimeSeriesConfigDTO, current_user: JWTUser = Depends(auth.get_current_user)
) -> None:
logger.info(
f"Updating Time Series config for study {uuid}",
f"Updating Time-Series generation config for study {uuid}",
extra={"user": current_user.id},
)
params = RequestParameters(user=current_user)
study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params)

study_service.ts_config_manager.set_field_values(study, field_values)
study_service.ts_config_manager.set_values(study, field_values)

@bp.get(
path="/table-schema/{table_type}",
Expand Down Expand Up @@ -1871,10 +1868,7 @@ def get_properties_form_values(
params = RequestParameters(user=current_user)
study = study_service.check_study_access(uuid, StudyPermissionType.READ, params)

return study_service.properties_manager.get_field_values(
study,
area_id,
)
return study_service.properties_manager.get_field_values(study, area_id)

@bp.put(
path="/studies/{uuid}/areas/{area_id}/properties/form",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ def test_lifecycle_nominal(self, client: TestClient, user_access_token: str) ->
area2_id = preparer.create_area(study_id, name="Area 2")["id"]
# Set nb timeseries thermal to 3.
nb_years = 3
body = {"thermal": {"number": nb_years}}
res = client.put(f"/v1/studies/{study_id}/config/timeseries/form", json=body)
res = client.put(f"/v1/studies/{study_id}/timeseries/config", json={"thermal": {"number": nb_years}})
assert res.status_code in {200, 201}

# Create 1 cluster in area1
Expand Down Expand Up @@ -158,9 +157,7 @@ def test_advanced_results(self, client: TestClient, user_access_token: str) -> N
preparer = PreparerProxy(client, user_access_token)
study_id = preparer.create_study("foo", version=860)
area_id = preparer.create_area(study_id, name="test")["id"]
nb_years = 10
body = {"thermal": {"number": nb_years}}
res = client.put(f"/v1/studies/{study_id}/config/timeseries/form", json=body)
res = client.put(f"/v1/studies/{study_id}/timeseries/config", json={"thermal": {"number": 10}})
cluster_id = "cluster_test"
assert res.status_code in {200, 201}

Expand Down
Loading

0 comments on commit 5e18c3e

Please sign in to comment.