-
Notifications
You must be signed in to change notification settings - Fork 29
6.2 E2E tests #3187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
6.2 E2E tests #3187
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
5a0df20
Input data for modeler test
tbittar 8c92ca2
remove duplication-by-timeblock
tbittar f638da7
Add expected outputs for invest tests
tbittar 32e2c95
Wip
JasonMarechal25 a5f0a99
13.2
JasonMarechal25 106e56f
Check master variables
JasonMarechal25 81d4d8d
Input data for modeler test
tbittar 1c69032
remove duplication-by-timeblock
tbittar 25dc528
Add expected outputs for invest tests
tbittar b4c2f1a
Fix typo
tbittar 17af25b
Update names in test 13_2
tbittar 1dc2f81
Check master expected
JasonMarechal25 3be47de
Format
JasonMarechal25 6f075c7
Compare master
JasonMarechal25 783751f
Path
JasonMarechal25 99ec704
Rename mps for 13_1
tbittar 2a9f3e7
Merge branch 'feature/add-modeler-invest-test' into feature/6.2_E2E_test
JasonMarechal25 bcb0eec
Compare subproblems
JasonMarechal25 11200d7
Compare structures
JasonMarechal25 1736b16
Better errors
JasonMarechal25 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
58 changes: 58 additions & 0 deletions
58
src/tests/cucumber/features/modeler-features/test_launcher_13.feature
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
10 changes: 8 additions & 2 deletions
10
src/tests/cucumber/features/steps/common_steps/assertions.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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}" | ||
| error = check_double_close(expected, actual, relative_tolerance, message_prefix) | ||
| assert error is None, error |
168 changes: 168 additions & 0 deletions
168
src/tests/cucumber/features/steps/common_steps/modeler_mps_steps.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| behave | ||
| pyyaml | ||
| pandas | ||
| pandas | ||
| xpress==9.6 |
16 changes: 16 additions & 0 deletions
16
src/tests/resources/modeler/13_1/expected_outputs/master.mps
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
2 changes: 2 additions & 0 deletions
2
src/tests/resources/modeler/13_1/expected_outputs/structure.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| master CAND.p_max 0 | ||
| subproblem CAND.p_max 4 |
32 changes: 32 additions & 0 deletions
32
src/tests/resources/modeler/13_1/expected_outputs/subproblem.mps
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
xpress requires a license and is nonfree, maybe we could find another python package for MPS parsing ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See https://pypi.org/project/pysmps/