# ____________________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2026 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 os
# import os.path
import subprocess
from pyomo.common import Executable
from pyomo.common.collections import Bunch
from pyomo.common.tempfiles import TempfileManager
from pyomo.opt.base import ProblemFormat, ResultsFormat
from pyomo.opt.base.solvers import _extract_version, SolverFactory
from pyomo.opt.results import SolverStatus, TerminationCondition, SolutionStatus
from pyomo.opt.solver import SystemCallSolver
import logging
logger = logging.getLogger('pyomo.solvers')
[docs]
@SolverFactory.register('scip', doc='The SCIP LP/MIP solver')
class SCIPAMPL(SystemCallSolver):
"""A generic optimizer that uses the AMPL Solver Library to interface with applications."""
# Cache default executable, so we do not need to repeatedly query the
# versions every time.
_known_versions = {}
[docs]
def __init__(self, **kwds):
#
# Call base constructor
#
kwds["type"] = "scip"
SystemCallSolver.__init__(self, **kwds)
#
# Setup valid problem formats, and valid results for each problem format
# Also set the default problem and results formats.
#
self._valid_problem_formats = [ProblemFormat.nl]
self._valid_result_formats = {}
self._valid_result_formats[ProblemFormat.nl] = [ResultsFormat.sol]
self.set_problem_format(ProblemFormat.nl)
# Note: Undefined capabilities default to 'None'
self._capabilities = Bunch()
self._capabilities.linear = True
self._capabilities.integer = True
self._capabilities.quadratic_objective = True
self._capabilities.quadratic_constraint = True
self._capabilities.sos1 = True
self._capabilities.sos2 = True
def _default_results_format(self, prob_format):
return ResultsFormat.sol
def _default_executable(self):
executable = Executable("scip")
if executable:
executable_path = executable.path()
if executable_path not in self._known_versions:
self._known_versions[executable_path] = self._get_version(
executable_path
)
_ver = self._known_versions[executable_path]
if _ver and _ver >= (8,):
return executable_path
# revert to scipampl for older versions
executable = Executable("scipampl")
if not executable:
logger.warning(
"Could not locate the 'scip' executable or"
" the older 'scipampl' executable, which is "
"required for solver %s" % self.name
)
self.enable = False
return None
return executable.path()
def _get_version(self, solver_exec=None):
"""
Returns a tuple describing the solver executable version.
"""
if solver_exec is None:
solver_exec = self.executable()
if solver_exec is None:
return _extract_version('')
results = subprocess.run(
[solver_exec, "--version"],
timeout=self._version_timeout,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
)
return _extract_version(results.stdout)
[docs]
def create_command_line(self, executable, problem_files):
assert self._problem_format == ProblemFormat.nl
assert self._results_format == ResultsFormat.sol
#
# Define log file
#
if self._log_file is None:
self._log_file = TempfileManager.create_tempfile(suffix="_scip.log")
fname = problem_files[0]
if '.' in fname:
tmp = fname.split('.')
if len(tmp) > 2:
fname = '.'.join(tmp[:-1])
else:
fname = tmp[0]
self._soln_file = fname + ".sol"
#
# Define results file (since an external parser is used)
#
self._results_file = self._soln_file
#
# Define command line
#
env = os.environ.copy()
#
# Merge the PYOMO_AMPLFUNC (externals defined within
# Pyomo/Pyomo) with any user-specified external function
# libraries
#
if 'PYOMO_AMPLFUNC' in env:
if 'AMPLFUNC' in env:
env['AMPLFUNC'] += "\n" + env['PYOMO_AMPLFUNC']
else:
env['AMPLFUNC'] = env['PYOMO_AMPLFUNC']
# Since Version 8.0.0 .nl problem file paths should be provided without the .nl
# extension
if executable not in self._known_versions:
self._known_versions[executable] = self._get_version(executable)
_ver = self._known_versions[executable]
if _ver and _ver >= (8, 0, 0):
problem_file = os.path.splitext(problem_files[0])[0]
else:
problem_file = problem_files[0]
cmd = [executable, problem_file, '-AMPL']
if self._timer:
cmd.insert(0, self._timer)
# GAH: I am going to re-add the code by Zev that passed options through
# to the command line. I'm not sure what solvers this method of passing options
# through the envstr variable works for, but it does not seem to work for cplex
# or gurobi
env_opt = []
of_opt = []
for key in self.options:
if key == 'solver':
continue
if isinstance(self.options[key], str) and ' ' in self.options[key]:
env_opt.append(key + "=\"" + str(self.options[key]) + "\"")
else:
env_opt.append(key + "=" + str(self.options[key]))
of_opt.append(str(key) + " = " + str(self.options[key]))
if (
self._timelimit is not None
and self._timelimit > 0.0
and 'limits/time' not in self.options
):
of_opt.append("limits/time = " + str(self._timelimit))
envstr = "%s_options" % self.options.solver
# Merge with any options coming in through the environment
env[envstr] = " ".join(env_opt)
if len(of_opt) > 0:
# Now check if an 'scip.set' file exists in the
# current working directory. If so, we need to
# make it clear that this file will be ignored.
default_of_name = os.path.join(os.getcwd(), 'scip.set')
if os.path.exists(default_of_name):
logger.warning(
"A file named '%s' exists in "
"the current working directory, but "
"SCIP options are being set using a "
"separate options file. The options "
"file '%s' will be ignored." % (default_of_name, default_of_name)
)
options_dir = TempfileManager.create_tempdir()
# Now write the new options file
with open(os.path.join(options_dir, 'scip.set'), 'w') as f:
for line in of_opt:
f.write(line + "\n")
else:
options_dir = None
return Bunch(cmd=cmd, log_file=self._log_file, env=env, cwd=options_dir)
def _postsolve(self):
# find SCIP version (calling version() and _get_version() mess things)
executable = self._command.cmd[0]
version = self._known_versions[executable]
if version < (8, 0, 0, 0):
# it may be possible to get results from older version but this was
# not tested, so the old way of doing things is here preserved
results = super(SCIPAMPL, self)._postsolve()
else:
# repeat code from super(SCIPAMPL, self)._postsolve()
# in order to access the log file and get the results from there
if self._log_file is not None:
OUTPUT = open(self._log_file, "w")
OUTPUT.write("Solver command line: " + str(self._command.cmd) + '\n')
OUTPUT.write("\n")
OUTPUT.write(self._log + '\n')
OUTPUT.close()
# JPW: The cleanup of the problem file probably shouldn't be here, but
# rather in the base OptSolver class. That would require movement of
# the keepfiles attribute and associated cleanup logic to the base
# class, which I didn't feel like doing at this present time. the
# base class remove_files method should clean up the problem file.
if (self._log_file is not None) and (not os.path.exists(self._log_file)):
msg = "File '%s' not generated while executing %s"
raise IOError(msg % (self._log_file, self.path))
results = None
if self._results_format is not None:
results = self.process_output(self._rc)
# read results from the log file
log_dict = self.read_scip_log(self._log_file)
if len(log_dict) != 0:
# if any were read, store them
results.solver.time = log_dict['solving_time']
results.solver.gap = log_dict['gap']
results.solver.node_count = log_dict['solving_nodes']
results.solver.primal_bound = log_dict['primal_bound']
results.solver.dual_bound = log_dict['dual_bound']
# TODO: get scip to produce a statistics file and read it
# Why? It has all the information one can possibly need.
#
# If keepfiles is true, then we pop the
# TempfileManager context while telling it to
# _not_ remove the files.
#
if not self._keepfiles:
# in some cases, the solution filename is
# not generated via the temp-file mechanism,
# instead being automatically derived from
# the input lp/nl filename. so, we may have
# to clean it up manually.
if (not self._soln_file is None) and os.path.exists(
self._soln_file
):
os.remove(self._soln_file)
TempfileManager.pop(remove=not self._keepfiles)
# **********************************************************************
# **********************************************************************
# UNKNOWN # unknown='unknown' # An uninitialized value
if "unknown" in results.solver.message:
results.solver.status = SolverStatus.unknown
results.solver.termination_condition = TerminationCondition.unknown
if len(results.solution) > 0:
results.solution(0).status = SolutionStatus.unknown
# ABORTED # userInterrupt='userInterrupt' # Interrupt signal generated by user
elif "user interrupt" in results.solver.message:
results.solver.status = SolverStatus.aborted
results.solver.termination_condition = TerminationCondition.userInterrupt
if len(results.solution) > 0:
results.solution(0).status = SolutionStatus.unknown
# OK # maxEvaluations='maxEvaluations' # Exceeded maximum number of problem evaluations
elif "node limit reached" in results.solver.message:
results.solver.status = SolverStatus.ok
results.solver.termination_condition = TerminationCondition.maxEvaluations
if len(results.solution) > 0:
results.solution(0).status = SolutionStatus.stoppedByLimit
# OK # maxEvaluations='maxEvaluations' # Exceeded maximum number of problem evaluations
elif "total node limit reached" in results.solver.message:
results.solver.status = SolverStatus.ok
results.solver.termination_condition = TerminationCondition.maxEvaluations
if len(results.solution) > 0:
results.solution(0).status = SolutionStatus.stoppedByLimit
# OK # maxEvaluations='maxEvaluations' # Exceeded maximum number of problem evaluations
elif "stall node limit reached" in results.solver.message:
results.solver.status = SolverStatus.ok
results.solver.termination_condition = TerminationCondition.maxEvaluations
if len(results.solution) > 0:
results.solution(0).status = SolutionStatus.stoppedByLimit
# OK # maxTimeLimit='maxTimeLimit' # Exceeded maximum time limited allowed by user but having return a feasible solution
elif "time limit reached" in results.solver.message:
results.solver.status = SolverStatus.ok
results.solver.termination_condition = TerminationCondition.maxTimeLimit
if len(results.solution) > 0:
results.solution(0).status = SolutionStatus.stoppedByLimit
# OK # other='other' # Other, uncategorized normal termination
elif "memory limit reached" in results.solver.message:
results.solver.status = SolverStatus.ok
results.solver.termination_condition = TerminationCondition.other
if len(results.solution) > 0:
results.solution(0).status = SolutionStatus.stoppedByLimit
# OK # other='other' # Other, uncategorized normal termination
elif "gap limit reached" in results.solver.message:
results.solver.status = SolverStatus.ok
results.solver.termination_condition = TerminationCondition.other
if len(results.solution) > 0:
results.solution(0).status = SolutionStatus.stoppedByLimit
# OK # other='other' # Other, uncategorized normal termination
elif "solution limit reached" in results.solver.message:
results.solver.status = SolverStatus.ok
results.solver.termination_condition = TerminationCondition.other
if len(results.solution) > 0:
results.solution(0).status = SolutionStatus.stoppedByLimit
# OK # other='other' # Other, uncategorized normal termination
elif "solution improvement limit reached" in results.solver.message:
results.solver.status = SolverStatus.ok
results.solver.termination_condition = TerminationCondition.other
if len(results.solution) > 0:
results.solution(0).status = SolutionStatus.stoppedByLimit
# OK # optimal='optimal' # Found an optimal solution
elif "optimal solution" in results.solver.message:
results.solver.status = SolverStatus.ok
results.solver.termination_condition = TerminationCondition.optimal
if len(results.solution) > 0:
results.solution(0).status = SolutionStatus.optimal
try:
if results.solver.primal_bound < results.solver.dual_bound:
results.problem.lower_bound = results.solver.primal_bound
results.problem.upper_bound = results.solver.dual_bound
else:
results.problem.lower_bound = results.solver.dual_bound
results.problem.upper_bound = results.solver.primal_bound
except AttributeError:
"""
This may occur if SCIP solves the problem during presolve. In that case,
the log file may not get parsed correctly (self.read_scip_log), and
results.solver.primal_bound will not be populated.
"""
pass
# WARNING # infeasible='infeasible' # Demonstrated that the problem is infeasible
elif "infeasible" in results.solver.message:
results.solver.status = SolverStatus.warning
results.solver.termination_condition = TerminationCondition.infeasible
if len(results.solution) > 0:
results.solution(0).status = SolutionStatus.infeasible
# WARNING # unbounded='unbounded' # Demonstrated that problem is unbounded
elif "unbounded" in results.solver.message:
results.solver.status = SolverStatus.warning
results.solver.termination_condition = TerminationCondition.unbounded
if len(results.solution) > 0:
results.solution(0).status = SolutionStatus.unbounded
# WARNING # infeasibleOrUnbounded='infeasibleOrUnbounded' # Problem is either infeasible or unbounded
elif "infeasible or unbounded" in results.solver.message:
results.solver.status = SolverStatus.warning
results.solver.termination_condition = (
TerminationCondition.infeasibleOrUnbounded
)
if len(results.solution) > 0:
results.solution(0).status = SolutionStatus.unsure
# UNKNOWN # unknown='unknown' # An uninitialized value
else:
logger.warning(
"Unexpected SCIP solver message: %s" % (results.solver.message)
)
results.solver.status = SolverStatus.unknown
results.solver.termination_condition = TerminationCondition.unknown
if len(results.solution) > 0:
results.solution(0).status = SolutionStatus.unknown
return results
@staticmethod
def read_scip_log(filename: str):
# TODO: check file exists, ensure opt has finished, etc
from collections import deque
with open(filename) as f:
scip_lines = list(deque(f, 7))
scip_lines.pop()
expected_labels = [
'SCIP Status :',
'Solving Time (sec) :',
'Solving Nodes :',
'Primal Bound :',
'Dual Bound :',
'Gap :',
]
colon_position = 19 # or scip_lines[0].index(':')
for i, log_file_line in enumerate(scip_lines):
if expected_labels[i] != log_file_line[0 : colon_position + 1]:
return {}
# get data
solver_status = scip_lines[0][colon_position + 2 : scip_lines[0].index('\n')]
solving_time = float(
scip_lines[1][colon_position + 2 : scip_lines[1].index('\n')].split(' ')[0]
)
try:
solving_nodes = int(
scip_lines[2][colon_position + 2 : scip_lines[2].index('(')]
)
except ValueError:
solving_nodes = int(
scip_lines[2][colon_position + 2 : scip_lines[2].index('\n')]
)
primal_bound = float(
scip_lines[3][colon_position + 2 : scip_lines[3].index('(')]
)
dual_bound = float(
scip_lines[4][colon_position + 2 : scip_lines[4].index('\n')]
)
try:
gap = float(scip_lines[5][colon_position + 2 : scip_lines[5].index('%')])
except ValueError:
gap = scip_lines[5][colon_position + 2 : scip_lines[5].index('\n')]
if gap == 'infinite':
gap = float('inf')
out_dict = {
'solver_status': solver_status,
'solving_time': solving_time,
'solving_nodes': solving_nodes,
'primal_bound': primal_bound,
'dual_bound': dual_bound,
'gap': gap,
}
return out_dict