diff --git a/antarest/study/business/areas/renewable_management.py b/antarest/study/business/areas/renewable_management.py index fac9d925a8..c4152924bf 100644 --- a/antarest/study/business/areas/renewable_management.py +++ b/antarest/study/business/areas/renewable_management.py @@ -49,7 +49,7 @@ class Config: def schema_extra(schema: t.MutableMapping[str, t.Any]) -> None: schema["example"] = RenewableClusterInput( group="Gas", - name="2 avail and must 1", + name="Gas Cluster XY", enabled=False, unitCount=100, nominalCapacity=1000.0, @@ -87,9 +87,9 @@ class Config: @staticmethod def schema_extra(schema: t.MutableMapping[str, t.Any]) -> None: schema["example"] = RenewableClusterOutput( - 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, @@ -159,23 +159,25 @@ def create_cluster( The newly created cluster. """ file_study = self._get_file_study(study) - study_version = study.version - cluster = cluster_data.to_config(study_version) - - command = CreateRenewablesCluster( - 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, [command], self.storage_service, ) + output = self.get_cluster(study, area_id, cluster.id) + return output - return self.get_cluster(study, area_id, cluster.id) + def _make_create_cluster_cmd(self, area_id: str, cluster: RenewableConfigType) -> CreateRenewablesCluster: + command = CreateRenewablesCluster( + 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 get_cluster(self, study: Study, area_id: str, cluster_id: str) -> RenewableClusterOutput: """ @@ -276,7 +278,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) -> RenewableClusterOutput: + def duplicate_cluster( + self, + study: Study, + area_id: str, + source_id: str, + new_cluster_name: str, + ) -> RenewableClusterOutput: """ Creates a duplicate cluster within the study area with a new name. @@ -284,7 +292,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. @@ -292,27 +300,30 @@ 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("Renewable", new_id) # Cluster duplication current_cluster = self.get_cluster(study, area_id, source_id) - current_cluster.name = new_name + current_cluster.name = new_cluster_name creation_form = RenewableClusterCreation(**current_cluster.dict(by_alias=False, exclude={"id"})) - new_cluster = self.create_cluster(study, area_id, creation_form) + new_config = creation_form.to_config(study.version) + create_cluster_cmd = self._make_create_cluster_cmd(area_id, new_config) # Matrix edition - current_path = f"input/renewables/series/{area_id}/{source_id.lower()}/series" - new_path = f"input/renewables/series/{area_id}/{new_id.lower()}/series" - current_matrix = self.storage_service.raw_study_service.get(study, current_path) - command = [ - ReplaceMatrix( - target=new_path, - matrix=current_matrix["data"], - command_context=self.storage_service.variant_study_service.command_factory.command_context, - ) - ] - execute_or_add_commands(study, self._get_file_study(study), command, self.storage_service) + lower_source_id = source_id.lower() + source_path = f"input/renewables/series/{area_id}/{lower_source_id}/series" + new_path = f"input/renewables/series/{area_id}/{lower_new_id}/series" + + # Prepare and execute commands + storage_service = self.storage_service.get_storage(study) + command_context = self.storage_service.variant_study_service.command_factory.command_context + current_matrix = storage_service.get(study, source_path)["data"] + replace_matrix_cmd = ReplaceMatrix(target=new_path, matrix=current_matrix, command_context=command_context) + commands = [create_cluster_cmd, replace_matrix_cmd] + + execute_or_add_commands(study, self._get_file_study(study), commands, self.storage_service) - return new_cluster + return RenewableClusterOutput(**new_config.dict(by_alias=False)) diff --git a/tests/integration/study_data_blueprint/test_renewable.py b/tests/integration/study_data_blueprint/test_renewable.py index 4bb71a791f..8447c0430f 100644 --- a/tests/integration/study_data_blueprint/test_renewable.py +++ b/tests/integration/study_data_blueprint/test_renewable.py @@ -25,6 +25,7 @@ """ import json import re +import typing as t import numpy as np import pytest @@ -488,3 +489,144 @@ def test_lifecycle( description = obj["description"] assert other_cluster_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 renewable 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 renewable cluster + cluster_name = "Th1" + res = client.post( + f"/v1/studies/{variant_id}/areas/{area_id}/clusters/renewable", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={ + "name": cluster_name, + "group": "Wind Offshore", + "unitCount": 13, + "nominalCapacity": 42500, + }, + ) + assert res.status_code in {200, 201}, res.json() + cluster_id: str = res.json()["id"] + + # Update the renewable cluster + res = client.patch( + f"/v1/studies/{variant_id}/areas/{area_id}/clusters/renewable/{cluster_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={"unitCount": 15}, + ) + assert res.status_code == 200, res.json() + cluster_cfg = res.json() + assert cluster_cfg["unitCount"] == 15 + + # Update the series matrix + matrix = np.random.randint(0, 2, size=(8760, 1)).tolist() + matrix_path = f"input/renewables/series/{area_id}/{cluster_id.lower()}/series" + 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 renewable cluster + new_name = "Th2" + res = client.post( + f"/v1/studies/{variant_id}/areas/{area_id}/renewables/{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/renewable/{new_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + cluster_cfg = res.json() + assert cluster_cfg["group"] == "Wind Offshore" + assert cluster_cfg["unitCount"] == 15 + assert cluster_cfg["nominalCapacity"] == 42500 + + # Check that the duplicate has the right matrix + new_cluster_matrix_path = f"input/renewables/series/{area_id}/{new_id.lower()}/series" + 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 renewable cluster + res = client.delete( + f"/v1/studies/{variant_id}/areas/{area_id}/clusters/renewable", + 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_renewables_cluster", + "update_config", + "replace_matrix", + "create_renewables_cluster", + "replace_matrix", + "remove_renewables_cluster", + ]