Skip to content

PyROS Add caching for computed uncertain parameter bounds#3877

Open
jas-yao wants to merge 19 commits intoPyomo:mainfrom
jas-yao:pyros-cache-computed-param-bounds
Open

PyROS Add caching for computed uncertain parameter bounds#3877
jas-yao wants to merge 19 commits intoPyomo:mainfrom
jas-yao:pyros-cache-computed-param-bounds

Conversation

@jas-yao
Copy link
Copy Markdown
Contributor

@jas-yao jas-yao commented Mar 20, 2026

Fixes

_fbbt_parameter_bounds in uncertainty_sets.py

Summary/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.

  1. Validation in 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.
  2. Validation in is_nonempty. This occurs for intersection, polyhedral, and custom uncertainty sets, where a feasibility problem is constructed.
  3. Preprocessing in get_effective_uncertain_dimensions if parameter bounds are not provided or the provided ones are not exact.
  4. Separation problem construction in add_uncertainty_set_constraints if 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_bounds where the returned bounds are not a float (e.g., a binary variable m.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:

  • Add a _solve_bounds_optimization method that uses functools.cache to cache solutions for bounding problems solved for any uncertain parameter that is used by _compute_exact_parameter_bounds
  • Add a line for clearing the _solve_bounds_optimization cache during validation, which is run at the start of every PyROS solve.
  • Add tests for checking caching behavior of _solve_bounds_optimization
  • Fix the issue in _fbbt_parameter_bounds

Legal Acknowledgement

By contributing to this software project, I have read the contribution guide and agree to the following terms and conditions for my contribution:

  1. I agree my contributions are submitted under the BSD license.
  2. I represent I am authorized to make the contributions and grant the license. If my employer has rights to intellectual property that includes these contributions, I represent that I have received permission to make contributions and grant the required license on behalf of that employer.

@jas-yao
Copy link
Copy Markdown
Contributor Author

jas-yao commented Mar 20, 2026

@shermanjasonaf @jsiirola

Copy link
Copy Markdown
Member

@jsiirola jsiirola left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks pretty good. Some questions and a couple minor suggestions.

Comment thread pyomo/contrib/pyros/uncertainty_sets.py Outdated
Comment thread pyomo/contrib/pyros/uncertainty_sets.py Outdated
Comment thread pyomo/contrib/pyros/uncertainty_sets.py Outdated
Comment thread pyomo/contrib/pyros/uncertainty_sets.py Outdated
Comment thread pyomo/contrib/pyros/uncertainty_sets.py Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 26, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.08%. Comparing base (f8fb5a9) to head (581a9f7).
⚠️ Report is 7 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3877      +/-   ##
==========================================
- Coverage   90.08%   90.08%   -0.01%     
==========================================
  Files         904      904              
  Lines      106999   107013      +14     
==========================================
+ Hits        96391    96402      +11     
- Misses      10608    10611       +3     
Flag Coverage Δ
builders 29.16% <1.88%> (+<0.01%) ⬆️
default 86.40% <100.00%> (?)
expensive 35.59% <1.88%> (?)
linux 87.56% <100.00%> (-2.02%) ⬇️
linux_other 87.56% <100.00%> (+<0.01%) ⬆️
oldsolvers 28.10% <1.88%> (-0.01%) ⬇️
osx 82.93% <100.00%> (+<0.01%) ⬆️
win 86.00% <100.00%> (+<0.01%) ⬆️
win_other 86.00% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Co-authored-by: John Siirola <jsiirola@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@shermanjasonaf shermanjasonaf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added a test for this.

Yes, I noticed that the FBBT issue comes up when uncertain parameter Var objects are given domains (e.g., Binary, NonNegativeReals, UnitInterval, etc.) and bounds at the same time. Setting bounds and domains leads to an expression object being returned by var.lower and var.upper rather than a value.

I think it is reasonable to not support altering the domains of uncertain parameters if they can already be specified through setting parameter bounds.

Comment thread pyomo/contrib/pyros/uncertainty_sets.py Outdated
@jsiirola
Copy link
Copy Markdown
Member

@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
Copy link
Copy Markdown
Contributor Author

jas-yao commented May 6, 2026

Thank you for all the detailed feedback @jsiirola and @shermanjasonaf .

I have updated the caching implementation and added additional tests. This PR is ready for review.

I had some questions come to mind when working on this and would appreciate any insights:

  1. Where is the best place for the ._cache attribute to be created? It is currently being created when needed in _solve_exact_bounds_optimization.
  2. Does it make sense to have the default behavior of parameter_bounds in UncertaintySet return _compute_exact_parameter_bounds?

@jas-yao jas-yao marked this pull request as ready for review May 6, 2026 20:52
Copy link
Copy Markdown
Member

@jsiirola jsiirola left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some more minor questions,a nd a potential significant design simplification: Instead of putting everything inside another context block, you could use a custom dict that supports context management:

diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py
index a925ab1939..8a056e05b2 100644
--- a/pyomo/contrib/pyros/uncertainty_sets.py
+++ b/pyomo/contrib/pyros/uncertainty_sets.py
@@ -460,6 +460,15 @@ def validate_array(
         )


+class ContextDict(dict):
+    def __enter__(self):
+        assert not self
+        return self
+
+    def __exit__(self, et, e, tb):
+        self.clear()
+
+
 class Geometry(Enum):
     """
     Geometry classifications for PyROS uncertainty set objects.
@@ -524,6 +533,14 @@ class UncertaintySet(object, metaclass=abc.ABCMeta):
         """
         raise NotImplementedError

+    @property
+    def _cache(self):
+        try:
+            return self.__cache
+        except AttributeError:
+            self.__cache = ContextDict()
+            return self.__cache
+
     def _create_bounding_model(self):
         """
         Make uncertain parameter value bounding problems (optimize

Then you could just include the cache management in with the main timer:

diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py
index 38104e98fa..c9fb37e611 100644
--- a/pyomo/contrib/pyros/pyros.py
+++ b/pyomo/contrib/pyros/pyros.py
@@ -386,10 +386,13 @@ class PyROS(object):
         )

         model_data = ModelData(original_model=model, timing=TimingData(), config=None)
-        with time_code(
-            timing_data_obj=model_data.timing,
-            code_block_name="main",
-            is_main_timer=True,
+        with (
+            uncertainty_set._cache,
+            time_code(
+                timing_data_obj=model_data.timing,
+                code_block_name="main",
+                is_main_timer=True,
+            ),
         ):
             kwds.update(
                 dict(


# Define the uncertainty set
interval_cache = CustomExactBoundsUncertaintySet(
bounds=[(0.25, 2)], sleep_time=0.5, cache=True
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be made smaller (maybe 0.1)? I worry about how long this test takes (it's about 5 seconds).

Comment on lines +5301 to +5302
local_subsolver = SolverFactory("baron")
global_subsolver = SolverFactory("baron")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to use baron here?

Comment on lines +5358 to +5359
local_subsolver = SolverFactory("baron")
global_subsolver = SolverFactory("baron")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need Baron here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants