PyROS Add caching for computed uncertain parameter bounds#3877
PyROS Add caching for computed uncertain parameter bounds#3877jas-yao wants to merge 8 commits intoPyomo:mainfrom
Conversation
jsiirola
left a comment
There was a problem hiding this comment.
This looks pretty good. Some questions and a couple minor suggestions.
| Optimizer to invoke on the bounding problems. | ||
| index : int | ||
| The index of the parameter to solve for bounds. | ||
| sense : Pyomo objective sense |
There was a problem hiding this comment.
| sense : Pyomo objective sense | |
| sense : ~pyomo.common.ObjectiveSense |
| coordinate. | ||
| """ | ||
| # create bounding model and get all objectives | ||
| bounding_model = self._create_bounding_model() |
There was a problem hiding this comment.
You are re-creating the bounding model for every variable and both directions. This seems wasteful. Given that this is typically called in a loop, maybe the model should be created once outside the look and passed in to this function?
That may foul up the use of @functools.cache, but we could just directly implement our own cache as an instance / model attribute.
| If nonemptiness check or boundedness check fails. | ||
| """ | ||
| # clear any cached exact parameter bounds | ||
| self._solve_bounds_optimization.cache_clear() |
There was a problem hiding this comment.
Is this the right place to clear the cache? This clears it at the very beginning of the PyROS algorithm, right? It should be empty now. Does it make more sense to clear the cache at the end of the algorithm so that we don't leave unnecessary models hanging around in memory?
| obj.sense = minimize | ||
| obj.deactivate() |
There was a problem hiding this comment.
If the solve fails, the objective is left activated. I recommend moving this earlier:
try:
res = solver.solve(bounding_model, load_solutions=False)
finally:
# ensure sense is minimize when done, deactivate
obj.sense = minimize
obj.deactivate()
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3877 +/- ##
=======================================
Coverage 89.92% 89.93%
=======================================
Files 901 901
Lines 106285 106290 +5
=======================================
+ Hits 95580 95587 +7
+ Misses 10705 10703 -2
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.
|
|
||
| param_bounds = [ | ||
| (var.lower, var.upper) for var in bounding_model.param_vars.values() | ||
| (value(var.lower), value(var.upper)) |
There was a problem hiding this comment.
We should consider adding a test that is affected by this change.
In light of the write-up for this PR, it may also be worth noting that as of PR #3733, the PyROS Uncertainty Sets documentation page explicitly states that mixed-integer uncertainty sets are not supported. More broadly, and perhaps in a future PR, we may want to make that documentation and/or the docstring for the UncertaintySet.set_as_constraint() method more specific/explicit about can/can't be done to model components within that method. (E.g., should altering the domains of the uncertain parameter Var objects be generally supported?)
| If nonemptiness check or boundedness check fails. | ||
| """ | ||
| # clear any cached exact parameter bounds | ||
| self._solve_bounds_optimization.cache_clear() |
There was a problem hiding this comment.
I anticipate that clearing the cache here will work for most use cases, but I am questioning whether this is the safest place to clear the cache. If, for example, the user implements a custom UncertaintySet subclass where the validate() method is overriden such that the cache clearing statement is omitted, then the _compute_exact_parameter_bounds() method may return an inaccurate result when, for example, the state of a subclass instance is altered between successive calls to PyROS.solve() that use the subclass instance.
Perhaps cache clearing should be invoked in PyROS.solve(), just before argument validation? We probably also want the cache cleared near the end of PyROS.solve() to clear memory (see @jsiirola's comment). To this end, I would consider modifying PyROS.solve() to the following:
def solve(...):
... # everything up to disclaimer logging
... # <-- clear cache here
try:
# everything from argument validation
# to finalization of results object
...
finally:
... # <-- clear cache here
... # log termination-related messages
return ...If we proceed with this change, then we may want to add to test_grcs.py a test to check that successive calls to PyROS.solve() using the same UncertaintySet object yield accurate results, particularly for a case where:
- The
validate()method is overriden - The exact bounds are computed exclusively using
_compute_exact_parameter_bounds. - Between the successive calls to
PyROS.solve(), the attributes of theUncertaintySetobject are changed such that the exact bounds are changed.
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: