2 # -*- coding: utf-8 -*-
11 from nepi.util.settools import setclusters
12 from nepi.util.settools import classify
14 class ResourceAllocationError(Exception):
17 def multicardinal(multiset):
18 return sum(quant for c,quant in multiset.iteritems())
20 def avail(cls, partition):
21 contains = classify.classContains
22 return reduce(operator.or_,
23 classify.classComponents(cls, partition),
26 def _log(logstream, message, *args, **kwargs):
29 logstream.write(message % args)
31 logstream.write(message % kwargs)
33 logstream.write(message)
36 def alloc(requests, logstream = None, nonseparable = False, saveinteresting = None, backtracklim = 100000000, verbose = True, sample = random.sample):
38 Takes an iterable over requests, which are iterables of candidate node ids,
39 and returns a specific node id for each request (if successful).
41 If it cannot, it will raise an ResourceAllocationError.
44 # First, materialize the request iterable
45 requests = map(set,requests)
47 # Classify all candidates
48 universe = reduce(operator.or_, requests, set())
49 partition = setclusters.disjoint_partition(*requests)
52 c_reqlist = classify.classify(requests, partition)
55 for c,r in c_reqlist.iteritems()
59 c_uni = map(len, partition)
61 # Perform invariant sanity checks
62 if multicardinal(c_req) > sum(c_uni):
63 raise ResourceAllocationError, "Insufficient resources to grant request"
65 for c,nreq in c_req.iteritems():
66 if nreq > len(avail(c, partition)):
67 raise ResourceAllocationError, "Insufficient resources to grant request, empty categories %s" % (
68 filter(lambda i : classify.classContains(c,i), xrange(len(c))),
71 # Test for separability
73 components = clusters = []
76 classify.classMembers(c, partition)
79 clusters = setclusters.disjoint_sets(*components)
83 _log(logstream, "\nDetected %d clusters", len(clusters))
85 # Requests are separable
86 # Solve each part separately, then rejoin them
88 # Build a class for each cluster
90 compmap = dict([(pid,idx) for idx,pid in enumerate(map(id,components))])
91 for cluster in clusters:
92 cluster_class = classify.getClass(
93 reduce(operator.or_, cluster, set()),
95 clustermaps.append(cluster_class)
97 # Build a plan: assign a cluster to each request
99 for cluster_class in clustermaps:
101 for c, c_requests in c_reqlist.iteritems():
102 if classify.isSubclass(cluster_class, c):
103 plan_reqs.extend(c_requests)
104 plan.append(plan_reqs)
108 for i,plan_req in enumerate(plan):
110 _log(logstream, "Solving cluster %d/%d", i+1, len(plan))
111 partial_results.append(alloc(plan_req,
114 saveinteresting = saveinteresting,
115 backtracklim = backtracklim,
120 _log(logstream, "Joining partial results")
121 reqmap = dict([(pid,idx) for idx,pid in enumerate(map(id,requests))])
122 joint = [None] * len(requests)
123 for partial_result, partial_requests in zip(partial_results, plan):
124 for assignment, partial_request in zip(partial_result, partial_requests):
125 joint[reqmap[id(partial_request)]] = assignment
129 # Non-separable request, solve
130 #_log(logstream, "Non-separable request")
133 partial = collections.defaultdict(list)
136 (c, len(avail(c, partition)))
141 # build a cardinality map
143 (c, [classify.classCardinality(c,partition), -nreq])
144 for c,nreq in req.iteritems()
147 classContains = classify.classContains
148 isSubclass = classify.isSubclass
153 0, # skipped branches
156 def recursive_alloc():
157 # Successful termination condition: all requests satisfied
161 # Try in cardinality order
163 order = heapq.nsmallest(2, req, key=Gavail.__getitem__)
165 order = sorted(req, key=Gavail.__getitem__)
167 # Do backtracking on those whose cardinality leaves a choice
168 # Force a pick when it does not
169 if order and (Gavail[order[0]] <= 1
170 or classify.classCardinality(order[0]) <= 1):
175 #carditem = cardinality[c]
176 for i,bit in enumerate(c):
177 if bit == "1" and Pavail[i]:
178 stats[0] += 1 # ops+1
180 subreq = min(Pavail[i], nreq)
182 # branch sanity check
184 for c2,navail in Gavail.iteritems():
185 if c2 != c and classContains(c2, i) and (navail - subreq) < req.get(c2,0):
186 # Fail branch, don't even try
190 stats[2] += 1 # skipped branches + 1
194 partial[c].append((i,subreq))
196 #carditem[1] -= subreq
199 if classContains(c2, i):
207 # Try to solve recursively
208 success = recursive_alloc()
216 #carditem[1] += subreq
219 if classContains(c2, i):
227 stats[1] += 1 # backtracks + 1
229 if (logstream or (saveinteresting is not None)) and (stats[1] & 0xffff) == 0:
230 _log(logstream, "%r\n%r\n... stats: ops=%d, backtracks=%d, skipped=%d", Gavail, req,
233 if stats[1] == 0x1400000:
234 # Interesting case, log it out
235 _log(logstream, "... interesting case: %r", requests)
237 if saveinteresting is not None:
238 saveinteresting.append(requests)
239 if stats[1] > backtracklim:
243 # We tried and tried... and failed
246 # First try quickly (assign most selective first exclusively)
248 success = recursive_alloc()
250 # If it fails, retry exhaustively (try all assignment orders)
252 success = recursive_alloc()
254 if verbose or (not success or stats[1] or stats[2]):
255 _log(logstream, "%s with stats: ops=%d, backtracks=%d, skipped=%d",
256 ("Succeeded" if success else "Failed"),
260 raise ResourceAllocationError, "Insufficient resources to grant request"
262 # Perform actual assignment
263 Pavail = map(set, partition)
265 for c, partial_assignments in partial.iteritems():
267 for i, nreq in partial_assignments:
270 raise AssertionError, "Cannot allocate resources for supposedly valid solution!"
271 assigned = set(sample(part, nreq))
276 # Format solution for the caller (return a node id for each request)
278 for c,reqs in c_reqlist.iteritems():
285 req_solution.append(solution[c].pop())
290 if __name__ == '__main__':
297 [[9, 11, 12, 14, 16, 17, 18, 20, 21],
300 [4, 5, 6, 7, 8, 11, 12, 13, 18, 22],
301 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22],
302 [6, 10, 11, 13, 14, 15, 16, 18, 20],
303 [3, 7, 8, 9, 10, 12, 14, 17, 22],
304 [0, 1, 3, 4, 5, 6, 7, 8, 10, 13, 14, 17, 19, 21, 22],
307 [[2, 10, 0, 3, 4, 8],
308 [4, 1, 6, 10, 2, 0, 5, 9, 8, 7],
309 [8, 3, 0, 2, 1, 4, 10, 7, 5],
313 [2, 7, 8, 3, 1, 0, 9, 10, 5, 4, 6],
314 [2, 4, 8, 10, 1, 3, 9],
317 [[2, 10, 0, 3, 4, 8],
318 [4, 1, 6, 10, 2, 0, 5, 9, 8, 7],
319 [8, 3, 0, 2, 1, 4, 10, 7, 5],
322 [2, 7, 8, 3, 1, 0, 9, 10, 5, 4, 6],
323 [2, 4, 8, 10, 1, 3, 9],
328 for n,(solvable,req) in enumerate(toughcases):
329 print "Trying #R = %4d, #S = %4d (tough case %d)" % (len(req), len(reduce(operator.or_, map(set,req), set())), n)
331 solution = alloc(req, sys.stdout, verbose=False)
333 print " OK - allocation successful"
335 raise AssertionError, "Case %r had no solution, but got %r" % (req, solution)
336 except ResourceAllocationError:
338 print " OK - allocation not possible"
340 raise AssertionError, "Case %r had a solution, but got none" % (req,)
344 suc_mostlypossible = mostlypossible = 0
345 suc_mostlyimpossible = mostlyimpossible = 0
349 # Fuzzer - mostly impossible cases
350 for i in xrange(10000):
351 nreq = random.randint(1,20)
352 nsrv = random.randint(max(1,nreq-5),50)
355 random.sample(srv, random.randint(1,nsrv))
356 for j in xrange(nreq)
358 print "Trying %5d: #R = %4d, #S = %4d... " % (i, nreq, nsrv),
360 mostlyimpossible += 1
362 solution = alloc(req, sys.stdout, saveinteresting = interesting, verbose=False)
363 suc_mostlyimpossible += 1
364 print " OK - allocation successful \r",
365 except ResourceAllocationError:
366 print " OK - allocation not possible \r",
367 except KeyboardInterrupt:
368 print "ABORTING CASE %r" % (req,)
372 # Fuzzer - mostly possible cases
373 for i in xrange(10000):
374 nreq = random.randint(1,10)
375 nsrv = random.randint(nreq,100)
378 random.sample(srv, random.randint(min(nreq,nsrv),nsrv))
379 for j in xrange(nreq)
381 print "Trying %5d: #R = %4d, #S = %4d... " % (i, nreq, nsrv),
385 solution = alloc(req, sys.stdout, saveinteresting = interesting, verbose=False)
386 suc_mostlypossible += 1
387 print " OK - allocation successful \r",
388 except ResourceAllocationError:
389 print " OK - allocation not possible \r",
390 except KeyboardInterrupt:
391 print "ABORTING CASE %r" % (req,)
395 # Fuzzer - biiig cases
397 nreq = random.randint(1,500)
398 nsrv = random.randint(1,8000)
401 random.sample(srv, random.randint(min(nreq,nsrv),nsrv))
402 for j in xrange(nreq)
404 print "Trying %4d: #R = %4d, #S = %4d... " % (i, nreq, nsrv),
408 solution = alloc(req, sys.stdout, saveinteresting = interesting, verbose=False)
410 print " OK - allocation successful \r",
411 except ResourceAllocationError:
412 print " OK - allocation not possible \r",
413 except KeyboardInterrupt:
414 print "ABORTING CASE %r" % (req,)
418 print "ABORTING TEST"
420 print "\nSuccess rates:"
421 print " Mostly possible: %d/%d (%.2f%%)" % (suc_mostlypossible, mostlypossible, 100.0 * suc_mostlypossible / max(1,mostlypossible))
422 print " Mostly impossible: %d/%d (%.2f%%)" % (suc_mostlyimpossible, mostlyimpossible, 100.0 * suc_mostlyimpossible / max(1,mostlyimpossible))
423 print " Huge: %d/%d (%.2f%%)" % (suc_huge, huge, 100.0 * suc_huge / max(1,huge))
426 print "%d interesting requests:" % (len(interesting),)
427 for n,req in enumerate(interesting):
428 print "Interesting request %d/%d: %r", (n,len(interesting),req,)