diff --git a/.gitignore b/.gitignore index 15b0b2b..b37e0d3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,13 +3,14 @@ # Pycache /test/__pycache__/* +/test/util/__pycache__/* +/test/scheduleGenerator/__pycache__/* + +# Test +/test/test.py # Test output -/test/schedule.json -/test/students.json -/test/classes.json -/test/realCourses.json +/test/output/* # Sample Data -/test/course_selection_data.csv -/test/course_Selection_data_table.txt +/test/sample_data/* diff --git a/test/scheduleGenerator/v1.py b/test/scheduleGenerator/v1.py new file mode 100644 index 0000000..c802791 --- /dev/null +++ b/test/scheduleGenerator/v1.py @@ -0,0 +1,197 @@ +#!/usr/bin/python3 +from util.courses import mockCourses, activeCourses # Fake courses +from util.mockStudents import generateMockStudents # Generate fake students +import json + + +''' + Block 1-4 is first semester while + block 5-8 is second semester +''' + +''' +schedule example: +schedule: { + "block1": "className", + "block2": "className", + "block3": "className", + "block4": "className" + "block5": "className", + "block6": "className", + "block7": "className", + "block8": "className" +} +''' + +''' +running example: +running: { + "block1": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}}, + "block2": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}}, + "block3": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}}, + "block4": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}}, + "block5": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}}, + "block6": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}}, + "block7": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}}, + "block8": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}} +} +''' + +# Error 1: No classes in schedule can fit this student +# Error 2: No more room in schedule for another class + +minReq, median, classCap, blockClassLimit = 18, 24, 30, 12 +activeCourses = {} +running = { + "block1": {}, + "block2": {}, + "block3": {}, + "block4": {}, + "block5": {}, + "block6": {}, + "block7": {}, + "block8": {} +} + +# Currently V1 has an average of 9.4% success rate, with an avverage of 90.6% error rate +def generateScheduleV1(mockStudents, mockCourses): + # Collect data and calculate schedules + for student in mockStudents: + # Tally class request + for request in student["requests"]: + mockCourses[request]["totalrequests"] += 1 + mockCourses[request]["studentindexes"].append(mockStudents.index(student)) + # Add course to active list if enough requests + if mockCourses[request]["totalrequests"] > minReq and mockCourses[request]["code"] not in activeCourses: activeCourses[mockCourses[request]["code"]] = mockCourses[request] + + for student in mockStudents: + alternateOffset = len(student["requests"])-8 + alternateIndex = 8 + for i in range(len(student["requests"])-alternateOffset): # Subtract x classes as they are alternatives + currentCourse = student["requests"][i] + generate = True + while generate: + # If class is allowed to run + if currentCourse in activeCourses: + blockIndex = 1 + courseNum = 1 + getFreeBlock = True + while getFreeBlock: + block = f"block{blockIndex}" + cname = f"{mockCourses[currentCourse]['name']}-{courseNum}" + if cname in running[block]: + if student["schedule"][block] == "": # Add student to class + if len(running[block][cname]["students"]) < classCap: + running[block][cname]["students"].append(student["name"]) + student["schedule"][block] = cname + getFreeBlock = False + else: # Find next available class or create new one + if blockIndex == 8: # No available classes + if len(student["requests"]) == 8: + # This student never had any alternatives + # How to solve? + + # print(f"\nError 1: No more available classes for student {student['name']}") + generate = False + else: + if alternateIndex <= (len(student["requests"]) - 1): + currentCourse = student["requests"][alternateIndex] + alternateIndex += 1 + else: # No more alterantive + # print(f"\nError 1: No more available classes for student {student['name']}") + generate = False + getFreeBlock = False + else: + if (mockCourses[currentCourse]["teachers"] - courseNum) > 0: + courseNum += 1 + else: + blockIndex += 1 + courseNum = 1 + else: + if blockIndex == 8: # No available classes + if len(student["requests"]) == 8: + # This student never had any alternatives + # How to solve? + + # print(f"\nError 1: No more available classes for student {student['name']}") + err1 += 1 + generate = False + else: + if alternateIndex <= (len(student["requests"]) - 1): + currentCourse = student["requests"][alternateIndex] + alternateIndex += 1 + else: # No more alternatives + # print(f"\nError 1: No more available classes for student {student['name']}") + generate = False + getFreeBlock = False + else: + blockIndex += 1 + courseNum = 1 + else: + if blockIndex == 8: + # Class does not exists + # Create new class in first available slot + blockNum = 1 + while True: + newBlock = f"block{blockNum}" + if cname not in running[newBlock] and len(running[newBlock]) < blockClassLimit: + if student["schedule"][newBlock] == "": # Add student to class + running[newBlock][cname] = { + "name": mockCourses[currentCourse]["name"], + "students": [student["name"]] + } + student["schedule"][newBlock] = cname + break + else: + if blockNum == 8: + # All student classes have been filled + break + else: + blockNum += 1 + courseNum = 1 + else: + if blockNum == 8: + # No room in school for more classes + # print(f"\nError 2: No more room in school for another class") + break + else: + if (mockCourses[currentCourse]["teachers"] - courseNum) > 0: + courseNum += 1 + else: + blockNum += 1 + courseNum = 1 + break + else: + blockIndex += 1 + courseNum = 1 + break + elif currentCourse not in activeCourses: + if len(student["requests"]) == 8: + # This student never had any alternatives + # How to solve? + + # print(f"\nError 1: No more available classes for student {student['name']}") + generate = False + else: + if alternateIndex <= (len(student["requests"]) - 1): + currentCourse = student["requests"][alternateIndex] + alternateIndex += 1 + else: # Out of alternatives + # print(f"\nError 1: No more available classes for student {student['name']}") + generate = False + + return running + + +if __name__ == '__main__': + print("Processing...") + + mockStudents = generateMockStudents(400) + timetable = {} + timetable["Version"] = 1 + timetable["timetable"] = generateScheduleV1(mockStudents, mockCourses) + + with open("../output/timetable.json", "w") as outfile: + json.dump(timetable, outfile, indent=2) + + print("Done") diff --git a/test/scheduleGenerator/v2.py b/test/scheduleGenerator/v2.py new file mode 100644 index 0000000..0af35b7 --- /dev/null +++ b/test/scheduleGenerator/v2.py @@ -0,0 +1,213 @@ +#!/usr/bin/python3 +from util.courses import mockCourses, activeCourses # Fake courses +from util.mockStudents import generateMockStudents # Generate fake students +import math +import json + + +''' + Block 1-4 is first semester while + block 5-8 is second semester +''' + +''' +schedule example: +schedule: { + "block1": "className", + "block2": "className", + "block3": "className", + "block4": "className" + "block5": "className", + "block6": "className", + "block7": "className", + "block8": "className" +} +''' + +''' +running example: +running: { + "block1": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}}, + "block2": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}}, + "block3": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}}, + "block4": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}}, + "block5": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}}, + "block6": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}}, + "block7": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}}, + "block8": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}} +} +''' + + +minReq, median, classCap, blockClassLimit = 18, 24, 30, 12 +mockStudents = [] +activeCourses = {} +running = { + "block1": {}, + "block2": {}, + "block3": {}, + "block4": {}, + "block5": {}, + "block6": {}, + "block7": {}, + "block8": {} +} + + +# Currently V2 has an average of 0.35% success rate, with an avverage of 99.65% error rate +def generateScheduleV2(mockStudents, mockCourses): + # Collect data and calculate schedules + for student in mockStudents: + # Tally class request + for request in student["requests"]: + mockCourses[request]["totalrequests"] += 1 + mockCourses[request]["studentindexes"].append(mockStudents.index(student)) + # Add course to active list if enough requests + if mockCourses[request]["totalrequests"] > minReq and mockCourses[request]["code"] not in activeCourses: activeCourses[mockCourses[request]["code"]] = mockCourses[request] + + # calculate # of times to run class + for i in range(len(activeCourses)): + index = list(activeCourses)[i] + classRunCount = math.floor(activeCourses[index]["totalrequests"] / classCap) + # If there is minReq+ requests left, 1 more class could be run + if (activeCourses[index]["totalrequests"] % classCap) > minReq: classRunCount += 1 + activeCourses[index]["classRunCount"] = classRunCount + + newBlockIndex = 1 + newCourseNum = 1 + while classRunCount > 0: + block = f"block{newBlockIndex}" + cname = f"{activeCourses[index]['name']}-{newCourseNum}" + if cname not in running[block] and len(running[block]) < blockClassLimit: + # Generate class and sub 1 from classRunCount + running[block][cname] = { + "name": activeCourses[index]["name"], + "students": [] + } + classRunCount -= 1 + else: + if newBlockIndex == 8: + if (activeCourses[index]["teachers"] - newCourseNum) > 0: + newCourseNum += 1 + newBlockIndex = 1 + else: + # No room in school for more classes + # print(f"\nError 2: No more room in school for another class") + classRunCount = 0 + else: newBlockIndex += 1 + + for student in mockStudents: + alternateOffset = len(student["requests"])-8 + alternateIndex = 8 + for i in range(len(student["requests"])-alternateOffset): # Subtract x classes as they are alternatives + currentCourse = student["requests"][i] + generate = True + courseNum = 1 + while generate: + # If class is allowed to run + if currentCourse in activeCourses: + blockIndex = 1 + getFreeBlock = True + cname = f"{mockCourses[currentCourse]['name']}-{courseNum}" + while getFreeBlock: + block = f"block{blockIndex}" + if cname in running[block]: + if student["schedule"][block] == "": # Add student to class + if len(running[block][cname]["students"]) < classCap: + running[block][cname]["students"].append(student["name"]) + student["schedule"][block] = cname + getFreeBlock = False + else: # Find next available class or create new one + if blockIndex == 8: # No available classes + if (activeCourses[index]["teachers"] - newCourseNum) > 0: + newCourseNum += 1 + newBlockIndex = 1 + else: + # No room in school for more classes + # print(f"\nError 2: No more room in school for another class") + classRunCount = 0 + if len(student["requests"]) == 8: + # This student never had any alternatives + # How to solve? + + # print(f"\nError 1: No more available classes for student {student['name']}") + generate = False + else: + if alternateIndex <= (len(student["requests"]) - 1): + currentCourse = student["requests"][alternateIndex] + alternateIndex += 1 + else: # No more alterantive + # print(f"\nError 1: No more available classes for student {student['name']}") + generate = False + getFreeBlock = False + else: blockIndex += 1 + else: + if blockIndex == 8: # No available classes + if len(student["requests"]) == 8: + # This student never had any alternatives + # How to solve? + + # print(f"\nError 1: No more available classes for student {student['name']}") + generate = False + else: + if alternateIndex <= (len(student["requests"]) - 1): + currentCourse = student["requests"][alternateIndex] + alternateIndex += 1 + else: # No more alternatives + # print(f"\nError 1: No more available classes for student {student['name']}") + generate = False + getFreeBlock = False + else: + if (mockCourses[currentCourse]["teachers"] - courseNum) > 0: + courseNum += 1 + else: + blockIndex += 1 + courseNum = 1 + else: + if blockIndex == 8: + # Class does not exists + # Resort to alternative + if alternateIndex <= (len(student["requests"]) - 1): + currentCourse = student["requests"][alternateIndex] + alternateIndex += 1 + else: # Out of alternatives + # print(f"\nError 1: No more available classes for student {student['name']}") + generate = False + break + else: + if (mockCourses[currentCourse]["teachers"] - courseNum) > 0: + courseNum += 1 + else: + blockIndex += 1 + courseNum = 1 + break + elif currentCourse not in activeCourses: + if len(student["requests"]) == 8: + # This student never had any alternatives + # How to solve? + + # print(f"\nError 1: No more available classes for student {student['name']}") + generate = False + else: + if alternateIndex <= (len(student["requests"]) - 1): + currentCourse = student["requests"][alternateIndex] + alternateIndex += 1 + else: # Out of alternatives + # print(f"\nError 1: No more available classes for student {student['name']}") + generate = False + + return running + + +if __name__ == '__main__': + print("Processing...") + + mockStudents = generateMockStudents(400) + timetable = {} + timetable["Version"] = 2 + timetable["timetable"] = generateScheduleV2(mockStudents, mockCourses) + + with open("../output/timetable.json", "w") as outfile: + json.dump(timetable, outfile, indent=2) + + print("Done") diff --git a/test/scheduleGenerator/v3.py b/test/scheduleGenerator/v3.py new file mode 100644 index 0000000..ec42981 --- /dev/null +++ b/test/scheduleGenerator/v3.py @@ -0,0 +1,267 @@ +#!/usr/bin/python3 +import json +import math +import random +from util.mockStudents import getSampleStudents +from util.generateCourses import getSampleCourses + + +''' + Block 1-5 is first semester while + block 6-10 is second semester + + + schedule example: + schedule: { + "block1": "className", + "block2": "className", + "block3": "className", + ... + } + + + running example: + running: { + "block1": { + classCode: { + "className": name, + "students": [student Name] + }, + classCode: { + "className": name, + "students": [student Name] + } + }, + ... + } +''' + +minReq, median, classCap, blockClassLimit = 18, 24, 30, 12 +mockStudents = [] +activeCourses = {} +running = { + "block1": {}, + "block2": {}, + "block3": {}, + "block4": {}, + "block5": {}, + "block6": {}, + "block7": {}, + "block8": {}, + "block9": {}, + "block10": {} +} + + +# V3 differs a lot by V1/2 as it does not focus on fitting the classes +# into the time table first. +# It starts by trying to get all classes full and give all students a full class list. +# Then it starts to attempt to fit all classes into a timetable, making corretions along +# the way. Corrections being moving a students class +def generateScheduleV3(students, courses): + def equal(l): # Used to equalize list of numbers + q,r = divmod(sum(l),len(l)) + return [q+1]*r + [q]*(len(l)-r) + + + # Step 1 - Calculate which classes can run + global err1, err2 + for student in students: + # Tally class request + for request in student["requests"]: + if not bool([i for i in ["XAT--12A-S", "XAT--12B-S"] if (i in request["CrsNo"])]): # Filters any requested study blocks (flex: no class block) + code = request["CrsNo"] + courses[code]["Requests"] += 1 + # Add course to active list if enough requests + if courses[code]["Requests"] > minReq and courses[code]["CrsNo"] not in activeCourses: + activeCourses[code] = courses[code] + + + # Step 2 - Generate empty classes + emptyClasses = {} # List of all classes with how many students should be entered during generation + # calculate # of times to run class + for i in range(len(activeCourses)): + index = list(activeCourses)[i] + if index not in emptyClasses: emptyClasses[index] = {} + classRunCount = math.floor(activeCourses[index]["Requests"] / median) + remaining = activeCourses[index]["Requests"] % median + + # Put # of classRunCount classes in emptyClasses + for j in range(classRunCount): + emptyClasses[index][f"{index}-{j}"] = { + "CrsNo": index, + "Description": activeCourses[index]["Description"], + "expectedLen": median # Number of students expected in this class / may be altered + } + + # If remaining fit in open slots in existing classes + if remaining <= classRunCount * (classCap - median): + # Equally disperse remaining into existing classes + while remaining > 0: + for j in range(classRunCount): + if remaining == 0: break + emptyClasses[index][f"{index}-{j}"]["expectedLen"] += 1 + remaining -= 1 + + # If we can create a class using remaining, but no other classes + # exists, create class, and do not equalize + elif remaining >= minReq and classRunCount == 0: + # Create a class using remaining + emptyClasses[index][f"{index}-{classRunCount}"] = { + "CrsNo": index, + "Description": activeCourses[index]["Description"], + "expectedLen": remaining + } + + classRunCount += 1 + + + # Else if the remaining can create a class + elif remaining >= minReq and classRunCount > 0: + # Create a class using remaining + emptyClasses[index][f"{index}-{classRunCount}"] = { + "CrsNo": index, + "Description": activeCourses[index]["Description"], + "expectedLen": remaining + } + + classRunCount += 1 + + # Equalize (level) class expectedLen's + expectedLengths = [emptyClasses[index][f"{index}-{j}"]["expectedLen"] for j in range(classRunCount)] + newExpectedLens = equal(expectedLengths) + for j in range(len(newExpectedLens)): + emptyClasses[index][f"{index}-{j}"]["expectedLen"] = newExpectedLens[j] + + # Else if we can't fit remaining in open slots in existing classes + # and it is unable to create its own class, + # and requiered number to make a class is less than the max number we can provide from existing classes + elif minReq - remaining < classRunCount * (median - minReq): + # Take 1 from each class till min requirment met + for j in range(classRunCount): + emptyClasses[index][f"{index}-{j}"]["expectedLen"] -= 1 + remaining += 1 + if remaining == minReq: break + + # Create a class using remaining + emptyClasses[index][f"{index}-{classRunCount}"] = { + "CrsNo": index, + "Description": activeCourses[index]["Description"], + "expectedLen": remaining + } + + classRunCount += 1 + + # Equalize (level) class expectedLen's + expectedLengths = [emptyClasses[index][f"{index}-{j}"]["expectedLen"] for j in range(classRunCount)] + newExpectedLens = equal(expectedLengths) + for j in range(len(newExpectedLens)): + emptyClasses[index][f"{index}-{j}"]["expectedLen"] = newExpectedLens[j] + + else: + # In the case that the remaining requests are unable to be resolved + # Fill as many requests into class as possible, any left that can't fit, + # Will need to be ignored so later we can fold them into their alternative + # choices + for j in range(classRunCount): + if emptyClasses[index][f"{index}-{j}"]["expectedLen"] < classCap and remaining > 0: + emptyClasses[index][f"{index}-{j}"]["expectedLen"] += 1 + remaining -= 1 + + + # Step 3 Fill emptyClasses with Students + selectedCourses = {} + tempStudents = students + + while len(tempStudents) > 0: + student = tempStudents[random.randint(0, len(students)-1)] + + alternates = [request for request in student["requests"] if request["alt"]] + altOffset = None + if len(alternates) > 0: altOffset = 0 + for request in (request for request in student["requests"] if not request["alt"] and request not in ["XAT--12A-S", "XAT--12B-S"]): + course = request["CrsNo"] + getAvailableCourse = True + while getAvailableCourse: + if course in emptyClasses: + # if course exists, get first available class + for cname in emptyClasses[course]: + if cname in selectedCourses: + if len(selectedCourses[cname]["students"]) < emptyClasses[course][cname]["expectedLen"]: + # Class exists with room for student + selectedCourses[cname]["students"].append(student["Pupil #"]) + getAvailableCourse = False + break + elif len(selectedCourses[cname]["students"]) == emptyClasses[course][cname]["expectedLen"]: + # If class is full, and is last class of that course + if cname[len(cname)-1] == f"{len(emptyClasses[course])-1}": + if altOffset is not None and altOffset <= len(alternates)-1: + # Use alternate + course = alternates[altOffset]["CrsNo"] + altOffset += 1 + break + else: + # Force break loop, ignore and let an admin + # handle options to solve for missing class + getAvailableCourse = False + break + + elif cname not in selectedCourses: + selectedCourses[cname] = { + "students": [student["Pupil #"]], + "CrsNo": course, + "Description": courses[course]["Description"] + } + getAvailableCourse = False + break + + elif course not in emptyClasses: + if altOffset is not None and altOffset <= len(alternates)-1: + # Use alternate + course = alternates[altOffset]["CrsNo"] + altOffset += 1 + else: + # Force break loop, ignore and let an admin + # handle options to solve for missing class + getAvailableCourse = False + + tempStudents.remove(student) + + # Step 4 - Attempt to fit classes into timetable + # TODO: Prioratize classes with maximum resouce limit; + # for example, classes that have lowest run counts, + # classes that have limited rooms dedicated for them. + # Fit these into the timetable first, then work from + # there on fitting other classes into the timetable. + + # This will require knowing how many classrooms there are, + # how many classrooms for a type of class, computer, wood, + # metal, art, drama, music. + # Languages, socials, math, science, and English all share + # the same classroom type and can be interchanged. + + # Teachers can be figured out at the end of this step, + # The school can hire more teachers in the area required, + # so we do not need to worry about how many tpye of classes + # are in the same block. + # For example: we don't need to worry if there is 4 english + # classes in the same block. even though the school may only + # have 2 english teachers. + + + # Step 5 - Evaluate, move classes or students to fix + return [] + +if __name__ == '__main__': + print("Processing...") + + sampleStudents = getSampleStudents(True) + samplemockCourses = getSampleCourses(True) + timetable = {} + timetable["Version"] = 3 + timetable["timetable"] = generateScheduleV3(sampleStudents, samplemockCourses) + + with open("../output/timetable.json", "w") as outfile: + json.dump(timetable, outfile, indent=2) + + print("Done") \ No newline at end of file diff --git a/test/test.js b/test/test.js deleted file mode 100644 index f672ec4..0000000 --- a/test/test.js +++ /dev/null @@ -1,21 +0,0 @@ -let schedule = require('./schedule.json'); -let students = require('./students.json'); - -// console.table(schedule) - -// Display first semester -console.table(schedule.block1) -console.table(schedule.block2) -console.table(schedule.block3) -console.table(schedule.block4) - -// Display second semester -console.table(schedule.block5) -console.table(schedule.block6) -console.table(schedule.block7) -console.table(schedule.block8) - -// Display each students schedule -// for (let i = 0; i < students.length; i++) { -// console.table(students[i].schedule) -// } diff --git a/test/tinker.py b/test/tinker.py index 4d530eb..df0c6ca 100644 --- a/test/tinker.py +++ b/test/tinker.py @@ -1,597 +1,55 @@ #!/usr/bin/python3 -import sys import json -import math -import random -from xml.etree.ElementTree import TreeBuilder -from courses import mockCourses, activeCourses -from mockStudents import generateMockStudents, getSampleStudents -from generateCourses import getSampleCourses +import sys +# Import required utilities +from util.mockStudents import generateMockStudents, getSampleStudents +from util.generateCourses import getSampleCourses +from util.courses import mockCourses -''' - Block 1-4 is first semester while - block 5-8 is second semester -''' - -''' -schedule example: -schedule: { - "block1": "className", - "block2": "className", - "block3": "className", - "block4": "className" - "block5": "className", - "block6": "className", - "block7": "className", - "block8": "className" -} -''' - -''' -running example: -running: { - "block1": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}}, - "block2": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}}, - "block3": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}}, - "block4": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}}, - "block5": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}}, - "block6": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}}, - "block7": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}}, - "block8": {classCode:{"className":name,"students":[student Name]},classCode:{"className":name,"students":[student Name]}} -} -''' - -# Error 1: No classes in schedule can fit this student -# Error 2: No more room in schedule for another class - -# 400 by default -studentsNum = 340 - -err1, err2, = 0,0 -minReq, median, classCap, blockClassLimit = 18, 24, 30, 12 -mockStudents = [] -running = { - "block1": {}, - "block2": {}, - "block3": {}, - "block4": {}, - "block5": {}, - "block6": {}, - "block7": {}, - "block8": {} -} - -# Currently V1 has an average of 9.4% success rate, with an avverage of 90.6% error rate -def generateScheduleV1(): - global err1, err2 - # Collect data and calculate schedules - for student in mockStudents: - # Tally class request - for request in student["requests"]: - mockCourses[request]["totalrequests"] += 1 - mockCourses[request]["studentindexes"].append(mockStudents.index(student)) - # Add course to active list if enough requests - if mockCourses[request]["totalrequests"] > minReq and mockCourses[request]["code"] not in activeCourses: activeCourses[mockCourses[request]["code"]] = mockCourses[request] - - for student in mockStudents: - alternateOffset = len(student["requests"])-8 - alternateIndex = 8 - for i in range(len(student["requests"])-alternateOffset): # Subtract x classes as they are alternatives - currentCourse = student["requests"][i] - generate = True - while generate: - # If class is allowed to run - if currentCourse in activeCourses: - blockIndex = 1 - courseNum = 1 - getFreeBlock = True - while getFreeBlock: - block = f"block{blockIndex}" - cname = f"{mockCourses[currentCourse]['name']}-{courseNum}" - if cname in running[block]: - if student["schedule"][block] == "": # Add student to class - if len(running[block][cname]["students"]) < classCap: - running[block][cname]["students"].append(student["name"]) - student["schedule"][block] = cname - getFreeBlock = False - else: # Find next available class or create new one - if blockIndex == 8: # No available classes - if len(student["requests"]) == 8: - # This student never had any alternatives - # How to solve? - - # print(f"\nError 1: No more available classes for student {student['name']}") - generate = False - else: - if alternateIndex <= (len(student["requests"]) - 1): - currentCourse = student["requests"][alternateIndex] - alternateIndex += 1 - else: # No more alterantive - # print(f"\nError 1: No more available classes for student {student['name']}") - err1 += 1 - generate = False - getFreeBlock = False - else: - if (mockCourses[currentCourse]["teachers"] - courseNum) > 0: - courseNum += 1 - else: - blockIndex += 1 - courseNum = 1 - else: - if blockIndex == 8: # No available classes - if len(student["requests"]) == 8: - # This student never had any alternatives - # How to solve? - - # print(f"\nError 1: No more available classes for student {student['name']}") - err1 += 1 - generate = False - else: - if alternateIndex <= (len(student["requests"]) - 1): - currentCourse = student["requests"][alternateIndex] - alternateIndex += 1 - else: # No more alternatives - # print(f"\nError 1: No more available classes for student {student['name']}") - err1 += 1 - generate = False - getFreeBlock = False - else: - blockIndex += 1 - courseNum = 1 - else: - if blockIndex == 8: - # Class does not exists - # Create new class in first available slot - blockNum = 1 - while True: - newBlock = f"block{blockNum}" - if cname not in running[newBlock] and len(running[newBlock]) < blockClassLimit: - if student["schedule"][newBlock] == "": # Add student to class - running[newBlock][cname] = { - "name": mockCourses[currentCourse]["name"], - "students": [student["name"]] - } - student["schedule"][newBlock] = cname - break - else: - if blockNum == 8: - # All student classes have been filled - break - else: - blockNum += 1 - courseNum = 1 - else: - if blockNum == 8: - # No room in school for more classes - # print(f"\nError 2: No more room in school for another class") - err2 += 1 - break - else: - if (mockCourses[currentCourse]["teachers"] - courseNum) > 0: - courseNum += 1 - else: - blockNum += 1 - courseNum = 1 - break - else: - blockIndex += 1 - courseNum = 1 - break - elif currentCourse not in activeCourses: - if len(student["requests"]) == 8: - # This student never had any alternatives - # How to solve? - - # print(f"\nError 1: No more available classes for student {student['name']}") - err1 += 1 - generate = False - else: - if alternateIndex <= (len(student["requests"]) - 1): - currentCourse = student["requests"][alternateIndex] - alternateIndex += 1 - else: # Out of alternatives - # print(f"\nError 1: No more available classes for student {student['name']}") - err1 += 1 - generate = False - - -# Currently V2 has an average of 0.35% success rate, with an avverage of 99.65% error rate -def generateScheduleV2(): - global err1, err2 - # Collect data and calculate schedules - for student in mockStudents: - # Tally class request - for request in student["requests"]: - mockCourses[request]["totalrequests"] += 1 - mockCourses[request]["studentindexes"].append(mockStudents.index(student)) - # Add course to active list if enough requests - if mockCourses[request]["totalrequests"] > minReq and mockCourses[request]["code"] not in activeCourses: activeCourses[mockCourses[request]["code"]] = mockCourses[request] - - # calculate # of times to run class - for i in range(len(activeCourses)): - index = list(activeCourses)[i] - classRunCount = math.floor(activeCourses[index]["totalrequests"] / classCap) - # If there is minReq+ requests left, 1 more class could be run - if (activeCourses[index]["totalrequests"] % classCap) > minReq: classRunCount += 1 - activeCourses[index]["classRunCount"] = classRunCount - - newBlockIndex = 1 - newCourseNum = 1 - while classRunCount > 0: - block = f"block{newBlockIndex}" - cname = f"{activeCourses[index]['name']}-{newCourseNum}" - if cname not in running[block] and len(running[block]) < blockClassLimit: - # Generate class and sub 1 from classRunCount - running[block][cname] = { - "name": activeCourses[index]["name"], - "students": [] - } - classRunCount -= 1 - else: - if newBlockIndex == 8: - if (activeCourses[index]["teachers"] - newCourseNum) > 0: - newCourseNum += 1 - newBlockIndex = 1 - else: - # No room in school for more classes - # print(f"\nError 2: No more room in school for another class") - err2 += 1 - classRunCount = 0 - else: newBlockIndex += 1 - - for student in mockStudents: - alternateOffset = len(student["requests"])-8 - alternateIndex = 8 - for i in range(len(student["requests"])-alternateOffset): # Subtract x classes as they are alternatives - currentCourse = student["requests"][i] - generate = True - courseNum = 1 - while generate: - # If class is allowed to run - if currentCourse in activeCourses: - blockIndex = 1 - getFreeBlock = True - cname = f"{mockCourses[currentCourse]['name']}-{courseNum}" - while getFreeBlock: - block = f"block{blockIndex}" - if cname in running[block]: - if student["schedule"][block] == "": # Add student to class - if len(running[block][cname]["students"]) < classCap: - running[block][cname]["students"].append(student["name"]) - student["schedule"][block] = cname - getFreeBlock = False - else: # Find next available class or create new one - if blockIndex == 8: # No available classes - if (activeCourses[index]["teachers"] - newCourseNum) > 0: - newCourseNum += 1 - newBlockIndex = 1 - else: - # No room in school for more classes - # print(f"\nError 2: No more room in school for another class") - err2 += 1 - classRunCount = 0 - if len(student["requests"]) == 8: - # This student never had any alternatives - # How to solve? - - # print(f"\nError 1: No more available classes for student {student['name']}") - err1 += 1 - generate = False - else: - if alternateIndex <= (len(student["requests"]) - 1): - currentCourse = student["requests"][alternateIndex] - alternateIndex += 1 - else: # No more alterantive - # print(f"\nError 1: No more available classes for student {student['name']}") - err1 += 1 - generate = False - getFreeBlock = False - else: blockIndex += 1 - else: - if blockIndex == 8: # No available classes - if len(student["requests"]) == 8: - # This student never had any alternatives - # How to solve? - - # print(f"\nError 1: No more available classes for student {student['name']}") - err1 += 1 - generate = False - else: - if alternateIndex <= (len(student["requests"]) - 1): - currentCourse = student["requests"][alternateIndex] - alternateIndex += 1 - else: # No more alternatives - # print(f"\nError 1: No more available classes for student {student['name']}") - err1 += 1 - generate = False - getFreeBlock = False - else: - if (mockCourses[currentCourse]["teachers"] - courseNum) > 0: - courseNum += 1 - else: - blockIndex += 1 - courseNum = 1 - else: - if blockIndex == 8: - # Class does not exists - # Resort to alternative - if alternateIndex <= (len(student["requests"]) - 1): - currentCourse = student["requests"][alternateIndex] - alternateIndex += 1 - else: # Out of alternatives - # print(f"\nError 1: No more available classes for student {student['name']}") - err1 += 1 - generate = False - break - else: - if (mockCourses[currentCourse]["teachers"] - courseNum) > 0: - courseNum += 1 - else: - blockIndex += 1 - courseNum = 1 - break - elif currentCourse not in activeCourses: - if len(student["requests"]) == 8: - # This student never had any alternatives - # How to solve? - - # print(f"\nError 1: No more available classes for student {student['name']}") - err1 += 1 - generate = False - else: - if alternateIndex <= (len(student["requests"]) - 1): - currentCourse = student["requests"][alternateIndex] - alternateIndex += 1 - else: # Out of alternatives - # print(f"\nError 1: No more available classes for student {student['name']}") - err1 += 1 - generate = False - - -# V3 differs a lot by V1/2 as it does not focus on fitting the classes -# into the time table first. -# It starts by trying to get all classes full and give all students a full class list. -# Then it starts to attempt to fit all classes into a timetable, making corretions along -# the way. Corrections being moving a students class -def generateScheduleV3(students, courses): - def equal(l): # Used to equalize list of numbers - q,r = divmod(sum(l),len(l)) - return [q+1]*r + [q]*(len(l)-r) - - # Step 1 - Calculate which classes can run - global err1, err2 - for student in students: - # Tally class request - for request in student["requests"]: - if not bool([i for i in ["XAT--12A-S", "XAT--12B-S"] if (i in request["CrsNo"])]): # Filters any requested study blocks (flex: no class block) - code = request["CrsNo"] - courses[code]["Requests"] += 1 - # Add course to active list if enough requests - if courses[code]["Requests"] > minReq and courses[code]["CrsNo"] not in activeCourses: - activeCourses[code] = courses[code] - - # Step 2 - Generate empty classes - emptyClasses = {} # List of all classes with how many students should be entered during generation - # calculate # of times to run class - for i in range(len(activeCourses)): - index = list(activeCourses)[i] - if index not in emptyClasses: emptyClasses[index] = {} - classRunCount = math.floor(activeCourses[index]["Requests"] / median) - remaining = activeCourses[index]["Requests"] % median - - # Put # of classRunCount classes in emptyClasses - for j in range(classRunCount): - emptyClasses[index][f"{index}-{j}"] = { - "CrsNo": index, - "Description": activeCourses[index]["Description"], - "expectedLen": median # Number of students expected in this class / may be altered - } - - # If remaining fit in open slots in existing classes - if remaining <= classRunCount * (classCap - median): - # Equally disperse remaining into existing classes - while remaining > 0: - for j in range(classRunCount): - if remaining == 0: break - emptyClasses[index][f"{index}-{j}"]["expectedLen"] += 1 - remaining -= 1 - - # If we can create a class using remaining, but no other classes - # exists, create class, and do not equalize - elif remaining >= minReq and classRunCount == 0: - # Create a class using remaining - emptyClasses[index][f"{index}-{classRunCount}"] = { - "CrsNo": index, - "Description": activeCourses[index]["Description"], - "expectedLen": remaining - } - - classRunCount += 1 - - - # Else if the remaining can create a class - elif remaining >= minReq and classRunCount > 0: - # Create a class using remaining - emptyClasses[index][f"{index}-{classRunCount}"] = { - "CrsNo": index, - "Description": activeCourses[index]["Description"], - "expectedLen": remaining - } - - classRunCount += 1 - - # Equalize (level) class expectedLen's - expectedLengths = [emptyClasses[index][f"{index}-{j}"]["expectedLen"] for j in range(classRunCount)] - newExpectedLens = equal(expectedLengths) - for j in range(len(newExpectedLens)): - emptyClasses[index][f"{index}-{j}"]["expectedLen"] = newExpectedLens[j] - - # Else if we can't fit remaining in open slots in existing classes - # and it is unable to create its own class, - # and requiered number to make a class is less than the max number we can provide from existing classes - elif minReq - remaining < classRunCount * (median - minReq): - # Take 1 from each class till min requirment met - for j in range(classRunCount): - emptyClasses[index][f"{index}-{j}"]["expectedLen"] -= 1 - remaining += 1 - if remaining == minReq: break - - # Create a class using remaining - emptyClasses[index][f"{index}-{classRunCount}"] = { - "CrsNo": index, - "Description": activeCourses[index]["Description"], - "expectedLen": remaining - } - - classRunCount += 1 - - # Equalize (level) class expectedLen's - expectedLengths = [emptyClasses[index][f"{index}-{j}"]["expectedLen"] for j in range(classRunCount)] - newExpectedLens = equal(expectedLengths) - for j in range(len(newExpectedLens)): - emptyClasses[index][f"{index}-{j}"]["expectedLen"] = newExpectedLens[j] - - else: - # In the case that the remaining requests are unable to be resolved - # Fill as many requests into class as possible, any left that can't fit, - # Will need to be ignored so later we can fold them into their alternative - # choices - for j in range(classRunCount): - if emptyClasses[index][f"{index}-{j}"]["expectedLen"] < classCap and remaining > 0: - emptyClasses[index][f"{index}-{j}"]["expectedLen"] += 1 - remaining -= 1 - - # Step 3 Fill emptyClasses with Students - selectedCourses = {} - tempStudents = students - - while len(tempStudents) > 0: - student = tempStudents[random.randint(0, len(students)-1)] - - alternates = [request for request in student["requests"] if request["alt"]] - altOffset = None - if len(alternates) > 0: altOffset = 0 - for request in (request for request in student["requests"] if not request["alt"] and request not in ["XAT--12A-S", "XAT--12B-S"]): - course = request["CrsNo"] - getAvailableCourse = True - while getAvailableCourse: - if course in emptyClasses: - # if course exists, get first available class - for cname in emptyClasses[course]: - if cname in selectedCourses: - if len(selectedCourses[cname]["students"]) < emptyClasses[course][cname]["expectedLen"]: - # Class exists with room for student - selectedCourses[cname]["students"].append(student["Pupil #"]) - getAvailableCourse = False - break - elif len(selectedCourses[cname]["students"]) == emptyClasses[course][cname]["expectedLen"]: - # If class is full, and is last class of that course - if cname[len(cname)-1] == f"{len(emptyClasses[course])-1}": - if altOffset is not None and altOffset <= len(alternates)-1: - # Use alternate - course = alternates[altOffset]["CrsNo"] - altOffset += 1 - break - else: - # Force break loop, ignore and let an admin - # handle options to solve for missing class - getAvailableCourse = False - break - - elif cname not in selectedCourses: - selectedCourses[cname] = { - "students": [student["Pupil #"]], - "CrsNo": course, - "Description": courses[course]["Description"] - } - getAvailableCourse = False - break - - elif course not in emptyClasses: - if altOffset is not None and altOffset <= len(alternates)-1: - # Use alternate - course = alternates[altOffset]["CrsNo"] - altOffset += 1 - else: - # Force break loop, ignore and let an admin - # handle options to solve for missing class - getAvailableCourse = False - - tempStudents.remove(student) - - - with open("classes.json", "w") as outfile: - json.dump(selectedCourses, outfile, indent=2) - - # Step 4 - Attempt to fit classes into timetable - - - # Step 5 - Evaluate, move classes or students to fix - return [] +# Import Algorithms +from scheduleGenerator.v1 import generateScheduleV1 +from scheduleGenerator.v2 import generateScheduleV2 +from scheduleGenerator.v3 import generateScheduleV3 if __name__ == '__main__': + if len(sys.argv) == 1: print("Missing argument") exit() + if sys.argv[1].upper() == 'V1': - if len(sys.argv) == 3: - try: - studentsNum = int(sys.argv[2]) - except: - print("Error parsing number of students") - exit() print("Processing...") - mockStudents = generateMockStudents(studentsNum) - generateScheduleV1() + + mockStudents = generateMockStudents(400) + timetable = {} + timetable["Version"] = 1 + timetable["timetable"] = generateScheduleV1(mockStudents, mockCourses) + elif sys.argv[1].upper() == 'V2': - if len(sys.argv) == 3: - try: - studentsNum = int(sys.argv[2]) - except: - print("Error parsing number of students") - exit() print("Processing...") - mockStudents = generateMockStudents(studentsNum) - generateScheduleV2() + + mockStudents = generateMockStudents(400) + timetable = {} + timetable["Version"] = 2 + timetable["timetable"] = generateScheduleV2(mockStudents, mockCourses) + + elif sys.argv[1].upper() == 'V3': + print("Processing...") - sampleStudents = getSampleStudents(True) - samplemockCourses = getSampleCourses(True) - coursesList = generateScheduleV3(sampleStudents, samplemockCourses) + + sampleStudents = getSampleStudents("./sample_data/course_selection_data.csv", True) + samplemockCourses = getSampleCourses("./sample_data/course_selection_data.csv", True) + timetable = {} + timetable["Version"] = 3 + timetable["timetable"] = generateScheduleV3(sampleStudents, samplemockCourses) + else: print("Invalid argument") exit() - # print("\n") - # print(f"Error 1: x{err1}") - # print(f"Error 2: x{err2}") - # print("\n") - - ### Displays the number of students in each class - # for block in running: - # print(f"\nBlock: {block}") - # print("=====================") - # for cl in running[block]: - # name = running[block][cl]["name"] - # students = len(running[block][cl]["students"]) - # print(f"Class: {name} | Students: {students}") - - # Count errors in students schedules - # errors = 0 - # for i in range(len(mockStudents)): - # count = 0 - # for course in mockStudents[i]["schedule"]: - # if mockStudents[i]["schedule"][course]=="": count+=1 - # if count > 0: errors += 1 - - # print(f"{errors}/{studentsNum} student(s) have a issue with their schedule") - - - # with open("schedule.json", "w") as outfile: - # json.dump(running, outfile, indent=2) - - # with open("students.json", "w") as outfile: - # json.dump(mockStudents, outfile, indent=2) + with open("./output/timetable.json", "w") as outfile: + json.dump(timetable, outfile, indent=2) print("Done") diff --git a/test/util/__init__.py b/test/util/__init__.py new file mode 100644 index 0000000..a93a4bf --- /dev/null +++ b/test/util/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/python3 diff --git a/test/courses.py b/test/util/courses.py similarity index 99% rename from test/courses.py rename to test/util/courses.py index c0e34c9..fbf8d0c 100644 --- a/test/courses.py +++ b/test/util/courses.py @@ -1,3 +1,5 @@ +# Fake sample data + mockCourses = { "P-CL": { "name": "Pre-Calculus", diff --git a/test/generateCourses.py b/test/util/generateCourses.py similarity index 71% rename from test/generateCourses.py rename to test/util/generateCourses.py index 9422f87..69f33cf 100644 --- a/test/generateCourses.py +++ b/test/util/generateCourses.py @@ -4,10 +4,9 @@ import csv realCourses = {} - # Get all courses from real sample data -def getSampleCourses(log=False): - with open("course_selection_data.csv", newline='') as csvfile: +def getSampleCourses(data_dir, log=False): + with open(data_dir, newline='') as csvfile: reader = csv.DictReader(csvfile) for row in reader: exists = False @@ -20,19 +19,18 @@ def getSampleCourses(log=False): "Requests": 0, "Description": row["Description"], "Credits": 4, - "Teachers": 3, "students": [] } if log: - with open("realCourses.json", "w") as outfile: + with open("./output/realCourses.json", "w") as outfile: json.dump(realCourses, outfile, indent=2) return realCourses if __name__ == '__main__': - courseSet = getSampleCourses() + courseSet = getSampleCourses("../sample_data/course_selection_data.csv") - with open("realCourses.json", "w") as outfile: + with open("../output/realCourses.json", "w") as outfile: json.dump(courseSet, outfile, indent=2) diff --git a/test/mockStudents.py b/test/util/mockStudents.py similarity index 72% rename from test/mockStudents.py rename to test/util/mockStudents.py index 7c6c5dc..121cc73 100644 --- a/test/mockStudents.py +++ b/test/util/mockStudents.py @@ -3,7 +3,8 @@ import random import names import json import csv -from courses import mockCourses +import sys +from util.courses import mockCourses mockStudents = [] @@ -35,8 +36,8 @@ def generateMockStudents(n): # sort real sample data into usable dictionary -def getSampleStudents(log=False): - with open("course_selection_data.csv", newline='') as csvfile: +def getSampleStudents(data_dir, log=False): + with open(data_dir, newline='') as csvfile: reader = csv.DictReader(csvfile) for row in reader: exists = False @@ -73,14 +74,28 @@ def getSampleStudents(log=False): mockStudents.append(newStudent) if log: - with open("students.json", "w") as outfile: + with open("./output/students.json", "w") as outfile: json.dump(mockStudents, outfile, indent=2) return mockStudents if __name__ == '__main__': - studentRequests = getSampleStudents() + if len(sys.argv) == 1: + print("Missing argument") + exit() + if sys.argv[1].lower() == 'sample': + studentRequests = getSampleStudents("../sample_data/course_selection_data.csv") - with open("students.json", "w") as outfile: - json.dump(studentRequests, outfile, indent=2) + with open("../output/students.json", "w") as outfile: + json.dump(studentRequests, outfile, indent=2) + elif sys.argv[1].lower() == 'mock': + studentRequests = generateMockStudents(400) + + with open("../output/students.json", "w") as outfile: + json.dump(studentRequests, outfile, indent=2) + else: + print("Invalid argument") + exit() + + print("done")