gamspy 1.15.0__tar.gz → 1.16.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. {gamspy-1.15.0 → gamspy-1.16.0}/PKG-INFO +6 -6
  2. {gamspy-1.15.0 → gamspy-1.16.0}/pyproject.toml +6 -6
  3. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_algebra/condition.py +37 -7
  4. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_algebra/domain.py +1 -0
  5. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_algebra/expression.py +3 -0
  6. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_algebra/operation.py +7 -1
  7. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_backend/engine.py +1 -6
  8. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_cli/install.py +3 -8
  9. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_cli/retrieve.py +13 -6
  10. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_config.py +5 -0
  11. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_container.py +1 -1
  12. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_convert.py +1 -1
  13. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_miro.py +5 -1
  14. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_symbols/alias.py +2 -2
  15. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_symbols/equation.py +65 -22
  16. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_symbols/implicits/implicit_equation.py +19 -5
  17. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_symbols/implicits/implicit_parameter.py +45 -53
  18. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_symbols/implicits/implicit_set.py +16 -5
  19. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_symbols/implicits/implicit_variable.py +18 -5
  20. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_symbols/parameter.py +9 -9
  21. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_symbols/set.py +8 -8
  22. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_symbols/symbol.py +1 -1
  23. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_symbols/variable.py +8 -8
  24. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_validation.py +44 -8
  25. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/formulations/__init__.py +8 -1
  26. gamspy-1.16.0/src/gamspy/formulations/ml/__init__.py +11 -0
  27. gamspy-1.16.0/src/gamspy/formulations/ml/gradient_boosting.py +211 -0
  28. gamspy-1.16.0/src/gamspy/formulations/ml/random_forest.py +193 -0
  29. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/formulations/ml/regression_tree.py +245 -135
  30. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/formulations/nn/avgpool2d.py +1 -1
  31. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/formulations/nn/conv1d.py +17 -4
  32. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/formulations/nn/conv2d.py +18 -4
  33. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/formulations/nn/linear.py +17 -4
  34. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/formulations/nn/mpool2d.py +1 -1
  35. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/formulations/piecewise.py +4 -6
  36. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/formulations/shape.py +1 -1
  37. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/math/activation.py +14 -0
  38. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/math/matrix.py +3 -1
  39. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/utils.py +16 -0
  40. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy.egg-info/PKG-INFO +6 -6
  41. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy.egg-info/SOURCES.txt +3 -1
  42. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy.egg-info/requires.txt +4 -4
  43. {gamspy-1.15.0 → gamspy-1.16.0}/tests/test_gamspy.py +1 -1
  44. gamspy-1.15.0/src/gamspy/formulations/ml/__init__.py +0 -4
  45. {gamspy-1.15.0 → gamspy-1.16.0}/LICENSE +0 -0
  46. {gamspy-1.15.0 → gamspy-1.16.0}/README.md +0 -0
  47. {gamspy-1.15.0 → gamspy-1.16.0}/README_PYPI.md +0 -0
  48. {gamspy-1.15.0 → gamspy-1.16.0}/setup.cfg +0 -0
  49. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/__init__.py +0 -0
  50. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/__main__.py +0 -0
  51. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_algebra/__init__.py +0 -0
  52. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_algebra/number.py +0 -0
  53. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_algebra/operable.py +0 -0
  54. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_backend/__init__.py +0 -0
  55. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_backend/backend.py +0 -0
  56. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_backend/local.py +0 -0
  57. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_backend/neos.py +0 -0
  58. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_cli/__init__.py +0 -0
  59. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_cli/cli.py +0 -0
  60. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_cli/gdx.py +0 -0
  61. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_cli/list.py +0 -0
  62. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_cli/probe.py +0 -0
  63. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_cli/run.py +0 -0
  64. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_cli/show.py +0 -0
  65. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_cli/uninstall.py +0 -0
  66. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_cli/util.py +0 -0
  67. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_database.py +0 -0
  68. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_extrinsic.py +0 -0
  69. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_model.py +0 -0
  70. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_model_instance.py +0 -0
  71. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_options.py +0 -0
  72. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_serialization.py +0 -0
  73. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_symbols/__init__.py +0 -0
  74. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_symbols/implicits/__init__.py +0 -0
  75. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_symbols/implicits/implicit_symbol.py +0 -0
  76. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_symbols/universe_alias.py +0 -0
  77. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_types.py +0 -0
  78. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/_workspace.py +0 -0
  79. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/exceptions.py +0 -0
  80. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/formulations/ml/decision_tree_struct.py +0 -0
  81. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/formulations/nn/__init__.py +0 -0
  82. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/formulations/nn/maxpool2d.py +0 -0
  83. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/formulations/nn/minpool2d.py +0 -0
  84. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/formulations/nn/torch_sequential.py +0 -0
  85. {gamspy-1.15.0/src/gamspy/formulations/nn → gamspy-1.16.0/src/gamspy/formulations}/utils.py +0 -0
  86. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/math/__init__.py +0 -0
  87. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/math/log_power.py +0 -0
  88. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/math/misc.py +0 -0
  89. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/math/probability.py +0 -0
  90. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/math/trigonometric.py +0 -0
  91. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/py.typed +0 -0
  92. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy/version.py +0 -0
  93. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy.egg-info/dependency_links.txt +0 -0
  94. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy.egg-info/entry_points.txt +0 -0
  95. {gamspy-1.15.0 → gamspy-1.16.0}/src/gamspy.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gamspy
3
- Version: 1.15.0
3
+ Version: 1.16.0
4
4
  Summary: Python-based algebraic modeling interface to GAMS
5
5
  Author-email: GAMS Development Corporation <support@gams.com>
6
6
  Project-URL: homepage, https://gams.com/sales/gamspy_facts/
@@ -19,11 +19,11 @@ Classifier: License :: OSI Approved :: MIT License
19
19
  Classifier: Programming Language :: Python
20
20
  Classifier: Programming Language :: Python :: 3
21
21
  Classifier: Programming Language :: Python :: 3 :: Only
22
- Classifier: Programming Language :: Python :: 3.9
23
22
  Classifier: Programming Language :: Python :: 3.10
24
23
  Classifier: Programming Language :: Python :: 3.11
25
24
  Classifier: Programming Language :: Python :: 3.12
26
25
  Classifier: Programming Language :: Python :: 3.13
26
+ Classifier: Programming Language :: Python :: 3.14
27
27
  Classifier: Operating System :: POSIX
28
28
  Classifier: Operating System :: Unix
29
29
  Classifier: Operating System :: MacOS
@@ -31,13 +31,12 @@ Classifier: Operating System :: Microsoft :: Windows
31
31
  Requires-Python: >=3.9
32
32
  Description-Content-Type: text/markdown
33
33
  License-File: LICENSE
34
- Requires-Dist: gamsapi[transfer]==50.4.0
35
- Requires-Dist: gamspy_base==50.4.0
34
+ Requires-Dist: gamsapi[transfer]>=51.1.0
35
+ Requires-Dist: gamspy_base>=51.1.0
36
36
  Requires-Dist: pydantic>=2.0
37
37
  Requires-Dist: certifi>=2022.09.14
38
38
  Requires-Dist: urllib3>=2.0.7
39
- Requires-Dist: typer>=0.15.1
40
- Requires-Dist: click<8.2.0
39
+ Requires-Dist: typer>=0.16.0
41
40
  Provides-Extra: dev
42
41
  Requires-Dist: ruff==0.12.0; extra == "dev"
43
42
  Requires-Dist: pre-commit>=3.5.0; extra == "dev"
@@ -67,6 +66,7 @@ Requires-Dist: nbmake>=1.5.3; extra == "doc"
67
66
  Requires-Dist: openpyxl>=3.1.2; extra == "doc"
68
67
  Requires-Dist: sphinx-tabs>=3.4.7; extra == "doc"
69
68
  Requires-Dist: towncrier>=24.8.0; extra == "doc"
69
+ Requires-Dist: geopandas>=1.1.1; extra == "doc"
70
70
  Provides-Extra: torch
71
71
  Requires-Dist: torch>=2.7.0; extra == "torch"
72
72
  Dynamic: license-file
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gamspy"
7
- version = "1.15.0"
7
+ version = "1.16.0"
8
8
  authors = [
9
9
  { name = "GAMS Development Corporation", email = "support@gams.com" },
10
10
  ]
@@ -25,24 +25,23 @@ classifiers = [
25
25
  "Programming Language :: Python",
26
26
  "Programming Language :: Python :: 3",
27
27
  "Programming Language :: Python :: 3 :: Only",
28
- "Programming Language :: Python :: 3.9",
29
28
  "Programming Language :: Python :: 3.10",
30
29
  "Programming Language :: Python :: 3.11",
31
30
  "Programming Language :: Python :: 3.12",
32
31
  "Programming Language :: Python :: 3.13",
32
+ "Programming Language :: Python :: 3.14",
33
33
  "Operating System :: POSIX",
34
34
  "Operating System :: Unix",
35
35
  "Operating System :: MacOS",
36
36
  "Operating System :: Microsoft :: Windows",
37
37
  ]
38
38
  dependencies = [
39
- "gamsapi[transfer] == 50.4.0",
40
- "gamspy_base == 50.4.0",
39
+ "gamsapi[transfer] >= 51.1.0",
40
+ "gamspy_base >= 51.1.0",
41
41
  "pydantic >= 2.0",
42
42
  "certifi >= 2022.09.14",
43
43
  "urllib3 >= 2.0.7",
44
- "typer >= 0.15.1",
45
- "click < 8.2.0",
44
+ "typer >= 0.16.0",
46
45
  ]
47
46
 
48
47
  [project.urls]
@@ -86,6 +85,7 @@ doc = [
86
85
  "openpyxl>=3.1.2",
87
86
  "sphinx-tabs>=3.4.7",
88
87
  "towncrier>= 24.8.0",
88
+ "geopandas >= 1.1.1",
89
89
  ]
90
90
 
91
91
  torch = [
@@ -2,12 +2,14 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
+ import gamspy._algebra.domain as domain
5
6
  import gamspy._algebra.expression as expression
6
7
  import gamspy._algebra.operable as operable
7
8
  import gamspy._symbols as syms
8
9
  import gamspy._symbols.implicits as implicits
9
10
  import gamspy.utils as utils
10
11
  from gamspy._symbols.implicits.implicit_symbol import ImplicitSymbol
12
+ from gamspy._symbols.symbol import Symbol
11
13
 
12
14
  if TYPE_CHECKING:
13
15
  import pandas as pd
@@ -103,6 +105,9 @@ class Condition(operable.Operable):
103
105
  if isinstance(self.conditioning_on, ImplicitSymbol):
104
106
  self.conditioning_on.parent._assignment = statement
105
107
  self.conditioning_on.parent._winner = "gams"
108
+ elif isinstance(self.conditioning_on, Symbol):
109
+ self.conditioning_on._assignment = statement
110
+ self.conditioning_on._winner = "gams"
106
111
 
107
112
  if isinstance(self.conditioning_on, implicits.ImplicitEquation):
108
113
  self.conditioning_on.parent._definition = statement
@@ -123,13 +128,38 @@ class Condition(operable.Operable):
123
128
  def records(self) -> pd.DataFrame | None:
124
129
  assert self.container is not None
125
130
  assert self.domain is not None
126
- temp_name = "a" + utils._get_unique_name()
127
- temp_param = syms.Parameter._constructor_bypass(
128
- self.container, temp_name, self.domain
129
- )
130
- temp_param[...] = self
131
- del self.container.data[temp_name]
132
- return temp_param.records
131
+ if isinstance(
132
+ self.conditioning_on,
133
+ (syms.Set, syms.Alias, implicits.ImplicitSet),
134
+ ):
135
+ temp_name = "c" + utils._get_unique_name()
136
+ temp_sym = syms.Set._constructor_bypass(
137
+ self.container,
138
+ temp_name,
139
+ self.domain, # type: ignore
140
+ )
141
+ temp_sym[...] = self
142
+ del self.container.data[temp_name]
143
+ elif isinstance(self.conditioning_on, domain.Domain):
144
+ temp_name = "c" + utils._get_unique_name()
145
+ temp_sym = syms.Set._constructor_bypass(
146
+ self.container,
147
+ temp_name,
148
+ self.domain, # type: ignore
149
+ )
150
+ temp_sym[...].where[self.condition] = True
151
+ del self.container.data[temp_name]
152
+ else:
153
+ temp_name = "c" + utils._get_unique_name()
154
+ temp_sym = syms.Parameter._constructor_bypass(
155
+ self.container,
156
+ temp_name,
157
+ self.domain, # type: ignore
158
+ )
159
+ temp_sym[...] = self
160
+ del self.container.data[temp_name]
161
+
162
+ return temp_sym.records
133
163
 
134
164
  def gamsRepr(self) -> str:
135
165
  condition_str = (
@@ -39,6 +39,7 @@ class Domain:
39
39
  self._sanity_check(sets)
40
40
  self.sets = sets
41
41
  self.container = self._find_container() # type: ignore
42
+ self.domain = sets
42
43
  self.where = condition.Condition(self)
43
44
 
44
45
  def __repr__(self) -> str:
@@ -650,6 +650,9 @@ class Expression(operable.Operable):
650
650
 
651
651
  if isinstance(node, Symbol):
652
652
  if node.name not in symbols:
653
+ if type(node) is gp_syms.Alias:
654
+ symbols.append(node.alias_with.name)
655
+
653
656
  symbols.append(node.name)
654
657
  stack += node.domain
655
658
  node = None
@@ -744,12 +744,18 @@ class Ord(operable.Operable):
744
744
 
745
745
  """
746
746
 
747
- def __init__(self, symbol: Set | Alias):
747
+ def __new__(cls, symbol: Set | Alias):
748
748
  if not isinstance(symbol, (syms.Set, syms.Alias)):
749
749
  raise ValidationError(
750
750
  "Ord operation is only for Set and Alias objects!"
751
751
  )
752
752
 
753
+ if symbol.is_singleton:
754
+ return 1
755
+
756
+ return super().__new__(cls)
757
+
758
+ def __init__(self, symbol: Set | Alias):
753
759
  self._symbol = symbol
754
760
  self.container = symbol.container
755
761
  self.domain: list[Set | Alias] = []
@@ -13,9 +13,6 @@ import urllib.parse
13
13
  import zipfile
14
14
  from typing import TYPE_CHECKING
15
15
 
16
- import certifi
17
- import urllib3
18
-
19
16
  import gamspy._backend.backend as backend
20
17
  import gamspy.utils as utils
21
18
  from gamspy._options import Options
@@ -747,9 +744,7 @@ class EngineClient:
747
744
  self.is_blocking = is_blocking
748
745
  self.tokens: list[str] = []
749
746
 
750
- self._http = urllib3.PoolManager(
751
- cert_reqs="CERT_REQUIRED", ca_certs=certifi.where()
752
- )
747
+ self._http = utils._make_http()
753
748
 
754
749
  self._engine_config = EngineConfiguration(
755
750
  self.host,
@@ -5,7 +5,6 @@ import shutil
5
5
  import sys
6
6
  from typing import Annotated, Iterable, Optional, Union
7
7
 
8
- import certifi
9
8
  import typer
10
9
  from gamspy.exceptions import GamspyException, ValidationError
11
10
  import gamspy.utils as utils
@@ -35,8 +34,6 @@ def license(
35
34
  import json
36
35
  from urllib.parse import urlencode
37
36
 
38
- import urllib3
39
-
40
37
  os.makedirs(utils.DEFAULT_DIR, exist_ok=True)
41
38
 
42
39
  is_alp = not os.path.isfile(license)
@@ -56,10 +53,8 @@ def license(
56
53
 
57
54
  # Make cmex_type check only for GAMS license server.
58
55
  if server == "https://license.gams.com":
59
- http = urllib3.PoolManager(
60
- cert_reqs="CERT_REQUIRED",
61
- ca_certs=certifi.where()
62
- )
56
+ http = utils._make_http()
57
+
63
58
  encoded_args = urlencode({"access_token": alp_id})
64
59
  request = http.request(
65
60
  "GET", f"{server}/license-type?" + encoded_args
@@ -261,7 +256,7 @@ def solver(
261
256
  _ = subprocess.run(command, check=True, encoding="utf-8", stderr=subprocess.PIPE)
262
257
  except subprocess.CalledProcessError as e:
263
258
  raise GamspyException(
264
- f"Could not install gamspy-{solver_name}. Please check your internet connection. If it's not related to your internet connection, PyPI servers might be down. Please retry it later. Here is the error message of pip:\n\n{e.stderr.decode('utf-8')}"
259
+ f"Could not install gamspy-{solver_name}. Please check your internet connection. If it's not related to your internet connection, PyPI servers might be down. Please retry it later. Here is the error message of pip:\n\n{e.stderr}"
265
260
  ) from e
266
261
  else:
267
262
  try:
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  import subprocess
5
+ from typing import Optional
5
6
 
6
7
  import typer
7
8
 
@@ -37,6 +38,7 @@ def license(
37
38
  "-o",
38
39
  help="Output path for the license file."
39
40
  ),
41
+ checkout_duration: Optional[int] = typer.Option(None, "--checkout-duration", "-c", help="Specifies a duration in hours to checkout a session."),
40
42
  ) -> None:
41
43
  if input is None or not os.path.isfile(input):
42
44
  raise ValidationError(
@@ -49,13 +51,18 @@ def license(
49
51
  )
50
52
 
51
53
  gamspy_base_dir = utils._get_gamspy_base_directory()
54
+ command = [
55
+ os.path.join(gamspy_base_dir, "gamsgetkey"),
56
+ access_code,
57
+ "-i",
58
+ input,
59
+ ]
60
+ if checkout_duration:
61
+ command.append("-c")
62
+ command.append(str(checkout_duration))
63
+
52
64
  process = subprocess.run(
53
- [
54
- os.path.join(gamspy_base_dir, "gamsgetkey"),
55
- access_code,
56
- "-i",
57
- input,
58
- ],
65
+ command,
59
66
  text=True,
60
67
  capture_output=True,
61
68
  )
@@ -55,6 +55,10 @@ def _set_default_options() -> None:
55
55
  strategy = int(os.getenv("GAMSPY_DROP_DOMAIN_VIOLATIONS", 0))
56
56
  configuration["DROP_DOMAIN_VIOLATIONS"] = strategy
57
57
 
58
+ # Do not allow ambiguity in MCP, EMP, MPEC models by default
59
+ ambiguity = os.getenv("GAMSPY_ALLOW_AMBIGUOUS_EQUATIONS", "auto")
60
+ configuration["ALLOW_AMBIGUOUS_EQUATIONS"] = ambiguity
61
+
58
62
 
59
63
  def set_options(
60
64
  options: dict[
@@ -67,6 +71,7 @@ def set_options(
67
71
  "ASSUME_VARIABLE_SUFFIX",
68
72
  "USE_PY_VAR_NAME",
69
73
  "DROP_DOMAIN_VIOLATIONS",
74
+ "ALLOW_AMBIGUOUS_EQUATIONS",
70
75
  ],
71
76
  Any,
72
77
  ],
@@ -580,7 +580,7 @@ class Container(gt.Container):
580
580
  modified_names = []
581
581
 
582
582
  for name, symbol in self:
583
- if symbol.modified:
583
+ if symbol._modified:
584
584
  if (
585
585
  type(symbol) is gp.Alias
586
586
  and symbol.alias_with.name not in modified_names # type: ignore
@@ -490,7 +490,7 @@ class GamsConverter:
490
490
  file.write(gams_string)
491
491
 
492
492
  logger.info(
493
- f"GAMS (.gms) file has been generated under {os.path.join(self.path, self.model.name + '.gms')}"
493
+ f"GAMS (.gms) file has been generated under {self.gms_path}"
494
494
  )
495
495
 
496
496
 
@@ -45,7 +45,11 @@ def load_miro_symbol_records(container: Container):
45
45
  for name in names:
46
46
  symbol = container[name]
47
47
  symbol._already_loaded = True
48
- if isinstance(symbol, gp.Parameter) and symbol._is_miro_table:
48
+ if (
49
+ isinstance(symbol, gp.Parameter)
50
+ and symbol._is_miro_table
51
+ and symbol._records is not None
52
+ ):
49
53
  symbol._records.columns = symbol.domain_names + ["value"]
50
54
 
51
55
  # Load records of miro output symbols
@@ -134,7 +134,7 @@ class Alias(gt.Alias, operable.Operable, Symbol, SetMixin):
134
134
  # reset some properties
135
135
  self._requires_state_check = True
136
136
  self.container._requires_state_check = True
137
- self.modified = True
137
+ self._modified = True
138
138
  self.alias_with = alias_with
139
139
  else:
140
140
  if container is None:
@@ -157,7 +157,7 @@ class Alias(gt.Alias, operable.Operable, Symbol, SetMixin):
157
157
  self.where = condition.Condition(self)
158
158
  self.container._add_statement(self)
159
159
 
160
- self.modified = False
160
+ self._modified = False
161
161
  self.container._synch_with_gams()
162
162
 
163
163
  def _serialize(self) -> dict:
@@ -18,6 +18,7 @@ from gams.transfer._internals import (
18
18
  import gamspy as gp
19
19
  import gamspy._algebra.condition as condition
20
20
  import gamspy._algebra.expression as expression
21
+ import gamspy._algebra.operable as operable
21
22
  import gamspy._symbols.implicits as implicits
22
23
  import gamspy._validation as validation
23
24
  import gamspy.utils as utils
@@ -33,9 +34,9 @@ if TYPE_CHECKING:
33
34
  from gamspy._types import EllipsisType
34
35
 
35
36
 
36
- eq_types = ["=e=", "=l=", "=g="]
37
+ EQ_TYPES = ["=e=", "=l=", "=g=", "=n=", "=x=", "=b="]
37
38
 
38
- non_regular_map = {
39
+ IRREGULAR_EQ_MAP = {
39
40
  "nonbinding": "=n=",
40
41
  "external": "=x=",
41
42
  "boolean": "=b=",
@@ -43,10 +44,10 @@ non_regular_map = {
43
44
 
44
45
 
45
46
  class EquationType(Enum):
46
- REGULAR = "REGULAR"
47
- NONBINDING = "NONBINDING"
48
- EXTERNAL = "EXTERNAL"
49
- BOOLEAN = "BOOLEAN"
47
+ REGULAR = "regular"
48
+ NONBINDING = "nonbinding"
49
+ EXTERNAL = "external"
50
+ BOOLEAN = "boolean"
50
51
 
51
52
  @classmethod
52
53
  def values(cls):
@@ -273,7 +274,7 @@ class Equation(gt.Equation, Symbol):
273
274
  " domains are equal"
274
275
  )
275
276
 
276
- if self.domain_forwarding != domain_forwarding:
277
+ if self._domain_forwarding != domain_forwarding:
277
278
  raise ValueError(
278
279
  "Cannot overwrite symbol in container unless"
279
280
  " 'domain_forwarding' is left unchanged"
@@ -287,8 +288,8 @@ class Equation(gt.Equation, Symbol):
287
288
 
288
289
  previous_state = self.container._options.miro_protect
289
290
  self.container._options.miro_protect = False
290
- self.records = None
291
- self.modified = True
291
+ self._records = None
292
+ self._modified = True
292
293
  self._init_definition(definition)
293
294
 
294
295
  # only set records if records are provided
@@ -360,14 +361,14 @@ class Equation(gt.Equation, Symbol):
360
361
  self.setRecords(records, uels_on_axes=uels_on_axes)
361
362
  else:
362
363
  if not self._is_miro_output:
363
- self.modified = False
364
+ self._modified = False
364
365
  self.container._synch_with_gams()
365
366
 
366
367
  container._options.miro_protect = previous_state
367
368
 
368
369
  def _serialize(self) -> dict:
369
370
  info = {
370
- "_domain_forwarding": self.domain_forwarding,
371
+ "_domain_forwarding": self._domain_forwarding,
371
372
  "_is_miro_output": self._is_miro_output,
372
373
  "_metadata": self._metadata,
373
374
  "_synchronize": self._synchronize,
@@ -526,18 +527,25 @@ class Equation(gt.Equation, Symbol):
526
527
 
527
528
  def _set_definition(self, domain, rhs):
528
529
  # self[domain] = rhs
530
+ rhs_repr = rhs.gamsRepr()
531
+ if self.type == "nonbinding" and not any(
532
+ eq_type in rhs_repr for eq_type in EQ_TYPES
533
+ ):
534
+ # x - c -> x - c == 0
535
+ rhs = rhs == 0
529
536
 
530
- if not any(eq_type in rhs.gamsRepr() for eq_type in eq_types):
537
+ rhs_repr = rhs.gamsRepr()
538
+ if not any(eq_type in rhs_repr for eq_type in EQ_TYPES):
531
539
  raise ValidationError(
532
540
  "Equation definition must contain at least one equality sign such as ==, <= or >=."
533
541
  )
534
542
 
543
+ if self.type in IRREGULAR_EQ_MAP and "=e=" in rhs_repr:
544
+ rhs.operator = IRREGULAR_EQ_MAP[self.type]
545
+
535
546
  if self.type == "external" and "=e=" not in rhs.gamsRepr():
536
547
  raise ValidationError("External equations must contain ==")
537
548
 
538
- if self.type in non_regular_map:
539
- rhs._replace_operator(non_regular_map[self.type])
540
-
541
549
  statement = expression.Expression(
542
550
  implicits.ImplicitEquation(
543
551
  self,
@@ -554,6 +562,37 @@ class Equation(gt.Equation, Symbol):
554
562
  self.container._add_statement(statement)
555
563
  self._definition = statement
556
564
 
565
+ def _check_ambiguity(self) -> None:
566
+ """Ambiguity check for MCP, EMP, MPEC models. See #610"""
567
+ # Looks for =e=, =l= and =g= in an equation definition
568
+ # with a stack based inorder traversal algorithm (O(N)).
569
+ stack = []
570
+
571
+ assert self._definition is not None
572
+ node = self._definition.right
573
+ while True:
574
+ if node is not None:
575
+ stack.append(node)
576
+ node = getattr(node, "left", None) # type: ignore
577
+ elif stack:
578
+ node = stack.pop()
579
+ if (
580
+ isinstance(node, expression.Expression)
581
+ and node.operator in {"=e=", "=l=", "=g=", "=x=", "=n="}
582
+ and not isinstance(node.right, operable.Operable)
583
+ ):
584
+ raise ValidationError(
585
+ f"Definition of `{self.name}` is ambigiuous. Please "
586
+ "use gp.Number for numeric values or disable ambiguity "
587
+ "check via gp.set_options({'ALLOW_AMBIGUOUS_EQUATIONS': 'no'}). "
588
+ "Using numeric values in equations without gp.Number can result in "
589
+ f"different order than expected. Print `{self.name}.getDefinition()` to "
590
+ "make sure that the equation definition is as expected."
591
+ )
592
+ node = getattr(node, "right", None)
593
+ else:
594
+ break # pragma: no cover
595
+
557
596
  @property
558
597
  def l(self): # noqa: E741, E743
559
598
  """
@@ -1000,12 +1039,12 @@ class Equation(gt.Equation, Symbol):
1000
1039
  self._records = records
1001
1040
 
1002
1041
  self._requires_state_check = True
1003
- self.modified = True
1042
+ self._modified = True
1004
1043
 
1005
1044
  self.container._requires_state_check = True
1006
1045
  self.container.modified = True
1007
1046
 
1008
- if self._records is not None and self.domain_forwarding:
1047
+ if self._records is not None and self._domain_forwarding:
1009
1048
  self._domainForwarding()
1010
1049
 
1011
1050
  # reset state check flags for all symbols in the container
@@ -1090,10 +1129,13 @@ class Equation(gt.Equation, Symbol):
1090
1129
  >>> i = gp.Set(m, "i", records=['i1','i2'])
1091
1130
  >>> e = gp.Equation(m, "e", domain=[i])
1092
1131
  >>> e.gamsRepr()
1093
- 'e'
1132
+ 'e(i)'
1094
1133
 
1095
1134
  """
1096
- return self.name
1135
+ representation = self.name
1136
+ if self.domain:
1137
+ representation += self._get_domain_str(self._domain_forwarding)
1138
+ return representation
1097
1139
 
1098
1140
  def latexRepr(self) -> str:
1099
1141
  if self._definition is None:
@@ -1159,7 +1201,7 @@ class Equation(gt.Equation, Symbol):
1159
1201
  output = f"Equation {self.name}"
1160
1202
 
1161
1203
  if self.domain:
1162
- output += self._get_domain_str(self.domain_forwarding)
1204
+ output += self._get_domain_str(self._domain_forwarding)
1163
1205
 
1164
1206
  if self.description:
1165
1207
  output += ' "' + self.description + '"'
@@ -1223,7 +1265,8 @@ class Equation(gt.Equation, Symbol):
1223
1265
 
1224
1266
  def cast_type(type: str | EquationType) -> str:
1225
1267
  if isinstance(type, str):
1226
- if type.lower() not in (
1268
+ type = type.lower()
1269
+ if type not in (
1227
1270
  "eq",
1228
1271
  "geq",
1229
1272
  "leq",
@@ -1238,7 +1281,7 @@ def cast_type(type: str | EquationType) -> str:
1238
1281
  )
1239
1282
 
1240
1283
  # assign eq by default
1241
- if type.upper() == "REGULAR":
1284
+ if type == "regular":
1242
1285
  type = "eq"
1243
1286
 
1244
1287
  elif isinstance(type, EquationType):
@@ -2,9 +2,11 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
+ import gamspy._symbols as syms
5
6
  import gamspy._symbols.alias as alias
6
7
  import gamspy._symbols.implicits as implicits
7
8
  import gamspy._symbols.set as gams_set
9
+ import gamspy.utils as utils
8
10
  from gamspy._symbols.implicits.implicit_symbol import ImplicitSymbol
9
11
 
10
12
  if TYPE_CHECKING:
@@ -137,14 +139,26 @@ class ImplicitEquation(ImplicitSymbol):
137
139
  return self.parent.slack
138
140
 
139
141
  @property
140
- def records(self) -> pd.DataFrame | float | None:
142
+ def records(self) -> pd.DataFrame | None:
141
143
  if self.parent.records is None:
142
144
  return None
143
145
 
144
- recs = self.parent.records
145
- for idx, literal in self._scalar_domains:
146
- column_name = recs.columns[idx]
147
- recs = recs[recs[column_name] == literal]
146
+ temp_name = "ie" + utils._get_unique_name()
147
+ temp_param = syms.Parameter._constructor_bypass(
148
+ self.container, temp_name, self.parent.domain
149
+ )
150
+ domain = list(self.domain)
151
+ for i, d in self._scalar_domains:
152
+ domain.insert(i, d)
153
+
154
+ temp_param[domain] = self.l
155
+ del self.container.data[temp_name]
156
+
157
+ recs = temp_param.records
158
+ if recs is not None:
159
+ columns = recs.columns.to_list()
160
+ columns[columns.index("value")] = "level"
161
+ recs.columns = columns
148
162
 
149
163
  return recs
150
164