Skip to content

Commit f1fd189

Browse files
Add tests for discrete uncertainty second-stage equality reformulation
1 parent fe2da21 commit f1fd189

File tree

2 files changed

+271
-4
lines changed

2 files changed

+271
-4
lines changed

Diff for: pyomo/contrib/pyros/tests/test_grcs.py

+183
Original file line numberDiff line numberDiff line change
@@ -1998,6 +1998,189 @@ def test_pyros_certain_params_ipopt_degrees_of_freedom(self):
19981998
self.assertEqual(m.x.value, 1)
19991999

20002000

2001+
@unittest.skipUnless(baron_available, "BARON not available")
2002+
class TestReformulateSecondStageEqualitiesDiscrete(unittest.TestCase):
2003+
"""
2004+
Test behavior of PyROS solver when the uncertainty set is
2005+
discrete and there are second-stage
2006+
equality constraints that are state-variable independent,
2007+
and therefore, subject to reformulation.
2008+
"""
2009+
2010+
def build_single_stage_model(self):
2011+
m = ConcreteModel()
2012+
m.x = Var(range(3), bounds=[-2, 2], initialize=0)
2013+
m.q = Param(range(3), initialize=0, mutable=True)
2014+
m.c = Param(range(3), initialize={0: 1, 1: 0, 2: 1})
2015+
m.obj = Objective(expr=sum(m.x[i] * m.c[i] for i in m.x), sense=maximize)
2016+
# when uncertainty set is discrete, the
2017+
# preprocessor should write out this constraint for
2018+
# each scenario as a first-stage constraint
2019+
m.xq_con = Constraint(expr=sum(m.x[i] * m.q[i] for i in m.x) == 0)
2020+
return m
2021+
2022+
def build_two_stage_model(self):
2023+
m = ConcreteModel()
2024+
m.x = Var(bounds=[None, None], initialize=0)
2025+
m.z = Var(bounds=[-2, 2], initialize=0)
2026+
m.q = Param(initialize=2, mutable=True)
2027+
m.obj = Objective(expr=m.x + m.z, sense=maximize)
2028+
# when uncertainty set is discrete, the
2029+
# preprocessor should write out this constraint for
2030+
# each scenario as a first-stage constraint
2031+
m.xz_con = Constraint(expr=m.x + m.q * m.z == 0)
2032+
return m
2033+
2034+
def test_single_stage_discrete_set_fullrank(self):
2035+
m = self.build_single_stage_model()
2036+
uncertainty_set = DiscreteScenarioSet(
2037+
# reformulating second-stage equality for these scenarios
2038+
# should result in first-stage equalities finally being
2039+
# (full-column-rank matrix) @ (x) == 0
2040+
# so x=0 is sole robust feasible solution
2041+
scenarios=[
2042+
[0] * len(m.q),
2043+
[1] * len(m.q),
2044+
list(range(1, len(m.q) + 1)),
2045+
[(idx + 1) ** 2 for idx in m.q],
2046+
]
2047+
)
2048+
baron = SolverFactory("baron")
2049+
res = SolverFactory("pyros").solve(
2050+
model=m,
2051+
first_stage_variables=m.x,
2052+
second_stage_variables=[],
2053+
uncertain_params=m.q,
2054+
uncertainty_set=uncertainty_set,
2055+
local_solver=baron,
2056+
global_solver=baron,
2057+
solve_master_globally=True,
2058+
bypass_local_separation=True,
2059+
objective_focus="worst_case",
2060+
)
2061+
self.assertEqual(
2062+
res.pyros_termination_condition, pyrosTerminationCondition.robust_optimal
2063+
)
2064+
self.assertEqual(res.iterations, 1)
2065+
self.assertAlmostEqual(res.final_objective_value, 0, places=4)
2066+
self.assertAlmostEqual(m.x[0].value, 0, places=4)
2067+
self.assertAlmostEqual(m.x[1].value, 0, places=4)
2068+
self.assertAlmostEqual(m.x[2].value, 0, places=4)
2069+
2070+
def test_single_stage_discrete_set_rank2(self):
2071+
m = self.build_single_stage_model()
2072+
uncertainty_set = DiscreteScenarioSet(
2073+
# reformulating second-stage equality for these scenarios
2074+
# should make the optimal solution unique
2075+
scenarios=[[0] * len(m.q), [1] * len(m.q), [(idx + 1) ** 2 for idx in m.q]]
2076+
)
2077+
baron = SolverFactory("baron")
2078+
res = SolverFactory("pyros").solve(
2079+
model=m,
2080+
first_stage_variables=m.x,
2081+
second_stage_variables=[],
2082+
uncertain_params=m.q,
2083+
uncertainty_set=uncertainty_set,
2084+
local_solver=baron,
2085+
global_solver=baron,
2086+
solve_master_globally=True,
2087+
bypass_local_separation=True,
2088+
objective_focus="worst_case",
2089+
)
2090+
self.assertEqual(
2091+
res.pyros_termination_condition, pyrosTerminationCondition.robust_optimal
2092+
)
2093+
self.assertEqual(res.iterations, 1)
2094+
self.assertAlmostEqual(res.final_objective_value, 2, places=4)
2095+
# optimal solution is unique
2096+
self.assertAlmostEqual(m.x[0].value, 5 / 4, places=4)
2097+
self.assertAlmostEqual(m.x[1].value, -2, places=4)
2098+
self.assertAlmostEqual(m.x[2].value, 3 / 4, places=4)
2099+
2100+
def test_single_stage_discrete_set_rank1(self):
2101+
m = self.build_single_stage_model()
2102+
uncertainty_set = DiscreteScenarioSet(
2103+
scenarios=[[0] * len(m.q), [2] * len(m.q), [3] * len(m.q)]
2104+
)
2105+
baron = SolverFactory("baron")
2106+
res = SolverFactory("pyros").solve(
2107+
model=m,
2108+
first_stage_variables=m.x,
2109+
second_stage_variables=[],
2110+
uncertain_params=m.q,
2111+
uncertainty_set=uncertainty_set,
2112+
local_solver=baron,
2113+
global_solver=baron,
2114+
solve_master_globally=True,
2115+
bypass_local_separation=True,
2116+
objective_focus="worst_case",
2117+
)
2118+
self.assertEqual(
2119+
res.pyros_termination_condition, pyrosTerminationCondition.robust_optimal
2120+
)
2121+
self.assertEqual(res.iterations, 1)
2122+
self.assertAlmostEqual(res.final_objective_value, 2, places=4)
2123+
# subject to these scenarios, the optimal solution is non-unique,
2124+
# but should satisfy this check
2125+
self.assertAlmostEqual(m.x[1].value, -2, places=4)
2126+
2127+
def test_two_stage_discrete_set_rank2_affine_dr(self):
2128+
m = self.build_two_stage_model()
2129+
uncertainty_set = DiscreteScenarioSet([[2], [3]])
2130+
baron = SolverFactory("baron")
2131+
res = SolverFactory("pyros").solve(
2132+
model=m,
2133+
first_stage_variables=m.x,
2134+
second_stage_variables=m.z,
2135+
uncertain_params=m.q,
2136+
uncertainty_set=uncertainty_set,
2137+
local_solver=baron,
2138+
global_solver=baron,
2139+
solve_master_globally=True,
2140+
bypass_local_separation=True,
2141+
decision_rule_order=1,
2142+
objective_focus="worst_case",
2143+
)
2144+
self.assertEqual(
2145+
res.pyros_termination_condition, pyrosTerminationCondition.robust_optimal
2146+
)
2147+
self.assertEqual(res.iterations, 1)
2148+
self.assertAlmostEqual(res.final_objective_value, 0, places=4)
2149+
# note: this solution is suboptimal (in the context of nonstatic DRs),
2150+
# but follows from the current efficiency for DRs
2151+
# (i.e. in first iteration, static DRs required)
2152+
self.assertAlmostEqual(m.x.value, 0, places=4)
2153+
self.assertAlmostEqual(m.z.value, 0, places=4)
2154+
2155+
def test_two_stage_discrete_set_fullrank_affine_dr(self):
2156+
m = self.build_two_stage_model()
2157+
uncertainty_set = DiscreteScenarioSet([[2], [3], [4]])
2158+
baron = SolverFactory("baron")
2159+
res = SolverFactory("pyros").solve(
2160+
model=m,
2161+
first_stage_variables=m.x,
2162+
second_stage_variables=m.z,
2163+
uncertain_params=m.q,
2164+
uncertainty_set=uncertainty_set,
2165+
local_solver=baron,
2166+
global_solver=baron,
2167+
solve_master_globally=True,
2168+
bypass_local_separation=True,
2169+
decision_rule_order=1,
2170+
objective_focus="worst_case",
2171+
)
2172+
self.assertEqual(
2173+
res.pyros_termination_condition, pyrosTerminationCondition.robust_optimal
2174+
)
2175+
self.assertEqual(res.iterations, 1)
2176+
self.assertAlmostEqual(res.final_objective_value, 0, places=4)
2177+
# the second-stage equalities are a full rank linear system
2178+
# in x and the DR variables, with RHS 0, so all
2179+
# variables must be 0
2180+
self.assertAlmostEqual(m.x.value, 0, places=4)
2181+
self.assertAlmostEqual(m.z.value, 0, places=4)
2182+
2183+
20012184
@unittest.skipUnless(ipopt_available, "IPOPT not available.")
20022185
class TestPyROSVarsAsUncertainParams(unittest.TestCase):
20032186
"""

Diff for: pyomo/contrib/pyros/tests/test_preprocessor.py

+88-4
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,17 @@
3636
Block,
3737
)
3838
from pyomo.core.base.set_types import NonNegativeReals, NonPositiveReals, Reals
39-
from pyomo.core.expr import LinearExpression, log, sin, exp, RangedExpression
39+
from pyomo.core.expr import (
40+
LinearExpression,
41+
log,
42+
sin,
43+
exp,
44+
RangedExpression,
45+
SumExpression,
46+
)
4047
from pyomo.core.expr.compare import assertExpressionsEqual
4148

42-
from pyomo.contrib.pyros.uncertainty_sets import BoxSet
49+
from pyomo.contrib.pyros.uncertainty_sets import BoxSet, DiscreteScenarioSet
4350
from pyomo.contrib.pyros.util import (
4451
ModelData,
4552
ObjectiveType,
@@ -1942,13 +1949,13 @@ class TestReformulateStateVarIndependentEqCons(unittest.TestCase):
19421949
state variable-independent second-stage equality constraints.
19431950
"""
19441951

1945-
def setup_test_model_data(self):
1952+
def setup_test_model_data(self, uncertainty_set=None):
19461953
"""
19471954
Set up simple test model for testing the reformulation
19481955
routine.
19491956
"""
19501957
model_data = Bunch()
1951-
model_data.config = Bunch()
1958+
model_data.config = Bunch(uncertainty_set=uncertainty_set or BoxSet([[0, 1]]))
19521959
model_data.working_model = working_model = ConcreteModel()
19531960
model_data.working_model.user_model = m = Block()
19541961

@@ -2155,6 +2162,83 @@ def test_reformulate_nonlinear_state_var_independent_eq_con(self):
21552162
model_data.separation_priority_order["reform_upper_bound_from_eq_con"], 0
21562163
)
21572164

2165+
def test_reformulate_equality_cons_discrete_set(self):
2166+
"""
2167+
Test routine for reformulating state-variable-independent
2168+
second-stage equality constraints under scenario-based
2169+
uncertainty works as expected.
2170+
"""
2171+
model_data = self.setup_test_model_data(
2172+
uncertainty_set=DiscreteScenarioSet([[0], [0.7]])
2173+
)
2174+
model_data.separation_priority_order = dict()
2175+
2176+
model_data.config.decision_rule_order = 1
2177+
model_data.config.progress_logger = logging.getLogger(
2178+
self.test_reformulate_nonlinear_state_var_independent_eq_con.__name__
2179+
)
2180+
model_data.config.progress_logger.setLevel(logging.DEBUG)
2181+
2182+
add_decision_rule_variables(model_data)
2183+
add_decision_rule_constraints(model_data)
2184+
2185+
ep = model_data.working_model.effective_var_partitioning
2186+
model_data.working_model.all_nonadjustable_variables = list(
2187+
ep.first_stage_variables
2188+
+ list(model_data.working_model.first_stage.decision_rule_var_0.values())
2189+
)
2190+
2191+
wm = model_data.working_model
2192+
m = model_data.working_model.user_model
2193+
wm.second_stage.equality_cons["eq_con_2"].set_value(m.u * (m.x1 - 1) == 0)
2194+
2195+
robust_infeasible = reformulate_state_var_independent_eq_cons(model_data)
2196+
2197+
# check constraint partitioning updated as expected
2198+
self.assertFalse(robust_infeasible)
2199+
self.assertFalse(wm.second_stage.equality_cons)
2200+
self.assertEqual(len(wm.second_stage.inequality_cons), 1)
2201+
self.assertEqual(len(wm.first_stage.equality_cons), 4)
2202+
2203+
self.assertTrue(wm.first_stage.equality_cons["scenario_0_eq_con"].active)
2204+
self.assertTrue(wm.first_stage.equality_cons["scenario_1_eq_con"].active)
2205+
self.assertTrue(wm.first_stage.equality_cons["scenario_0_eq_con_2"].active)
2206+
self.assertTrue(wm.first_stage.equality_cons["scenario_1_eq_con_2"].active)
2207+
2208+
# expressions for the new opposing inequalities
2209+
# and coefficient matching constraint
2210+
dr_vars = list(wm.first_stage.decision_rule_vars[0].values())
2211+
assertExpressionsEqual(
2212+
self,
2213+
wm.first_stage.equality_cons["scenario_0_eq_con"].expr,
2214+
(
2215+
0 * SumExpression([dr_vars[0] + 0 * dr_vars[1], -1])
2216+
+ 0 * (m.x1**3 + 0.5)
2217+
- ((0 * m.u_cert * m.x1) * (dr_vars[0] + 0 * dr_vars[1]))
2218+
== (0 * (m.x1 + 2))
2219+
),
2220+
)
2221+
assertExpressionsEqual(
2222+
self,
2223+
wm.first_stage.equality_cons["scenario_1_eq_con"].expr,
2224+
(
2225+
(0.7**2) * SumExpression([dr_vars[0] + 0.7 * dr_vars[1], -1])
2226+
+ 0.7 * (m.x1**3 + 0.5)
2227+
- ((5 * 0.7 * m.u_cert * m.x1) * (dr_vars[0] + 0.7 * dr_vars[1]))
2228+
== (-0.7 * (m.x1 + 2))
2229+
),
2230+
)
2231+
assertExpressionsEqual(
2232+
self,
2233+
wm.first_stage.equality_cons["scenario_0_eq_con_2"].expr,
2234+
0 * (m.x1 - 1) == 0,
2235+
)
2236+
assertExpressionsEqual(
2237+
self,
2238+
wm.first_stage.equality_cons["scenario_1_eq_con_2"].expr,
2239+
0.7 * (m.x1 - 1) == 0,
2240+
)
2241+
21582242
def test_coefficient_matching_robust_infeasible_proof(self):
21592243
"""
21602244
Test coefficient matching detects robust infeasibility

0 commit comments

Comments
 (0)