from __future__ import annotations
import os
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal
from pydantic import BaseModel, ConfigDict
from typing_extensions import override
from gamspy.exceptions import ValidationError
if TYPE_CHECKING:
import io
from types import FrameType
from gamspy._model import Problem
SOLVE_LINK_MAP = {"disk": 2, "memory": 5}
SOLVE_LINK_MAP_REVERSE = dict(
zip(SOLVE_LINK_MAP.values(), SOLVE_LINK_MAP.keys(), strict=False)
)
# GAMSPy to GAMS option mapping
OPTION_MAP = {
"cns": "cns",
"dnlp": "dnlp",
"emp": "emp",
"lp": "lp",
"mcp": "mcp",
"minlp": "minlp",
"mip": "mip",
"miqcp": "miqcp",
"mpec": "mpec",
"nlp": "nlp",
"qcp": "qcp",
"rminlp": "rminlp",
"rmip": "rmip",
"rmiqcp": "rmiqcp",
"rmpec": "rmpec",
"license": "license",
"allow_suffix_in_equation": "suffixalgebravars",
"allow_suffix_in_limited_variables": "suffixdlvars",
"append_to_log_file": "appendLog",
"basis_detection_threshold": "bratio",
"compile_error_limit": "cerr",
"domain_violation_limit": "domlim",
"hold_fixed_variables": "holdfixed",
"iteration_limit": "iterlim",
"keep_temporary_files": "keep",
"listing_file": "output",
"log_file": "logfile",
"variable_listing_limit": "limcol",
"equation_listing_limit": "limrow",
"node_limit": "nodlim",
"absolute_optimality_gap": "optca",
"relative_optimality_gap": "optcr",
"monitor_process_tree_memory": "procTreeMemMonitor",
"memory_tick_interval": "procTreeMemTicks",
"profile": "profile",
"profile_file": "profileFile",
"profile_tolerance": "profiletol",
"reference_file": "reference",
"time_limit": "reslim",
"savepoint": "savepoint",
"seed": "seed",
"report_solution": "solprint",
"show_os_memory": "showosmemory",
"solve_link_type": "solvelink",
"merge_strategy": "solveopt",
"step_summary": "stepsum",
"suppress_compiler_listing": "suppress",
"report_solver_status": "sysout",
"threads": "threads",
"write_listing_file": "writeoutput",
"zero_rounding_threshold": "zerores",
"report_underflow": "zeroresrep",
}
OPTION_MAP_REVERSE = dict(zip(OPTION_MAP.values(), OPTION_MAP.keys(), strict=False))
MODEL_ATTR_OPTION_MAP = {
"generate_name_dict": "dictfile",
"enable_scaling": "scaleopt",
"min_improvement_threshold": "cheat",
"cutoff": "cutOff",
"default_point": "defPoint",
"enable_prior": "priorOpt",
"infeasibility_tolerance": "tolInfRep",
"try_partial_integer_solution": "tryInt",
"examine_linearity": "tryLinear",
"bypass_solver": "justscrdir",
}
EXECUTION_OPTIONS = {"loadpoint": "execute_loadpoint"}
[docs]
class Options(BaseModel):
"""
Options class to set GAMS options for the model.
Attributes
----------
cns: str | None
Default **cns** solver
dnlp: str | None
Default **dnlp** solver
emp: str | None
Default **emp** solver
lp: str | None
Default **lp** solver
mcp: str | None
Default **mcp** solver
minlp: str | None
Default **minlp** solver
mip: str | None
Default **mip** solver
miqcp: str | None
Default **miqcp** solver
mpec: str | None
Default **mpec** solver
nlp: str | None
Default **nlp** solver
qcp: str | None
Default **qcp** solver
rminlp: str | None
Default **rminlp** solver
rmip: str | None
Default **rmip** solver
rmiqcp: str | None
Default **rmiqcp** solver
rmpec: str | None
Default **rmpec** solver
allow_suffix_in_equation: bool | None
Flag to allow variables with suffixes in model algebra
allow_suffix_in_limited_variables: bool | None
Flag to allow **domain limited variables** with suffixes in model
append_to_log_file: bool | None
Setting this option to True means that the log file will be appended to and not overwritten (replaced).
basis_detection_threshold: float | None
Basis detection threshold
compile_error_limit: int = 1
Compile time error limit
domain_violation_limit: int | None
Domain violation limit
cutoff: float | None
Within a branch-and-bound based solver, the parts of the tree with an objective value worse than
the cutoff value are ignored. Note that this may speed up the initial phase of the branch and
bound algorithm (before the first integer solution is found). However, the true optimum may be
beyond the cutoff value. In this case the true optimum will be missed and moreover, no solution
will be found. Observe that this option is specified in absolute terms.
default_point: int | None
This option determines the point that is passed to the solver as a basis. By default, the levels
and marginals from the current basis are passed to the solver. In some circumstances (mostly during debugging),
it can be useful to pass a standard default input point, i.e. with all levels set to 0 or lower bound.
* **Option 0** Pass user defined levels and marginals to solver
* **Option 1** Pass default levels and marginals to solver
* **Option 2** Pass default marginals to solver
generate_name_dict: bool | None
If this option is set, it will instruct GAMS to make the GAMS names
of variables and equations that have been generated by the solve statement
available to the solver. In many solver links, these names are registered
with the solver and hence messages from the solver that involve variables
and equations (e.g. an infeasible row or duplicate columns) can be easily
interpreted by the user. However, the dictionary comes at a price.
Generating the names and calculating and storing the map takes time and space.
In addition, GAMS names take up space in the solver. Thus, if the user needs
very fast generation and does not need names, setting dictFile to zero is a
good option.
enable_scaling: bool | None
This option determines whether GAMS will employ user-specified variable and equation scaling factors.
It must be set to True if scaling factors are to be used.
enable_prior: bool | None
Instructs the solver to use the priority branching information passed by GAMS through variable suffix
values variable.prior. If and how priorities are used is solver-dependent.
infeasibility_tolerance: float | None
This option sets the tolerance for marking an equation infeasible in the equation listing. By default,
1.0e-13.
try_partial_integer_solution: bool | None
Signals the solver to make use of a partial or near-integer-feasible solution stored in current variable
values to get a quick integer-feasible point. The exact form of implementation depends on the solver and
may be partly controlled by solver settings or options. See the solver manuals for details.
examine_linearity: bool | None
Examine empirical NLP model to see if there are any NLP terms active. If there are none the default LP
solver will be used. If this option is set to True, empirical NLP models will be examined to determine
if there are any active NLP terms. If there are none, the default LP solver will be used. The procedure
also checks to see if QCP and DNLP models can be reduced to an LP; MIQCP and MINLP can be solved as a MIP;
RMIQCP and RMINLP can be solved as an RMIP.
bypass_solver: bool | None
If True, GAMSPy does not pass the generated model to the solver. Useful for model generation time analysis.
hold_fixed_variables: bool | None
Treat fixed variables as constants
iteration_limit: int | None
Iteration limit of solver
keep_temporary_files: bool = False
Controls keeping or deletion of process directory and scratch files
license: str | None
Absolute path of the license.
listing_file: str | None
Listing file name
loadpoint: os.PathLike | str | None
Path to the loadpoint GDX file that contains starting point records.
log_file: str | None
Log file name
variable_listing_limit: int
Maximum number of columns listed in one variable block
equation_listing_limit: int
Maximum number of rows listed in one equation block
min_improvement_threshold: float | None
For a branch-and-bound based solver, each new feasible solution must be at least the value of min_improvement_threshold
better than the current best feasible solution. Note that this may speed up the search, but may cause some solutions,
including optimal ones, to be missed. If a model has been solved with a nonzero min_improvement_threshold, then the
optimal solution will be within the min_improvement_threshold or less of the found solution. Observe that the option
min_improvement_threshold is specified in absolute terms, therefore non-negative values are appropriate for both
minimization and maximization models.
cutoff: float | None
Within a branch-and-bound based solver, the parts of the tree with an objective value worse than the cutoff value are
ignored. Note that this may speed up the initial phase of the branch and bound algorithm (before the first integer
solution is found). However, the true optimum may be beyond the cutoff value. In this case the true optimum will be
missed and moreover, no solution will be found.
miro_protect:
Protects MIRO input symbol records from being re-assigned, by default True.
node_limit: int | None
Node limit in branch and bound tree
absolute_optimality_gap: float | None
Absolute Optimality criterion solver default
relative_optimality_gap: float | None
Relative Optimality criterion solver default
memory_tick_interval: float | None
Wait interval between memory monitor checks: ticks = milliseconds
monitor_process_tree_memory: bool | None
Monitor the memory used by the GAMS process tree
profile: int | None
Execution profiling
* **Option 0**: Do not profile.
* **Option 1**: Minimum profiling.
* **Option n**: Profiling depth for nested control structures.
profile_file: str
Write profile information to this file
profile_tolerance: float | None
Minimum time a statement must use to appear in profile generated output
reference_file: str | None
Symbol reference file
time_limit: float | None
Wall-clock time limit for solver
savepoint: Optional[Literal[0, 1, 2, 3, 4]] = None
Save solver point in GDX file
* **Option 0**: No point GDX file is to be saved
* **Option 1**: A point GDX file from the last solve is to be saved
* **Option 2**: A point GDX file from every solve is to be saved
* **Option 3**: A point GDX file from the last solve is to be saved in the scratch directory
* **Option 4**: A point GDX file from every solve is to be saved in the scratch directory
seed: int | None
Random number seed
report_solution: Literal[0, 1, 2] = 2
Solution report print option
* **Option 0**: Remove solution listings following solves
* **Option 1**: Include solution listings following solves
* **Option 2**: Suppress all solution information
show_os_memory: Literal[0, 1, 2] = 0
Show the memory usage reported by the Operating System instead of the internal counting
* **Option 0**: Show memory reported by internal accounting
* **Option 1**: Show resident set size reported by operating system
* **Option 2**: Show virtual set size reported by operating system
solve_link_type: Optional[Literal["disk", "memory"]] = None
Solver link option
* **disk**: Model instance saved to scratch directory, the solver is called with a spawn (if possible) or a shell (if spawn is not possible) while GAMS remains open.
* **memory**: The model instance is passed to the solver in-memory - If this is not supported by the selected solver, it gets reset to **disk** automatically.
merge_strategy: Optional[Literal["replace", "merge", "clear"]] = None
* **Replace**: The solution information for all equations and variables is merged into the existing solution information
* **Merge**: The solution information for all equations appearing in the model is completely replaced by the new model results; variables are only replaced if they appear in the final model
* **Clear**: The solution information for all equations appearing in the model is completely replaced; in addition, variables appearing in the symbolic equations but removed by conditionals will be removed
step_summary: bool | None
Summary of computing resources used by job steps
suppress_compiler_listing: bool = False
Compiler listing option
report_solver_status: bool | None
Solver Status file reporting option
threads: int | None
Number of processors to be used by a solver
write_listing_file: bool = True
Switch to write a Listing file
zero_rounding_threshold: float | None
The results of certain operations will be set to zero if abs(result) LE ZeroRes
report_underflow: bool | None
Report underflow as a warning when abs(results) LE ZeroRes and result set to zero
Examples
--------
>>> import gamspy as gp
>>> options = gp.Options(
... listing_file="output.lst",
... time_limit=100,
... iteration_limit=1000
... )
"""
model_config = ConfigDict(extra="forbid")
cns: str | None = None
dnlp: str | None = None
emp: str | None = None
lp: str | None = None
mcp: str | None = None
minlp: str | None = None
mip: str | None = None
miqcp: str | None = None
mpec: str | None = None
nlp: str | None = None
qcp: str | None = None
rminlp: str | None = None
rmip: str | None = None
rmiqcp: str | None = None
rmpec: str | None = None
allow_suffix_in_equation: bool | None = None
allow_suffix_in_limited_variables: bool | None = None
append_to_log_file: bool | None = None
basis_detection_threshold: float | None = None
compile_error_limit: int | None = None
domain_violation_limit: int | None = None
generate_name_dict: bool | None = None
enable_scaling: bool | None = None
enable_prior: bool | None = None
infeasibility_tolerance: float | None = None
try_partial_integer_solution: bool | None = None
examine_linearity: bool | None = None
bypass_solver: bool | None = None
min_improvement_threshold: float | None = None
cutoff: float | None = None
default_point: int | None = None
miro_protect: bool = True
hold_fixed_variables: bool | None = None
iteration_limit: int | None = None
keep_temporary_files: int | None = None
license: str | None = None
listing_file: str | None = None
loadpoint: str | os.PathLike | None = None
log_file: str | None = None
variable_listing_limit: int = 0
equation_listing_limit: int = 0
node_limit: int | None = None
absolute_optimality_gap: float | None = None
relative_optimality_gap: float | None = None
monitor_process_tree_memory: bool | None = None
memory_tick_interval: float | None = None
profile: int | None = None
profile_file: str | None = None
profile_tolerance: float | None = None
reference_file: str | None = None
time_limit: float | None = None
savepoint: Literal[0, 1, 2, 3, 4] | None = None
seed: int | None = None
report_solution: Literal[0, 1, 2] = 0
show_os_memory: Literal[0, 1, 2] | None = None
solve_link_type: Literal["disk", "memory"] | None = "disk"
merge_strategy: Literal["replace", "merge", "clear"] | None = None
step_summary: bool | None = None
suppress_compiler_listing: bool | None = None
report_solver_status: bool | None = None
threads: int | None = None
write_listing_file: bool | None = None
zero_rounding_threshold: float | None = None
report_underflow: bool | None = None
@override
def model_post_init(self, context: Any) -> None:
self._hidden_options: dict[str, Any] = {}
self._debug_options: dict[str, Any] = {}
self._solver: str | None = None
self._problem: str | None = None
self._solver_options_file: str = "0"
self._frame: FrameType | None = None
[docs]
@staticmethod
def fromGams(options: dict) -> Options:
"""
Generates a gp.Options object from a dictionary of GAMS options
where keys are the GAMS option names.
Parameters
----------
options : dict
GAMS options.
Returns
-------
Options
Generated GAMSPy options
Raises
------
ValidationError
In case the option is not supported in GAMSPy.
Examples
--------
>>> import gamspy as gp
>>> m = gp.Container()
>>> options = gp.Options.fromGams({"reslim": 5})
"""
gamspy_options = {}
for key, value in options.items():
if key.lower() in OPTION_MAP_REVERSE:
if key.lower() == "solvelink":
try:
value = SOLVE_LINK_MAP_REVERSE[value]
except KeyError as e:
raise ValidationError(
f"`{value}` is not a valid value for `{key}`. Possible values are 2 and 5."
) from e
gamspy_options[OPTION_MAP_REVERSE[key.lower()]] = value
else:
raise ValidationError(f"`{key}` is not a supported option in GAMSPy.")
return Options(**gamspy_options)
def _get_gams_compatible_options(
self, output: io.TextIOWrapper | None = None
) -> dict:
gamspy_options = self.model_dump(exclude_none=True)
if "allow_suffix_in_equation" in gamspy_options:
allows_suffix = gamspy_options["allow_suffix_in_equation"]
gamspy_options["allow_suffix_in_equation"] = (
"on" if allows_suffix else "off"
)
if "allow_suffix_in_limited_variables" in gamspy_options:
allows_suffix = gamspy_options["allow_suffix_in_limited_variables"]
gamspy_options["allow_suffix_in_limited_variables"] = (
"on" if allows_suffix else "off"
)
if "solve_link_type" in gamspy_options:
link_type = gamspy_options["solve_link_type"]
gamspy_options["solve_link_type"] = SOLVE_LINK_MAP[link_type]
if "listing_file" in gamspy_options:
os.makedirs(
Path(gamspy_options["listing_file"]).parent.absolute(),
exist_ok=True,
)
if "log_file" in gamspy_options:
os.makedirs(
Path(gamspy_options["log_file"]).parent.absolute(),
exist_ok=True,
)
gams_options = {}
for key, value in gamspy_options.items():
if key not in OPTION_MAP:
continue
value = int(value) if isinstance(value, bool) else value
gams_options[OPTION_MAP[key]] = value
gams_options["previouswork"] = 1 # # In case GAMS version differs on backend
gams_options["traceopt"] = 3
if self.log_file:
if output is not None:
gams_options["logoption"] = 4
else:
gams_options["logoption"] = 2
else:
if output is not None:
gams_options["logoption"] = 3
else:
gams_options["logoption"] = 0
return gams_options
def _set_model_info(
self, solver: str, problem: Problem, solver_options: dict | Path | None
):
"""Set the solver and the solver options"""
self._solver = solver
self._problem = str(problem)
if solver_options:
self._solver_options_file = "1"
else:
self._solver_options_file = "0"
def _set_hidden_options(self, options: dict) -> None:
"""Set extra options of the backend"""
self._hidden_options = options
def _set_debug_options(self, options: dict) -> None:
"""Set debugging options"""
self._debug_options = options
[docs]
@staticmethod
def fromFile(path: str) -> Options:
"""
Generates an Options object with the key-value pairs in a file.
The file in given path must consist of one key-value pair in each line.
Parameters
----------
path : str
Path to the option file.
Returns
-------
Options
Raises
------
ValidationError
In case the given path is not a file.
Examples
--------
>>> import gamspy as gp
>>> import os
>>> # Create a dummy option file
>>> with open("options.txt", "w") as file:
... _ = file.write("lp = conopt")
>>> options = gp.Options.fromFile("options.txt")
>>> options.lp
'conopt'
>>> # Clean up
>>> os.remove("options.txt")
"""
if not os.path.isfile(path):
raise ValidationError(f"No such file in the given path: {path}")
attributes = {}
with open(path, encoding="utf-8") as file:
lines = file.readlines()
for line in lines:
if line in {"\n", ""}:
continue # pragma: no cover
key, value = line.split("=")
attributes[key.strip()] = value.strip()
return Options(**attributes)
[docs]
def export(self, pf_file: str) -> None:
"""
Exports options to the pf_file. Each line contains a key-value pair.
Parameters
----------
pf_file : str
Examples
--------
>>> import gamspy as gp
>>> import os
>>> options = gp.Options(solve_link_type="disk")
>>> options.export("options.pf")
>>> with open("options.pf") as file:
... print(file.read())
optfile = "0"
limcol = "0"
limrow = "0"
solprint = "0"
solvelink = "2"
previouswork = "1"
traceopt = "3"
logoption = "0"
>>> os.remove("options.pf")
"""
self._export(pf_file)
def _export(self, pf_file: str, output: io.TextIOWrapper | None = None) -> None:
"""
Exports options to the pf_file. Each line contains a key-value pair.
Parameters
----------
pf_file : str
output : io.TextIOWrapper | None, optional
"""
all_options = {}
# Solver options
if self._solver is not None:
all_options[self._problem] = self._solver
all_options["optfile"] = self._solver_options_file
# Extra options
all_options.update(**self._hidden_options)
all_options.update(**self._debug_options)
if self._frame is not None:
filename = self._frame.f_code.co_filename
line_number = self._frame.f_lineno
all_options["GP_SolveLine"] = f"{filename} line {line_number}"
# User options
user_options = self._get_gams_compatible_options(output)
all_options.update(**user_options)
# Generate pf file
with open(pf_file, "w", encoding="utf-8") as file:
file.write(
"\n".join([f'{key} = "{value}"' for key, value in all_options.items()])
)
[docs]
class FreezeOptions(BaseModel):
"""
Options for the freeze method of a Model.
Attributes
----------
no_match_limit : int, optional
Maximum number of non-matching elements to allow. Default is 0.
debug : bool, optional
Enables debug mode. Default is False.
update_type : Literal["0", "base_case", "accumulate", "inherit"], optional
Specifies the update type for the model instance.
- "base_case": Update from the base case.
- "accumulate": Accumulate changes.
- "inherit": Inherit from the parent.
Default is "base_case".
Examples
--------
>>> import gamspy as gp
>>> m = gp.Container()
>>> i = gp.Set(m, "i", records=["i1", "i2"])
>>> p = gp.Parameter(m, "p", domain=i, records=[("i1", 1), ("i2", 2)])
>>> v = gp.Variable(m, "v", domain=i)
>>> e = gp.Equation(m, "e", domain=i)
>>> e[i] = v[i] == p[i]
>>> model = gp.Model(m, "test", equations=[e], problem="LP", sense="min", objective=gp.Sum(i, v[i]))
>>> options = gp.FreezeOptions(update_type="accumulate")
>>> instance = model.freeze(modifiables=[p])
>>> p["i1"] = 3
>>> summary = model.solve(freeze_options=options)
"""
no_match_limit: int = 0
debug: bool = False
update_type: Literal["0", "base_case", "accumulate", "inherit"] = "base_case"
[docs]
class ConvertOptions(BaseModel):
"""
Options for the conversion of GAMSPy models into different formats.
Attributes
----------
AmplNLBin : Optional[bool]
Enables binary .nl file. False by default.
AmplNlInitDual : Optional[int]
Specifies which initial equation marginal values to write to the .nl file.
* **0**: Write no values
* **1**: Write only nondefault values
* **2**: Write all values.
1 by default.
AmplNlInitPrimal : Optional[int]
Specifies which initial primal values to write to the .nl file.
* **0**: Write no values
* **1**: Write only nondefault values
* **2**: Write all values.
2 by default.
GDXHessian : Optional[bool]
Controls whether Hessian information is included in GDX Jacobian file.
False by default.
GDXNames : Optional[bool]
Controls whether variable and equation names are included in GDX Jacobian file.
True by default.
GDXQuadratic : Optional[bool]
Specifies whether quadratic terms are included in GDX Jacobian file.
False by default.
GDXUELs : Optional[bool]
Controls whether Universal Element List (UEL) information is included in GDX Jacobian file.
True by default.
GAMSInsert : Optional[str]
Allows the insertion of custom GAMS code into the model.
HeaderTimeStamp : Optional[bool]
Specifies a timestamp to include in the header of the output file.
GDXIntervalEval : Optional[bool]
Controls the inclusion of interval evaluation (symbols `A_int` and `e_int`) into the GDX Jacobian format.
False by default.
GAMSObjVar : Optional[str]
Specifies the name of the objective variable in the GAMS scalar model.
PermuteEqus : Optional[bool]
Enables or disables the permutation of equations.
False by default.
PermuteVars : Optional[bool]
Enables or disables the permutation of variables.
False by default.
QExtractAlg : Optional[int]
Specifies the algorithm used for quadratic extraction.
* **0**: Automatic
* **1**: ThreePass: Uses a three-pass forward / backward / forward AD technique to compute function / gradient / Hessian values and a hybrid scheme for storage.
* **2**: DoubleForward: Uses forward-mode AD to compute and store function, gradient, and Hessian values at each node or stack level as required. The gradients and Hessians are stored in linked lists.
* **3**: Concurrent: Uses ThreePass and DoubleForward in parallel. As soon as one finishes, the other one stops.
0 by default.
Reform : Optional[int]
Controls the reformulation of certain structures in the model.
0: No reformulation, 1: Apply reformulation.
SkipNRows : Optional[int]
Skip constraints of type `NONBINDING`.
Width : Optional[int]
Sets the width for certain output formats, such as tables or reports.
Examples
--------
>>> import gamspy as gp
>>> m = gp.Container()
>>> i = gp.Set(m, "i", records=["i1", "i2"])
>>> x = gp.Variable(m, "x", domain=i)
>>> eq = gp.Equation(m, "eq", domain=i)
>>> eq[i] = x[i] >= 1
>>> model = gp.Model(m, "test_model", equations=[eq], problem="LP", sense="min", objective=gp.Sum(i, x[i]))
>>> options = gp.ConvertOptions(GDXNames=False)
>>> model.convert("jacobian", file_format=gp.FileFormat.GDXJacobian, options=options) # doctest: +SKIP
"""
model_config = ConfigDict(extra="forbid")
AmplNLBin: bool | None = None
AmplNlInitDual: int | None = None
AmplNlInitPrimal: int | None = None
GDXHessian: bool | None = None
GDXNames: bool | None = None
GDXQuadratic: bool | None = None
GDXUELs: bool | None = None
GAMSInsert: str | None = None
HeaderTimeStamp: bool | None = None
GDXIntervalEval: bool | None = None
GAMSObjVar: str | None = None
PermuteEqus: bool | None = None
PermuteVars: bool | None = None
QExtractAlg: int | None = None
Reform: int | None = None
SkipNRows: int | None = None
Width: int | None = None