-
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
@jas-yao is this ready for review or should we wait for the TODO's in the description to be completed? |
Hi @blnicho, I will need to finish some of the TODOs before this will be ready for review. |
@jas-yao I'm going to convert this to a draft until it is ready for review. |
… into pyros-uncertaintyset-validation
@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.
@@ -218,7 +221,7 @@ def validate_arg_type( | |||
Name of argument to be displayed in exception message. | |||
arg_val : object | |||
Value of argument to be checked. | |||
valid_types : type or tuple of types | |||
valid_types : type, tuple of types, or iterable of 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.
Since a tuple is an iterable and validate_arg_type
now performs no treatment specific to tuples:
valid_types : type, tuple of types, or iterable of types | |
valid_types : type or iterable of types |
@@ -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.
or a list of length = self.dim if it can. | |
or a list of length ``self.dim`` if it can. |
optimality, then False is returned. | ||
This method is invoked during the validation step of a PyROS | ||
solver call. | ||
This method is invoked by validate. |
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 method is invoked by validate. | |
This method is invoked by ``self.validate()``. |
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.
True if the nominal point is within the set, | ||
and False otherwise. |
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.
Since this method does not necessarily check that the nominal point is located in the set:
True if the nominal point is within the set, | |
and False otherwise. | |
True if the uncertainty set is nonempty, | |
and False otherwise. |
""" | ||
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. (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.
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: