diff --git a/src/antares/model/area.py b/src/antares/model/area.py index f56905d6..6f1f9221 100644 --- a/src/antares/model/area.py +++ b/src/antares/model/area.py @@ -25,6 +25,7 @@ from antares.model.commons import FilterOption, sort_filter_values from antares.model.hydro import HydroProperties, HydroMatrixName, Hydro +from antares.model.load import Load from antares.model.misc_gen import MiscGen from antares.model.renewable import RenewableCluster, RenewableClusterProperties from antares.model.reserves import Reserves @@ -242,6 +243,7 @@ def __init__( # type: ignore # TODO: Find a way to avoid circular imports *, renewables: Optional[Dict[str, RenewableCluster]] = None, thermals: Optional[Dict[str, ThermalCluster]] = None, + load: Optional[Load] = None, st_storages: Optional[Dict[str, STStorage]] = None, hydro: Optional[Hydro] = None, wind: Optional[Wind] = None, @@ -259,6 +261,7 @@ def __init__( # type: ignore # TODO: Find a way to avoid circular imports self._renewable_service = renewable_service self._renewables = renewables or dict() self._thermals = thermals or dict() + self._load = load self._st_storages = st_storages or dict() self._hydro = hydro self._wind = wind @@ -327,6 +330,11 @@ def create_renewable_cluster( self._renewables[renewable.id] = renewable return renewable + def create_load(self, series: Optional[pd.DataFrame]) -> Load: + load = self._area_service.create_load(self, series) + self._load = load + return load + def create_st_storage(self, st_storage_name: str, properties: Optional[STStorageProperties] = None) -> STStorage: storage = self._area_service.create_st_storage(self.id, st_storage_name, properties) self._st_storages[storage.id] = storage diff --git a/src/antares/model/load.py b/src/antares/model/load.py new file mode 100644 index 00000000..49a681ce --- /dev/null +++ b/src/antares/model/load.py @@ -0,0 +1,40 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +from pathlib import Path +from typing import Optional + +import pandas as pd + +from antares.tools.prepro_folder import PreproFolder +from antares.tools.time_series_tool import TimeSeries, TimeSeriesFile + + +class Load: + def __init__( + self, + time_series: pd.DataFrame = pd.DataFrame([]), + local_file: Optional[TimeSeriesFile] = None, + study_path: Optional[Path] = None, + area_id: Optional[str] = None, + ) -> None: + self._time_series = TimeSeries(time_series, local_file) + self._prepro = ( + PreproFolder(folder="load", study_path=study_path, area_id=area_id) if study_path and area_id else None + ) + + @property + def time_series(self) -> TimeSeries: + return self._time_series + + @property + def prepro(self) -> Optional[PreproFolder]: + return self._prepro diff --git a/src/antares/model/study.py b/src/antares/model/study.py index eeca334e..d136c593 100644 --- a/src/antares/model/study.py +++ b/src/antares/model/study.py @@ -168,6 +168,7 @@ def _correlation_defaults() -> dict[str, dict[str, str]]: correlation_inis_to_create = [ ("solar_correlation", IniFileTypes.SOLAR_CORRELATION_INI), ("wind_correlation", IniFileTypes.WIND_CORRELATION_INI), + ("load_correlation", IniFileTypes.LOAD_CORRELATION_INI), ] ini_files = { correlation: IniFile(study_directory, file_type, ini_contents=_correlation_defaults()) diff --git a/src/antares/service/api_services/area_api.py b/src/antares/service/api_services/area_api.py index 2883c23b..52a4769f 100644 --- a/src/antares/service/api_services/area_api.py +++ b/src/antares/service/api_services/area_api.py @@ -37,6 +37,7 @@ ) from antares.model.area import AreaProperties, AreaUi, Area from antares.model.hydro import HydroProperties, HydroMatrixName, Hydro +from antares.model.load import Load from antares.model.misc_gen import MiscGen from antares.model.renewable import RenewableClusterProperties, RenewableCluster from antares.model.reserves import Reserves @@ -323,6 +324,9 @@ def create_renewable_cluster( return RenewableCluster(self.renewable_service, area_id, name, properties) + def create_load(self, area: Area, series: Optional[pd.DataFrame]) -> Load: + raise NotImplementedError + def create_st_storage( self, area_id: str, st_storage_name: str, properties: Optional[STStorageProperties] = None ) -> STStorage: diff --git a/src/antares/service/base_services.py b/src/antares/service/base_services.py index abdd04d4..f372d6aa 100644 --- a/src/antares/service/base_services.py +++ b/src/antares/service/base_services.py @@ -26,6 +26,7 @@ ) from antares.model.hydro import HydroProperties, HydroMatrixName, Hydro from antares.model.link import LinkProperties, LinkUi, Link +from antares.model.load import Load from antares.model.misc_gen import MiscGen from antares.model.renewable import RenewableClusterProperties, RenewableCluster from antares.model.reserves import Reserves @@ -120,6 +121,16 @@ def create_renewable_cluster( """ pass + @abstractmethod + def create_load(self, area: Area, series: Optional[pd.DataFrame]) -> Load: + """ + Args: + area: area to create load series matrices + series: load/series/load_{area_id}.txt + + """ + pass + @abstractmethod def create_st_storage( self, area_id: str, st_storage_name: str, properties: Optional[STStorageProperties] = None diff --git a/src/antares/service/local_services/area_local.py b/src/antares/service/local_services/area_local.py index 7f855f85..4ae4870a 100644 --- a/src/antares/service/local_services/area_local.py +++ b/src/antares/service/local_services/area_local.py @@ -21,6 +21,7 @@ from antares.exceptions.exceptions import CustomError from antares.model.area import AreaProperties, AreaUi, Area, AreaPropertiesLocal, AreaUiLocal from antares.model.hydro import HydroProperties, HydroMatrixName, Hydro, HydroPropertiesLocal +from antares.model.load import Load from antares.model.misc_gen import MiscGen from antares.model.renewable import RenewableClusterProperties, RenewableCluster, RenewableClusterPropertiesLocal from antares.model.reserves import Reserves @@ -116,6 +117,11 @@ def create_renewable_cluster( self.renewable_service, area_id, renewable_name, local_properties.yield_renewable_cluster_properties() ) + def create_load(self, area: Area, series: Optional[pd.DataFrame]) -> Load: + series = series if series is not None else pd.DataFrame([]) + local_file = TimeSeriesFile(TimeSeriesFileType.LOAD, self.config.study_path, area.id, series) + return Load(time_series=series, local_file=local_file, study_path=self.config.study_path, area_id=area.id) + def create_st_storage( self, area_id: str, st_storage_name: str, properties: Optional[STStorageProperties] = None ) -> STStorage: diff --git a/src/antares/tools/ini_tool.py b/src/antares/tools/ini_tool.py index e04c1d85..5eccbfeb 100644 --- a/src/antares/tools/ini_tool.py +++ b/src/antares/tools/ini_tool.py @@ -34,6 +34,8 @@ class IniFileTypes(Enum): AREA_ADEQUACY_PATCH_INI = "input/areas/{area_name}/adequacy_patch.ini" HYDRO_INI = "input/hydro/hydro.ini" LINK_PROPERTIES_INI = "input/links/{area_name}/properties.ini" + LOAD_CORRELATION_INI = "input/load/prepro/correlation.ini" + LOAD_SETTINGS_INI = "input/load/prepro/{area_name}/settings.ini" RENEWABLES_LIST_INI = "input/renewables/clusters/{area_name}/list.ini" SOLAR_CORRELATION_INI = "input/solar/prepro/correlation.ini" SOLAR_SETTINGS_INI = "input/solar/prepro/{area_name}/settings.ini" diff --git a/src/antares/tools/prepro_folder.py b/src/antares/tools/prepro_folder.py index f520b3cd..5f053556 100644 --- a/src/antares/tools/prepro_folder.py +++ b/src/antares/tools/prepro_folder.py @@ -24,6 +24,12 @@ def __init__(self, folder: str, study_path: Path, area_id: str) -> None: data = TimeSeriesFileType.WIND_DATA k = TimeSeriesFileType.WIND_K translation = TimeSeriesFileType.WIND_TRANSLATION + elif folder == "load": + settings = IniFileTypes.LOAD_SETTINGS_INI + conversion = TimeSeriesFileType.LOAD_CONVERSION + data = TimeSeriesFileType.LOAD_DATA + k = TimeSeriesFileType.LOAD_K + translation = TimeSeriesFileType.LOAD_TRANSLATION self._settings = IniFile(study_path, settings, area_id) self._conversion = TimeSeries( diff --git a/src/antares/tools/time_series_tool.py b/src/antares/tools/time_series_tool.py index 55116f3e..35bf1bb9 100644 --- a/src/antares/tools/time_series_tool.py +++ b/src/antares/tools/time_series_tool.py @@ -31,6 +31,11 @@ class TimeSeriesFileType(Enum): TimeSeriesFileType.SOLAR.value.format(area_id="test_area") """ + LOAD = "input/load/series/load_{area_id}.txt" + LOAD_CONVERSION = "input/load/prepro/{area_id}/conversion.txt" + LOAD_DATA = "input/load/prepro/{area_id}/data.txt" + LOAD_K = "input/load/prepro/{area_id}/k.txt" + LOAD_TRANSLATION = "input/load/prepro/{area_id}/translation.txt" MISC_GEN = "input/misc-gen/miscgen-{area_id}.txt" RESERVES = "input/reserves/{area_id}.txt" SOLAR = "input/solar/series/solar_{area_id}.txt" diff --git a/tests/antares/services/local_services/conftest.py b/tests/antares/services/local_services/conftest.py index 296238ca..6147e5db 100644 --- a/tests/antares/services/local_services/conftest.py +++ b/tests/antares/services/local_services/conftest.py @@ -15,6 +15,7 @@ from antares.config.local_configuration import LocalConfiguration from antares.model.area import Area from antares.model.hydro import HydroProperties +from antares.model.load import Load from antares.model.renewable import RenewableClusterProperties, TimeSeriesInterpretation, RenewableClusterGroup from antares.model.solar import Solar from antares.model.st_storage import STStorageProperties, STStorageGroup @@ -222,3 +223,8 @@ def fr_solar(area_fr) -> Solar: @pytest.fixture def fr_wind(area_fr) -> Wind: return area_fr.create_wind(None) + + +@pytest.fixture +def fr_load(area_fr) -> Load: + return area_fr.create_load(None) diff --git a/tests/antares/services/local_services/test_area.py b/tests/antares/services/local_services/test_area.py index fe220793..67b59c70 100644 --- a/tests/antares/services/local_services/test_area.py +++ b/tests/antares/services/local_services/test_area.py @@ -1031,3 +1031,158 @@ def test_translation_txt_is_empty_by_default(self, area_fr, fr_solar): # Then assert actual_file_contents == expected_file_contents + + +class TestCreateLoad: + def test_can_create_load_ts_file(self, area_fr): + # Given + load_file_path = area_fr._area_service.config.study_path / TimeSeriesFileType.LOAD.value.format( + area_id=area_fr.id + ) + expected_load_file_path = area_fr._area_service.config.study_path / "input/load/series/load_fr.txt" + + # When + area_fr.create_load(None) + + # Then + assert load_file_path == expected_load_file_path + assert load_file_path.exists() + assert load_file_path.is_file() + + def test_can_create_load_ts_file_with_time_series(self, area_fr): + # Given + load_file_path = area_fr._area_service.config.study_path / TimeSeriesFileType.LOAD.value.format( + area_id=area_fr.id + ) + expected_time_series_string = """1.0\t1.0\t1.0 +1.0\t1.0\t1.0 +""" + expected_time_series = pd.read_csv(StringIO(expected_time_series_string), sep="\t", header=None) + + # When + area_fr.create_load(pd.DataFrame(np.ones([2, 3]))) + actual_time_series = pd.read_csv(load_file_path, sep="\t", header=None) + with load_file_path.open("r") as load_ts_file: + actual_time_series_string = load_ts_file.read() + + # Then + assert actual_time_series.equals(expected_time_series) + assert actual_time_series_string == expected_time_series_string + + def test_settings_ini_exists(self, area_fr, fr_load): + # Given + expected_ini_path = area_fr._area_service.config.study_path / "input/load/prepro/fr/settings.ini" + + # Then + assert expected_ini_path.exists() + assert expected_ini_path.is_file() + assert expected_ini_path == fr_load.prepro.settings.ini_path + + def test_conversion_txt_exists(self, area_fr, fr_load): + # Given + expected_file_path = area_fr._area_service.config.study_path / TimeSeriesFileType.LOAD_CONVERSION.value.format( + area_id=area_fr.id + ) + + # Then + assert expected_file_path.exists() + assert expected_file_path.is_file() + assert fr_load.prepro.conversion.local_file.file_path == expected_file_path + + def test_conversion_txt_has_correct_default_values(self, area_fr, fr_load): + # Given + expected_file_contents = """-9999999980506447872\t0\t9999999980506447872 +0\t0\t0 +""" + # data has to be compared as strings as the first value in the first column is too small for python apparently + expected_file_data = pd.read_csv(StringIO(expected_file_contents), sep="\t", header=None).astype(str) + + # When + with fr_load.prepro.conversion.local_file.file_path.open("r") as fr_load_file: + actual_file_contents = fr_load_file.read() + actual_file_data = fr_load.prepro.conversion.time_series.astype(str) + + # Then + assert actual_file_data.equals(expected_file_data) + assert actual_file_contents == expected_file_contents + + def test_data_txt_exists(self, area_fr, fr_load): + # Given + expected_file_path = area_fr._area_service.config.study_path / TimeSeriesFileType.LOAD_DATA.value.format( + area_id=area_fr.id + ) + + # Then + assert expected_file_path.exists() + assert expected_file_path.is_file() + assert fr_load.prepro.data.local_file.file_path == expected_file_path + + def test_data_txt_has_correct_default_values(self, area_fr, fr_load): + # Given + expected_file_contents = """1\t1\t0\t1\t1\t1 +1\t1\t0\t1\t1\t1 +1\t1\t0\t1\t1\t1 +1\t1\t0\t1\t1\t1 +1\t1\t0\t1\t1\t1 +1\t1\t0\t1\t1\t1 +1\t1\t0\t1\t1\t1 +1\t1\t0\t1\t1\t1 +1\t1\t0\t1\t1\t1 +1\t1\t0\t1\t1\t1 +1\t1\t0\t1\t1\t1 +1\t1\t0\t1\t1\t1 +""" + expected_file_data = pd.read_csv(StringIO(expected_file_contents), sep="\t", header=None) + + # When + with fr_load.prepro.data.local_file.file_path.open("r") as fr_load_file: + actual_file_contents = fr_load_file.read() + actual_file_data = fr_load.prepro.data.time_series + + # Then + assert actual_file_data.equals(expected_file_data) + assert actual_file_contents == expected_file_contents + + def test_k_txt_exists(self, area_fr, fr_load): + # Given + expected_file_path = area_fr._area_service.config.study_path / TimeSeriesFileType.LOAD_K.value.format( + area_id=area_fr.id + ) + + # Then + assert expected_file_path.exists() + assert expected_file_path.is_file() + assert fr_load.prepro.k.local_file.file_path == expected_file_path + + def test_k_txt_is_empty_by_default(self, area_fr, fr_load): + # Given + expected_file_contents = """""" + + # When + with fr_load.prepro.k.local_file.file_path.open("r") as fr_load_file: + actual_file_contents = fr_load_file.read() + + # Then + assert actual_file_contents == expected_file_contents + + def test_translation_txt_exists(self, area_fr, fr_load): + # Given + expected_file_path = area_fr._area_service.config.study_path / TimeSeriesFileType.LOAD_TRANSLATION.value.format( + area_id=area_fr.id + ) + + # Then + assert expected_file_path.exists() + assert expected_file_path.is_file() + assert fr_load.prepro.translation.local_file.file_path == expected_file_path + + def test_translation_txt_is_empty_by_default(self, area_fr, fr_load): + # Given + expected_file_contents = """""" + + # When + with fr_load.prepro.translation.local_file.file_path.open("r") as fr_load_file: + actual_file_contents = fr_load_file.read() + + # Then + assert actual_file_contents == expected_file_contents diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index 5f8495ca..5280c9b2 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -221,6 +221,60 @@ def test_wind_correlation_ini_has_default_values(self, local_study_with_hydro): assert actual_ini.parsed_ini.sections() == expected_ini.sections() assert actual_ini.parsed_ini == expected_ini + def test_load_correlation_ini_exists(self, local_study_with_hydro): + # Given + expected_ini_path = local_study_with_hydro.service.config.study_path / "input/load/prepro/correlation.ini" + + # Then + assert expected_ini_path.exists() + assert expected_ini_path.is_file() + assert local_study_with_hydro._ini_files["load_correlation"].ini_path == expected_ini_path + + def test_load_correlation_ini_has_default_values(self, local_study_with_hydro): + # Given + expected_ini_content = """[general] +mode = annual + +[annual] + +[0] + +[1] + +[2] + +[3] + +[4] + +[5] + +[6] + +[7] + +[8] + +[9] + +[10] + +[11] + +""" + expected_ini = ConfigParser() + actual_ini = local_study_with_hydro._ini_files["load_correlation"] + + # When + expected_ini.read_string(expected_ini_content) + with actual_ini.ini_path.open("r") as ini_file: + actual_ini_content = ini_file.read() + + # Then + assert actual_ini_content == expected_ini_content + assert actual_ini.parsed_ini.sections() == expected_ini.sections() + assert actual_ini.parsed_ini == expected_ini + class TestCreateArea: def test_areas_sets_ini_content(self, tmp_path, local_study):