Skip to content

Commit 39b315e

Browse files
authored
Merge pull request #3990 from emma58/linear-pw-linear
contrib.piecewise: Add special case for single-segment "piecewise" linear functions
2 parents 9e9902c + ac7a115 commit 39b315e

16 files changed

Lines changed: 125 additions & 9 deletions

pyomo/contrib/piecewise/tests/common_tests.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
import pyomo.contrib.piecewise.tests.models as models
1111
from pyomo.core import Var
1212
from pyomo.core.base import TransformationFactory
13-
from pyomo.environ import value
13+
from pyomo.core.expr.compare import assertExpressionsEqual
14+
from pyomo.environ import value, Expression
1415
from pyomo.gdp import Disjunct, Disjunction
1516

1617

@@ -85,3 +86,25 @@ def check_descend_into_expressions_objective_target(test, transformation, m=None
8586
test.check_pw_log(m)
8687
# And check that the paraboloid was *not* transformed.
8788
test.assertIsNone(m.pw_paraboloid.get_transformation_var(m.paraboloid_expr))
89+
90+
91+
def check_single_segment_no_disjunction(test, transformation, m=None):
92+
if m is None:
93+
m = models.make_single_segment_log_x_model()
94+
transform = TransformationFactory(transformation)
95+
transform.apply_to(m)
96+
97+
# A single segment doesn't require making a discrete choice, so this
98+
# should not have created any GDP components.
99+
test.assertEqual(len(list(m.component_data_objects(Disjunct))), 0)
100+
test.assertEqual(len(list(m.component_data_objects(Disjunction))), 0)
101+
# Nor should it have introduced any new Vars (such as a substitute_var
102+
# or an indicator_var)--m.x should be the only Var in the model.
103+
model_vars = list(m.component_data_objects(Var))
104+
test.assertEqual(len(model_vars), 1)
105+
test.assertIs(model_vars[0], m.x)
106+
107+
z = m.pw_log.get_transformation_var(m.log_expr)
108+
test.assertIsInstance(z, Expression)
109+
assertExpressionsEqual(test, z.expr, m.f1(m.x), places=7)
110+
test.assertIs(m.log_expr.expr, z)

pyomo/contrib/piecewise/tests/models.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def f3(x):
4545
m.x1 = Var(bounds=(0, 3))
4646
m.x2 = Var(bounds=(1, 7))
4747

48-
## apprximates paraboloid x1**2 + x2**2
48+
## approximates paraboloid x1**2 + x2**2
4949
def g1(x1, x2):
5050
return 3 * x1 + 5 * x2 - 4
5151

@@ -72,3 +72,19 @@ def c_rule(m, i):
7272
m.indexed_c = Constraint([0, 1], rule=c_rule)
7373

7474
return m
75+
76+
77+
def make_single_segment_log_x_model():
78+
m = ConcreteModel()
79+
m.x = Var(bounds=(1, 10))
80+
m.pw_log = PiecewiseLinearFunction(points=[1, 10], function=log)
81+
82+
def f1(x):
83+
return (log(10) / 9) * x - log(10) / 9
84+
85+
m.f1 = f1
86+
87+
m.log_expr = m.pw_log(m.x)
88+
m.obj = Objective(expr=m.log_expr)
89+
90+
return m

pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,11 @@ def test_descend_into_expressions_objective_target(self):
263263
self, 'contrib.piecewise.disaggregated_logarithmic'
264264
)
265265

266+
def test_single_segment_no_disjunction(self):
267+
ct.check_single_segment_no_disjunction(
268+
self, 'contrib.piecewise.disaggregated_logarithmic'
269+
)
270+
266271
# Check solution of the log(x) model
267272
@unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available')
268273
@unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license')

pyomo/contrib/piecewise/tests/test_incremental.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@ def test_descend_into_expressions_objective_target(self):
174174
make_log_x_model(simplices=self.ordered_simplices),
175175
)
176176

177+
def test_single_segment_no_disjunction(self):
178+
ct.check_single_segment_no_disjunction(self, 'contrib.piecewise.incremental')
179+
177180
@unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available')
178181
@unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license')
179182
def test_solve_log_model(self):

pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ def test_descend_into_expressions_objective_target(self):
106106
self, 'contrib.piecewise.inner_repn_gdp'
107107
)
108108

109+
def test_single_segment_no_disjunction(self):
110+
ct.check_single_segment_no_disjunction(self, 'contrib.piecewise.inner_repn_gdp')
111+
109112
@unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available')
110113
@unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license')
111114
def test_solve_disaggregated_convex_combo_model(self):

pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@ def test_descend_into_expressions_objective_target(self):
122122
self, 'contrib.piecewise.nested_inner_repn_gdp'
123123
)
124124

125+
def test_single_segment_no_disjunction(self):
126+
ct.check_single_segment_no_disjunction(
127+
self, 'contrib.piecewise.nested_inner_repn_gdp'
128+
)
129+
125130
# Check the solution of the log(x) model
126131
@unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available')
127132
@unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license')

pyomo/contrib/piecewise/tests/test_outer_repn_gdp.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,6 @@ def test_solve_multiple_choice_model(self):
178178
SolverFactory('gurobi').solve(m)
179179

180180
ct.check_log_x_model_soln(self, m)
181+
182+
def test_single_segment_no_disjunction(self):
183+
ct.check_single_segment_no_disjunction(self, 'contrib.piecewise.outer_repn_gdp')

pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,20 @@ def test_pw_linear_approx_of_ln_x_points(self):
102102
m.pw = PiecewiseLinearFunction(points=[1, 3, 6, 10], function=m.f)
103103
self.check_ln_x_approx(m.pw, m.x)
104104

105+
def test_pw_linear_approx_of_ln_x_single_segment(self):
106+
m = self.make_ln_x_model()
107+
m.pw = PiecewiseLinearFunction(points=[1, 10], function=m.f)
108+
109+
self.assertEqual(len(m.pw._simplices), 1)
110+
self.assertEqual(len(m.pw._linear_functions), 1)
111+
self.assertEqual(m.pw._simplices[0], (0, 1))
112+
113+
slope = log(10) / 9
114+
intercept = -log(10) / 9
115+
assertExpressionsEqual(
116+
self, m.pw._linear_functions[0](m.x), slope * m.x + intercept, places=7
117+
)
118+
105119
def test_pw_linear_approx_of_ln_x_linear_funcs(self):
106120
m = self.make_ln_x_model()
107121
m.pw = PiecewiseLinearFunction(

pyomo/contrib/piecewise/tests/test_reduced_inner_repn.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,11 @@ def test_descend_into_expressions_objective_target(self):
245245
self, 'contrib.piecewise.reduced_inner_repn_gdp'
246246
)
247247

248+
def test_single_segment_no_disjunction(self):
249+
ct.check_single_segment_no_disjunction(
250+
self, 'contrib.piecewise.reduced_inner_repn_gdp'
251+
)
252+
248253
@unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available')
249254
@unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license')
250255
def test_solve_convex_combo_model(self):

pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ class DisaggregatedLogarithmicMIPTransformation(PiecewiseLinearTransformationBas
4444

4545
# Implement to use PiecewiseLinearTransformationBase. This function returns the Var
4646
# that replaces the transformed piecewise linear expr
47-
def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block):
47+
def _transform_multiple_segment_pw_linear_expr(
48+
self, pw_expr, pw_linear_func, transformation_block
49+
):
4850
# Get a new Block for our transformation in transformation_block.transformed_functions,
4951
# which is a Block(Any). This is where we will put our new components.
5052
transBlock = transformation_block.transformed_functions[

0 commit comments

Comments
 (0)