Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
53bbc30
Move bounds calculation to separate, cached method
jas-yao Mar 20, 2026
b6a03aa
Clear any cached bounds at set validation
jas-yao Mar 20, 2026
65e9564
Add tests checking caching.
jas-yao Mar 20, 2026
a483ab6
Fix _fbbt_parameter_bounds bound value issues
jas-yao Mar 20, 2026
549d5b8
Run black
jas-yao Mar 20, 2026
bb7fca5
Fix test comments
jas-yao Mar 20, 2026
a6ef4f5
Merge branch 'main' into pyros-cache-computed-param-bounds
jas-yao Mar 22, 2026
3a955f6
Update _solve_bounds_optimization docstring
jas-yao Mar 26, 2026
d003145
Merge branch 'main' into pyros-cache-computed-param-bounds
jsiirola Apr 2, 2026
c573512
Merge branch 'main' into pyros-cache-computed-param-bounds
jas-yao May 6, 2026
f0e86b6
Update bounds optimization caching
jas-yao May 6, 2026
2af61c8
Add test_solve_exact_bounds_optimization
jas-yao May 6, 2026
bb53c9d
Add test_fbbt_values
jas-yao May 6, 2026
be21c04
Move cache clearing to before/after solving
jas-yao May 6, 2026
7619f3e
Add tests for PyROS caching
jas-yao May 6, 2026
81a98ab
Run black
jas-yao May 6, 2026
4f78486
Run updated black
jas-yao May 6, 2026
581a9f7
Update CHANGELOG
jas-yao May 6, 2026
7aeeef5
Merge branch 'main' into pyros-cache-computed-param-bounds
jsiirola May 8, 2026
6de83b7
Merge branch 'main' into pyros-cache-computed-param-bounds
jas-yao May 11, 2026
31e96fd
Update caching with custom dict
jas-yao May 11, 2026
43f09d8
Simplify cache clearing setup
jas-yao May 11, 2026
ee87245
Update caching tests
jas-yao May 11, 2026
c1145cd
Run black
jas-yao May 11, 2026
d905ab4
Merge branch 'main' into pyros-cache-computed-param-bounds
jas-yao May 12, 2026
5518ee8
Use `var.lb` and `var.ub` for numerical bounds
jas-yao May 12, 2026
d954aba
Add assertion check for empty _cache
jas-yao May 12, 2026
c242b6a
Update caching tests for assertion error test
jas-yao May 12, 2026
e0863b0
Run black
jas-yao May 12, 2026
4ebca37
Merge branch 'main' into pyros-cache-computed-param-bounds
jsiirola May 14, 2026
6259595
Merge branch 'main' into pyros-cache-computed-param-bounds
jas-yao May 15, 2026
43079be
Apply suggestion from @shermanjasonaf
jas-yao May 15, 2026
32b278b
Apply descriptive TestPyROSCacheUncertaintySetBounds name and docstring
jas-yao May 15, 2026
15c0218
Update caching unit tests.
jas-yao May 15, 2026
d12f066
Merge branch 'main' into pyros-cache-computed-param-bounds
jas-yao May 15, 2026
acecc15
Update CHANGELOG
jas-yao May 15, 2026
60d2491
Update uncertainty set caching documentation
jas-yao May 15, 2026
f55f97c
Run black
jas-yao May 15, 2026
c152970
Update uncertaintyset cache manager design to support CartesianProduc…
jsiirola May 15, 2026
1632ba1
NFC: fix typo
jsiirola May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions pyomo/contrib/pyros/tests/test_uncertainty_sets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3257,10 +3257,41 @@ 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
# 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)
self.assertEqual(info.misses, 0)
self.assertEqual(info.maxsize, None)
self.assertEqual(info.currsize, 0)

# check cache info
# Expecting 4 misses and size 4
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):
"""
Expand Down
95 changes: 67 additions & 28 deletions pyomo/contrib/pyros/uncertainty_sets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------
Expand All @@ -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()
Comment thread
jas-yao marked this conversation as resolved.
Outdated
Comment thread
jas-yao marked this conversation as resolved.
Outdated

# perform validation checks
if not self.is_nonempty(config=config):
raise ValueError(f"Nonemptiness check failed for uncertainty set {self}.")

Expand Down Expand Up @@ -788,45 +793,78 @@ 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(value(obj))
bounds.append(self._solve_bounds_optimization(solver, idx, sense))

# 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.opt.base.solvers.OptSolver
Optimizer to invoke on the bounding problems.
index : int
The index of the parameter to solve for bounds.
Comment thread
jas-yao marked this conversation as resolved.
Outdated
sense : Pyomo objective sense
Comment thread
jas-yao marked this conversation as resolved.
Outdated
A Pyomo objective sense to optimize for the bounding model.
`maximize` solves for an upper bound and
`minimize` solves for a lower bound.
Comment thread
jas-yao marked this conversation as resolved.
Outdated

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.
Comment thread
jas-yao marked this conversation as resolved.
Outdated
"""
# create bounding model and get all objectives
bounding_model = self._create_bounding_model()
Comment thread
jas-yao marked this conversation as resolved.
Outdated

# 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()
Comment thread
jas-yao marked this conversation as resolved.
Outdated

bound = value(obj)

return bound

def _fbbt_parameter_bounds(self, config):
"""
Obtain parameter bounds of the uncertainty set using FBBT.
Expand Down Expand Up @@ -858,7 +896,8 @@ 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))
Comment thread
jas-yao marked this conversation as resolved.
Outdated
for var in bounding_model.param_vars.values()
]

return param_bounds
Expand Down
Loading