From 8ffe20ce2a47b353a590c0782d5e04a874ce076c Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Mon, 7 Apr 2025 15:31:35 -0400 Subject: [PATCH 01/83] update UncertaintySet is_bounded, is_nonempty --- pyomo/contrib/pyros/uncertainty_sets.py | 158 +++++++++++++++++++++--- 1 file changed, 140 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index a4b6ba6aa1a..cfb84e32137 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -36,6 +36,7 @@ minimize, Var, VarData, + NonNegativeReals ) from pyomo.core.expr import mutable_expression, native_numeric_types, value from pyomo.core.util import quicksum, dot_product @@ -502,6 +503,9 @@ def parameter_bounds(self): """ Bounds for the value of each uncertain parameter constrained by the set (i.e. bounds for each set dimension). + + This method should return an empty list if it can't be calculated + or a list of length = self.dim if it can. """ raise NotImplementedError @@ -552,23 +556,32 @@ def is_bounded(self, config): Notes ----- - This check is carried out by solving a sequence of maximization - and minimization problems (in which the objective for each - problem is the value of a single uncertain parameter). If any of - the optimization models cannot be solved successfully to + This check is carried out by checking if all parameter bounds + are finite. + + If no parameter bounds are available, the check is done by + solving a sequence of maximization and minimization problems + (in which the objective for each problem is the value of a + single uncertain parameter). + If any of the optimization models cannot be solved successfully to optimality, then False is returned. - This method is invoked during the validation step of a PyROS - solver call. + This method is invoked by validate. """ - # initialize uncertain parameter variables - param_bounds_arr = np.array( - self._compute_parameter_bounds(solver=config.global_solver) - ) + # use parameter bounds if they are available + param_bounds_arr = self.parameter_bounds + if param_bounds_arr: + all_bounds_finite = np.all(np.isfinite(param_bounds_arr)) + else: + # initialize uncertain parameter variables + param_bounds_arr = np.array( + self._compute_parameter_bounds(solver=config.global_solver) + ) + all_bounds_finite = np.all(np.isfinite(param_bounds_arr)) - all_bounds_finite = np.all(np.isfinite(param_bounds_arr)) + # log result if not all_bounds_finite: - config.progress_logger.info( + config.progress_logger.error( "Computed coordinate value bounds are not all finite. " f"Got bounds: {param_bounds_arr}" ) @@ -577,16 +590,73 @@ def is_bounded(self, config): def is_nonempty(self, config): """ - Return True if the uncertainty set is nonempty, else False. + Determine whether the uncertainty set is nonempty. + + Parameters + ---------- + config : ConfigDict + PyROS solver configuration. + + Returns + ------- + : bool + True if the nominal point is within the set, + and False otherwise. """ - return self.is_bounded(config) + # check if nominal point is in set for quick test + set_nonempty = False + if config.nominal_uncertain_param_vals: + if self.point_in_set(config.nominal_uncertain_param_vals): + set_nonempty = True + else: + # construct feasibility problem and solve otherwise + set_nonempty = self._solve_feasibility(config.global_solver) + + # parameter bounds for logging + param_bounds_arr = self.parameter_bounds + if not param_bounds_arr: + param_bounds_arr = np.array( + self._compute_parameter_bounds(solver=config.global_solver) + ) + + # log result + if not set_nonempty: + config.progress_logger.error( + "Nominal point is not within the uncertainty set. " + f"Set parameter bounds: {param_bounds_arr}" + f"Got nominal point: {config.nominal_uncertain_param_vals}" + ) + + return set_nonempty - def is_valid(self, config): + def validate(self, config): """ - Return True if the uncertainty set is bounded and non-empty, - else False. + Validate the uncertainty set with a nonemptiness and boundedness check. + + Parameters + ---------- + config : ConfigDict + PyROS solver configuration. + + Raises + ------ + ValueError + If nonemptiness check or boundedness check fail. """ - return self.is_nonempty(config=config) and self.is_bounded(config=config) + check_nonempty = self.is_nonempty(config=config) + check_bounded = self.is_bounded(config=config) + + if not check_nonempty: + raise ValueError( + "Failed nonemptiness check. Nominal point is not in the set. " + f"Nominal point:\n {config.nominal_uncertain_param_vals}." + ) + + if not check_bounded: + raise ValueError( + "Failed boundedness check. Parameter bounds are not finite. " + f"Parameter bounds:\n {self.parameter_bounds}." + ) @abc.abstractmethod def set_as_constraint(self, uncertain_params=None, block=None): @@ -698,6 +768,58 @@ def _compute_parameter_bounds(self, solver): return param_bounds + def _solve_feasibility(self, solver): + """ + Construct and solve feasibility problem using uncertainty set + constraints and parameter bounds using `set_as_constraint` and + `_add_bounds_on_uncertain_parameters` of self. + + Parameters + ---------- + solver : Pyomo solver + Optimizer capable of solving bounding problems to + global optimality. + + Returns + ------- + : bool + True if the feasibility problem solves successfully, + and raises an exception otherwise + + Raises + ------ + ValueError + If feasibility problem fails to solve. + """ + model = ConcreteModel() + model.u = Var(within=NonNegativeReals) + + # construct param vars + model.param_vars = Var(range(self.dim)) + + # add bounds on param vars + self._add_bounds_on_uncertain_parameters( + model.param_vars, global_solver=solver + ) + + # add constraints + self.set_as_constraint(uncertain_params=model.param_vars, block=model) + + # add objective with dummy variable model.u + @model.Objective(sense=minimize) + def feasibility_objective(self): + return model.u + + # solve feasibility problem + res = solver.solve(model, load_solutions=False) + if not check_optimal_termination(res): + raise ValueError( + "Could not successfully solve feasibility problem. " + f"Solver status summary:\n {res.solver}." + ) + + return True + def _add_bounds_on_uncertain_parameters( self, uncertain_param_vars, global_solver=None ): From 4cd7f8df4cd6c770dabf3e89eb3b48b8578f48cc Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Mon, 7 Apr 2025 15:34:52 -0400 Subject: [PATCH 02/83] change is_valid to validate and preprocess order --- pyomo/contrib/pyros/util.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index a3206b2ccbb..9e93c5773b5 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -926,8 +926,7 @@ def validate_uncertainty_specification(model, config): `config.second_stage_variables` - dimension of uncertainty set does not equal number of uncertain parameters - - uncertainty set `is_valid()` method does not return - true. + - uncertainty set `validate()` method fails. - nominal parameter realization is not in the uncertainty set. """ check_components_descended_from_model( @@ -964,13 +963,6 @@ def validate_uncertainty_specification(model, config): f"({len(config.uncertain_params)} != {config.uncertainty_set.dim})." ) - # validate uncertainty set - if not config.uncertainty_set.is_valid(config=config): - raise ValueError( - f"Uncertainty set {config.uncertainty_set} is invalid, " - "as it is either empty or unbounded." - ) - # fill-in nominal point as necessary, if not provided. # otherwise, check length matches uncertainty dimension if not config.nominal_uncertain_param_vals: @@ -993,6 +985,9 @@ def validate_uncertainty_specification(model, config): f"{len(config.nominal_uncertain_param_vals)})." ) + # validate uncertainty set + config.uncertainty_set.validate(config=config) + # uncertainty set should contain nominal point nominal_point_in_set = config.uncertainty_set.point_in_set( point=config.nominal_uncertain_param_vals From 8609c3242ebe6b76b186330f89e726dfbe3ff89d Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Mon, 7 Apr 2025 15:37:33 -0400 Subject: [PATCH 03/83] Add setter for CustomUncertaintySet --- pyomo/contrib/pyros/tests/test_uncertainty_sets.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index e0e5bb7d137..7e5cf1cd6d4 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -2429,6 +2429,7 @@ class CustomUncertaintySet(UncertaintySet): def __init__(self, dim): self._dim = dim + self._parameter_bounds = [(-1, 1)] * self.dim @property def geometry(self): @@ -2464,7 +2465,12 @@ def point_in_set(self, point): @property def parameter_bounds(self): - return [(-1, 1)] * self.dim + return self._parameter_bounds + + @parameter_bounds.setter + def parameter_bounds(self, val): + self._parameter_bounds = val + class TestCustomUncertaintySet(unittest.TestCase): From 2e076942bb18185193df293163754b79e032467c Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Mon, 7 Apr 2025 15:38:15 -0400 Subject: [PATCH 04/83] Add tests for is_bounded, is_nonempty --- .../pyros/tests/test_uncertainty_sets.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 7e5cf1cd6d4..620281030f4 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -44,6 +44,9 @@ _setup_standard_uncertainty_set_constraint_block, ) +from pyomo.contrib.pyros.config import pyros_config +import time + import logging logger = logging.getLogger(__name__) @@ -2503,6 +2506,68 @@ def test_compute_parameter_bounds(self): self.assertEqual(custom_set.parameter_bounds, [(-1, 1)] * 2) self.assertEqual(custom_set._compute_parameter_bounds(baron), [(-1, 1)] * 2) + # test default is_bounded + @unittest.skipUnless(baron_available, "BARON is not available") + def test_is_bounded(self): + """ + Test boundedness check computations give expected results. + """ + custom_set = CustomUncertaintySet(dim=2) + CONFIG = pyros_config() + CONFIG.global_solver = global_solver + + # using provided parameter_bounds + start = time.time() + self.assertTrue(custom_set.is_bounded(config=CONFIG), "Set is not bounded") + end = time.time() + time_with_bounds_provided = end - start + + # when parameter_bounds is not available + custom_set.parameter_bounds = None + start = time.time() + self.assertTrue(custom_set.is_bounded(config=CONFIG), "Set is not bounded") + end = time.time() + time_without_bounds_provided = end - start + + # check with parameter_bounds should always take less time than solving 2N + # optimization problems + self.assertLess(time_with_bounds_provided, time_without_bounds_provided, + "Boundedness check with provided parameter_bounds took longer than expected.") + + # when bad bounds are provided + for val_str in ["inf", "nan"]: + bad_bounds = [[1, float(val_str)], [2, 3]] + custom_set.parameter_bounds = bad_bounds + self.assertFalse(custom_set.is_bounded(config=CONFIG), "Set is bounded") + + # test default is_nonempty + @unittest.skipUnless(baron_available, "BARON is not available") + def test_is_nonempty(self): + """ + Test nonemptiness check computations give expected results. + """ + custom_set = CustomUncertaintySet(dim=2) + CONFIG = pyros_config() + CONFIG.global_solver = global_solver + + # constructing a feasibility problem + self.assertTrue(custom_set.is_nonempty(config=CONFIG), "Set is empty") + + # using provided nominal point + CONFIG.nominal_uncertain_param_vals = [0, 0] + self.assertTrue(custom_set.is_nonempty(config=CONFIG), "Set is empty") + + # check when nominal point is not in set + CONFIG.nominal_uncertain_param_vals = [-2, -2] + self.assertFalse(custom_set.is_nonempty(config=CONFIG), "Nominal point is in set") + + # check when feasibility problem fails + CONFIG.nominal_uncertain_param_vals = None + custom_set.parameter_bounds = [[1, 2], [3, 4]] + exc_str = r"Could not successfully solve feasibility problem. .*" + with self.assertRaisesRegex(ValueError, exc_str): + custom_set.is_nonempty(config=CONFIG) + if __name__ == "__main__": unittest.main() From 6bd2d66ce78c0535c1022fd6b959524fabc2c129 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Mon, 7 Apr 2025 17:16:48 -0400 Subject: [PATCH 05/83] Move BoxSet setter checks to validate --- pyomo/contrib/pyros/uncertainty_sets.py | 27 +++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index cfb84e32137..7800c439a0d 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -1139,10 +1139,6 @@ def bounds(self, val): bounds_arr = np.array(val) - for lb, ub in bounds_arr: - if lb > ub: - raise ValueError(f"Lower bound {lb} exceeds upper bound {ub}") - # box set dimension is immutable if hasattr(self, "_bounds") and bounds_arr.shape[0] != self.dim: raise ValueError( @@ -1203,6 +1199,29 @@ def set_as_constraint(self, uncertain_params=None, block=None): auxiliary_vars=aux_var_list, ) + def validate(self, config): + """ + Check BoxSet validity. + + Raises + ------ + ValueError + If finiteness and LB<=UB checks fail. + """ + bounds_arr = np.array(self.parameter_bounds) + + # finiteness check + if not np.all(np.isfinite(bounds_arr)): + raise ValueError( + "Not all bounds are finite. " + f"Got bounds:\n {bounds_arr}" + ) + + # check LB <= UB + for lb, ub in bounds_arr: + if lb > ub: + raise ValueError(f"Lower bound {lb} exceeds upper bound {ub}") + class CardinalitySet(UncertaintySet): """ From cfb93993b600fa3add2c012171fa0d65d0ee2322 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Mon, 7 Apr 2025 17:17:19 -0400 Subject: [PATCH 06/83] Add bounded_and_nonempty_check --- .../contrib/pyros/tests/test_uncertainty_sets.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 620281030f4..ac4a51e492e 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -73,6 +73,22 @@ baron_version = (0, 0, 0) +def bounded_and_nonempty_check(unc_set): + """ + All uncertainty sets should pass these checks, + regardless of their custom `validate` method. + """ + CONFIG = pyros_config() + CONFIG.global_solver = global_solver + + # check is_bounded + check_bounded = unc_set.is_bounded(config=CONFIG) + # check is_nonempty + check_nonempty = unc_set.is_nonempty(config=CONFIG) + + return check_bounded and check_nonempty + + class TestBoxSet(unittest.TestCase): """ Tests for the BoxSet. From 010047d7ddb7f2330dc43b5714e4f2ccc8524914 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Mon, 7 Apr 2025 17:18:49 -0400 Subject: [PATCH 07/83] Add test_validate for BoxSet --- .../pyros/tests/test_uncertainty_sets.py | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index ac4a51e492e..02a00c9d9ca 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -377,6 +377,42 @@ def test_add_bounds_on_uncertain_parameters(self): self.assertEqual(m.uncertain_param_vars[0].bounds, (1, 2)) self.assertEqual(m.uncertain_param_vars[1].bounds, (3, 4)) + def test_validate(self): + """ + Test validate checks perform as expected. + """ + CONFIG = pyros_config() + CONFIG.global_solver = global_solver + + # construct valid box set + box_set = BoxSet(bounds=[[1., 2.], [3., 4.]]) + + # validate raises no issues on valid set + box_set.validate(config=CONFIG) + + # check when bounds are not finite + box_set.bounds[0][0] = np.nan + exc_str = r"Not all bounds are finite. Got bounds:.*" + with self.assertRaisesRegex(ValueError, exc_str): + box_set.validate(config=CONFIG) + + # check when LB >= UB + box_set.bounds[0][0] = 5 + exc_str = r"Lower bound 5.0 exceeds upper bound 2.0" + with self.assertRaisesRegex(ValueError, exc_str): + box_set.validate(config=CONFIG) + + @unittest.skipUnless(baron_available, "BARON is not available") + def test_bounded_and_nonempty(self): + """ + Test `is_bounded` and `is_nonempty` for a valid box set. + """ + box_set = BoxSet(bounds=[[1., 2.], [3., 4.]]) + self.assertTrue( + bounded_and_nonempty_check(box_set), + "Set is not bounded or not nonempty" + ) + class TestBudgetSet(unittest.TestCase): """ @@ -2491,7 +2527,6 @@ def parameter_bounds(self, val): self._parameter_bounds = val - class TestCustomUncertaintySet(unittest.TestCase): """ Test for a custom uncertainty set subclass. From 13cd9de117c984913702cea17b5b2abde80551bc Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Mon, 7 Apr 2025 17:19:25 -0400 Subject: [PATCH 08/83] Remove tests for BoxSet setters --- .../pyros/tests/test_uncertainty_sets.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 02a00c9d9ca..58722ce41f5 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -125,25 +125,6 @@ def test_error_on_box_set_dim_change(self): with self.assertRaisesRegex(ValueError, exc_str): bset.bounds = [[1, 2], [3, 4], [5, 6]] - def test_error_on_lb_exceeds_ub(self): - """ - Test exception raised when an LB exceeds a UB. - """ - bad_bounds = [[1, 2], [4, 3]] - - exc_str = r"Lower bound 4 exceeds upper bound 3" - - # assert error on construction - with self.assertRaisesRegex(ValueError, exc_str): - BoxSet(bad_bounds) - - # construct a valid box set - bset = BoxSet([[1, 2], [3, 4]]) - - # assert error on update - with self.assertRaisesRegex(ValueError, exc_str): - bset.bounds = bad_bounds - def test_error_on_ragged_bounds_array(self): """ Test ValueError raised on attempting to set BoxSet bounds From 1f9a8f8941fc32e4607efb952ecf652cd33f3d00 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 10:32:00 -0400 Subject: [PATCH 09/83] Remove unnecessary parameter_bounds logging --- pyomo/contrib/pyros/uncertainty_sets.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 7800c439a0d..b6f0e3c4fc0 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -612,18 +612,10 @@ def is_nonempty(self, config): # construct feasibility problem and solve otherwise set_nonempty = self._solve_feasibility(config.global_solver) - # parameter bounds for logging - param_bounds_arr = self.parameter_bounds - if not param_bounds_arr: - param_bounds_arr = np.array( - self._compute_parameter_bounds(solver=config.global_solver) - ) - # log result if not set_nonempty: config.progress_logger.error( "Nominal point is not within the uncertainty set. " - f"Set parameter bounds: {param_bounds_arr}" f"Got nominal point: {config.nominal_uncertain_param_vals}" ) From a0a92dc4fe2168e25cb6092fcf8aca03107c6b3b Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 11:26:26 -0400 Subject: [PATCH 10/83] Update BoxSet validate test,bounded/nonempty check --- .../pyros/tests/test_uncertainty_sets.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 58722ce41f5..ba7909024af 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -23,6 +23,7 @@ scipy as sp, scipy_available, ) +from pyomo.common.collections import Bunch from pyomo.environ import SolverFactory from pyomo.core.base import ConcreteModel, Param, Var from pyomo.core.expr import RangedExpression @@ -73,7 +74,7 @@ baron_version = (0, 0, 0) -def bounded_and_nonempty_check(unc_set): +def bounded_and_nonempty_check(test, unc_set): """ All uncertainty sets should pass these checks, regardless of their custom `validate` method. @@ -82,11 +83,16 @@ def bounded_and_nonempty_check(unc_set): CONFIG.global_solver = global_solver # check is_bounded - check_bounded = unc_set.is_bounded(config=CONFIG) - # check is_nonempty - check_nonempty = unc_set.is_nonempty(config=CONFIG) + test.assertTrue( + unc_set.is_bounded(config=CONFIG), + "Set is not bounded." + ) - return check_bounded and check_nonempty + # check is_nonempty + test.assertTrue( + unc_set.is_nonempty(config=CONFIG), + "Set is not bounded." + ) class TestBoxSet(unittest.TestCase): @@ -362,8 +368,7 @@ def test_validate(self): """ Test validate checks perform as expected. """ - CONFIG = pyros_config() - CONFIG.global_solver = global_solver + CONFIG = Bunch() # construct valid box set box_set = BoxSet(bounds=[[1., 2.], [3., 4.]]) @@ -389,10 +394,7 @@ def test_bounded_and_nonempty(self): Test `is_bounded` and `is_nonempty` for a valid box set. """ box_set = BoxSet(bounds=[[1., 2.], [3., 4.]]) - self.assertTrue( - bounded_and_nonempty_check(box_set), - "Set is not bounded or not nonempty" - ) + bounded_and_nonempty_check(self, box_set), class TestBudgetSet(unittest.TestCase): From c032dd80d8346aa6f2c176c0412817a261e85456 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 17:17:11 -0400 Subject: [PATCH 11/83] Add test for _solve_feasibility --- .../pyros/tests/test_uncertainty_sets.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index ba7909024af..67b29abca6c 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -91,7 +91,7 @@ def bounded_and_nonempty_check(test, unc_set): # check is_nonempty test.assertTrue( unc_set.is_nonempty(config=CONFIG), - "Set is not bounded." + "Set is empty." ) @@ -2540,6 +2540,23 @@ def test_compute_parameter_bounds(self): self.assertEqual(custom_set.parameter_bounds, [(-1, 1)] * 2) self.assertEqual(custom_set._compute_parameter_bounds(baron), [(-1, 1)] * 2) + @unittest.skipUnless(baron_available, "BARON is not available") + def test_solve_feasibility(self): + """ + Test uncertainty set feasibility problem gives expected results. + """ + # feasibility problem passes + baron = SolverFactory("baron") + custom_set = CustomUncertaintySet(dim=2) + self.assertTrue(custom_set._solve_feasibility(baron)) + + # feasibility problem fails + custom_set.parameter_bounds = [[1, 2], [3, 4]] + exc_str = r"Could not successfully solve feasibility problem. .*" + with self.assertRaisesRegex(ValueError, exc_str): + custom_set._solve_feasibility(baron) + + # test default is_bounded @unittest.skipUnless(baron_available, "BARON is not available") def test_is_bounded(self): From 8a8b74631449fee6606fca05482d3919b0d1d3fc Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 17:21:36 -0400 Subject: [PATCH 12/83] Move CardinalitySet setter checks to validate --- pyomo/contrib/pyros/uncertainty_sets.py | 51 ++++++++++++++++++------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index b6f0e3c4fc0..02e012e166d 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -1310,13 +1310,6 @@ def positive_deviation(self, val): valid_type_desc="a valid numeric type", ) - for dev_val in val: - if dev_val < 0: - raise ValueError( - f"Entry {dev_val} of attribute 'positive_deviation' " - f"is negative value" - ) - val_arr = np.array(val) # dimension of the set is immutable @@ -1349,13 +1342,6 @@ def gamma(self): @gamma.setter def gamma(self, val): validate_arg_type("gamma", val, valid_num_types, "a valid numeric type", False) - if val < 0 or val > self.dim: - raise ValueError( - "Cardinality set attribute " - f"'gamma' must be a real number between 0 and dimension " - f"{self.dim} " - f"(provided value {val})" - ) self._gamma = val @@ -1470,6 +1456,43 @@ def point_in_set(self, point): and np.all(aux_space_pt <= 1) ) + def validate(self, config): + """ + Check CardinalitySet validity. + + Raises + ------ + ValueError + If finiteness, positive deviation, or gamma checks fail. + """ + orig_val = self.origin + pos_dev = self.positive_deviation + gamma = self.gamma + + # finiteness check + if not (np.all(np.isfinite(orig_val)) and np.all(np.isfinite(pos_dev))): + raise ValueError( + "Origin value and/or positive deviation are not finite. " + f"Got origin: {orig_val}, positive deviation: {pos_dev}" + ) + + # check deviation is positive + for dev_val in pos_dev: + if dev_val < 0: + raise ValueError( + f"Entry {dev_val} of attribute 'positive_deviation' " + f"is negative value" + ) + + # check gamma between 0 and n + if gamma < 0 or gamma > self.dim: + raise ValueError( + "Cardinality set attribute " + f"'gamma' must be a real number between 0 and dimension " + f"{self.dim} " + f"(provided value {gamma})" + ) + class PolyhedralSet(UncertaintySet): """ From 867b189a2bf5b4c08fe50e74fc545037e0cf32d7 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 17:23:01 -0400 Subject: [PATCH 13/83] Add validation tests for CardinalitySet --- .../pyros/tests/test_uncertainty_sets.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 67b29abca6c..9eb4d661589 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -1604,6 +1604,80 @@ def test_add_bounds_on_uncertain_parameters(self): self.assertEqual(m.uncertain_param_vars[1].bounds, (1, 4)) self.assertEqual(m.uncertain_param_vars[2].bounds, (2, 2)) + def test_validate(self): + """ + Test validate checks perform as expected. + """ + CONFIG = Bunch() + + # construct a valid cardinality set + cardinality_set = CardinalitySet( + origin=[0., 0.], positive_deviation=[1., 1.], gamma=2 + ) + + # validate raises no issues on valid set + cardinality_set.validate(config=CONFIG) + + # check when bounds are not finite + cardinality_set.origin[0] = np.nan + cardinality_set.positive_deviation[0] = np.nan + exc_str = ( + r"Origin value and\/or positive deviation are not finite. " + + r"Got origin: \[nan 0.\], positive deviation: \[nan 1.\]" + ) + with self.assertRaisesRegex(ValueError, exc_str): + cardinality_set.validate(config=CONFIG) + + cardinality_set.origin[0] = np.nan + cardinality_set.positive_deviation[0] = 1 + exc_str = ( + r"Origin value and\/or positive deviation are not finite. " + + r"Got origin: \[nan 0.\], positive deviation: \[1. 1.\]" + ) + with self.assertRaisesRegex(ValueError, exc_str): + cardinality_set.validate(config=CONFIG) + + cardinality_set.origin[0] = 0 + cardinality_set.positive_deviation[0] = np.nan + exc_str = ( + r"Origin value and\/or positive deviation are not finite. " + + r"Got origin: \[0. 0.\], positive deviation: \[nan 1.\]" + ) + with self.assertRaisesRegex(ValueError, exc_str): + cardinality_set.validate(config=CONFIG) + + # check when deviation is negative + cardinality_set.positive_deviation[0] = -2 + exc_str = r"Entry -2.0 of attribute 'positive_deviation' is negative value" + with self.assertRaisesRegex(ValueError, exc_str): + cardinality_set.validate(config=CONFIG) + + # check when gamma is invalid + cardinality_set.positive_deviation[0] = 1 + cardinality_set.gamma = 3 + exc_str = ( + r".*attribute 'gamma' must be a real number " + r"between 0 and dimension 2 \(provided value 3\)" + ) + with self.assertRaisesRegex(ValueError, exc_str): + cardinality_set.validate(config=CONFIG) + + cardinality_set.gamma = -1 + exc_str = ( + r".*attribute 'gamma' must be a real number " + r"between 0 and dimension 2 \(provided value -1\)" + ) + with self.assertRaisesRegex(ValueError, exc_str): + cardinality_set.validate(config=CONFIG) + + @unittest.skipUnless(baron_available, "BARON is not available") + def test_bounded_and_nonempty(self): + """ + Test `is_bounded` and `is_nonempty` for a valid cardinality set. + """ + cardinality_set = CardinalitySet(origin=[0, 0], positive_deviation=[1, 1], gamma=2) + bounded_and_nonempty_check(self, cardinality_set), + class TestDiscreteScenarioSet(unittest.TestCase): """ From 5279b5626896dda8dbb19fda7b74c0f1fdf5ac1e Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 17:23:34 -0400 Subject: [PATCH 14/83] Remove test for CardinalitySet setters --- .../pyros/tests/test_uncertainty_sets.py | 52 ------------------- 1 file changed, 52 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 9eb4d661589..31f8cf47a20 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -1435,58 +1435,6 @@ def test_normal_cardinality_construction_and_update(self): np.testing.assert_allclose(cset.positive_deviation, [3, 0]) np.testing.assert_allclose(cset.gamma, 0.5) - def test_error_on_neg_positive_deviation(self): - """ - Cardinality set positive deviation attribute should - contain nonnegative numerical entries. - - Check ValueError raised if any negative entries provided. - """ - origin = [0, 0] - positive_deviation = [1, -2] # invalid - gamma = 2 - - exc_str = r"Entry -2 of attribute 'positive_deviation' is negative value" - - # assert error on construction - with self.assertRaisesRegex(ValueError, exc_str): - cset = CardinalitySet(origin, positive_deviation, gamma) - - # construct a valid cardinality set - cset = CardinalitySet(origin, [1, 1], gamma) - - # assert error on update - with self.assertRaisesRegex(ValueError, exc_str): - cset.positive_deviation = positive_deviation - - def test_error_on_invalid_gamma(self): - """ - Cardinality set gamma attribute should be a float-like - between 0 and the set dimension. - - Check ValueError raised if gamma attribute is set - to an invalid value. - """ - origin = [0, 0] - positive_deviation = [1, 1] - gamma = 3 # should be invalid - - exc_str = ( - r".*attribute 'gamma' must be a real number " - r"between 0 and dimension 2 \(provided value 3\)" - ) - - # assert error on construction - with self.assertRaisesRegex(ValueError, exc_str): - CardinalitySet(origin, positive_deviation, gamma) - - # construct a valid cardinality set - cset = CardinalitySet(origin, positive_deviation, gamma=2) - - # assert error on update - with self.assertRaisesRegex(ValueError, exc_str): - cset.gamma = gamma - def test_error_on_cardinality_set_dim_change(self): """ Dimension is considered immutable. From dbd135ec365645e7781d11eb18dbb2c34bba9b9e Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 18:32:55 -0400 Subject: [PATCH 15/83] Update BoxSet ValueError message --- .../pyros/tests/test_uncertainty_sets.py | 22 +++---------------- pyomo/contrib/pyros/uncertainty_sets.py | 2 +- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 31f8cf47a20..b6d867ab39a 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -378,7 +378,7 @@ def test_validate(self): # check when bounds are not finite box_set.bounds[0][0] = np.nan - exc_str = r"Not all bounds are finite. Got bounds:.*" + exc_str = r"Not all bounds are finite. \nGot bounds:.*" with self.assertRaisesRegex(ValueError, exc_str): box_set.validate(config=CONFIG) @@ -1568,29 +1568,13 @@ def test_validate(self): # check when bounds are not finite cardinality_set.origin[0] = np.nan - cardinality_set.positive_deviation[0] = np.nan - exc_str = ( - r"Origin value and\/or positive deviation are not finite. " - + r"Got origin: \[nan 0.\], positive deviation: \[nan 1.\]" - ) - with self.assertRaisesRegex(ValueError, exc_str): - cardinality_set.validate(config=CONFIG) - - cardinality_set.origin[0] = np.nan - cardinality_set.positive_deviation[0] = 1 - exc_str = ( - r"Origin value and\/or positive deviation are not finite. " - + r"Got origin: \[nan 0.\], positive deviation: \[1. 1.\]" - ) + exc_str = r"Origin value and/or positive deviation are not finite. .*" with self.assertRaisesRegex(ValueError, exc_str): cardinality_set.validate(config=CONFIG) cardinality_set.origin[0] = 0 cardinality_set.positive_deviation[0] = np.nan - exc_str = ( - r"Origin value and\/or positive deviation are not finite. " - + r"Got origin: \[0. 0.\], positive deviation: \[nan 1.\]" - ) + exc_str = r"Origin value and/or positive deviation are not finite. .*" with self.assertRaisesRegex(ValueError, exc_str): cardinality_set.validate(config=CONFIG) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 02e012e166d..f81934755c3 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -1206,7 +1206,7 @@ def validate(self, config): if not np.all(np.isfinite(bounds_arr)): raise ValueError( "Not all bounds are finite. " - f"Got bounds:\n {bounds_arr}" + f"\nGot bounds:\n {bounds_arr}" ) # check LB <= UB From 2c5761ccfb8562fb7283c3fbf5312e4f06aeb664 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 18:34:26 -0400 Subject: [PATCH 16/83] Move PolyhedralSet setter checks to validate --- pyomo/contrib/pyros/uncertainty_sets.py | 51 ++++++++++++++++++------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index f81934755c3..3edcec587dd 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -1538,6 +1538,8 @@ def __init__(self, lhs_coefficients_mat, rhs_vec): # This check is only performed at construction. self._validate() + # TODO this has a _validate method... + # seems redundant with new validate method and should be consolidated def _validate(self): """ Check polyhedral set attributes are such that set is nonempty @@ -1621,19 +1623,6 @@ def coefficients_mat(self, val): f"to match shape of attribute 'rhs_vec' " f"(provided {lhs_coeffs_arr.shape[0]} rows)" ) - - # check no column is all zeros. otherwise, set is unbounded - cols_with_all_zeros = np.nonzero( - [np.all(col == 0) for col in lhs_coeffs_arr.T] - )[0] - if cols_with_all_zeros.size > 0: - col_str = ", ".join(str(val) for val in cols_with_all_zeros) - raise ValueError( - "Attempting to set attribute 'coefficients_mat' to value " - f"with all entries zero in columns at indexes: {col_str}. " - "Ensure column has at least one nonzero entry" - ) - self._coefficients_mat = lhs_coeffs_arr @property @@ -1715,6 +1704,42 @@ def set_as_constraint(self, uncertain_params=None, block=None): ) + def validate(self, config): + """ + Check PolyhedralSet validity. + + Raises + ------ + ValueError + If finiteness, full column rank of LHS matrix, is_bounded, + or is_nonempty checks fail. + """ + lhs_coeffs_arr = self.coefficients_mat + rhs_vec_arr = self.rhs_vec + + # finiteness check + if not (np.all(np.isfinite(lhs_coeffs_arr)) and np.all(np.isfinite(rhs_vec_arr))): + raise ValueError( + "LHS coefficient matrix or RHS vector are not finite. " + f"\nGot LHS matrix:\n{lhs_coeffs_arr},\nRHS vector:\n{rhs_vec_arr}" + ) + + # check no column is all zeros. otherwise, set is unbounded + cols_with_all_zeros = np.nonzero( + [np.all(col == 0) for col in lhs_coeffs_arr.T] + )[0] + if cols_with_all_zeros.size > 0: + col_str = ", ".join(str(val) for val in cols_with_all_zeros) + raise ValueError( + "Attempting to set attribute 'coefficients_mat' to value " + f"with all entries zero in columns at indexes: {col_str}. " + "Ensure column has at least one nonzero entry" + ) + + # check boundedness and nonemptiness + super().validate(config) + + class BudgetSet(UncertaintySet): """ A budget set. From 183b54fb36f8dc501110479d83de86a6c542868c Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 18:35:12 -0400 Subject: [PATCH 17/83] Add validation tests for PolyhedralSet --- .../pyros/tests/test_uncertainty_sets.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index b6d867ab39a..dbdacdbbb96 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -2465,6 +2465,49 @@ def test_add_bounds_on_uncertain_parameters(self): self.assertEqual(m.uncertain_param_vars[0].bounds, (1, 2)) self.assertEqual(m.uncertain_param_vars[1].bounds, (-1, 1)) + @unittest.skipUnless(baron_available, "BARON is not available") + def test_validate(self): + """ + Test validate checks perform as expected. + """ + CONFIG = pyros_config() + CONFIG.global_solver = global_solver + + # construct a valid polyhedral set + polyhedral_set = PolyhedralSet( + lhs_coefficients_mat=[[1., 0.], [-1., 1.], [-1., -1.]], + rhs_vec=[2., -1., -1.] + ) + + # validate raises no issues on valid set + polyhedral_set.validate(config=CONFIG) + + # check when bounds are not finite + polyhedral_set.rhs_vec[0] = np.nan + exc_str = r"LHS coefficient matrix or RHS vector are not finite. .*" + with self.assertRaisesRegex(ValueError, exc_str): + polyhedral_set.validate(config=CONFIG) + + polyhedral_set.rhs_vec[0] = 2 + polyhedral_set.coefficients_mat[0][0] = np.nan + exc_str = r"LHS coefficient matrix or RHS vector are not finite. .*" + with self.assertRaisesRegex(ValueError, exc_str): + polyhedral_set.validate(config=CONFIG) + + # check when LHS matrix is not full column rank + polyhedral_set.coefficients_mat = [[0., 0.], [0., 1.], [0., -1.]] + exc_str = r".*all entries zero in columns at indexes: 0.*" + with self.assertRaisesRegex(ValueError, exc_str): + polyhedral_set.validate(config=CONFIG) + + @unittest.skipUnless(baron_available, "BARON is not available") + def test_bounded_and_nonempty(self): + """ + Test `is_bounded` and `is_nonempty` for a valid cardinality set. + """ + cardinality_set = CardinalitySet(origin=[0, 0], positive_deviation=[1, 1], gamma=2) + bounded_and_nonempty_check(self, cardinality_set), + class CustomUncertaintySet(UncertaintySet): """ From b09647551d369c091d1046baf8af9197b667eff3 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 18:35:34 -0400 Subject: [PATCH 18/83] Remove tests for PolyhedralSet setters --- .../pyros/tests/test_uncertainty_sets.py | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index dbdacdbbb96..3ac3daab3a0 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -2340,27 +2340,6 @@ def test_error_on_empty_set(self): with self.assertRaisesRegex(ValueError, exc_str): PolyhedralSet([[1], [-1]], rhs_vec=[1, -3]) - def test_error_on_polyhedral_mat_all_zero_columns(self): - """ - Test ValueError raised if budget membership mat - has a column with all zeros. - """ - invalid_col_mat = [[0, 0, 1], [0, 0, 1], [0, 0, 1]] - rhs_vec = [1, 1, 2] - - exc_str = r".*all entries zero in columns at indexes: 0, 1.*" - - # assert error on construction - with self.assertRaisesRegex(ValueError, exc_str): - PolyhedralSet(invalid_col_mat, rhs_vec) - - # construct a valid budget set - pset = PolyhedralSet([[1, 0, 1], [1, 1, 0], [1, 1, 1]], rhs_vec) - - # assert error on update - with self.assertRaisesRegex(ValueError, exc_str): - pset.coefficients_mat = invalid_col_mat - def test_set_as_constraint(self): """ Test method for setting up constraints works correctly. From 5cf5d6a0e6616a84305529a83862c81337b00f52 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 20:46:06 -0400 Subject: [PATCH 19/83] Move BudgetSet setter checks to validate --- pyomo/contrib/pyros/uncertainty_sets.py | 105 +++++++++++++++--------- 1 file changed, 65 insertions(+), 40 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 3edcec587dd..9a80c929f29 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -1875,38 +1875,6 @@ def budget_membership_mat(self, val): f"to match shape of attribute 'budget_rhs_vec' " f"(provided {lhs_coeffs_arr.shape[0]} rows)" ) - - # ensure all entries are 0-1 values - uniq_entries = np.unique(lhs_coeffs_arr) - non_bool_entries = uniq_entries[(uniq_entries != 0) & (uniq_entries != 1)] - if non_bool_entries.size > 0: - raise ValueError( - "Attempting to set attribute `budget_membership_mat` to value " - "containing entries that are not 0-1 values " - f"(example: {non_bool_entries[0]}). " - "Ensure all entries are of value 0 or 1" - ) - - # check no row is all zeros - rows_with_zero_sums = np.nonzero(lhs_coeffs_arr.sum(axis=1) == 0)[0] - if rows_with_zero_sums.size > 0: - row_str = ", ".join(str(val) for val in rows_with_zero_sums) - raise ValueError( - "Attempting to set attribute `budget_membership_mat` to value " - f"with all entries zero in rows at indexes: {row_str}. " - "Ensure each row and column has at least one nonzero entry" - ) - - # check no column is all zeros - cols_with_zero_sums = np.nonzero(lhs_coeffs_arr.sum(axis=0) == 0)[0] - if cols_with_zero_sums.size > 0: - col_str = ", ".join(str(val) for val in cols_with_zero_sums) - raise ValueError( - "Attempting to set attribute `budget_membership_mat` to value " - f"with all entries zero in columns at indexes: {col_str}. " - "Ensure each row and column has at least one nonzero entry" - ) - # matrix is valid; update self._budget_membership_mat = lhs_coeffs_arr @@ -1942,14 +1910,6 @@ def budget_rhs_vec(self, val): f"(provided {rhs_vec_arr.size} entries)" ) - # ensure all entries are nonnegative - for entry in rhs_vec_arr: - if entry < 0: - raise ValueError( - f"Entry {entry} of attribute 'budget_rhs_vec' is " - "negative. Ensure all entries are nonnegative" - ) - self._budget_rhs_vec = rhs_vec_arr @property @@ -2023,6 +1983,71 @@ def parameter_bounds(self): def set_as_constraint(self, **kwargs): return PolyhedralSet.set_as_constraint(self, **kwargs) + def validate(self, config): + """ + Check BudgetSet validity. + + Raises + ------ + ValueError + If finiteness, full 0 column or row of LHS matrix, + or positive RHS vector checks fail. + """ + lhs_coeffs_arr = self.budget_membership_mat + rhs_vec_arr = self.budget_rhs_vec + orig_val = self.origin + + # finiteness check + if not ( + np.all(np.isfinite(lhs_coeffs_arr)) + and np.all(np.isfinite(rhs_vec_arr)) + and np.all(np.isfinite(orig_val)) + ): + raise ValueError( + "Origin, LHS coefficient matrix or RHS vector are not finite. " + f"\nGot origin:\n{orig_val},\nLHS matrix:\n{lhs_coeffs_arr},\nRHS vector:\n{rhs_vec_arr}" + ) + + # check no row, col, are all zeros and all values are 0-1. + # ensure all entries are 0-1 values + uniq_entries = np.unique(lhs_coeffs_arr) + non_bool_entries = uniq_entries[(uniq_entries != 0) & (uniq_entries != 1)] + if non_bool_entries.size > 0: + raise ValueError( + "Attempting to set attribute `budget_membership_mat` to value " + "containing entries that are not 0-1 values " + f"(example: {non_bool_entries[0]}). " + "Ensure all entries are of value 0 or 1" + ) + + # check no row is all zeros + rows_with_zero_sums = np.nonzero(lhs_coeffs_arr.sum(axis=1) == 0)[0] + if rows_with_zero_sums.size > 0: + row_str = ", ".join(str(val) for val in rows_with_zero_sums) + raise ValueError( + "Attempting to set attribute `budget_membership_mat` to value " + f"with all entries zero in rows at indexes: {row_str}. " + "Ensure each row and column has at least one nonzero entry" + ) + + # check no column is all zeros + cols_with_zero_sums = np.nonzero(lhs_coeffs_arr.sum(axis=0) == 0)[0] + if cols_with_zero_sums.size > 0: + col_str = ", ".join(str(val) for val in cols_with_zero_sums) + raise ValueError( + "Attempting to set attribute `budget_membership_mat` to value " + f"with all entries zero in columns at indexes: {col_str}. " + "Ensure each row and column has at least one nonzero entry" + ) + + # ensure all rhs entries are nonnegative + for entry in rhs_vec_arr: + if entry < 0: + raise ValueError( + f"Entry {entry} of attribute 'budget_rhs_vec' is " + "negative. Ensure all entries are nonnegative" + ) + class FactorModelSet(UncertaintySet): """ From cbd32eba425fc901f2ad51133889655709424532 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 20:46:39 -0400 Subject: [PATCH 20/83] Add validation tests for BudgetSet --- .../pyros/tests/test_uncertainty_sets.py | 79 ++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 3ac3daab3a0..39ebec55fe1 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -683,6 +683,78 @@ def test_add_bounds_on_uncertain_parameters(self): self.assertEqual(m.v[0].bounds, (1, 3)) self.assertEqual(m.v[1].bounds, (3, 5)) + def test_validate(self): + """ + Test validate checks perform as expected. + """ + CONFIG = Bunch() + + # construct a valid budget set + budget_mat = [[1., 0., 1.], [0., 1., 0.]] + budget_rhs_vec = [1., 3.] + budget_set = BudgetSet(budget_mat, budget_rhs_vec) + + # validate raises no issues on valid set + budget_set.validate(config=CONFIG) + + # check when bounds are not finite + budget_set.origin[0] = np.nan + exc_str = r"Origin, LHS coefficient matrix or RHS vector are not finite. .*" + with self.assertRaisesRegex(ValueError, exc_str): + budget_set.validate(config=CONFIG) + budget_set.origin[0] = 0 + + budget_set.budget_rhs_vec[0] = np.nan + exc_str = r"Origin, LHS coefficient matrix or RHS vector are not finite. .*" + with self.assertRaisesRegex(ValueError, exc_str): + budget_set.validate(config=CONFIG) + budget_set.budget_rhs_vec[0] = 1 + + budget_set.budget_membership_mat[0][0] = np.nan + exc_str = r"LHS coefficient matrix or RHS vector are not finite. .*" + with self.assertRaisesRegex(ValueError, exc_str): + budget_set.validate(config=CONFIG) + budget_set.budget_membership_mat[0][0] = 1 + + # check when rhs has negative element + budget_set.budget_rhs_vec = [1, -1] + exc_str = r"Entry -1 of.*'budget_rhs_vec' is negative*" + with self.assertRaisesRegex(ValueError, exc_str): + budget_set.validate(config=CONFIG) + budget_set.budget_rhs_vec = budget_rhs_vec + + # check when not all lhs entries are 0-1 + budget_set.budget_membership_mat = [[1, 0, 1], [1, 1, 0.1]] + exc_str = r"Attempting.*entries.*not 0-1 values \(example: 0.1\).*" + with self.assertRaisesRegex(ValueError, exc_str): + budget_set.validate(config=CONFIG) + budget_set.budget_membership_mat = budget_mat + + # check when row has all zeros + invalid_row_mat = [[0, 0, 0], [1, 1, 1], [0, 0, 0]] + budget_rhs_vec = [1, 1, 2] + budget_set = BudgetSet(invalid_row_mat, budget_rhs_vec) + exc_str = r".*all entries zero in rows at indexes: 0, 2.*" + with self.assertRaisesRegex(ValueError, exc_str): + budget_set.validate(config=CONFIG) + + # check when column has all zeros + budget_set.budget_membership_mat = [[0, 0, 1], [0, 0, 1], [0, 0, 1]] + budget_set.budget_rhs_vec = [1, 1, 2] + exc_str = r".*all entries zero in columns at indexes: 0, 1.*" + with self.assertRaisesRegex(ValueError, exc_str): + budget_set.validate(config=CONFIG) + + @unittest.skipUnless(baron_available, "BARON is not available") + def test_bounded_and_nonempty(self): + """ + Test `is_bounded` and `is_nonempty` for a valid cardinality set. + """ + budget_mat = [[1., 0., 1.], [0., 1., 0.]] + budget_rhs_vec = [1., 3.] + budget_set = BudgetSet(budget_mat, budget_rhs_vec) + bounded_and_nonempty_check(self, budget_set), + class TestFactorModelSet(unittest.TestCase): """ @@ -2484,8 +2556,11 @@ def test_bounded_and_nonempty(self): """ Test `is_bounded` and `is_nonempty` for a valid cardinality set. """ - cardinality_set = CardinalitySet(origin=[0, 0], positive_deviation=[1, 1], gamma=2) - bounded_and_nonempty_check(self, cardinality_set), + polyhedral_set = PolyhedralSet( + lhs_coefficients_mat=[[1., 0.], [-1., 1.], [-1., -1.]], + rhs_vec=[2., -1., -1.] + ) + bounded_and_nonempty_check(self, polyhedral_set), class CustomUncertaintySet(UncertaintySet): From 3308cde353150db45271909be8a3b511b62530f1 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 20:48:08 -0400 Subject: [PATCH 21/83] Remove tests for BudgetSet setters --- .../pyros/tests/test_uncertainty_sets.py | 84 ------------------- 1 file changed, 84 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 39ebec55fe1..70549800aac 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -486,90 +486,6 @@ def test_error_on_budget_member_mat_row_change(self): with self.assertRaisesRegex(ValueError, exc_str): bu_set.budget_rhs_vec = [1] - def test_error_on_neg_budget_rhs_vec_entry(self): - """ - Test ValueError raised if budget RHS vec has entry - with negative value entry. - """ - budget_mat = [[1, 0, 1], [1, 1, 0]] - neg_val_rhs_vec = [1, -1] - - exc_str = r"Entry -1 of.*'budget_rhs_vec' is negative*" - - # assert error on construction - with self.assertRaisesRegex(ValueError, exc_str): - BudgetSet(budget_mat, neg_val_rhs_vec) - - # construct a valid budget set - buset = BudgetSet(budget_mat, [1, 1]) - - # assert error on update - with self.assertRaisesRegex(ValueError, exc_str): - buset.budget_rhs_vec = neg_val_rhs_vec - - def test_error_on_non_bool_budget_mat_entry(self): - """ - Test ValueError raised if budget membership mat has - entry which is not a 0-1 value. - """ - invalid_budget_mat = [[1, 0, 1], [1, 1, 0.1]] - budget_rhs_vec = [1, 1] - - exc_str = r"Attempting.*entries.*not 0-1 values \(example: 0.1\).*" - - # assert error on construction - with self.assertRaisesRegex(ValueError, exc_str): - BudgetSet(invalid_budget_mat, budget_rhs_vec) - - # construct a valid budget set - buset = BudgetSet([[1, 0, 1], [1, 1, 0]], budget_rhs_vec) - - # assert error on update - with self.assertRaisesRegex(ValueError, exc_str): - buset.budget_membership_mat = invalid_budget_mat - - def test_error_on_budget_mat_all_zero_rows(self): - """ - Test ValueError raised if budget membership mat - has a row with all zeros. - """ - invalid_row_mat = [[0, 0, 0], [1, 1, 1], [0, 0, 0]] - budget_rhs_vec = [1, 1, 2] - - exc_str = r".*all entries zero in rows at indexes: 0, 2.*" - - # assert error on construction - with self.assertRaisesRegex(ValueError, exc_str): - BudgetSet(invalid_row_mat, budget_rhs_vec) - - # construct a valid budget set - buset = BudgetSet([[1, 0, 1], [1, 1, 0], [1, 1, 1]], budget_rhs_vec) - - # assert error on update - with self.assertRaisesRegex(ValueError, exc_str): - buset.budget_membership_mat = invalid_row_mat - - def test_error_on_budget_mat_all_zero_columns(self): - """ - Test ValueError raised if budget membership mat - has a column with all zeros. - """ - invalid_col_mat = [[0, 0, 1], [0, 0, 1], [0, 0, 1]] - budget_rhs_vec = [1, 1, 2] - - exc_str = r".*all entries zero in columns at indexes: 0, 1.*" - - # assert error on construction - with self.assertRaisesRegex(ValueError, exc_str): - BudgetSet(invalid_col_mat, budget_rhs_vec) - - # construct a valid budget set - buset = BudgetSet([[1, 0, 1], [1, 1, 0], [1, 1, 1]], budget_rhs_vec) - - # assert error on update - with self.assertRaisesRegex(ValueError, exc_str): - buset.budget_membership_mat = invalid_col_mat - @unittest.skipUnless(baron_available, "BARON is not available") def test_compute_parameter_bounds(self): """ From 53db9d357ba2040b497c6ef067bd0f311233a119 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 21:18:50 -0400 Subject: [PATCH 22/83] Move FactorModelSet setter checks to validate --- pyomo/contrib/pyros/uncertainty_sets.py | 55 ++++++++++++++++++------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 9a80c929f29..82bed09b5ea 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -2208,16 +2208,6 @@ def psi_mat(self, val): f"(provided shape {psi_mat_arr.shape})" ) - psi_mat_rank = np.linalg.matrix_rank(psi_mat_arr) - is_full_column_rank = psi_mat_rank == self.number_of_factors - if not is_full_column_rank: - raise ValueError( - "Attribute 'psi_mat' should be full column rank. " - f"(Got a matrix of shape {psi_mat_arr.shape} and rank {psi_mat_rank}.) " - "Ensure `psi_mat` does not have more columns than rows, " - "and the columns of `psi_mat` are linearly independent." - ) - self._psi_mat = psi_mat_arr @property @@ -2237,12 +2227,6 @@ def beta(self): @beta.setter def beta(self, val): - if val > 1 or val < 0: - raise ValueError( - "Beta parameter must be a real number between 0 " - f"and 1 inclusive (provided value {val})" - ) - self._beta = val @property @@ -2393,6 +2377,45 @@ def point_in_set(self, point): np.abs(aux_space_pt) <= 1 + tol ) + def validate(self, config): + """ + Check FactorModelSet validity. + + Raises + ------ + ValueError + If finiteness full column rank of Psi matrix, or + beta between 0 and 1 checks fail. + """ + orig_val = self.origin + psi_mat_arr = self.psi_mat + beta = self.beta + + # finiteness check + if not np.all(np.isfinite(orig_val)): + raise ValueError( + "Origin is not finite. " + f"Got origin: {orig_val}" + ) + + # check psi is full column rank + psi_mat_rank = np.linalg.matrix_rank(psi_mat_arr) + check_full_column_rank = psi_mat_rank == self.number_of_factors + if not check_full_column_rank: + raise ValueError( + "Attribute 'psi_mat' should be full column rank. " + f"(Got a matrix of shape {psi_mat_arr.shape} and rank {psi_mat_rank}.) " + "Ensure `psi_mat` does not have more columns than rows, " + "and the columns of `psi_mat` are linearly independent." + ) + + # check beta is between 0 and 1 + if beta > 1 or beta < 0: + raise ValueError( + "Beta parameter must be a real number between 0 " + f"and 1 inclusive (provided value {beta})" + ) + class AxisAlignedEllipsoidalSet(UncertaintySet): """ From 72a1c1809ea31e5a36bafc5cb3aace3cd47894d8 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 21:20:37 -0400 Subject: [PATCH 23/83] Add validation tests for FactorModelSet --- .../pyros/tests/test_uncertainty_sets.py | 78 +++++++++++++++++-- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 70549800aac..9e131323708 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -376,7 +376,7 @@ def test_validate(self): # validate raises no issues on valid set box_set.validate(config=CONFIG) - # check when bounds are not finite + # check when values are not finite box_set.bounds[0][0] = np.nan exc_str = r"Not all bounds are finite. \nGot bounds:.*" with self.assertRaisesRegex(ValueError, exc_str): @@ -613,7 +613,7 @@ def test_validate(self): # validate raises no issues on valid set budget_set.validate(config=CONFIG) - # check when bounds are not finite + # check when values are not finite budget_set.origin[0] = np.nan exc_str = r"Origin, LHS coefficient matrix or RHS vector are not finite. .*" with self.assertRaisesRegex(ValueError, exc_str): @@ -644,7 +644,6 @@ def test_validate(self): exc_str = r"Attempting.*entries.*not 0-1 values \(example: 0.1\).*" with self.assertRaisesRegex(ValueError, exc_str): budget_set.validate(config=CONFIG) - budget_set.budget_membership_mat = budget_mat # check when row has all zeros invalid_row_mat = [[0, 0, 0], [1, 1, 1], [0, 0, 0]] @@ -1034,6 +1033,75 @@ def test_add_bounds_on_uncertain_parameters(self): self.assertEqual(m.uncertain_param_vars[2].bounds, (-13.0, 17.0)) self.assertEqual(m.uncertain_param_vars[3].bounds, (-12.0, 18.0)) + def test_validate(self): + """ + Test validate checks perform as expected. + """ + CONFIG = Bunch() + + # construct a valid budget set + origin = [0., 0., 0.] + number_of_factors = 2 + psi_mat = [[1, 0], [0, 1], [1, 1]] + beta = 0.5 + factor_set = FactorModelSet(origin, number_of_factors, psi_mat, beta) + + # validate raises no issues on valid set + factor_set.validate(config=CONFIG) + + # check when values are not finite + factor_set.origin[0] = np.nan + exc_str = r"Origin is not finite. .*" + with self.assertRaisesRegex(ValueError, exc_str): + factor_set.validate(config=CONFIG) + factor_set.origin[0] = 0 + + # check when beta is invalid + neg_beta = -0.5 + big_beta = 1.5 + neg_exc_str = ( + r".*must be a real number between 0 and 1.*\(provided value -0.5\)" + ) + big_exc_str = r".*must be a real number between 0 and 1.*\(provided value 1.5\)" + factor_set.beta = neg_beta + with self.assertRaisesRegex(ValueError, neg_exc_str): + factor_set.validate(config=CONFIG) + factor_set.beta = big_beta + with self.assertRaisesRegex(ValueError, big_exc_str): + factor_set.validate(config=CONFIG) + + # check when psi matrix is rank defficient + with self.assertRaisesRegex(ValueError, r"full column rank.*\(2, 3\)"): + # more columns than rows + factor_set = FactorModelSet( + origin=[0, 0], + number_of_factors=3, + psi_mat=[[1, -1, 1], [1, 0.1, 1]], + beta=1 / 6, + ) + factor_set.validate(config=CONFIG) + with self.assertRaisesRegex(ValueError, r"full column rank.*\(2, 2\)"): + # linearly dependent columns + factor_set = FactorModelSet( + origin=[0, 0], + number_of_factors=2, + psi_mat=[[1, -1], [1, -1]], + beta=1 / 6, + ) + factor_set.validate(config=CONFIG) + + @unittest.skipUnless(baron_available, "BARON is not available") + def test_bounded_and_nonempty(self): + """ + Test `is_bounded` and `is_nonempty` for a valid cardinality set. + """ + origin = [0., 0., 0.] + number_of_factors = 2 + psi_mat = [[1, 0], [0, 1], [1, 1]] + beta = 0.5 + factor_set = FactorModelSet(origin, number_of_factors, psi_mat, beta) + bounded_and_nonempty_check(self, factor_set), + class TestIntersectionSet(unittest.TestCase): """ @@ -1554,7 +1622,7 @@ def test_validate(self): # validate raises no issues on valid set cardinality_set.validate(config=CONFIG) - # check when bounds are not finite + # check when values are not finite cardinality_set.origin[0] = np.nan exc_str = r"Origin value and/or positive deviation are not finite. .*" with self.assertRaisesRegex(ValueError, exc_str): @@ -2449,7 +2517,7 @@ def test_validate(self): # validate raises no issues on valid set polyhedral_set.validate(config=CONFIG) - # check when bounds are not finite + # check when values are not finite polyhedral_set.rhs_vec[0] = np.nan exc_str = r"LHS coefficient matrix or RHS vector are not finite. .*" with self.assertRaisesRegex(ValueError, exc_str): From 97fe35eb6bbc98f90e32d190f5dce98929de9f75 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 21:20:59 -0400 Subject: [PATCH 24/83] Remove tests for FactorModelSet setters --- .../pyros/tests/test_uncertainty_sets.py | 52 ------------------- 1 file changed, 52 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 9e131323708..e6f3aa18802 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -753,58 +753,6 @@ def test_error_on_invalid_number_of_factors(self): with self.assertRaisesRegex(AttributeError, exc_str): fset.number_of_factors = 3 - def test_error_on_invalid_beta(self): - """ - Test ValueError raised if beta is invalid (exceeds 1 or - is negative) - """ - origin = [0, 0, 0] - number_of_factors = 2 - psi_mat = [[1, 0], [0, 1], [1, 1]] - neg_beta = -0.5 - big_beta = 1.5 - - # assert error on construction - neg_exc_str = ( - r".*must be a real number between 0 and 1.*\(provided value -0.5\)" - ) - big_exc_str = r".*must be a real number between 0 and 1.*\(provided value 1.5\)" - with self.assertRaisesRegex(ValueError, neg_exc_str): - FactorModelSet(origin, number_of_factors, psi_mat, neg_beta) - with self.assertRaisesRegex(ValueError, big_exc_str): - FactorModelSet(origin, number_of_factors, psi_mat, big_beta) - - # create a valid factor model set - fset = FactorModelSet(origin, number_of_factors, psi_mat, 1) - - # assert error on update - with self.assertRaisesRegex(ValueError, neg_exc_str): - fset.beta = neg_beta - with self.assertRaisesRegex(ValueError, big_exc_str): - fset.beta = big_beta - - def test_error_on_rank_deficient_psi_mat(self): - """ - Test exception raised if factor loading matrix `psi_mat` - is rank-deficient. - """ - with self.assertRaisesRegex(ValueError, r"full column rank.*\(2, 3\)"): - # more columns than rows - FactorModelSet( - origin=[0, 0], - number_of_factors=3, - psi_mat=[[1, -1, 1], [1, 0.1, 1]], - beta=1 / 6, - ) - with self.assertRaisesRegex(ValueError, r"full column rank.*\(2, 2\)"): - # linearly dependent columns - FactorModelSet( - origin=[0, 0], - number_of_factors=2, - psi_mat=[[1, -1], [1, -1]], - beta=1 / 6, - ) - @parameterized.expand( [ # map beta to expected parameter bounds From 48553567e99c744d513c6527c435d52f08eaf183 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 21:33:56 -0400 Subject: [PATCH 25/83] Move AxisAlignedEllipsoidalSet checks to validate --- pyomo/contrib/pyros/uncertainty_sets.py | 38 +++++++++++++++++++------ 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 82bed09b5ea..e5f2b8a68c7 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -2516,14 +2516,6 @@ def half_lengths(self, val): f"to value of dimension {val_arr.size}" ) - # ensure half-lengths are non-negative - for half_len in val_arr: - if half_len < 0: - raise ValueError( - f"Entry {half_len} of 'half_lengths' " - "is negative. All half-lengths must be nonnegative" - ) - self._half_lengths = val_arr @property @@ -2593,6 +2585,36 @@ def set_as_constraint(self, uncertain_params=None, block=None): auxiliary_vars=aux_var_list, ) + def validate(self, config): + """ + Check AxisAlignedEllipsoidalSet validity. + + Raises + ------ + ValueError + If finiteness or positive half-length checks fail. + """ + ctr = self.center + half_lengths = self.half_lengths + + # finiteness check + if not ( + np.all(np.isfinite(ctr)) + and np.all(np.isfinite(half_lengths)) + ): + raise ValueError( + "Center or half-lengths are not finite. " + f"Got center: {ctr}, half-lengths: {half_lengths}" + ) + + # ensure half-lengths are non-negative + for half_len in half_lengths: + if half_len < 0: + raise ValueError( + f"Entry {half_len} of 'half_lengths' " + "is negative. All half-lengths must be nonnegative" + ) + class EllipsoidalSet(UncertaintySet): """ From 19b7d3fbfc22a1cdcbc00afa94b958eefcbf7687 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 21:34:26 -0400 Subject: [PATCH 26/83] Add validation tests for AxisAlignedEllipsoidalSet --- .../pyros/tests/test_uncertainty_sets.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index e6f3aa18802..248e1347ba4 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -1886,6 +1886,49 @@ def test_add_bounds_on_uncertain_parameters(self): self.assertEqual(m.uncertain_param_vars[1].bounds, (-0.5, 3.5)) self.assertEqual(m.uncertain_param_vars[2].bounds, (1, 1)) + def test_validate(self): + """ + Test validate checks perform as expected. + """ + CONFIG = Bunch() + + # construct a valid budget set + center = [0., 0.] + half_lengths = [1., 3.] + a_ellipsoid_set = AxisAlignedEllipsoidalSet(center, half_lengths) + + # validate raises no issues on valid set + a_ellipsoid_set.validate(config=CONFIG) + + # check when values are not finite + a_ellipsoid_set.center[0] = np.nan + exc_str = r"Center or half-lengths are not finite. .*" + with self.assertRaisesRegex(ValueError, exc_str): + a_ellipsoid_set.validate(config=CONFIG) + a_ellipsoid_set.center[0] = 0 + + a_ellipsoid_set.half_lengths[0] = np.nan + exc_str = r"Center or half-lengths are not finite. .*" + with self.assertRaisesRegex(ValueError, exc_str): + a_ellipsoid_set.validate(config=CONFIG) + a_ellipsoid_set.half_lengths[0] = 1 + + # check when half lengths are negative + a_ellipsoid_set.half_lengths = [1, -1] + exc_str = r"Entry -1 of.*'half_lengths' is negative.*" + with self.assertRaisesRegex(ValueError, exc_str): + a_ellipsoid_set.validate(config=CONFIG) + + @unittest.skipUnless(baron_available, "BARON is not available") + def test_bounded_and_nonempty(self): + """ + Test `is_bounded` and `is_nonempty` for a valid cardinality set. + """ + center = [0., 0.] + half_lengths = [1., 3.] + a_ellipsoid_set = AxisAlignedEllipsoidalSet(center, half_lengths) + bounded_and_nonempty_check(self, a_ellipsoid_set), + class TestEllipsoidalSet(unittest.TestCase): """ From b7dd69c136ac730e112139b493a8a161174e61b0 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 21:34:44 -0400 Subject: [PATCH 27/83] Remove tests for AxisAlignedEllipsoidalSet setters --- .../pyros/tests/test_uncertainty_sets.py | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 248e1347ba4..e901197cc06 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -1779,26 +1779,6 @@ def test_error_on_axis_aligned_dim_change(self): with self.assertRaisesRegex(ValueError, exc_str): aset.half_lengths = [0, 0, 1] - def test_error_on_negative_axis_aligned_half_lengths(self): - """ - Test ValueError if half lengths for AxisAlignedEllipsoidalSet - contains a negative value. - """ - center = [1, 1] - invalid_half_lengths = [1, -1] - exc_str = r"Entry -1 of.*'half_lengths' is negative.*" - - # assert error on construction - with self.assertRaisesRegex(ValueError, exc_str): - AxisAlignedEllipsoidalSet(center, invalid_half_lengths) - - # construct a valid axis-aligned ellipsoidal set - aset = AxisAlignedEllipsoidalSet(center, [1, 0]) - - # assert error on update - with self.assertRaisesRegex(ValueError, exc_str): - aset.half_lengths = invalid_half_lengths - def test_set_as_constraint(self): """ Test method for setting up constraints works correctly. From 8e603f9c1b3327fc4ff47fe619357143fbf3dfa7 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 21:54:54 -0400 Subject: [PATCH 28/83] Move EllipsoidalSet setter checks to validate --- pyomo/contrib/pyros/uncertainty_sets.py | 39 ++++++++++++++++++++----- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index e5f2b8a68c7..6e357298660 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -2827,7 +2827,6 @@ def shape_matrix(self, val): f"(provided matrix with shape {shape_mat_arr.shape})" ) - self._verify_positive_definite(shape_mat_arr) self._shape_matrix = shape_mat_arr @property @@ -2842,12 +2841,6 @@ def scale(self): @scale.setter def scale(self, val): validate_arg_type("scale", val, valid_num_types, "a valid numeric type", False) - if val < 0: - raise ValueError( - f"{type(self).__name__} attribute " - f"'scale' must be a non-negative real " - f"(provided value {val})" - ) self._scale = val self._gaussian_conf_lvl = sp.stats.chi2.cdf(x=val, df=self.dim) @@ -2967,6 +2960,38 @@ def set_as_constraint(self, uncertain_params=None, block=None): auxiliary_vars=aux_var_list, ) + def validate(self, config): + """ + Check EllipsoidalSet validity. + + Raises + ------ + ValueError + If finiteness, positive semi-definite, or + positive scale checks fail. + """ + ctr = self.center + shape_mat_arr = self.shape_matrix + scale = self.scale + + # finiteness check + if not np.all(np.isfinite(ctr)): + raise ValueError( + "Center is not finite. " + f"Got center: {ctr}" + ) + + # check shape matrix is positive semidefinite + self._verify_positive_definite(shape_mat_arr) + + # ensure scale is non-negative + if scale < 0: + raise ValueError( + f"{type(self).__name__} attribute " + f"'scale' must be a non-negative real " + f"(provided value {scale})" + ) + class DiscreteScenarioSet(UncertaintySet): """ From 08626e3b999ba84efff2cf639446d247a50a236d Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 21:55:49 -0400 Subject: [PATCH 29/83] Add validation tests for EllipsoidalSet --- .../pyros/tests/test_uncertainty_sets.py | 68 ++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index e901197cc06..ea256ba0967 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -987,7 +987,7 @@ def test_validate(self): """ CONFIG = Bunch() - # construct a valid budget set + # construct a valid factor model set origin = [0., 0., 0.] number_of_factors = 2 psi_mat = [[1, 0], [0, 1], [1, 1]] @@ -1872,7 +1872,7 @@ def test_validate(self): """ CONFIG = Bunch() - # construct a valid budget set + # construct a valid axis aligned ellipsoidal set center = [0., 0.] half_lengths = [1., 3.] a_ellipsoid_set = AxisAlignedEllipsoidalSet(center, half_lengths) @@ -2281,6 +2281,70 @@ def test_add_bounds_on_uncertain_parameters(self): self.assertEqual(m.uncertain_param_vars[0].bounds, (0.5, 1.5)) self.assertEqual(m.uncertain_param_vars[1].bounds, (1, 2)) + def test_validate(self): + """ + Test validate checks perform as expected. + """ + CONFIG = Bunch() + + # construct a valid ellipsoidal set + center = [0., 0.] + shape_matrix = [[1., 0.], [0., 2.]] + scale = 1 + ellipsoid_set = EllipsoidalSet(center, shape_matrix, scale) + + # validate raises no issues on valid set + ellipsoid_set.validate(config=CONFIG) + + # check when values are not finite + ellipsoid_set.center[0] = np.nan + exc_str = r"Center is not finite. .*" + with self.assertRaisesRegex(ValueError, exc_str): + ellipsoid_set.validate(config=CONFIG) + ellipsoid_set.center[0] = 0 + + # check when scale is not positive + ellipsoid_set.scale = -1 + exc_str = r".*must be a non-negative real \(provided.*-1\)" + with self.assertRaisesRegex(ValueError, exc_str): + ellipsoid_set.validate(config=CONFIG) + + # check when shape matrix is invalid + center = [0, 0] + scale = 3 + + # assert error on construction + with self.assertRaisesRegex( + ValueError, + r"Shape matrix must be symmetric", + msg="Asymmetric shape matrix test failed", + ): + ellipsoid_set = EllipsoidalSet(center, [[1, 1], [0, 1]], scale) + ellipsoid_set.validate(config=CONFIG) + with self.assertRaises( + np.linalg.LinAlgError, msg="Singular shape matrix test failed" + ): + ellipsoid_set = EllipsoidalSet(center, [[0, 0], [0, 0]], scale) + ellipsoid_set.validate(config=CONFIG) + with self.assertRaisesRegex( + ValueError, + r"Non positive-definite.*", + msg="Indefinite shape matrix test failed", + ): + ellipsoid_set = EllipsoidalSet(center, [[1, 0], [0, -2]], scale) + ellipsoid_set.validate(config=CONFIG) + + @unittest.skipUnless(baron_available, "BARON is not available") + def test_bounded_and_nonempty(self): + """ + Test `is_bounded` and `is_nonempty` for a valid cardinality set. + """ + center = [0., 0.] + shape_matrix = [[1., 0.], [0., 2.]] + scale = 1 + ellipsoid_set = EllipsoidalSet(center, shape_matrix, scale) + bounded_and_nonempty_check(self, ellipsoid_set), + class TestPolyhedralSet(unittest.TestCase): """ From 82a92aafb396384297ccdd24b13a897526355d75 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 21:56:18 -0400 Subject: [PATCH 30/83] Remove tests for EllipsoidalSet setters --- .../pyros/tests/test_uncertainty_sets.py | 69 ------------------- 1 file changed, 69 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index ea256ba0967..6997eb44c65 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -2019,28 +2019,6 @@ def test_error_on_ellipsoidal_dim_change(self): with self.assertRaisesRegex(ValueError, exc_str): eset.center = [0, 0, 0] - def test_error_on_neg_scale(self): - """ - Test ValueError raised if scale attribute set to negative - value. - """ - center = [0, 0] - shape_matrix = [[1, 0], [0, 2]] - neg_scale = -1 - - exc_str = r".*must be a non-negative real \(provided.*-1\)" - - # assert error on construction - with self.assertRaisesRegex(ValueError, exc_str): - EllipsoidalSet(center, shape_matrix, neg_scale) - - # construct a valid EllipsoidalSet - eset = EllipsoidalSet(center, shape_matrix, scale=2) - - # assert error on update - with self.assertRaisesRegex(ValueError, exc_str): - eset.scale = neg_scale - def test_error_invalid_gaussian_conf_lvl(self): """ Test error when attempting to initialize with Gaussian @@ -2105,53 +2083,6 @@ def test_error_on_shape_matrix_with_wrong_size(self): with self.assertRaisesRegex(ValueError, exc_str): eset.shape_matrix = invalid_shape_matrix - def test_error_on_invalid_shape_matrix(self): - """ - Test exceptional cases of invalid square shape matrix - arguments - """ - center = [0, 0] - scale = 3 - - # assert error on construction - with self.assertRaisesRegex( - ValueError, - r"Shape matrix must be symmetric", - msg="Asymmetric shape matrix test failed", - ): - EllipsoidalSet(center, [[1, 1], [0, 1]], scale) - with self.assertRaises( - np.linalg.LinAlgError, msg="Singular shape matrix test failed" - ): - EllipsoidalSet(center, [[0, 0], [0, 0]], scale) - with self.assertRaisesRegex( - ValueError, - r"Non positive-definite.*", - msg="Indefinite shape matrix test failed", - ): - EllipsoidalSet(center, [[1, 0], [0, -2]], scale) - - # construct a valid EllipsoidalSet - eset = EllipsoidalSet(center, [[1, 0], [0, 2]], scale) - - # assert error on update - with self.assertRaisesRegex( - ValueError, - r"Shape matrix must be symmetric", - msg="Asymmetric shape matrix test failed", - ): - eset.shape_matrix = [[1, 1], [0, 1]] - with self.assertRaises( - np.linalg.LinAlgError, msg="Singular shape matrix test failed" - ): - eset.shape_matrix = [[0, 0], [0, 0]] - with self.assertRaisesRegex( - ValueError, - r"Non positive-definite.*", - msg="Indefinite shape matrix test failed", - ): - eset.shape_matrix = [[1, 0], [0, -2]] - def test_set_as_constraint(self): """ Test method for setting up constraints works correctly. From c418e0fb08f04bc1b7b9d62136223cc882813dcc Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 22:31:43 -0400 Subject: [PATCH 31/83] Add DiscreteSet validate method and tests --- .../pyros/tests/test_uncertainty_sets.py | 39 +++++++++++++++++++ pyomo/contrib/pyros/uncertainty_sets.py | 26 +++++++++++++ 2 files changed, 65 insertions(+) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 6997eb44c65..905b7f580c5 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -1720,6 +1720,45 @@ def test_add_bounds_on_uncertain_parameters(self): self.assertEqual(m.uncertain_param_vars[0].bounds, (0, 2)) self.assertEqual(m.uncertain_param_vars[1].bounds, (0, 1.0)) + def test_validate(self): + """ + Test validate checks perform as expected. + """ + CONFIG = Bunch() + + # construct a valid discrete scenario set + discrete_set = DiscreteScenarioSet([[1, 2], [3, 4]]) + + # validate raises no issues on valid set + discrete_set.validate(config=CONFIG) + + # check when scenario set is empty + # TODO should this method can be used to create ragged arrays + # after a set is created. There are currently no checks for this + # in any validate method. It may be good to included validate_array + # in all validate methods as well to guard against it. + discrete_set = DiscreteScenarioSet([[0]]) + discrete_set.scenarios.pop(0) + exc_str = r"Scenarios set must be nonempty. .*" + with self.assertRaisesRegex(ValueError, exc_str): + discrete_set.validate(config=CONFIG) + + # check when not all scenarios are finite + discrete_set = DiscreteScenarioSet([[1, 2], [3, 4]]) + exc_str = r"Not all scenarios are finite. .*" + for val_str in ["inf", "nan"]: + discrete_set.scenarios[0] = [1, float(val_str)] + with self.assertRaisesRegex(ValueError, exc_str): + discrete_set.validate(config=CONFIG) + + @unittest.skipUnless(baron_available, "BARON is not available") + def test_bounded_and_nonempty(self): + """ + Test `is_bounded` and `is_nonempty` for a valid cardinality set. + """ + discrete_set = DiscreteScenarioSet([[1, 2], [3, 4]]) + bounded_and_nonempty_check(self, discrete_set), + class TestAxisAlignedEllipsoidalSet(unittest.TestCase): """ diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 6e357298660..e59493fbc8b 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -3158,6 +3158,32 @@ def point_in_set(self, point): rounded_point = np.round(point, decimals=num_decimals) return np.any(np.all(rounded_point == rounded_scenarios, axis=1)) + def validate(self, config): + """ + Check DiscreteScenarioSet validity. + + Raises + ------ + ValueError + If finiteness or nonemptiness checks fail. + """ + scenario_arr = self.scenarios + + # check nonemptiness + if len(scenario_arr) < 1: + raise ValueError( + "Scenarios set must be nonempty. " + f"Got scenarios: {scenario_arr}" + ) + + # check finiteness + for scenario in scenario_arr: + if not np.all(np.isfinite(scenario)): + raise ValueError( + "Not all scenarios are finite. " + f"Got scenario: {scenario}" + ) + class IntersectionSet(UncertaintySet): """ From 4e03241bc7ec694eae9318a63f5ec39a8eea35ce Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 22:45:48 -0400 Subject: [PATCH 32/83] Add IntersectionSet validate method and tests --- .../pyros/tests/test_uncertainty_sets.py | 40 +++++++++++++++++++ pyomo/contrib/pyros/uncertainty_sets.py | 18 +++++++++ 2 files changed, 58 insertions(+) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 905b7f580c5..c72f775c3aa 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -1409,6 +1409,46 @@ def test_add_bounds_on_uncertain_parameters(self): with self.assertRaisesRegex(ValueError, ".*to match the set dimension.*"): iset.point_in_set([1, 2, 3]) + @unittest.skipUnless(baron_available, "BARON is not available") + def test_validate(self): + """ + Test validate checks perform as expected. + """ + CONFIG = pyros_config() + CONFIG.global_solver = global_solver + + # construct a valid intersection set + bset = BoxSet(bounds=[[-1, 1], [-1, 1], [-1, 1]]) + aset = AxisAlignedEllipsoidalSet([0, 0, 0], [1, 1, 1]) + intersection_set = IntersectionSet(box_set=bset, axis_aligned_set=aset) + + # validate raises no issues on valid set + intersection_set.validate(config=CONFIG) + + # check when individual sets fail validation method + bset = BoxSet(bounds=[[-1, 1], [-1, 1], [-1, 1]]) + bset.bounds[0][0] = 2 + aset = AxisAlignedEllipsoidalSet([0, 0, 0], [1, 1, 1]) + intersection_set = IntersectionSet(box_set=bset, axis_aligned_set=aset) + exc_str = r"Lower bound 2 exceeds upper bound 1" + with self.assertRaisesRegex(ValueError, exc_str): + intersection_set.validate(config=CONFIG) + + # check when individual sets are not actually intersecting + bset1 = BoxSet(bounds=[[1, 2], [1, 2]]) + bset2 = BoxSet(bounds=[[-2, -1], [-2, -1]]) + intersection_set = IntersectionSet(box_set1=bset1, box_set2=bset2) + exc_str = r"Could not compute.*bound in dimension.*Solver status summary:.*" + with self.assertRaisesRegex(ValueError, exc_str): + intersection_set.validate(config=CONFIG) + + @unittest.skipUnless(baron_available, "BARON is not available") + def test_bounded_and_nonempty(self): + """ + Test `is_bounded` and `is_nonempty` for a valid cardinality set. + """ + discrete_set = DiscreteScenarioSet([[1, 2], [3, 4]]) + bounded_and_nonempty_check(self, discrete_set), class TestCardinalitySet(unittest.TestCase): """ diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index e59493fbc8b..91251ef60e4 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -3370,3 +3370,21 @@ def set_as_constraint(self, uncertain_params=None, block=None): uncertainty_cons=all_cons, auxiliary_vars=all_aux_vars, ) + + def validate(self, config): + """ + Check IntersectionSet validity. + + Raises + ------ + ValueError + If finiteness or nonemptiness checks fail. + """ + the_sets = self.all_sets + + # validate each set + for a_set in the_sets: + a_set.validate(config) + + # check boundedness and nonemptiness of intersected set + super().validate(config) From 72d89bef51b973fc2ed71c2d996fb4e08549caa3 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 10 Apr 2025 22:51:13 -0400 Subject: [PATCH 33/83] Run black --- .../pyros/tests/test_uncertainty_sets.py | 71 ++++++++++--------- pyomo/contrib/pyros/uncertainty_sets.py | 53 ++++++-------- 2 files changed, 56 insertions(+), 68 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index c72f775c3aa..a48546ed11c 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -83,16 +83,10 @@ def bounded_and_nonempty_check(test, unc_set): CONFIG.global_solver = global_solver # check is_bounded - test.assertTrue( - unc_set.is_bounded(config=CONFIG), - "Set is not bounded." - ) + test.assertTrue(unc_set.is_bounded(config=CONFIG), "Set is not bounded.") # check is_nonempty - test.assertTrue( - unc_set.is_nonempty(config=CONFIG), - "Set is empty." - ) + test.assertTrue(unc_set.is_nonempty(config=CONFIG), "Set is empty.") class TestBoxSet(unittest.TestCase): @@ -371,7 +365,7 @@ def test_validate(self): CONFIG = Bunch() # construct valid box set - box_set = BoxSet(bounds=[[1., 2.], [3., 4.]]) + box_set = BoxSet(bounds=[[1.0, 2.0], [3.0, 4.0]]) # validate raises no issues on valid set box_set.validate(config=CONFIG) @@ -393,7 +387,7 @@ def test_bounded_and_nonempty(self): """ Test `is_bounded` and `is_nonempty` for a valid box set. """ - box_set = BoxSet(bounds=[[1., 2.], [3., 4.]]) + box_set = BoxSet(bounds=[[1.0, 2.0], [3.0, 4.0]]) bounded_and_nonempty_check(self, box_set), @@ -606,8 +600,8 @@ def test_validate(self): CONFIG = Bunch() # construct a valid budget set - budget_mat = [[1., 0., 1.], [0., 1., 0.]] - budget_rhs_vec = [1., 3.] + budget_mat = [[1.0, 0.0, 1.0], [0.0, 1.0, 0.0]] + budget_rhs_vec = [1.0, 3.0] budget_set = BudgetSet(budget_mat, budget_rhs_vec) # validate raises no issues on valid set @@ -665,8 +659,8 @@ def test_bounded_and_nonempty(self): """ Test `is_bounded` and `is_nonempty` for a valid cardinality set. """ - budget_mat = [[1., 0., 1.], [0., 1., 0.]] - budget_rhs_vec = [1., 3.] + budget_mat = [[1.0, 0.0, 1.0], [0.0, 1.0, 0.0]] + budget_rhs_vec = [1.0, 3.0] budget_set = BudgetSet(budget_mat, budget_rhs_vec) bounded_and_nonempty_check(self, budget_set), @@ -988,7 +982,7 @@ def test_validate(self): CONFIG = Bunch() # construct a valid factor model set - origin = [0., 0., 0.] + origin = [0.0, 0.0, 0.0] number_of_factors = 2 psi_mat = [[1, 0], [0, 1], [1, 1]] beta = 0.5 @@ -1043,7 +1037,7 @@ def test_bounded_and_nonempty(self): """ Test `is_bounded` and `is_nonempty` for a valid cardinality set. """ - origin = [0., 0., 0.] + origin = [0.0, 0.0, 0.0] number_of_factors = 2 psi_mat = [[1, 0], [0, 1], [1, 1]] beta = 0.5 @@ -1450,6 +1444,7 @@ def test_bounded_and_nonempty(self): discrete_set = DiscreteScenarioSet([[1, 2], [3, 4]]) bounded_and_nonempty_check(self, discrete_set), + class TestCardinalitySet(unittest.TestCase): """ Tests for the CardinalitySet. @@ -1604,7 +1599,7 @@ def test_validate(self): # construct a valid cardinality set cardinality_set = CardinalitySet( - origin=[0., 0.], positive_deviation=[1., 1.], gamma=2 + origin=[0.0, 0.0], positive_deviation=[1.0, 1.0], gamma=2 ) # validate raises no issues on valid set @@ -1651,7 +1646,9 @@ def test_bounded_and_nonempty(self): """ Test `is_bounded` and `is_nonempty` for a valid cardinality set. """ - cardinality_set = CardinalitySet(origin=[0, 0], positive_deviation=[1, 1], gamma=2) + cardinality_set = CardinalitySet( + origin=[0, 0], positive_deviation=[1, 1], gamma=2 + ) bounded_and_nonempty_check(self, cardinality_set), @@ -1952,8 +1949,8 @@ def test_validate(self): CONFIG = Bunch() # construct a valid axis aligned ellipsoidal set - center = [0., 0.] - half_lengths = [1., 3.] + center = [0.0, 0.0] + half_lengths = [1.0, 3.0] a_ellipsoid_set = AxisAlignedEllipsoidalSet(center, half_lengths) # validate raises no issues on valid set @@ -1983,8 +1980,8 @@ def test_bounded_and_nonempty(self): """ Test `is_bounded` and `is_nonempty` for a valid cardinality set. """ - center = [0., 0.] - half_lengths = [1., 3.] + center = [0.0, 0.0] + half_lengths = [1.0, 3.0] a_ellipsoid_set = AxisAlignedEllipsoidalSet(center, half_lengths) bounded_and_nonempty_check(self, a_ellipsoid_set), @@ -2298,8 +2295,8 @@ def test_validate(self): CONFIG = Bunch() # construct a valid ellipsoidal set - center = [0., 0.] - shape_matrix = [[1., 0.], [0., 2.]] + center = [0.0, 0.0] + shape_matrix = [[1.0, 0.0], [0.0, 2.0]] scale = 1 ellipsoid_set = EllipsoidalSet(center, shape_matrix, scale) @@ -2349,8 +2346,8 @@ def test_bounded_and_nonempty(self): """ Test `is_bounded` and `is_nonempty` for a valid cardinality set. """ - center = [0., 0.] - shape_matrix = [[1., 0.], [0., 2.]] + center = [0.0, 0.0] + shape_matrix = [[1.0, 0.0], [0.0, 2.0]] scale = 1 ellipsoid_set = EllipsoidalSet(center, shape_matrix, scale) bounded_and_nonempty_check(self, ellipsoid_set), @@ -2555,8 +2552,8 @@ def test_validate(self): # construct a valid polyhedral set polyhedral_set = PolyhedralSet( - lhs_coefficients_mat=[[1., 0.], [-1., 1.], [-1., -1.]], - rhs_vec=[2., -1., -1.] + lhs_coefficients_mat=[[1.0, 0.0], [-1.0, 1.0], [-1.0, -1.0]], + rhs_vec=[2.0, -1.0, -1.0], ) # validate raises no issues on valid set @@ -2575,7 +2572,7 @@ def test_validate(self): polyhedral_set.validate(config=CONFIG) # check when LHS matrix is not full column rank - polyhedral_set.coefficients_mat = [[0., 0.], [0., 1.], [0., -1.]] + polyhedral_set.coefficients_mat = [[0.0, 0.0], [0.0, 1.0], [0.0, -1.0]] exc_str = r".*all entries zero in columns at indexes: 0.*" with self.assertRaisesRegex(ValueError, exc_str): polyhedral_set.validate(config=CONFIG) @@ -2586,8 +2583,8 @@ def test_bounded_and_nonempty(self): Test `is_bounded` and `is_nonempty` for a valid cardinality set. """ polyhedral_set = PolyhedralSet( - lhs_coefficients_mat=[[1., 0.], [-1., 1.], [-1., -1.]], - rhs_vec=[2., -1., -1.] + lhs_coefficients_mat=[[1.0, 0.0], [-1.0, 1.0], [-1.0, -1.0]], + rhs_vec=[2.0, -1.0, -1.0], ) bounded_and_nonempty_check(self, polyhedral_set), @@ -2688,7 +2685,6 @@ def test_solve_feasibility(self): with self.assertRaisesRegex(ValueError, exc_str): custom_set._solve_feasibility(baron) - # test default is_bounded @unittest.skipUnless(baron_available, "BARON is not available") def test_is_bounded(self): @@ -2714,8 +2710,11 @@ def test_is_bounded(self): # check with parameter_bounds should always take less time than solving 2N # optimization problems - self.assertLess(time_with_bounds_provided, time_without_bounds_provided, - "Boundedness check with provided parameter_bounds took longer than expected.") + self.assertLess( + time_with_bounds_provided, + time_without_bounds_provided, + "Boundedness check with provided parameter_bounds took longer than expected.", + ) # when bad bounds are provided for val_str in ["inf", "nan"]: @@ -2742,7 +2741,9 @@ def test_is_nonempty(self): # check when nominal point is not in set CONFIG.nominal_uncertain_param_vals = [-2, -2] - self.assertFalse(custom_set.is_nonempty(config=CONFIG), "Nominal point is in set") + self.assertFalse( + custom_set.is_nonempty(config=CONFIG), "Nominal point is in set" + ) # check when feasibility problem fails CONFIG.nominal_uncertain_param_vals = None diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 91251ef60e4..65369fe3241 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -36,7 +36,7 @@ minimize, Var, VarData, - NonNegativeReals + NonNegativeReals, ) from pyomo.core.expr import mutable_expression, native_numeric_types, value from pyomo.core.util import quicksum, dot_product @@ -640,15 +640,15 @@ def validate(self, config): if not check_nonempty: raise ValueError( - "Failed nonemptiness check. Nominal point is not in the set. " - f"Nominal point:\n {config.nominal_uncertain_param_vals}." - ) + "Failed nonemptiness check. Nominal point is not in the set. " + f"Nominal point:\n {config.nominal_uncertain_param_vals}." + ) if not check_bounded: raise ValueError( - "Failed boundedness check. Parameter bounds are not finite. " - f"Parameter bounds:\n {self.parameter_bounds}." - ) + "Failed boundedness check. Parameter bounds are not finite. " + f"Parameter bounds:\n {self.parameter_bounds}." + ) @abc.abstractmethod def set_as_constraint(self, uncertain_params=None, block=None): @@ -790,9 +790,7 @@ def _solve_feasibility(self, solver): model.param_vars = Var(range(self.dim)) # add bounds on param vars - self._add_bounds_on_uncertain_parameters( - model.param_vars, global_solver=solver - ) + self._add_bounds_on_uncertain_parameters(model.param_vars, global_solver=solver) # add constraints self.set_as_constraint(uncertain_params=model.param_vars, block=model) @@ -1205,8 +1203,7 @@ def validate(self, config): # finiteness check if not np.all(np.isfinite(bounds_arr)): raise ValueError( - "Not all bounds are finite. " - f"\nGot bounds:\n {bounds_arr}" + "Not all bounds are finite. " f"\nGot bounds:\n {bounds_arr}" ) # check LB <= UB @@ -1703,7 +1700,6 @@ def set_as_constraint(self, uncertain_params=None, block=None): auxiliary_vars=aux_var_list, ) - def validate(self, config): """ Check PolyhedralSet validity. @@ -1718,7 +1714,9 @@ def validate(self, config): rhs_vec_arr = self.rhs_vec # finiteness check - if not (np.all(np.isfinite(lhs_coeffs_arr)) and np.all(np.isfinite(rhs_vec_arr))): + if not ( + np.all(np.isfinite(lhs_coeffs_arr)) and np.all(np.isfinite(rhs_vec_arr)) + ): raise ValueError( "LHS coefficient matrix or RHS vector are not finite. " f"\nGot LHS matrix:\n{lhs_coeffs_arr},\nRHS vector:\n{rhs_vec_arr}" @@ -1999,9 +1997,9 @@ def validate(self, config): # finiteness check if not ( - np.all(np.isfinite(lhs_coeffs_arr)) - and np.all(np.isfinite(rhs_vec_arr)) - and np.all(np.isfinite(orig_val)) + np.all(np.isfinite(lhs_coeffs_arr)) + and np.all(np.isfinite(rhs_vec_arr)) + and np.all(np.isfinite(orig_val)) ): raise ValueError( "Origin, LHS coefficient matrix or RHS vector are not finite. " @@ -2393,10 +2391,7 @@ def validate(self, config): # finiteness check if not np.all(np.isfinite(orig_val)): - raise ValueError( - "Origin is not finite. " - f"Got origin: {orig_val}" - ) + raise ValueError("Origin is not finite. " f"Got origin: {orig_val}") # check psi is full column rank psi_mat_rank = np.linalg.matrix_rank(psi_mat_arr) @@ -2598,10 +2593,7 @@ def validate(self, config): half_lengths = self.half_lengths # finiteness check - if not ( - np.all(np.isfinite(ctr)) - and np.all(np.isfinite(half_lengths)) - ): + if not (np.all(np.isfinite(ctr)) and np.all(np.isfinite(half_lengths))): raise ValueError( "Center or half-lengths are not finite. " f"Got center: {ctr}, half-lengths: {half_lengths}" @@ -2976,10 +2968,7 @@ def validate(self, config): # finiteness check if not np.all(np.isfinite(ctr)): - raise ValueError( - "Center is not finite. " - f"Got center: {ctr}" - ) + raise ValueError("Center is not finite. " f"Got center: {ctr}") # check shape matrix is positive semidefinite self._verify_positive_definite(shape_mat_arr) @@ -3172,16 +3161,14 @@ def validate(self, config): # check nonemptiness if len(scenario_arr) < 1: raise ValueError( - "Scenarios set must be nonempty. " - f"Got scenarios: {scenario_arr}" + "Scenarios set must be nonempty. " f"Got scenarios: {scenario_arr}" ) # check finiteness for scenario in scenario_arr: if not np.all(np.isfinite(scenario)): raise ValueError( - "Not all scenarios are finite. " - f"Got scenario: {scenario}" + "Not all scenarios are finite. " f"Got scenario: {scenario}" ) From af2d26101c8e6d345a933766b3a2b006747e3bdf Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Fri, 11 Apr 2025 09:26:43 -0400 Subject: [PATCH 34/83] Fix typos in test_bounded_and_nonempty docstring --- .../pyros/tests/test_uncertainty_sets.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index a48546ed11c..ab3e4b8e8e7 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -657,7 +657,7 @@ def test_validate(self): @unittest.skipUnless(baron_available, "BARON is not available") def test_bounded_and_nonempty(self): """ - Test `is_bounded` and `is_nonempty` for a valid cardinality set. + Test `is_bounded` and `is_nonempty` for a valid budget set. """ budget_mat = [[1.0, 0.0, 1.0], [0.0, 1.0, 0.0]] budget_rhs_vec = [1.0, 3.0] @@ -1035,7 +1035,7 @@ def test_validate(self): @unittest.skipUnless(baron_available, "BARON is not available") def test_bounded_and_nonempty(self): """ - Test `is_bounded` and `is_nonempty` for a valid cardinality set. + Test `is_bounded` and `is_nonempty` for a valid factor model set. """ origin = [0.0, 0.0, 0.0] number_of_factors = 2 @@ -1439,10 +1439,12 @@ def test_validate(self): @unittest.skipUnless(baron_available, "BARON is not available") def test_bounded_and_nonempty(self): """ - Test `is_bounded` and `is_nonempty` for a valid cardinality set. + Test `is_bounded` and `is_nonempty` for a valid intersection set. """ - discrete_set = DiscreteScenarioSet([[1, 2], [3, 4]]) - bounded_and_nonempty_check(self, discrete_set), + bset = BoxSet(bounds=[[-1, 1], [-1, 1], [-1, 1]]) + aset = AxisAlignedEllipsoidalSet([0, 0, 0], [1, 1, 1]) + intersection_set = IntersectionSet(box_set=bset, axis_aligned_set=aset) + bounded_and_nonempty_check(self, intersection_set), class TestCardinalitySet(unittest.TestCase): @@ -1791,7 +1793,7 @@ def test_validate(self): @unittest.skipUnless(baron_available, "BARON is not available") def test_bounded_and_nonempty(self): """ - Test `is_bounded` and `is_nonempty` for a valid cardinality set. + Test `is_bounded` and `is_nonempty` for a valid discrete scenario set. """ discrete_set = DiscreteScenarioSet([[1, 2], [3, 4]]) bounded_and_nonempty_check(self, discrete_set), @@ -1978,7 +1980,7 @@ def test_validate(self): @unittest.skipUnless(baron_available, "BARON is not available") def test_bounded_and_nonempty(self): """ - Test `is_bounded` and `is_nonempty` for a valid cardinality set. + Test `is_bounded` and `is_nonempty` for a valid axis aligned ellipsoidal set. """ center = [0.0, 0.0] half_lengths = [1.0, 3.0] @@ -2344,7 +2346,7 @@ def test_validate(self): @unittest.skipUnless(baron_available, "BARON is not available") def test_bounded_and_nonempty(self): """ - Test `is_bounded` and `is_nonempty` for a valid cardinality set. + Test `is_bounded` and `is_nonempty` for a valid ellipsoidal set. """ center = [0.0, 0.0] shape_matrix = [[1.0, 0.0], [0.0, 2.0]] @@ -2580,7 +2582,7 @@ def test_validate(self): @unittest.skipUnless(baron_available, "BARON is not available") def test_bounded_and_nonempty(self): """ - Test `is_bounded` and `is_nonempty` for a valid cardinality set. + Test `is_bounded` and `is_nonempty` for a valid polyhedral set. """ polyhedral_set = PolyhedralSet( lhs_coefficients_mat=[[1.0, 0.0], [-1.0, 1.0], [-1.0, -1.0]], From 9c97b6904b171c6354224488e97817fd1addac53 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 24 Apr 2025 21:56:31 -0400 Subject: [PATCH 35/83] make valid_num_types set, update validate_arg_type --- pyomo/contrib/pyros/uncertainty_sets.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index cd3f2f325ef..7dff0d38f1a 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -50,7 +50,7 @@ ) -valid_num_types = tuple(native_numeric_types) +valid_num_types = native_numeric_types def standardize_uncertain_param_vars(obj, dim): @@ -219,7 +219,7 @@ def validate_arg_type( Name of argument to be displayed in exception message. arg_val : object Value of argument to be checked. - valid_types : type or tuple of types + valid_types : type, tuple of types, or iterable of types Valid types for the argument value. valid_type_desc : str or None, optional Description of valid types for the argument value; @@ -242,6 +242,9 @@ def validate_arg_type( If the finiteness check on a numerical value returns a negative result. """ + # convert to tuple if necessary + if isinstance(valid_types, Iterable): + valid_types = tuple(valid_types) if not isinstance(arg_val, valid_types): if valid_type_desc is not None: type_phrase = f"not {valid_type_desc}" From 1e508f31ca67c99869e275601a78c171fd528e76 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 24 Apr 2025 21:58:56 -0400 Subject: [PATCH 36/83] Update EllipsoidalSet docstring --- 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 7dff0d38f1a..8fabbf0e320 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -2772,7 +2772,7 @@ class EllipsoidalSet(UncertaintySet): [0, 0, 3, 0]], [0, 0, 0. 4]]) >>> conf_ellipsoid.scale - ...9.4877... + np.float64(9.4877...) >>> conf_ellipsoid.gaussian_conf_lvl 0.95 From 0bad4b59b6dfa52ab28daf1f6d44761c6685a4b8 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Wed, 14 May 2025 15:00:56 -0500 Subject: [PATCH 37/83] Add FBBT method for obtaining param_bounds --- pyomo/contrib/pyros/uncertainty_sets.py | 40 ++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 8fabbf0e320..a201bc08e0f 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -48,6 +48,8 @@ POINT_IN_UNCERTAINTY_SET_TOL, standardize_component_data, ) +from pyomo.contrib.fbbt.fbbt import fbbt +from pyomo.common.errors import InfeasibleConstraintException valid_num_types = native_numeric_types @@ -584,8 +586,13 @@ def is_bounded(self, config): else: # initialize uncertain parameter variables param_bounds_arr = np.array( - self._compute_parameter_bounds(solver=config.global_solver) + self._fbbt_parameter_bounds(config) ) + if not all(map(lambda x: all(x), param_bounds_arr)): + # solve bounding problems if FBBT cannot find bounds + param_bounds_arr = np.array( + self._compute_parameter_bounds(solver=config.global_solver) + ) all_bounds_finite = np.all(np.isfinite(param_bounds_arr)) # log result @@ -800,6 +807,37 @@ def _compute_parameter_bounds(self, solver, index=None): return param_bounds + def _fbbt_parameter_bounds(self, config): + """ + Obtain parameter bounds of the uncertainty set using FBBT. + + Parameters + ---------- + config : ConfigDict + PyROS solver configuration. + """ + bounding_model = self._create_bounding_model() + + # calculate bounds with FBBT + fbbt_exception_str = f"Error computing parameter bounds with FBBT for {self}" + try: + fbbt(bounding_model) + except InfeasibleConstraintException as fbbt_infeasible_con_exception: + config.progress_logger.error( + f"{fbbt_exception_str}\n" + f"{fbbt_infeasible_con_exception}" + ) + except Exception as fbbt_exception: + config.progress_logger.error( + f"{fbbt_exception_str}\n" + f"{fbbt_exception}" + ) + + param_bounds = [(var.lower, var.upper) for var in bounding_model.param_vars.values()] + + return param_bounds + + def _solve_feasibility(self, solver): """ Construct and solve feasibility problem using uncertainty set From 8cb4497887d8b2c0901a6a26ae3300976a9568c9 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 15 May 2025 16:20:24 -0500 Subject: [PATCH 38/83] Remove catching general FBBT exception --- pyomo/contrib/pyros/uncertainty_sets.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index a201bc08e0f..9e1606469d8 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -827,11 +827,6 @@ def _fbbt_parameter_bounds(self, config): f"{fbbt_exception_str}\n" f"{fbbt_infeasible_con_exception}" ) - except Exception as fbbt_exception: - config.progress_logger.error( - f"{fbbt_exception_str}\n" - f"{fbbt_exception}" - ) param_bounds = [(var.lower, var.upper) for var in bounding_model.param_vars.values()] From 42fce228a7aa162e9659225e178188466a0f8f5d Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 15 May 2025 16:23:48 -0500 Subject: [PATCH 39/83] Remove PolyhedralSet _validate method and test --- .../pyros/tests/test_uncertainty_sets.py | 11 ------ pyomo/contrib/pyros/uncertainty_sets.py | 39 ------------------- 2 files changed, 50 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index f072744e523..86d780285f0 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -2540,17 +2540,6 @@ def test_error_on_inconsistent_rows(self): # 3-vector mismatches 2 rows pset.rhs_vec = [1, 3, 2] - def test_error_on_empty_set(self): - """ - Check ValueError raised if nonemptiness check performed - at construction returns a negative result. - """ - exc_str = r"PolyhedralSet.*is empty.*" - - # assert error on construction - with self.assertRaisesRegex(ValueError, exc_str): - PolyhedralSet([[1], [-1]], rhs_vec=[1, -3]) - def test_set_as_constraint(self): """ Test method for setting up constraints works correctly. diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 9e1606469d8..8231c9af88a 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -1649,45 +1649,6 @@ def __init__(self, lhs_coefficients_mat, rhs_vec): self.coefficients_mat = lhs_coefficients_mat self.rhs_vec = rhs_vec - # validate nonemptiness and boundedness here. - # This check is only performed at construction. - self._validate() - - # TODO this has a _validate method... - # seems redundant with new validate method and should be consolidated - def _validate(self): - """ - Check polyhedral set attributes are such that set is nonempty - (solve a feasibility problem). - - Raises - ------ - ValueError - If set is empty, or the check was not - successfully completed due to numerical issues. - """ - # solve LP - res = sp.optimize.linprog( - c=np.zeros(self.coefficients_mat.shape[1]), - A_ub=self.coefficients_mat, - b_ub=self.rhs_vec, - method="highs", - bounds=(None, None), - ) - - # check termination - if res.status == 1 or res.status == 4: - raise ValueError( - "Could not verify nonemptiness of the " - "polyhedral set (`scipy.optimize.linprog(method='highs')` " - f" status {res.status}) " - ) - elif res.status == 2: - raise ValueError( - "PolyhedralSet defined by 'coefficients_mat' and " - "'rhs_vec' is empty. Check arguments" - ) - @property def type(self): """ From 754be47cc8a32540d75310cbba77850805d3c377 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 15 May 2025 16:24:31 -0500 Subject: [PATCH 40/83] Add validate_array to all validate methods --- pyomo/contrib/pyros/uncertainty_sets.py | 199 +++++++++++++++++------- 1 file changed, 145 insertions(+), 54 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 8231c9af88a..b86c07dd81f 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -1312,15 +1312,21 @@ def validate(self, config): Raises ------ ValueError - If finiteness and LB<=UB checks fail. + If any uncertainty set attributes are not valid. + If bound are not valid or finiteness and LB<=UB checks fail. """ bounds_arr = np.array(self.parameter_bounds) - # finiteness check - if not np.all(np.isfinite(bounds_arr)): - raise ValueError( - "Not all bounds are finite. " f"\nGot bounds:\n {bounds_arr}" - ) + # check bounds are valid + # this includes a finiteness check + validate_array( + arr=bounds_arr, + arr_name="bounds", + dim=2, + valid_types=valid_num_types, + valid_type_desc="a valid numeric type", + required_shape=[None, 2], + ) # check LB <= UB for lb, ub in bounds_arr: @@ -1578,18 +1584,30 @@ def validate(self, config): Raises ------ ValueError + If any uncertainty set attributes are not valid. If finiteness, positive deviation, or gamma checks fail. """ orig_val = self.origin pos_dev = self.positive_deviation gamma = self.gamma - # finiteness check - if not (np.all(np.isfinite(orig_val)) and np.all(np.isfinite(pos_dev))): - raise ValueError( - "Origin value and/or positive deviation are not finite. " - f"Got origin: {orig_val}, positive deviation: {pos_dev}" - ) + # check origin, positive deviation, and gamma are valid + # this includes a finiteness check + validate_array( + arr=orig_val, + arr_name="origin", + dim=1, + valid_types=valid_num_types, + valid_type_desc="a valid numeric type", + ) + validate_array( + arr=pos_dev, + arr_name="positive_deviation", + dim=1, + valid_types=valid_num_types, + valid_type_desc="a valid numeric type", + ) + validate_arg_type("gamma", gamma, valid_num_types, "a valid numeric type", False) # check deviation is positive for dev_val in pos_dev: @@ -1786,20 +1804,31 @@ def validate(self, config): Raises ------ ValueError + If any uncertainty set attributes are not valid. If finiteness, full column rank of LHS matrix, is_bounded, or is_nonempty checks fail. """ lhs_coeffs_arr = self.coefficients_mat rhs_vec_arr = self.rhs_vec - # finiteness check - if not ( - np.all(np.isfinite(lhs_coeffs_arr)) and np.all(np.isfinite(rhs_vec_arr)) - ): - raise ValueError( - "LHS coefficient matrix or RHS vector are not finite. " - f"\nGot LHS matrix:\n{lhs_coeffs_arr},\nRHS vector:\n{rhs_vec_arr}" - ) + # check lhs matrix and rhs vector are valid + # this includes a finiteness check + validate_array( + arr=lhs_coeffs_arr, + arr_name="coefficients_mat", + dim=2, + valid_types=valid_num_types, + valid_type_desc="a valid numeric type", + required_shape=None, + ) + validate_array( + arr=rhs_vec_arr, + arr_name="rhs_vec", + dim=1, + valid_types=valid_num_types, + valid_type_desc="a valid numeric type", + required_shape=None, + ) # check no column is all zeros. otherwise, set is unbounded cols_with_all_zeros = np.nonzero( @@ -2069,6 +2098,7 @@ def validate(self, config): Raises ------ ValueError + If any uncertainty set attributes are not valid. If finiteness, full 0 column or row of LHS matrix, or positive RHS vector checks fail. """ @@ -2076,16 +2106,32 @@ def validate(self, config): rhs_vec_arr = self.budget_rhs_vec orig_val = self.origin - # finiteness check - if not ( - np.all(np.isfinite(lhs_coeffs_arr)) - and np.all(np.isfinite(rhs_vec_arr)) - and np.all(np.isfinite(orig_val)) - ): - raise ValueError( - "Origin, LHS coefficient matrix or RHS vector are not finite. " - f"\nGot origin:\n{orig_val},\nLHS matrix:\n{lhs_coeffs_arr},\nRHS vector:\n{rhs_vec_arr}" - ) + # check budget matrix, budget limits, and origin are valid + # this includes a finiteness check + validate_array( + arr=lhs_coeffs_arr, + arr_name="budget_membership_mat", + dim=2, + valid_types=valid_num_types, + valid_type_desc="a valid numeric type", + required_shape=None, + ) + validate_array( + arr=rhs_vec_arr, + arr_name="budget_rhs_vec", + dim=1, + valid_types=valid_num_types, + valid_type_desc="a valid numeric type", + required_shape=None, + ) + validate_array( + arr=orig_val, + arr_name="origin", + dim=1, + valid_types=valid_num_types, + valid_type_desc="a valid numeric type", + required_shape=None, + ) # check no row, col, are all zeros and all values are 0-1. # ensure all entries are 0-1 values @@ -2465,6 +2511,7 @@ def validate(self, config): Raises ------ ValueError + If any uncertainty set attributes are not valid. If finiteness full column rank of Psi matrix, or beta between 0 and 1 checks fail. """ @@ -2472,9 +2519,24 @@ def validate(self, config): psi_mat_arr = self.psi_mat beta = self.beta - # finiteness check - if not np.all(np.isfinite(orig_val)): - raise ValueError("Origin is not finite. " f"Got origin: {orig_val}") + # check origin, psi matrix, and beta are valid + # this includes a finiteness check + validate_array( + arr=orig_val, + arr_name="origin", + dim=1, + valid_types=valid_num_types, + valid_type_desc="a valid numeric type", + ) + validate_array( + arr=psi_mat_arr, + arr_name="psi_mat", + dim=2, + valid_types=valid_num_types, + valid_type_desc="a valid numeric type", + required_shape=None, + ) + validate_arg_type("beta", beta, valid_num_types, "a valid numeric type", False) # check psi is full column rank psi_mat_rank = np.linalg.matrix_rank(psi_mat_arr) @@ -2672,17 +2734,30 @@ def validate(self, config): Raises ------ ValueError + If any uncertainty set attributes are not valid. If finiteness or positive half-length checks fail. """ ctr = self.center half_lengths = self.half_lengths - # finiteness check - if not (np.all(np.isfinite(ctr)) and np.all(np.isfinite(half_lengths))): - raise ValueError( - "Center or half-lengths are not finite. " - f"Got center: {ctr}, half-lengths: {half_lengths}" - ) + # check center and half lengths are valid + # this includes a finiteness check + validate_array( + arr=ctr, + arr_name="center", + dim=1, + valid_types=valid_num_types, + valid_type_desc="a valid numeric type", + required_shape=None, + ) + validate_array( + arr=half_lengths, + arr_name="half_lengths", + dim=1, + valid_types=valid_num_types, + valid_type_desc="a valid numeric type", + required_shape=None, + ) # ensure half-lengths are non-negative for half_len in half_lengths: @@ -3046,6 +3121,7 @@ def validate(self, config): Raises ------ ValueError + If any uncertainty set attributes are not valid. If finiteness, positive semi-definite, or positive scale checks fail. """ @@ -3053,9 +3129,25 @@ def validate(self, config): shape_mat_arr = self.shape_matrix scale = self.scale - # finiteness check - if not np.all(np.isfinite(ctr)): - raise ValueError("Center is not finite. " f"Got center: {ctr}") + # check center, shape matrix, and scale are valid + # this includes a finiteness check + validate_array( + arr=ctr, + arr_name="center", + dim=1, + valid_types=valid_num_types, + valid_type_desc="a valid numeric type", + required_shape=None, + ) + validate_array( + arr=shape_mat_arr, + arr_name="shape_matrix", + dim=2, + valid_types=valid_num_types, + valid_type_desc="a valid numeric type", + required_shape=None, + ) + validate_arg_type("scale", scale, valid_num_types, "a valid numeric type", False) # check shape matrix is positive semidefinite self._verify_positive_definite(shape_mat_arr) @@ -3247,18 +3339,17 @@ def validate(self, config): """ scenario_arr = self.scenarios - # check nonemptiness - if len(scenario_arr) < 1: - raise ValueError( - "Scenarios set must be nonempty. " f"Got scenarios: {scenario_arr}" - ) - - # check finiteness - for scenario in scenario_arr: - if not np.all(np.isfinite(scenario)): - raise ValueError( - "Not all scenarios are finite. " f"Got scenario: {scenario}" - ) + # check that all scenarios are valid + # this includes a nonemptiness check and a finiteness check + # using the validate_arr method + validate_array( + arr=scenario_arr, + arr_name="scenarios", + dim=2, + valid_types=valid_num_types, + valid_type_desc="a valid numeric type", + required_shape=None, + ) class IntersectionSet(UncertaintySet): From b01710842776fb010418ab61121da2c17cc303e2 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 15 May 2025 16:25:06 -0500 Subject: [PATCH 41/83] Split test_validate for each set specific check --- .../pyros/tests/test_uncertainty_sets.py | 347 ++++++++++++++++-- 1 file changed, 310 insertions(+), 37 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 86d780285f0..7d65c16bff4 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -363,7 +363,7 @@ def test_add_bounds_on_uncertain_parameters(self): def test_validate(self): """ - Test validate checks perform as expected. + Test validate performs as expected. """ CONFIG = Bunch() @@ -373,18 +373,40 @@ def test_validate(self): # validate raises no issues on valid set box_set.validate(config=CONFIG) + def test_validate_finiteness(self): + """ + Test validate finiteness check performs as expected. + """ + CONFIG = Bunch() + + # construct valid box set + box_set = BoxSet(bounds=[[1.0, 2.0], [3.0, 4.0]]) + # check when values are not finite box_set.bounds[0][0] = np.nan - exc_str = r"Not all bounds are finite. \nGot bounds:.*" + exc_str = ( + r"Entry 'nan' of the argument `bounds` " + r"is not a finite numeric value" + ) with self.assertRaisesRegex(ValueError, exc_str): box_set.validate(config=CONFIG) + def test_validate_bounds(self): + """ + Test validate bounds check performs as expected. + """ + CONFIG = Bunch() + + # construct valid box set + box_set = BoxSet(bounds=[[1.0, 2.0], [3.0, 4.0]]) + # check when LB >= UB box_set.bounds[0][0] = 5 exc_str = r"Lower bound 5.0 exceeds upper bound 2.0" with self.assertRaisesRegex(ValueError, exc_str): box_set.validate(config=CONFIG) + @unittest.skipUnless(baron_available, "BARON is not available") def test_bounded_and_nonempty(self): """ @@ -618,7 +640,7 @@ def test_add_bounds_on_uncertain_parameters(self): def test_validate(self): """ - Test validate checks perform as expected. + Test validate performs as expected. """ CONFIG = Bunch() @@ -630,25 +652,56 @@ def test_validate(self): # validate raises no issues on valid set budget_set.validate(config=CONFIG) + def test_validate_finiteness(self): + """ + Test validate finiteness check performs as expected. + """ + CONFIG = Bunch() + + # construct a valid budget set + budget_mat = [[1.0, 0.0, 1.0], [0.0, 1.0, 0.0]] + budget_rhs_vec = [1.0, 3.0] + budget_set = BudgetSet(budget_mat, budget_rhs_vec) + # check when values are not finite budget_set.origin[0] = np.nan - exc_str = r"Origin, LHS coefficient matrix or RHS vector are not finite. .*" + exc_str = ( + r"Entry 'nan' of the argument `origin` " + r"is not a finite numeric value" + ) with self.assertRaisesRegex(ValueError, exc_str): budget_set.validate(config=CONFIG) budget_set.origin[0] = 0 budget_set.budget_rhs_vec[0] = np.nan - exc_str = r"Origin, LHS coefficient matrix or RHS vector are not finite. .*" + exc_str = ( + r"Entry 'nan' of the argument `budget_rhs_vec` " + r"is not a finite numeric value" + ) with self.assertRaisesRegex(ValueError, exc_str): budget_set.validate(config=CONFIG) budget_set.budget_rhs_vec[0] = 1 budget_set.budget_membership_mat[0][0] = np.nan - exc_str = r"LHS coefficient matrix or RHS vector are not finite. .*" + exc_str = ( + r"Entry 'nan' of the argument `budget_membership_mat` " + r"is not a finite numeric value" + ) with self.assertRaisesRegex(ValueError, exc_str): budget_set.validate(config=CONFIG) budget_set.budget_membership_mat[0][0] = 1 + def test_validate_rhs(self): + """ + Test validate RHS check performs as expected. + """ + CONFIG = Bunch() + + # construct a valid budget set + budget_mat = [[1.0, 0.0, 1.0], [0.0, 1.0, 0.0]] + budget_rhs_vec = [1.0, 3.0] + budget_set = BudgetSet(budget_mat, budget_rhs_vec) + # check when rhs has negative element budget_set.budget_rhs_vec = [1, -1] exc_str = r"Entry -1 of.*'budget_rhs_vec' is negative*" @@ -656,12 +709,29 @@ def test_validate(self): budget_set.validate(config=CONFIG) budget_set.budget_rhs_vec = budget_rhs_vec + def test_validate_non_bool_budget_mat_entry(self): + """ + Test validate LHS matrix 0-1 entries check performs as expected. + """ + CONFIG = Bunch() + + # construct a valid budget set + budget_mat = [[1.0, 0.0, 1.0], [0.0, 1.0, 0.0]] + budget_rhs_vec = [1.0, 3.0] + budget_set = BudgetSet(budget_mat, budget_rhs_vec) + # check when not all lhs entries are 0-1 budget_set.budget_membership_mat = [[1, 0, 1], [1, 1, 0.1]] exc_str = r"Attempting.*entries.*not 0-1 values \(example: 0.1\).*" with self.assertRaisesRegex(ValueError, exc_str): budget_set.validate(config=CONFIG) + def test_validate_budget_mat_all_zero_rows(self): + """ + Test validate LHS matrix all zero row check performs as expected. + """ + CONFIG = Bunch() + # check when row has all zeros invalid_row_mat = [[0, 0, 0], [1, 1, 1], [0, 0, 0]] budget_rhs_vec = [1, 1, 2] @@ -670,9 +740,16 @@ def test_validate(self): with self.assertRaisesRegex(ValueError, exc_str): budget_set.validate(config=CONFIG) + def test_validate_budget_mat_all_zero_columns(self): + """ + Test validate LHS matrix all zero column check performs as expected. + """ + CONFIG = Bunch() + # check when column has all zeros - budget_set.budget_membership_mat = [[0, 0, 1], [0, 0, 1], [0, 0, 1]] - budget_set.budget_rhs_vec = [1, 1, 2] + invalid_col_mat = [[0, 0, 1], [0, 0, 1], [0, 0, 1]] + budget_rhs_vec = [1, 1, 2] + budget_set = BudgetSet(invalid_col_mat, budget_rhs_vec) exc_str = r".*all entries zero in columns at indexes: 0, 1.*" with self.assertRaisesRegex(ValueError, exc_str): budget_set.validate(config=CONFIG) @@ -1012,7 +1089,7 @@ def test_add_bounds_on_uncertain_parameters(self): def test_validate(self): """ - Test validate checks perform as expected. + Test validate performs as expected. """ CONFIG = Bunch() @@ -1026,12 +1103,40 @@ def test_validate(self): # validate raises no issues on valid set factor_set.validate(config=CONFIG) + def test_validate_finiteness(self): + """ + Test validate finiteness check performs as expected. + """ + CONFIG = Bunch() + + # construct a valid factor model set + origin = [0.0, 0.0, 0.0] + number_of_factors = 2 + psi_mat = [[1, 0], [0, 1], [1, 1]] + beta = 0.5 + factor_set = FactorModelSet(origin, number_of_factors, psi_mat, beta) + # check when values are not finite factor_set.origin[0] = np.nan - exc_str = r"Origin is not finite. .*" + exc_str = ( + r"Entry 'nan' of the argument `origin` " + r"is not a finite numeric value" + ) with self.assertRaisesRegex(ValueError, exc_str): factor_set.validate(config=CONFIG) - factor_set.origin[0] = 0 + + def test_validate_beta(self): + """ + Test validate beta check performs as expected. + """ + CONFIG = Bunch() + + # construct a valid factor model set + origin = [0.0, 0.0, 0.0] + number_of_factors = 2 + psi_mat = [[1, 0], [0, 1], [1, 1]] + beta = 0.5 + factor_set = FactorModelSet(origin, number_of_factors, psi_mat, beta) # check when beta is invalid neg_beta = -0.5 @@ -1047,6 +1152,12 @@ def test_validate(self): with self.assertRaisesRegex(ValueError, big_exc_str): factor_set.validate(config=CONFIG) + def test_validate_psi_matrix(self): + """ + Test validate psi matrix check performs as expected. + """ + CONFIG = Bunch() + # check when psi matrix is rank defficient with self.assertRaisesRegex(ValueError, r"full column rank.*\(2, 3\)"): # more columns than rows @@ -1662,7 +1773,7 @@ def test_add_bounds_on_uncertain_parameters(self): def test_validate(self): """ - Test validate checks perform as expected. + Test validate performs as expected. """ CONFIG = Bunch() @@ -1674,26 +1785,64 @@ def test_validate(self): # validate raises no issues on valid set cardinality_set.validate(config=CONFIG) + def test_validate_finiteness(self): + """ + Test validate finiteness check performs as expected. + """ + CONFIG = Bunch() + + # construct a valid cardinality set + cardinality_set = CardinalitySet( + origin=[0.0, 0.0], positive_deviation=[1.0, 1.0], gamma=2 + ) + # check when values are not finite cardinality_set.origin[0] = np.nan - exc_str = r"Origin value and/or positive deviation are not finite. .*" + exc_str = ( + r"Entry 'nan' of the argument `origin` " + r"is not a finite numeric value" + ) with self.assertRaisesRegex(ValueError, exc_str): cardinality_set.validate(config=CONFIG) cardinality_set.origin[0] = 0 cardinality_set.positive_deviation[0] = np.nan - exc_str = r"Origin value and/or positive deviation are not finite. .*" + exc_str = ( + r"Entry 'nan' of the argument `positive_deviation` " + r"is not a finite numeric value" + ) with self.assertRaisesRegex(ValueError, exc_str): cardinality_set.validate(config=CONFIG) + def test_validate_pos_deviation(self): + """ + Test validate positive deviation check performs as expected. + """ + CONFIG = Bunch() + + # construct a valid cardinality set + cardinality_set = CardinalitySet( + origin=[0.0, 0.0], positive_deviation=[1.0, 1.0], gamma=2 + ) + # check when deviation is negative cardinality_set.positive_deviation[0] = -2 exc_str = r"Entry -2.0 of attribute 'positive_deviation' is negative value" with self.assertRaisesRegex(ValueError, exc_str): cardinality_set.validate(config=CONFIG) + def test_validate_gamma(self): + """ + Test validate gamma check performs as expected. + """ + CONFIG = Bunch() + + # construct a valid cardinality set + cardinality_set = CardinalitySet( + origin=[0.0, 0.0], positive_deviation=[1.0, 1.0], gamma=2 + ) + # check when gamma is invalid - cardinality_set.positive_deviation[0] = 1 cardinality_set.gamma = 3 exc_str = ( r".*attribute 'gamma' must be a real number " @@ -1838,7 +1987,7 @@ def test_add_bounds_on_uncertain_parameters(self): def test_validate(self): """ - Test validate checks perform as expected. + Test validate performs as expected. """ CONFIG = Bunch() @@ -1848,25 +1997,49 @@ def test_validate(self): # validate raises no issues on valid set discrete_set.validate(config=CONFIG) - # check when scenario set is empty - # TODO should this method can be used to create ragged arrays - # after a set is created. There are currently no checks for this - # in any validate method. It may be good to included validate_array - # in all validate methods as well to guard against it. - discrete_set = DiscreteScenarioSet([[0]]) - discrete_set.scenarios.pop(0) - exc_str = r"Scenarios set must be nonempty. .*" - with self.assertRaisesRegex(ValueError, exc_str): - discrete_set.validate(config=CONFIG) + def test_validate_finiteness(self): + """ + Test validate finiteness check performs as expected. + """ + CONFIG = Bunch() + + # construct a valid discrete scenario set + discrete_set = DiscreteScenarioSet([[1, 2], [3, 4]]) + + # validate raises no issues on valid set + discrete_set.validate(config=CONFIG) # check when not all scenarios are finite discrete_set = DiscreteScenarioSet([[1, 2], [3, 4]]) - exc_str = r"Not all scenarios are finite. .*" for val_str in ["inf", "nan"]: + exc_str = ( + fr"Entry '{val_str}' of the argument `scenarios` " + r"is not a finite numeric value" + ) discrete_set.scenarios[0] = [1, float(val_str)] with self.assertRaisesRegex(ValueError, exc_str): discrete_set.validate(config=CONFIG) + def test_validate_nonemptiness(self): + """ + Test validate finiteness check performs as expected. + """ + CONFIG = Bunch() + + # construct a valid discrete scenario set + discrete_set = DiscreteScenarioSet([[1, 2], [3, 4]]) + + # validate raises no issues on valid set + discrete_set.validate(config=CONFIG) + + # check when scenario set is empty + discrete_set = DiscreteScenarioSet([[0]]) + discrete_set.scenarios.pop(0) # remove initial scenario + discrete_set.scenarios.append([]) # add empty scenario + exc_str = r".* argument `scenarios` must be non-empty" + with self.assertRaisesRegex(ValueError, exc_str): + discrete_set.validate(config=CONFIG) + @unittest.skipUnless(baron_available, "BARON is not available") def test_bounded_and_nonempty(self): """ @@ -2033,7 +2206,7 @@ def test_add_bounds_on_uncertain_parameters(self): def test_validate(self): """ - Test validate checks perform as expected. + Test validate performs as expected. """ CONFIG = Bunch() @@ -2045,19 +2218,47 @@ def test_validate(self): # validate raises no issues on valid set a_ellipsoid_set.validate(config=CONFIG) + def test_validate_finiteness(self): + """ + Test validate finiteness check performs as expected. + """ + CONFIG = Bunch() + + # construct a valid axis aligned ellipsoidal set + center = [0.0, 0.0] + half_lengths = [1.0, 3.0] + a_ellipsoid_set = AxisAlignedEllipsoidalSet(center, half_lengths) + # check when values are not finite a_ellipsoid_set.center[0] = np.nan - exc_str = r"Center or half-lengths are not finite. .*" + exc_str = ( + r"Entry 'nan' of the argument `center` " + r"is not a finite numeric value" + ) with self.assertRaisesRegex(ValueError, exc_str): a_ellipsoid_set.validate(config=CONFIG) a_ellipsoid_set.center[0] = 0 a_ellipsoid_set.half_lengths[0] = np.nan - exc_str = r"Center or half-lengths are not finite. .*" + exc_str = ( + r"Entry 'nan' of the argument `half_lengths` " + r"is not a finite numeric value" + ) with self.assertRaisesRegex(ValueError, exc_str): a_ellipsoid_set.validate(config=CONFIG) a_ellipsoid_set.half_lengths[0] = 1 + def test_validate_half_length(self): + """ + Test validate half-lengths check performs as expected. + """ + CONFIG = Bunch() + + # construct a valid axis aligned ellipsoidal set + center = [0.0, 0.0] + half_lengths = [1.0, 3.0] + a_ellipsoid_set = AxisAlignedEllipsoidalSet(center, half_lengths) + # check when half lengths are negative a_ellipsoid_set.half_lengths = [1, -1] exc_str = r"Entry -1 of.*'half_lengths' is negative.*" @@ -2389,7 +2590,7 @@ def test_add_bounds_on_uncertain_parameters(self): def test_validate(self): """ - Test validate checks perform as expected. + Test validate performs as expected. """ CONFIG = Bunch() @@ -2402,12 +2603,38 @@ def test_validate(self): # validate raises no issues on valid set ellipsoid_set.validate(config=CONFIG) + def test_validate_finiteness(self): + """ + Test validate finiteness check performs as expected. + """ + CONFIG = Bunch() + + # construct a valid ellipsoidal set + center = [0.0, 0.0] + shape_matrix = [[1.0, 0.0], [0.0, 2.0]] + scale = 1 + ellipsoid_set = EllipsoidalSet(center, shape_matrix, scale) + # check when values are not finite ellipsoid_set.center[0] = np.nan - exc_str = r"Center is not finite. .*" + exc_str = ( + r"Entry 'nan' of the argument `center` " + r"is not a finite numeric value" + ) with self.assertRaisesRegex(ValueError, exc_str): ellipsoid_set.validate(config=CONFIG) - ellipsoid_set.center[0] = 0 + + def test_validate_scale(self): + """ + Test validate scale check performs as expected. + """ + CONFIG = Bunch() + + # construct a valid ellipsoidal set + center = [0.0, 0.0] + shape_matrix = [[1.0, 0.0], [0.0, 2.0]] + scale = 1 + ellipsoid_set = EllipsoidalSet(center, shape_matrix, scale) # check when scale is not positive ellipsoid_set.scale = -1 @@ -2415,11 +2642,22 @@ def test_validate(self): with self.assertRaisesRegex(ValueError, exc_str): ellipsoid_set.validate(config=CONFIG) + def test_validate_shape_matrix(self): + """ + Test validate shape matrix check performs as expected. + """ + CONFIG = Bunch() + + # construct a valid ellipsoidal set + center = [0.0, 0.0] + shape_matrix = [[1.0, 0.0], [0.0, 2.0]] + scale = 1 + ellipsoid_set = EllipsoidalSet(center, shape_matrix, scale) + # check when shape matrix is invalid center = [0, 0] scale = 3 - # assert error on construction with self.assertRaisesRegex( ValueError, r"Shape matrix must be symmetric", @@ -2440,6 +2678,7 @@ def test_validate(self): ellipsoid_set = EllipsoidalSet(center, [[1, 0], [0, -2]], scale) ellipsoid_set.validate(config=CONFIG) + @unittest.skipUnless(baron_available, "BARON is not available") def test_bounded_and_nonempty(self): """ @@ -2647,7 +2886,7 @@ def test_add_bounds_on_uncertain_parameters(self): @unittest.skipUnless(baron_available, "BARON is not available") def test_validate(self): """ - Test validate checks perform as expected. + Test validate performs as expected. """ CONFIG = pyros_config() CONFIG.global_solver = global_solver @@ -2661,18 +2900,52 @@ def test_validate(self): # validate raises no issues on valid set polyhedral_set.validate(config=CONFIG) + @unittest.skipUnless(baron_available, "BARON is not available") + def test_validate_finiteness(self): + """ + Test validate finiteness check performs as expected. + """ + CONFIG = pyros_config() + CONFIG.global_solver = global_solver + + # construct a valid polyhedral set + polyhedral_set = PolyhedralSet( + lhs_coefficients_mat=[[1.0, 0.0], [-1.0, 1.0], [-1.0, -1.0]], + rhs_vec=[2.0, -1.0, -1.0], + ) + # check when values are not finite polyhedral_set.rhs_vec[0] = np.nan - exc_str = r"LHS coefficient matrix or RHS vector are not finite. .*" + exc_str = ( + r"Entry 'nan' of the argument `rhs_vec` " + r"is not a finite numeric value" + ) with self.assertRaisesRegex(ValueError, exc_str): polyhedral_set.validate(config=CONFIG) polyhedral_set.rhs_vec[0] = 2 polyhedral_set.coefficients_mat[0][0] = np.nan - exc_str = r"LHS coefficient matrix or RHS vector are not finite. .*" + exc_str = ( + r"Entry 'nan' of the argument `coefficients_mat` " + r"is not a finite numeric value" + ) with self.assertRaisesRegex(ValueError, exc_str): polyhedral_set.validate(config=CONFIG) + @unittest.skipUnless(baron_available, "BARON is not available") + def test_validate_full_column_rank(self): + """ + Test validate full column rank check performs as expected. + """ + CONFIG = pyros_config() + CONFIG.global_solver = global_solver + + # construct a valid polyhedral set + polyhedral_set = PolyhedralSet( + lhs_coefficients_mat=[[1.0, 0.0], [-1.0, 1.0], [-1.0, -1.0]], + rhs_vec=[2.0, -1.0, -1.0], + ) + # check when LHS matrix is not full column rank polyhedral_set.coefficients_mat = [[0.0, 0.0], [0.0, 1.0], [0.0, -1.0]] exc_str = r".*all entries zero in columns at indexes: 0.*" From d946d4d5c43c0e1ddafa5fda0644ffc1317ea308 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Thu, 15 May 2025 16:40:54 -0500 Subject: [PATCH 42/83] Run black --- .../pyros/tests/test_uncertainty_sets.py | 23 ++++++------------- pyomo/contrib/pyros/uncertainty_sets.py | 20 ++++++++-------- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 7d65c16bff4..c70792ec6ed 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -385,8 +385,7 @@ def test_validate_finiteness(self): # check when values are not finite box_set.bounds[0][0] = np.nan exc_str = ( - r"Entry 'nan' of the argument `bounds` " - r"is not a finite numeric value" + r"Entry 'nan' of the argument `bounds` " r"is not a finite numeric value" ) with self.assertRaisesRegex(ValueError, exc_str): box_set.validate(config=CONFIG) @@ -406,7 +405,6 @@ def test_validate_bounds(self): with self.assertRaisesRegex(ValueError, exc_str): box_set.validate(config=CONFIG) - @unittest.skipUnless(baron_available, "BARON is not available") def test_bounded_and_nonempty(self): """ @@ -666,8 +664,7 @@ def test_validate_finiteness(self): # check when values are not finite budget_set.origin[0] = np.nan exc_str = ( - r"Entry 'nan' of the argument `origin` " - r"is not a finite numeric value" + r"Entry 'nan' of the argument `origin` " r"is not a finite numeric value" ) with self.assertRaisesRegex(ValueError, exc_str): budget_set.validate(config=CONFIG) @@ -1119,8 +1116,7 @@ def test_validate_finiteness(self): # check when values are not finite factor_set.origin[0] = np.nan exc_str = ( - r"Entry 'nan' of the argument `origin` " - r"is not a finite numeric value" + r"Entry 'nan' of the argument `origin` " r"is not a finite numeric value" ) with self.assertRaisesRegex(ValueError, exc_str): factor_set.validate(config=CONFIG) @@ -1799,8 +1795,7 @@ def test_validate_finiteness(self): # check when values are not finite cardinality_set.origin[0] = np.nan exc_str = ( - r"Entry 'nan' of the argument `origin` " - r"is not a finite numeric value" + r"Entry 'nan' of the argument `origin` " r"is not a finite numeric value" ) with self.assertRaisesRegex(ValueError, exc_str): cardinality_set.validate(config=CONFIG) @@ -2232,8 +2227,7 @@ def test_validate_finiteness(self): # check when values are not finite a_ellipsoid_set.center[0] = np.nan exc_str = ( - r"Entry 'nan' of the argument `center` " - r"is not a finite numeric value" + r"Entry 'nan' of the argument `center` " r"is not a finite numeric value" ) with self.assertRaisesRegex(ValueError, exc_str): a_ellipsoid_set.validate(config=CONFIG) @@ -2618,8 +2612,7 @@ def test_validate_finiteness(self): # check when values are not finite ellipsoid_set.center[0] = np.nan exc_str = ( - r"Entry 'nan' of the argument `center` " - r"is not a finite numeric value" + r"Entry 'nan' of the argument `center` " r"is not a finite numeric value" ) with self.assertRaisesRegex(ValueError, exc_str): ellipsoid_set.validate(config=CONFIG) @@ -2678,7 +2671,6 @@ def test_validate_shape_matrix(self): ellipsoid_set = EllipsoidalSet(center, [[1, 0], [0, -2]], scale) ellipsoid_set.validate(config=CONFIG) - @unittest.skipUnless(baron_available, "BARON is not available") def test_bounded_and_nonempty(self): """ @@ -2917,8 +2909,7 @@ def test_validate_finiteness(self): # check when values are not finite polyhedral_set.rhs_vec[0] = np.nan exc_str = ( - r"Entry 'nan' of the argument `rhs_vec` " - r"is not a finite numeric value" + r"Entry 'nan' of the argument `rhs_vec` " r"is not a finite numeric value" ) with self.assertRaisesRegex(ValueError, exc_str): polyhedral_set.validate(config=CONFIG) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index b86c07dd81f..85894987bbe 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -585,9 +585,7 @@ def is_bounded(self, config): all_bounds_finite = np.all(np.isfinite(param_bounds_arr)) else: # initialize uncertain parameter variables - param_bounds_arr = np.array( - self._fbbt_parameter_bounds(config) - ) + param_bounds_arr = np.array(self._fbbt_parameter_bounds(config)) if not all(map(lambda x: all(x), param_bounds_arr)): # solve bounding problems if FBBT cannot find bounds param_bounds_arr = np.array( @@ -824,15 +822,15 @@ def _fbbt_parameter_bounds(self, config): fbbt(bounding_model) except InfeasibleConstraintException as fbbt_infeasible_con_exception: config.progress_logger.error( - f"{fbbt_exception_str}\n" - f"{fbbt_infeasible_con_exception}" + f"{fbbt_exception_str}\n" f"{fbbt_infeasible_con_exception}" ) - param_bounds = [(var.lower, var.upper) for var in bounding_model.param_vars.values()] + param_bounds = [ + (var.lower, var.upper) for var in bounding_model.param_vars.values() + ] return param_bounds - def _solve_feasibility(self, solver): """ Construct and solve feasibility problem using uncertainty set @@ -1607,7 +1605,9 @@ def validate(self, config): valid_types=valid_num_types, valid_type_desc="a valid numeric type", ) - validate_arg_type("gamma", gamma, valid_num_types, "a valid numeric type", False) + validate_arg_type( + "gamma", gamma, valid_num_types, "a valid numeric type", False + ) # check deviation is positive for dev_val in pos_dev: @@ -3147,7 +3147,9 @@ def validate(self, config): valid_type_desc="a valid numeric type", required_shape=None, ) - validate_arg_type("scale", scale, valid_num_types, "a valid numeric type", False) + validate_arg_type( + "scale", scale, valid_num_types, "a valid numeric type", False + ) # check shape matrix is positive semidefinite self._verify_positive_definite(shape_mat_arr) From fd2ae35cbcae0ef16b3f98b702311fe232febca7 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Fri, 16 May 2025 13:26:13 -0500 Subject: [PATCH 43/83] Fix EllipsoidalSet docstring typos --- pyomo/contrib/pyros/uncertainty_sets.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 85894987bbe..1b93084eb6f 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -2834,12 +2834,12 @@ class EllipsoidalSet(UncertaintySet): ... gaussian_conf_lvl=0.95, ... ) >>> conf_ellipsoid.center - array([0, 0, 0, 0]) + array([0., 0., 0., 0.]) >>> conf_ellipsoid.shape_matrix - array([[1, 0, 0, 0]], - [0, 2, 0, 0]], - [0, 0, 3, 0]], - [0, 0, 0. 4]]) + array([[1, 0, 0, 0], + [0, 2, 0, 0], + [0, 0, 3, 0], + [0, 0, 0, 4]]) >>> conf_ellipsoid.scale np.float64(9.4877...) >>> conf_ellipsoid.gaussian_conf_lvl From f39c7ed6a36b5c752cd85e17434834d01e6f6a72 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Fri, 16 May 2025 14:25:00 -0500 Subject: [PATCH 44/83] Update is_bounded docs, fix validate docs typos --- pyomo/contrib/pyros/uncertainty_sets.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 1b93084eb6f..de525881cad 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -570,8 +570,11 @@ def is_bounded(self, config): This check is carried out by checking if all parameter bounds are finite. - If no parameter bounds are available, the check is done by - solving a sequence of maximization and minimization problems + If no parameter bounds are available, the following processes are run + to perform the check: + (i) feasibility-based bounds tightening is used to obtain parameter + bounds, and if not all bound are found, + (ii) solving a sequence of maximization and minimization problems (in which the objective for each problem is the value of a single uncertain parameter). If any of the optimization models cannot be solved successfully to @@ -1311,7 +1314,7 @@ def validate(self, config): ------ ValueError If any uncertainty set attributes are not valid. - If bound are not valid or finiteness and LB<=UB checks fail. + If finiteness or bounds checks fail. """ bounds_arr = np.array(self.parameter_bounds) @@ -1805,8 +1808,8 @@ def validate(self, config): ------ ValueError If any uncertainty set attributes are not valid. - If finiteness, full column rank of LHS matrix, is_bounded, - or is_nonempty checks fail. + If finiteness, full column rank of LHS matrix, bounded, + or nonempty checks fail. """ lhs_coeffs_arr = self.coefficients_mat rhs_vec_arr = self.rhs_vec @@ -2512,7 +2515,7 @@ def validate(self, config): ------ ValueError If any uncertainty set attributes are not valid. - If finiteness full column rank of Psi matrix, or + If finiteness, full column rank of Psi matrix, or beta between 0 and 1 checks fail. """ orig_val = self.origin From 04e70829d03289519dadcf71738490e2a7cf788d Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Fri, 16 May 2025 16:03:57 -0500 Subject: [PATCH 45/83] Replace EllipsoidalSet doc for np.float with ... --- 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 de525881cad..24e08e4bb82 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -2844,7 +2844,7 @@ class EllipsoidalSet(UncertaintySet): [0, 0, 3, 0], [0, 0, 0, 4]]) >>> conf_ellipsoid.scale - np.float64(9.4877...) + ...9.4877... >>> conf_ellipsoid.gaussian_conf_lvl 0.95 From 1273c764c0815fb45dc2afd8ed321e57296f8744 Mon Sep 17 00:00:00 2001 From: Jason Yao <131029507+jas-yao@users.noreply.github.com> Date: Mon, 23 Jun 2025 19:59:57 -0700 Subject: [PATCH 46/83] Update validate_arg_type docstring Co-authored-by: Jason Sherman --- 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 24e08e4bb82..7a8d2b98b21 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -221,7 +221,7 @@ def validate_arg_type( Name of argument to be displayed in exception message. arg_val : object Value of argument to be checked. - valid_types : type, tuple of types, or iterable of types + valid_types : type or iterable of types Valid types for the argument value. valid_type_desc : str or None, optional Description of valid types for the argument value; From 28e134fdb6ff3991e9cad666cc496ed1132a10a1 Mon Sep 17 00:00:00 2001 From: Jason Yao <131029507+jas-yao@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:02:16 -0700 Subject: [PATCH 47/83] Update is_bounded docstring Co-authored-by: Jason Sherman --- 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 7a8d2b98b21..7c6d0d52acd 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -580,7 +580,7 @@ def is_bounded(self, config): If any of the optimization models cannot be solved successfully to optimality, then False is returned. - This method is invoked by validate. + This method is invoked by ``self.validate()``. """ # use parameter bounds if they are available param_bounds_arr = self.parameter_bounds From e45ca30948dc4d40a3ec56e6971eda524145fe70 Mon Sep 17 00:00:00 2001 From: Jason Yao <131029507+jas-yao@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:03:27 -0700 Subject: [PATCH 48/83] Update is_nonempty docstring Co-authored-by: Jason Sherman --- 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 7c6d0d52acd..7428c1471fc 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -617,7 +617,7 @@ def is_nonempty(self, config): Returns ------- : bool - True if the nominal point is within the set, + True if the uncertainty set is nonempty, and False otherwise. """ # check if nominal point is in set for quick test From 37e2c746c549b8573a0e99d3210688f6e3458fe4 Mon Sep 17 00:00:00 2001 From: Jason Yao <131029507+jas-yao@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:06:51 -0700 Subject: [PATCH 49/83] Simplify is_nonempty nominal point check Co-authored-by: Jason Sherman --- pyomo/contrib/pyros/uncertainty_sets.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 7428c1471fc..2b2e0e51e2d 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -621,10 +621,8 @@ def is_nonempty(self, config): and False otherwise. """ # check if nominal point is in set for quick test - set_nonempty = False if config.nominal_uncertain_param_vals: - if self.point_in_set(config.nominal_uncertain_param_vals): - set_nonempty = True + set_nonempty = self.point_in_set(config.nominal_uncertain_param_vals) else: # construct feasibility problem and solve otherwise set_nonempty = self._solve_feasibility(config.global_solver) From 40d609dbac610e65a04a93627df33110ae434d73 Mon Sep 17 00:00:00 2001 From: Jason Yao <131029507+jas-yao@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:13:16 -0700 Subject: [PATCH 50/83] Update _fbbt_parameter_bounds error message Co-authored-by: Jason Sherman --- pyomo/contrib/pyros/uncertainty_sets.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 2b2e0e51e2d..e05251c854d 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -823,7 +823,10 @@ def _fbbt_parameter_bounds(self, config): fbbt(bounding_model) except InfeasibleConstraintException as fbbt_infeasible_con_exception: config.progress_logger.error( - f"{fbbt_exception_str}\n" f"{fbbt_infeasible_con_exception}" + "Encountered the following exception " + f"while computing parameter bounds with FBBT " + f"for uncertainty set {self}:\n " + f"{fbbt_infeasible_con_exception!r}" ) param_bounds = [ From 6b8a2c56e093364d1610ec198bd9894516cafb34 Mon Sep 17 00:00:00 2001 From: Jason Yao <131029507+jas-yao@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:14:47 -0700 Subject: [PATCH 51/83] Fix validate docstring typo Co-authored-by: Jason Sherman --- 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 e05251c854d..08964f72844 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -648,7 +648,7 @@ def validate(self, config): Raises ------ ValueError - If nonemptiness check or boundedness check fail. + If nonemptiness check or boundedness check fails. """ check_nonempty = self.is_nonempty(config=config) check_bounded = self.is_bounded(config=config) From 8954a5edd49b322a289c0340cf6f1f047c1ec1e2 Mon Sep 17 00:00:00 2001 From: Jason Yao <131029507+jas-yao@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:17:17 -0700 Subject: [PATCH 52/83] Update validate nonemptiness check error message Co-authored-by: Jason Sherman --- pyomo/contrib/pyros/uncertainty_sets.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 08964f72844..05b4c58da7d 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -654,10 +654,7 @@ def validate(self, config): check_bounded = self.is_bounded(config=config) if not check_nonempty: - raise ValueError( - "Failed nonemptiness check. Nominal point is not in the set. " - f"Nominal point:\n {config.nominal_uncertain_param_vals}." - ) + raise ValueError(f"Nonemptiness check failed for uncertainty set {self}.") if not check_bounded: raise ValueError( From 759109fd3119424f548c8b0e185683a0af640e01 Mon Sep 17 00:00:00 2001 From: Jason Yao <131029507+jas-yao@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:18:06 -0700 Subject: [PATCH 53/83] Update validate boundedness check error message Co-authored-by: Jason Sherman --- pyomo/contrib/pyros/uncertainty_sets.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 05b4c58da7d..6e71722c4ce 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -657,10 +657,7 @@ def validate(self, config): raise ValueError(f"Nonemptiness check failed for uncertainty set {self}.") if not check_bounded: - raise ValueError( - "Failed boundedness check. Parameter bounds are not finite. " - f"Parameter bounds:\n {self.parameter_bounds}." - ) + raise ValueError(f"Boundedness check failed for uncertainty set {self}.") @abc.abstractmethod def set_as_constraint(self, uncertain_params=None, block=None): From 5972216cd84aa0e85b1432a80f2a6287d4cbf943 Mon Sep 17 00:00:00 2001 From: Jason Yao <131029507+jas-yao@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:22:49 -0700 Subject: [PATCH 54/83] Fix EllipsoidalSet doctest test error Co-authored-by: Jason Sherman --- pyomo/contrib/pyros/uncertainty_sets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 6e71722c4ce..8f481d1efac 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -2838,7 +2838,8 @@ class EllipsoidalSet(UncertaintySet): [0, 2, 0, 0], [0, 0, 3, 0], [0, 0, 0, 4]]) - >>> conf_ellipsoid.scale + >>> conf_ellipsoid.scale # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE + ...9.4877... >>> conf_ellipsoid.gaussian_conf_lvl 0.95 From 8f5787e940a264e39146a4745947e2f045c5fce7 Mon Sep 17 00:00:00 2001 From: Jason Yao <131029507+jas-yao@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:25:20 -0700 Subject: [PATCH 55/83] Update test_is_bounded no parameter_bounds test Co-authored-by: Jason Sherman --- pyomo/contrib/pyros/tests/test_uncertainty_sets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index c70792ec6ed..a2fff2dde29 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -3082,7 +3082,7 @@ def test_is_bounded(self): time_with_bounds_provided = end - start # when parameter_bounds is not available - custom_set.parameter_bounds = None + custom_set.parameter_bounds = [] start = time.time() self.assertTrue(custom_set.is_bounded(config=CONFIG), "Set is not bounded") end = time.time() From 4ef4ffb08045901d24f55101ce3f7958909aba5c Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Sun, 29 Jun 2025 07:19:26 -0700 Subject: [PATCH 56/83] Remove trailing comma in test_bounded_and_nonempty --- .../pyros/tests/test_uncertainty_sets.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index a2fff2dde29..c75b70ece5f 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -411,7 +411,7 @@ def test_bounded_and_nonempty(self): Test `is_bounded` and `is_nonempty` for a valid box set. """ box_set = BoxSet(bounds=[[1.0, 2.0], [3.0, 4.0]]) - bounded_and_nonempty_check(self, box_set), + bounded_and_nonempty_check(self, box_set) def test_is_coordinate_fixed(self): """ @@ -759,7 +759,7 @@ def test_bounded_and_nonempty(self): budget_mat = [[1.0, 0.0, 1.0], [0.0, 1.0, 0.0]] budget_rhs_vec = [1.0, 3.0] budget_set = BudgetSet(budget_mat, budget_rhs_vec) - bounded_and_nonempty_check(self, budget_set), + bounded_and_nonempty_check(self, budget_set) def test_is_coordinate_fixed(self): """ @@ -1184,7 +1184,7 @@ def test_bounded_and_nonempty(self): psi_mat = [[1, 0], [0, 1], [1, 1]] beta = 0.5 factor_set = FactorModelSet(origin, number_of_factors, psi_mat, beta) - bounded_and_nonempty_check(self, factor_set), + bounded_and_nonempty_check(self, factor_set) def test_is_coordinate_fixed(self): """ @@ -1605,7 +1605,7 @@ def test_bounded_and_nonempty(self): bset = BoxSet(bounds=[[-1, 1], [-1, 1], [-1, 1]]) aset = AxisAlignedEllipsoidalSet([0, 0, 0], [1, 1, 1]) intersection_set = IntersectionSet(box_set=bset, axis_aligned_set=aset) - bounded_and_nonempty_check(self, intersection_set), + bounded_and_nonempty_check(self, intersection_set) def test_is_coordinate_fixed(self): """ @@ -1862,7 +1862,7 @@ def test_bounded_and_nonempty(self): cardinality_set = CardinalitySet( origin=[0, 0], positive_deviation=[1, 1], gamma=2 ) - bounded_and_nonempty_check(self, cardinality_set), + bounded_and_nonempty_check(self, cardinality_set) def test_is_coordinate_fixed(self): """ @@ -2041,7 +2041,7 @@ def test_bounded_and_nonempty(self): Test `is_bounded` and `is_nonempty` for a valid discrete scenario set. """ discrete_set = DiscreteScenarioSet([[1, 2], [3, 4]]) - bounded_and_nonempty_check(self, discrete_set), + bounded_and_nonempty_check(self, discrete_set) def test_is_coordinate_fixed(self): """ @@ -2267,7 +2267,7 @@ def test_bounded_and_nonempty(self): center = [0.0, 0.0] half_lengths = [1.0, 3.0] a_ellipsoid_set = AxisAlignedEllipsoidalSet(center, half_lengths) - bounded_and_nonempty_check(self, a_ellipsoid_set), + bounded_and_nonempty_check(self, a_ellipsoid_set) def test_is_coordinate_fixed(self): """ @@ -2680,7 +2680,7 @@ def test_bounded_and_nonempty(self): shape_matrix = [[1.0, 0.0], [0.0, 2.0]] scale = 1 ellipsoid_set = EllipsoidalSet(center, shape_matrix, scale) - bounded_and_nonempty_check(self, ellipsoid_set), + bounded_and_nonempty_check(self, ellipsoid_set) def test_is_coordinate_fixed(self): """ @@ -2952,7 +2952,7 @@ def test_bounded_and_nonempty(self): lhs_coefficients_mat=[[1.0, 0.0], [-1.0, 1.0], [-1.0, -1.0]], rhs_vec=[2.0, -1.0, -1.0], ) - bounded_and_nonempty_check(self, polyhedral_set), + bounded_and_nonempty_check(self, polyhedral_set) def test_is_coordinate_fixed(self): """ From 90d1eb7f9adff2873b2c7ba7938d3301ccecf29f Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Sun, 29 Jun 2025 07:23:42 -0700 Subject: [PATCH 57/83] Remove import redefinitions --- pyomo/contrib/pyros/tests/test_uncertainty_sets.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index c75b70ece5f..e0de1a7a03f 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -25,12 +25,10 @@ scipy_available, ) -from pyomo.common.collections import Bunch from pyomo.environ import SolverFactory from pyomo.core.base import ConcreteModel, Param, Var 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, From 0fc2ab00d27aae7b0c7b0fac2299fba77dd5a6c5 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Sun, 29 Jun 2025 15:14:44 -0700 Subject: [PATCH 58/83] Update UncertaintySet parameter_bounds docstring --- pyomo/contrib/pyros/uncertainty_sets.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 8f481d1efac..0429692f447 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -515,8 +515,13 @@ def parameter_bounds(self): Bounds for the value of each uncertain parameter constrained by the set (i.e. bounds for each set dimension). - This method should return an empty list if it can't be calculated - or a list of length = self.dim if it can. + Returns + ------- + : list of tuple + If the bounds can be calculated, then the list is of + length `N`, and each entry is a pair of numeric + (lower, upper) bounds for the corresponding + (Cartesian) coordinate. Otherwise, the list is empty. """ raise NotImplementedError From 77b490df2b35172274908dfee90a0d0e9d81e687 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Sun, 29 Jun 2025 15:17:26 -0700 Subject: [PATCH 59/83] update _fbbt_parameter_bounds docstring --- pyomo/contrib/pyros/uncertainty_sets.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 0429692f447..28060f93988 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -813,11 +813,17 @@ def _fbbt_parameter_bounds(self, config): ---------- config : ConfigDict PyROS solver configuration. + + Returns + ------- + param_bounds : list of tuple + List, of length `N`, containing + (lower bound, upper bound) pairs + for the uncertain parameters. """ bounding_model = self._create_bounding_model() # calculate bounds with FBBT - fbbt_exception_str = f"Error computing parameter bounds with FBBT for {self}" try: fbbt(bounding_model) except InfeasibleConstraintException as fbbt_infeasible_con_exception: From 8a5eb7d4a0b4f9a8e2f2e8c47775649eaf780809 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Sun, 29 Jun 2025 15:19:40 -0700 Subject: [PATCH 60/83] Add BARON check for test_is_coordinate_fixed --- pyomo/contrib/pyros/tests/test_uncertainty_sets.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index e0de1a7a03f..684641c85d3 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -1605,6 +1605,7 @@ def test_bounded_and_nonempty(self): intersection_set = IntersectionSet(box_set=bset, axis_aligned_set=aset) bounded_and_nonempty_check(self, intersection_set) + @unittest.skipUnless(baron_available, "BARON is not available") def test_is_coordinate_fixed(self): """ Test method for checking whether there are coordinates @@ -2952,6 +2953,7 @@ def test_bounded_and_nonempty(self): ) bounded_and_nonempty_check(self, polyhedral_set) + @unittest.skipUnless(baron_available, "BARON is not available") def test_is_coordinate_fixed(self): """ Test method for checking whether there are coordinates @@ -3130,6 +3132,7 @@ def test_is_nonempty(self): with self.assertRaisesRegex(ValueError, exc_str): custom_set.is_nonempty(config=CONFIG) + @unittest.skipUnless(baron_available, "BARON is not available") def test_is_coordinate_fixed(self): """ Test method for checking whether there are coordinates From a3e0439a6b152a1053a39851c221e22b905223d8 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Sun, 29 Jun 2025 15:47:16 -0700 Subject: [PATCH 61/83] Wrap long docstrings in test_uncertainty_sets --- pyomo/contrib/pyros/tests/test_uncertainty_sets.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 684641c85d3..433e0ea08d5 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -2037,7 +2037,8 @@ def test_validate_nonemptiness(self): @unittest.skipUnless(baron_available, "BARON is not available") def test_bounded_and_nonempty(self): """ - Test `is_bounded` and `is_nonempty` for a valid discrete scenario set. + Test `is_bounded` and `is_nonempty` for a valid + discrete scenario set. """ discrete_set = DiscreteScenarioSet([[1, 2], [3, 4]]) bounded_and_nonempty_check(self, discrete_set) @@ -2261,7 +2262,8 @@ def test_validate_half_length(self): @unittest.skipUnless(baron_available, "BARON is not available") def test_bounded_and_nonempty(self): """ - Test `is_bounded` and `is_nonempty` for a valid axis aligned ellipsoidal set. + Test `is_bounded` and `is_nonempty` for a valid + axis aligned ellipsoidal set. """ center = [0.0, 0.0] half_lengths = [1.0, 3.0] From 84a73a71dc9186d1f17830bffff54eaf2f8ef407 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Sun, 29 Jun 2025 16:08:09 -0700 Subject: [PATCH 62/83] Clarify checks performed in validate docstrings. --- pyomo/contrib/pyros/uncertainty_sets.py | 30 ++++++++++++++++--------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 28060f93988..6ac483d0e99 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -1320,7 +1320,8 @@ def validate(self, config): ------ ValueError If any uncertainty set attributes are not valid. - If finiteness or bounds checks fail. + (e.g., numeric values are infinite, + or ``self.parameter_bounds`` has LB > UB.) """ bounds_arr = np.array(self.parameter_bounds) @@ -1592,7 +1593,9 @@ def validate(self, config): ------ ValueError If any uncertainty set attributes are not valid. - If finiteness, positive deviation, or gamma checks fail. + (e.g., numeric values are infinite, + ``self.positive_deviation`` has negative values, + or ``self.gamma`` is out of range). """ orig_val = self.origin pos_dev = self.positive_deviation @@ -1814,8 +1817,9 @@ def validate(self, config): ------ ValueError If any uncertainty set attributes are not valid. - If finiteness, full column rank of LHS matrix, bounded, - or nonempty checks fail. + (e.g., numeric values are infinite, + or ``self.coefficients_mat`` has column of zeros). + If bounded and nonempty checks fail. """ lhs_coeffs_arr = self.coefficients_mat rhs_vec_arr = self.rhs_vec @@ -2108,8 +2112,9 @@ def validate(self, config): ------ ValueError If any uncertainty set attributes are not valid. - If finiteness, full 0 column or row of LHS matrix, - or positive RHS vector checks fail. + (e.g., numeric values are infinite, + ``self.budget_membership_mat`` has full column or row of zeros, + or ``self.budget_rhs_vec`` has negative values). """ lhs_coeffs_arr = self.budget_membership_mat rhs_vec_arr = self.budget_rhs_vec @@ -2521,8 +2526,9 @@ def validate(self, config): ------ ValueError If any uncertainty set attributes are not valid. - If finiteness, full column rank of Psi matrix, or - beta between 0 and 1 checks fail. + (e.g., numeric values are infinite, + ``self.psi_mat`` is not full column rank, + or ``self.beta`` is not between 0 and 1). """ orig_val = self.origin psi_mat_arr = self.psi_mat @@ -2744,7 +2750,8 @@ def validate(self, config): ------ ValueError If any uncertainty set attributes are not valid. - If finiteness or positive half-length checks fail. + (e.g., numeric values are infinite, + or ``self.half_lengths`` are negative). """ ctr = self.center half_lengths = self.half_lengths @@ -3132,8 +3139,9 @@ def validate(self, config): ------ ValueError If any uncertainty set attributes are not valid. - If finiteness, positive semi-definite, or - positive scale checks fail. + (e.g., numeric values are infinite, + ``self.shape_matrix`` is not positive semidefinite, + or ``self.scale`` is negative). """ ctr = self.center shape_mat_arr = self.shape_matrix From 743c5a8ce1b8b91ecb25ea9f0c397a0b7daf586a Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Sun, 29 Jun 2025 16:14:04 -0700 Subject: [PATCH 63/83] Wrap docstrings --- pyomo/contrib/pyros/uncertainty_sets.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 6ac483d0e99..d6d1fe7f099 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -575,15 +575,15 @@ def is_bounded(self, config): This check is carried out by checking if all parameter bounds are finite. - If no parameter bounds are available, the following processes are run - to perform the check: - (i) feasibility-based bounds tightening is used to obtain parameter - bounds, and if not all bound are found, - (ii) solving a sequence of maximization and minimization problems - (in which the objective for each problem is the value of a - single uncertain parameter). - If any of the optimization models cannot be solved successfully to - optimality, then False is returned. + If no parameter bounds are available, the following processes + are run to perform the check: + (i) feasibility-based bounds tightening is used to obtain + parameter bounds, and if not all bound are found, + (ii) solving a sequence of maximization and minimization + problems (in which the objective for each problem is the value + of a single uncertain parameter). + If any of the optimization models cannot be solved successfully + to optimality, then False is returned. This method is invoked by ``self.validate()``. """ @@ -643,7 +643,8 @@ def is_nonempty(self, config): def validate(self, config): """ - Validate the uncertainty set with a nonemptiness and boundedness check. + Validate the uncertainty set with a nonemptiness + and boundedness check. Parameters ---------- @@ -2113,7 +2114,8 @@ def validate(self, config): ValueError If any uncertainty set attributes are not valid. (e.g., numeric values are infinite, - ``self.budget_membership_mat`` has full column or row of zeros, + ``self.budget_membership_mat`` contains a + full column or row of zeros, or ``self.budget_rhs_vec`` has negative values). """ lhs_coeffs_arr = self.budget_membership_mat From 45742965d6b82325161e75c75f0680617de1b04a Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Sun, 29 Jun 2025 16:26:54 -0700 Subject: [PATCH 64/83] Update cols_with_all_zeros --- pyomo/contrib/pyros/uncertainty_sets.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index d6d1fe7f099..20fc1b95dcf 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -1845,9 +1845,7 @@ def validate(self, config): ) # check no column is all zeros. otherwise, set is unbounded - cols_with_all_zeros = np.nonzero( - [np.all(col == 0) for col in lhs_coeffs_arr.T] - )[0] + cols_with_all_zeros = np.nonzero(np.all(lhs_coeffs_arr == 0, axis=0))[0] if cols_with_all_zeros.size > 0: col_str = ", ".join(str(val) for val in cols_with_all_zeros) raise ValueError( @@ -3577,3 +3575,11 @@ def validate(self, config): # check boundedness and nonemptiness of intersected set super().validate(config) + + +# TODO: General +# 1. redundant trailing commas in test_uncertainty set +# 2. Skip BARON test in test_uncertainty +# 3. Wrap all docstrings to ~72 chars +# 4. Update validate() method docstrings to be more specific +# 5. wrap sentences in test_uncertainty errors and docstrings From 4e7ed53cb5084a50efb69fce05b91705a2d96548 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Sun, 29 Jun 2025 16:33:36 -0700 Subject: [PATCH 65/83] Fix IntersectionSet doctest error --- 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 20fc1b95dcf..f45bf3334c0 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -3399,7 +3399,7 @@ class IntersectionSet(UncertaintySet): ... ) >>> # to construct intersection, pass sets as keyword arguments >>> intersection = IntersectionSet(set1=square, set2=circle) - >>> intersection.all_sets + >>> intersection.all_sets # doctest: +ELLIPSIS UncertaintySetList([...]) """ From d10cfc0c828d675f4b97d69a0851d6a258f1af79 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Sun, 29 Jun 2025 17:22:52 -0700 Subject: [PATCH 66/83] Add FBBT exception test --- pyomo/contrib/pyros/tests/test_uncertainty_sets.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 433e0ea08d5..ab0589f81f7 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -411,6 +411,20 @@ def test_bounded_and_nonempty(self): box_set = BoxSet(bounds=[[1.0, 2.0], [3.0, 4.0]]) bounded_and_nonempty_check(self, box_set) + def test_fbbt_error(self): + """ + Test that `_fbbt_parameter_bounds` error message with bad bounds. + """ + CONFIG = pyros_config() + + # construct box set with invalid bounds + box_set = BoxSet(bounds=[[2, 1]]) + exc_str = ("Encountered the following exception while " + "computing parameter bounds with FBBT") + with self.assertLogs(CONFIG.progress_logger, level='ERROR') as cm: + box_set._fbbt_parameter_bounds(config=CONFIG) + self.assertIn(exc_str, cm.output[0]) + def test_is_coordinate_fixed(self): """ Test method for checking whether there are coordinates From 13a61456875a47576a44f061f17bd27d95658a0c Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Sun, 29 Jun 2025 17:27:26 -0700 Subject: [PATCH 67/83] Remove time based test for test_is_bounded --- pyomo/contrib/pyros/tests/test_uncertainty_sets.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index ab0589f81f7..690dac30cac 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -3092,25 +3092,11 @@ def test_is_bounded(self): CONFIG.global_solver = global_solver # using provided parameter_bounds - start = time.time() self.assertTrue(custom_set.is_bounded(config=CONFIG), "Set is not bounded") - end = time.time() - time_with_bounds_provided = end - start # when parameter_bounds is not available custom_set.parameter_bounds = [] - start = time.time() self.assertTrue(custom_set.is_bounded(config=CONFIG), "Set is not bounded") - end = time.time() - time_without_bounds_provided = end - start - - # check with parameter_bounds should always take less time than solving 2N - # optimization problems - self.assertLess( - time_with_bounds_provided, - time_without_bounds_provided, - "Boundedness check with provided parameter_bounds took longer than expected.", - ) # when bad bounds are provided for val_str in ["inf", "nan"]: From d6391f15f402ca20203004f53afdffcdaadd6b6b Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Sun, 29 Jun 2025 17:38:00 -0700 Subject: [PATCH 68/83] Run black --- pyomo/contrib/pyros/tests/test_uncertainty_sets.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 690dac30cac..75a3c798750 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -419,8 +419,10 @@ def test_fbbt_error(self): # construct box set with invalid bounds box_set = BoxSet(bounds=[[2, 1]]) - exc_str = ("Encountered the following exception while " - "computing parameter bounds with FBBT") + exc_str = ( + "Encountered the following exception while " + "computing parameter bounds with FBBT" + ) with self.assertLogs(CONFIG.progress_logger, level='ERROR') as cm: box_set._fbbt_parameter_bounds(config=CONFIG) self.assertIn(exc_str, cm.output[0]) From ba10efc160f276c1d6f89e33fec004d7441f6ad1 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Sun, 29 Jun 2025 21:13:23 -0700 Subject: [PATCH 69/83] Remove TODOs --- pyomo/contrib/pyros/uncertainty_sets.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index f45bf3334c0..851f393421e 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -3575,11 +3575,3 @@ def validate(self, config): # check boundedness and nonemptiness of intersected set super().validate(config) - - -# TODO: General -# 1. redundant trailing commas in test_uncertainty set -# 2. Skip BARON test in test_uncertainty -# 3. Wrap all docstrings to ~72 chars -# 4. Update validate() method docstrings to be more specific -# 5. wrap sentences in test_uncertainty errors and docstrings From 119518a46253b8a69f139406d25089c5e1bcd4b3 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Mon, 30 Jun 2025 15:11:05 -0700 Subject: [PATCH 70/83] Remove unneeded variables in validate --- pyomo/contrib/pyros/uncertainty_sets.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 851f393421e..47a817ba90e 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -656,13 +656,10 @@ def validate(self, config): ValueError If nonemptiness check or boundedness check fails. """ - check_nonempty = self.is_nonempty(config=config) - check_bounded = self.is_bounded(config=config) - - if not check_nonempty: + if not self.is_nonempty(config=config): raise ValueError(f"Nonemptiness check failed for uncertainty set {self}.") - if not check_bounded: + if not self.is_bounded(config=config): raise ValueError(f"Boundedness check failed for uncertainty set {self}.") @abc.abstractmethod From 747d43992fe70e6103dcf7211117deedac9f0da6 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Tue, 1 Jul 2025 15:23:13 -0700 Subject: [PATCH 71/83] Robustify is_bounded fbbt bounds check --- pyomo/contrib/pyros/uncertainty_sets.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 47a817ba90e..eb2c67738aa 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -590,16 +590,24 @@ def is_bounded(self, config): # use parameter bounds if they are available param_bounds_arr = self.parameter_bounds if param_bounds_arr: - all_bounds_finite = np.all(np.isfinite(param_bounds_arr)) + all_bounds_finite = ( + np.all(np.isfinite(param_bounds_arr)) + ) else: - # initialize uncertain parameter variables - param_bounds_arr = np.array(self._fbbt_parameter_bounds(config)) - if not all(map(lambda x: all(x), param_bounds_arr)): - # solve bounding problems if FBBT cannot find bounds + # use FBBT + param_bounds_arr = np.array( + self._fbbt_parameter_bounds(config), + dtype="float", + ) + all_bounds_finite = np.isfinite(param_bounds_arr).all() + + if not all_bounds_finite: + # solve bounding problems param_bounds_arr = np.array( self._compute_parameter_bounds(solver=config.global_solver) ) - all_bounds_finite = np.all(np.isfinite(param_bounds_arr)) + all_bounds_finite = np.isfinite(param_bounds_arr).all() + # log result if not all_bounds_finite: From 3ca7580c3fde397380825f328861c53b9189b12a Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Tue, 1 Jul 2025 15:24:13 -0700 Subject: [PATCH 72/83] Update _solve_feasibility --- pyomo/contrib/pyros/tests/test_uncertainty_sets.py | 2 +- pyomo/contrib/pyros/uncertainty_sets.py | 11 ++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 75a3c798750..e009731f2aa 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -3075,7 +3075,7 @@ def test_solve_feasibility(self): # feasibility problem passes baron = SolverFactory("baron") custom_set = CustomUncertaintySet(dim=2) - self.assertTrue(custom_set._solve_feasibility(baron)) + custom_set._solve_feasibility(baron) # feasibility problem fails custom_set.parameter_bounds = [[1, 2], [3, 4]] diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index eb2c67738aa..5307ddfbafa 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -638,7 +638,8 @@ def is_nonempty(self, config): set_nonempty = self.point_in_set(config.nominal_uncertain_param_vals) else: # construct feasibility problem and solve otherwise - set_nonempty = self._solve_feasibility(config.global_solver) + self._solve_feasibility(config.global_solver) + set_nonempty = True # log result if not set_nonempty: @@ -858,12 +859,6 @@ def _solve_feasibility(self, solver): Optimizer capable of solving bounding problems to global optimality. - Returns - ------- - : bool - True if the feasibility problem solves successfully, - and raises an exception otherwise - Raises ------ ValueError @@ -894,8 +889,6 @@ def feasibility_objective(self): f"Solver status summary:\n {res.solver}." ) - return True - def _add_bounds_on_uncertain_parameters( self, uncertain_param_vars, global_solver=None ): From feda11cb18d705edcead11b0d67249a4489cc532 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Wed, 2 Jul 2025 10:50:34 -0700 Subject: [PATCH 73/83] Update _compute_parameter_bounds index handling --- pyomo/contrib/pyros/uncertainty_sets.py | 29 +++++++++++++++---------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 5307ddfbafa..35b0b5fd9fb 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -753,11 +753,14 @@ def _compute_parameter_bounds(self, solver, index=None): ---------- solver : Pyomo solver type Optimizer to invoke on the bounding problems. - index : list of int, optional - Positional indices of the coordinates for which - to compute bounds. If None is passed, - then the argument is set to ``list(range(self.dim))``, - so that the bounds for all coordinates are computed. + index : list of 2-tuple of bool, optional + A list of tuples for each index of the coordinates for + which to compute bounds. A lower or upper bound is + computed for any value that is True, while False + indicates that the bound should be skipped. + If None is passed, then the argument is set to + ``[(True, True)]*self.dim``, so that the bounds + for all coordinates are computed. Returns ------- @@ -773,14 +776,12 @@ def _compute_parameter_bounds(self, solver, index=None): coordinate. """ if index is None: - index = list(range(self.dim)) + index = [(True, True)]*self.dim + # create bounding model and get all objectives bounding_model = self._create_bounding_model() - objs_to_optimize = ( - (idx, obj) - for idx, obj in bounding_model.param_var_objectives.items() - if idx in index - ) + objs_to_optimize = bounding_model.param_var_objectives.items() + param_bounds = [] for idx, obj in objs_to_optimize: # activate objective for corresponding dimension @@ -789,7 +790,11 @@ def _compute_parameter_bounds(self, solver, index=None): # solve for lower bound, then upper bound # solve should be successful - for sense in (minimize, maximize): + for i, sense in enumerate((minimize, maximize)): + # check if the LB or UB should be solved + if not index[idx][i]: + bounds.append(None) + continue obj.sense = sense res = solver.solve(bounding_model, load_solutions=False) if check_optimal_termination(res): From 9e4a20ab3769bb8129959bdb861f218b68c73f23 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Wed, 2 Jul 2025 10:51:09 -0700 Subject: [PATCH 74/83] Update is_bounded for fbbt and bounding problems --- pyomo/contrib/pyros/uncertainty_sets.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 35b0b5fd9fb..1961353557e 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -602,10 +602,15 @@ def is_bounded(self, config): all_bounds_finite = np.isfinite(param_bounds_arr).all() if not all_bounds_finite: - # solve bounding problems - param_bounds_arr = np.array( - self._compute_parameter_bounds(solver=config.global_solver) + # get bounds that need to be solved + index = np.isnan(param_bounds_arr) + # solve bounding problems for bounds that have not been found + opt_bounds_arr = np.array( + self._compute_parameter_bounds(solver=config.global_solver, index=index), + dtype="float", ) + # combine with previously found bounds + param_bounds_arr[index] = opt_bounds_arr[index] all_bounds_finite = np.isfinite(param_bounds_arr).all() From acc907b153f75e9ab1821aa000010c0f0344b9e1 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Wed, 2 Jul 2025 10:58:17 -0700 Subject: [PATCH 75/83] Replace valid_num_types with native_numeric_types --- pyomo/contrib/pyros/uncertainty_sets.py | 89 ++++++++++++------------- 1 file changed, 43 insertions(+), 46 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 1961353557e..809bdfee3f5 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -52,9 +52,6 @@ from pyomo.common.errors import InfeasibleConstraintException -valid_num_types = native_numeric_types - - def standardize_uncertain_param_vars(obj, dim): """ Standardize an object castable to a list of VarData objects @@ -270,9 +267,9 @@ def validate_arg_type( # check for finiteness, if desired if check_numeric_type_finite: if isinstance(valid_types, type): - numeric_types_required = valid_types in valid_num_types + numeric_types_required = valid_types in native_numeric_types else: - numeric_types_required = set(valid_types).issubset(valid_num_types) + numeric_types_required = set(valid_types).issubset(native_numeric_types) if numeric_types_required and (math.isinf(arg_val) or math.isnan(arg_val)): if is_entry_of_arg: raise ValueError( @@ -727,7 +724,7 @@ def point_in_set(self, point): arr=point, arr_name="point", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="numeric type", required_shape=[self.dim], required_shape_qual="to match the set dimension", @@ -1254,7 +1251,7 @@ def bounds(self, val): arr=val, arr_name="bounds", dim=2, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=[None, 2], ) @@ -1340,7 +1337,7 @@ def validate(self, config): arr=bounds_arr, arr_name="bounds", dim=2, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=[None, 2], ) @@ -1414,7 +1411,7 @@ def origin(self, val): arr=val, arr_name="origin", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", ) @@ -1444,7 +1441,7 @@ def positive_deviation(self, val): arr=val, arr_name="positive_deviation", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", ) @@ -1479,7 +1476,7 @@ def gamma(self): @gamma.setter def gamma(self, val): - validate_arg_type("gamma", val, valid_num_types, "a valid numeric type", False) + validate_arg_type("gamma", val, native_numeric_types, "a valid numeric type", False) self._gamma = val @@ -1556,7 +1553,7 @@ def compute_auxiliary_uncertain_param_vals(self, point, solver=None): arr=point, arr_name="point", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="numeric type", required_shape=[self.dim], required_shape_qual="to match the set dimension", @@ -1616,18 +1613,18 @@ def validate(self, config): arr=orig_val, arr_name="origin", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", ) validate_array( arr=pos_dev, arr_name="positive_deviation", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", ) validate_arg_type( - "gamma", gamma, valid_num_types, "a valid numeric type", False + "gamma", gamma, native_numeric_types, "a valid numeric type", False ) # check deviation is positive @@ -1713,7 +1710,7 @@ def coefficients_mat(self, val): arr=val, arr_name="coefficients_mat", dim=2, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) @@ -1754,7 +1751,7 @@ def rhs_vec(self, val): arr=val, arr_name="rhs_vec", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) @@ -1839,7 +1836,7 @@ def validate(self, config): arr=lhs_coeffs_arr, arr_name="coefficients_mat", dim=2, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) @@ -1847,7 +1844,7 @@ def validate(self, config): arr=rhs_vec_arr, arr_name="rhs_vec", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) @@ -1977,7 +1974,7 @@ def budget_membership_mat(self, val): arr=val, arr_name="budget_membership_mat", dim=2, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) @@ -2020,7 +2017,7 @@ def budget_rhs_vec(self, val): arr=val, arr_name="budget_rhs_vec", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) @@ -2053,7 +2050,7 @@ def origin(self, val): arr=val, arr_name="origin", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) @@ -2134,7 +2131,7 @@ def validate(self, config): arr=lhs_coeffs_arr, arr_name="budget_membership_mat", dim=2, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) @@ -2142,7 +2139,7 @@ def validate(self, config): arr=rhs_vec_arr, arr_name="budget_rhs_vec", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) @@ -2150,7 +2147,7 @@ def validate(self, config): arr=orig_val, arr_name="origin", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) @@ -2277,7 +2274,7 @@ def origin(self, val): arr=val, arr_name="origin", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", ) @@ -2340,7 +2337,7 @@ def psi_mat(self, val): arr=val, arr_name="psi_mat", dim=2, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) @@ -2488,7 +2485,7 @@ def compute_auxiliary_uncertain_param_vals(self, point, solver=None): arr=point, arr_name="point", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="numeric type", required_shape=[self.dim], required_shape_qual="to match the set dimension", @@ -2548,18 +2545,18 @@ def validate(self, config): arr=orig_val, arr_name="origin", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", ) validate_array( arr=psi_mat_arr, arr_name="psi_mat", dim=2, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) - validate_arg_type("beta", beta, valid_num_types, "a valid numeric type", False) + validate_arg_type("beta", beta, native_numeric_types, "a valid numeric type", False) # check psi is full column rank psi_mat_rank = np.linalg.matrix_rank(psi_mat_arr) @@ -2634,7 +2631,7 @@ def center(self, val): arr=val, arr_name="center", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) @@ -2665,7 +2662,7 @@ def half_lengths(self, val): arr=val, arr_name="half_lengths", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) @@ -2770,7 +2767,7 @@ def validate(self, config): arr=ctr, arr_name="center", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) @@ -2778,7 +2775,7 @@ def validate(self, config): arr=half_lengths, arr_name="half_lengths", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) @@ -2909,7 +2906,7 @@ def center(self, val): arr=val, arr_name="center", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) @@ -2989,7 +2986,7 @@ def shape_matrix(self, val): arr=val, arr_name="shape_matrix", dim=2, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) @@ -3019,7 +3016,7 @@ def scale(self): @scale.setter def scale(self, val): - validate_arg_type("scale", val, valid_num_types, "a valid numeric type", False) + validate_arg_type("scale", val, native_numeric_types, "a valid numeric type", False) self._scale = val self._gaussian_conf_lvl = sp.stats.chi2.cdf(x=val, df=self.dim) @@ -3037,7 +3034,7 @@ def gaussian_conf_lvl(self): @gaussian_conf_lvl.setter def gaussian_conf_lvl(self, val): validate_arg_type( - "gaussian_conf_lvl", val, valid_num_types, "a valid numeric type", False + "gaussian_conf_lvl", val, native_numeric_types, "a valid numeric type", False ) scale_val = sp.stats.chi2.isf(q=1 - val, df=self.dim) @@ -3096,7 +3093,7 @@ def point_in_set(self, point): arr=point, arr_name="point", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="numeric type", required_shape=[self.dim], required_shape_qual="to match the set dimension", @@ -3161,7 +3158,7 @@ def validate(self, config): arr=ctr, arr_name="center", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) @@ -3169,12 +3166,12 @@ def validate(self, config): arr=shape_mat_arr, arr_name="shape_matrix", dim=2, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) validate_arg_type( - "scale", scale, valid_num_types, "a valid numeric type", False + "scale", scale, native_numeric_types, "a valid numeric type", False ) # check shape matrix is positive semidefinite @@ -3243,7 +3240,7 @@ def scenarios(self, val): arr=val, arr_name="scenarios", dim=2, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) @@ -3345,7 +3342,7 @@ def point_in_set(self, point): arr=point, arr_name="point", dim=1, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="numeric type", required_shape=[self.dim], required_shape_qual="to match the set dimension", @@ -3374,7 +3371,7 @@ def validate(self, config): arr=scenario_arr, arr_name="scenarios", dim=2, - valid_types=valid_num_types, + valid_types=native_numeric_types, valid_type_desc="a valid numeric type", required_shape=None, ) From 5cf81a2605bdd0e4d617686389db636a75f350ac Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Wed, 2 Jul 2025 10:59:23 -0700 Subject: [PATCH 76/83] Update _fbbt_parameter_bounds docstring --- pyomo/contrib/pyros/uncertainty_sets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 809bdfee3f5..b6cb3496ab5 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -822,6 +822,7 @@ def _compute_parameter_bounds(self, solver, index=None): def _fbbt_parameter_bounds(self, config): """ Obtain parameter bounds of the uncertainty set using FBBT. + The bounds returned by FBBT may be inexact. Parameters ---------- From a6beaea7a919061aec9be68dcbbd4cea0b2c3795 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Wed, 2 Jul 2025 11:02:55 -0700 Subject: [PATCH 77/83] _compute_parameter_bounds -> _compute_exact_parameter_bounds --- .../pyros/tests/test_uncertainty_sets.py | 42 +++++++++---------- pyomo/contrib/pyros/uncertainty_sets.py | 8 ++-- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index e009731f2aa..946a0210e17 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -316,12 +316,12 @@ def test_set_as_constraint_type_mismatch(self): box_set.set_as_constraint(uncertain_params=m.p1, block=m) @unittest.skipUnless(baron_available, "BARON is not available.") - def test_compute_parameter_bounds(self): + def test_compute_exact_parameter_bounds(self): """ Test parameter bounds computations give expected results. """ box_set = BoxSet([[1, 2], [3, 4]]) - computed_bounds = box_set._compute_parameter_bounds(SolverFactory("baron")) + computed_bounds = box_set._compute_exact_parameter_bounds(SolverFactory("baron")) np.testing.assert_allclose(computed_bounds, [[1, 2], [3, 4]]) np.testing.assert_allclose(computed_bounds, box_set.parameter_bounds) @@ -538,7 +538,7 @@ def test_error_on_budget_member_mat_row_change(self): bu_set.budget_rhs_vec = [1] @unittest.skipUnless(baron_available, "BARON is not available") - def test_compute_parameter_bounds(self): + def test_compute_exact_parameter_bounds(self): """ Test parameter bounds computations give expected results. """ @@ -546,7 +546,7 @@ def test_compute_parameter_bounds(self): buset1 = BudgetSet([[1, 1], [0, 1]], rhs_vec=[2, 3], origin=None) np.testing.assert_allclose( - buset1.parameter_bounds, buset1._compute_parameter_bounds(solver) + buset1.parameter_bounds, buset1._compute_exact_parameter_bounds(solver) ) # this also checks that the list entries are tuples @@ -554,10 +554,10 @@ def test_compute_parameter_bounds(self): buset2 = BudgetSet([[1, 0], [1, 1]], rhs_vec=[3, 2], origin=[1, 2]) self.assertEqual( - buset2.parameter_bounds, buset2._compute_parameter_bounds(solver) + buset2.parameter_bounds, buset2._compute_exact_parameter_bounds(solver) ) np.testing.assert_allclose( - buset2.parameter_bounds, buset2._compute_parameter_bounds(solver) + buset2.parameter_bounds, buset2._compute_exact_parameter_bounds(solver) ) self.assertEqual(buset2.parameter_bounds, [(1, 3), (2, 4)]) @@ -904,7 +904,7 @@ def test_error_on_invalid_number_of_factors(self): ] ) @unittest.skipUnless(baron_available, "BARON is not available") - def test_compute_parameter_bounds(self, name, beta, expected_param_bounds): + def test_compute_exact_parameter_bounds(self, name, beta, expected_param_bounds): """ Test parameter bounds computations give expected results. """ @@ -923,7 +923,7 @@ def test_compute_parameter_bounds(self, name, beta, expected_param_bounds): # check parameter bounds matches LP results # exactly for each case - solver_param_bounds = fset._compute_parameter_bounds(solver) + solver_param_bounds = fset._compute_exact_parameter_bounds(solver) np.testing.assert_allclose( solver_param_bounds, param_bounds, @@ -1500,7 +1500,7 @@ def test_set_as_constraint_type_mismatch(self): i_set.set_as_constraint(uncertain_params=m.p1, block=m) @unittest.skipUnless(baron_available, "BARON is not available.") - def test_compute_parameter_bounds(self): + def test_compute_exact_parameter_bounds(self): """ Test parameter bounds computations give expected results. """ @@ -1517,7 +1517,7 @@ def test_compute_parameter_bounds(self): # ellipsoid is enclosed by everyone else, so # that determines the bounds - computed_bounds = i_set._compute_parameter_bounds(SolverFactory("baron")) + computed_bounds = i_set._compute_exact_parameter_bounds(SolverFactory("baron")) np.testing.assert_allclose(computed_bounds, [[-0.25, 0.25], [-0.25, 0.25]]) # returns empty list @@ -1757,14 +1757,14 @@ def test_point_in_set(self): cset.point_in_set([1, 2, 3, 4]) @unittest.skipUnless(baron_available, "BARON is not available.") - def test_compute_parameter_bounds(self): + def test_compute_exact_parameter_bounds(self): """ Test parameter bounds computations give expected results. """ cset = CardinalitySet( origin=[-0.5, 1, 2], positive_deviation=[2.5, 3, 0], gamma=1.5 ) - computed_bounds = cset._compute_parameter_bounds(SolverFactory("baron")) + computed_bounds = cset._compute_exact_parameter_bounds(SolverFactory("baron")) np.testing.assert_allclose(computed_bounds, [[-0.5, 2], [1, 4], [2, 2]]) np.testing.assert_allclose(computed_bounds, cset.parameter_bounds) @@ -2179,12 +2179,12 @@ def test_set_as_constraint_type_mismatch(self): aeset.set_as_constraint(uncertain_params=m.p1, block=m) @unittest.skipUnless(baron_available, "BARON is not available.") - def test_compute_parameter_bounds(self): + def test_compute_exact_parameter_bounds(self): """ Test parameter bounds computations give expected results. """ aeset = AxisAlignedEllipsoidalSet(center=[0, 1.5, 1], half_lengths=[1.5, 2, 0]) - computed_bounds = aeset._compute_parameter_bounds(SolverFactory("baron")) + computed_bounds = aeset._compute_exact_parameter_bounds(SolverFactory("baron")) np.testing.assert_allclose(computed_bounds, [[-1.5, 1.5], [-0.5, 3.5], [1, 1]]) np.testing.assert_allclose(computed_bounds, aeset.parameter_bounds) @@ -2565,7 +2565,7 @@ def test_point_in_set(self): eset.point_in_set([1, 2, 3, 4]) @unittest.skipUnless(baron_available, "BARON is not available.") - def test_compute_parameter_bounds(self): + def test_compute_exact_parameter_bounds(self): """ Test parameter bounds computations give expected results. """ @@ -2573,14 +2573,14 @@ def test_compute_parameter_bounds(self): eset = EllipsoidalSet( center=[1, 1.5], shape_matrix=[[1, 0.5], [0.5, 1]], scale=0.25 ) - computed_bounds = eset._compute_parameter_bounds(baron) + computed_bounds = eset._compute_exact_parameter_bounds(baron) np.testing.assert_allclose(computed_bounds, [[0.5, 1.5], [1.0, 2.0]]) np.testing.assert_allclose(computed_bounds, eset.parameter_bounds) eset2 = EllipsoidalSet( center=[1, 1.5], shape_matrix=[[1, 0.5], [0.5, 1]], scale=2.25 ) - computed_bounds_2 = eset2._compute_parameter_bounds(baron) + computed_bounds_2 = eset2._compute_exact_parameter_bounds(baron) # add absolute tolerance to account from # matrix inversion and roundoff errors @@ -2849,7 +2849,7 @@ def test_set_as_constraint_type_mismatch(self): pset.set_as_constraint(uncertain_params=m.p1, block=m) @unittest.skipUnless(baron_available, "BARON is not available.") - def test_compute_parameter_bounds(self): + def test_compute_exact_parameter_bounds(self): """ Test parameter bounds computations give expected results. """ @@ -2857,7 +2857,7 @@ def test_compute_parameter_bounds(self): lhs_coefficients_mat=[[1, 0], [-1, 1], [-1, -1]], rhs_vec=[2, -1, -1] ) self.assertEqual(pset.parameter_bounds, []) - computed_bounds = pset._compute_parameter_bounds(SolverFactory("baron")) + computed_bounds = pset._compute_exact_parameter_bounds(SolverFactory("baron")) self.assertEqual(computed_bounds, [(1, 2), (-1, 1)]) def test_point_in_set(self): @@ -3058,14 +3058,14 @@ def test_set_as_constraint(self): self.assertEqual(len(uq.uncertain_param_vars), 2) @unittest.skipUnless(baron_available, "BARON is not available") - def test_compute_parameter_bounds(self): + def test_compute_exact_parameter_bounds(self): """ Test parameter bounds computations give expected results. """ baron = SolverFactory("baron") custom_set = CustomUncertaintySet(dim=2) self.assertEqual(custom_set.parameter_bounds, [(-1, 1)] * 2) - self.assertEqual(custom_set._compute_parameter_bounds(baron), [(-1, 1)] * 2) + self.assertEqual(custom_set._compute_exact_parameter_bounds(baron), [(-1, 1)] * 2) @unittest.skipUnless(baron_available, "BARON is not available") def test_solve_feasibility(self): diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index b6cb3496ab5..dd6fbd98351 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -603,7 +603,7 @@ def is_bounded(self, config): index = np.isnan(param_bounds_arr) # solve bounding problems for bounds that have not been found opt_bounds_arr = np.array( - self._compute_parameter_bounds(solver=config.global_solver, index=index), + self._compute_exact_parameter_bounds(solver=config.global_solver, index=index), dtype="float", ) # combine with previously found bounds @@ -746,7 +746,7 @@ def point_in_set(self, point): return is_in_set - def _compute_parameter_bounds(self, solver, index=None): + def _compute_exact_parameter_bounds(self, solver, index=None): """ Compute lower and upper coordinate value bounds for every dimension of `self` by solving a bounding model. @@ -925,7 +925,7 @@ def _add_bounds_on_uncertain_parameters( parameter_bounds = self.parameter_bounds if not parameter_bounds: - parameter_bounds = self._compute_parameter_bounds(global_solver) + parameter_bounds = self._compute_exact_parameter_bounds(global_solver) for (lb, ub), param_var in zip(parameter_bounds, uncertain_param_vars): param_var.setlb(lb) @@ -986,7 +986,7 @@ def _values_close(a, b): param_bounds = self.parameter_bounds if not (param_bounds and self._PARAMETER_BOUNDS_EXACT): # we need the exact bounding box - param_bounds = self._compute_parameter_bounds( + param_bounds = self._compute_exact_parameter_bounds( solver=config.global_solver, index=index ) else: From 03249e8fa1eaeb42a7e6eebca35381a30771f9dc Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Wed, 2 Jul 2025 11:05:44 -0700 Subject: [PATCH 78/83] Run black --- .../pyros/tests/test_uncertainty_sets.py | 8 +++-- pyomo/contrib/pyros/uncertainty_sets.py | 32 ++++++++++++------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 946a0210e17..b71e1ed84d2 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -321,7 +321,9 @@ def test_compute_exact_parameter_bounds(self): Test parameter bounds computations give expected results. """ box_set = BoxSet([[1, 2], [3, 4]]) - computed_bounds = box_set._compute_exact_parameter_bounds(SolverFactory("baron")) + computed_bounds = box_set._compute_exact_parameter_bounds( + SolverFactory("baron") + ) np.testing.assert_allclose(computed_bounds, [[1, 2], [3, 4]]) np.testing.assert_allclose(computed_bounds, box_set.parameter_bounds) @@ -3065,7 +3067,9 @@ def test_compute_exact_parameter_bounds(self): baron = SolverFactory("baron") custom_set = CustomUncertaintySet(dim=2) self.assertEqual(custom_set.parameter_bounds, [(-1, 1)] * 2) - self.assertEqual(custom_set._compute_exact_parameter_bounds(baron), [(-1, 1)] * 2) + self.assertEqual( + custom_set._compute_exact_parameter_bounds(baron), [(-1, 1)] * 2 + ) @unittest.skipUnless(baron_available, "BARON is not available") def test_solve_feasibility(self): diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index dd6fbd98351..8ec3cb13486 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -587,14 +587,11 @@ def is_bounded(self, config): # use parameter bounds if they are available param_bounds_arr = self.parameter_bounds if param_bounds_arr: - all_bounds_finite = ( - np.all(np.isfinite(param_bounds_arr)) - ) + all_bounds_finite = np.all(np.isfinite(param_bounds_arr)) else: # use FBBT param_bounds_arr = np.array( - self._fbbt_parameter_bounds(config), - dtype="float", + self._fbbt_parameter_bounds(config), dtype="float" ) all_bounds_finite = np.isfinite(param_bounds_arr).all() @@ -603,14 +600,15 @@ def is_bounded(self, config): index = np.isnan(param_bounds_arr) # solve bounding problems for bounds that have not been found opt_bounds_arr = np.array( - self._compute_exact_parameter_bounds(solver=config.global_solver, index=index), + self._compute_exact_parameter_bounds( + solver=config.global_solver, index=index + ), dtype="float", ) # combine with previously found bounds param_bounds_arr[index] = opt_bounds_arr[index] all_bounds_finite = np.isfinite(param_bounds_arr).all() - # log result if not all_bounds_finite: config.progress_logger.error( @@ -778,7 +776,7 @@ def _compute_exact_parameter_bounds(self, solver, index=None): coordinate. """ if index is None: - index = [(True, True)]*self.dim + index = [(True, True)] * self.dim # create bounding model and get all objectives bounding_model = self._create_bounding_model() @@ -1477,7 +1475,9 @@ def gamma(self): @gamma.setter def gamma(self, val): - validate_arg_type("gamma", val, native_numeric_types, "a valid numeric type", False) + validate_arg_type( + "gamma", val, native_numeric_types, "a valid numeric type", False + ) self._gamma = val @@ -2557,7 +2557,9 @@ def validate(self, config): valid_type_desc="a valid numeric type", required_shape=None, ) - validate_arg_type("beta", beta, native_numeric_types, "a valid numeric type", False) + validate_arg_type( + "beta", beta, native_numeric_types, "a valid numeric type", False + ) # check psi is full column rank psi_mat_rank = np.linalg.matrix_rank(psi_mat_arr) @@ -3017,7 +3019,9 @@ def scale(self): @scale.setter def scale(self, val): - validate_arg_type("scale", val, native_numeric_types, "a valid numeric type", False) + validate_arg_type( + "scale", val, native_numeric_types, "a valid numeric type", False + ) self._scale = val self._gaussian_conf_lvl = sp.stats.chi2.cdf(x=val, df=self.dim) @@ -3035,7 +3039,11 @@ def gaussian_conf_lvl(self): @gaussian_conf_lvl.setter def gaussian_conf_lvl(self, val): validate_arg_type( - "gaussian_conf_lvl", val, native_numeric_types, "a valid numeric type", False + "gaussian_conf_lvl", + val, + native_numeric_types, + "a valid numeric type", + False, ) scale_val = sp.stats.chi2.isf(q=1 - val, df=self.dim) From 78b125a906266c67b9cdc16845a65c261b0e9174 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Thu, 17 Jul 2025 07:25:41 -0600 Subject: [PATCH 79/83] Update pyomo/contrib/pyros/tests/test_uncertainty_sets.py Co-authored-by: Bethany Nicholson --- pyomo/contrib/pyros/tests/test_uncertainty_sets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index b71e1ed84d2..ff54ab3d8ec 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -1812,7 +1812,7 @@ def test_validate_finiteness(self): # check when values are not finite cardinality_set.origin[0] = np.nan exc_str = ( - r"Entry 'nan' of the argument `origin` " r"is not a finite numeric value" + r"Entry 'nan' of the argument `origin` is not a finite numeric value" ) with self.assertRaisesRegex(ValueError, exc_str): cardinality_set.validate(config=CONFIG) From ea460910cc06828b8dd206aa7dc8d04a2d13a717 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Thu, 17 Jul 2025 07:26:04 -0600 Subject: [PATCH 80/83] Update pyomo/contrib/pyros/tests/test_uncertainty_sets.py Co-authored-by: Bethany Nicholson --- pyomo/contrib/pyros/tests/test_uncertainty_sets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index ff54ab3d8ec..af7f9c938cc 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -2245,7 +2245,7 @@ def test_validate_finiteness(self): # check when values are not finite a_ellipsoid_set.center[0] = np.nan exc_str = ( - r"Entry 'nan' of the argument `center` " r"is not a finite numeric value" + r"Entry 'nan' of the argument `center` is not a finite numeric value" ) with self.assertRaisesRegex(ValueError, exc_str): a_ellipsoid_set.validate(config=CONFIG) From 45bcafa656cc2df2ccd6ad386e5bbd390114eaa5 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Thu, 17 Jul 2025 07:26:21 -0600 Subject: [PATCH 81/83] Update pyomo/contrib/pyros/tests/test_uncertainty_sets.py Co-authored-by: Bethany Nicholson --- pyomo/contrib/pyros/tests/test_uncertainty_sets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index af7f9c938cc..a11dc8a9b55 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -2631,7 +2631,7 @@ def test_validate_finiteness(self): # check when values are not finite ellipsoid_set.center[0] = np.nan exc_str = ( - r"Entry 'nan' of the argument `center` " r"is not a finite numeric value" + r"Entry 'nan' of the argument `center` is not a finite numeric value" ) with self.assertRaisesRegex(ValueError, exc_str): ellipsoid_set.validate(config=CONFIG) From 35d4105bca9dcec53590dde512a726b9bb67817f Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Thu, 17 Jul 2025 07:27:13 -0600 Subject: [PATCH 82/83] Update pyomo/contrib/pyros/tests/test_uncertainty_sets.py Co-authored-by: Bethany Nicholson --- pyomo/contrib/pyros/tests/test_uncertainty_sets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index a11dc8a9b55..6f44123aaaf 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -2928,7 +2928,7 @@ def test_validate_finiteness(self): # check when values are not finite polyhedral_set.rhs_vec[0] = np.nan exc_str = ( - r"Entry 'nan' of the argument `rhs_vec` " r"is not a finite numeric value" + r"Entry 'nan' of the argument `rhs_vec` is not a finite numeric value" ) with self.assertRaisesRegex(ValueError, exc_str): polyhedral_set.validate(config=CONFIG) From e3cf83d067501fbadcd5c5f8b86700d3faa86c62 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 17 Jul 2025 07:40:39 -0600 Subject: [PATCH 83/83] Apply black --- .../contrib/pyros/tests/test_uncertainty_sets.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 6f44123aaaf..920b22feaad 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -1811,9 +1811,7 @@ def test_validate_finiteness(self): # check when values are not finite cardinality_set.origin[0] = np.nan - exc_str = ( - r"Entry 'nan' of the argument `origin` is not a finite numeric value" - ) + exc_str = r"Entry 'nan' of the argument `origin` is not a finite numeric value" with self.assertRaisesRegex(ValueError, exc_str): cardinality_set.validate(config=CONFIG) @@ -2244,9 +2242,7 @@ def test_validate_finiteness(self): # check when values are not finite a_ellipsoid_set.center[0] = np.nan - exc_str = ( - r"Entry 'nan' of the argument `center` is not a finite numeric value" - ) + exc_str = r"Entry 'nan' of the argument `center` is not a finite numeric value" with self.assertRaisesRegex(ValueError, exc_str): a_ellipsoid_set.validate(config=CONFIG) a_ellipsoid_set.center[0] = 0 @@ -2630,9 +2626,7 @@ def test_validate_finiteness(self): # check when values are not finite ellipsoid_set.center[0] = np.nan - exc_str = ( - r"Entry 'nan' of the argument `center` is not a finite numeric value" - ) + exc_str = r"Entry 'nan' of the argument `center` is not a finite numeric value" with self.assertRaisesRegex(ValueError, exc_str): ellipsoid_set.validate(config=CONFIG) @@ -2927,9 +2921,7 @@ def test_validate_finiteness(self): # check when values are not finite polyhedral_set.rhs_vec[0] = np.nan - exc_str = ( - r"Entry 'nan' of the argument `rhs_vec` is not a finite numeric value" - ) + exc_str = r"Entry 'nan' of the argument `rhs_vec` is not a finite numeric value" with self.assertRaisesRegex(ValueError, exc_str): polyhedral_set.validate(config=CONFIG)