From 3d0b57793f8060ed7f812b1c308f4e46e300c2d5 Mon Sep 17 00:00:00 2001 From: Braeden Sowinski Date: Fri, 9 Sep 2022 14:03:34 -0700 Subject: [PATCH] debug step 4 perf. issues --- .gitignore | 10 + README.md | 46 + error.py | 20 + preview/error-preview.png | Bin 0 -> 32516 bytes preview/generator-preview.png | Bin 0 -> 23443 bytes sample_data/course_selection_data.csv | 13060 ++++++++++++++++++++++++ scheduleGenerator/generator.py | 550 + tinker.py | 167 + util/estimateGrade.py | 26 + util/generateCourses.py | 36 + util/mockStudents.py | 68 + 11 files changed, 13983 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 error.py create mode 100644 preview/error-preview.png create mode 100644 preview/generator-preview.png create mode 100644 sample_data/course_selection_data.csv create mode 100644 scheduleGenerator/generator.py create mode 100644 tinker.py create mode 100644 util/estimateGrade.py create mode 100644 util/generateCourses.py create mode 100644 util/mockStudents.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a824c6f --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Pycache +/__pycache__/* +/util/__pycache__/* +/scheduleGenerator/__pycache__/* + +# Test +/*_test.py + +# Test output +/output/* diff --git a/README.md b/README.md new file mode 100644 index 0000000..c3c7a73 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# Schedule Generator + +Hello there! This is the standalone repository for the schedule generator from my [school-management-api](https://github.com/SowinskiBraeden/school-management-api) project. + +The algorithm reads student requests and generates a timetable for all requested classes and fills them with students, giving the students their own schedules. + +The algorithm has been moved out of `tinker.py` into its own python script. Version 1 and 2 are removed and the script can be found in the `/scheduleGenerator` folder. The algorithm has come a long way; using real 2018 course selection data from my school, enabling me to better test the script. V3 has a entirely different approach from V1 and V2, that you can read about at the top of the function in [`generator.py`](/scheduleGenerator/generator.py). The function is broken up into 6 steps, each step is labeled within the function with a comment, giving a brief explination of what that step contributes to the algorithm. + +Here is the list of commands. +1. Collect latest data from files and generate timetable +``` + $ ./tinker.py +``` + +2. Collect Data and store it in files for later +``` + $ ./tinker.py generate_data +``` + +3. Generate timetable from existing files (faster), requires last command run once +``` + $ ./tinker.py no_refresh +``` + +4. More details on errors +``` + $ ./tinker.py errors +``` + +
+ +This is the output after running it with the following command. +``` + $ ./tinker.py no_refresh +``` + +![preview](/preview/generator-preview.png) + +
+ +A more in depth error log can be read with the additional argument +``` + $ ./tinker.py errors +``` + +![preview](/preview/error-preview.png) diff --git a/error.py b/error.py new file mode 100644 index 0000000..f86feef --- /dev/null +++ b/error.py @@ -0,0 +1,20 @@ +#! python +import json + +with open('./output/students.json') as f: students = json.load(f) + +couldnt_resolve = 0 +missing_classes = 0 +acceptable_missing_classes = 0 + +for student in students: + blocks = [student["schedule"][block] for block in student["schedule"]] + conflicts = sum(1 for b in blocks if len(b)>1) + if conflicts == 0 and student["classes"] == student["expectedClasses"]: continue + if conflicts > 0: couldnt_resolve += 1 + if (student["expectedClasses"] - 2) <= student["classes"] < student["expectedClasses"]: acceptable_missing_classes += 1 + if student["classes"] < (student["expectedClasses"] - 2): missing_classes += 1 + +print(f"Couldn't resolve : {couldnt_resolve}/{len(students)} - {round((couldnt_resolve/len(students))*100, 2)}%") +print(f"Missing classes : {missing_classes}/{len(students)} - {round((missing_classes/len(students))*100, 2)}%") +print(f"Acceptable Missing classes : {acceptable_missing_classes}/{len(students)} - {round((acceptable_missing_classes/len(students))*100, 2)}%") diff --git a/preview/error-preview.png b/preview/error-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd1a44be3bf2042a82e2cef512c01705c29fb59 GIT binary patch literal 32516 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 literal 0 HcmV?d00001 diff --git a/preview/generator-preview.png b/preview/generator-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..0d412a3e3e42c5a22090c0229659385c82e6f049 GIT binary patch literal 23443 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 bool: + exists = True if pupilNum in logs else False + log = { + "Pupil #": pupilNum, + "Email": email, + "Type": conflictType, + "Code": code, + "Conflict": description + } + if exists: logs[pupilNum].append(log) + else: logs[pupilNum] = [log] + return exists if conflictType == "Critical" else False + +minReq, median, classCap = 18, 24, 30 +running = { + "block1": {}, + "block2": {}, + "block3": {}, + "block4": {}, + "block5": {}, + "block6": {}, + "block7": {}, + "block8": {}, + "block9": {}, + "block10": {} +} + +# These are the codes for Flex (spare) blocks +# Semester 1 and 2 +flex = ("XAT--12A-S", "XAT--12B-S") + +# 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: list, # Refer to ../util/mockStudents.py to see the students list structure + courses: dict, # Reger to ../util/generateCourses.py to see the courses dictionary structure + blockClassLimit: int=40, # Block class limit is the number of classrooms available per block. Default 40 classes per block + studentsDir: str="../output/students.json", + conflictsDir: str="../output/conflicts.json" + ) -> dict[str, dict]: # Returns the completed 'running' dictionary from above + + + def equal(l: list) -> list: # 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 + activeCourses = {} + for student in students: + # Tally class request + for request in (request for request in student["requests"] if not request["alt"] and request["CrsNo"] not in flex): + 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 + allClassRunCounts = [] + courseRunInfo = {} # Generated now, used in step 4 + emptyClasses = {} # List of all classes with how many students should be entered during step 3 + # calculate number of times to run class + for i in range(len(activeCourses)): + index = list(activeCourses)[i] + if index not in emptyClasses: emptyClasses[index] = {} + classRunCount = activeCourses[index]["Requests"] // median + remaining = activeCourses[index]["Requests"] % median + + # Put number of classRunCount classes in emptyClasses + for j in range(classRunCount): + emptyClasses[index][f"{index}-{hexdigits[j]}"] = { + "CrsNo": index, + "Description": activeCourses[index]["Description"], + "expectedLen": median # Number of students expected in this class / may be altered later + } + + # If remaining fit in open slots in existing classes + if remaining <= classRunCount * (classCap - median): + # Equally disperse remaining into existing classes + for j in range(classRunCount): + if remaining == 0: break + emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] += 1 + remaining -= 1 + + # If we can create a class using remaining, create class + elif remaining >= minReq: + # Create a class using remaining + emptyClasses[index][f"{index}-{hexdigits[classRunCount]}"] = { + "CrsNo": index, + "Description": activeCourses[index]["Description"], + "expectedLen": remaining + } + + classRunCount += 1 + # If a class previously existed + if classRunCount >= 2: + # Equalize (level) class expectedLen's + expectedLengths = [emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] for j in range(classRunCount)] + newExpectedLens = equal(expectedLengths) + for j in range(len(newExpectedLens)): + emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] = newExpectedLens[j] + + # Else if we can't fit remaining into available slots in existing classes, + # and it's unable to create its own class, + # and the required amount (minReq - remaining) to make a class is less than + # the number that existing classes can provide (classRunCount * (median - minReq)) + elif minReq - remaining < classRunCount * (median - minReq): + # Take 1 from each class till min requirment met + for j in range(classRunCount): + emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] -= 1 + remaining += 1 + if remaining == minReq: break + + # Create a class using remaining + required amount from existing classes + emptyClasses[index][f"{index}-{hexdigits[classRunCount]}"] = { + "CrsNo": index, + "Description": activeCourses[index]["Description"], + "expectedLen": remaining + } + + classRunCount += 1 + + # Equalize (level) class expectedLen's + expectedLengths = [emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] for j in range(classRunCount)] + newExpectedLens = equal(expectedLengths) + for j in range(len(newExpectedLens)): + emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] = newExpectedLens[j] + + else: + # In the case that the remaining requests are unable to be resolved + # Fill as many requests into existing classes. Any left that can't fit, + # Will need to be ignored so later we can fold them into their alternative + # requests + for j in range(classRunCount): + if remaining == 0: break + if emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] < classCap: + emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] += 1 + remaining -= 1 + + courseRunInfo[index] = { + "Total": classRunCount, + "CrsNo": index + } + allClassRunCounts.append(classRunCount) + + + # Step 3 - Fill 'emptyClasses' with Students + selectedCourses = {} + tempStudents = list(students) + + while len(tempStudents) > 0: + # Choose random student to prevent any success + # bias to students at the top of the list + student = tempStudents[random.randint(0, len(tempStudents)-1)] + + alternates = [request for request in student["requests"] if request["alt"]] + for request in (request for request in student["requests"] if not request["alt"] and request["CrsNo"] not in flex): + course = request["CrsNo"] + getAvailableCourse = True + isAlt = False + while getAvailableCourse: + if course in emptyClasses: + # if course exists, get first available class + for cname in emptyClasses[course]: + if cname in selectedCourses: + if isAlt and emptyClasses[course][cname]["expectedLen"] < classCap: + emptyClasses[course][cname]["expectedLen"] += 1 + if len(selectedCourses[cname]["students"]) < emptyClasses[course][cname]["expectedLen"]: + # Class exists with room for student + selectedCourses[cname]["students"].append({ + "Pupil #": student["Pupil #"], + "index": student["studentIndex"] + }) + getAvailableCourse = False + break + elif len(selectedCourses[cname]["students"]) == emptyClasses[course][cname]["expectedLen"]: + # If class is full, and there's no more classes available for that course + if cname[len(cname)-1] == f"{len(emptyClasses[course])-1}": + if len(alternates) > 0: + # Use alternate + course = alternates[0]["CrsNo"] + alternates.remove(alternates[0]) + isAlt = True + 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": [{ + "Pupil #": student["Pupil #"], + "index": student["studentIndex"] + }], + "CrsNo": course, + "Description": courses[course]["Description"] + } + getAvailableCourse = False + break + + elif course not in emptyClasses: + if len(alternates) > 0: + # Use alternate + course = alternates[0]["CrsNo"] + alternates.remove(alternates[0]) + isAlt = True + else: + # Force break loop, ignore and let an admin + # handle options to solve for missing class + getAvailableCourse = False + + students[student["studentIndex"]]["remainingAlts"] = alternates + tempStudents.remove(student) + + + # Step 4 - Attempt to fit classes into timetable + def stepIndex(offset: int, stepType: int) -> int: + # stepType 0 is for stepping between first and second semester + if stepType == 0: return 5 if offset == 0 or offset == -4 else -4 + + # stepType 1 is for stepping between second and first semester + elif stepType == 1: return -5 if offset == 0 or offset == 6 else 6 + + # Return Error if code is altered to cause error + else: raise SystemExit(f"Invalid 'stepType' in func 'stepIndex' line {getLineNumber()}") + + # Create copy for step 6 + courseRunInfoCopy = dict(courseRunInfo) + + while len(allClassRunCounts) > 0: + # Get highest resource class (most times run) + index = allClassRunCounts.index(max(allClassRunCounts)) + course = list(courseRunInfo)[index] + + # Tally first and second semester + sem1, sem2 = 0, 0 + sem1List, sem2List = {}, {} + for i in range(1, 11): + if i <= 5: + sem1 += len(running[f"block{i}"]) + sem1List[f"block{i}"] = running[f"block{i}"] + elif i > 5: + sem2 += len(running[f"block{i}"]) + sem2List[f"block{i}"] = running[f"block{i}"] + + # If there is more than one class Running + if allClassRunCounts[index] > 1: + blockIndex = 0 if sem1 <= sem2 else 5 + stepType = 0 if sem1 <= sem2 else 1 + offset = 0 + + # Spread classes throughout both semesters + for i in range(courseRunInfo[course]["Total"]): + cname = f"{course}-{hexdigits[i]}" + classInserted = False + while not classInserted: + + blockIndex += offset + if len(running[list(running)[blockIndex]]) < blockClassLimit: + running[list(running)[blockIndex]][cname] = { + "CrsNo": course, + "Description": emptyClasses[course][cname]["Description"], + "students": selectedCourses[cname]["students"] + } + allClassRunCounts[index] -= 1 + classInserted = True + + offset = stepIndex(offset, stepType) + + if blockIndex >= 9: + blockIndex = 0 if sem1 <= sem2 else 5 + offset = 0 + + # If the class only runs once, place in semester with least classes + elif allClassRunCounts[index] == 1: + # Equally disperse into semesters classes + semBlocks = [] + offset = 1 + + # If sem1 is less than or equal to sem2, add to sem1 + if sem1 <= sem2: [semBlocks.append(len(block)) for block in sem1List] + + # If sem2 is less than sem1, add to sem2 + elif sem1 > sem2: [semBlocks.append(len(block)) for block in sem2List] + + # Get block with least classes + leastBlock = semBlocks.index(min(semBlocks)) + cname = f"{course}-0" + + running[f"block{leastBlock+offset}"][cname] = { + "CrsNo": course, + "Description": emptyClasses[course][cname]["Description"], + "students": selectedCourses[cname]["students"], + } + + allClassRunCounts[index] -= 1 + + # Remove course when fully inserted + if allClassRunCounts[index] == 0: + allClassRunCounts.remove(allClassRunCounts[index]) + courseRunInfo.pop(list(courseRunInfo)[index]) + + + # Step 5 - Fill student schedule + for block in running: + for cname in running[block]: + for student in running[block][cname]["students"]: + students[student["index"]]["schedule"][block].append(cname) + students[student["index"]]["classes"] += 1 + + + # Step 6 - Evaluate, move students to fix conflicts + conflictLogs = {} + criticalCount, acceptableCount = 0, 0 + c_mc_count, c_cr_count, a_mc_count = 0, 0, 0 + studentsCritical, studentsAcceptable = 0, 0 + + for student in students: + blocks = [student["schedule"][block] for block in student["schedule"]] + hasConflicts = True if sum(1 for b in blocks if len(b)>1) > 0 else False + + # If there is no conflicts + # and classes inserted to is equal to expectedClasses + # or classes the student is inserted to is missing + # no more than two classes: + # continue to next student + if not hasConflicts and student["classes"] == student["expectedClasses"]: continue + elif not hasConflicts and (student["expectedClasses"]-2) <= student["classes"] < student["expectedClasses"]: + a_mc_count += 1 + acceptableCount += 1 + if not newConflict(student["Pupil #"], "", "Acceptable", "A-MC", "Missing 1-2 Classses", conflictLogs): studentsAcceptable += 1 + continue + + studentData = { + "Pupil #": student["Pupil #"], + "index": student["studentIndex"] + } + + if hasConflicts: + # Clear student schedule to restructure + for block in student["schedule"]: + [running[block][cname]["students"].remove(studentData) for cname in student["schedule"][block]] + student["schedule"][block] = [] + + # Find class in student schedule that's run the least + classes, runCounts = [], [] + for block in blocks: + for cname in block: + classes.append(cname[:-2]) + runCounts.append(courseRunInfoCopy[cname[:-2]]["Total"]) + + # Rebuild student schedule + availableBlocks = [f'block{i}' for i in range(1, 11)] + while len(classes) > 0: + index = runCounts.index(min(runCounts)) # Get class least run + found = False + + # find slot for class + for block in availableBlocks: + if found: break + for cname in running[block]: + if cname[:-2] == classes[index] and len(running[block][cname]["students"]) < classCap: + running[block][cname]["students"].append(studentData) + student["schedule"][block].append(cname) + availableBlocks.remove(block) + found = True + break + + if not found: + # Determine all places class exists + existsIn, existingClassNames = [], [] + for block in running: + for cname in running[block]: + if cname[:-2] == classes[index] and len(running[block][cname]["students"]) < classCap: + existsIn.append(block) + existingClassNames.append(cname) + + # Attempt to fix + solution = False + if len(existsIn) > 0: + for i, existing in enumerate(existsIn): + if solution: break + classOut = student["schedule"][existing][0] + for block in running: + if solution: break + if block == existing or block not in availableBlocks: continue + for cname in running[block]: + if cname[:-2] == classOut[:-2] and len(running[block][cname]["students"]) < classCap: + # Move to existing class elsewhere + student["schedule"][block].append(cname) + running[block][cname]["students"].append(studentData) + + # Overwrite old class + running[existing][student["schedule"][existing][0]]["students"].remove(studentData) + student["schedule"][existing][0] = existingClassNames[i] + running[existing][existingClassNames[i]]["students"].append(studentData) + + solution = True + break + + if not solution: + # Try alternate + alternates = [alt["CrsNo"] for alt in students[student["studentIndex"]]["remainingAlts"] if alt["CrsNo"] not in flex and alt["CrsNo"] in courseRunInfoCopy] + if len(alternates) == 0: # If no alternates, create critical error + c_cr_count += 1 + criticalCount += 1 + if not newConflict(student["Pupil #"], "", "Critical", "C-CR", "Couldn't Resolve", conflictLogs): studentsCritical += 1 + + else: + # Get alternate least run + altRunCounts = [courseRunInfoCopy[alt]["Total"] for alt in alternates] + altIndex = altRunCounts.index(min(altRunCounts)) + + # add alternate + classes.append(alternates[altIndex]) + runCounts.append(altRunCounts[altIndex]) + + # Remove alternate from remaining alternates + for remaining in students[student["studentIndex"]]["remainingAlts"]: + if remaining["CrsNo"] == alternates[altIndex]: + students[student["studentIndex"]]["remainingAlts"].remove(remaining) + + # Remove class after inserted or failed to insert + classes.remove(classes[index]) + runCounts.remove(runCounts[index]) + + metSelfRequirements = True if student["classes"] == student["expectedClasses"] else False + if not metSelfRequirements: + + if (student["expectedClasses"] - 2) <= student["classes"] < student["expectedClasses"]: + a_mc_count += 1 + acceptableCount += 1 + if not newConflict(student["Pupil #"], "", "Acceptable", "A-MC", "Missing 1-2 Classses", conflictLogs): studentsAcceptable += 1 + + elif student["classes"] < (student["expectedClasses"] - 2): + # Difference between classes inserted to and + # expected classes is too great + if student["Pupil #"] in conflictLogs: + c_mc_count += 1 + criticalCount += 1 + if not newConflict(student["Pupil #"], "", "Critical", "C-MC", "Missing too many Classses", conflictLogs): studentsCritical += 1 + + else: + print(f"Fatal error ({getLineNumber()}): Impossible error") # Just in case + + finalConflictLogs = { + "Conflicts": conflictLogs, + "Critical": { + "Total": criticalCount, + "Students": studentsCritical, + "Errors": [{ + "Total": c_mc_count, + "Description": "Missing too many Classes", + "Code": "C-MC" + }, { + "Total": c_cr_count, + "Description": "Couldn't Resolve", + "Code": "C-CR" + }] + }, + "Acceptable": { + "Total": acceptableCount, + "Students": studentsAcceptable, + "Errors": [{ + "Total": a_mc_count, + "Description": "Missing 1-2 Classes", + "Code": "A-MC" + }] + } + } + + # Log Conflict to records + with open(conflictsDir, "w") as outfile: + json.dump(finalConflictLogs, outfile, indent=2) + + # Insert flex (spare) course codes to empty blocks + for student in students: + for block in student["schedule"]: + if len(student["schedule"][block]) == 0: + student["schedule"][block].append(flex[0]) if int(block[5:]) <= 5 else student["schedule"][block].append(flex[1]) + + # Update Student records + with open(studentsDir, "w") as outfile: + json.dump(students, outfile, indent=2) + + return running + +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/tinker.py b/tinker.py new file mode 100644 index 0000000..1db6765 --- /dev/null +++ b/tinker.py @@ -0,0 +1,167 @@ +#! python +from os.path import exists as file_exists +from prettytable import PrettyTable +from typing import Tuple +from time import time, sleep +import itertools +import threading +import json +import sys + +# Import required utilities +from util.mockStudents import getSampleStudents +from util.generateCourses import getSampleCourses + +# Import Algorithm +from scheduleGenerator.generator import generateScheduleV3 + +done = False + +# consts +blockClassLimit = 68 + +print(f'\n>> Debug: Current blockClassLimit = {blockClassLimit}') + +def processing(msg: str): + for c in itertools.cycle(['|', '/', '-', '\\']): + if done: break + sys.stdout.write(f'\r{msg} {c}') + sys.stdout.flush() + sleep(0.1) + +def errorOutput(students): #-> Tuple[PrettyTable, dict, dict]: + # Error Table calulation / output + f = open('./output/conflicts.json') + conflicts = json.load(f) + f.close() + totalCritical = conflicts["Critical"]["Students"] + totalAcceptable = conflicts["Acceptable"]["Students"] + + t = PrettyTable(['Type', 'Error %', 'Success %', 'Student Error Ratio']) + + errorsC = round(totalCritical / len(students) * 100, 2) + successC = round(100 - errorsC, 2) + errorsA = round(totalAcceptable / len(students) * 100, 2) + successA = round(100 - errorsA, 2) + + t.add_row(['Critical', f"{errorsC} %", f"{successC} %", f"{totalCritical}/{len(students)} Students"]) + t.add_row(['Acceptable', f"{errorsA} %", f"{successA} %", f"{totalAcceptable}/{len(students)} Students"]) + + return t, conflicts["Critical"], conflicts["Acceptable"] + +if __name__ == '__main__': + + if len(sys.argv) == 1: + print() + + st = time() # Start time + + t = threading.Thread(target=processing, args=('Collection student requests',)) + t.start() # Start animation + sampleStudents = getSampleStudents("./sample_data/course_selection_data.csv", True) + done = True # End Animation + print('\nStudent list generated.\n') + + t = threading.Thread(target=processing, args=('Collection Course Information',)) + done = False # reset animation + t.start() # Start animation + sampleCourses = getSampleCourses("./sample_data/course_selection_data.csv", True) + done = True # End Animation + print('\nCourse Information Collected.\n') + + sleep(0.1) + t = threading.Thread(target=processing, args=('Processing',)) + done = False # reset animation + t.start() # Start animation + timetable = {} + timetable["Version"] = 3 + timetable["timetable"] = generateScheduleV3(sampleStudents, sampleCourses, blockClassLimit, "./output/students.json", "./output/conflicts.json") + + done = True # End Animation + et = time() # End time + elapsed_time = round((et - st), 3) # Execution time + print(f'\n\nDone - Finished in {elapsed_time} seconds\n') + + for e in sys.argv: + if e.lower() == 'showerror': + errors, _, _ = errorOutput(sampleStudents) + print(errors) + break + + with open("./output/timetable.json", "w") as outfile: + json.dump(timetable, outfile, indent=2) + + elif sys.argv[1].lower() == "no_refresh": + print() + + st = time() # Start time + + if not file_exists('./output/students.json') or not file_exists('./output/courses.json'): + print('\n No previous data exists, please use the following command\n./tinker.py generate_data') + exit() + + with open('./output/students.json') as f: sampleStudents = json.load(f) + with open('./output/courses.json') as f: sampleCourses = json.load(f) + + for student in sampleStudents: + student["remainingAlts"] = [] + for i in range(1, 11): student["schedule"][f"block{i}"] = [] + student["classes"] = 0 + + t = threading.Thread(target=processing, args=('Processing',)) + t.start() # Start animation + timetable = {} + timetable["Version"] = 3 + timetable["timetable"] = generateScheduleV3(sampleStudents, sampleCourses, blockClassLimit, "./output/students.json", "./output/conflicts.json") + + done = True # End Animation + et = time() # End time + elapsed_time = round((et - st), 3) # Execution time + print(f'\n\nDone - Finished in {elapsed_time} seconds\n') + + for e in sys.argv: + if e.lower() == 'showerror': + errors, _, _ = errorOutput(sampleStudents) + print(errors) + break + + with open("./output/timetable.json", "w") as outfile: + json.dump(timetable, outfile, indent=2) + + elif sys.argv[1].lower() == "generate_data": + t = threading.Thread(target=processing, args=('Collection student requests',)) + t.start() # Start animation + _ = getSampleStudents("./sample_data/course_selection_data.csv", True) + done = True # End Animation + print('\nStudent list generated.\n') + + t = threading.Thread(target=processing, args=('Collection Course Information',)) + done = False # reset animation + t.start() # Start animation + _ = getSampleCourses("./sample_data/course_selection_data.csv", True) + done = True # End Animation + print('\nCourse Information Collected.\n') + print('You can now use the following command\n./tinker.py no_refresh') + + elif sys.argv[1].upper() == "ERRORS": + f = open('./output/students.json') + studentData = json.load(f) + f.close() + errors, critical, acceptable = errorOutput(studentData) + print() + print(errors) + + print(f"\n{critical['Total']} critical errors") + for i in range(len(critical["Errors"])): + print(f"x{critical['Errors'][i]['Total']} {critical['Errors'][i]['Code']} Errors: Critical - {critical['Errors'][i]['Description']}") + + print(f"\n{acceptable['Total']} acceptable errors") + for i in range(len(acceptable["Errors"])): + print(f"x{acceptable['Errors'][i]['Total']} {acceptable['Errors'][i]['Code']} Errors: Critical - {acceptable['Errors'][i]['Description']}") + + exit() + + else: + print("Invalid argument") + exit() + diff --git a/util/estimateGrade.py b/util/estimateGrade.py new file mode 100644 index 0000000..cf1213f --- /dev/null +++ b/util/estimateGrade.py @@ -0,0 +1,26 @@ +#! python +import json + +# Get students +try: + with open("../output/students.json") as studentFile: students = json.load(studentFile) +except FileNotFoundError: + with open("./output/students.json") as studentFile: students = json.load(studentFile) + +flex = ("XAT--12A-S", "XAT--12B-S") + +most_frequent = lambda l : max(set(l), key = l.count) + +def getEstimatedGrade(pupil: dict) -> int: + grades = [] # List of all possible grades + for request in pupil["requests"]: + if request["CrsNo"] in flex: continue + for extractedGrade in [int(s) for s in request["CrsNo"].split("-") if s.isdigit()]: + grades.append(extractedGrade) + + return None if len(grades) == 0 else most_frequent(grades) # Final estimate of grade + +if __name__ == '__main__': + # Define students grade level + for student in students: + student["gradelevel"] = getEstimatedGrade(student) diff --git a/util/generateCourses.py b/util/generateCourses.py new file mode 100644 index 0000000..276bd8c --- /dev/null +++ b/util/generateCourses.py @@ -0,0 +1,36 @@ +#! python +import json +import csv + +realCourses = {} + +# Get all courses from real sample data +def getSampleCourses(data_dir, log=False) -> dict: + with open(data_dir, newline='') as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + exists = False + for course in realCourses: + exists = True if realCourses[course]["CrsNo"] == row["CrsNo"] else False + if exists: break + if not exists: + realCourses[row["CrsNo"]] = { + "CrsNo": row["CrsNo"], + "Requests": 0, + "Description": row["Description"], + "Credits": 4, + "students": [] + } + + if log: + with open("./output/courses.json", "w") as outfile: + json.dump(realCourses, outfile, indent=2) + + return realCourses + + +if __name__ == '__main__': + courseSet: dict = getSampleCourses("../sample_data/course_selection_data.csv") + + with open("../output/courses.json", "w") as outfile: + json.dump(courseSet, outfile, indent=2) diff --git a/util/mockStudents.py b/util/mockStudents.py new file mode 100644 index 0000000..034c9cf --- /dev/null +++ b/util/mockStudents.py @@ -0,0 +1,68 @@ +#! python +import json +import csv + +flex= ("XAT--12A-S", "XAT--12B-S") + +mockStudents: list[dict] = [] + +# sort real sample data into usable dictionary +def getSampleStudents(data_dir: str, log: bool = False) -> list[dict]: + with open(data_dir, newline='') as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + exists = False + for student in mockStudents: + exists = True if student["Pupil #"] == row["Pupil #"] else False + if exists: break + alternate = True if row["Alternate?"] == 'TRUE' else False + if exists: + if len(mockStudents[student["studentIndex"]]["requests"]) >= 10 and not alternate and row["CrsNo"] not in flex: alternate = True + mockStudents[student["studentIndex"]]["requests"].append({ + "CrsNo": row["CrsNo"], + "Description": row["Description"], + "alt": alternate + }) + if row["CrsNo"] not in flex and not alternate and mockStudents[student["studentIndex"]]["expectedClasses"] < 10: + mockStudents[student["studentIndex"]]["expectedClasses"] += 1 + else: + newStudent = { + "Pupil #": row["Pupil #"], + "requests": [{ + "CrsNo": row["CrsNo"], + "Description": row["Description"], + "alt": alternate + }], + "schedule": { + "block1": [], + "block2": [], + "block3": [], + "block4": [], + "block5": [], + "block6": [], + "block7": [], + "block8": [], + "block9": [], + "block10": [] + }, + "expectedClasses": 1, + "classes": 0, + "remainingAlts": [], + "studentIndex": len(mockStudents) + } + mockStudents.append(newStudent) + + if log: + with open("./output/students.json", "w") as outfile: + json.dump(mockStudents, outfile, indent=2) + + return mockStudents + + +if __name__ == '__main__': + studentRequests: list[dict] = getSampleStudents("../sample_data/course_selection_data.csv") + + with open("../output/students.json", "w") as outfile: + json.dump(studentRequests, outfile, indent=2) + + print("done")