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.
Files changed (89) hide show
  1. gamspy/__init__.py +86 -98
  2. gamspy/__main__.py +6 -6
  3. gamspy/_algebra/__init__.py +13 -13
  4. gamspy/_algebra/condition.py +290 -194
  5. gamspy/_algebra/domain.py +103 -93
  6. gamspy/_algebra/expression.py +820 -799
  7. gamspy/_algebra/number.py +79 -70
  8. gamspy/_algebra/operable.py +185 -185
  9. gamspy/_algebra/operation.py +948 -845
  10. gamspy/_backend/backend.py +313 -311
  11. gamspy/_backend/engine.py +960 -960
  12. gamspy/_backend/local.py +124 -124
  13. gamspy/_backend/neos.py +567 -567
  14. gamspy/_cli/__init__.py +1 -1
  15. gamspy/_cli/cli.py +64 -64
  16. gamspy/_cli/gdx.py +377 -377
  17. gamspy/_cli/install.py +375 -372
  18. gamspy/_cli/list.py +94 -94
  19. gamspy/_cli/mps2gms.py +128 -128
  20. gamspy/_cli/probe.py +52 -52
  21. gamspy/_cli/retrieve.py +79 -79
  22. gamspy/_cli/run.py +158 -158
  23. gamspy/_cli/show.py +246 -255
  24. gamspy/_cli/uninstall.py +165 -165
  25. gamspy/_cli/util.py +94 -94
  26. gamspy/_communication.py +215 -215
  27. gamspy/_config.py +132 -132
  28. gamspy/_container.py +1694 -1452
  29. gamspy/_convert.py +720 -720
  30. gamspy/_database.py +271 -271
  31. gamspy/_extrinsic.py +181 -181
  32. gamspy/_miro.py +356 -352
  33. gamspy/_model.py +1803 -1615
  34. gamspy/_model_instance.py +701 -701
  35. gamspy/_options.py +780 -700
  36. gamspy/_serialization.py +156 -144
  37. gamspy/_symbols/__init__.py +17 -17
  38. gamspy/_symbols/alias.py +305 -299
  39. gamspy/_symbols/equation.py +1407 -1298
  40. gamspy/_symbols/implicits/__init__.py +11 -11
  41. gamspy/_symbols/implicits/implicit_equation.py +186 -186
  42. gamspy/_symbols/implicits/implicit_parameter.py +272 -272
  43. gamspy/_symbols/implicits/implicit_set.py +124 -124
  44. gamspy/_symbols/implicits/implicit_symbol.py +315 -315
  45. gamspy/_symbols/implicits/implicit_variable.py +255 -255
  46. gamspy/_symbols/parameter.py +648 -609
  47. gamspy/_symbols/set.py +985 -923
  48. gamspy/_symbols/symbol.py +395 -386
  49. gamspy/_symbols/universe_alias.py +182 -182
  50. gamspy/_symbols/variable.py +1101 -1017
  51. gamspy/_types.py +7 -7
  52. gamspy/_validation.py +735 -735
  53. gamspy/_workspace.py +72 -72
  54. gamspy/exceptions.py +128 -128
  55. gamspy/formulations/__init__.py +46 -46
  56. gamspy/formulations/ml/__init__.py +11 -11
  57. gamspy/formulations/ml/decision_tree_struct.py +80 -80
  58. gamspy/formulations/ml/gradient_boosting.py +203 -203
  59. gamspy/formulations/ml/random_forest.py +187 -187
  60. gamspy/formulations/ml/regression_tree.py +533 -533
  61. gamspy/formulations/nn/__init__.py +19 -19
  62. gamspy/formulations/nn/avgpool2d.py +232 -232
  63. gamspy/formulations/nn/conv1d.py +533 -533
  64. gamspy/formulations/nn/conv2d.py +529 -529
  65. gamspy/formulations/nn/linear.py +341 -341
  66. gamspy/formulations/nn/maxpool2d.py +88 -88
  67. gamspy/formulations/nn/minpool2d.py +88 -88
  68. gamspy/formulations/nn/mpool2d.py +245 -245
  69. gamspy/formulations/nn/torch_sequential.py +278 -278
  70. gamspy/formulations/piecewise.py +682 -682
  71. gamspy/formulations/result.py +119 -119
  72. gamspy/formulations/shape.py +188 -188
  73. gamspy/formulations/utils.py +173 -173
  74. gamspy/math/__init__.py +215 -215
  75. gamspy/math/activation.py +783 -767
  76. gamspy/math/log_power.py +435 -435
  77. gamspy/math/matrix.py +534 -534
  78. gamspy/math/misc.py +1709 -1625
  79. gamspy/math/probability.py +170 -170
  80. gamspy/math/trigonometric.py +232 -232
  81. gamspy/utils.py +810 -791
  82. gamspy/version.py +5 -5
  83. {gamspy-1.18.3.dist-info → gamspy-1.19.0.dist-info}/METADATA +90 -121
  84. gamspy-1.19.0.dist-info/RECORD +90 -0
  85. {gamspy-1.18.3.dist-info → gamspy-1.19.0.dist-info}/WHEEL +1 -1
  86. {gamspy-1.18.3.dist-info → gamspy-1.19.0.dist-info}/licenses/LICENSE +22 -22
  87. gamspy-1.18.3.dist-info/RECORD +0 -90
  88. {gamspy-1.18.3.dist-info → gamspy-1.19.0.dist-info}/entry_points.txt +0 -0
  89. {gamspy-1.18.3.dist-info → gamspy-1.19.0.dist-info}/top_level.txt +0 -0
@@ -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
- Expression of two operands and an operation.
297
-
298
- Parameters
299
- ----------
300
- left: str | int | float | Parameter | Variable | None
301
- Left operand
302
- data: str
303
- Operation
304
- right: str | int | float | Parameter | Variable | None
305
- Right operand
306
-
307
- Examples
308
- --------
309
- >>> import gamspy as gp
310
- >>> m = gp.Container()
311
- >>> a = gp.Parameter(m, name="a")
312
- >>> b = gp.Parameter(m, name="b")
313
- >>> expression = a * b
314
- >>> expression.gamsRepr()
315
- 'a * b'
316
-
317
- """
318
-
319
- def __init__(
320
- self,
321
- left: OperableType | ImplicitEquation | None,
322
- operator: str,
323
- right: OperableType | str | None,
324
- ):
325
- self.left = utils._map_special_values(left) if isinstance(left, float) else left
326
- self.operator = operator
327
- self.right = (
328
- utils._map_special_values(right) if isinstance(right, float) else right
329
- )
330
-
331
- if operator == "=" and isinstance(right, Expression):
332
- right._fix_equalities()
333
-
334
- self._representation: str | None = None
335
- self.where = condition.Condition(self)
336
- self._create_domain()
337
- left_control = getattr(left, "controlled_domain", [])
338
- right_control = getattr(right, "controlled_domain", [])
339
- self.controlled_domain: list[Set | Alias] = list(
340
- {*left_control, *right_control}
341
- )
342
- self.container = None
343
- if hasattr(left, "container"):
344
- self.container = left.container # type: ignore
345
- elif hasattr(right, "container"):
346
- self.container = right.container # type: ignore
347
-
348
- @property
349
- def representation(self) -> str:
350
- if self._representation is None:
351
- self._representation = create_gams_expression(self)
352
-
353
- return self._representation
354
-
355
- def _create_domain(self):
356
- for loc, result in (
357
- (self.left, "_left_domain"),
358
- (self.right, "_right_domain"),
359
- ):
360
- if isinstance(loc, condition.Condition):
361
- loc = loc.conditioning_on
362
-
363
- if loc is None or isinstance(loc, (int, float, str)):
364
- result_domain = [] # left is a scalar
365
- elif isinstance(loc, domain.Domain):
366
- result_domain = loc.sets
367
- else:
368
- result_domain = loc.domain
369
-
370
- setattr(self, result, result_domain)
371
-
372
- left_domain = self._left_domain
373
- right_domain = self._right_domain
374
-
375
- set_to_index = {}
376
-
377
- for domain_char, domain_ptr in (
378
- ("l", left_domain),
379
- ("r", right_domain),
380
- ):
381
- for i, d in enumerate(domain_ptr):
382
- if isinstance(d, str):
383
- continue # string domains are fixed and they do not count
384
-
385
- if d not in set_to_index:
386
- set_to_index[d] = []
387
-
388
- set_to_index[d].append((domain_char, i))
389
-
390
- shadow_domain = []
391
- result_domain = []
392
- for d in (*left_domain, *right_domain):
393
- if isinstance(d, str):
394
- continue
395
-
396
- if d not in result_domain:
397
- result_domain.append(d)
398
- indices = set_to_index[d]
399
- shadow_domain.append(DomainPlaceHolder(indices=indices))
400
-
401
- self._shadow_domain = shadow_domain
402
- self.domain = result_domain
403
- self.dimension = validation.get_dimension(self.domain)
404
-
405
- def __getitem__(self, indices):
406
- indices = validation.validate_domain(self, indices)
407
- left_domain = list(self._left_domain)
408
- right_domain = list(self._right_domain)
409
- for i, s in enumerate(indices):
410
- for lr, pos in self._shadow_domain[i].indices:
411
- if lr == "l":
412
- left_domain[pos] = s
413
- else:
414
- right_domain[pos] = s
415
-
416
- left = self.left[left_domain] if left_domain else self.left
417
- right = self.right[right_domain] if right_domain else self.right
418
-
419
- return Expression(left, self.operator, right)
420
-
421
- @property
422
- def records(self) -> pd.DataFrame | None:
423
- """
424
- Evaluates the expression and returns the resulting records.
425
-
426
- Returns
427
- -------
428
- pd.DataFrame | None
429
-
430
- Examples
431
- --------
432
- >>> import gamspy as gp
433
- >>> m = gp.Container()
434
- >>> a = gp.Parameter(m, records=5)
435
- >>> b = gp.Parameter(m, records=6)
436
- >>> (a + b).records
437
- value
438
- 0 11.0
439
-
440
- """
441
- assert self.container is not None
442
- temp_name = "a" + utils._get_unique_name()
443
- temp_param = gp_syms.Parameter._constructor_bypass(
444
- self.container, temp_name, self.domain
445
- )
446
- temp_param[...] = self
447
- del self.container.data[temp_name]
448
- return temp_param.records
449
-
450
- def toValue(self) -> float | None:
451
- """
452
- 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.
453
-
454
- Returns
455
- -------
456
- float | None
457
-
458
- Raises
459
- ------
460
- TypeError
461
- In case the dimension of the expression is not zero.
462
-
463
- Examples
464
- --------
465
- >>> import gamspy as gp
466
- >>> m = gp.Container()
467
- >>> a = gp.Parameter(m, records=5)
468
- >>> b = gp.Parameter(m, records=6)
469
- >>> (a + b).toValue()
470
- np.float64(11.0)
471
-
472
- """
473
- if self.dimension != 0:
474
- raise TypeError(
475
- f"Cannot extract value data for non-scalar expressions (expression dimension is {self.dimension})"
476
- )
477
-
478
- records = self.records
479
- if records is not None:
480
- return records["value"][0]
481
-
482
- return records
483
-
484
- def toList(self) -> list | None:
485
- """
486
- Convenience method to return the records of the expression as a list.
487
-
488
- Returns
489
- -------
490
- list | None
491
-
492
- Examples
493
- --------
494
- >>> import numpy as np
495
- >>> import gamspy as gp
496
- >>> m = gp.Container()
497
- >>> i = gp.Set(m, records=range(3))
498
- >>> a = gp.Parameter(m, domain=i, records=np.array([1,2,3]))
499
- >>> b = gp.Parameter(m, domain=i, records=np.array([4,5,6]))
500
- >>> (a + b).toList()
501
- [['0', 5.0], ['1', 7.0], ['2', 9.0]]
502
-
503
- """
504
- records = self.records
505
- if records is not None:
506
- return records.values.tolist()
507
-
508
- return None
509
-
510
- def __eq__(self, other):
511
- return Expression(self, "=e=", other)
512
-
513
- def __ne__(self, other):
514
- return Expression(self, "ne", other)
515
-
516
- def __bool__(self):
517
- raise ValidationError(
518
- "An expression cannot be used as a truth value. If you are "
519
- "trying to generate an expression, use binary operators "
520
- "instead (e.g. &, |, ^). For more details, see: "
521
- "https://gamspy.readthedocs.io/en/latest/user/gamspy_for_gams_users.html#logical-operations"
522
- )
523
-
524
- def __repr__(self) -> str:
525
- return f"Expression(left={self.left}, data={self.operator}, right={self.right})"
526
-
527
- def _replace_operator(self, operator: str):
528
- self.operator = operator
529
-
530
- def latexRepr(self) -> str:
531
- """
532
- Representation of this Expression in Latex.
533
-
534
- Returns
535
- -------
536
- str
537
- """
538
- return create_latex_expression(self)
539
-
540
- def gamsRepr(self) -> str:
541
- """
542
- Representation of this Expression in GAMS language.
543
-
544
- Returns
545
- -------
546
- str
547
-
548
- Examples
549
- --------
550
- >>> import gamspy as gp
551
- >>> m = gp.Container()
552
- >>> a = gp.Parameter(m, name="a")
553
- >>> b = gp.Parameter(m, name="b")
554
- >>> expression = a * b
555
- >>> expression.gamsRepr()
556
- 'a * b'
557
-
558
- """
559
- return self.representation
560
-
561
- def getDeclaration(self) -> str:
562
- """
563
- Declaration of the Expression in GAMS
564
-
565
- Returns
566
- -------
567
- str
568
-
569
- Examples
570
- --------
571
- >>> import gamspy as gp
572
- >>> m = gp.Container()
573
- >>> a = gp.Parameter(m, name="a")
574
- >>> b = gp.Parameter(m, name="b")
575
- >>> expression = a * b
576
- >>> expression.getDeclaration()
577
- 'a * b'
578
-
579
- """
580
- return self.gamsRepr()
581
-
582
- def _fix_equalities(self) -> None:
583
- # Equality operations on Parameter and Variable objects generate
584
- # GAMS equality signs: =g=, =e=, =l=. If these signs appear on
585
- # assignments, replace them with regular equality ops.
586
- # Uses a stack based post-order traversal algorithm.
587
- EQ_MAP: dict[str, str] = {"=g=": ">=", "=e=": "eq", "=l=": "<="}
588
- stack = []
589
- root = self
590
-
591
- while True:
592
- while root is not None:
593
- if hasattr(root, "right"):
594
- stack.append(root.right)
595
-
596
- stack.append(root)
597
- root = root.left if hasattr(root, "left") else None # type: ignore
598
-
599
- if len(stack) == 0:
600
- break
601
-
602
- root = stack.pop()
603
-
604
- if isinstance(root, Expression) and root.operator in EQ_MAP:
605
- root._replace_operator(EQ_MAP[root.operator])
606
-
607
- last_item = peek(stack)
608
- if (
609
- hasattr(root, "right")
610
- and last_item is not None
611
- and last_item is root.right
612
- ):
613
- stack.pop()
614
- stack.append(root)
615
- root = root.right
616
- else:
617
- root = None
618
-
619
- def _find_all_symbols(self) -> list[str]:
620
- # Finds all symbols in an expression with a stack based inorder
621
- # traversal algorithm (O(N)).
622
- symbols: list[str] = []
623
- stack = []
624
-
625
- node = self
626
- while True:
627
- if node is not None:
628
- stack.append(node)
629
- node = getattr(node, "left", None) # type: ignore
630
- elif stack:
631
- node = stack.pop()
632
-
633
- if isinstance(node, Symbol):
634
- if node.name not in symbols:
635
- if type(node) is gp_syms.Alias:
636
- symbols.append(node.alias_with.name)
637
-
638
- symbols.append(node.name)
639
- stack.extend(node.domain)
640
- node = None
641
- elif isinstance(node, ImplicitSymbol):
642
- if node.parent.name not in symbols:
643
- symbols.append(node.parent.name)
644
- stack.extend(node.domain)
645
- stack.extend(node.container[node.parent.name].domain)
646
- node = None
647
- elif isinstance(node, operation.Operation):
648
- stack.extend(node.op_domain)
649
- node = node.rhs
650
- elif isinstance(node, condition.Condition):
651
- stack.append(node.conditioning_on)
652
-
653
- if isinstance(node.condition, Expression):
654
- node = node.condition
655
- else:
656
- stack.append(node.condition)
657
- node = None
658
- elif isinstance(node, (operation.Ord, operation.Card)):
659
- stack.append(node._symbol)
660
- node = None
661
- elif isinstance(node, MathOp):
662
- if isinstance(node.elements[0], Expression):
663
- node = node.elements[0]
664
- else:
665
- stack.extend(node.elements)
666
- node = None
667
- elif isinstance(node, ExtrinsicFunction):
668
- stack.extend(list(node.args))
669
- node = None
670
- else:
671
- node = getattr(node, "right", None)
672
- else:
673
- break # pragma: no cover
674
-
675
- return symbols
676
-
677
- def _find_symbols_in_conditions(self) -> list[str]:
678
- symbols: list[str] = []
679
- stack = []
680
-
681
- node = self
682
- while True:
683
- if node is not None:
684
- stack.append(node)
685
- node = getattr(node, "left", None) # type: ignore
686
- elif stack:
687
- node = stack.pop()
688
-
689
- if isinstance(node, condition.Condition):
690
- given_condition = node.condition
691
-
692
- if isinstance(given_condition, Expression):
693
- symbols.extend(given_condition._find_all_symbols())
694
- elif isinstance(given_condition, ImplicitSymbol):
695
- symbols.append(given_condition.parent.name)
696
-
697
- if isinstance(node, operation.Operation):
698
- stack.extend(node.op_domain)
699
- node = node.rhs
700
- else:
701
- node = getattr(node, "right", None)
702
- else:
703
- break # pragma: no cover
704
-
705
- return symbols
706
-
707
- def _validate_definition(
708
- self, control_stack: list[Set | Alias | ImplicitSet]
709
- ) -> None:
710
- if not get_option("DOMAIN_VALIDATION") or not get_option("DOMAIN_VALIDATION"):
711
- return
712
-
713
- stack = []
714
-
715
- node = self.right
716
- while True:
717
- if node is not None:
718
- stack.append(node)
719
- node = getattr(node, "left", None) # type: ignore
720
- elif stack:
721
- node = stack.pop()
722
-
723
- if isinstance(node, operation.Operation):
724
- node._validate_operation(control_stack.copy())
725
- elif isinstance(node, ImplicitSymbol):
726
- for elem in node.domain:
727
- if hasattr(elem, "is_singleton") and elem.is_singleton:
728
- continue
729
-
730
- if isinstance(elem, Symbol) and elem not in control_stack:
731
- raise ValidationError(
732
- f"Uncontrolled set `{elem}` entered as constant!"
733
- )
734
- elif (
735
- isinstance(elem, ImplicitSymbol)
736
- and elem.parent not in control_stack
737
- ):
738
- raise ValidationError(
739
- f"Uncontrolled set `{elem.parent}` entered as constant!"
740
- )
741
-
742
- node = getattr(node, "right", None)
743
- else:
744
- break # pragma: no cover
745
-
746
-
747
- class SetExpression(Expression):
748
- def __init__(
749
- self,
750
- left: OperableType,
751
- data: Literal["+", "-", "*", "not"],
752
- right: OperableType,
753
- ):
754
- super().__init__(left, data, right)
755
- self._adjust_left_right()
756
-
757
- def _adjust_left_right(self) -> None:
758
- if isinstance(self.left, (ImplicitSet, SetExpression)):
759
- if isinstance(self.right, (int, float)):
760
- if self.right == 0:
761
- self.right = "no"
762
- elif self.right == 1:
763
- self.right = "yes"
764
- else:
765
- raise ValidationError(
766
- f"Incompatible operand `{self.right}` for the set operation `{self.operator}`."
767
- )
768
- elif isinstance(self.right, condition.Condition) and isinstance(
769
- self.right.conditioning_on, number.Number
770
- ):
771
- if self.right.conditioning_on._value == 0:
772
- self.right.conditioning_on._value = "no"
773
- elif self.right.conditioning_on._value == 1:
774
- self.right.conditioning_on._value = "yes"
775
- raise ValidationError(
776
- f"Incompatible operand `{self.right}` for the set operation `{self.operator}`."
777
- )
778
-
779
- if isinstance(self.right, (ImplicitSet, SetExpression)):
780
- if isinstance(self.left, (int, float)):
781
- if self.left == 0:
782
- self.left = "no"
783
- elif self.left == 1:
784
- self.left = "yes"
785
- else:
786
- raise ValidationError(
787
- f"Incompatible operand `{self.left}` for the set operation `{self.operator}`."
788
- )
789
- elif isinstance(self.left, condition.Condition) and isinstance(
790
- self.left.conditioning_on, number.Number
791
- ):
792
- if self.left.conditioning_on._value == 0:
793
- self.left.conditioning_on._value = "no"
794
- elif self.left.conditioning_on._value == 1:
795
- self.left.conditioning_on._value = "yes"
796
- else:
797
- raise ValidationError(
798
- f"Incompatible operand `{self.left}` for the set operation `{self.operator}`."
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
+ )