From d8cd120bcd0c88f3f7c2b955e6bc23e250006e8e Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Dec 2022 01:35:33 -0500 Subject: [PATCH 01/18] Add more judicious enforcement of PyROS time limit --- pyomo/contrib/pyros/master_problem_methods.py | 48 +++++++++++++++- .../contrib/pyros/pyros_algorithm_methods.py | 57 +++++++++++-------- .../pyros/separation_problem_methods.py | 3 +- pyomo/contrib/pyros/util.py | 43 ++++++++++++++ 4 files changed, 124 insertions(+), 27 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index f24262bc9a6..c89a9f65034 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -6,12 +6,15 @@ Objective, Constraint, ConstraintList, SortComponents) from pyomo.opt import TerminationCondition as tc +from pyomo.opt import SolverResults from pyomo.core.expr import value from pyomo.core.base.set_types import NonNegativeIntegers, NonNegativeReals from pyomo.contrib.pyros.util import (selective_clone, ObjectiveType, pyrosTerminationCondition, process_termination_condition_master_problem, + adjust_solver_time_settings, + get_main_elapsed_time, output_logger) from pyomo.contrib.pyros.solve_data import (MasterProblemData, MasterResult) @@ -241,6 +244,7 @@ def solve_master_feasibility_problem(model_data, config): else: solver = config.local_solver + adjust_solver_time_settings(model_data.timing, solver, config) try: results = solver.solve(model, tee=config.tee, load_solutions=False) except ApplicationError: @@ -400,6 +404,7 @@ def minimize_dr_vars(model_data, config): # === Solve the polishing model timer = TicTocTimer() + adjust_solver_time_settings(model_data.timing, solver, config) timer.tic(msg=None) try: results = solver.solve( @@ -564,6 +569,7 @@ def solver_call_master(model_data, config, solver, solve_data): timer = TicTocTimer() for opt in backup_solvers: + adjust_solver_time_settings(model_data.timing, opt, config) timer.tic(msg=None) try: results = opt.solve( @@ -619,7 +625,6 @@ def solver_call_master(model_data, config, solver, solve_data): v.value for v in nlp_model.scenarios[0, 0].util.first_stage_variables ) - if config.objective_focus is ObjectiveType.nominal: master_soln.ssv_vals = list( v.value @@ -647,6 +652,20 @@ def solver_call_master(model_data, config, solver, solve_data): master_soln.results = results master_soln.master_model = nlp_model + # if PyROS time limit exceeded, exit loop and return solution + elapsed = get_main_elapsed_time(model_data.timing) + if config.time_limit: + if elapsed >= config.time_limit: + try_backup = False + master_soln.master_subsolver_results = ( + None, + pyrosTerminationCondition.time_out + ) + master_soln.pyros_termination_condition = ( + pyrosTerminationCondition.time_out + ) + output_logger(config=config, time_out=True, elapsed=elapsed) + if not try_backup: return master_soln @@ -691,6 +710,33 @@ def solve_master(model_data, config): results = solve_master_feasibility_problem(model_data, config) master_soln.feasibility_problem_results = results + # if pyros time limit reached, load time out status + # to master results and return to caller + elapsed = get_main_elapsed_time(model_data.timing) + if config.time_limit: + if elapsed >= config.time_limit: + # load master model + master_soln.master_model = model_data.master_model + master_soln.nominal_block = model_data.master_model.scenarios[0, 0] + + # empty results object, with master solve time of zero + master_soln.results = SolverResults() + setattr(master_soln.results.solver, TIC_TOC_SOLVE_TIME_ATTR, 0) + + # PyROS time out status + master_soln.pyros_termination_condition = ( + pyrosTerminationCondition.time_out + ) + master_soln.master_subsolver_results = ( + None, + pyrosTerminationCondition.time_out + ) + + # log time out message + output_logger(config=config, time_out=True, elapsed=elapsed) + + return master_soln + solver = config.global_solver if config.solve_master_globally else config.local_solver return solver_call_master(model_data=model_data, config=config, solver=solver, diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index 5e370a3a0d7..92433e2313c 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -178,27 +178,25 @@ def ROSolver_iterative_solve(model_data, config): output_logger(config=config, robust_infeasible=True) elif master_soln.pyros_termination_condition is pyrosTerminationCondition.subsolver_error: term_cond = pyrosTerminationCondition.subsolver_error + elif master_soln.pyros_termination_condition is pyrosTerminationCondition.time_out: + term_cond = pyrosTerminationCondition.time_out else: term_cond = None - if term_cond == pyrosTerminationCondition.subsolver_error or \ - term_cond == pyrosTerminationCondition.robust_infeasible: - update_grcs_solve_data(pyros_soln=model_data, k=k, term_cond=term_cond, - nominal_data=nominal_data, - timing_data=timing_data, - separation_data=separation_data, - master_soln=master_soln) + if term_cond in { + pyrosTerminationCondition.subsolver_error, + pyrosTerminationCondition.time_out, + pyrosTerminationCondition.robust_infeasible, + }: + update_grcs_solve_data( + pyros_soln=model_data, + k=k, + term_cond=term_cond, + nominal_data=nominal_data, + timing_data=timing_data, + separation_data=separation_data, + master_soln=master_soln, + ) return model_data, [] - # === Check if time limit reached - elapsed = get_main_elapsed_time(model_data.timing) - if config.time_limit: - if elapsed >= config.time_limit: - output_logger(config=config, time_out=True, elapsed=elapsed) - update_grcs_solve_data(pyros_soln=model_data, k=k, term_cond=pyrosTerminationCondition.time_out, - nominal_data=nominal_data, - timing_data=timing_data, - separation_data=separation_data, - master_soln=master_soln) - return model_data, [] # === Save nominal information if k == 0: @@ -212,7 +210,6 @@ def ROSolver_iterative_solve(model_data, config): nominal_data.nom_second_stage_cost = master_soln.second_stage_objective nominal_data.nom_obj = value(master_data.master_model.obj) - if ( # === Decision rule polishing (do not polish on first iteration if no ssv or if decision_rule_order = 0) (config.decision_rule_order != 0 and len(config.second_stage_variables) > 0 and k != 0) @@ -234,6 +231,22 @@ def ROSolver_iterative_solve(model_data, config): vals.append(dvar.value) dr_var_lists_polished.append(vals) + # === Check if time limit reached + elapsed = get_main_elapsed_time(model_data.timing) + if config.time_limit: + if elapsed >= config.time_limit: + output_logger(config=config, time_out=True, elapsed=elapsed) + update_grcs_solve_data( + pyros_soln=model_data, + k=k, + term_cond=pyrosTerminationCondition.time_out, + nominal_data=nominal_data, + timing_data=timing_data, + separation_data=separation_data, + master_soln=master_soln, + ) + return model_data, [] + # === Set up for the separation problem separation_data.opt_fsv_vals = [v.value for v in master_soln.master_model.scenarios[0,0].util.first_stage_variables] separation_data.opt_ssv_vals = master_soln.ssv_vals @@ -337,9 +350,3 @@ def ROSolver_iterative_solve(model_data, config): # === In this case we still return the final solution objects for the last iteration return model_data, separation_solns - - - - - - diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index 9e7e70dc5f3..efaf374345f 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -21,7 +21,7 @@ from pyomo.common.errors import ApplicationError from pyomo.contrib.pyros.util import ABS_CON_CHECK_FEAS_TOL from pyomo.common.timing import TicTocTimer -from pyomo.contrib.pyros.util import TIC_TOC_SOLVE_TIME_ATTR +from pyomo.contrib.pyros.util import TIC_TOC_SOLVE_TIME_ATTR, adjust_solver_time_settings import os from copy import deepcopy @@ -564,6 +564,7 @@ def solver_call_separation(model_data, config, solver, solve_data, is_global): timer = TicTocTimer() for opt in backup_solvers: + adjust_solver_time_settings(model_data.timing, opt, config) timer.tic(msg=None) try: results = opt.solve( diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index ccbb84fb24a..a2604dfdc2f 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -20,6 +20,8 @@ from pyomo.core.expr.numvalue import native_types from pyomo.util.vars_from_expressions import get_vars_from_components from pyomo.core.expr.numeric_expr import SumExpression +from pyomo.environ import SolverFactory + import itertools as it import timeit from contextlib import contextmanager @@ -66,6 +68,46 @@ def get_main_elapsed_time(timing_data_obj): "You need to be in a 'time_code' context to use `get_main_elapsed_time()`." ) + +def adjust_solver_time_settings(timing_data_obj, solver, config): + """ + Adjust solver max time based on current PyROS elapsed time. + + Returns + ------- + new_solver : solver type + Clone of solver provided, with time limit setting + adjusted. + """ + if config.time_limit is not None: + # determine name of option to adjust + if isinstance(solver, type(SolverFactory("baron"))): + options_key = "MaxTime" + elif isinstance(solver, type(SolverFactory("gams"))): + options_key = "reslim" + elif isinstance(solver, type(SolverFactory("ipopt"))): + options_key = "max_cpu_time" + else: + options_key = None + + # NOTE: + # (1) adjustment only supported for GAMS, BARON, and IPOPT + # interfaces. Generalize after interface to max time + # introduced + # (2) for IPOPT, and probably also BARON, the CPU time limit + # rather than the wallclock time limit, is adjusted, as + # no interface to wallclock limit available. + # For this reason, extra 30s is added to time remaining + # for subsolver time limit + if options_key is not None: + time_remaining = ( + config.time_limit - get_main_elapsed_time(timing_data_obj) + ) + + # ensure positive value assigned to avoid application error + solver.options[options_key] = max(30, 30 + time_remaining) + + def a_logger(str_or_logger): """Returns a logger when passed either a logger name or logger object.""" if isinstance(str_or_logger, logging.Logger): @@ -73,6 +115,7 @@ def a_logger(str_or_logger): else: return logging.getLogger(str_or_logger) + def ValidEnum(enum_class): ''' Python 3 dependent format string From 3d0cf2f1ca96755c42210dc34628e37e24874ec4 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 12 Dec 2022 06:39:33 -0500 Subject: [PATCH 02/18] Resolve failing unit test attribute errors --- pyomo/contrib/pyros/tests/test_grcs.py | 31 +++++++++++++++++++++----- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 9036d7588d9..83cfe05ab10 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -16,6 +16,8 @@ from pyomo.contrib.pyros.util import replace_uncertain_bounds_with_constraints from pyomo.contrib.pyros.util import get_vars_from_component from pyomo.contrib.pyros.util import identify_objective_functions +from pyomo.common.collections import Bunch +from pyomo.contrib.pyros.util import time_code from pyomo.core.expr import current as EXPR from pyomo.contrib.pyros.uncertainty_sets import * from pyomo.contrib.pyros.master_problem_methods import add_scenario_to_master, initial_construct_master, solve_master, \ @@ -3070,6 +3072,8 @@ def test_solve_master(self): master_data.master_model.scenarios[0, 0].second_stage_objective = \ Expression(expr=master_data.master_model.scenarios[0, 0].x) master_data.iteration = 0 + master_data.timing = Bunch() + box_set = BoxSet(bounds=[(0,2)]) solver = SolverFactory(global_solver) config = ConfigBlock() @@ -3082,9 +3086,18 @@ def test_solve_master(self): config.declare("objective_focus", ConfigValue(default=ObjectiveType.worst_case)) config.declare("second_stage_variables", ConfigValue(default=master_data.master_model.scenarios[0, 0].util.second_stage_variables)) config.declare("subproblem_file_directory", ConfigValue(default=None)) - master_soln = solve_master(master_data, config) - self.assertEqual(master_soln.termination_condition, TerminationCondition.optimal, - msg="Could not solve simple master problem with solve_master function.") + config.declare("time_limit", ConfigValue(default=None)) + + with time_code(master_data.timing, "total", is_main_timer=True): + master_soln = solve_master(master_data, config) + self.assertEqual( + master_soln.termination_condition, + TerminationCondition.optimal, + msg=( + "Could not solve simple master problem with solve_master " + "function." + ), + ) # === regression test for the solver class coefficientMatchingTests(unittest.TestCase): @@ -3299,6 +3312,7 @@ def test_minimize_dr_norm(self): config.uncertain_params = m.working_model.util.uncertain_params config.tee = False config.solve_master_globally = True + config.time_limit = None add_decision_rule_variables(model_data=m, config=config) add_decision_rule_constraints(model_data=m, config=config) @@ -3315,10 +3329,15 @@ def test_minimize_dr_norm(self): master_data.master_model = master master_data.master_model.const_efficiency_applied = False master_data.master_model.linear_efficiency_applied = False - results = minimize_dr_vars(model_data=master_data, config=config) - self.assertEqual(results.solver.termination_condition, TerminationCondition.optimal, - msg="Minimize dr norm did not solve to optimality.") + master_data.timing = Bunch() + with time_code(master_data.timing, "total", is_main_timer=True): + results = minimize_dr_vars(model_data=master_data, config=config) + self.assertEqual( + results.solver.termination_condition, + TerminationCondition.optimal, + msg="Minimize dr norm did not solve to optimality.", + ) @unittest.skipUnless(SolverFactory('baron').license_is_valid(), "Global NLP solver is not available and licensed.") From 017299f4e6b538a42a93cddf549832d27ac37123 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 12 Dec 2022 06:40:12 -0500 Subject: [PATCH 03/18] Copy solver object in PyROS solver argument parsing --- pyomo/contrib/pyros/pyros.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index ac6f2161118..7d890732e0e 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -10,6 +10,7 @@ # ___________________________________________________________________________ # pyros.py: Generalized Robust Cutting-Set Algorithm for Pyomo +from copy import deepcopy import logging from pyomo.common.collections import Bunch, ComponentSet from pyomo.common.config import ( @@ -79,13 +80,13 @@ class SolverResolvable(object): def __call__(self, obj): ''' if obj is a string, return the Solver object for that solver name - if obj is a Solver object, return the Solver + if obj is a Solver object, return a copy of the Solver if obj is a list, and each element of list is solver resolvable, return list of solvers ''' if isinstance(obj, str): return SolverFactory(obj.lower()) elif callable(getattr(obj, "solve", None)): - return obj + return deepcopy(obj) elif isinstance(obj, list): return [self(o) for o in obj] else: From 13414de1f3879460451dc9118b8b163a6f71138d Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 12 Dec 2022 06:42:03 -0500 Subject: [PATCH 04/18] Update version number, changelog --- pyomo/contrib/pyros/CHANGELOG.txt | 6 ++++++ pyomo/contrib/pyros/pyros.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/CHANGELOG.txt b/pyomo/contrib/pyros/CHANGELOG.txt index bce6e937bed..c4889a8df2d 100644 --- a/pyomo/contrib/pyros/CHANGELOG.txt +++ b/pyomo/contrib/pyros/CHANGELOG.txt @@ -2,6 +2,12 @@ PyROS CHANGELOG =============== +------------------------------------------------------------------------------- +PyROS 1.2.6 07 Dec 2022 +------------------------------------------------------------------------------- +- Add more judicious enforcement of PyROS time limit. + + ------------------------------------------------------------------------------- PyROS 1.2.5 06 Dec 2022 ------------------------------------------------------------------------------- diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 7d890732e0e..89cd352e55b 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -47,7 +47,7 @@ from pyomo.core.base import Constraint -__version__ = "1.2.5" +__version__ = "1.2.6" def NonNegIntOrMinusOne(obj): From 5281fd952eaf0e1ee3d95bb820a4c87e9db65830 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 19 Dec 2022 16:09:24 -0500 Subject: [PATCH 05/18] Add logger warning if subsolver time limit not adjusted --- pyomo/contrib/pyros/util.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index a2604dfdc2f..638d8034b29 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -106,6 +106,12 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): # ensure positive value assigned to avoid application error solver.options[options_key] = max(30, 30 + time_remaining) + else: + config.progress_logger.warning( + "Subproblem time limit setting not adjusted for " + f"subsolver of type {type(solver)}. PyROS time limit " + "may not be honored " + ) def a_logger(str_or_logger): From f806144d6c5d4c815c79228924652c189d79a638 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 19 Dec 2022 16:15:08 -0500 Subject: [PATCH 06/18] Format subsolver time limit warning message --- pyomo/contrib/pyros/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 638d8034b29..9d52b4e082b 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -109,8 +109,8 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): else: config.progress_logger.warning( "Subproblem time limit setting not adjusted for " - f"subsolver of type {type(solver)}. PyROS time limit " - "may not be honored " + f"subsolver of type:\n {type(solver)}.\n" + " PyROS time limit may not be honored " ) From d6205589e6e40de10f45f549e3ba62a3b27fa457 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 21 Dec 2022 19:34:42 -0500 Subject: [PATCH 07/18] Resolve issues with gams solver time limit adjustment --- pyomo/contrib/pyros/util.py | 68 ++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 9d52b4e082b..3ee8f89baa3 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -80,38 +80,44 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): adjusted. """ if config.time_limit is not None: - # determine name of option to adjust - if isinstance(solver, type(SolverFactory("baron"))): - options_key = "MaxTime" - elif isinstance(solver, type(SolverFactory("gams"))): - options_key = "reslim" - elif isinstance(solver, type(SolverFactory("ipopt"))): - options_key = "max_cpu_time" - else: - options_key = None - - # NOTE: - # (1) adjustment only supported for GAMS, BARON, and IPOPT - # interfaces. Generalize after interface to max time - # introduced - # (2) for IPOPT, and probably also BARON, the CPU time limit - # rather than the wallclock time limit, is adjusted, as - # no interface to wallclock limit available. - # For this reason, extra 30s is added to time remaining - # for subsolver time limit - if options_key is not None: - time_remaining = ( - config.time_limit - get_main_elapsed_time(timing_data_obj) - ) - - # ensure positive value assigned to avoid application error - solver.options[options_key] = max(30, 30 + time_remaining) + time_remaining = ( + config.time_limit - get_main_elapsed_time(timing_data_obj) + ) + if isinstance(solver, type(SolverFactory("gams"))): + # round up to nearest integer (as gams requires integral time + # limit) + reslim_str = f"option reslim={max(30, 30 + time_remaining)};" + if isinstance(solver.options["add_options"], list): + solver.options["add_options"].append(reslim_str) + else: + solver.options["add_options"] = [reslim_str] else: - config.progress_logger.warning( - "Subproblem time limit setting not adjusted for " - f"subsolver of type:\n {type(solver)}.\n" - " PyROS time limit may not be honored " - ) + # determine name of option to adjust + if isinstance(solver, type(SolverFactory("baron"))): + options_key = "MaxTime" + elif isinstance(solver, type(SolverFactory("ipopt"))): + options_key = "max_cpu_time" + else: + options_key = None + + # NOTE: + # (1) adjustment only supported for GAMS, BARON, and IPOPT + # interfaces. Generalize after interface to max time + # introduced + # (2) for IPOPT, and probably also BARON, the CPU time limit + # rather than the wallclock time limit, is adjusted, as + # no interface to wallclock limit available. + # For this reason, extra 30s is added to time remaining + # for subsolver time limit + if options_key is not None: + # ensure positive value assigned to avoid application error + solver.options[options_key] = max(30, 30 + time_remaining) + else: + config.progress_logger.warning( + "Subproblem time limit setting not adjusted for " + f"subsolver of type:\n {type(solver)}.\n" + " PyROS time limit may not be honored " + ) def a_logger(str_or_logger): From ceca30df445774bfee4bb8f04d03268b9d4197b1 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 21 Dec 2022 19:35:23 -0500 Subject: [PATCH 08/18] More comprehensive PyROS time limit unit tests --- pyomo/contrib/pyros/tests/test_grcs.py | 197 +++++++++++++++++++++++++ 1 file changed, 197 insertions(+) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 83cfe05ab10..3e65512fcb4 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -38,6 +38,86 @@ global_solver_args = dict() nlp_solver_args = dict() + +@SolverFactory.register("time_delay_solver") +class TimeDelaySolver(object): + """ + Solver which puts program to sleep for a specified + duration after having been invoked a specified number + of times. + """ + def __init__(self, calls_to_sleep, max_time, sub_solver): + self.max_time = max_time + self.calls_to_sleep = calls_to_sleep + self.sub_solver = sub_solver + + self.num_calls = 0 + + def solve(self, model, **kwargs): + """ + 'Solve' a model. + + Parameters + ---------- + model : ConcreteModel + Model of interest. + + Returns + ------- + results : SolverResults + Solver results. + """ + import time + from pyomo.opt import ( + SolverResults, + SolverStatus, + SolutionStatus, + TerminationCondition, + Solution, + ) + from pyomo.environ import ( + Objective, + value, + Var, + ) + + # ensure only one active objective + active_objs = [ + obj for obj in model.component_data_objects(Objective, active=True) + ] + assert len(active_objs) == 1 + + if self.num_calls < self.calls_to_sleep: + # invoke subsolver + results = self.sub_solver.solve(model, **kwargs) + self.num_calls += 1 + else: + # trigger time delay + self.calls_to_sleep = 0 + + time.sleep(self.max_time) + results = SolverResults() + + # generate solution (current model variable values) + sol = Solution() + sol.variable = { + var.name: {"Value": value(var)} + for var in model.component_data_objects(Var, active=True) + } + sol._cuid = False + sol.status = SolutionStatus.stoppedByLimit + results.solution.insert(sol) + + # set up results.solver + results.solver.time = self.max_time + results.solver.termination_condition = ( + TerminationCondition.maxTimeLimit + ) + results.solver.status = SolverStatus.warning + + return results + + # === util.py class testSelectiveClone(unittest.TestCase): ''' @@ -3464,6 +3544,123 @@ def test_terminate_with_time_limit(self): self.assertEqual(results.pyros_termination_condition, pyrosTerminationCondition.time_out, msg="Returned termination condition is not return time_out.") + @unittest.skipUnless( + SolverFactory('baron').license_is_valid(), + "Global NLP solver is not available and licensed." + ) + def test_separation_terminate_time_limit(self): + """ + Test PyROS time limit status returned in event + separation problem times out. + """ + m = ConcreteModel() + m.x1 = Var(initialize=0, bounds=(0, None)) + m.x2 = Var(initialize=0, bounds=(0, None)) + m.x3 = Var(initialize=0, bounds=(None, None)) + m.u = Param(initialize=1.125, mutable=True) + + m.con1 = Constraint(expr=m.x1 * m.u**(0.5) - m.x2 * m.u <= 2) + m.con2 = Constraint(expr=m.x1 ** 2 - m.x2 ** 2 * m.u == m.x3) + + m.obj = Objective(expr=(m.x1 - 4) ** 2 + (m.x2 - 1) ** 2) + + # Define the uncertainty set + interval = BoxSet(bounds=[(0.25, 2)]) + + # Instantiate the PyROS solver + pyros_solver = SolverFactory("pyros") + + # Define subsolvers utilized in the algorithm + local_subsolver = SolverFactory( + 'time_delay_solver', + calls_to_sleep=0, + sub_solver=SolverFactory("baron"), + max_time=1, + ) + global_subsolver = SolverFactory("baron") + + # Call the PyROS solver + results = pyros_solver.solve( + model=m, + first_stage_variables=[m.x1, m.x2], + second_stage_variables=[], + uncertain_params=[m.u], + uncertainty_set=interval, + local_solver=local_subsolver, + global_solver=global_subsolver, + objective_focus=ObjectiveType.worst_case, + solve_master_globally=True, + time_limit=1, + ) + + self.assertEqual( + results.pyros_termination_condition, + pyrosTerminationCondition.time_out, + msg="Returned termination condition is not return time_out.", + ) + + @unittest.skipUnless( + SolverFactory('gams').license_is_valid() + and SolverFactory('baron').license_is_valid(), + "Global NLP solver is not available and licensed." + ) + def test_gams_successful_time_limit(self): + """ + Test PyROS time limit status returned in event + separation problem times out. + """ + m = ConcreteModel() + m.x1 = Var(initialize=0, bounds=(0, None)) + m.x2 = Var(initialize=0, bounds=(0, None)) + m.x3 = Var(initialize=0, bounds=(None, None)) + m.u = Param(initialize=1.125, mutable=True) + + m.con1 = Constraint(expr=m.x1 * m.u**(0.5) - m.x2 * m.u <= 2) + m.con2 = Constraint(expr=m.x1 ** 2 - m.x2 ** 2 * m.u == m.x3) + + m.obj = Objective(expr=(m.x1 - 4) ** 2 + (m.x2 - 1) ** 2) + + # Define the uncertainty set + interval = BoxSet(bounds=[(0.25, 2)]) + + # Instantiate the PyROS solver + pyros_solver = SolverFactory("pyros") + + # Define subsolvers utilized in the algorithm + # two GAMS solvers, one of which has reslim set + # (should be overriden when invoked in PyROS) + local_subsolvers = [ + SolverFactory("gams:conopt"), + SolverFactory("gams:conopt"), + SolverFactory("ipopt"), + ] + local_subsolvers[0].options["add_options"] = ["option reslim=100;"] + global_subsolver = SolverFactory("baron") + + # Call the PyROS solver + for idx, opt in enumerate(local_subsolvers): + results = pyros_solver.solve( + model=m, + first_stage_variables=[m.x1, m.x2], + second_stage_variables=[], + uncertain_params=[m.u], + uncertainty_set=interval, + local_solver=opt, + global_solver=global_subsolver, + objective_focus=ObjectiveType.worst_case, + solve_master_globally=True, + time_limit=100, + ) + + self.assertEqual( + results.pyros_termination_condition, + pyrosTerminationCondition.robust_optimal, + msg=( + f"Returned termination condition with local " + "subsolver {idx + 1} of 2 is not robust_optimal." + ), + ) + @unittest.skipUnless(SolverFactory('baron').license_is_valid(), "Global NLP solver is not available and licensed.") def test_terminate_with_application_error(self): From a56c476e0928432793697c8cc72a779c7a79cedd Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 22 Dec 2022 06:44:09 -0500 Subject: [PATCH 09/18] Add `options` attribute to test time delay solver --- pyomo/contrib/pyros/tests/test_grcs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 3e65512fcb4..12135b508f9 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -52,6 +52,7 @@ def __init__(self, calls_to_sleep, max_time, sub_solver): self.sub_solver = sub_solver self.num_calls = 0 + self.options = Bunch() def solve(self, model, **kwargs): """ From 121518fe98627b46509122c299836fff5a59129f Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 22 Dec 2022 06:55:03 -0500 Subject: [PATCH 10/18] Dont clone subsolvers, add time limit reversion routine --- pyomo/contrib/pyros/master_problem_methods.py | 40 ++++++- pyomo/contrib/pyros/pyros.py | 3 +- .../pyros/separation_problem_methods.py | 19 +++- pyomo/contrib/pyros/util.py | 103 +++++++++++++++--- 4 files changed, 144 insertions(+), 21 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index c89a9f65034..cea75e7bbfc 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -14,6 +14,7 @@ pyrosTerminationCondition, process_termination_condition_master_problem, adjust_solver_time_settings, + revert_solver_max_time_adjustment, get_main_elapsed_time, output_logger) from pyomo.contrib.pyros.solve_data import (MasterProblemData, @@ -244,7 +245,11 @@ def solve_master_feasibility_problem(model_data, config): else: solver = config.local_solver - adjust_solver_time_settings(model_data.timing, solver, config) + orig_setting, custom_setting_present = adjust_solver_time_settings( + model_data.timing, + solver, + config, + ) try: results = solver.solve(model, tee=config.tee, load_solutions=False) except ApplicationError: @@ -257,6 +262,13 @@ def solve_master_feasibility_problem(model_data, config): f"{model_data.iteration}" ) raise + finally: + revert_solver_max_time_adjustment( + solver, + orig_setting, + custom_setting_present, + config, + ) feasible_terminations = { tc.optimal, tc.locallyOptimal, tc.globallyOptimal, tc.feasible @@ -404,7 +416,11 @@ def minimize_dr_vars(model_data, config): # === Solve the polishing model timer = TicTocTimer() - adjust_solver_time_settings(model_data.timing, solver, config) + orig_setting, custom_setting_present = adjust_solver_time_settings( + model_data.timing, + solver, + config, + ) timer.tic(msg=None) try: results = solver.solve( @@ -425,6 +441,13 @@ def minimize_dr_vars(model_data, config): TIC_TOC_SOLVE_TIME_ATTR, timer.toc(msg=None), ) + finally: + revert_solver_max_time_adjustment( + solver, + orig_setting, + custom_setting_present, + config, + ) # === Process solution by termination condition acceptable = { @@ -569,7 +592,11 @@ def solver_call_master(model_data, config, solver, solve_data): timer = TicTocTimer() for opt in backup_solvers: - adjust_solver_time_settings(model_data.timing, opt, config) + orig_setting, custom_setting_present = adjust_solver_time_settings( + model_data.timing, + opt, + config, + ) timer.tic(msg=None) try: results = opt.solve( @@ -593,6 +620,13 @@ def solver_call_master(model_data, config, solver, solve_data): TIC_TOC_SOLVE_TIME_ATTR, timer.toc(msg=None), ) + finally: + revert_solver_max_time_adjustment( + solver, + orig_setting, + custom_setting_present, + config, + ) optimal_termination = check_optimal_termination(results) infeasible = results.solver.termination_condition == tc.infeasible diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 89cd352e55b..bf5a5e4c775 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -10,7 +10,6 @@ # ___________________________________________________________________________ # pyros.py: Generalized Robust Cutting-Set Algorithm for Pyomo -from copy import deepcopy import logging from pyomo.common.collections import Bunch, ComponentSet from pyomo.common.config import ( @@ -86,7 +85,7 @@ def __call__(self, obj): if isinstance(obj, str): return SolverFactory(obj.lower()) elif callable(getattr(obj, "solve", None)): - return deepcopy(obj) + return obj elif isinstance(obj, list): return [self(o) for o in obj] else: diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index efaf374345f..a4b0885d15a 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -21,7 +21,11 @@ from pyomo.common.errors import ApplicationError from pyomo.contrib.pyros.util import ABS_CON_CHECK_FEAS_TOL from pyomo.common.timing import TicTocTimer -from pyomo.contrib.pyros.util import TIC_TOC_SOLVE_TIME_ATTR, adjust_solver_time_settings +from pyomo.contrib.pyros.util import ( + TIC_TOC_SOLVE_TIME_ATTR, + adjust_solver_time_settings, + revert_solver_max_time_adjustment, +) import os from copy import deepcopy @@ -564,7 +568,11 @@ def solver_call_separation(model_data, config, solver, solve_data, is_global): timer = TicTocTimer() for opt in backup_solvers: - adjust_solver_time_settings(model_data.timing, opt, config) + orig_setting, custom_setting_present = adjust_solver_time_settings( + model_data.timing, + opt, + config, + ) timer.tic(msg=None) try: results = opt.solve( @@ -588,6 +596,13 @@ def solver_call_separation(model_data, config, solver, solve_data, is_global): TIC_TOC_SOLVE_TIME_ATTR, timer.toc(msg=None), ) + finally: + revert_solver_max_time_adjustment( + opt, + orig_setting, + custom_setting_present, + config, + ) # record termination condition for this particular solver solver_status_dict[str(opt)] = results.solver.termination_condition diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 3ee8f89baa3..2b5280ab31e 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -73,19 +73,49 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): """ Adjust solver max time based on current PyROS elapsed time. + Parameters + ---------- + timing_data_obj : Bunch + PyROS timekeeper. + solver : solver type + Solver for which to adjust the max time setting. + config : ConfigDict + PyROS solver config. + Returns ------- - new_solver : solver type - Clone of solver provided, with time limit setting - adjusted. + original_max_time_setting : float or None + If IPOPT or BARON is used, a float is returned. + If GAMS is used, the ``options.add_options`` attribute + is returned. + Otherwise, None is returned. + custom_setting_present : bool, optional + If IPOPT or BARON is used, True if the max time is + specified, False otherwise. + If GAMS is used, True if the attribute ``options.add_options`` + is not None, False otherwise. + + Note + ---- + (1) Adjustment only supported for GAMS, BARON, and IPOPT + interfaces. This routine can be generalized to other solvers + after a generic interface to the time limit setting + is introduced. + (2) For IPOPT, and probably also BARON, the CPU time limit + rather than the wallclock time limit, is adjusted, as + no interface to wallclock limit available. + For this reason, extra 30s is added to time remaining + for subsolver time limit. """ if config.time_limit is not None: time_remaining = ( config.time_limit - get_main_elapsed_time(timing_data_obj) ) if isinstance(solver, type(SolverFactory("gams"))): - # round up to nearest integer (as gams requires integral time - # limit) + original_max_time_setting = solver.options["add_options"] + custom_setting_present = "add_options" in solver.options + + # adjust GAMS solver time reslim_str = f"option reslim={max(30, 30 + time_remaining)};" if isinstance(solver.options["add_options"], list): solver.options["add_options"].append(reslim_str) @@ -100,25 +130,70 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): else: options_key = None - # NOTE: - # (1) adjustment only supported for GAMS, BARON, and IPOPT - # interfaces. Generalize after interface to max time - # introduced - # (2) for IPOPT, and probably also BARON, the CPU time limit - # rather than the wallclock time limit, is adjusted, as - # no interface to wallclock limit available. - # For this reason, extra 30s is added to time remaining - # for subsolver time limit if options_key is not None: + custom_setting_present = options_key in solver.options + original_max_time_setting = solver.options[options_key] + # ensure positive value assigned to avoid application error solver.options[options_key] = max(30, 30 + time_remaining) else: + custom_setting_present = False + original_max_time_setting = None config.progress_logger.warning( "Subproblem time limit setting not adjusted for " f"subsolver of type:\n {type(solver)}.\n" " PyROS time limit may not be honored " ) + return original_max_time_setting, custom_setting_present + else: + return None, None + + +def revert_solver_max_time_adjustment( + solver, + original_max_time_setting, + custom_setting_present, + config, + ): + """ + Revert solver options to its state prior to a + time limit adjustment performed via + the routine `adjust_solver_time_settings`. + + Parameters + ---------- + solver : solver type + Solver of interest. + original_max_time_setting : float, list, or None + Original solver settings. Type depends on the + solver type. + custom_setting_present : bool + Was the max time, or other custom solver settings, + specified prior to the adjustment? + """ + if config.time_limit is not None: + # determine name of option to adjust + if isinstance(solver, type(SolverFactory("gams"))): + options_key = "add_options" + if isinstance(solver, type(SolverFactory("baron"))): + options_key = "MaxTime" + elif isinstance(solver, type(SolverFactory("ipopt"))): + options_key = "max_cpu_time" + else: + options_key = None + + if options_key is not None: + if custom_setting_present: + # restore original setting + solver.options[options_key] = original_max_time_setting + else: + # remove the max time specification introduced. + # Both lines are needed here to completely remove the option + # from access through getattr and dictionary reference + delattr(solver.options, options_key) + del solver.options[options_key] + def a_logger(str_or_logger): """Returns a logger when passed either a logger name or logger object.""" From 45cafe01267f9861b7ed0b210fa52b58bdc11f7b Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 22 Dec 2022 08:17:46 -0500 Subject: [PATCH 11/18] Resolve error with solver option key --- pyomo/contrib/pyros/util.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 2b5280ab31e..b3eeb8a2691 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -176,7 +176,7 @@ def revert_solver_max_time_adjustment( # determine name of option to adjust if isinstance(solver, type(SolverFactory("gams"))): options_key = "add_options" - if isinstance(solver, type(SolverFactory("baron"))): + elif isinstance(solver, type(SolverFactory("baron"))): options_key = "MaxTime" elif isinstance(solver, type(SolverFactory("ipopt"))): options_key = "max_cpu_time" @@ -187,6 +187,12 @@ def revert_solver_max_time_adjustment( if custom_setting_present: # restore original setting solver.options[options_key] = original_max_time_setting + + # if GAMS solver used, need to remove the last entry + # of 'add_options', which contains the max time setting + # added by PyROS + if isinstance(solver, type(SolverFactory("gams"))): + solver.options[options_key].pop() else: # remove the max time specification introduced. # Both lines are needed here to completely remove the option From 5ed6fc20636ace929a1725c7ab6125688c4f207d Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 22 Dec 2022 08:18:11 -0500 Subject: [PATCH 12/18] More comprehensive solver settings check tests --- pyomo/contrib/pyros/tests/test_grcs.py | 132 ++++++++++++++++++++++--- 1 file changed, 117 insertions(+), 15 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 12135b508f9..4fd9a9eaa26 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -3529,21 +3529,42 @@ def test_terminate_with_time_limit(self): global_subsolver = SolverFactory("baron") # Call the PyROS solver - results = pyros_solver.solve(model=m, - first_stage_variables=[m.x1, m.x2], - second_stage_variables=[], - uncertain_params=[m.u], - uncertainty_set=interval, - local_solver=local_subsolver, - global_solver=global_subsolver, - options={ - "objective_focus": ObjectiveType.worst_case, - "solve_master_globally": True, - "time_limit": 0.001 - }) + results = pyros_solver.solve( + model=m, + first_stage_variables=[m.x1, m.x2], + second_stage_variables=[], + uncertain_params=[m.u], + uncertainty_set=interval, + local_solver=local_subsolver, + global_solver=global_subsolver, + objective_focus=ObjectiveType.worst_case, + solve_master_globally=True, + time_limit=0.001, + ) + + # validate termination condition + self.assertEqual( + results.pyros_termination_condition, + pyrosTerminationCondition.time_out, + msg="Returned termination condition is not return time_out.", + ) - self.assertEqual(results.pyros_termination_condition, pyrosTerminationCondition.time_out, - msg="Returned termination condition is not return time_out.") + # verify subsolver options are unchanged + subsolvers = [local_subsolver, global_subsolver] + for slvr, desc in zip(subsolvers, ["Local", "Global"]): + self.assertEqual( + len(list(slvr.options.keys())), + 0, + msg=f"{desc} subsolver options were changed by PyROS", + ) + self.assertIs( + getattr(slvr.options, "MaxTime", None), + None, + msg=( + f"{desc} subsolver (BARON) MaxTime setting was added " + "by PyROS, but not reverted" + ), + ) @unittest.skipUnless( SolverFactory('baron').license_is_valid(), @@ -3629,7 +3650,7 @@ def test_gams_successful_time_limit(self): # Define subsolvers utilized in the algorithm # two GAMS solvers, one of which has reslim set - # (should be overriden when invoked in PyROS) + # (overriden when invoked in PyROS) local_subsolvers = [ SolverFactory("gams:conopt"), SolverFactory("gams:conopt"), @@ -3637,6 +3658,7 @@ def test_gams_successful_time_limit(self): ] local_subsolvers[0].options["add_options"] = ["option reslim=100;"] global_subsolver = SolverFactory("baron") + global_subsolver.options["MaxTime"] = 300 # Call the PyROS solver for idx, opt in enumerate(local_subsolvers): @@ -3662,6 +3684,59 @@ def test_gams_successful_time_limit(self): ), ) + # check first local subsolver settings + # remain unchanged after PyROS exit + self.assertEqual( + len(list(local_subsolvers[0].options["add_options"])), + 1, + msg=( + f"Local subsolver {local_subsolvers[0]} options 'add_options'" + "were changed by PyROS" + ), + ) + self.assertEqual( + local_subsolvers[0].options["add_options"][0], + "option reslim=100;", + msg=( + f"Local subsolver {local_subsolvers[0]} setting " + "'add_options' was modified " + "by PyROS, but changes were not properly undone" + ), + ) + + # check global subsolver settings unchanged + self.assertEqual( + len(list(global_subsolver.options.keys())), + 1, + msg=( + f"Global subsolver {global_subsolver} options " + "were changed by PyROS" + ), + ) + self.assertEqual( + global_subsolver.options["MaxTime"], + 300, + msg=( + f"Global subsolver {global_subsolver} setting " + "'MaxTime' was modified " + "by PyROS, but changes were not properly undone" + ), + ) + + # check other local subsolvers remain unchanged + for slvr, key in zip(local_subsolvers[1:], ["add_options", "max_cpu_time"]): + # no custom options were added to the `options` + # attribute of the optimizer, so any attribute + # of `options` should be `None` + self.assertIs( + getattr(slvr.options, key, None), + None, + msg=( + f"Local subsolver {slvr} setting '{key}' was added " + "by PyROS, but not reverted" + ), + ) + @unittest.skipUnless(SolverFactory('baron').license_is_valid(), "Global NLP solver is not available and licensed.") def test_terminate_with_application_error(self): @@ -3694,7 +3769,34 @@ def test_terminate_with_application_error(self): local_solver=solver, global_solver=baron, objective_focus=ObjectiveType.nominal, + time_limit=1000, + ) + + # check solver settings are unchanged + self.assertEqual( + len(list(solver.options.keys())), + 1, + msg=( + f"Local subsolver {solver} options " + "were changed by PyROS" + ), + ) + self.assertEqual( + solver.options["halt_on_ampl_error"], + "yes", + msg=( + f"Local subsolver {solver} option " + "'halt_on_ampl_error' was changed by PyROS" ) + ) + self.assertEqual( + len(list(baron.options.keys())), + 0, + msg=( + f"Global subsolver {baron} options " + "were changed by PyROS" + ), + ) @unittest.skipUnless(SolverFactory('baron').license_is_valid(), "Global NLP solver is not available and licensed.") From d5b61fbed4c9b937adb83a2720cc582fb0c4f40e Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 22 Dec 2022 08:27:26 -0500 Subject: [PATCH 13/18] Tweak docstrings, add assertion --- pyomo/contrib/pyros/util.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index b3eeb8a2691..45ec4b88eba 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -71,7 +71,8 @@ def get_main_elapsed_time(timing_data_obj): def adjust_solver_time_settings(timing_data_obj, solver, config): """ - Adjust solver max time based on current PyROS elapsed time. + Adjust solver max time setting based on current PyROS elapsed + time. Parameters ---------- @@ -87,13 +88,14 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): original_max_time_setting : float or None If IPOPT or BARON is used, a float is returned. If GAMS is used, the ``options.add_options`` attribute - is returned. + of ``solver`` is returned. Otherwise, None is returned. - custom_setting_present : bool, optional + custom_setting_present : bool or None If IPOPT or BARON is used, True if the max time is specified, False otherwise. If GAMS is used, True if the attribute ``options.add_options`` is not None, False otherwise. + If ``config.time_limit`` is None, then None is returned. Note ---- @@ -157,7 +159,7 @@ def revert_solver_max_time_adjustment( config, ): """ - Revert solver options to its state prior to a + Revert solver `options` attribute to its state prior to a time limit adjustment performed via the routine `adjust_solver_time_settings`. @@ -168,11 +170,16 @@ def revert_solver_max_time_adjustment( original_max_time_setting : float, list, or None Original solver settings. Type depends on the solver type. - custom_setting_present : bool + custom_setting_present : bool or None Was the max time, or other custom solver settings, specified prior to the adjustment? + Can be None if ``config.time_limit`` is None. + config : ConfigDict + PyROS solver config. """ if config.time_limit is not None: + assert isinstance(custom_setting_present, bool) + # determine name of option to adjust if isinstance(solver, type(SolverFactory("gams"))): options_key = "add_options" From 8173e0053b1aa0024ced5661fd38d4291d10f171 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 22 Dec 2022 09:05:27 -0500 Subject: [PATCH 14/18] Clarify solver options attribute deletion --- pyomo/contrib/pyros/util.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 45ec4b88eba..9500a2c02f8 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -202,10 +202,11 @@ def revert_solver_max_time_adjustment( solver.options[options_key].pop() else: # remove the max time specification introduced. - # Both lines are needed here to completely remove the option - # from access through getattr and dictionary reference + # All lines are needed here to completely remove the option + # from access through getattr and dictionary reference. delattr(solver.options, options_key) - del solver.options[options_key] + if options_key in solver.options.keys(): + del solver.options[options_key] def a_logger(str_or_logger): From bad09982a04afed173ff618527c433e86252ca52 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 22 Dec 2022 09:07:32 -0500 Subject: [PATCH 15/18] Fixes to `TimeDelaySolver` for unit tests --- pyomo/contrib/pyros/tests/test_grcs.py | 43 +++++++++++++++++--------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 4fd9a9eaa26..47069175c3b 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -17,6 +17,7 @@ from pyomo.contrib.pyros.util import get_vars_from_component from pyomo.contrib.pyros.util import identify_objective_functions from pyomo.common.collections import Bunch +import time from pyomo.contrib.pyros.util import time_code from pyomo.core.expr import current as EXPR from pyomo.contrib.pyros.uncertainty_sets import * @@ -27,6 +28,18 @@ from pyomo.common.dependencies import scipy as sp, scipy_available from pyomo.environ import maximize as pyo_max from pyomo.common.errors import ApplicationError +from pyomo.opt import ( + SolverResults, + SolverStatus, + SolutionStatus, + TerminationCondition, + Solution, +) +from pyomo.environ import ( + Objective, + value, + Var, +) if not (numpy_available and scipy_available): @@ -54,6 +67,18 @@ def __init__(self, calls_to_sleep, max_time, sub_solver): self.num_calls = 0 self.options = Bunch() + def available(self): + return True + + def license_is_valid(self): + return True + + def __enter__(self): + return self + + def __exit__(self, et, ev, tb): + pass + def solve(self, model, **kwargs): """ 'Solve' a model. @@ -68,19 +93,6 @@ def solve(self, model, **kwargs): results : SolverResults Solver results. """ - import time - from pyomo.opt import ( - SolverResults, - SolverStatus, - SolutionStatus, - TerminationCondition, - Solution, - ) - from pyomo.environ import ( - Objective, - value, - Var, - ) # ensure only one active objective active_objs = [ @@ -94,11 +106,12 @@ def solve(self, model, **kwargs): self.num_calls += 1 else: # trigger time delay - self.calls_to_sleep = 0 - time.sleep(self.max_time) results = SolverResults() + # reset number of calls + self.num_calls = 0 + # generate solution (current model variable values) sol = Solution() sol.variable = { From 7beb29cabff0f887d50d90035d7b73fd172ce5c6 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 23 Dec 2022 08:36:11 -0500 Subject: [PATCH 16/18] Instantiate `TimeDelaySolver` directly for tests --- pyomo/contrib/pyros/tests/test_grcs.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 47069175c3b..550e32d2b6e 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -52,7 +52,7 @@ nlp_solver_args = dict() -@SolverFactory.register("time_delay_solver") +# @SolverFactory.register("time_delay_solver") class TimeDelaySolver(object): """ Solver which puts program to sleep for a specified @@ -3606,8 +3606,7 @@ def test_separation_terminate_time_limit(self): pyros_solver = SolverFactory("pyros") # Define subsolvers utilized in the algorithm - local_subsolver = SolverFactory( - 'time_delay_solver', + local_subsolver = TimeDelaySolver( calls_to_sleep=0, sub_solver=SolverFactory("baron"), max_time=1, From ff34d7a9f4c62d52eaf20bd1f49812f09d33419f Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 13 Jan 2023 14:05:00 -0500 Subject: [PATCH 17/18] Use more efficient SolverFactory class refs --- pyomo/contrib/pyros/util.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 9500a2c02f8..d118f7696d6 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -113,7 +113,7 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): time_remaining = ( config.time_limit - get_main_elapsed_time(timing_data_obj) ) - if isinstance(solver, type(SolverFactory("gams"))): + if isinstance(solver, SolverFactory.get_class("gams")): original_max_time_setting = solver.options["add_options"] custom_setting_present = "add_options" in solver.options @@ -125,9 +125,9 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): solver.options["add_options"] = [reslim_str] else: # determine name of option to adjust - if isinstance(solver, type(SolverFactory("baron"))): + if isinstance(solver, SolverFactory.get_class("baron")): options_key = "MaxTime" - elif isinstance(solver, type(SolverFactory("ipopt"))): + elif isinstance(solver, SolverFactory.get_class("ipopt")): options_key = "max_cpu_time" else: options_key = None @@ -181,11 +181,11 @@ def revert_solver_max_time_adjustment( assert isinstance(custom_setting_present, bool) # determine name of option to adjust - if isinstance(solver, type(SolverFactory("gams"))): + if isinstance(solver, SolverFactory.get_class("gams")): options_key = "add_options" - elif isinstance(solver, type(SolverFactory("baron"))): + elif isinstance(solver, SolverFactory.get_class("baron")): options_key = "MaxTime" - elif isinstance(solver, type(SolverFactory("ipopt"))): + elif isinstance(solver, SolverFactory.get_class("ipopt")): options_key = "max_cpu_time" else: options_key = None @@ -198,7 +198,7 @@ def revert_solver_max_time_adjustment( # if GAMS solver used, need to remove the last entry # of 'add_options', which contains the max time setting # added by PyROS - if isinstance(solver, type(SolverFactory("gams"))): + if isinstance(solver, SolverFactory.get_class("gams")): solver.options[options_key].pop() else: # remove the max time specification introduced. From 2d97f2765038cfa7b998f1bf069720fb379399d7 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 13 Jan 2023 14:35:16 -0500 Subject: [PATCH 18/18] Add comment on subsolver time limit adjustment --- pyomo/contrib/pyros/util.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index d118f7696d6..095421c4286 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -108,6 +108,10 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): no interface to wallclock limit available. For this reason, extra 30s is added to time remaining for subsolver time limit. + (The extra 30s is large enough to ensure solver + elapsed time is not beneath elapsed time - user time limit, + but not so large as to overshoot the user-specified time limit + by an inordinate margin.) """ if config.time_limit is not None: time_remaining = (