Skip to content

Commit 9c37423

Browse files
Enable nonadjustable multistage DRs
1 parent f368d35 commit 9c37423

File tree

3 files changed

+125
-3
lines changed

3 files changed

+125
-3
lines changed

pyomo/contrib/pyros/config.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,35 @@ def uncertain_param_data_validator(uncertain_obj):
154154
)
155155

156156

157+
class MultistageInputDataStandardizer(object):
158+
"""
159+
Standardizer for modeling objects representing multi-stage
160+
variables or uncertain parameters. Standard form is a
161+
list of lists of `cdatatype` objects.
162+
"""
163+
def __init__(self, ctype, cdatatype):
164+
self.ctype = ctype
165+
self.cdatatype = cdatatype
166+
167+
def __call__(self, obj):
168+
if isinstance(obj, self.ctype):
169+
return [list(obj.values())]
170+
if isinstance(obj, self.cdatatype):
171+
return [[obj]]
172+
if isinstance(obj, (list, tuple)):
173+
base_std = InputDataStandardizer(self.ctype, self.cdatatype)
174+
if all(isinstance(itm, (list, tuple)) for itm in obj):
175+
ans = list()
176+
for item in obj:
177+
ans.append(base_std(item))
178+
else:
179+
ans = [base_std(obj)]
180+
else:
181+
raise TypeError("Not supported")
182+
183+
return ans
184+
185+
157186
class InputDataStandardizer:
158187
"""
159188
Domain validator for an object that is castable to
@@ -544,6 +573,26 @@ def pyros_config():
544573
),
545574
)
546575

576+
# for multi-stage
577+
CONFIG.declare("nested_second_stage_variables", ConfigValue(
578+
default=[],
579+
domain=MultistageInputDataStandardizer(Var, VarData),
580+
description=(
581+
"A two-dimensional list for extending the second-stage "
582+
"variables to multi-stage variables. Each list specifies "
583+
"the variables adjusted in each stage."
584+
)
585+
))
586+
CONFIG.declare("nested_uncertain_params", ConfigValue(
587+
default=[],
588+
domain=MultistageInputDataStandardizer(Param, ParamData),
589+
description=(
590+
"A two-dimensional list for extending the uncertain parameters "
591+
"variables to multi-stage context. Each list specifies "
592+
"the parameters realized by the corresponding stage."
593+
)
594+
))
595+
547596
# ================================================
548597
# === Required User Inputs
549598
# ================================================

pyomo/contrib/pyros/pyros.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,20 +424,45 @@ def solve(
424424
config, user_var_partitioning = self._resolve_and_validate_pyros_args(
425425
model, **kwds
426426
)
427+
428+
# for multi-stage: confirm there's a match
429+
config.nested_second_stage_variables = second_stage_variables
430+
config.nested_uncertain_params = uncertain_params
431+
matching_stages = (
432+
len(config.nested_uncertain_params)
433+
== len(config.nested_second_stage_variables)
434+
)
435+
if config.second_stage_variables and not matching_stages:
436+
raise ValueError(
437+
"Number of stages mismatch: "
438+
f"there are {len(config.nested_second_stage_variables)} "
439+
"stages of second-stage variables, but "
440+
f"{len(config.nested_uncertain_params)} "
441+
"stages of uncertain parameters."
442+
)
443+
427444
self._log_config_user_values(
428445
logger=config.progress_logger,
429446
config=config,
430447
exclude_options=(
431448
self._DEFAULT_CONFIG_USER_OPTIONS
432449
+ ["nominal_uncertain_param_vals"]
433450
* (not nominal_param_vals_in_kwds)
451+
# for multi-stage
452+
+ ["nested_second_stage_variables"]
453+
+ ["nested_uncertain_params"]
434454
),
435455
level=logging.INFO,
436456
)
437457
self._log_config(
438458
logger=config.progress_logger,
439459
config=config,
440-
exclude_options=None,
460+
exclude_options=(
461+
self._DEFAULT_CONFIG_USER_OPTIONS
462+
# for multi-stage
463+
+ ["nested_second_stage_variables"]
464+
+ ["nested_uncertain_params"]
465+
),
441466
level=logging.DEBUG,
442467
)
443468
model_data.config = config

pyomo/contrib/pyros/util.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1933,6 +1933,11 @@ def setup_working_model(model_data, user_var_partitioning):
19331933
orig_temp_util_block.user_var_partitioning = VariablePartitioning(
19341934
**user_var_partitioning._asdict()
19351935
)
1936+
# for multi-stage
1937+
orig_temp_util_block.nested_second_stage_variables = (
1938+
config.nested_second_stage_variables
1939+
)
1940+
orig_temp_util_block.nested_uncertain_params = config.nested_uncertain_params
19361941

19371942
# now set up working model
19381943
model_data.working_model = working_model = ConcreteModel()
@@ -1961,6 +1966,14 @@ def setup_working_model(model_data, user_var_partitioning):
19611966
**working_temp_util_block.user_var_partitioning._asdict()
19621967
)
19631968

1969+
# for multi-stage
1970+
working_model.nested_second_stage_variables = (
1971+
working_temp_util_block.nested_second_stage_variables
1972+
)
1973+
working_model.nested_uncertain_params = (
1974+
working_temp_util_block.nested_uncertain_params
1975+
)
1976+
19641977
# we are done with the util blocks
19651978
delattr(original_model, temp_util_block_attr_name)
19661979
delattr(working_model.user_model, temp_util_block_attr_name)
@@ -2017,6 +2030,22 @@ def setup_working_model(model_data, user_var_partitioning):
20172030
else:
20182031
working_model.original_active_inequality_cons.append(con)
20192032

2033+
# for multi-stage: map adjustable vars to stages
2034+
# and also uncertain params (assuming all were originally Params)
2035+
working_model.adj_var_to_stage_map = ComponentMap()
2036+
working_model.param_to_stage_map = ComponentMap()
2037+
ss_vars_param_zip = zip(
2038+
working_model.nested_second_stage_variables,
2039+
working_model.nested_uncertain_params,
2040+
)
2041+
for stage_idx, (varlist, paramlist) in enumerate(ss_vars_param_zip):
2042+
working_model.adj_var_to_stage_map.update(
2043+
(var, stage_idx) for var in varlist
2044+
)
2045+
working_model.param_to_stage_map.update(
2046+
(param, stage_idx) for param in paramlist
2047+
)
2048+
20202049

20212050
def standardize_inequality_constraints(model_data):
20222051
"""
@@ -3140,6 +3169,7 @@ def add_decision_rule_constraints(model_data):
31403169
indexed_dr_var_list = model_data.working_model.first_stage.decision_rule_vars
31413170
uncertain_params = model_data.working_model.effective_uncertain_params
31423171
degree = config.decision_rule_order
3172+
param_to_stage_map = model_data.working_model.param_to_stage_map
31433173

31443174
model_data.working_model.second_stage.decision_rule_eqns = decision_rule_eqns = (
31453175
Constraint(range(len(effective_second_stage_vars)))
@@ -3162,6 +3192,9 @@ def add_decision_rule_constraints(model_data):
31623192
ComponentMap()
31633193
)
31643194

3195+
# for multi-stage
3196+
model_data.working_model.dr_var_to_stage_map = dr_var_to_stage_map = ComponentMap()
3197+
31653198
# set up uncertain parameter combinations for
31663199
# construction of the monomials of the DR expressions
31673200
monomial_param_combos = []
@@ -3192,6 +3225,10 @@ def add_decision_rule_constraints(model_data):
31923225
# associated monomial with respect to the uncertain params
31933226
dr_var_to_exponent_map[dr_var] = len(param_combo)
31943227
dr_var_to_param_combo_map[dr_var] = param_combo
3228+
# for multi-stage
3229+
dr_var_to_stage_map[dr_var] = max(
3230+
param_to_stage_map[param] for param in param_combo
3231+
) if param_combo else 0
31953232

31963233
# declare constraint on model
31973234
decision_rule_eqns[idx] = dr_expression - eff_ss_var == 0
@@ -3213,14 +3250,25 @@ def enforce_dr_degree(working_blk, config, degree):
32133250
degree : int
32143251
Degree of the DR polynomials that is to be enforced.
32153252
"""
3216-
for indexed_dr_var in working_blk.first_stage.decision_rule_vars:
3253+
# account for multi-stage setup
3254+
dr_var_to_stage_map = working_blk.dr_var_to_stage_map
3255+
adj_var_to_stage_map = working_blk.adj_var_to_stage_map
3256+
for eff_ss_var, indexed_dr_var in working_blk.eff_ss_var_to_dr_var_map.items():
32173257
for dr_var in indexed_dr_var.values():
32183258
dr_var_degree = working_blk.dr_var_to_exponent_map[dr_var]
3219-
if dr_var_degree > degree:
3259+
dr_var_stage = dr_var_to_stage_map[dr_var]
3260+
ss_var_stage = adj_var_to_stage_map[eff_ss_var]
3261+
if (dr_var_degree > degree) or (dr_var_stage > ss_var_stage):
32203262
dr_var.fix(0)
32213263
else:
32223264
dr_var.unfix()
32233265

3266+
# config.progress_logger.debug(
3267+
# f"{dr_var.name}, SS var {eff_ss_var.name}, "
3268+
# f"{dr_var_degree=}, {ss_var_stage=}, {dr_var_stage=}, "
3269+
# f"{dr_var.fixed=}, DR > SS stage {dr_var_stage > ss_var_stage}"
3270+
# )
3271+
32243272

32253273
def load_final_solution(model_data, master_soln, original_user_var_partitioning):
32263274
"""

0 commit comments

Comments
 (0)