From 0bad75a3021cd9582bdcf9942b7c888d4177d269 Mon Sep 17 00:00:00 2001 From: Braeden Sowinski Date: Thu, 18 Aug 2022 00:02:49 -0700 Subject: [PATCH] bug fix/better comments/no_refresh option --- scheduleGenerator/generator.py | 74 ++++++++++++++++------------------ tinker.py | 60 ++++++++++++++++++++++----- util/generateCourses.py | 4 +- 3 files changed, 87 insertions(+), 51 deletions(-) diff --git a/scheduleGenerator/generator.py b/scheduleGenerator/generator.py index 2c2745c..c214279 100644 --- a/scheduleGenerator/generator.py +++ b/scheduleGenerator/generator.py @@ -56,7 +56,6 @@ def newConflict(pupilNum: str, email: str, conflictType: str, code: str, descrip return exists if conflictType == "Critical" else False minReq, median, classCap = 18, 24, 30 -activeCourses = {} running = { "block1": {}, "block2": {}, @@ -80,19 +79,21 @@ flex = ("XAT--12A-S", "XAT--12B-S") # 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, - courses: dict, - blockClassLimit: int=40, + 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]: + ) -> 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): @@ -107,32 +108,30 @@ def generateScheduleV3( allClassRunCounts = [] courseRunInfo = {} # Generated now, used in step 4 emptyClasses = {} # List of all classes with how many students should be entered during step 3 - # calculate # of times to run class + # 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 # of classRunCount classes in emptyClasses + # 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 + "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 - while remaining > 0: - for j in range(classRunCount): - if remaining == 0: break - emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] += 1 - remaining -= 1 + 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, but no other classes - # exists, create class, and do not equalize + # If we can create a class using remaining, create class elif remaining >= minReq: # Create a class using remaining emptyClasses[index][f"{index}-{hexdigits[classRunCount]}"] = { @@ -142,6 +141,7 @@ def generateScheduleV3( } 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)] @@ -149,9 +149,10 @@ def generateScheduleV3( for j in range(len(newExpectedLens)): 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, - # and requiered number to make a class is less than the max number we can provide from existing classes + # 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): @@ -159,7 +160,7 @@ def generateScheduleV3( remaining += 1 if remaining == minReq: break - # Create a class using remaining + # Create a class using remaining + required amount from existing classes emptyClasses[index][f"{index}-{hexdigits[classRunCount]}"] = { "CrsNo": index, "Description": activeCourses[index]["Description"], @@ -176,9 +177,9 @@ def generateScheduleV3( 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, + # 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 - # choices + # requests for j in range(classRunCount): if remaining == 0: break if emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] < classCap: @@ -192,11 +193,13 @@ def generateScheduleV3( allClassRunCounts.append(classRunCount) - # Step 3 Fill emptyClasses with Students + # 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"]] @@ -220,7 +223,7 @@ def generateScheduleV3( getAvailableCourse = False break elif len(selectedCourses[cname]["students"]) == emptyClasses[course][cname]["expectedLen"]: - # If class is full, and is last class of that course + # 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 @@ -276,7 +279,7 @@ def generateScheduleV3( while len(allClassRunCounts) > 0: # Get highest resource class (most times run) - index = allClassRunCounts.index(min(allClassRunCounts)) + index = allClassRunCounts.index(max(allClassRunCounts)) course = list(courseRunInfo)[index] # Tally first and second semester @@ -324,10 +327,6 @@ def generateScheduleV3( semBlocks = [] offset = 1 - if sem1 <= sem2: - for block in sem1List: - semBlocks.append(len(block)) - # If sem1 is less than or equal to sem2, add to sem1 if sem1 <= sem2: [semBlocks.append(len(block)) for block in sem1List] @@ -388,12 +387,12 @@ def generateScheduleV3( } if hasConflicts: - # Clear student schedule + # 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 what class in student schedule has least run time + # Find class in student schedule that's run the least classes, runCounts = [], [] for block in blocks: for cname in block: @@ -452,7 +451,7 @@ def generateScheduleV3( 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 + 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 @@ -476,27 +475,23 @@ def generateScheduleV3( runCounts.remove(runCounts[index]) metSelfRequirements = True if student["classes"] == student["expectedClasses"] else False - while not metSelfRequirements: + 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 - break elif student["classes"] < (student["expectedClasses"] - 2): # Difference between classes inserted to and - # expected classes is too great, attempt to fix + # 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 - break - else: - print(f"Fatal error ({getLineNumber()}): Impossible error") - break + print(f"Fatal error ({getLineNumber()}): Impossible error") # Just in case finalConflictLogs = { "Conflicts": conflictLogs, @@ -528,6 +523,7 @@ def generateScheduleV3( 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: diff --git a/tinker.py b/tinker.py index af0a969..fa87ca9 100644 --- a/tinker.py +++ b/tinker.py @@ -16,12 +16,12 @@ from scheduleGenerator.generator import generateScheduleV3 done = False -def processing(): +def processing(msg: str): for c in itertools.cycle(['|', '/', '-', '\\']): - if done: break - sys.stdout.write(f'\rProcessing {c}') - sys.stdout.flush() - sleep(0.1) + 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 @@ -46,17 +46,57 @@ def errorOutput(students) -> Tuple[PrettyTable, dict, dict]: if __name__ == '__main__': if len(sys.argv) == 1: - print() + print('\nGenerating...\n') st = time() # Start time - t = threading.Thread(target=processing) - t.start() # Start animation + t = threading.Thread(target=processing, args=('Collection student requests',)) + t.start() # Start animation sampleStudents = getSampleStudents("./sample_data/course_selection_data.csv", True) - samplemockCourses = getSampleCourses("./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, samplemockCourses, 40, "./output/students.json", "./output/conflicts.json") + timetable["timetable"] = generateScheduleV3(sampleStudents, sampleCourses, 40, "./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') + + errors, _, _ = errorOutput(sampleStudents) + print(errors) + + elif sys.argv[1].lower() == "norefresh" or sys.argv[1].lower() == "no_refresh": + print('\nGenerating...\n') + + st = time() # Start time + + 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, 40, "./output/students.json", "./output/conflicts.json") done = True # End Animation et = time() # End time diff --git a/util/generateCourses.py b/util/generateCourses.py index 8f19e48..276bd8c 100644 --- a/util/generateCourses.py +++ b/util/generateCourses.py @@ -23,7 +23,7 @@ def getSampleCourses(data_dir, log=False) -> dict: } if log: - with open("./output/realCourses.json", "w") as outfile: + with open("./output/courses.json", "w") as outfile: json.dump(realCourses, outfile, indent=2) return realCourses @@ -32,5 +32,5 @@ def getSampleCourses(data_dir, log=False) -> dict: if __name__ == '__main__': courseSet: dict = getSampleCourses("../sample_data/course_selection_data.csv") - with open("../output/realCourses.json", "w") as outfile: + with open("../output/courses.json", "w") as outfile: json.dump(courseSet, outfile, indent=2)