Skip to content

Commit 8dfd55b

Browse files
authored
Merge pull request #3649 from shermanjasonaf/pyros-custom-subproblem-formats
Make exporting of PyROS subproblems more customizable
2 parents 14a1c10 + 425e76c commit 8dfd55b

File tree

9 files changed

+232
-79
lines changed

9 files changed

+232
-79
lines changed

doc/OnlineDocs/explanation/solvers/pyros.rst

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -906,13 +906,13 @@ Observe that the log contains the following information:
906906
information, (UTC) time at which the solver was invoked,
907907
and, if available, information on the local Git branch and
908908
commit hash.
909-
* **Summary of solver options** (lines 19--38).
910-
* **Preprocessing information** (lines 39--41).
909+
* **Summary of solver options** (lines 19--40).
910+
* **Preprocessing information** (lines 41--43).
911911
Wall time required for preprocessing
912912
the deterministic model and associated components,
913913
i.e., standardizing model components and adding the decision rule
914914
variables and equations.
915-
* **Model component statistics** (lines 42--58).
915+
* **Model component statistics** (lines 44--61).
916916
Breakdown of model component statistics.
917917
Includes components added by PyROS, such as the decision rule variables
918918
and equations.
@@ -927,7 +927,7 @@ Observe that the log contains the following information:
927927
The number of truly uncertain parameters detected during preprocessing
928928
is also noted in parentheses
929929
(in which "eff." is an abbreviation for "effective").
930-
* **Iteration log table** (lines 59--69).
930+
* **Iteration log table** (lines 62--69).
931931
Summary information on the problem iterates and subproblem outcomes.
932932
The constituent columns are defined in detail in
933933
:ref:`the table following the snippet <table-iteration-log-columns>`.
@@ -953,7 +953,7 @@ Observe that the log contains the following information:
953953

954954
* **Termination statistics** (lines 89--94). Summary of statistics related to the
955955
iterate at which PyROS terminates.
956-
* **Exit message** (lines 95--96).
956+
* **Exit message** (lines 95--97).
957957

958958

959959
.. _solver-log-snippet:
@@ -963,10 +963,10 @@ Observe that the log contains the following information:
963963
:linenos:
964964
965965
==============================================================================
966-
PyROS: The Pyomo Robust Optimization Solver, v1.3.8.
966+
PyROS: The Pyomo Robust Optimization Solver, v1.3.9.
967967
Pyomo version: 6.9.3dev0
968968
Commit hash: unknown
969-
Invoked at UTC 2025-05-05T00:00:00.000000+00:00
969+
Invoked at UTC 2025-07-21T00:00:00.000000+00:00
970970
971971
Developed by: Natalie M. Isenberg (1), Jason A. F. Sherman (1),
972972
John D. Siirola (2), Chrysanthos E. Gounaris (1)
@@ -977,7 +977,7 @@ Observe that the log contains the following information:
977977
of Energy's Institute for the Design of Advanced Energy Systems (IDAES).
978978
==============================================================================
979979
================================= DISCLAIMER =================================
980-
PyROS is still under development.
980+
PyROS is still under development.
981981
Please provide feedback and/or report any issues by creating a ticket at
982982
https://github.com/Pyomo/pyomo/issues/new/choose
983983
==============================================================================
@@ -998,6 +998,7 @@ Observe that the log contains the following information:
998998
backup_local_solvers=[]
999999
backup_global_solvers=[]
10001000
subproblem_file_directory=None
1001+
subproblem_format_options={'bar': {'symbolic_solver_labels': True}}
10011002
bypass_local_separation=False
10021003
bypass_global_separation=False
10031004
p_robustness={}
@@ -1025,33 +1026,34 @@ Observe that the log contains the following information:
10251026
------------------------------------------------------------------------------
10261027
Itn Objective 1-Stg Shift 2-Stg Shift #CViol Max Viol Wall Time (s)
10271028
------------------------------------------------------------------------------
1028-
0 3.5838e+07 - - 5 1.8832e+04 0.759
1029-
1 3.5838e+07 2.9329e-09 5.0030e-10 5 2.1295e+04 1.573
1030-
2 3.6285e+07 7.6526e-01 2.0398e-01 2 2.2457e+02 2.272
1031-
3 3.6285e+07 7.7212e-13 1.2525e-10 0 7.2940e-08g 5.280
1029+
0 3.5838e+07 - - 5 1.8832e+04 0.611
1030+
1 3.5838e+07 1.2289e-09 1.5886e-12 5 2.8919e+02 1.702
1031+
2 3.6269e+07 3.1647e-01 1.0432e-01 4 2.9020e+02 3.407
1032+
3 3.6285e+07 7.6526e-01 1.4596e-04 7 7.5966e+03 5.919
1033+
4 3.6285e+07 1.1608e-11 2.2270e-01 0 1.5084e-12g 8.823
10321034
------------------------------------------------------------------------------
10331035
Robust optimal solution identified.
10341036
------------------------------------------------------------------------------
10351037
Timing breakdown:
1036-
1038+
10371039
Identifier ncalls cumtime percall %
10381040
-----------------------------------------------------------
1039-
main 1 5.281 5.281 100.0
1041+
main 1 8.824 8.824 100.0
10401042
------------------------------------------------------
1041-
dr_polishing 3 0.155 0.052 2.9
1042-
global_separation 27 1.280 0.047 24.2
1043-
local_separation 108 2.200 0.020 41.7
1044-
master 4 0.727 0.182 13.8
1045-
master_feasibility 3 0.103 0.034 1.9
1046-
preprocessing 1 0.021 0.021 0.4
1047-
other n/a 0.794 n/a 15.0
1043+
dr_polishing 4 0.547 0.137 6.2
1044+
global_separation 27 0.978 0.036 11.1
1045+
local_separation 135 4.645 0.034 52.6
1046+
master 5 1.720 0.344 19.5
1047+
master_feasibility 4 0.239 0.060 2.7
1048+
preprocessing 1 0.013 0.013 0.2
1049+
other n/a 0.681 n/a 7.7
10481050
======================================================
10491051
===========================================================
1050-
1052+
10511053
------------------------------------------------------------------------------
10521054
Termination stats:
1053-
Iterations : 4
1054-
Solve time (wall s) : 5.281
1055+
Iterations : 5
1056+
Solve time (wall s) : 8.824
10551057
Final objective value : 3.6285e+07
10561058
Termination condition : pyrosTerminationCondition.robust_optimal
10571059
------------------------------------------------------------------------------

pyomo/contrib/pyros/CHANGELOG.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ PyROS CHANGELOG
33
===============
44

55

6+
-------------------------------------------------------------------------------
7+
PyROS 1.3.9 19 Jul 2025
8+
-------------------------------------------------------------------------------
9+
- Update uncertainty set validation methods with efficient, set-specific checks
10+
- Adjust PyROS handling of separation objective evaluation errors
11+
- Allow user to configure formats to which PyROS should export subproblems
12+
not solved to an acceptable level
13+
14+
615
-------------------------------------------------------------------------------
716
PyROS 1.3.8 28 Apr 2025
817
-------------------------------------------------------------------------------

pyomo/contrib/pyros/config.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,26 @@ def pyros_config():
855855
),
856856
),
857857
)
858+
CONFIG.declare(
859+
"subproblem_format_options",
860+
ConfigValue(
861+
default={"bar": {"symbolic_solver_labels": True}},
862+
# note: we leave all validation of the dict entries
863+
# to ``BlockData.write()``
864+
domain=dict,
865+
description=(
866+
"""
867+
File format options for writing/exporting subproblems
868+
that were not solved to an acceptable level
869+
if ``keepfiles=True`` is specified.
870+
Each entry of the dict should map a Pyomo WriterFactory
871+
format (e.g., 'bar' for BARON, 'gams' for GAMS)
872+
to a value for the argument ``io_options``
873+
to the method ``BlockData.write()``.
874+
"""
875+
),
876+
),
877+
)
858878

859879
# ================================================
860880
# === Advanced Options

pyomo/contrib/pyros/master_problem_methods.py

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
Functions for construction and solution of the PyROS master problem.
1414
"""
1515

16-
import os
17-
1816
from pyomo.common.collections import ComponentMap, ComponentSet
1917
from pyomo.common.modeling import unique_component_name
2018
from pyomo.core import TransformationFactory
@@ -37,6 +35,7 @@
3735
ObjectiveType,
3836
pyrosTerminationCondition,
3937
TIC_TOC_SOLVE_TIME_ATTR,
38+
write_subproblem,
4039
)
4140

4241

@@ -828,31 +827,8 @@ def solver_call_master(master_data):
828827

829828
# all solvers have failed to return an acceptable status.
830829
# we will terminate PyROS with subsolver error status.
831-
# at this point, export subproblem to file, if desired.
832-
# NOTE: subproblem is written with variables set to their
833-
# initial values (not the final subsolver iterate)
834-
save_dir = config.subproblem_file_directory
835-
serialization_msg = ""
836-
if save_dir and config.keepfiles:
837-
output_problem_path = os.path.join(
838-
save_dir,
839-
(
840-
config.uncertainty_set.type
841-
+ "_"
842-
+ master_data.original_model_name
843-
+ "_master_"
844-
+ str(master_data.iteration)
845-
+ ".bar"
846-
),
847-
)
848-
master_model.write(
849-
output_problem_path, io_options={'symbolic_solver_labels': True}
850-
)
851-
serialization_msg = (
852-
" For debugging, problem has been serialized to the file "
853-
f"{output_problem_path!r}."
854-
)
855830

831+
# log subproblem solve failure warning
856832
deterministic_model_qual = (
857833
" (i.e., the deterministic model)" if master_data.iteration == 0 else ""
858834
)
@@ -867,7 +843,6 @@ def solver_call_master(master_data):
867843
if master_data.iteration == 0
868844
else ""
869845
)
870-
871846
master_soln.pyros_termination_condition = pyrosTerminationCondition.subsolver_error
872847
subsolver_termination_conditions = [
873848
res.solver.termination_condition for res in master_soln.master_results_list
@@ -879,9 +854,22 @@ def solver_call_master(master_data):
879854
f"(Termination statuses: "
880855
f"{[term_cond for term_cond in subsolver_termination_conditions]}.)"
881856
f"{deterministic_msg}"
882-
f"{serialization_msg}"
883857
)
884858

859+
# at this point, export subproblem to file, if desired.
860+
# NOTE: subproblem is written with variables set to their
861+
# initial values (not the final subsolver iterate)
862+
if config.keepfiles and config.subproblem_file_directory is not None:
863+
write_subproblem(
864+
model=master_model,
865+
fname=(
866+
f"{config.uncertainty_set.type}"
867+
f"_{master_data.original_model_name}"
868+
f"_master_{master_data.iteration}"
869+
),
870+
config=config,
871+
)
872+
885873
return master_soln
886874

887875

pyomo/contrib/pyros/pyros.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
)
3434

3535

36-
__version__ = "1.3.8"
36+
__version__ = "1.3.9"
3737

3838

3939
default_pyros_solver_logger = setup_pyros_logger()

pyomo/contrib/pyros/separation_problem_methods.py

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
"""
1616

1717
from itertools import product
18-
import os
1918

2019
from pyomo.common.collections import ComponentSet, ComponentMap
2120
from pyomo.common.dependencies import numpy as np
@@ -39,6 +38,7 @@
3938
call_solver,
4039
check_time_limit_reached,
4140
get_all_first_stage_eq_cons,
41+
write_subproblem,
4242
)
4343

4444

@@ -1093,41 +1093,27 @@ def solver_call_separation(
10931093
# termination condition. PyROS will terminate with subsolver
10941094
# error. At this point, export model if desired
10951095
solve_call_results.subsolver_error = True
1096-
save_dir = config.subproblem_file_directory
1097-
serialization_msg = ""
1098-
if save_dir and config.keepfiles:
1099-
objective = separation_obj.name
1100-
output_problem_path = os.path.join(
1101-
save_dir,
1102-
(
1103-
config.uncertainty_set.type
1104-
+ "_"
1105-
+ separation_model.name
1106-
+ "_separation_"
1107-
+ str(separation_data.iteration)
1108-
+ "_obj_"
1109-
+ objective
1110-
+ ".bar"
1111-
),
1112-
)
1113-
separation_model.write(
1114-
output_problem_path, io_options={'symbolic_solver_labels': True}
1115-
)
1116-
serialization_msg = (
1117-
" For debugging, problem has been serialized to the file "
1118-
f"{output_problem_path!r}."
1119-
)
11201096
solve_call_results.message = (
11211097
"Could not successfully solve separation problem of iteration "
11221098
f"{separation_data.iteration} "
11231099
f"for second-stage inequality constraint {con_name_repr} with any of the "
11241100
f"provided subordinate {solve_mode} optimizers. "
11251101
f"(Termination statuses: "
11261102
f"{[str(term_cond) for term_cond in solver_status_dict.values()]}.)"
1127-
f"{serialization_msg}"
11281103
)
11291104
config.progress_logger.warning(solve_call_results.message)
11301105

1106+
if config.keepfiles and config.subproblem_file_directory is not None:
1107+
write_subproblem(
1108+
model=separation_model,
1109+
fname=(
1110+
f"{config.uncertainty_set.type}_{separation_model.name}"
1111+
f"_separation_{separation_data.iteration}"
1112+
f"_obj_{separation_obj.name}"
1113+
),
1114+
config=config,
1115+
)
1116+
11311117
separation_obj.deactivate()
11321118

11331119
return solve_call_results

pyomo/contrib/pyros/tests/test_config.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,38 @@ def test_config_objective_focus(self):
696696
with self.assertRaisesRegex(ValueError, exc_str):
697697
config.objective_focus = invalid_focus
698698

699+
def test_config_subproblem_formats(self):
700+
config = self.CONFIG()
701+
702+
# test default
703+
self.assertEqual(
704+
config.subproblem_format_options,
705+
{"bar": {"symbolic_solver_labels": True}},
706+
msg=(
707+
"Default value for PyROS config option "
708+
"subproblem_format_options' not as expected."
709+
),
710+
)
711+
712+
config.subproblem_format_options = {}
713+
self.assertEqual(config.subproblem_format_options, {})
714+
715+
nondefault_test_val = {"fmt1": {"symbolic_solver_labels": False}, "fmt2": {}}
716+
config.subproblem_format_options = nondefault_test_val
717+
self.assertEqual(config.subproblem_format_options, nondefault_test_val)
718+
719+
# anything castable to dict should also be acceptable
720+
config.subproblem_format_options = list(nondefault_test_val.items())
721+
self.assertEqual(config.subproblem_format_options, nondefault_test_val)
722+
723+
exc_str = (
724+
# contents of the error message are version dependent
725+
"(cannot convert dictionary update sequence"
726+
"|'int' object is not iterable)"
727+
)
728+
with self.assertRaisesRegex(ValueError, exc_str):
729+
config.subproblem_format_options = [1, 2, 3]
730+
699731

700732
class TestPositiveIntOrMinusOne(unittest.TestCase):
701733
"""

0 commit comments

Comments
 (0)