diff options
author | Bjoern Brandenburg <bbb@mpi-sws.org> | 2012-08-08 08:09:48 -0400 |
---|---|---|
committer | Bjoern Brandenburg <bbb@mpi-sws.org> | 2013-02-12 06:49:40 -0500 |
commit | 04d02fb9b93315f7c5d177909b627c5795d9d931 (patch) | |
tree | 4518ca04f047c30e46713c63040c9c3fcde1a581 | |
parent | 8c2cbdfa067751e3e3ccf73fc145cb31d3961e0a (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.py | 235 |
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 @@ | |||
1 | from StringIO import StringIO | ||
2 | from collections import defaultdict | ||
3 | |||
4 | |||
5 | try: | ||
6 | import cplex | ||
7 | from itertools import izip | ||
8 | from tempfile import NamedTemporaryFile as TmpFile | ||
9 | |||
10 | cplex_available = True | ||
11 | except 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 | |||
17 | def 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 | |||
24 | def 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 | |||
37 | def filter_vars(removed, sum): | ||
38 | return [(c, v) for (c, v) in sum if not v in removed] | ||
39 | |||
40 | def 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 | |||
45 | class 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 | |||
60 | class 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 | |||
222 | def 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 | |||
234 | if __name__ == '__main__': | ||
235 | example() | ||