# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2024
# National Technology and Engineering Solutions of Sandia, LLC
# Under the terms of Contract DE-NA0003525 with National Technology and
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
# rights in this software.
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________
import logging
import sys
from os import stat
from abc import ABC, abstractmethod
from io import StringIO
from pyomo.opt.base import ProblemFormat, ResultsFormat, OptSolver
from pyomo.opt.base.solvers import SolverFactory
from pyomo.common.collections import Bunch
from pyomo.common.dependencies import attempt_import
from pyomo.opt.results import (
SolverResults,
SolverStatus,
TerminationCondition,
SolutionStatus,
ProblemSense,
)
from pyomo.common.tempfiles import TempfileManager
from pyomo.core.base import Var
from pyomo.core.base.block import BlockData
from pyomo.core.kernel.block import IBlock
from pyomo.common.log import LogStream
from pyomo.common.tee import capture_output, TeeStream
uuid, uuid_available = attempt_import('uuid')
logger = logging.getLogger("pyomo.solvers")
STATUS_TO_SOLVERSTATUS = {
"OK": SolverStatus.ok,
"SYNTAX_ERROR": SolverStatus.error,
"DATA_ERROR": SolverStatus.error,
"OUT_OF_MEMORY": SolverStatus.aborted,
"IO_ERROR": SolverStatus.error,
"ERROR": SolverStatus.error,
}
# This combines all status codes from OPTLP/solvelp and OPTMILP/solvemilp
SOLSTATUS_TO_TERMINATIONCOND = {
"OPTIMAL": TerminationCondition.optimal,
"OPTIMAL_AGAP": TerminationCondition.optimal,
"OPTIMAL_RGAP": TerminationCondition.optimal,
"OPTIMAL_COND": TerminationCondition.optimal,
"TARGET": TerminationCondition.optimal,
"CONDITIONAL_OPTIMAL": TerminationCondition.optimal,
"FEASIBLE": TerminationCondition.feasible,
"INFEASIBLE": TerminationCondition.infeasible,
"UNBOUNDED": TerminationCondition.unbounded,
"INFEASIBLE_OR_UNBOUNDED": TerminationCondition.infeasibleOrUnbounded,
"SOLUTION_LIM": TerminationCondition.maxEvaluations,
"NODE_LIM_SOL": TerminationCondition.maxEvaluations,
"NODE_LIM_NOSOL": TerminationCondition.maxEvaluations,
"ITERATION_LIMIT_REACHED": TerminationCondition.maxIterations,
"TIME_LIM_SOL": TerminationCondition.maxTimeLimit,
"TIME_LIM_NOSOL": TerminationCondition.maxTimeLimit,
"TIME_LIMIT_REACHED": TerminationCondition.maxTimeLimit,
"ABORTED": TerminationCondition.userInterrupt,
"ABORT_SOL": TerminationCondition.userInterrupt,
"ABORT_NOSOL": TerminationCondition.userInterrupt,
"OUTMEM_SOL": TerminationCondition.solverFailure,
"OUTMEM_NOSOL": TerminationCondition.solverFailure,
"FAILED": TerminationCondition.solverFailure,
"FAIL_SOL": TerminationCondition.solverFailure,
"FAIL_NOSOL": TerminationCondition.solverFailure,
}
SOLSTATUS_TO_MESSAGE = {
"OPTIMAL": "The solution is optimal.",
"OPTIMAL_AGAP": "The solution is optimal within the absolute gap specified by the ABSOBJGAP= option.",
"OPTIMAL_RGAP": "The solution is optimal within the relative gap specified by the RELOBJGAP= option.",
"OPTIMAL_COND": "The solution is optimal, but some infeasibilities (primal, bound, or integer) exceed tolerances due to scaling or choice of a small INTTOL= value.",
"TARGET": "The solution is not worse than the target specified by the TARGET= option.",
"CONDITIONAL_OPTIMAL": "The solution is optimal, but some infeasibilities (primal, dual or bound) exceed tolerances due to scaling or preprocessing.",
"FEASIBLE": "The problem is feasible. This status is displayed when the IIS=TRUE option is specified and the problem is feasible.",
"INFEASIBLE": "The problem is infeasible.",
"UNBOUNDED": "The problem is unbounded.",
"INFEASIBLE_OR_UNBOUNDED": "The problem is infeasible or unbounded.",
"SOLUTION_LIM": "The solver reached the maximum number of solutions specified by the MAXSOLS= option.",
"NODE_LIM_SOL": "The solver reached the maximum number of nodes specified by the MAXNODES= option and found a solution.",
"NODE_LIM_NOSOL": "The solver reached the maximum number of nodes specified by the MAXNODES= option and did not find a solution.",
"ITERATION_LIMIT_REACHED": "The maximum allowable number of iterations was reached.",
"TIME_LIM_SOL": "The solver reached the execution time limit specified by the MAXTIME= option and found a solution.",
"TIME_LIM_NOSOL": "The solver reached the execution time limit specified by the MAXTIME= option and did not find a solution.",
"TIME_LIMIT_REACHED": "The solver reached its execution time limit.",
"ABORTED": "The solver was interrupted externally.",
"ABORT_SOL": "The solver was stopped by the user but still found a solution.",
"ABORT_NOSOL": "The solver was stopped by the user and did not find a solution.",
"OUTMEM_SOL": "The solver ran out of memory but still found a solution.",
"OUTMEM_NOSOL": "The solver ran out of memory and either did not find a solution or failed to output the solution due to insufficient memory.",
"FAILED": "The solver failed to converge, possibly due to numerical issues.",
"FAIL_SOL": "The solver stopped due to errors but still found a solution.",
"FAIL_NOSOL": "The solver stopped due to errors and did not find a solution.",
}
[docs]
@SolverFactory.register("sas", doc="The SAS LP/MIP solver")
class SAS(OptSolver):
"""The SAS optimization solver"""
def __new__(cls, *args, **kwds):
mode = kwds.pop("solver_io", None)
if mode != None:
return SolverFactory(mode, **kwds)
else:
# Choose solver factory automatically
# based on what can be loaded.
s = SolverFactory("_sas94", **kwds)
if not s.available():
s = SolverFactory("_sascas", **kwds)
return s
[docs]
class SASAbc(ABC, OptSolver):
"""Abstract base class for the SAS solver interfaces. Simply to avoid code duplication."""
[docs]
def __init__(self, **kwds):
"""Initialize the SAS solver interfaces."""
kwds["type"] = "sas"
super(SASAbc, self).__init__(**kwds)
#
# Set up valid problem formats and valid results for each
# problem format
#
self._valid_problem_formats = [ProblemFormat.mps]
self._valid_result_formats = {ProblemFormat.mps: [ResultsFormat.soln]}
self._keepfiles = False
self._capabilities.linear = True
self._capabilities.integer = True
super(SASAbc, self).set_problem_format(ProblemFormat.mps)
def _presolve(self, *args, **kwds):
"""Set things up for the actual solve."""
# create a context in the temporary file manager for
# this plugin - is "pop"ed in the _postsolve method.
TempfileManager.push()
# Get the warmstart flag
self.warmstart_flag = kwds.pop("warmstart", False)
# Call parent presolve function
super(SASAbc, self)._presolve(*args, **kwds)
# Store the model, too bad this is not done in the base class
for arg in args:
if isinstance(arg, (BlockData, IBlock)):
# Store the instance
self._instance = arg
self._vars = []
for block in self._instance.block_data_objects(active=True):
for vardata in block.component_data_objects(
Var, active=True, descend_into=False
):
self._vars.append(vardata)
# Store the symbol map, we need this for example when writing the warmstart file
if isinstance(self._instance, IBlock):
self._smap = getattr(self._instance, "._symbol_maps")[self._smap_id]
else:
self._smap = self._instance.solutions.symbol_map[self._smap_id]
# Create the primalin data
if self.warmstart_flag:
filename = self._warm_start_file_name = TempfileManager.create_tempfile(
".sol", text=True
)
smap = self._smap
numWritten = 0
with open(filename, "w") as file:
file.write("_VAR_,_VALUE_\n")
for var in self._vars:
if (var.value is not None) and (id(var) in smap.byObject):
name = smap.byObject[id(var)]
file.write(
"{name},{value}\n".format(name=name, value=var.value)
)
numWritten += 1
if numWritten == 0:
# No solution available, disable warmstart
self.warmstart_flag = False
[docs]
def available(self, exception_flag=False):
"""True if the solver is available"""
if not self._python_api_exists:
return False
return self.start_sas_session() is not None
def _has_integer_variables(self):
"""True if the problem has integer variables."""
for vardata in self._vars:
if vardata.is_binary() or vardata.is_integer():
return True
return False
def _create_results_from_status(self, status, solution_status):
"""Create a results object and set the status code and messages."""
results = SolverResults()
results.solver.name = "SAS"
results.solver.status = STATUS_TO_SOLVERSTATUS[status]
results.solver.hasSolution = False
if results.solver.status == SolverStatus.ok:
results.solver.termination_condition = SOLSTATUS_TO_TERMINATIONCOND[
solution_status
]
results.solver.message = results.solver.termination_message = (
SOLSTATUS_TO_MESSAGE[solution_status]
)
results.solver.status = TerminationCondition.to_solver_status(
results.solver.termination_condition
)
if "OPTIMAL" in solution_status or "_SOL" in solution_status:
results.solver.hasSolution = True
elif results.solver.status == SolverStatus.aborted:
results.solver.termination_condition = TerminationCondition.userInterrupt
if solution_status != "ERROR":
results.solver.message = results.solver.termination_message = (
SOLSTATUS_TO_MESSAGE[solution_status]
)
else:
results.solver.termination_condition = TerminationCondition.error
results.solver.message = results.solver.termination_message = (
SOLSTATUS_TO_MESSAGE["FAILED"]
)
return results
@abstractmethod
def _apply_solver(self):
pass
def _postsolve(self):
"""Clean up at the end, especially the temp files."""
# Let the base class deal with returning results.
results = super(SASAbc, self)._postsolve()
# Finally, clean any temporary files registered with the temp file
# manager, created populated *directly* by this plugin. does not
# include, for example, the execution script. but does include
# the warm-start file.
TempfileManager.pop(remove=not self._keepfiles)
return results
[docs]
def warm_start_capable(self):
"""True if the solver interface supports MILP warmstarting."""
return True
[docs]
@SolverFactory.register("_sas94", doc="SAS 9.4 interface")
class SAS94(SASAbc):
"""
Solver interface for SAS 9.4 using saspy. See the saspy documentation about
how to create a connection.
The swat connection options can be specified on the SolverFactory call.
"""
[docs]
def __init__(self, **kwds):
"""Initialize the solver interface and see if the saspy package is available."""
super(SAS94, self).__init__(**kwds)
try:
import saspy
self._sas = saspy
except ImportError:
self._python_api_exists = False
except Exception as e:
self._python_api_exists = False
# For other exceptions, raise it so that it does not get lost
raise e
else:
self._python_api_exists = True
self._sas.logger.setLevel(logger.level)
# Store other options for the SAS session
self._session_options = kwds
self._sas_session = None
def __del__(self):
# Close the session, if we created one
if self._sas_session:
self._sas_session.endsas()
del self._sas_session
def _create_statement_str(self, statement):
"""Helper function to create the strings for the statements of the proc OPTLP/OPTMILP code."""
stmt = self.options.pop(statement, None)
if stmt:
return (
statement.strip()
+ " "
+ " ".join(option + "=" + str(value) for option, value in stmt.items())
+ ";"
)
else:
return ""
def sas_version(self):
return self._sasver
def start_sas_session(self):
if self._sas_session is None:
# Create (and cache) the session
try:
self._sas_session = self._sas.SASsession(**self._session_options)
except:
pass
return self._sas_session
def _apply_solver(self):
""" "Prepare the options and run the solver. Then store the data to be returned."""
logger.debug("Running SAS")
# Set return code to issue an error if we get interrupted
self._rc = -1
# Figure out if the problem has integer variables
with_opt = self.options.pop("with", None)
if with_opt == "lp":
proc = "OPTLP"
elif with_opt == "milp":
proc = "OPTMILP"
else:
# Check if there are integer variables, this might be slow
proc = "OPTMILP" if self._has_integer_variables() else "OPTLP"
# Get the rootnode options
decomp_str = self._create_statement_str("decomp")
decompmaster_str = self._create_statement_str("decompmaster")
decompmasterip_str = self._create_statement_str("decompmasterip")
decompsubprob_str = self._create_statement_str("decompsubprob")
rootnode_str = self._create_statement_str("rootnode")
# Get a unique identifier, always use the same with different prefixes
unique = uuid.uuid4().hex[:16]
# Create unique filename for output datasets
primalout_dataset_name = "pout" + unique
dualout_dataset_name = "dout" + unique
primalin_dataset_name = None
# Handle warmstart
warmstart_str = ""
if self.warmstart_flag:
# Set the warmstart basis option
primalin_dataset_name = "pin" + unique
if proc != "OPTLP":
warmstart_str = """
proc import datafile='{primalin}'
out={primalin_dataset_name}
dbms=csv
replace;
getnames=yes;
run;
""".format(
primalin=self._warm_start_file_name,
primalin_dataset_name=primalin_dataset_name,
)
self.options["primalin"] = primalin_dataset_name
# Convert options to string
opt_str = " ".join(
option + "=" + str(value) for option, value in self.options.items()
)
# Set some SAS options to make the log more clean
sas_options = "option notes nonumber nodate nosource pagesize=max;"
# Get the current SAS session, submit the code and return the results
sas = self.start_sas_session()
# Find the version of 9.4 we are using
self._sasver = sas.sasver
# Upload files, only if not accessible locally
upload_mps = False
if not sas.file_info(self._problem_files[0], quiet=True):
sas.upload(self._problem_files[0], self._problem_files[0], overwrite=True)
upload_mps = True
upload_pin = False
if self.warmstart_flag and not sas.file_info(
self._warm_start_file_name, quiet=True
):
sas.upload(
self._warm_start_file_name, self._warm_start_file_name, overwrite=True
)
upload_pin = True
# Using a function call to make it easier to mock the version check
major_version = self.sas_version()[0]
minor_version = self.sas_version().split("M", 1)[1][0]
if major_version == "9" and int(minor_version) < 5:
raise NotImplementedError(
"Support for SAS 9.4 M4 and earlier is not implemented."
)
elif major_version == "9" and int(minor_version) == 5:
# In 9.4M5 we have to create an MPS data set from an MPS file first
# Earlier versions will not work because the MPS format in incompatible
mps_dataset_name = "mps" + unique
res = sas.submit(
"""
{sas_options}
{warmstart}
%MPS2SASD(MPSFILE="{mpsfile}", OUTDATA={mps_dataset_name}, MAXLEN=256, FORMAT=FREE);
proc {proc} data={mps_dataset_name} {options} primalout={primalout_dataset_name} dualout={dualout_dataset_name};
{decomp}
{decompmaster}
{decompmasterip}
{decompsubprob}
{rootnode}
run;
""".format(
sas_options=sas_options,
warmstart=warmstart_str,
proc=proc,
mpsfile=self._problem_files[0],
mps_dataset_name=mps_dataset_name,
options=opt_str,
primalout_dataset_name=primalout_dataset_name,
dualout_dataset_name=dualout_dataset_name,
decomp=decomp_str,
decompmaster=decompmaster_str,
decompmasterip=decompmasterip_str,
decompsubprob=decompsubprob_str,
rootnode=rootnode_str,
),
results="TEXT",
)
sas.sasdata(mps_dataset_name).delete(quiet=True)
else:
# Since 9.4M6+ optlp/optmilp can read mps files directly (this includes Viya-based local installs)
res = sas.submit(
"""
{sas_options}
{warmstart}
proc {proc} mpsfile=\"{mpsfile}\" {options} primalout={primalout_dataset_name} dualout={dualout_dataset_name};
{decomp}
{decompmaster}
{decompmasterip}
{decompsubprob}
{rootnode}
run;
""".format(
sas_options=sas_options,
warmstart=warmstart_str,
proc=proc,
mpsfile=self._problem_files[0],
options=opt_str,
primalout_dataset_name=primalout_dataset_name,
dualout_dataset_name=dualout_dataset_name,
decomp=decomp_str,
decompmaster=decompmaster_str,
decompmasterip=decompmasterip_str,
decompsubprob=decompsubprob_str,
rootnode=rootnode_str,
),
results="TEXT",
)
# Delete uploaded file
if upload_mps:
sas.file_delete(self._problem_files[0], quiet=True)
if self.warmstart_flag and upload_pin:
sas.file_delete(self._warm_start_file_name, quiet=True)
# Store log and ODS output
self._log = res["LOG"]
self._lst = res["LST"]
if "ERROR 22-322: Syntax error" in self._log:
raise ValueError(
"An option passed to the SAS solver caused a syntax error: {log}".format(
log=self._log
)
)
else:
# Print log if requested by the user, only if we did not already print it
if self._tee:
print(self._log)
self._macro = dict(
(key.strip(), value.strip())
for key, value in (
pair.split("=") for pair in sas.symget("_OR" + proc + "_").split()
)
)
if self._macro.get("STATUS", "ERROR") == "OK":
primal_out = sas.sd2df(primalout_dataset_name)
dual_out = sas.sd2df(dualout_dataset_name)
# Delete data sets, they will go away automatically, but does not hurt to delete them
if primalin_dataset_name:
sas.sasdata(primalin_dataset_name).delete(quiet=True)
sas.sasdata(primalout_dataset_name).delete(quiet=True)
sas.sasdata(dualout_dataset_name).delete(quiet=True)
# Prepare the solver results
results = self.results = self._create_results_from_status(
self._macro.get("STATUS", "ERROR"),
self._macro.get("SOLUTION_STATUS", "ERROR"),
)
if "Objective Sense Maximization" in self._lst:
results.problem.sense = ProblemSense.maximize
else:
results.problem.sense = ProblemSense.minimize
# Prepare the solution information
if results.solver.hasSolution:
sol = results.solution.add()
# Store status in solution
sol.status = SolutionStatus.feasible
sol.termination_condition = SOLSTATUS_TO_TERMINATIONCOND[
self._macro.get("SOLUTION_STATUS", "ERROR")
]
# Store objective value in solution
sol.objective["__default_objective__"] = {"Value": self._macro["OBJECTIVE"]}
if proc == "OPTLP":
# Convert primal out data set to variable dictionary
# Use pandas functions for efficiency
primal_out = primal_out[["_VAR_", "_VALUE_", "_STATUS_", "_R_COST_"]]
primal_out = primal_out.set_index("_VAR_", drop=True)
primal_out = primal_out.rename(
{"_VALUE_": "Value", "_STATUS_": "Status", "_R_COST_": "rc"},
axis="columns",
)
sol.variable = primal_out.to_dict("index")
# Convert dual out data set to constraint dictionary
# Use pandas functions for efficiency
dual_out = dual_out[["_ROW_", "_VALUE_", "_STATUS_", "_ACTIVITY_"]]
dual_out = dual_out.set_index("_ROW_", drop=True)
dual_out = dual_out.rename(
{"_VALUE_": "dual", "_STATUS_": "Status", "_ACTIVITY_": "slack"},
axis="columns",
)
sol.constraint = dual_out.to_dict("index")
else:
# Convert primal out data set to variable dictionary
# Use pandas functions for efficiency
primal_out = primal_out[["_VAR_", "_VALUE_"]]
primal_out = primal_out.set_index("_VAR_", drop=True)
primal_out = primal_out.rename({"_VALUE_": "Value"}, axis="columns")
sol.variable = primal_out.to_dict("index")
self._rc = 0
return Bunch(rc=self._rc, log=self._log)
[docs]
@SolverFactory.register("_sascas", doc="SAS Viya CAS Server interface")
class SASCAS(SASAbc):
"""
Solver interface connection to a SAS Viya CAS server using swat.
See the documentation for the swat package about how to create a connection.
The swat connection options can be specified on the SolverFactory call.
"""
[docs]
def __init__(self, **kwds):
"""Initialize and try to load the swat package."""
super(SASCAS, self).__init__(**kwds)
try:
import swat
self._sas = swat
except ImportError:
self._python_api_exists = False
except Exception as e:
self._python_api_exists = False
# For other exceptions, raise it so that it does not get lost
raise e
else:
self._python_api_exists = True
self._session_options = kwds
self._sas_session = None
def __del__(self):
# Close the session, if we created one
if self._sas_session:
self._sas_session.close()
del self._sas_session
def start_sas_session(self):
if self._sas_session is None:
# Create (and cache) the session
try:
self._sas_session = self._sas.CAS(**self._session_options)
except:
pass
return self._sas_session
def _uploadMpsFile(self, s, unique):
# Declare a unique table name for the mps table
mpsdata_table_name = "mps" + unique
# Upload mps file to CAS, if the file is larger than 2 GB, we need to use convertMps instead of loadMps
# Note that technically it is 2 Gibibytes file size that trigger the issue, but 2 GB is the safer threshold
if stat(self._problem_files[0]).st_size > 2e9:
# For files larger than 2 GB (this is a limitation of the loadMps action used in the else part).
# Use convertMPS, first create file for upload.
mpsWithIdFileName = TempfileManager.create_tempfile(".mps.csv", text=True)
with open(mpsWithIdFileName, "w") as mpsWithId:
mpsWithId.write("_ID_\tText\n")
with open(self._problem_files[0], "r") as f:
id = 0
for line in f:
id += 1
mpsWithId.write(str(id) + "\t" + line.rstrip() + "\n")
# Upload .mps.csv file
mpscsv_table_name = "csv" + unique
s.upload_file(
mpsWithIdFileName,
casout={"name": mpscsv_table_name, "replace": True},
importoptions={"filetype": "CSV", "delimiter": "\t"},
)
# Convert .mps.csv file to .mps
s.optimization.convertMps(
data=mpscsv_table_name,
casOut={"name": mpsdata_table_name, "replace": True},
format="FREE",
maxLength=256,
)
# Delete the table we don't need anymore
if mpscsv_table_name:
s.dropTable(name=mpscsv_table_name, quiet=True)
else:
# For small files (less than 2 GB), use loadMps
with open(self._problem_files[0], "r") as mps_file:
s.optimization.loadMps(
mpsFileString=mps_file.read(),
casout={"name": mpsdata_table_name, "replace": True},
format="FREE",
maxLength=256,
)
return mpsdata_table_name
def _uploadPrimalin(self, s, unique):
# Upload warmstart file to CAS with a unique name
primalin_table_name = "pin" + unique
s.upload_file(
self._warm_start_file_name,
casout={"name": primalin_table_name, "replace": True},
importoptions={"filetype": "CSV"},
)
self.options["primalin"] = primalin_table_name
return primalin_table_name
def _retrieveSolution(
self, s, r, results, action, primalout_table_name, dualout_table_name
):
# Create solution
sol = results.solution.add()
# Store status in solution
sol.status = SolutionStatus.feasible
sol.termination_condition = SOLSTATUS_TO_TERMINATIONCOND[
r.get("solutionStatus", "ERROR")
]
# Store objective value in solution
sol.objective["__default_objective__"] = {"Value": r["objective"]}
if action == "solveMilp":
primal_out = s.CASTable(name=primalout_table_name)
# Use pandas functions for efficiency
primal_out = primal_out[["_VAR_", "_VALUE_"]]
sol.variable = {}
for row in primal_out.itertuples(index=False):
sol.variable[row[0]] = {"Value": row[1]}
else:
# Convert primal out data set to variable dictionary
# Use panda functions for efficiency
primal_out = s.CASTable(name=primalout_table_name)
primal_out = primal_out[["_VAR_", "_VALUE_", "_STATUS_", "_R_COST_"]]
sol.variable = {}
for row in primal_out.itertuples(index=False):
sol.variable[row[0]] = {"Value": row[1], "Status": row[2], "rc": row[3]}
# Convert dual out data set to constraint dictionary
# Use pandas functions for efficiency
dual_out = s.CASTable(name=dualout_table_name)
dual_out = dual_out[["_ROW_", "_VALUE_", "_STATUS_", "_ACTIVITY_"]]
sol.constraint = {}
for row in dual_out.itertuples(index=False):
sol.constraint[row[0]] = {
"dual": row[1],
"Status": row[2],
"slack": row[3],
}
def _apply_solver(self):
""" "Prepare the options and run the solver. Then store the data to be returned."""
logger.debug("Running SAS Viya")
# Set return code to issue an error if we get interrupted
self._rc = -1
# Figure out if the problem has integer variables
with_opt = self.options.pop("with", None)
if with_opt == "lp":
action = "solveLp"
elif with_opt == "milp":
action = "solveMilp"
else:
# Check if there are integer variables, this might be slow
action = "solveMilp" if self._has_integer_variables() else "solveLp"
# Get a unique identifier, always use the same with different prefixes
unique = uuid.uuid4().hex[:16]
# Creat the output stream, we want to print to a log string as well as to the console
self._log = StringIO()
ostreams = [LogStream(level=logging.INFO, logger=logger)]
ostreams.append(self._log)
if self._tee:
ostreams.append(sys.stdout)
# Connect to CAS server
with TeeStream(*ostreams) as t:
with capture_output(output=t.STDOUT, capture_fd=False):
s = self._sas_session
if s == None:
s = self._sas_session = self._sas.CAS(**self._session_options)
try:
# Load the optimization action set
s.loadactionset("optimization")
mpsdata_table_name = self._uploadMpsFile(s, unique)
primalin_table_name = None
if self.warmstart_flag:
primalin_table_name = self._uploadPrimalin(s, unique)
# Define output table names
primalout_table_name = "pout" + unique
dualout_table_name = None
# Solve the problem in CAS
if action == "solveMilp":
r = s.optimization.solveMilp(
data={"name": mpsdata_table_name},
primalOut={"name": primalout_table_name, "replace": True},
**self.options
)
else:
dualout_table_name = "dout" + unique
r = s.optimization.solveLp(
data={"name": mpsdata_table_name},
primalOut={"name": primalout_table_name, "replace": True},
dualOut={"name": dualout_table_name, "replace": True},
**self.options
)
# Prepare the solver results
if r:
# Get back the primal and dual solution data sets
results = self.results = self._create_results_from_status(
r.get("status", "ERROR"), r.get("solutionStatus", "ERROR")
)
if results.solver.status != SolverStatus.error:
if r.ProblemSummary["cValue1"][1] == "Maximization":
results.problem.sense = ProblemSense.maximize
else:
results.problem.sense = ProblemSense.minimize
# Prepare the solution information
if results.solver.hasSolution:
self._retrieveSolution(
s,
r,
results,
action,
primalout_table_name,
dualout_table_name,
)
else:
raise ValueError("The SAS solver returned an error status.")
else:
results = self.results = SolverResults()
results.solver.name = "SAS"
results.solver.status = SolverStatus.error
raise ValueError(
"An option passed to the SAS solver caused a syntax error."
)
finally:
if mpsdata_table_name:
s.dropTable(name=mpsdata_table_name, quiet=True)
if primalin_table_name:
s.dropTable(name=primalin_table_name, quiet=True)
if primalout_table_name:
s.dropTable(name=primalout_table_name, quiet=True)
if dualout_table_name:
s.dropTable(name=dualout_table_name, quiet=True)
self._log = self._log.getvalue()
self._rc = 0
return Bunch(rc=self._rc, log=self._log)