PyROS Add caching for computed uncertain parameter bounds#3877
PyROS Add caching for computed uncertain parameter bounds#3877jas-yao wants to merge 44 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.12% 90.12%
=======================================
Files 905 905
Lines 107648 107682 +34
=======================================
+ Hits 97021 97052 +31
- Misses 10627 10630 +3
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. |
|
@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.
|
@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. |
| ) | ||
|
|
||
|
|
||
| # @SolverFactory.register("slow_solver") |
There was a problem hiding this comment.
I added this to copy the format the other solvers in test_grcs.py were written with.
If this is removed, should we also remove # @SolverFactory.register("time_delay_solver") and # @SolverFactory.register("subsolver_error__solver")?
| def _cache_manager(self): | ||
| with contextlib.ExitStack() as stack: | ||
| # Verify this (IntersectionSet'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.
It's not a showstopper, but this exact code is repeated, no differences, in 4095-4105. It'd be better to not have repeat code.
There was a problem hiding this comment.
I tried to update the implementation to move to move the repeated code to a single class (shown below). Would this make sense?
1 file changed, 21 insertions(+), 28 deletions(-)
pyomo/contrib/pyros/uncertainty_sets.py | 49 ++++++++++++++-------------------
modified pyomo/contrib/pyros/uncertainty_sets.py
@@ -1283,6 +1283,25 @@ class UncertaintySetList(MutableSequence):
return self._dim
+class UncertaintySetOperation:
+ """
+ Mixin class for uncertainty sets that perform operations
+ on other uncertainty sets (e.g., IntersectionSet, CartesianProductSet).
+ """
+ @contextlib.contextmanager
+ def _cache_manager(self):
+ with contextlib.ExitStack() as stack:
+ # Verify this set'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.
+
+
class BoxSet(UncertaintySet):
"""
A hyperrectangle (or box).
@@ -3676,7 +3695,7 @@ class DiscreteScenarioSet(UncertaintySet):
)
-class IntersectionSet(UncertaintySet):
+class IntersectionSet(UncertaintySetOperation, UncertaintySet):
"""
An intersection of two or more uncertainty sets, each of which
is represented by an `UncertaintySet` object.
@@ -3831,19 +3850,6 @@ class IntersectionSet(UncertaintySet):
return []
- @contextlib.contextmanager
- def _cache_manager(self):
- with contextlib.ExitStack() as stack:
- # Verify this (IntersectionSet'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.
-
def point_in_set(self, point):
"""
Determine whether a given point lies in the intersection set.
@@ -3955,7 +3961,7 @@ class IntersectionSet(UncertaintySet):
super().validate(config)
-class CartesianProductSet(UncertaintySet):
+class CartesianProductSet(UncertaintySetOperation, UncertaintySet):
"""
A Cartesian product of uncertainty sets.
@@ -4091,19 +4097,6 @@ class CartesianProductSet(UncertaintySet):
return []
return parameter_bounds
- @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.
-
def _iterate_over_all_sets(self):
"""
Iterate over the sets contained in `self`.
shermanjasonaf
left a comment
There was a problem hiding this comment.
@jas-yao Looks good to me. I have just one suggestion for testing.
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: