From 11eaed9163b1a018bbb5ff2713d3ced6975dd029 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 4 Jun 2026 18:53:01 -0400 Subject: [PATCH 01/12] Tweak `CardinalitySet` class documentation --- pyomo/contrib/pyros/uncertainty_sets.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 33e68a53859..a62bd8715e2 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -1475,16 +1475,16 @@ class CardinalitySet(UncertaintySet): origin : (N,) array_like Origin of the set (e.g., nominal uncertain parameter values). positive_deviation : (N,) array_like - Upper bounds for absolute values of the positive coordinate - deviations from the origin. + Maximal absolute deviation from the origin in the + positive coordinate direction. gamma : numeric type Upper bound for the number of coordinates that can simultaneously realize their maximal deviations from the origin. Must be a numerical value ranging from 0 to the set dimension `N`. negative_deviation : (N,) array_like, optional - Upper bounds for absolute values of the negative coordinate - deviations from the origin. + Maximal absolute deviation from the origin in the + negative coordinate direction. If `None` is passed, then this argument is set to an (`N`,) shaped array of zeros. @@ -1538,7 +1538,8 @@ class CardinalitySet(UncertaintySet): \\right\\}, the cardinality-constrained set implicitly defined - in the popular robust optimization work [BS04]_. + in the popular robust optimization work by Bertsimas and Sim + [BS04]_. Examples -------- From 6af87c91b9cd1554bae5d4c9e3b6437c874e71c5 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 4 Jun 2026 18:54:15 -0400 Subject: [PATCH 02/12] Fix date in PyROS changelog --- pyomo/contrib/pyros/CHANGELOG.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/CHANGELOG.txt b/pyomo/contrib/pyros/CHANGELOG.txt index 1ebf2bc60ce..def9abd64d1 100644 --- a/pyomo/contrib/pyros/CHANGELOG.txt +++ b/pyomo/contrib/pyros/CHANGELOG.txt @@ -4,7 +4,7 @@ PyROS CHANGELOG ------------------------------------------------------------------------------- -PyROS 1.3.15 12 May 2026 +PyROS 1.3.15 04 Jun 2026 ------------------------------------------------------------------------------- - Extend `CardinalitySet` to allow for negative deviations From 7a0e2ae3595a8c47dd5563fd106f6457f9d27214 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 4 Jun 2026 19:13:16 -0400 Subject: [PATCH 03/12] Update PyROS solver log introductory material --- pyomo/contrib/pyros/pyros.py | 19 +++++++++++-------- pyomo/contrib/pyros/tests/test_grcs.py | 23 +++++++++++------------ 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 7013ca3fa24..aa425775aef 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -194,15 +194,22 @@ def _log_intro(self, logger, **log_kwargs): logger.log( msg=( "of Energy's " - "Institute for the Design of Advanced Energy Systems (IDAES)." + "Institute for the Design of Advanced Energy Systems (IDAES)" + ), + **log_kwargs, + ) + logger.log( + msg=( + "and Carbon Capture Simulation for Industry Impact " + "(CCSI2) projects." ), **log_kwargs, ) logger.log(msg="=" * self._LOG_LINE_LENGTH, **log_kwargs) - def _log_disclaimer(self, logger, **log_kwargs): + def _log_feedback_guidance(self, logger, **log_kwargs): """ - Log PyROS solver disclaimer messages. + Log PyROS solver guidance on providing user feedback. Parameters ---------- @@ -212,10 +219,6 @@ def _log_disclaimer(self, logger, **log_kwargs): Keyword arguments to ``logger.log()`` callable. Should not include `msg`. """ - disclaimer_header = " DISCLAIMER ".center(self._LOG_LINE_LENGTH, "=") - - logger.log(msg=disclaimer_header, **log_kwargs) - logger.log(msg="PyROS is currently under active development. ", **log_kwargs) logger.log( msg=( "Please provide feedback and/or report any issues by creating " @@ -419,7 +422,7 @@ def solve( ) ) self._log_intro(logger=progress_logger, level=logging.INFO) - self._log_disclaimer(logger=progress_logger, level=logging.INFO) + self._log_feedback_guidance(logger=progress_logger, level=logging.INFO) config, user_var_partitioning = self._resolve_and_validate_pyros_args( model, **kwds diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 01a8f3b3a17..8b9852c7f7c 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -3979,7 +3979,7 @@ def test_log_intro(self): # check number of lines is as expected self.assertEqual( len(intro_msg_lines), - 14, + 15, msg=( "PyROS solver introductory message does not contain" "the expected number of lines." @@ -3993,16 +3993,17 @@ def test_log_intro(self): # check regex main text self.assertRegex( " ".join(intro_msg_lines[1:-1]), - r"PyROS: The Pyomo Robust Optimization Solver, v.* \(IDAES\)\.", + r"PyROS: The Pyomo Robust Optimization Solver, v.* \(CCSI2\) " + r"projects\.", ) - def test_log_disclaimer(self): + def test_log_feedback_ref(self): """ - Test logging of PyROS solver disclaimer messages. + Test logging of PyROS solver guidance on providing feedback. """ pyros_solver = SolverFactory("pyros") with LoggingIntercept(level=logging.INFO) as LOG: - pyros_solver._log_disclaimer(logger=logger, level=logging.INFO) + pyros_solver._log_feedback_guidance(logger=logger, level=logging.INFO) disclaimer_msgs = LOG.getvalue() @@ -4012,23 +4013,21 @@ def test_log_disclaimer(self): # check number of lines is as expected self.assertEqual( len(disclaimer_msg_lines), - 5, + 3, msg=( - "PyROS solver disclaimer message does not contain" + "PyROS solver disclaimer message does not contain " "the expected number of lines." ), ) # regex first line of disclaimer section - self.assertRegex(disclaimer_msg_lines[0], r"=.* DISCLAIMER .*=") # check last line of disclaimer section - self.assertEqual(disclaimer_msg_lines[-1], "=" * 78) - # check regex main text self.assertRegex( - " ".join(disclaimer_msg_lines[1:-1]), - r"PyROS is currently under active development.*ticket at.*", + " ".join(disclaimer_msg_lines[0:-1]), + r"Please provide feedback.*ticket at.*", ) + self.assertEqual(disclaimer_msg_lines[-1], "=" * 78) class UnavailableSolver: From 2380a2069d703e89ab6cf593267948d9d71e8127 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 4 Jun 2026 19:27:32 -0400 Subject: [PATCH 04/12] Update PyROS solver logging example --- .../explanation/solvers/pyros/solver_log.rst | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/doc/OnlineDocs/explanation/solvers/pyros/solver_log.rst b/doc/OnlineDocs/explanation/solvers/pyros/solver_log.rst index 2459eafed50..2ae7d46f45b 100644 --- a/doc/OnlineDocs/explanation/solvers/pyros/solver_log.rst +++ b/doc/OnlineDocs/explanation/solvers/pyros/solver_log.rst @@ -26,10 +26,10 @@ your console output will, by default, look like this: :linenos: ============================================================================== - PyROS: The Pyomo Robust Optimization Solver, v1.3.11. - Pyomo version: 6.9.5 + PyROS: The Pyomo Robust Optimization Solver, v1.3.15. + Pyomo version: 6.10.2.dev0 Commit hash: unknown - Invoked at UTC 2025-10-18T00:00:00.000000+00:00 + Invoked at UTC 2026-06-05T00:00:00.000000+00:00 Developed by: Natalie M. Isenberg (1), Jason A. F. Sherman (1), John D. Siirola (2), Chrysanthos E. Gounaris (1) @@ -37,17 +37,18 @@ your console output will, by default, look like this: (2) Sandia National Laboratories, Center for Computing Research The developers gratefully acknowledge support from the U.S. Department - of Energy's Institute for the Design of Advanced Energy Systems (IDAES). + of Energy's Institute for the Design of Advanced Energy Systems (IDAES) + and Carbon Capture Simulation for Industry Impact (CCSI2) projects. ============================================================================== - ================================= DISCLAIMER ================================= - PyROS is currently under active development. Please provide feedback and/or report any issues by creating a ticket at https://github.com/Pyomo/pyomo/issues/new/choose ============================================================================== User-provided solver options: + tee=False objective_focus= decision_rule_order=1 solve_master_globally=True + bypass_local_separation=False ------------------------------------------------------------------------------ Model Statistics (before preprocessing): Number of variables : 4 @@ -60,21 +61,23 @@ your console output will, by default, look like this: Inequality constraints : 2 ------------------------------------------------------------------------------ Preprocessing... - Done preprocessing; required wall time of 0.004s. + Done preprocessing; required wall time of 0.003s. ------------------------------------------------------------------------------ Itn Objective 1-Stg Shift 2-Stg Shift #CViol Max Viol Wall Time (s) ------------------------------------------------------------------------------ - 0 5.4079e+03 - - 3 7.9226e+00 0.209 - 1 5.4079e+03 6.0451e-10 1.0717e-10 2 1.0250e-01 0.476 - 2 6.5403e+03 1.0018e-01 7.4564e-03 1 1.0249e-02 0.786 - 3 6.5403e+03 1.9372e-16 2.0321e-05 2 8.7074e-03 1.132 - 4 6.5403e+03 0.0000e+00 2.0311e-05 0 1.2310e-06g 1.956 + 0 5.4079e+03 - - 3 4.6876e+02 0.185 + 1 5.4079e+03 6.0451e-10 1.0717e-10 2 6.1500e+01 0.496 + 2 6.5403e+03 1.0018e-01 7.4564e-03 1 1.7142e-03 0.804 + 3 6.5403e+03 1.9372e-16 3.6832e-06 2 2.7964e-01 1.136 + 4 6.5403e+03 0.0000e+00 3.8115e-06 1 1.7141e-03 1.465 + 5 6.5403e+03 0.0000e+00 8.4872e-03 1 4.7920e-01 1.855 + 6 6.5403e+03 0.0000e+00 2.0736e-04 0 1.3594e-06g 2.756 ------------------------------------------------------------------------------ Robust optimal solution identified. ------------------------------------------------------------------------------ Termination stats: - Iterations : 5 - Solve time (wall s) : 1.956 + Iterations : 7 + Solve time (wall s) : 2.756 Final objective value : 6.5403e+03 Termination condition : pyrosTerminationCondition.robust_optimal ------------------------------------------------------------------------------ @@ -87,32 +90,32 @@ Observe that the log contains the following information (listed in order of appearance): -* **Introductory information and disclaimer** (lines 1--19): +* **Introductory information** (lines 1--18): Includes the version number, author information, (UTC) time at which the solver was invoked, and, if available, information on the local Git branch and commit hash. -* **Summary of solver options** (lines 20--24): Enumeration of +* **Summary of solver options** (lines 19--25): Enumeration of specifications for optional arguments to the solver. -* **Model component statistics** (lines 25--34): +* **Model component statistics** (lines 26--35): Breakdown of component statistics for the user-provided model and variable selection (before preprocessing). -* **Preprocessing information** (lines 35--37): +* **Preprocessing information** (lines 36--38): Wall time required for preprocessing the deterministic model and associated components, i.e., standardizing model components and adding the decision rule variables and equations. -* **Iteration log table** (lines 38--45): +* **Iteration log table** (lines 39--48): Summary information on the problem iterates and subproblem outcomes. The constituent columns are defined in detail in :ref:`the table that follows `. -* **Termination message** (lines 46--47): One-line message briefly summarizing +* **Termination message** (lines 49--50): One-line message briefly summarizing the reason the solver has terminated. -* **Final result** (lines 48--53): +* **Final result** (lines 51--56): A printout of the :class:`~pyomo.contrib.pyros.solve_data.ROSolveResults` object that is finally returned. -* **Exit message** (lines 54--55): Confirmation that the +* **Exit message** (lines 57--58): Confirmation that the solver has been exited properly. The iteration log table is designed to provide, in a concise manner, From 61cb1eeeb5f341a51f1ddff9ae85cb4b2bd4a1db Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 4 Jun 2026 19:29:42 -0400 Subject: [PATCH 05/12] Update changelog --- pyomo/contrib/pyros/CHANGELOG.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/pyros/CHANGELOG.txt b/pyomo/contrib/pyros/CHANGELOG.txt index def9abd64d1..58d1e42ebdc 100644 --- a/pyomo/contrib/pyros/CHANGELOG.txt +++ b/pyomo/contrib/pyros/CHANGELOG.txt @@ -7,6 +7,7 @@ PyROS CHANGELOG PyROS 1.3.15 04 Jun 2026 ------------------------------------------------------------------------------- - Extend `CardinalitySet` to allow for negative deviations +- Update PyROS solver output log introductory information ------------------------------------------------------------------------------- From ddce22ae17bbcd231b5cc6c827d4274252ff8832 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 4 Jun 2026 19:41:06 -0400 Subject: [PATCH 06/12] Apply black --- pyomo/contrib/pyros/pyros.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index aa425775aef..51a95d52b45 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -199,10 +199,7 @@ def _log_intro(self, logger, **log_kwargs): **log_kwargs, ) logger.log( - msg=( - "and Carbon Capture Simulation for Industry Impact " - "(CCSI2) projects." - ), + msg="and Carbon Capture Simulation for Industry Impact (CCSI2) projects.", **log_kwargs, ) logger.log(msg="=" * self._LOG_LINE_LENGTH, **log_kwargs) From e7a3e257b42cd92c4b8f8f0b8f24044fe4a90457 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 8 Jun 2026 16:39:04 -0400 Subject: [PATCH 07/12] Change "circle"/"sphere" to "disk"/"ball" in docs --- pyomo/contrib/pyros/uncertainty_sets.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index a62bd8715e2..85c4a4ec27c 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -3808,20 +3808,19 @@ class IntersectionSet(UncertaintySet): Examples -------- - Intersection of origin-centered 2D box (square) and 2D - hypersphere (circle): + Intersection of origin-centered 2D box (square) and 2D ball (disk): >>> from pyomo.contrib.pyros import ( ... BoxSet, AxisAlignedEllipsoidalSet, IntersectionSet, ... ) >>> square = BoxSet(bounds=[[-1.5, 1.5], [-1.5, 1.5]]) - >>> circle = AxisAlignedEllipsoidalSet( + >>> disk = AxisAlignedEllipsoidalSet( ... center=[0, 0], ... half_lengths=[2, 2], ... ) >>> # to construct intersection, pass sets as keyword arguments. >>> # keywords are arbitrary - >>> intersection = IntersectionSet(set1=square, set2=circle) + >>> intersection = IntersectionSet(set1=square, set2=disk) >>> intersection.all_sets # doctest: +ELLIPSIS UncertaintySetList([...]) @@ -4096,18 +4095,17 @@ class CartesianProductSet(UncertaintySet): Examples -------- - Cartesian product of 1D box/interval and 2D - hypersphere (circle): + Cartesian product of 1D box/interval and 2D ball (disk): >>> from pyomo.contrib.pyros import ( ... BoxSet, AxisAlignedEllipsoidalSet, CartesianProductSet, ... ) >>> interval = BoxSet(bounds=[[-1.5, 1.5]]) - >>> circle = AxisAlignedEllipsoidalSet( + >>> disk = AxisAlignedEllipsoidalSet( ... center=[0, 0], ... half_lengths=[2, 2], ... ) - >>> cartesian_product = CartesianProductSet([interval, circle]) + >>> cartesian_product = CartesianProductSet([interval, disk]) """ def __init__(self, all_sets): From 9d507cb5838d18001a7ba5e2a5a57ef583bb47db Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 8 Jun 2026 16:40:54 -0400 Subject: [PATCH 08/12] Tweak `CartesianProductSet` documentation --- pyomo/contrib/pyros/uncertainty_sets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 85c4a4ec27c..70b0dbb90ee 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -4095,7 +4095,7 @@ class CartesianProductSet(UncertaintySet): Examples -------- - Cartesian product of 1D box/interval and 2D ball (disk): + Cartesian product of 1D box (interval) and 2D ball (disk): >>> from pyomo.contrib.pyros import ( ... BoxSet, AxisAlignedEllipsoidalSet, CartesianProductSet, From 27b5a969dce85279766bb8e46dcbd6e37d16f216 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 11 Jun 2026 23:18:03 -0400 Subject: [PATCH 09/12] Update documentation of `CardinalitySet` positive and negative deviations --- pyomo/contrib/pyros/uncertainty_sets.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 70b0dbb90ee..0eff25298d0 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -1613,8 +1613,8 @@ def origin(self, val): @property def positive_deviation(self): """ - (N,) numpy.ndarray : Upper bounds for absolute values of - the positive coordinate deviations from the origin. + (N,) numpy.ndarray : Maximal absolute deviation from + the origin in the positive coordinate direction. """ return self._positive_deviation @@ -1644,8 +1644,8 @@ def positive_deviation(self, val): @property def negative_deviation(self): """ - (N,) numpy.ndarray : Upper bounds for absolute values of - the negative coordinate deviations from the origin. + (N,) numpy.ndarray : Maximal absolute deviation from + the origin in the negative coordinate direction. """ return self._negative_deviation From bec57c010e38a69366a9572e29acddfe04f25577 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 12 Jun 2026 15:51:35 -0400 Subject: [PATCH 10/12] Swap order of arguments to `CardinalitySet` constructor --- pyomo/contrib/pyros/config.py | 138 ++++++++++++------ pyomo/contrib/pyros/tests/test_grcs.py | 10 +- .../contrib/pyros/tests/test_preprocessor.py | 12 +- .../pyros/tests/test_uncertainty_sets.py | 107 ++++++++++---- pyomo/contrib/pyros/uncertainty_sets.py | 39 +++-- 5 files changed, 214 insertions(+), 92 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 786f0e46536..9d4637be792 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -490,11 +490,13 @@ def pyros_config(): ConfigValue( default=None, domain=NonNegativeFloat, - doc=(""" + doc=( + """ Wall time limit for the execution of the PyROS solver in seconds (including time spent by subsolvers). If `None` is provided, then no time limit is enforced. - """), + """ + ), ), ) CONFIG.declare( @@ -502,12 +504,14 @@ def pyros_config(): ConfigValue( default=False, domain=bool, - description=(""" + description=( + """ Export subproblems with a non-acceptable termination status for debugging purposes. If True is provided, then the argument `subproblem_file_directory` must also be specified. - """), + """ + ), ), ) CONFIG.declare( @@ -523,10 +527,12 @@ def pyros_config(): ConfigValue( default=True, domain=bool, - description=(""" + description=( + """ Load final solution(s) found by PyROS to the deterministic model provided. - """), + """ + ), ), ) CONFIG.declare( @@ -534,12 +540,14 @@ def pyros_config(): ConfigValue( default=False, domain=bool, - description=(""" + description=( + """ True to ensure the component names given to the subordinate solvers for every subproblem reflect the names of the corresponding Pyomo modeling components, False otherwise. - """), + """ + ), ), ) @@ -575,13 +583,15 @@ def pyros_config(): cdatatype_validator=uncertain_param_data_validator, allow_repeats=False, ), - description=(""" + description=( + """ Uncertain model parameters. Of every constituent `Param` object, the `mutable` attribute must be set to True. All constituent `Var`/`VarData` objects should be fixed. - """), + """ + ), visibility=1, ), ) @@ -590,11 +600,13 @@ def pyros_config(): ConfigValue( default=None, domain=IsInstance(UncertaintySet), - description=(""" + description=( + """ Uncertainty set against which the final solution(s) returned by PyROS should be certified to be robust. - """), + """ + ), visibility=1, ), ) @@ -626,12 +638,15 @@ def pyros_config(): ConfigValue( default=ObjectiveType.nominal, domain=InEnum(ObjectiveType), - description=(""" + description=( + """ Choice of objective focus to optimize in the master problems. Choices are: `ObjectiveType.worst_case`, `ObjectiveType.nominal`. - """), - doc=(""" + """ + ), + doc=( + """ Objective focus for the master problems: - `ObjectiveType.nominal`: @@ -648,7 +663,8 @@ def pyros_config(): by PyROS. If a nominal objective focus is chosen, then only robust feasibility is guaranteed. - """), + """ + ), ), ) CONFIG.declare( @@ -656,13 +672,15 @@ def pyros_config(): ConfigValue( default=[], domain=list, - doc=(""" + doc=( + """ Nominal uncertain parameter realization. Entries should be provided in an order consistent with the entries of the argument `uncertain_params`. If an empty list is provided, then the values of the `Param` objects specified through `uncertain_params` are chosen. - """), + """ + ), ), ) CONFIG.declare( @@ -670,12 +688,15 @@ def pyros_config(): ConfigValue( default=0, domain=In([0, 1, 2]), - description=(""" + description=( + """ Order (or degree) of the polynomial decision rule functions used for approximating the adjustability of the second stage variables with respect to the uncertain parameters. - """), - doc=(""" + """ + ), + doc=( + """ Order (or degree) of the polynomial decision rule functions for approximating the adjustability of the second stage variables with respect to the uncertain parameters. @@ -685,7 +706,8 @@ def pyros_config(): - 0: static recourse - 1: affine recourse - 2: quadratic recourse - """), + """ + ), ), ) CONFIG.declare( @@ -693,7 +715,8 @@ def pyros_config(): ConfigValue( default=False, domain=bool, - doc=(""" + doc=( + """ True to solve all master problems with the subordinate global solver, False to solve all master problems with the subordinate local solver. @@ -703,7 +726,8 @@ def pyros_config(): for certification of robust optimality of the final solution(s) returned by PyROS. Otherwise, only robust feasibility is guaranteed. - """), + """ + ), ), ) CONFIG.declare( @@ -711,10 +735,12 @@ def pyros_config(): ConfigValue( default=-1, domain=positive_int_or_minus_one, - description=(""" + description=( + """ Iteration limit. If -1 is provided, then no iteration limit is enforced. - """), + """ + ), ), ) CONFIG.declare( @@ -722,10 +748,12 @@ def pyros_config(): ConfigValue( default=1e-4, domain=NonNegativeFloat, - description=(""" + description=( + """ Relative tolerance for assessing maximal inequality constraint violations during the GRCS separation step. - """), + """ + ), ), ) CONFIG.declare( @@ -733,7 +761,8 @@ def pyros_config(): ConfigValue( default={}, domain=_deprecated_separation_priority_order, - doc=(""" + doc=( + """ (DEPRECATED) A dict-like object, each entry of which maps the full name of a model ``Var`` or ``Constraint`` @@ -759,7 +788,8 @@ def pyros_config(): Specify separation priorities by declaring, on your model, `Suffix` components with local name 'pyros_separation_priority'. - """), + """ + ), ), ) CONFIG.declare( @@ -767,7 +797,8 @@ def pyros_config(): ConfigValue( default=default_pyros_solver_logger, domain=logger_domain, - doc=(""" + doc=( + """ Logger (or name thereof) used for reporting PyROS solver progress. If `None` or a `str` is provided, then ``progress_logger`` @@ -775,7 +806,8 @@ def pyros_config(): In the default case, `progress_logger` is set to a :class:`pyomo.contrib.pyros.util.PreformattedLogger` object of level ``logging.INFO``. - """), + """ + ), ), ) CONFIG.declare( @@ -787,11 +819,13 @@ def pyros_config(): require_available=False, filter_by_availability=True, ), - doc=(""" + doc=( + """ Additional subordinate local NLP optimizers to invoke in the event the primary local NLP optimizer fails to solve a subproblem to an acceptable termination condition. - """), + """ + ), ), ) CONFIG.declare( @@ -803,11 +837,13 @@ def pyros_config(): require_available=False, filter_by_availability=True, ), - doc=(""" + doc=( + """ Additional subordinate global NLP optimizers to invoke in the event the primary global NLP optimizer fails to solve a subproblem to an acceptable termination condition. - """), + """ + ), ), ) CONFIG.declare( @@ -815,13 +851,15 @@ def pyros_config(): ConfigValue( default=None, domain=Path(), - description=(""" + description=( + """ Directory to which to export subproblems not successfully solved to an acceptable termination condition. In the event ``keepfiles=True`` is specified, a str or path-like referring to an existing directory must be provided. - """), + """ + ), ), ) CONFIG.declare( @@ -831,7 +869,8 @@ def pyros_config(): # note: we leave all validation of the dict entries # to ``BlockData.write()`` domain=dict, - description=(""" + description=( + """ File format options for writing/exporting subproblems that were not solved to an acceptable level if ``keepfiles=True`` is specified. @@ -839,7 +878,8 @@ def pyros_config(): format (e.g., 'bar' for BARON, 'gams' for GAMS) to a value for the argument ``io_options`` to the method ``BlockData.write()``. - """), + """ + ), ), ) @@ -851,14 +891,16 @@ def pyros_config(): ConfigValue( default=False, domain=bool, - description=(""" + description=( + """ This is an advanced option. Solve all separation subproblems with the subordinate global solver(s) only. This option is useful for expediting PyROS in the event that the subordinate global optimizer(s) provided can quickly solve separation subproblems to global optimality. - """), + """ + ), ), ) CONFIG.declare( @@ -866,7 +908,8 @@ def pyros_config(): ConfigValue( default=False, domain=bool, - doc=(""" + doc=( + """ This is an advanced option. Solve all separation subproblems with the subordinate local solver(s) only. @@ -877,7 +920,8 @@ def pyros_config(): in the event that the subordinate global optimizer provided cannot tractably solve separation subproblems to global optimality. - """), + """ + ), ), ) CONFIG.declare( @@ -885,7 +929,8 @@ def pyros_config(): ConfigValue( default={}, domain=dict, - doc=(""" + doc=( + """ This is an advanced option. Add p-robustness constraints to all master subproblems. If an empty dict is provided, then p-robustness constraints @@ -897,7 +942,8 @@ def pyros_config(): objective function value under any PyROS-sampled uncertain parameter realization to the objective function under the nominal parameter realization. - """), + """ + ), visibility=1, ), ) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 8b9852c7f7c..6bbb0bc7fd1 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -277,7 +277,7 @@ def test_cardinality_set_solve(self): m.ineq_con = Constraint(expr=m.x >= m.q[0] + m.q[1] - m.q[2] - m.q[3]) cset = CardinalitySet( - origin=[1] * 4, positive_deviation=[1, 0, 2, 0.5], gamma=2 + origin=[1] * 4, gamma=2, positive_deviation=[1, 0, 2, 0.5] ) res = SolverFactory("pyros").solve( model=m, @@ -545,7 +545,7 @@ def test_solve_with_cartesian_product_set(self): FactorModelSet( origin=[0, 0], number_of_factors=1, beta=1, psi_mat=[[1], [3]] ), - CardinalitySet(origin=[0], positive_deviation=[0.5], gamma=1), + CardinalitySet(origin=[0], gamma=1, positive_deviation=[0.5]), ] ) results = SolverFactory("pyros").solve( @@ -3359,7 +3359,8 @@ def test_log_model_statistics(self): state_variables=[m.y], ) - expected_log_str = textwrap.dedent(""" + expected_log_str = textwrap.dedent( + """ Model Statistics (before preprocessing): Number of variables : 3 First-stage variables : 2 @@ -3369,7 +3370,8 @@ def test_log_model_statistics(self): Number of constraints : 3 Equality constraints : 1 Inequality constraints : 2 - """) + """ + ) with LoggingIntercept(module=__name__, level=logging.DEBUG) as LOG: log_original_model_statistics(model_data, user_var_partitioning) diff --git a/pyomo/contrib/pyros/tests/test_preprocessor.py b/pyomo/contrib/pyros/tests/test_preprocessor.py index e5f833cb649..e24a4c4aed2 100644 --- a/pyomo/contrib/pyros/tests/test_preprocessor.py +++ b/pyomo/contrib/pyros/tests/test_preprocessor.py @@ -3195,7 +3195,8 @@ def test_preprocessor_log_model_statistics_affine_dr(self, obj_focus): preprocess_model_data(model_data, user_var_partitioning) # expected model stats worked out by hand - expected_log_str = textwrap.dedent(f""" + expected_log_str = textwrap.dedent( + f""" Model Statistics (after preprocessing): Number of variables : 16 Epigraph variable : 1 @@ -3213,7 +3214,8 @@ def test_preprocessor_log_model_statistics_affine_dr(self, obj_focus): Inequality constraints : 16 First-stage inequalities : {4 if obj_focus == 'nominal' else 3} Second-stage inequalities : {12 if obj_focus == 'nominal' else 13} - """) + """ + ) with LoggingIntercept(module=__name__, level=logging.DEBUG) as LOG: log_preprocessed_model_statistics(model_data) @@ -3244,7 +3246,8 @@ def test_preprocessor_log_model_statistics_quadratic_dr(self, obj_focus): preprocess_model_data(model_data, user_var_partitioning) # expected model stats worked out by hand - expected_log_str = textwrap.dedent(f""" + expected_log_str = textwrap.dedent( + f""" Model Statistics (after preprocessing): Number of variables : 22 Epigraph variable : 1 @@ -3262,7 +3265,8 @@ def test_preprocessor_log_model_statistics_quadratic_dr(self, obj_focus): Inequality constraints : 18 First-stage inequalities : {4 if obj_focus == 'nominal' else 3} Second-stage inequalities : {14 if obj_focus == 'nominal' else 15} - """) + """ + ) with LoggingIntercept(module=__name__, level=logging.DEBUG) as LOG: log_preprocessed_model_statistics(model_data) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index e53606769f4..6acf03bd85b 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -22,11 +22,11 @@ scipy as sp, scipy_available, ) - -from pyomo.environ import SolverFactory +from pyomo.common.tee import LoggingIntercept from pyomo.core.base import ConcreteModel, Param, Var, minimize, UnitInterval from pyomo.core.expr import RangedExpression from pyomo.core.expr.compare import assertExpressionsEqual +from pyomo.environ import SolverFactory from pyomo.contrib.pyros.uncertainty_sets import ( AxisAlignedEllipsoidalSet, @@ -1462,7 +1462,7 @@ def test_set_as_constraint(self): set2=FactorModelSet( origin=[0, 0], number_of_factors=2, beta=0.75, psi_mat=[[1, 1], [1, 2]] ), - set3=CardinalitySet([-0.5, -0.5], [2, 2], 2, [1.5, 0]), + set3=CardinalitySet([-0.5, -0.5], 2, [2, 2], [1.5, 0]), # ellipsoid. this is enclosed in all the other sets set4=AxisAlignedEllipsoidalSet([0, 0], [0.25, 0.25]), ) @@ -1570,7 +1570,7 @@ def test_compute_exact_parameter_bounds(self): origin=[0, 0], number_of_factors=2, beta=0.75, psi_mat=[[1, 1], [1, 2]] ), # another origin-centered square - set3=CardinalitySet([-0.5, -0.5], [2, 2], 2), + set3=CardinalitySet([-0.5, -0.5], 2, [2, 2]), # ellipsoid. this is enclosed in all the other sets set4=AxisAlignedEllipsoidalSet([0, 0], [0.25, 0.25]), ) @@ -1593,7 +1593,7 @@ def test_point_in_set(self): set2=FactorModelSet( origin=[0, 0], number_of_factors=2, beta=0.75, psi_mat=[[1, 1], [1, 2]] ), - set3=CardinalitySet([-0.5, -0.5], [2, 2], 2), + set3=CardinalitySet([-0.5, -0.5], 2, [2, 2]), # ellipsoid. this is enclosed in all the other sets set4=AxisAlignedEllipsoidalSet([0, 0], [0.25, 0.25]), ) @@ -1620,7 +1620,7 @@ def test_add_bounds_on_uncertain_parameters(self): set2=FactorModelSet( origin=[0, 0], number_of_factors=2, beta=0.75, psi_mat=[[1, 1], [1, 2]] ), - set3=CardinalitySet([-0.5, -0.5], [2, 2], 2), + set3=CardinalitySet([-0.5, -0.5], 2, [2, 2]), # ellipsoid. this is enclosed in all the other sets set4=AxisAlignedEllipsoidalSet([0, 0], [0.25, 0.25]), ) @@ -1705,7 +1705,7 @@ def test_intersection_aux_param_set(self): set1=FactorModelSet( origin=[0, 0], psi_mat=np.eye(2), beta=0.2, number_of_factors=2 ), - set2=CardinalitySet(origin=[0, 0], positive_deviation=[0.8, 0.8], gamma=1), + set2=CardinalitySet(origin=[0, 0], gamma=1, positive_deviation=[0.8, 0.8]), ) self.assertIs(iset.geometry, Geometry.LINEAR) @@ -1831,12 +1831,12 @@ def test_normal_cardinality_construction_and_update(self): when bounds are appropriate. """ # valid inputs - cset = CardinalitySet(origin=[0, 0], positive_deviation=[1, 3], gamma=2) + cset = CardinalitySet(origin=[0, 0], gamma=2, positive_deviation=[1, 3]) # check attributes are as expected np.testing.assert_allclose(cset.origin, [0, 0]) - np.testing.assert_allclose(cset.positive_deviation, [1, 3]) np.testing.assert_allclose(cset.gamma, 2) + np.testing.assert_allclose(cset.positive_deviation, [1, 3]) # check defined attributes/methods inherited from base class self.assertIs(cset.geometry, Geometry.LINEAR) @@ -1849,16 +1849,65 @@ def test_normal_cardinality_construction_and_update(self): # update the set cset.origin = [1, 2] - cset.positive_deviation = [3, 0] cset.gamma = 0.5 + cset.positive_deviation = [3, 0] cset.negative_deviation = [0, -1.5] # check updates work np.testing.assert_allclose(cset.origin, [1, 2]) - np.testing.assert_allclose(cset.positive_deviation, [3, 0]) np.testing.assert_allclose(cset.gamma, 0.5) + np.testing.assert_allclose(cset.positive_deviation, [3, 0]) np.testing.assert_equal(cset.negative_deviation, [0, -1.5]) + def test_cardinality_constructor_args_order(self): + """ + Check that `CardinalitySet` constructor allows + for `positive_deviation` and `gamma` to be swapped, + for backward compatibility. + """ + with LoggingIntercept(level=logging.WARNING) as LOG: + # since the second positional argument `gamma` + # is not a scalar, it is swapped with the third + # positional argument `positive_deviation` + cset = CardinalitySet([0, 0], [1, 3], 2) + self.assertRegex(LOG.getvalue(), r"DEPRECATED.*Order.*arguments.*`gamma`") + np.testing.assert_allclose(cset.origin, [0, 0]) + np.testing.assert_allclose(cset.gamma, 2) + np.testing.assert_allclose(cset.positive_deviation, [1, 3]) + np.testing.assert_allclose(cset.negative_deviation, [0, 0]) + + with LoggingIntercept(level=logging.WARNING) as LOG2: + # since the keyword argument `gamma` is not a scalar, it + # is swapped with the keyword argument `positive_deviation` + cset2 = CardinalitySet(origin=[0, 0], gamma=[1, 3], positive_deviation=2) + self.assertRegex(LOG2.getvalue(), r"DEPRECATED.*Order.*arguments.*`gamma`") + np.testing.assert_allclose(cset2.origin, [0, 0]) + np.testing.assert_allclose(cset2.gamma, 2) + np.testing.assert_allclose(cset2.positive_deviation, [1, 3]) + np.testing.assert_allclose(cset2.negative_deviation, [0, 0]) + + type_exc_str = r"Argument `gamma` is not a valid numeric type.*str" + with ( + self.assertRaisesRegex(TypeError, type_exc_str), + LoggingIntercept(level=logging.WARNING) as LOG3, + ): + # here second positional argument `gamma` is not a scalar, + # so arguments `gamma` and `positive_deviation` are swapped + CardinalitySet([0, 0], [1, 2], "test") + self.assertRegex(LOG3.getvalue(), r"DEPRECATED.*Order.*arguments.*`gamma`") + + exc_str = r"Argument `positive_deviation` must be a 1-dimensional.*0 dimensions" + with ( + self.assertRaisesRegex(ValueError, exc_str), + LoggingIntercept(level=logging.WARNING) as LOG4, + ): + # here `gamma` is the 0D array and therefore not a scalar, + # so it is swapped with `positive_deviation`, + # and an exception should be raised + # due to the type mismatch + CardinalitySet([0, 0], np.array(2), 2) + self.assertRegex(LOG4.getvalue(), r"DEPRECATED.*Order.*arguments.*`gamma`") + def test_error_on_cardinality_set_dim_change(self): """ Dimension is considered immutable. @@ -1866,7 +1915,7 @@ def test_error_on_cardinality_set_dim_change(self): set dimension (i.e. number of entries of `origin`). """ # construct a valid cardinality-constrained set - cset = CardinalitySet(origin=[0, 0], positive_deviation=[1, 1], gamma=2) + cset = CardinalitySet(origin=[0, 0], gamma=2, positive_deviation=[1, 1]) exc_str = r"Attempting to set.*dimension 2 to value of dimension 3" @@ -1883,7 +1932,7 @@ def test_set_as_constraint(self): Test method for setting up constraints works correctly. """ m = ConcreteModel() - cset = CardinalitySet([-0.5, 1, 2], [2.5, 3, 0], 1.5, [1.5, 0, 1]) + cset = CardinalitySet([-0.5, 1, 2], 1.5, [2.5, 3, 0], [1.5, 0, 1]) uq = cset.set_as_constraint(uncertain_params=None, block=m) self.assertEqual(len(uq.uncertainty_cons), 4) @@ -1927,7 +1976,7 @@ def test_set_as_constraint_dim_mismatch(self): """ m = ConcreteModel() m.v1 = Var(initialize=0) - cset = CardinalitySet([-0.5, 1, 2], [2.5, 3, 0], 1.5) + cset = CardinalitySet([-0.5, 1, 2], 1.5, [2.5, 3, 0]) with self.assertRaisesRegex(ValueError, ".*dimension"): cset.set_as_constraint(uncertain_params=[m.v1], block=m) @@ -1938,7 +1987,7 @@ def test_set_as_constraint_type_mismatch(self): """ m = ConcreteModel() m.p1 = Param([0, 1, 2], initialize=0, mutable=True) - cset = CardinalitySet([-0.5, 1, 2], [2.5, 3, 0], 1.5) + cset = CardinalitySet([-0.5, 1, 2], 1.5, [2.5, 3, 0]) with self.assertRaisesRegex(TypeError, ".*valid component type"): cset.set_as_constraint(uncertain_params=[m.p1[0], m.p1[1]], block=m) @@ -1948,8 +1997,8 @@ def test_set_as_constraint_type_mismatch(self): def test_point_in_set(self): cset = CardinalitySet( origin=[-0.5, 1, 2, 0], - positive_deviation=[2.5, 3, 0, 0], gamma=1.5, + positive_deviation=[2.5, 3, 0, 0], negative_deviation=[1.5, 0, 0, 3], ) @@ -1992,8 +2041,8 @@ def test_compute_exact_parameter_bounds(self): """ cset = CardinalitySet( origin=[-0.5, 1, 2, 0], - positive_deviation=[2.5, 3, 0, 0], gamma=1.5, + positive_deviation=[2.5, 3, 0, 0], negative_deviation=[1.5, 0, 0, 3], ) computed_bounds = cset._compute_exact_parameter_bounds(SolverFactory("baron")) @@ -2004,7 +2053,7 @@ def test_add_bounds_on_uncertain_parameters(self): m = ConcreteModel() m.uncertain_param_vars = Var([0, 1, 2], initialize=0) cset = CardinalitySet( - origin=[-0.5, 1, 2], positive_deviation=[2.5, 3, 0], gamma=1.5 + origin=[-0.5, 1, 2], gamma=1.5, positive_deviation=[2.5, 3, 0] ) cset._add_bounds_on_uncertain_parameters( @@ -2022,7 +2071,7 @@ def test_validate(self): # construct a valid cardinality-constrained set cardinality_set = CardinalitySet( - origin=[0.0, 0.0], positive_deviation=[1.0, 1.0], gamma=2 + origin=[0.0, 0.0], gamma=2, positive_deviation=[1.0, 1.0] ) # validate raises no issues on valid set @@ -2036,7 +2085,7 @@ def test_validate_finiteness(self): # construct a valid cardinality-constrained set cardinality_set = CardinalitySet( - origin=[0.0, 0.0], positive_deviation=[1.0, 1.0], gamma=2 + origin=[0.0, 0.0], gamma=2, positive_deviation=[1.0, 1.0] ) # check when values are not finite @@ -2063,7 +2112,7 @@ def test_validate_deviations(self): # construct a valid cardinality-constrained set cardinality_set = CardinalitySet( - origin=[0.0, 0.0], positive_deviation=[1.0, 1.0], gamma=2 + origin=[0.0, 0.0], gamma=2, positive_deviation=[1.0, 1.0] ) # positive_deviation has negative entries @@ -2086,7 +2135,7 @@ def test_validate_gamma(self): # construct a valid cardinality-constrained set cardinality_set = CardinalitySet( - origin=[0.0, 0.0], positive_deviation=[1.0, 1.0], gamma=2 + origin=[0.0, 0.0], gamma=2, positive_deviation=[1.0, 1.0] ) # check when gamma is invalid @@ -2113,7 +2162,7 @@ def test_bounded_and_nonempty(self): cardinality-constrained set. """ cardinality_set = CardinalitySet( - origin=[0, 0], positive_deviation=[1, 1], gamma=2 + origin=[0, 0], gamma=2, positive_deviation=[1, 1] ) bounded_and_nonempty_check(self, cardinality_set) @@ -2122,7 +2171,7 @@ def test_is_coordinate_fixed(self): Test method for checking whether there are coordinates constrained to a single value. """ - cset = CardinalitySet(origin=np.zeros(3), positive_deviation=[1, 1, 0], gamma=1) + cset = CardinalitySet(origin=np.zeros(3), gamma=1, positive_deviation=[1, 1, 0]) self.assertEqual( cset._is_coordinate_fixed(config=Bunch()), [False, False, True] ) @@ -3304,7 +3353,7 @@ def test_set_as_constraint(self): FactorModelSet( origin=[0, 1], number_of_factors=1, beta=0.75, psi_mat=[[1], [3]] ), - CardinalitySet([-0.5, -0.5], [2, 2], 2), + CardinalitySet([-0.5, -0.5], 2, [2, 2]), AxisAlignedEllipsoidalSet([0, 0, 0], [0.25, 0.25, 0.25]), ] ) @@ -3412,7 +3461,7 @@ def test_compute_exact_parameter_bounds(self): beta=0.75, psi_mat=[[1, 1], [1, 2]], ), - CardinalitySet([-0.5, -0.5], [2, 2], 2), + CardinalitySet([-0.5, -0.5], 2, [2, 2]), AxisAlignedEllipsoidalSet([0, 0, 1], [0.25, 0.8, 0.25]), ] ) @@ -3481,7 +3530,7 @@ def test_parameter_bounds(self): cpset = CartesianProductSet( [ BoxSet([(-0.5, 0.5)]), - CardinalitySet([-0.5, -0.5], [2, 2], 2), + CardinalitySet([-0.5, -0.5], 2, [2, 2]), AxisAlignedEllipsoidalSet([0, 0, 1], [0.25, 0.8, 0.25]), ] ) @@ -3572,7 +3621,7 @@ def test_add_bounds_on_uncertain_parameters(self): cpset = CartesianProductSet( [ BoxSet([(-0.5, 0.5)]), - CardinalitySet([-0.5, -0.5], [2, 2], 2), + CardinalitySet([-0.5, -0.5], 2, [2, 2]), AxisAlignedEllipsoidalSet([0, 0, 1], [0.25, 0.8, 0.25]), ] ) @@ -3694,7 +3743,7 @@ def test_compute_auxiliary_param_vals(self): FactorModelSet( origin=[0, 1], number_of_factors=1, beta=0.75, psi_mat=[[1], [4]] ), - CardinalitySet([-0.5, -0.5], [2, 2], 1, [1.5, 0]), + CardinalitySet([-0.5, -0.5], 1, [2, 2], [1.5, 0]), AxisAlignedEllipsoidalSet([0, 0, 0], [0.25, 0.25, 0.25]), ] ) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 0eff25298d0..040c946c7ea 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -27,6 +27,7 @@ from enum import Enum from pyomo.common.dependencies import numpy as np, scipy as sp +from pyomo.common.deprecation import deprecation_warning from pyomo.common.modeling import unique_component_name from pyomo.core.base import ( Block, @@ -1474,14 +1475,14 @@ class CardinalitySet(UncertaintySet): ---------- origin : (N,) array_like Origin of the set (e.g., nominal uncertain parameter values). - positive_deviation : (N,) array_like - Maximal absolute deviation from the origin in the - positive coordinate direction. gamma : numeric type Upper bound for the number of coordinates that can simultaneously realize their maximal deviations from the origin. Must be a numerical value ranging from 0 to the set dimension `N`. + positive_deviation : (N,) array_like + Maximal absolute deviation from the origin in the + positive coordinate direction. negative_deviation : (N,) array_like, optional Maximal absolute deviation from the origin in the negative coordinate direction. @@ -1548,27 +1549,47 @@ class CardinalitySet(UncertaintySet): >>> from pyomo.contrib.pyros import CardinalitySet >>> gamma_set = CardinalitySet( ... origin=[0, 0, 0, 0], - ... positive_deviation=[1.0, 2.0, 1.5, 0.0], ... gamma=1, + ... positive_deviation=[1.0, 2.0, 1.5, 0.0], ... negative_deviation=[0.0, 2.0, 0.0, 5.0], ... ) >>> gamma_set.origin array([0, 0, 0, 0]) - >>> gamma_set.positive_deviation - array([1. , 2. , 1.5, 0. ]) >>> gamma_set.gamma 1 + >>> gamma_set.positive_deviation + array([1. , 2. , 1.5, 0. ]) >>> gamma_set.negative_deviation array([0., 2., 0., 5.]) """ _PARAMETER_BOUNDS_EXACT = True - def __init__(self, origin, positive_deviation, gamma, negative_deviation=None): + def __init__(self, origin, gamma, positive_deviation, negative_deviation=None): """Initialize self (see class docstring).""" self.origin = origin - self.positive_deviation = positive_deviation - self.gamma = gamma + + if np.isscalar(gamma): + self.gamma = gamma + self.positive_deviation = positive_deviation + else: + # for backward compatibility, silently allow user + # to swap arguments `gamma` and `positive_deviation`, + # if `gamma` is not a scalar + deprecation_warning( + ( + f"Order of {type(self).__name__} arguments `gamma` " + "and `positive_deviation` has been swapped, " + "as `gamma` is not a scalar object. " + "Ensure that `gamma` is a scalar object and " + "(if both arguments are passed positionally) " + "passed before `positive_deviation`." + ), + version="6.10.2.dev0", + ) + self.gamma = positive_deviation + self.positive_deviation = gamma + if negative_deviation is None: negative_deviation = np.zeros(self.dim) self.negative_deviation = negative_deviation From eb696968ce3793ffd409275ae0285060cfd4fa24 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 15 Jun 2026 08:35:50 -0600 Subject: [PATCH 11/12] NFC: apply black --- pyomo/contrib/pyros/config.py | 138 ++++++------------ pyomo/contrib/pyros/tests/test_grcs.py | 6 +- .../contrib/pyros/tests/test_preprocessor.py | 12 +- 3 files changed, 52 insertions(+), 104 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 9d4637be792..786f0e46536 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -490,13 +490,11 @@ def pyros_config(): ConfigValue( default=None, domain=NonNegativeFloat, - doc=( - """ + doc=(""" Wall time limit for the execution of the PyROS solver in seconds (including time spent by subsolvers). If `None` is provided, then no time limit is enforced. - """ - ), + """), ), ) CONFIG.declare( @@ -504,14 +502,12 @@ def pyros_config(): ConfigValue( default=False, domain=bool, - description=( - """ + description=(""" Export subproblems with a non-acceptable termination status for debugging purposes. If True is provided, then the argument `subproblem_file_directory` must also be specified. - """ - ), + """), ), ) CONFIG.declare( @@ -527,12 +523,10 @@ def pyros_config(): ConfigValue( default=True, domain=bool, - description=( - """ + description=(""" Load final solution(s) found by PyROS to the deterministic model provided. - """ - ), + """), ), ) CONFIG.declare( @@ -540,14 +534,12 @@ def pyros_config(): ConfigValue( default=False, domain=bool, - description=( - """ + description=(""" True to ensure the component names given to the subordinate solvers for every subproblem reflect the names of the corresponding Pyomo modeling components, False otherwise. - """ - ), + """), ), ) @@ -583,15 +575,13 @@ def pyros_config(): cdatatype_validator=uncertain_param_data_validator, allow_repeats=False, ), - description=( - """ + description=(""" Uncertain model parameters. Of every constituent `Param` object, the `mutable` attribute must be set to True. All constituent `Var`/`VarData` objects should be fixed. - """ - ), + """), visibility=1, ), ) @@ -600,13 +590,11 @@ def pyros_config(): ConfigValue( default=None, domain=IsInstance(UncertaintySet), - description=( - """ + description=(""" Uncertainty set against which the final solution(s) returned by PyROS should be certified to be robust. - """ - ), + """), visibility=1, ), ) @@ -638,15 +626,12 @@ def pyros_config(): ConfigValue( default=ObjectiveType.nominal, domain=InEnum(ObjectiveType), - description=( - """ + description=(""" Choice of objective focus to optimize in the master problems. Choices are: `ObjectiveType.worst_case`, `ObjectiveType.nominal`. - """ - ), - doc=( - """ + """), + doc=(""" Objective focus for the master problems: - `ObjectiveType.nominal`: @@ -663,8 +648,7 @@ def pyros_config(): by PyROS. If a nominal objective focus is chosen, then only robust feasibility is guaranteed. - """ - ), + """), ), ) CONFIG.declare( @@ -672,15 +656,13 @@ def pyros_config(): ConfigValue( default=[], domain=list, - doc=( - """ + doc=(""" Nominal uncertain parameter realization. Entries should be provided in an order consistent with the entries of the argument `uncertain_params`. If an empty list is provided, then the values of the `Param` objects specified through `uncertain_params` are chosen. - """ - ), + """), ), ) CONFIG.declare( @@ -688,15 +670,12 @@ def pyros_config(): ConfigValue( default=0, domain=In([0, 1, 2]), - description=( - """ + description=(""" Order (or degree) of the polynomial decision rule functions used for approximating the adjustability of the second stage variables with respect to the uncertain parameters. - """ - ), - doc=( - """ + """), + doc=(""" Order (or degree) of the polynomial decision rule functions for approximating the adjustability of the second stage variables with respect to the uncertain parameters. @@ -706,8 +685,7 @@ def pyros_config(): - 0: static recourse - 1: affine recourse - 2: quadratic recourse - """ - ), + """), ), ) CONFIG.declare( @@ -715,8 +693,7 @@ def pyros_config(): ConfigValue( default=False, domain=bool, - doc=( - """ + doc=(""" True to solve all master problems with the subordinate global solver, False to solve all master problems with the subordinate local solver. @@ -726,8 +703,7 @@ def pyros_config(): for certification of robust optimality of the final solution(s) returned by PyROS. Otherwise, only robust feasibility is guaranteed. - """ - ), + """), ), ) CONFIG.declare( @@ -735,12 +711,10 @@ def pyros_config(): ConfigValue( default=-1, domain=positive_int_or_minus_one, - description=( - """ + description=(""" Iteration limit. If -1 is provided, then no iteration limit is enforced. - """ - ), + """), ), ) CONFIG.declare( @@ -748,12 +722,10 @@ def pyros_config(): ConfigValue( default=1e-4, domain=NonNegativeFloat, - description=( - """ + description=(""" Relative tolerance for assessing maximal inequality constraint violations during the GRCS separation step. - """ - ), + """), ), ) CONFIG.declare( @@ -761,8 +733,7 @@ def pyros_config(): ConfigValue( default={}, domain=_deprecated_separation_priority_order, - doc=( - """ + doc=(""" (DEPRECATED) A dict-like object, each entry of which maps the full name of a model ``Var`` or ``Constraint`` @@ -788,8 +759,7 @@ def pyros_config(): Specify separation priorities by declaring, on your model, `Suffix` components with local name 'pyros_separation_priority'. - """ - ), + """), ), ) CONFIG.declare( @@ -797,8 +767,7 @@ def pyros_config(): ConfigValue( default=default_pyros_solver_logger, domain=logger_domain, - doc=( - """ + doc=(""" Logger (or name thereof) used for reporting PyROS solver progress. If `None` or a `str` is provided, then ``progress_logger`` @@ -806,8 +775,7 @@ def pyros_config(): In the default case, `progress_logger` is set to a :class:`pyomo.contrib.pyros.util.PreformattedLogger` object of level ``logging.INFO``. - """ - ), + """), ), ) CONFIG.declare( @@ -819,13 +787,11 @@ def pyros_config(): require_available=False, filter_by_availability=True, ), - doc=( - """ + doc=(""" Additional subordinate local NLP optimizers to invoke in the event the primary local NLP optimizer fails to solve a subproblem to an acceptable termination condition. - """ - ), + """), ), ) CONFIG.declare( @@ -837,13 +803,11 @@ def pyros_config(): require_available=False, filter_by_availability=True, ), - doc=( - """ + doc=(""" Additional subordinate global NLP optimizers to invoke in the event the primary global NLP optimizer fails to solve a subproblem to an acceptable termination condition. - """ - ), + """), ), ) CONFIG.declare( @@ -851,15 +815,13 @@ def pyros_config(): ConfigValue( default=None, domain=Path(), - description=( - """ + description=(""" Directory to which to export subproblems not successfully solved to an acceptable termination condition. In the event ``keepfiles=True`` is specified, a str or path-like referring to an existing directory must be provided. - """ - ), + """), ), ) CONFIG.declare( @@ -869,8 +831,7 @@ def pyros_config(): # note: we leave all validation of the dict entries # to ``BlockData.write()`` domain=dict, - description=( - """ + description=(""" File format options for writing/exporting subproblems that were not solved to an acceptable level if ``keepfiles=True`` is specified. @@ -878,8 +839,7 @@ def pyros_config(): format (e.g., 'bar' for BARON, 'gams' for GAMS) to a value for the argument ``io_options`` to the method ``BlockData.write()``. - """ - ), + """), ), ) @@ -891,16 +851,14 @@ def pyros_config(): ConfigValue( default=False, domain=bool, - description=( - """ + description=(""" This is an advanced option. Solve all separation subproblems with the subordinate global solver(s) only. This option is useful for expediting PyROS in the event that the subordinate global optimizer(s) provided can quickly solve separation subproblems to global optimality. - """ - ), + """), ), ) CONFIG.declare( @@ -908,8 +866,7 @@ def pyros_config(): ConfigValue( default=False, domain=bool, - doc=( - """ + doc=(""" This is an advanced option. Solve all separation subproblems with the subordinate local solver(s) only. @@ -920,8 +877,7 @@ def pyros_config(): in the event that the subordinate global optimizer provided cannot tractably solve separation subproblems to global optimality. - """ - ), + """), ), ) CONFIG.declare( @@ -929,8 +885,7 @@ def pyros_config(): ConfigValue( default={}, domain=dict, - doc=( - """ + doc=(""" This is an advanced option. Add p-robustness constraints to all master subproblems. If an empty dict is provided, then p-robustness constraints @@ -942,8 +897,7 @@ def pyros_config(): objective function value under any PyROS-sampled uncertain parameter realization to the objective function under the nominal parameter realization. - """ - ), + """), visibility=1, ), ) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 6bbb0bc7fd1..aec405a6cb1 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -3359,8 +3359,7 @@ def test_log_model_statistics(self): state_variables=[m.y], ) - expected_log_str = textwrap.dedent( - """ + expected_log_str = textwrap.dedent(""" Model Statistics (before preprocessing): Number of variables : 3 First-stage variables : 2 @@ -3370,8 +3369,7 @@ def test_log_model_statistics(self): Number of constraints : 3 Equality constraints : 1 Inequality constraints : 2 - """ - ) + """) with LoggingIntercept(module=__name__, level=logging.DEBUG) as LOG: log_original_model_statistics(model_data, user_var_partitioning) diff --git a/pyomo/contrib/pyros/tests/test_preprocessor.py b/pyomo/contrib/pyros/tests/test_preprocessor.py index e24a4c4aed2..e5f833cb649 100644 --- a/pyomo/contrib/pyros/tests/test_preprocessor.py +++ b/pyomo/contrib/pyros/tests/test_preprocessor.py @@ -3195,8 +3195,7 @@ def test_preprocessor_log_model_statistics_affine_dr(self, obj_focus): preprocess_model_data(model_data, user_var_partitioning) # expected model stats worked out by hand - expected_log_str = textwrap.dedent( - f""" + expected_log_str = textwrap.dedent(f""" Model Statistics (after preprocessing): Number of variables : 16 Epigraph variable : 1 @@ -3214,8 +3213,7 @@ def test_preprocessor_log_model_statistics_affine_dr(self, obj_focus): Inequality constraints : 16 First-stage inequalities : {4 if obj_focus == 'nominal' else 3} Second-stage inequalities : {12 if obj_focus == 'nominal' else 13} - """ - ) + """) with LoggingIntercept(module=__name__, level=logging.DEBUG) as LOG: log_preprocessed_model_statistics(model_data) @@ -3246,8 +3244,7 @@ def test_preprocessor_log_model_statistics_quadratic_dr(self, obj_focus): preprocess_model_data(model_data, user_var_partitioning) # expected model stats worked out by hand - expected_log_str = textwrap.dedent( - f""" + expected_log_str = textwrap.dedent(f""" Model Statistics (after preprocessing): Number of variables : 22 Epigraph variable : 1 @@ -3265,8 +3262,7 @@ def test_preprocessor_log_model_statistics_quadratic_dr(self, obj_focus): Inequality constraints : 18 First-stage inequalities : {4 if obj_focus == 'nominal' else 3} Second-stage inequalities : {14 if obj_focus == 'nominal' else 15} - """ - ) + """) with LoggingIntercept(module=__name__, level=logging.DEBUG) as LOG: log_preprocessed_model_statistics(model_data) From dfbde86f1ab6d3ea1feface4202dffede176eb34 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 15 Jun 2026 08:36:56 -0600 Subject: [PATCH 12/12] Doc: adjust Pyomo release --- doc/OnlineDocs/explanation/solvers/pyros/solver_log.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/explanation/solvers/pyros/solver_log.rst b/doc/OnlineDocs/explanation/solvers/pyros/solver_log.rst index 2ae7d46f45b..99f2225b346 100644 --- a/doc/OnlineDocs/explanation/solvers/pyros/solver_log.rst +++ b/doc/OnlineDocs/explanation/solvers/pyros/solver_log.rst @@ -27,7 +27,7 @@ your console output will, by default, look like this: ============================================================================== PyROS: The Pyomo Robust Optimization Solver, v1.3.15. - Pyomo version: 6.10.2.dev0 + Pyomo version: 6.10.1 Commit hash: unknown Invoked at UTC 2026-06-05T00:00:00.000000+00:00