desdeo 1.2__py3-none-any.whl → 2.0.0__py3-none-any.whl
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.
- desdeo/__init__.py +8 -8
- desdeo/api/README.md +73 -0
- desdeo/api/__init__.py +15 -0
- desdeo/api/app.py +40 -0
- desdeo/api/config.py +69 -0
- desdeo/api/config.toml +53 -0
- desdeo/api/db.py +25 -0
- desdeo/api/db_init.py +79 -0
- desdeo/api/db_models.py +164 -0
- desdeo/api/malaga_db_init.py +27 -0
- desdeo/api/models/__init__.py +66 -0
- desdeo/api/models/archive.py +34 -0
- desdeo/api/models/preference.py +90 -0
- desdeo/api/models/problem.py +507 -0
- desdeo/api/models/reference_point_method.py +18 -0
- desdeo/api/models/session.py +46 -0
- desdeo/api/models/state.py +96 -0
- desdeo/api/models/user.py +51 -0
- desdeo/api/routers/_NAUTILUS.py +245 -0
- desdeo/api/routers/_NAUTILUS_navigator.py +233 -0
- desdeo/api/routers/_NIMBUS.py +762 -0
- desdeo/api/routers/__init__.py +5 -0
- desdeo/api/routers/problem.py +110 -0
- desdeo/api/routers/reference_point_method.py +117 -0
- desdeo/api/routers/session.py +76 -0
- desdeo/api/routers/test.py +16 -0
- desdeo/api/routers/user_authentication.py +366 -0
- desdeo/api/schema.py +94 -0
- desdeo/api/tests/__init__.py +0 -0
- desdeo/api/tests/conftest.py +59 -0
- desdeo/api/tests/test_models.py +701 -0
- desdeo/api/tests/test_routes.py +216 -0
- desdeo/api/utils/database.py +274 -0
- desdeo/api/utils/logger.py +29 -0
- desdeo/core.py +27 -0
- desdeo/emo/__init__.py +29 -0
- desdeo/emo/hooks/archivers.py +172 -0
- desdeo/emo/methods/EAs.py +418 -0
- desdeo/emo/methods/__init__.py +0 -0
- desdeo/emo/methods/bases.py +59 -0
- desdeo/emo/operators/__init__.py +1 -0
- desdeo/emo/operators/crossover.py +780 -0
- desdeo/emo/operators/evaluator.py +118 -0
- desdeo/emo/operators/generator.py +356 -0
- desdeo/emo/operators/mutation.py +1053 -0
- desdeo/emo/operators/selection.py +1036 -0
- desdeo/emo/operators/termination.py +178 -0
- desdeo/explanations/__init__.py +6 -0
- desdeo/explanations/explainer.py +100 -0
- desdeo/explanations/utils.py +90 -0
- desdeo/mcdm/__init__.py +19 -0
- desdeo/mcdm/nautili.py +345 -0
- desdeo/mcdm/nautilus.py +477 -0
- desdeo/mcdm/nautilus_navigator.py +655 -0
- desdeo/mcdm/nimbus.py +417 -0
- desdeo/mcdm/pareto_navigator.py +269 -0
- desdeo/mcdm/reference_point_method.py +116 -0
- desdeo/problem/__init__.py +79 -0
- desdeo/problem/evaluator.py +561 -0
- desdeo/problem/gurobipy_evaluator.py +562 -0
- desdeo/problem/infix_parser.py +341 -0
- desdeo/problem/json_parser.py +944 -0
- desdeo/problem/pyomo_evaluator.py +468 -0
- desdeo/problem/schema.py +1808 -0
- desdeo/problem/simulator_evaluator.py +298 -0
- desdeo/problem/sympy_evaluator.py +244 -0
- desdeo/problem/testproblems/__init__.py +73 -0
- desdeo/problem/testproblems/binh_and_korn_problem.py +88 -0
- desdeo/problem/testproblems/dtlz2_problem.py +102 -0
- desdeo/problem/testproblems/forest_problem.py +275 -0
- desdeo/problem/testproblems/knapsack_problem.py +163 -0
- desdeo/problem/testproblems/mcwb_problem.py +831 -0
- desdeo/problem/testproblems/mixed_variable_dimenrions_problem.py +83 -0
- desdeo/problem/testproblems/momip_problem.py +172 -0
- desdeo/problem/testproblems/nimbus_problem.py +143 -0
- desdeo/problem/testproblems/pareto_navigator_problem.py +89 -0
- desdeo/problem/testproblems/re_problem.py +492 -0
- desdeo/problem/testproblems/river_pollution_problem.py +434 -0
- desdeo/problem/testproblems/rocket_injector_design_problem.py +140 -0
- desdeo/problem/testproblems/simple_problem.py +351 -0
- desdeo/problem/testproblems/simulator_problem.py +92 -0
- desdeo/problem/testproblems/spanish_sustainability_problem.py +945 -0
- desdeo/problem/testproblems/zdt_problem.py +271 -0
- desdeo/problem/utils.py +245 -0
- desdeo/tools/GenerateReferencePoints.py +181 -0
- desdeo/tools/__init__.py +102 -0
- desdeo/tools/generics.py +145 -0
- desdeo/tools/gurobipy_solver_interfaces.py +258 -0
- desdeo/tools/indicators_binary.py +11 -0
- desdeo/tools/indicators_unary.py +375 -0
- desdeo/tools/interaction_schema.py +38 -0
- desdeo/tools/intersection.py +54 -0
- desdeo/tools/iterative_pareto_representer.py +99 -0
- desdeo/tools/message.py +234 -0
- desdeo/tools/ng_solver_interfaces.py +199 -0
- desdeo/tools/non_dominated_sorting.py +133 -0
- desdeo/tools/patterns.py +281 -0
- desdeo/tools/proximal_solver.py +99 -0
- desdeo/tools/pyomo_solver_interfaces.py +464 -0
- desdeo/tools/reference_vectors.py +462 -0
- desdeo/tools/scalarization.py +3138 -0
- desdeo/tools/scipy_solver_interfaces.py +454 -0
- desdeo/tools/score_bands.py +464 -0
- desdeo/tools/utils.py +320 -0
- desdeo/utopia_stuff/__init__.py +0 -0
- desdeo/utopia_stuff/data/1.json +15 -0
- desdeo/utopia_stuff/data/2.json +13 -0
- desdeo/utopia_stuff/data/3.json +15 -0
- desdeo/utopia_stuff/data/4.json +17 -0
- desdeo/utopia_stuff/data/5.json +15 -0
- desdeo/utopia_stuff/from_json.py +40 -0
- desdeo/utopia_stuff/reinit_user.py +38 -0
- desdeo/utopia_stuff/utopia_db_init.py +212 -0
- desdeo/utopia_stuff/utopia_problem.py +403 -0
- desdeo/utopia_stuff/utopia_problem_old.py +415 -0
- desdeo/utopia_stuff/utopia_reference_solutions.py +79 -0
- desdeo-2.0.0.dist-info/LICENSE +21 -0
- desdeo-2.0.0.dist-info/METADATA +168 -0
- desdeo-2.0.0.dist-info/RECORD +120 -0
- {desdeo-1.2.dist-info → desdeo-2.0.0.dist-info}/WHEEL +1 -1
- desdeo-1.2.dist-info/METADATA +0 -16
- desdeo-1.2.dist-info/RECORD +0 -4
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
"""Defines a parser to parse multiobjective optimziation problems defined in a JSON format."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from functools import reduce
|
|
6
|
+
|
|
7
|
+
import gurobipy as gp
|
|
8
|
+
import numpy as np
|
|
9
|
+
import polars as pl
|
|
10
|
+
import pyomo.environ as pyomo
|
|
11
|
+
import sympy as sp
|
|
12
|
+
from pyomo.core.expr.numeric_expr import MaxExpression as _PyomoMax
|
|
13
|
+
from pyomo.core.expr.numeric_expr import MinExpression as _PyomoMin
|
|
14
|
+
|
|
15
|
+
# Mathematical objects in gurobipy can take many types
|
|
16
|
+
gpexpression = gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr | gp.GenExpr
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FormatEnum(str, Enum):
|
|
20
|
+
"""Enumerates the supported formats the JSON format may be parsed to."""
|
|
21
|
+
|
|
22
|
+
polars = "polars"
|
|
23
|
+
pyomo = "pyomo"
|
|
24
|
+
sympy = "sympy"
|
|
25
|
+
gurobipy = "gurobipy"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ParserError(Exception):
|
|
29
|
+
"""Raised when an error related to the MathParser class in encountered."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MathParser:
|
|
33
|
+
"""A class to instantiate MathJSON parsers.
|
|
34
|
+
|
|
35
|
+
Currently only parses MathJSON to polars expressions. Pyomo WIP.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, to_format: FormatEnum = "polars"):
|
|
39
|
+
"""Create a parser instance for parsing MathJSON notation into polars expressions.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
to_format (FormatEnum, optional): to which format a JSON representation should be parsed to.
|
|
43
|
+
Defaults to "polars".
|
|
44
|
+
"""
|
|
45
|
+
# Define operator names. Change these when the name is altered in the JSON format.
|
|
46
|
+
# Basic arithmetic operators
|
|
47
|
+
self.NEGATE: str = "Negate"
|
|
48
|
+
self.ADD: str = "Add"
|
|
49
|
+
self.SUB: str = "Subtract"
|
|
50
|
+
self.MUL: str = "Multiply"
|
|
51
|
+
self.DIV: str = "Divide"
|
|
52
|
+
|
|
53
|
+
# Vector and matrix operations
|
|
54
|
+
self.MATMUL: str = "MatMul"
|
|
55
|
+
self.SUM: str = "Sum"
|
|
56
|
+
self.RANDOM_ACCESS = "At"
|
|
57
|
+
|
|
58
|
+
# Exponentation and logarithms
|
|
59
|
+
self.EXP: str = "Exp"
|
|
60
|
+
self.LN: str = "Ln"
|
|
61
|
+
self.LB: str = "Lb"
|
|
62
|
+
self.LG: str = "Lg"
|
|
63
|
+
self.LOP: str = "LogOnePlus"
|
|
64
|
+
self.SQRT: str = "Sqrt"
|
|
65
|
+
self.SQUARE: str = "Square"
|
|
66
|
+
self.POW: str = "Power"
|
|
67
|
+
|
|
68
|
+
# Rounding operators
|
|
69
|
+
self.ABS: str = "Abs"
|
|
70
|
+
self.CEIL: str = "Ceil"
|
|
71
|
+
self.FLOOR: str = "Floor"
|
|
72
|
+
|
|
73
|
+
# Trigonometric operations
|
|
74
|
+
self.ARCCOS: str = "Arccos"
|
|
75
|
+
self.ARCCOSH: str = "Arccosh"
|
|
76
|
+
self.ARCSIN: str = "Arcsin"
|
|
77
|
+
self.ARCSINH: str = "Arcsinh"
|
|
78
|
+
self.ARCTAN: str = "Arctan"
|
|
79
|
+
self.ARCTANH: str = "Arctanh"
|
|
80
|
+
self.COS: str = "Cos"
|
|
81
|
+
self.COSH: str = "Cosh"
|
|
82
|
+
self.SIN: str = "Sin"
|
|
83
|
+
self.SINH: str = "Sinh"
|
|
84
|
+
self.TAN: str = "Tan"
|
|
85
|
+
self.TANH: str = "Tanh"
|
|
86
|
+
|
|
87
|
+
# Comparison operators
|
|
88
|
+
self.EQUAL: str = "Equal"
|
|
89
|
+
self.GREATER: str = "Greater"
|
|
90
|
+
self.GE: str = "GreaterEqual"
|
|
91
|
+
self.LESS: str = "Less"
|
|
92
|
+
self.LE: str = "LessEqual"
|
|
93
|
+
self.NE: str = "NotEqual"
|
|
94
|
+
|
|
95
|
+
# Other operators
|
|
96
|
+
self.MAX: str = "Max"
|
|
97
|
+
self.MIN: str = "Min"
|
|
98
|
+
self.RATIONAL: str = "Rational"
|
|
99
|
+
|
|
100
|
+
self.literals = int | float
|
|
101
|
+
|
|
102
|
+
def to_expr(x: self.literals | pl.Expr):
|
|
103
|
+
"""Helper function to convert literals to polars expressions."""
|
|
104
|
+
return pl.lit(x) if isinstance(x, self.literals) else x
|
|
105
|
+
|
|
106
|
+
def to_sympy_expr(x):
|
|
107
|
+
return sp.sympify(x, evaluate=False) if isinstance(x, self.literals) else x
|
|
108
|
+
|
|
109
|
+
def gp_error():
|
|
110
|
+
msg = "The gurobipy model format only supports linear and quadratic expressions."
|
|
111
|
+
ParserError(msg)
|
|
112
|
+
|
|
113
|
+
def _polars_reduce(ufunc, exprs):
|
|
114
|
+
def _reduce_function(acc, x, ufunc=ufunc):
|
|
115
|
+
acc_numpy = acc.to_numpy()
|
|
116
|
+
x_numpy = x.to_numpy()
|
|
117
|
+
|
|
118
|
+
if acc_numpy.shape == x_numpy.shape:
|
|
119
|
+
return pl.Series(values=ufunc(acc_numpy, x_numpy))
|
|
120
|
+
|
|
121
|
+
expanded_shape = acc_numpy.shape + (1,) * (x_numpy.ndim - acc_numpy.ndim)
|
|
122
|
+
|
|
123
|
+
return pl.Series(values=ufunc(acc_numpy.reshape(expanded_shape), x_numpy))
|
|
124
|
+
|
|
125
|
+
return pl.reduce(function=_reduce_function, exprs=exprs)
|
|
126
|
+
|
|
127
|
+
def _polars_reduce_unary(expr, ufunc):
|
|
128
|
+
def _reduce_function(acc, _, ufunc=ufunc):
|
|
129
|
+
return pl.Series(values=ufunc(acc.to_numpy()))
|
|
130
|
+
|
|
131
|
+
return pl.reduce(function=_reduce_function, exprs=[expr, None])
|
|
132
|
+
|
|
133
|
+
def _polars_reduce_matmul(*exprs):
|
|
134
|
+
def _reduce_function(acc, x):
|
|
135
|
+
acc = acc.to_numpy()
|
|
136
|
+
x = x.to_numpy()
|
|
137
|
+
|
|
138
|
+
if len(acc.shape) == 2 and len(x.shape) == 2:
|
|
139
|
+
# Row vectors, just return the dot product, polars does not handle
|
|
140
|
+
# "column" vectors anyway
|
|
141
|
+
return pl.Series(values=np.einsum("ij,ij->i", acc, x, optimize=True))
|
|
142
|
+
|
|
143
|
+
# actual matrix product required
|
|
144
|
+
return pl.Series(values=np.matmul(acc, x))
|
|
145
|
+
|
|
146
|
+
return pl.reduce(function=_reduce_function, exprs=exprs)
|
|
147
|
+
|
|
148
|
+
def _polars_summation(expr):
|
|
149
|
+
"""Polars matrix summation."""
|
|
150
|
+
|
|
151
|
+
def _reduce_function(acc, _):
|
|
152
|
+
acc_numpy = acc.to_numpy()
|
|
153
|
+
return pl.Series(values=np.sum(acc_numpy, axis=tuple(range(1, acc_numpy.ndim))))
|
|
154
|
+
|
|
155
|
+
return pl.reduce(function=_reduce_function, exprs=[expr, None])
|
|
156
|
+
|
|
157
|
+
def _polars_random_access(expr, *indices):
|
|
158
|
+
"""Polars tensor random access."""
|
|
159
|
+
for index in indices:
|
|
160
|
+
expr = expr.arr.get(index - 1) # 1 indexing assumed in JSON format
|
|
161
|
+
|
|
162
|
+
return expr
|
|
163
|
+
|
|
164
|
+
def _polars_generic_apply(a, b):
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
polars_env = {
|
|
168
|
+
# Define the operations for the different operators.
|
|
169
|
+
# Basic arithmetic operations
|
|
170
|
+
self.NEGATE: lambda x: _polars_reduce_unary(x, np.negative),
|
|
171
|
+
self.ADD: lambda *args: _polars_reduce(np.add, args),
|
|
172
|
+
self.SUB: lambda *args: _polars_reduce(np.subtract, args),
|
|
173
|
+
self.MUL: lambda *args: _polars_reduce(np.multiply, args),
|
|
174
|
+
self.DIV: lambda *args: _polars_reduce(np.divide, args),
|
|
175
|
+
# Vector and matrix operations
|
|
176
|
+
self.MATMUL: _polars_reduce_matmul,
|
|
177
|
+
self.SUM: lambda x: _polars_summation(x),
|
|
178
|
+
self.RANDOM_ACCESS: _polars_random_access,
|
|
179
|
+
# Exponentiation and logarithms
|
|
180
|
+
self.EXP: lambda x: _polars_reduce_unary(x, np.exp),
|
|
181
|
+
self.LN: lambda x: _polars_reduce_unary(x, np.log),
|
|
182
|
+
self.LB: lambda x: _polars_reduce_unary(x, np.log2),
|
|
183
|
+
self.LG: lambda x: _polars_reduce_unary(x, np.log10),
|
|
184
|
+
self.LOP: lambda x: _polars_reduce_unary(x, np.log1p),
|
|
185
|
+
self.SQRT: lambda x: _polars_reduce_unary(x, np.sqrt),
|
|
186
|
+
self.SQUARE: lambda x: _polars_reduce_unary(x, lambda y: np.power(y, 2)),
|
|
187
|
+
self.POW: lambda *args: _polars_reduce(np.power, args),
|
|
188
|
+
# Trigonometric operations
|
|
189
|
+
self.ARCCOS: lambda x: _polars_reduce_unary(x, np.arccos),
|
|
190
|
+
self.ARCCOSH: lambda x: _polars_reduce_unary(x, np.arccosh),
|
|
191
|
+
self.ARCSIN: lambda x: _polars_reduce_unary(x, np.arcsin),
|
|
192
|
+
self.ARCSINH: lambda x: _polars_reduce_unary(x, np.arcsinh),
|
|
193
|
+
self.ARCTAN: lambda x: _polars_reduce_unary(x, np.arctan),
|
|
194
|
+
self.ARCTANH: lambda x: _polars_reduce_unary(x, np.arctanh),
|
|
195
|
+
self.COS: lambda x: _polars_reduce_unary(x, np.cos),
|
|
196
|
+
self.COSH: lambda x: _polars_reduce_unary(x, np.cosh),
|
|
197
|
+
self.SIN: lambda x: _polars_reduce_unary(x, np.sin),
|
|
198
|
+
self.SINH: lambda x: _polars_reduce_unary(x, np.sinh),
|
|
199
|
+
self.TAN: lambda x: _polars_reduce_unary(x, np.tan),
|
|
200
|
+
self.TANH: lambda x: _polars_reduce_unary(x, np.tanh),
|
|
201
|
+
# Rounding operations
|
|
202
|
+
self.ABS: lambda x: _polars_reduce_unary(x, np.abs),
|
|
203
|
+
self.CEIL: lambda x: _polars_reduce_unary(x, np.ceil),
|
|
204
|
+
self.FLOOR: lambda x: _polars_reduce_unary(x, np.floor),
|
|
205
|
+
# Other operations
|
|
206
|
+
self.RATIONAL: lambda lst: reduce(lambda x, y: x / y, lst), # Not supported
|
|
207
|
+
self.MAX: lambda *args: reduce(lambda x, y: pl.max_horizontal(to_expr(x), to_expr(y)), args),
|
|
208
|
+
self.MIN: lambda *args: reduce(lambda x, y: pl.min_horizontal(to_expr(x), to_expr(y)), args),
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
def _pyomo_negate(x):
|
|
212
|
+
"""Negates the given operand."""
|
|
213
|
+
|
|
214
|
+
def _expr_negate_rule(x):
|
|
215
|
+
def _inner(_, *indices):
|
|
216
|
+
return -x[indices]
|
|
217
|
+
|
|
218
|
+
return _inner
|
|
219
|
+
|
|
220
|
+
def _negate(x):
|
|
221
|
+
# check if operand in indexed
|
|
222
|
+
if hasattr(x, "index_set") and x.is_indexed():
|
|
223
|
+
# indexed, return new pyomo expression
|
|
224
|
+
expr = pyomo.Expression(x.index_set(), rule=_expr_negate_rule(x))
|
|
225
|
+
expr.construct()
|
|
226
|
+
|
|
227
|
+
return expr
|
|
228
|
+
|
|
229
|
+
# not indexed, just regular negate
|
|
230
|
+
return -x
|
|
231
|
+
|
|
232
|
+
return _negate(x)
|
|
233
|
+
|
|
234
|
+
def _pyomo_pow(base, exp):
|
|
235
|
+
"""Implements a power operator compatible with Pyomo expressions."""
|
|
236
|
+
|
|
237
|
+
def _expr_pow_rule(base, exp):
|
|
238
|
+
def _inner(_, *indices):
|
|
239
|
+
return base[indices] ** exp
|
|
240
|
+
|
|
241
|
+
return _inner
|
|
242
|
+
|
|
243
|
+
def _pow(base, exp=exp):
|
|
244
|
+
# check if operand in indexed
|
|
245
|
+
if hasattr(base, "index_set") and base.is_indexed():
|
|
246
|
+
# indexed, return new pyomo expression
|
|
247
|
+
expr = pyomo.Expression(base.index_set(), rule=_expr_pow_rule(base, exp))
|
|
248
|
+
expr.construct()
|
|
249
|
+
|
|
250
|
+
return expr
|
|
251
|
+
|
|
252
|
+
# not indexed, just regular power
|
|
253
|
+
return base**exp
|
|
254
|
+
|
|
255
|
+
return _pow(base, exp)
|
|
256
|
+
|
|
257
|
+
def _pyomo_unary(x, op):
|
|
258
|
+
"""Implements unary operators to work with indexed expressions."""
|
|
259
|
+
|
|
260
|
+
def _expr_rule(x, op):
|
|
261
|
+
def _inner(_, *indices, op=op):
|
|
262
|
+
return op(x[indices])
|
|
263
|
+
|
|
264
|
+
return _inner
|
|
265
|
+
|
|
266
|
+
def _op(x, op):
|
|
267
|
+
# check if operand in indexed
|
|
268
|
+
if hasattr(x, "index_set") and x.is_indexed():
|
|
269
|
+
# indexed, return new pyomo expression
|
|
270
|
+
expr = pyomo.Expression(x.index_set(), rule=_expr_rule(x, op))
|
|
271
|
+
expr.construct()
|
|
272
|
+
|
|
273
|
+
return expr
|
|
274
|
+
|
|
275
|
+
# not indexed, just regular power
|
|
276
|
+
return op(x)
|
|
277
|
+
|
|
278
|
+
return _op(x, op)
|
|
279
|
+
|
|
280
|
+
def _pyomo_addition(*args, subtraction=False):
|
|
281
|
+
"""Add (subtract) scalars or tensors to (from) each other."""
|
|
282
|
+
|
|
283
|
+
def _expr_matrix_addition_rule(x, y, subtraction=subtraction):
|
|
284
|
+
def _inner(_, *args):
|
|
285
|
+
return x[*args] + y[*args] if not subtraction else x[*args] - y[*args]
|
|
286
|
+
|
|
287
|
+
return _inner
|
|
288
|
+
|
|
289
|
+
def _expr_elementwise_with_single_indexed(indexed, not_indexed, subtraction=subtraction):
|
|
290
|
+
def _inner(_, *indices):
|
|
291
|
+
return (indexed[indices] + not_indexed) if not subtraction else (indexed[indices] - not_indexed)
|
|
292
|
+
|
|
293
|
+
return _inner
|
|
294
|
+
|
|
295
|
+
def _add(x, y, subtraction=subtraction):
|
|
296
|
+
# if both are indexed, try matrix addition
|
|
297
|
+
if (hasattr(x, "index_set") and x.is_indexed()) and (hasattr(y, "index_set") and y.is_indexed()):
|
|
298
|
+
# try matrix addition
|
|
299
|
+
# check that the dimensions of x and y matches
|
|
300
|
+
if x.index_set().set_tuple != y.index_set().set_tuple:
|
|
301
|
+
msg = (
|
|
302
|
+
f"The dimensions of x {x.index_set().set_tuple} must match that"
|
|
303
|
+
f" of y {y.index_set().set_tuple} for matrix addition."
|
|
304
|
+
)
|
|
305
|
+
raise ParserError(msg)
|
|
306
|
+
|
|
307
|
+
expr = pyomo.Expression(
|
|
308
|
+
x.index_set(), rule=_expr_matrix_addition_rule(x, y, subtraction=subtraction)
|
|
309
|
+
)
|
|
310
|
+
expr.construct()
|
|
311
|
+
|
|
312
|
+
return expr
|
|
313
|
+
|
|
314
|
+
# if neither is indexed, do normal addition
|
|
315
|
+
if not (hasattr(x, "index_set") and x.is_indexed()) and not (
|
|
316
|
+
hasattr(y, "index_set") and y.is_indexed()
|
|
317
|
+
):
|
|
318
|
+
if not subtraction:
|
|
319
|
+
# try regular addition
|
|
320
|
+
return x + y
|
|
321
|
+
# try regular subtraction
|
|
322
|
+
return x - y
|
|
323
|
+
|
|
324
|
+
# x is indexed, y is not
|
|
325
|
+
if (hasattr(x, "index_set")) and x.is_indexed():
|
|
326
|
+
expr = pyomo.Expression(
|
|
327
|
+
x.index_set(), rule=_expr_elementwise_with_single_indexed(x, y, subtraction=subtraction)
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
expr.construct()
|
|
331
|
+
return expr
|
|
332
|
+
|
|
333
|
+
# if y is indexed, x is not
|
|
334
|
+
if (hasattr(y, "index_set")) and y.is_indexed():
|
|
335
|
+
expr = pyomo.Expression(
|
|
336
|
+
y.index_set(), rule=_expr_elementwise_with_single_indexed(y, x, subtraction=subtraction)
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
expr.construct()
|
|
340
|
+
return expr
|
|
341
|
+
|
|
342
|
+
# if only one of the operands is indexed, then addition is not supported. Throw error.
|
|
343
|
+
msg = "For addition, both operands must be either scalars or matrices with matching dimensions."
|
|
344
|
+
raise ParserError(msg)
|
|
345
|
+
|
|
346
|
+
return reduce(_add, args)
|
|
347
|
+
|
|
348
|
+
def _pyomo_subtraction(*args):
|
|
349
|
+
return _pyomo_addition(*args, subtraction=True)
|
|
350
|
+
|
|
351
|
+
def _pyomo_multiply(*args, division=False):
|
|
352
|
+
"""Multiply tensor with a scalar."""
|
|
353
|
+
|
|
354
|
+
def _expr_multiply_with_scalar_rule(scalar_value, to_multiply, division=division):
|
|
355
|
+
def _inner(_, *indices):
|
|
356
|
+
return to_multiply[indices] * scalar_value if not division else to_multiply[indices] / scalar_value
|
|
357
|
+
|
|
358
|
+
return _inner
|
|
359
|
+
|
|
360
|
+
def _expr_matrix_multiply_rule(x, y, division=division):
|
|
361
|
+
def _inner(_, *args):
|
|
362
|
+
return x[*args] * y[*args] if not division else x[*args] / y[*args]
|
|
363
|
+
|
|
364
|
+
return _inner
|
|
365
|
+
|
|
366
|
+
def _multiply(x, y, division=division):
|
|
367
|
+
if not hasattr(x, "is_indexed") and not hasattr(y, "is_indexed"):
|
|
368
|
+
# x and y are scalars
|
|
369
|
+
return x * y if not division else x / y
|
|
370
|
+
|
|
371
|
+
# check if x or y is scalar
|
|
372
|
+
if (
|
|
373
|
+
hasattr(x, "is_indexed")
|
|
374
|
+
and x.is_indexed()
|
|
375
|
+
and x.dim() > 0
|
|
376
|
+
and (not hasattr(y, "is_indexed") or not y.is_indexed() or y.is_indexed() and y.dim() == 0)
|
|
377
|
+
):
|
|
378
|
+
# x is a tensor, y is scalar
|
|
379
|
+
expr = pyomo.Expression(
|
|
380
|
+
x.index_set(), rule=_expr_multiply_with_scalar_rule(y, x, division=division)
|
|
381
|
+
)
|
|
382
|
+
expr.construct()
|
|
383
|
+
return expr
|
|
384
|
+
if (
|
|
385
|
+
hasattr(y, "is_indexed")
|
|
386
|
+
and y.is_indexed()
|
|
387
|
+
and y.dim() > 0
|
|
388
|
+
and (not hasattr(x, "is_indexed") or not x.is_indexed() or x.is_indexed() and x.dim() == 0)
|
|
389
|
+
):
|
|
390
|
+
# y is a tensor, x is scalar
|
|
391
|
+
expr = pyomo.Expression(
|
|
392
|
+
y.index_set(), rule=_expr_multiply_with_scalar_rule(x, y, division=division)
|
|
393
|
+
)
|
|
394
|
+
expr.construct()
|
|
395
|
+
return expr
|
|
396
|
+
|
|
397
|
+
# check if both are indexed
|
|
398
|
+
if hasattr(x, "index_set") and hasattr(y, "index_set"):
|
|
399
|
+
# both are indexed, neither is a scalar, check dims and sized, if match,
|
|
400
|
+
# multiply together element-wise
|
|
401
|
+
if x.index_set() != y.index_set():
|
|
402
|
+
msg = (
|
|
403
|
+
f"The dimensions of x {x.index_set().set_tuple} must match that"
|
|
404
|
+
f" of y {y.index_set().set_tuple} for element-wise matrix multiplication."
|
|
405
|
+
)
|
|
406
|
+
raise ParserError(msg)
|
|
407
|
+
|
|
408
|
+
expr = pyomo.Expression(x.index_set(), rule=_expr_matrix_multiply_rule(x, y, division=division))
|
|
409
|
+
expr.construct()
|
|
410
|
+
|
|
411
|
+
return expr
|
|
412
|
+
|
|
413
|
+
# both are scalars
|
|
414
|
+
return x * y if not division else x / y
|
|
415
|
+
|
|
416
|
+
return reduce(_multiply, args)
|
|
417
|
+
|
|
418
|
+
def _pyomo_division(*args):
|
|
419
|
+
return _pyomo_multiply(*args, division=True)
|
|
420
|
+
|
|
421
|
+
def _pyomo_matrix_multiplication(*args):
|
|
422
|
+
"""Multiply two matrices together."""
|
|
423
|
+
|
|
424
|
+
def _expr_matmul_rule(mat_a, mat_b, j_indices):
|
|
425
|
+
def _inner(_, i_index, k_index):
|
|
426
|
+
return sum(mat_a[i_index, j] * mat_b[j, k_index] for j in j_indices)
|
|
427
|
+
|
|
428
|
+
return _inner
|
|
429
|
+
|
|
430
|
+
def _matmul(mat_a, mat_b):
|
|
431
|
+
if not (hasattr(mat_a, "is_indexed") and mat_a.is_indexed()) or not (
|
|
432
|
+
hasattr(mat_b, "is_indexed") and mat_b.is_indexed()
|
|
433
|
+
):
|
|
434
|
+
# either mat_a or mat_b is not tensor
|
|
435
|
+
msg = "Either mat_a or mat_b, or both, is not indexed. Cannot perform matrix multiplication."
|
|
436
|
+
raise ParserError(msg)
|
|
437
|
+
|
|
438
|
+
# check for regular vectors, then do dot product
|
|
439
|
+
if mat_a.dim() == 1 and mat_b.dim() == 1:
|
|
440
|
+
if (len_a := len(mat_a.index_set())) != (len_b := len(mat_b.index_set())):
|
|
441
|
+
msg = (
|
|
442
|
+
"For dot product, the sizes of the vectors must match."
|
|
443
|
+
f" Sizes mat_a = {len_a} and mat_b = {len_b}."
|
|
444
|
+
)
|
|
445
|
+
raise ParserError(msg)
|
|
446
|
+
|
|
447
|
+
return pyomo.sum_product(mat_a, mat_b, index=mat_a.index_set())
|
|
448
|
+
|
|
449
|
+
# assuming mat_a has dimensions i,j; and mat_b j,k;
|
|
450
|
+
# then the j dimension is squeezed and the i and k dimensions are kept.
|
|
451
|
+
|
|
452
|
+
# check that we are dealing with matrices
|
|
453
|
+
min_dimension = 2
|
|
454
|
+
if (
|
|
455
|
+
not hasattr(mat_a.index_set(), "set_tuple") or len(mat_a.index_set().set_tuple) < min_dimension
|
|
456
|
+
) or (not hasattr(mat_b.index_set(), "set_tuple") and len(mat_b.index_set().set_tuple) < min_dimension):
|
|
457
|
+
msg = "Both mat_a and mat_b must have at least two dimensions."
|
|
458
|
+
raise ParserError(msg)
|
|
459
|
+
|
|
460
|
+
# check that the outer dimensions (the one to be squeezed) matches
|
|
461
|
+
if len(mat_a.index_set().set_tuple[-1]) != len(mat_b.index_set().set_tuple[0]):
|
|
462
|
+
msg = (
|
|
463
|
+
f"The last dimension size of mat_a ({mat_a.index_set().set_tuple[-1]}) must "
|
|
464
|
+
f"match the first dimension of mat_b ({mat_b.index_set().set_tuple[0]})"
|
|
465
|
+
)
|
|
466
|
+
raise ParserError(msg)
|
|
467
|
+
|
|
468
|
+
expr = pyomo.Expression(
|
|
469
|
+
mat_a.index_set().set_tuple[0],
|
|
470
|
+
mat_b.index_set().set_tuple[1],
|
|
471
|
+
rule=_expr_matmul_rule(mat_a, mat_b, mat_a.index_set().set_tuple[1]),
|
|
472
|
+
)
|
|
473
|
+
expr.construct()
|
|
474
|
+
|
|
475
|
+
return expr
|
|
476
|
+
|
|
477
|
+
return reduce(_matmul, args)
|
|
478
|
+
|
|
479
|
+
def _pyomo_summation(summand):
|
|
480
|
+
"""Sum an indexed Pyomo object."""
|
|
481
|
+
return pyomo.sum_product(summand, index=summand.index_set())
|
|
482
|
+
|
|
483
|
+
def _pyomo_random_access(indexed, *indices):
|
|
484
|
+
return indexed[*indices]
|
|
485
|
+
|
|
486
|
+
pyomo_env = {
|
|
487
|
+
# Define the operations for the different operators.
|
|
488
|
+
# Basic arithmetic operations
|
|
489
|
+
self.NEGATE: _pyomo_negate,
|
|
490
|
+
self.ADD: _pyomo_addition,
|
|
491
|
+
self.SUB: _pyomo_subtraction,
|
|
492
|
+
self.MUL: _pyomo_multiply,
|
|
493
|
+
self.DIV: _pyomo_division,
|
|
494
|
+
# Vector and matrix operations
|
|
495
|
+
self.MATMUL: _pyomo_matrix_multiplication,
|
|
496
|
+
self.SUM: _pyomo_summation,
|
|
497
|
+
self.RANDOM_ACCESS: _pyomo_random_access,
|
|
498
|
+
# Exponentiation and logarithms
|
|
499
|
+
self.EXP: lambda x: _pyomo_unary(x, pyomo.exp),
|
|
500
|
+
self.LN: lambda x: _pyomo_unary(x, pyomo.log),
|
|
501
|
+
self.LB: lambda x: _pyomo_unary(
|
|
502
|
+
x, lambda x: pyomo.log(x) / pyomo.log(2)
|
|
503
|
+
), # change of base, pyomo has no log2
|
|
504
|
+
self.LG: lambda x: _pyomo_unary(x, pyomo.log10),
|
|
505
|
+
self.LOP: lambda x: _pyomo_unary(x, lambda x: pyomo.log(x + 1)),
|
|
506
|
+
self.SQRT: lambda x: _pyomo_unary(x, pyomo.sqrt),
|
|
507
|
+
self.SQUARE: lambda x: _pyomo_pow(x, 2),
|
|
508
|
+
self.POW: lambda x, y: _pyomo_pow(x, y),
|
|
509
|
+
# Trigonometric operations
|
|
510
|
+
self.ARCCOS: lambda x: _pyomo_unary(x, pyomo.acos),
|
|
511
|
+
self.ARCCOSH: lambda x: _pyomo_unary(x, pyomo.acosh),
|
|
512
|
+
self.ARCSIN: lambda x: _pyomo_unary(x, pyomo.asin),
|
|
513
|
+
self.ARCSINH: lambda x: _pyomo_unary(x, pyomo.asinh),
|
|
514
|
+
self.ARCTAN: lambda x: _pyomo_unary(x, pyomo.atan),
|
|
515
|
+
self.ARCTANH: lambda x: _pyomo_unary(x, pyomo.atanh),
|
|
516
|
+
self.COS: lambda x: _pyomo_unary(x, pyomo.cos),
|
|
517
|
+
self.COSH: lambda x: _pyomo_unary(x, pyomo.cosh),
|
|
518
|
+
self.SIN: lambda x: _pyomo_unary(x, pyomo.sin),
|
|
519
|
+
self.SINH: lambda x: _pyomo_unary(x, pyomo.sinh),
|
|
520
|
+
self.TAN: lambda x: _pyomo_unary(x, pyomo.tan),
|
|
521
|
+
self.TANH: lambda x: _pyomo_unary(x, pyomo.tanh),
|
|
522
|
+
# Rounding operations
|
|
523
|
+
self.ABS: lambda x: _pyomo_unary(x, abs),
|
|
524
|
+
self.CEIL: lambda x: _pyomo_unary(x, pyomo.ceil),
|
|
525
|
+
self.FLOOR: lambda x: _pyomo_unary(x, pyomo.floor),
|
|
526
|
+
# Other operations
|
|
527
|
+
self.RATIONAL: lambda lst: reduce(lambda x, y: x / y, lst), # not supported
|
|
528
|
+
# probably a better idea to reformulate expressions with a max when utilized with pyomo
|
|
529
|
+
# self.MAX: lambda *args: reduce(lambda x, y: _PyomoMax((x, y)), args),
|
|
530
|
+
self.MAX: lambda *args: _PyomoMax(args),
|
|
531
|
+
self.MIN: lambda *args: _PyomoMin(args),
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
def _sympy_matmul(*args):
|
|
535
|
+
"""Sympy matrix multiplication."""
|
|
536
|
+
msg = (
|
|
537
|
+
"Matrix multiplication '@' has not been implemented for the Sympy parser yet."
|
|
538
|
+
" Feel free to contribute!"
|
|
539
|
+
)
|
|
540
|
+
raise NotImplementedError(msg)
|
|
541
|
+
|
|
542
|
+
def _sympy_summation(summand):
|
|
543
|
+
"""Sympy matrix summation."""
|
|
544
|
+
msg = (
|
|
545
|
+
"Matrix summation 'Sum' has not been implemented for the Sympy parser yet." " Feel free to contribute!"
|
|
546
|
+
)
|
|
547
|
+
raise NotImplementedError(msg)
|
|
548
|
+
|
|
549
|
+
def _sympy_random_access(*args):
|
|
550
|
+
msg = (
|
|
551
|
+
"Tensor random access with 'At' has not been implemented for the Sympy parser yet. "
|
|
552
|
+
"Feel free to contribute!"
|
|
553
|
+
)
|
|
554
|
+
raise NotImplementedError(msg)
|
|
555
|
+
|
|
556
|
+
sympy_env = {
|
|
557
|
+
# Basic arithmetic operations
|
|
558
|
+
self.NEGATE: lambda x: -to_sympy_expr(x),
|
|
559
|
+
self.ADD: lambda *args: reduce(lambda x, y: to_sympy_expr(x) + to_sympy_expr(y), args),
|
|
560
|
+
self.SUB: lambda *args: reduce(lambda x, y: to_sympy_expr(x) - to_sympy_expr(y), args),
|
|
561
|
+
self.MUL: lambda *args: reduce(lambda x, y: to_sympy_expr(x) * to_sympy_expr(y), args),
|
|
562
|
+
self.DIV: lambda *args: reduce(lambda x, y: to_sympy_expr(x) / to_sympy_expr(y), args),
|
|
563
|
+
# Vector and matrix operations
|
|
564
|
+
self.MATMUL: _sympy_matmul,
|
|
565
|
+
self.SUM: _sympy_summation,
|
|
566
|
+
self.RANDOM_ACCESS: _sympy_random_access,
|
|
567
|
+
# Exponentiation and logarithms
|
|
568
|
+
self.EXP: lambda x: sp.exp(to_sympy_expr(x)),
|
|
569
|
+
self.LN: lambda x: sp.log(to_sympy_expr(x)),
|
|
570
|
+
self.LB: lambda x: sp.log(to_sympy_expr(x), 2),
|
|
571
|
+
self.LG: lambda x: sp.log(to_sympy_expr(x), 10),
|
|
572
|
+
self.LOP: lambda x: sp.log(1 + to_sympy_expr(x)),
|
|
573
|
+
self.SQRT: lambda x: sp.sqrt(to_sympy_expr(x)),
|
|
574
|
+
self.SQUARE: lambda x: to_sympy_expr(x) ** 2,
|
|
575
|
+
self.POW: lambda x, y: to_sympy_expr(x) ** to_sympy_expr(y),
|
|
576
|
+
# Trigonometric operations
|
|
577
|
+
self.SIN: lambda x: sp.sin(to_sympy_expr(x)),
|
|
578
|
+
self.COS: lambda x: sp.cos(to_sympy_expr(x)),
|
|
579
|
+
self.TAN: lambda x: sp.tan(to_sympy_expr(x)),
|
|
580
|
+
self.ARCSIN: lambda x: sp.asin(to_sympy_expr(x)),
|
|
581
|
+
self.ARCCOS: lambda x: sp.acos(to_sympy_expr(x)),
|
|
582
|
+
self.ARCTAN: lambda x: sp.atan(to_sympy_expr(x)),
|
|
583
|
+
# Hyperbolic functions
|
|
584
|
+
self.SINH: lambda x: sp.sinh(to_sympy_expr(x)),
|
|
585
|
+
self.COSH: lambda x: sp.cosh(to_sympy_expr(x)),
|
|
586
|
+
self.TANH: lambda x: sp.tanh(to_sympy_expr(x)),
|
|
587
|
+
self.ARCSINH: lambda x: sp.asinh(to_sympy_expr(x)),
|
|
588
|
+
self.ARCCOSH: lambda x: sp.acosh(to_sympy_expr(x)),
|
|
589
|
+
self.ARCTANH: lambda x: sp.atanh(to_sympy_expr(x)),
|
|
590
|
+
# Other
|
|
591
|
+
self.ABS: lambda x: sp.Abs(to_sympy_expr(x)),
|
|
592
|
+
self.CEIL: lambda x: sp.ceiling(to_sympy_expr(x)),
|
|
593
|
+
self.FLOOR: lambda x: sp.floor(to_sympy_expr(x)),
|
|
594
|
+
# Note: Max and Min in sympy take any number of arguments
|
|
595
|
+
self.MAX: lambda *args: sp.Max(*args),
|
|
596
|
+
self.MIN: lambda *args: sp.Min(*args),
|
|
597
|
+
# Rational numbers, for now assuming two-element list for numerator and denominator
|
|
598
|
+
self.RATIONAL: lambda x, y: sp.Rational(x, y),
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
def _gurobipy_matmul(*args):
|
|
602
|
+
"""Gurobipy matrix multiplication."""
|
|
603
|
+
|
|
604
|
+
def _matmul(a, b):
|
|
605
|
+
if isinstance(a, list):
|
|
606
|
+
a = np.array(a)
|
|
607
|
+
if isinstance(b, list):
|
|
608
|
+
b = np.array(b)
|
|
609
|
+
if len(np.shape(a @ b)) == 1:
|
|
610
|
+
return a @ b
|
|
611
|
+
return (a @ b).sum()
|
|
612
|
+
|
|
613
|
+
return reduce(_matmul, args)
|
|
614
|
+
msg = (
|
|
615
|
+
"Matrix multiplication '@' has not been implemented for the Gurobipy parser yet."
|
|
616
|
+
" Feel free to contribute!"
|
|
617
|
+
)
|
|
618
|
+
raise NotImplementedError(msg)
|
|
619
|
+
|
|
620
|
+
def _gurobipy_summation(summand):
|
|
621
|
+
"""Gurobipy matrix summation."""
|
|
622
|
+
|
|
623
|
+
def _sum(summand):
|
|
624
|
+
if isinstance(summand, list):
|
|
625
|
+
summand = np.array(summand)
|
|
626
|
+
return summand.sum()
|
|
627
|
+
|
|
628
|
+
return _sum(summand)
|
|
629
|
+
msg = (
|
|
630
|
+
"Matrix summation 'Sum' has not been implemented for the Gurobipy parser yet."
|
|
631
|
+
" Feel free to contribute!"
|
|
632
|
+
)
|
|
633
|
+
raise NotImplementedError(msg)
|
|
634
|
+
|
|
635
|
+
def _gurobipy_random_access(*args):
|
|
636
|
+
msg = (
|
|
637
|
+
"Tensor random access with 'At' has not been implemented for the Gurobipy parser yet. "
|
|
638
|
+
"Feel free to contribute!"
|
|
639
|
+
)
|
|
640
|
+
raise NotImplementedError(msg)
|
|
641
|
+
|
|
642
|
+
gurobipy_env = {
|
|
643
|
+
# Define the operations for the different operators.
|
|
644
|
+
# Basic arithmetic operations
|
|
645
|
+
self.NEGATE: lambda x: -x,
|
|
646
|
+
self.ADD: lambda *args: reduce(lambda x, y: x + y, args),
|
|
647
|
+
self.SUB: lambda *args: reduce(lambda x, y: x - y, args),
|
|
648
|
+
self.MUL: lambda *args: reduce(lambda x, y: x * y, args),
|
|
649
|
+
self.DIV: lambda *args: reduce(lambda x, y: x / y, args),
|
|
650
|
+
# Vector and matrix operations
|
|
651
|
+
self.MATMUL: _gurobipy_matmul,
|
|
652
|
+
self.SUM: _gurobipy_summation,
|
|
653
|
+
self.RANDOM_ACCESS: _gurobipy_random_access,
|
|
654
|
+
# Exponentiation and logarithms
|
|
655
|
+
# it would be possible to implement some of these with the special functions that
|
|
656
|
+
# gurobi has to offer, but they would only work under specific circumstances
|
|
657
|
+
self.EXP: lambda x: gp_error(),
|
|
658
|
+
self.LN: lambda x: gp_error(),
|
|
659
|
+
self.LB: lambda x: gp_error(),
|
|
660
|
+
self.LG: lambda x: gp_error(),
|
|
661
|
+
self.LOP: lambda x: gp_error(),
|
|
662
|
+
self.SQRT: lambda x: gp_error(),
|
|
663
|
+
self.SQUARE: lambda x: x**2,
|
|
664
|
+
self.POW: lambda x, y: x**y, # this will likely cause an error at some point for most y
|
|
665
|
+
# Trigonometric operations
|
|
666
|
+
# it would be possible to implement some of these with the special functions that
|
|
667
|
+
# gurobi has to offer, but they would only work under specific circumstances
|
|
668
|
+
self.ARCCOS: lambda x: gp_error(),
|
|
669
|
+
self.ARCCOSH: lambda x: gp_error(),
|
|
670
|
+
self.ARCSIN: lambda x: gp_error(),
|
|
671
|
+
self.ARCSINH: lambda x: gp_error(),
|
|
672
|
+
self.ARCTAN: lambda x: gp_error(),
|
|
673
|
+
self.ARCTANH: lambda x: gp_error(),
|
|
674
|
+
self.COS: lambda x: gp_error(),
|
|
675
|
+
self.COSH: lambda x: gp_error(),
|
|
676
|
+
self.SIN: lambda x: gp_error(),
|
|
677
|
+
self.SINH: lambda x: gp_error(),
|
|
678
|
+
self.TAN: lambda x: gp_error(),
|
|
679
|
+
self.TANH: lambda x: gp_error(),
|
|
680
|
+
# Rounding operations
|
|
681
|
+
self.ABS: lambda x: gp.abs_(x),
|
|
682
|
+
self.CEIL: lambda x: gp_error(),
|
|
683
|
+
self.FLOOR: lambda x: gp_error(),
|
|
684
|
+
# Other operations
|
|
685
|
+
self.RATIONAL: lambda lst: reduce(lambda x, y: x / y, lst), # not supported
|
|
686
|
+
self.MAX: lambda *args: gp.max_(args), ## OBS! max and min are unsupported, but left here for reasons
|
|
687
|
+
self.MIN: lambda *args: gp.min_(args),
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
match to_format:
|
|
691
|
+
case FormatEnum.polars:
|
|
692
|
+
self.env = polars_env
|
|
693
|
+
self.parse = self._parse_to_polars
|
|
694
|
+
case FormatEnum.pyomo:
|
|
695
|
+
self.env = pyomo_env
|
|
696
|
+
self.parse = self._parse_to_pyomo
|
|
697
|
+
case FormatEnum.sympy:
|
|
698
|
+
self.env = sympy_env
|
|
699
|
+
self.parse = self._parse_to_sympy
|
|
700
|
+
case FormatEnum.gurobipy:
|
|
701
|
+
self.env = gurobipy_env
|
|
702
|
+
self.parse = self._parse_to_gurobipy
|
|
703
|
+
case _:
|
|
704
|
+
msg = f"Given target format {to_format} not supported. Must be one of {FormatEnum}."
|
|
705
|
+
raise ParserError(msg)
|
|
706
|
+
|
|
707
|
+
def _parse_to_polars(self, expr: list | str | int | float) -> pl.Expr:
|
|
708
|
+
"""Recursively parses JSON math expressions and returns a polars expression.
|
|
709
|
+
|
|
710
|
+
Arguments:
|
|
711
|
+
expr (list): A list with a Polish notation expression that describes a, e.g.,
|
|
712
|
+
["Multiply", ["Sqrt", 2], "x2"]
|
|
713
|
+
|
|
714
|
+
Raises:
|
|
715
|
+
ParserError: when a unsupported operator type is encountered.
|
|
716
|
+
|
|
717
|
+
Returns:
|
|
718
|
+
pl.Expr: A polars expression that may be evaluated further.
|
|
719
|
+
|
|
720
|
+
"""
|
|
721
|
+
if isinstance(expr, pl.Expr):
|
|
722
|
+
# Terminal case: polars expression
|
|
723
|
+
return expr
|
|
724
|
+
if isinstance(expr, str):
|
|
725
|
+
# Terminal case: str expression (represents a column name)
|
|
726
|
+
return pl.col(expr)
|
|
727
|
+
if isinstance(expr, self.literals):
|
|
728
|
+
# Terminal case: numeric literal
|
|
729
|
+
return pl.lit(expr)
|
|
730
|
+
|
|
731
|
+
if isinstance(expr, list):
|
|
732
|
+
if len(expr) == 1 and isinstance(expr[0], str | self.literals):
|
|
733
|
+
# Terminal case, single symbol expression or literal
|
|
734
|
+
if isinstance(expr[0], str):
|
|
735
|
+
return pl.col(expr)
|
|
736
|
+
# just a literal
|
|
737
|
+
return pl.lit(expr[0])
|
|
738
|
+
|
|
739
|
+
# Extract the operation name
|
|
740
|
+
if isinstance(expr[0], str) and expr[0] in self.env:
|
|
741
|
+
op_name = expr[0]
|
|
742
|
+
# Parse the operands
|
|
743
|
+
operands = [self.parse(e) for e in expr[1:]]
|
|
744
|
+
|
|
745
|
+
if isinstance(operands, list) and len(operands) == 1:
|
|
746
|
+
# if the operands have redundant brackets, remove them
|
|
747
|
+
operands = operands[0]
|
|
748
|
+
|
|
749
|
+
if isinstance(operands, list):
|
|
750
|
+
return self.env[op_name](*operands)
|
|
751
|
+
|
|
752
|
+
return self.env[op_name](operands)
|
|
753
|
+
|
|
754
|
+
# else, assume the list contents are parseable expressions
|
|
755
|
+
return [self.parse(e) for e in expr]
|
|
756
|
+
|
|
757
|
+
msg = f"Encountered unsupported type '{type(expr)}' during parsing."
|
|
758
|
+
raise ParserError(msg)
|
|
759
|
+
|
|
760
|
+
def _parse_to_pyomo(
|
|
761
|
+
self, expr: list | str | int | float | pyomo.Expression, model: pyomo.Model
|
|
762
|
+
) -> pyomo.Expression:
|
|
763
|
+
"""Parses the MathJSON format recursively into a Pyomo expression.
|
|
764
|
+
|
|
765
|
+
Args:
|
|
766
|
+
expr (list | str | int | float): a list with a Polish notation expression that describes a, e.g.,
|
|
767
|
+
["Multiply", ["Sqrt", 2], "x2"]
|
|
768
|
+
model (pyomo.Model): a pyomo model with the symbols defined appearing in the expression.
|
|
769
|
+
E.g., "x2" -> model.x2 must be defined.
|
|
770
|
+
|
|
771
|
+
Raises:
|
|
772
|
+
ParserError: when a unsupported operator type is encountered.
|
|
773
|
+
|
|
774
|
+
Returns:
|
|
775
|
+
pyomo.Expression: returns a pyomo expression equivalent to the original expressions.
|
|
776
|
+
"""
|
|
777
|
+
if isinstance(expr, pyomo.Expression):
|
|
778
|
+
# Terminal case: pyomo expression
|
|
779
|
+
return expr
|
|
780
|
+
if isinstance(expr, str):
|
|
781
|
+
# Terminal case: str expression, represent a variable or expression
|
|
782
|
+
return getattr(model, expr)
|
|
783
|
+
if isinstance(expr, self.literals):
|
|
784
|
+
# Terminal case: numeric literal
|
|
785
|
+
return expr
|
|
786
|
+
|
|
787
|
+
if isinstance(expr, list):
|
|
788
|
+
if len(expr) == 1 and isinstance(expr[0], str | self.literals):
|
|
789
|
+
# Terminal case, single symbol expression or literal
|
|
790
|
+
if isinstance(expr[0], str):
|
|
791
|
+
return getattr(model, expr[0])
|
|
792
|
+
# just a literal
|
|
793
|
+
return pyomo.Expression(expr=expr[0])
|
|
794
|
+
|
|
795
|
+
# Extract the operation name
|
|
796
|
+
if isinstance(expr[0], str) and expr[0] in self.env:
|
|
797
|
+
op_name = expr[0]
|
|
798
|
+
# Parse the operands
|
|
799
|
+
operands = [self._parse_to_pyomo(e, model) for e in expr[1:]]
|
|
800
|
+
|
|
801
|
+
if isinstance(operands, list) and len(operands) == 1:
|
|
802
|
+
# if the operands have redundant brackets, remove them
|
|
803
|
+
operands = operands[0]
|
|
804
|
+
|
|
805
|
+
if isinstance(operands, list):
|
|
806
|
+
return self.env[op_name](*operands)
|
|
807
|
+
|
|
808
|
+
return self.env[op_name](operands)
|
|
809
|
+
|
|
810
|
+
# else, assume the list contents are parseable expressions
|
|
811
|
+
return [self._parse_to_pyomo(e, model) for e in expr]
|
|
812
|
+
|
|
813
|
+
msg = f"Encountered unsupported type '{type(expr)}' during parsing."
|
|
814
|
+
raise ParserError(msg)
|
|
815
|
+
|
|
816
|
+
def _parse_to_sympy(self, expr: list | str | int | float | sp.Basic) -> sp.Basic:
|
|
817
|
+
"""Parse the MathJSON format recursively into a sympy expression.
|
|
818
|
+
|
|
819
|
+
Args:
|
|
820
|
+
expr (list | str | int | float | sp.Basic): base call should be a list in Polish
|
|
821
|
+
notation representing a mathematical expression. Recursion calls can be of various
|
|
822
|
+
types.
|
|
823
|
+
|
|
824
|
+
Raises:
|
|
825
|
+
ParserError: when a unsupported operator type is encountered.
|
|
826
|
+
|
|
827
|
+
Returns:
|
|
828
|
+
sp.Basic: a sympy expression that represents the original mathematical
|
|
829
|
+
expression in the supplied MathJSON format.
|
|
830
|
+
"""
|
|
831
|
+
if isinstance(expr, sp.Basic):
|
|
832
|
+
# Terminal case: sympy expression
|
|
833
|
+
return expr
|
|
834
|
+
if isinstance(expr, str):
|
|
835
|
+
# Terminal case: represents a variable
|
|
836
|
+
return sp.sympify(expr, evaluate=False)
|
|
837
|
+
if isinstance(expr, self.literals):
|
|
838
|
+
# Terminal case: numeric literal
|
|
839
|
+
return sp.sympify(expr, evaluate=False)
|
|
840
|
+
|
|
841
|
+
if isinstance(expr, list):
|
|
842
|
+
if len(expr) == 1 and isinstance(expr[0], str | self.literals):
|
|
843
|
+
# Terminal case, single symbol expression or literal
|
|
844
|
+
return sp.sympify(expr[0], evaluate=False)
|
|
845
|
+
|
|
846
|
+
# Extract the operation name
|
|
847
|
+
if isinstance(expr[0], str) and expr[0] in self.env:
|
|
848
|
+
op_name = expr[0]
|
|
849
|
+
# Parse the operands
|
|
850
|
+
operands = [self.parse(e) for e in expr[1:]]
|
|
851
|
+
|
|
852
|
+
if isinstance(operands, list) and len(operands) == 1:
|
|
853
|
+
# if the operands have redundant brackets, remove them
|
|
854
|
+
operands = operands[0]
|
|
855
|
+
|
|
856
|
+
if isinstance(operands, list):
|
|
857
|
+
return self.env[op_name](*operands)
|
|
858
|
+
|
|
859
|
+
return self.env[op_name](operands)
|
|
860
|
+
|
|
861
|
+
# else, assume the list contents are parseable expressions
|
|
862
|
+
return [self.parse(e) for e in expr]
|
|
863
|
+
|
|
864
|
+
msg = f"Encountered unsupported type '{type(expr)}' during parsing."
|
|
865
|
+
raise ParserError(msg)
|
|
866
|
+
|
|
867
|
+
def _parse_to_gurobipy(
|
|
868
|
+
self, expr: list | str | int | float, callback: Callable[[str], gpexpression | int | float]
|
|
869
|
+
) -> gpexpression | int | float:
|
|
870
|
+
"""Parses the MathJSON format recursively into a gurobipy expression.
|
|
871
|
+
|
|
872
|
+
Gurobi only fundamentally supports linear and quadratic expressions, and this parser
|
|
873
|
+
does not check that the inputs are valid. If you try to input something else, you will
|
|
874
|
+
likely encounter an error at some point.
|
|
875
|
+
|
|
876
|
+
Args:
|
|
877
|
+
expr (list | str | int | float): a list with a Polish notation expression that describes a, e.g.,
|
|
878
|
+
["Multiply", ["Sqrt", 2], "x2"]
|
|
879
|
+
callback (Callable): A function that can return a gurobipy expression associated with the
|
|
880
|
+
correct model when called with symbol str.
|
|
881
|
+
|
|
882
|
+
Returns:
|
|
883
|
+
Returns a gurobipy expression (that can belong into one of multiple types) equivalent to the original
|
|
884
|
+
expressions.
|
|
885
|
+
All possible output types should be supported as parts of gurobipy constraints. gurobipy.GenExpr at
|
|
886
|
+
least isn't supported as an objective function.
|
|
887
|
+
"""
|
|
888
|
+
if isinstance(expr, gpexpression):
|
|
889
|
+
# Terminal case: gurobipy expression
|
|
890
|
+
return expr
|
|
891
|
+
if isinstance(expr, str):
|
|
892
|
+
# Terminal case: str expression, represent a variable or expression
|
|
893
|
+
return callback(expr)
|
|
894
|
+
if isinstance(expr, self.literals):
|
|
895
|
+
# Terminal case: numeric literal
|
|
896
|
+
return expr
|
|
897
|
+
|
|
898
|
+
if isinstance(expr, list):
|
|
899
|
+
# Extract the operation name
|
|
900
|
+
if isinstance(expr[0], str) and expr[0] in self.env:
|
|
901
|
+
op_name = expr[0]
|
|
902
|
+
# Parse the operands
|
|
903
|
+
operands = [self._parse_to_gurobipy(e, callback) for e in expr[1:]]
|
|
904
|
+
|
|
905
|
+
while isinstance(operands, list) and len(operands) == 1:
|
|
906
|
+
# if the operands have redundant brackets, remove them
|
|
907
|
+
operands = operands[0]
|
|
908
|
+
|
|
909
|
+
if isinstance(operands, list):
|
|
910
|
+
return self.env[op_name](*operands)
|
|
911
|
+
|
|
912
|
+
return self.env[op_name](operands)
|
|
913
|
+
|
|
914
|
+
# else, assume the list contents are parseable expressions
|
|
915
|
+
return [self._parse_to_gurobipy(e, callback) for e in expr]
|
|
916
|
+
|
|
917
|
+
msg = f"Encountered unsupported type '{type(expr)}' during parsing."
|
|
918
|
+
raise ParserError(msg)
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def replace_str(lst: list | str, target: str, sub: list | str | float | int) -> list:
|
|
922
|
+
"""Replace a target in list with a substitution recursively.
|
|
923
|
+
|
|
924
|
+
Arguments:
|
|
925
|
+
lst (list or str): The list where the substitution is to be made.
|
|
926
|
+
target (str): The target of the substitution.
|
|
927
|
+
sub (list or str): The content to substitute the target.
|
|
928
|
+
|
|
929
|
+
Return:
|
|
930
|
+
list or str: The list or str with the substitution.
|
|
931
|
+
|
|
932
|
+
Example:
|
|
933
|
+
replace_str("["Max", "g_i", ["Add","g_i","f_i"]]]", "_i", "_1") --->
|
|
934
|
+
["Max", "g_1", ["Add","g_1","f_1"]]]
|
|
935
|
+
"""
|
|
936
|
+
if isinstance(lst, list):
|
|
937
|
+
return [replace_str(item, target, sub) for item in lst]
|
|
938
|
+
if isinstance(lst, str):
|
|
939
|
+
if target == lst:
|
|
940
|
+
if isinstance(sub, str):
|
|
941
|
+
return lst.replace(target, sub)
|
|
942
|
+
return sub
|
|
943
|
+
return lst
|
|
944
|
+
return lst
|