-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathsoplex.pyx
386 lines (341 loc) · 14.9 KB
/
soplex.pyx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
# cython: embedsignature=True
from libc.stdlib cimport malloc, free
from cpython.exc cimport PyErr_CheckSignals
from decimal import Decimal
from fractions import Fraction
try:
from sympy import Basic, Number
except:
class Basic:
pass
Number = Basic
include "soplex_constants.pxi"
__version__ = "0.0.5"
__soplex_version__ = "%.2f.%d" % (SOPLEX_VERSION/100., SOPLEX_SUBVERSION)
__soplex_git_hash__ = getGitHash()
cpdef dict get_status_mapping():
return {i.real: i.name for i in STATUS}
cdef Rational rationalize(number):
cdef Rational r
if isinstance(number, (int, Number, Decimal, Fraction)):
r = Rational()
r.readString(str(number).encode())
return r
elif isinstance(number, Basic):
# TODO handle better
return Rational(0)
else:
return Rational(float(number))
cdef bool is_status_error(STATUS status) nogil:
if status == OPTIMAL or status == INFEASIBLE or status == UNBOUNDED:
return False
return True
cdef rational_to_frac(Rational rational):
return Fraction(rationalToString(rational))
cdef class Soplex:
"""cobra SoPlex solver object"""
# Because we use the default constructor for soplex,
# we can do this instead of self.soplex = new SoPlex() in
# __cinit__() and del self.soplex in __dealloc__().
cdef SoPlex soplex
cdef VarStatus* row_basis
cdef VarStatus* col_basis
cdef readonly bool reset_basis
cdef int _reset_basis_iter_cutoff
cdef bool verbose(self):
return self.soplex.intParam(VERBOSITY)
cpdef int soplex_status(self):
return self.soplex.status()
def __dealloc__(self):
if self.row_basis is not NULL:
free(self.row_basis)
if self.col_basis is not NULL:
free(self.col_basis)
def __init__(self, cobra_model=None):
# set the default paramters
# should sync automatically between Real and Rational
self.soplex.setIntParam(SYNCMODE, SYNCMODE_AUTO)
# set default solving parameters
self.soplex.setIntParam(VERBOSITY, 0)
self.soplex.setIntParam(ITERLIMIT, 2147483647) # 2 ** 31 - 1
self.soplex.setIntParam(SOLVEMODE, SOLVEMODE_RATIONAL)
self.soplex.setRealParam(FEASTOL, 1e-20)
self.soplex.setRealParam(OPTTOL, 1e-20)
self._reset_basis_iter_cutoff = 10000
self.reset_basis = False
# create an LP from the cobra model
if cobra_model is None:
return
cdef DSVectorRational vector
cdef LPColRational col
cdef Rational bound
cdef int i, s
cdef DSVectorReal r_vector = DSVectorReal(0)
cdef int m = len(cobra_model.metabolites)
cdef int n = len(cobra_model.reactions)
# To get around lack of infinity in Rational, create bounds with Real
# first, then convert to Rational
for i in range(m):
self.soplex.addRowReal(LPRowReal(0, r_vector, 0))
for i, metabolite in enumerate(cobra_model.metabolites):
self.change_constraint(i, metabolite._constraint_sense,
metabolite._bound)
for reaction in cobra_model.reactions:
vector = DSVectorRational(len(reaction._metabolites))
for metabolite, stoichiometry in reaction._metabolites.items():
if isinstance(stoichiometry, Basic) and \
not isinstance(stoichiometry, Number):
continue
vector.add(cobra_model.metabolites.index(metabolite.id),
rationalize(stoichiometry))
col = LPColRational(rationalize(reaction.objective_coefficient),
vector,
rationalize(reaction.upper_bound),
rationalize(reaction.lower_bound))
self.soplex.addColRational(col)
# initialize the row and column basis
with nogil:
s = sizeof(VarStatus)
self.row_basis = <VarStatus *>malloc(m * s)
self.col_basis = <VarStatus *>malloc(n * s)
for i in range(m):
self.row_basis[i] = ON_UPPER
for i in range(n):
self.col_basis[i] = ON_UPPER
self.soplex.getBasis(self.row_basis, self.col_basis)
@classmethod
def create_problem(cls, cobra_model, objective_sense="maximize"):
problem = cls(cobra_model)
problem.set_objective_sense(objective_sense)
return problem
cpdef set_objective_sense(self, objective_sense="maximize"):
objective_sense = objective_sense.lower()
if objective_sense == "maximize":
self.soplex.setIntParam(OBJSENSE, OBJSENSE_MAXIMIZE)
elif objective_sense == "minimize":
self.soplex.setIntParam(OBJSENSE, OBJSENSE_MINIMIZE)
cpdef change_variable_bounds(self, int index, lower_bound, upper_bound):
self.soplex.changeLowerRational(index, rationalize(lower_bound))
self.soplex.changeUpperRational(index, rationalize(upper_bound))
cpdef change_variable_objective(self, int index, value):
self.soplex.changeObjRational(index, rationalize(value))
cpdef change_coefficient(self, int met_index, int rxn_index, value):
self.soplex.changeElementRational(met_index, rxn_index,
rationalize(value))
cpdef change_constraint(self, int met_index, str constraint_sense, value):
cdef Rational bound = rationalize(value)
if constraint_sense == "E":
self.soplex.changeLhsRational(met_index, bound)
self.soplex.changeRhsRational(met_index, bound)
elif constraint_sense == "L":
self.soplex.changeLhsReal(met_index, -infinity)
self.soplex.changeRhsRational(met_index, bound)
elif constraint_sense == "G":
self.soplex.changeLhsRational(met_index, bound)
self.soplex.changeRhsReal(met_index, infinity)
else:
raise ValueError(
"constraint sense %d (%s) not in {'E', 'G', 'L'}" %
(met_index, constraint_sense))
cpdef set_parameter(self, parameter_name, value):
name_upper = parameter_name.upper()
if parameter_name == "objective_sense":
self.set_objective_sense(value)
elif parameter_name == "verbose" or name_upper == "VERBOSITY":
if value is True:
self.soplex.setIntParam(VERBOSITY, 3)
else:
self.soplex.setIntParam(VERBOSITY, value)
elif name_upper == "SOLVEMODE":
self.soplex.setIntParam(SOLVEMODE, SOLVEMODE_VALUES[value.upper()])
elif name_upper == "CHECKMODE":
self.soplex.setIntParam(CHECKMODE, CHECKMODE_VALUES[value.upper()])
elif name_upper == "FACTOR_UPDATE_MAX":
self.soplex.setIntParam(FACTOR_UPDATE_MAX, int(value))
elif name_upper == "ITERLIMIT":
self.soplex.setIntParam(ITERLIMIT, int(value))
elif name_upper == "REFLIMIT":
self.soplex.setIntParam(REFLIMIT, int(value))
elif name_upper == "STALLREFLIMIT":
self.soplex.setIntParam(STALLREFLIMIT, int(value))
elif name_upper == "RATFAC_MINSTALLS":
self.soplex.setIntParam(RATFAC_MINSTALLS, int(value))
elif parameter_name in IntParameters:
raise NotImplementedError("todo implement " + parameter_name)
# setRealParam section
elif name_upper == "FEASTOL" or parameter_name == "tolerance_feasibility":
self.soplex.setRealParam(FEASTOL, value)
elif name_upper == "OPTTOL":
self.soplex.setRealParam(OPTTOL, value)
elif name_upper == "EPSILON_ZERO":
self.soplex.setRealParam(EPSILON_ZERO, value)
elif name_upper == "EPSILON_FACTORIZATION":
self.soplex.setRealParam(EPSILON_FACTORIZATION, value)
elif name_upper == "EPSILON_UPDATE":
self.soplex.setRealParam(EPSILON_UPDATE, value)
elif name_upper == "EPSILON_PIVOT":
self.soplex.setRealParam(EPSILON_PIVOT, value)
elif name_upper == "INFTY":
self.soplex.setRealParam(INFTY, value)
elif name_upper == "TIMELIMIT" or parameter_name == "time_limit":
self.soplex.setRealParam(TIMELIMIT, value)
elif name_upper == "OBJLIMIT_LOWER":
self.soplex.setRealParam(OBJLIMIT_LOWER, value)
elif name_upper == "OBJLIMIT_UPPER":
self.soplex.setRealParam(OBJLIMIT_UPPER, value)
elif name_upper == "FPFEASTOL":
self.soplex.setRealParam(FPFEASTOL, value)
elif name_upper == "FPOPTTOL":
self.soplex.setRealParam(FPOPTTOL, value)
elif name_upper == "MAXSCALEINCR":
self.soplex.setRealParam(MAXSCALEINCR, value)
elif name_upper == "LIFTMINVAL":
self.soplex.setRealParam(LIFTMINVAL, value)
elif name_upper == "LIFTMAXVAL":
self.soplex.setRealParam(LIFTMAXVAL, value)
elif name_upper == "SPARSITY_THRESHOLD":
self.soplex.setRealParam(SPARSITY_THRESHOLD, value)
elif name_upper == "RESET_BASIS_ITER_CUTOFF":
self._reset_basis_iter_cutoff = value
else:
raise ValueError("Unknown parameter '%s'" % parameter_name)
def solve_problem(self, **kwargs):
cdef STATUS result
if "objective_sense" in kwargs:
self.set_objective_sense(kwargs.pop("objective_sense"))
for key, value in kwargs.items():
self.set_parameter(key, value)
# try to solve with a set basis
self.reset_basis = False
cdef int iterlim = self.soplex.intParam(ITERLIMIT)
cdef int new_iterlim
if iterlim > 0: # -1 iterlim means it's unlimited
new_iterlim = min(self._reset_basis_iter_cutoff, iterlim)
else:
new_iterlim = self._reset_basis_iter_cutoff
self.soplex.setIntParam(ITERLIMIT, new_iterlim)
self.soplex.setBasis(self.row_basis, self.col_basis)
with nogil:
result = self.soplex.solve()
self.soplex.setIntParam(ITERLIMIT, iterlim) # reset iterlim
PyErr_CheckSignals()
if self.verbose():
print(self.soplex.statisticString())
# if it didn't solve with the set basis, try again
with nogil:
if is_status_error(result):
self.reset_basis = True
self.soplex.clearBasis()
result = self.soplex.solve()
if self.verbose():
print(self.soplex.statisticString())
# save the basis for next time
if result == OPTIMAL:
self.soplex.getBasis(self.row_basis, self.col_basis)
return self.get_status()
cpdef get_status(self):
cdef int status = self.soplex.status()
if status == OPTIMAL:
return "optimal"
elif status == INFEASIBLE:
return "infeasible"
else:
mapping = get_status_mapping()
status_str = mapping.get(status)
return status_str if status_str is not None else "failed"
cpdef get_objective_value(self, rational=False):
if rational:
return rational_to_frac(self.soplex.objValueRational())
else:
return self.soplex.objValueReal()
cpdef format_solution(self, cobra_model):
status = self.get_status()
Solution = cobra_model.solution.__class__
if status != "optimal": # todo handle other possible
return Solution(None, status=status)
solution = Solution(self.get_objective_value(), status=status)
cdef int i
# get primals
cdef int nCols = self.soplex.numColsReal()
cdef DVectorReal x_vals = DVectorReal(nCols)
self.soplex.getPrimalReal(x_vals)
solution.x = [x_vals[i] for i in range(nCols)]
solution.x_dict = {cobra_model.reactions[i].id: x_vals[i]
for i in range(nCols)}
# get duals
cdef int nRows = self.soplex.numRowsReal()
cdef DVectorReal y_vals = DVectorReal(nRows)
self.soplex.getDualReal(y_vals)
solution.y = [y_vals[i] for i in range(nRows)]
solution.y_dict = {cobra_model.metabolites[i].id: y_vals[i]
for i in range(nRows)}
return solution
@classmethod
def solve(cls, cobra_model, **kwargs):
problem = cls.create_problem(cobra_model)
problem.solve_problem(**kwargs)
solution = problem.format_solution(cobra_model)
return solution
cpdef clear_basis(self):
IF False:
for i in range(self.soplex.numRowsReal()):
self.row_basis[i] = ON_UPPER
for i in range(self.soplex.numColsReal()):
self.col_basis[i] = ON_UPPER
self.soplex.setBasis(self.row_basis, self.col_basis)
self.soplex.clearBasis()
@property
def numRows(self):
return self.soplex.numRowsReal()
@property
def numCols(self):
return self.soplex.numColsReal()
cpdef write(self, filename, state=True, rational=False):
if state:
if filename.endswith(b".lp"):
filename = filename[:-3]
if rational:
self.soplex.writeStateRational(filename, NULL, NULL, True)
return
else:
self.soplex.writeStateReal(filename, NULL, NULL, True)
return
if rational:
return self.soplex.writeFileRational(filename)
else:
return self.soplex.writeFileReal(filename)
@property
def hasPrimal(self):
return self.soplex.hasPrimal()
@property
def hasBasis(self):
return self.soplex.hasBasis()
@property
def solveTime(self):
return self.soplex.solveTime()
@property
def numIterations(self):
return self.soplex.numIterations()
# wrappers for all the functions at the module level
create_problem = Soplex.create_problem
def set_objective_sense(lp, objective_sense="maximize"):
return lp.set_objective_sense(lp, objective_sense=objective_sense)
cpdef change_variable_bounds(lp, int index, lower_bound, upper_bound):
return lp.change_variable_bounds(index, lower_bound, upper_bound)
cpdef change_variable_objective(lp, int index, value):
return lp.change_variable_objective(index, value)
cpdef change_coefficient(lp, int met_index, int rxn_index, value):
return lp.change_coefficient(met_index, rxn_index, value)
cpdef change_constraint(lp, int met_index, str constraint_sense, value):
return lp.change_constraint(met_index, constraint_sense, value)
cpdef set_parameter(lp, parameter_name, value):
return lp.set_parameter(parameter_name, value)
def solve_problem(lp, **kwargs):
return lp.solve_problem(**kwargs)
cpdef get_status(lp):
return lp.get_status()
cpdef get_objective_value(lp):
return lp.get_objective_value()
cpdef format_solution(lp, cobra_model):
return lp.format_solution(cobra_model)
solve = Soplex.solve