Skip to content

Commit 3b8dcb5

Browse files
1.3.0
- Symengine-based symbolics is now a fully supported feature - Added the first exact LP solver, available as glpk_exact_interface - Fixed an issue with indicator variables in cplex - Minor bugfixes
1 parent 102437f commit 3b8dcb5

15 files changed

+858
-105
lines changed

.travis.yml

+44-41
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
language: python
22
sudo: false
33
python:
4-
- '2.7'
5-
- '3.4'
6-
- '3.5'
4+
- '2.7'
5+
- '3.4'
6+
- '3.5'
7+
env:
8+
global:
9+
- secure: PBcwrLg4ZwVi9Gw25Q2adNd0uK+NCqFSiYl+ZuumkEXs4NQBbSXVd719wrWKUFdIqBy+h9ETo4Vsz/QsTZzI2mSG4zqTkm+oUxMW1tJxYEeKZvCo7GfZ883VlKFgdqvh6iouEvHSjfGbwc5cUp98CbjgI5ni01vGpDQh2hgokkI=
10+
matrix:
11+
- OPTLANG_USE_SYMENGINE=False
12+
- OPTLANG_USE_SYMENGINE=True
713
cache:
8-
- pip: true
14+
- pip: true
915
addons:
1016
apt:
1117
packages:
@@ -16,49 +22,46 @@ addons:
1622
- glpk-utils
1723
- pandoc
1824
before_install:
19-
- export SYMPY_USE_CACHE=no
20-
- export OPTLANG_USE_SYMENGINE=no
21-
- pip install pip --upgrade
22-
- 'echo "this is a build for: $TRAVIS_BRANCH"'
23-
- 'if [[ "$TRAVIS_PYTHON_VERSION" != "3.5" ]]; then bash ./.travis/install_cplex.sh; fi'
25+
- export SYMPY_USE_CACHE=no
26+
- pip install pip --upgrade
27+
- 'echo "this is a build for: $TRAVIS_BRANCH"'
28+
- 'if [[ "$TRAVIS_PYTHON_VERSION" != "3.5" ]]; then bash ./.travis/install_cplex.sh; fi'
2429
install:
25-
- pip install nose nose-progressive rednose coverage docutils flake8 codecov jsonschema
26-
- pip install -r requirements.txt
27-
- pip install inspyred
28-
- pip install pypandoc
29-
- pip install swiglpk
30-
- pip install scipy
31-
- python setup.py install
30+
- pip install nose nose-progressive rednose coverage docutils flake8 codecov jsonschema
31+
- pip install -r requirements.txt
32+
- pip install inspyred
33+
- pip install pypandoc
34+
- pip install swiglpk
35+
- pip install scipy
36+
- pip install symengine
37+
- python setup.py install
3238
before_script:
33-
- flake8 .
39+
- flake8 .
3440
script: nosetests
3541
after_success:
36-
- codecov
42+
- codecov
3743
notifications:
3844
slack:
3945
secure: s8Dj0MFreNwZ3Zhb0+5yJiHPL33JsxLjmoRo8f0ohLdD15L//E4VjkCsYkNEcLzid6HarEL/1JSmzAuGl40fCdLqTAoDRy01shT1zmfWQPXQlaALh5f8ExBAlyDHxKhd/B2SytYu6uhe0WOuxu/oo4c33a7pKhuV1piNcevPZew=
4046
before_deploy:
41-
- pip install twine
42-
- python setup.py sdist bdist_wheel
43-
env:
44-
global:
45-
- secure: PBcwrLg4ZwVi9Gw25Q2adNd0uK+NCqFSiYl+ZuumkEXs4NQBbSXVd719wrWKUFdIqBy+h9ETo4Vsz/QsTZzI2mSG4zqTkm+oUxMW1tJxYEeKZvCo7GfZ883VlKFgdqvh6iouEvHSjfGbwc5cUp98CbjgI5ni01vGpDQh2hgokkI=
47+
- pip install twine
48+
- python setup.py sdist bdist_wheel
4649
deploy:
47-
- provider: releases
48-
api_key:
49-
secure: u4aJv+5YoH3gjJpyiVoq33SqKIUtx8LWPp15pIh8hKHmUgJNyjGm7ELXOeczfQ5W7ZpnWj+ogewaes2oA0NLxBB1/MBPL7kr77hmzp+XhZomh73DzFKegbpBTgqpioBRxvPlq3HYNIWqrLkeg/HYlBW1WM6mKifFUwqbIaL+++4=
50-
file_glob: true
51-
file: dist/optlang*.whl
52-
skip_cleanup: true
53-
on:
54-
branch: master
55-
tags: true
56-
repo: biosustain/optlang
57-
- provider: pypi
58-
user: Nikolaus.Sonnenschein
59-
password:
60-
secure: Gn23MUvzP1DPJXxRXUOXGBJjyMamawxey5ByrOd+JT90roljHKSk8v1wdBMH7+s1DB/ygUJqB2Zy0cBC3mr0waY6HmxKpXhddgzQzG56Eua/npTxpz58Y8xfSYF+5QqS3gcyBrYEXmeHWuEURERy0b7uYKMx/QcHAHYhTaVy4zE=
61-
on:
62-
branch: master
63-
tags: true
64-
repo: biosustain/optlang
50+
- provider: releases
51+
api_key:
52+
secure: u4aJv+5YoH3gjJpyiVoq33SqKIUtx8LWPp15pIh8hKHmUgJNyjGm7ELXOeczfQ5W7ZpnWj+ogewaes2oA0NLxBB1/MBPL7kr77hmzp+XhZomh73DzFKegbpBTgqpioBRxvPlq3HYNIWqrLkeg/HYlBW1WM6mKifFUwqbIaL+++4=
53+
file_glob: true
54+
file: dist/optlang*.whl
55+
skip_cleanup: true
56+
on:
57+
branch: master
58+
tags: true
59+
repo: biosustain/optlang
60+
- provider: pypi
61+
user: Nikolaus.Sonnenschein
62+
password:
63+
secure: Gn23MUvzP1DPJXxRXUOXGBJjyMamawxey5ByrOd+JT90roljHKSk8v1wdBMH7+s1DB/ygUJqB2Zy0cBC3mr0waY6HmxKpXhddgzQzG56Eua/npTxpz58Y8xfSYF+5QqS3gcyBrYEXmeHWuEURERy0b7uYKMx/QcHAHYhTaVy4zE=
64+
on:
65+
branch: master
66+
tags: true
67+
repo: biosustain/optlang

examples/simple_numpy.py

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import numpy as np
2+
from optlang import Model, Variable, Constraint, Objective
3+
4+
# All the (symbolic) variables are declared, with a name and optionally a lower
5+
# and/or upper bound.
6+
x = np.array([Variable('x{}'.format(i), lb=0) for i in range(1, 4)])
7+
8+
bounds = [100, 600, 300]
9+
10+
A = np.array([[1, 1, 1],
11+
[10, 4, 5],
12+
[2, 2, 6]])
13+
14+
w = np.array([10, 6, 4])
15+
16+
obj = Objective(w.dot(x), direction='max')
17+
18+
c = np.array([Constraint(row, ub=bound) for row, bound in zip(A.dot(x), bounds)])
19+
20+
model = Model(name='Numpy model')
21+
model.objective = obj
22+
model.add(c)
23+
24+
status = model.optimize()
25+
26+
print("status:", model.status)
27+
print("objective value:", model.objective.value)
28+
print("----------")
29+
for var_name, var in model.variables.iteritems():
30+
print(var_name, "=", var.primal)

optlang/__init__.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
if available_solvers['GLPK']:
3535
try:
3636
from optlang import glpk_interface
37+
from optlang import glpk_exact_interface
3738
except Exception:
3839
log.error('GLPK is available but could not load with error:\n ' + str(traceback.format_exc()).strip().replace('\n','\n '))
3940

@@ -57,10 +58,10 @@
5758

5859

5960
# Go through and find the best solver that loaded. Load that one as the default
60-
for engine_str in ['cplex_interface','gurobi_interface','glpk_interface','scipy_interface']:
61+
for engine_str in ['cplex_interface', 'gurobi_interface', 'glpk_interface', 'scipy_interface']:
6162
# Must check globals since not all interface variables will be defined
6263
if engine_str in globals():
63-
engine=globals()[engine_str]
64+
engine = globals()[engine_str]
6465
Model = engine.Model
6566
Variable = engine.Variable
6667
Constraint = engine.Constraint

optlang/cplex_interface.py

+17-9
Original file line numberDiff line numberDiff line change
@@ -219,13 +219,15 @@ def __init__(self, expression, sloppy=False, *args, **kwargs):
219219

220220
def set_linear_coefficients(self, coefficients):
221221
if self.problem is not None:
222+
self.problem.update()
222223
triplets = [(self.name, var.name, float(coeff)) for var, coeff in six.iteritems(coefficients)]
223224
self.problem.problem.linear_constraints.set_coefficients(triplets)
224225
else:
225226
raise Exception("Can't change coefficients if constraint is not associated with a model.")
226227

227228
def get_linear_coefficients(self, variables):
228229
if self.problem is not None:
230+
self.problem.update()
229231
coefs = self.problem.problem.linear_constraints.get_coefficients([(self.name, v.name) for v in variables])
230232
return {v: c for v, c in zip(variables, coefs)}
231233
else:
@@ -234,7 +236,13 @@ def get_linear_coefficients(self, variables):
234236
def _get_expression(self):
235237
if self.problem is not None:
236238
cplex_problem = self.problem.problem
237-
cplex_row = cplex_problem.linear_constraints.get_rows(self.name)
239+
try:
240+
cplex_row = cplex_problem.linear_constraints.get_rows(self.name)
241+
except CplexSolverError as e:
242+
if 'CPLEX Error 1219:' not in str(e):
243+
raise e
244+
else:
245+
cplex_row = cplex_problem.indicator_constraints.get_linear_components(self.name)
238246
variables = self.problem._variables
239247
expression = add(
240248
[mul((symbolics.Real(cplex_row.val[i]), variables[ind])) for i, ind in
@@ -367,13 +375,15 @@ def _get_expression(self):
367375

368376
def set_linear_coefficients(self, coefficients):
369377
if self.problem is not None:
378+
self.problem.update()
370379
self.problem.problem.objective.set_linear([(variable.name, float(coefficient)) for variable, coefficient in coefficients.items()])
371380
self._expression_expired = True
372381
else:
373382
raise Exception("Can't change coefficients if objective is not associated with a model.")
374383

375384
def get_linear_coefficients(self, variables):
376385
if self.problem is not None:
386+
self.problem.update()
377387
coefs = self.problem.problem.objective.get_linear([v.name for v in variables])
378388
return {v: c for v, c in zip(variables, coefs)}
379389
else:
@@ -608,11 +618,7 @@ def __init__(self, problem=None, *args, **kwargs):
608618

609619
# Since constraint expressions are lazily retrieved from the solver they don't have to be built here
610620
# lhs = _unevaluated_Add(*[val * variables[i - 1] for i, val in zip(row.ind, row.val)])
611-
lhs = 0
612-
if isinstance(lhs, int):
613-
lhs = symbolics.Integer(lhs)
614-
elif isinstance(lhs, float):
615-
lhs = symbolics.Real(lhs)
621+
lhs = symbolics.Integer(0)
616622
if sense == 'E':
617623
constr = Constraint(lhs, lb=rhs, ub=rhs, name=name, problem=self)
618624
elif sense == 'G':
@@ -625,7 +631,7 @@ def __init__(self, problem=None, *args, **kwargs):
625631
constr = Constraint(lhs, lb=rhs, ub=rhs + range_val, name=name, problem=self)
626632
else:
627633
constr = Constraint(lhs, lb=rhs + range_val, ub=rhs, name=name, problem=self)
628-
else:
634+
else: # pragma: no cover
629635
raise Exception('%s is not a recognized constraint sense.' % sense)
630636

631637
for variable in constraint_variables:
@@ -640,7 +646,7 @@ def __init__(self, problem=None, *args, **kwargs):
640646
)
641647
try:
642648
objective_name = self.problem.objective.get_name()
643-
except cplex.exceptions.CplexSolverError as e:
649+
except CplexSolverError as e:
644650
if 'CPLEX Error 1219:' not in str(e):
645651
raise e
646652
else:
@@ -885,7 +891,9 @@ def _add_constraints(self, constraints, sloppy=False):
885891
def _remove_constraints(self, constraints):
886892
super(Model, self)._remove_constraints(constraints)
887893
for constraint in constraints:
888-
if constraint.is_Linear:
894+
if constraint.indicator_variable is not None:
895+
self.problem.indicator_constraints.delete(constraint.name)
896+
elif constraint.is_Linear:
889897
self.problem.linear_constraints.delete(constraint.name)
890898
elif constraint.is_Quadratic:
891899
self.problem.quadratic_constraints.delete(constraint.name)

optlang/glpk_exact_interface.py

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Copyright 2017 Novo Nordisk Foundation Center for Biosustainability,
2+
# Technical University of Denmark.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
17+
"""
18+
Interface for the GNU Linear Programming Kit (GLPK)
19+
20+
GLPK is an open source LP solver, with MILP capabilities. This interface exposes its GLPK's exact solver.
21+
To use GLPK you need to install the 'swiglpk' python package (with pip or from http://github.com/biosustain/swiglpk)
22+
and make sure that 'import swiglpk' runs without error.
23+
"""
24+
25+
import logging
26+
27+
import six
28+
29+
from optlang.util import inheritdocstring
30+
from optlang import interface
31+
from optlang import glpk_interface
32+
from optlang.glpk_interface import _GLPK_STATUS_TO_STATUS
33+
34+
log = logging.getLogger(__name__)
35+
36+
from swiglpk import glp_exact, glp_create_prob, glp_get_status, \
37+
GLP_SF_AUTO, GLP_ETMLIM, glp_adv_basis, glp_read_lp, glp_scale_prob
38+
39+
40+
@six.add_metaclass(inheritdocstring)
41+
class Variable(glpk_interface.Variable):
42+
def __init__(self, name, index=None, type="continuous", **kwargs):
43+
if type in ("integer", "binary"):
44+
raise ValueError("The GLPK exact solver does not support integer and mixed integer problems")
45+
super(Variable, self).__init__(name, index, type=type, **kwargs)
46+
47+
@glpk_interface.Variable.type.setter
48+
def type(self, value):
49+
if value in ("integer", "binary"):
50+
raise ValueError("The GLPK exact solver does not support integer and mixed integer problems")
51+
super(Variable, Variable).type.fset(self, value)
52+
53+
54+
@six.add_metaclass(inheritdocstring)
55+
class Constraint(glpk_interface.Constraint):
56+
pass
57+
58+
59+
@six.add_metaclass(inheritdocstring)
60+
class Objective(glpk_interface.Objective):
61+
pass
62+
63+
64+
@six.add_metaclass(inheritdocstring)
65+
class Configuration(glpk_interface.Configuration):
66+
pass
67+
68+
69+
@six.add_metaclass(inheritdocstring)
70+
class Model(glpk_interface.Model):
71+
def _run_glp_exact(self):
72+
return_value = glp_exact(self.problem, self.configuration._smcp)
73+
glpk_status = glp_get_status(self.problem)
74+
if return_value == 0:
75+
status = _GLPK_STATUS_TO_STATUS[glpk_status]
76+
elif return_value == GLP_ETMLIM:
77+
status = interface.TIME_LIMIT
78+
else:
79+
status = _GLPK_STATUS_TO_STATUS[glpk_status]
80+
if status == interface.UNDEFINED:
81+
log.debug("Status undefined. GLPK status code returned by glp_simplex was %d" % return_value)
82+
return status
83+
84+
def _optimize(self):
85+
# Solving inexact first per GLPK manual
86+
# Computations in exact arithmetic are very time consuming, so solving LP
87+
# problem with the routine glp_exact from the very beginning is not a good
88+
# idea. It is much better at first to find an optimal basis with the routine
89+
# glp_simplex and only then to call glp_exact, in which case only a few
90+
# simplex iterations need to be performed in exact arithmetic.
91+
status = super(Model, self)._optimize()
92+
if status != interface.OPTIMAL:
93+
return status
94+
else:
95+
status = self._run_glp_exact()
96+
97+
if status == interface.UNDEFINED and self.configuration.presolve is True:
98+
# If presolve is on, status will be undefined if not optimal
99+
self.configuration.presolve = False
100+
status = self._run_glp_exact()
101+
self.configuration.presolve = True
102+
return status
103+
104+
105+
if __name__ == '__main__':
106+
import pickle
107+
108+
x1 = Variable('x1', lb=0)
109+
x2 = Variable('x2', lb=0)
110+
x3 = Variable('x3', lb=0, ub=1, type='binary')
111+
c1 = Constraint(x1 + x2 + x3, lb=-100, ub=100, name='c1')
112+
c2 = Constraint(10 * x1 + 4 * x2 + 5 * x3, ub=600, name='c2')
113+
c3 = Constraint(2 * x1 + 2 * x2 + 6 * x3, ub=300, name='c3')
114+
obj = Objective(10 * x1 + 6 * x2 + 4 * x3, direction='max')
115+
model = Model(name='Simple model')
116+
model.objective = obj
117+
model.add([c1, c2, c3])
118+
model.configuration.verbosity = 3
119+
status = model.optimize()
120+
print("status:", model.status)
121+
print("objective value:", model.objective.value)
122+
123+
for var_name, var in model.variables.items():
124+
print(var_name, "=", var.primal)
125+
126+
print(model)
127+
128+
problem = glp_create_prob()
129+
glp_read_lp(problem, None, "tests/data/model.lp")
130+
131+
solver = Model(problem=problem)
132+
print(solver.optimize())
133+
print(solver.objective)
134+
135+
import time
136+
137+
t1 = time.time()
138+
print("pickling")
139+
pickle_string = pickle.dumps(solver)
140+
resurrected_solver = pickle.loads(pickle_string)
141+
t2 = time.time()
142+
print("Execution time: %s" % (t2 - t1))
143+
144+
resurrected_solver.optimize()
145+
print("Halelujah!", resurrected_solver.objective.value)

0 commit comments

Comments
 (0)