def assign(availability, A, D, slot, deg, current_hours): ''' Is there actually an availability? ''' if not availability.pk in map_pk(A[slot.pk]): return (D, current_hours, False) ''' Is the user not already working on this shift? ''' if availability.user.pk in map_user_pk(D[slot.pk]): return (D, current_hours, False) ''' Update the times of new and possibly old user ''' current_hours[availability.user.pk] += to_hours(slot.duration) if D[slot.pk][deg] is not None: current_hours[D[slot.pk][deg].user.pk] -= to_hours(slot.duration) ''' And finally, the actual update ''' D[slot.pk][deg] = availability return (D, current_hours, True)
def invite_workers(request, roster): try: roster = Roster.objects.get(name = roster) except Roster.DoesNotExist: return notification(request, 'Er is geen rooster genaamd \'%s\' gevonden' % roster) if roster.state > 1: return notification(request, 'Je kunt nog niet, of niet langer, mensen uitnodigen') workers = RosterWorker.objects.filter(roster = roster) if not workers: return notification(request, 'Voeg een of meer werkers toe') else: roster.state = 1 roster.save() for worker in workers: availabilities = filter(lambda av: av.roster == roster, Availability.objects.filter(user = worker.user)) if availabilities: worker.hours_entered = to_hours(reduce(lambda d1, d2: d1 + d2, map(lambda av: av.timeslot.duration, availabilities))) else: worker.hours_entered = 0 base_url = 'http://' + request.META['HTTP_HOST'] timedelta_total = roster.total_work_time hours_total = timedelta_total.days * 24 + timedelta_total.seconds / 3600 return render(request, 'invite_workers.html', { 'hours_total': hours_total, 'hours_pp': round(hours_total / len(workers), 1), 'roster': roster, 'workers': workers, 'base_url': base_url, })
def handle(self, *args, **options): """ Input arguments """ t_init = time.time() try: roster = Roster.objects.get(pk=int(args[0])) except IndexError: print 'Please provide a roster ID' show_rosters() return except ValueError: print 'Please provide an integer roster ID\n' show_rosters() return except Roster.DoesNotExist: print 'Roster with ID %s not found\n' % args[0] show_rosters() return N = options['steps'] check_consistency = options['check'] alpha = options['alpha'] if roster.state < 1: print 'This roster is is not ready to distribute yet, complete other steps first' return elif roster.state == 3: if options['force']: roster.state = 2 roster.save() else: print 'This roster has already been distributed; use -f to force redistribution' return elif roster.state > 3: print 'This roster has already been distributed' return elif roster.state == 1: roster.state = 2 roster.save() print '* %s *' % roster.name.upper() ''' Create the availability data structure ''' t_load = time.time() slotmap = {ts.pk: ts for ts in TimeSlot.objects.filter(roster=roster)} availabilities_qs = Availability.objects.filter( timeslot__roster=roster) t_assign = time.time() print 'TIME query data: %.3fs' % (t_assign - t_load) A, D = {}, {} degeneracy, duration, similar = {}, {}, {} slots = [] total_hours = 0 for slot in slotmap.values(): slots.append(slot) D[slot.pk] = slot.degeneracy * [None] A[slot.pk] = [] degeneracy[slot.pk] = slot.degeneracy duration[slot.pk] = to_hours(slot.duration) similar[slot.pk] = [] total_hours += slot.degeneracy * to_hours(slot.duration) ''' Find similar shifts (exactly a week difference) ''' monday = week_start_date(slot.start.isocalendar()[0], slot.start.isocalendar()[1]) day = datetime.timedelta(days=1) weeks = 0 * day while monday + weeks >= datetime.datetime.combine( roster.start, datetime.time()): weeks -= 7 * day while monday + weeks <= datetime.datetime.combine( roster.end, datetime.time()): if not ((monday + weeks).isocalendar()[0] == slot.start.isocalendar()[0] and (monday + weeks).isocalendar()[1] == slot.start.isocalendar()[1]): sim_slot = TimeSlot.objects.filter(roster=roster).filter( roster=slot.roster, start=slot.start + weeks, end=slot.end + weeks) if sim_slot: similar[slot.pk].append(sim_slot[0]) weeks += 7 * day for availability in availabilities_qs: ''' There should be no duplicates but it happens, somehow. Don't know where they come from, but I can't block them at database level because sqlite seems to throw random errors for unique_together... ''' if availability.user.pk not in map_user_pk( A[availability.timeslot.pk]): A[availability.timeslot.pk].append(availability) else: print 'There was a duplicate Availability; #%d removed' % availability.pk Availability.objects.get(pk=availability.pk).delete() if not N: N = 10 * len(slots) ''' Calculate the hours ''' """ extra hours for flexibility are calculated as: alpha * sqrt(user_available_hours / per_person_expected) [but never negative] """ t_hours = time.time() print 'TIME assign data: %.3fs' % (t_hours - t_assign) workers = RosterWorker.objects.filter(roster=roster) current_hours = {} extra_hours = {} flexibility = {} expected_hours = float(total_hours) / len(workers) available_hours = {worker.user.pk: 0 for worker in workers} for availability_list in A.values(): for availability in availability_list: available_hours[availability.user.pk] += to_hours( availability.timeslot.duration) for worker in workers: extra_hours[worker.user.pk] = worker.extra flexibility[worker.user.pk] = sqrt( available_hours[worker.user.pk] / expected_hours) extra_hours[worker.user.pk] += alpha * flexibility[worker.user.pk] current_hours[worker.user.pk] = 0 ''' Monte Carlo (with only favourable steps though) ''' t_monte = time.time() no_change_steps = 0 counter = 500 * len(slots) while no_change_steps < N and counter > 0: updated = False counter -= 1 ''' Trivial slots are removed; if there are no slots left we quit ''' if not len(slots): print 'DETERMINISTIC / TRIVIAL' break slot = weighted_choice(slots, degeneracy.values()) if len(A[slot.pk]) <= degeneracy[slot.pk]: ''' No competition for this position ''' for deg in range(degeneracy[slot.pk]): min_av = select_fewest_hours( set(A[slot.pk]) - set(D[slot.pk]), current_hours, extra_hours) ''' Note that one of the positions might have been filled through the recursion mechanism ''' if min_av and not D[slot.pk][deg]: (D, current_hours, updated) = assign(min_av, A, D, slot, deg, current_hours) slots.remove(slot) degeneracy.pop(slot.pk) else: deg = random.randint(0, len(D[slot.pk]) - 1) if D[slot.pk][deg] == None: ''' No current worker, so no comparison ''' worker = random.choice(A[slot.pk]) (D, current_hours, updated) = assign(worker, A, D, slot, deg, current_hours) elif len(A[slot.pk]) > 1: ''' There is a selected worker, and at least one alternative ''' worker = random.choice(A[slot.pk]) while worker.pk == D[slot.pk][deg].pk: worker = random.choice(A[slot.pk]) (D, current_hours, updated) = switch(slot, deg, worker, A, D, duration, similar, current_hours, extra_hours, recursive=True) ''' Count the steps without change (the while loop quits after N of them) The only way assign is called multiple times is when there is one succesful switch, in which case True is returned anyway ''' no_change_steps += 1 if updated: no_change_steps = 0 ''' Prevent unnecessary blanks (generally none are found) ''' t_post = time.time() print 'TIME monte carlo: %.3fs' % (t_post - t_monte) for slot in slots: for deg in range(degeneracy[slot.pk]): if D[slot.pk][deg] == None: ''' There is an empty slot; find the candidate with fewest hours ''' min_av = select_fewest_hours( set(A[slot.pk]) - set(D[slot.pk]), current_hours, extra_hours) if min_av: (D, current_hours, updated) = assign(min_av, A, D, slot, deg, current_hours) ''' Consistency checks ''' t_check = time.time() print 'TIME post check: %.3fs' % (t_check - t_post) if check_consistency: all_slot_check = 0 for slot in slots: all_slot_check += to_hours(slot.duration) * slot.degeneracy ''' Check the degeneracy values ''' assert len(D[slot.pk]) == slot.degeneracy ''' Check that there aren't more shifts occupied than there are available people ''' assert len(filter(lambda x: x, D[slot.pk])) <= len(A[slot.pk]) ''' Check that a person doesn't have two shifts in one slot ''' assert len(map_user_pk(D[slot.pk])) == len( set(map_user_pk(D[slot.pk]))) ''' Check that the total hours of all slots does not exceed the assigned hours ''' all_assign_check = 0 for slot_pk, avity_list in D.items(): for deg, avity in enumerate(avity_list): if avity: all_assign_check += to_hours(avity.timeslot.duration) ''' Check that all assigned shifts belong to someone that is available ''' assert avity.pk in map_pk(A[slot_pk]) else: ''' If the slot is empty, then no one is available that isn't already working ''' if len( set(map_pk(A[slot_pk])) - set(map_pk(D[slot_pk]))): print '>> %s | %s (%d)' % (set(map_pk( A[slot_pk])), set(map_pk( D[slot_pk])), len(D[slot_pk])) print map_user_pk(A[slot_pk]) assert len( set(map_pk(A[slot_pk])) - set(map_pk(D[slot_pk]))) == 0 ''' check sum of hours (bugged end 2013) and empty slots ''' current_hours_check = {} for rw in workers: current_hours_check[rw.user.pk] = 0 for slot_pk, avity_list in D.items(): for deg, avity in enumerate(avity_list): if avity is None: ''' check that, for every empty slot, there are fewer availabilities than places ''' assert len(A[slot_pk]) <= [ slot.degeneracy for slot in slotmap.values() if slot.pk == slot_pk ][0] else: current_hours_check[avity.user.pk] += to_hours( avity.timeslot.duration) ''' check that the total hours are the same ''' assert sum(current_hours.values()) == sum( current_hours_check.values()) assert len(current_hours) == len(current_hours_check) ''' check that the hours for individual users are the same ''' for user_pk in current_hours.keys(): assert current_hours[user_pk] == current_hours_check[user_pk] ''' Check that the hours of assigned shifts match the total of current_hours ''' assert all_assign_check == sum(current_hours.values()) ''' Store result ''' t_store = time.time() if check_consistency: print 'TIME checks: %.3fs' % (t_store - t_check) else: print 'TIME checks: off' roster = Roster.objects.get(pk=roster.pk) if not roster.state == 2: print 'The state of the roster has changed! There may be a concurrent process. Mission aborted; no changes will be made.' return Assignment.objects.filter(timeslot__in=slotmap.values()).delete() batch = [] empty_slots = [] for key, availability_list in D.items(): for deg, availability in enumerate(availability_list): if availability is None: empty_slots.append(slotmap[key]) else: batch.append( Assignment(user=availability.user, timeslot=availability.timeslot, note='shift %d' % deg)) Assignment.objects.bulk_create(batch) roster.state = 3 roster.save() ''' Process the result ''' t_result = time.time() print 'TIME store result: %.3fs' % (t_result - t_store) print 'TIME other steps: %.3fs' % (t_result - t_init - (t_check - t_monte) - (t_hours - t_assign) - (t_assign - t_load) - (t_result - t_store) - (t_store - t_check)) print 'TIME total time: %.3fs' % (t_result - t_init) print 'remaining hours: %d' % (total_hours - sum(current_hours.values())) for empty_slot in empty_slots: print '\t %s (%.1fh)' % (empty_slot, to_hours(empty_slot.duration)) print 'with alpha = %.3f max extra hours is %.1fh' % ( alpha, alpha * (max(flexibility.values()) - min(flexibility.values()))) print ' user\tfinal\textra (goal)' for user_pk in current_hours.keys(): print ' %-20s\t%d\t%.1f' % (unicode(get_user_model().objects.get( pk=user_pk))[:20], int( current_hours[user_pk]), extra_hours[user_pk]) if not counter: print '(Monte Carlo reached iteration limit)'
def handle(self, *args, **options): """ Input arguments """ t_init = time.time() try: roster = Roster.objects.get(pk = int(args[0])) except IndexError: print 'Please provide a roster ID' show_rosters() return except ValueError: print 'Please provide an integer roster ID\n' show_rosters() return except Roster.DoesNotExist: print 'Roster with ID %s not found\n' % args[0] show_rosters() return N = options['steps'] check_consistency = options['check'] alpha = options['alpha'] if roster.state < 1: print 'This roster is is not ready to distribute yet, complete other steps first' return elif roster.state == 3: if options['force']: roster.state = 2 roster.save() else: print 'This roster has already been distributed; use -f to force redistribution' return elif roster.state > 3: print 'This roster has already been distributed' return elif roster.state == 1: roster.state = 2 roster.save() print '* %s *' % roster.name.upper() ''' Create the availability data structure ''' t_load = time.time() slotmap = {ts.pk: ts for ts in TimeSlot.objects.filter(roster = roster)} availabilities_qs = Availability.objects.filter(timeslot__roster = roster) t_assign = time.time() print 'TIME query data: %.3fs' % (t_assign - t_load) A, D = {}, {} degeneracy, duration, similar = {}, {}, {} slots = [] total_hours = 0 for slot in slotmap.values(): slots.append(slot) D[slot.pk] = slot.degeneracy * [None] A[slot.pk] = [] degeneracy[slot.pk] = slot.degeneracy duration[slot.pk] = to_hours(slot.duration) similar[slot.pk] = [] total_hours += slot.degeneracy * to_hours(slot.duration) ''' Find similar shifts (exactly a week difference) ''' monday = week_start_date(slot.start.isocalendar()[0], slot.start.isocalendar()[1]) day = datetime.timedelta(days = 1) weeks = 0 * day while monday + weeks >= datetime.datetime.combine(roster.start, datetime.time()): weeks -= 7 * day while monday + weeks <= datetime.datetime.combine(roster.end, datetime.time()): if not ((monday + weeks).isocalendar()[0] == slot.start.isocalendar()[0] and (monday + weeks).isocalendar()[1] == slot.start.isocalendar()[1]): sim_slot = TimeSlot.objects.filter(roster = roster).filter(roster = slot.roster, start = slot.start + weeks, end = slot.end + weeks) if sim_slot: similar[slot.pk].append(sim_slot[0]) weeks += 7 * day for availability in availabilities_qs: ''' There should be no duplicates but it happens, somehow. Don't know where they come from, but I can't block them at database level because sqlite seems to throw random errors for unique_together... ''' if availability.user.pk not in map_user_pk(A[availability.timeslot.pk]): A[availability.timeslot.pk].append(availability) else: print 'There was a duplicate Availability; #%d removed' % availability.pk Availability.objects.get(pk = availability.pk).delete() if not N: N = 10 * len(slots) ''' Calculate the hours ''' """ extra hours for flexibility are calculated as: alpha * sqrt(user_available_hours / per_person_expected) [but never negative] """ t_hours = time.time() print 'TIME assign data: %.3fs' % (t_hours - t_assign) workers = RosterWorker.objects.filter(roster = roster) current_hours = {} extra_hours = {} flexibility = {} expected_hours = float(total_hours) / len(workers) available_hours = {worker.user.pk: 0 for worker in workers} for availability_list in A.values(): for availability in availability_list: available_hours[availability.user.pk] += to_hours(availability.timeslot.duration) for worker in workers: extra_hours[worker.user.pk] = worker.extra flexibility[worker.user.pk] = sqrt(available_hours[worker.user.pk] / expected_hours) extra_hours[worker.user.pk] += alpha * flexibility[worker.user.pk] current_hours[worker.user.pk] = 0 ''' Monte Carlo (with only favourable steps though) ''' t_monte = time.time() no_change_steps = 0 counter = 500 * len(slots) while no_change_steps < N and counter> 0: updated = False counter -= 1 ''' Trivial slots are removed; if there are no slots left we quit ''' if not len(slots): print 'DETERMINISTIC / TRIVIAL' break slot = weighted_choice(slots, degeneracy.values()) if len(A[slot.pk]) <= degeneracy[slot.pk]: ''' No competition for this position ''' for deg in range(degeneracy[slot.pk]): min_av = select_fewest_hours(set(A[slot.pk]) - set(D[slot.pk]), current_hours, extra_hours) ''' Note that one of the positions might have been filled through the recursion mechanism ''' if min_av and not D[slot.pk][deg]: (D, current_hours, updated) = assign(min_av, A, D, slot, deg, current_hours) slots.remove(slot) degeneracy.pop(slot.pk) else: deg = random.randint(0, len(D[slot.pk]) - 1) if D[slot.pk][deg] == None: ''' No current worker, so no comparison ''' worker = random.choice(A[slot.pk]) (D, current_hours, updated) = assign(worker, A, D, slot, deg, current_hours) elif len(A[slot.pk]) > 1: ''' There is a selected worker, and at least one alternative ''' worker = random.choice(A[slot.pk]) while worker.pk == D[slot.pk][deg].pk: worker = random.choice(A[slot.pk]) (D, current_hours, updated) = switch(slot, deg, worker, A, D, duration, similar, current_hours, extra_hours, recursive = True) ''' Count the steps without change (the while loop quits after N of them) The only way assign is called multiple times is when there is one succesful switch, in which case True is returned anyway ''' no_change_steps += 1 if updated: no_change_steps = 0 ''' Prevent unnecessary blanks (generally none are found) ''' t_post = time.time() print 'TIME monte carlo: %.3fs' % (t_post - t_monte) for slot in slots: for deg in range(degeneracy[slot.pk]): if D[slot.pk][deg] == None: ''' There is an empty slot; find the candidate with fewest hours ''' min_av = select_fewest_hours(set(A[slot.pk]) - set(D[slot.pk]), current_hours, extra_hours) if min_av: (D, current_hours, updated) = assign(min_av, A, D, slot, deg, current_hours) ''' Consistency checks ''' t_check = time.time() print 'TIME post check: %.3fs' % (t_check - t_post) if check_consistency: all_slot_check = 0 for slot in slots: all_slot_check += to_hours(slot.duration) * slot.degeneracy ''' Check the degeneracy values ''' assert len(D[slot.pk]) == slot.degeneracy ''' Check that there aren't more shifts occupied than there are available people ''' assert len(filter(lambda x: x, D[slot.pk])) <= len(A[slot.pk]) ''' Check that a person doesn't have two shifts in one slot ''' assert len(map_user_pk(D[slot.pk])) == len(set(map_user_pk(D[slot.pk]))) ''' Check that the total hours of all slots does not exceed the assigned hours ''' all_assign_check = 0 for slot_pk, avity_list in D.items(): for deg, avity in enumerate(avity_list): if avity: all_assign_check += to_hours(avity.timeslot.duration) ''' Check that all assigned shifts belong to someone that is available ''' assert avity.pk in map_pk(A[slot_pk]) else: ''' If the slot is empty, then no one is available that isn't already working ''' if len(set(map_pk(A[slot_pk])) - set(map_pk(D[slot_pk]))): print '>> %s | %s (%d)' % (set(map_pk(A[slot_pk])), set(map_pk(D[slot_pk])), len(D[slot_pk])) print map_user_pk(A[slot_pk]) assert len(set(map_pk(A[slot_pk])) - set(map_pk(D[slot_pk]))) == 0 ''' check sum of hours (bugged end 2013) and empty slots ''' current_hours_check = {} for rw in workers: current_hours_check[rw.user.pk] = 0 for slot_pk, avity_list in D.items(): for deg, avity in enumerate(avity_list): if avity is None: ''' check that, for every empty slot, there are fewer availabilities than places ''' assert len(A[slot_pk]) <= [slot.degeneracy for slot in slotmap.values() if slot.pk == slot_pk][0] else: current_hours_check[avity.user.pk] += to_hours(avity.timeslot.duration) ''' check that the total hours are the same ''' assert sum(current_hours.values()) == sum(current_hours_check.values()) assert len(current_hours) == len(current_hours_check) ''' check that the hours for individual users are the same ''' for user_pk in current_hours.keys(): assert current_hours[user_pk] == current_hours_check[user_pk] ''' Check that the hours of assigned shifts match the total of current_hours ''' assert all_assign_check == sum(current_hours.values()) ''' Store result ''' t_store = time.time() if check_consistency: print 'TIME checks: %.3fs' % (t_store - t_check) else: print 'TIME checks: off' roster = Roster.objects.get(pk = roster.pk) if not roster.state == 2: print 'The state of the roster has changed! There may be a concurrent process. Mission aborted; no changes will be made.' return Assignment.objects.filter(timeslot__in = slotmap.values()).delete() batch = [] empty_slots = [] for key, availability_list in D.items(): for deg, availability in enumerate(availability_list): if availability is None: empty_slots.append(slotmap[key]) else: batch.append(Assignment(user = availability.user, timeslot = availability.timeslot, note = 'shift %d' % deg)) Assignment.objects.bulk_create(batch) roster.state = 3 roster.save() ''' Process the result ''' t_result = time.time() print 'TIME store result: %.3fs' % (t_result - t_store) print 'TIME other steps: %.3fs' % (t_result - t_init - (t_check - t_monte) - (t_hours - t_assign) - (t_assign - t_load) - (t_result - t_store) - (t_store - t_check)) print 'TIME total time: %.3fs' % (t_result - t_init) print 'remaining hours: %d' % (total_hours - sum(current_hours.values())) for empty_slot in empty_slots: print '\t %s (%.1fh)' % (empty_slot, to_hours(empty_slot.duration)) print 'with alpha = %.3f max extra hours is %.1fh' % (alpha, alpha * (max(flexibility.values()) - min(flexibility.values()))) print ' user\tfinal\textra (goal)' for user_pk in current_hours.keys(): print ' %-20s\t%d\t%.1f' % (unicode(get_user_model().objects.get(pk = user_pk))[:20], int(current_hours[user_pk]), extra_hours[user_pk]) if not counter: print '(Monte Carlo reached iteration limit)'