diff --git a/src/tests/cucumber/features/modeler-features/test_launcher_13.feature b/src/tests/cucumber/features/modeler-features/test_launcher_13.feature new file mode 100644 index 00000000000..162e8ca7b4b --- /dev/null +++ b/src/tests/cucumber/features/modeler-features/test_launcher_13.feature @@ -0,0 +1,58 @@ +Feature: 13 - Invest in object + + + # Simple generation expansion problem on one node, one timestep and one scenario with one candidate. + + # Demand = 400 + # Generator : P_max : 200, Cost : 45 + # Unsupplied energy : Cost : 501 + + # -> 200 of unsupplied energy + # -> Total cost without investment = 45 * 200 + 501 * 200 = 109_200 + + # Continuous candidate : Invest cost : 400 / MW; Prod cost : 10 + + # Optimal investment : 200 MW + + # -> Optimal cost = 400 * 200 + 10 * 200 (Invest cost + prod cost of new generator) + # + 45 * 200 (Generator) + # = 80_000 + 11_000 + # = 91_000 + @fast + Scenario: 13.1: One system with one electric node (1 thermal cluster, 1 load) and one candidate with continuous invest option + Given the modeler study path is "modeler/13_1" + When I run antares modeler + Then the simulation succeeds + And the objective value is greater than 90999 and lower than 91001 + And the master problem is as expected + And the sub problems are as expected + | Sub problem name (mps file) | + | subproblem.mps | + And the structure are exported properly + + # Study case 13_2 : to test investment problems + # Simple generation expansion problem on one node, one timestep and one scenario with two candidates: one continuous and one discrete. + + # Demand = 400 + # Generator : P_max : 200, Cost : 45 + # Unsupplied energy : Cost : 501 + + # -> 200 of unsupplied energy + # -> Total cost without investment = 45 * 200 + 501 * 200 = 109_200 + + # Continuous candidate : Invest cost : 490 / MW; Prod cost : 10 + # Discrete candidate : Invest cost : 200 / MW; Prod cost : 10; Nb of units: 10; Prod per unit: 10 + + # Optimal investment : 100 MW (Discrete) + 100 MW (Continuous) + + # -> Optimal cost = 490 * 100 + 10 * 100 (Continuous) + # + 200 * 100 + 10 * 100 (Discrete) + # + 45 * 200 (Generator) + # = 69_000 + 11_000 + # = 80_000 + @fast + Scenario: 13.2: One system with one electric node (1 thermal cluster, 1 load) and two candidates : one with continuous invest option and one with discrete invest option + Given the modeler study path is "modeler/13_2" + When I run antares modeler + Then the simulation succeeds + And the objective value is greater than 79999 and lower than 80001 \ No newline at end of file diff --git a/src/tests/cucumber/features/steps/common_steps/assertions.py b/src/tests/cucumber/features/steps/common_steps/assertions.py index 5130696ea41..a23293c9e19 100644 --- a/src/tests/cucumber/features/steps/common_steps/assertions.py +++ b/src/tests/cucumber/features/steps/common_steps/assertions.py @@ -1,5 +1,11 @@ # Custom assertions +def check_double_close(expected, actual, relative_tolerance, message_prefix="Compared"): + if not (abs((actual - expected) / max(1e-6, abs(expected))) <= relative_tolerance): + return f"{message_prefix} values are not close: expected = {expected} ; actual = {actual}" + return None + + def assert_double_close(expected, actual, relative_tolerance, message_prefix="Compared"): - assert abs((actual - expected) / max(1e-6, abs(expected))) <= relative_tolerance, \ - f"{message_prefix} values are not close: expected = {expected} ; actual = {actual}" \ No newline at end of file + error = check_double_close(expected, actual, relative_tolerance, message_prefix) + assert error is None, error diff --git a/src/tests/cucumber/features/steps/common_steps/modeler_mps_steps.py b/src/tests/cucumber/features/steps/common_steps/modeler_mps_steps.py new file mode 100644 index 00000000000..4065263e5a6 --- /dev/null +++ b/src/tests/cucumber/features/steps/common_steps/modeler_mps_steps.py @@ -0,0 +1,168 @@ +from behave import * +import xpress as xp +import os +from common_steps.assertions import * + + +def get_problem_data(mps_file: str): + prob = xp.problem() + prob.read(mps_file) + + rows = prob.getAttrib("rows") + cols = prob.getAttrib("cols") + data = { + "rows": rows, + "cols": cols, + "vars": {}, + "cons": {} + } + + col_names = prob.getnamelist(2, 0, cols - 1) if cols > 0 else [] + obj_coeffs = [] + lower_bounds = [] + upper_bounds = [] + if cols > 0: + prob.getobj(obj_coeffs, 0, cols - 1) + if cols > 0: + prob.getlb(lower_bounds, 0, cols - 1) + if cols > 0: + prob.getub(upper_bounds, 0, cols - 1) + print(f"Cols : {data["cols"]}") + print(f"Col name size: {len(col_names)}\n") + for i in range(data["cols"]): + name = col_names[i] + data["vars"][name] = { + "obj": obj_coeffs[i], + "lb": lower_bounds[i], + "ub": upper_bounds[i] + } + + row_names = prob.getnamelist(1, 0, rows - 1) if rows > 0 else [] + rhs = [] + if rows > 0: + prob.getrhs(rhs, 0, rows - 1) + row_types = [] + prob.getrowtype(row_types, 0, rows - 1) + for i in range(data["rows"]): + name = row_names[i] + data["cons"][name] = { + "rhs": rhs[i], + "type": row_types[i] + } + return data + + +def problems_are_same(expected_data, actual_data): + errors = [] + if expected_data["rows"] != actual_data["rows"]: + errors.append(f"Number of rows does not match: expected={expected_data['rows']}, actual={actual_data['rows']}") + if expected_data["cols"] != actual_data["cols"]: + errors.append( + f"Number of columns does not match: expected={expected_data['cols']}, actual={actual_data['cols']}") + + if len(expected_data["vars"]) != len(actual_data["vars"]): + errors.append( + f"Number of variables does not match: expected={len(expected_data['vars'])}, actual={len(actual_data['vars'])}") + else: + for name, var_data in expected_data["vars"].items(): + if name not in actual_data["vars"]: + errors.append(f"Variable '{name}' not found in actual data") + continue + actual_var_data = actual_data["vars"][name] + error = check_double_close(var_data["obj"], actual_var_data["obj"], 1e-6, f"Var '{name}' obj") + if error: + errors.append(error) + error = check_double_close(var_data["lb"], actual_var_data["lb"], 1e-6, f"Var '{name}' lb") + if error: + errors.append(error) + error = check_double_close(var_data["ub"], actual_var_data["ub"], 1e-6, f"Var '{name}' ub") + if error: + errors.append(error) + + if len(expected_data["cons"]) != len(actual_data["cons"]): + errors.append( + f"Number of constraints does not match: expected={len(expected_data['cons'])}, actual={len(actual_data['cons'])}") + else: + for name, con_data in expected_data["cons"].items(): + if name not in actual_data["cons"]: + errors.append(f"Constraint '{name}' not found in actual data") + continue + actual_con_data = actual_data["cons"][name] + error = check_double_close(con_data["rhs"], actual_con_data["rhs"], 1e-6, f"Con '{name}' rhs") + if error: + errors.append(error) + if con_data["type"] != actual_con_data["type"]: + errors.append( + f"Con '{name}' type does not match: expected={con_data['type']}, actual={actual_con_data['type']}") + return errors + + +@step('the master problem is as expected') +def master_is_same(context): + expected_mps_path = os.path.join(context.study_path, "expected_outputs", "master.mps") + actual_mps_path = os.path.join(context.output_path, "master.mps") + + expected_data = get_problem_data(expected_mps_path) + actual_data = get_problem_data(actual_mps_path) + + errors = problems_are_same(expected_data, actual_data) + if errors: + raise AssertionError("\n".join(errors)) + + +@step('the sub problems are as expected') +def subproblems_are_same(context): + all_errors = [] + for row in context.table: + sub_problem_name = row["Sub problem name (mps file)"] + expected_mps_path = os.path.join(context.study_path, "expected_outputs", sub_problem_name) + actual_mps_path = os.path.join(context.output_path, sub_problem_name) + + expected_data = get_problem_data(expected_mps_path) + actual_data = get_problem_data(actual_mps_path) + print(f"Comparing sub problem: {sub_problem_name}") + errors = problems_are_same(expected_data, actual_data) + if errors: + all_errors.append(f"Subproblem '{sub_problem_name}' has errors:") + all_errors.extend(errors) + if all_errors: + raise AssertionError("\n".join(all_errors)) + + +def parse_structure_file(file_path: str): + """ + Parses a structure file and returns a dictionary of problems. + The file format is expected to be: problem_name candidate_name value + """ + problems = {} + if not os.path.exists(file_path): + return problems + + with open(file_path, 'r') as f: + for line in f: + line = line.strip() + if not line: + continue + parts = line.split() + if len(parts) != 3: + continue + problem_name, candidate_name, value = parts + if problem_name not in problems: + problems[problem_name] = {} + problems[problem_name][candidate_name] = value + return problems + + +@step('the structure are exported properly') +def structure_is_exported_properly(context): + expected_structure_path = os.path.join(context.study_path, "expected_outputs", "structure.txt") + actual_structure_path = os.path.join(context.output_path, "structure.txt") + + expected_problems = parse_structure_file(expected_structure_path) + actual_problems = parse_structure_file(actual_structure_path) + + assert expected_problems.keys() == actual_problems.keys(), "Problem names do not match" + + for problem_name, expected_candidates in expected_problems.items(): + actual_candidates = actual_problems[problem_name] + assert expected_candidates == actual_candidates, f"Candidates for problem {problem_name} do not match" diff --git a/src/tests/cucumber/features/steps/common_steps/modeler_output_handler.py b/src/tests/cucumber/features/steps/common_steps/modeler_output_handler.py index 7144a59d8c3..d5923c729d3 100644 --- a/src/tests/cucumber/features/steps/common_steps/modeler_output_handler.py +++ b/src/tests/cucumber/features/steps/common_steps/modeler_output_handler.py @@ -3,6 +3,7 @@ import pandas as pd import numpy as np + class modeler_output_handler: def __init__(self, simulation_table_location): @@ -20,7 +21,7 @@ def __read_simulation_table(absolute_path) -> pd.DataFrame: df[col] = df[col].astype(float) return df - def get_simulation_table_entry(self, component : str, output : str, block : int, timestep : int, scenario : int): + def get_simulation_table_entry(self, component: str, output: str, block: int, timestep: int, scenario: int): df = self.simulation_table[(self.simulation_table["component"] == component) & (self.simulation_table["output"] == output)] if not pd.isna(block): @@ -34,7 +35,8 @@ def get_simulation_table_entry(self, component : str, output : str, block : int, else: df = df[df["scenario_index"] == scenario] if len(df) != 1: - raise LookupError(f"Simulation table contains {len(df)} row(s) (expected 1) for component '{component}', output '{output}', block '{block}', timestep '{timestep}', scenario '{scenario}'") + raise LookupError( + f"Simulation table contains {len(df)} row(s) (expected 1) for component '{component}', output '{output}', block '{block}', timestep '{timestep}', scenario '{scenario}'") return df["value"].iloc[0] def get_objective_value(self): diff --git a/src/tests/cucumber/features/steps/common_steps/modeler_steps.py b/src/tests/cucumber/features/steps/common_steps/modeler_steps.py index 0e096186b93..983c106f1f8 100644 --- a/src/tests/cucumber/features/steps/common_steps/modeler_steps.py +++ b/src/tests/cucumber/features/steps/common_steps/modeler_steps.py @@ -64,10 +64,13 @@ def modeler_output_values(context): for scenario in scenario_range: for ts in ts_range: assert_double_close( - get_value(row, ts), context.moh.get_simulation_table_entry(row["component"], row["output"], block, ts, scenario), 1e-6 + get_value(row, ts), + context.moh.get_simulation_table_entry(row["component"], row["output"], block, ts, scenario), + 1e-6 ) -def read_int_range(row, key : str): + +def read_int_range(row, key: str): if row[key] != "": array = row[key].split("-") start = int(array[0]) @@ -76,6 +79,7 @@ def read_int_range(row, key : str): else: return [math.nan] + def get_value(row, ts): ret = row["value"] @@ -123,7 +127,6 @@ def parse_simulation_table_from_logs(logs: str) -> str: raise LookupError("Could not find simulation table location in output logs") - def build_antares_modeler_command(context): command = [context.config.userdata["antares-modeler"], str(context.study_path)] return command diff --git a/src/tests/cucumber/features/steps/common_steps/solver_output_handler.py b/src/tests/cucumber/features/steps/common_steps/solver_output_handler.py index 24bab6cded5..3e4e253f6be 100644 --- a/src/tests/cucumber/features/steps/common_steps/solver_output_handler.py +++ b/src/tests/cucumber/features/steps/common_steps/solver_output_handler.py @@ -14,6 +14,7 @@ class result_type(Enum): DETAILS = "details" DETAILS_STS = "details-STstorage" + class solver_output_handler: def __init__(self, study_output_path, mode): @@ -82,7 +83,8 @@ def __get_values_hourly(self, area: str, year: int): def __get_values_hourly_for_specific_week(self, area: str, year: int, week: int): df = self.__if_none_then_parse(result_type.VALUES, area.lower(), year, "values-hourly.txt") - return df[(df['hourly']['Unnamed: 1_level_1'] > (week - 1) * 168) & (df['hourly']['Unnamed: 1_level_1'] <= week * 168)] + return df[(df['hourly']['Unnamed: 1_level_1'] > (week - 1) * 168) & ( + df['hourly']['Unnamed: 1_level_1'] <= week * 168)] def __get_values_hourly_for_specific_hour(self, area: str, year: int, datetime: str): df = self.__get_values_hourly(area, year) diff --git a/src/tests/cucumber/features/steps/common_steps/solver_steps.py b/src/tests/cucumber/features/steps/common_steps/solver_steps.py index 7a7066832e7..81a7504bd5d 100644 --- a/src/tests/cucumber/features/steps/common_steps/solver_steps.py +++ b/src/tests/cucumber/features/steps/common_steps/solver_steps.py @@ -240,6 +240,8 @@ def check_simulation_tables(context): ref_simulation_table2 = context.sih.get_optim2_simulation_table() if ref_simulation_table2: assert ref_simulation_table2 == context.soh.get_optim2_simulation_table(), "second simulation table does not match the reference" + + def should_check(row, key): return key in row.headings and len(row[key]) > 0 diff --git a/src/tests/cucumber/requirements.txt b/src/tests/cucumber/requirements.txt index 2e68e9859af..08811e90087 100644 --- a/src/tests/cucumber/requirements.txt +++ b/src/tests/cucumber/requirements.txt @@ -1,3 +1,4 @@ behave pyyaml -pandas \ No newline at end of file +pandas +xpress==9.6 diff --git a/src/tests/resources/modeler/13_1/expected_outputs/master.mps b/src/tests/resources/modeler/13_1/expected_outputs/master.mps new file mode 100644 index 00000000000..97abac10c51 --- /dev/null +++ b/src/tests/resources/modeler/13_1/expected_outputs/master.mps @@ -0,0 +1,16 @@ +* Generated by MPModelProtoExporter +* Name : +* Format : Free +* Constraints : 0 +* Variables : 1 +* Binary : 0 +* Integer : 0 +* Continuous : 1 +NAME +ROWS + N COST +COLUMNS + CAND.p_max COST 400 +BOUNDS + UP BOUND CAND.p_max 1000 +ENDATA diff --git a/src/tests/resources/modeler/13_1/expected_outputs/structure.txt b/src/tests/resources/modeler/13_1/expected_outputs/structure.txt new file mode 100644 index 00000000000..a5a89cd17da --- /dev/null +++ b/src/tests/resources/modeler/13_1/expected_outputs/structure.txt @@ -0,0 +1,2 @@ + master CAND.p_max 0 + subproblem CAND.p_max 4 diff --git a/src/tests/resources/modeler/13_1/expected_outputs/subproblem.mps b/src/tests/resources/modeler/13_1/expected_outputs/subproblem.mps new file mode 100644 index 00000000000..2d3e3816dab --- /dev/null +++ b/src/tests/resources/modeler/13_1/expected_outputs/subproblem.mps @@ -0,0 +1,32 @@ +* Generated by MPModelProtoExporter +* Name : +* Format : Free +* Constraints : 3 +* Variables : 5 +* Binary : 0 +* Integer : 0 +* Continuous : 5 +NAME +ROWS + N COST + E N.Balance + L G1.Max_generation + L CAND.Max_generation +COLUMNS + N.spillage COST 1 N.Balance -1 + N.unsupplied_energy COST 501 N.Balance 1 + G1.generation COST 45 N.Balance 1 + G1.generation G1.Max_generation 1 + CAND.generation COST 10 N.Balance 1 + CAND.generation CAND.Max_generation 1 + CAND.p_max CAND.Max_generation -1 +RHS + RHS N.Balance 400 G1.Max_generation 200 + RHS CAND.Max_generation 0 +BOUNDS + PL BOUND N.spillage + PL BOUND N.unsupplied_energy + PL BOUND G1.generation + PL BOUND CAND.generation + UP BOUND CAND.p_max 1000 +ENDATA diff --git a/src/tests/resources/modeler/13_1/input/model-libraries/lib_example_13_1.yml b/src/tests/resources/modeler/13_1/input/model-libraries/lib_example_13_1.yml new file mode 100644 index 00000000000..ff5ac393339 --- /dev/null +++ b/src/tests/resources/modeler/13_1/input/model-libraries/lib_example_13_1.yml @@ -0,0 +1,115 @@ +# 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. +library: + id: lib_example_13_1 + description: Library for test cases with investment + + port-types: + - id: flow + description: A port which transfers power flow + fields: + - id: flow + + models: + - id: area + parameters: + - id: spillage_cost + - id: ens_cost + variables: + - id: spillage + lower-bound: 0 + upper-bound: 1000000 + variable-type: continuous + - id: unsupplied_energy + lower-bound: 0 + upper-bound: 1000000 + variable-type: continuous + ports: + - id: balance_port + type: flow + binding-constraints: + - id: balance + expression: sum_connections(balance_port.flow) = spillage - unsupplied_energy + objective-contributions: + - id: operational_objective + expression: sum(spillage_cost * spillage + ens_cost * unsupplied_energy) + + - id: load + parameters: + - id: load + time-dependent: true + scenario-dependent: true + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: -load + + - id: generator + parameters: + - id: p_max + scenario-dependent: false + time-dependent: false + - id: cost + scenario-dependent: false + time-dependent: false + variables: + - id: generation + lower-bound: 0 + upper-bound: p_max + variable-type: continuous + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: generation + objective-contributions: + - id: operational_objective + expression: sum(cost * generation) + + - id: generator_with_continuous_invest + parameters: + - id: op_cost + scenario-dependent: false + time-dependent: false + - id: invest_cost + scenario-dependent: false + time-dependent: false + variables: + - id: p_max + scenario-dependent: false + time-dependent: false + lower-bound: 0 + upper-bound: 1000 + variable-type: continuous + - id: generation + lower-bound: 0 + variable-type: continuous + constraints: + - id: max_generation + expression: generation <= p_max + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: generation + objective-contributions: + - id: operational_objective + expression: sum(op_cost * generation) + - id: invest_objective + expression: invest_cost * p_max \ No newline at end of file diff --git a/src/tests/resources/modeler/13_1/input/optim-config.yml b/src/tests/resources/modeler/13_1/input/optim-config.yml new file mode 100644 index 00000000000..2cb1d97aa50 --- /dev/null +++ b/src/tests/resources/modeler/13_1/input/optim-config.yml @@ -0,0 +1,23 @@ +# 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. +models: + - id: lib_example_13_1.generator_with_continuous_invest + model-decomposition: + variables: + - id: pmax + location: master-and-subproblems + # Variable generation has default settings, no need to make it appear here + objective-contributions: + - id: invest_objective + location: master + - id: operational_objective + location: subproblems \ No newline at end of file diff --git a/src/tests/resources/modeler/13_1/input/system.yml b/src/tests/resources/modeler/13_1/input/system.yml new file mode 100644 index 00000000000..400b8012fd5 --- /dev/null +++ b/src/tests/resources/modeler/13_1/input/system.yml @@ -0,0 +1,93 @@ +# 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 case 13_1 : to test investment problems +# Simple generation expansion problem on one node, one timestep and one scenario with one candidate. + +# Demand = 400 +# Generator : P_max : 200, Cost : 45 +# Unsupplied energy : Cost : 501 + +# -> 200 of unsupplied energy +# -> Total cost without investment = 45 * 200 + 501 * 200 = 109_200 + +# Continuous candidate : Invest cost : 400 / MW; Prod cost : 10 + +# Optimal investment : 200 MW + +# -> Optimal cost = 400 * 200 + 10 * 200 (Invest cost + prod cost of new generator) +# + 45 * 200 (Generator) +# = 80_000 + 11_000 +# = 91_000 + +system: + id: case_13_1 + components: + - id: my_node + model: lib_example_13_1.area + parameters: + - id: spillage_cost + time-dependent: false + scenario-dependent: false + value: 1 + - id: ens_cost + time-dependent: false + scenario-dependent: false + value: 501 + + - id: load + model: lib_example_13_1.load + parameters: + - id: load + time-dependent: false + scenario-dependent: false + value: 400 + + - id: already_installed_generator + model: lib_example_13_1.generator + parameters: + - id: p_max + scenario-dependent: false + time-dependent: false + value: 200 + - id: cost + scenario-dependent: false + time-dependent: false + value: 45 + + - id: continuous_generator_candidate + model: lib_example_13_1.generator_with_continuous_invest + parameters: + - id: op_cost + scenario-dependent: false + time-dependent: false + value: 10 + - id: invest_cost + scenario-dependent: false + time-dependent: false + value: 400 + + connections: + - component1: my_node + port1: balance_port + component2: load + port2: balance_port + + - component1: my_node + port1: balance_port + component2: already_installed_generator + port2: balance_port + + - component1: my_node + port1: balance_port + component2: continuous_generator_candidate + port2: balance_port diff --git a/src/tests/resources/modeler/13_1/parameters.yml b/src/tests/resources/modeler/13_1/parameters.yml new file mode 100644 index 00000000000..e6d94cbe8b1 --- /dev/null +++ b/src/tests/resources/modeler/13_1/parameters.yml @@ -0,0 +1,6 @@ +solver: highs +solver-logs: true +solver-parameters: THREADS 1 +no-output: false +first-time-step: 0 +last-time-step: 0 \ No newline at end of file diff --git a/src/tests/resources/modeler/13_2/expected_outputs/master.mps b/src/tests/resources/modeler/13_2/expected_outputs/master.mps new file mode 100644 index 00000000000..b7783cfbf5c --- /dev/null +++ b/src/tests/resources/modeler/13_2/expected_outputs/master.mps @@ -0,0 +1,23 @@ +* Generated by MPModelProtoExporter +* Name : +* Format : Free +* Constraints : 1 +* Variables : 3 +* Binary : 0 +* Integer : 0 +* Continuous : 3 +NAME +ROWS + N COST + E DISCRETE.Max_investment +COLUMNS + CAND.p_max COST 490 + DISCRETE.p_max COST 200 DISCRETE.Max_investment 1 + DISCRETE.nb_units DISCRETE.Max_investment -10 +RHS + RHS DISCRETE.Max_investment 0 +BOUNDS + UP BOUND CAND.p_max 1000 + PL BOUND DISCRETE.p_max + UP BOUND DISCRETE.nb_units 10 +ENDATA diff --git a/src/tests/resources/modeler/13_2/expected_outputs/structure.txt b/src/tests/resources/modeler/13_2/expected_outputs/structure.txt new file mode 100644 index 00000000000..e692a811ee3 --- /dev/null +++ b/src/tests/resources/modeler/13_2/expected_outputs/structure.txt @@ -0,0 +1,5 @@ + master CAND.p_max 0 + master DISCRETE.p_max 1 + master DISCRETE.nb_units 2 + subproblem CAND.p_max 4 + subproblem DISCRETE.p_max 6 diff --git a/src/tests/resources/modeler/13_2/expected_outputs/subproblem.mps b/src/tests/resources/modeler/13_2/expected_outputs/subproblem.mps new file mode 100644 index 00000000000..b7c2243fb02 --- /dev/null +++ b/src/tests/resources/modeler/13_2/expected_outputs/subproblem.mps @@ -0,0 +1,38 @@ +* Generated by MPModelProtoExporter +* Name : +* Format : Free +* Constraints : 4 +* Variables : 7 +* Binary : 0 +* Integer : 0 +* Continuous : 7 +NAME +ROWS + N COST + E N.Balance + L G1.Max_generation + L CAND.Max_generation + L DISCRETE.Max_generation +COLUMNS + N.spillage COST 1 N.Balance -1 + N.unsupplied_energy COST 501 N.Balance 1 + G1.generation COST 45 N.Balance 1 + G1.generation G1.Max_generation 1 + CAND.generation COST 10 N.Balance 1 + CAND.generation CAND.Max_generation 1 + CAND.p_max CAND.Max_generation -1 + DISCRETE.generation COST 10 N.Balance 1 + DISCRETE.generation DISCRETE.Max_generation 1 + DISCRETE.p_max DISCRETE.Max_generation -1 +RHS + RHS N.Balance 400 G1.Max_generation 200 + RHS CAND.Max_generation 0 DISCRETE.Max_generation 0 +BOUNDS + PL BOUND N.spillage + PL BOUND N.unsupplied_energy + PL BOUND G1.generation + PL BOUND CAND.generation + UP BOUND CAND.p_max 1000 + PL BOUND DISCRETE.generation + PL BOUND DISCRETE.p_max +ENDATA diff --git a/src/tests/resources/modeler/13_2/input/model-libraries/lib_example_13_2.yml b/src/tests/resources/modeler/13_2/input/model-libraries/lib_example_13_2.yml new file mode 100644 index 00000000000..c8fb2b6c933 --- /dev/null +++ b/src/tests/resources/modeler/13_2/input/model-libraries/lib_example_13_2.yml @@ -0,0 +1,160 @@ +# 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. +library: + id: lib_example_13_2 + description: Library for test cases with investment + + port-types: + - id: flow + description: A port which transfers power flow + fields: + - id: flow + + models: + - id: area + parameters: + - id: spillage_cost + - id: ens_cost + variables: + - id: spillage + lower-bound: 0 + upper-bound: 1000000 + variable-type: continuous + - id: unsupplied_energy + lower-bound: 0 + upper-bound: 1000000 + variable-type: continuous + ports: + - id: balance_port + type: flow + binding-constraints: + - id: balance + expression: sum_connections(balance_port.flow) = spillage - unsupplied_energy + objective-contributions: + - id: operational_objective + expression: sum(spillage_cost * spillage + ens_cost * unsupplied_energy) + + - id: load + parameters: + - id: load + time-dependent: true + scenario-dependent: true + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: -load + + - id: generator + parameters: + - id: p_max + scenario-dependent: false + time-dependent: false + - id: cost + scenario-dependent: false + time-dependent: false + variables: + - id: generation + lower-bound: 0 + upper-bound: p_max + variable-type: continuous + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: generation + objective-contributions: + - id: operational_objective + expression: sum(cost * generation) + + - id: generator_with_continuous_invest + parameters: + - id: op_cost + scenario-dependent: false + time-dependent: false + - id: invest_cost + scenario-dependent: false + time-dependent: false + variables: + - id: p_max + scenario-dependent: false + time-dependent: false + lower-bound: 0 + upper-bound: 1000 + variable-type: continuous + - id: generation + lower-bound: 0 + variable-type: continuous + constraints: + - id: max_generation + expression: generation <= p_max + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: generation + objective-contributions: + - id: operational_objective + expression: sum(op_cost * generation) + - id: invest_objective + expression: invest_cost * p_max + + - id: generator_with_discrete_invest + parameters: + - id: op_cost + scenario-dependent: false + time-dependent: false + - id: invest_cost + scenario-dependent: false + time-dependent: false + - id: p_max_per_unit + scenario-dependent: false + time-dependent: false + variables: + - id: p_max + scenario-dependent: false + time-dependent: false + lower-bound: 0 + variable-type: continuous + - id: generation + lower-bound: 0 + variable-type: continuous + - id: nb_units + scenario-dependent: false + time-dependent: false + lower-bound: 0 + upper-bound: 10 + variable-type: integer + constraints: + - id: max_generation + expression: generation <= p_max + - id: p_max_nb_units_relation + expression: p_max = p_max_per_unit * nb_units + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: generation + objective-contributions: + - id: operational_objective + expression: sum(op_cost * generation) + - id: invest_objective + expression: invest_cost * p_max + \ No newline at end of file diff --git a/src/tests/resources/modeler/13_2/input/optim-config.yml b/src/tests/resources/modeler/13_2/input/optim-config.yml new file mode 100644 index 00000000000..c8f6e2153d5 --- /dev/null +++ b/src/tests/resources/modeler/13_2/input/optim-config.yml @@ -0,0 +1,41 @@ +# 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. +models: + - id: lib_example_13_2.generator_with_discrete_invest + model-decomposition: + variables: + - id: nb_units + location: master + - id: pmax + location: master-and-subproblems + # Variable generation has default settings, no need to make it appear here + constraints: + - id: p_max_nb_units_relation + location: master + # Constraint max_generation has default settings, no need to make it appear here + objective-contributions: + - id: invest_objective + location: master + - id: operational_objective + location: subproblems + + - id: lib_example_13_2.generator_with_continuous_invest + model-decomposition: + variables: + - id: pmax + location: master-and-subproblems + # Variable generation has default settings, no need to make it appear here + objective-contributions: + - id: invest_objective + location: master + - id: operational_objective + location: subproblems \ No newline at end of file diff --git a/src/tests/resources/modeler/13_2/input/system.yml b/src/tests/resources/modeler/13_2/input/system.yml new file mode 100644 index 00000000000..46447365688 --- /dev/null +++ b/src/tests/resources/modeler/13_2/input/system.yml @@ -0,0 +1,116 @@ +# 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 case 13_2 : to test investment problems +# Simple generation expansion problem on one node, one timestep and one scenario with two candidates: one continuous and one discrete. + +# Demand = 400 +# Generator : P_max : 200, Cost : 45 +# Unsupplied energy : Cost : 501 + +# -> 200 of unsupplied energy +# -> Total cost without investment = 45 * 200 + 501 * 200 = 109_200 + +# Continuous candidate : Invest cost : 490 / MW; Prod cost : 10 +# Discrete candidate : Invest cost : 200 / MW; Prod cost : 10; Nb of units: 10; Prod per unit: 10 + +# Optimal investment : 100 MW (Discrete) + 100 MW (Continuous) + +# -> Optimal cost = 490 * 100 + 10 * 100 (Continuous) +# + 200 * 100 + 10 * 100 (Discrete) +# + 45 * 200 (Generator) +# = 69_000 + 11_000 +# = 80_000 + +system: + id: case_13_2 + components: + - id: my_node + model: lib_example_13_2.area + parameters: + - id: spillage_cost + time-dependent: false + scenario-dependent: false + value: 1 + - id: ens_cost + time-dependent: false + scenario-dependent: false + value: 501 + + - id: load + model: lib_example_13_2.load + parameters: + - id: load + time-dependent: false + scenario-dependent: false + value: 400 + + - id: already_installed_generator + model: lib_example_13_2.generator + parameters: + - id: p_max + scenario-dependent: false + time-dependent: false + value: 200 + - id: cost + scenario-dependent: false + time-dependent: false + value: 45 + + - id: continuous_generator_candidate + model: lib_example_13_2.generator_with_continuous_invest + parameters: + - id: op_cost + scenario-dependent: false + time-dependent: false + value: 10 + - id: invest_cost + scenario-dependent: false + time-dependent: false + value: 490 + + - id: discrete_generator_candidate + model: lib_example_13_2.generator_with_discrete_invest + parameters: + - id: op_cost + scenario-dependent: false + time-dependent: false + value: 10 + - id: invest_cost + scenario-dependent: false + time-dependent: false + value: 200 + - id: p_max_per_unit + scenario-dependent: false + time-dependent: false + value: 10 + + connections: + - component1: my_node + port1: balance_port + component2: load + port2: balance_port + + - component1: my_node + port1: balance_port + component2: already_installed_generator + port2: balance_port + + - component1: my_node + port1: balance_port + component2: continuous_generator_candidate + port2: balance_port + + - component1: my_node + port1: balance_port + component2: discrete_generator_candidate + port2: balance_port diff --git a/src/tests/resources/modeler/13_2/parameters.yml b/src/tests/resources/modeler/13_2/parameters.yml new file mode 100644 index 00000000000..e6d94cbe8b1 --- /dev/null +++ b/src/tests/resources/modeler/13_2/parameters.yml @@ -0,0 +1,6 @@ +solver: highs +solver-logs: true +solver-parameters: THREADS 1 +no-output: false +first-time-step: 0 +last-time-step: 0 \ No newline at end of file