diff --git a/src/andromede/study/data.py b/src/andromede/study/data.py index 958ff378..cdfd4fba 100644 --- a/src/andromede/study/data.py +++ b/src/andromede/study/data.py @@ -105,7 +105,9 @@ def check_requirement(self, time: bool, scenario: bool) -> bool: def load_ts_from_txt(file_ts: Optional[str]) -> pd.DataFrame: - path = Path(str(file_ts)) + base_path = Path.cwd().resolve().parent / "data" + if file_ts is not None: + path = base_path / file_ts try: return pd.read_csv(path, header=None, sep="\s+") except FileNotFoundError: 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..411ba6f6 --- /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.txt + - 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/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/study/test_components_parsing.py b/tests/unittests/study/test_components_parsing.py index d97664e1..2c60152d 100644 --- a/tests/unittests/study/test_components_parsing.py +++ b/tests/unittests/study/test_components_parsing.py @@ -1,10 +1,12 @@ 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 TimeBlock, build_problem +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, @@ -76,3 +78,67 @@ def test_basic_balance_using_yaml(input_component, input_library) -> None: 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) + 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