forked from scipopt/PySCIPOpt
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtest_gomory.py
345 lines (283 loc) · 14.8 KB
/
test_gomory.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
import pytest
from pyscipopt import Model, Sepa, SCIP_RESULT, SCIP_PARAMSETTING
from pyscipopt.scip import is_memory_freed
class GMI(Sepa):
def __init__(self):
self.ncuts = 0
def getGMIFromRow(self, cols, rows, binvrow, binvarow, primsol):
""" Given the row (binvarow, binvrow) of the tableau, computes gomory cut
:param primsol: is the rhs of the tableau row.
:param cols: are the variables
:param rows: are the slack variables
:param binvrow: components of the tableau row associated to the basis inverse
:param binvarow: components of the tableau row associated to the basis inverse * A
The GMI is given by
sum(f_j x_j , j in J_I s.t. f_j <= f_0) +
sum((1-f_j)*f_0/(1 - f_0) x_j, j in J_I s.t. f_j > f_0) +
sum(a_j x_j, , j in J_C s.t. a_j >= 0) -
sum(a_j*f_0/(1-f_0) x_j , j in J_C s.t. a_j < 0) >= f_0.
where J_I are the integer non-basic variables and J_C are the continuous.
f_0 is the fractional part of primsol
a_j is the j-th coefficient of the row and f_j its fractional part
Note: we create -% <= -f_0 !!
Note: this formula is valid for a problem of the form Ax = b, x>= 0. Since we do not have
such problem structure in general, we have to (implicitely) transform whatever we are given
to that form. Specifically, non-basic variables at their lower bound are shifted so that the lower
bound is 0 and non-basic at their upper bound are complemented.
"""
# initialize
cutcoefs = [0] * len(cols)
cutrhs = 0
# get scip
scip = self.model
# Compute cut fractionality f0 and f0/(1-f0)
f0 = scip.frac(primsol)
ratiof0compl = f0/(1-f0)
# rhs of the cut is the fractional part of the LP solution for the basic variable
cutrhs = -f0
# Generate cut coefficients for the original variables
for c in range(len(cols)):
col = cols[c]
assert col is not None # is this the equivalent of col != NULL? does it even make sense to have this assert?
status = col.getBasisStatus()
# Get simplex tableau coefficient
if status == "lower":
# Take coefficient if nonbasic at lower bound
rowelem = binvarow[c]
elif status == "upper":
# Flip coefficient if nonbasic at upper bound: x --> u - x
rowelem = -binvarow[c]
else:
# variable is nonbasic free at zero -> cut coefficient is zero, skip OR
# variable is basic, skip
assert status == "zero" or status == "basic"
continue
# Integer variables
if col.isIntegral():
# warning: because of numerics cutelem < 0 is possible (though the fractional part is, mathematically, always positive)
# However, when cutelem < 0 it is also very close to 0, enough that isZero(cutelem) is true, so we ignore
# the coefficient (see below)
cutelem = scip.frac(rowelem)
if cutelem > f0:
# sum((1-f_j)*f_0/(1 - f_0) x_j, j in J_I s.t. f_j > f_0) +
cutelem = -((1.0 - cutelem) * ratiof0compl)
else:
# sum(f_j x_j , j in J_I s.t. f_j <= f_0) +
cutelem = -cutelem
else:
# Continuous variables
if rowelem < 0.0:
# -sum(a_j*f_0/(1-f_0) x_j , j in J_C s.t. a_j < 0) >= f_0.
cutelem = rowelem * ratiof0compl
else:
# sum(a_j x_j, , j in J_C s.t. a_j >= 0) -
cutelem = -rowelem
# cut is define when variables are in [0, infty). Translate to general bounds
if not scip.isZero(cutelem):
if col.getBasisStatus() == "upper":
cutelem = -cutelem
cutrhs += cutelem * col.getUb()
else:
cutrhs += cutelem * col.getLb()
# Add coefficient to cut in dense form
cutcoefs[col.getLPPos()] = cutelem
# Generate cut coefficients for the slack variables; skip basic ones
for c in range(len(rows)):
row = rows[c]
assert row != None
status = row.getBasisStatus()
# free slack variable shouldn't appear
assert status != "zero"
# Get simplex tableau coefficient
if status == "lower":
# Take coefficient if nonbasic at lower bound
rowelem = binvrow[row.getLPPos()]
# But if this is a >= or ranged constraint at the lower bound, we have to flip the row element
if not scip.isInfinity(-row.getLhs()):
rowelem = -rowelem
elif status == "upper":
# Take element if nonbasic at upper bound - see notes at beginning of file: only nonpositive slack variables
# can be nonbasic at upper, therefore they should be flipped twice and we can take the element directly.
rowelem = binvrow[row.getLPPos()]
else:
assert status == "basic"
continue
# if row is integral we can strengthen the cut coefficient
if row.isIntegral() and not row.isModifiable():
# warning: because of numerics cutelem < 0 is possible (though the fractional part is, mathematically, always positive)
# However, when cutelem < 0 it is also very close to 0, enough that isZero(cutelem) is true (see later)
cutelem = scip.frac(rowelem)
if cutelem > f0:
# sum((1-f_j)*f_0/(1 - f_0) x_j, j in J_I s.t. f_j > f_0) +
cutelem = -((1.0 - cutelem) * ratiof0compl)
else:
# sum(f_j x_j , j in J_I s.t. f_j <= f_0) +
cutelem = -cutelem
else:
# Continuous variables
if rowelem < 0.0:
# -sum(a_j*f_0/(1-f_0) x_j , j in J_C s.t. a_j < 0) >= f_0.
cutelem = rowelem * ratiof0compl
else:
# sum(a_j x_j, , j in J_C s.t. a_j >= 0) -
cutelem = -rowelem
# cut is define in original variables, so we replace slack by its definition
if not scip.isZero(cutelem):
# get lhs/rhs
rlhs = row.getLhs()
rrhs = row.getRhs()
assert scip.isLE(rlhs, rrhs)
assert not scip.isInfinity(rlhs) or not scip.isInfinity(rrhs)
# If the slack variable is fixed, we can ignore this cut coefficient
if scip.isFeasZero(rrhs - rlhs):
continue
# Unflip slack variable and adjust rhs if necessary: row at lower means the slack variable is at its upper bound.
# Since SCIP adds +1 slacks, this can only happen when constraints have a finite lhs
if row.getBasisStatus() == "lower":
assert not scip.isInfinity(-rlhs)
cutelem = -cutelem
rowcols = row.getCols()
rowvals = row.getVals()
assert len(rowcols) == len(rowvals)
# Eliminate slack variable: rowcols is sorted: [columns in LP, columns not in LP]
for i in range(row.getNLPNonz()):
cutcoefs[rowcols[i].getLPPos()] -= cutelem * rowvals[i]
act = scip.getRowLPActivity(row)
rhsslack = rrhs - act
if scip.isFeasZero(rhsslack):
assert row.getBasisStatus() == "upper" # cutelem != 0 and row active at upper bound -> slack at lower, row at upper
cutrhs -= cutelem * (rrhs - row.getConstant())
else:
assert scip.isFeasZero(act - rlhs)
cutrhs -= cutelem * (rlhs - row.getConstant())
return cutcoefs, cutrhs
def sepaexeclp(self):
result = SCIP_RESULT.DIDNOTRUN
scip = self.model
if not scip.isLPSolBasic():
return {"result": result}
#TODO: add SCIPgetNLPBranchCands
# get var data ---> this is the same as getVars!
vars = scip.getVars(transformed = True)
# get LP data
cols = scip.getLPColsData()
rows = scip.getLPRowsData()
# exit if LP is trivial
if len(cols) == 0 or len(rows) == 0:
return {"result": result}
result = SCIP_RESULT.DIDNOTFIND
# get basis indices
basisind = scip.getLPBasisInd()
# For all basic columns (not slacks) belonging to integer variables, try to generate a gomory cut
for i in range(len(rows)):
tryrow = False
c = basisind[i]
#primsol = SCIP_INVALID
#print("Row %d/%d basic index: %d"%(i, len(rows),c))
#print("Row %d with value %f"%(i, cols[c].getPrimsol() if c >=0 else scip.getRowActivity(rows[-c-1])))
if c >= 0:
assert c < len(cols)
var = cols[c].getVar()
if var.vtype() != "CONTINUOUS":
primsol = cols[c].getPrimsol()
assert scip.getSolVal(None, var) == primsol
#print("var ", var," is not continuous. primsol = ", primsol)
if 0.005 <= scip.frac(primsol) <= 1 - 0.005:
#print("####trying gomory cut for col <%s> [%g] row %i"%(var, primsol, i))
tryrow = True
# generate the cut!
if tryrow:
# get the row of B^-1 for this basic integer variable with fractional solution value
binvrow = scip.getLPBInvRow(i)
# get the tableau row for this basic integer variable with fractional solution value
binvarow = scip.getLPBInvARow(i)
# get cut's coefficients
cutcoefs, cutrhs = self.getGMIFromRow(cols, rows, binvrow, binvarow, primsol)
#########################################
#### This code is for testing only!! ####
# the first two cuts are -x1 + 2y <= 0 and -x2 +2y <= 0
# the next two (when both previous ones are added) are -2x1 + 6y <=0 and -2x2 + 6y <=0
assert scip.isZero(cutcoefs[0]) or scip.isZero(cutcoefs[1])
if self.ncuts < 2:
assert scip.isZero(cutcoefs[0] + 1) or scip.isZero(cutcoefs[1] + 1)
assert scip.isZero(cutcoefs[2] - 2)
else:
assert scip.isZero(cutcoefs[0] + 2) or scip.isZero(cutcoefs[1] + 2)
assert scip.isZero(cutcoefs[2] - 6)
##########################################
# add cut
# TODO: here it might make sense just to have a function `addCut` just like `addCons`. Or maybe better `createCut`
# so that then one can ask stuff about it, like its efficacy, etc. This function would receive all coefficients
# and basically do what we do here: cacheRowExtension etc up to releaseRow
cut = scip.createEmptyRowSepa(self, "gmi%d_x%d"%(self.ncuts,c if c >= 0 else -c-1), lhs = None, rhs = cutrhs)
scip.cacheRowExtensions(cut)
for j in range(len(cutcoefs)):
if scip.isZero(cutcoefs[j]): # maybe here we need isFeasZero
continue
#print("var : ", cols[j].getVar(), " coef: ", cutcoefs[j])
#print("cut.lhs : ", cut.getLhs(), " cut.rhs: ", cut.getRhs())
scip.addVarToRow(cut, cols[j].getVar(), cutcoefs[j])
if cut.getNNonz() == 0:
assert scip.isFeasNegative(cutrhs)
#print("Gomory cut is infeasible: 0 <= ", cutrhs)
return {"result": SCIP_RESULT.CUTOFF}
# Only take efficacious cuts, except for cuts with one non-zero coefficient (= bound changes)
# the latter cuts will be handeled internally in sepastore.
if cut.getNNonz() == 1 or scip.isCutEfficacious(cut):
#print(" -> gomory cut for <%s>: rhs=%f, eff=%f"%(cols[c].getVar() if c >= 0 else rows[-c-1].getName(),
# cutrhs, scip.getCutEfficacy(cut)))
#SCIPdebugMessage(" -> found gomory cut <%s>: act=%f, rhs=%f, norm=%f, eff=%f, min=%f, max=%f (range=%f)\n",
# cutname, SCIPgetRowLPActivity(scip, cut), SCIProwGetRhs(cut), SCIProwGetNorm(cut),
# SCIPgetCutEfficacy(scip, NULL, cut),
# SCIPgetRowMinCoef(scip, cut), SCIPgetRowMaxCoef(scip, cut),
# SCIPgetRowMaxCoef(scip, cut)/SCIPgetRowMinCoef(scip, cut))
# flush all changes before adding the cut
scip.flushRowExtensions(cut)
infeasible = scip.addCut(cut, forcecut=True)
self.ncuts += 1
if infeasible:
result = SCIP_RESULT.CUTOFF
else:
result = SCIP_RESULT.SEPARATED
scip.releaseRow(cut)
return {"result": result}
def model():
# create solver instance
s = Model()
# include separator
sepa = GMI()
s.includeSepa(sepa, "python_gmi", "generates gomory mixed integer cuts", priority = 1000, freq = 1)
# turn off presolve
s.setPresolve(SCIP_PARAMSETTING.OFF)
# turn off heuristics
s.setHeuristics(SCIP_PARAMSETTING.OFF)
# turn off propagation
s.setIntParam("propagating/maxrounds", 0)
s.setIntParam("propagating/maxroundsroot", 0)
# turn off some cuts
s.setIntParam("separating/strongcg/freq", -1)
s.setIntParam("separating/gomory/freq", -1)
s.setIntParam("separating/aggregation/freq", -1)
s.setIntParam("separating/mcf/freq", -1)
s.setIntParam("separating/closecuts/freq", -1)
s.setIntParam("separating/clique/freq", -1)
s.setIntParam("separating/zerohalf/freq", -1)
# only two rounds of cuts
s.setIntParam("separating/maxroundsroot", 2)
return s
# we use Cook Kannan and Schrijver's example
def test_CKS():
s = model()
# add variables
x1 = s.addVar("x1", vtype='I')
x2 = s.addVar("x2", vtype='I')
y = s.addVar("y", obj=-1, vtype='C')
# add constraint
s.addCons(-x1 + y <= 0)
s.addCons( - x2 + y <= 0)
s.addCons( x1 + x2 + y <= 2)
# solve problem
s.optimize()
s.printStatistics()
if __name__ == "__main__":
test_CKS()