Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add efficiency for handling PyROS separation problem sub-solver errors #3441

Merged
merged 13 commits into from
Feb 12, 2025
Merged
2 changes: 1 addition & 1 deletion doc/OnlineDocs/explanation/solvers/pyros.rst
Original file line number Diff line number Diff line change
@@ -958,7 +958,7 @@ Observe that the log contains the following information:
:linenos:

==============================================================================
PyROS: The Pyomo Robust Optimization Solver, v1.3.2.
PyROS: The Pyomo Robust Optimization Solver, v1.3.3.
Pyomo version: 6.9.0
Commit hash: unknown
Invoked at UTC 2024-11-01T00:00:00.000000
10 changes: 10 additions & 0 deletions pyomo/contrib/pyros/CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -2,6 +2,16 @@
PyROS CHANGELOG
===============

-------------------------------------------------------------------------------
PyROS 1.3.3 03 Dec 2024
-------------------------------------------------------------------------------
- Add efficiency for handling PyROS separation problem sub-solver errors
- Add logger warnings to report sub-solver errors and inform that PyROS
will continue to solve if a violation is found
- Add unit tests for new sub-solver error handling for continuous
and discrete uncertainty sets


-------------------------------------------------------------------------------
PyROS 1.3.2 29 Nov 2024
-------------------------------------------------------------------------------
2 changes: 1 addition & 1 deletion pyomo/contrib/pyros/pyros.py
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@
)


__version__ = "1.3.2"
__version__ = "1.3.3"


default_pyros_solver_logger = setup_pyros_logger()
7 changes: 7 additions & 0 deletions pyomo/contrib/pyros/pyros_algorithm_methods.py
Original file line number Diff line number Diff line change
@@ -273,6 +273,13 @@ def ROSolver_iterative_solve(model_data):

# terminate on time limit
if separation_results.time_out or separation_results.subsolver_error:
# report PyROS failure to find violated constraint for subsolver error
if separation_results.subsolver_error:
config.progress_logger.warning(
"PyROS failed to find a constraint violation and "
"will terminate with sub-solver error."
)

pyros_term_cond = (
pyrosTerminationCondition.time_out
if separation_results.time_out
51 changes: 40 additions & 11 deletions pyomo/contrib/pyros/separation_problem_methods.py
Original file line number Diff line number Diff line change
@@ -423,8 +423,13 @@ def get_worst_discrete_separation_solution(
# violation of specified second-stage inequality
# constraint by separation
# problem solutions for all scenarios
# scenarios with subsolver errors are replaced with nan
violations_of_ss_ineq_con = [
solve_call_res.scaled_violations[ss_ineq_con]
(
solve_call_res.scaled_violations[ss_ineq_con]
if not solve_call_res.subsolver_error
else np.nan
)
for solve_call_res in discrete_solve_results.solver_call_results.values()
]

@@ -433,9 +438,9 @@ def get_worst_discrete_separation_solution(
# determine separation solution for which scaled violation of this
# second-stage inequality constraint is the worst
worst_case_res = discrete_solve_results.solver_call_results[
list_of_scenario_idxs[np.argmax(violations_of_ss_ineq_con)]
list_of_scenario_idxs[np.nanargmax(violations_of_ss_ineq_con)]
]
worst_case_violation = np.max(violations_of_ss_ineq_con)
worst_case_violation = np.nanmax(violations_of_ss_ineq_con)
assert worst_case_violation in worst_case_res.scaled_violations.values()

# evaluate violations for specified second-stage inequality constraints
@@ -463,6 +468,13 @@ def get_worst_discrete_separation_solution(
else:
results_list = []

# check if there were any failed scenarios for subsolver_error
# if there are failed scenarios, subsolver error triggers for all ineq
if any(np.isnan(violations_of_ss_ineq_con)):
subsolver_error_flag = True
else:
subsolver_error_flag = False

return SeparationSolveCallResults(
solved_globally=worst_case_res.solved_globally,
results_list=results_list,
@@ -471,7 +483,7 @@ def get_worst_discrete_separation_solution(
variable_values=worst_case_res.variable_values,
found_violation=(worst_case_violation > config.robust_feasibility_tolerance),
time_out=False,
subsolver_error=False,
subsolver_error=subsolver_error_flag,
discrete_set_scenario_index=worst_case_res.discrete_set_scenario_index,
)

@@ -642,9 +654,7 @@ def perform_separation_loop(separation_data, master_data, solve_globally):

priority_group_solve_call_results[ss_ineq_con] = solve_call_results

termination_not_ok = (
solve_call_results.time_out or solve_call_results.subsolver_error
)
termination_not_ok = solve_call_results.time_out
if termination_not_ok:
all_solve_call_results.update(priority_group_solve_call_results)
return SeparationLoopResults(
@@ -653,6 +663,14 @@ def perform_separation_loop(separation_data, master_data, solve_globally):
worst_case_ss_ineq_con=None,
)

# provide message that PyROS will attempt to find a violation and move
# to the next iteration even after subsolver error
if solve_call_results.subsolver_error:
config.progress_logger.warning(
"PyROS is attempting to recover and will continue to "
"the next iteration if a constraint violation is found."
)

all_solve_call_results.update(priority_group_solve_call_results)

# there may be multiple separation problem solutions
@@ -1139,13 +1157,19 @@ def discrete_solve(
]

solve_call_results_dict = {}
for scenario_idx in scenario_idxs_to_separate:
for idx, scenario_idx in enumerate(scenario_idxs_to_separate):
# fix uncertain parameters to scenario value
# hence, no need to activate uncertainty set constraints
scenario = config.uncertainty_set.scenarios[scenario_idx]
for param, coord_val in zip(uncertain_param_vars, scenario):
param.fix(coord_val)

# debug statement for solving square problem for each scenario
config.progress_logger.debug(
f"Attempting to solve square problem for discrete scenario {scenario}"
f", {idx + 1} of {len(scenario_idxs_to_separate)} total"
)

# obtain separation problem solution
solve_call_results = solver_call_separation(
separation_data=separation_data,
@@ -1158,12 +1182,17 @@ def discrete_solve(
solve_call_results_dict[scenario_idx] = solve_call_results

# halt at first encounter of unacceptable termination
termination_not_ok = (
solve_call_results.subsolver_error or solve_call_results.time_out
)
termination_not_ok = solve_call_results.time_out
if termination_not_ok:
break

# report any subsolver errors, but continue
if solve_call_results.subsolver_error:
config.progress_logger.warning(
f"All solvers failed to solve discrete scenario {scenario_idx}: "
f"{scenario}"
)

return DiscreteSeparationSolveCallResults(
solved_globally=solve_globally,
solver_call_results=solve_call_results_dict,
17 changes: 10 additions & 7 deletions pyomo/contrib/pyros/solve_data.py
Original file line number Diff line number Diff line change
@@ -277,11 +277,11 @@ def time_out(self):
@property
def subsolver_error(self):
"""
bool : True if there is a subsolver error status for at least
one of the the ``SeparationSolveCallResults`` objects listed
bool : True if there is a subsolver error status for all
of the ``SeparationSolveCallResults`` objects listed
in `self`, False otherwise.
"""
return any(res.subsolver_error for res in self.solver_call_results.values())
return all(res.subsolver_error for res in self.solver_call_results.values())


class SeparationLoopResults:
@@ -430,11 +430,14 @@ def subsolver_error(self):
"""
bool : Return True if subsolver error reported for
at least one ``SeparationSolveCallResults`` stored in
`self`, False otherwise.
`self` and no violations are found, False otherwise.
"""
return any(
solver_call_res.subsolver_error
for solver_call_res in self.solver_call_results.values()
return (
any(
solver_call_res.subsolver_error
for solver_call_res in self.solver_call_results.values()
)
and not self.found_violation
)

@property
Loading
Loading