Skip to content

Commit 02eaac2

Browse files
committed
clique merge
Former-commit-id: 2a315a6
1 parent a9442a6 commit 02eaac2

File tree

4 files changed

+101
-0
lines changed

4 files changed

+101
-0
lines changed

Diff for: examples/clique_merge.py

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""Example with clique merge: a formulation of a knapsack problem
2+
with conflicting items where clique merge can improve it"""
3+
4+
from mip import Model, xsum, maximize, BINARY
5+
6+
I = set(range(1, 7))
7+
8+
# weigths
9+
w = {1: -4, 2: 4, 3: 5, 4: 6, 5: 7, 6: 10}
10+
11+
# profits
12+
p = {1: -2, 2: 8, 3: 10, 4: 12, 5: 13, 6: 13}
13+
14+
# capacity
15+
c = 6
16+
17+
# conflicting items
18+
C = ((2, 3), (2, 4), (3, 4), (2, 5))
19+
20+
m = Model()
21+
22+
x = {i: m.add_var("x({})".format(i), var_type=BINARY) for i in I}
23+
24+
m.objective = maximize(xsum(p[i] * x[i] for i in I))
25+
26+
m += xsum(w[i] * x[i] for i in I) <= c
27+
28+
for (i, j) in C:
29+
m += x[i] + x[j] <= 1
30+
31+
m.verbose = 0
32+
m.write("b.lp")
33+
m.optimize(relax=True)
34+
35+
print(
36+
"constraints before clique merging: {}. lower bound:"
37+
"{}.".format(len(m.constrs), m.objective_value)
38+
)
39+
40+
m.clique_merge()
41+
42+
m.optimize(relax=True)
43+
44+
print(
45+
"constraints after clique merging: {}. lower bound:"
46+
"{}.".format(len(m.constrs), m.objective_value)
47+
)
48+
49+
m.optimize()
50+
51+
print("optimal: {}".format(m.objective_value))
52+
53+
54+
# sanity tests
55+
from mip import OptimizationStatus
56+
57+
assert m.status == OptimizationStatus.OPTIMAL
58+
if m.solver_name.upper() == "CBC":
59+
assert m.num_rows == 2
60+
assert abs(m.objective_value - 12) <= 1e-4

Diff for: mip/cbc.py

+9
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,15 @@ def generate_cuts(
933933
OsiCuts_delete(osi_cuts)
934934
return cp
935935

936+
def clique_merge(self, constrs: Optional[List["mip.Constr"]] = None):
937+
if constrs is None:
938+
cbclib.Cbc_strengthenPacking(self._model)
939+
else:
940+
nr = len(constrs)
941+
idxr = ffi.new("int[]", [c.idx for c in constrs])
942+
strengthenPacking = cbclib.Cbc_strengthenPackingRows
943+
strengthenPacking(self._model, nr, idxr)
944+
936945
def optimize(self, relax: bool = False) -> OptimizationStatus:
937946
# get name indexes from an osi problem
938947
def cbc_get_osi_name_indexes(osi_solver) -> Dict[str, int]:

Diff for: mip/model.py

+29
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,35 @@ def var_by_name(self: "Model", name: str) -> Optional["mip.Var"]:
473473
return None
474474
return self.vars[v]
475475

476+
def clique_merge(self, constrs: Optional[List["mip.Constr"]] = None):
477+
r"""This procedure searches for constraints with conflicting variables
478+
and attempts to group these constraints in larger constraints with all
479+
conflicts merged.
480+
481+
For example, if your model has the following constraints:
482+
483+
.. math::
484+
485+
x_1 + x_2 \leq 1
486+
487+
x_2 + x_3 \leq 1
488+
489+
x_1 + x_3 \leq 1
490+
491+
Then they can all be removed and replaced by the stronger inequality:
492+
493+
.. math::
494+
495+
x_1 + x_2 + x_3 \leq 1
496+
497+
Args:
498+
constrs (Optional[List[Constrs]]): constraints that should be checked for
499+
merging. If nor informed, all constraints will be checked.
500+
501+
"""
502+
self.solver.clique_merge(constrs)
503+
self.constrs.update_constrs(self.solver.num_rows())
504+
476505
def generate_cuts(
477506
self: "Model",
478507
cut_types: Optional[List["mip.CutType"]] = None,

Diff for: mip/solver.py

+3
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ def generate_cuts(
6565
) -> "mip.CutPool":
6666
pass
6767

68+
def clique_merge(self, constrs: Optional[List["mip.Constr"]] = None):
69+
pass
70+
6871
def optimize(self: "Solver", relax: bool = False,) -> "mip.OptimizationStatus":
6972
pass
7073

0 commit comments

Comments
 (0)