Created
April 24, 2017 16:48
-
-
Save andrewjbennett/8b9ae439ffbc8b3648a17388c7ca461d to your computer and use it in GitHub Desktop.
Hacky script to generate my possible timetables for next semester, eliminate any that don't meet my requirements, then rank them in order of "goodness".
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Andrew Bennett <andrew.bennett@unsw.edu.au> | |
# April 2017 | |
# Hacky script to generate my possible timetables for next semester, | |
# eliminate any that don't meet my requirements, then rank them in | |
# order of "goodness". | |
import json | |
import random | |
import operator | |
import itertools | |
# have an object representing a given possible timetable arrangement, | |
# to make sorting by given attributes nicer | |
class PossibleTimetable(object): | |
def __init__(self, classes, info): | |
self.intervals = generate_intervals(classes) | |
self.info = info | |
self._gen_days_off() | |
self._gen_start_time() | |
self._gen_tutor_distribution() | |
self._forensics_lab_slot() | |
self._teaching_days() | |
def _teaching_days(self): | |
self.teaching_days = [0,0,0,0,0] | |
upto_day = -1 | |
for day in self.intervals: | |
upto_day += 1 | |
for hour in day: | |
if hour in tutoring: | |
self.teaching_days[upto_day] = 1 | |
break | |
def _forensics_lab_slot(self): | |
upto_day = -1 | |
for day in self.intervals: | |
upto_day += 1 | |
for hour in range(len(day)-2): | |
if day[hour] == "COMP6845" and day[hour+1] == day[hour] and day[hour+2] == day[hour]: | |
break | |
if day[hour] == "COMP6845" and day[hour] == day[hour+1] and day[hour] != day[hour+2]: | |
self._forensics_lab_day = upto_day | |
self._forensics_lab_hour = hour | |
return | |
def _gen_days_off(self): | |
self.days_off = 0 | |
for day in self.intervals: | |
off = True | |
for x in day: | |
if len(x)>4: | |
off = False | |
break | |
if off: | |
self.days_off += 1 | |
def _gen_tutor_distribution(self): | |
self.tutor_days = 0 | |
for day in range(len(self.intervals)): | |
for x in self.intervals[day]: | |
if len(x) > 4 and x in tutoring: | |
self.tutor_days += day+1 | |
break | |
self.tutor_days *= -1 | |
def _gen_start_time(self): | |
self.start_time = 100 | |
sum_times = 0 | |
for day in self.intervals: | |
for x in range(len(day)): | |
if len(day[x]) > 4: | |
self.start_time = min(x, self.start_time) | |
sum_times += x | |
break | |
self._avg_start_time = sum_times/(5-self.days_off) | |
def _print(self): | |
print "=" * (22+36) | |
print "====== %35s ======" % self.info | |
print "=" * (22+36) | |
print "Earliest start: %d" % self.start_time | |
print "Average start: %d" % self._avg_start_time | |
print "Days off: %d" % self.days_off | |
print_timetable_intervals(self.intervals) | |
def calc_possible_timetables(student_classes, tutor_classes, timetables, info): | |
all_classes = student_classes + tutor_classes | |
combs = itertools.product(*all_classes) | |
for comb in combs: | |
if valid_combination(comb): | |
timetable = generate_timetable(comb, info) | |
if timetable not in timetables: | |
timetables.add(timetable) | |
def add_thesis_classes_old(student_classes): | |
# add some pretend blocks of thesis time | |
# let's go for: 2x four hour blocks of time between 1pm and 8pm | |
# 1-5, 2-6, 3-7, 4-8, 5-9 | |
duration = 6 | |
end_time = 21 | |
the_classes = [] | |
for day in range(2,5): | |
for start_time in range(14, end_time-duration): | |
finish_time = start_time + duration | |
the_classes.append( | |
{'course': "THESIS00", | |
'day': day, | |
'start': start_time, | |
'end': finish_time, | |
'tutor': False} | |
) | |
# turns out doing it "nicely" makes ugly collisions; just hardcode in two blocks on thu and fri | |
def add_thesis_classes(student_classes): | |
student_classes.append([{'course': "THESIS00", | |
'day': 3, | |
'start': 14, | |
'end': 20, | |
'tutor': False} ]) | |
student_classes.append([{'course': "THESIS00", | |
'day': 4, | |
'start': 14, | |
'end': 20, | |
'tutor': False} ]) | |
def generate_timetable(classes, info): | |
timetable = PossibleTimetable(classes, info) | |
return timetable | |
def get_class_types(course_code): | |
return tmp_courses[course_code]['class_types'].keys() | |
def get_classes(course_code, class_type): | |
try: | |
return tmp_courses[course_code]['class_types'][class_type] | |
except: | |
pass | |
def print_timetable_intervals(intervals): | |
day_upto = 0 | |
print "----------------------------------------------------------" | |
print " | MONDAY | TUESDAY | WEDNSDAY | THURSDAY | FRIDAY |" | |
print "----------------------------------------------------------" | |
for x in range(len(intervals[0])): | |
if x < 9: | |
continue | |
print "%02d|" % x, | |
for day in intervals: | |
# print "Day %d" % day_upto | |
# day_upto += 1 | |
course = day[x] | |
if len(course) == 0: | |
course = "" | |
print "%8s |" % course, | |
#for x in range(len(day)): | |
# print "%02d: %s" % (x, day[x]) | |
print "----------------------------------------------------------" | |
# --------------------- timetable constraints --------------------- # | |
def generate_intervals(classes): | |
intervals = [[[] for _ in range(0,22)] for _ in range(5)] | |
for c in classes: | |
for x in range(c['start'], c['end']): | |
if not intervals[c['day']][x]: | |
intervals[c['day']][x] = c['course'] | |
else: | |
return False | |
return intervals | |
def print_timetable(classes): | |
intervals = generate_intervals(classes) | |
print_timetable_intervals(intervals) | |
def hours_per_day(intervals): | |
max_hours = 0 | |
for day in intervals: | |
filled = 0 | |
for h in day: | |
if h: | |
filled += 1 | |
max_hours = max(filled, max_hours) | |
return max_hours | |
def too_early(classes): | |
TOO_EARLY = 11 | |
for c in classes: | |
if c['start'] < TOO_EARLY: | |
return True | |
if c['day'] == 2 and c['start'] < TOO_EARLY+2: | |
return True | |
if c['start'] < TOO_EARLY+3 and not c['tutor']: | |
return True | |
return False | |
def too_late(classes): | |
TOO_LATE = 20 | |
for c in classes: | |
if c['end'] > TOO_LATE and c['day'] != 1: | |
#print "rip" | |
#print c | |
return True | |
return False | |
def class_on_monday(intervals): | |
for x in intervals[0]: | |
if len(x) > 4: | |
return True | |
return False | |
def no_gaps(intervals): | |
for day in intervals: | |
for x in range(len(day)-1): | |
if len(day[x]) < 5 or len(day[x+1]) < 5: | |
continue | |
if day[x] != day[x+1]: | |
#print_timetable(intervals) | |
return True | |
return False | |
def studenting_before_tutoring(intervals): | |
for day in intervals: | |
prev_course = None | |
curr_course = None | |
for hour in day: | |
if len(hour) < 4: | |
continue | |
if prev_course is None: | |
prev_course = hour | |
continue | |
if hour == prev_course: | |
continue | |
if prev_course in studenting: | |
if hour in tutoring: | |
return True | |
prev_course = hour | |
return False | |
def too_many_hours_tutoring(intervals): | |
MAX_TUTORING_HOURS = 5 | |
for day in intervals: | |
tutoring_hours = 0 | |
for hour in day: | |
if len(hour) < 4: | |
continue | |
if hour in tutoring: | |
tutoring_hours += 1 | |
if tutoring_hours > MAX_TUTORING_HOURS: | |
return True | |
return False | |
def valid_combination(classes): | |
intervals = generate_intervals(classes) | |
if not intervals: # clashes | |
return False | |
if too_early(classes): | |
return False | |
if too_late(classes): | |
return False | |
if no_gaps(intervals): | |
return False | |
if hours_per_day(intervals) > 6: | |
return False | |
if studenting_before_tutoring(intervals): | |
return False | |
if too_many_hours_tutoring(intervals): | |
return False | |
if not class_on_monday(intervals): | |
return False | |
#print_timetable_intervals(intervals) | |
return True | |
# --------------------- actually do stuff --------------------- # | |
with open("data/courses.json", "rb") as courses: | |
a = json.loads(courses.read()) | |
studenting = [] | |
tutoring = [] | |
tmp_courses = {} | |
courses = {} | |
for x in a['courses']: | |
code = x['course_code'] | |
tmp_courses[code] = x | |
courses[code] = {} | |
courses[code]['class_types'] = {} | |
for class_type in get_class_types(code): | |
times = set() | |
courses[code]['class_types'][class_type] = [] | |
classes = get_classes(code, class_type) | |
for clas in classes: | |
start_time = clas[0]['start_time'] | |
end_time = clas[-1]['end_time'] | |
class_day = clas[0]['class_day'] | |
if start_time < 11: # TOO EARLY | |
continue | |
# deal with multiple tutes at the same time | |
times_tuple = (start_time, end_time, class_day) | |
if times_tuple in times: | |
continue | |
times.add(times_tuple) | |
courses[code]['class_types'][class_type].append({'course': code, | |
'day': class_day, | |
'start': start_time, | |
'end': end_time, | |
'tutor':x['tutor']}) | |
if x['tutor'] is True: | |
tutoring.append(x['course_code']) | |
else: | |
studenting.append(x['course_code']) | |
# --------------- generate all potential timetables --------------- # | |
all_classes = [] | |
student_classes = [] | |
tut_1_1521_1_2521 = [] | |
tut_2_1521_1_2521 = [] | |
tut_2_1521_0_2521 = [] | |
for course in studenting: | |
for class_type in courses[course]['class_types']: | |
student_classes.append(courses[course]['class_types'][class_type]) | |
# ---------- one of each tute ----------- # | |
for course in tutoring: | |
tut_1_1521_1_2521.append(courses[course]['class_types']['TLB']) | |
# ---------- 2x 1521, 1x 2521 ----------- # | |
for course in tutoring: | |
tut_2_1521_1_2521.append(courses[course]['class_types']['TLB']) | |
if course == "COMP1521": | |
print "hi" | |
tut_2_1521_1_2521.append(courses[course]['class_types']['TLB']) | |
# ---------- 2x 1521, 0x 2521 ----------- # | |
for course in tutoring: | |
if course != "COMP1521": | |
continue | |
print "lo" | |
tut_2_1521_0_2521.append(courses[course]['class_types']['TLB']) | |
tut_2_1521_0_2521.append(courses[course]['class_types']['TLB']) | |
timetables = set() | |
# ---------- one of each tute ----------- # | |
calc_possible_timetables(student_classes, tut_1_1521_1_2521, timetables, | |
"forensics, 4336, 1x 1521, 1x 2521") | |
# ---------- 2x 1521, 1x 2521 ----------- # | |
calc_possible_timetables(student_classes, tut_2_1521_1_2521, timetables, | |
"forensics, 4336, 2x 1521, 1x 2521") | |
# ---------- 2x 1521, 0x 2521 ----------- # | |
calc_possible_timetables(student_classes, tut_2_1521_0_2521, timetables, | |
"forensics, 4336, 2x 1521, 0x 2521") | |
# ==== not 4336 === | |
for class_type in courses["COMP6845"]['class_types']: | |
student_classes.append(courses["COMP6845"]['class_types'][class_type]) | |
# ---------- one of each tute ----------- # | |
calc_possible_timetables(student_classes, tut_1_1521_1_2521, timetables, | |
"forensics, , 1x 1521, 1x 2521") | |
# ---------- 2x 1521, 1x 2521 ----------- # | |
calc_possible_timetables(student_classes, tut_2_1521_1_2521, timetables, | |
"forensics, , 2x 1521, 1x 2521") | |
# ---------- 2x 1521, 0x 2521 ----------- # | |
calc_possible_timetables(student_classes, tut_2_1521_0_2521, timetables, | |
"forensics, , 2x 1521, 0x 2521") | |
# ==== not 4336; thesis ==== | |
student_classes = [] | |
for class_type in courses["COMP6845"]['class_types']: | |
student_classes.append(courses["COMP6845"]['class_types'][class_type]) | |
add_thesis_classes(student_classes) | |
print student_classes | |
# ---------- one of each tute ----------- # | |
calc_possible_timetables(student_classes, tut_1_1521_1_2521, timetables, | |
"forensics, THS , 1x 1521, 1x 2521") | |
# ---------- 2x 1521, 0x 2521 ----------- # | |
calc_possible_timetables(student_classes, tut_2_1521_0_2521, timetables, | |
"forensics, THS , 2x 1521, 0x 2521") | |
""" | |
combs = itertools.product(*all_classes) | |
for comb in combs: | |
if valid_combination(comb): | |
if timetable not in timetables: | |
timetables.append(generate_timetable(comb)) | |
""" | |
print len(timetables) | |
# ----------------- sort by things we care about ----------------- # | |
for x in sorted(timetables, key=operator.attrgetter("tutor_days", | |
"days_off", | |
"_avg_start_time"), reverse=True): | |
x._print() | |
print "\n\n\n~~~~~~~~~~~~~~~~~~~~~~~~\n\n\n" | |
# print ones given the fixed class I've decided on | |
wed_forlab = [x for x in timetables if x._forensics_lab_day == 2 and x._forensics_lab_hour == 17 and x.teaching_days[4] == 0] | |
print len(wed_forlab) | |
for x in sorted(wed_forlab, key=operator.attrgetter("tutor_days", | |
"days_off", | |
"_avg_start_time"), reverse=True): | |
x._print() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment