@@ -348,6 +348,38 @@ def parameter_bounds(self):
348348 """
349349 raise NotImplementedError
350350
351+ def bounding_model(self):
352+ """
353+ Make uncertain parameter value bounding problems (optimize
354+ value of each uncertain parameter subject to constraints on the
355+ uncertain parameters).
356+
357+ Returns
358+ -------
359+ model : ConcreteModel
360+ Bounding problem, with all Objectives deactivated.
361+ """
362+ model = ConcreteModel()
363+ model.util = Block()
364+
365+ # construct param vars, initialize to nominal point
366+ model.param_vars = Var(range(self.dim))
367+
368+ # add constraints
369+ model.cons = self.set_as_constraint(
370+ uncertain_params=model.param_vars,
371+ model=model,
372+ )
373+
374+ @model.Objective(range(self.dim))
375+ def param_var_objectives(self, idx):
376+ return model.param_vars[idx]
377+
378+ # deactivate all objectives
379+ model.param_var_objectives.deactivate()
380+
381+ return model
382+
351383 def is_bounded(self, config):
352384 """
353385 Determine whether the uncertainty set is bounded.
@@ -374,35 +406,36 @@ def is_bounded(self, config):
374406 This method is invoked during the validation step of a PyROS
375407 solver call.
376408 """
377- # === Determine bounds on all uncertain params
378- bounding_model = ConcreteModel()
379- bounding_model.util = Block() # So that boundedness checks work for Cardinality and FactorModel sets
380- bounding_model.uncertain_param_vars = IndexedVar(range(len(config.uncertain_params)), initialize=1)
381- for idx, param in enumerate(config.uncertain_params):
382- bounding_model.uncertain_param_vars[idx].set_value(
383- param.value, skip_validation=True)
384-
385- bounding_model.add_component("uncertainty_set_constraint",
386- config.uncertainty_set.set_as_constraint(
387- uncertain_params=bounding_model.uncertain_param_vars,
388- model=bounding_model,
389- config=config
390- ))
391-
392- for idx, param in enumerate(list(bounding_model.uncertain_param_vars.values())):
393- bounding_model.add_component("lb_obj_" + str(idx), Objective(expr=param, sense=minimize))
394- bounding_model.add_component("ub_obj_" + str(idx), Objective(expr=param, sense=maximize))
395-
396- for o in bounding_model.component_data_objects(Objective):
397- o.deactivate()
398-
399- for i in range(len(bounding_model.uncertain_param_vars)):
400- for limit in ("lb", "ub"):
401- getattr(bounding_model, limit + "_obj_" + str(i)).activate()
402- res = config.global_solver.solve(bounding_model, tee=False)
403- getattr(bounding_model, limit + "_obj_" + str(i)).deactivate()
409+ bounding_model = self.bounding_model()
410+ solver = config.global_solver
411+
412+ # initialize uncertain parameter variables
413+ for param, param_var in zip(
414+ config.uncertain_params,
415+ bounding_model.param_vars.values(),
416+ ):
417+ param_var.set_value(param.value, skip_validation=True)
418+
419+ for idx, obj in bounding_model.param_var_objectives.items():
420+ # activate objective for corresponding dimension
421+ obj.activate()
422+
423+ # solve for lower bound, then upper bound
424+ for sense in (minimize, maximize):
425+ obj.sense = sense
426+ res = solver.solve(
427+ bounding_model,
428+ load_solutions=False,
429+ tee=False,
430+ )
431+
404432 if not check_optimal_termination(res):
405433 return False
434+
435+ # ensure sense is minimize when done, deactivate
436+ obj.sense = minimize
437+ obj.deactivate()
438+
406439 return True
407440
408441 def is_nonempty(self, config):
@@ -1572,10 +1605,9 @@ class FactorModelSet(UncertaintySet):
15721605 Natural number representing the dimensionality of the
15731606 space to which the set projects.
15741607 psi_mat : (N, `number_of_factors`) array_like
1575- Matrix with nonnegative entries designating each
1576- uncertain parameter's contribution to each factor.
1577- Each row is associated with a separate uncertain parameter.
1578- Each column is associated with a separate factor.
1608+ Matrix designating each uncertain parameter's contribution to
1609+ each factor. Each row is associated with a separate uncertain
1610+ parameter. Each column is associated with a separate factor.
15791611 beta : numeric type
15801612 Real value between 0 and 1 specifying the fraction of the
15811613 independent factors that can simultaneously attain
@@ -1661,8 +1693,7 @@ def psi_mat(self):
16611693 (N, `number_of_factors`) numpy.ndarray : Matrix designating each
16621694 uncertain parameter's contribution to each factor. Each row is
16631695 associated with a separate uncertain parameter. Each column with
1664- a separate factor. Every entry of the matrix must be
1665- nonnegative.
1696+ a separate factor.
16661697 """
16671698 return self._psi_mat
16681699
@@ -1697,13 +1728,6 @@ def psi_mat(self, val):
16971728 "one nonzero entry"
16981729 )
16991730
1700- for entry in column:
1701- if entry < 0:
1702- raise ValueError(
1703- f"Entry {entry} of attribute 'psi_mat' is negative. "
1704- "Ensure all entries are nonnegative"
1705- )
1706-
17071731 self._psi_mat = psi_mat_arr
17081732
17091733 @property
@@ -1758,27 +1782,51 @@ def parameter_bounds(self):
17581782 the uncertain parameter bounds for the corresponding set
17591783 dimension.
17601784 """
1761- nom_val = self.origin
1785+ F = self.number_of_factors
17621786 psi_mat = self.psi_mat
17631787
1764- F = self.number_of_factors
1765- beta_F = self.beta * F
1766- floor_beta_F = math.floor(beta_F)
1788+ # evaluate some important quantities
1789+ beta_F = self.beta * self.number_of_factors
1790+ crit_pt_type = int((beta_F + F) / 2)
1791+ beta_F_fill_in = (beta_F + F) - 2 * crit_pt_type - 1
1792+
1793+ # argsort rows of psi_mat in descending order
1794+ row_wise_args = np.argsort(-psi_mat, axis=1)
1795+
17671796 parameter_bounds = []
1768- for i in range(len(nom_val)):
1769- non_decreasing_factor_row = sorted(psi_mat[i], reverse=True)
1770- # deviation = sum_j=1^floor(beta F) {psi_if_j} + (beta F - floor(beta F)) psi_{if_{betaF +1}}
1771- # because indexing starts at 0, we adjust the limit on the sum and the final factor contribution
1772- if beta_F - floor_beta_F == 0:
1773- deviation = sum(non_decreasing_factor_row[j] for j in range(floor_beta_F - 1))
1797+ for idx, orig_val in enumerate(self.origin):
1798+ # number nonnegative values in row
1799+ M = len(psi_mat[idx][psi_mat[idx] >= 0])
1800+
1801+ # argsort psi matrix row in descending order
1802+ sorted_psi_row_args = row_wise_args[idx]
1803+ sorted_psi_row = psi_mat[idx, sorted_psi_row_args]
1804+
1805+ # now evaluate max deviation from origin
1806+ # (depends on number nonneg entries and critical point type)
1807+ if M > crit_pt_type:
1808+ max_deviation = (
1809+ sorted_psi_row[:crit_pt_type].sum()
1810+ + beta_F_fill_in * sorted_psi_row[crit_pt_type]
1811+ - sorted_psi_row[crit_pt_type + 1:].sum()
1812+ )
1813+ elif M < F - crit_pt_type:
1814+ max_deviation = (
1815+ sorted_psi_row[:F - crit_pt_type - 1].sum()
1816+ - beta_F_fill_in * sorted_psi_row[F - crit_pt_type - 1]
1817+ - sorted_psi_row[F - crit_pt_type:].sum()
1818+ )
17741819 else:
1775- deviation = sum(non_decreasing_factor_row[j] for j in range(floor_beta_F - 1)) + (
1776- beta_F - floor_beta_F) * psi_mat[i][floor_beta_F]
1777- lb = nom_val[i] - deviation
1778- ub = nom_val[i] + deviation
1779- if lb > ub:
1780- raise AttributeError("The computed lower bound on uncertain parameters must be less than or equal to the upper bound.")
1781- parameter_bounds.append((lb, ub))
1820+ max_deviation = (
1821+ sorted_psi_row[:M].sum()
1822+ - sorted_psi_row[M:].sum()
1823+ )
1824+
1825+ # finally, evaluate the bounds for this dimension
1826+ parameter_bounds.append(
1827+ (orig_val - max_deviation, orig_val + max_deviation),
1828+ )
1829+
17821830 return parameter_bounds
17831831
17841832 def set_as_constraint(self, uncertain_params, **kwargs):
0 commit comments