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 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
|
from z3 import *
import math
# Rudimentary bin cover solver using the UserPropagator feature.
# It supports the most basic propagation for bin covering.
# - each bin has a propositional variable set to true if the bin is covered
# - each item has a bit-vector recording the assigned bin
# It searches for a locally optimal solution.
class Bin:
"""
Each bin carries values:
- min_bound - the lower bound required to be added to bin
- weight - the sum of weight of items currently added to bin
- slack - the difference between the maximal possible assignment and the assignments to other bin2bound.
- var - is propagated to true/false if the bin gets filled/cannot be filled.
"""
def __init__(self, min_bound, index):
assert min_bound > 0
assert index >= 0
self.index = index
self.min_bound = min_bound
self.weight = 0
self.slack = 0
self.added = []
self.var = Bool(f"bin-{index}")
def set_slack(self, slack):
self.slack = slack
def set_fill(self, fill):
self.weight = fill
def __repr__(self):
return f"{self.var}:bound-{self.min_bound}"
class Item:
def __init__(self, weight, index):
self.weight = weight
self.index = index
self.var = None
def set_var(self, num_bits):
self.var = BitVec(f"binof-{self.index}", num_bits)
def __repr__(self):
return f"binof-{self.index}:weight-{self.weight}"
class BranchAndBound:
"""Branch and Bound solver.
It keeps track of a current best score and a slack that tracks bins that are set unfilled.
It blocks branches that are worse than the current best score.
In Final check it blocks the current assignment.
"""
def __init__(self, user_propagator):
self.up = user_propagator
def init(self, soft_literals):
self.value = 0
self.best = 0
self.slack = 0
self.id2weight = {}
self.assigned_to_false = []
for p, weight in soft_literals:
self.slack += weight
self.id2weight[p.get_id()] = weight
def fixed(self, p, value):
weight = self.id2weight[p.get_id()]
if is_true(value):
old_value = self.value
self.up.trail += [lambda : self._undo_value(old_value)]
self.value += weight
elif self.best > self.slack - weight:
self.assigned_to_false += [ p ]
self.up.conflict(self.assigned_to_false)
self.assigned_to_false.pop(-1)
else:
old_slack = self.slack
self.up.trail += [lambda : self._undo_slack(old_slack)]
self.slack -= weight
self.assigned_to_false += [p]
def final(self):
if self.value > self.best:
self.best = self.value
print("Number of bins filled", self.value)
for bin in self.up.bins:
print(bin.var, bin.added)
self.up.conflict(self.assigned_to_false)
def _undo_value(self, old_value):
self.value = old_value
def _undo_slack(self, old_slack):
self.slack = old_slack
self.assigned_to_false.pop(-1)
class BinCoverSolver(UserPropagateBase):
"""Represent a bin-covering problem by associating each bin with a variable
For each item i associate a bit-vector
- bin-of-i that carries the bin identifier where an item is assigned.
"""
def __init__(self, s=None, ctx=None):
UserPropagateBase.__init__(self, s, ctx)
self.bins = []
self.items = []
self.item2index = {}
self.trail = [] # Undo stack
self.lim = []
self.solver = s
self.initialized = False
self.add_fixed(lambda x, v : self._fixed(x, v))
self.branch_and_bound = None
# Initialize bit-vector variables for items.
# Register the bit-vector variables with the user propagator to get callbacks
# Ensure the bit-vector variables are assigned to a valid bin.
# Initialize the slack of each bin.
def init(self):
print(self.bins, len(self.bins))
print(self.items)
assert not self.initialized
self.initialized = True
powerof2, num_bits = self._num_bits()
for item in self.items:
item.set_var(num_bits)
self.item2index[item.var.get_id()] = item.index
self.add(item.var)
if not powerof2:
bound = BitVecVal(len(self.bins), num_bits)
ineq = ULT(item.var, bound)
self.solver.add(ineq)
total_weight = sum(item.weight for item in self.items)
for bin in self.bins:
bin.slack = total_weight
#
# Register optional branch and bound weighted solver.
# If it is registered, it
def init_branch_and_bound(self):
soft = [(bin.var, 1) for bin in self.bins]
self.branch_and_bound = BranchAndBound(self)
self.branch_and_bound.init(soft)
for bin in self.bins:
self.add(bin.var)
self.add_final(lambda : self.branch_and_bound.final())
def add_bin(self, min_bound):
assert not self.initialized
index = len(self.bins)
bin = Bin(min_bound, index)
self.bins += [bin]
return bin
def add_item(self, weight):
assert not self.initialized
assert weight > 0
index = len(self.items)
item = Item(weight, index)
self.items += [item]
return item
def num_items(self):
return len(self.items)
def num_bins(self):
return len(self.bins)
def _num_bits(self):
log = math.log2(self.num_bins())
if log.is_integer():
return True, int(log)
else:
return False, int(log) + 1
def _set_slack(self, bin, slack_value):
bin.slack = slack_value
def _set_fill(self, bin, fill_value):
bin.weight = fill_value
bin.added.pop()
def _itemvar2item(self, v):
index = self.item2index[v.get_id()]
if index >= len(self.items):
return None
return self.items[index]
def _value2bin(self, value):
assert isinstance(value, BitVecNumRef)
bin_index = value.as_long()
if bin_index >= len(self.bins):
return NOne
return self.bins[bin_index]
def _add_item2bin(self, item, bin):
# print("add", item, "to", bin)
old_weight = bin.weight
bin.weight += item.weight
bin.added += [item]
self.trail += [lambda : self._set_fill(bin, old_weight)]
if old_weight < bin.min_bound and old_weight + item.weight >= bin.min_bound:
self._propagate_filled(bin)
# This item can never go into bin
def _exclude_item2bin(self, item, bin):
# print("exclude", item, "from", bin)
# Check if bin has already been blocked
if bin.slack < bin.min_bound:
return
if bin.weight >= bin.min_bound:
return
old_slack = bin.slack
new_slack = old_slack - item.weight
bin.slack = new_slack
self.trail += [lambda : self._set_slack(bin, old_slack)]
# If the new slack does not permit the bin to be filled, propagate
if new_slack < bin.min_bound:
self._propagate_slack(bin)
# Callback from Z3 when an item gets fixed.
def _fixed(self, _item, value):
if self.branch_and_bound and is_bool(value):
self.branch_and_bound.fixed(_item, value)
return
item = self._itemvar2item(_item)
if item is None:
print("no item for ", _item)
return
bin = self._value2bin(value)
if bin is None:
print("no bin for ", value)
return
self._add_item2bin(item, bin)
for idx in range(len(self.bins)):
if idx == bin.index:
continue
other_bin = self.bins[idx]
self._exclude_item2bin(item, other_bin)
def _propagate_filled(self, bin):
"""Propagate that bin_index is filled justified by the set of
items that have been added
"""
justification = [i.var for i in bin.added]
self.propagate(bin.var, justification)
def _propagate_slack(self, bin):
"""Propagate that bin_index cannot be filled"""
justification = []
for other_bin in self.bins:
if other_bin.index == bin.index:
continue
justification += other_bin.added
justification = [item.var for item in justification]
self.propagate(Not(bin.var), justification)
def push(self):
self.lim += [len(self.trail)]
def pop(self, n):
head = self.lim[len(self.lim) - n]
while len(self.trail) > head:
self.trail[-1]()
self.trail.pop(-1)
self.lim = self.lim[0:len(self.lim)-n]
# Find a first maximally satisfying subset
class MaximalSatisfyingSubset:
def __init__(self, s):
self.s = s
self.model = None
def tt(self, f):
return is_true(self.model.eval(f))
def get_mss(self, ps):
s = self.s
if sat != s.check():
return []
self.model = s.model()
mss = { q for q in ps if self.tt(q) }
return self._get_mss(mss, ps)
def _get_mss(self, mss, ps):
ps = set(ps) - mss
backbones = set([])
s = self.s
while len(ps) > 0:
p = ps.pop()
if sat == s.check(mss | backbones | { p }):
self.model = s.model()
mss = mss | { p } | { q for q in ps if self.tt(q) }
ps = ps - mss
else:
backbones = backbones | { Not(p) }
return mss
class OptimizeBinCoverSolver:
def __init__(self):
self.solver = Solver()
self.bin_solver = BinCoverSolver(self.solver)
self.mss_solver = MaximalSatisfyingSubset(self.solver)
#
# Facilities to set up solver
# First add items and bins.
# Keep references to the returned objects.
# Then call init
# Then add any other custom constraints to the "solver" object.
#
def init(self):
self.bin_solver.init()
def add_item(self, weight):
return self.bin_solver.add_item(weight)
def add_bin(self, min_bound):
return self.bin_solver.add_bin(min_bound)
def optimize(self):
self.init()
mss = self.mss_solver.get_mss([bin.var for bin in self.bin_solver.bins])
print(self.mss_solver.model)
print("filled bins", mss)
print("bin contents")
for bin in self.bin_solver.bins:
print(bin, bin.added)
def example1():
s = OptimizeBinCoverSolver()
i1 = s.add_item(2)
i2 = s.add_item(4)
i3 = s.add_item(5)
i4 = s.add_item(2)
b1 = s.add_bin(3)
b2 = s.add_bin(6)
b3 = s.add_bin(1)
s.optimize()
#example1()
class BranchAndBoundCoverSolver:
def __init__(self):
self.solver = Solver()
self.bin_solver = BinCoverSolver(self.solver)
def init(self):
self.bin_solver.init()
self.bin_solver.init_branch_and_bound()
def add_item(self, weight):
return self.bin_solver.add_item(weight)
def add_bin(self, min_bound):
return self.bin_solver.add_bin(min_bound)
def optimize(self):
self.init()
self.solver.check()
def example2():
s = BranchAndBoundCoverSolver()
i1 = s.add_item(2)
i2 = s.add_item(4)
i3 = s.add_item(5)
i4 = s.add_item(2)
b1 = s.add_bin(3)
b2 = s.add_bin(6)
b3 = s.add_bin(1)
s.optimize()
example2()
|