From b6a73372666b71e696616e95e0500a4d53a327a5 Mon Sep 17 00:00:00 2001 From: Ana Mileva Date: Tue, 21 Jun 2022 13:34:54 -0700 Subject: [PATCH 1/3] Export and import options (#933) --- gridpath/auxiliary/import_export_rules.py | 76 +++++++++ gridpath/common_functions.py | 38 +++-- gridpath/get_scenario_inputs.py | 4 +- gridpath/import_scenario_results.py | 36 ++-- gridpath/run_end_to_end.py | 17 +- gridpath/run_scenario.py | 190 +++++++++++++++------- 6 files changed, 262 insertions(+), 99 deletions(-) create mode 100644 gridpath/auxiliary/import_export_rules.py diff --git a/gridpath/auxiliary/import_export_rules.py b/gridpath/auxiliary/import_export_rules.py new file mode 100644 index 000000000..b7c778a7b --- /dev/null +++ b/gridpath/auxiliary/import_export_rules.py @@ -0,0 +1,76 @@ +# Copyright 2016-2022 Blue Marble Analytics LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os.path +from pyomo.environ import value + + +# Import-export rules + +# Export & import if USE is found only +def export_rule_use(instance, quiet): + unserved_energy_found = any( + [ + value(instance.Unserved_Energy_MW_Expression[z, tmp]) + for z in getattr(instance, "LOAD_ZONES") + for tmp in getattr(instance, "TMPS") + ] + ) + + if unserved_energy_found: + if not quiet: + print("unserved energy found; exporting results") + + return unserved_energy_found + + +def summarize_results_use( + scenario_directory, subproblem_directory, stage_directory, quiet +): + if os.path.exists( + os.path.join( + scenario_directory, + subproblem_directory, + stage_directory, + "results", + "load_balance.csv", + ) + ): + return True + else: + if not quiet: + print("skipping results summary") + return False + + +def import_rule_use(results_directory, quiet): + if os.path.exists(os.path.join(results_directory, "load_balance.csv")): + import_results = True + if not quiet: + print("unserved energy found -- importing") + else: + import_results = False + if not quiet: + print("no unserved energy -- skipping") + + return import_results + + +import_export_rules = { + "USE": { + "export": export_rule_use, + "summarize": summarize_results_use, + "import": import_rule_use, + } +} diff --git a/gridpath/common_functions.py b/gridpath/common_functions.py index 913f6eb05..93be0db92 100644 --- a/gridpath/common_functions.py +++ b/gridpath/common_functions.py @@ -140,7 +140,7 @@ def get_db_parser(): return parser -def get_parallel_get_inputs_parser(): +def get_get_inputs_parser(): """ """ parser = ArgumentParser(add_help=False) @@ -153,18 +153,7 @@ def get_parallel_get_inputs_parser(): return parser -def get_parallel_solve_parser(): - """ """ - - parser = ArgumentParser(add_help=False) - parser.add_argument( - "--n_parallel_solve", default=1, help="Solve n subproblems in parallel." - ) - - return parser - - -def get_solve_parser(): +def get_run_scenario_parser(): """ Create ArgumentParser object which has the common set of arguments for solving a scenario (see run_scenario.py and run_end_to_end.py). @@ -237,6 +226,29 @@ def get_solve_parser(): help="Flag for test suite runs. Results not saved.", ) + # Parallel solve + parser.add_argument( + "--n_parallel_solve", + default=1, + help="Solve n subproblems in parallel.", + ) + + # Results export rule name + parser.add_argument( + "--results_export_rule", + help="The name of the rule to use to decide whether to export results.", + ) + + return parser + + +def get_import_results_parser(): + parser = ArgumentParser(add_help=False) + parser.add_argument( + "--results_import_rule", + help="The name of the rule to use to decide whether to import results.", + ) + return parser diff --git a/gridpath/get_scenario_inputs.py b/gridpath/get_scenario_inputs.py index e28b5b07d..463dba628 100644 --- a/gridpath/get_scenario_inputs.py +++ b/gridpath/get_scenario_inputs.py @@ -36,7 +36,7 @@ create_directory_if_not_exists, get_db_parser, get_required_e2e_arguments_parser, - get_parallel_get_inputs_parser, + get_get_inputs_parser, ) from gridpath.auxiliary.module_list import determine_modules, load_modules from gridpath.auxiliary.scenario_chars import ( @@ -303,7 +303,7 @@ def parse_arguments(args): parents=[ get_db_parser(), get_required_e2e_arguments_parser(), - get_parallel_get_inputs_parser(), + get_get_inputs_parser(), ], ) diff --git a/gridpath/import_scenario_results.py b/gridpath/import_scenario_results.py index 02df749b1..8039b718a 100644 --- a/gridpath/import_scenario_results.py +++ b/gridpath/import_scenario_results.py @@ -22,16 +22,17 @@ """ from argparse import ArgumentParser -import csv import os.path import pandas as pd import sys from gridpath.auxiliary.db_interface import get_scenario_id_and_name +from gridpath.auxiliary.import_export_rules import import_export_rules from gridpath.common_functions import ( determine_scenario_directory, get_db_parser, get_required_e2e_arguments_parser, + get_import_results_parser, ) from db.common_functions import connect_to_database, spin_on_database_lock from db.utilities.scenario import delete_scenario_results @@ -39,9 +40,7 @@ from gridpath.auxiliary.scenario_chars import get_subproblem_structure_from_db -def _import_rule( - db, scenario_id, subproblem, stage, results_directory, loaded_modules, quiet -): +def _import_rule(results_directory, quiet): """ :return: boolean @@ -216,15 +215,12 @@ def import_subproblem_stage_results_into_database( Import results for a subproblem/stage. We first check the import rule to determine whether to import. """ - import_results = import_rule( - db=db, - scenario_id=scenario_id, - subproblem=subproblem, - stage=stage, - results_directory=results_directory, - loaded_modules=loaded_modules, - quiet=quiet, - ) + if import_rule is None: + import_results = _import_rule(results_directory=results_directory, quiet=quiet) + else: + import_results = import_export_rules[import_rule]["import"]( + results_directory=results_directory, quiet=quiet + ) if import_results: c = db.cursor() @@ -257,14 +253,19 @@ def parse_arguments(args): :return: """ parser = ArgumentParser( - add_help=True, parents=[get_db_parser(), get_required_e2e_arguments_parser()] + add_help=True, + parents=[ + get_db_parser(), + get_required_e2e_arguments_parser(), + get_import_results_parser(), + ], ) parsed_arguments = parser.parse_known_args(args=args)[0] return parsed_arguments -def main(import_rule, args=None): +def main(args=None): """ :return: @@ -279,6 +280,7 @@ def main(import_rule, args=None): scenario_name_arg = parsed_arguments.scenario scenario_location = parsed_arguments.scenario_location quiet = parsed_arguments.quiet + import_rule = parsed_arguments.results_import_rule conn = connect_to_database(db_path=db_path) c = conn.cursor() @@ -324,7 +326,7 @@ def main(import_rule, args=None): # Import appropriate results into database import_scenario_results_into_database( - import_rule=import_rule, + import_rule=parsed_arguments.results_import_rule, loaded_modules=loaded_modules, scenario_id=scenario_id, subproblems=subproblem_structure, @@ -339,4 +341,4 @@ def main(import_rule, args=None): if __name__ == "__main__": - main(import_rule=_import_rule) + main() diff --git a/gridpath/run_end_to_end.py b/gridpath/run_end_to_end.py index ad6344dff..5c1c24dd1 100644 --- a/gridpath/run_end_to_end.py +++ b/gridpath/run_end_to_end.py @@ -34,10 +34,9 @@ from db.common_functions import connect_to_database, spin_on_database_lock from gridpath.common_functions import ( get_db_parser, - get_solve_parser, + get_run_scenario_parser, get_required_e2e_arguments_parser, - get_parallel_get_inputs_parser, - get_parallel_solve_parser, + get_get_inputs_parser, create_logs_directory_if_not_exists, Logging, determine_scenario_directory, @@ -48,6 +47,7 @@ import_scenario_results, process_results, ) +from gridpath.run_scenario import _export_rule, _summarize_rule from gridpath.import_scenario_results import _import_rule from gridpath.auxiliary.db_interface import get_scenario_id_and_name @@ -66,9 +66,8 @@ def parse_arguments(args): parents=[ get_db_parser(), get_required_e2e_arguments_parser(), - get_solve_parser(), - get_parallel_get_inputs_parser(), - get_parallel_solve_parser(), + get_run_scenario_parser(), + get_get_inputs_parser(), ], ) @@ -374,7 +373,9 @@ def main(args=None): try: # make sure run_scenario.py gets the required --scenario argument run_scenario_args = args + ["--scenario", scenario] - expected_objective_values = run_scenario.main(args=run_scenario_args) + expected_objective_values = run_scenario.main( + args=run_scenario_args, + ) except Exception as e: logging.exception(e) end_time = update_db_for_run_end( @@ -395,7 +396,7 @@ def main(args=None): if not skip_import_results and not parsed_args.skip_import_results: try: - import_scenario_results.main(import_rule=_import_rule, args=args) + import_scenario_results.main(args=args) except Exception as e: logging.exception(e) end_time = update_db_for_run_end( diff --git a/gridpath/run_scenario.py b/gridpath/run_scenario.py index 47d3ca6a0..1caeac14e 100644 --- a/gridpath/run_scenario.py +++ b/gridpath/run_scenario.py @@ -39,13 +39,13 @@ import sys import warnings +from gridpath.auxiliary.import_export_rules import import_export_rules from gridpath.auxiliary.scenario_chars import get_subproblem_structure_from_disk from gridpath.common_functions import ( determine_scenario_directory, get_scenario_name_parser, get_required_e2e_arguments_parser, - get_solve_parser, - get_parallel_solve_parser, + get_run_scenario_parser, create_logs_directory_if_not_exists, Logging, ) @@ -136,7 +136,10 @@ def create_and_solve_problem(scenario_directory, subproblem, stage, parsed_argum def run_optimization_for_subproblem_stage( - scenario_directory, subproblem_directory, stage_directory, parsed_arguments + scenario_directory, + subproblem_directory, + stage_directory, + parsed_arguments, ): """ :param scenario_directory: the main scenario directory @@ -224,7 +227,10 @@ def run_optimization_for_subproblem_stage( # Summarize results summarize_results( - scenario_directory, subproblem_directory, stage_directory, parsed_arguments + scenario_directory, + subproblem_directory, + stage_directory, + parsed_arguments, ) # If logging, we need to return sys.stdout to original (i.e. stop writing @@ -273,7 +279,10 @@ def run_optimization_for_subproblem( if subproblem_structure.SUBPROBLEM_STAGES[subproblem] == [1]: stage_directory = "" objective_values[subproblem] = run_optimization_for_subproblem_stage( - scenario_directory, subproblem_directory, stage_directory, parsed_arguments + scenario_directory, + subproblem_directory, + stage_directory, + parsed_arguments, ) # Otherwise, run the stage problem else: @@ -309,19 +318,23 @@ def run_optimization_for_subproblem_pool(pool_datum): ) -def run_scenario(scenario_directory, subproblem_structure, parsed_arguments): +def run_scenario( + scenario_directory, + subproblem_structure, + parsed_arguments, +): """ - :param scenario_directory: scenario directory path - :param subproblem_structure: the subproblem structure object - :param parsed_arguments: - :return: the objective function value (NPV); only used in - 'testing' mode. - Check the scenario structure, iterate over all subproblems if they exist, and run the subproblem optimization. The objective function is returned, but it's only really used if we are in 'testing' mode. + + :param scenario_directory: scenario directory path + :param subproblem_structure: the subproblem structure object + :param parsed_arguments: + :return: the objective function value (NPV); only used in + 'testing' mode. """ try: n_parallel_subproblems = int(parsed_arguments.n_parallel_solve) @@ -485,8 +498,21 @@ def save_results( else: print("Solution is not optimal.") # Continue with results export + # Parse arguments to see if we're following a special rule for whether to + # export results + if parsed_arguments.results_export_rule is None: + export_rule = _export_rule(instance=instance, quiet=parsed_arguments.quiet) + else: + export_rule = import_export_rules[parsed_arguments.results_export_rule][ + "export" + ](instance=instance, quiet=parsed_arguments.quiet) export_results( - scenario_directory, subproblem, stage, instance, dynamic_components + scenario_directory, + subproblem, + stage, + instance, + dynamic_components, + export_rule, ) export_pass_through_inputs(scenario_directory, subproblem, stage, instance) @@ -761,7 +787,9 @@ def solve(instance, parsed_arguments): return results -def export_results(scenario_directory, subproblem, stage, instance, dynamic_components): +def export_results( + scenario_directory, subproblem, stage, instance, dynamic_components, export_rule +): """ :param scenario_directory: :param subproblem: @@ -772,18 +800,19 @@ def export_results(scenario_directory, subproblem, stage, instance, dynamic_comp Export results for each loaded module (if applicable) """ - # Determine/load modules and dynamic components - modules_to_use, loaded_modules = set_up_gridpath_modules( - scenario_directory=scenario_directory, subproblem=subproblem, stage=stage - ) + if export_rule: + # Determine/load modules and dynamic components + modules_to_use, loaded_modules = set_up_gridpath_modules( + scenario_directory=scenario_directory, subproblem=subproblem, stage=stage + ) - for m in loaded_modules: - if hasattr(m, "export_results"): - m.export_results( - scenario_directory, subproblem, stage, instance, dynamic_components - ) - else: - pass + for m in loaded_modules: + if hasattr(m, "export_results"): + m.export_results( + scenario_directory, subproblem, stage, instance, dynamic_components + ) + else: + pass def export_pass_through_inputs(scenario_directory, subproblem, stage, instance): @@ -906,46 +935,66 @@ def summarize_results(scenario_directory, subproblem, stage, parsed_arguments): Summarize results (after results export) """ - # Only summarize results if solver status was "optimal" - with open( - os.path.join( - scenario_directory, subproblem, stage, "results", "solver_status.txt" - ), - "r", - ) as f: - solver_status = f.read() + if parsed_arguments.results_export_rule is None: + summarize_rule = _summarize_rule( + scenario_directory=scenario_directory, + subproblem=subproblem, + stage=stage, + quiet=parsed_arguments.quiet, + ) + else: + summarize_rule = import_export_rules[parsed_arguments.results_export_rule][ + "summarize" + ]( + scenario_directory=scenario_directory, + subproblem=subproblem, + stage=stage, + quiet=parsed_arguments.quiet, + ) - if solver_status == "ok": - if not parsed_arguments.quiet: - print("Summarizing results...") + if summarize_rule: + # Only summarize results if solver status was "optimal" + with open( + os.path.join( + scenario_directory, subproblem, stage, "results", "solver_status.txt" + ), + "r", + ) as f: + solver_status = f.read() - # Determine/load modules and dynamic components - modules_to_use, loaded_modules = set_up_gridpath_modules( - scenario_directory=scenario_directory, subproblem=subproblem, stage=stage - ) + if solver_status == "ok": + if not parsed_arguments.quiet: + print("Summarizing results...") - # Make the summary results file - summary_results_file = os.path.join( - scenario_directory, subproblem, stage, "results", "summary_results.txt" - ) + # Determine/load modules and dynamic components + modules_to_use, loaded_modules = set_up_gridpath_modules( + scenario_directory=scenario_directory, + subproblem=subproblem, + stage=stage, + ) - # TODO: how to handle results from previous runs - # Overwrite prior results - with open(summary_results_file, "w", newline="") as outfile: - outfile.write( - "##### SUMMARY RESULTS FOR SCENARIO *{}* #####\n".format( - parsed_arguments.scenario - ) + # Make the summary results file + summary_results_file = os.path.join( + scenario_directory, subproblem, stage, "results", "summary_results.txt" ) - # Go through the modules and get the appropriate results - for m in loaded_modules: - if hasattr(m, "summarize_results"): - m.summarize_results(scenario_directory, subproblem, stage) + # TODO: how to handle results from previous runs + # Overwrite prior results + with open(summary_results_file, "w", newline="") as outfile: + outfile.write( + "##### SUMMARY RESULTS FOR SCENARIO *{}* #####\n".format( + parsed_arguments.scenario + ) + ) + + # Go through the modules and get the appropriate results + for m in loaded_modules: + if hasattr(m, "summarize_results"): + m.summarize_results(scenario_directory, subproblem, stage) + else: + pass else: pass - else: - pass def set_up_gridpath_modules(scenario_directory, subproblem, stage): @@ -981,8 +1030,7 @@ def parse_arguments(args): parents=[ get_scenario_name_parser(), get_required_e2e_arguments_parser(), - get_solve_parser(), - get_parallel_solve_parser(), + get_run_scenario_parser(), ], ) @@ -1043,5 +1091,29 @@ def main(args=None): return expected_objective_values +def _export_rule(instance, quiet): + """ + :return: boolean + + Rule for whether to export results for the current proble. Write your + custom rule here to use this functionality. Must return True or False. + """ + export_results = True + + return export_results + + +def _summarize_rule(scenario_directory, subproblem, stage, quiet): + """ + :return: boolean + + Rule for whether to summarize results for a subproblem/stage. Write your + custom rule here to use this functionality. Must return True or False. + """ + summarize_results = True + + return summarize_results + + if __name__ == "__main__": main() From 23aa58012e51875ef81bfdcaf5aa04a757100b1b Mon Sep 17 00:00:00 2001 From: Ana Mileva Date: Tue, 21 Jun 2022 13:40:38 -0700 Subject: [PATCH 2/3] Bump version to 0.14.1 --- ui/package-lock.json | 2 +- ui/package.json | 2 +- ui/src/index.html | 2 +- version.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 216e19a12..6a10d6e05 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "gridpath-ui", - "version": "0.14.0", + "version": "0.14.1", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/ui/package.json b/ui/package.json index b40de59a5..9faa04843 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "gridpath-ui", - "version": "0.14.0", + "version": "0.14.1", "main": "src/electron-main.js", "scripts": { "ng": "ng", diff --git a/ui/src/index.html b/ui/src/index.html index 3df5a3179..a0b867c33 100644 --- a/ui/src/index.html +++ b/ui/src/index.html @@ -2,7 +2,7 @@ - GridPath v0.14.0 + GridPath v0.14.1 diff --git a/version.py b/version.py index b45f46458..9155e966c 100644 --- a/version.py +++ b/version.py @@ -1 +1 @@ -__version__ = "v0.14.0" +__version__ = "v0.14.1" From 61960b4c91a59873dd2677c355442877471f7f06 Mon Sep 17 00:00:00 2001 From: Ana Mileva Date: Tue, 21 Jun 2022 14:31:06 -0700 Subject: [PATCH 3/3] Fix to summarize rule (#935) --- gridpath/auxiliary/import_export_rules.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/gridpath/auxiliary/import_export_rules.py b/gridpath/auxiliary/import_export_rules.py index b7c778a7b..5eebf8384 100644 --- a/gridpath/auxiliary/import_export_rules.py +++ b/gridpath/auxiliary/import_export_rules.py @@ -35,14 +35,12 @@ def export_rule_use(instance, quiet): return unserved_energy_found -def summarize_results_use( - scenario_directory, subproblem_directory, stage_directory, quiet -): +def summarize_results_use(scenario_directory, subproblem, stage, quiet): if os.path.exists( os.path.join( scenario_directory, - subproblem_directory, - stage_directory, + subproblem, + stage, "results", "load_balance.csv", )