From 2f48c2499591aaa25480d46d02e67f0ee5367031 Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Mon, 11 Mar 2024 20:26:14 +0100 Subject: [PATCH] feat(thermals): new fields for v8.7: `cost_generation`, `efficiency`, `variable_o_m_cost` --- .../model/filesystem/config/thermal.py | 33 +++++++++--- .../study_data_blueprint/test_thermal.py | 21 ++++---- tests/integration/test_integration.py | 4 +- .../filesystem/config/test_config_files.py | 9 +++- .../business/areas/test_thermal_management.py | 52 ++++++++++++++----- 5 files changed, 86 insertions(+), 33 deletions(-) diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/thermal.py b/antarest/study/storage/rawstudy/model/filesystem/config/thermal.py index 86a75608bb..f2a810025a 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/thermal.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/thermal.py @@ -36,7 +36,7 @@ class LocalTSGenerationBehavior(EnumIgnoreCase): FORCE_NO_GENERATION = "force no generation" FORCE_GENERATION = "force generation" - def __repr__(self) -> str: + def __repr__(self) -> str: # pragma: no cover return f"{self.__class__.__name__}.{self.name}" @@ -49,7 +49,7 @@ class LawOption(EnumIgnoreCase): UNIFORM = "uniform" GEOMETRIC = "geometric" - def __repr__(self) -> str: + def __repr__(self) -> str: # pragma: no cover return f"{self.__class__.__name__}.{self.name}" @@ -70,7 +70,7 @@ class ThermalClusterGroup(EnumIgnoreCase): OTHER3 = "Other 3" OTHER4 = "Other 4" - def __repr__(self) -> str: + def __repr__(self) -> str: # pragma: no cover return f"{self.__class__.__name__}.{self.name}" @classmethod @@ -274,16 +274,29 @@ class Thermal860Properties(ThermalProperties): ) +# noinspection SpellCheckingInspection class Thermal870Properties(Thermal860Properties): """ Thermal cluster configuration model for study in version 8.7 or above. """ - costgeneration: ThermalCostGeneration = Field(default=ThermalCostGeneration.SET_MANUALLY) - efficiency: float = Field(default=100.0, ge=0, description="Efficiency (%)") - variableomcost: float = Field( - default=0, description="Operating and Maintenance Cost (€/MWh)" - ) # Even if it's a cost it could be negative. + cost_generation: ThermalCostGeneration = Field( + default=ThermalCostGeneration.SET_MANUALLY, + alias="costgeneration", + description="Cost generation option", + ) + efficiency: float = Field( + default=100.0, + ge=0, + le=100, + description="Efficiency (%)", + ) + # Even if `variableomcost` is a cost it could be negative. + variable_o_m_cost: float = Field( + default=0.0, + description="Operating and Maintenance Cost (€/MWh)", + alias="variableomcost", + ) class ThermalConfig(ThermalProperties, IgnoreCaseIdentifier): @@ -350,6 +363,10 @@ class Thermal870Config(Thermal870Properties, IgnoreCaseIdentifier): 0.0 >>> cl.efficiency 97.0 + >>> cl.variable_o_m_cost + 0.0 + >>> cl.cost_generation == ThermalCostGeneration.SET_MANUALLY + True """ diff --git a/tests/integration/study_data_blueprint/test_thermal.py b/tests/integration/study_data_blueprint/test_thermal.py index 75c7470811..a44d7058ac 100644 --- a/tests/integration/study_data_blueprint/test_thermal.py +++ b/tests/integration/study_data_blueprint/test_thermal.py @@ -289,25 +289,26 @@ def test_lifecycle( assert task.status == TaskStatus.COMPLETED, task - # ============================= + # ================================= # UPDATE EXPECTED POLLUTANTS LIST - # ============================= + # ================================= + # noinspection SpellCheckingInspection pollutants_names = ["nh3", "nmvoc", "nox", "op1", "op2", "op3", "op4", "op5", "pm10", "pm25", "pm5", "so2"] pollutants_values = 0.0 if version >= 860 else None for existing_cluster in EXISTING_CLUSTERS: existing_cluster.update({p: pollutants_values for p in pollutants_names}) existing_cluster.update( { - "costgeneration": "SetManually" if version == 870 else None, + "costGeneration": "SetManually" if version == 870 else None, "efficiency": 100.0 if version == 870 else None, - "variableomcost": 0.0 if version == 870 else None, + "variableOMCost": 0.0 if version == 870 else None, } ) - # ============================= + # ========================== # THERMAL CLUSTER CREATION - # ============================= + # ========================== area_id = transform_name_to_id("FR") fr_gas_conventional = "FR_Gas conventional" @@ -358,9 +359,9 @@ def test_lifecycle( fr_gas_conventional_cfg = { **fr_gas_conventional_cfg, **{ - "costgeneration": "SetManually" if version == 870 else None, + "costGeneration": "SetManually" if version == 870 else None, "efficiency": 100.0 if version == 870 else None, - "variableomcost": 0.0 if version == 870 else None, + "variableOMCost": 0.0 if version == 870 else None, }, } assert res.json() == fr_gas_conventional_cfg @@ -373,9 +374,9 @@ def test_lifecycle( assert res.status_code == 200, res.json() assert res.json() == fr_gas_conventional_cfg - # ============================= + # ========================== # THERMAL CLUSTER MATRICES - # ============================= + # ========================== matrix = np.random.randint(0, 2, size=(8760, 1)).tolist() matrix_path = f"input/thermal/prepro/{area_id}/{fr_gas_conventional_id.lower()}/data" diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 5015cca783..33b059ccae 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -1780,9 +1780,9 @@ def test_area_management(client: TestClient, admin_access_token: str, study_id: "op3": 3, "op4": 2.4, "op5": 0, - "costgeneration": "SetManually", + "costGeneration": "SetManually", "efficiency": 100.0, - "variableomcost": 0.0, + "variableOMCost": 0.0, } res = client.put( # This URL is deprecated, but we must check it for backward compatibility. diff --git a/tests/storage/repository/filesystem/config/test_config_files.py b/tests/storage/repository/filesystem/config/test_config_files.py index c9411f6529..7cbab645bc 100644 --- a/tests/storage/repository/filesystem/config/test_config_files.py +++ b/tests/storage/repository/filesystem/config/test_config_files.py @@ -31,6 +31,7 @@ Thermal860Config, Thermal870Config, ThermalConfig, + ThermalCostGeneration, ) from tests.storage.business.assets import ASSETS_DIR @@ -347,7 +348,13 @@ def test_parse_thermal_860(tmp_path: Path, version, caplog) -> None: expected = [ Thermal870Config(id="t1", name="t1"), Thermal870Config( - id="t2", name="t2", co2=156, nh3=456, costgeneration="SetManually", efficiency=100.0, variableomcost=0 + id="t2", + name="t2", + co2=156, + nh3=456, + cost_generation=ThermalCostGeneration.SET_MANUALLY, + efficiency=100.0, + variable_o_m_cost=0, ), ] assert not caplog.text diff --git a/tests/study/business/areas/test_thermal_management.py b/tests/study/business/areas/test_thermal_management.py index 7c3759af3f..dd52ec9538 100644 --- a/tests/study/business/areas/test_thermal_management.py +++ b/tests/study/business/areas/test_thermal_management.py @@ -23,6 +23,34 @@ from tests.study.business.areas.assets import ASSETS_DIR +class TestThermalClusterGroup: + """ + Tests for the `ThermalClusterGroup` enumeration. + """ + + def test_nominal_case(self): + """ + When a group is read from a INI file, the group should be the same as the one in the file. + """ + group = ThermalClusterGroup("gas") # different case: original is "Gas" + assert group == ThermalClusterGroup.GAS + + def test_unknown(self): + """ + When an unknown group is read from a INI file, the group should be `OTHER1`. + Note that this is the current behavior in Antares Solver. + """ + group = ThermalClusterGroup("unknown") + assert group == ThermalClusterGroup.OTHER1 + + def test_invalid_type(self): + """ + When an invalid type is used to create a group, a `ValueError` should be raised. + """ + with pytest.raises(ValueError): + ThermalClusterGroup(123) + + @pytest.fixture(name="zip_legacy_path") def zip_legacy_path_fixture(tmp_path: Path) -> Path: target_dir = tmp_path.joinpath("resources") @@ -142,9 +170,9 @@ def test_get_cluster__study_legacy( "op4": None, "op5": None, # These values are also None as they are defined in v8.7+ - "costgeneration": None, + "costGeneration": None, "efficiency": None, - "variableomcost": None, + "variableOMCost": None, } assert actual == expected @@ -207,9 +235,9 @@ def test_get_clusters__study_legacy( "op3": None, "op4": None, "op5": None, - "costgeneration": None, + "costGeneration": None, "efficiency": None, - "variableomcost": None, + "variableOMCost": None, }, { "id": "on and must 2", @@ -246,9 +274,9 @@ def test_get_clusters__study_legacy( "op3": None, "op4": None, "op5": None, - "costgeneration": None, + "costGeneration": None, "efficiency": None, - "variableomcost": None, + "variableOMCost": None, }, { "id": "2 avail and must 2", @@ -285,9 +313,9 @@ def test_get_clusters__study_legacy( "op3": None, "op4": None, "op5": None, - "costgeneration": None, + "costGeneration": None, "efficiency": None, - "variableomcost": None, + "variableOMCost": None, }, ] assert actual == expected @@ -356,9 +384,9 @@ def test_create_cluster__study_legacy( "pm25": None, "pm5": None, "so2": None, - "costgeneration": None, + "costGeneration": None, "efficiency": None, - "variableomcost": None, + "variableOMCost": None, "spinning": 0.0, "spreadCost": 0.0, "startupCost": 0.0, @@ -424,9 +452,9 @@ def test_update_cluster( "op4": None, "op5": None, # These values are also None as they are defined in v8.7+ - "costgeneration": None, + "costGeneration": None, "efficiency": None, - "variableomcost": None, + "variableOMCost": None, } assert actual == expected