Source code for pyomo.solvers.plugins.solvers.SCIPAMPL

# ____________________________________________________________________________________
#
# 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