Skip to content
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

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

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion doc/OnlineDocs/explanation/solvers/pyros.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions pyomo/contrib/pyros/CHANGELOG.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion pyomo/contrib/pyros/pyros.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
)


__version__ = "1.3.2"
__version__ = "1.3.3"


default_pyros_solver_logger = setup_pyros_logger()
Expand Down
7 changes: 7 additions & 0 deletions pyomo/contrib/pyros/pyros_algorithm_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 40 additions & 11 deletions pyomo/contrib/pyros/separation_problem_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
]

Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
)

Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
17 changes: 10 additions & 7 deletions pyomo/contrib/pyros/solve_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 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:
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading