Пример #1
0
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)
Пример #2
0
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,
    })
Пример #3
0
	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)'