diff --git a/test/scheduleGenerator/generator.py b/test/scheduleGenerator/generator.py index 039b786..2c2745c 100644 --- a/test/scheduleGenerator/generator.py +++ b/test/scheduleGenerator/generator.py @@ -2,13 +2,12 @@ import json import random from inspect import currentframe -import pprint +from string import hexdigits # Import from custom utilities from util.mockStudents import getSampleStudents from util.generateCourses import getSampleCourses - ''' Block 1-5 is first semester while block 6-10 is second semester @@ -38,7 +37,8 @@ from util.generateCourses import getSampleCourses } ''' -def getLineNumber(): return currentframe().f_back.f_lineno +exists = lambda n : True if n not in ('', None) else False +getLineNumber = lambda : currentframe().f_back.f_lineno # Takes in information to create or add a new conflict # Returns if the particular student has a previous error @@ -116,7 +116,7 @@ def generateScheduleV3( # Put # of classRunCount classes in emptyClasses for j in range(classRunCount): - emptyClasses[index][f"{index}-{j}"] = { + emptyClasses[index][f"{index}-{hexdigits[j]}"] = { "CrsNo": index, "Description": activeCourses[index]["Description"], "expectedLen": median # Number of students expected in this class / may be altered @@ -128,14 +128,14 @@ def generateScheduleV3( while remaining > 0: for j in range(classRunCount): if remaining == 0: break - emptyClasses[index][f"{index}-{j}"]["expectedLen"] += 1 + emptyClasses[index][f"{index}-{hexdigits[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: # Create a class using remaining - emptyClasses[index][f"{index}-{classRunCount}"] = { + emptyClasses[index][f"{index}-{hexdigits[classRunCount]}"] = { "CrsNo": index, "Description": activeCourses[index]["Description"], "expectedLen": remaining @@ -144,10 +144,10 @@ def generateScheduleV3( classRunCount += 1 if classRunCount >= 2: # Equalize (level) class expectedLen's - expectedLengths = [emptyClasses[index][f"{index}-{j}"]["expectedLen"] for j in range(classRunCount)] + 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}-{j}"]["expectedLen"] = newExpectedLens[j] + emptyClasses[index][f"{index}-{hexdigits[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, @@ -155,12 +155,12 @@ def generateScheduleV3( 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 + emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] -= 1 remaining += 1 if remaining == minReq: break # Create a class using remaining - emptyClasses[index][f"{index}-{classRunCount}"] = { + emptyClasses[index][f"{index}-{hexdigits[classRunCount]}"] = { "CrsNo": index, "Description": activeCourses[index]["Description"], "expectedLen": remaining @@ -169,10 +169,10 @@ def generateScheduleV3( classRunCount += 1 # Equalize (level) class expectedLen's - expectedLengths = [emptyClasses[index][f"{index}-{j}"]["expectedLen"] for j in range(classRunCount)] + 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}-{j}"]["expectedLen"] = newExpectedLens[j] + emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] = newExpectedLens[j] else: # In the case that the remaining requests are unable to be resolved @@ -180,8 +180,9 @@ def generateScheduleV3( # 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 + 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] = { @@ -190,7 +191,7 @@ def generateScheduleV3( } allClassRunCounts.append(classRunCount) - + # Step 3 Fill emptyClasses with Students selectedCourses = {} tempStudents = list(students) @@ -270,6 +271,9 @@ def generateScheduleV3( # 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(min(allClassRunCounts)) @@ -294,7 +298,7 @@ def generateScheduleV3( # Spread classes throughout both semesters for i in range(courseRunInfo[course]["Total"]): - cname = f"{course}-{i}" + cname = f"{course}-{hexdigits[i]}" classInserted = False while not classInserted: @@ -334,7 +338,7 @@ def generateScheduleV3( leastBlock = semBlocks.index(min(semBlocks)) cname = f"{course}-0" - running[f"block{leastBlock+offset}"][course] = { + running[f"block{leastBlock+offset}"][cname] = { "CrsNo": course, "Description": emptyClasses[course][cname]["Description"], "students": selectedCourses[cname]["students"], @@ -363,9 +367,8 @@ def generateScheduleV3( studentsCritical, studentsAcceptable = 0, 0 for student in students: - initialBlocks = [student["schedule"][block] for block in student["schedule"]] - conflicts = sum(1 for b in initialBlocks if len(b)>1) - hasConflicts = True if conflicts > 0 else False + 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 @@ -384,146 +387,93 @@ def generateScheduleV3( "index": student["studentIndex"] } - # If we are unable to solve, we add to exceptions - # and keep trying to resolve the rest of the schedule - exceptions = [] + if hasConflicts: + # Clear student schedule + for block in student["schedule"]: + [running[block][cname]["students"].remove(studentData) for cname in student["schedule"][block]] + student["schedule"][block] = [] - 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 ignore them when looking for clashes, and not to - # accidently overwrite while evaluating other classes - if len(exceptions) > 0: - for i in range(len(blocks)): - if i in exceptions: blocks[i] = ['EXPT'] - conflicts = sum(1 for b in blocks if len(b)>1) + # Find what class in student schedule has least run time + classes, runCounts = [], [] + for block in blocks: + for cname in block: + classes.append(cname[:-2]) + runCounts.append(courseRunInfoCopy[cname[:-2]]["Total"]) - if conflicts == 0: break - - elif conflicts > 0: + # 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 - blockLens = [len(block) for block in blocks] - freeBlocks = [index for index in range(len(blocks)) if len(blocks[index]) == 0] + # 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 - clashIndex = blockLens.index(max(blockLens)) - blockOut = f"block{clashIndex+1}" - classIndex = 0 - done, advancedConflict = False, False + 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) - while not done: - classOut = blocks[clashIndex][classIndex] - found = False - for index in freeBlocks: - if found: break - if index != clashIndex: - blockIn = list(running)[index] - for cname in running[blockIn]: - if cname[:-2] == classOut[:-2] and len(running[blockIn][cname]["students"]) < classCap: + # 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) - # Update records - student["schedule"][blockOut].remove(classOut) - student["schedule"][blockIn].append(cname) + # 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 - running[blockOut][classOut]["students"].remove(studentData) - running[blockIn][cname]["students"].append(studentData) + 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 not alternates, create critical error + c_cr_count += 1 + criticalCount += 1 + if not newConflict(student["Pupil #"], "", "Critical", "C-CR", "Couldn't Resolve", conflictLogs): studentsCritical += 1 - found = True - break - - if not found: - if classIndex < len(blocks[clashIndex])-1: classIndex += 1 - elif classIndex == len(blocks[clashIndex])-1: - classIndex = 0 - advancedConflict = True - done = True else: - print(f"Fatal error ({getLineNumber()}): Impossible error") - continue - - elif found: done = True + # Get alternate least run + altRunCounts = [courseRunInfoCopy[alt]["Total"] for alt in alternates] + altIndex = altRunCounts.index(min(altRunCounts)) - if advancedConflict: - attemptResolve = True - classOutIndex = 0 - while attemptResolve: - foundSolution = False - newClassOut = blocks[clashIndex][classOutIndex] - for blockIndex in range(len(running)): - if foundSolution: break - if blockIndex != clashIndex and blockIndex not in exceptions: - for cname in running[list(running)[blockIndex]]: - 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(newClassOut) - student["schedule"][blockIn].append(cname) + # add alternate + classes.append(alternates[altIndex]) + runCounts.append(altRunCounts[altIndex]) - running[blockOut][newClassOut]["students"].remove(studentData) - running[blockIn][cname]["students"].append(studentData) - foundSolution = True - break + # Remove alternate from remaining alternates + for remaining in students[student["studentIndex"]]["remainingAlts"]: + if remaining["CrsNo"] == alternates[altIndex]: + students[student["studentIndex"]]["remainingAlts"].remove(remaining) - elif len(blocks[blockIndex]) == 1: - 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) - criticalCount += 1 - c_cr_count += 1 - if not newConflict(student["Pupil #"], "", "Critial", "C-CR", "Couldn't Resolve", conflictLogs): studentsCritical += 1 - - 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) - criticalCount += 1 - c_cr_count += 1 - if not newConflict(student["Pupil #"], "", "Critical", "C-CR", "Couldn't Resolve", conflictLogs): studentsCritical += 1 - - attemptResolve = False - # Attempt to use alt - elif len(student["remainingAlts"]) == 0: - exceptions.append(clashIndex) - criticalCount += 1 - c_cr_count += 1 - if not newConflict(student["Pupil #"], "", "Critical", "C-CR", "Couldn't Resolve", conflictLogs): studentsCritical += 1 - - attemptResolve = False - else: print("Impossible - Thanos") - elif foundSolution: - attemptResolve = False - - else: - print(f"Fatal error ({getLineNumber()}): Impossible error") - continue + # 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 while not metSelfRequirements: @@ -581,7 +531,7 @@ def generateScheduleV3( for student in students: for block in student["schedule"]: if len(student["schedule"][block]) == 0: - student["schedule"][block].append(flex[0]) if int(block[len(block)-1]) <= 5 else student["schedule"][block].append(flex[1]) + 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: