diff --git a/doc/OnlineDocs/explanation/solvers/pyros/solver_log.rst b/doc/OnlineDocs/explanation/solvers/pyros/solver_log.rst index 2459eafed50..99f2225b346 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.1 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, diff --git a/pyomo/contrib/pyros/CHANGELOG.txt b/pyomo/contrib/pyros/CHANGELOG.txt index 1ebf2bc60ce..58d1e42ebdc 100644 --- a/pyomo/contrib/pyros/CHANGELOG.txt +++ b/pyomo/contrib/pyros/CHANGELOG.txt @@ -4,9 +4,10 @@ PyROS CHANGELOG ------------------------------------------------------------------------------- -PyROS 1.3.15 12 May 2026 +PyROS 1.3.15 04 Jun 2026 ------------------------------------------------------------------------------- - Extend `CardinalitySet` to allow for negative deviations +- Update PyROS solver output log introductory information ------------------------------------------------------------------------------- diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 7013ca3fa24..51a95d52b45 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -194,15 +194,19 @@ 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 +216,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 +419,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..aec405a6cb1 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( @@ -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: 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 33e68a53859..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,17 +1475,17 @@ 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. 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 - 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 +1539,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 -------- @@ -1547,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 @@ -1612,8 +1634,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 @@ -1643,8 +1665,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 @@ -3807,20 +3829,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([...]) @@ -4095,18 +4116,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):