From b9d71e0e45a6ab62f8007a3b08f8aac8abf9a939 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Fri, 5 Apr 2024 11:02:47 +0200 Subject: [PATCH] feat(thermal): add new matrices for v8.7 --- .../business/areas/thermal_management.py | 40 +++++++- .../thermal/series/area/thermal/thermal.py | 14 +++ .../storage/study_upgrader/upgrader_870.py | 13 ++- .../model/command/create_cluster.py | 7 ++ antarest/study/web/study_data_blueprint.py | 31 ++++++ .../study_data_blueprint/test_thermal.py | 89 +++++++++++++++++- .../business/test_study_version_upgrader.py | 3 + .../little_study_860.expected.zip | Bin 124012 -> 128048 bytes .../little_study_860.expected.zip | Bin 126978 -> 128576 bytes 9 files changed, 191 insertions(+), 6 deletions(-) diff --git a/antarest/study/business/areas/thermal_management.py b/antarest/study/business/areas/thermal_management.py index 9fea2c568d..8444065a6a 100644 --- a/antarest/study/business/areas/thermal_management.py +++ b/antarest/study/business/areas/thermal_management.py @@ -1,9 +1,15 @@ import json import typing as t +from pathlib import Path from pydantic import validator -from antarest.core.exceptions import DuplicateThermalCluster, ThermalClusterConfigNotFound, ThermalClusterNotFound +from antarest.core.exceptions import ( + DuplicateThermalCluster, + IncoherenceBetweenMatricesLength, + ThermalClusterConfigNotFound, + ThermalClusterNotFound, +) from antarest.study.business.utils import AllOptionalMetaclass, camel_case_model, execute_or_add_commands from antarest.study.model import Study from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id @@ -338,6 +344,11 @@ def duplicate_cluster( f"input/thermal/prepro/{area_id}/{lower_new_id}/modulation", f"input/thermal/prepro/{area_id}/{lower_new_id}/data", ] + if int(study.version) >= 870: + source_paths.append(f"input/thermal/series/{area_id}/{lower_source_id}/CO2Cost") + source_paths.append(f"input/thermal/series/{area_id}/{lower_source_id}/fuelCost") + new_paths.append(f"input/thermal/series/{area_id}/{lower_new_id}/CO2Cost") + new_paths.append(f"input/thermal/series/{area_id}/{lower_new_id}/fuelCost") # Prepare and execute commands commands: t.List[t.Union[CreateCluster, ReplaceMatrix]] = [create_cluster_cmd] @@ -351,3 +362,30 @@ def duplicate_cluster( execute_or_add_commands(study, self._get_file_study(study), commands, self.storage_service) return ThermalClusterOutput(**new_config.dict(by_alias=False)) + + def validate_series(self, study: Study, area_id: str, cluster_id: str) -> bool: + cluster_id_lowered = cluster_id.lower() + matrices_path = [f"input/thermal/series/{area_id}/{cluster_id_lowered}/series"] + if int(study.version) >= 870: + matrices_path.append(f"input/thermal/series/{area_id}/{cluster_id_lowered}/CO2Cost") + matrices_path.append(f"input/thermal/series/{area_id}/{cluster_id_lowered}/fuelCost") + + matrices_width = [] + for matrix_path in matrices_path: + matrix = self.storage_service.get_storage(study).get(study, matrix_path) + matrix_data = matrix["data"] + matrix_length = len(matrix_data) + if matrix_length > 0 and matrix_length != 8760: + raise IncoherenceBetweenMatricesLength( + f"The matrix {Path(matrix_path).name} should have 8760 rows, currently: {matrix_length}" + ) + matrices_width.append(len(matrix_data[0])) + comparison_set = set(matrices_width) + comparison_set.discard(0) + comparison_set.discard(1) + if len(comparison_set) > 1: + raise IncoherenceBetweenMatricesLength( + f"Matrix columns mismatch in thermal cluster '{cluster_id}' series. Columns size are {matrices_width}" + ) + + return True diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/series/area/thermal/thermal.py b/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/series/area/thermal/thermal.py index a93c4378cb..c11083a882 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/series/area/thermal/thermal.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/series/area/thermal/thermal.py @@ -2,6 +2,7 @@ from antarest.study.storage.rawstudy.model.filesystem.inode import TREE from antarest.study.storage.rawstudy.model.filesystem.matrix.constants import default_scenario_hourly from antarest.study.storage.rawstudy.model.filesystem.matrix.input_series_matrix import InputSeriesMatrix +from antarest.study.storage.rawstudy.model.filesystem.matrix.matrix import MatrixFrequency class InputThermalSeriesAreaThermal(FolderNode): @@ -13,4 +14,17 @@ def build(self) -> TREE: default_empty=default_scenario_hourly, ), } + if self.config.version >= 870: + children["CO2Cost"] = InputSeriesMatrix( + self.context, + self.config.next_file("CO2Cost.txt"), + freq=MatrixFrequency.HOURLY, + default_empty=default_scenario_hourly, + ) + children["fuelCost"] = InputSeriesMatrix( + self.context, + self.config.next_file("fuelCost.txt"), + freq=MatrixFrequency.HOURLY, + default_empty=default_scenario_hourly, + ) return children diff --git a/antarest/study/storage/study_upgrader/upgrader_870.py b/antarest/study/storage/study_upgrader/upgrader_870.py index a2afc4bd1f..0635215896 100644 --- a/antarest/study/storage/study_upgrader/upgrader_870.py +++ b/antarest/study/storage/study_upgrader/upgrader_870.py @@ -50,10 +50,15 @@ def upgrade_870(study_path: Path) -> None: # Add properties for thermal clusters in .ini file ini_files = study_path.glob("input/thermal/clusters/*/list.ini") + thermal_path = study_path / Path("input/thermal/series") for ini_file_path in ini_files: data = IniReader().read(ini_file_path) - for section in data: - data[section]["costgeneration"] = "SetManually" - data[section]["efficiency"] = 100 - data[section]["variableomcost"] = 0 + area_id = ini_file_path.parent.name + for cluster in data.keys(): + new_thermal_path = thermal_path / area_id / cluster.lower() + (new_thermal_path / "CO2Cost.txt").touch() + (new_thermal_path / "fuelCost.txt").touch() + data[cluster]["costgeneration"] = "SetManually" + data[cluster]["efficiency"] = 100 + data[cluster]["variableomcost"] = 0 IniWriter().write(data, ini_file_path) diff --git a/antarest/study/storage/variantstudy/model/command/create_cluster.py b/antarest/study/storage/variantstudy/model/command/create_cluster.py index f9edfba949..80b7fbe580 100644 --- a/antarest/study/storage/variantstudy/model/command/create_cluster.py +++ b/antarest/study/storage/variantstudy/model/command/create_cluster.py @@ -135,6 +135,13 @@ def _apply(self, study_data: FileStudy) -> CommandOutput: } } } + if study_data.config.version >= 870: + new_cluster_data["input"]["thermal"]["series"][self.area_id][series_id][ + "CO2Cost" + ] = self.command_context.generator_matrix_constants.get_null_matrix() + new_cluster_data["input"]["thermal"]["series"][self.area_id][series_id][ + "fuelCost" + ] = self.command_context.generator_matrix_constants.get_null_matrix() study_data.tree.save(new_cluster_data) return output diff --git a/antarest/study/web/study_data_blueprint.py b/antarest/study/web/study_data_blueprint.py index 6d7dff831e..53935986b0 100644 --- a/antarest/study/web/study_data_blueprint.py +++ b/antarest/study/web/study_data_blueprint.py @@ -1894,6 +1894,37 @@ def redirect_update_thermal_cluster( # We cannot perform redirection, because we have a PUT, where a PATCH is required. return update_thermal_cluster(uuid, area_id, cluster_id, cluster_data, current_user=current_user) + @bp.get( + path="/studies/{uuid}/areas/{area_id}/clusters/thermal/{cluster_id}/validate", + tags=[APITag.study_data], + summary="Validates the thermal cluster series", + response_model=None, + ) + def validate_cluster_series( + uuid: str, + area_id: str, + cluster_id: str, + current_user: JWTUser = Depends(auth.get_current_user), + ) -> bool: + """ + Validate the consistency of all time series for the given thermal cluster. + + Args: + - `uuid`: The UUID of the study. + - `area_id`: the area ID. + - `cluster_id`: the ID of the thermal cluster. + + Permissions: + - User must have READ permission on the study. + """ + logger.info( + f"Validating thermal series values for study {uuid} and cluster {cluster_id}", + extra={"user": current_user.id}, + ) + params = RequestParameters(user=current_user) + study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) + return study_service.thermal_manager.validate_series(study, area_id, cluster_id) + @bp.delete( path="/studies/{uuid}/areas/{area_id}/clusters/thermal", tags=[APITag.study_data], diff --git a/tests/integration/study_data_blueprint/test_thermal.py b/tests/integration/study_data_blueprint/test_thermal.py index 3297a1fba8..88aa5b7e5f 100644 --- a/tests/integration/study_data_blueprint/test_thermal.py +++ b/tests/integration/study_data_blueprint/test_thermal.py @@ -27,12 +27,13 @@ * delete a cluster (or several clusters) * validate the consistency of the matrices (and properties) """ - +import io import json import re import typing as t import numpy as np +import pandas as pd import pytest from starlette.testclient import TestClient @@ -265,6 +266,21 @@ ] +def _upload_matrix( + client: TestClient, user_access_token: str, study_id: str, matrix_path: str, df: pd.DataFrame +) -> None: + tsv = io.BytesIO() + df.to_csv(tsv, sep="\t", index=False, header=False) + tsv.seek(0) + res = client.put( + f"/v1/studies/{study_id}/raw", + params={"path": matrix_path}, + headers={"Authorization": f"Bearer {user_access_token}"}, + files={"file": tsv}, + ) + res.raise_for_status() + + @pytest.mark.unit_test class TestThermal: @pytest.mark.parametrize( @@ -527,6 +543,77 @@ def test_lifecycle( assert res.status_code == 200 assert res.json()["data"] == matrix + # ============================= + # THERMAL CLUSTER VALIDATION + # ============================= + + # Everything is fine at the beginning + res = client.get( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/thermal/{fr_gas_conventional_id}/validate", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200 + assert res.json() is True + + # Modifies series matrix with wrong length (!= 8760) + _upload_matrix( + client, + user_access_token, + study_id, + f"input/thermal/series/{area_id}/{fr_gas_conventional_id.lower()}/series", + pd.DataFrame(np.random.randint(0, 10, size=(4, 1))), + ) + + # Validation should fail + res = client.get( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/thermal/{fr_gas_conventional_id}/validate", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 422 + obj = res.json() + assert obj["exception"] == "IncoherenceBetweenMatricesLength" + assert obj["description"] == "The matrix series should have 8760 rows, currently: 4" + + # Update with the right length + _upload_matrix( + client, + user_access_token, + study_id, + f"input/thermal/series/{area_id}/{fr_gas_conventional_id.lower()}/series", + pd.DataFrame(np.random.randint(0, 10, size=(8760, 4))), + ) + + # Validation should succeed again + res = client.get( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/thermal/{fr_gas_conventional_id}/validate", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200 + assert res.json() is True + + if version >= 870: + # Adds a CO2Cost matrix with different columns size + _upload_matrix( + client, + user_access_token, + study_id, + f"input/thermal/series/{area_id}/{fr_gas_conventional_id.lower()}/CO2Cost", + pd.DataFrame(np.random.randint(0, 10, size=(8760, 3))), + ) + + # Validation should fail + res = client.get( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/thermal/{fr_gas_conventional_id}/validate", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 422 + obj = res.json() + assert obj["exception"] == "IncoherenceBetweenMatricesLength" + assert ( + obj["description"] + == "Matrix columns mismatch in thermal cluster 'FR_Gas conventional' series. Columns size are [4, 3, 1]" + ) + # ============================= # THERMAL CLUSTER DELETION # ============================= diff --git a/tests/storage/business/test_study_version_upgrader.py b/tests/storage/business/test_study_version_upgrader.py index ef3ddf97c4..f0c0cee009 100644 --- a/tests/storage/business/test_study_version_upgrader.py +++ b/tests/storage/business/test_study_version_upgrader.py @@ -211,8 +211,11 @@ def assert_inputs_are_updated(tmp_path: Path, old_area_values: dict, old_binding # thermal cluster part for area in list_areas: reader = IniReader(DUPLICATE_KEYS) + thermal_series_path = tmp_path / "input" / "thermal" / "series" / area thermal_cluster_list = reader.read(tmp_path / "input" / "thermal" / "clusters" / area / "list.ini") for cluster in thermal_cluster_list: + assert (thermal_series_path / cluster.lower() / "fuelCost.txt").exists() + assert (thermal_series_path / cluster.lower() / "CO2Cost.txt").exists() assert thermal_cluster_list[cluster]["costgeneration"] == "SetManually" assert thermal_cluster_list[cluster]["efficiency"] == 100 assert thermal_cluster_list[cluster]["variableomcost"] == 0 diff --git a/tests/storage/study_upgrader/upgrade_870/empty_binding_constraints/little_study_860.expected.zip b/tests/storage/study_upgrader/upgrade_870/empty_binding_constraints/little_study_860.expected.zip index 508430ffc5b01313f9157d424d75df6dae64ad2d..11b6e564d44485ae041f46ff53cdb41fd95a1164 100644 GIT binary patch delta 18004 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 HuTK9DcfReq diff --git a/tests/storage/study_upgrader/upgrade_870/nominal_case/little_study_860.expected.zip b/tests/storage/study_upgrader/upgrade_870/nominal_case/little_study_860.expected.zip index e8e657ae9819a8c23051efbef4c69b82d5304191..bb83d7774579e14cb7323c200b3408052271027f 100644 GIT binary patch delta 10776 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 K^kPIu-2V><=?sbh delta 10094 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