From 53bbc30372baba4fa880754b04a2fb9ed617614f Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Fri, 20 Mar 2026 14:23:29 -0400 Subject: [PATCH 1/7] Move bounds calculation to separate, cached method --- pyomo/contrib/pyros/uncertainty_sets.py | 89 +++++++++++++++++-------- 1 file changed, 62 insertions(+), 27 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index a925ab19396..a45bcb3e9a2 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -788,45 +788,80 @@ def _compute_exact_parameter_bounds(self, solver, index=None): if index is None: index = [(True, True)] * self.dim - # create bounding model and get all objectives - bounding_model = self._create_bounding_model() - objs_to_optimize = bounding_model.param_var_objectives.items() - param_bounds = [] - for idx, obj in objs_to_optimize: - # activate objective for corresponding dimension - obj.activate() + for idx in range(self.dim): bounds = [] - - # solve for lower bound, then upper bound - # solve should be successful 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): - bounding_model.solutions.load_from(res) - else: - raise ValueError( - "Could not compute " - f"{'lower' if sense == minimize else 'upper'} " - f"bound in dimension {idx + 1} of {self.dim}. " - f"Solver status summary:\n {res.solver}." + bounds.append( + self._solve_bounds_optimization(solver, idx, sense) ) - bounds.append(value(obj)) - + # add parameter bounds for current dimension param_bounds.append(tuple(bounds)) - # ensure sense is minimize when done, deactivate - obj.sense = minimize - obj.deactivate() - return param_bounds + @functools.cache + def _solve_bounds_optimization(self, solver, index, sense): + """ + Compute value of bounds for a single parameter + of `self` at a specified index by solving a bounding model. + Results are cached as efficiency for large uncertainty sets. + + Parameters + ---------- + solver : Pyomo solver type + Optimizer to invoke on the bounding problems. + index : int + The index of the parameter to solve for bounds. + sense : Pyomo objective sense + A Pyomo objective sense to optimize for the bounding model. + `maximize` solves for an upper bound and + `minimize` solves for a lower bound. + + Returns + ------- + bound : float + A value of the lower or upper bound for + the corresponding dimension at the specified index. + + Raises + ------ + ValueError + If solver failed to compute a bound for a + coordinate. + """ + # create bounding model and get all objectives + bounding_model = self._create_bounding_model() + + # select objective corresponding to specified index + obj = bounding_model.param_var_objectives[index] + obj.activate() + + # optimize with specified sense + obj.sense = sense + res = solver.solve(bounding_model, load_solutions=False) + if check_optimal_termination(res): + bounding_model.solutions.load_from(res) + else: + raise ValueError( + "Could not compute " + f"{'lower' if sense == minimize else 'upper'} " + f"bound in dimension {index + 1} of {self.dim}. " + f"Solver status summary:\n {res.solver}." + ) + + # ensure sense is minimize when done, deactivate + obj.sense = minimize + obj.deactivate() + + bound = value(obj) + + return bound + def _fbbt_parameter_bounds(self, config): """ Obtain parameter bounds of the uncertainty set using FBBT. From b6a03aac11fe428e7dd1e0606f0a449bd30bc100 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Fri, 20 Mar 2026 14:24:18 -0400 Subject: [PATCH 2/7] Clear any cached bounds at set validation --- pyomo/contrib/pyros/uncertainty_sets.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index a45bcb3e9a2..d4ce82139e5 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -656,6 +656,7 @@ def validate(self, config): """ Validate the uncertainty set with a nonemptiness and boundedness check. + Clears any cached exact parameter bounds. Parameters ---------- @@ -667,6 +668,10 @@ def validate(self, config): ValueError If nonemptiness check or boundedness check fails. """ + # clear any cached exact parameter bounds + self._solve_bounds_optimization.cache_clear() + + # perform validation checks if not self.is_nonempty(config=config): raise ValueError(f"Nonemptiness check failed for uncertainty set {self}.") From 65e95647e758e0fbcbf7cbe9c127f6cbb02e5470 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Fri, 20 Mar 2026 14:24:59 -0400 Subject: [PATCH 3/7] Add tests checking caching. --- .../pyros/tests/test_uncertainty_sets.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 28e974598e3..dcd11ef6420 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -3257,10 +3257,65 @@ def test_compute_exact_parameter_bounds(self): baron = SolverFactory("baron") custom_set = CustomUncertaintySet(dim=2) self.assertEqual(custom_set.parameter_bounds, [(-1, 1)] * 2) + + # check clearing cache + custom_set._solve_bounds_optimization.cache_clear() + info = custom_set._solve_bounds_optimization.cache_info() + self.assertEqual( + info.hits, 0 + ) + self.assertEqual( + info.misses, 0 + ) + self.assertEqual( + info.maxsize, None + ) + self.assertEqual( + info.currsize, 0 + ) + + # check cache info + # Expecting 4 misses and 4 cached values for each lower/upper bound + self.assertEqual( + custom_set._compute_exact_parameter_bounds(baron), [(-1, 1)] * 2 + ) + + info = custom_set._solve_bounds_optimization.cache_info() + self.assertEqual( + info.hits, 0 + ) + self.assertEqual( + info.misses, 4 + ) + self.assertEqual( + info.maxsize, None + ) + self.assertEqual( + info.currsize, 4 + ) + + # run again and check caching + # Expecting additional 4 hits from accessing cached values self.assertEqual( custom_set._compute_exact_parameter_bounds(baron), [(-1, 1)] * 2 ) + info = custom_set._solve_bounds_optimization.cache_info() + self.assertEqual( + info.hits, 4 + ) + self.assertEqual( + info.misses, 4 + ) + self.assertEqual( + info.maxsize, None + ) + self.assertEqual( + info.currsize, 4 + ) + custom_set._solve_bounds_optimization.cache_clear() + + @unittest.skipUnless(baron_available, "BARON is not available") def test_solve_feasibility(self): """ From a483ab6fe7ce33a7406051bb5e709595422b37b8 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Fri, 20 Mar 2026 14:25:19 -0400 Subject: [PATCH 4/7] Fix _fbbt_parameter_bounds bound value issues --- 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 d4ce82139e5..195ddcf0431 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -898,7 +898,7 @@ def _fbbt_parameter_bounds(self, config): ) param_bounds = [ - (var.lower, var.upper) for var in bounding_model.param_vars.values() + (value(var.lower), value(var.upper)) for var in bounding_model.param_vars.values() ] return param_bounds From 549d5b8da8c1bf9d19fc810d3eaaab5624094a0c Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Fri, 20 Mar 2026 15:47:47 -0400 Subject: [PATCH 5/7] Run black --- .../pyros/tests/test_uncertainty_sets.py | 49 +++++-------------- pyomo/contrib/pyros/uncertainty_sets.py | 13 +++-- 2 files changed, 18 insertions(+), 44 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index dcd11ef6420..0d4fb2146d8 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -3261,18 +3261,10 @@ def test_compute_exact_parameter_bounds(self): # check clearing cache custom_set._solve_bounds_optimization.cache_clear() info = custom_set._solve_bounds_optimization.cache_info() - self.assertEqual( - info.hits, 0 - ) - self.assertEqual( - info.misses, 0 - ) - self.assertEqual( - info.maxsize, None - ) - self.assertEqual( - info.currsize, 0 - ) + self.assertEqual(info.hits, 0) + self.assertEqual(info.misses, 0) + self.assertEqual(info.maxsize, None) + self.assertEqual(info.currsize, 0) # check cache info # Expecting 4 misses and 4 cached values for each lower/upper bound @@ -3281,18 +3273,10 @@ def test_compute_exact_parameter_bounds(self): ) info = custom_set._solve_bounds_optimization.cache_info() - self.assertEqual( - info.hits, 0 - ) - self.assertEqual( - info.misses, 4 - ) - self.assertEqual( - info.maxsize, None - ) - self.assertEqual( - info.currsize, 4 - ) + self.assertEqual(info.hits, 0) + self.assertEqual(info.misses, 4) + self.assertEqual(info.maxsize, None) + self.assertEqual(info.currsize, 4) # run again and check caching # Expecting additional 4 hits from accessing cached values @@ -3301,20 +3285,11 @@ def test_compute_exact_parameter_bounds(self): ) info = custom_set._solve_bounds_optimization.cache_info() - self.assertEqual( - info.hits, 4 - ) - self.assertEqual( - info.misses, 4 - ) - self.assertEqual( - info.maxsize, None - ) - self.assertEqual( - info.currsize, 4 - ) + self.assertEqual(info.hits, 4) + self.assertEqual(info.misses, 4) + self.assertEqual(info.maxsize, None) + self.assertEqual(info.currsize, 4) custom_set._solve_bounds_optimization.cache_clear() - @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 195ddcf0431..c3544230e89 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -670,7 +670,7 @@ def validate(self, config): """ # clear any cached exact parameter bounds self._solve_bounds_optimization.cache_clear() - + # perform validation checks if not self.is_nonempty(config=config): raise ValueError(f"Nonemptiness check failed for uncertainty set {self}.") @@ -800,10 +800,8 @@ def _compute_exact_parameter_bounds(self, solver, index=None): if not index[idx][i]: bounds.append(None) continue - bounds.append( - self._solve_bounds_optimization(solver, idx, sense) - ) - + bounds.append(self._solve_bounds_optimization(solver, idx, sense)) + # add parameter bounds for current dimension param_bounds.append(tuple(bounds)) @@ -864,7 +862,7 @@ def _solve_bounds_optimization(self, solver, index, sense): obj.deactivate() bound = value(obj) - + return bound def _fbbt_parameter_bounds(self, config): @@ -898,7 +896,8 @@ def _fbbt_parameter_bounds(self, config): ) param_bounds = [ - (value(var.lower), value(var.upper)) for var in bounding_model.param_vars.values() + (value(var.lower), value(var.upper)) + for var in bounding_model.param_vars.values() ] return param_bounds From bb7fca5a0bcd3510ae0046239af4ad3882b9063a Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Fri, 20 Mar 2026 16:29:58 -0400 Subject: [PATCH 6/7] Fix test comments --- pyomo/contrib/pyros/tests/test_uncertainty_sets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 0d4fb2146d8..0ddd51e56e9 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -3259,6 +3259,7 @@ def test_compute_exact_parameter_bounds(self): self.assertEqual(custom_set.parameter_bounds, [(-1, 1)] * 2) # check clearing cache + # Expecting 0 hits, misses, size custom_set._solve_bounds_optimization.cache_clear() info = custom_set._solve_bounds_optimization.cache_info() self.assertEqual(info.hits, 0) @@ -3267,7 +3268,7 @@ def test_compute_exact_parameter_bounds(self): self.assertEqual(info.currsize, 0) # check cache info - # Expecting 4 misses and 4 cached values for each lower/upper bound + # Expecting 4 misses and size 4 self.assertEqual( custom_set._compute_exact_parameter_bounds(baron), [(-1, 1)] * 2 ) From 3a955f6a2ee15f2016aefc1b585bb3cb789d4c22 Mon Sep 17 00:00:00 2001 From: Jason Yao <131029507+jas-yao@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:01:01 -0400 Subject: [PATCH 7/7] Update _solve_bounds_optimization docstring Co-authored-by: John Siirola --- 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 c3544230e89..8445441678a 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -816,7 +816,7 @@ def _solve_bounds_optimization(self, solver, index, sense): Parameters ---------- - solver : Pyomo solver type + solver : ~pyomo.opt.base.solvers.OptSolver Optimizer to invoke on the bounding problems. index : int The index of the parameter to solve for bounds.