From 1c1bbd23f6878f927a6da43dfaa3d1465a93d1b7 Mon Sep 17 00:00:00 2001 From: vargastat <130074062+vargastat@users.noreply.github.com> Date: Mon, 13 May 2024 13:59:08 +0200 Subject: [PATCH] Yaml format for components and TSV format for timeseries (#37) --- requirements-dev.txt | 2 + src/andromede/study/data.py | 24 ++- src/andromede/study/parsing.py | 55 ++++++ src/andromede/study/resolve_components.py | 170 ++++++++++++++++++ tests/functional/test_andromede.py | 38 ++-- tests/functional/test_xpansion.py | 8 +- tests/unittests/data/components.yml | 46 +++++ .../components_for_short_term_storage.yml | 83 +++++++++ tests/unittests/data/demand-ts.txt | 10 ++ tests/unittests/data/gen-costs.txt | 2 + tests/unittests/data/lib.yml | 33 ++++ tests/unittests/model/test_model_parsing.py | 4 +- tests/unittests/study/__init__.py | 0 .../study/test_components_parsing.py | 143 +++++++++++++++ tests/unittests/test_data.py | 69 ++++--- tests/unittests/test_utils.py | 9 +- 16 files changed, 640 insertions(+), 56 deletions(-) create mode 100644 src/andromede/study/parsing.py create mode 100644 src/andromede/study/resolve_components.py create mode 100644 tests/unittests/data/components.yml create mode 100644 tests/unittests/data/components_for_short_term_storage.yml create mode 100644 tests/unittests/data/demand-ts.txt create mode 100644 tests/unittests/data/gen-costs.txt create mode 100644 tests/unittests/study/__init__.py create mode 100644 tests/unittests/study/test_components_parsing.py diff --git a/requirements-dev.txt b/requirements-dev.txt index 4f6c0409..647280ff 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,3 +6,5 @@ pytest-cov pre-commit~=3.5.0 types-PyYAML~=6.0.12.12 antlr4-tools~=0.2.1 +pandas~=2.0.3 +pandas-stubs<=2.0.3 diff --git a/src/andromede/study/data.py b/src/andromede/study/data.py index 9cb792f8..50d4acda 100644 --- a/src/andromede/study/data.py +++ b/src/andromede/study/data.py @@ -9,10 +9,13 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. - +import os from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Dict +from pathlib import Path +from typing import Dict, Optional + +import pandas as pd from andromede.study.network import Network @@ -101,6 +104,18 @@ def check_requirement(self, time: bool, scenario: bool) -> bool: return scenario +def load_ts_from_txt( + timeseries_name: Optional[str], path_to_file: Optional[Path] +) -> pd.DataFrame: + if path_to_file is not None and timeseries_name is not None: + timeseries_with_extension = timeseries_name + ".txt" + ts_path = path_to_file / timeseries_with_extension + try: + return pd.read_csv(ts_path, header=None, sep="\s+") + except Exception: + raise Exception(f"An error has arrived when processing '{ts_path}'") + + @dataclass(frozen=True) class TimeScenarioSeriesData(AbstractDataStructure): """ @@ -109,10 +124,11 @@ class TimeScenarioSeriesData(AbstractDataStructure): can be defined by referencing one of those timeseries by its ID. """ - time_scenario_series: Dict[TimeScenarioIndex, float] + time_scenario_series: pd.DataFrame def get_value(self, timestep: int, scenario: int) -> float: - return self.time_scenario_series[TimeScenarioIndex(timestep, scenario)] + value = str(self.time_scenario_series.iloc[timestep, scenario]) + return float(value) def check_requirement(self, time: bool, scenario: bool) -> bool: if not isinstance(self, TimeScenarioSeriesData): diff --git a/src/andromede/study/parsing.py b/src/andromede/study/parsing.py new file mode 100644 index 00000000..6f3723fe --- /dev/null +++ b/src/andromede/study/parsing.py @@ -0,0 +1,55 @@ +# 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. +import typing +from typing import List, Optional + +from pydantic import BaseModel, Field +from yaml import safe_load + + +def parse_yaml_components(input_components: typing.TextIO) -> "InputComponents": + tree = safe_load(input_components) + return InputComponents.model_validate(tree["study"]) + + +# Design note: actual parsing and validation is delegated to pydantic models +def _to_kebab(snake: str) -> str: + return snake.replace("_", "-") + + +class InputPortConnections(BaseModel): + component1: str + port_1: str + component2: str + port_2: str + + +class InputComponentParameter(BaseModel): + name: str + type: str + value: Optional[float] = None + timeseries: Optional[str] = None + + +class InputComponent(BaseModel): + id: str + model: str + parameters: Optional[List[InputComponentParameter]] = None + + +class InputComponents(BaseModel): + nodes: List[InputComponent] = Field(default_factory=list) + components: List[InputComponent] = Field(default_factory=list) + connections: List[InputPortConnections] = Field(default_factory=list) + + class Config: + alias_generator = _to_kebab diff --git a/src/andromede/study/resolve_components.py b/src/andromede/study/resolve_components.py new file mode 100644 index 00000000..d6650e78 --- /dev/null +++ b/src/andromede/study/resolve_components.py @@ -0,0 +1,170 @@ +# 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 dataclasses import dataclass +from pathlib import Path +from typing import Dict, Iterable, List, Optional + +import pandas as pd + +from andromede.model import Model +from andromede.model.library import Library +from andromede.study import ( + Component, + ConstantData, + DataBase, + Network, + Node, + PortRef, + PortsConnection, +) +from andromede.study.data import ( + AbstractDataStructure, + TimeScenarioIndex, + TimeScenarioSeriesData, + load_ts_from_txt, +) +from andromede.study.parsing import ( + InputComponent, + InputComponents, + InputPortConnections, +) + + +@dataclass(frozen=True) +class NetworkComponents: + components: Dict[str, Component] + nodes: Dict[str, Component] + connections: List[PortsConnection] + + +def network_components( + components_list: Iterable[Component], + nodes: Iterable[Component], + connections: Iterable[PortsConnection], +) -> NetworkComponents: + return NetworkComponents( + components=dict((m.id, m) for m in components_list), + nodes=dict((n.id, n) for n in nodes), + connections=list(connections), + ) + + +def resolve_components_and_cnx( + input_comp: InputComponents, library: Library +) -> NetworkComponents: + """ + Resolves: + - components to be used for study + - connections between components""" + components_list = [_resolve_component(library, m) for m in input_comp.components] + nodes = [_resolve_component(library, n) for n in input_comp.nodes] + all_components: List[Component] = components_list + nodes + connections = [] + for cnx in input_comp.connections: + resolved_cnx = _resolve_connections(cnx, all_components) + connections.append(resolved_cnx) + + return network_components(components_list, nodes, connections) + + +def _resolve_component(library: Library, component: InputComponent) -> Component: + model = library.models[component.model] + + return Component( + model=model, + id=component.id, + ) + + +def _resolve_connections( + connection: InputPortConnections, + all_components: List[Component], +) -> PortsConnection: + cnx_component1 = connection.component1 + cnx_component2 = connection.component2 + port1 = connection.port_1 + port2 = connection.port_2 + + component_1 = _get_component_by_id(all_components, cnx_component1) + component_2 = _get_component_by_id(all_components, cnx_component2) + assert component_1 is not None and component_2 is not None + port_ref_1 = PortRef(component_1, port1) + port_ref_2 = PortRef(component_2, port2) + + return PortsConnection(port_ref_1, port_ref_2) + + +def _get_component_by_id( + all_components: List[Component], component_id: str +) -> Optional[Component]: + components_dict = {component.id: component for component in all_components} + return components_dict.get(component_id) + + +def consistency_check( + input_components: Dict[str, Component], input_models: Dict[str, Model] +) -> bool: + """ + Checks if all components in the Components instances have a valid model from the library. + Returns True if all components are consistent, raises ValueError otherwise. + """ + model_ids_set = input_models.keys() + for component_id, component in input_components.items(): + if component.model.id not in model_ids_set: + raise ValueError( + f"Error: Component {component_id} has invalid model ID: {component.model.id}" + ) + return True + + +def build_network(comp_network: NetworkComponents) -> Network: + network = Network("study") + + for node_id, node in comp_network.nodes.items(): + node = Node(model=node.model, id=node_id) + network.add_node(node) + + for component_id, component in comp_network.components.items(): + network.add_component(component) + + for connection in comp_network.connections: + network.connect(connection.port1, connection.port2) + return network + + +def build_data_base( + input_comp: InputComponents, timeseries_dir: Optional[Path] +) -> DataBase: + database = DataBase() + for comp in input_comp.components: + for param in comp.parameters or []: + param_value = _evaluate_param_type( + param.type, param.value, param.timeseries, timeseries_dir + ) + database.add_data(comp.id, param.name, param_value) + + return database + + +def _evaluate_param_type( + param_type: str, + param_value: Optional[float], + timeseries_name: Optional[str], + timeseries_dir: Optional[Path], +) -> AbstractDataStructure: + if param_type == "constant" and param_value is not None: + return ConstantData(float(param_value)) + + elif param_type == "timeseries": + return TimeScenarioSeriesData(load_ts_from_txt(timeseries_name, timeseries_dir)) + + raise ValueError(f"Data should be either constant or timeseries ") diff --git a/tests/functional/test_andromede.py b/tests/functional/test_andromede.py index 300ff88d..e977605f 100644 --- a/tests/functional/test_andromede.py +++ b/tests/functional/test_andromede.py @@ -10,6 +10,7 @@ # # This file is part of the Antares project. +import pandas as pd import pytest from andromede.expression import literal, param, var @@ -215,11 +216,17 @@ def test_timeseries() -> None: database.add_data("G", "p_max", ConstantData(100)) database.add_data("G", "cost", ConstantData(30)) - - demand_data = TimeScenarioSeriesData( - {TimeScenarioIndex(0, 0): 100, TimeScenarioIndex(1, 0): 50} + demand_data = pd.DataFrame( + [ + [100], + [50], + ], + index=[0, 1], + columns=[0], ) - database.add_data("D", "demand", demand_data) + + demand_time_scenario_series = TimeScenarioSeriesData(demand_data) + database.add_data("D", "demand", demand_time_scenario_series) node = Node(model=NODE_BALANCE_MODEL, id="1") demand = create_component( @@ -401,14 +408,18 @@ def test_min_up_down_times() -> None: database.add_data("U", "cost", ConstantData(3000)) database.add_data("S", "cost", ConstantData(10)) - demand_data = TimeScenarioSeriesData( - { - TimeScenarioIndex(0, 0): 500, - TimeScenarioIndex(1, 0): 0, - TimeScenarioIndex(2, 0): 0, - } + demand_data = pd.DataFrame( + [ + [500], + [0], + [0], + ], + index=[0, 1, 2], + columns=[0], ) - database.add_data("D", "demand", demand_data) + + demand_time_scenario_series = TimeScenarioSeriesData(demand_data) + database.add_data("D", "demand", demand_time_scenario_series) time_block = TimeBlock(1, [0, 1, 2]) scenarios = 1 @@ -473,7 +484,10 @@ def generate_data( data[TimeScenarioIndex(absolute_timestep, scenario)] = -18 else: data[TimeScenarioIndex(absolute_timestep, scenario)] = 2 * efficiency - return TimeScenarioSeriesData(time_scenario_series=data) + + values = [value for value in data.values()] + data_df = pd.DataFrame(values, columns=["Value"]) + return TimeScenarioSeriesData(data_df) def short_term_storage_base(efficiency: float, horizon: int) -> None: diff --git a/tests/functional/test_xpansion.py b/tests/functional/test_xpansion.py index e7e5847b..cba44bca 100644 --- a/tests/functional/test_xpansion.py +++ b/tests/functional/test_xpansion.py @@ -10,6 +10,7 @@ # # This file is part of the Antares project. +import pandas as pd import pytest from andromede.expression.expression import literal, param, port_field, var @@ -47,7 +48,6 @@ Network, Node, PortRef, - TimeScenarioIndex, TimeScenarioSeriesData, create_component, ) @@ -347,11 +347,7 @@ def test_generation_xpansion_two_time_steps_two_scenarios( horizon = 2 time_block = TimeBlock(1, list(range(horizon))) - data = {} - data[TimeScenarioIndex(0, 0)] = 300 - data[TimeScenarioIndex(1, 0)] = 500 - data[TimeScenarioIndex(0, 1)] = 200 - data[TimeScenarioIndex(1, 1)] = 400 + data = pd.DataFrame([[300, 200], [500, 400]], index=[0, 1], columns=[0, 1]) demand_data = TimeScenarioSeriesData(time_scenario_series=data) diff --git a/tests/unittests/data/components.yml b/tests/unittests/data/components.yml new file mode 100644 index 00000000..b5ae7570 --- /dev/null +++ b/tests/unittests/data/components.yml @@ -0,0 +1,46 @@ +# 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. +study: + nodes: + - id: N + model: node + + components: + - id: G + model: generator + parameters: + - name: cost + type: constant + value: 30 + - name: p_max + type: constant + value: 100 + - id: D + model: demand + parameters: + - name: demand + type: constant + value: 100 + + connections: + - component1: N + port_1: injection_port + component2: D + port_2: injection_port + + - component1: N + port_1: injection_port + component2: G + port_2: injection_port + + + diff --git a/tests/unittests/data/components_for_short_term_storage.yml b/tests/unittests/data/components_for_short_term_storage.yml new file mode 100644 index 00000000..d63f12a3 --- /dev/null +++ b/tests/unittests/data/components_for_short_term_storage.yml @@ -0,0 +1,83 @@ +# 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. +study: + nodes: + - id: N + model: node + + components: + - id: D + model: demand + parameters: + - name: demand + type: timeseries + timeseries: demand-ts + - id: S + model: spillage + parameters: + - name: cost + type: constant + value: 1 + - id: U + model: unsupplied + parameters: + - name: cost + type: constant + value: 10 + - id: STS1 + model: short-term-storage + parameters: + - name: p_max_injection + type: constant + value: 100 + - name: p_max_withdrawal + type: constant + value: 50 + - name: level_min + type: constant + value: 0 + - name: level_max + type: constant + value: 1000 + - name: inflows + type: constant + value: 0 + - name: efficiency + type: constant + value: 0.8 + + + connections: + - component1: N + port_1: injection_port + component2: D + port_2: injection_port + + - component1: N + port_1: injection_port + component2: STS1 + port_2: injection_port + + - component1: N + port_1: injection_port + component2: U + port_2: injection_port + + - component1: N + port_1: injection_port + component2: S + port_2: injection_port + + + + + diff --git a/tests/unittests/data/demand-ts.txt b/tests/unittests/data/demand-ts.txt new file mode 100644 index 00000000..96bc6ccd --- /dev/null +++ b/tests/unittests/data/demand-ts.txt @@ -0,0 +1,10 @@ +-18.0 +1.6 +1.6 +1.6 +1.6 +1.6 +1.6 +1.6 +1.6 +1.6 \ No newline at end of file diff --git a/tests/unittests/data/gen-costs.txt b/tests/unittests/data/gen-costs.txt new file mode 100644 index 00000000..d1edf6de --- /dev/null +++ b/tests/unittests/data/gen-costs.txt @@ -0,0 +1,2 @@ +100 200 +50 100 \ No newline at end of file diff --git a/tests/unittests/data/lib.yml b/tests/unittests/data/lib.yml index 2d4daa9d..2941c9b9 100644 --- a/tests/unittests/data/lib.yml +++ b/tests/unittests/data/lib.yml @@ -51,6 +51,39 @@ library: binding-constraints: - name: balance expression: sum_connections(injection_port.flow) = 0 + - id: spillage + description: A basic spillage model + parameters: + - name: cost + time-dependent: false + scenario-dependent: false + variables: + - name: spillage + lower-bound: 0 + ports: + - name: injection_port + type: flow + port-field-definitions: + - port: injection_port + field: flow + definition: -spillage + - id: unsupplied + description: A basic unsupplied model + parameters: + - name: cost + time-dependent: false + scenario-dependent: false + variables: + - name: unsupplied_energy + lower-bound: 0 + ports: + - name: injection_port + type: flow + port-field-definitions: + - port: injection_port + field: flow + definition: unsupplied_energy + - id: demand description: A basic fixed demand model diff --git a/tests/unittests/model/test_model_parsing.py b/tests/unittests/model/test_model_parsing.py index e2856309..a9234839 100644 --- a/tests/unittests/model/test_model_parsing.py +++ b/tests/unittests/model/test_model_parsing.py @@ -38,11 +38,11 @@ def test_library_parsing(data_dir: Path): with lib_file.open() as f: input_lib = parse_yaml_library(f) assert input_lib.id == "basic" - assert len(input_lib.models) == 5 + assert len(input_lib.models) == 7 assert len(input_lib.port_types) == 1 lib = resolve_library(input_lib) - assert len(lib.models) == 5 + assert len(lib.models) == 7 assert len(lib.port_types) == 1 port_type = lib.port_types["flow"] assert port_type == PortType(id="flow", fields=[PortField(name="flow")]) diff --git a/tests/unittests/study/__init__.py b/tests/unittests/study/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unittests/study/test_components_parsing.py b/tests/unittests/study/test_components_parsing.py new file mode 100644 index 00000000..ddae9cbd --- /dev/null +++ b/tests/unittests/study/test_components_parsing.py @@ -0,0 +1,143 @@ +from pathlib import Path + +import pandas as pd +import pytest + +from andromede.model.parsing import InputLibrary, parse_yaml_library +from andromede.model.resolve_library import resolve_library +from andromede.simulation import BlockBorderManagement, TimeBlock, build_problem +from andromede.study import TimeScenarioIndex, TimeScenarioSeriesData +from andromede.study.parsing import InputComponents, parse_yaml_components +from andromede.study.resolve_components import ( + build_data_base, + build_network, + consistency_check, + resolve_components_and_cnx, +) + + +@pytest.fixture +def input_component( + data_dir: Path, +) -> InputComponents: + compo_file = data_dir / "components.yml" + + with compo_file.open() as c: + return parse_yaml_components(c) + + +@pytest.fixture +def input_library( + data_dir: Path, +) -> InputLibrary: + library = data_dir / "lib.yml" + + with library.open() as lib: + return parse_yaml_library(lib) + + +def test_parsing_components_ok(input_component, input_library): + assert len(input_component.components) == 2 + assert len(input_component.nodes) == 1 + assert len(input_component.connections) == 2 + lib = resolve_library(input_library) + result = resolve_components_and_cnx(input_component, lib) + + assert len(result.components) == 2 + assert len(result.nodes) == 1 + assert len(result.connections) == 2 + + +def test_consistency_check_ok(input_component, input_library): + result_lib = resolve_library(input_library) + result_comp = resolve_components_and_cnx(input_component, result_lib) + consistency_check(result_comp.components, result_lib.models) + + +def test_consistency_check_ko(input_component, input_library): + result_lib = resolve_library(input_library) + result_comp = resolve_components_and_cnx(input_component, result_lib) + result_lib.models.pop("generator") + with pytest.raises( + ValueError, + match=r"Error: Component G has invalid model ID: generator", + ): + consistency_check(result_comp.components, result_lib.models) + + +def test_basic_balance_using_yaml(input_component, input_library) -> None: + result_lib = resolve_library(input_library) + components_input = resolve_components_and_cnx(input_component, result_lib) + consistency_check(components_input.components, result_lib.models) + + database = build_data_base(input_component, None) + network = build_network(components_input) + + scenarios = 1 + problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) + status = problem.solver.Solve() + assert status == problem.solver.OPTIMAL + assert problem.solver.Objective().Value() == 3000 + + +def generate_data_for_short_term_storage_test(scenarios: int) -> TimeScenarioSeriesData: + data = {} + horizon = 10 + efficiency = 0.8 + for scenario in range(scenarios): + for absolute_timestep in range(10): + if absolute_timestep == 0: + data[TimeScenarioIndex(absolute_timestep, scenario)] = -18 + else: + data[TimeScenarioIndex(absolute_timestep, scenario)] = 2 * efficiency + + values = [value for value in data.values()] + data_df = pd.DataFrame(values, columns=["Value"]) + return TimeScenarioSeriesData(data_df) + + +def test_short_term_storage_base_with_yaml(data_dir: Path) -> None: + compo_file = data_dir / "components_for_short_term_storage.yml" + lib_file = data_dir / "lib.yml" + with lib_file.open() as lib: + input_library = parse_yaml_library(lib) + + with compo_file.open() as c: + components_file = parse_yaml_components(c) + library = resolve_library(input_library) + components_input = resolve_components_and_cnx(components_file, library) + # 18 produced in the 1st time-step, then consumed 2 * efficiency in the rest + scenarios = 1 + horizon = 10 + time_blocks = [TimeBlock(0, list(range(horizon)))] + + database = build_data_base(components_file, data_dir) + network = build_network(components_input) + + problem = build_problem( + network, + database, + time_blocks[0], + scenarios, + border_management=BlockBorderManagement.CYCLE, + ) + status = problem.solver.Solve() + + assert status == problem.solver.OPTIMAL + + # The short-term storage should satisfy the load + # No spillage / unsupplied energy is expected + assert problem.solver.Objective().Value() == 0 + + count_variables = 0 + for variable in problem.solver.variables(): + if "injection" in variable.name(): + count_variables += 1 + assert 0 <= variable.solution_value() <= 100 + elif "withdrawal" in variable.name(): + count_variables += 1 + assert 0 <= variable.solution_value() <= 50 + elif "level" in variable.name(): + count_variables += 1 + assert 0 <= variable.solution_value() <= 1000 + assert count_variables == 3 * horizon diff --git a/tests/unittests/test_data.py b/tests/unittests/test_data.py index bd243c34..02f9d979 100644 --- a/tests/unittests/test_data.py +++ b/tests/unittests/test_data.py @@ -9,13 +9,13 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. - +from pathlib import Path from typing import Union +import pandas as pd import pytest from andromede.expression import param, var -from andromede.expression.expression import port_field from andromede.expression.indexing_structure import IndexingStructure from andromede.libs.standard import ( BALANCE_PORT_TYPE, @@ -43,11 +43,11 @@ ScenarioIndex, ScenarioSeriesData, TimeIndex, - TimeScenarioIndex, TimeScenarioSeriesData, TimeSeriesData, create_component, ) +from andromede.study.data import load_ts_from_txt @pytest.fixture @@ -123,7 +123,23 @@ def mock_generator_with_scenario_varying_fixed_time_param() -> Model: return scenario_varying_fixed_time_generator -def test_requirements_consistency_demand_model_fix_ok(mock_network: Network) -> None: +@pytest.fixture +def demand_data() -> TimeScenarioSeriesData: + demand_data = pd.DataFrame( + [ + [100], + [50], + ], + index=[0, 1], + columns=[0], + ) + + return TimeScenarioSeriesData(demand_data) + + +def test_requirements_consistency_demand_model_fix_ok( + mock_network: Network, demand_data: TimeScenarioSeriesData +) -> None: # Given # database data for "demand" defined as Time varying # and model "D" DEMAND_MODEL is TIME_AND_SCENARIO_FREE @@ -131,9 +147,6 @@ def test_requirements_consistency_demand_model_fix_ok(mock_network: Network) -> database.add_data("G", "p_max", ConstantData(100)) database.add_data("G", "cost", ConstantData(30)) - demand_data = TimeScenarioSeriesData( - {TimeScenarioIndex(0, 0): 100, TimeScenarioIndex(1, 0): 50} - ) database.add_data("D", "demand", demand_data) # When @@ -155,7 +168,7 @@ def test_requirements_consistency_generator_model_ok(mock_network: Network) -> N def test_consistency_generation_time_free_for_constant_model_raises_exception( - mock_network: Network, + mock_network: Network, demand_data: TimeScenarioSeriesData ) -> None: # Given # database data for "p_max" defined as time varying @@ -165,9 +178,6 @@ def test_consistency_generation_time_free_for_constant_model_raises_exception( database.add_data("G", "cost", ConstantData(30)) - demand_data = TimeScenarioSeriesData( - {TimeScenarioIndex(0, 0): 100, TimeScenarioIndex(1, 0): 50} - ) database.add_data("D", "demand", demand_data) database.add_data("G", "p_max", demand_data) @@ -177,7 +187,7 @@ def test_consistency_generation_time_free_for_constant_model_raises_exception( def test_requirements_consistency_demand_model_time_varying_ok( - mock_network: Network, + mock_network: Network, demand_data: TimeScenarioSeriesData ) -> None: # Given # database data for "demand" defined as constant @@ -185,10 +195,6 @@ def test_requirements_consistency_demand_model_time_varying_ok( database = DataBase() database.add_data("G", "p_max", ConstantData(100)) database.add_data("G", "cost", ConstantData(30)) - - demand_data = TimeScenarioSeriesData( - {TimeScenarioIndex(0, 0): 100, TimeScenarioIndex(1, 0): 50} - ) database.add_data("D", "demand", demand_data) # When @@ -227,12 +233,14 @@ def test_requirements_consistency_time_varying_parameter_with_correct_data_passe (ScenarioSeriesData({ScenarioIndex(0): 100, ScenarioIndex(1): 50})), ( TimeScenarioSeriesData( - { - TimeScenarioIndex(0, 0): 100, - TimeScenarioIndex(0, 1): 50, - TimeScenarioIndex(1, 0): 500, - TimeScenarioIndex(1, 1): 540, - } + pd.DataFrame( + [ + [100, 500], + [500, 540], + ], + index=[0, 1], + columns=[0, 1], + ) ) ), ], @@ -270,12 +278,7 @@ def test_requirements_consistency_time_varying_parameter_with_scenario_varying_d (TimeSeriesData({TimeIndex(0): 100, TimeIndex(1): 50})), ( TimeScenarioSeriesData( - { - TimeScenarioIndex(0, 0): 100, - TimeScenarioIndex(0, 1): 50, - TimeScenarioIndex(1, 0): 500, - TimeScenarioIndex(1, 1): 540, - } + pd.DataFrame({(0, 0): [100, 500], (0, 1): [50, 540]}, index=[0, 1]) ) ), ], @@ -328,3 +331,13 @@ def test_requirements_consistency_scenario_varying_parameter_with_correct_data_p # No ValueError should be raised database.requirements_consistency(network) + + +def test_load_data_from_txt(data_dir: Path): + txt_file = "gen-costs" + + gen_costs = load_ts_from_txt(txt_file, data_dir) + expected_timeseries = pd.DataFrame( + [[100, 200], [50, 100]], index=[0, 1], columns=[0, 1] + ) + assert gen_costs.equals(expected_timeseries) diff --git a/tests/unittests/test_utils.py b/tests/unittests/test_utils.py index 306fd821..5d24a348 100644 --- a/tests/unittests/test_utils.py +++ b/tests/unittests/test_utils.py @@ -10,6 +10,7 @@ # # This file is part of the Antares project. +import pandas as pd import pytest from andromede.study import TimeScenarioIndex, TimeScenarioSeriesData @@ -33,8 +34,8 @@ def value_factory() -> str: def generate_data(value: float, horizon: int, scenarios: int) -> TimeScenarioSeriesData: - data = {} - for absolute_timestep in range(horizon): - for scenario in range(scenarios): - data[TimeScenarioIndex(absolute_timestep, scenario)] = value + data = pd.DataFrame(index=range(horizon), columns=range(scenarios)) + + data.fillna(value, inplace=True) + return TimeScenarioSeriesData(time_scenario_series=data)