PyROS Add caching for computed uncertain parameter bounds#3877
PyROS Add caching for computed uncertain parameter bounds#3877jas-yao wants to merge 43 commits into
Conversation
jsiirola
left a comment
There was a problem hiding this comment.
This looks pretty good. Some questions and a couple minor suggestions.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3877 +/- ##
=======================================
Coverage 90.11% 90.12%
=======================================
Files 905 905
Lines 107502 107522 +20
=======================================
+ Hits 96878 96899 +21
+ Misses 10624 10623 -1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Co-authored-by: John Siirola <jsiirola@users.noreply.github.com>
shermanjasonaf
left a comment
There was a problem hiding this comment.
Good PR. I have a few questions about edge cases and testing.
|
@jas-yao: we are adopting a new review process where we convert PRs that are "waiting on the author" back to "draft" (to signal the PR state to both the author and the dev team). Once you have had a chance to address the comments, please mark it as "ready for review" so the developers can get it back into the review queue. |
|
@jas-yao @jsiirola As PR #3927 has been merged ahead of this PR, we will need to add bounds caching for the new
|
|
@shermanjasonaf I vote for option 2 and doing a single update to the PyROS version and changelog |
shermanjasonaf
left a comment
There was a problem hiding this comment.
After the more recent changes, this PR looks good overall. I have comments on functionality for IntersectionSet and CartesianProductSet cases. I also have comments on testing and several suggestions on documentation.
@jas-yao To address the second question of your earlier comment: I do not think that we should make UncertaintySet.parameter_bounds return the results of UncertaintySet._compute_exact_parameter_bounds() by default, for a few reasons:
_compute_exact_parameter_bounds()formally requires the user to provide a solver, whereasparameter_boundsdoes not.parameter_boundsis a property (basically an attribute), so we may want the calculations performed within to be somewhat quick. In contrast,_compute_exact_parameter_bounds()is generally slow when the bounds are not cached, due to the time required to construct and repeatedly solve the bounding model.parameter_boundsis public (and therefore accessible). If the default behavior ofparameter_boundsis changed in the suggested way, then accessingparameter_boundsresults by default in population of the cache, a protected attribute that must be clear(ed) ahead of callingPyROS.solve().
|
|
||
| # validate each set | ||
| for a_set in the_sets: | ||
| a_set.validate(config) |
There was a problem hiding this comment.
Isn't it possible that a_set._cache gets populated here, particularly for cases where calling a_set.validate() entails calling a_set._compute_exact_parameter_bounds()? Do we need to be careful to clear a_set._cache here?
Update CustomExactBoundsUncertaintySet docstring Co-authored-by: Jason Sherman <sherman.jasonaf@gmail.com>
Co-authored-by: Jason Sherman <sherman.jasonaf@gmail.com>
|
@shermanjasonaf @jas-yao: I pushed the updated cache context manager. Are we good to "final review" and merge? (@shermanjasonaf: are you satisfied with how caching interacts with the |
shermanjasonaf
left a comment
There was a problem hiding this comment.
@jsiirola @jas-yao I think we are close to merging. The new cache context manager works nicely. I am satisfied with how caching interacts with the CartesianProductSet; my comments include suggestions for additional testing. We may also want a similar custom implementation for IntersectionSet._cache_manager(), as bounds caching of the operand UncertaintySet attributes may also occur in some (seemingly rare) cases.
| @contextlib.contextmanager | ||
| def _cache_manager(self): | ||
| with contextlib.ExitStack() as stack: | ||
| # Verify this (CartesianProductSet's) cache is empty | ||
| stack.enter_context(super()._cache_manager()) | ||
| for uset in self._all_sets: | ||
| # Verify all component caches are empty | ||
| stack.enter_context(uset._cache_manager()) | ||
| yield self | ||
| # This will re-enter when this context manager is exited, | ||
| # which will exit the stack context, triggering all the | ||
| # context managers entered above to exit. | ||
|
|
There was a problem hiding this comment.
This works nicely. Should IntersectionSet._cache_manager() be implemented similarly, to ensure that the bounds caches of an IntersectionSet's operand UncertaintySet objects are also cleared?
There was a problem hiding this comment.
Yes, that makes sense to me. I have added this.
|
|
||
| self.assertAlmostEqual(interval._cache[0, minimize], 25, places=2) | ||
| self.assertAlmostEqual(interval._cache[0, maximize], 200, places=2) | ||
|
|
There was a problem hiding this comment.
I would add here a test based on a problem with a CartesianProductSet, to formally check that CartesianProductSet._cache_manager() works according to expectations. Something like:
diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py
index 410593c82..8f577d879 100644
--- a/pyomo/contrib/pyros/tests/test_grcs.py
+++ b/pyomo/contrib/pyros/tests/test_grcs.py
@@ -75,6 +75,7 @@ from pyomo.contrib.pyros.uncertainty_sets import (
FactorModelSet,
Geometry,
IntersectionSet,
+ PolyhedralSet,
UncertaintyQuantification,
UncertaintySet,
)
@@ -5468,6 +5469,71 @@ class TestPyROSCacheUncertaintySetBounds(unittest.TestCase):
self.assertAlmostEqual(interval._cache[0, minimize], 25, places=2)
self.assertAlmostEqual(interval._cache[0, maximize], 200, places=2)
+ def test_solve_cartesian_product_set_bounds_cache(self):
+ """
+ Test management of uncertainty set bounds caches
+ is carried out as expected in the context of a PyROS solve.
+ """
+ # deterministic model
+ m = ConcreteModel()
+ m.q = Param(range(4), initialize=0, mutable=True)
+ m.x = Var(initialize=0, bounds=(0, 100))
+ m.obj = Objective(expr=m.x, sense=minimize)
+ m.con = Constraint(expr=m.x >= sum(m.q.values()))
+
+ # uncertainty set(s)
+ poly_set = PolyhedralSet(
+ # this is just the cube [-1, 1]^3
+ lhs_coefficients_mat=np.vstack([np.eye(3), -np.eye(3)]),
+ rhs_vec=[1] * 6,
+ )
+ # inclusion of PolyhedralSet means bounds caching takes place
+ cpset = CartesianProductSet([BoxSet([[0, 1]]), poly_set])
+ res1 = SolverFactory("pyros").solve(
+ model=m,
+ first_stage_variables=m.x,
+ second_stage_variables=[],
+ uncertain_params=m.q,
+ uncertainty_set=cpset,
+ local_solver=SolverFactory("ipopt"),
+ global_solver=SolverFactory("ipopt"),
+ objective_focus="worst_case",
+ solve_master_globally=True,
+ )
+
+ # check caches cleared
+ self.assertEqual(cpset._cache, {})
+ self.assertEqual(cpset._all_sets[0]._cache, {})
+ self.assertEqual(cpset._all_sets[1]._cache, {})
+ # check results: worst case objective is just sum of the
+ # uncertainty set upper bounds
+ self.assertEqual(res1.iterations, 2)
+ self.assertAlmostEqual(res1.final_objective_value, 4.0, places=6)
+ self.assertAlmostEqual(m.x.value, 4.0, places=6)
+
+ # expand the polyhedralset to the cube [-2, 2]^3
+ poly_set.rhs_vec = [2] * 6
+ # solve again. since caches cleared, PyROS should work normally
+ res2 = SolverFactory("pyros").solve(
+ model=m,
+ first_stage_variables=m.x,
+ second_stage_variables=[],
+ uncertain_params=m.q,
+ uncertainty_set=cpset,
+ local_solver=SolverFactory("ipopt"),
+ global_solver=SolverFactory("ipopt"),
+ objective_focus="worst_case",
+ solve_master_globally=True,
+ )
+ # check caches cleared
+ self.assertEqual(cpset._cache, {})
+ self.assertEqual(cpset._all_sets[0]._cache, {})
+ self.assertEqual(cpset._all_sets[1]._cache, {})
+ # results have changed, since the polyhedral set was expanded
+ self.assertEqual(res2.iterations, 2)
+ self.assertAlmostEqual(res2.final_objective_value, 7.0, places=6)
+ self.assertAlmostEqual(m.x.value, 7.0, places=6)
+
if __name__ == "__main__":
unittest.main()There was a problem hiding this comment.
I have added this. I renamed test_solve_cartesian_product_set_bounds_cache to test_solve_cartesian_product_and_intersection_set_bounds_cache and added a couple additional checks for IntersectionSet.
|
@jsiirola @shermanjasonaf , I've taken a look at the caching behavior with the latest updates and everything works as expected. I added the suggestions made by @shermanjasonaf for Looks good to me for merging, pending any final feedback on my latest changes. |
Fixes
_fbbt_parameter_boundsinuncertainty_sets.pySummary/Motivation:
PyROS solves optimization bounding problems for every uncertain parameter multiple times throughout its routine using
_compute_exact_parameter_bounds. There are up to 4 instances of PyROS accessing this method during its runtime.is_bounded. This occurs when no parameter bounds are provided and FBBT fails to find bounds. Only the bounds that FBBT has not found are evaluated.is_nonempty. This occurs for intersection, polyhedral, and custom uncertainty sets, where a feasibility problem is constructed.get_effective_uncertain_dimensionsif parameter bounds are not provided or the provided ones are not exact.add_uncertainty_set_constraintsif no parameter bounds are provided.The time taken to repeatedly solve these bounding problems may add up and be significant for larger uncertainty sets.
This PR adds a method for caching the solutions of these bounding problems so that subsequent processes do not need to solve the bounding problems again.
This PR also fixes a small bug in
_fbbt_parameter_boundswhere the returned bounds are not a float (e.g., a binary variablem.Var = pyo.Var(within=pyo.Binary, bounds=(0,1))or a binary variable in a model that FBBT has been used on has lower bound max(0,0) and upper bound min(1,1)).Changes proposed in this PR:
_solve_bounds_optimizationmethod that usesfunctools.cacheto cache solutions for bounding problems solved for any uncertain parameter that is used by_compute_exact_parameter_bounds_solve_bounds_optimizationcache during validation, which is run at the start of every PyROS solve._solve_bounds_optimization_fbbt_parameter_boundsLegal Acknowledgement
By contributing to this software project, I have read the contribution guide and agree to the following terms and conditions for my contribution: