Skip to content

Commit

Permalink
fix(clusters): correct thermal cluster duplication for variants
Browse files Browse the repository at this point in the history
  • Loading branch information
laurent-laporte-pro committed Mar 8, 2024
1 parent 7eddfc6 commit 9336442
Show file tree
Hide file tree
Showing 2 changed files with 193 additions and 36 deletions.
79 changes: 45 additions & 34 deletions antarest/study/business/areas/thermal_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -289,36 +292,43 @@ 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.
Args:
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.
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",
Expand All @@ -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))
150 changes: 148 additions & 2 deletions tests/integration/study_data_blueprint/test_thermal.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"""
import json
import re
import typing as t

import numpy as np
import pytest
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
]

0 comments on commit 9336442

Please sign in to comment.