diff --git a/pyomo/contrib/piecewise/tests/common_tests.py b/pyomo/contrib/piecewise/tests/common_tests.py index 14ce6f5ae12..523853ec3c5 100644 --- a/pyomo/contrib/piecewise/tests/common_tests.py +++ b/pyomo/contrib/piecewise/tests/common_tests.py @@ -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 @@ -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) diff --git a/pyomo/contrib/piecewise/tests/models.py b/pyomo/contrib/piecewise/tests/models.py index dc96e052c38..7e2efa2728e 100644 --- a/pyomo/contrib/piecewise/tests/models.py +++ b/pyomo/contrib/piecewise/tests/models.py @@ -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 @@ -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 diff --git a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py index 9ffa35fcfb3..575e9c02728 100644 --- a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py +++ b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py @@ -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') diff --git a/pyomo/contrib/piecewise/tests/test_incremental.py b/pyomo/contrib/piecewise/tests/test_incremental.py index e6e8d659d1f..015cbb02261 100644 --- a/pyomo/contrib/piecewise/tests/test_incremental.py +++ b/pyomo/contrib/piecewise/tests/test_incremental.py @@ -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): diff --git a/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py index 57d15dc3398..73b9e997fcf 100644 --- a/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py @@ -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): diff --git a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py index b863cb0267b..a867ad3538e 100644 --- a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py @@ -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') diff --git a/pyomo/contrib/piecewise/tests/test_outer_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_outer_repn_gdp.py index 902ec21abc5..d00a01a32c0 100644 --- a/pyomo/contrib/piecewise/tests/test_outer_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_outer_repn_gdp.py @@ -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') diff --git a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py index 5bfc564fbad..117fd186d25 100644 --- a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py @@ -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( diff --git a/pyomo/contrib/piecewise/tests/test_reduced_inner_repn.py b/pyomo/contrib/piecewise/tests/test_reduced_inner_repn.py index 670386a4df7..46f13f42871 100644 --- a/pyomo/contrib/piecewise/tests/test_reduced_inner_repn.py +++ b/pyomo/contrib/piecewise/tests/test_reduced_inner_repn.py @@ -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): diff --git a/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py b/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py index 8350051309c..7133caf78b5 100644 --- a/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py @@ -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[ diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index fb90b328280..8cca6f53bc3 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -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, diff --git a/pyomo/contrib/piecewise/transform/inner_representation_gdp.py b/pyomo/contrib/piecewise/transform/inner_representation_gdp.py index a7d365224d7..1e5f93a853d 100644 --- a/pyomo/contrib/piecewise/transform/inner_representation_gdp.py +++ b/pyomo/contrib/piecewise/transform/inner_representation_gdp.py @@ -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) ] diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 3128c8002ef..b4b51a70697 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -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[ diff --git a/pyomo/contrib/piecewise/transform/outer_representation_gdp.py b/pyomo/contrib/piecewise/transform/outer_representation_gdp.py index 1229ce5993b..1bbad6a28ad 100644 --- a/pyomo/contrib/piecewise/transform/outer_representation_gdp.py +++ b/pyomo/contrib/piecewise/transform/outer_representation_gdp.py @@ -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) ] diff --git a/pyomo/contrib/piecewise/transform/piecewise_linear_transformation_base.py b/pyomo/contrib/piecewise/transform/piecewise_linear_transformation_base.py index 080eff5aa79..09d3e8ff9d8 100644 --- a/pyomo/contrib/piecewise/transform/piecewise_linear_transformation_base.py +++ b/pyomo/contrib/piecewise/transform/piecewise_linear_transformation_base.py @@ -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'" ) diff --git a/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py b/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py index 3ff3395c9fc..49b8efdc9d9 100644 --- a/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py +++ b/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py @@ -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) ]