gamspy 1.17.1__tar.gz → 1.18.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 (96) hide show
  1. {gamspy-1.17.1 → gamspy-1.18.0}/PKG-INFO +3 -3
  2. {gamspy-1.17.1 → gamspy-1.18.0}/pyproject.toml +3 -3
  3. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_backend/backend.py +3 -2
  4. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_backend/engine.py +6 -3
  5. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_backend/local.py +6 -2
  6. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_backend/neos.py +27 -5
  7. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_cli/install.py +10 -0
  8. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_cli/retrieve.py +6 -3
  9. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_cli/run.py +22 -13
  10. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_cli/uninstall.py +4 -0
  11. gamspy-1.18.0/src/gamspy/_communication.py +215 -0
  12. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_container.py +48 -211
  13. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_convert.py +2 -0
  14. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_miro.py +8 -0
  15. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_model.py +118 -26
  16. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_model_instance.py +7 -6
  17. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_options.py +2 -8
  18. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_symbols/alias.py +0 -3
  19. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_symbols/equation.py +7 -0
  20. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_symbols/variable.py +17 -0
  21. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_validation.py +0 -5
  22. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/formulations/__init__.py +2 -0
  23. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/formulations/nn/torch_sequential.py +80 -8
  24. gamspy-1.18.0/src/gamspy/formulations/result.py +119 -0
  25. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/math/__init__.py +2 -0
  26. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/math/activation.py +132 -6
  27. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/utils.py +2 -6
  28. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy.egg-info/PKG-INFO +3 -3
  29. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy.egg-info/SOURCES.txt +2 -0
  30. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy.egg-info/requires.txt +2 -2
  31. {gamspy-1.17.1 → gamspy-1.18.0}/tests/test_gamspy.py +1 -1
  32. {gamspy-1.17.1 → gamspy-1.18.0}/LICENSE +0 -0
  33. {gamspy-1.17.1 → gamspy-1.18.0}/README.md +0 -0
  34. {gamspy-1.17.1 → gamspy-1.18.0}/README_PYPI.md +0 -0
  35. {gamspy-1.17.1 → gamspy-1.18.0}/setup.cfg +0 -0
  36. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/__init__.py +0 -0
  37. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/__main__.py +0 -0
  38. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_algebra/__init__.py +0 -0
  39. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_algebra/condition.py +0 -0
  40. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_algebra/domain.py +0 -0
  41. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_algebra/expression.py +0 -0
  42. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_algebra/number.py +0 -0
  43. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_algebra/operable.py +0 -0
  44. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_algebra/operation.py +0 -0
  45. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_backend/__init__.py +0 -0
  46. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_cli/__init__.py +0 -0
  47. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_cli/cli.py +0 -0
  48. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_cli/gdx.py +0 -0
  49. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_cli/list.py +0 -0
  50. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_cli/probe.py +0 -0
  51. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_cli/show.py +0 -0
  52. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_cli/util.py +0 -0
  53. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_config.py +0 -0
  54. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_database.py +0 -0
  55. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_extrinsic.py +0 -0
  56. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_serialization.py +0 -0
  57. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_symbols/__init__.py +0 -0
  58. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_symbols/implicits/__init__.py +0 -0
  59. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_symbols/implicits/implicit_equation.py +0 -0
  60. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_symbols/implicits/implicit_parameter.py +0 -0
  61. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_symbols/implicits/implicit_set.py +0 -0
  62. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_symbols/implicits/implicit_symbol.py +0 -0
  63. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_symbols/implicits/implicit_variable.py +0 -0
  64. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_symbols/parameter.py +0 -0
  65. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_symbols/set.py +0 -0
  66. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_symbols/symbol.py +0 -0
  67. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_symbols/universe_alias.py +0 -0
  68. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_types.py +0 -0
  69. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/_workspace.py +0 -0
  70. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/exceptions.py +0 -0
  71. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/formulations/ml/__init__.py +0 -0
  72. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/formulations/ml/decision_tree_struct.py +0 -0
  73. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/formulations/ml/gradient_boosting.py +0 -0
  74. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/formulations/ml/random_forest.py +0 -0
  75. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/formulations/ml/regression_tree.py +0 -0
  76. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/formulations/nn/__init__.py +0 -0
  77. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/formulations/nn/avgpool2d.py +0 -0
  78. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/formulations/nn/conv1d.py +0 -0
  79. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/formulations/nn/conv2d.py +0 -0
  80. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/formulations/nn/linear.py +0 -0
  81. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/formulations/nn/maxpool2d.py +0 -0
  82. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/formulations/nn/minpool2d.py +0 -0
  83. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/formulations/nn/mpool2d.py +0 -0
  84. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/formulations/piecewise.py +0 -0
  85. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/formulations/shape.py +0 -0
  86. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/formulations/utils.py +0 -0
  87. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/math/log_power.py +0 -0
  88. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/math/matrix.py +0 -0
  89. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/math/misc.py +0 -0
  90. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/math/probability.py +0 -0
  91. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/math/trigonometric.py +0 -0
  92. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/py.typed +0 -0
  93. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy/version.py +0 -0
  94. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy.egg-info/dependency_links.txt +0 -0
  95. {gamspy-1.17.1 → gamspy-1.18.0}/src/gamspy.egg-info/entry_points.txt +0 -0
  96. {gamspy-1.17.1 → gamspy-1.18.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.17.1
3
+ Version: 1.18.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/
@@ -31,8 +31,8 @@ Classifier: Operating System :: Microsoft :: Windows
31
31
  Requires-Python: >=3.10
32
32
  Description-Content-Type: text/markdown
33
33
  License-File: LICENSE
34
- Requires-Dist: gamsapi<52.0.0,>=51.1.0
35
- Requires-Dist: gamspy_base<52.0.0,>=51.1.0
34
+ Requires-Dist: gamsapi<53.0.0,>=52.1.0
35
+ Requires-Dist: gamspy_base<53.0.0,>=52.1.0
36
36
  Requires-Dist: pandas<2.4,>=2.2.2
37
37
  Requires-Dist: pydantic>=2.0
38
38
  Requires-Dist: requests>=2.28.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gamspy"
7
- version = "1.17.1"
7
+ version = "1.18.0"
8
8
  authors = [
9
9
  { name = "GAMS Development Corporation", email = "support@gams.com" },
10
10
  ]
@@ -36,8 +36,8 @@ classifiers = [
36
36
  "Operating System :: Microsoft :: Windows",
37
37
  ]
38
38
  dependencies = [
39
- "gamsapi >= 51.1.0, < 52.0.0",
40
- "gamspy_base >= 51.1.0, < 52.0.0",
39
+ "gamsapi >= 52.1.0, < 53.0.0",
40
+ "gamspy_base >= 52.1.0, < 53.0.0",
41
41
  "pandas >= 2.2.2, < 2.4",
42
42
  "pydantic >= 2.0",
43
43
  "requests >= 2.28.0",
@@ -12,6 +12,7 @@ from gamspy.exceptions import GamspyException, ValidationError
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  import io
15
+ from pathlib import Path
15
16
 
16
17
  from gamspy import Container, Model, Options
17
18
  from gamspy._backend.engine import EngineClient, GAMSEngine
@@ -52,7 +53,7 @@ def backend_factory(
52
53
  container: Container,
53
54
  options: Options | None = None,
54
55
  solver: str | None = None,
55
- solver_options: dict | None = None,
56
+ solver_options: dict | Path | None = None,
56
57
  output: io.TextIOWrapper | None = None,
57
58
  backend: Literal["local", "engine", "neos"] = "local",
58
59
  client: EngineClient | NeosClient | None = None,
@@ -126,7 +127,7 @@ class Backend(ABC):
126
127
  model: Model | None,
127
128
  options: Options,
128
129
  solver: str | None,
129
- solver_options: dict | None,
130
+ solver_options: dict | Path | None,
130
131
  output: io.TextIOWrapper | None,
131
132
  load_symbols: list[Symbol] | None,
132
133
  ):
@@ -17,6 +17,7 @@ import requests
17
17
 
18
18
  import gamspy._backend.backend as backend
19
19
  import gamspy.utils as utils
20
+ from gamspy._communication import send_job
20
21
  from gamspy._options import Options
21
22
  from gamspy.exceptions import (
22
23
  EngineClientException,
@@ -26,6 +27,8 @@ from gamspy.exceptions import (
26
27
  )
27
28
 
28
29
  if TYPE_CHECKING:
30
+ from pathlib import Path
31
+
29
32
  from gamspy import Container, Model
30
33
  from gamspy._symbols.symbol import Symbol
31
34
 
@@ -742,7 +745,7 @@ class GAMSEngine(backend.Backend):
742
745
  client: EngineClient | None,
743
746
  options: Options,
744
747
  solver: str,
745
- solver_options: dict | None,
748
+ solver_options: dict | Path | None,
746
749
  output: io.TextIOWrapper | None,
747
750
  model: Model,
748
751
  load_symbols: list[Symbol] | None,
@@ -916,7 +919,7 @@ class GAMSEngine(backend.Backend):
916
919
  options._extra_options["save"] = self.restart_file
917
920
  options._export(self.pf_file)
918
921
 
919
- self.container._send_job(self.job_name, self.pf_file)
922
+ send_job(self.container._comm_pair_id, self.job_name, self.pf_file)
920
923
 
921
924
  def _sync(self):
922
925
  symbols = utils._get_symbol_names_from_gdx(
@@ -931,7 +934,7 @@ class GAMSEngine(backend.Backend):
931
934
  options._set_extra_options(extra_options)
932
935
  options._export(self.pf_file)
933
936
 
934
- self.container._send_job(self.job_name, self.pf_file)
937
+ send_job(self.container._comm_pair_id, self.job_name, self.pf_file)
935
938
 
936
939
  def _append_gamspy_files(self) -> list[str]:
937
940
  extra_model_files = self.client.extra_model_files + [self.restart_file]
@@ -5,10 +5,12 @@ from typing import TYPE_CHECKING
5
5
 
6
6
  import gamspy._backend.backend as backend
7
7
  import gamspy._miro as miro
8
+ from gamspy._communication import send_job
8
9
  from gamspy.exceptions import GamspyException, _customize_exception
9
10
 
10
11
  if TYPE_CHECKING:
11
12
  import io
13
+ from pathlib import Path
12
14
 
13
15
  from gamspy import Container, Model, Options
14
16
  from gamspy._symbols.symbol import Symbol
@@ -20,7 +22,7 @@ class Local(backend.Backend):
20
22
  container: Container,
21
23
  options: Options,
22
24
  solver: str | None,
23
- solver_options: dict | None,
25
+ solver_options: dict | Path | None,
24
26
  output: io.TextIOWrapper | None,
25
27
  model: Model | None,
26
28
  load_symbols: list[Symbol] | None,
@@ -89,7 +91,9 @@ class Local(backend.Backend):
89
91
  self.options._export(self.pf_file, self.output)
90
92
 
91
93
  try:
92
- self.container._send_job(self.job_name, self.pf_file, self.output)
94
+ send_job(
95
+ self.container._comm_pair_id, self.job_name, self.pf_file, self.output
96
+ )
93
97
 
94
98
  if not self.is_async() and self.model:
95
99
  self.model._update_model_attributes()
@@ -7,12 +7,14 @@ import shutil
7
7
  import ssl
8
8
  import xmlrpc.client
9
9
  import zipfile
10
- from typing import TYPE_CHECKING
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, Any
11
12
 
12
13
  import certifi
13
14
 
14
15
  import gamspy._backend.backend as backend
15
16
  import gamspy.utils as utils
17
+ from gamspy._communication import send_job
16
18
  from gamspy._options import Options
17
19
  from gamspy.exceptions import (
18
20
  GamspyException,
@@ -256,7 +258,7 @@ class NeosClient:
256
258
  def _prepare_xml(
257
259
  self,
258
260
  gams_string: str,
259
- solver_options: dict | None,
261
+ solver_options: dict | Path | None,
260
262
  gdx_path: str,
261
263
  restart_path: str,
262
264
  options: Options,
@@ -282,6 +284,9 @@ class NeosClient:
282
284
 
283
285
  solver_options_str = ""
284
286
  if solver_options:
287
+ if isinstance(solver_options, Path):
288
+ solver_options = _parse_solver_options(solver_options)
289
+
285
290
  solver_options_str = "\n".join(
286
291
  [f"{key} {value}" for key, value in solver_options.items()]
287
292
  )
@@ -380,7 +385,7 @@ class NEOSServer(backend.Backend):
380
385
  container: Container,
381
386
  options: Options,
382
387
  solver: str,
383
- solver_options: dict | None,
388
+ solver_options: dict | Path | None,
384
389
  client: NeosClient | None,
385
390
  output: io.TextIOWrapper | None,
386
391
  model: Model,
@@ -527,7 +532,7 @@ class NEOSServer(backend.Backend):
527
532
  options._extra_options["save"] = self.restart_file
528
533
  options._export(self.pf_file)
529
534
 
530
- self.container._send_job(self.job_name, self.pf_file)
535
+ send_job(self.container._comm_pair_id, self.job_name, self.pf_file)
531
536
 
532
537
  def _sync(self):
533
538
  symbols = utils._get_symbol_names_from_gdx(
@@ -542,4 +547,21 @@ class NEOSServer(backend.Backend):
542
547
  options._set_extra_options(extra_options)
543
548
  options._export(self.pf_file)
544
549
 
545
- self.container._send_job(self.job_name, self.pf_file)
550
+ send_job(self.container._comm_pair_id, self.job_name, self.pf_file)
551
+
552
+
553
+ def _parse_solver_options(path: Path) -> dict[str, Any]:
554
+ """Parse solver options to a dict"""
555
+ solver_options = {}
556
+ with open(path) as file:
557
+ lines = file.readlines()
558
+
559
+ for line in lines:
560
+ if not line or line.startswith("*"):
561
+ continue
562
+
563
+ splitter = "=" if "=" in line else " "
564
+ key, value = line.split(splitter)
565
+ solver_options[key.strip()] = value
566
+
567
+ return solver_options
@@ -7,6 +7,7 @@ import subprocess
7
7
  import sys
8
8
  from typing import TYPE_CHECKING, Annotated
9
9
 
10
+ import certifi
10
11
  import requests
11
12
  import typer
12
13
 
@@ -133,11 +134,16 @@ def license(
133
134
  command.append("-o")
134
135
  command.append(license_path)
135
136
 
137
+ environment_variables = os.environ.copy()
138
+ if "CURL_CA_BUNDLE" not in environment_variables:
139
+ environment_variables["CURL_CA_BUNDLE"] = certifi.where()
140
+
136
141
  process = subprocess.run(
137
142
  command,
138
143
  text=True,
139
144
  capture_output=True,
140
145
  encoding="utf-8",
146
+ env=environment_variables,
141
147
  )
142
148
  if process.returncode:
143
149
  raise ValidationError(process.stderr)
@@ -243,6 +249,9 @@ def solver(
243
249
 
244
250
  addons_path = os.path.join(utils.DEFAULT_DIR, "solvers.txt")
245
251
  os.makedirs(utils.DEFAULT_DIR, exist_ok=True)
252
+ environment_variables = os.environ.copy()
253
+ if "CURL_CA_BUNDLE" not in environment_variables:
254
+ environment_variables["CURL_CA_BUNDLE"] = certifi.where()
246
255
 
247
256
  def install_addons(addons: Iterable[str]):
248
257
  for item in addons:
@@ -291,6 +300,7 @@ def solver(
291
300
  check=True,
292
301
  encoding="utf-8",
293
302
  stderr=subprocess.PIPE,
303
+ env=environment_variables,
294
304
  )
295
305
  except subprocess.CalledProcessError as e:
296
306
  raise GamspyException(
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import os
4
4
  import subprocess
5
5
 
6
+ import certifi
6
7
  import typer
7
8
 
8
9
  import gamspy.utils as utils
@@ -46,6 +47,10 @@ def license(
46
47
  if access_code is None:
47
48
  raise ValidationError(f"Given licence id `{access_code}` is not valid!")
48
49
 
50
+ environment_variables = os.environ.copy()
51
+ if "CURL_CA_BUNDLE" not in environment_variables:
52
+ environment_variables["CURL_CA_BUNDLE"] = certifi.where()
53
+
49
54
  gamspy_base_dir = utils._get_gamspy_base_directory()
50
55
  command = [
51
56
  os.path.join(gamspy_base_dir, "gamsgetkey"),
@@ -58,9 +63,7 @@ def license(
58
63
  command.append(str(checkout_duration))
59
64
 
60
65
  process = subprocess.run(
61
- command,
62
- text=True,
63
- capture_output=True,
66
+ command, text=True, capture_output=True, env=environment_variables
64
67
  )
65
68
 
66
69
  if process.returncode:
@@ -5,11 +5,12 @@ import platform
5
5
  import subprocess
6
6
  import sys
7
7
  from enum import Enum
8
- from typing import Annotated
8
+ from pathlib import Path # noqa: TC003
9
+ from typing import Annotated, Optional
9
10
 
10
11
  import typer
11
12
 
12
- from gamspy.exceptions import GamspyException, ValidationError
13
+ from gamspy.exceptions import ValidationError
13
14
 
14
15
  app = typer.Typer(
15
16
  rich_markup_mode="rich",
@@ -19,7 +20,7 @@ app = typer.Typer(
19
20
  )
20
21
 
21
22
 
22
- class ModeEnum(Enum):
23
+ class ModeEnum(str, Enum):
23
24
  config = "config"
24
25
  base = "base"
25
26
  deploy = "deploy"
@@ -35,10 +36,11 @@ def miro(
35
36
  typer.Option("--mode", "-m", help="Execution mode of MIRO"),
36
37
  ] = ModeEnum.base, # type: ignore
37
38
  path: Annotated[
38
- str | None,
39
+ Optional[Path], # noqa: UP045
39
40
  typer.Option(
40
41
  "--path",
41
42
  "-p",
43
+ exists=True,
42
44
  help="Path to the MIRO executable (.exe, .app, or .AppImage)",
43
45
  ),
44
46
  ] = None,
@@ -47,8 +49,8 @@ def miro(
47
49
  typer.Option("--skip-execution", help="Whether to skip model execution."),
48
50
  ] = False,
49
51
  model: Annotated[
50
- str | None,
51
- typer.Option("--model", "-g", help="Path to the GAMSPy model."),
52
+ Optional[Path], # noqa: UP045,
53
+ typer.Option("--model", "-g", exists=True, help="Path to the GAMSPy model."),
52
54
  ] = None,
53
55
  args: Annotated[
54
56
  list[str] | None,
@@ -56,17 +58,18 @@ def miro(
56
58
  ] = None,
57
59
  ) -> None:
58
60
  if model is None:
59
- raise ValidationError("--model must be provided to run MIRO")
61
+ typer.echo("--model must be provided to run MIRO", file=sys.stderr)
62
+ sys.exit(1)
60
63
 
61
64
  model = os.path.abspath(model)
62
65
  execution_mode = mode.value
63
- path = os.getenv("MIRO_PATH", None)
66
+ path = os.getenv("MIRO_PATH", path)
64
67
 
65
68
  if path is None:
66
69
  path = path if path is not None else discover_miro()
67
70
 
68
71
  if path is None:
69
- raise GamspyException("--path must be provided to run MIRO")
72
+ raise ValidationError("--path must be provided to run MIRO")
70
73
 
71
74
  if platform.system() == "Darwin" and os.path.splitext(path)[1] == ".app":
72
75
  path = os.path.join(path, "Contents", "MacOS", "GAMS MIRO")
@@ -79,10 +82,16 @@ def miro(
79
82
  if args is not None:
80
83
  command += args
81
84
 
82
- try:
83
- subprocess.run(command, env=subprocess_env, check=True)
84
- except subprocess.CalledProcessError:
85
- return
85
+ process = subprocess.run(
86
+ command,
87
+ env=subprocess_env,
88
+ capture_output=True,
89
+ text=True,
90
+ encoding="utf-8",
91
+ )
92
+ if process.returncode != 0:
93
+ typer.echo(process.stderr, file=sys.stderr)
94
+ sys.exit(process.returncode)
86
95
 
87
96
  # Run MIRO
88
97
  subprocess_env = os.environ.copy()
@@ -5,6 +5,7 @@ import subprocess
5
5
  import sys
6
6
  from typing import TYPE_CHECKING, Annotated
7
7
 
8
+ import certifi
8
9
  import typer
9
10
 
10
11
  import gamspy.utils as utils
@@ -70,6 +71,9 @@ def solver(
70
71
  ) from e
71
72
 
72
73
  addons_path = os.path.join(utils.DEFAULT_DIR, "solvers.txt")
74
+ environment_variables = os.environ.copy()
75
+ if "CURL_CA_BUNDLE" not in environment_variables:
76
+ environment_variables["CURL_CA_BUNDLE"] = certifi.where()
73
77
 
74
78
  def remove_addons(addons: Iterable[str]):
75
79
  for item in addons:
@@ -0,0 +1,215 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import platform
5
+ import signal
6
+ import socket
7
+ import subprocess
8
+ import threading
9
+ import time
10
+ from typing import TYPE_CHECKING
11
+
12
+ import certifi
13
+
14
+ from gamspy import utils
15
+ from gamspy.exceptions import FatalError, GamspyException, ValidationError
16
+
17
+ if TYPE_CHECKING:
18
+ import io
19
+
20
+ from gamspy import Container
21
+
22
+
23
+ _comm_pairs: dict[str, tuple[socket.socket, subprocess.Popen]] = {}
24
+
25
+
26
+ def open_connection(container: Container) -> None:
27
+ LOOPBACK = "127.0.0.1"
28
+ TIMEOUT = 30
29
+
30
+ initial_pf_file = os.path.join(container._process_directory, "gamspy.pf")
31
+ with open(initial_pf_file, "w") as file:
32
+ file.write(
33
+ 'incrementalMode="2"\n'
34
+ f'procdir="{container._process_directory}"\n'
35
+ f'license="{container._license_path}"\n'
36
+ f'curdir="{os.getcwd()}"\n'
37
+ )
38
+
39
+ command = [
40
+ os.path.join(container.system_directory, "gams"),
41
+ "GAMSPY_JOB",
42
+ "pf",
43
+ initial_pf_file,
44
+ ]
45
+
46
+ certificate_path = os.path.join(utils.DEFAULT_DIR, "gamspy_cert.crt")
47
+ env = os.environ.copy()
48
+ if os.path.isfile(certificate_path):
49
+ env["GAMSLICECRT"] = certificate_path
50
+
51
+ env = os.environ.copy()
52
+ if "CURL_CA_BUNDLE" not in env:
53
+ env["CURL_CA_BUNDLE"] = certifi.where()
54
+
55
+ process = subprocess.Popen(
56
+ command,
57
+ text=True,
58
+ stdout=subprocess.PIPE,
59
+ stderr=subprocess.STDOUT,
60
+ errors="replace",
61
+ start_new_session=platform.system() != "Windows",
62
+ env=env,
63
+ )
64
+
65
+ port_info = process.stdout.readline().strip()
66
+
67
+ try:
68
+ port = int(port_info.removeprefix("port: "))
69
+ except ValueError as e:
70
+ raise ValidationError(
71
+ f"Error while reading the port! {port_info + process.stdout.read()}"
72
+ ) from e
73
+
74
+ def handler(signum, frame):
75
+ if platform.system() != "Windows":
76
+ os.kill(process.pid, signal.SIGINT)
77
+
78
+ if threading.current_thread() is threading.main_thread():
79
+ signal.signal(signal.SIGINT, handler)
80
+
81
+ start = time.time()
82
+ while True:
83
+ if process.poll() is not None: # pragma: no cover
84
+ raise ValidationError(process.communicate()[0])
85
+
86
+ try:
87
+ new_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
88
+ new_socket.connect((LOOPBACK, port))
89
+ break
90
+ except (ConnectionRefusedError, OSError) as e:
91
+ new_socket.close()
92
+ end = time.time()
93
+
94
+ if end - start > TIMEOUT: # pragma: no cover
95
+ raise FatalError(
96
+ f"Timeout while establishing the connection with socket. {process.communicate()[0]}"
97
+ ) from e
98
+
99
+ _comm_pairs[container._comm_pair_id] = (new_socket, process)
100
+
101
+
102
+ def get_connection(pair_id: str) -> tuple[socket.socket, subprocess.Popen]:
103
+ return _comm_pairs[pair_id]
104
+
105
+
106
+ def close_connection(pair_id: str):
107
+ try:
108
+ _socket, process = get_connection(pair_id)
109
+ except KeyError:
110
+ # This means that the connection is already closed.
111
+ return
112
+
113
+ _socket.sendall(b"stop")
114
+ _socket.close()
115
+
116
+ if not process.stdout.closed:
117
+ process.stdout.close()
118
+
119
+ # Wait until the GAMS process dies.
120
+ while process.poll() is None:
121
+ ...
122
+
123
+ del _comm_pairs[pair_id]
124
+
125
+
126
+ def _read_output(process: subprocess.Popen, output: io.TextIOWrapper | None) -> None:
127
+ if output is not None:
128
+ while True:
129
+ data = process.stdout.readline()
130
+ output.write(data)
131
+ output.flush()
132
+ if data.startswith("--- Job ") and "elapsed" in data:
133
+ break
134
+
135
+
136
+ def check_response(response: bytes, job_name: str) -> None:
137
+ GAMS_STATUS = {
138
+ 1: "Solver is to be called, the system should never return this number.",
139
+ 2: "There was a compilation error.",
140
+ 3: "There was an execution error.",
141
+ 4: "System limits were reached.",
142
+ 5: "There was a file error.",
143
+ 6: "There was a parameter error.",
144
+ 7: "The solve has failed due to a license error. The license you are using may impose model size limits (demo/community license) or you are using a GAMSPy incompatible professional license. Please contact sales@gams.com to find out about license options.",
145
+ 8: "There was a GAMS system error.",
146
+ 9: "GAMS could not be started.",
147
+ 10: "Out of memory.",
148
+ 11: "Out of disk.",
149
+ 109: "Could not create process/scratch directory.",
150
+ 110: "Too many process/scratch directories.",
151
+ 112: "Could not delete the process/scratch directory.",
152
+ 113: "Could not write the script gamsnext.",
153
+ 114: "Could not write the parameter file.",
154
+ 115: "Could not read environment variable.",
155
+ 400: "Could not spawn the GAMS language compiler (gamscmex).",
156
+ 401: "Current directory (curdir) does not exist.",
157
+ 402: "Cannot set current directory (curdir).",
158
+ 404: "Blank in system directory (UNIX only).",
159
+ 405: "Blank in current directory (UNIX only).",
160
+ 406: "Blank in scratch extension (scrext)",
161
+ 407: "Unexpected cmexRC.",
162
+ 408: "Could not find the process directory (procdir).",
163
+ 409: "CMEX library not be found (experimental).",
164
+ 410: "Entry point in CMEX library could not be found (experimental).",
165
+ 411: "Blank in process directory (UNIX only).",
166
+ 412: "Blank in scratch directory (UNIX only).",
167
+ 909: "Cannot add path / unknown UNIX environment / cannot set environment variable.",
168
+ 1000: "Driver error: incorrect command line parameters for gams.",
169
+ 2000: "Driver error: internal error: cannot install interrupt handler.",
170
+ 3000: "Driver error: problems getting current directory.",
171
+ 4000: "Driver error: internal error: GAMS compile and execute module not found.",
172
+ 5000: "Driver error: internal error: cannot load option handling library.",
173
+ }
174
+
175
+ value = response[: response.find(b"#")].decode("ascii")
176
+ if not value:
177
+ raise FatalError(
178
+ "Error while getting the return code from GAMS backend. This means that GAMS is in a bad state. Try to backtrack for previous errors."
179
+ )
180
+
181
+ return_code = int(value)
182
+
183
+ if return_code in GAMS_STATUS:
184
+ try:
185
+ info = GAMS_STATUS[return_code]
186
+ except IndexError: # pragma: no cover
187
+ info = ""
188
+
189
+ raise GamspyException(
190
+ f"Return code {return_code}. {info} Check {job_name + '.lst'} for more information.",
191
+ return_code,
192
+ )
193
+
194
+
195
+ def send_job(
196
+ comm_pair_id: str,
197
+ job_name: str,
198
+ pf_file: str,
199
+ output: io.TextIOWrapper | None = None,
200
+ ):
201
+ _socket, process = get_connection(comm_pair_id)
202
+ try:
203
+ # Send pf file
204
+ _socket.sendall(pf_file.encode("utf-8"))
205
+
206
+ # Read output
207
+ _read_output(process, output)
208
+
209
+ # Receive response
210
+ response = _socket.recv(256)
211
+ check_response(response, job_name)
212
+ except ConnectionError as e:
213
+ raise FatalError(
214
+ f"There was an error while communicating with GAMS server: {e}",
215
+ ) from e