@@ -348,6 +348,38 @@ def parameter_bounds(self):
348
348
"""
349
349
raise NotImplementedError
350
350
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
+
351
383
def is_bounded (self , config ):
352
384
"""
353
385
Determine whether the uncertainty set is bounded.
@@ -374,35 +406,36 @@ def is_bounded(self, config):
374
406
This method is invoked during the validation step of a PyROS
375
407
solver call.
376
408
"""
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
+
404
432
if not check_optimal_termination (res ):
405
433
return False
434
+
435
+ # ensure sense is minimize when done, deactivate
436
+ obj .sense = minimize
437
+ obj .deactivate ()
438
+
406
439
return True
407
440
408
441
def is_nonempty (self , config ):
@@ -1572,10 +1605,9 @@ class FactorModelSet(UncertaintySet):
1572
1605
Natural number representing the dimensionality of the
1573
1606
space to which the set projects.
1574
1607
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.
1579
1611
beta : numeric type
1580
1612
Real value between 0 and 1 specifying the fraction of the
1581
1613
independent factors that can simultaneously attain
@@ -1661,8 +1693,7 @@ def psi_mat(self):
1661
1693
(N, `number_of_factors`) numpy.ndarray : Matrix designating each
1662
1694
uncertain parameter's contribution to each factor. Each row is
1663
1695
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.
1666
1697
"""
1667
1698
return self ._psi_mat
1668
1699
@@ -1697,13 +1728,6 @@ def psi_mat(self, val):
1697
1728
"one nonzero entry"
1698
1729
)
1699
1730
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
-
1707
1731
self ._psi_mat = psi_mat_arr
1708
1732
1709
1733
@property
@@ -1758,27 +1782,51 @@ def parameter_bounds(self):
1758
1782
the uncertain parameter bounds for the corresponding set
1759
1783
dimension.
1760
1784
"""
1761
- nom_val = self .origin
1785
+ F = self .number_of_factors
1762
1786
psi_mat = self .psi_mat
1763
1787
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
+
1767
1796
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
+ )
1774
1819
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
+
1782
1830
return parameter_bounds
1783
1831
1784
1832
def set_as_constraint (self , uncertain_params , ** kwargs ):
0 commit comments