-
Notifications
You must be signed in to change notification settings - Fork 546
Update PyROS Uncertainty Set Validation Methods #3558
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
@shermanjasonaf, @blnicho, @jsiirola I have completed the TODOs and think this PR should be ready for review now. |
|
||
# check with parameter_bounds should always take less time than solving 2N | ||
# optimization problems | ||
self.assertLess( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed that this test (check that the new is_bounded
method is faster than the old 2N optimization problems) is failing in some cases. Specifically:
AssertionError: 0.0 not less than 0.0 : Boundedness check with provided parameter_bounds took longer than expected.
I realize this timing test is not the best due to different runtimes on different systems and was wondering if I should just remove this? Using assertLessEqual
may also resolve the issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Over all, this looks pretty good. A couple small questions / edits and I think we can be good to go.
|
||
|
||
valid_num_types = tuple(native_numeric_types) | ||
valid_num_types = native_numeric_types |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead if "renaming" native_numeric_types
, it would be better to just reference that set directly. In particular, it will help developers who are familiar with its use elsewhere to recognize what you are doing here.
if not all(map(lambda x: all(x), param_bounds_arr)): | ||
# solve bounding problems if FBBT cannot find bounds | ||
param_bounds_arr = np.array( | ||
self._compute_parameter_bounds(solver=config.global_solver) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably update _compute_parameter_bounds
so that any (partially known) bounds (from FBBT) can be skipped (and not re-solved for)?
Also, when looking at _compute_parameter_bounds
:
if index is None:
index = list(range(self.dim))
bounding_model = self._create_bounding_model()
objs_to_optimize = (
(idx, obj)
for idx, obj in bounding_model.param_var_objectives.items()
if idx in index
)
This i very inefficient (it is a quadratic search). I would recommend rewriting it as
bounding_model = self._create_bounding_model()
objs_to_optimize = bounding_model.param_var_objectives.items()
if index is not None:
set_of_target_indices = set(index)
objs_to_optimize = filter(lambda idx, obj: idx in set_of_target_indices, objs_to_optimize)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jsiirola I agree that we should probably modify _compute_parameter_bounds()
such that we can skip the global solution of bounding problems corresponding to finite bounds that were already obtained via FBBT. In so doing, we should take into consideration other methods that invoke _compute_parameter_bounds()
and rely on the exactness of the bounds returned. One such method is _is_coordinate_fixed()
, added in #3503.
A possible resolution:
- Allow the optional
index
argument to_compute_parameter_bounds()
to be None or either of the following:- A list (with length
self.dim
) of 2-tuple of bool, such that for each entry of each tuple, a value of True [False] indicates that the corresponding bounding problem should be solved [skipped]. In this case, make_compute_parameter_bounds()
return a list of 2-tuples such that the entries corresponding to skipped bounds are of valueNone
. - A list of 2-tuples corresponding to the bounding problems that should be solved. The first entry of each tuple indicates the ordinal position of the uncertain parameter and the second entry is a 0-1 value indicating the bound (lower or upper). In this case, make
_compute_parameter_bounds()
return a dict mapping the tuples to the calculated bounds.
- A list (with length
- Make use of the modified
index
argument to_compute_parameter_bounds()
inis_bounded()
, such that only bounds for which non-finite values were reported by the FBBT method are (re-)calculated. - Account for changes to the logic of the
index
argument to_compute_parameter_bounds()
throughout the rest of the PyROS codebase. In particular, the method_is_coordinate_fixed()
may need to be modified to account for changes to the structure of the returned bounds.
For clarity, we should probably also rename _compute_parameter_bounds()
to _compute_exact_parameter_bounds()
and/or ensure that the introductory one-sentence summary in the docstring explicitly conveys that the bounds returned are exact (that is, modify the sentence to "Compute exact lower and upper bounds...").
I would probably also ensure that the docstring of _fbbt_parameter_bounds()
explicitly conveys that the bounds returned by _fbbt_parameter_bounds()
are, or may be, inexact.
if not check_nonempty: | ||
raise ValueError( | ||
"Failed nonemptiness check. Nominal point is not in the set. " | ||
f"Nominal point:\n {config.nominal_uncertain_param_vals}." | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you assign these to new variables? I think the code would be easier to read if you just:
if not check_nonempty: | |
raise ValueError( | |
"Failed nonemptiness check. Nominal point is not in the set. " | |
f"Nominal point:\n {config.nominal_uncertain_param_vals}." | |
) | |
if not self.is_nonempty(config=config): | |
raise ValueError( | |
"Failed nonemptiness check. Nominal point is not in the set. " | |
f"Nominal point:\n {config.nominal_uncertain_param_vals}." | |
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jas-yao Overall, this PR looks good. I have several comments related to the documentation and logging/exception messages. A few tests need to be modified to ensure that all tests pass.
@@ -508,6 +514,9 @@ def parameter_bounds(self): | |||
""" | |||
Bounds for the value of each uncertain parameter constrained | |||
by the set (i.e. bounds for each set dimension). | |||
|
|||
This method should return an empty list if it can't be calculated | |||
or a list of length = self.dim if it can. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would replace this sentence with a full description of the (structure of the) returned value. Something like:
Returns
-------
: list of tuple
If the bounds can be calculated, then the list is of
length `N`, and each entry is a pair of numeric
(lower, upper) bounds for the corresponding
(Cartesian) coordinate. Otherwise, the list is empty.
if not all(map(lambda x: all(x), param_bounds_arr)): | ||
# solve bounding problems if FBBT cannot find bounds | ||
param_bounds_arr = np.array( | ||
self._compute_parameter_bounds(solver=config.global_solver) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jsiirola I agree that we should probably modify _compute_parameter_bounds()
such that we can skip the global solution of bounding problems corresponding to finite bounds that were already obtained via FBBT. In so doing, we should take into consideration other methods that invoke _compute_parameter_bounds()
and rely on the exactness of the bounds returned. One such method is _is_coordinate_fixed()
, added in #3503.
A possible resolution:
- Allow the optional
index
argument to_compute_parameter_bounds()
to be None or either of the following:- A list (with length
self.dim
) of 2-tuple of bool, such that for each entry of each tuple, a value of True [False] indicates that the corresponding bounding problem should be solved [skipped]. In this case, make_compute_parameter_bounds()
return a list of 2-tuples such that the entries corresponding to skipped bounds are of valueNone
. - A list of 2-tuples corresponding to the bounding problems that should be solved. The first entry of each tuple indicates the ordinal position of the uncertain parameter and the second entry is a 0-1 value indicating the bound (lower or upper). In this case, make
_compute_parameter_bounds()
return a dict mapping the tuples to the calculated bounds.
- A list (with length
- Make use of the modified
index
argument to_compute_parameter_bounds()
inis_bounded()
, such that only bounds for which non-finite values were reported by the FBBT method are (re-)calculated. - Account for changes to the logic of the
index
argument to_compute_parameter_bounds()
throughout the rest of the PyROS codebase. In particular, the method_is_coordinate_fixed()
may need to be modified to account for changes to the structure of the returned bounds.
For clarity, we should probably also rename _compute_parameter_bounds()
to _compute_exact_parameter_bounds()
and/or ensure that the introductory one-sentence summary in the docstring explicitly conveys that the bounds returned are exact (that is, modify the sentence to "Compute exact lower and upper bounds...").
I would probably also ensure that the docstring of _fbbt_parameter_bounds()
explicitly conveys that the bounds returned by _fbbt_parameter_bounds()
are, or may be, inexact.
""" | ||
Return True if the uncertainty set is bounded and non-empty, | ||
else False. | ||
Validate the uncertainty set with a nonemptiness and boundedness check. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider wrapping this sentence:
Validate the uncertainty set with a nonemptiness and boundedness check. | |
Validate the uncertainty set with a nonemptiness | |
and boundedness check. |
This check is carried out by checking if all parameter bounds | ||
are finite. | ||
|
||
If no parameter bounds are available, the following processes are run | ||
to perform the check: | ||
(i) feasibility-based bounds tightening is used to obtain parameter | ||
bounds, and if not all bound are found, | ||
(ii) solving a sequence of maximization and minimization problems | ||
(in which the objective for each problem is the value of a | ||
single uncertain parameter). | ||
If any of the optimization models cannot be solved successfully to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider (re-)wrapping these sentences (and all other added/modified docstrings in this PR) to a line length of ~72 (including the indentation spaces). I generally wrap docstrings to that length to avoid long lines and since black
does not wrap docstrings.
# check no column is all zeros. otherwise, set is unbounded | ||
cols_with_all_zeros = np.nonzero( | ||
[np.all(col == 0) for col in lhs_coeffs_arr.T] | ||
)[0] | ||
if cols_with_all_zeros.size > 0: | ||
col_str = ", ".join(str(val) for val in cols_with_all_zeros) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check is not (necessarily) consistent with the method docstring ("...full column rank of the LHS matrix..."). Was your final decision to check that the LHS matrix is full column rank, or that there is no column of zeros? Either the docstring or this check should be modified accordingly. If this check is to be kept as-is, then the first assignment can be simplified to:
cols_with_all_zeros = np.nonzero(np.all(lhs_coeffs_arr == 0, axis=0))[0]
If you intend to check that the matrix is full column rank, then you can adopt the check used in FactorModelSet.validate()
.
If any uncertainty set attributes are not valid. | ||
If finiteness or bounds checks fail. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this and other UncertaintySet
subclass validate()
method docstrings, the phrase "finiteness ... checks" should be modified for clarity, particularly to new readers viewing this docstring through the Online Docs. (E.g., of what is the "finiteness" being checked?) I would probably change this particular sentence to "If self.bounds
contains invalid (e.g., infinite) values."
If any uncertainty set attributes are not valid. | ||
If finiteness, positive deviation, or gamma checks fail. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Echoing the comments on the docstring of BoxSet.validate()
:
If any uncertainty set attributes are not valid. | |
If finiteness, positive deviation, or gamma checks fail. | |
If any uncertainty set attributes are not valid, | |
(e.g., numeric values are infinite, | |
``self.positive_deviation`` has negative values, | |
or ``self.gamma`` is out of range). |
I would modify the other subclass-specific validate()
docstrings similarly.
# use parameter bounds if they are available | ||
param_bounds_arr = self.parameter_bounds | ||
if param_bounds_arr: | ||
all_bounds_finite = np.all(np.isfinite(param_bounds_arr)) | ||
else: | ||
# initialize uncertain parameter variables | ||
param_bounds_arr = np.array(self._fbbt_parameter_bounds(config)) | ||
if not all(map(lambda x: all(x), param_bounds_arr)): | ||
# solve bounding problems if FBBT cannot find bounds | ||
param_bounds_arr = np.array( | ||
self._compute_parameter_bounds(solver=config.global_solver) | ||
) | ||
all_bounds_finite = np.all(np.isfinite(param_bounds_arr)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I propose that this be modified to something like:
# use parameter bounds if they are available
param_bounds_arr = np.array(self.parameter_bounds)
all_bounds_finite = (
param_bounds_arr.size > 0
and np.isfinite(param_bounds_arr).all()
)
if not all_bounds_finite:
# use FBBT
param_bounds_arr = np.array(
self._fbbt_parameter_bounds(config),
dtype="float",
)
all_bounds_finite = np.isfinite(param_bounds_arr).all()
if not all_bounds_finite:
# solve bounding problems
param_bounds_arr = np.array(
self._compute_parameter_bounds(solver=config.global_solver)
)
all_bounds_finite = np.isfinite(param_bounds_arr).all()
In particular, this robustifies the check on the FBBT bounds. The current FBBT bounds finiteness check all(map(lambda x: all(x), param_bounds_arr))
may return an inaccurate result if param_bounds_arr
contains an entry of value 0.
self.assertLess( | ||
time_with_bounds_provided, | ||
time_without_bounds_provided, | ||
"Boundedness check with provided parameter_bounds took longer than expected.", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrap this string to reduce the line length
"Boundedness check with provided parameter_bounds took longer than expected.", | |
"Boundedness check with provided parameter_bounds took longer than " | |
"expected.", |
Co-authored-by: Jason Sherman <[email protected]>
Co-authored-by: Jason Sherman <[email protected]>
Co-authored-by: Jason Sherman <[email protected]>
Co-authored-by: Jason Sherman <[email protected]>
Co-authored-by: Jason Sherman <[email protected]>
Co-authored-by: Jason Sherman <[email protected]>
Co-authored-by: Jason Sherman <[email protected]>
Co-authored-by: Jason Sherman <[email protected]>
Co-authored-by: Jason Sherman <[email protected]>
Co-authored-by: Jason Sherman <[email protected]>
Fixes: #2724, #3508
Summary/Motivation:
This PR provides updates to PyROS uncertainty set validation methods and related tests.
Here, a
validate
method replaces theis_valid
method (which solves 2N bounding problems to check for set boundedness) in all uncertainty sets, with each set having its own customvalidate
method that efficiently checks set-specific attributes and raises informative exceptions if any issues are found.Changes proposed in this PR:
is_bounded
andis_nonempty
methods in baseUncertaintySet
class_solve_feasibility
method in baseUncertaintySet
classis_valid
withvalidate
method that runsis_bounded
andis_nonempty
in the baseUncertaintySet
classvalidate
in subclass uncertainty sets to check set-specific attributesvalidate
methodis_bounded
,is_nonempty
,_solve_feasibility
, andvalidate
methodsTODO
_compute_parameter_bounds
validate_array
to allvalidate
methodstest_validate
tests into separate tests for each validation check_validate
method forPolyhedralSet
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: