11from pulp import *
2- import copy
2+ import io
33from subprocess import Popen , PIPE , STDOUT
44
5- class FSCIP_CMD ( LpSolver_CMD ):
6- """The FSCIP optimization solver"""
5+ class FSCIP_CMD_INTERACTIVE ( FSCIP_CMD ):
6+ """Interactive specialization with callback and termination method of the FSCIP optimization solver"""
77
8- name = "FSCIP_CMD "
8+ name = "FSCIP_CMD_INTERACTIVE "
99
1010 def __init__ (
1111 self ,
1212 path = None ,
13- keepFiles = False ,
1413 mip = True ,
14+ keepFiles = False ,
1515 msg = True ,
1616 options = None ,
1717 timeLimit = None ,
18- warmStart = False ,
19- threads = 0 ,
20- logPath = None
18+ gapRel = None ,
19+ gapAbs = None ,
20+ maxNodes = None ,
21+ threads = None ,
22+ logPath = None ,
2123 ):
2224 """
2325 :param bool mip: if False, assume LP even if integer variables
@@ -30,29 +32,23 @@ def __init__(
3032 :param int threads: sets the maximum number of threads
3133 :param str logPath: path to the log file
3234 """
33- LpSolver_CMD .__init__ (
35+ FSCIP_CMD .__init__ (
3436 self ,
3537 mip = mip ,
3638 msg = msg ,
3739 options = options ,
3840 path = path ,
3941 keepFiles = keepFiles ,
4042 timeLimit = timeLimit ,
41- warmStart = warmStart ,
42- logPath = logPath
43+ gapRel = gapRel ,
44+ gapAbs = gapAbs ,
45+ maxNodes = maxNodes ,
46+ threads = threads ,
47+ logPath = logPath ,
4348 )
44- self .threads = threads
45- self .logPath = logPath
46- self .warmStart = warmStart
49+
4750 self .process = None
48-
49- def defaultPath (self ):
50- return self .executableExtension ("fscip.exe" )
5151
52- def available (self ):
53- """True if the solver is available"""
54- return self .executable (self .path )
55-
5652 def terminate (self ):
5753 if self .process is not None :
5854 self .process .terminate ()
@@ -62,31 +58,67 @@ def actualSolve(self, lp, outputHandler = None):
6258 if not self .executable (self .path ):
6359 raise PulpSolverError ("PuLP: cannot execute " + self .path )
6460
65- tmpLp , tmpSol , tmpMst , tmpPrm = self .create_tmp_files (lp .name , "lp" , "sol" , "mst" , "prm" )
66- vs = lp .writeLP (tmpLp , writeSOS = 1 )
67-
68- options = copy .deepcopy (self .options )
69- if options is None :
70- options = []
61+ tmpLp , tmpSol , tmpOptions , tmpParams = self .create_tmp_files (
62+ lp .name , "lp" , "sol" , "set" , "prm"
63+ )
64+ lp .writeLP (tmpLp )
65+
66+ file_options : List [str ] = []
67+ if "gapRel" in self .optionsDict :
68+ file_options .append (f"limits/gap={ self .optionsDict ['gapRel' ]} " )
69+ if "gapAbs" in self .optionsDict :
70+ file_options .append (f"limits/absgap={ self .optionsDict ['gapAbs' ]} " )
71+ if "maxNodes" in self .optionsDict :
72+ file_options .append (f"limits/nodes={ self .optionsDict ['maxNodes' ]} " )
73+ if not self .mip :
74+ warnings .warn (f"{ self .name } does not allow a problem to be relaxed" )
75+
76+ file_parameters : List [str ] = []
7177 if self .timeLimit is not None :
72- options .append (["TimeLimit" , str (self .timeLimit )])
73- with open (tmpPrm , "w" ) as f :
74- f .write ("\n " .join (["%s = %s" % (key , value ) for key , value in options ]))
75- f .close ()
76-
77- proc = ["%s" % self .path , tmpPrm , tmpLp ]
78- proc .extend (["-sth" , str (self .threads )])
79-
80- if self .logPath is not None :
81- proc .extend (["-l" , str (self .logPath )])
82- proc .extend (["-fsol" , str (tmpSol )])
83- if self .warmStart == True :
84- self .writesol (filename = tmpMst , vs = vs )
85- proc .extend (["-isol" , str (tmpMst )])
86-
78+ file_parameters .append (f"TimeLimit = { self .timeLimit } " )
79+ # disable presolving in the LoadCoordinator to make sure a solution file is always written
80+ file_parameters .append ("NoPreprocessingInLC = TRUE" )
81+
82+ command : List [str ] = []
83+ command .append (self .path )
84+ command .append (tmpParams )
85+ command .append (tmpLp )
86+ command .extend (["-s" , tmpOptions ])
87+ command .extend (["-fsol" , tmpSol ])
8788 if not self .msg :
88- proc .append ("-q" )
89-
89+ command .append ("-q" )
90+ if "logPath" in self .optionsDict :
91+ command .extend (["-l" , self .optionsDict ["logPath" ]])
92+ if "threads" in self .optionsDict :
93+ command .extend (["-sth" , f"{ self .optionsDict ['threads' ]} " ])
94+
95+ options = iter (self .options )
96+ for option in options :
97+ # identify cli options by a leading dash (-) and treat other options as file options
98+ if option .startswith ("-" ):
99+ # assumption: all cli options require an argument which is provided as a separate parameter
100+ argument = next (options )
101+ command .extend ([option , argument ])
102+ else :
103+ # assumption: all file options contain a slash (/)
104+ is_file_options = "/" in option
105+
106+ # assumption: all file options and parameters require an argument which is provided after the equal sign (=)
107+ if "=" not in option :
108+ argument = next (options )
109+ option += f"={ argument } "
110+
111+ if is_file_options :
112+ file_options .append (option )
113+ else :
114+ file_parameters .append (option )
115+
116+ # wipe the solution file since FSCIP does not overwrite it if no solution was found which causes parsing errors
117+ self .silent_remove (tmpSol )
118+ with open (tmpOptions , "w" ) as options_file :
119+ options_file .write ("\n " .join (file_options ))
120+ with open (tmpParams , "w" ) as parameters_file :
121+ parameters_file .write ("\n " .join (file_parameters ))
90122
91123 if outputHandler is None :
92124 stdout = self .firstWithFilenoSupport (sys .stdout , sys .__stdout__ )
@@ -95,65 +127,27 @@ def actualSolve(self, lp, outputHandler = None):
95127
96128 stderr = self .firstWithFilenoSupport (sys .stderr , sys .__stderr__ )
97129
98- self .solution_time = - clock ()
99- self .process = Popen (proc , stdout = stdout , stderr = stderr )
130+ self .process = Popen (command , stdout = stdout , stderr = stderr )
100131 if outputHandler is not None :
101132 for line in self .process .stdout :
102133 outputHandler (line )
103134 exitcode = self .process .wait ()
104- self .solution_time += clock ()
105-
135+
106136 self .process = None
107137
108138 if not os .path .exists (tmpSol ):
109139 raise PulpSolverError ("PuLP: Error while executing " + self .path )
110-
111140 status , values = self .readsol (tmpSol )
112-
113141 # Make sure to add back in any 0-valued variables SCIP leaves out.
114142 finalVals = {}
115143 for v in lp .variables ():
116144 finalVals [v .name ] = values .get (v .name , 0.0 )
117145
118146 lp .assignVarsVals (finalVals )
119147 lp .assignStatus (status )
120- self .delete_tmp_files (tmpLp , tmpSol ,tmpMst , tmpPrm )
148+ self .delete_tmp_files (tmpLp , tmpSol , tmpOptions , tmpParams )
121149 return status
122150
123- @staticmethod
124- def readsol (filename ):
125- """Read a SCIP solution file"""
126- with open (filename ) as f :
127-
128- # Ignore first line it is different from scip sol file
129- try :
130- line = f .readline ()
131- except Exception :
132- raise PulpSolverError ("Can't get SCIP solver status" )
133-
134- status = constants .LpStatusOptimal
135- values = {}
136-
137- # Look for an objective value. If we can't find one, stop.
138- try :
139- line = f .readline ()
140- comps = line .split (": " )
141- assert comps [0 ] == "objective value"
142- assert len (comps ) == 2
143- float (comps [1 ].strip ())
144- except Exception :
145- raise PulpSolverError ("Can't get SCIP solver objective: %r" % line )
146-
147- # Parse the variable values.
148- for line in f :
149- try :
150- comps = line .split ()
151- values [comps [0 ]] = float (comps [1 ])
152- except :
153- raise PulpSolverError ("Can't read SCIP solver output: %r" % line )
154-
155- return status , values
156-
157151 @staticmethod
158152 def firstWithFilenoSupport (* streams ):
159153 for stream in streams :
0 commit comments