diff --git a/test/scheduleGenerator/generator_v3.py b/test/scheduleGenerator/generator_v3.py index f011dcf..cea828e 100644 --- a/test/scheduleGenerator/generator_v3.py +++ b/test/scheduleGenerator/generator_v3.py @@ -4,6 +4,8 @@ import math import random from inspect import currentframe +from click import pass_context + # Import from custom utilities from util.mockStudents import getSampleStudents from util.generateCourses import getSampleCourses @@ -347,7 +349,7 @@ def generateScheduleV3( # Step 6 - Evaluate, move students to fix conflicts - conflictLogs = [] + conflictLogs = {} for student in students: initialBlocks = [student["schedule"][block] for block in student["schedule"]] @@ -378,11 +380,18 @@ def generateScheduleV3( while hasConflicts: # Check if conflicts have been resolved blocks = [student["schedule"][block] for block in student["schedule"]] + # If there is exceptions, make them look normal in blocks + # list to ginore them when looking for clashes, and not to + # accidently overwrite while evaluating other classes + if len(exceptions) > 0: + for i, block in enumerate(blocks): + if i in exceptions: blocks[i] = ['EXPT'] conflicts = sum(1 for b in blocks if len(b)>1) - + if conflicts == 0: hasConflicts = False elif conflicts > 0: + blockLens = [len(block) for block in blocks] freeBlocks = [index for index in range(len(blocks)) if len(blocks[index]) == 0] @@ -422,44 +431,92 @@ def generateScheduleV3( continue elif found: done = True + if advancedConflict: - classIndex = 0 - solved, done = False, False - classOut = blocks[clashIndex][classIndex] - while not done: + attemptResolve = True + classOutIndex = 0 + while attemptResolve: + foundSolution = False + newClassOut = blocks[clashIndex][classOutIndex] for blockIndex in range(len(running)): - if solved: break - if blockIndex != clashIndex: + if foundSolution: break + if blockIndex != clashIndex and blockIndex not in exceptions: for cname in running[list(running)[blockIndex]]: - if cname[:-2] == classOut[:-2]: + if cname[:-2] == newClassOut[:-2]: blockIn = f"block{blockIndex+1}" if blockIndex in freeBlocks: if len(running[blockIn][cname]["students"]) < classCap: # In the rare or impossible case a student was not inserted to a class # And it weren't full, insert them into the class - student["schedule"][blockOut].remove(classOut) + student["schedule"][blockOut].remove(newClassOut) student["schedule"][blockIn].append(cname) - running[blockOut][classOut]["students"].remove(studentData) + running[blockOut][newClassOut]["students"].remove(studentData) running[blockIn][cname]["students"].append(studentData) - - solved = True + foundSolution = True break - else: - # Resort to remaining alternates - if len(student["remainingAlts"]) > 0: - - pass - else: - # Add to exceptions try to continue - print("Create exception and continue") - pass elif len(blocks[blockIndex]) == 1: - pass - elif len(blocks[blockIndex]) > 1: - print("How to solve") - + oClassOut = blocks[blockIndex][0] + found_oBlockSolution = False + for oBlockIndex in range(len(running)): + if found_oBlockSolution: break + if oBlockIndex != clashIndex and oBlockIndex not in exceptions and oBlockIndex in freeBlocks: + for ocname in running[list(running)[oBlockIndex]]: + if ocname[:-2] == oClassOut[:-2]: + oBlockIn = f"block{oBlockIndex+1}" + if len(running[oBlockIn][ocname]["students"]) < classCap: + student["schedule"][blockOut].remove(newClassOut) + student["schedule"][blockIn].append(cname) + student["schedule"][blockIn].remove(oClassOut) + student["schedule"][oBlockIn].append(ocname) + + running[blockOut][newClassOut]["students"].remove(studentData) + running[blockIn][cname]["students"].append(studentData) + running[blockIn][oClassOut]["students"].remove(studentData) + running[oBlockIn][ocname]["students"].append(studentData) + + found_oBlockSolution = True + break + + + if not found_oBlockSolution: exceptions.append(clashIndex) + + foundSolution = True + break + + if not foundSolution: + if classOutIndex < len(blocks[clashIndex]) - 1: classOutIndex += 1 + elif classOutIndex == len(blocks[clashIndex]) - 1: + if len(student["remainingAlts"]) > 0: + exceptions.append(clashIndex) + print("Attempt to use alt") + attemptResolve = False + # Attempt to use alt + elif len(student["remainingAlts"]) == 0: + exceptions.append(clashIndex) + if student["Pupil #"] in conflictLogs: + conflictLogs[student["Pupil #"]].append({ + "Pupil #": student["Pupil #"], + "Email": "", + "Type": "Critical", + "Code": "C-CR", + "Conflict": "Couldn't Resolve" + }) + else: + conflictLogs[student["Pupil #"]] = [{ + "Pupil #": student["Pupil #"], + "Email": "", + "Type": "Critical", + "Code": "C-CR", + "Conflict": "Couldn't Resolve" + }] + + attemptResolve = False + else: print("Impossible - Thanos") + elif foundSolution: + attemptResolve = False + else: print(f"Fatal error ({getLineNumber()}): Impossible error") continue @@ -468,34 +525,60 @@ def generateScheduleV3( while not metSelfRequirements: if (student["expectedClasses"] - 2) <= student["classes"] < student["expectedClasses"]: - conflictLogs.append({ - "Pupil #": student["Pupil #"], - "Email": "", - "Type": "Acceptable", - "Code": "A-MC", - "Conflict": "Missing 1-2 classes" - }) + if student["Pupil #"] in conflictLogs: + conflictLogs[student["Pupil #"]].append({ + "Pupil #": student["Pupil #"], + "Email": "", + "Type": "Acceptable", + "Code": "A-MC", + "Conflict": "Missing 1-2 classes" + }) + else: + conflictLogs[student["Pupil #"]] = [{ + "Pupil #": student["Pupil #"], + "Email": "", + "Type": "Acceptable", + "Code": "A-MC", + "Conflict": "Missing 1-2 classes" + }] metSelfRequirements = True elif student["classes"] < (student["expectedClasses"] - 2): # Difference between classes inserted to and # expected classes is too great, attempt to fix - conflictLogs.append({ - "Pupil #": student["Pupil #"], - "Email": "", - "Type": "Critical", - "Code": "C-MC", - "Conflict": "Missing too many classes" - }) + if student["Pupil #"] in conflictLogs: + conflictLogs[student["Pupil #"]].append({ + "Pupil #": student["Pupil #"], + "Email": "", + "Type": "Critical", + "Code": "C-MC", + "Conflict": "Missing too many classes" + }) + else: + conflictLogs[student["Pupil #"]] = [{ + "Pupil #": student["Pupil #"], + "Email": "", + "Type": "Critical", + "Code": "C-MC", + "Conflict": "Missing too many classes" + }] metSelfRequirements = True else: print(f"Fatal error ({getLineNumber()}): Impossible error") continue + criticals = [] + acceptables = [] + for student in conflictLogs: + for conflict in conflictLogs[student]: + if conflict["Type"] == "Critical": criticals.append(conflict) + elif conflict["Type"] == "Acceptable": acceptables.append(conflict) + finalConflictLogs = { - "Critical": [conflict for conflict in conflictLogs if conflict["Type"] == "Critical"], - "Acceptable": [conflict for conflict in conflictLogs if conflict["Type"] == "Acceptable"] + "All": conflictLogs, + "Critical": criticals, + "Acceptable": acceptables } # Update Student records diff --git a/test/tinker.py b/test/tinker.py index fd0665f..4d3214d 100644 --- a/test/tinker.py +++ b/test/tinker.py @@ -20,15 +20,31 @@ def errorOutput(students) -> Tuple[PrettyTable, dict, dict]: conflicts = json.load(f) f.close() + critical, acceptable = 0, 0 + for student in conflicts["All"]: + studentHasCritical, studentHasAcceptable = False, False + read = False + for conflict in conflicts["All"][student]: + if read: break + if conflict["Type"] == "Critical": + critical += 1 + studentHasCritical = True + if studentHasAcceptable: read = True + elif conflict["Type"] == "Acceptable": + acceptable += 1 + studentHasAcceptable = True + if studentHasCritical: read = True + continue + t = PrettyTable(['Type', 'Error %', 'Success %', 'Error Ratio']) - errorsC = round(len(conflicts["Critical"]) / len(students) * 100, 2) + errorsC = round(critical / len(students) * 100, 2) successC = round(100 - errorsC, 2) - errorsA = round(len(conflicts["Acceptable"]) / len(students) * 100, 2) + errorsA = round(acceptable / len(students) * 100, 2) successA = round(100 - errorsA, 2) - t.add_row(['Critical', f"{errorsC} %", f"{successC} %", f"{len(conflicts['Critical'])}/{len(students)}"]) - t.add_row(['Acceptable', f"{errorsA} %", f"{successA} %", f"{len(conflicts['Acceptable'])}/{len(students)}"]) + t.add_row(['Critical', f"{errorsC} %", f"{successC} %", f"{critical}/{len(students)}"]) + t.add_row(['Acceptable', f"{errorsA} %", f"{successA} %", f"{acceptable}/{len(students)}"]) return t, conflicts["Critical"], conflicts["Acceptable"] @@ -69,8 +85,10 @@ if __name__ == '__main__': print(errors) elif sys.argv[1].upper() == "ERRORS": - sampleStudents = getSampleStudents("./sample_data/course_selection_data.csv", True) - errors, critical, acceptable = errorOutput(sampleStudents) + f = open('./output/students.json') + studentData = json.load(f) + f.close() + errors, critical, acceptable = errorOutput(studentData) print() print(errors) @@ -78,11 +96,11 @@ if __name__ == '__main__': c1, c2, co = 0, 0, 0 for error in critical: if error["Code"] == "C-MC": c1 +=1 - elif error["Code"] == "C-CSS": c2 += 1 + elif error["Code"] == "C-CR": c2 += 1 else: co += 1 - print(f"x{c1} C-MC Errors: Critical - Missing Classes") - print(f"x{c2} C-CSS Errors: Critical - Couldn't Solve Schedule") + print(f"x{c1} C-MC Errors: Critical - Missing too many Classes") + print(f"x{c2} C-CR Errors: Critical - Couldn't Resolve") print(f"x{co} Other/Undefined Critical Errors") print(f"\n{len(acceptable)} acceptable errors")