Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Piecewise Linear transformations: Logarithmic and nested inner GDP representation #3036

Merged
merged 40 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
df19b6b
add initial work on nested inner repn pw to gdp transformation. ident…
sadavis1 Jul 17, 2023
3e600ce
wip: working on some other pw linear representations
sadavis1 Aug 1, 2023
c862204
properly handle one-simplex case instead of ignoring
sadavis1 Aug 23, 2023
8b1997f
nested inner repn: remove non-working variable identification code
sadavis1 Aug 23, 2023
b1cc434
fix errors
sadavis1 Aug 23, 2023
601abcb
disaggregated logarithmic reworking
sadavis1 Oct 12, 2023
ee616bf
remove printf debugging
sadavis1 Oct 12, 2023
dbecedd
fix strange reverse indexing
sadavis1 Oct 12, 2023
8ff5d5c
minor changes and add basic test for disaggregated log
sadavis1 Nov 9, 2023
6f4de26
apply black
sadavis1 Nov 9, 2023
c11fa70
rename: PiecewiseLinearToGDP->PiecewiseLinearTransformationBase, sinc…
sadavis1 Nov 14, 2023
f34bf30
apply black
sadavis1 Nov 14, 2023
7acb187
fix typos and add title for reference
sadavis1 Nov 14, 2023
6548b62
fix incorrect imports and comments
sadavis1 Nov 14, 2023
ae953e8
register transformations in __init__.py so they don't need to be impo…
sadavis1 Nov 14, 2023
63af626
remove unused (for now) imports
sadavis1 Nov 14, 2023
acfa476
do the reference properly
sadavis1 Nov 14, 2023
81960f1
stop using `self` unnecessarily
sadavis1 Dec 14, 2023
c7975cd
rename file to match refactored class name
sadavis1 Dec 14, 2023
526305c
minor refactors
sadavis1 Dec 15, 2023
d6b9f88
Fix needless quadratic loops
sadavis1 Dec 22, 2023
1248806
fix scoping error
sadavis1 Dec 22, 2023
18821c7
proper testing for nested_inner_repn_gdp
sadavis1 Dec 22, 2023
d1a92e0
apply black
sadavis1 Dec 22, 2023
14aa6fc
fix initialization bug
sadavis1 Dec 22, 2023
98ed119
Fix up tests for disaggreggated logarithmic
sadavis1 Feb 15, 2024
1312af5
fix name typo
sadavis1 Feb 15, 2024
b9a3e44
satisfy black?
sadavis1 Feb 15, 2024
133d0ff
update and add copyright notices
sadavis1 Feb 20, 2024
423dbf8
rename and cleanup, try 2
sadavis1 Feb 22, 2024
290be25
redo cleanup I managed to lose
sadavis1 Feb 22, 2024
dd6ed3e
fix again something strange I did
sadavis1 Feb 22, 2024
289915d
move duplicated methods into common file for inner-repn-GDP based tra…
sadavis1 Feb 22, 2024
035cf9c
replace an error check that should never happen with a comment
sadavis1 Mar 22, 2024
dbe9e85
Merge branch 'main' into logarithmic_pwlinear
emma58 Apr 2, 2024
e954043
Merge branch 'main' into logarithmic_pwlinear
blnicho Apr 2, 2024
1cd0bd2
Merge branch 'logarithmic_pwlinear' of github.com:sadavis1/pyomo into…
sadavis1 Mar 22, 2024
6132077
Merge branch 'main' into logarithmic_pwlinear
emma58 Apr 17, 2024
b74995f
Merge branch 'main' into logarithmic_pwlinear
jsiirola May 8, 2024
8ebd61c
Generate binary vectors without going through strings
jsiirola May 8, 2024
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
6 changes: 6 additions & 0 deletions pyomo/contrib/piecewise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@
from pyomo.contrib.piecewise.transform.convex_combination import (
ConvexCombinationTransformation,
)
from pyomo.contrib.piecewise.transform.nested_inner_repn import (
NestedInnerRepresentationGDPTransformation,
)
from pyomo.contrib.piecewise.transform.disaggregated_logarithmic import (
DisaggregatedLogarithmicMIPTransformation,
)
80 changes: 80 additions & 0 deletions pyomo/contrib/piecewise/tests/common_inner_repn_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2024
# National Technology and Engineering Solutions of Sandia, LLC
# Under the terms of Contract DE-NA0003525 with National Technology and
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
# rights in this software.
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________

from pyomo.core import Var
from pyomo.core.base import Constraint
from pyomo.core.expr.compare import assertExpressionsEqual

# This file contains check methods shared between GDP inner representation-based
# transformations. Currently, those are the inner_representation_gdp and
# nested_inner_repn_gdp transformations, since each have disjuncts with the
# same structure.


# Check one disjunct from the log model for proper contents
def check_log_disjunct(test, d, pts, f, substitute_var, x):
test.assertEqual(len(d.component_map(Constraint)), 3)
# lambdas and indicator_var
test.assertEqual(len(d.component_map(Var)), 2)
test.assertIsInstance(d.lambdas, Var)
test.assertEqual(len(d.lambdas), 2)
for lamb in d.lambdas.values():
test.assertEqual(lamb.lb, 0)
test.assertEqual(lamb.ub, 1)
test.assertIsInstance(d.convex_combo, Constraint)
assertExpressionsEqual(test, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] == 1)
test.assertIsInstance(d.set_substitute, Constraint)
assertExpressionsEqual(
test, d.set_substitute.expr, substitute_var == f(x), places=7
)
test.assertIsInstance(d.linear_combo, Constraint)
test.assertEqual(len(d.linear_combo), 1)
assertExpressionsEqual(
test, d.linear_combo[0].expr, x == pts[0] * d.lambdas[0] + pts[1] * d.lambdas[1]
)


# Check one disjunct from the paraboloid model for proper contents.
def check_paraboloid_disjunct(test, d, pts, f, substitute_var, x1, x2):
test.assertEqual(len(d.component_map(Constraint)), 3)
# lambdas and indicator_var
test.assertEqual(len(d.component_map(Var)), 2)
test.assertIsInstance(d.lambdas, Var)
test.assertEqual(len(d.lambdas), 3)
for lamb in d.lambdas.values():
test.assertEqual(lamb.lb, 0)
test.assertEqual(lamb.ub, 1)
test.assertIsInstance(d.convex_combo, Constraint)
assertExpressionsEqual(
test, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] + d.lambdas[2] == 1
)
test.assertIsInstance(d.set_substitute, Constraint)
assertExpressionsEqual(
test, d.set_substitute.expr, substitute_var == f(x1, x2), places=7
)
test.assertIsInstance(d.linear_combo, Constraint)
test.assertEqual(len(d.linear_combo), 2)
assertExpressionsEqual(
test,
d.linear_combo[0].expr,
x1
== pts[0][0] * d.lambdas[0]
+ pts[1][0] * d.lambdas[1]
+ pts[2][0] * d.lambdas[2],
)
assertExpressionsEqual(
test,
d.linear_combo[1].expr,
x2
== pts[0][1] * d.lambdas[0]
+ pts[1][1] * d.lambdas[1]
+ pts[2][1] * d.lambdas[2],
)
275 changes: 275 additions & 0 deletions pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2024
# National Technology and Engineering Solutions of Sandia, LLC
# Under the terms of Contract DE-NA0003525 with National Technology and
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
# rights in this software.
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________

import pyomo.common.unittest as unittest
from pyomo.contrib.piecewise.tests import models
import pyomo.contrib.piecewise.tests.common_tests as ct
from pyomo.core.base import TransformationFactory
from pyomo.environ import SolverFactory, Var, Constraint
from pyomo.core.expr.compare import assertExpressionsEqual


class TestTransformPiecewiseModelToNestedInnerRepnMIP(unittest.TestCase):
def check_pw_log(self, m):
z = m.pw_log.get_transformation_var(m.log_expr)
self.assertIsInstance(z, Var)
# Now we can use those Vars to check on what the transformation created
log_block = z.parent_block()

# We should have three Vars, two of which are indexed, and five
# Constraints, three of which are indexed

self.assertEqual(len(log_block.component_map(Var)), 3)
self.assertEqual(len(log_block.component_map(Constraint)), 5)

# Constants
simplex_count = 3
log_simplex_count = 2
simplex_point_count = 2

# Substitute var
self.assertIsInstance(log_block.substitute_var, Var)
self.assertIs(m.obj.expr.expr, log_block.substitute_var)
# Binaries
self.assertIsInstance(log_block.binaries, Var)
self.assertEqual(len(log_block.binaries), log_simplex_count)
# Lambdas
self.assertIsInstance(log_block.lambdas, Var)
self.assertEqual(len(log_block.lambdas), simplex_count * simplex_point_count)
for l in log_block.lambdas.values():
self.assertEqual(l.lb, 0)
self.assertEqual(l.ub, 1)

# Convex combo constraint
self.assertIsInstance(log_block.convex_combo, Constraint)
assertExpressionsEqual(
self,
log_block.convex_combo.expr,
log_block.lambdas[0, 0]
+ log_block.lambdas[0, 1]
+ log_block.lambdas[1, 0]
+ log_block.lambdas[1, 1]
+ log_block.lambdas[2, 0]
+ log_block.lambdas[2, 1]
== 1,
)

# Set substitute constraint
self.assertIsInstance(log_block.set_substitute, Constraint)
assertExpressionsEqual(
self,
log_block.set_substitute.expr,
log_block.substitute_var
== log_block.lambdas[0, 0] * m.f1(1)
+ log_block.lambdas[1, 0] * m.f2(3)
+ log_block.lambdas[2, 0] * m.f3(6)
+ log_block.lambdas[0, 1] * m.f1(3)
+ log_block.lambdas[1, 1] * m.f2(6)
+ log_block.lambdas[2, 1] * m.f3(10),
places=7,
)

# x constraint
self.assertIsInstance(log_block.x_constraint, Constraint)
# one-dimensional case, so there is only one x variable here
self.assertEqual(len(log_block.x_constraint), 1)
assertExpressionsEqual(
self,
log_block.x_constraint[0].expr,
m.x
== 1 * log_block.lambdas[0, 0]
+ 3 * log_block.lambdas[0, 1]
+ 3 * log_block.lambdas[1, 0]
+ 6 * log_block.lambdas[1, 1]
+ 6 * log_block.lambdas[2, 0]
+ 10 * log_block.lambdas[2, 1],
)

# simplex choice 1 constraint enables lambdas when binaries are on
self.assertEqual(len(log_block.simplex_choice_1), log_simplex_count)
assertExpressionsEqual(
self,
log_block.simplex_choice_1[0].expr,
log_block.lambdas[2, 0] + log_block.lambdas[2, 1] <= log_block.binaries[0],
)
assertExpressionsEqual(
self,
log_block.simplex_choice_1[1].expr,
log_block.lambdas[1, 0] + log_block.lambdas[1, 1] <= log_block.binaries[1],
)
# simplex choice 2 constraint enables lambdas when binaries are off
self.assertEqual(len(log_block.simplex_choice_2), log_simplex_count)
assertExpressionsEqual(
self,
log_block.simplex_choice_2[0].expr,
log_block.lambdas[0, 0]
+ log_block.lambdas[0, 1]
+ log_block.lambdas[1, 0]
+ log_block.lambdas[1, 1]
<= 1 - log_block.binaries[0],
)
assertExpressionsEqual(
self,
log_block.simplex_choice_2[1].expr,
log_block.lambdas[0, 0]
+ log_block.lambdas[0, 1]
+ log_block.lambdas[2, 0]
+ log_block.lambdas[2, 1]
<= 1 - log_block.binaries[1],
)

def check_pw_paraboloid(self, m):
# This is a little larger, but at least test that the right numbers of
# everything are created
z = m.pw_paraboloid.get_transformation_var(m.paraboloid_expr)
self.assertIsInstance(z, Var)
paraboloid_block = z.parent_block()

self.assertEqual(len(paraboloid_block.component_map(Var)), 3)
self.assertEqual(len(paraboloid_block.component_map(Constraint)), 5)

# Constants
simplex_count = 4
log_simplex_count = 2
simplex_point_count = 3

# Substitute var
self.assertIsInstance(paraboloid_block.substitute_var, Var)
# Binaries
self.assertIsInstance(paraboloid_block.binaries, Var)
self.assertEqual(len(paraboloid_block.binaries), log_simplex_count)
# Lambdas
self.assertIsInstance(paraboloid_block.lambdas, Var)
self.assertEqual(
len(paraboloid_block.lambdas), simplex_count * simplex_point_count
)
for l in paraboloid_block.lambdas.values():
self.assertEqual(l.lb, 0)
self.assertEqual(l.ub, 1)

# Convex combo constraint
self.assertIsInstance(paraboloid_block.convex_combo, Constraint)
assertExpressionsEqual(
self,
paraboloid_block.convex_combo.expr,
paraboloid_block.lambdas[0, 0]
+ paraboloid_block.lambdas[0, 1]
+ paraboloid_block.lambdas[0, 2]
+ paraboloid_block.lambdas[1, 0]
+ paraboloid_block.lambdas[1, 1]
+ paraboloid_block.lambdas[1, 2]
+ paraboloid_block.lambdas[2, 0]
+ paraboloid_block.lambdas[2, 1]
+ paraboloid_block.lambdas[2, 2]
+ paraboloid_block.lambdas[3, 0]
+ paraboloid_block.lambdas[3, 1]
+ paraboloid_block.lambdas[3, 2]
== 1,
)

# Set substitute constraint
self.assertIsInstance(paraboloid_block.set_substitute, Constraint)
assertExpressionsEqual(
self,
paraboloid_block.set_substitute.expr,
paraboloid_block.substitute_var
== paraboloid_block.lambdas[0, 0] * m.g1(0, 1)
+ paraboloid_block.lambdas[1, 0] * m.g1(0, 1)
+ paraboloid_block.lambdas[2, 0] * m.g2(3, 4)
+ paraboloid_block.lambdas[3, 0] * m.g2(0, 7)
+ paraboloid_block.lambdas[0, 1] * m.g1(0, 4)
+ paraboloid_block.lambdas[1, 1] * m.g1(3, 4)
+ paraboloid_block.lambdas[2, 1] * m.g2(3, 7)
+ paraboloid_block.lambdas[3, 1] * m.g2(0, 4)
+ paraboloid_block.lambdas[0, 2] * m.g1(3, 4)
+ paraboloid_block.lambdas[1, 2] * m.g1(3, 1)
+ paraboloid_block.lambdas[2, 2] * m.g2(0, 7)
+ paraboloid_block.lambdas[3, 2] * m.g2(3, 4),
places=7,
)

# x constraint
self.assertIsInstance(paraboloid_block.x_constraint, Constraint)
# Here we have two x variables
self.assertEqual(len(paraboloid_block.x_constraint), 2)
assertExpressionsEqual(
self,
paraboloid_block.x_constraint[0].expr,
m.x1
== 0 * paraboloid_block.lambdas[0, 0]
+ 0 * paraboloid_block.lambdas[0, 1]
+ 3 * paraboloid_block.lambdas[0, 2]
+ 0 * paraboloid_block.lambdas[1, 0]
+ 3 * paraboloid_block.lambdas[1, 1]
+ 3 * paraboloid_block.lambdas[1, 2]
+ 3 * paraboloid_block.lambdas[2, 0]
+ 3 * paraboloid_block.lambdas[2, 1]
+ 0 * paraboloid_block.lambdas[2, 2]
+ 0 * paraboloid_block.lambdas[3, 0]
+ 0 * paraboloid_block.lambdas[3, 1]
+ 3 * paraboloid_block.lambdas[3, 2],
)
assertExpressionsEqual(
self,
paraboloid_block.x_constraint[1].expr,
m.x2
== 1 * paraboloid_block.lambdas[0, 0]
+ 4 * paraboloid_block.lambdas[0, 1]
+ 4 * paraboloid_block.lambdas[0, 2]
+ 1 * paraboloid_block.lambdas[1, 0]
+ 4 * paraboloid_block.lambdas[1, 1]
+ 1 * paraboloid_block.lambdas[1, 2]
+ 4 * paraboloid_block.lambdas[2, 0]
+ 7 * paraboloid_block.lambdas[2, 1]
+ 7 * paraboloid_block.lambdas[2, 2]
+ 7 * paraboloid_block.lambdas[3, 0]
+ 4 * paraboloid_block.lambdas[3, 1]
+ 4 * paraboloid_block.lambdas[3, 2],
)

# The choices will get long, so let's just assert we have enough
self.assertEqual(len(paraboloid_block.simplex_choice_1), log_simplex_count)
self.assertEqual(len(paraboloid_block.simplex_choice_2), log_simplex_count)

# Test methods using the common_tests.py code.
def test_transformation_do_not_descend(self):
ct.check_transformation_do_not_descend(
self, 'contrib.piecewise.disaggregated_logarithmic'
)

def test_transformation_PiecewiseLinearFunction_targets(self):
ct.check_transformation_PiecewiseLinearFunction_targets(
self, 'contrib.piecewise.disaggregated_logarithmic'
)

def test_descend_into_expressions(self):
ct.check_descend_into_expressions(
self, 'contrib.piecewise.disaggregated_logarithmic'
)

def test_descend_into_expressions_constraint_target(self):
ct.check_descend_into_expressions_constraint_target(
self, 'contrib.piecewise.disaggregated_logarithmic'
)

def test_descend_into_expressions_objective_target(self):
ct.check_descend_into_expressions_objective_target(
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')
def test_solve_log_model(self):
m = models.make_log_x_model()
TransformationFactory("contrib.piecewise.disaggregated_logarithmic").apply_to(m)
SolverFactory("gurobi").solve(m)
ct.check_log_x_model_soln(self, m)
Loading
Loading