gamspy 1.17.2__py3-none-any.whl → 1.18.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gamspy/_backend/backend.py +3 -2
- gamspy/_backend/engine.py +6 -3
- gamspy/_backend/local.py +6 -2
- gamspy/_backend/neos.py +27 -5
- gamspy/_cli/install.py +10 -0
- gamspy/_cli/retrieve.py +6 -3
- gamspy/_cli/run.py +22 -13
- gamspy/_cli/uninstall.py +4 -0
- gamspy/_communication.py +215 -0
- gamspy/_container.py +30 -211
- gamspy/_convert.py +2 -0
- gamspy/_miro.py +8 -0
- gamspy/_model.py +14 -26
- gamspy/_model_instance.py +7 -6
- gamspy/_options.py +2 -8
- gamspy/_symbols/alias.py +0 -3
- gamspy/utils.py +0 -5
- {gamspy-1.17.2.dist-info → gamspy-1.18.0.dist-info}/METADATA +3 -3
- {gamspy-1.17.2.dist-info → gamspy-1.18.0.dist-info}/RECORD +23 -22
- {gamspy-1.17.2.dist-info → gamspy-1.18.0.dist-info}/WHEEL +0 -0
- {gamspy-1.17.2.dist-info → gamspy-1.18.0.dist-info}/entry_points.txt +0 -0
- {gamspy-1.17.2.dist-info → gamspy-1.18.0.dist-info}/licenses/LICENSE +0 -0
- {gamspy-1.17.2.dist-info → gamspy-1.18.0.dist-info}/top_level.txt +0 -0
gamspy/_backend/backend.py
CHANGED
|
@@ -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
|
):
|
gamspy/_backend/engine.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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]
|
gamspy/_backend/local.py
CHANGED
|
@@ -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
|
-
|
|
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()
|
gamspy/_backend/neos.py
CHANGED
|
@@ -7,12 +7,14 @@ import shutil
|
|
|
7
7
|
import ssl
|
|
8
8
|
import xmlrpc.client
|
|
9
9
|
import zipfile
|
|
10
|
-
from
|
|
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.
|
|
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.
|
|
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
|
gamspy/_cli/install.py
CHANGED
|
@@ -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(
|
gamspy/_cli/retrieve.py
CHANGED
|
@@ -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:
|
gamspy/_cli/run.py
CHANGED
|
@@ -5,11 +5,12 @@ import platform
|
|
|
5
5
|
import subprocess
|
|
6
6
|
import sys
|
|
7
7
|
from enum import Enum
|
|
8
|
-
from
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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()
|
gamspy/_cli/uninstall.py
CHANGED
|
@@ -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:
|
gamspy/_communication.py
ADDED
|
@@ -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
|
gamspy/_container.py
CHANGED
|
@@ -3,14 +3,14 @@ from __future__ import annotations
|
|
|
3
3
|
import atexit
|
|
4
4
|
import os
|
|
5
5
|
import platform
|
|
6
|
+
import shutil
|
|
6
7
|
import signal
|
|
7
|
-
import
|
|
8
|
-
import subprocess
|
|
8
|
+
import sys
|
|
9
9
|
import tempfile
|
|
10
10
|
import threading
|
|
11
|
-
import time
|
|
12
11
|
import traceback
|
|
13
12
|
import weakref
|
|
13
|
+
from pathlib import Path
|
|
14
14
|
from typing import TYPE_CHECKING
|
|
15
15
|
|
|
16
16
|
import gams.transfer as gt
|
|
@@ -20,12 +20,13 @@ import gamspy._miro as miro
|
|
|
20
20
|
import gamspy._validation as validation
|
|
21
21
|
import gamspy.utils as utils
|
|
22
22
|
from gamspy._backend.backend import backend_factory
|
|
23
|
+
from gamspy._communication import close_connection, get_connection, open_connection
|
|
23
24
|
from gamspy._config import get_option
|
|
24
25
|
from gamspy._extrinsic import ExtrinsicLibrary
|
|
25
26
|
from gamspy._miro import MiroJSONEncoder
|
|
26
27
|
from gamspy._model import Problem, Sense
|
|
27
28
|
from gamspy._workspace import Workspace
|
|
28
|
-
from gamspy.exceptions import
|
|
29
|
+
from gamspy.exceptions import ValidationError
|
|
29
30
|
|
|
30
31
|
if TYPE_CHECKING:
|
|
31
32
|
import io
|
|
@@ -67,79 +68,6 @@ def add_sysdir_to_path(system_directory: str) -> None:
|
|
|
67
68
|
os.environ["PATH"] = system_directory
|
|
68
69
|
|
|
69
70
|
|
|
70
|
-
def open_connection(
|
|
71
|
-
system_directory: str, process_directory: str, license_path: str
|
|
72
|
-
) -> tuple[socket.socket, subprocess.Popen]:
|
|
73
|
-
TIMEOUT = 30
|
|
74
|
-
|
|
75
|
-
initial_pf_file = os.path.join(process_directory, "gamspy.pf")
|
|
76
|
-
with open(initial_pf_file, "w") as file:
|
|
77
|
-
file.write(
|
|
78
|
-
'incrementalMode="2"\n'
|
|
79
|
-
f'procdir="{process_directory}"\n'
|
|
80
|
-
f'license="{license_path}"\n'
|
|
81
|
-
f'curdir="{os.getcwd()}"\n'
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
command = [
|
|
85
|
-
os.path.join(system_directory, "gams"),
|
|
86
|
-
"GAMSPY_JOB",
|
|
87
|
-
"pf",
|
|
88
|
-
initial_pf_file,
|
|
89
|
-
]
|
|
90
|
-
|
|
91
|
-
certificate_path = os.path.join(utils.DEFAULT_DIR, "gamspy_cert.crt")
|
|
92
|
-
env = os.environ.copy()
|
|
93
|
-
if os.path.isfile(certificate_path):
|
|
94
|
-
env["GAMSLICECRT"] = certificate_path
|
|
95
|
-
|
|
96
|
-
process = subprocess.Popen(
|
|
97
|
-
command,
|
|
98
|
-
text=True,
|
|
99
|
-
stdout=subprocess.PIPE,
|
|
100
|
-
stderr=subprocess.STDOUT,
|
|
101
|
-
errors="replace",
|
|
102
|
-
start_new_session=platform.system() != "Windows",
|
|
103
|
-
env=env,
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
port_info = process.stdout.readline().strip()
|
|
107
|
-
|
|
108
|
-
try:
|
|
109
|
-
port = int(port_info.removeprefix("port: "))
|
|
110
|
-
except ValueError as e:
|
|
111
|
-
raise ValidationError(
|
|
112
|
-
f"Error while reading the port! {port_info + process.stdout.read()}"
|
|
113
|
-
) from e
|
|
114
|
-
|
|
115
|
-
def handler(signum, frame):
|
|
116
|
-
if platform.system() != "Windows":
|
|
117
|
-
os.kill(process.pid, signal.SIGINT)
|
|
118
|
-
|
|
119
|
-
if threading.current_thread() is threading.main_thread():
|
|
120
|
-
signal.signal(signal.SIGINT, handler)
|
|
121
|
-
|
|
122
|
-
start = time.time()
|
|
123
|
-
while True:
|
|
124
|
-
if process.poll() is not None: # pragma: no cover
|
|
125
|
-
raise ValidationError(process.communicate()[0])
|
|
126
|
-
|
|
127
|
-
try:
|
|
128
|
-
new_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
129
|
-
new_socket.connect((LOOPBACK, port))
|
|
130
|
-
break
|
|
131
|
-
except (ConnectionRefusedError, OSError) as e:
|
|
132
|
-
new_socket.close()
|
|
133
|
-
end = time.time()
|
|
134
|
-
|
|
135
|
-
if end - start > TIMEOUT: # pragma: no cover
|
|
136
|
-
raise FatalError(
|
|
137
|
-
f"Timeout while establishing the connection with socket. {process.communicate()[0]}"
|
|
138
|
-
) from e
|
|
139
|
-
|
|
140
|
-
return new_socket, process
|
|
141
|
-
|
|
142
|
-
|
|
143
71
|
def get_system_directory(system_directory: str | os.PathLike | None) -> str:
|
|
144
72
|
if isinstance(system_directory, os.PathLike):
|
|
145
73
|
system_directory = os.fspath(system_directory)
|
|
@@ -171,65 +99,6 @@ def get_options_file_name(solver: str, file_number: int) -> str:
|
|
|
171
99
|
return options_file_name
|
|
172
100
|
|
|
173
101
|
|
|
174
|
-
def check_response(response: bytes, job_name: str) -> None:
|
|
175
|
-
GAMS_STATUS = {
|
|
176
|
-
1: "Solver is to be called, the system should never return this number.",
|
|
177
|
-
2: "There was a compilation error.",
|
|
178
|
-
3: "There was an execution error.",
|
|
179
|
-
4: "System limits were reached.",
|
|
180
|
-
5: "There was a file error.",
|
|
181
|
-
6: "There was a parameter error.",
|
|
182
|
-
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.",
|
|
183
|
-
8: "There was a GAMS system error.",
|
|
184
|
-
9: "GAMS could not be started.",
|
|
185
|
-
10: "Out of memory.",
|
|
186
|
-
11: "Out of disk.",
|
|
187
|
-
109: "Could not create process/scratch directory.",
|
|
188
|
-
110: "Too many process/scratch directories.",
|
|
189
|
-
112: "Could not delete the process/scratch directory.",
|
|
190
|
-
113: "Could not write the script gamsnext.",
|
|
191
|
-
114: "Could not write the parameter file.",
|
|
192
|
-
115: "Could not read environment variable.",
|
|
193
|
-
400: "Could not spawn the GAMS language compiler (gamscmex).",
|
|
194
|
-
401: "Current directory (curdir) does not exist.",
|
|
195
|
-
402: "Cannot set current directory (curdir).",
|
|
196
|
-
404: "Blank in system directory (UNIX only).",
|
|
197
|
-
405: "Blank in current directory (UNIX only).",
|
|
198
|
-
406: "Blank in scratch extension (scrext)",
|
|
199
|
-
407: "Unexpected cmexRC.",
|
|
200
|
-
408: "Could not find the process directory (procdir).",
|
|
201
|
-
409: "CMEX library not be found (experimental).",
|
|
202
|
-
410: "Entry point in CMEX library could not be found (experimental).",
|
|
203
|
-
411: "Blank in process directory (UNIX only).",
|
|
204
|
-
412: "Blank in scratch directory (UNIX only).",
|
|
205
|
-
909: "Cannot add path / unknown UNIX environment / cannot set environment variable.",
|
|
206
|
-
1000: "Driver error: incorrect command line parameters for gams.",
|
|
207
|
-
2000: "Driver error: internal error: cannot install interrupt handler.",
|
|
208
|
-
3000: "Driver error: problems getting current directory.",
|
|
209
|
-
4000: "Driver error: internal error: GAMS compile and execute module not found.",
|
|
210
|
-
5000: "Driver error: internal error: cannot load option handling library.",
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
value = response[: response.find(b"#")].decode("ascii")
|
|
214
|
-
if not value:
|
|
215
|
-
raise FatalError(
|
|
216
|
-
"Error while getting the return code from GAMS backend. This means that GAMS is in a bad state. Try to backtrack for previous errors."
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
return_code = int(value)
|
|
220
|
-
|
|
221
|
-
if return_code in GAMS_STATUS:
|
|
222
|
-
try:
|
|
223
|
-
info = GAMS_STATUS[return_code]
|
|
224
|
-
except IndexError: # pragma: no cover
|
|
225
|
-
info = ""
|
|
226
|
-
|
|
227
|
-
raise GamspyException(
|
|
228
|
-
f"Return code {return_code}. {info} Check {job_name + '.lst'} for more information.",
|
|
229
|
-
return_code,
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
|
|
233
102
|
class Container(gt.Container):
|
|
234
103
|
"""
|
|
235
104
|
A container is an object that holds all symbols and operates on them.
|
|
@@ -266,14 +135,13 @@ class Container(gt.Container):
|
|
|
266
135
|
options: Options | None = None,
|
|
267
136
|
output: io.TextIOWrapper | None = None,
|
|
268
137
|
):
|
|
138
|
+
self._comm_pair_id = utils._get_unique_name()
|
|
269
139
|
self.output = output
|
|
270
140
|
self._gams_string = ""
|
|
271
141
|
self.models: dict[str, Model] = {}
|
|
272
142
|
if IS_MIRO_INIT:
|
|
273
143
|
atexit.register(self._write_miro_files)
|
|
274
144
|
|
|
275
|
-
self._is_socket_open = True
|
|
276
|
-
|
|
277
145
|
system_directory = get_system_directory(system_directory)
|
|
278
146
|
add_sysdir_to_path(system_directory)
|
|
279
147
|
|
|
@@ -306,16 +174,8 @@ class Container(gt.Container):
|
|
|
306
174
|
self._miro_input_symbols: list[str] = []
|
|
307
175
|
self._miro_output_symbols: list[str] = []
|
|
308
176
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
)
|
|
312
|
-
weakref.finalize(
|
|
313
|
-
self,
|
|
314
|
-
self._release_resources,
|
|
315
|
-
self._is_socket_open,
|
|
316
|
-
self._socket,
|
|
317
|
-
self._process,
|
|
318
|
-
)
|
|
177
|
+
open_connection(self)
|
|
178
|
+
weakref.finalize(self, close_connection, self._comm_pair_id)
|
|
319
179
|
|
|
320
180
|
self._is_restarted = False
|
|
321
181
|
if load_from is not None:
|
|
@@ -434,58 +294,12 @@ class Container(gt.Container):
|
|
|
434
294
|
|
|
435
295
|
return name
|
|
436
296
|
|
|
437
|
-
@staticmethod
|
|
438
|
-
def _release_resources(
|
|
439
|
-
is_socket_open: bool, socket: socket.socket, process: subprocess.Popen
|
|
440
|
-
):
|
|
441
|
-
if is_socket_open:
|
|
442
|
-
try:
|
|
443
|
-
socket.sendall(b"stop")
|
|
444
|
-
socket.close()
|
|
445
|
-
|
|
446
|
-
if not process.stdout.closed:
|
|
447
|
-
process.stdout.close()
|
|
448
|
-
|
|
449
|
-
while process.poll() is None:
|
|
450
|
-
...
|
|
451
|
-
except OSError: # Socket may have been already closed.
|
|
452
|
-
...
|
|
453
|
-
|
|
454
|
-
def _read_output(self, output: io.TextIOWrapper | None) -> None:
|
|
455
|
-
if output is not None:
|
|
456
|
-
while True:
|
|
457
|
-
data = self._process.stdout.readline()
|
|
458
|
-
output.write(data)
|
|
459
|
-
output.flush()
|
|
460
|
-
if data.startswith("--- Job ") and "elapsed" in data:
|
|
461
|
-
break
|
|
462
|
-
|
|
463
297
|
def _interrupt(self) -> None:
|
|
298
|
+
_, process = get_connection(self._comm_pair_id)
|
|
464
299
|
if platform.system() == "Windows":
|
|
465
|
-
os.kill(
|
|
300
|
+
os.kill(process.pid, signal.CTRL_C_EVENT)
|
|
466
301
|
else:
|
|
467
|
-
os.kill(
|
|
468
|
-
|
|
469
|
-
def _send_job(
|
|
470
|
-
self,
|
|
471
|
-
job_name: str,
|
|
472
|
-
pf_file: str,
|
|
473
|
-
output: io.TextIOWrapper | None = None,
|
|
474
|
-
):
|
|
475
|
-
try:
|
|
476
|
-
# Send pf file
|
|
477
|
-
self._socket.sendall(pf_file.encode("utf-8"))
|
|
478
|
-
|
|
479
|
-
# Read output
|
|
480
|
-
self._read_output(output)
|
|
481
|
-
|
|
482
|
-
# Receive response
|
|
483
|
-
response = self._socket.recv(256)
|
|
484
|
-
check_response(response, job_name)
|
|
485
|
-
except ConnectionError as e:
|
|
486
|
-
raise FatalError(
|
|
487
|
-
f"There was an error while communicating with GAMS server: {e}",
|
|
488
|
-
) from e
|
|
302
|
+
os.kill(process.pid, signal.SIGINT)
|
|
489
303
|
|
|
490
304
|
def _write_miro_files(self):
|
|
491
305
|
# create conf_<model>/<model>_io.json
|
|
@@ -493,9 +307,8 @@ class Container(gt.Container):
|
|
|
493
307
|
encoder = MiroJSONEncoder(self)
|
|
494
308
|
encoder.write_json()
|
|
495
309
|
except Exception:
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
traceback.print_exc()
|
|
310
|
+
close_connection(self._comm_pair_id)
|
|
311
|
+
traceback.print_exc(file=sys.stderr)
|
|
499
312
|
os._exit(1)
|
|
500
313
|
|
|
501
314
|
def _add_statement(self, statement) -> None:
|
|
@@ -855,7 +668,7 @@ class Container(gt.Container):
|
|
|
855
668
|
)
|
|
856
669
|
|
|
857
670
|
def writeSolverOptions(
|
|
858
|
-
self, solver: str, solver_options: dict, file_number: int = 1
|
|
671
|
+
self, solver: str, solver_options: dict | str | Path, file_number: int = 1
|
|
859
672
|
) -> None:
|
|
860
673
|
"""
|
|
861
674
|
Writes solver options of the specified solver to the working directory.
|
|
@@ -864,8 +677,8 @@ class Container(gt.Container):
|
|
|
864
677
|
----------
|
|
865
678
|
solver : str
|
|
866
679
|
Name of the solver.
|
|
867
|
-
solver_options : dict
|
|
868
|
-
Options of the specified solver.
|
|
680
|
+
solver_options : dict | str | Path
|
|
681
|
+
Options of the specified solver or path to an existing solver options file.
|
|
869
682
|
file_number : int
|
|
870
683
|
Solver option file number. Equivalent to optfile option of GAMS. See https://gams.com/latest/docs/UG_GamsCall.html#GAMSAOoptfile for more details.
|
|
871
684
|
|
|
@@ -881,17 +694,24 @@ class Container(gt.Container):
|
|
|
881
694
|
f"The smallest number `file_number` can get is 1 but received {file_number}"
|
|
882
695
|
)
|
|
883
696
|
|
|
697
|
+
if solver.lower() == "conopt":
|
|
698
|
+
solver = "conopt4"
|
|
699
|
+
|
|
884
700
|
options_file_name = os.path.join(
|
|
885
701
|
self.working_directory, get_options_file_name(solver, file_number)
|
|
886
702
|
)
|
|
887
703
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
704
|
+
if isinstance(solver_options, (str, Path)):
|
|
705
|
+
path = Path(solver_options)
|
|
706
|
+
shutil.copy2(path, options_file_name)
|
|
707
|
+
else:
|
|
708
|
+
with open(options_file_name, "w", encoding="utf-8") as solver_file:
|
|
709
|
+
for key, value in solver_options.items():
|
|
710
|
+
row = f"{key} {value}\n"
|
|
711
|
+
if solver.upper() in ("SHOT", "SOPLEX", "SCIP", "HIGHS"):
|
|
712
|
+
row = f"{key} = {value}\n"
|
|
893
713
|
|
|
894
|
-
|
|
714
|
+
solver_file.write(row)
|
|
895
715
|
|
|
896
716
|
# The following solvers do not use the opt<solver>.def file
|
|
897
717
|
if solver.upper() in (
|
|
@@ -1026,8 +846,7 @@ class Container(gt.Container):
|
|
|
1026
846
|
to communicate with the GAMS execution engine, e.g. creating new symbols, changing data,
|
|
1027
847
|
solves, etc. The container data (Container.data) is still available for read operations.
|
|
1028
848
|
"""
|
|
1029
|
-
|
|
1030
|
-
self._is_socket_open = False
|
|
849
|
+
close_connection(self._comm_pair_id)
|
|
1031
850
|
|
|
1032
851
|
def addAlias(
|
|
1033
852
|
self,
|
gamspy/_convert.py
CHANGED
|
@@ -311,6 +311,8 @@ def get_convert_solver_options(
|
|
|
311
311
|
FORMAT_RENAME_MAP = {
|
|
312
312
|
"GDXJacobian": "DumpGDX",
|
|
313
313
|
"GDXIntervalEval": "IntervalEval",
|
|
314
|
+
"GAMSDict": "Dict",
|
|
315
|
+
"GAMSDictMap": "DictMap",
|
|
314
316
|
}
|
|
315
317
|
OPTION_RENAME_MAP = {"GAMSInsert": "GmsInsert", "GAMSObjVar": "ObjVar"}
|
|
316
318
|
|
gamspy/_miro.py
CHANGED
|
@@ -80,6 +80,14 @@ class MiroJSONEncoder:
|
|
|
80
80
|
self,
|
|
81
81
|
container: Container,
|
|
82
82
|
):
|
|
83
|
+
if (
|
|
84
|
+
len(container._miro_input_symbols) == 0
|
|
85
|
+
and len(container._miro_output_symbols) == 0
|
|
86
|
+
):
|
|
87
|
+
raise ValidationError(
|
|
88
|
+
"There must be at least one miro input/output symbol in the model"
|
|
89
|
+
)
|
|
90
|
+
|
|
83
91
|
self.container = container
|
|
84
92
|
self.model_title = "GAMSPy App"
|
|
85
93
|
self.input_symbols = container._miro_input_symbols
|
gamspy/_model.py
CHANGED
|
@@ -4,7 +4,6 @@ import inspect
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import threading
|
|
7
|
-
import warnings
|
|
8
7
|
from collections.abc import Sequence
|
|
9
8
|
from enum import Enum
|
|
10
9
|
from pathlib import Path
|
|
@@ -31,14 +30,12 @@ from gamspy._options import (
|
|
|
31
30
|
MODEL_ATTR_OPTION_MAP,
|
|
32
31
|
ConvertOptions,
|
|
33
32
|
FreezeOptions,
|
|
34
|
-
ModelInstanceOptions,
|
|
35
33
|
Options,
|
|
36
34
|
)
|
|
37
35
|
from gamspy.exceptions import GamspyException, ValidationError
|
|
38
36
|
|
|
39
37
|
if TYPE_CHECKING:
|
|
40
|
-
import
|
|
41
|
-
from typing import Literal
|
|
38
|
+
from typing import Literal, TextIO
|
|
42
39
|
|
|
43
40
|
import pandas as pd
|
|
44
41
|
|
|
@@ -1307,10 +1304,9 @@ class Model:
|
|
|
1307
1304
|
self,
|
|
1308
1305
|
solver: str | None = None,
|
|
1309
1306
|
options: Options | None = None,
|
|
1310
|
-
solver_options: dict | None = None,
|
|
1311
|
-
model_instance_options: ModelInstanceOptions | None = None,
|
|
1307
|
+
solver_options: dict | str | Path | None = None,
|
|
1312
1308
|
freeze_options: FreezeOptions | None = None,
|
|
1313
|
-
output:
|
|
1309
|
+
output: TextIO | None = None,
|
|
1314
1310
|
backend: Literal["local", "engine", "neos"] = "local",
|
|
1315
1311
|
client: EngineClient | NeosClient | None = None,
|
|
1316
1312
|
load_symbols: list[Symbol] | None = None,
|
|
@@ -1324,14 +1320,12 @@ class Model:
|
|
|
1324
1320
|
Solver name
|
|
1325
1321
|
options : Options, optional
|
|
1326
1322
|
GAMSPy options.
|
|
1327
|
-
solver_options : dict, optional
|
|
1328
|
-
|
|
1329
|
-
model_instance_options : ModelInstanceOptions, optional
|
|
1330
|
-
Options to solve a frozen model. This argument will be deprecated in GAMSPy 1.10.0. Please use freeze_options instead.
|
|
1323
|
+
solver_options : dict | str | Path, optional
|
|
1324
|
+
Dictionary of solver options or path to an existing option file.
|
|
1331
1325
|
freeze_options : FreezeOptions, optional
|
|
1332
1326
|
Options to solve a frozen model.
|
|
1333
|
-
output :
|
|
1334
|
-
Output redirection target
|
|
1327
|
+
output : TextIO, optional
|
|
1328
|
+
Output redirection target. Set this to sys.stdout to see the output in your terminal.
|
|
1335
1329
|
backend : str, optional
|
|
1336
1330
|
Backend to run on
|
|
1337
1331
|
client : EngineClient, NeosClient, optional
|
|
@@ -1395,21 +1389,20 @@ class Model:
|
|
|
1395
1389
|
if options is None:
|
|
1396
1390
|
options = self.container._options
|
|
1397
1391
|
|
|
1392
|
+
if isinstance(solver_options, (str, Path)):
|
|
1393
|
+
solver_options = Path(solver_options)
|
|
1394
|
+
|
|
1398
1395
|
# Only for local until GAMS Engine and NEOS Server backends adopt the new GP_SolveLine option.
|
|
1399
1396
|
if backend == "local":
|
|
1400
1397
|
frame = inspect.currentframe().f_back
|
|
1401
1398
|
assert isinstance(options, gp.Options)
|
|
1402
1399
|
options._frame = frame
|
|
1403
1400
|
|
|
1401
|
+
if solver_options is not None:
|
|
1402
|
+
self.container.writeSolverOptions(solver, solver_options)
|
|
1403
|
+
|
|
1404
1404
|
if self._is_frozen:
|
|
1405
1405
|
instance_options = FreezeOptions()
|
|
1406
|
-
if model_instance_options is not None:
|
|
1407
|
-
warnings.warn(
|
|
1408
|
-
"model_instance_options will be renamed to freeze_options in GAMSPy 1.10.0",
|
|
1409
|
-
DeprecationWarning,
|
|
1410
|
-
stacklevel=2,
|
|
1411
|
-
)
|
|
1412
|
-
instance_options = model_instance_options
|
|
1413
1406
|
|
|
1414
1407
|
if freeze_options is not None:
|
|
1415
1408
|
instance_options = freeze_options
|
|
@@ -1423,12 +1416,7 @@ class Model:
|
|
|
1423
1416
|
self._add_runtime_options(options, backend)
|
|
1424
1417
|
self.container._add_statement(self._generate_solve_string() + "\n")
|
|
1425
1418
|
self._create_model_attributes()
|
|
1426
|
-
options.
|
|
1427
|
-
container=self.container,
|
|
1428
|
-
solver=solver,
|
|
1429
|
-
problem=self.problem,
|
|
1430
|
-
solver_options=solver_options,
|
|
1431
|
-
)
|
|
1419
|
+
options._set_model_info(solver, self.problem, solver_options)
|
|
1432
1420
|
|
|
1433
1421
|
runner = backend_factory(
|
|
1434
1422
|
self.container,
|
gamspy/_model_instance.py
CHANGED
|
@@ -59,6 +59,7 @@ from gams.core.gmo import (
|
|
|
59
59
|
import gamspy as gp
|
|
60
60
|
import gamspy._symbols.implicits as implicits
|
|
61
61
|
import gamspy.utils as utils
|
|
62
|
+
from gamspy._communication import send_job
|
|
62
63
|
from gamspy._database import (
|
|
63
64
|
Database,
|
|
64
65
|
GamsEquation,
|
|
@@ -73,6 +74,7 @@ from gamspy.exceptions import (
|
|
|
73
74
|
|
|
74
75
|
if TYPE_CHECKING:
|
|
75
76
|
import io
|
|
77
|
+
from pathlib import Path
|
|
76
78
|
|
|
77
79
|
from gamspy import Container, Model, Parameter
|
|
78
80
|
from gamspy._options import FreezeOptions, Options
|
|
@@ -324,7 +326,9 @@ class ModelInstance:
|
|
|
324
326
|
# Run
|
|
325
327
|
try:
|
|
326
328
|
self.container._job = self.job_name
|
|
327
|
-
|
|
329
|
+
send_job(
|
|
330
|
+
self.container._comm_pair_id, self.job_name, self.pf_file, self.output
|
|
331
|
+
)
|
|
328
332
|
except GamspyException as exception:
|
|
329
333
|
self.container._workspace._errors.append(str(exception))
|
|
330
334
|
message = _customize_exception(
|
|
@@ -354,14 +358,11 @@ class ModelInstance:
|
|
|
354
358
|
self,
|
|
355
359
|
solver: str,
|
|
356
360
|
instance_options: FreezeOptions,
|
|
357
|
-
solver_options: dict | None,
|
|
361
|
+
solver_options: dict | Path | None,
|
|
358
362
|
output: io.TextIOWrapper | None,
|
|
359
363
|
) -> pd.DataFrame:
|
|
360
364
|
# write solver options file
|
|
361
|
-
option_file = 0
|
|
362
|
-
if solver_options:
|
|
363
|
-
self.container.writeSolverOptions(solver, solver_options)
|
|
364
|
-
option_file = 1
|
|
365
|
+
option_file = 1 if solver_options else 0
|
|
365
366
|
|
|
366
367
|
names_to_write = []
|
|
367
368
|
for symbol in self.modifiables:
|
gamspy/_options.py
CHANGED
|
@@ -13,7 +13,6 @@ if TYPE_CHECKING:
|
|
|
13
13
|
import io
|
|
14
14
|
from types import FrameType
|
|
15
15
|
|
|
16
|
-
from gamspy import Container
|
|
17
16
|
from gamspy._model import Problem
|
|
18
17
|
|
|
19
18
|
SOLVE_LINK_MAP = {"disk": 2, "memory": 5}
|
|
@@ -482,19 +481,14 @@ class Options(BaseModel):
|
|
|
482
481
|
|
|
483
482
|
return gams_options
|
|
484
483
|
|
|
485
|
-
def
|
|
486
|
-
self,
|
|
487
|
-
container: Container,
|
|
488
|
-
solver: str,
|
|
489
|
-
problem: Problem,
|
|
490
|
-
solver_options: dict | None,
|
|
484
|
+
def _set_model_info(
|
|
485
|
+
self, solver: str, problem: Problem, solver_options: dict | Path | None
|
|
491
486
|
):
|
|
492
487
|
"""Set the solver and the solver options"""
|
|
493
488
|
self._solver = solver
|
|
494
489
|
self._problem = str(problem)
|
|
495
490
|
|
|
496
491
|
if solver_options:
|
|
497
|
-
container.writeSolverOptions(solver, solver_options)
|
|
498
492
|
self._solver_options_file = "1"
|
|
499
493
|
else:
|
|
500
494
|
self._solver_options_file = "0"
|
gamspy/_symbols/alias.py
CHANGED
|
@@ -88,9 +88,6 @@ class Alias(gt.Alias, operable.Operable, Symbol, SetMixin):
|
|
|
88
88
|
f"Container must of type `Container` but found {type(container)}"
|
|
89
89
|
)
|
|
90
90
|
|
|
91
|
-
if not isinstance(alias_with, (gp.Set, gp.Alias)):
|
|
92
|
-
raise TypeError(f"alias_with must be a Set but found {type(alias_with)}")
|
|
93
|
-
|
|
94
91
|
if name is None:
|
|
95
92
|
return object.__new__(cls)
|
|
96
93
|
else:
|
gamspy/utils.py
CHANGED
|
@@ -492,11 +492,6 @@ def _get_license_path(system_directory: str) -> str:
|
|
|
492
492
|
if os.path.exists(gamspy_license_path):
|
|
493
493
|
return gamspy_license_path
|
|
494
494
|
|
|
495
|
-
# Check old license installation path.
|
|
496
|
-
user_license_path = os.path.join(system_directory, "user_license.txt")
|
|
497
|
-
if os.path.exists(user_license_path):
|
|
498
|
-
return user_license_path
|
|
499
|
-
|
|
500
495
|
# No preinstalled licenses on the machine. Use the demo license.
|
|
501
496
|
return os.path.join(system_directory, "gamslice.txt")
|
|
502
497
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gamspy
|
|
3
|
-
Version: 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<
|
|
35
|
-
Requires-Dist: gamspy_base<
|
|
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
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
gamspy/__init__.py,sha256=HZulKBgt2ubfX6r6v6Xv3MJVI6Prm40j5W45aUetQok,1797
|
|
2
2
|
gamspy/__main__.py,sha256=3HW3NYEe4pZnGhJ10s1G0vaZAkI9Zl_lXqHT9iDNXQg,108
|
|
3
|
+
gamspy/_communication.py,sha256=18drDsKxi1Iv6oAKZfKGJcPYYWebBQsyeOxiEKXZngE,7148
|
|
3
4
|
gamspy/_config.py,sha256=n7RXUFczIlol15-mxhnj54fHXHet8HcOnWiGm3mpMgg,3589
|
|
4
|
-
gamspy/_container.py,sha256=
|
|
5
|
-
gamspy/_convert.py,sha256=
|
|
5
|
+
gamspy/_container.py,sha256=w5zftBCrgFSkZEOgwh2zyFjiDA4AohzNHi-BuF0Hjas,45884
|
|
6
|
+
gamspy/_convert.py,sha256=z_ju0T2duAAeYE0ahW7E2GMdMMVZsQ5936HV583hXzs,22294
|
|
6
7
|
gamspy/_database.py,sha256=SVo4lEfS0Q2f80uMu7k71BBQNRwUei-2KFc_EGxsOcw,6873
|
|
7
8
|
gamspy/_extrinsic.py,sha256=6pBam_ruzKaYjEpALjAQ5d9uPrlOf1lVMYjRYqHH8o0,4337
|
|
8
|
-
gamspy/_miro.py,sha256=
|
|
9
|
-
gamspy/_model.py,sha256=
|
|
10
|
-
gamspy/_model_instance.py,sha256=
|
|
11
|
-
gamspy/_options.py,sha256=
|
|
9
|
+
gamspy/_miro.py,sha256=LLmo5L8i7LRdvcvZn2V9S2OCSQ-8KsiUZnOaGxwohgw,12511
|
|
10
|
+
gamspy/_model.py,sha256=Qd8JdDDUUSjWXQUKGR4N0-2J_PHnzh8sjSG3U7A4aKk,51359
|
|
11
|
+
gamspy/_model_instance.py,sha256=ZHG3-inc6vDx7F2Oo9HYWYAi8hki6upIgAQn5II_o1U,24384
|
|
12
|
+
gamspy/_options.py,sha256=FTty65WRdikvnhPyWSTuwuUz0Rs6aZwDFWL-UsfEgd0,27211
|
|
12
13
|
gamspy/_serialization.py,sha256=fwAQpABCZQyxtAkddsvCDjtyQyPUT3EJy19r9BKR6aY,4062
|
|
13
14
|
gamspy/_types.py,sha256=_KQjRntvIAicc9_sZ5BYWAHjs-WBURf2NJwTpJQT5xw,162
|
|
14
15
|
gamspy/_validation.py,sha256=k4e8M8X32hEFybJyISyXLNrzO1mmWXQPnSThT7rOC5U,21608
|
|
15
16
|
gamspy/_workspace.py,sha256=QrnC8aPN5C9zyqbaYvfuPlRHvBrDx_i4Imw1AzsJs7k,2175
|
|
16
17
|
gamspy/exceptions.py,sha256=q-Cb6syXv1abf2JBfNFidGkvOWkp9FXU5NzJwkR5W7o,2929
|
|
17
18
|
gamspy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
gamspy/utils.py,sha256=
|
|
19
|
+
gamspy/utils.py,sha256=6eO4R0iBLRm2oTRCz_croiQhKpiIyF8bOa0NDfw9aUU,19777
|
|
19
20
|
gamspy/version.py,sha256=GsqTw9rQ4Zz80ZSlIo44vYqgcVBJSbGOqJFJW3j_z88,109
|
|
20
21
|
gamspy/_algebra/__init__.py,sha256=HdTHRBzfOGXihibQOAOAHGHKinlMuHVnpuP4pJWSQCc,479
|
|
21
22
|
gamspy/_algebra/condition.py,sha256=mVJeCk65dvLgYalTQHfZt0bEFFf9yoTWLXjhODf7gvg,6229
|
|
@@ -25,23 +26,23 @@ gamspy/_algebra/number.py,sha256=Yf7kYLwkmXCipiAzQP7LtTchMMK7OccChuXnTIYl1w4,168
|
|
|
25
26
|
gamspy/_algebra/operable.py,sha256=1sFr29_7A9RKVxDlTY9fC_g4sNRavNOYpjACWzvyjRc,6315
|
|
26
27
|
gamspy/_algebra/operation.py,sha256=e3gYxS5OPh8JYGAnAS1wEpzafFpfBt0UbcL4PKZyoY0,22990
|
|
27
28
|
gamspy/_backend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
-
gamspy/_backend/backend.py,sha256=
|
|
29
|
-
gamspy/_backend/engine.py,sha256=
|
|
30
|
-
gamspy/_backend/local.py,sha256
|
|
31
|
-
gamspy/_backend/neos.py,sha256=
|
|
29
|
+
gamspy/_backend/backend.py,sha256=VgLL3CiFydkf1ZF8ttaOlWsDtkJIoRdi_md4tVh789Q,9217
|
|
30
|
+
gamspy/_backend/engine.py,sha256=N3BIb4CDmH76CQIj87JSJBNnMLln3jsUuLy9_O5sEJ0,31826
|
|
31
|
+
gamspy/_backend/local.py,sha256=-LE7obmZT3zdB-0LJHJ9xfulMBw5vvzFIS9WJ7ISE-4,3804
|
|
32
|
+
gamspy/_backend/neos.py,sha256=I21TOqPZC5cBp_rQpz5S9cmLPRJcizIT0KuncwMcUrw,17762
|
|
32
33
|
gamspy/_cli/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
|
|
33
34
|
gamspy/_cli/cli.py,sha256=a4AtD-VI5jk0ZcO0JAwD0f0KJRwmxA8MzR_2MmhPWpg,1580
|
|
34
35
|
gamspy/_cli/gdx.py,sha256=YRu1delsmDh-dpzEr8n1kTK0JFdXlNqz4YuUQzK1mvo,12024
|
|
35
|
-
gamspy/_cli/install.py,sha256=
|
|
36
|
+
gamspy/_cli/install.py,sha256=jwJZYRZ_-yxlN0yNT9BitDfGN4q6WM9oq-OUpQIwxHU,13312
|
|
36
37
|
gamspy/_cli/list.py,sha256=fMmaIZcZf-Tf_-J37tJ4zrk2bcVO9cPSfs08mQyGWiY,2907
|
|
37
38
|
gamspy/_cli/probe.py,sha256=K7fYwfdk-2mGGQKAxXFvkWMRJ1qYTUpX2tvklU56JZU,1122
|
|
38
|
-
gamspy/_cli/retrieve.py,sha256=
|
|
39
|
-
gamspy/_cli/run.py,sha256=
|
|
39
|
+
gamspy/_cli/retrieve.py,sha256=bnTcIbHJfkq-dvrFitdkxsjQNZz-Cx4-LiaRfhe0cYM,2446
|
|
40
|
+
gamspy/_cli/run.py,sha256=oaWT4alxSXqPDF2mRYmfy5aOWCKoEExUfqYoAT_zSUM,4387
|
|
40
41
|
gamspy/_cli/show.py,sha256=NTZJBtdns0Bd6UOdlTDIelhU92xmLJjT9HuDLqS3YJ4,11144
|
|
41
|
-
gamspy/_cli/uninstall.py,sha256=
|
|
42
|
+
gamspy/_cli/uninstall.py,sha256=szQJYFoOubn35rmTRT-rQrJ25drH5HU9FY3hEONh3Lc,5060
|
|
42
43
|
gamspy/_cli/util.py,sha256=ey3Wn7tCEggkr-sjcLqrVBda-jdadjWOn-APhSe4toE,2849
|
|
43
44
|
gamspy/_symbols/__init__.py,sha256=p0Zd7XytuOe8kRclN1lFrRFaN_btFSSGdIBSXrlp03E,450
|
|
44
|
-
gamspy/_symbols/alias.py,sha256=
|
|
45
|
+
gamspy/_symbols/alias.py,sha256=lIoNynvLd-tfXrVBs8Asy0TEkhs-xkTeNl4yLtzehiA,8795
|
|
45
46
|
gamspy/_symbols/equation.py,sha256=W_8eQdZ9Mou9YVI1l0X_gwFB--TVNOK02OGpWmO3Xek,39719
|
|
46
47
|
gamspy/_symbols/parameter.py,sha256=i5UWz8KQEHDBOjGV7JmL8GhnLnL-CiFMUhfSrBZOqQs,19721
|
|
47
48
|
gamspy/_symbols/set.py,sha256=j3Izvgs470xiauwCbgXZOelZy-KAzslDGXuKeHn_g3g,29130
|
|
@@ -80,9 +81,9 @@ gamspy/math/matrix.py,sha256=PmYsMhF8Pq3bcgtyINryq4xV36i4KW0BtcFApOvt8GQ,16365
|
|
|
80
81
|
gamspy/math/misc.py,sha256=FaSRYFjMN6AULqOqGIF6_AXDw6KVqgxm2EtZsDcBFmE,37432
|
|
81
82
|
gamspy/math/probability.py,sha256=gAVU_PTfbjXuN6i8XNjZpIYvFg8dPk1imZCbKQloSG0,4290
|
|
82
83
|
gamspy/math/trigonometric.py,sha256=VBWQcP3ggl1oGDEzRs2M_T88HPJn9N0QfMJZwEUkx4w,4276
|
|
83
|
-
gamspy-1.
|
|
84
|
-
gamspy-1.
|
|
85
|
-
gamspy-1.
|
|
86
|
-
gamspy-1.
|
|
87
|
-
gamspy-1.
|
|
88
|
-
gamspy-1.
|
|
84
|
+
gamspy-1.18.0.dist-info/licenses/LICENSE,sha256=D4HfHbHRs3LVWo3uXq44WQc1FkBP5srUCEa-vO0W9tE,1189
|
|
85
|
+
gamspy-1.18.0.dist-info/METADATA,sha256=TI0qm0fzdv_VotY38KXNVVz7xwD_947xuFOfA4DhWFg,5301
|
|
86
|
+
gamspy-1.18.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
87
|
+
gamspy-1.18.0.dist-info/entry_points.txt,sha256=tzgC7L0Y_BiyagBCaYOPK7lE4olRJKM_PH2inmMvj60,48
|
|
88
|
+
gamspy-1.18.0.dist-info/top_level.txt,sha256=fsq4q5lfdb2GEZC9O3PUih38s7TfIgolIaO5NgR3Hf8,7
|
|
89
|
+
gamspy-1.18.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|