diff --git a/antarest/study/business/areas/thermal_management.py b/antarest/study/business/areas/thermal_management.py index 00b193bd09..f44ad7ba10 100644 --- a/antarest/study/business/areas/thermal_management.py +++ b/antarest/study/business/areas/thermal_management.py @@ -42,7 +42,7 @@ class Config: def schema_extra(schema: t.MutableMapping[str, t.Any]) -> None: schema["example"] = ThermalClusterInput( group="Gas", - name="2 avail and must 1", + name="Gas Cluster XY", enabled=False, unitCount=100, nominalCapacity=1000.0, @@ -81,9 +81,9 @@ class Config: @staticmethod def schema_extra(schema: t.MutableMapping[str, t.Any]) -> None: schema["example"] = ThermalClusterOutput( - id="2 avail and must 1", + id="Gas cluster YZ", group="Gas", - name="2 avail and must 1", + name="Gas Cluster YZ", enabled=False, unitCount=100, nominalCapacity=1000.0, @@ -192,16 +192,8 @@ def create_cluster(self, study: Study, area_id: str, cluster_data: ThermalCluste """ file_study = self._get_file_study(study) - study_version = study.version - cluster = cluster_data.to_config(study_version) - # NOTE: currently, in the `CreateCluster` class, there is a confusion - # between the cluster name and the cluster ID (which is a section name). - command = CreateCluster( - area_id=area_id, - cluster_name=cluster.id, - parameters=cluster.dict(by_alias=True, exclude={"id"}), - command_context=self.storage_service.variant_study_service.command_factory.command_context, - ) + cluster = cluster_data.to_config(study.version) + command = self._make_create_cluster_cmd(area_id, cluster) execute_or_add_commands( study, file_study, @@ -211,6 +203,17 @@ def create_cluster(self, study: Study, area_id: str, cluster_data: ThermalCluste output = self.get_cluster(study, area_id, cluster.id) return output + def _make_create_cluster_cmd(self, area_id: str, cluster: ThermalConfigType) -> CreateCluster: + # NOTE: currently, in the `CreateCluster` class, there is a confusion + # between the cluster name and the cluster ID (which is a section name). + command = CreateCluster( + area_id=area_id, + cluster_name=cluster.id, + parameters=cluster.dict(by_alias=True, exclude={"id"}), + command_context=self.storage_service.variant_study_service.command_factory.command_context, + ) + return command + def update_cluster( self, study: Study, @@ -289,7 +292,13 @@ def delete_clusters(self, study: Study, area_id: str, cluster_ids: t.Sequence[st execute_or_add_commands(study, file_study, commands, self.storage_service) - def duplicate_cluster(self, study: Study, area_id: str, source_id: str, new_name: str) -> ThermalClusterOutput: + def duplicate_cluster( + self, + study: Study, + area_id: str, + source_id: str, + new_cluster_name: str, + ) -> ThermalClusterOutput: """ Creates a duplicate cluster within the study area with a new name. @@ -297,7 +306,7 @@ def duplicate_cluster(self, study: Study, area_id: str, source_id: str, new_name study: The study in which the cluster will be duplicated. area_id: The identifier of the area where the cluster will be duplicated. source_id: The identifier of the cluster to be duplicated. - new_name: The new name for the duplicated cluster. + new_cluster_name: The new name for the duplicated cluster. Returns: The duplicated cluster configuration. @@ -305,20 +314,21 @@ def duplicate_cluster(self, study: Study, area_id: str, source_id: str, new_name Raises: ClusterAlreadyExists: If a cluster with the new name already exists in the area. """ - new_id = transform_name_to_id(new_name, lower=False) - if any(new_id.lower() == cluster.id.lower() for cluster in self.get_clusters(study, area_id)): + new_id = transform_name_to_id(new_cluster_name, lower=False) + lower_new_id = new_id.lower() + if any(lower_new_id == cluster.id.lower() for cluster in self.get_clusters(study, area_id)): raise ClusterAlreadyExists("Thermal", new_id) # Cluster duplication - current_cluster = self.get_cluster(study, area_id, source_id) - current_cluster.name = new_name - creation_form = ThermalClusterCreation(**current_cluster.dict(by_alias=False, exclude={"id"})) - new_cluster = self.create_cluster(study, area_id, creation_form) + source_cluster = self.get_cluster(study, area_id, source_id) + source_cluster.name = new_cluster_name + creation_form = ThermalClusterCreation(**source_cluster.dict(by_alias=False, exclude={"id"})) + new_config = creation_form.to_config(study.version) + create_cluster_cmd = self._make_create_cluster_cmd(area_id, new_config) # Matrix edition lower_source_id = source_id.lower() - lower_new_id = new_id.lower() - paths = [ + source_paths = [ f"input/thermal/series/{area_id}/{lower_source_id}/series", f"input/thermal/prepro/{area_id}/{lower_source_id}/modulation", f"input/thermal/prepro/{area_id}/{lower_source_id}/data", @@ -328,15 +338,16 @@ def duplicate_cluster(self, study: Study, area_id: str, source_id: str, new_name f"input/thermal/prepro/{area_id}/{lower_new_id}/modulation", f"input/thermal/prepro/{area_id}/{lower_new_id}/data", ] - commands = [] - storage_service = self.storage_service - for k, matrix_path in enumerate(paths): - current_matrix = storage_service.raw_study_service.get(study, matrix_path) - command = ReplaceMatrix( - target=new_paths[k], - matrix=current_matrix["data"], - command_context=storage_service.variant_study_service.command_factory.command_context, - ) + + # Prepare and execute commands + commands: t.List[t.Union[CreateCluster, ReplaceMatrix]] = [create_cluster_cmd] + storage_service = self.storage_service.get_storage(study) + command_context = self.storage_service.variant_study_service.command_factory.command_context + for source_path, new_path in zip(source_paths, new_paths): + current_matrix = storage_service.get(study, source_path)["data"] + command = ReplaceMatrix(target=new_path, matrix=current_matrix, command_context=command_context) commands.append(command) - execute_or_add_commands(study, self._get_file_study(study), commands, storage_service) - return new_cluster + + execute_or_add_commands(study, self._get_file_study(study), commands, self.storage_service) + + return ThermalClusterOutput(**new_config.dict(by_alias=False)) diff --git a/tests/integration/study_data_blueprint/test_thermal.py b/tests/integration/study_data_blueprint/test_thermal.py index ab3e7745a8..9fc7388642 100644 --- a/tests/integration/study_data_blueprint/test_thermal.py +++ b/tests/integration/study_data_blueprint/test_thermal.py @@ -29,6 +29,7 @@ """ import json import re +import typing as t import numpy as np import pytest @@ -566,9 +567,9 @@ def test_lifecycle( assert res.status_code in {200, 201}, res.json() # asserts the config is the same duplicated_config = dict(fr_gas_conventional_cfg) - duplicated_config["name"] = new_name # type: ignore + duplicated_config["name"] = new_name duplicated_id = transform_name_to_id(new_name, lower=False) - duplicated_config["id"] = duplicated_id # type: ignore + duplicated_config["id"] = duplicated_id assert res.json() == duplicated_config # asserts the matrix has also been duplicated @@ -814,3 +815,148 @@ def test_lifecycle( description = obj["description"] assert new_name.upper() in description assert obj["exception"] == "ClusterAlreadyExists" + + @pytest.fixture(name="base_study_id") + def base_study_id_fixture(self, request: t.Any, client: TestClient, user_access_token: str) -> str: + """Prepare a managed study for the variant study tests.""" + params = request.param + res = client.post( + "/v1/studies", + headers={"Authorization": f"Bearer {user_access_token}"}, + params=params, + ) + assert res.status_code in {200, 201}, res.json() + study_id: str = res.json() + return study_id + + @pytest.fixture(name="variant_id") + def variant_id_fixture(self, request: t.Any, client: TestClient, user_access_token: str, base_study_id: str) -> str: + """Prepare a variant study for the variant study tests.""" + name = request.param + res = client.post( + f"/v1/studies/{base_study_id}/variants", + headers={"Authorization": f"Bearer {user_access_token}"}, + params={"name": name}, + ) + assert res.status_code in {200, 201}, res.json() + study_id: str = res.json() + return study_id + + # noinspection PyTestParametrized + @pytest.mark.parametrize("base_study_id", [{"name": "Base Study", "version": 860}], indirect=True) + @pytest.mark.parametrize("variant_id", ["Variant Study"], indirect=True) + def test_variant_lifecycle(self, client: TestClient, user_access_token: str, variant_id: str) -> None: + """ + In this test, we want to check that thermal clusters can be managed + in the context of a "variant" study. + """ + # Create an area + area_name = "France" + res = client.post( + f"/v1/studies/{variant_id}/areas", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={"name": area_name, "type": "AREA"}, + ) + assert res.status_code in {200, 201}, res.json() + area_cfg = res.json() + area_id = area_cfg["id"] + + # Create a thermal cluster + cluster_name = "Th1" + res = client.post( + f"/v1/studies/{variant_id}/areas/{area_id}/clusters/thermal", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={ + "name": cluster_name, + "group": "Nuclear", + "unitCount": 13, + "nominalCapacity": 42500, + "marginalCost": 0.1, + }, + ) + assert res.status_code in {200, 201}, res.json() + cluster_id: str = res.json()["id"] + + # Update the thermal cluster + res = client.patch( + f"/v1/studies/{variant_id}/areas/{area_id}/clusters/thermal/{cluster_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={ + "marginalCost": 0.2, + }, + ) + assert res.status_code == 200, res.json() + cluster_cfg = res.json() + assert cluster_cfg["marginalCost"] == 0.2 + + # Update the prepro matrix + matrix = np.random.randint(0, 2, size=(8760, 1)).tolist() + matrix_path = f"input/thermal/prepro/{area_id}/{cluster_id.lower()}/data" + args = {"target": matrix_path, "matrix": matrix} + res = client.post( + f"/v1/studies/{variant_id}/commands", + json=[{"action": "replace_matrix", "args": args}], + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code in {200, 201}, res.json() + + # Duplicate the thermal cluster + new_name = "Th2" + res = client.post( + f"/v1/studies/{variant_id}/areas/{area_id}/thermals/{cluster_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + params={"newName": new_name}, + ) + assert res.status_code in {200, 201}, res.json() + cluster_cfg = res.json() + assert cluster_cfg["name"] == new_name + new_id = cluster_cfg["id"] + + # Check that the duplicate has the right properties + res = client.get( + f"/v1/studies/{variant_id}/areas/{area_id}/clusters/thermal/{new_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + cluster_cfg = res.json() + assert cluster_cfg["group"] == "Nuclear" + assert cluster_cfg["unitCount"] == 13 + assert cluster_cfg["nominalCapacity"] == 42500 + assert cluster_cfg["marginalCost"] == 0.2 + + # Check that the duplicate has the right matrix + new_cluster_matrix_path = f"input/thermal/prepro/{area_id}/{new_id.lower()}/data" + res = client.get( + f"/v1/studies/{variant_id}/raw", + params={"path": new_cluster_matrix_path}, + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200 + assert res.json()["data"] == matrix + + # Delete the thermal cluster + res = client.delete( + f"/v1/studies/{variant_id}/areas/{area_id}/clusters/thermal", + headers={"Authorization": f"Bearer {user_access_token}"}, + json=[cluster_id], + ) + assert res.status_code == 204, res.json() + + # Check the list of variant commands + res = client.get( + f"/v1/studies/{variant_id}/commands", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + commands = res.json() + assert len(commands) == 7 + actions = [command["action"] for command in commands] + assert actions == [ + "create_area", + "create_cluster", + "update_config", + "replace_matrix", + "create_cluster", + "replace_matrix", + "remove_cluster", + ]