Skip to content

Commit a5696d4

Browse files
committed
cut generators can also generate lazy constraints (initially for gurobi)
Former-commit-id: 25e31e4 [formerly 3e81e0f] Former-commit-id: 54c66ef
1 parent 5be75f2 commit a5696d4

File tree

5 files changed

+120
-11
lines changed

5 files changed

+120
-11
lines changed

examples/queens-lazy.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Example of a solver to the n-queens problem:
2+
n chess queens should be placed in a n x n
3+
chess board so that no queen can attack another,
4+
i.e., just one queen per line, column and diagonal.
5+
"""
6+
7+
from sys import stdout
8+
from mip.model import Model, xsum
9+
from mip.constants import MAXIMIZE, BINARY
10+
from mip.callbacks import CutsGenerator
11+
12+
13+
class DiagonalCutGenerator(CutsGenerator):
14+
15+
def generate_cuts(self, model: Model):
16+
def row(vname: str) -> str:
17+
return int(vname.split('(')[1].split(',')[0].split(')')[0])
18+
19+
def col(vname: str) -> str:
20+
return int(vname.split('(')[1].split(',')[1].split(')')[0])
21+
22+
x = {(row(v.name), col(v.name)): v for v in model.vars}
23+
for p, k in enumerate(range(2 - n, n - 2 + 1)):
24+
cut = xsum(x[i, j] for i in range(n) for j in range(n)
25+
if i - j == k) <= 1
26+
if cut.violation > 0.001:
27+
model.add_cut(cut)
28+
29+
for p, k in enumerate(range(3, n + n)):
30+
cut = xsum(x[i, j] for i in range(n) for j in range(n)
31+
if i + j == k) <= 1
32+
if cut.violation > 0.001:
33+
model.add_cut(cut)
34+
35+
36+
# number of queens
37+
n = 8
38+
39+
queens = Model('queens', MAXIMIZE)
40+
41+
x = [[queens.add_var('x({},{})'.format(i, j), var_type=BINARY)
42+
for j in range(n)] for i in range(n)]
43+
44+
# one per row
45+
for i in range(n):
46+
queens += xsum(x[i][j] for j in range(n)) == 1, 'row({})'.format(i)
47+
48+
# one per column
49+
for j in range(n):
50+
queens += xsum(x[i][j] for i in range(n)) == 1, 'col({})'.format(j)
51+
52+
53+
queens.cuts_generator = DiagonalCutGenerator()
54+
queens.cuts_generator.lazy_constraints = True
55+
queens.optimize()
56+
57+
stdout.write('\n')
58+
for i, v in enumerate(queens.vars):
59+
stdout.write('O ' if v.x >= 0.99 else '. ')
60+
if i % n == n-1:
61+
stdout.write('\n')

mip/callbacks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class CutsGenerator:
1515
"""abstract class for implementing cut generators"""
1616

1717
def __init__(self):
18-
self.lazyConstraints = False
18+
self.lazy_constraints = False
1919

2020
def generate_cuts(self, model: Model):
2121
"""Method called by the solver engine to generate cuts

mip/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from enum import Enum
44

5-
VERSION = '1.3.10'
5+
VERSION = '1.3.11'
66

77
# epsilon number (practical zero)
88
EPS = 10e-6

mip/gurobi.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@
2020
if 'GUROBI_HOME' in environ:
2121
if platform.lower().startswith('win'):
2222
libfile = glob(os.path.join(os.environ['GUROBI_HOME'],
23-
'bin\gurobi[0-9][0-9].dll'))
23+
'bin\\gurobi[0-9][0-9].dll'))
2424
else:
2525
libfile = glob(os.path.join(os.environ['GUROBI_HOME'],
2626
'lib/libgurobi[0-9][0-9].*'))
27-
27+
2828
if libfile:
2929
lib_path = libfile[0]
3030

@@ -249,6 +249,8 @@
249249
GRBgetstrattr = grblib.GRBgetstrattr
250250
GRBsetstrattr = grblib.GRBsetstrattr
251251

252+
GRB_CB_MIPSOL = 4
253+
GRB_CB_MIPNODE = 5
252254

253255
GRB_CB_PRE_COLDEL = 1000
254256
GRB_CB_PRE_ROWDEL = 1001
@@ -390,7 +392,6 @@ def add_var(self,
390392
def add_cut(self, lin_expr: LinExpr):
391393
# int GRBcbcut(void *cbdata, int cutlen, const int *cutind, const double *cutval, char cutsense, double cutrhs);
392394
# int GRBcbcut(void *cbdata, int cutlen, const int *cutind, const double *cutval, char cutsense, double cutrhs);
393-
394395

395396
return
396397

@@ -498,9 +499,12 @@ def callback(p_model: CData,
498499
obj_bound, obj_best))
499500
log.append((sec, (obj_bound, obj_best)))
500501

501-
# adding cuts
502-
if where == 5: # MIPNODE == 5
503-
if self.model.cuts_generator:
502+
# adding cuts or lazy constraints
503+
if self.model.cuts_generator:
504+
if where == GRB_CB_MIPNODE or \
505+
(where == GRB_CB_MIPSOL and
506+
hasattr(self.model.cuts_generator, 'lazy_constraints') and
507+
self.model.cuts_generator.lazy_constraints):
504508
mgc = ModelGurobiCB(p_model, p_cbdata, where)
505509
self.model.cuts_generator.generate_cuts(mgc)
506510

@@ -538,6 +542,12 @@ def callback(p_model: CData,
538542
self.model.store_search_progress_log:
539543
GRBsetcallbackfunc(self._model, callback, ffi.NULL)
540544

545+
if (self.model.cuts_generator is not None and
546+
hasattr(self.model.cuts_generator, 'lazy_constraints') and
547+
self.model.cuts_generator.lazy_constraints) or \
548+
self.model.lazy_constrs_generator is not None:
549+
self.set_int_param("LazyConstraints", 1)
550+
541551
if self.__threads >= 1:
542552
self.set_int_param("Threads", self.__threads)
543553

@@ -1141,15 +1151,16 @@ def __init__(self, model: Model, grb_model: CData = ffi.NULL,
11411151
self._obj_value = INF
11421152
self._best_bound = INF
11431153
self._status = OptimizationStatus.LOADED
1154+
self._where = where
11441155

11451156
# pre-allocate temporary space to query names
11461157
self.__name_space = ffi.new("char[{}]".format(MAX_NAME_SIZE))
11471158
# in cut generation
11481159
self.__name_spacec = ffi.new("char[{}]".format(MAX_NAME_SIZE))
11491160

11501161
self.__relaxed = False
1151-
if where == 5:
1152-
gstatus = ffi.new('int *')
1162+
gstatus = ffi.new('int *')
1163+
if where == 5: # GRB_CB_MIPNODE
11531164
res = GRBcbget(cb_data, where, GRB_CB_MIPNODE_STATUS, gstatus)
11541165
if res != 0:
11551166
raise Exception('Error getting status')
@@ -1168,6 +1179,21 @@ def __init__(self, model: Model, grb_model: CData = ffi.NULL,
11681179
raise Exception('Error getting fractional solution')
11691180
else:
11701181
self._cb_sol = ffi.NULL
1182+
elif where == 4: # GRB_CB_MIPSOL
1183+
self._status = OptimizationStatus.FEASIBLE
1184+
ires = ffi.new('int *')
1185+
st = GRBgetintattr(grb_model, 'NumVars'.encode('utf-8'), ires)
1186+
if st != 0:
1187+
raise Exception('Could not query number of variables in Gurobi \
1188+
callback')
1189+
ncols = ires[0]
1190+
1191+
self._cb_sol = \
1192+
ffi.new('double[{}]'.format(ncols))
1193+
res = GRBcbget(cb_data, where, GRB_CB_MIPSOL_SOL, self._cb_sol)
1194+
if res != 0:
1195+
raise Exception('Error getting integer solution in gurobi \
1196+
callback')
11711197
else:
11721198
self._cb_sol = ffi.NULL
11731199

@@ -1182,7 +1208,10 @@ def add_cut(self, cut: LinExpr):
11821208
sense = cut.sense.encode("utf-8")
11831209
rhs = -cut.const
11841210

1185-
GRBcbcut(self._cb_data, numnz, cind, cval, sense, rhs)
1211+
if self._where == GRB_CB_MIPNODE:
1212+
GRBcbcut(self._cb_data, numnz, cind, cval, sense, rhs)
1213+
elif self._where == GRB_CB_MIPSOL:
1214+
GRBcblazy(self._cb_data, numnz, cind, cval, sense, rhs)
11861215

11871216
def get_status(self):
11881217
return self._status

mip/model.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,25 @@ def sense(self, value):
363363
"""
364364
self.__sense = value
365365

366+
@property
367+
def violation(self):
368+
"""Amount that current solution violates this constraint
369+
370+
If a solution is available, than this property indicates how much
371+
the current solution violates this constraint.
372+
"""
373+
lhs = sum(coef*var.x for (var, coef) in self.__expr.items())
374+
rhs = -self.const
375+
viol = 0.0
376+
if self.sense == '=':
377+
viol = abs(lhs-rhs)
378+
elif self.sense == '<':
379+
viol = max(lhs-rhs, 0.0)
380+
elif self.sense == '>':
381+
viol = max(rhs-lhs, 0.0)
382+
383+
return viol
384+
366385

367386
class ProgressLog:
368387
"""Class to store the improvement of lower

0 commit comments

Comments
 (0)