Skip to content

Commit a384784

Browse files
committed
Merge branch 'logarithmic_pwlinear' of github.com:sadavis1/pyomo into logarithmic_pwlinear
2 parents 7c7a276 + 22228c1 commit a384784

File tree

1 file changed

+201
-0
lines changed

1 file changed

+201
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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.contrib.piecewise.transform.piecewise_linear_transformation_base import (
13+
PiecewiseLinearTransformationBase,
14+
)
15+
from pyomo.core import Constraint, Binary, Var, RangeSet, Set
16+
from pyomo.core.base import TransformationFactory
17+
from pyomo.common.errors import DeveloperError
18+
from math import ceil, log2
19+
20+
21+
@TransformationFactory.register(
22+
"contrib.piecewise.disaggregated_logarithmic",
23+
doc="""
24+
Represent a piecewise linear function "logarithmically" by using a MIP with
25+
log_2(|P|) binary decision variables. This is a direct-to-MIP transformation;
26+
GDP is not used.
27+
""",
28+
)
29+
class DisaggregatedLogarithmicInnerMIPTransformation(PiecewiseLinearTransformationBase):
30+
"""
31+
Represent a piecewise linear function "logarithmically" by using a MIP with
32+
log_2(|P|) binary decision variables, following the "disaggregated logarithmic"
33+
method from [1]. This is a direct-to-MIP transformation; GDP is not used.
34+
This method of logarithmically formulating the piecewise linear function
35+
imposes no restrictions on the family of polytopes, but we assume we have
36+
simplices in this code.
37+
38+
References
39+
----------
40+
[1] J.P. Vielma, S. Ahmed, and G. Nemhauser, "Mixed-integer models
41+
for nonseparable piecewise-linear optimization: unifying framework
42+
and extensions," Operations Research, vol. 58, no. 2, pp. 305-315,
43+
2010.
44+
"""
45+
46+
CONFIG = PiecewiseLinearTransformationBase.CONFIG()
47+
_transformation_name = "pw_linear_disaggregated_log"
48+
49+
# Implement to use PiecewiseLinearTransformationBase. This function returns the Var
50+
# that replaces the transformed piecewise linear expr
51+
def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block):
52+
# Get a new Block for our transformation in transformation_block.transformed_functions,
53+
# which is a Block(Any). This is where we will put our new components.
54+
transBlock = transformation_block.transformed_functions[
55+
len(transformation_block.transformed_functions)
56+
]
57+
58+
# Dimensionality of the PWLF
59+
dimension = pw_expr.nargs()
60+
transBlock.dimension_indices = RangeSet(0, dimension - 1)
61+
62+
# Substitute Var that will hold the value of the PWLE
63+
substitute_var = transBlock.substitute_var = Var()
64+
pw_linear_func.map_transformation_var(pw_expr, substitute_var)
65+
66+
# Bounds for the substitute_var that we will widen
67+
substitute_var_lb = float("inf")
68+
substitute_var_ub = -float("inf")
69+
70+
# Simplices are tuples of indices of points. Give them their own indices, too
71+
simplices = pw_linear_func._simplices
72+
num_simplices = len(simplices)
73+
transBlock.simplex_indices = RangeSet(0, num_simplices - 1)
74+
# Assumption: the simplices are really full-dimensional simplices and all have the
75+
# same number of points, which is dimension + 1
76+
transBlock.simplex_point_indices = RangeSet(0, dimension)
77+
78+
# Enumeration of simplices: map from simplex number to simplex object
79+
idx_to_simplex = {k: v for k, v in zip(transBlock.simplex_indices, simplices)}
80+
81+
# List of tuples of simplex indices with their linear function
82+
simplex_indices_and_lin_funcs = list(
83+
zip(transBlock.simplex_indices, pw_linear_func._linear_functions)
84+
)
85+
86+
# We don't seem to get a convenient opportunity later, so let's just widen
87+
# the bounds here. All we need to do is go through the corners of each simplex.
88+
for P, linear_func in simplex_indices_and_lin_funcs:
89+
for v in transBlock.simplex_point_indices:
90+
val = linear_func(*pw_linear_func._points[idx_to_simplex[P][v]])
91+
if val < substitute_var_lb:
92+
substitute_var_lb = val
93+
if val > substitute_var_ub:
94+
substitute_var_ub = val
95+
transBlock.substitute_var.setlb(substitute_var_lb)
96+
transBlock.substitute_var.setub(substitute_var_ub)
97+
98+
log_dimension = ceil(log2(num_simplices))
99+
transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1)
100+
transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary)
101+
102+
# Injective function B: \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices
103+
# (really just polytopes are required) with binary vectors. Any injective function
104+
# is enough here.
105+
B = {}
106+
for i in transBlock.simplex_indices:
107+
# map index(P) -> corresponding vector in {0, 1}^n
108+
B[i] = self._get_binary_vector(i, log_dimension)
109+
110+
# Build up P_0 and P_plus ahead of time.
111+
112+
# {P \in \mathcal{P} | B(P)_l = 0}
113+
def P_0_init(m, l):
114+
return [p for p in transBlock.simplex_indices if B[p][l] == 0]
115+
116+
transBlock.P_0 = Set(transBlock.log_simplex_indices, initialize=P_0_init)
117+
118+
# {P \in \mathcal{P} | B(P)_l = 1}
119+
def P_plus_init(m, l):
120+
return [p for p in transBlock.simplex_indices if B[p][l] == 1]
121+
122+
transBlock.P_plus = Set(transBlock.log_simplex_indices, initialize=P_plus_init)
123+
124+
# The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it
125+
transBlock.lambdas = Var(
126+
transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1)
127+
)
128+
129+
# Numbered citations are from Vielma et al 2010, Mixed-Integer Models
130+
# for Nonseparable Piecewise-Linear Optimization
131+
132+
# Sum of all lambdas is one (6b)
133+
transBlock.convex_combo = Constraint(
134+
expr=sum(
135+
transBlock.lambdas[P, v]
136+
for P in transBlock.simplex_indices
137+
for v in transBlock.simplex_point_indices
138+
)
139+
== 1
140+
)
141+
142+
# The branching rules, establishing using the binaries that only one simplex's lambda
143+
# coefficients may be nonzero
144+
# Enabling lambdas when binaries are on
145+
@transBlock.Constraint(transBlock.log_simplex_indices) # (6c.1)
146+
def simplex_choice_1(b, l):
147+
return (
148+
sum(
149+
transBlock.lambdas[P, v]
150+
for P in transBlock.P_plus[l]
151+
for v in transBlock.simplex_point_indices
152+
)
153+
<= transBlock.binaries[l]
154+
)
155+
156+
# Disabling lambdas when binaries are on
157+
@transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2)
158+
def simplex_choice_2(b, l):
159+
return (
160+
sum(
161+
transBlock.lambdas[P, v]
162+
for P in transBlock.P_0[l]
163+
for v in transBlock.simplex_point_indices
164+
)
165+
<= 1 - transBlock.binaries[l]
166+
)
167+
168+
# for i, (simplex, pwlf) in enumerate(choices):
169+
# x_i = sum(lambda_P,v v_i, P in polytopes, v in V(P))
170+
@transBlock.Constraint(transBlock.dimension_indices) # (6a.1)
171+
def x_constraint(b, i):
172+
return pw_expr.args[i] == sum(
173+
transBlock.lambdas[P, v]
174+
* pw_linear_func._points[idx_to_simplex[P][v]][i]
175+
for P in transBlock.simplex_indices
176+
for v in transBlock.simplex_point_indices
177+
)
178+
179+
# Make the substitute Var equal the PWLE (6a.2)
180+
transBlock.set_substitute = Constraint(
181+
expr=substitute_var
182+
== sum(
183+
sum(
184+
transBlock.lambdas[P, v]
185+
* linear_func(*pw_linear_func._points[idx_to_simplex[P][v]])
186+
for v in transBlock.simplex_point_indices
187+
)
188+
for (P, linear_func) in simplex_indices_and_lin_funcs
189+
)
190+
)
191+
192+
return substitute_var
193+
194+
# Not a Gray code, just a regular binary representation
195+
# TODO test the Gray codes too
196+
def _get_binary_vector(self, num, length):
197+
if num != 0 and ceil(log2(num)) > length:
198+
raise DeveloperError("Invalid input in _get_binary_vector")
199+
# Use python's string formatting instead of bothering with modular
200+
# arithmetic. Hopefully not slow.
201+
return tuple(int(x) for x in format(num, f"0{length}b"))

0 commit comments

Comments
 (0)