aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBjoern Brandenburg <bbb@mpi-sws.org>2012-08-08 08:09:48 -0400
committerBjoern Brandenburg <bbb@mpi-sws.org>2013-02-12 06:49:40 -0500
commit04d02fb9b93315f7c5d177909b627c5795d9d931 (patch)
tree4518ca04f047c30e46713c63040c9c3fcde1a581
parent8c2cbdfa067751e3e3ccf73fc145cb31d3961e0a (diff)
Add Python linear programming support
This patch adds the utility class LinearProgram, which implements a simple representation of linear programs. Solving with CPLEX is available as well, subject to the availability of the CPLEX Python API.
-rw-r--r--schedcat/util/linprog.py235
1 files changed, 235 insertions, 0 deletions
diff --git a/schedcat/util/linprog.py b/schedcat/util/linprog.py
new file mode 100644
index 0000000..428028e
--- /dev/null
+++ b/schedcat/util/linprog.py
@@ -0,0 +1,235 @@
1from StringIO import StringIO
2from collections import defaultdict
3
4
5try:
6 import cplex
7 from itertools import izip
8 from tempfile import NamedTemporaryFile as TmpFile
9
10 cplex_available = True
11except ImportError:
12 cplex_available = False
13
14# Constraint format: tuple of ( vector, value),
15# where vector is a list of (coefficient, variable_name) pairs.
16
17def write_cplex_terms(file, vector):
18 for (c, v) in vector:
19 if c < 0:
20 file.write("- %s %s " % (-c, v))
21 else:
22 file.write("+ %s %s " % (c, v))
23
24def write_cplex_sum(file, sum, per_line=None):
25 if len(sum) == 0:
26 file.write("0")
27 return
28 file.write("%s %s " % sum[0])
29 if per_line is None:
30 write_cplex_terms(file, sum[1:])
31 else:
32 write_cplex_terms(file, sum[1:per_line])
33 for i in xrange(per_line, len(sum), per_line):
34 file.write('\n ')
35 write_cplex_terms(file, sum[i:i+per_line])
36
37def filter_vars(removed, sum):
38 return [(c, v) for (c, v) in sum if not v in removed]
39
40def apply_prefix(vector, prefix):
41 "Prepend prefix to each variable name in vector."
42 return [(coeff, prefix + '_' + var) for (coeff, var) in vector]
43
44
45class Solution(defaultdict):
46 def __init__(self):
47 super(Solution, self).__init__(int)
48
49 def evaluate(self, vector):
50 value = 0
51 for (coeff, variable) in vector:
52 if variable in self:
53 value += coeff * self[variable]
54 return value
55
56 def __call__(self, vector):
57 return self.evaluate(vector)
58
59
60class LinearProgram(object):
61
62 def __init__(self):
63 self.equalities = []
64 self.inequalities = []
65 self.objective_function = None
66 self.goal = 'Maximize'
67 self.name = 'OPT'
68
69 ## Data model
70
71 def add_inequality(self, vector, upper_bound):
72 self.inequalities.append((vector, upper_bound))
73
74 def add_equality(self, vector, value):
75 self.equalities.append((vector, value))
76
77 def set_objective(self, objective_function):
78 self.objective_function = objective_function
79
80 def apply_variable_prefix(self, prefix):
81 self.inequalities = [(apply_prefix(vector, prefix), upper_bound)
82 for (vector, upper_bound) in self.inequalities]
83 self.equalities = [(apply_prefix(vector, prefix), bound)
84 for (vector, bound) in self.equalities]
85 self.objective_function = apply_prefix(self.objective_function, prefix)
86
87 def merge(self, other_LP):
88 assert(other_LP.goal == self.goal)
89 for ineq in other_LP.inequalities:
90 self.inequalities.append(ineq)
91 for eq in other_LP.equalities:
92 self.equalities.append(eq)
93 if self.objective_function == None:
94 self.objective_function = []
95 for term in other_LP.objective_function:
96 self.objective_function.append(term)
97
98 ## Convenience constraint DSL
99
100 def equality(self, *args, **kargs):
101 """Specify equality constraint as:
102
103 equality(coeff1, var1, coeff2, var2, ..., equal_to=value)
104 """
105 assert len(args) % 2 == 0 # num coefficients == num variables?
106 assert len(kargs) == 1 and 'equal_to' in kargs # constraint?
107
108 vector = zip(args[0::2], args[1::2])
109 self.add_equality(vector, kargs['equal_to'])
110
111 def inequality(self, *args, **kargs):
112 """Specify inequality constraint as:
113
114 inequality(coeff1, var1, coeff2, var2, ..., at_most=value)
115 """
116 assert len(args) % 2 == 0 # num coefficients == num variables?
117 assert len(kargs) == 1 and 'at_most' in kargs # constraint?
118
119 vector = zip(args[0::2], args[1::2])
120 self.add_inequality(vector, kargs['at_most'])
121
122 def objective(self, *args):
123 """Specify objective function as:
124
125 objective(coeff1, var1, coeff2, var2, ...)
126 """
127 assert len(args) % 2 == 0 # num coefficients == num variables?
128
129 vector = zip(args[0::2], args[1::2])
130 self.set_objective(vector)
131
132 ## Helper for model conversion
133
134 def write_cplex_lp_format(self, file):
135 file.write('%s\n' % self.goal)
136 write_cplex_sum(file, self.objective_function, per_line=7)
137 file.write('\nSubject To\n')
138 for vector, value in self.equalities:
139 write_cplex_sum(file, vector, per_line=7)
140 file.write(' = %f\n' % value)
141 for vector, value in self.inequalities:
142 write_cplex_sum(file, vector, per_line=7)
143 file.write(' <= %f\n' % value)
144 file.write('End\n')
145 file.flush()
146
147
148 def kill_non_positive_vars(self):
149 """Removes all variables that are forced to <= 0.
150 Assumes that all coefficients are positive.
151 """
152 # find pointless variables
153 killed = set()
154 for vector, value in self.equalities:
155 if value <= 0:
156 for coeff, var in vector:
157 killed.add(var)
158 for vector, value in self.inequalities:
159 if value <= 0:
160 for coeff, var in vector:
161 killed.add(var)
162 # remove pointless variables
163 self.objective_function = filter_vars(killed, self.objective_function)
164 if 'local_objective' in self.__dict__:
165 self.local_objective = filter_vars(killed, self.local_objective)
166 self.equalities = [(filter_vars(killed, vect), bound) for
167 (vect, bound) in self.equalities if bound > 0]
168 self.equalities = [(vect, bound) for (vect, bound) in self.equalities if vect]
169 self.inequalities = [(filter_vars(killed, vect), bound) for
170 (vect, bound) in self.inequalities if bound > 0]
171 self.inequalities = [(vect, bound) for (vect, bound) in self.inequalities if vect]
172
173 def __str__(self):
174 f = StringIO()
175 self.write_cplex_lp_format(f)
176 return f.getvalue()
177
178 ## Conditional solver integration.
179
180 if cplex_available:
181
182 def get_cplex_model(self, model=None):
183 if model is None:
184 model = cplex.Cplex()
185
186 # load problem via file to avoid API overheads
187 tmp = TmpFile()
188 self.write_cplex_lp_format(tmp)
189 model.read(tmp.name)
190 model.set_results_stream(None)
191 del tmp
192 return model
193
194 def solve_with_cplex(self):
195 solution = Solution()
196
197 # If the objective is empty (e.g., after killing
198 # all zero-constrained variables), then we automatically have
199 # a trivial solution.
200 if len(self.objective_function) == 0:
201 return solution
202
203 # Let CPLEX do the job.
204 model = self.get_cplex_model()
205 model.solve()
206
207 # Extract the variables that we care about.
208 variables = [var for (coeff, var) in self.objective_function]
209 values = model.solution.get_values(variables)
210 for (var, val) in izip(variables, values):
211 solution[var] = val
212
213 return solution
214
215 def solve(self):
216 if cplex_available:
217 return self.solve_with_cplex()
218 # Add additional solvers here...
219 else:
220 assert False # No solver available!
221
222def example():
223 # simple example of the convenience API
224 x = LinearProgram()
225 x.objective(-3, 'x2', 99, 'x1')
226 x.inequality(10, 'x1', at_most=100)
227 x.inequality(-9, 'x1', 10, 'x2', at_most=99)
228 x.equality(10, 'x2', equal_to=0)
229 print x
230
231 x.inequality(0, 'x3', at_most=1)
232 x.equality(0, 'x10', equal_to=0)
233
234if __name__ == '__main__':
235 example()