Skip to content

Commit 12bcecf

Browse files
authored
Merge pull request #3036 from sadavis1/logarithmic_pwlinear
Piecewise Linear transformations: Logarithmic and nested inner GDP representation
2 parents a44a832 + 8ebd61c commit 12bcecf

11 files changed

+926
-77
lines changed

pyomo/contrib/piecewise/__init__.py

+6
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,9 @@
3333
from pyomo.contrib.piecewise.transform.convex_combination import (
3434
ConvexCombinationTransformation,
3535
)
36+
from pyomo.contrib.piecewise.transform.nested_inner_repn import (
37+
NestedInnerRepresentationGDPTransformation,
38+
)
39+
from pyomo.contrib.piecewise.transform.disaggregated_logarithmic import (
40+
DisaggregatedLogarithmicMIPTransformation,
41+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# ___________________________________________________________________________
2+
#
3+
# Pyomo: Python Optimization Modeling Objects
4+
# Copyright (c) 2008-2024
5+
# National Technology and Engineering Solutions of Sandia, LLC
6+
# Under the terms of Contract DE-NA0003525 with National Technology and
7+
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
8+
# rights in this software.
9+
# This software is distributed under the 3-clause BSD License.
10+
# ___________________________________________________________________________
11+
12+
from pyomo.core import Var
13+
from pyomo.core.base import Constraint
14+
from pyomo.core.expr.compare import assertExpressionsEqual
15+
16+
# This file contains check methods shared between GDP inner representation-based
17+
# transformations. Currently, those are the inner_representation_gdp and
18+
# nested_inner_repn_gdp transformations, since each have disjuncts with the
19+
# same structure.
20+
21+
22+
# Check one disjunct from the log model for proper contents
23+
def check_log_disjunct(test, d, pts, f, substitute_var, x):
24+
test.assertEqual(len(d.component_map(Constraint)), 3)
25+
# lambdas and indicator_var
26+
test.assertEqual(len(d.component_map(Var)), 2)
27+
test.assertIsInstance(d.lambdas, Var)
28+
test.assertEqual(len(d.lambdas), 2)
29+
for lamb in d.lambdas.values():
30+
test.assertEqual(lamb.lb, 0)
31+
test.assertEqual(lamb.ub, 1)
32+
test.assertIsInstance(d.convex_combo, Constraint)
33+
assertExpressionsEqual(test, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] == 1)
34+
test.assertIsInstance(d.set_substitute, Constraint)
35+
assertExpressionsEqual(
36+
test, d.set_substitute.expr, substitute_var == f(x), places=7
37+
)
38+
test.assertIsInstance(d.linear_combo, Constraint)
39+
test.assertEqual(len(d.linear_combo), 1)
40+
assertExpressionsEqual(
41+
test, d.linear_combo[0].expr, x == pts[0] * d.lambdas[0] + pts[1] * d.lambdas[1]
42+
)
43+
44+
45+
# Check one disjunct from the paraboloid model for proper contents.
46+
def check_paraboloid_disjunct(test, d, pts, f, substitute_var, x1, x2):
47+
test.assertEqual(len(d.component_map(Constraint)), 3)
48+
# lambdas and indicator_var
49+
test.assertEqual(len(d.component_map(Var)), 2)
50+
test.assertIsInstance(d.lambdas, Var)
51+
test.assertEqual(len(d.lambdas), 3)
52+
for lamb in d.lambdas.values():
53+
test.assertEqual(lamb.lb, 0)
54+
test.assertEqual(lamb.ub, 1)
55+
test.assertIsInstance(d.convex_combo, Constraint)
56+
assertExpressionsEqual(
57+
test, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] + d.lambdas[2] == 1
58+
)
59+
test.assertIsInstance(d.set_substitute, Constraint)
60+
assertExpressionsEqual(
61+
test, d.set_substitute.expr, substitute_var == f(x1, x2), places=7
62+
)
63+
test.assertIsInstance(d.linear_combo, Constraint)
64+
test.assertEqual(len(d.linear_combo), 2)
65+
assertExpressionsEqual(
66+
test,
67+
d.linear_combo[0].expr,
68+
x1
69+
== pts[0][0] * d.lambdas[0]
70+
+ pts[1][0] * d.lambdas[1]
71+
+ pts[2][0] * d.lambdas[2],
72+
)
73+
assertExpressionsEqual(
74+
test,
75+
d.linear_combo[1].expr,
76+
x2
77+
== pts[0][1] * d.lambdas[0]
78+
+ pts[1][1] * d.lambdas[1]
79+
+ pts[2][1] * d.lambdas[2],
80+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
# ___________________________________________________________________________
2+
#
3+
# Pyomo: Python Optimization Modeling Objects
4+
# Copyright (c) 2008-2024
5+
# National Technology and Engineering Solutions of Sandia, LLC
6+
# Under the terms of Contract DE-NA0003525 with National Technology and
7+
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
8+
# rights in this software.
9+
# This software is distributed under the 3-clause BSD License.
10+
# ___________________________________________________________________________
11+
12+
import pyomo.common.unittest as unittest
13+
from pyomo.contrib.piecewise.tests import models
14+
import pyomo.contrib.piecewise.tests.common_tests as ct
15+
from pyomo.core.base import TransformationFactory
16+
from pyomo.environ import SolverFactory, Var, Constraint
17+
from pyomo.core.expr.compare import assertExpressionsEqual
18+
19+
20+
class TestTransformPiecewiseModelToNestedInnerRepnMIP(unittest.TestCase):
21+
def check_pw_log(self, m):
22+
z = m.pw_log.get_transformation_var(m.log_expr)
23+
self.assertIsInstance(z, Var)
24+
# Now we can use those Vars to check on what the transformation created
25+
log_block = z.parent_block()
26+
27+
# We should have three Vars, two of which are indexed, and five
28+
# Constraints, three of which are indexed
29+
30+
self.assertEqual(len(log_block.component_map(Var)), 3)
31+
self.assertEqual(len(log_block.component_map(Constraint)), 5)
32+
33+
# Constants
34+
simplex_count = 3
35+
log_simplex_count = 2
36+
simplex_point_count = 2
37+
38+
# Substitute var
39+
self.assertIsInstance(log_block.substitute_var, Var)
40+
self.assertIs(m.obj.expr.expr, log_block.substitute_var)
41+
# Binaries
42+
self.assertIsInstance(log_block.binaries, Var)
43+
self.assertEqual(len(log_block.binaries), log_simplex_count)
44+
# Lambdas
45+
self.assertIsInstance(log_block.lambdas, Var)
46+
self.assertEqual(len(log_block.lambdas), simplex_count * simplex_point_count)
47+
for l in log_block.lambdas.values():
48+
self.assertEqual(l.lb, 0)
49+
self.assertEqual(l.ub, 1)
50+
51+
# Convex combo constraint
52+
self.assertIsInstance(log_block.convex_combo, Constraint)
53+
assertExpressionsEqual(
54+
self,
55+
log_block.convex_combo.expr,
56+
log_block.lambdas[0, 0]
57+
+ log_block.lambdas[0, 1]
58+
+ log_block.lambdas[1, 0]
59+
+ log_block.lambdas[1, 1]
60+
+ log_block.lambdas[2, 0]
61+
+ log_block.lambdas[2, 1]
62+
== 1,
63+
)
64+
65+
# Set substitute constraint
66+
self.assertIsInstance(log_block.set_substitute, Constraint)
67+
assertExpressionsEqual(
68+
self,
69+
log_block.set_substitute.expr,
70+
log_block.substitute_var
71+
== log_block.lambdas[0, 0] * m.f1(1)
72+
+ log_block.lambdas[1, 0] * m.f2(3)
73+
+ log_block.lambdas[2, 0] * m.f3(6)
74+
+ log_block.lambdas[0, 1] * m.f1(3)
75+
+ log_block.lambdas[1, 1] * m.f2(6)
76+
+ log_block.lambdas[2, 1] * m.f3(10),
77+
places=7,
78+
)
79+
80+
# x constraint
81+
self.assertIsInstance(log_block.x_constraint, Constraint)
82+
# one-dimensional case, so there is only one x variable here
83+
self.assertEqual(len(log_block.x_constraint), 1)
84+
assertExpressionsEqual(
85+
self,
86+
log_block.x_constraint[0].expr,
87+
m.x
88+
== 1 * log_block.lambdas[0, 0]
89+
+ 3 * log_block.lambdas[0, 1]
90+
+ 3 * log_block.lambdas[1, 0]
91+
+ 6 * log_block.lambdas[1, 1]
92+
+ 6 * log_block.lambdas[2, 0]
93+
+ 10 * log_block.lambdas[2, 1],
94+
)
95+
96+
# simplex choice 1 constraint enables lambdas when binaries are on
97+
self.assertEqual(len(log_block.simplex_choice_1), log_simplex_count)
98+
assertExpressionsEqual(
99+
self,
100+
log_block.simplex_choice_1[0].expr,
101+
log_block.lambdas[2, 0] + log_block.lambdas[2, 1] <= log_block.binaries[0],
102+
)
103+
assertExpressionsEqual(
104+
self,
105+
log_block.simplex_choice_1[1].expr,
106+
log_block.lambdas[1, 0] + log_block.lambdas[1, 1] <= log_block.binaries[1],
107+
)
108+
# simplex choice 2 constraint enables lambdas when binaries are off
109+
self.assertEqual(len(log_block.simplex_choice_2), log_simplex_count)
110+
assertExpressionsEqual(
111+
self,
112+
log_block.simplex_choice_2[0].expr,
113+
log_block.lambdas[0, 0]
114+
+ log_block.lambdas[0, 1]
115+
+ log_block.lambdas[1, 0]
116+
+ log_block.lambdas[1, 1]
117+
<= 1 - log_block.binaries[0],
118+
)
119+
assertExpressionsEqual(
120+
self,
121+
log_block.simplex_choice_2[1].expr,
122+
log_block.lambdas[0, 0]
123+
+ log_block.lambdas[0, 1]
124+
+ log_block.lambdas[2, 0]
125+
+ log_block.lambdas[2, 1]
126+
<= 1 - log_block.binaries[1],
127+
)
128+
129+
def check_pw_paraboloid(self, m):
130+
# This is a little larger, but at least test that the right numbers of
131+
# everything are created
132+
z = m.pw_paraboloid.get_transformation_var(m.paraboloid_expr)
133+
self.assertIsInstance(z, Var)
134+
paraboloid_block = z.parent_block()
135+
136+
self.assertEqual(len(paraboloid_block.component_map(Var)), 3)
137+
self.assertEqual(len(paraboloid_block.component_map(Constraint)), 5)
138+
139+
# Constants
140+
simplex_count = 4
141+
log_simplex_count = 2
142+
simplex_point_count = 3
143+
144+
# Substitute var
145+
self.assertIsInstance(paraboloid_block.substitute_var, Var)
146+
# Binaries
147+
self.assertIsInstance(paraboloid_block.binaries, Var)
148+
self.assertEqual(len(paraboloid_block.binaries), log_simplex_count)
149+
# Lambdas
150+
self.assertIsInstance(paraboloid_block.lambdas, Var)
151+
self.assertEqual(
152+
len(paraboloid_block.lambdas), simplex_count * simplex_point_count
153+
)
154+
for l in paraboloid_block.lambdas.values():
155+
self.assertEqual(l.lb, 0)
156+
self.assertEqual(l.ub, 1)
157+
158+
# Convex combo constraint
159+
self.assertIsInstance(paraboloid_block.convex_combo, Constraint)
160+
assertExpressionsEqual(
161+
self,
162+
paraboloid_block.convex_combo.expr,
163+
paraboloid_block.lambdas[0, 0]
164+
+ paraboloid_block.lambdas[0, 1]
165+
+ paraboloid_block.lambdas[0, 2]
166+
+ paraboloid_block.lambdas[1, 0]
167+
+ paraboloid_block.lambdas[1, 1]
168+
+ paraboloid_block.lambdas[1, 2]
169+
+ paraboloid_block.lambdas[2, 0]
170+
+ paraboloid_block.lambdas[2, 1]
171+
+ paraboloid_block.lambdas[2, 2]
172+
+ paraboloid_block.lambdas[3, 0]
173+
+ paraboloid_block.lambdas[3, 1]
174+
+ paraboloid_block.lambdas[3, 2]
175+
== 1,
176+
)
177+
178+
# Set substitute constraint
179+
self.assertIsInstance(paraboloid_block.set_substitute, Constraint)
180+
assertExpressionsEqual(
181+
self,
182+
paraboloid_block.set_substitute.expr,
183+
paraboloid_block.substitute_var
184+
== paraboloid_block.lambdas[0, 0] * m.g1(0, 1)
185+
+ paraboloid_block.lambdas[1, 0] * m.g1(0, 1)
186+
+ paraboloid_block.lambdas[2, 0] * m.g2(3, 4)
187+
+ paraboloid_block.lambdas[3, 0] * m.g2(0, 7)
188+
+ paraboloid_block.lambdas[0, 1] * m.g1(0, 4)
189+
+ paraboloid_block.lambdas[1, 1] * m.g1(3, 4)
190+
+ paraboloid_block.lambdas[2, 1] * m.g2(3, 7)
191+
+ paraboloid_block.lambdas[3, 1] * m.g2(0, 4)
192+
+ paraboloid_block.lambdas[0, 2] * m.g1(3, 4)
193+
+ paraboloid_block.lambdas[1, 2] * m.g1(3, 1)
194+
+ paraboloid_block.lambdas[2, 2] * m.g2(0, 7)
195+
+ paraboloid_block.lambdas[3, 2] * m.g2(3, 4),
196+
places=7,
197+
)
198+
199+
# x constraint
200+
self.assertIsInstance(paraboloid_block.x_constraint, Constraint)
201+
# Here we have two x variables
202+
self.assertEqual(len(paraboloid_block.x_constraint), 2)
203+
assertExpressionsEqual(
204+
self,
205+
paraboloid_block.x_constraint[0].expr,
206+
m.x1
207+
== 0 * paraboloid_block.lambdas[0, 0]
208+
+ 0 * paraboloid_block.lambdas[0, 1]
209+
+ 3 * paraboloid_block.lambdas[0, 2]
210+
+ 0 * paraboloid_block.lambdas[1, 0]
211+
+ 3 * paraboloid_block.lambdas[1, 1]
212+
+ 3 * paraboloid_block.lambdas[1, 2]
213+
+ 3 * paraboloid_block.lambdas[2, 0]
214+
+ 3 * paraboloid_block.lambdas[2, 1]
215+
+ 0 * paraboloid_block.lambdas[2, 2]
216+
+ 0 * paraboloid_block.lambdas[3, 0]
217+
+ 0 * paraboloid_block.lambdas[3, 1]
218+
+ 3 * paraboloid_block.lambdas[3, 2],
219+
)
220+
assertExpressionsEqual(
221+
self,
222+
paraboloid_block.x_constraint[1].expr,
223+
m.x2
224+
== 1 * paraboloid_block.lambdas[0, 0]
225+
+ 4 * paraboloid_block.lambdas[0, 1]
226+
+ 4 * paraboloid_block.lambdas[0, 2]
227+
+ 1 * paraboloid_block.lambdas[1, 0]
228+
+ 4 * paraboloid_block.lambdas[1, 1]
229+
+ 1 * paraboloid_block.lambdas[1, 2]
230+
+ 4 * paraboloid_block.lambdas[2, 0]
231+
+ 7 * paraboloid_block.lambdas[2, 1]
232+
+ 7 * paraboloid_block.lambdas[2, 2]
233+
+ 7 * paraboloid_block.lambdas[3, 0]
234+
+ 4 * paraboloid_block.lambdas[3, 1]
235+
+ 4 * paraboloid_block.lambdas[3, 2],
236+
)
237+
238+
# The choices will get long, so let's just assert we have enough
239+
self.assertEqual(len(paraboloid_block.simplex_choice_1), log_simplex_count)
240+
self.assertEqual(len(paraboloid_block.simplex_choice_2), log_simplex_count)
241+
242+
# Test methods using the common_tests.py code.
243+
def test_transformation_do_not_descend(self):
244+
ct.check_transformation_do_not_descend(
245+
self, 'contrib.piecewise.disaggregated_logarithmic'
246+
)
247+
248+
def test_transformation_PiecewiseLinearFunction_targets(self):
249+
ct.check_transformation_PiecewiseLinearFunction_targets(
250+
self, 'contrib.piecewise.disaggregated_logarithmic'
251+
)
252+
253+
def test_descend_into_expressions(self):
254+
ct.check_descend_into_expressions(
255+
self, 'contrib.piecewise.disaggregated_logarithmic'
256+
)
257+
258+
def test_descend_into_expressions_constraint_target(self):
259+
ct.check_descend_into_expressions_constraint_target(
260+
self, 'contrib.piecewise.disaggregated_logarithmic'
261+
)
262+
263+
def test_descend_into_expressions_objective_target(self):
264+
ct.check_descend_into_expressions_objective_target(
265+
self, 'contrib.piecewise.disaggregated_logarithmic'
266+
)
267+
268+
# Check solution of the log(x) model
269+
@unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available')
270+
@unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license')
271+
def test_solve_log_model(self):
272+
m = models.make_log_x_model()
273+
TransformationFactory("contrib.piecewise.disaggregated_logarithmic").apply_to(m)
274+
SolverFactory("gurobi").solve(m)
275+
ct.check_log_x_model_soln(self, m)

0 commit comments

Comments
 (0)