gamspy 1.18.3__py3-none-any.whl → 1.19.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.
- gamspy/__init__.py +86 -98
- gamspy/__main__.py +6 -6
- gamspy/_algebra/__init__.py +13 -13
- gamspy/_algebra/condition.py +290 -194
- gamspy/_algebra/domain.py +103 -93
- gamspy/_algebra/expression.py +820 -799
- gamspy/_algebra/number.py +79 -70
- gamspy/_algebra/operable.py +185 -185
- gamspy/_algebra/operation.py +948 -845
- gamspy/_backend/backend.py +313 -311
- gamspy/_backend/engine.py +960 -960
- gamspy/_backend/local.py +124 -124
- gamspy/_backend/neos.py +567 -567
- gamspy/_cli/__init__.py +1 -1
- gamspy/_cli/cli.py +64 -64
- gamspy/_cli/gdx.py +377 -377
- gamspy/_cli/install.py +375 -372
- gamspy/_cli/list.py +94 -94
- gamspy/_cli/mps2gms.py +128 -128
- gamspy/_cli/probe.py +52 -52
- gamspy/_cli/retrieve.py +79 -79
- gamspy/_cli/run.py +158 -158
- gamspy/_cli/show.py +246 -255
- gamspy/_cli/uninstall.py +165 -165
- gamspy/_cli/util.py +94 -94
- gamspy/_communication.py +215 -215
- gamspy/_config.py +132 -132
- gamspy/_container.py +1694 -1452
- gamspy/_convert.py +720 -720
- gamspy/_database.py +271 -271
- gamspy/_extrinsic.py +181 -181
- gamspy/_miro.py +356 -352
- gamspy/_model.py +1803 -1615
- gamspy/_model_instance.py +701 -701
- gamspy/_options.py +780 -700
- gamspy/_serialization.py +156 -144
- gamspy/_symbols/__init__.py +17 -17
- gamspy/_symbols/alias.py +305 -299
- gamspy/_symbols/equation.py +1407 -1298
- gamspy/_symbols/implicits/__init__.py +11 -11
- gamspy/_symbols/implicits/implicit_equation.py +186 -186
- gamspy/_symbols/implicits/implicit_parameter.py +272 -272
- gamspy/_symbols/implicits/implicit_set.py +124 -124
- gamspy/_symbols/implicits/implicit_symbol.py +315 -315
- gamspy/_symbols/implicits/implicit_variable.py +255 -255
- gamspy/_symbols/parameter.py +648 -609
- gamspy/_symbols/set.py +985 -923
- gamspy/_symbols/symbol.py +395 -386
- gamspy/_symbols/universe_alias.py +182 -182
- gamspy/_symbols/variable.py +1101 -1017
- gamspy/_types.py +7 -7
- gamspy/_validation.py +735 -735
- gamspy/_workspace.py +72 -72
- gamspy/exceptions.py +128 -128
- gamspy/formulations/__init__.py +46 -46
- gamspy/formulations/ml/__init__.py +11 -11
- gamspy/formulations/ml/decision_tree_struct.py +80 -80
- gamspy/formulations/ml/gradient_boosting.py +203 -203
- gamspy/formulations/ml/random_forest.py +187 -187
- gamspy/formulations/ml/regression_tree.py +533 -533
- gamspy/formulations/nn/__init__.py +19 -19
- gamspy/formulations/nn/avgpool2d.py +232 -232
- gamspy/formulations/nn/conv1d.py +533 -533
- gamspy/formulations/nn/conv2d.py +529 -529
- gamspy/formulations/nn/linear.py +341 -341
- gamspy/formulations/nn/maxpool2d.py +88 -88
- gamspy/formulations/nn/minpool2d.py +88 -88
- gamspy/formulations/nn/mpool2d.py +245 -245
- gamspy/formulations/nn/torch_sequential.py +278 -278
- gamspy/formulations/piecewise.py +682 -682
- gamspy/formulations/result.py +119 -119
- gamspy/formulations/shape.py +188 -188
- gamspy/formulations/utils.py +173 -173
- gamspy/math/__init__.py +215 -215
- gamspy/math/activation.py +783 -767
- gamspy/math/log_power.py +435 -435
- gamspy/math/matrix.py +534 -534
- gamspy/math/misc.py +1709 -1625
- gamspy/math/probability.py +170 -170
- gamspy/math/trigonometric.py +232 -232
- gamspy/utils.py +810 -791
- gamspy/version.py +5 -5
- {gamspy-1.18.3.dist-info → gamspy-1.19.0.dist-info}/METADATA +90 -121
- gamspy-1.19.0.dist-info/RECORD +90 -0
- {gamspy-1.18.3.dist-info → gamspy-1.19.0.dist-info}/WHEEL +1 -1
- {gamspy-1.18.3.dist-info → gamspy-1.19.0.dist-info}/licenses/LICENSE +22 -22
- gamspy-1.18.3.dist-info/RECORD +0 -90
- {gamspy-1.18.3.dist-info → gamspy-1.19.0.dist-info}/entry_points.txt +0 -0
- {gamspy-1.18.3.dist-info → gamspy-1.19.0.dist-info}/top_level.txt +0 -0
gamspy/_algebra/expression.py
CHANGED
|
@@ -1,799 +1,820 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from typing import TYPE_CHECKING, Literal
|
|
5
|
-
|
|
6
|
-
import gamspy._algebra.condition as condition
|
|
7
|
-
import gamspy._algebra.domain as domain
|
|
8
|
-
import gamspy._algebra.number as number
|
|
9
|
-
import gamspy._algebra.operable as operable
|
|
10
|
-
import gamspy._algebra.operation as operation
|
|
11
|
-
import gamspy._symbols as gp_syms
|
|
12
|
-
import gamspy._validation as validation
|
|
13
|
-
import gamspy.utils as utils
|
|
14
|
-
from gamspy._config import get_option
|
|
15
|
-
from gamspy._extrinsic import ExtrinsicFunction
|
|
16
|
-
from gamspy._symbols.implicits import ImplicitSet
|
|
17
|
-
from gamspy._symbols.implicits.implicit_symbol import ImplicitSymbol
|
|
18
|
-
from gamspy._symbols.symbol import Symbol
|
|
19
|
-
from gamspy.exceptions import ValidationError
|
|
20
|
-
from gamspy.math.misc import MathOp
|
|
21
|
-
|
|
22
|
-
if TYPE_CHECKING:
|
|
23
|
-
import pandas as pd
|
|
24
|
-
|
|
25
|
-
from gamspy import Alias, Set
|
|
26
|
-
from gamspy._symbols.implicits import ImplicitEquation
|
|
27
|
-
from gamspy._types import OperableType
|
|
28
|
-
|
|
29
|
-
GMS_MAX_LINE_LENGTH = 80000
|
|
30
|
-
LINE_LENGTH_OFFSET = 79000
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@dataclass
|
|
34
|
-
class DomainPlaceHolder:
|
|
35
|
-
indices: list[tuple[str, int]]
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def peek(stack):
|
|
39
|
-
if len(stack) > 0:
|
|
40
|
-
return stack[-1]
|
|
41
|
-
return None
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# Higher number means higher precedence.
|
|
45
|
-
PRECEDENCE = {
|
|
46
|
-
"..": 0,
|
|
47
|
-
"=": 0,
|
|
48
|
-
"or": 1,
|
|
49
|
-
"xor": 2,
|
|
50
|
-
"and": 3,
|
|
51
|
-
"=e=": 4,
|
|
52
|
-
"=n=": 4,
|
|
53
|
-
"=b=": 4,
|
|
54
|
-
"eq": 4,
|
|
55
|
-
"ne": 4,
|
|
56
|
-
">=": 4,
|
|
57
|
-
"<=": 4,
|
|
58
|
-
">": 4,
|
|
59
|
-
"<": 4,
|
|
60
|
-
"=g=": 4,
|
|
61
|
-
"=l=": 4,
|
|
62
|
-
"=x=": 4,
|
|
63
|
-
"+": 5,
|
|
64
|
-
"-": 5,
|
|
65
|
-
"*": 6,
|
|
66
|
-
"/": 6,
|
|
67
|
-
"not": 7,
|
|
68
|
-
"u-": 7,
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
# Defines how operators of the same precedence are grouped.
|
|
72
|
-
ASSOCIATIVITY = {
|
|
73
|
-
"or": "left",
|
|
74
|
-
"xor": "left",
|
|
75
|
-
"and": "left",
|
|
76
|
-
"=e=": "left",
|
|
77
|
-
"=n=": "left",
|
|
78
|
-
"=x=": "left",
|
|
79
|
-
"=b=": "left",
|
|
80
|
-
"eq": "left",
|
|
81
|
-
"ne": "left",
|
|
82
|
-
"=": "left",
|
|
83
|
-
">=": "left",
|
|
84
|
-
"<=": "left",
|
|
85
|
-
">": "left",
|
|
86
|
-
"<": "left",
|
|
87
|
-
"=g=": "left",
|
|
88
|
-
"=l=": "left",
|
|
89
|
-
"..": "non",
|
|
90
|
-
"+": "left",
|
|
91
|
-
"-": "left",
|
|
92
|
-
"*": "left",
|
|
93
|
-
"/": "left",
|
|
94
|
-
"not": "right",
|
|
95
|
-
"u-": "right",
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
# Precedence for a leaf node is considered infinite.
|
|
99
|
-
LEAF_PRECEDENCE = float("inf")
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def get_operand_gams_repr(operand) -> str:
|
|
103
|
-
if hasattr(operand, "gamsRepr"):
|
|
104
|
-
return operand.gamsRepr()
|
|
105
|
-
|
|
106
|
-
representation = str(operand)
|
|
107
|
-
|
|
108
|
-
# b[i] * -1 -> not valid
|
|
109
|
-
# b[i] * (-1) -> valid
|
|
110
|
-
if isinstance(operand, (int, float)) and operand < 0:
|
|
111
|
-
representation = f"({representation})"
|
|
112
|
-
|
|
113
|
-
return representation
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def get_operand_latex_repr(operand) -> str:
|
|
117
|
-
if hasattr(operand, "latexRepr"):
|
|
118
|
-
return operand.latexRepr()
|
|
119
|
-
|
|
120
|
-
if isinstance(operand, float):
|
|
121
|
-
operand = utils._map_special_values(operand)
|
|
122
|
-
|
|
123
|
-
representation = str(operand)
|
|
124
|
-
|
|
125
|
-
return representation
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def create_gams_expression(root_node: Expression) -> str:
|
|
129
|
-
"""
|
|
130
|
-
Creates GAMS representation of a binary expression tree without recursion.
|
|
131
|
-
It uses an iterative post-order traversal to build the expression,
|
|
132
|
-
adding parentheses only when necessary based on operator precedence and
|
|
133
|
-
associativity rules.
|
|
134
|
-
"""
|
|
135
|
-
if not isinstance(root_node, Expression):
|
|
136
|
-
return get_operand_gams_repr(root_node)
|
|
137
|
-
|
|
138
|
-
# 1. Get nodes in post-order (left - right - parent).
|
|
139
|
-
s1: list[OperableType | ImplicitEquation | str] = [root_node]
|
|
140
|
-
post_order_nodes = []
|
|
141
|
-
while s1:
|
|
142
|
-
node = s1.pop()
|
|
143
|
-
post_order_nodes.append(node)
|
|
144
|
-
if isinstance(node, Expression):
|
|
145
|
-
if node.left is not None:
|
|
146
|
-
s1.append(node.left)
|
|
147
|
-
if node.right is not None:
|
|
148
|
-
s1.append(node.right)
|
|
149
|
-
|
|
150
|
-
# 2. Build the GAMS expression
|
|
151
|
-
eval_stack: list[tuple[str, float]] = []
|
|
152
|
-
for node in reversed(post_order_nodes):
|
|
153
|
-
if not isinstance(node, Expression):
|
|
154
|
-
eval_stack.append((get_operand_gams_repr(node), LEAF_PRECEDENCE))
|
|
155
|
-
continue
|
|
156
|
-
|
|
157
|
-
op = node.operator
|
|
158
|
-
op_prec = PRECEDENCE[op]
|
|
159
|
-
op_assoc = ASSOCIATIVITY[op]
|
|
160
|
-
|
|
161
|
-
# Handle unary ops
|
|
162
|
-
if op in ("u-", "not"):
|
|
163
|
-
operand_str, operand_prec = eval_stack.pop()
|
|
164
|
-
|
|
165
|
-
# Add parentheses if the operand's operator has lower precedence
|
|
166
|
-
if operand_prec < op_prec:
|
|
167
|
-
operand_str = f"({operand_str})"
|
|
168
|
-
|
|
169
|
-
if op == "u-":
|
|
170
|
-
new_str = f"(-{operand_str})"
|
|
171
|
-
# A parenthesized expression has the highest precedence
|
|
172
|
-
eval_stack.append((new_str, LEAF_PRECEDENCE))
|
|
173
|
-
else: # Standard handling for 'not'
|
|
174
|
-
new_str = f"not {operand_str}"
|
|
175
|
-
eval_stack.append((new_str, op_prec))
|
|
176
|
-
|
|
177
|
-
# Handle binary ops
|
|
178
|
-
else:
|
|
179
|
-
right_str, right_prec = eval_stack.pop()
|
|
180
|
-
left_str, left_prec = eval_stack.pop()
|
|
181
|
-
|
|
182
|
-
if left_prec < op_prec or (left_prec == op_prec and op_assoc == "right"):
|
|
183
|
-
left_str = f"({left_str})"
|
|
184
|
-
|
|
185
|
-
if right_prec < op_prec or (right_prec == op_prec and op_assoc == "left"):
|
|
186
|
-
right_str = f"({right_str})"
|
|
187
|
-
|
|
188
|
-
# get around 80000 line length limitation in GAMS
|
|
189
|
-
length = len(left_str) + len(op) + len(right_str)
|
|
190
|
-
if length >= GMS_MAX_LINE_LENGTH - LINE_LENGTH_OFFSET:
|
|
191
|
-
new_str = f"{left_str} {op}\n {right_str}"
|
|
192
|
-
else:
|
|
193
|
-
new_str = f"{left_str} {op} {right_str}"
|
|
194
|
-
eval_stack.append((new_str, op_prec))
|
|
195
|
-
|
|
196
|
-
final_string = eval_stack[0][0]
|
|
197
|
-
|
|
198
|
-
if root_node.operator in ("=", ".."):
|
|
199
|
-
return f"{final_string};"
|
|
200
|
-
|
|
201
|
-
return final_string
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def create_latex_expression(root_node: Expression) -> str:
|
|
205
|
-
"""
|
|
206
|
-
Creates LaTeX representation of a binary expression tree without recursion.
|
|
207
|
-
It uses an iterative post-order traversal to build the expression,
|
|
208
|
-
adding parentheses only when necessary based on operator precedence and
|
|
209
|
-
associativity rules.
|
|
210
|
-
"""
|
|
211
|
-
if not isinstance(root_node, Expression):
|
|
212
|
-
return get_operand_latex_repr(root_node)
|
|
213
|
-
|
|
214
|
-
op_map = {
|
|
215
|
-
"=g=": "\\geq",
|
|
216
|
-
"=l=": "\\leq",
|
|
217
|
-
"=e=": "=",
|
|
218
|
-
"*": "\\cdot",
|
|
219
|
-
"and": "\\wedge",
|
|
220
|
-
"or": "\\vee",
|
|
221
|
-
"xor": "\\oplus",
|
|
222
|
-
"$": "|",
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
# 1. Get nodes in post-order (left - right - parent).
|
|
226
|
-
s1: list[OperableType | ImplicitEquation | str] = [root_node]
|
|
227
|
-
post_order_nodes = []
|
|
228
|
-
while s1:
|
|
229
|
-
node = s1.pop()
|
|
230
|
-
post_order_nodes.append(node)
|
|
231
|
-
if isinstance(node, Expression):
|
|
232
|
-
if node.left is not None:
|
|
233
|
-
s1.append(node.left)
|
|
234
|
-
if node.right is not None:
|
|
235
|
-
s1.append(node.right)
|
|
236
|
-
|
|
237
|
-
# 2. Build the LaTeX expression
|
|
238
|
-
eval_stack: list[tuple[str, float]] = []
|
|
239
|
-
for node in reversed(post_order_nodes):
|
|
240
|
-
if not isinstance(node, Expression):
|
|
241
|
-
eval_stack.append((get_operand_latex_repr(node), LEAF_PRECEDENCE))
|
|
242
|
-
continue
|
|
243
|
-
|
|
244
|
-
op = node.operator
|
|
245
|
-
op_prec = PRECEDENCE[op]
|
|
246
|
-
op_assoc = ASSOCIATIVITY[op]
|
|
247
|
-
|
|
248
|
-
# Handle unary ops
|
|
249
|
-
if op in ("u-", "not"):
|
|
250
|
-
operand_str, operand_prec = eval_stack.pop()
|
|
251
|
-
|
|
252
|
-
# Add parentheses if the operand's operator has lower precedence
|
|
253
|
-
if operand_prec < op_prec:
|
|
254
|
-
operand_str = f"({operand_str})"
|
|
255
|
-
|
|
256
|
-
if op == "u-":
|
|
257
|
-
new_str = f"(-{operand_str})"
|
|
258
|
-
# A parenthesized expression has the highest precedence
|
|
259
|
-
eval_stack.append((new_str, LEAF_PRECEDENCE))
|
|
260
|
-
else: # Standard handling for 'not'
|
|
261
|
-
new_str = f"not {operand_str}"
|
|
262
|
-
eval_stack.append((new_str, op_prec))
|
|
263
|
-
|
|
264
|
-
# Handle binary ops
|
|
265
|
-
else:
|
|
266
|
-
right_str, right_prec = eval_stack.pop()
|
|
267
|
-
left_str, left_prec = eval_stack.pop()
|
|
268
|
-
|
|
269
|
-
if left_prec < op_prec or (left_prec == op_prec and op_assoc == "right"):
|
|
270
|
-
left_str = f"({left_str})"
|
|
271
|
-
|
|
272
|
-
if right_prec < op_prec or (right_prec == op_prec and op_assoc == "left"):
|
|
273
|
-
right_str = f"({right_str})"
|
|
274
|
-
|
|
275
|
-
if op == "/":
|
|
276
|
-
eval_stack.append((f"\\frac{{{left_str}}}{{{right_str}}}", op_prec))
|
|
277
|
-
continue
|
|
278
|
-
|
|
279
|
-
op = op_map.get(op, op)
|
|
280
|
-
|
|
281
|
-
# get around 80000 line length limitation in GAMS
|
|
282
|
-
length = len(left_str) + len(op) + len(right_str)
|
|
283
|
-
if length >= GMS_MAX_LINE_LENGTH - LINE_LENGTH_OFFSET:
|
|
284
|
-
new_str = f"{left_str} {op}\n {right_str}"
|
|
285
|
-
else:
|
|
286
|
-
new_str = f"{left_str} {op} {right_str}"
|
|
287
|
-
eval_stack.append((new_str, op_prec))
|
|
288
|
-
|
|
289
|
-
final_string = eval_stack[0][0]
|
|
290
|
-
|
|
291
|
-
return final_string
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
class Expression(operable.Operable):
|
|
295
|
-
"""
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
>>>
|
|
313
|
-
>>>
|
|
314
|
-
>>>
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
self.
|
|
340
|
-
|
|
341
|
-
)
|
|
342
|
-
self.
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if d
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if d
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
>>>
|
|
436
|
-
>>>
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
)
|
|
446
|
-
temp_param
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
>>>
|
|
469
|
-
>>>
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
>>>
|
|
498
|
-
>>>
|
|
499
|
-
>>>
|
|
500
|
-
>>>
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
return
|
|
512
|
-
|
|
513
|
-
def
|
|
514
|
-
return Expression(self, "
|
|
515
|
-
|
|
516
|
-
def
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
"
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
def
|
|
528
|
-
self.operator =
|
|
529
|
-
|
|
530
|
-
def
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
stack.append(
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
node
|
|
658
|
-
|
|
659
|
-
stack.
|
|
660
|
-
node = None
|
|
661
|
-
elif isinstance(node,
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
if isinstance(self.
|
|
780
|
-
if isinstance(self.
|
|
781
|
-
if self.
|
|
782
|
-
self.
|
|
783
|
-
elif self.
|
|
784
|
-
self.
|
|
785
|
-
else:
|
|
786
|
-
raise ValidationError(
|
|
787
|
-
f"Incompatible operand `{self.
|
|
788
|
-
)
|
|
789
|
-
elif isinstance(self.
|
|
790
|
-
self.
|
|
791
|
-
):
|
|
792
|
-
if self.
|
|
793
|
-
self.
|
|
794
|
-
elif self.
|
|
795
|
-
self.
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Literal
|
|
5
|
+
|
|
6
|
+
import gamspy._algebra.condition as condition
|
|
7
|
+
import gamspy._algebra.domain as domain
|
|
8
|
+
import gamspy._algebra.number as number
|
|
9
|
+
import gamspy._algebra.operable as operable
|
|
10
|
+
import gamspy._algebra.operation as operation
|
|
11
|
+
import gamspy._symbols as gp_syms
|
|
12
|
+
import gamspy._validation as validation
|
|
13
|
+
import gamspy.utils as utils
|
|
14
|
+
from gamspy._config import get_option
|
|
15
|
+
from gamspy._extrinsic import ExtrinsicFunction
|
|
16
|
+
from gamspy._symbols.implicits import ImplicitSet
|
|
17
|
+
from gamspy._symbols.implicits.implicit_symbol import ImplicitSymbol
|
|
18
|
+
from gamspy._symbols.symbol import Symbol
|
|
19
|
+
from gamspy.exceptions import ValidationError
|
|
20
|
+
from gamspy.math.misc import MathOp
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
import pandas as pd
|
|
24
|
+
|
|
25
|
+
from gamspy import Alias, Set
|
|
26
|
+
from gamspy._symbols.implicits import ImplicitEquation
|
|
27
|
+
from gamspy._types import OperableType
|
|
28
|
+
|
|
29
|
+
GMS_MAX_LINE_LENGTH = 80000
|
|
30
|
+
LINE_LENGTH_OFFSET = 79000
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class DomainPlaceHolder:
|
|
35
|
+
indices: list[tuple[str, int]]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def peek(stack):
|
|
39
|
+
if len(stack) > 0:
|
|
40
|
+
return stack[-1]
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Higher number means higher precedence.
|
|
45
|
+
PRECEDENCE = {
|
|
46
|
+
"..": 0,
|
|
47
|
+
"=": 0,
|
|
48
|
+
"or": 1,
|
|
49
|
+
"xor": 2,
|
|
50
|
+
"and": 3,
|
|
51
|
+
"=e=": 4,
|
|
52
|
+
"=n=": 4,
|
|
53
|
+
"=b=": 4,
|
|
54
|
+
"eq": 4,
|
|
55
|
+
"ne": 4,
|
|
56
|
+
">=": 4,
|
|
57
|
+
"<=": 4,
|
|
58
|
+
">": 4,
|
|
59
|
+
"<": 4,
|
|
60
|
+
"=g=": 4,
|
|
61
|
+
"=l=": 4,
|
|
62
|
+
"=x=": 4,
|
|
63
|
+
"+": 5,
|
|
64
|
+
"-": 5,
|
|
65
|
+
"*": 6,
|
|
66
|
+
"/": 6,
|
|
67
|
+
"not": 7,
|
|
68
|
+
"u-": 7,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Defines how operators of the same precedence are grouped.
|
|
72
|
+
ASSOCIATIVITY = {
|
|
73
|
+
"or": "left",
|
|
74
|
+
"xor": "left",
|
|
75
|
+
"and": "left",
|
|
76
|
+
"=e=": "left",
|
|
77
|
+
"=n=": "left",
|
|
78
|
+
"=x=": "left",
|
|
79
|
+
"=b=": "left",
|
|
80
|
+
"eq": "left",
|
|
81
|
+
"ne": "left",
|
|
82
|
+
"=": "left",
|
|
83
|
+
">=": "left",
|
|
84
|
+
"<=": "left",
|
|
85
|
+
">": "left",
|
|
86
|
+
"<": "left",
|
|
87
|
+
"=g=": "left",
|
|
88
|
+
"=l=": "left",
|
|
89
|
+
"..": "non",
|
|
90
|
+
"+": "left",
|
|
91
|
+
"-": "left",
|
|
92
|
+
"*": "left",
|
|
93
|
+
"/": "left",
|
|
94
|
+
"not": "right",
|
|
95
|
+
"u-": "right",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Precedence for a leaf node is considered infinite.
|
|
99
|
+
LEAF_PRECEDENCE = float("inf")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_operand_gams_repr(operand) -> str:
|
|
103
|
+
if hasattr(operand, "gamsRepr"):
|
|
104
|
+
return operand.gamsRepr()
|
|
105
|
+
|
|
106
|
+
representation = str(operand)
|
|
107
|
+
|
|
108
|
+
# b[i] * -1 -> not valid
|
|
109
|
+
# b[i] * (-1) -> valid
|
|
110
|
+
if isinstance(operand, (int, float)) and operand < 0:
|
|
111
|
+
representation = f"({representation})"
|
|
112
|
+
|
|
113
|
+
return representation
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_operand_latex_repr(operand) -> str:
|
|
117
|
+
if hasattr(operand, "latexRepr"):
|
|
118
|
+
return operand.latexRepr()
|
|
119
|
+
|
|
120
|
+
if isinstance(operand, float):
|
|
121
|
+
operand = utils._map_special_values(operand)
|
|
122
|
+
|
|
123
|
+
representation = str(operand)
|
|
124
|
+
|
|
125
|
+
return representation
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def create_gams_expression(root_node: Expression) -> str:
|
|
129
|
+
"""
|
|
130
|
+
Creates GAMS representation of a binary expression tree without recursion.
|
|
131
|
+
It uses an iterative post-order traversal to build the expression,
|
|
132
|
+
adding parentheses only when necessary based on operator precedence and
|
|
133
|
+
associativity rules.
|
|
134
|
+
"""
|
|
135
|
+
if not isinstance(root_node, Expression):
|
|
136
|
+
return get_operand_gams_repr(root_node)
|
|
137
|
+
|
|
138
|
+
# 1. Get nodes in post-order (left - right - parent).
|
|
139
|
+
s1: list[OperableType | ImplicitEquation | str] = [root_node]
|
|
140
|
+
post_order_nodes = []
|
|
141
|
+
while s1:
|
|
142
|
+
node = s1.pop()
|
|
143
|
+
post_order_nodes.append(node)
|
|
144
|
+
if isinstance(node, Expression):
|
|
145
|
+
if node.left is not None:
|
|
146
|
+
s1.append(node.left)
|
|
147
|
+
if node.right is not None:
|
|
148
|
+
s1.append(node.right)
|
|
149
|
+
|
|
150
|
+
# 2. Build the GAMS expression
|
|
151
|
+
eval_stack: list[tuple[str, float]] = []
|
|
152
|
+
for node in reversed(post_order_nodes):
|
|
153
|
+
if not isinstance(node, Expression):
|
|
154
|
+
eval_stack.append((get_operand_gams_repr(node), LEAF_PRECEDENCE))
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
op = node.operator
|
|
158
|
+
op_prec = PRECEDENCE[op]
|
|
159
|
+
op_assoc = ASSOCIATIVITY[op]
|
|
160
|
+
|
|
161
|
+
# Handle unary ops
|
|
162
|
+
if op in ("u-", "not"):
|
|
163
|
+
operand_str, operand_prec = eval_stack.pop()
|
|
164
|
+
|
|
165
|
+
# Add parentheses if the operand's operator has lower precedence
|
|
166
|
+
if operand_prec < op_prec:
|
|
167
|
+
operand_str = f"({operand_str})"
|
|
168
|
+
|
|
169
|
+
if op == "u-":
|
|
170
|
+
new_str = f"(-{operand_str})"
|
|
171
|
+
# A parenthesized expression has the highest precedence
|
|
172
|
+
eval_stack.append((new_str, LEAF_PRECEDENCE))
|
|
173
|
+
else: # Standard handling for 'not'
|
|
174
|
+
new_str = f"not {operand_str}"
|
|
175
|
+
eval_stack.append((new_str, op_prec))
|
|
176
|
+
|
|
177
|
+
# Handle binary ops
|
|
178
|
+
else:
|
|
179
|
+
right_str, right_prec = eval_stack.pop()
|
|
180
|
+
left_str, left_prec = eval_stack.pop()
|
|
181
|
+
|
|
182
|
+
if left_prec < op_prec or (left_prec == op_prec and op_assoc == "right"):
|
|
183
|
+
left_str = f"({left_str})"
|
|
184
|
+
|
|
185
|
+
if right_prec < op_prec or (right_prec == op_prec and op_assoc == "left"):
|
|
186
|
+
right_str = f"({right_str})"
|
|
187
|
+
|
|
188
|
+
# get around 80000 line length limitation in GAMS
|
|
189
|
+
length = len(left_str) + len(op) + len(right_str)
|
|
190
|
+
if length >= GMS_MAX_LINE_LENGTH - LINE_LENGTH_OFFSET:
|
|
191
|
+
new_str = f"{left_str} {op}\n {right_str}"
|
|
192
|
+
else:
|
|
193
|
+
new_str = f"{left_str} {op} {right_str}"
|
|
194
|
+
eval_stack.append((new_str, op_prec))
|
|
195
|
+
|
|
196
|
+
final_string = eval_stack[0][0]
|
|
197
|
+
|
|
198
|
+
if root_node.operator in ("=", ".."):
|
|
199
|
+
return f"{final_string};"
|
|
200
|
+
|
|
201
|
+
return final_string
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def create_latex_expression(root_node: Expression) -> str:
|
|
205
|
+
"""
|
|
206
|
+
Creates LaTeX representation of a binary expression tree without recursion.
|
|
207
|
+
It uses an iterative post-order traversal to build the expression,
|
|
208
|
+
adding parentheses only when necessary based on operator precedence and
|
|
209
|
+
associativity rules.
|
|
210
|
+
"""
|
|
211
|
+
if not isinstance(root_node, Expression):
|
|
212
|
+
return get_operand_latex_repr(root_node)
|
|
213
|
+
|
|
214
|
+
op_map = {
|
|
215
|
+
"=g=": "\\geq",
|
|
216
|
+
"=l=": "\\leq",
|
|
217
|
+
"=e=": "=",
|
|
218
|
+
"*": "\\cdot",
|
|
219
|
+
"and": "\\wedge",
|
|
220
|
+
"or": "\\vee",
|
|
221
|
+
"xor": "\\oplus",
|
|
222
|
+
"$": "|",
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# 1. Get nodes in post-order (left - right - parent).
|
|
226
|
+
s1: list[OperableType | ImplicitEquation | str] = [root_node]
|
|
227
|
+
post_order_nodes = []
|
|
228
|
+
while s1:
|
|
229
|
+
node = s1.pop()
|
|
230
|
+
post_order_nodes.append(node)
|
|
231
|
+
if isinstance(node, Expression):
|
|
232
|
+
if node.left is not None:
|
|
233
|
+
s1.append(node.left)
|
|
234
|
+
if node.right is not None:
|
|
235
|
+
s1.append(node.right)
|
|
236
|
+
|
|
237
|
+
# 2. Build the LaTeX expression
|
|
238
|
+
eval_stack: list[tuple[str, float]] = []
|
|
239
|
+
for node in reversed(post_order_nodes):
|
|
240
|
+
if not isinstance(node, Expression):
|
|
241
|
+
eval_stack.append((get_operand_latex_repr(node), LEAF_PRECEDENCE))
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
op = node.operator
|
|
245
|
+
op_prec = PRECEDENCE[op]
|
|
246
|
+
op_assoc = ASSOCIATIVITY[op]
|
|
247
|
+
|
|
248
|
+
# Handle unary ops
|
|
249
|
+
if op in ("u-", "not"):
|
|
250
|
+
operand_str, operand_prec = eval_stack.pop()
|
|
251
|
+
|
|
252
|
+
# Add parentheses if the operand's operator has lower precedence
|
|
253
|
+
if operand_prec < op_prec:
|
|
254
|
+
operand_str = f"({operand_str})"
|
|
255
|
+
|
|
256
|
+
if op == "u-":
|
|
257
|
+
new_str = f"(-{operand_str})"
|
|
258
|
+
# A parenthesized expression has the highest precedence
|
|
259
|
+
eval_stack.append((new_str, LEAF_PRECEDENCE))
|
|
260
|
+
else: # Standard handling for 'not'
|
|
261
|
+
new_str = f"not {operand_str}"
|
|
262
|
+
eval_stack.append((new_str, op_prec))
|
|
263
|
+
|
|
264
|
+
# Handle binary ops
|
|
265
|
+
else:
|
|
266
|
+
right_str, right_prec = eval_stack.pop()
|
|
267
|
+
left_str, left_prec = eval_stack.pop()
|
|
268
|
+
|
|
269
|
+
if left_prec < op_prec or (left_prec == op_prec and op_assoc == "right"):
|
|
270
|
+
left_str = f"({left_str})"
|
|
271
|
+
|
|
272
|
+
if right_prec < op_prec or (right_prec == op_prec and op_assoc == "left"):
|
|
273
|
+
right_str = f"({right_str})"
|
|
274
|
+
|
|
275
|
+
if op == "/":
|
|
276
|
+
eval_stack.append((f"\\frac{{{left_str}}}{{{right_str}}}", op_prec))
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
op = op_map.get(op, op)
|
|
280
|
+
|
|
281
|
+
# get around 80000 line length limitation in GAMS
|
|
282
|
+
length = len(left_str) + len(op) + len(right_str)
|
|
283
|
+
if length >= GMS_MAX_LINE_LENGTH - LINE_LENGTH_OFFSET:
|
|
284
|
+
new_str = f"{left_str} {op}\n {right_str}"
|
|
285
|
+
else:
|
|
286
|
+
new_str = f"{left_str} {op} {right_str}"
|
|
287
|
+
eval_stack.append((new_str, op_prec))
|
|
288
|
+
|
|
289
|
+
final_string = eval_stack[0][0]
|
|
290
|
+
|
|
291
|
+
return final_string
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class Expression(operable.Operable):
|
|
295
|
+
"""
|
|
296
|
+
Represents an expression involving two operands and an operator.
|
|
297
|
+
|
|
298
|
+
This class constructs a binary expression tree that can be evaluated or
|
|
299
|
+
translated into GAMS syntax.
|
|
300
|
+
|
|
301
|
+
Parameters
|
|
302
|
+
----------
|
|
303
|
+
left : OperableType | ImplicitEquation | None
|
|
304
|
+
Left operand.
|
|
305
|
+
operator : str
|
|
306
|
+
The operator symbol (e.g., '+', '-', '*', '=', 'eq').
|
|
307
|
+
right : OperableType | str | None
|
|
308
|
+
Right operand.
|
|
309
|
+
|
|
310
|
+
Examples
|
|
311
|
+
--------
|
|
312
|
+
>>> import gamspy as gp
|
|
313
|
+
>>> m = gp.Container()
|
|
314
|
+
>>> a = gp.Parameter(m, name="a")
|
|
315
|
+
>>> b = gp.Parameter(m, name="b")
|
|
316
|
+
>>> expression = a * b
|
|
317
|
+
>>> expression.gamsRepr()
|
|
318
|
+
'a * b'
|
|
319
|
+
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
def __init__(
|
|
323
|
+
self,
|
|
324
|
+
left: OperableType | ImplicitEquation | None,
|
|
325
|
+
operator: str,
|
|
326
|
+
right: OperableType | str | None,
|
|
327
|
+
):
|
|
328
|
+
self.left = utils._map_special_values(left) if isinstance(left, float) else left
|
|
329
|
+
self.operator = operator
|
|
330
|
+
self.right = (
|
|
331
|
+
utils._map_special_values(right) if isinstance(right, float) else right
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
if operator == "=" and isinstance(right, Expression):
|
|
335
|
+
right._fix_equalities()
|
|
336
|
+
|
|
337
|
+
self._representation: str | None = None
|
|
338
|
+
self.where = condition.Condition(self)
|
|
339
|
+
self._create_domain()
|
|
340
|
+
left_control = getattr(left, "controlled_domain", [])
|
|
341
|
+
right_control = getattr(right, "controlled_domain", [])
|
|
342
|
+
self.controlled_domain: list[Set | Alias] = list(
|
|
343
|
+
{*left_control, *right_control}
|
|
344
|
+
)
|
|
345
|
+
self.container = None
|
|
346
|
+
if hasattr(left, "container"):
|
|
347
|
+
self.container = left.container # type: ignore
|
|
348
|
+
elif hasattr(right, "container"):
|
|
349
|
+
self.container = right.container # type: ignore
|
|
350
|
+
|
|
351
|
+
@property
|
|
352
|
+
def representation(self) -> str:
|
|
353
|
+
if self._representation is None:
|
|
354
|
+
self._representation = create_gams_expression(self)
|
|
355
|
+
|
|
356
|
+
return self._representation
|
|
357
|
+
|
|
358
|
+
def _create_domain(self):
|
|
359
|
+
for loc, result in (
|
|
360
|
+
(self.left, "_left_domain"),
|
|
361
|
+
(self.right, "_right_domain"),
|
|
362
|
+
):
|
|
363
|
+
if isinstance(loc, condition.Condition):
|
|
364
|
+
loc = loc.conditioning_on
|
|
365
|
+
|
|
366
|
+
if loc is None or isinstance(loc, (int, float, str)):
|
|
367
|
+
result_domain = [] # left is a scalar
|
|
368
|
+
elif isinstance(loc, domain.Domain):
|
|
369
|
+
result_domain = loc.sets
|
|
370
|
+
else:
|
|
371
|
+
result_domain = loc.domain
|
|
372
|
+
|
|
373
|
+
setattr(self, result, result_domain)
|
|
374
|
+
|
|
375
|
+
left_domain = self._left_domain
|
|
376
|
+
right_domain = self._right_domain
|
|
377
|
+
|
|
378
|
+
set_to_index = {}
|
|
379
|
+
|
|
380
|
+
for domain_char, domain_ptr in (
|
|
381
|
+
("l", left_domain),
|
|
382
|
+
("r", right_domain),
|
|
383
|
+
):
|
|
384
|
+
for i, d in enumerate(domain_ptr):
|
|
385
|
+
if isinstance(d, str):
|
|
386
|
+
continue # string domains are fixed and they do not count
|
|
387
|
+
|
|
388
|
+
if d not in set_to_index:
|
|
389
|
+
set_to_index[d] = []
|
|
390
|
+
|
|
391
|
+
set_to_index[d].append((domain_char, i))
|
|
392
|
+
|
|
393
|
+
shadow_domain = []
|
|
394
|
+
result_domain = []
|
|
395
|
+
for d in (*left_domain, *right_domain):
|
|
396
|
+
if isinstance(d, str):
|
|
397
|
+
continue
|
|
398
|
+
|
|
399
|
+
if d not in result_domain:
|
|
400
|
+
result_domain.append(d)
|
|
401
|
+
indices = set_to_index[d]
|
|
402
|
+
shadow_domain.append(DomainPlaceHolder(indices=indices))
|
|
403
|
+
|
|
404
|
+
self._shadow_domain = shadow_domain
|
|
405
|
+
self.domain = result_domain
|
|
406
|
+
self.dimension = validation.get_dimension(self.domain)
|
|
407
|
+
|
|
408
|
+
def __getitem__(self, indices):
|
|
409
|
+
indices = validation.validate_domain(self, indices)
|
|
410
|
+
left_domain = list(self._left_domain)
|
|
411
|
+
right_domain = list(self._right_domain)
|
|
412
|
+
for i, s in enumerate(indices):
|
|
413
|
+
for lr, pos in self._shadow_domain[i].indices:
|
|
414
|
+
if lr == "l":
|
|
415
|
+
left_domain[pos] = s
|
|
416
|
+
else:
|
|
417
|
+
right_domain[pos] = s
|
|
418
|
+
|
|
419
|
+
left = self.left[left_domain] if left_domain else self.left
|
|
420
|
+
right = self.right[right_domain] if right_domain else self.right
|
|
421
|
+
|
|
422
|
+
return Expression(left, self.operator, right)
|
|
423
|
+
|
|
424
|
+
@property
|
|
425
|
+
def records(self) -> pd.DataFrame | None:
|
|
426
|
+
"""
|
|
427
|
+
Evaluates the expression and returns the resulting records.
|
|
428
|
+
|
|
429
|
+
Returns
|
|
430
|
+
-------
|
|
431
|
+
pd.DataFrame | None
|
|
432
|
+
|
|
433
|
+
Examples
|
|
434
|
+
--------
|
|
435
|
+
>>> import gamspy as gp
|
|
436
|
+
>>> m = gp.Container()
|
|
437
|
+
>>> a = gp.Parameter(m, records=5)
|
|
438
|
+
>>> b = gp.Parameter(m, records=6)
|
|
439
|
+
>>> (a + b).records
|
|
440
|
+
value
|
|
441
|
+
0 11.0
|
|
442
|
+
|
|
443
|
+
"""
|
|
444
|
+
assert self.container is not None
|
|
445
|
+
temp_name = "a" + utils._get_unique_name()
|
|
446
|
+
temp_param = gp_syms.Parameter._constructor_bypass(
|
|
447
|
+
self.container, temp_name, self.domain
|
|
448
|
+
)
|
|
449
|
+
temp_param[...] = self
|
|
450
|
+
del self.container.data[temp_name]
|
|
451
|
+
return temp_param.records
|
|
452
|
+
|
|
453
|
+
def toValue(self) -> float | None:
|
|
454
|
+
"""
|
|
455
|
+
Convenience method to return expression records as a Python float. Only possible if there is a single record as a result of the expression evaluation.
|
|
456
|
+
|
|
457
|
+
Returns
|
|
458
|
+
-------
|
|
459
|
+
float | None
|
|
460
|
+
|
|
461
|
+
Raises
|
|
462
|
+
------
|
|
463
|
+
TypeError
|
|
464
|
+
In case the dimension of the expression is not zero.
|
|
465
|
+
|
|
466
|
+
Examples
|
|
467
|
+
--------
|
|
468
|
+
>>> import gamspy as gp
|
|
469
|
+
>>> m = gp.Container()
|
|
470
|
+
>>> a = gp.Parameter(m, records=5)
|
|
471
|
+
>>> b = gp.Parameter(m, records=6)
|
|
472
|
+
>>> (a + b).toValue()
|
|
473
|
+
np.float64(11.0)
|
|
474
|
+
|
|
475
|
+
"""
|
|
476
|
+
if self.dimension != 0:
|
|
477
|
+
raise TypeError(
|
|
478
|
+
f"Cannot extract value data for non-scalar expressions (expression dimension is {self.dimension})"
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
records = self.records
|
|
482
|
+
if records is not None:
|
|
483
|
+
return records["value"][0]
|
|
484
|
+
|
|
485
|
+
return records
|
|
486
|
+
|
|
487
|
+
def toList(self) -> list | None:
|
|
488
|
+
"""
|
|
489
|
+
Convenience method to return the records of the expression as a list.
|
|
490
|
+
|
|
491
|
+
Returns
|
|
492
|
+
-------
|
|
493
|
+
list | None
|
|
494
|
+
|
|
495
|
+
Examples
|
|
496
|
+
--------
|
|
497
|
+
>>> import numpy as np
|
|
498
|
+
>>> import gamspy as gp
|
|
499
|
+
>>> m = gp.Container()
|
|
500
|
+
>>> i = gp.Set(m, records=range(3))
|
|
501
|
+
>>> a = gp.Parameter(m, domain=i, records=np.array([1,2,3]))
|
|
502
|
+
>>> b = gp.Parameter(m, domain=i, records=np.array([4,5,6]))
|
|
503
|
+
>>> (a + b).toList()
|
|
504
|
+
[['0', 5.0], ['1', 7.0], ['2', 9.0]]
|
|
505
|
+
|
|
506
|
+
"""
|
|
507
|
+
records = self.records
|
|
508
|
+
if records is not None:
|
|
509
|
+
return records.values.tolist()
|
|
510
|
+
|
|
511
|
+
return None
|
|
512
|
+
|
|
513
|
+
def __eq__(self, other):
|
|
514
|
+
return Expression(self, "=e=", other)
|
|
515
|
+
|
|
516
|
+
def __ne__(self, other):
|
|
517
|
+
return Expression(self, "ne", other)
|
|
518
|
+
|
|
519
|
+
def __bool__(self):
|
|
520
|
+
raise ValidationError(
|
|
521
|
+
"An expression cannot be used as a truth value. If you are "
|
|
522
|
+
"trying to generate an expression, use binary operators "
|
|
523
|
+
"instead (e.g. &, |, ^). For more details, see: "
|
|
524
|
+
"https://gamspy.readthedocs.io/en/latest/user/gamspy_for_gams_users.html#logical-operations"
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
def __repr__(self) -> str:
|
|
528
|
+
return f"Expression(left={self.left}, data={self.operator}, right={self.right})"
|
|
529
|
+
|
|
530
|
+
def _replace_operator(self, operator: str):
|
|
531
|
+
self.operator = operator
|
|
532
|
+
|
|
533
|
+
def latexRepr(self) -> str:
|
|
534
|
+
"""
|
|
535
|
+
Returns the LaTeX representation of this Expression.
|
|
536
|
+
|
|
537
|
+
Returns
|
|
538
|
+
-------
|
|
539
|
+
str
|
|
540
|
+
The LaTeX string.
|
|
541
|
+
|
|
542
|
+
Examples
|
|
543
|
+
--------
|
|
544
|
+
>>> import gamspy as gp
|
|
545
|
+
>>> m = gp.Container()
|
|
546
|
+
>>> a = gp.Parameter(m, name="a")
|
|
547
|
+
>>> b = gp.Parameter(m, name="b")
|
|
548
|
+
>>> (a + b).latexRepr()
|
|
549
|
+
'a + b'
|
|
550
|
+
|
|
551
|
+
"""
|
|
552
|
+
return create_latex_expression(self)
|
|
553
|
+
|
|
554
|
+
def gamsRepr(self) -> str:
|
|
555
|
+
"""
|
|
556
|
+
Representation of this Expression in GAMS language.
|
|
557
|
+
|
|
558
|
+
Returns
|
|
559
|
+
-------
|
|
560
|
+
str
|
|
561
|
+
|
|
562
|
+
Examples
|
|
563
|
+
--------
|
|
564
|
+
>>> import gamspy as gp
|
|
565
|
+
>>> m = gp.Container()
|
|
566
|
+
>>> a = gp.Parameter(m, name="a")
|
|
567
|
+
>>> b = gp.Parameter(m, name="b")
|
|
568
|
+
>>> expression = a * b
|
|
569
|
+
>>> expression.gamsRepr()
|
|
570
|
+
'a * b'
|
|
571
|
+
|
|
572
|
+
"""
|
|
573
|
+
return self.representation
|
|
574
|
+
|
|
575
|
+
def getDeclaration(self) -> str:
|
|
576
|
+
"""
|
|
577
|
+
Declaration of the Expression in GAMS
|
|
578
|
+
|
|
579
|
+
Returns
|
|
580
|
+
-------
|
|
581
|
+
str
|
|
582
|
+
|
|
583
|
+
Examples
|
|
584
|
+
--------
|
|
585
|
+
>>> import gamspy as gp
|
|
586
|
+
>>> m = gp.Container()
|
|
587
|
+
>>> a = gp.Parameter(m, name="a")
|
|
588
|
+
>>> b = gp.Parameter(m, name="b")
|
|
589
|
+
>>> expression = a * b
|
|
590
|
+
>>> expression.getDeclaration()
|
|
591
|
+
'a * b'
|
|
592
|
+
|
|
593
|
+
"""
|
|
594
|
+
return self.gamsRepr()
|
|
595
|
+
|
|
596
|
+
def _fix_equalities(self) -> None:
|
|
597
|
+
# Equality operations on Parameter and Variable objects generate
|
|
598
|
+
# GAMS equality signs: =g=, =e=, =l=. If these signs appear on
|
|
599
|
+
# assignments, replace them with regular equality ops.
|
|
600
|
+
# Uses a stack based post-order traversal algorithm.
|
|
601
|
+
EQ_MAP: dict[str, str] = {"=g=": ">=", "=e=": "eq", "=l=": "<="}
|
|
602
|
+
stack = []
|
|
603
|
+
root = self
|
|
604
|
+
|
|
605
|
+
while True:
|
|
606
|
+
while root is not None:
|
|
607
|
+
if hasattr(root, "right"):
|
|
608
|
+
stack.append(root.right)
|
|
609
|
+
|
|
610
|
+
stack.append(root)
|
|
611
|
+
root = root.left if hasattr(root, "left") else None # type: ignore
|
|
612
|
+
|
|
613
|
+
if len(stack) == 0:
|
|
614
|
+
break
|
|
615
|
+
|
|
616
|
+
root = stack.pop()
|
|
617
|
+
|
|
618
|
+
if isinstance(root, Expression) and root.operator in EQ_MAP:
|
|
619
|
+
root._replace_operator(EQ_MAP[root.operator])
|
|
620
|
+
|
|
621
|
+
last_item = peek(stack)
|
|
622
|
+
if (
|
|
623
|
+
hasattr(root, "right")
|
|
624
|
+
and last_item is not None
|
|
625
|
+
and last_item is root.right
|
|
626
|
+
):
|
|
627
|
+
stack.pop()
|
|
628
|
+
stack.append(root)
|
|
629
|
+
root = root.right
|
|
630
|
+
else:
|
|
631
|
+
root = None
|
|
632
|
+
|
|
633
|
+
def _find_all_symbols(self) -> list[str]:
|
|
634
|
+
# Finds all symbols in an expression with a stack based inorder
|
|
635
|
+
# traversal algorithm (O(N)).
|
|
636
|
+
symbols: list[str] = []
|
|
637
|
+
stack = []
|
|
638
|
+
|
|
639
|
+
node = self
|
|
640
|
+
while True:
|
|
641
|
+
if node is not None:
|
|
642
|
+
stack.append(node)
|
|
643
|
+
node = getattr(node, "left", None) # type: ignore
|
|
644
|
+
elif stack:
|
|
645
|
+
node = stack.pop()
|
|
646
|
+
|
|
647
|
+
if isinstance(node, Symbol):
|
|
648
|
+
if node.name not in symbols:
|
|
649
|
+
if type(node) is gp_syms.Alias:
|
|
650
|
+
symbols.append(node.alias_with.name)
|
|
651
|
+
|
|
652
|
+
symbols.append(node.name)
|
|
653
|
+
stack.extend(node.domain)
|
|
654
|
+
node = None
|
|
655
|
+
elif isinstance(node, ImplicitSymbol):
|
|
656
|
+
if node.parent.name not in symbols:
|
|
657
|
+
symbols.append(node.parent.name)
|
|
658
|
+
stack.extend(node.domain)
|
|
659
|
+
stack.extend(node.container[node.parent.name].domain)
|
|
660
|
+
node = None
|
|
661
|
+
elif isinstance(node, operation.Operation):
|
|
662
|
+
stack.extend(node.op_domain)
|
|
663
|
+
node = node.rhs
|
|
664
|
+
elif isinstance(node, condition.Condition):
|
|
665
|
+
stack.append(node.conditioning_on)
|
|
666
|
+
|
|
667
|
+
if isinstance(node.condition, Expression):
|
|
668
|
+
node = node.condition
|
|
669
|
+
else:
|
|
670
|
+
stack.append(node.condition)
|
|
671
|
+
node = None
|
|
672
|
+
elif isinstance(node, (operation.Ord, operation.Card)):
|
|
673
|
+
stack.append(node._symbol)
|
|
674
|
+
node = None
|
|
675
|
+
elif isinstance(node, MathOp):
|
|
676
|
+
if isinstance(node.elements[0], Expression):
|
|
677
|
+
node = node.elements[0]
|
|
678
|
+
else:
|
|
679
|
+
stack.extend(node.elements)
|
|
680
|
+
node = None
|
|
681
|
+
elif isinstance(node, ExtrinsicFunction):
|
|
682
|
+
stack.extend(list(node.args))
|
|
683
|
+
node = None
|
|
684
|
+
else:
|
|
685
|
+
node = getattr(node, "right", None)
|
|
686
|
+
else:
|
|
687
|
+
break # pragma: no cover
|
|
688
|
+
|
|
689
|
+
return symbols
|
|
690
|
+
|
|
691
|
+
def _find_symbols_in_conditions(self) -> list[str]:
|
|
692
|
+
symbols: list[str] = []
|
|
693
|
+
stack = []
|
|
694
|
+
|
|
695
|
+
node = self
|
|
696
|
+
while True:
|
|
697
|
+
if node is not None:
|
|
698
|
+
stack.append(node)
|
|
699
|
+
node = getattr(node, "left", None) # type: ignore
|
|
700
|
+
elif stack:
|
|
701
|
+
node = stack.pop()
|
|
702
|
+
|
|
703
|
+
if isinstance(node, condition.Condition):
|
|
704
|
+
given_condition = node.condition
|
|
705
|
+
|
|
706
|
+
if isinstance(given_condition, Expression):
|
|
707
|
+
symbols.extend(given_condition._find_all_symbols())
|
|
708
|
+
elif isinstance(given_condition, ImplicitSymbol):
|
|
709
|
+
symbols.append(given_condition.parent.name)
|
|
710
|
+
|
|
711
|
+
if isinstance(node, operation.Operation):
|
|
712
|
+
stack.extend(node.op_domain)
|
|
713
|
+
node = node.rhs
|
|
714
|
+
else:
|
|
715
|
+
node = getattr(node, "right", None)
|
|
716
|
+
else:
|
|
717
|
+
break # pragma: no cover
|
|
718
|
+
|
|
719
|
+
return symbols
|
|
720
|
+
|
|
721
|
+
def _validate_definition(
|
|
722
|
+
self, control_stack: list[Set | Alias | ImplicitSet]
|
|
723
|
+
) -> None:
|
|
724
|
+
if not get_option("DOMAIN_VALIDATION") or not get_option("DOMAIN_VALIDATION"):
|
|
725
|
+
return
|
|
726
|
+
|
|
727
|
+
stack = []
|
|
728
|
+
|
|
729
|
+
node = self.right
|
|
730
|
+
while True:
|
|
731
|
+
if node is not None:
|
|
732
|
+
stack.append(node)
|
|
733
|
+
node = getattr(node, "left", None) # type: ignore
|
|
734
|
+
elif stack:
|
|
735
|
+
node = stack.pop()
|
|
736
|
+
|
|
737
|
+
if isinstance(node, operation.Operation):
|
|
738
|
+
node._validate_operation(control_stack.copy())
|
|
739
|
+
elif isinstance(node, ImplicitSymbol):
|
|
740
|
+
for elem in node.domain:
|
|
741
|
+
if hasattr(elem, "is_singleton") and elem.is_singleton:
|
|
742
|
+
continue
|
|
743
|
+
|
|
744
|
+
if isinstance(elem, Symbol) and elem not in control_stack:
|
|
745
|
+
raise ValidationError(
|
|
746
|
+
f"Uncontrolled set `{elem}` entered as constant!"
|
|
747
|
+
)
|
|
748
|
+
elif (
|
|
749
|
+
isinstance(elem, ImplicitSymbol)
|
|
750
|
+
and elem.parent not in control_stack
|
|
751
|
+
):
|
|
752
|
+
raise ValidationError(
|
|
753
|
+
f"Uncontrolled set `{elem.parent}` entered as constant!"
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
node = getattr(node, "right", None)
|
|
757
|
+
else:
|
|
758
|
+
break # pragma: no cover
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
class SetExpression(Expression):
|
|
762
|
+
"""
|
|
763
|
+
Represents an expression involving set operations.
|
|
764
|
+
|
|
765
|
+
This class handles operations specifically for Sets and Aliases, such as
|
|
766
|
+
union, intersection, difference, and complement.
|
|
767
|
+
"""
|
|
768
|
+
|
|
769
|
+
def __init__(
|
|
770
|
+
self,
|
|
771
|
+
left: OperableType,
|
|
772
|
+
data: Literal["+", "-", "*", "not"],
|
|
773
|
+
right: OperableType,
|
|
774
|
+
):
|
|
775
|
+
super().__init__(left, data, right)
|
|
776
|
+
self._adjust_left_right()
|
|
777
|
+
|
|
778
|
+
def _adjust_left_right(self) -> None:
|
|
779
|
+
if isinstance(self.left, (ImplicitSet, SetExpression)):
|
|
780
|
+
if isinstance(self.right, (int, float)):
|
|
781
|
+
if self.right == 0:
|
|
782
|
+
self.right = "no"
|
|
783
|
+
elif self.right == 1:
|
|
784
|
+
self.right = "yes"
|
|
785
|
+
else:
|
|
786
|
+
raise ValidationError(
|
|
787
|
+
f"Incompatible operand `{self.right}` for the set operation `{self.operator}`."
|
|
788
|
+
)
|
|
789
|
+
elif isinstance(self.right, condition.Condition) and isinstance(
|
|
790
|
+
self.right.conditioning_on, number.Number
|
|
791
|
+
):
|
|
792
|
+
if self.right.conditioning_on._value == 0:
|
|
793
|
+
self.right.conditioning_on._value = "no"
|
|
794
|
+
elif self.right.conditioning_on._value == 1:
|
|
795
|
+
self.right.conditioning_on._value = "yes"
|
|
796
|
+
raise ValidationError(
|
|
797
|
+
f"Incompatible operand `{self.right}` for the set operation `{self.operator}`."
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
if isinstance(self.right, (ImplicitSet, SetExpression)):
|
|
801
|
+
if isinstance(self.left, (int, float)):
|
|
802
|
+
if self.left == 0:
|
|
803
|
+
self.left = "no"
|
|
804
|
+
elif self.left == 1:
|
|
805
|
+
self.left = "yes"
|
|
806
|
+
else:
|
|
807
|
+
raise ValidationError(
|
|
808
|
+
f"Incompatible operand `{self.left}` for the set operation `{self.operator}`."
|
|
809
|
+
)
|
|
810
|
+
elif isinstance(self.left, condition.Condition) and isinstance(
|
|
811
|
+
self.left.conditioning_on, number.Number
|
|
812
|
+
):
|
|
813
|
+
if self.left.conditioning_on._value == 0:
|
|
814
|
+
self.left.conditioning_on._value = "no"
|
|
815
|
+
elif self.left.conditioning_on._value == 1:
|
|
816
|
+
self.left.conditioning_on._value = "yes"
|
|
817
|
+
else:
|
|
818
|
+
raise ValidationError(
|
|
819
|
+
f"Incompatible operand `{self.left}` for the set operation `{self.operator}`."
|
|
820
|
+
)
|