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.
@@ -1,13 +1,11 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: OptlangHelper
3
- Version: 0.0.2
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()
@@ -6,4 +6,5 @@ OptlangHelper.egg-info/dependency_links.txt
6
6
  OptlangHelper.egg-info/requires.txt
7
7
  OptlangHelper.egg-info/top_level.txt
8
8
  optlanghelper/__init__.py
9
- optlanghelper/optlanghelper.py
9
+ optlanghelper/optlanghelper.py
10
+ tests/test_optlanghelper.py
@@ -0,0 +1,6 @@
1
+ optlang
2
+
3
+ [matrix]
4
+ numpy
5
+ scipy
6
+ gurobipy
@@ -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.2",
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