Skip to content

Commit e316806

Browse files
authored
Merge pull request #3441 from jas-yao/pyros-sp-efficiency
Add efficiency for handling PyROS separation problem sub-solver errors
2 parents e43c075 + dd69a9c commit e316806

File tree

7 files changed

+287
-20
lines changed

7 files changed

+287
-20
lines changed

doc/OnlineDocs/explanation/solvers/pyros.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -958,7 +958,7 @@ Observe that the log contains the following information:
958958
:linenos:
959959
960960
==============================================================================
961-
PyROS: The Pyomo Robust Optimization Solver, v1.3.2.
961+
PyROS: The Pyomo Robust Optimization Solver, v1.3.3.
962962
Pyomo version: 6.9.0
963963
Commit hash: unknown
964964
Invoked at UTC 2024-11-01T00:00:00.000000

pyomo/contrib/pyros/CHANGELOG.txt

+10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22
PyROS CHANGELOG
33
===============
44

5+
-------------------------------------------------------------------------------
6+
PyROS 1.3.3 03 Dec 2024
7+
-------------------------------------------------------------------------------
8+
- Add efficiency for handling PyROS separation problem sub-solver errors
9+
- Add logger warnings to report sub-solver errors and inform that PyROS
10+
will continue to solve if a violation is found
11+
- Add unit tests for new sub-solver error handling for continuous
12+
and discrete uncertainty sets
13+
14+
515
-------------------------------------------------------------------------------
616
PyROS 1.3.2 29 Nov 2024
717
-------------------------------------------------------------------------------

pyomo/contrib/pyros/pyros.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
)
3434

3535

36-
__version__ = "1.3.2"
36+
__version__ = "1.3.3"
3737

3838

3939
default_pyros_solver_logger = setup_pyros_logger()

pyomo/contrib/pyros/pyros_algorithm_methods.py

+7
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,13 @@ def ROSolver_iterative_solve(model_data):
273273

274274
# terminate on time limit
275275
if separation_results.time_out or separation_results.subsolver_error:
276+
# report PyROS failure to find violated constraint for subsolver error
277+
if separation_results.subsolver_error:
278+
config.progress_logger.warning(
279+
"PyROS failed to find a constraint violation and "
280+
"will terminate with sub-solver error."
281+
)
282+
276283
pyros_term_cond = (
277284
pyrosTerminationCondition.time_out
278285
if separation_results.time_out

pyomo/contrib/pyros/separation_problem_methods.py

+40-11
Original file line numberDiff line numberDiff line change
@@ -423,8 +423,13 @@ def get_worst_discrete_separation_solution(
423423
# violation of specified second-stage inequality
424424
# constraint by separation
425425
# problem solutions for all scenarios
426+
# scenarios with subsolver errors are replaced with nan
426427
violations_of_ss_ineq_con = [
427-
solve_call_res.scaled_violations[ss_ineq_con]
428+
(
429+
solve_call_res.scaled_violations[ss_ineq_con]
430+
if not solve_call_res.subsolver_error
431+
else np.nan
432+
)
428433
for solve_call_res in discrete_solve_results.solver_call_results.values()
429434
]
430435

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

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

471+
# check if there were any failed scenarios for subsolver_error
472+
# if there are failed scenarios, subsolver error triggers for all ineq
473+
if any(np.isnan(violations_of_ss_ineq_con)):
474+
subsolver_error_flag = True
475+
else:
476+
subsolver_error_flag = False
477+
466478
return SeparationSolveCallResults(
467479
solved_globally=worst_case_res.solved_globally,
468480
results_list=results_list,
@@ -471,7 +483,7 @@ def get_worst_discrete_separation_solution(
471483
variable_values=worst_case_res.variable_values,
472484
found_violation=(worst_case_violation > config.robust_feasibility_tolerance),
473485
time_out=False,
474-
subsolver_error=False,
486+
subsolver_error=subsolver_error_flag,
475487
discrete_set_scenario_index=worst_case_res.discrete_set_scenario_index,
476488
)
477489

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

643655
priority_group_solve_call_results[ss_ineq_con] = solve_call_results
644656

645-
termination_not_ok = (
646-
solve_call_results.time_out or solve_call_results.subsolver_error
647-
)
657+
termination_not_ok = solve_call_results.time_out
648658
if termination_not_ok:
649659
all_solve_call_results.update(priority_group_solve_call_results)
650660
return SeparationLoopResults(
@@ -653,6 +663,14 @@ def perform_separation_loop(separation_data, master_data, solve_globally):
653663
worst_case_ss_ineq_con=None,
654664
)
655665

666+
# provide message that PyROS will attempt to find a violation and move
667+
# to the next iteration even after subsolver error
668+
if solve_call_results.subsolver_error:
669+
config.progress_logger.warning(
670+
"PyROS is attempting to recover and will continue to "
671+
"the next iteration if a constraint violation is found."
672+
)
673+
656674
all_solve_call_results.update(priority_group_solve_call_results)
657675

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

11411159
solve_call_results_dict = {}
1142-
for scenario_idx in scenario_idxs_to_separate:
1160+
for idx, scenario_idx in enumerate(scenario_idxs_to_separate):
11431161
# fix uncertain parameters to scenario value
11441162
# hence, no need to activate uncertainty set constraints
11451163
scenario = config.uncertainty_set.scenarios[scenario_idx]
11461164
for param, coord_val in zip(uncertain_param_vars, scenario):
11471165
param.fix(coord_val)
11481166

1167+
# debug statement for solving square problem for each scenario
1168+
config.progress_logger.debug(
1169+
f"Attempting to solve square problem for discrete scenario {scenario}"
1170+
f", {idx + 1} of {len(scenario_idxs_to_separate)} total"
1171+
)
1172+
11491173
# obtain separation problem solution
11501174
solve_call_results = solver_call_separation(
11511175
separation_data=separation_data,
@@ -1158,12 +1182,17 @@ def discrete_solve(
11581182
solve_call_results_dict[scenario_idx] = solve_call_results
11591183

11601184
# halt at first encounter of unacceptable termination
1161-
termination_not_ok = (
1162-
solve_call_results.subsolver_error or solve_call_results.time_out
1163-
)
1185+
termination_not_ok = solve_call_results.time_out
11641186
if termination_not_ok:
11651187
break
11661188

1189+
# report any subsolver errors, but continue
1190+
if solve_call_results.subsolver_error:
1191+
config.progress_logger.warning(
1192+
f"All solvers failed to solve discrete scenario {scenario_idx}: "
1193+
f"{scenario}"
1194+
)
1195+
11671196
return DiscreteSeparationSolveCallResults(
11681197
solved_globally=solve_globally,
11691198
solver_call_results=solve_call_results_dict,

pyomo/contrib/pyros/solve_data.py

+10-7
Original file line numberDiff line numberDiff line change
@@ -277,11 +277,11 @@ def time_out(self):
277277
@property
278278
def subsolver_error(self):
279279
"""
280-
bool : True if there is a subsolver error status for at least
281-
one of the the ``SeparationSolveCallResults`` objects listed
280+
bool : True if there is a subsolver error status for all
281+
of the ``SeparationSolveCallResults`` objects listed
282282
in `self`, False otherwise.
283283
"""
284-
return any(res.subsolver_error for res in self.solver_call_results.values())
284+
return all(res.subsolver_error for res in self.solver_call_results.values())
285285

286286

287287
class SeparationLoopResults:
@@ -430,11 +430,14 @@ def subsolver_error(self):
430430
"""
431431
bool : Return True if subsolver error reported for
432432
at least one ``SeparationSolveCallResults`` stored in
433-
`self`, False otherwise.
433+
`self` and no violations are found, False otherwise.
434434
"""
435-
return any(
436-
solver_call_res.subsolver_error
437-
for solver_call_res in self.solver_call_results.values()
435+
return (
436+
any(
437+
solver_call_res.subsolver_error
438+
for solver_call_res in self.solver_call_results.values()
439+
)
440+
and not self.found_violation
438441
)
439442

440443
@property

0 commit comments

Comments
 (0)