OptlangHelper 0.0.2__tar.gz → 0.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {OptlangHelper-0.0.2 → optlanghelper-0.0.3/OptlangHelper.egg-info}/PKG-INFO +47 -4
- {OptlangHelper-0.0.2 → optlanghelper-0.0.3}/OptlangHelper.egg-info/SOURCES.txt +2 -1
- optlanghelper-0.0.3/OptlangHelper.egg-info/requires.txt +6 -0
- OptlangHelper-0.0.2/README.rst → optlanghelper-0.0.3/PKG-INFO +66 -0
- OptlangHelper-0.0.2/OptlangHelper.egg-info/PKG-INFO → optlanghelper-0.0.3/README.rst +30 -21
- optlanghelper-0.0.3/optlanghelper/__init__.py +15 -0
- optlanghelper-0.0.3/optlanghelper/optlanghelper.py +254 -0
- {OptlangHelper-0.0.2 → optlanghelper-0.0.3}/setup.py +4 -4
- optlanghelper-0.0.3/tests/test_optlanghelper.py +81 -0
- OptlangHelper-0.0.2/OptlangHelper.egg-info/requires.txt +0 -1
- OptlangHelper-0.0.2/optlanghelper/__init__.py +0 -15
- OptlangHelper-0.0.2/optlanghelper/optlanghelper.py +0 -134
- {OptlangHelper-0.0.2 → optlanghelper-0.0.3}/OptlangHelper.egg-info/dependency_links.txt +0 -0
- {OptlangHelper-0.0.2 → optlanghelper-0.0.3}/OptlangHelper.egg-info/top_level.txt +0 -0
- {OptlangHelper-0.0.2 → optlanghelper-0.0.3}/setup.cfg +0 -0
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: OptlangHelper
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: Python package for building Linear Programming models that are compatible with Optlang
|
|
5
5
|
Home-page: https://github.com/Freiburgermsu/OptlangHelper
|
|
6
6
|
Author: Andrew Freiburger
|
|
7
7
|
Author-email: afreiburger@anl.gov
|
|
8
|
-
License: UNKNOWN
|
|
9
8
|
Project-URL: Issues, https://github.com/Freiburgermsu/OptlangHelper/issues
|
|
10
|
-
Platform: UNKNOWN
|
|
11
9
|
Classifier: Development Status :: 3 - Alpha
|
|
12
10
|
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
13
11
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -18,6 +16,21 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
18
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
17
|
Classifier: Natural Language :: English
|
|
20
18
|
Description-Content-Type: text/x-rst
|
|
19
|
+
Requires-Dist: optlang
|
|
20
|
+
Provides-Extra: matrix
|
|
21
|
+
Requires-Dist: numpy; extra == "matrix"
|
|
22
|
+
Requires-Dist: scipy; extra == "matrix"
|
|
23
|
+
Requires-Dist: gurobipy; extra == "matrix"
|
|
24
|
+
Dynamic: author
|
|
25
|
+
Dynamic: author-email
|
|
26
|
+
Dynamic: classifier
|
|
27
|
+
Dynamic: description
|
|
28
|
+
Dynamic: description-content-type
|
|
29
|
+
Dynamic: home-page
|
|
30
|
+
Dynamic: project-url
|
|
31
|
+
Dynamic: provides-extra
|
|
32
|
+
Dynamic: requires-dist
|
|
33
|
+
Dynamic: summary
|
|
21
34
|
|
|
22
35
|
|
|
23
36
|
|
|
@@ -168,4 +181,34 @@ Example
|
|
|
168
181
|
model = define_model("< model name>", list(variables.values()), list(constraints.values()), objective, True)
|
|
169
182
|
|
|
170
183
|
|
|
184
|
+
Release 0.0.3
|
|
185
|
+
----------------------
|
|
186
|
+
|
|
187
|
+
Bug fixes
|
|
188
|
+
|
|
189
|
+
+ the published 0.0.2 wheel was unimportable (``__init__`` imported from the package
|
|
190
|
+
name instead of the submodule) and crashed on nested expressions (a stale internal
|
|
191
|
+
reference to the pre-rename class); both are fixed and covered by tests.
|
|
192
|
+
+ ``__version__`` is synchronized with the package metadata, and the import-time
|
|
193
|
+
``print`` is now a debug log line.
|
|
194
|
+
|
|
195
|
+
New
|
|
196
|
+
|
|
197
|
+
+ the historic ``OptlangHelper`` class is restored as the solver-agnostic base;
|
|
198
|
+
``GLPKHelper`` / ``CPLEXHelper`` / ``GurobiHelper`` are interface-pinned subclasses
|
|
199
|
+
(previously the latter two were empty stubs).
|
|
200
|
+
+ ``MatrixHelper.define_model`` builds very large LPs as a sparse CSR matrix directly
|
|
201
|
+
through ``gurobipy`` (install with ``pip install OptlangHelper[matrix]``): optlang's
|
|
202
|
+
per-constraint interfaces scale super-linearly (~n^1.8 measured on Gurobi — 48 s at
|
|
203
|
+
1e5 constraints, ~90 min extrapolated at 1.4e6), while the CSR path ingests 1.4e6
|
|
204
|
+
constraints in seconds.
|
|
205
|
+
+ ``define_model_auto(name, variables, constraints, objective, limit=300000)``
|
|
206
|
+
dispatches by size: an optlang model at or below ``limit`` constraints, a gurobipy
|
|
207
|
+
model above it (both expose ``.optimize()``; their result APIs differ).
|
|
208
|
+
|
|
209
|
+
.. code-block:: python
|
|
210
|
+
|
|
211
|
+
from optlanghelper import define_model_auto
|
|
171
212
|
|
|
213
|
+
model = define_model_auto("community_fba", variables, constraints, objective)
|
|
214
|
+
model.optimize()
|
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: OptlangHelper
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: Python package for building Linear Programming models that are compatible with Optlang
|
|
5
|
+
Home-page: https://github.com/Freiburgermsu/OptlangHelper
|
|
6
|
+
Author: Andrew Freiburger
|
|
7
|
+
Author-email: afreiburger@anl.gov
|
|
8
|
+
Project-URL: Issues, https://github.com/Freiburgermsu/OptlangHelper/issues
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Natural Language :: English
|
|
18
|
+
Description-Content-Type: text/x-rst
|
|
19
|
+
Requires-Dist: optlang
|
|
20
|
+
Provides-Extra: matrix
|
|
21
|
+
Requires-Dist: numpy; extra == "matrix"
|
|
22
|
+
Requires-Dist: scipy; extra == "matrix"
|
|
23
|
+
Requires-Dist: gurobipy; extra == "matrix"
|
|
24
|
+
Dynamic: author
|
|
25
|
+
Dynamic: author-email
|
|
26
|
+
Dynamic: classifier
|
|
27
|
+
Dynamic: description
|
|
28
|
+
Dynamic: description-content-type
|
|
29
|
+
Dynamic: home-page
|
|
30
|
+
Dynamic: project-url
|
|
31
|
+
Dynamic: provides-extra
|
|
32
|
+
Dynamic: requires-dist
|
|
33
|
+
Dynamic: summary
|
|
34
|
+
|
|
1
35
|
|
|
2
36
|
|
|
3
37
|
Efficiently creating optimization models
|
|
@@ -146,3 +180,35 @@ Example
|
|
|
146
180
|
# create an optlang model from all of the variables, constraints, and objective defined above
|
|
147
181
|
model = define_model("< model name>", list(variables.values()), list(constraints.values()), objective, True)
|
|
148
182
|
|
|
183
|
+
|
|
184
|
+
Release 0.0.3
|
|
185
|
+
----------------------
|
|
186
|
+
|
|
187
|
+
Bug fixes
|
|
188
|
+
|
|
189
|
+
+ the published 0.0.2 wheel was unimportable (``__init__`` imported from the package
|
|
190
|
+
name instead of the submodule) and crashed on nested expressions (a stale internal
|
|
191
|
+
reference to the pre-rename class); both are fixed and covered by tests.
|
|
192
|
+
+ ``__version__`` is synchronized with the package metadata, and the import-time
|
|
193
|
+
``print`` is now a debug log line.
|
|
194
|
+
|
|
195
|
+
New
|
|
196
|
+
|
|
197
|
+
+ the historic ``OptlangHelper`` class is restored as the solver-agnostic base;
|
|
198
|
+
``GLPKHelper`` / ``CPLEXHelper`` / ``GurobiHelper`` are interface-pinned subclasses
|
|
199
|
+
(previously the latter two were empty stubs).
|
|
200
|
+
+ ``MatrixHelper.define_model`` builds very large LPs as a sparse CSR matrix directly
|
|
201
|
+
through ``gurobipy`` (install with ``pip install OptlangHelper[matrix]``): optlang's
|
|
202
|
+
per-constraint interfaces scale super-linearly (~n^1.8 measured on Gurobi — 48 s at
|
|
203
|
+
1e5 constraints, ~90 min extrapolated at 1.4e6), while the CSR path ingests 1.4e6
|
|
204
|
+
constraints in seconds.
|
|
205
|
+
+ ``define_model_auto(name, variables, constraints, objective, limit=300000)``
|
|
206
|
+
dispatches by size: an optlang model at or below ``limit`` constraints, a gurobipy
|
|
207
|
+
model above it (both expose ``.optimize()``; their result APIs differ).
|
|
208
|
+
|
|
209
|
+
.. code-block:: python
|
|
210
|
+
|
|
211
|
+
from optlanghelper import define_model_auto
|
|
212
|
+
|
|
213
|
+
model = define_model_auto("community_fba", variables, constraints, objective)
|
|
214
|
+
model.optimize()
|
|
@@ -1,24 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: OptlangHelper
|
|
3
|
-
Version: 0.0.2
|
|
4
|
-
Summary: Python package for building Linear Programming models that are compatible with Optlang
|
|
5
|
-
Home-page: https://github.com/Freiburgermsu/OptlangHelper
|
|
6
|
-
Author: Andrew Freiburger
|
|
7
|
-
Author-email: afreiburger@anl.gov
|
|
8
|
-
License: UNKNOWN
|
|
9
|
-
Project-URL: Issues, https://github.com/Freiburgermsu/OptlangHelper/issues
|
|
10
|
-
Platform: UNKNOWN
|
|
11
|
-
Classifier: Development Status :: 3 - Alpha
|
|
12
|
-
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
13
|
-
Classifier: Intended Audience :: Science/Research
|
|
14
|
-
Classifier: Operating System :: OS Independent
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
-
Classifier: Natural Language :: English
|
|
20
|
-
Description-Content-Type: text/x-rst
|
|
21
|
-
|
|
22
1
|
|
|
23
2
|
|
|
24
3
|
Efficiently creating optimization models
|
|
@@ -168,4 +147,34 @@ Example
|
|
|
168
147
|
model = define_model("< model name>", list(variables.values()), list(constraints.values()), objective, True)
|
|
169
148
|
|
|
170
149
|
|
|
150
|
+
Release 0.0.3
|
|
151
|
+
----------------------
|
|
152
|
+
|
|
153
|
+
Bug fixes
|
|
154
|
+
|
|
155
|
+
+ the published 0.0.2 wheel was unimportable (``__init__`` imported from the package
|
|
156
|
+
name instead of the submodule) and crashed on nested expressions (a stale internal
|
|
157
|
+
reference to the pre-rename class); both are fixed and covered by tests.
|
|
158
|
+
+ ``__version__`` is synchronized with the package metadata, and the import-time
|
|
159
|
+
``print`` is now a debug log line.
|
|
160
|
+
|
|
161
|
+
New
|
|
162
|
+
|
|
163
|
+
+ the historic ``OptlangHelper`` class is restored as the solver-agnostic base;
|
|
164
|
+
``GLPKHelper`` / ``CPLEXHelper`` / ``GurobiHelper`` are interface-pinned subclasses
|
|
165
|
+
(previously the latter two were empty stubs).
|
|
166
|
+
+ ``MatrixHelper.define_model`` builds very large LPs as a sparse CSR matrix directly
|
|
167
|
+
through ``gurobipy`` (install with ``pip install OptlangHelper[matrix]``): optlang's
|
|
168
|
+
per-constraint interfaces scale super-linearly (~n^1.8 measured on Gurobi — 48 s at
|
|
169
|
+
1e5 constraints, ~90 min extrapolated at 1.4e6), while the CSR path ingests 1.4e6
|
|
170
|
+
constraints in seconds.
|
|
171
|
+
+ ``define_model_auto(name, variables, constraints, objective, limit=300000)``
|
|
172
|
+
dispatches by size: an optlang model at or below ``limit`` constraints, a gurobipy
|
|
173
|
+
model above it (both expose ``.optimize()``; their result APIs differ).
|
|
174
|
+
|
|
175
|
+
.. code-block:: python
|
|
176
|
+
|
|
177
|
+
from optlanghelper import define_model_auto
|
|
171
178
|
|
|
179
|
+
model = define_model_auto("community_fba", variables, constraints, objective)
|
|
180
|
+
model.optimize()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from __future__ import absolute_import
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
__author__ = "Andrew Freiburger"
|
|
8
|
+
__email__ = "afreiburger@anl.gov"
|
|
9
|
+
__version__ = "0.0.3"
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
logger.debug("OptlangHelper %s", __version__)
|
|
13
|
+
|
|
14
|
+
from .optlanghelper import (OptlangHelper, GLPKHelper, CPLEXHelper, GurobiHelper, MatrixHelper,
|
|
15
|
+
Bounds, tupVariable, tupConstraint, tupObjective, define_model_auto)
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Created on Thu Aug 18 10:26:32 2022
|
|
4
|
+
@author: Andrew Freiburger
|
|
5
|
+
"""
|
|
6
|
+
from collections import namedtuple
|
|
7
|
+
from importlib import import_module
|
|
8
|
+
from optlang import Model
|
|
9
|
+
from typing import Iterable, Union
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
Bounds = namedtuple("Bounds", ("lb", "ub"), defaults=(0,1000))
|
|
15
|
+
tupVariable = namedtuple("tupVariable", ("name", "bounds", "type"), defaults=("varName", Bounds(), "continuous"))
|
|
16
|
+
tupConstraint = namedtuple("tupConstraint", ("name", "bounds", "expr"), defaults=("consName", Bounds(0,0), None))
|
|
17
|
+
tupObjective = namedtuple("tupObjective", ("name", "expr", "direction"), defaults=("objectiveName", None, "max"))
|
|
18
|
+
|
|
19
|
+
def isIterable(term):
|
|
20
|
+
try:
|
|
21
|
+
iter(term)
|
|
22
|
+
if type(term) is not str: return True
|
|
23
|
+
return False
|
|
24
|
+
except: return False
|
|
25
|
+
|
|
26
|
+
def isnumber(obj):
|
|
27
|
+
try: float(obj) ; return True
|
|
28
|
+
except: return False
|
|
29
|
+
|
|
30
|
+
def define_term(value):
|
|
31
|
+
if isnumber(value):
|
|
32
|
+
return {"type":"Number", "value": value}
|
|
33
|
+
if isinstance(value, str):
|
|
34
|
+
return {"type":"Symbol", "name": value}
|
|
35
|
+
logger.error(f"The {value} of type {type(value)} is not known.")
|
|
36
|
+
|
|
37
|
+
def get_expression_template(expr):
|
|
38
|
+
if isinstance(expr, list):
|
|
39
|
+
return {"type": "Add", "args": []}
|
|
40
|
+
return {"type": expr["operation"], "args": []}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class OptlangHelper:
|
|
44
|
+
"""Bulk construction of an optlang model from tuple/dict descriptions.
|
|
45
|
+
|
|
46
|
+
``interface`` pins the optlang solver interface (``"glpk"``, ``"cplex"``,
|
|
47
|
+
``"gurobi"``); when None, optlang's preferred available interface is used
|
|
48
|
+
(whatever ``from optlang import Model`` resolves to)."""
|
|
49
|
+
|
|
50
|
+
interface = None
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def _model_cls(cls):
|
|
54
|
+
if cls.interface is None:
|
|
55
|
+
return Model
|
|
56
|
+
try:
|
|
57
|
+
return import_module(f"optlang.{cls.interface}_interface").Model
|
|
58
|
+
except Exception as exc:
|
|
59
|
+
raise ImportError(
|
|
60
|
+
f"The optlang {cls.interface} interface is unavailable: {exc}") from exc
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def add_variables(var_name:str, var_bounds:(list, tuple), var_type:str="continuous"):
|
|
64
|
+
assert var_bounds[0] <= var_bounds[1], f"The {var_name} variable lower bound {var_bounds[0]} is greater than the upper bound {var_bounds[1]}. The lower bound must be less than the upper bound."
|
|
65
|
+
return {"name": var_name.replace(" ", "_"), "lb": var_bounds[0], "ub": var_bounds[1], "type": var_type}
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def add_constraint(cls, cons_name:str, cons_bounds:(list, tuple), cons_expr:dict):
|
|
69
|
+
assert cons_bounds[0] is None or cons_bounds[1] is None or cons_bounds[0] <= cons_bounds[1], f"The {cons_name} constraint lower bound {cons_bounds[0]} is greater than the upper bound {cons_bounds[1]}. The lower bound must be less than the upper bound."
|
|
70
|
+
return {"name": cons_name.replace(" ", "_"),
|
|
71
|
+
"expression": cls._define_expression(cons_expr),
|
|
72
|
+
"lb": cons_bounds[0], "ub": cons_bounds[1], "indicator_variable": None, "active_when": 1}
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def add_objective(cls, obj_name:str, objective_expr:Union[dict, list], direction:str):
|
|
76
|
+
if isinstance(objective_expr, list):
|
|
77
|
+
obj_expr = {"type": "Add", "args": [
|
|
78
|
+
cls._define_expression(expr) for expr in objective_expr]}
|
|
79
|
+
elif isinstance(objective_expr, dict):
|
|
80
|
+
obj_expr = {"type": objective_expr["operation"],
|
|
81
|
+
"args": [define_term(term) for term in objective_expr["elements"]]}
|
|
82
|
+
return {"name": obj_name.replace(" ", "_"), "expression": obj_expr, "direction": direction}
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def define_model(cls, model_name, variables, constraints, objective, optlang=False):
|
|
86
|
+
model = {'name':model_name, 'variables':[], 'constraints':[]}
|
|
87
|
+
for var in variables:
|
|
88
|
+
var = list(var)
|
|
89
|
+
if len(var) == 2: var.append("continuous")
|
|
90
|
+
model["variables"].append(cls.add_variables(var[0], var[1], var[2]))
|
|
91
|
+
for cons in constraints:
|
|
92
|
+
model["constraints"].append(cls.add_constraint(cons[0], cons[1], cons[2]))
|
|
93
|
+
model["objective"] = cls.add_objective(objective[0], objective[1], objective[2])
|
|
94
|
+
if optlang: return cls._model_cls().from_json(model)
|
|
95
|
+
return model
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def _define_expression(cls, expr:dict):
|
|
99
|
+
expression = get_expression_template(expr)
|
|
100
|
+
level1_coef = 0
|
|
101
|
+
for ele in expr["elements"]:
|
|
102
|
+
if not isnumber(ele) and not isinstance(ele, str):
|
|
103
|
+
arguments = []
|
|
104
|
+
level2_coef = 0
|
|
105
|
+
for ele2 in ele["elements"]:
|
|
106
|
+
if not isnumber(ele2) and not isinstance(ele2, str):
|
|
107
|
+
arguments.append(cls._define_expression(ele2))
|
|
108
|
+
elif isinstance(ele2, str): arguments.append(define_term(ele2))
|
|
109
|
+
else: level2_coef += float(ele2)
|
|
110
|
+
expression["args"].append(get_expression_template(ele))
|
|
111
|
+
if level2_coef != 0: arguments.append(define_term(level2_coef))
|
|
112
|
+
expression["args"][-1]["args"] = arguments
|
|
113
|
+
elif isinstance(ele, str): expression["args"].append(define_term(ele))
|
|
114
|
+
else: level1_coef += float(ele)
|
|
115
|
+
if level1_coef != 0: expression["args"].append(define_term(level1_coef))
|
|
116
|
+
return expression
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def dot_product(zipped_to_sum, heuns_coefs=None):
|
|
120
|
+
# ensure that the lengths are compatible for heun's dot-products
|
|
121
|
+
if heuns_coefs is not None:
|
|
122
|
+
coefs = heuns_coefs if isinstance(heuns_coefs, (list, set)) else heuns_coefs.tolist()
|
|
123
|
+
zipped_length = len(zipped_to_sum); coefs_length = len(coefs)
|
|
124
|
+
if zipped_length != coefs_length:
|
|
125
|
+
raise IndexError(f"ERROR: The length of zipped elements {zipped_length}"
|
|
126
|
+
f" is unequal to that of coefficients {coefs_length}")
|
|
127
|
+
|
|
128
|
+
elements = []
|
|
129
|
+
for index, (term1, term2) in enumerate(zipped_to_sum):
|
|
130
|
+
if heuns_coefs is not None:
|
|
131
|
+
elements.extend([{"operation": "Mul", "elements": [heuns_coefs[index], term1]},
|
|
132
|
+
{"operation": "Mul", "elements": [heuns_coefs[index], term2]}])
|
|
133
|
+
else:
|
|
134
|
+
elements.append({"operation": "Mul", "elements": [term1, term2]})
|
|
135
|
+
return elements
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class GLPKHelper(OptlangHelper):
|
|
139
|
+
interface = "glpk"
|
|
140
|
+
|
|
141
|
+
class CPLEXHelper(OptlangHelper):
|
|
142
|
+
interface = "cplex"
|
|
143
|
+
|
|
144
|
+
class GurobiHelper(OptlangHelper):
|
|
145
|
+
interface = "gurobi"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# --------------------------------------------------------------------------- matrix path
|
|
149
|
+
def _linear_terms(expr, var_index):
|
|
150
|
+
"""Flatten a tuple-format linear expression into ({var_idx: coef}, constant).
|
|
151
|
+
|
|
152
|
+
Handles the shapes produced by ``dot_product``/``tupConstraint``: an Add of
|
|
153
|
+
Mul-dicts, bare variable names, and numeric constants; Muls may hold the
|
|
154
|
+
coefficient and variable in either order. Nested Adds are recursed."""
|
|
155
|
+
coefs, const = {}, 0.0
|
|
156
|
+
elements = expr["elements"] if isinstance(expr, dict) else expr
|
|
157
|
+
for ele in elements:
|
|
158
|
+
if isinstance(ele, str):
|
|
159
|
+
coefs[var_index[ele]] = coefs.get(var_index[ele], 0.0) + 1.0
|
|
160
|
+
elif isnumber(ele):
|
|
161
|
+
const += float(ele)
|
|
162
|
+
elif isinstance(ele, dict) and ele.get("operation") == "Mul":
|
|
163
|
+
coef, name = 1.0, None
|
|
164
|
+
for ele2 in ele["elements"]:
|
|
165
|
+
if isinstance(ele2, str): name = ele2
|
|
166
|
+
elif isnumber(ele2): coef *= float(ele2)
|
|
167
|
+
else: raise ValueError(f"Non-linear or nested Mul term is not supported by the matrix path: {ele}")
|
|
168
|
+
if name is None: const += coef
|
|
169
|
+
else: coefs[var_index[name]] = coefs.get(var_index[name], 0.0) + coef
|
|
170
|
+
elif isinstance(ele, dict) and ele.get("operation") == "Add":
|
|
171
|
+
sub_coefs, sub_const = _linear_terms(ele, var_index)
|
|
172
|
+
const += sub_const
|
|
173
|
+
for j, c in sub_coefs.items(): coefs[j] = coefs.get(j, 0.0) + c
|
|
174
|
+
else:
|
|
175
|
+
raise ValueError(f"Unsupported expression element for the matrix path: {ele}")
|
|
176
|
+
return coefs, const
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class MatrixHelper:
|
|
180
|
+
"""CSR/gurobipy construction for very large LPs.
|
|
181
|
+
|
|
182
|
+
optlang's per-constraint interfaces scale super-linearly (~n^1.8 measured on
|
|
183
|
+
Gurobi: 48 s at 1e5 constraints, ~90 min extrapolated at 1.4e6), while
|
|
184
|
+
gurobipy ingests a CSR matrix in C time (seconds at 1.4e6 constraints).
|
|
185
|
+
Requires numpy, scipy, and gurobipy (``pip install OptlangHelper[matrix]``).
|
|
186
|
+
Returns a ``gurobipy.Model`` (``.optimize()``; status 2 == optimal), NOT an
|
|
187
|
+
optlang model."""
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def define_model(model_name, variables, constraints, objective, verbose=False):
|
|
191
|
+
import numpy as np
|
|
192
|
+
from scipy.sparse import coo_matrix
|
|
193
|
+
import gurobipy as gp
|
|
194
|
+
|
|
195
|
+
names, lbs, ubs = [], [], []
|
|
196
|
+
for var in variables:
|
|
197
|
+
var = list(var)
|
|
198
|
+
name = var[0].replace(" ", "_")
|
|
199
|
+
names.append(name); lbs.append(float(var[1][0])); ubs.append(float(var[1][1]))
|
|
200
|
+
var_index = {n: j for j, n in enumerate(names)}
|
|
201
|
+
|
|
202
|
+
rows_eq, rows_le, rows_ge = [], [], [] # (coefs, rhs)
|
|
203
|
+
for cons in constraints:
|
|
204
|
+
_, bounds, expr = cons[0], cons[1], cons[2]
|
|
205
|
+
coefs, const = _linear_terms(expr, var_index)
|
|
206
|
+
lb = None if bounds[0] is None else float(bounds[0]) - const
|
|
207
|
+
ub = None if bounds[1] is None else float(bounds[1]) - const
|
|
208
|
+
if lb is not None and ub is not None and lb == ub:
|
|
209
|
+
rows_eq.append((coefs, lb))
|
|
210
|
+
else:
|
|
211
|
+
if ub is not None: rows_le.append((coefs, ub))
|
|
212
|
+
if lb is not None: rows_ge.append((coefs, lb))
|
|
213
|
+
|
|
214
|
+
env = gp.Env(empty=True); env.setParam("OutputFlag", 1 if verbose else 0); env.start()
|
|
215
|
+
model = gp.Model(model_name, env=env)
|
|
216
|
+
x = model.addMVar(len(names), lb=np.array(lbs), ub=np.array(ubs))
|
|
217
|
+
model._var_names = names
|
|
218
|
+
model._var_index = var_index
|
|
219
|
+
|
|
220
|
+
def add_block(rows, sense):
|
|
221
|
+
if not rows: return
|
|
222
|
+
r, c, v, rhs = [], [], [], []
|
|
223
|
+
for i, (coefs, b) in enumerate(rows):
|
|
224
|
+
rhs.append(b)
|
|
225
|
+
for j, coef in coefs.items():
|
|
226
|
+
r.append(i); c.append(j); v.append(coef)
|
|
227
|
+
S = coo_matrix((v, (r, c)), shape=(len(rows), len(names))).tocsr()
|
|
228
|
+
model.addMConstr(S, x, sense, np.array(rhs))
|
|
229
|
+
add_block(rows_eq, "="); add_block(rows_le, "<"); add_block(rows_ge, ">")
|
|
230
|
+
|
|
231
|
+
obj_expr = objective[1] if isinstance(objective[1], (list, dict)) else [objective[1]]
|
|
232
|
+
if isinstance(obj_expr, list):
|
|
233
|
+
obj_expr = {"elements": obj_expr, "operation": "Add"}
|
|
234
|
+
ocoefs, oconst = _linear_terms(obj_expr, var_index)
|
|
235
|
+
lin = gp.LinExpr(oconst)
|
|
236
|
+
for j, coef in ocoefs.items(): lin += coef * x[j]
|
|
237
|
+
model.setObjective(lin, gp.GRB.MAXIMIZE if objective[2] in ("max", "maximize") else gp.GRB.MINIMIZE)
|
|
238
|
+
model._mvar = x
|
|
239
|
+
return model
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def define_model_auto(model_name, variables, constraints, objective, limit=300000, helper=None):
|
|
243
|
+
"""Size-aware dispatch: optlang construction up to ``limit`` constraints,
|
|
244
|
+
CSR/gurobipy (``MatrixHelper``) beyond it.
|
|
245
|
+
|
|
246
|
+
Below the limit an OPTLANG model is returned (from ``helper`` or
|
|
247
|
+
``OptlangHelper``); above it a GUROBIPY model is returned — both expose
|
|
248
|
+
``.optimize()``, but their status/attribute APIs differ, so branch on
|
|
249
|
+
``isinstance`` or on ``len(constraints) > limit`` when reading results."""
|
|
250
|
+
if len(constraints) <= limit:
|
|
251
|
+
return (helper or OptlangHelper).define_model(model_name, variables, constraints, objective, optlang=True)
|
|
252
|
+
logger.info(f"{model_name}: {len(constraints)} constraints exceeds limit={limit}; "
|
|
253
|
+
"building through MatrixHelper (gurobipy CSR)")
|
|
254
|
+
return MatrixHelper.define_model(model_name, variables, constraints, objective)
|
|
@@ -7,7 +7,7 @@ with open("README.rst") as f:
|
|
|
7
7
|
|
|
8
8
|
setup(
|
|
9
9
|
name="OptlangHelper",
|
|
10
|
-
version="0.0.
|
|
10
|
+
version="0.0.3",
|
|
11
11
|
description="Python package for building Linear Programming models that are compatible with Optlang",
|
|
12
12
|
long_description_content_type="text/x-rst",
|
|
13
13
|
long_description=readme,
|
|
@@ -28,10 +28,10 @@ setup(
|
|
|
28
28
|
],
|
|
29
29
|
install_requires=[
|
|
30
30
|
"optlang",
|
|
31
|
-
# "glpk",
|
|
32
|
-
# "cplex",
|
|
33
|
-
# "gurobipy"
|
|
34
31
|
],
|
|
32
|
+
extras_require={
|
|
33
|
+
"matrix": ["numpy", "scipy", "gurobipy"],
|
|
34
|
+
},
|
|
35
35
|
# tests_require=[
|
|
36
36
|
# "pytest",
|
|
37
37
|
# ],
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from optlanghelper import (OptlangHelper, GLPKHelper, GurobiHelper, MatrixHelper,
|
|
3
|
+
Bounds, tupVariable, tupConstraint, tupObjective, define_model_auto)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _small_problem():
|
|
7
|
+
# max v0 s.t. v0 - v1 = 0; v1 + v2 <= 8; 0 <= v* <= 5
|
|
8
|
+
variables = [[f"v{i}", [0, 5]] for i in range(3)]
|
|
9
|
+
constraints = [
|
|
10
|
+
["c_eq", [0, 0], {"operation": "Add", "elements": [
|
|
11
|
+
{"operation": "Mul", "elements": [1.0, "v0"]},
|
|
12
|
+
{"operation": "Mul", "elements": [-1.0, "v1"]}]}],
|
|
13
|
+
["c_le", [None, 8], {"operation": "Add", "elements": [
|
|
14
|
+
{"operation": "Mul", "elements": [1.0, "v1"]},
|
|
15
|
+
{"operation": "Mul", "elements": [1.0, "v2"]}]}],
|
|
16
|
+
]
|
|
17
|
+
objective = ["obj", [{"operation": "Mul", "elements": [1.0, "v0"]}], "max"]
|
|
18
|
+
return variables, constraints, objective
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_optlang_default_interface():
|
|
22
|
+
v, c, o = _small_problem()
|
|
23
|
+
mdl = OptlangHelper.define_model("t", v, c, o, optlang=True)
|
|
24
|
+
mdl.optimize()
|
|
25
|
+
assert mdl.status == "optimal"
|
|
26
|
+
assert abs(mdl.objective.value - 5.0) < 1e-6
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_glpk_interface():
|
|
30
|
+
v, c, o = _small_problem()
|
|
31
|
+
mdl = GLPKHelper.define_model("t", v, c, o, optlang=True)
|
|
32
|
+
assert type(mdl).__module__ == "optlang.glpk_interface"
|
|
33
|
+
mdl.optimize()
|
|
34
|
+
assert abs(mdl.objective.value - 5.0) < 1e-6
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_gurobi_interface():
|
|
38
|
+
pytest.importorskip("gurobipy")
|
|
39
|
+
v, c, o = _small_problem()
|
|
40
|
+
mdl = GurobiHelper.define_model("t", v, c, o, optlang=True)
|
|
41
|
+
assert type(mdl).__module__ == "optlang.gurobi_interface"
|
|
42
|
+
mdl.optimize()
|
|
43
|
+
assert abs(mdl.objective.value - 5.0) < 1e-6
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_dict_output_backcompat():
|
|
47
|
+
v, c, o = _small_problem()
|
|
48
|
+
d = OptlangHelper.define_model("t", v, c, o, optlang=False)
|
|
49
|
+
assert set(d) == {"name", "variables", "constraints", "objective"}
|
|
50
|
+
assert len(d["variables"]) == 3 and len(d["constraints"]) == 2
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_matrix_helper_matches_optlang():
|
|
54
|
+
pytest.importorskip("gurobipy")
|
|
55
|
+
v, c, o = _small_problem()
|
|
56
|
+
g = MatrixHelper.define_model("t", v, c, o)
|
|
57
|
+
g.optimize()
|
|
58
|
+
assert g.Status == 2
|
|
59
|
+
assert abs(g.ObjVal - 5.0) < 1e-6
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_auto_dispatch():
|
|
63
|
+
pytest.importorskip("gurobipy")
|
|
64
|
+
v, c, o = _small_problem()
|
|
65
|
+
small = define_model_auto("t", v, c, o, limit=10)
|
|
66
|
+
assert hasattr(small, "objective") # optlang model
|
|
67
|
+
big = define_model_auto("t", v, c, o, limit=1)
|
|
68
|
+
assert type(big).__module__.startswith("gurobipy")
|
|
69
|
+
big.optimize()
|
|
70
|
+
assert abs(big.ObjVal - 5.0) < 1e-6
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_tuple_inputs_accepted():
|
|
74
|
+
v = [tupVariable(f"v{i}", Bounds(0, 5)) for i in range(2)]
|
|
75
|
+
c = [tupConstraint("c", Bounds(0, 0), {"operation": "Add", "elements": [
|
|
76
|
+
{"operation": "Mul", "elements": [1.0, "v0"]},
|
|
77
|
+
{"operation": "Mul", "elements": [-1.0, "v1"]}]})]
|
|
78
|
+
o = tupObjective("obj", [{"operation": "Mul", "elements": [1.0, "v0"]}], "max")
|
|
79
|
+
mdl = OptlangHelper.define_model("t", v, c, o, optlang=True)
|
|
80
|
+
mdl.optimize()
|
|
81
|
+
assert abs(mdl.objective.value - 5.0) < 1e-6
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
optlang
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
|
|
3
|
-
from __future__ import absolute_import
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
|
|
7
|
-
__author__ = "Andrew Freiburger"
|
|
8
|
-
__email__ = "afreiburger@anl.gov"
|
|
9
|
-
__version__ = "0.0.1"
|
|
10
|
-
|
|
11
|
-
logger = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
|
-
print("OptlangHelper", __version__)
|
|
14
|
-
|
|
15
|
-
from optlanghelper import GLPKHelper, CPLEXHelper, GurobiHelper, Bounds, tupVariable, tupConstraint, tupObjective
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""
|
|
3
|
-
Created on Thu Aug 18 10:26:32 2022
|
|
4
|
-
@author: Andrew Freiburger
|
|
5
|
-
"""
|
|
6
|
-
from collections import namedtuple
|
|
7
|
-
from optlang import Model
|
|
8
|
-
from typing import Iterable, Union
|
|
9
|
-
from pprint import pprint
|
|
10
|
-
import logging
|
|
11
|
-
|
|
12
|
-
logger = logging.getLogger(__name__)
|
|
13
|
-
|
|
14
|
-
Bounds = namedtuple("Bounds", ("lb", "ub"), defaults=(0,1000))
|
|
15
|
-
tupVariable = namedtuple("tupVariable", ("name", "bounds", "type"), defaults=("varName", Bounds(), "continuous"))
|
|
16
|
-
tupConstraint = namedtuple("tupConstraint", ("name", "bounds", "expr"), defaults=("consName", Bounds(0,0), None))
|
|
17
|
-
tupObjective = namedtuple("tupObjective", ("name", "expr", "direction"), defaults=("objectiveName", None, "max"))
|
|
18
|
-
|
|
19
|
-
def isIterable(term):
|
|
20
|
-
try:
|
|
21
|
-
iter(term)
|
|
22
|
-
if type(term) is not str: return True
|
|
23
|
-
return False
|
|
24
|
-
except: return False
|
|
25
|
-
|
|
26
|
-
def isnumber(obj):
|
|
27
|
-
try: float(obj) ; return True
|
|
28
|
-
except: return False
|
|
29
|
-
|
|
30
|
-
def define_term(value):
|
|
31
|
-
if isnumber(value):
|
|
32
|
-
return {"type":"Number", "value": value}
|
|
33
|
-
if isinstance(value, str):
|
|
34
|
-
return {"type":"Symbol", "name": value}
|
|
35
|
-
print(f"ERROR: The {value} of type {type(value)} is not known.")
|
|
36
|
-
|
|
37
|
-
def get_expression_template(expr):
|
|
38
|
-
# print(expr)
|
|
39
|
-
if isinstance(expr, list):
|
|
40
|
-
return {"type": "Add", "args": []}
|
|
41
|
-
return {"type": expr["operation"], "args": []}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class GLPKHelper:
|
|
45
|
-
|
|
46
|
-
@staticmethod
|
|
47
|
-
def add_variables(var_name:str, var_bounds:(list, tuple), var_type:str="continuous"):
|
|
48
|
-
assert var_bounds[0] <= var_bounds[1], f"The {var_name} variable lower bound {var_bounds[0]} is greater than the upper bound {var_bounds[1]}. The lower bound must be less than the upper bound."
|
|
49
|
-
return {"name": var_name.replace(" ", "_"), "lb": var_bounds[0], "ub": var_bounds[1], "type": var_type}
|
|
50
|
-
|
|
51
|
-
@staticmethod
|
|
52
|
-
def add_constraint(cons_name:str, cons_bounds:(list, tuple), cons_expr:dict):
|
|
53
|
-
assert cons_bounds[0] <= cons_bounds[1], f"The {cons_name} constraint lower bound {cons_bounds[0]} is greater than the upper bound {cons_bounds[1]}. The lower bound must be less than the upper bound."
|
|
54
|
-
return {"name": cons_name.replace(" ", "_"),
|
|
55
|
-
"expression": OptlangHelper._define_expression(cons_expr),
|
|
56
|
-
"lb": cons_bounds[0], "ub": cons_bounds[1], "indicator_variable": None, "active_when": 1}
|
|
57
|
-
|
|
58
|
-
@staticmethod
|
|
59
|
-
def add_objective(obj_name:str, objective_expr:Union[dict, list], direction:str):
|
|
60
|
-
if isinstance(objective_expr, list):
|
|
61
|
-
obj_expr = {"type": "Add", "args": [
|
|
62
|
-
OptlangHelper._define_expression(expr) for expr in objective_expr]}
|
|
63
|
-
elif isinstance(objective_expr, dict):
|
|
64
|
-
obj_expr = {"type": objective_expr["operation"],
|
|
65
|
-
"args": [define_term(term) for term in objective_expr["elements"]]}
|
|
66
|
-
return {"name": obj_name.replace(" ", "_"), "expression": obj_expr, "direction": direction}
|
|
67
|
-
|
|
68
|
-
@staticmethod
|
|
69
|
-
def define_model(model_name, variables, constraints, objective, optlang=False):
|
|
70
|
-
model = {'name':model_name, 'variables':[], 'constraints':[]}
|
|
71
|
-
# pprint(objective)
|
|
72
|
-
for var in variables:
|
|
73
|
-
if len(var) == 2: var.append("continuous")
|
|
74
|
-
model["variables"].append(OptlangHelper.add_variables(var[0], var[1], var[2]))
|
|
75
|
-
for cons in constraints:
|
|
76
|
-
model["constraints"].append(OptlangHelper.add_constraint(cons[0], cons[1], cons[2]))
|
|
77
|
-
# if not isinstance(obj, str): # catches a strange error of the objective name as the objective itself
|
|
78
|
-
model["objective"] = OptlangHelper.add_objective(objective[0], objective[1], objective[2])
|
|
79
|
-
if optlang: return Model.from_json(model)
|
|
80
|
-
return model
|
|
81
|
-
|
|
82
|
-
@staticmethod
|
|
83
|
-
def _define_expression(expr:dict):
|
|
84
|
-
expression = get_expression_template(expr)
|
|
85
|
-
level1_coef = 0
|
|
86
|
-
for ele in expr["elements"]:
|
|
87
|
-
if not isnumber(ele) and not isinstance(ele, str):
|
|
88
|
-
# print(expr, ele, end="\r")
|
|
89
|
-
arguments = []
|
|
90
|
-
level2_coef = 0
|
|
91
|
-
for ele2 in ele["elements"]:
|
|
92
|
-
if not isnumber(ele2) and not isinstance(ele2, str):
|
|
93
|
-
# print("recursive ele\t\t", type(ele2), ele2)
|
|
94
|
-
arguments.append(OptlangHelper._define_expression(ele2))
|
|
95
|
-
elif isinstance(ele2, str): arguments.append(define_term(ele2))
|
|
96
|
-
else: level2_coef += float(ele2)
|
|
97
|
-
expression["args"].append(get_expression_template(ele))
|
|
98
|
-
if level2_coef != 0: arguments.append(define_term(level2_coef))
|
|
99
|
-
expression["args"][-1]["args"] = arguments
|
|
100
|
-
elif isinstance(ele, str): expression["args"].append(define_term(ele))
|
|
101
|
-
else: level1_coef += float(ele)
|
|
102
|
-
if level1_coef != 0: expression["args"].append(define_term(level1_coef))
|
|
103
|
-
# pprint(expression)
|
|
104
|
-
return expression
|
|
105
|
-
|
|
106
|
-
@staticmethod
|
|
107
|
-
def dot_product(zipped_to_sum, heuns_coefs=None):
|
|
108
|
-
# ensure that the lengths are compatible for heun's dot-products
|
|
109
|
-
if heuns_coefs is not None:
|
|
110
|
-
coefs = heuns_coefs if isinstance(heuns_coefs, (list, set)) else heuns_coefs.tolist()
|
|
111
|
-
zipped_length = len(zipped_to_sum); coefs_length = len(coefs)
|
|
112
|
-
if zipped_length != coefs_length:
|
|
113
|
-
raise IndexError(f"ERROR: The length of zipped elements {zipped_length}"
|
|
114
|
-
f" is unequal to that of coefficients {coefs_length}")
|
|
115
|
-
|
|
116
|
-
elements = []
|
|
117
|
-
for index, (term1, term2) in enumerate(zipped_to_sum):
|
|
118
|
-
if heuns_coefs is not None:
|
|
119
|
-
elements.extend([{"operation": "Mul", "elements": [heuns_coefs[index], term1]},
|
|
120
|
-
{"operation": "Mul", "elements": [heuns_coefs[index], term2]}])
|
|
121
|
-
else:
|
|
122
|
-
elements.append({"operation": "Mul", "elements": [term1, term2]})
|
|
123
|
-
return elements
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
class CPLEXHelper:
|
|
128
|
-
pass
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
class GurobiHelper:
|
|
134
|
-
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|