gamspy 1.17.1__tar.gz → 1.17.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. {gamspy-1.17.1 → gamspy-1.17.2}/PKG-INFO +1 -1
  2. {gamspy-1.17.1 → gamspy-1.17.2}/pyproject.toml +1 -1
  3. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_container.py +18 -0
  4. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_model.py +104 -0
  5. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_symbols/equation.py +7 -0
  6. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_symbols/variable.py +17 -0
  7. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_validation.py +0 -5
  8. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/formulations/__init__.py +2 -0
  9. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/formulations/nn/torch_sequential.py +80 -8
  10. gamspy-1.17.2/src/gamspy/formulations/result.py +119 -0
  11. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/math/__init__.py +2 -0
  12. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/math/activation.py +132 -6
  13. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/utils.py +2 -1
  14. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy.egg-info/PKG-INFO +1 -1
  15. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy.egg-info/SOURCES.txt +1 -0
  16. {gamspy-1.17.1 → gamspy-1.17.2}/tests/test_gamspy.py +1 -1
  17. {gamspy-1.17.1 → gamspy-1.17.2}/LICENSE +0 -0
  18. {gamspy-1.17.1 → gamspy-1.17.2}/README.md +0 -0
  19. {gamspy-1.17.1 → gamspy-1.17.2}/README_PYPI.md +0 -0
  20. {gamspy-1.17.1 → gamspy-1.17.2}/setup.cfg +0 -0
  21. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/__init__.py +0 -0
  22. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/__main__.py +0 -0
  23. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_algebra/__init__.py +0 -0
  24. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_algebra/condition.py +0 -0
  25. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_algebra/domain.py +0 -0
  26. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_algebra/expression.py +0 -0
  27. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_algebra/number.py +0 -0
  28. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_algebra/operable.py +0 -0
  29. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_algebra/operation.py +0 -0
  30. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_backend/__init__.py +0 -0
  31. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_backend/backend.py +0 -0
  32. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_backend/engine.py +0 -0
  33. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_backend/local.py +0 -0
  34. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_backend/neos.py +0 -0
  35. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_cli/__init__.py +0 -0
  36. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_cli/cli.py +0 -0
  37. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_cli/gdx.py +0 -0
  38. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_cli/install.py +0 -0
  39. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_cli/list.py +0 -0
  40. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_cli/probe.py +0 -0
  41. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_cli/retrieve.py +0 -0
  42. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_cli/run.py +0 -0
  43. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_cli/show.py +0 -0
  44. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_cli/uninstall.py +0 -0
  45. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_cli/util.py +0 -0
  46. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_config.py +0 -0
  47. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_convert.py +0 -0
  48. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_database.py +0 -0
  49. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_extrinsic.py +0 -0
  50. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_miro.py +0 -0
  51. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_model_instance.py +0 -0
  52. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_options.py +0 -0
  53. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_serialization.py +0 -0
  54. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_symbols/__init__.py +0 -0
  55. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_symbols/alias.py +0 -0
  56. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_symbols/implicits/__init__.py +0 -0
  57. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_symbols/implicits/implicit_equation.py +0 -0
  58. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_symbols/implicits/implicit_parameter.py +0 -0
  59. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_symbols/implicits/implicit_set.py +0 -0
  60. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_symbols/implicits/implicit_symbol.py +0 -0
  61. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_symbols/implicits/implicit_variable.py +0 -0
  62. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_symbols/parameter.py +0 -0
  63. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_symbols/set.py +0 -0
  64. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_symbols/symbol.py +0 -0
  65. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_symbols/universe_alias.py +0 -0
  66. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_types.py +0 -0
  67. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/_workspace.py +0 -0
  68. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/exceptions.py +0 -0
  69. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/formulations/ml/__init__.py +0 -0
  70. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/formulations/ml/decision_tree_struct.py +0 -0
  71. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/formulations/ml/gradient_boosting.py +0 -0
  72. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/formulations/ml/random_forest.py +0 -0
  73. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/formulations/ml/regression_tree.py +0 -0
  74. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/formulations/nn/__init__.py +0 -0
  75. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/formulations/nn/avgpool2d.py +0 -0
  76. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/formulations/nn/conv1d.py +0 -0
  77. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/formulations/nn/conv2d.py +0 -0
  78. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/formulations/nn/linear.py +0 -0
  79. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/formulations/nn/maxpool2d.py +0 -0
  80. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/formulations/nn/minpool2d.py +0 -0
  81. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/formulations/nn/mpool2d.py +0 -0
  82. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/formulations/piecewise.py +0 -0
  83. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/formulations/shape.py +0 -0
  84. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/formulations/utils.py +0 -0
  85. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/math/log_power.py +0 -0
  86. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/math/matrix.py +0 -0
  87. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/math/misc.py +0 -0
  88. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/math/probability.py +0 -0
  89. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/math/trigonometric.py +0 -0
  90. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/py.typed +0 -0
  91. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy/version.py +0 -0
  92. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy.egg-info/dependency_links.txt +0 -0
  93. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy.egg-info/entry_points.txt +0 -0
  94. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy.egg-info/requires.txt +0 -0
  95. {gamspy-1.17.1 → gamspy-1.17.2}/src/gamspy.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gamspy
3
- Version: 1.17.1
3
+ Version: 1.17.2
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/
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gamspy"
7
- version = "1.17.1"
7
+ version = "1.17.2"
8
8
  authors = [
9
9
  { name = "GAMS Development Corporation", email = "support@gams.com" },
10
10
  ]
@@ -1064,6 +1064,9 @@ class Container(gt.Container):
1064
1064
  >>> a = m.addAlias("a", i)
1065
1065
 
1066
1066
  """
1067
+ if name is None:
1068
+ name = self._get_symbol_name(prefix="a")
1069
+
1067
1070
  return gp.Alias(self, name, alias_with)
1068
1071
 
1069
1072
  def addSet(
@@ -1121,6 +1124,9 @@ class Container(gt.Container):
1121
1124
  >>> i = m.addSet("i")
1122
1125
 
1123
1126
  """
1127
+ if name is None:
1128
+ name = self._get_symbol_name(prefix="s")
1129
+
1124
1130
  return gp.Set(
1125
1131
  self,
1126
1132
  name,
@@ -1189,6 +1195,9 @@ class Container(gt.Container):
1189
1195
  >>> a = m.addParameter("a")
1190
1196
 
1191
1197
  """
1198
+ if name is None:
1199
+ name = self._get_symbol_name(prefix="p")
1200
+
1192
1201
  return gp.Parameter(
1193
1202
  self,
1194
1203
  name,
@@ -1252,6 +1261,9 @@ class Container(gt.Container):
1252
1261
  >>> v = m.addVariable("v")
1253
1262
 
1254
1263
  """
1264
+ if name is None:
1265
+ name = self._get_symbol_name(prefix="v")
1266
+
1255
1267
  return gp.Variable(
1256
1268
  self,
1257
1269
  name,
@@ -1322,6 +1334,9 @@ class Container(gt.Container):
1322
1334
  >>> i = m.addEquation("i")
1323
1335
 
1324
1336
  """
1337
+ if name is None:
1338
+ name = self._get_symbol_name(prefix="e")
1339
+
1325
1340
  return gp.Equation(
1326
1341
  self,
1327
1342
  name,
@@ -1389,6 +1404,9 @@ class Container(gt.Container):
1389
1404
  >>> model = m.addModel("my_model", "LP", [e])
1390
1405
 
1391
1406
  """
1407
+ if name is None:
1408
+ name = self._get_symbol_name(prefix="m")
1409
+
1392
1410
  return gp.Model(
1393
1411
  self,
1394
1412
  name,
@@ -73,21 +73,52 @@ class Problem(Enum):
73
73
  """An enumeration for problem all problem types"""
74
74
 
75
75
  LP = "LP"
76
+ "Linear Programming"
77
+
76
78
  NLP = "NLP"
79
+ """Non-Linear Programming"""
80
+
77
81
  QCP = "QCP"
82
+ """Quadratically Constrained Programs"""
83
+
78
84
  DNLP = "DNLP"
85
+ """Nonlinear Programming with Discontinuous Derivatives"""
86
+
79
87
  MIP = "MIP"
88
+ """Mixed Integer Programming"""
89
+
80
90
  RMIP = "RMIP"
91
+ """Relaxed Mixed Integer Program"""
92
+
81
93
  MINLP = "MINLP"
94
+ """Mixed Integer Nonlinear Program"""
95
+
82
96
  RMINLP = "RMINLP"
97
+ """Relaxed Mixed Integer Nonlinear Program"""
98
+
83
99
  MIQCP = "MIQCP"
100
+ """Mixed Integer Quadratically Constrained Program"""
101
+
84
102
  RMIQCP = "RMIQCP"
103
+ """Relaxed Mixed Integer Quadratically Constrained Program"""
104
+
85
105
  MCP = "MCP"
106
+ """Mixed Complementarity Problem"""
107
+
86
108
  CNS = "CNS"
109
+ """Constrained Nonlinear System"""
110
+
87
111
  MPEC = "MPEC"
112
+ """Mathematical Programs with Equilibrium Constraints"""
113
+
88
114
  RMPEC = "RMPEC"
115
+ """Relaxed Mathematical Program with Equilibrium Constraints"""
116
+
89
117
  EMP = "EMP"
118
+ """Extended Mathematical Program"""
119
+
90
120
  MPSGE = "MPSGE"
121
+ """General Equilibrium"""
91
122
 
92
123
  @classmethod
93
124
  def values(cls):
@@ -102,8 +133,13 @@ class Sense(Enum):
102
133
  """An enumeration for sense types"""
103
134
 
104
135
  MIN = "MIN"
136
+ """Minimize the objective."""
137
+
105
138
  MAX = "MAX"
139
+ """Maximize the objective."""
140
+
106
141
  FEASIBILITY = "FEASIBILITY"
142
+ """Assess feasibility."""
107
143
 
108
144
  @classmethod
109
145
  def values(cls):
@@ -118,24 +154,61 @@ class ModelStatus(Enum):
118
154
  """An enumeration for model status types"""
119
155
 
120
156
  OptimalGlobal = 1
157
+ """The solution is optimal, that is, it is feasible (within tolerances) and it has been proven that no other feasible solution with better objective value exists."""
158
+
121
159
  OptimalLocal = 2
160
+ """A local optimum for an NLP has been found. That is, a solution that is feasible (within tolerances) and it has been proven that there exists a neighborhood of this solution in which no other feasible solution with better objective value exists."""
161
+
122
162
  Unbounded = 3
163
+ """The solution is unbounded. This message is reliable if the problem is linear, but occasionally it appears for difficult nonlinear problems that are not truly unbounded, but that lack some strategically placed bounds to limit the variables to sensible values."""
164
+
123
165
  InfeasibleGlobal = 4
166
+ """The problem has been proven to be infeasible. If this was not intended, something is probably misspecified in the logic or the data."""
167
+
124
168
  InfeasibleLocal = 5
169
+ """No feasible point could be found for the NLP problem from the given starting point. It does not necessarily mean that no feasible point exists."""
170
+
125
171
  InfeasibleIntermed = 6
172
+ """The current solution is not feasible, but the solver stopped, either because of a limit (for example, iteration or resource) or because of some sort of difficulty. The solver status will give more information."""
173
+
126
174
  Feasible = 7
175
+ """A feasible solution to a problem without discrete variables has been found."""
176
+
127
177
  Integer = 8
178
+ """A feasible solution to a problem with discrete variables has been found."""
179
+
128
180
  NonIntegerIntermed = 9
181
+ """An incomplete solution to a problem with discrete variables. A feasible solution has not yet been found."""
182
+
129
183
  IntegerInfeasible = 10
184
+ """It has been proven that there is no feasible solution to a problem with discrete variables."""
185
+
130
186
  LicenseError = 11
187
+ """The solver cannot find the appropriate license key needed to use a specific subsolver."""
188
+
131
189
  ErrorUnknown = 12
190
+ """After a solver error the model status is unknown."""
191
+
132
192
  ErrorNoSolution = 13
193
+ """An error occurred and no solution has been returned. No solution will be returned to GAMS because of errors in the solution process."""
194
+
133
195
  NoSolutionReturned = 14
196
+ """A solution is not expected for this solve. For example, the CONVERT solver only reformats the model but does not give a solution."""
197
+
134
198
  SolvedUnique = 15
199
+ """Indicates the solution returned is unique, i.e. no other solution exists. Used for CNS models. Examples where this status could be returned include non-singular linear models, triangular models with constant non-zero elements on the diagonal, and triangular models where the functions are monotone in the variable on the diagonal."""
200
+
135
201
  Solved = 16
202
+ """Indicates the model has been solved: used for CNS models. The solution might or might not be unique. If the solver uses status SOLVED SINGULAR wherever possible then this status implies that the Jacobian is non-singular, i.e. that the solution is at least locally unique."""
203
+
136
204
  SolvedSingular = 17
205
+ """Indicates the CNS model has been solved, but the Jacobian is singular at the solution. This can indicate that other solutions exist, either along a line (for linear models) or a curve (for nonlinear models) including the solution returned."""
206
+
137
207
  UnboundedNoSolution = 18
208
+ """The model is unbounded and no solution can be provided."""
209
+
138
210
  InfeasibleNoSolution = 19
211
+ """The model is infeasible and no solution can be provided."""
139
212
 
140
213
 
141
214
  class SolveStatus(Enum):
@@ -185,21 +258,52 @@ class FileFormat(Enum):
185
258
  """An enumeration for file format types"""
186
259
 
187
260
  AMPL = "ampl.mod"
261
+ """AMPL input format."""
262
+
188
263
  AMPLNL = "ampl.nl"
264
+ """AMPL nl format."""
265
+
189
266
  CPLEXLP = "cplex.lp"
267
+ """CPLEX LP format."""
268
+
190
269
  CPLEXMPS = "cplex.mps"
270
+ """CPLEX MPS format."""
271
+
191
272
  GAMSDict = "dict.txt"
273
+ """GAMS dictionary format."""
274
+
192
275
  GAMSDictMap = "dictmap.gdx"
276
+ """GAMS dictionary map format."""
277
+
193
278
  GAMSJacobian = "jacobian.gms"
279
+ """Jacobian in GAMS."""
280
+
194
281
  GAMSPyJacobian = "jacobian.py"
282
+ """Jacobian in GAMSPy."""
283
+
195
284
  GDXJacobian = "jacobian.gdx"
285
+ """GDX file with model data incl. Jacobian and Hessian evaluated at current point."""
286
+
196
287
  FileList = "files.txt"
288
+ """List of file formats generated."""
289
+
197
290
  FixedMPS = "fixed.mps"
291
+ """Fixed format mps file."""
292
+
198
293
  GAMS = "gams.gms"
294
+ """GAMS scalar model."""
295
+
199
296
  JuMP = "jump.jl"
297
+ """JuMP scalar model."""
298
+
200
299
  LINGO = "lingo.lng"
300
+ """Lingo format."""
301
+
201
302
  OSiL = "osil.xml"
303
+ """Optimization Services instance Language (OSiL) format."""
304
+
202
305
  Pyomo = "pyomo.py"
306
+ """Pyomo concrete scalar model."""
203
307
 
204
308
 
205
309
  INTERRUPT_STATUS = [
@@ -45,9 +45,16 @@ IRREGULAR_EQ_MAP = {
45
45
 
46
46
  class EquationType(Enum):
47
47
  REGULAR = "regular"
48
+ """Regular equations with =, >= and <= sign."""
49
+
48
50
  NONBINDING = "nonbinding"
51
+ """No relationship implied between left-hand side and right-hand side. This equation type is ideally suited for use in MCP models and in variational inequalities."""
52
+
49
53
  EXTERNAL = "external"
54
+ """Equation is defined by external programs."""
55
+
50
56
  BOOLEAN = "boolean"
57
+ """Boolean equations."""
51
58
 
52
59
  @classmethod
53
60
  def values(cls):
@@ -35,14 +35,31 @@ if TYPE_CHECKING:
35
35
 
36
36
  class VariableType(Enum):
37
37
  BINARY = "binary"
38
+ """Discrete variable that can only take values of 0 or 1."""
39
+
38
40
  INTEGER = "integer"
41
+ """Discrete variable that can only take integer values between the bounds."""
42
+
39
43
  POSITIVE = "positive"
44
+ """No negative values are allowed for variable. The user may change both bounds from the default value."""
45
+
40
46
  NEGATIVE = "negative"
47
+ """No positive values are allowed for variables. The user may change both bounds from the default value."""
48
+
41
49
  FREE = "free"
50
+ """No bounds on variable. Both bounds may be changed from the default values by the user."""
51
+
42
52
  SOS1 = "sos1"
53
+ """A set of variables, such that at most one variable within a group may have a non-zero value."""
54
+
43
55
  SOS2 = "sos2"
56
+ """A set of variables, such that at most two variables within a group may have non-zero values and the two non-zero values are adjacent."""
57
+
44
58
  SEMICONT = "semicont"
59
+ """Semi-continuous, must be zero or above a given minimum level."""
60
+
45
61
  SEMIINT = "semiint"
62
+ """Semi-integer, must be zero or above a given minimum level and integer."""
46
63
 
47
64
  @classmethod
48
65
  def values(cls):
@@ -385,11 +385,6 @@ def validate_name(word: str) -> str:
385
385
  f" words: {RESERVED_WORDS}"
386
386
  )
387
387
 
388
- if word.endswith("gpauto"):
389
- raise ValidationError(
390
- "Name cannot end with one of the reserved words. `gpauto` is a reserverd word."
391
- )
392
-
393
388
  if not re.match(NAME_MATCH_REGEX, word):
394
389
  raise ValidationError(
395
390
  f"`{word}` is an invalid GAMSPy symbol name. "
@@ -20,6 +20,7 @@ from gamspy.formulations.piecewise import (
20
20
  pwl_convexity_formulation,
21
21
  pwl_interval_formulation,
22
22
  )
23
+ from gamspy.formulations.result import FormulationResult
23
24
  from gamspy.formulations.shape import flatten_dims
24
25
 
25
26
  __all__ = [
@@ -41,4 +42,5 @@ __all__ = [
41
42
  "pwl_convexity_formulation",
42
43
  "pwl_interval_formulation",
43
44
  "utils",
45
+ "FormulationResult",
44
46
  ]
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import typing
3
4
  from functools import partial
4
5
  from typing import TYPE_CHECKING
5
6
 
@@ -196,11 +197,82 @@ class TorchSequential:
196
197
  l = self._layer_converters[clz](container, layer)
197
198
  return l
198
199
 
199
- def __call__(self, input: gp.Variable) -> tuple[gp.Variable, list[gp.Equation]]:
200
- out = input
201
- equations = []
202
- for layer in self.layers:
203
- out, layer_eqs = layer(out)
204
- equations.extend(layer_eqs)
205
-
206
- return out, equations
200
+ def _update_dict_layered(
201
+ self, large: dict[str, typing.Any], small: dict[str, typing.Any], layer_num: int
202
+ ):
203
+ for key in small:
204
+ large[f"{layer_num}.{key}"] = small[key]
205
+
206
+ def __call__(self, input: gp.Variable) -> gp.formulations.FormulationResult:
207
+ """
208
+
209
+ This method returns a **`FormulationResult`** object, which includes symbols
210
+ and outputs created by its underlying layers.
211
+
212
+ The way to access these underlying symbols depends on what the sub-layer
213
+ returns:
214
+
215
+ 1. **If a Sub-Layer Returns a `FormulationResult`**
216
+
217
+ All symbols created by that sub-layer can be accessed within the main
218
+ `FormulationResult`. Each symbol's name is **prefixed** with its layer
219
+ number, followed by a dot (`.`).
220
+
221
+ * **Access Format:** `<layer_num>.<symbol_name>`
222
+ * **Example:** If the first layer creates a parameter named `bias`, it is accessed as `0.bias` in `parameters_created`.
223
+
224
+ 2. **If a Sub-Layer Returns the "Old Style" Output (Output Variable and List of Equations)**
225
+
226
+ For backward compatibility, if a sub-layer returns an output variable and
227
+ a list of equations instead of a `FormulationResult`, they are accessed
228
+ as follows:
229
+
230
+ * **Output Variable:** The main output variable is named:
231
+ * **Access Format:** `<layer_num>.output`
232
+ * **Equations:** Each returned equation is sequentially named:
233
+ * **Access Format:** `<layer_num>.eq_<eq_number>` (where `eq_number` starts at 0, 1, 2...)
234
+ * **Example:** The first equation from the third layer is accessed as `2.eq_0` in `equations_created`.
235
+
236
+
237
+ Returns
238
+ -------
239
+ FormulationResult
240
+
241
+ """
242
+ result = gp.formulations.FormulationResult()
243
+
244
+ out: gp.Variable | gp.Parameter | None = input
245
+ for layer_num, layer in enumerate(self.layers):
246
+ output = layer(out)
247
+ if isinstance(output, gp.formulations.FormulationResult):
248
+ self._update_dict_layered(
249
+ result.equations_created, output.equations_created, layer_num
250
+ )
251
+ self._update_dict_layered(
252
+ result.variables_created, output.variables_created, layer_num
253
+ )
254
+ self._update_dict_layered(
255
+ result.sets_created, output.sets_created, layer_num
256
+ )
257
+ self._update_dict_layered(
258
+ result.parameters_created, output.parameters_created, layer_num
259
+ )
260
+ self._update_dict_layered(result.other, output.other, layer_num)
261
+
262
+ # matches are not named
263
+ result.matches.update(output.matches)
264
+ out = output.result
265
+ else:
266
+ # old interface only provides output var and list of equations
267
+ out, layer_eqs = output
268
+ result.variables_created[f"{layer_num}.output"] = out
269
+ for eq_num, eq in enumerate(layer_eqs):
270
+ result.equations_created[f"{layer_num}.eq_{eq_num}"] = eq
271
+
272
+ result.result = out
273
+
274
+ # to prevent forgetting matches when unpacking
275
+ if result.matches:
276
+ result.extra_return = result.matches
277
+
278
+ return result
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, TypeAlias
4
+
5
+ if TYPE_CHECKING:
6
+ from collections.abc import Sequence
7
+
8
+ import gamspy as gp
9
+
10
+ MatchesType: TypeAlias = dict[
11
+ gp.Equation | Sequence[gp.Equation],
12
+ gp.Variable | Sequence[gp.Variable],
13
+ ]
14
+
15
+
16
+ class FormulationResult:
17
+ """
18
+ FormulationResult class provides a common interface for returning results
19
+ when formulations are called. In the old convention, formulations returned
20
+ a tuple of result variable and list of equations. In some cases it was
21
+ possible to get extra output from the formulation. To provide backwards
22
+ compatibility, FormulationResult class can be unpacked into a result
23
+ variable and list of equations. Also it supports returning extra output in
24
+ unpacking.
25
+
26
+ With the FormulationResult you can have more access to underlying symbols
27
+ created such as equations, variables, parameters and sets. Since many
28
+ formulations created symbols with randomized names, it was tedious to find
29
+ intermediate symbols created. FormulationResult has dictionaries where keys
30
+ are expected to be documented in the formulation returning the
31
+ FormulationResult therefore you can access a symbol via its known key.
32
+
33
+ For example:
34
+
35
+ Examples
36
+ --------
37
+ >>> import gamspy as gp
38
+ >>> m = gp.Container()
39
+ >>> x = gp.Variable(m)
40
+ >>> res = gp.math.activation.relu_with_binary_var(x)
41
+ >>> aux_binary_var = res.variables_created["binary"]
42
+
43
+ Therefore, it is important for the formulation returning a
44
+ FormulationResult to properly list the keys to the symbols that are
45
+ created.
46
+
47
+ FormulationResult has the following attributes that might be useful:
48
+ - `result`
49
+ - `equations_created`
50
+ - `sets_created`
51
+ - `parameters_created`
52
+ - `matches`
53
+ - `other`
54
+ - `extra_return`
55
+
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ result: gp.Variable | gp.Parameter | None = None,
61
+ equations_created: dict[str, gp.Equation] | None = None,
62
+ extra_return: gp.Variable | MatchesType | None = None,
63
+ ) -> None:
64
+ self.result = result
65
+ self.equations_created: dict[str, gp.Equation] = (
66
+ equations_created if equations_created is not None else {}
67
+ )
68
+
69
+ self.variables_created: dict[str, gp.Variable] = {}
70
+ self.sets_created: dict[str, gp.Set] = {}
71
+ self.parameters_created: dict[str, gp.Parameter] = {}
72
+ self.matches: MatchesType = {}
73
+ self.other: dict[str, Any] = {}
74
+
75
+ # Extra return is only here for backwards compat reasons
76
+ # try to use other when you can
77
+ self.extra_return = extra_return
78
+
79
+ def __len__(self):
80
+ # For backwards compat
81
+ return 2 + (self.extra_return is not None)
82
+
83
+ def __iter__(self):
84
+ # For backwards compat
85
+ if self.extra_return is None:
86
+ return [
87
+ self.result,
88
+ list(self.equations_created.values()),
89
+ ].__iter__()
90
+
91
+ return [
92
+ self.result,
93
+ self.extra_return,
94
+ list(self.equations_created.values()),
95
+ ].__iter__()
96
+
97
+ def __str__(self) -> str:
98
+ return (
99
+ "FormulationResult(\n"
100
+ + " Equations: "
101
+ + str(self.equations_created.keys())
102
+ + "\n"
103
+ + " Variables: "
104
+ + str(self.variables_created.keys())
105
+ + "\n"
106
+ + " Sets: "
107
+ + str(self.sets_created.keys())
108
+ + "\n"
109
+ + " Parameters: "
110
+ + str(self.parameters_created.keys())
111
+ + "\n"
112
+ + " Num Matches: "
113
+ + str(len(self.matches))
114
+ + "\n"
115
+ + " Other: "
116
+ + str(self.other)
117
+ + "\n"
118
+ + ")"
119
+ )
@@ -5,6 +5,7 @@ from gamspy.math.activation import (
5
5
  log_softmax,
6
6
  relu_with_binary_var,
7
7
  relu_with_complementarity_var,
8
+ relu_with_equilibrium,
8
9
  relu_with_sos1_var,
9
10
  softmax,
10
11
  softplus,
@@ -184,6 +185,7 @@ __all__ = [
184
185
  "rel_ne",
185
186
  "relu_with_binary_var",
186
187
  "relu_with_complementarity_var",
188
+ "relu_with_equilibrium",
187
189
  "relu_with_sos1_var",
188
190
  "rpower",
189
191
  "same_as",
@@ -5,10 +5,12 @@ from typing import TYPE_CHECKING
5
5
  import gamspy._symbols.implicits as implicits
6
6
  import gamspy.math
7
7
  import gamspy.utils as utils
8
+ from gamspy._algebra.number import Number
8
9
  from gamspy._container import Container
9
10
  from gamspy._symbols.equation import Equation
10
11
  from gamspy._symbols.variable import Variable
11
12
  from gamspy.exceptions import ValidationError
13
+ from gamspy.formulations.result import FormulationResult
12
14
  from gamspy.math.matrix import next_alias
13
15
 
14
16
  if TYPE_CHECKING:
@@ -272,7 +274,7 @@ def relu_with_binary_var(
272
274
  default_lb: float = -(10**6),
273
275
  default_ub: float = 10**6,
274
276
  return_binary_var: bool = False,
275
- ):
277
+ ) -> FormulationResult:
276
278
  """
277
279
  Implements the ReLU activation function using binary variables. The ReLU
278
280
  function is defined as ReLU(x) = max(x, 0). This implementation **generates**
@@ -292,6 +294,10 @@ def relu_with_binary_var(
292
294
 
293
295
  Adapted from `OMLT <https://github.com/cog-imperial/OMLT/blob/e60563859a66ac5dd3348bf1763de57eec95171e/src/omlt/neuralnet/activations/relu.py>`_
294
296
 
297
+ FormulationResult:
298
+ - variables_created: ["output", "binary"]
299
+ - equations_created: ["y_gte_x", "y_lte_x_1", "y_lte_x_2"]
300
+
295
301
  Parameters
296
302
  ----------
297
303
  x : Parameter | Variable | implicits.ImplicitParameter | implicits.ImplicitVariable | Expression | Operation
@@ -301,7 +307,7 @@ def relu_with_binary_var(
301
307
 
302
308
  Returns
303
309
  -------
304
- tuple[Variable, list[Equation]] | tuple[Variable, Variable, list[Equation]]
310
+ FormulationResult
305
311
 
306
312
  Examples
307
313
  --------
@@ -310,7 +316,7 @@ def relu_with_binary_var(
310
316
  >>> m = Container()
311
317
  >>> i = Set(m, "i", records=range(3))
312
318
  >>> x = Variable(m, "x", domain=[i])
313
- >>> y, eqs = relu_with_binary_var(x)
319
+ >>> y, eqs = relu_with_binary_var(x) # FormulationResult can be unpacked for backwards compat
314
320
  >>> y.type
315
321
  'positive'
316
322
  >>> len(eqs)
@@ -322,6 +328,9 @@ def relu_with_binary_var(
322
328
  [Set(name='i', domain=['*'])]
323
329
  >>> b.domain # i many binary variables
324
330
  [Set(name='i', domain=['*'])]
331
+ >>> output = relu_with_binary_var(x) # You can use FormulationResult too
332
+ >>> binary_var = output.variables_created["binary"]
333
+ >>> y = output.result
325
334
 
326
335
  """
327
336
  assert isinstance(x.container, Container)
@@ -359,10 +368,127 @@ def relu_with_binary_var(
359
368
  eq[2][...] = y <= sigma * default_ub
360
369
  y.lo[...] = 0
361
370
 
362
- if return_binary_var:
363
- return y, sigma, eq
371
+ result = FormulationResult(
372
+ y,
373
+ {
374
+ "y_gte_x": eq[0],
375
+ "y_lte_x_1": eq[1],
376
+ "y_lte_x_2": eq[2],
377
+ },
378
+ )
379
+ result.variables_created = {"binary": sigma, "output": y}
380
+ result.extra_return = sigma if return_binary_var else None
381
+ return result
382
+
383
+
384
+ def relu_with_equilibrium(
385
+ x: (
386
+ Parameter
387
+ | Variable
388
+ | implicits.ImplicitParameter
389
+ | implicits.ImplicitVariable
390
+ | Expression
391
+ | Operation
392
+ ),
393
+ ) -> FormulationResult:
394
+ """
395
+ Implements the ReLU activation function using Equilibrium Constraints.
396
+ This implementation is suitable for models of type Mathematical Program with
397
+ Equilibrium Constraints (MPEC) or Mixed Complementarity Problem (MCP).
398
+ One positive variable is **generated**, which serves as the activation
399
+ variable and no equations. The activation variable shares the same domain as
400
+ the input. Lower and upper bounds are not required for this formulation.
401
+
402
+ Returns FormulationResult which can be unpacked as activation variable,
403
+ matches dictionary and the equation list (empty).
404
+
405
+ FormulationResult:
406
+ - variables_created: ["output"]
407
+ - equations_created: []
408
+ - matches: {(output - x) : output}
409
+ - extra_return: yes (matches)
410
+
411
+ or if the provided input was not a Variable, this formulation assigns it to a
412
+ new variable and uses the new variable instead
413
+
414
+ FormulationResult:
415
+ - variables_created: ["output", "new_input"]
416
+ - equations_created: ["set_new_input"]
417
+ - matches: {(output - new_input) : output}
418
+ - extra_return: yes (matches)
419
+
420
+
421
+ Parameters
422
+ ----------
423
+ x : Variable
424
+
425
+ Returns
426
+ -------
427
+ FormulationResult
428
+
429
+ Examples
430
+ --------
431
+ >>> from gamspy import Container, Variable, Set
432
+ >>> from gamspy.math.activation import relu_with_equilibrium
433
+ >>> m = Container()
434
+ >>> i = Set(m, "i", records=range(3))
435
+ >>> x = Variable(m, "x", domain=[i])
436
+ >>> y, matches, eqs = relu_with_equilibrium(x)
437
+ >>> y.type
438
+ 'positive'
439
+ >>> len(eqs)
440
+ 0
441
+ >>> len(matches)
442
+ 1
443
+ >>> result = relu_with_equilibrium(x)
444
+ >>> type(result)
445
+ <class 'gamspy.formulations.result.FormulationResult'>
446
+ >>> result = relu_with_equilibrium(x - 5)
447
+ >>> new_input = result.variables_created["new_input"]
448
+
449
+ """
450
+ assert isinstance(x.container, Container)
451
+ domain = x.domain
452
+
453
+ y = x.container.addVariable(
454
+ type="positive",
455
+ domain=domain,
456
+ )
457
+
458
+ new_input = None
459
+ set_new_input = None
460
+ if not isinstance(x, Variable):
461
+ new_input = Variable._constructor_bypass(
462
+ x.container,
463
+ _get_random_name("new_input"),
464
+ domain=domain,
465
+ )
466
+ set_new_input = Equation._constructor_bypass(
467
+ x.container,
468
+ _get_random_name("set_new_input"),
469
+ domain=domain,
470
+ )
471
+ set_new_input[...] = new_input == x
364
472
  else:
365
- return y, eq
473
+ new_input = x
474
+
475
+ eq = Equation._constructor_bypass(
476
+ x.container,
477
+ _get_random_name("matches_eq"),
478
+ domain=domain,
479
+ )
480
+
481
+ eq[...] = y - new_input >= Number(0)
482
+
483
+ result = FormulationResult(y, {})
484
+ result.variables_created["output"] = y
485
+ if set_new_input is not None:
486
+ result.variables_created["new_input"] = new_input
487
+ result.equations_created["set_new_input"] = set_new_input
488
+
489
+ result.extra_return = {eq: y}
490
+ result.matches = {eq: y}
491
+ return result
366
492
 
367
493
 
368
494
  def relu_with_complementarity_var(
@@ -385,7 +385,8 @@ def _get_name_from_stack() -> str:
385
385
  # Current frame is this function (_get_name_from_stack)
386
386
  # The first f_back takes us to _get_symbol_name
387
387
  # The second f_back takes us to the __init__ function of
388
- # the symbol. The third f_back takes us to the user code.
388
+ # the symbol or addX function of Container. The third f_back
389
+ # takes us to the user code.
389
390
  frame = inspect.currentframe().f_back.f_back.f_back
390
391
  assert frame is not None
391
392
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gamspy
3
- Version: 1.17.1
3
+ Version: 1.17.2
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/
@@ -66,6 +66,7 @@ src/gamspy/_symbols/implicits/implicit_symbol.py
66
66
  src/gamspy/_symbols/implicits/implicit_variable.py
67
67
  src/gamspy/formulations/__init__.py
68
68
  src/gamspy/formulations/piecewise.py
69
+ src/gamspy/formulations/result.py
69
70
  src/gamspy/formulations/shape.py
70
71
  src/gamspy/formulations/utils.py
71
72
  src/gamspy/formulations/ml/__init__.py
@@ -17,7 +17,7 @@ from gamspy.exceptions import GamspyException, ValidationError
17
17
 
18
18
  @pytest.mark.unit
19
19
  def test_version():
20
- assert gp.__version__ == "1.17.1"
20
+ assert gp.__version__ == "1.17.2"
21
21
 
22
22
 
23
23
  @pytest.mark.unit
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes