3 # Copyright 2009 the Melange authors.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """Slot allocation logic.
21 '"Sverre Rabbelier" <sverre@rabbelier.nl>',
28 class Error(Exception):
29 """Error class for the Allocation module.
35 class Allocator(object):
36 """A simple student slots allocator.
38 The buildSets method is used to validate the allocation data as well as
39 construct the sets that the algorithm then uses to distribute the slots.
40 By separating these steps it is possible to write a different allocation
41 algorithm but re-use the sets and validation logic.
44 # I tried to write explicit code that does not require any
45 # additional comments (with the exception of the set notation for
46 # the convenience of any mathematicians that happen to read this
49 def __init__(self
, orgs
, popularity
, max, slots
,
50 max_slots_per_org
, min_slots_per_org
, iterative
):
51 """Initializes the allocator.
54 orgs: a list of all the orgs that need to be allocated
55 popularity: the amount of applications per org
56 mentors: the amount of assigned mentors per org
57 slots: the total amount of available slots
58 max_slots_per_org: how many slots an org should get at most
59 min_slots_per_org: how many slots an org should at least get
62 self
.locked_slots
= {}
63 self
.adjusted_slots
= {}
64 self
.adjusted_orgs
= []
66 self
.unlocked_orgs
= []
67 self
.unlocked_applications
= []
69 self
.max_slots_per_org
= max_slots_per_org
70 self
.min_slots_per_org
= min_slots_per_org
72 self
.popularity
= None
73 self
.total_popularity
= None
74 self
.initial_popularity
= popularity
76 self
.iterative
= iterative
78 def allocate(self
, locked_slots
):
79 """Allocates the slots and returns the result.
82 locked_slots: a dict with orgs and the number of slots they get
83 adjusted_slots: a dict with orgs and the number of extra slots they get
86 self
.locked_slots
= locked_slots
90 if not sum(self
.popularity
.values()) or not sum(self
.max.values()):
91 return self
.popularity
94 return self
.iterativeAllocation()
96 return self
.preprocessingAllocation()
99 """Allocates slots with the specified constraints.
102 popularity
= self
.initial_popularity
.copy()
105 locked_slots
= self
.locked_slots
108 locked_orgs
= set(locked_slots
.keys())
111 unlocked_orgs
= self
.orgs
.difference(locked_orgs
)
113 # a+o and b+o should be o
114 locked_orgs_or_orgs
= self
.orgs
.union(locked_orgs
)
116 total_popularity
= sum(popularity
.values())
118 # a+o should be o, testing length is enough though
119 if len(locked_orgs_or_orgs
) != len(self
.orgs
):
120 raise Error("Unknown org as locked slot")
122 self
.unlocked_orgs
= unlocked_orgs
123 self
.locked_orgs
= locked_orgs
124 self
.popularity
= popularity
125 self
.total_popularity
= total_popularity
127 def rangeSlots(self
, slots
, org
):
128 """Returns the amount of slots for the org within the required bounds.
131 slots
= int(math
.floor(float(slots
)))
132 slots
= min(slots
, self
.max_slots_per_org
)
133 slots
= max(slots
, self
.min_slots_per_org
)
134 slots
= min(slots
, self
.max[org
])
138 def iterativeAllocation(self
):
139 """A simple iterative algorithm.
142 adjusted_orgs
= self
.adjusted_orgs
143 adjusted_slots
= self
.adjusted_slots
144 locked_orgs
= self
.locked_orgs
145 locked_slots
= self
.locked_slots
147 unallocated_popularity
= self
.total_popularity
- len(locked_slots
)
149 available_slots
= self
.slots
152 for org
in self
.orgs
:
153 popularity
= self
.popularity
[org
]
154 mentors
= self
.mentors
[org
]
156 if org
in locked_orgs
:
157 slots
= locked_slots
[org
]
158 elif unallocated_popularity
:
159 weight
= float(popularity
) / float(unallocated_popularity
)
160 slots
= int(math
.floor(weight
*available_slots
))
162 if org
in adjusted_orgs
:
163 slots
+= adjusted_slots
[org
]
165 slots
= min(slots
, self
.max_slots_per_org
)
166 slots
= min(slots
, mentors
)
167 slots
= min(slots
, available_slots
)
169 allocations
[org
] = slots
170 available_slots
-= slots
171 unallocated_popularity
-= popularity
175 def preprocessingAllocation(self
):
176 """An algorithm that pre-processes the input before running as normal.
179 adjusted_orgs
= self
.adjusted_orgs
180 adjusted_slots
= self
.adjusted_slots
181 locked_orgs
= self
.locked_orgs
182 locked_slots
= self
.locked_slots
183 unlocked_orgs
= self
.unlocked_orgs
184 total_popularity
= self
.total_popularity
186 available_slots
= self
.slots
190 for org
in locked_orgs
:
191 popularity
= self
.popularity
[org
]
192 slots
= locked_slots
[org
]
193 slots
= self
.rangeSlots(slots
, org
)
195 total_popularity
-= popularity
196 available_slots
-= slots
197 allocations
[org
] = slots
198 del self
.popularity
[org
]
200 # adjust the orgs in need of adjusting
201 for org
in adjusted_orgs
:
202 slots
= float(adjusted_slots
[org
])
204 adjustment
= (float(total_popularity
)/float(available_slots
))*slots
205 adjustment
= int(math
.ceil(adjustment
))
206 self
.popularity
[org
] += adjustment
207 total_popularity
+= adjustment
209 # adjust the popularity so that the invariants are always met
210 for org
in unlocked_orgs
:
211 popularity
= self
.popularity
[org
]
212 # mentors = self.mentors[org]
214 slots
= (float(popularity
)/float(total_popularity
))*available_slots
215 slots
= self
.rangeSlots(slots
, org
)
217 popularity
= (float(total_popularity
)/float(available_slots
))*slots
219 self
.popularity
[org
] = popularity
221 total_popularity
= sum(self
.popularity
.values())
223 # do the actual calculation
224 for org
in unlocked_orgs
:
225 popularity
= self
.popularity
[org
]
226 raw_slots
= (float(popularity
)/float(total_popularity
))*available_slots
227 slots
= int(math
.floor(raw_slots
))
229 slack
[org
] = raw_slots
- slots
230 allocations
[org
] = slots
232 slots_left
= available_slots
- sum(allocations
.values())
234 # add leftover slots, sorted by slack, decending
235 for org
, slack
in sorted(slack
.iteritems(),
236 key
=lambda (k
, v
): v
, reverse
=True):
240 current
= allocations
[org
]
241 slots
= self
.rangeSlots(current
+ 1, org
)
243 slots_left
+= slots
- current
244 allocations
[org
] = slots