gamspy 1.18.2__py3-none-any.whl → 1.18.4__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/_container.py CHANGED
@@ -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
@@ -101,28 +103,76 @@ def get_options_file_name(solver: str, file_number: int) -> str:
101
103
 
102
104
  class Container(gt.Container):
103
105
  """
104
- A container is an object that holds all symbols and operates on them.
106
+ Central workspace for building, modifying, executing, and exchanging data
107
+ with GAMS models.
108
+
109
+ https://gamspy.readthedocs.io/en/latest/reference/gamspy._container.html
110
+
111
+
112
+ A :class:`Container` owns all GAMSPy symbols (sets, parameters, variables,
113
+ equations, models) and manages synchronization with the GAMS execution
114
+ engine.
115
+
116
+
117
+ A container can:
118
+
119
+
120
+ * Create and store symbols
121
+ * Load symbols and records from GDX/G00 files
122
+ * Synchronize modified symbols with GAMS
123
+ * Generate and persist GAMS code and data files
124
+ * Act as a context manager.
105
125
 
106
126
  Parameters
107
127
  ----------
108
128
  load_from : str, os.PathLike, Container, gt.Container, optional
109
- Path to the GDX file to be loaded from, by default None
129
+ Source to initialize the container from:
130
+
131
+ * ``.gdx`` file: loads symbols and records from a GDX file.
132
+ * ``.g00`` file: restarts from a GAMS save file.
133
+ * Container: Copies symbols from the given container into the new container.
110
134
  system_directory : str, os.PathLike, optional
111
135
  Path to the directory that holds the GAMS installation, by default None
112
136
  working_directory : str, os.PathLike, optional
113
- Path to the working directory to store temporary files such .lst, .gms,
114
- .gdx, .g00 files.
115
- debugging_level : str, optional
116
- Decides on keeping the temporary files generate by GAMS, by default
117
- "keep_on_error"
137
+ Directory used for temporary files (``.gms``, ``.lst``, ``.gdx``).
138
+ If omitted, a temporary directory is created.
139
+ debugging_level : {"keep", "keep_on_error", "delete"}, optional
140
+ Controls whether temporary files are retained:
141
+
142
+
143
+ * ``"keep"``: Keep files and generated GAMS code.
144
+ * ``"keep_on_error"``: Keep files only on errors (default).
145
+ * ``"delete"``: Always clean up.
118
146
  options : Options, optional
119
147
  Global options for the overall execution
148
+ output : io.TextIOWrapper, optional
149
+ Stream to which GAMS output is written.
120
150
 
121
151
  Examples
122
152
  --------
153
+ Basic usage
154
+
155
+
123
156
  >>> import gamspy as gp
124
157
  >>> m = gp.Container()
125
- >>> i = gp.Set(m, "i")
158
+ >>> i = gp.Set(m, "i", records=["i1", "i2"])
159
+
160
+
161
+ Loading from an existing GDX
162
+
163
+
164
+ >>> m.write("data.gdx")
165
+ >>> m2 = gp.Container(load_from="data.gdx")
166
+ >>> list(m2.data.keys()) == list(m.data.keys())
167
+ True
168
+
169
+
170
+ Using as a context manager
171
+
172
+
173
+ >>> with gp.Container() as m:
174
+ ... i = gp.Set(m, "i")
175
+ ... j = gp.Set(m, "j", domain=i)
126
176
 
127
177
  """
128
178
 
@@ -139,6 +189,7 @@ class Container(gt.Container):
139
189
  self.output = output
140
190
  self._gams_string = ""
141
191
  self.models: dict[str, Model] = {}
192
+ self._mpsge_models: list[str] = []
142
193
  if IS_MIRO_INIT:
143
194
  atexit.register(self._write_miro_files)
144
195
 
@@ -241,26 +292,51 @@ class Container(gt.Container):
241
292
  @property
242
293
  def working_directory(self) -> str:
243
294
  """
244
- Working directory path.
295
+ Absolute path of the working directory used by this container.
296
+
297
+
298
+ The directory contains generated GAMS input/output files such as
299
+ ``.gms``, ``.lst``, ``.gdx`` and temporary scratch files.
300
+
245
301
 
246
302
  Returns
247
303
  -------
248
304
  str
305
+
306
+
307
+ Examples
308
+ --------
309
+ >>> import gamspy as gp
310
+ >>> m = gp.Container()
311
+ >>> m.working_directory # doctest: +ELLIPSIS
312
+ '...'
313
+
249
314
  """
250
315
  return self._working_directory
251
316
 
252
317
  @property
253
318
  def in_miro(self) -> bool:
254
319
  """
255
- When running a GAMSPy job from GAMS MIRO, you may not want to
256
- perform certain expensive operations, such as loading MIRO input
257
- data from an Excel workbook, as this data comes from MIRO. In that
258
- case, one can conditionally load the data by using the ``in_miro``
259
- attribute of `Container`.
320
+ Indicates whether the container is executed inside a GAMS MIRO context.
321
+
322
+
323
+ When running under MIRO, input data is typically provided by MIRO itself.
324
+ This flag can be used to skip expensive or redundant data-loading steps
325
+ (e.g., reading Excel files).
326
+
260
327
 
261
328
  Returns
262
329
  -------
263
330
  bool
331
+
332
+
333
+ Examples
334
+ --------
335
+ >>> import gamspy as gp
336
+ >>> m = gp.Container()
337
+ >>> if not m.in_miro:
338
+ ... pass # e.g. load data from files
339
+
264
340
  """
265
341
  return MIRO_GDX_IN is not None
266
342
 
@@ -563,28 +639,51 @@ class Container(gt.Container):
563
639
  encoding: str | None = None,
564
640
  ) -> None:
565
641
  """
566
- Reads specified symbols from the GDX file. If symbol_names are
567
- not provided, it reads all symbols from the GDX file.
642
+ Read symbols and records from a GDX file or another container.
643
+
568
644
 
569
645
  Parameters
570
646
  ----------
571
- load_from : str, os.PathLike, Container, gt.Container
572
- symbol_names : List[str], optional
573
- load_records : bool
647
+ load_from : str | os.PathLike | Container | gt.Container
648
+ Source to read from.
649
+
650
+
651
+ symbol_names : list[str], optional
652
+ Names of symbols to read. If omitted, all symbols are read.
653
+
654
+
655
+ load_records : bool, optional
656
+ Whether to load symbol records (default: True).
657
+
658
+
574
659
  mode : str, optional
660
+ GDX read mode ("category", or "string", default: "category").
661
+
662
+
575
663
  encoding : str, optional
664
+ Text encoding for symbol metadata.
665
+
576
666
 
577
667
  Examples
578
668
  --------
579
669
  >>> import gamspy as gp
580
670
  >>> m = gp.Container()
581
- >>> i = gp.Set(m, "i", records=['i1', 'i2'])
582
- >>> m.write("test.gdx")
583
- >>> new_container = gp.Container()
584
- >>> new_container.read("test.gdx")
585
- >>> new_container.data.keys() == m.data.keys()
671
+ >>> i = gp.Set(m, "i", records=["a", "b"])
672
+ >>> m.write("example.gdx")
673
+
674
+
675
+ >>> m2 = gp.Container()
676
+ >>> m2.read("example.gdx")
677
+ >>> "i" in m2.data
586
678
  True
587
679
 
680
+
681
+ Reading selected symbols only
682
+
683
+
684
+ >>> m2 = gp.Container()
685
+ >>> m2.read("example.gdx", symbol_names=["i"])
686
+
588
687
  """
589
688
  if isinstance(load_from, os.PathLike):
590
689
  load_from = os.fspath(load_from)
@@ -599,22 +698,37 @@ class Container(gt.Container):
599
698
  uels_on_axes: bool | list[bool] = False,
600
699
  ) -> None:
601
700
  """
602
- Batched setRecords call where one can set the records of many symbols at once.
701
+ Set records for multiple symbols in a single batch operation.
702
+
703
+
704
+ This is functionally equivalent to calling ``symbol.setRecords`` for
705
+ each symbol, but triggers only one synchronization with GAMS.
706
+
603
707
 
604
708
  Parameters
605
709
  ----------
606
710
  records : dict[SymbolType, Any]
607
- Dictionary of records where keys are symbols are values are their records.
608
- uels_on_axes : bool, optional
609
- If uels_on_axes=True setRecords will assume that all domain information is contained in the axes of the pandas object. Data will be flattened (if necessary).
711
+ Mapping from symbols to their new records.
712
+
713
+
714
+ uels_on_axes : bool | list[bool], optional
715
+ Whether domain labels (UELs) are stored on pandas axes. Either a single
716
+ boolean applied to all symbols, or a list matching ``records`` order.
717
+
718
+
719
+ Raises
720
+ ------
721
+ ValidationError
722
+ If ``uels_on_axes`` length does not match ``records`` length.
723
+
610
724
 
611
725
  Examples
612
726
  --------
613
727
  >>> import gamspy as gp
614
728
  >>> m = gp.Container()
615
729
  >>> i = gp.Set(m, "i")
616
- >>> k = gp.Set(m, "k")
617
- >>> m.setRecords({i: range(10), k: range(5)})
730
+ >>> j = gp.Set(m, "j")
731
+ >>> m.setRecords({i: ["a", "b"], j: [1, 2, 3]})
618
732
 
619
733
  """
620
734
  if not isinstance(uels_on_axes, list):
@@ -640,23 +754,37 @@ class Container(gt.Container):
640
754
  eps_to_zero: bool = True,
641
755
  ) -> None:
642
756
  """
643
- Writes specified symbols to the GDX file. If symbol_names are
644
- not provided, it writes all symbols to the GDX file.
757
+ Write symbols and records to a GDX file.
758
+
645
759
 
646
760
  Parameters
647
761
  ----------
648
762
  write_to : str
649
- symbol_names : List[str], optional
650
- compress : bool
763
+ Target GDX file path.
764
+
765
+
766
+ symbol_names : list[str], optional
767
+ Symbols to write. If omitted, all symbols are written.
768
+
769
+
770
+ compress : bool, optional
771
+ Whether to compress the GDX file.
772
+
773
+
651
774
  mode : str, optional
652
- eps_to_zero : bool
775
+ Write mode (passed to GAMS Transfer).
776
+
777
+
778
+ eps_to_zero : bool, optional
779
+ Convert EPS values to zero before writing.
780
+
653
781
 
654
782
  Examples
655
783
  --------
656
784
  >>> import gamspy as gp
657
785
  >>> m = gp.Container()
658
- >>> i = gp.Set(m, "i", records=['i1', 'i2'])
659
- >>> m.write("test.gdx")
786
+ >>> i = gp.Set(m, "i", records=["x", "y"])
787
+ >>> m.write("out.gdx")
660
788
 
661
789
  """
662
790
  super().write(
@@ -733,26 +861,42 @@ class Container(gt.Container):
733
861
  self, path: str | None = None, *, show_raw: bool = False
734
862
  ) -> str:
735
863
  """
736
- Generates the GAMS code
864
+ Return the generated GAMS code executed by this container.
865
+
866
+
867
+ Only available when ``debugging_level='keep'`` was specified at
868
+ container creation time.
869
+
737
870
 
738
871
  Parameters
739
872
  ----------
740
873
  path : str, optional
741
- Path to the file that will contain the executed GAMS code.
874
+ File path to write the generated GAMS code to.
875
+
876
+
742
877
  show_raw : bool, optional
743
- Shows the raw model without data and other necessary
744
- GAMS statements, by default False.
878
+ If True, strips data-loading and auxiliary statements, leaving
879
+ the core model formulation.
880
+
745
881
 
746
882
  Returns
747
883
  -------
748
884
  str
885
+ Generated GAMS code.
886
+
887
+
888
+ Raises
889
+ ------
890
+ ValidationError
891
+ If debugging level is not ``"keep"``.
892
+
749
893
 
750
894
  Examples
751
895
  --------
752
896
  >>> import gamspy as gp
753
897
  >>> m = gp.Container(debugging_level="keep")
754
- >>> i = gp.Set(m, name="i")
755
- >>> gams_code = m.generateGamsString()
898
+ >>> i = gp.Set(m, "i")
899
+ >>> gams = m.generateGamsString()
756
900
 
757
901
  """
758
902
  if self._debugging_level != "keep":
@@ -838,13 +982,31 @@ class Container(gt.Container):
838
982
 
839
983
  for _, symbol in self:
840
984
  symbol.modified = False
985
+
986
+ # Unfortunately MPSGE requires a dirty trick
987
+ pattern = re.compile(r"^\$sysInclude\s+mpsgeset\s+(\w+)\s*$", re.MULTILINE)
988
+ match = pattern.search(gams_code)
989
+ if match:
990
+ model_name = match.group(1)
991
+ self._mpsge_models.append(model_name.lower())
992
+
841
993
  self._unsaved_statements = []
842
994
 
843
995
  def close(self) -> None:
844
996
  """
845
- Stops the socket and releases resources. The container should not be used afterwards
846
- to communicate with the GAMS execution engine, e.g. creating new symbols, changing data,
847
- solves, etc. The container data (Container.data) is still available for read operations.
997
+ Close the connection to the GAMS execution engine and release resources.
998
+
999
+ After calling this method, the container must not be used for further
1000
+ model execution or data synchronization. Symbol data remains accessible
1001
+ for read-only inspection.
1002
+
1003
+
1004
+ Examples
1005
+ --------
1006
+ >>> import gamspy as gp
1007
+ >>> m = gp.Container() # Starts running the GAMS execution engine.
1008
+ >>> m.close() # Closes the connection to the execution engine.
1009
+
848
1010
  """
849
1011
  close_connection(self._comm_pair_id)
850
1012
 
@@ -888,6 +1050,37 @@ class Container(gt.Container):
888
1050
 
889
1051
  return gp.Alias(self, name, alias_with)
890
1052
 
1053
+ def addUniverseAlias(self, name: str | None = None) -> UniverseAlias:
1054
+ """
1055
+ Creates a new UniverseAlias and adds it to the container
1056
+
1057
+ Parameters
1058
+ ----------
1059
+ name : str, optional
1060
+ Name of the universe alias.
1061
+
1062
+ Returns
1063
+ -------
1064
+ UniverseAlias
1065
+
1066
+ Raises
1067
+ ------
1068
+ ValueError
1069
+ If there is symbol with same name but different type in the
1070
+ Container
1071
+
1072
+ Examples
1073
+ --------
1074
+ >>> import gamspy as gp
1075
+ >>> m = gp.Container()
1076
+ >>> a = m.addUniverseAlias("a")
1077
+
1078
+ """
1079
+ if name is None:
1080
+ name = self._get_symbol_name(prefix="u")
1081
+
1082
+ return gp.UniverseAlias(self, name)
1083
+
891
1084
  def addSet(
892
1085
  self,
893
1086
  name: str | None = None,
@@ -901,22 +1094,22 @@ class Container(gt.Container):
901
1094
  is_miro_output: bool = False,
902
1095
  ) -> Set:
903
1096
  """
904
- Creates a Set and adds it to the container
1097
+ Creates a Set and adds it to the container.
905
1098
 
906
1099
  Parameters
907
1100
  ----------
908
1101
  name : str, optional
909
- Name of the set. Name is autogenerated by default.
1102
+ Name of the set. If omitted, a unique name is generated.
910
1103
  domain : Sequence[Set | Alias | str] | Set | Alias | str, optional
911
- Domain of the set.
1104
+ Domain over which the set is defined.
912
1105
  is_singleton : bool, optional
913
- Whether the set is a singleton set. Singleton sets cannot contain more than one element.
1106
+ If True, the set may contain at most one element.
914
1107
  records : pd.DataFrame | np.ndarray | list, optional
915
- Records of the set.
1108
+ Initial elements of the set.
916
1109
  domain_forwarding : bool | list[bool], optional
917
- Whether the set forwards the domain.
1110
+ Enable domain forwarding.
918
1111
  description : str, optional
919
- Description of the set.
1112
+ Human-readable description.
920
1113
  uels_on_axes : bool
921
1114
  Assume that symbol domain information is contained in the axes of the given records.
922
1115
  is_miro_input : bool
@@ -938,9 +1131,24 @@ class Container(gt.Container):
938
1131
 
939
1132
  Examples
940
1133
  --------
1134
+ Simple set:
1135
+
1136
+
941
1137
  >>> import gamspy as gp
942
1138
  >>> m = gp.Container()
943
- >>> i = m.addSet("i")
1139
+ >>> i = m.addSet("i", records=["a", "b"])
1140
+
1141
+
1142
+ Indexed set:
1143
+
1144
+
1145
+ >>> j = m.addSet("j", domain=i)
1146
+
1147
+
1148
+ Singleton set:
1149
+
1150
+
1151
+ >>> s = m.addSet("s", is_singleton=True, records=["s1"])
944
1152
 
945
1153
  """
946
1154
  if name is None:
@@ -977,15 +1185,15 @@ class Container(gt.Container):
977
1185
  Parameters
978
1186
  ----------
979
1187
  name : str, optional
980
- Name of the parameter. Name is autogenerated by default.
1188
+ Name of the parameter. If omitted, a unique name is generated.
981
1189
  domain : Sequence[Set | Alias | str] | Set | Alias | Dim | str, optional
982
- Domain of the parameter.
1190
+ Domain over which the parameter is defined.
983
1191
  records : int | float | pd.DataFrame | np.ndarray | list, optional
984
1192
  Records of the parameter.
985
1193
  domain_forwarding : bool | list[bool], optional
986
1194
  Whether the parameter forwards the domain.
987
1195
  description : str, optional
988
- Description of the parameter.
1196
+ Human-readable description.
989
1197
  uels_on_axes : bool
990
1198
  Assume that symbol domain information is contained in the axes of the given records.
991
1199
  is_miro_input : bool
@@ -1047,7 +1255,7 @@ class Container(gt.Container):
1047
1255
  Parameters
1048
1256
  ----------
1049
1257
  name : str, optional
1050
- Name of the variable. Name is autogenerated by default.
1258
+ Name of the variable. If omitted, a unique name is generated.
1051
1259
  type : str, optional
1052
1260
  Type of the variable. "free" by default.
1053
1261
  domain : Sequence[Set | Alias | str] | Set | Alias | Dim | str, optional
@@ -1114,7 +1322,7 @@ class Container(gt.Container):
1114
1322
  Parameters
1115
1323
  ----------
1116
1324
  name : str, optional
1117
- Name of the equation. Name is autogenerated by default.
1325
+ Name of the equation. If omitted, a unique name is generated.
1118
1326
  type : str
1119
1327
  Type of the equation. "regular" by default.
1120
1328
  domain : Sequence[Set | Alias] | Set | Alias, optional
@@ -1192,7 +1400,7 @@ class Container(gt.Container):
1192
1400
  Parameters
1193
1401
  ----------
1194
1402
  name : str, optional
1195
- Name of the model. Name is autogenerated by default.
1403
+ Name of the model. If omitted, a unique name is generated.
1196
1404
  description : str, optional
1197
1405
  Description of the model.
1198
1406
  equations : Sequence[Equation]
@@ -1219,8 +1427,9 @@ class Container(gt.Container):
1219
1427
  --------
1220
1428
  >>> import gamspy as gp
1221
1429
  >>> m = gp.Container()
1222
- >>> e = gp.Equation(m, "e")
1223
- >>> model = m.addModel("my_model", "LP", [e])
1430
+ >>> x = gp.Variable(m, "x")
1431
+ >>> e = gp.Equation(m, "e", definition=x >= 1)
1432
+ >>> model = m.addModel(name="demo", equations=[e], problem="LP", sense="MIN", objective=x)
1224
1433
 
1225
1434
  """
1226
1435
  if name is None:
@@ -1295,6 +1504,14 @@ class Container(gt.Container):
1295
1504
  ----------
1296
1505
  path : str
1297
1506
  Path to the zip file.
1507
+
1508
+ Examples
1509
+ --------
1510
+ >>> import gamspy as gp
1511
+ >>> m = gp.Container()
1512
+ >>> i = gp.Set(m, "i", records=range(3))
1513
+ >>> gp.serialize(m, "serialization_path.zip")
1514
+
1298
1515
  """
1299
1516
  gp.serialize(self, path)
1300
1517
 
@@ -1344,6 +1561,15 @@ class Container(gt.Container):
1344
1561
  ------
1345
1562
  FileNotFoundError
1346
1563
  In case the extrinsic library does not exist in the given path.
1564
+
1565
+ Examples
1566
+ --------
1567
+ >>> import gamspy as gp
1568
+ >>> m = gp.Container()
1569
+ >>> # Assuming 'trilib.so' is a valid library path
1570
+ >>> # lib = m.importExtrinsicLibrary("trilib.so", {"my_cos": "Cosine", "my_sin": "Sine"})
1571
+ >>> # c = lib.my_cos(0)
1572
+
1347
1573
  """
1348
1574
  if not os.path.exists(lib_path):
1349
1575
  raise FileNotFoundError(f"`{lib_path}` is not a valid path.")
gamspy/_convert.py CHANGED
@@ -585,20 +585,26 @@ class LatexConverter:
585
585
  self.latex_str = latex_str
586
586
 
587
587
  def to_pdf(self) -> None:
588
- process = subprocess.run(["pdflatex", "-v"], capture_output=True)
589
- if process.returncode:
590
- print(process.stderr)
588
+ try:
589
+ subprocess.run(["xelatex", "-version"], check=True, text=True)
590
+ except subprocess.CalledProcessError as e:
591
591
  raise ValidationError(
592
- "`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,
593
605
  )
594
-
595
- process = subprocess.run(
596
- ["pdflatex", f"-output-directory={self.path}", self.tex_path],
597
- capture_output=True,
598
- text=True,
599
- )
600
- if process.returncode:
601
- 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
602
608
 
603
609
  def get_table(self, symbol_type) -> str:
604
610
  table = [TABLE_HEADER]
@@ -629,10 +635,10 @@ class LatexConverter:
629
635
  continue
630
636
 
631
637
  domain_str = ",".join([elem.latexRepr() for elem in equation.domain])
632
- header = "\\subsubsection*{$" + equation._latex_name
638
+ header = "\\subsubsection*{" + equation._latex_name
633
639
  if domain_str:
634
- header += f"_{{{domain_str}}}"
635
- header += "$}\n"
640
+ header += f"$_{{{domain_str}}}$"
641
+ header += "}\n"
636
642
 
637
643
  footer = "\n\\vspace{5pt}\n\\hrule"
638
644
  latex_repr = f"{header}{equation.latexRepr()}{footer}"
@@ -641,13 +647,13 @@ class LatexConverter:
641
647
  return "\n".join(definitions)
642
648
 
643
649
  def get_constraints(self) -> str:
644
- constraints = ["\\bigskip"]
650
+ constraints = [r"\begin{flalign*}"]
645
651
  for name in self.symbols:
646
652
  symbol = self.container[name]
647
653
  if not isinstance(symbol, syms.Variable):
648
654
  continue
649
655
 
650
- constraint = "$" + symbol.latexRepr()
656
+ constraint = "&" + symbol.latexRepr()
651
657
  if symbol.type == "binary":
652
658
  constraint += "\\in " + r"\{0,1\}"
653
659
  if symbol.domain:
@@ -687,9 +693,10 @@ class LatexConverter:
687
693
  else:
688
694
  continue
689
695
 
690
- constraint += "\\\\$"
696
+ constraint += "&\\\\"
691
697
  constraints.append(constraint)
692
698
 
699
+ constraints.append(r"\end{flalign*}")
693
700
  return "\n".join(constraints)
694
701
 
695
702
  def get_header(self) -> str: