Skip to content

Commit d05dc6c

Browse files
author
Nico Höllerich
committed
Update for pulp 2.8 and gurobipy 11
1 parent 77f0f03 commit d05dc6c

File tree

4 files changed

+88
-95
lines changed

4 files changed

+88
-95
lines changed

comparison.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@
387387
"name": "python",
388388
"nbconvert_exporter": "python",
389389
"pygments_lexer": "ipython3",
390-
"version": "3.8.17"
390+
"version": "3.12.2"
391391
}
392392
},
393393
"nbformat": 4,

skyscraper_levels.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"from IPython.display import clear_output\n",
4747
"\n",
4848
"if importlib.util.find_spec(\"pulp\") is None :\n",
49-
" ! pip install --user pulp\n",
49+
" ! pip install --user pulp==2.8.0\n",
5050
" clear_output(wait=True) \n",
5151
" \n",
5252
"from pulp import *\n",
@@ -56,7 +56,7 @@
5656
"\n",
5757
"if \"GUROBI_CMD\" in available_solvers :\n",
5858
" if importlib.util.find_spec(\"gurobipy\") is None :\n",
59-
" ! pip install --user gurobipy\n",
59+
" ! pip install --user gurobipy==11.0.2\n",
6060
" clear_output(wait=True)\n",
6161
"\n",
6262
"from tools.skyscraper_levels import *\n",

tools/fscip_api.py

Lines changed: 79 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
from pulp import *
2-
import copy
2+
import io
33
from 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:

tools/skyscraper_levels.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
TAG = "v2.3"
1+
TAG = "v2.4"
22

33
import numpy as np
44

@@ -24,6 +24,7 @@
2424

2525
from pulp import *
2626
available_solvers = listSolvers(onlyAvailable=True)
27+
#available_solvers = []
2728

2829
if "GUROBI_CMD" in available_solvers :
2930
from gurobipy import GRB
@@ -32,8 +33,7 @@
3233
import cplex
3334
SOLVER = "CPLEX"
3435
else :
35-
from tools.fscip_api import FSCIP_CMD
36-
from subprocess import Popen, PIPE, STDOUT
36+
from tools.fscip_api import FSCIP_CMD_INTERACTIVE
3737
SOLVER = "FSCIP"
3838

3939
i18n = {
@@ -2781,7 +2781,7 @@ class GurobiWatcher(Watcher):
27812781

27822782
def __init__(self, prob, timeLimit, logPath):
27832783
self.prob = prob
2784-
self.solver = GUROBI(msg=False, timeLimit = timeLimit, logPath = logPath, warmStart = True, options=[('MIPConcurrent',4)])
2784+
self.solver = GUROBI(msg=False, timeLimit = timeLimit, logPath = logPath, warmStart = True)
27852785

27862786
def terminate(self):
27872787
GurobiWatcher.stop = True
@@ -2804,8 +2804,7 @@ def callback(model, where):
28042804
objbnd = model.cbGet(GRB.Callback.MIP_OBJBND)
28052805
log_callback(runtime, objbst, objbnd)
28062806

2807-
self.solver.buildSolverModel(self.prob)
2808-
self.solver.callSolver(self.prob, callback=callback)
2807+
self.solver.actualSolve(self.prob, callback=callback)
28092808
return self.extract_solution(self.prob)
28102809

28112810
def extract_solution(self, lp):
@@ -2919,7 +2918,7 @@ def optimize(self, log_callback = None):
29192918
class FscipWatcher(Watcher):
29202919
def __init__(self, prob, timeLimit, logPath):
29212920
self.prob = prob
2922-
self.solver = FSCIP_CMD(msg=False, timeLimit = timeLimit, logPath = logPath, warmStart=False, path=os.getcwd() + "/tools/fscip.exe")
2921+
self.solver = FSCIP_CMD_INTERACTIVE(msg=False, timeLimit = timeLimit, logPath = logPath, path=os.getcwd() + "/tools/fscip.exe")
29232922

29242923
def terminate(self):
29252924
self.solver.terminate()

0 commit comments

Comments
 (0)