Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion pyomo/contrib/piecewise/tests/common_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
import pyomo.contrib.piecewise.tests.models as models
from pyomo.core import Var
from pyomo.core.base import TransformationFactory
from pyomo.environ import value
from pyomo.core.expr.compare import assertExpressionsEqual
from pyomo.environ import value, Expression
from pyomo.gdp import Disjunct, Disjunction


Expand Down Expand Up @@ -85,3 +86,25 @@ def check_descend_into_expressions_objective_target(test, transformation, m=None
test.check_pw_log(m)
# And check that the paraboloid was *not* transformed.
test.assertIsNone(m.pw_paraboloid.get_transformation_var(m.paraboloid_expr))


def check_single_segment_no_disjunction(test, transformation, m=None):
if m is None:
m = models.make_single_segment_log_x_model()
transform = TransformationFactory(transformation)
transform.apply_to(m)

# A single segment doesn't require making a discrete choice, so this
# should not have created any GDP components.
test.assertEqual(len(list(m.component_data_objects(Disjunct))), 0)
test.assertEqual(len(list(m.component_data_objects(Disjunction))), 0)
# Nor should it have introduced any new Vars (such as a substitute_var
# or an indicator_var)--m.x should be the only Var in the model.
model_vars = list(m.component_data_objects(Var))
test.assertEqual(len(model_vars), 1)
test.assertIs(model_vars[0], m.x)

z = m.pw_log.get_transformation_var(m.log_expr)
test.assertIsInstance(z, Expression)
assertExpressionsEqual(test, z.expr, m.f1(m.x), places=7)
test.assertIs(m.log_expr.expr, z)
18 changes: 17 additions & 1 deletion pyomo/contrib/piecewise/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def f3(x):
m.x1 = Var(bounds=(0, 3))
m.x2 = Var(bounds=(1, 7))

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

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

return m


def make_single_segment_log_x_model():
m = ConcreteModel()
m.x = Var(bounds=(1, 10))
m.pw_log = PiecewiseLinearFunction(points=[1, 10], function=log)

def f1(x):
return (log(10) / 9) * x - log(10) / 9

m.f1 = f1

m.log_expr = m.pw_log(m.x)
m.obj = Objective(expr=m.log_expr)

return m
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,11 @@ def test_descend_into_expressions_objective_target(self):
self, 'contrib.piecewise.disaggregated_logarithmic'
)

def test_single_segment_no_disjunction(self):
ct.check_single_segment_no_disjunction(
self, 'contrib.piecewise.disaggregated_logarithmic'
)

# Check solution of the log(x) model
@unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available')
@unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license')
Expand Down
3 changes: 3 additions & 0 deletions pyomo/contrib/piecewise/tests/test_incremental.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ def test_descend_into_expressions_objective_target(self):
make_log_x_model(simplices=self.ordered_simplices),
)

def test_single_segment_no_disjunction(self):
ct.check_single_segment_no_disjunction(self, 'contrib.piecewise.incremental')

@unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available')
@unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license')
def test_solve_log_model(self):
Expand Down
3 changes: 3 additions & 0 deletions pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ def test_descend_into_expressions_objective_target(self):
self, 'contrib.piecewise.inner_repn_gdp'
)

def test_single_segment_no_disjunction(self):
ct.check_single_segment_no_disjunction(self, 'contrib.piecewise.inner_repn_gdp')

@unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available')
@unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license')
def test_solve_disaggregated_convex_combo_model(self):
Expand Down
5 changes: 5 additions & 0 deletions pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ def test_descend_into_expressions_objective_target(self):
self, 'contrib.piecewise.nested_inner_repn_gdp'
)

def test_single_segment_no_disjunction(self):
ct.check_single_segment_no_disjunction(
self, 'contrib.piecewise.nested_inner_repn_gdp'
)

# Check the solution of the log(x) model
@unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available')
@unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license')
Expand Down
3 changes: 3 additions & 0 deletions pyomo/contrib/piecewise/tests/test_outer_repn_gdp.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,6 @@ def test_solve_multiple_choice_model(self):
SolverFactory('gurobi').solve(m)

ct.check_log_x_model_soln(self, m)

def test_single_segment_no_disjunction(self):
ct.check_single_segment_no_disjunction(self, 'contrib.piecewise.outer_repn_gdp')
14 changes: 14 additions & 0 deletions pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ def test_pw_linear_approx_of_ln_x_points(self):
m.pw = PiecewiseLinearFunction(points=[1, 3, 6, 10], function=m.f)
self.check_ln_x_approx(m.pw, m.x)

def test_pw_linear_approx_of_ln_x_single_segment(self):
m = self.make_ln_x_model()
m.pw = PiecewiseLinearFunction(points=[1, 10], function=m.f)

self.assertEqual(len(m.pw._simplices), 1)
self.assertEqual(len(m.pw._linear_functions), 1)
self.assertEqual(m.pw._simplices[0], (0, 1))

slope = log(10) / 9
intercept = -log(10) / 9
assertExpressionsEqual(
self, m.pw._linear_functions[0](m.x), slope * m.x + intercept, places=7
)

def test_pw_linear_approx_of_ln_x_linear_funcs(self):
m = self.make_ln_x_model()
m.pw = PiecewiseLinearFunction(
Expand Down
5 changes: 5 additions & 0 deletions pyomo/contrib/piecewise/tests/test_reduced_inner_repn.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,11 @@ def test_descend_into_expressions_objective_target(self):
self, 'contrib.piecewise.reduced_inner_repn_gdp'
)

def test_single_segment_no_disjunction(self):
ct.check_single_segment_no_disjunction(
self, 'contrib.piecewise.reduced_inner_repn_gdp'
)

@unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available')
@unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license')
def test_solve_convex_combo_model(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ class DisaggregatedLogarithmicMIPTransformation(PiecewiseLinearTransformationBas

# Implement to use PiecewiseLinearTransformationBase. This function returns the Var
# that replaces the transformed piecewise linear expr
def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block):
def _transform_multiple_segment_pw_linear_expr(
self, pw_expr, pw_linear_func, transformation_block
):
# Get a new Block for our transformation in transformation_block.transformed_functions,
# which is a Block(Any). This is where we will put our new components.
transBlock = transformation_block.transformed_functions[
Expand Down
4 changes: 3 additions & 1 deletion pyomo/contrib/piecewise/transform/incremental.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ class IncrementalMIPTransformation(PiecewiseLinearTransformationBase):

# Implement to use PiecewiseLinearTransformationBase. This function returns the Var
# that replaces the transformed piecewise linear expr
def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block):
def _transform_multiple_segment_pw_linear_expr(
self, pw_expr, pw_linear_func, transformation_block
):
if pw_linear_func.triangulation not in (
Triangulation.OrderedJ1,
Triangulation.AssumeValid,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ class InnerRepresentationGDPTransformation(PiecewiseLinearTransformationBase):
CONFIG = PiecewiseLinearTransformationBase.CONFIG()
_transformation_name = 'pw_linear_inner_repn'

def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block):
def _transform_multiple_segment_pw_linear_expr(
self, pw_expr, pw_linear_func, transformation_block
):
transBlock = transformation_block.transformed_functions[
len(transformation_block.transformed_functions)
]
Expand Down
4 changes: 3 additions & 1 deletion pyomo/contrib/piecewise/transform/nested_inner_repn.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ class NestedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationBa

# Implement to use PiecewiseLinearTransformationBase. This function returns the Var
# that replaces the transformed piecewise linear expr
def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block):
def _transform_multiple_segment_pw_linear_expr(
self, pw_expr, pw_linear_func, transformation_block
):
# Get a new Block() in transformation_block.transformed_functions, which
# is a Block(Any)
transBlock = transformation_block.transformed_functions[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ class OuterRepresentationGDPTransformation(PiecewiseLinearTransformationBase):
CONFIG = PiecewiseLinearTransformationBase.CONFIG()
_transformation_name = 'pw_linear_outer_repn'

def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block):
def _transform_multiple_segment_pw_linear_expr(
self, pw_expr, pw_linear_func, transformation_block
):
transBlock = transformation_block.transformed_functions[
len(transformation_block.transformed_functions)
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,33 @@ def _transform_objective(self, objective, descend_into_expressions):
visitor.walk_expression((o.expr, o, 0))

def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block):
if len(pw_linear_func._simplices) == 1:
return self._transform_single_segment_pw_linear_expr(
pw_expr, pw_linear_func, transformation_block
)
return self._transform_multiple_segment_pw_linear_expr(
pw_expr, pw_linear_func, transformation_block
)

def _transform_single_segment_pw_linear_expr(
self, pw_expr, pw_linear_func, transformation_block
):
# There's no actual choice to make between pieces, so we don't need
# any discrete variables: We can just substitute the (single) linear
# function directly in place of the PiecewiseLinearExpression.
transBlock = transformation_block.transformed_functions[
len(transformation_block.transformed_functions)
]
linear_func = pw_linear_func._linear_functions[0]
transBlock.substitute_expr = Expression(expr=linear_func(*pw_expr.args))
pw_linear_func.map_transformation_var(pw_expr, transBlock.substitute_expr)

return transBlock.substitute_expr

def _transform_multiple_segment_pw_linear_expr(
self, pw_expr, pw_linear_func, transformation_block
):
raise DeveloperError(
"Derived class failed to implement '_transform_pw_linear_expr'"
"Derived class failed to implement "
"'_transform_multiple_segment_pw_linear_expr'"
)
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ class ReducedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationB
CONFIG = PiecewiseLinearTransformationBase.CONFIG()
_transformation_name = 'pw_linear_reduced_inner_repn'

def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block):
def _transform_multiple_segment_pw_linear_expr(
self, pw_expr, pw_linear_func, transformation_block
):
transBlock = transformation_block.transformed_functions[
len(transformation_block.transformed_functions)
]
Expand Down
Loading