app/soc/logic/allocations.py
changeset 1056 d0c82bdc2de2
child 1307 091a21cf3627
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/soc/logic/allocations.py	Thu Jan 29 01:39:31 2009 +0000
@@ -0,0 +1,167 @@
+#!/usr/bin/python2.5
+#
+# Copyright 2008 the Melange authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Slot allocation logic.
+"""
+
+__authors__ = [
+  '"Sverre Rabbelier" <sverre@rabbelier.nl>',
+  ]
+
+
+import itertools
+import math
+
+
+class Error(Exception):
+  """Error class for the Allocation module.
+  """
+
+  pass
+
+
+class Allocator(object):
+  """A simple student slots allocator.
+
+  The buildSets method is used to validate the allocation data as well as
+  construct the sets that the algorithm then uses to distribute the slots.
+  By separating these steps it is possible to write a different allocation
+  algorithm but re-use the sets and validation logic.
+  """
+
+  # I tried to write explicit code that does not require any
+  # additional comments (with the exception of the set notation for
+  # the convenience of any mathematicians that happen to read this
+  # piece of code ;).
+
+  def __init__(self, orgs, applications, slots, max_slots_per_org):
+    """Initializes the allocator.
+
+    Args:
+      orgs: a list of all the orgs that need to be allocated
+      applications: a dictionary with for each org a list of applicants
+      slots: the total amount of available slots
+    """
+
+    all_applications = []
+
+    for _, value in applications.iteritems():
+      all_applications += value
+
+    self.slots = slots
+    self.max_slots_per_org = max_slots_per_org
+    self.orgs = set(orgs)
+    self.applications = applications
+    self.all_applications = set(all_applications)
+
+  def allocate(self, locked_slots, adjusted_slots):
+    """Allocates the slots and returns the result.
+
+    Args:
+      locked_slots: a dict with orgs and the number of slots they get
+      adjusted_slots: a dict with orgs and the number of extra slots they get
+    """
+
+    self.locked_slots = locked_slots
+    self.adjusted_slots = adjusted_slots
+
+    self.buildSets()
+
+    return self.iterativeAllocation()
+
+  def buildSets(self):
+    """Allocates slots with the specified constraints
+    """
+
+    # set s
+    all_applications = self.all_applications
+    locked_slots = self.locked_slots
+    adjusted_slots = self.adjusted_slots
+
+    # set a and b
+    locked_orgs = set(locked_slots.keys())
+    adjusted_orgs = set(adjusted_slots.keys())
+
+    # set a' and b'
+    unlocked_orgs = self.orgs.difference(locked_orgs)
+    unadjusted_orgs = self.orgs.difference(adjusted_orgs)
+
+    # set a*b and a'*b'
+    locked_and_adjusted_orgs = locked_orgs.intersection(adjusted_orgs)
+    unlocked_and_unadjusted_orgs = unlocked_orgs.intersection(unadjusted_orgs)
+
+    # a+o and b+o should be o
+    locked_orgs_or_orgs = self.orgs.union(locked_orgs)
+    adjusted_orgs_or_orgs = self.orgs.union(adjusted_orgs)
+
+    # an item can be only a or b, so a*b should be empty
+    if locked_and_adjusted_orgs:
+      raise Error("Cannot have an org locked and adjusted")
+
+    # a+o should be o, testing length is enough though
+    if len(locked_orgs_or_orgs) != len(self.orgs):
+      raise Error("Unknown org as locked slot")
+
+    # same for b+o
+    if len(adjusted_orgs_or_orgs) != len(self.orgs):
+      raise Error("Unknown org as adjusted slot")
+
+    # set l and l'
+    locked_applications = set(itertools.chain(*locked_slots.keys()))
+    unlocked_applications = all_applications.difference(locked_applications)
+
+    self.adjusted_orgss = adjusted_orgs
+    self.locked_orgs = locked_orgs
+    self.unlocked_applications = unlocked_applications
+
+  def iterativeAllocation(self):
+    """A simple iterative algorithm.
+    """
+
+    adjusted_orgs = self.adjusted_orgss
+    adjusted_slots = self.adjusted_slots
+    locked_orgs = self.locked_orgs
+    locked_slots = self.locked_slots
+    unlocked_applications = self.unlocked_applications
+
+    unlocked_applications_count = len(unlocked_applications)
+    unallocated_applications_count = unlocked_applications_count
+
+    available_slots = self.slots
+    allocations = {}
+
+    for org in self.orgs:
+      org_applications = self.applications[org]
+      org_applications_count = len(org_applications)
+
+      if org in locked_orgs:
+        slots = locked_slots[org]
+      else:
+        weight = float(org_applications_count) / unallocated_applications_count
+        slots = int(math.floor(weight*available_slots))
+
+      if org in adjusted_orgs:
+        slots += adjusted_slots[org]
+
+      slots = min(slots, self.max_slots_per_org)
+      slots = min(slots, org_applications_count)
+      slots = min(slots, available_slots)
+
+      allocations[org] = slots
+      available_slots -= slots
+      unallocated_applications_count -= org_applications_count
+
+    return allocations