gamspy 1.18.1__tar.gz → 1.18.3__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 (97) hide show
  1. {gamspy-1.18.1 → gamspy-1.18.3}/PKG-INFO +3 -3
  2. {gamspy-1.18.1 → gamspy-1.18.3}/pyproject.toml +3 -3
  3. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_algebra/domain.py +1 -1
  4. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_algebra/expression.py +1 -1
  5. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_algebra/operation.py +10 -3
  6. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_cli/cli.py +2 -1
  7. gamspy-1.18.3/src/gamspy/_cli/mps2gms.py +128 -0
  8. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_container.py +44 -2
  9. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_convert.py +97 -66
  10. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_extrinsic.py +17 -1
  11. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_model.py +44 -9
  12. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_symbols/alias.py +2 -0
  13. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_symbols/equation.py +15 -7
  14. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_symbols/implicits/implicit_set.py +2 -1
  15. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_symbols/implicits/implicit_symbol.py +6 -1
  16. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_symbols/parameter.py +2 -0
  17. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_symbols/set.py +2 -0
  18. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_symbols/symbol.py +1 -2
  19. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_symbols/universe_alias.py +26 -1
  20. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_symbols/variable.py +2 -0
  21. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_validation.py +10 -1
  22. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/math/misc.py +19 -6
  23. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/utils.py +22 -11
  24. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy.egg-info/PKG-INFO +3 -3
  25. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy.egg-info/SOURCES.txt +1 -0
  26. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy.egg-info/requires.txt +2 -2
  27. {gamspy-1.18.1 → gamspy-1.18.3}/tests/test_gamspy.py +1 -1
  28. {gamspy-1.18.1 → gamspy-1.18.3}/LICENSE +0 -0
  29. {gamspy-1.18.1 → gamspy-1.18.3}/README.md +0 -0
  30. {gamspy-1.18.1 → gamspy-1.18.3}/README_PYPI.md +0 -0
  31. {gamspy-1.18.1 → gamspy-1.18.3}/setup.cfg +0 -0
  32. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/__init__.py +0 -0
  33. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/__main__.py +0 -0
  34. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_algebra/__init__.py +0 -0
  35. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_algebra/condition.py +0 -0
  36. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_algebra/number.py +0 -0
  37. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_algebra/operable.py +0 -0
  38. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_backend/__init__.py +0 -0
  39. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_backend/backend.py +0 -0
  40. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_backend/engine.py +0 -0
  41. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_backend/local.py +0 -0
  42. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_backend/neos.py +0 -0
  43. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_cli/__init__.py +0 -0
  44. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_cli/gdx.py +0 -0
  45. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_cli/install.py +0 -0
  46. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_cli/list.py +0 -0
  47. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_cli/probe.py +0 -0
  48. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_cli/retrieve.py +0 -0
  49. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_cli/run.py +0 -0
  50. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_cli/show.py +0 -0
  51. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_cli/uninstall.py +0 -0
  52. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_cli/util.py +0 -0
  53. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_communication.py +0 -0
  54. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_config.py +0 -0
  55. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_database.py +0 -0
  56. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_miro.py +0 -0
  57. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_model_instance.py +0 -0
  58. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_options.py +0 -0
  59. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_serialization.py +0 -0
  60. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_symbols/__init__.py +0 -0
  61. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_symbols/implicits/__init__.py +0 -0
  62. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_symbols/implicits/implicit_equation.py +0 -0
  63. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_symbols/implicits/implicit_parameter.py +0 -0
  64. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_symbols/implicits/implicit_variable.py +0 -0
  65. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_types.py +0 -0
  66. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/_workspace.py +0 -0
  67. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/exceptions.py +0 -0
  68. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/formulations/__init__.py +0 -0
  69. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/formulations/ml/__init__.py +0 -0
  70. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/formulations/ml/decision_tree_struct.py +0 -0
  71. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/formulations/ml/gradient_boosting.py +0 -0
  72. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/formulations/ml/random_forest.py +0 -0
  73. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/formulations/ml/regression_tree.py +0 -0
  74. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/formulations/nn/__init__.py +0 -0
  75. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/formulations/nn/avgpool2d.py +0 -0
  76. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/formulations/nn/conv1d.py +0 -0
  77. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/formulations/nn/conv2d.py +0 -0
  78. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/formulations/nn/linear.py +0 -0
  79. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/formulations/nn/maxpool2d.py +0 -0
  80. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/formulations/nn/minpool2d.py +0 -0
  81. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/formulations/nn/mpool2d.py +0 -0
  82. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/formulations/nn/torch_sequential.py +0 -0
  83. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/formulations/piecewise.py +0 -0
  84. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/formulations/result.py +0 -0
  85. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/formulations/shape.py +0 -0
  86. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/formulations/utils.py +0 -0
  87. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/math/__init__.py +0 -0
  88. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/math/activation.py +0 -0
  89. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/math/log_power.py +0 -0
  90. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/math/matrix.py +0 -0
  91. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/math/probability.py +0 -0
  92. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/math/trigonometric.py +0 -0
  93. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/py.typed +0 -0
  94. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy/version.py +0 -0
  95. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy.egg-info/dependency_links.txt +0 -0
  96. {gamspy-1.18.1 → gamspy-1.18.3}/src/gamspy.egg-info/entry_points.txt +0 -0
  97. {gamspy-1.18.1 → gamspy-1.18.3}/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.18.1
3
+ Version: 1.18.3
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<53.0.0,>=52.1.0
35
- Requires-Dist: gamspy_base<53.0.0,>=52.1.0
34
+ Requires-Dist: gamsapi<53.0.0,>=52.2.0
35
+ Requires-Dist: gamspy_base<53.0.0,>=52.2.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.18.1"
7
+ version = "1.18.3"
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 >= 52.1.0, < 53.0.0",
40
- "gamspy_base >= 52.1.0, < 53.0.0",
39
+ "gamsapi >= 52.2.0, < 53.0.0",
40
+ "gamspy_base >= 52.2.0, < 53.0.0",
41
41
  "pandas >= 2.2.2, < 2.4",
42
42
  "pydantic >= 2.0",
43
43
  "requests >= 2.28.0",
@@ -90,4 +90,4 @@ class Domain:
90
90
  -------
91
91
  str
92
92
  """
93
- return utils._get_domain_str(self.sets)[1:-1]
93
+ return utils._get_domain_str(self.sets, latex=True)[1:-1]
@@ -234,7 +234,7 @@ def create_latex_expression(root_node: Expression) -> str:
234
234
  if node.right is not None:
235
235
  s1.append(node.right)
236
236
 
237
- # 2. Build the GAMS expression
237
+ # 2. Build the LaTeX expression
238
238
  eval_stack: list[tuple[str, float]] = []
239
239
  for node in reversed(post_order_nodes):
240
240
  if not isinstance(node, Expression):
@@ -96,8 +96,15 @@ class Operation(operable.Operable):
96
96
  """
97
97
  assert self.container is not None
98
98
  temp_name = "a" + utils._get_unique_name()
99
+ domain: list[Set | Alias] = []
100
+ for elem in self.domain:
101
+ if hasattr(elem, "dimension") and elem.dimension > 1:
102
+ domain.extend(elem.domain)
103
+ else:
104
+ domain.append(elem)
105
+
99
106
  temp_param = syms.Parameter._constructor_bypass(
100
- self.container, temp_name, self.domain
107
+ self.container, temp_name, domain
101
108
  )
102
109
  temp_param[...] = self
103
110
  del self.container.data[temp_name]
@@ -756,7 +763,7 @@ class Ord(operable.Operable):
756
763
  -------
757
764
  str
758
765
  """
759
- return f"ord({self._symbol.name})"
766
+ return f"ord({self._symbol._latex_name})"
760
767
 
761
768
 
762
769
  class Card(operable.Operable):
@@ -835,4 +842,4 @@ class Card(operable.Operable):
835
842
  -------
836
843
  str
837
844
  """
838
- return f"card({self._symbol.name})"
845
+ return f"card({self._symbol._latex_name})"
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import typer
4
4
 
5
- from . import gdx, install, list, probe, retrieve, run, show, uninstall
5
+ from . import gdx, install, list, mps2gms, probe, retrieve, run, show, uninstall
6
6
 
7
7
  app = typer.Typer(
8
8
  rich_markup_mode="rich",
@@ -16,6 +16,7 @@ app.add_typer(retrieve.app, name="retrieve")
16
16
  app.add_typer(run.app, name="run")
17
17
  app.add_typer(show.app, name="show")
18
18
  app.add_typer(uninstall.app, name="uninstall")
19
+ app.command(name="mps2gms")(mps2gms.mps2gms)
19
20
 
20
21
 
21
22
  def version_callback(value: bool):
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import platform
5
+ import subprocess
6
+ from pathlib import Path # noqa: TC003
7
+ from typing import Annotated
8
+
9
+ import gamspy_base
10
+ import typer
11
+
12
+ # Valid values for categorical parameters based on help string
13
+ VALID_YN = {"0", "N", "1", "Y"}
14
+ VALID_DUPLICATES = {"NOCHECK", "ADD", "IGNORE", "ERROR"}
15
+ VALID_ORIGNAMES = {"NO", "MODIFIED", "ALL"}
16
+ VALID_CONVERTSENSE = {"0", "N", "1", "Y", "MIN", "-1", "MAX"}
17
+
18
+
19
+ # Completion functions for Typer
20
+ def complete_yn(ctx: typer.Context, incomplete: str):
21
+ return [v for v in VALID_YN if v.lower().startswith(incomplete.lower())]
22
+
23
+
24
+ def complete_duplicates(ctx: typer.Context, incomplete: str):
25
+ return [v for v in VALID_DUPLICATES if v.lower().startswith(incomplete.lower())]
26
+
27
+
28
+ def complete_orignames(ctx: typer.Context, incomplete: str):
29
+ return [v for v in VALID_ORIGNAMES if v.lower().startswith(incomplete.lower())]
30
+
31
+
32
+ def complete_convertsense(ctx: typer.Context, incomplete: str):
33
+ return [v for v in VALID_CONVERTSENSE if v.lower().startswith(incomplete.lower())]
34
+
35
+
36
+ def mps2gms(
37
+ input_file: Annotated[
38
+ Path, typer.Argument(help="MPS or LP file to translate.", exists=True)
39
+ ],
40
+ gdx_file: Annotated[
41
+ Path | None, typer.Argument(help="Name of GDX output file.")
42
+ ] = None,
43
+ gms_file: Annotated[
44
+ Path | None, typer.Argument(help="Name of GAMS program output file.")
45
+ ] = None,
46
+ py_file: Annotated[
47
+ str | None, typer.Option("--py", help="Name of GAMSPy program output file.")
48
+ ] = None,
49
+ dec_file: Annotated[
50
+ str | None, typer.Option("--dec", help="DEC file for decomposition info.")
51
+ ] = None,
52
+ column_int_vars_binary: Annotated[
53
+ str | None,
54
+ typer.Option(
55
+ "--columnintvarsarebinary",
56
+ help="Integer variables appearing first are binary.",
57
+ autocompletion=complete_yn,
58
+ ),
59
+ ] = None,
60
+ duplicates: Annotated[
61
+ str | None,
62
+ typer.Option(
63
+ help="How to handle multiple coefficients.",
64
+ autocompletion=complete_duplicates,
65
+ ),
66
+ ] = None,
67
+ orignames: Annotated[
68
+ str | None,
69
+ typer.Option(
70
+ help="Whether to make original names available.",
71
+ autocompletion=complete_orignames,
72
+ ),
73
+ ] = None,
74
+ stageshift: Annotated[
75
+ int | None, typer.Option(help="Shift block numbers by this integer.")
76
+ ] = None,
77
+ convertsense: Annotated[
78
+ str | None,
79
+ typer.Option(
80
+ help="Convert the objective function sense.",
81
+ autocompletion=complete_convertsense,
82
+ ),
83
+ ] = None,
84
+ ):
85
+ """
86
+ Translates an MPS or LP file into equivalent generic GAMS and GAMSPy programs.
87
+ Defaults to writing .py and .gdx files if no outputs are specified.
88
+ """
89
+ binary_name = "mps2gms.exe" if platform.system() == "Windows" else "mps2gms"
90
+ MPS2GMS_PATH = os.path.join(gamspy_base.directory, binary_name)
91
+
92
+ if not os.path.exists(MPS2GMS_PATH):
93
+ typer.echo(f"Binary not found: {MPS2GMS_PATH}", err=True)
94
+ raise typer.Exit(code=1)
95
+
96
+ # Determine default names based on input stem
97
+ input_name = input_file.with_suffix("")
98
+ actual_gdx = gdx_file if gdx_file else f"{input_name}.gdx"
99
+ actual_py = py_file if py_file else f"{input_name}.py"
100
+
101
+ # mps2gms <input> <gdx>
102
+ cmd = [MPS2GMS_PATH, str(input_file), str(actual_gdx)]
103
+
104
+ # If gms_file is provided, add it as the third positional;
105
+ # otherwise, explicitly disable GMS output to prioritize py/gdx.
106
+ if gms_file:
107
+ cmd.append(str(gms_file))
108
+ else:
109
+ cmd.append("GMS=")
110
+
111
+ # Construct key=value parameters for the binary call
112
+ params = {
113
+ "PY": actual_py,
114
+ "DEC": dec_file,
115
+ "COLUMNINTVARSAREBINARY": column_int_vars_binary,
116
+ "DUPLICATES": duplicates,
117
+ "ORIGNAMES": orignames,
118
+ "STAGESHIFT": stageshift,
119
+ "CONVERTSENSE": convertsense,
120
+ }
121
+
122
+ for key, value in params.items():
123
+ if value is not None:
124
+ cmd.append(f"{key}={value}")
125
+
126
+ print(f"Running command: {' '.join(cmd)}")
127
+ result = subprocess.run(cmd, text=True)
128
+ raise typer.Exit(code=result.returncode)
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import atexit
4
4
  import os
5
5
  import platform
6
+ import re
6
7
  import shutil
7
8
  import signal
8
9
  import sys
@@ -42,6 +43,7 @@ if TYPE_CHECKING:
42
43
  Model,
43
44
  Parameter,
44
45
  Set,
46
+ UniverseAlias,
45
47
  Variable,
46
48
  )
47
49
  from gamspy._algebra.expression import Expression
@@ -139,6 +141,7 @@ class Container(gt.Container):
139
141
  self.output = output
140
142
  self._gams_string = ""
141
143
  self.models: dict[str, Model] = {}
144
+ self._mpsge_models: list[str] = []
142
145
  if IS_MIRO_INIT:
143
146
  atexit.register(self._write_miro_files)
144
147
 
@@ -838,6 +841,14 @@ class Container(gt.Container):
838
841
 
839
842
  for _, symbol in self:
840
843
  symbol.modified = False
844
+
845
+ # Unfortunately MPSGE requires a dirty trick
846
+ pattern = re.compile(r"^\$sysInclude\s+mpsgeset\s+(\w+)\s*$", re.MULTILINE)
847
+ match = pattern.search(gams_code)
848
+ if match:
849
+ model_name = match.group(1)
850
+ self._mpsge_models.append(model_name.lower())
851
+
841
852
  self._unsaved_statements = []
842
853
 
843
854
  def close(self) -> None:
@@ -888,6 +899,37 @@ class Container(gt.Container):
888
899
 
889
900
  return gp.Alias(self, name, alias_with)
890
901
 
902
+ def addUniverseAlias(self, name: str | None = None) -> UniverseAlias:
903
+ """
904
+ Creates a new UniverseAlias and adds it to the container
905
+
906
+ Parameters
907
+ ----------
908
+ name : str, optional
909
+ Name of the universe alias.
910
+
911
+ Returns
912
+ -------
913
+ UniverseAlias
914
+
915
+ Raises
916
+ ------
917
+ ValueError
918
+ If there is symbol with same name but different type in the
919
+ Container
920
+
921
+ Examples
922
+ --------
923
+ >>> import gamspy as gp
924
+ >>> m = gp.Container()
925
+ >>> a = m.addUniverseAlias("a")
926
+
927
+ """
928
+ if name is None:
929
+ name = self._get_symbol_name(prefix="u")
930
+
931
+ return gp.UniverseAlias(self, name)
932
+
891
933
  def addSet(
892
934
  self,
893
935
  name: str | None = None,
@@ -1099,7 +1141,7 @@ class Container(gt.Container):
1099
1141
  self,
1100
1142
  name: str | None = None,
1101
1143
  type: str | EquationType = "regular",
1102
- domain: Sequence[Set | Alias | str] | Set | Alias | str | None = None,
1144
+ domain: Sequence[Set | Alias] | Set | Alias | None = None,
1103
1145
  definition: Variable | Operation | Expression | None = None,
1104
1146
  records: Any | None = None,
1105
1147
  domain_forwarding: bool | list[bool] = False,
@@ -1117,7 +1159,7 @@ class Container(gt.Container):
1117
1159
  Name of the equation. Name is autogenerated by default.
1118
1160
  type : str
1119
1161
  Type of the equation. "regular" by default.
1120
- domain : Sequence[Set | Alias | str] | Set | Alias | str, optional
1162
+ domain : Sequence[Set | Alias] | Set | Alias, optional
1121
1163
  Domain of the variable.
1122
1164
  definition: Expression, optional
1123
1165
  Definition of the equation.
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import logging
4
3
  import os
5
4
  import subprocess
6
5
  from collections.abc import Sequence
@@ -28,14 +27,6 @@ if TYPE_CHECKING:
28
27
 
29
28
  SymbolType: TypeAlias = Alias | Set | Parameter | Variable | Equation
30
29
 
31
- logger = logging.getLogger("CONVERTER")
32
- logger.setLevel(logging.INFO)
33
- stream_handler = logging.StreamHandler()
34
- stream_handler.setLevel(logging.INFO)
35
- formatter = logging.Formatter("[%(name)s - %(levelname)s] %(message)s")
36
- stream_handler.setFormatter(formatter)
37
- logger.addHandler(stream_handler)
38
-
39
30
  GAMS_JACOBIAN_TEMPLATE = """$onEmpty
40
31
  Set
41
32
  i 'equations index'
@@ -388,9 +379,9 @@ class GamsConverter:
388
379
  self.path = path
389
380
  self.options = options
390
381
  self.dump_gams_state = dump_gams_state
391
- self.gdx_path = str((path / f"{model.name}_data.gdx").resolve())
392
- self.gms_path = str((path / f"{model.name}.gms").resolve())
393
- self.g00_path = str((path / f"{model.name}.g00").resolve())
382
+ self.gdx_path = path / f"{model.name}_data.gdx"
383
+ self.gms_path = path / f"{model.name}.gms"
384
+ self.g00_path = path / f"{model.name}.g00"
394
385
 
395
386
  def get_definitions(self) -> list[str]:
396
387
  definitions = []
@@ -438,8 +429,8 @@ class GamsConverter:
438
429
  em_name = self.model._external_module
439
430
  em_file = self.model._external_module_file
440
431
  declarations.append(f"File {em_file} /{em_name}/;")
441
- logger.info("Converter will not copy external module files")
442
- logger.info(
432
+ print("Converter will not copy external module files")
433
+ print(
443
434
  f"You need to ensure your external module is accessible from {self.path}"
444
435
  )
445
436
 
@@ -482,7 +473,9 @@ class GamsConverter:
482
473
  with open(self.gms_path, "w", encoding="utf-8") as file:
483
474
  file.write(gams_string)
484
475
 
485
- logger.info(f"GAMS (.gms) file has been generated under {self.gms_path}")
476
+ print("=" * 80)
477
+ print(f"GAMS (.gms) file has been generated under {self.gms_path}")
478
+ print("=" * 80)
486
479
 
487
480
 
488
481
  TABLE_HEADER = """\\begin{tabularx}{\\textwidth}{| l | l | X |}
@@ -496,7 +489,7 @@ TABLE_FOOTER = "\\hline\n\\end{tabularx}"
496
489
 
497
490
 
498
491
  class LatexConverter:
499
- def __init__(self, model: Model, path: Path) -> None:
492
+ def __init__(self, model: Model, path: Path, rename: dict[str, str] | None) -> None:
500
493
  os.makedirs(path, exist_ok=True)
501
494
  self.model = model
502
495
  self.container = model.container
@@ -512,11 +505,21 @@ class LatexConverter:
512
505
  ):
513
506
  symbols.append(elem.name)
514
507
 
508
+ for name in self.model._autogen_symbols:
509
+ if name in symbols:
510
+ symbols.remove(name)
511
+
515
512
  self.symbols = sorted(symbols, key=list(self.container.data.keys()).index)
516
513
 
517
- for name in self.model._autogen_symbols:
518
- if name in self.symbols:
519
- self.symbols.remove(name)
514
+ if rename is not None:
515
+ for name, latex_name in rename.items():
516
+ try:
517
+ symbol = self.container[name]
518
+ symbol._latex_name = latex_name
519
+ except KeyError as e:
520
+ raise KeyError(
521
+ f"`{name}` does not exist in the Container. You can only rename symbols that already exist in the container."
522
+ ) from e
520
523
 
521
524
  self.header = self.get_header()
522
525
  self.set_header = "\\subsection*{Sets}"
@@ -551,8 +554,9 @@ class LatexConverter:
551
554
  if self.model._objective is None:
552
555
  ...
553
556
  elif isinstance(self.model._objective, syms.Variable):
557
+ var_name = self.model._objective._latex_name
554
558
  latex_strs.append(
555
- f"\\textbf{{{str(self.model.sense).lower()}}} ${self.model._objective_variable.name}$\\\\" # type: ignore
559
+ f"\\textbf{{{str(self.model.sense).lower()}}} ${var_name}$\\\\"
556
560
  )
557
561
  latex_strs.append("\\textbf{s.t.}")
558
562
  else:
@@ -572,26 +576,35 @@ class LatexConverter:
572
576
  with open(self.tex_path, "w", encoding="utf-8") as file: # Write the TEX file
573
577
  file.write(latex_str)
574
578
 
575
- logger.info(
579
+ print("=" * 80)
580
+ print(
576
581
  f"LaTeX (.tex) file has been generated under {os.path.join(self.path, self.model.name + '.tex')}"
577
582
  )
583
+ print("=" * 80)
578
584
 
579
585
  self.latex_str = latex_str
580
586
 
581
587
  def to_pdf(self) -> None:
582
- process = subprocess.run(["pdflatex", "-v"])
583
- if process.returncode:
588
+ try:
589
+ subprocess.run(["xelatex", "-version"], check=True, text=True)
590
+ except subprocess.CalledProcessError as e:
584
591
  raise ValidationError(
585
- "`pdflatex` is required to generate the pdf! Please install `pdflatex` and add it to the path."
592
+ "`xelatex` is required to generate the pdf! Please install `xelatex` and add it to the path."
593
+ ) from e
594
+
595
+ try:
596
+ subprocess.run(
597
+ [
598
+ "xelatex",
599
+ "-verbose",
600
+ f"-output-directory={self.path}",
601
+ self.tex_path,
602
+ ],
603
+ check=True,
604
+ text=True,
586
605
  )
587
-
588
- process = subprocess.run(
589
- ["pdflatex", f"-output-directory={self.path}", self.tex_path],
590
- capture_output=True,
591
- text=True,
592
- )
593
- if process.returncode:
594
- raise LatexException(f"Could not generate pdf file: {process.stderr}")
606
+ except subprocess.CalledProcessError as e:
607
+ raise LatexException(f"Could not generate pdf file: {e}") from e
595
608
 
596
609
  def get_table(self, symbol_type) -> str:
597
610
  table = [TABLE_HEADER]
@@ -604,10 +617,11 @@ class LatexConverter:
604
617
  if isinstance(symbol, syms.Variable) and self.model._limited_variables:
605
618
  for elem in self.model._limited_variables:
606
619
  if elem.name == symbol.name:
607
- domain_str = utils._get_domain_str(elem.domain)[1:-1]
620
+ domain_str = utils._get_domain_str(elem.domain, latex=True)[
621
+ 1:-1
622
+ ]
608
623
 
609
- row = f"{summary['name']} & {domain_str} & {summary['description']}\\\\"
610
- row = row.replace("_", "\\_")
624
+ row = f"{symbol._latex_name} & {domain_str} & {summary['description']}\\\\"
611
625
  table.append(row)
612
626
 
613
627
  table.append(TABLE_FOOTER)
@@ -615,16 +629,16 @@ class LatexConverter:
615
629
  return "\n".join(table)
616
630
 
617
631
  def get_definitions(self) -> str:
618
- definitions = []
632
+ definitions: list[str] = []
619
633
  for equation in self.model.equations:
620
634
  if equation.name in self.model._autogen_symbols:
621
635
  continue
622
636
 
623
- domain_str = ",".join([elem.name for elem in equation.domain])
624
- header = "\\subsubsection*{$" + equation.name.replace("_", "\\_")
637
+ domain_str = ",".join([elem.latexRepr() for elem in equation.domain])
638
+ header = "\\subsubsection*{" + equation._latex_name
625
639
  if domain_str:
626
- header += f"_{{{domain_str}}}"
627
- header += "$}\n"
640
+ header += f"$_{{{domain_str}}}$"
641
+ header += "}\n"
628
642
 
629
643
  footer = "\n\\vspace{5pt}\n\\hrule"
630
644
  latex_repr = f"{header}{equation.latexRepr()}{footer}"
@@ -633,28 +647,41 @@ class LatexConverter:
633
647
  return "\n".join(definitions)
634
648
 
635
649
  def get_constraints(self) -> str:
636
- constraints = ["\\bigskip"]
650
+ constraints = [r"\begin{flalign*}"]
637
651
  for name in self.symbols:
638
652
  symbol = self.container[name]
639
653
  if not isinstance(symbol, syms.Variable):
640
654
  continue
641
655
 
642
- constraint = "$" + symbol.latexRepr()
656
+ constraint = "&" + symbol.latexRepr()
643
657
  if symbol.type == "binary":
644
- constraint += (
645
- "\\in "
646
- + r"\{0,1\}"
647
- + " ~ \\forall "
648
- + ",".join(symbol.domain_names)
649
- )
658
+ constraint += "\\in " + r"\{0,1\}"
659
+ if symbol.domain:
660
+ constraint += (
661
+ " ~ \\forall "
662
+ + utils._get_domain_str(symbol.domain, latex=True)[1:-1]
663
+ )
650
664
  elif symbol.type == "integer":
651
- constraint += "\\in \\mathbb{Z}_{+} \\forall " + ",".join(
652
- symbol.domain_names
653
- )
665
+ constraint += "\\in \\mathbb{Z}_{+}"
666
+ if symbol.domain:
667
+ constraint += (
668
+ " ~ \\forall "
669
+ + utils._get_domain_str(symbol.domain, latex=True)[1:-1]
670
+ )
654
671
  elif symbol.type == "positive":
655
- constraint += "\\geq 0 ~ \\forall " + ",".join(symbol.domain_names)
672
+ constraint += "\\geq 0"
673
+ if symbol.domain:
674
+ constraint += (
675
+ " ~ \\forall "
676
+ + utils._get_domain_str(symbol.domain, latex=True)[1:-1]
677
+ )
656
678
  elif symbol.type == "negative":
657
- constraint += "\\leq 0 ~ \\forall " + ",".join(symbol.domain_names)
679
+ constraint += "\\leq 0"
680
+ if symbol.domain:
681
+ constraint += (
682
+ " ~ \\forall "
683
+ + utils._get_domain_str(symbol.domain, latex=True)[1:-1]
684
+ )
658
685
  elif symbol.type == "sos1":
659
686
  constraint += "SOS1"
660
687
  elif symbol.type == "sos2":
@@ -666,24 +693,28 @@ class LatexConverter:
666
693
  else:
667
694
  continue
668
695
 
669
- constraint += "\\\\$"
696
+ constraint += "&\\\\"
670
697
  constraints.append(constraint)
671
698
 
699
+ constraints.append(r"\end{flalign*}")
672
700
  return "\n".join(constraints)
673
701
 
674
702
  def get_header(self) -> str:
675
- header = """\\documentclass[11pt]{article}
676
- \\usepackage{geometry}
677
- \\usepackage[american]{babel}
678
- \\usepackage{amsmath}
679
- \\usepackage{amssymb}
680
- \\usepackage[hidelinks]{hyperref}
681
- \\usepackage{tabularx}
682
- \\usepackage{ltablex}
683
- \\keepXColumns
684
-
685
- \\begin{document}
686
- \\section*{Symbols}
703
+ header = r"""\documentclass[11pt]{article}
704
+ \usepackage{geometry}
705
+ \usepackage[american]{babel}
706
+ \usepackage{amsmath}
707
+ \usepackage{amssymb}
708
+ \usepackage{fontspec}
709
+ \setmainfont{CMU Serif} % 1. Sets a font that knows Greek in text mode
710
+ \usepackage{unicode-math} % 2. Allows Greek keys to work in math mode
711
+ \usepackage[hidelinks]{hyperref}
712
+ \usepackage{tabularx}
713
+ \usepackage{ltablex}
714
+ \keepXColumns
715
+
716
+ \begin{document}
717
+ \section*{Symbols}
687
718
 
688
719
  """
689
720
  return header
@@ -120,7 +120,23 @@ class ExtrinsicFunction(operable.Operable):
120
120
  -------
121
121
  str
122
122
  """
123
- return self.gamsRepr()
123
+ representation = self.name.replace("_", r"\_")
124
+
125
+ if self.args:
126
+ arg_strs = []
127
+ for arg in self.args:
128
+ arg_str = (
129
+ str(arg).replace("_", r"\_")
130
+ if isinstance(arg, (int, float, str))
131
+ else arg.latexRepr()
132
+ )
133
+ arg_strs.append(arg_str)
134
+
135
+ args = ",".join(arg_strs)
136
+
137
+ representation = f"{representation}({args})"
138
+
139
+ return representation
124
140
 
125
141
 
126
142
  class ExtrinsicLibrary: