Skip to content

Instantly share code, notes, and snippets.

@andrewjbennett
Created April 24, 2017 16:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save andrewjbennett/8b9ae439ffbc8b3648a17388c7ca461d to your computer and use it in GitHub Desktop.
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".
# 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)
print
print
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
print "----------------------------------------------------------"
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)
print
# ----------------- 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)
print
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