bug fix/better comments/no_refresh option
This commit is contained in:
3 files changed
+87
-51
No files matched your search
@@ -56,7 +56,6 @@ def newConflict(pupilNum: str, email: str, conflictType: str, code: str, descrip
|
|||||||
return exists if conflictType == "Critical" else False
|
return exists if conflictType == "Critical" else False
|
||||||
|
|
||||||
minReq, median, classCap = 18, 24, 30
|
minReq, median, classCap = 18, 24, 30
|
||||||
activeCourses = {}
|
|
||||||
running = {
|
running = {
|
||||||
"block1": {},
|
"block1": {},
|
||||||
"block2": {},
|
"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
|
# Then it starts to attempt to fit all classes into a timetable, making corretions along
|
||||||
# the way. Corrections being moving a students class
|
# the way. Corrections being moving a students class
|
||||||
def generateScheduleV3(
|
def generateScheduleV3(
|
||||||
students: list,
|
students: list, # Refer to ../util/mockStudents.py to see the students list structure
|
||||||
courses: dict,
|
courses: dict, # Reger to ../util/generateCourses.py to see the courses dictionary structure
|
||||||
blockClassLimit: int=40,
|
blockClassLimit: int=40, # Block class limit is the number of classrooms available per block. Default 40 classes per block
|
||||||
studentsDir: str="../output/students.json",
|
studentsDir: str="../output/students.json",
|
||||||
conflictsDir: str="../output/conflicts.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
|
def equal(l: list) -> list: # Used to equalize list of numbers
|
||||||
q,r = divmod(sum(l),len(l))
|
q,r = divmod(sum(l),len(l))
|
||||||
return [q+1]*r + [q]*(len(l)-r)
|
return [q+1]*r + [q]*(len(l)-r)
|
||||||
|
|
||||||
|
|
||||||
# Step 1 - Calculate which classes can run
|
# Step 1 - Calculate which classes can run
|
||||||
|
activeCourses = {}
|
||||||
for student in students:
|
for student in students:
|
||||||
# Tally class request
|
# Tally class request
|
||||||
for request in (request for request in student["requests"] if not request["alt"] and request["CrsNo"] not in flex):
|
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 = []
|
allClassRunCounts = []
|
||||||
courseRunInfo = {} # Generated now, used in step 4
|
courseRunInfo = {} # Generated now, used in step 4
|
||||||
emptyClasses = {} # List of all classes with how many students should be entered during step 3
|
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)):
|
for i in range(len(activeCourses)):
|
||||||
index = list(activeCourses)[i]
|
index = list(activeCourses)[i]
|
||||||
if index not in emptyClasses: emptyClasses[index] = {}
|
if index not in emptyClasses: emptyClasses[index] = {}
|
||||||
classRunCount = activeCourses[index]["Requests"] // median
|
classRunCount = activeCourses[index]["Requests"] // median
|
||||||
remaining = 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):
|
for j in range(classRunCount):
|
||||||
emptyClasses[index][f"{index}-{hexdigits[j]}"] = {
|
emptyClasses[index][f"{index}-{hexdigits[j]}"] = {
|
||||||
"CrsNo": index,
|
"CrsNo": index,
|
||||||
"Description": activeCourses[index]["Description"],
|
"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 fit in open slots in existing classes
|
||||||
if remaining <= classRunCount * (classCap - median):
|
if remaining <= classRunCount * (classCap - median):
|
||||||
# Equally disperse remaining into existing classes
|
# Equally disperse remaining into existing classes
|
||||||
while remaining > 0:
|
for j in range(classRunCount):
|
||||||
for j in range(classRunCount):
|
if remaining == 0: break
|
||||||
if remaining == 0: break
|
emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] += 1
|
||||||
emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] += 1
|
remaining -= 1
|
||||||
remaining -= 1
|
|
||||||
|
|
||||||
# If we can create a class using remaining, but no other classes
|
# If we can create a class using remaining, create class
|
||||||
# exists, create class, and do not equalize
|
|
||||||
elif remaining >= minReq:
|
elif remaining >= minReq:
|
||||||
# Create a class using remaining
|
# Create a class using remaining
|
||||||
emptyClasses[index][f"{index}-{hexdigits[classRunCount]}"] = {
|
emptyClasses[index][f"{index}-{hexdigits[classRunCount]}"] = {
|
||||||
@@ -142,6 +141,7 @@ def generateScheduleV3(
|
|||||||
}
|
}
|
||||||
|
|
||||||
classRunCount += 1
|
classRunCount += 1
|
||||||
|
# If a class previously existed
|
||||||
if classRunCount >= 2:
|
if classRunCount >= 2:
|
||||||
# Equalize (level) class expectedLen's
|
# Equalize (level) class expectedLen's
|
||||||
expectedLengths = [emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] for j in range(classRunCount)]
|
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)):
|
for j in range(len(newExpectedLens)):
|
||||||
emptyClasses[index][f"{index}-{hexdigits[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
|
# Else if we can't fit remaining into available slots in existing classes,
|
||||||
# and it is unable to create its own class,
|
# and it's 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
|
# 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):
|
elif minReq - remaining < classRunCount * (median - minReq):
|
||||||
# Take 1 from each class till min requirment met
|
# Take 1 from each class till min requirment met
|
||||||
for j in range(classRunCount):
|
for j in range(classRunCount):
|
||||||
@@ -159,7 +160,7 @@ def generateScheduleV3(
|
|||||||
remaining += 1
|
remaining += 1
|
||||||
if remaining == minReq: break
|
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]}"] = {
|
emptyClasses[index][f"{index}-{hexdigits[classRunCount]}"] = {
|
||||||
"CrsNo": index,
|
"CrsNo": index,
|
||||||
"Description": activeCourses[index]["Description"],
|
"Description": activeCourses[index]["Description"],
|
||||||
@@ -176,9 +177,9 @@ def generateScheduleV3(
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
# In the case that the remaining requests are unable to be resolved
|
# 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
|
# Will need to be ignored so later we can fold them into their alternative
|
||||||
# choices
|
# requests
|
||||||
for j in range(classRunCount):
|
for j in range(classRunCount):
|
||||||
if remaining == 0: break
|
if remaining == 0: break
|
||||||
if emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] < classCap:
|
if emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] < classCap:
|
||||||
@@ -192,11 +193,13 @@ def generateScheduleV3(
|
|||||||
allClassRunCounts.append(classRunCount)
|
allClassRunCounts.append(classRunCount)
|
||||||
|
|
||||||
|
|
||||||
# Step 3 Fill emptyClasses with Students
|
# Step 3 - Fill 'emptyClasses' with Students
|
||||||
selectedCourses = {}
|
selectedCourses = {}
|
||||||
tempStudents = list(students)
|
tempStudents = list(students)
|
||||||
|
|
||||||
while len(tempStudents) > 0:
|
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)]
|
student = tempStudents[random.randint(0, len(tempStudents)-1)]
|
||||||
|
|
||||||
alternates = [request for request in student["requests"] if request["alt"]]
|
alternates = [request for request in student["requests"] if request["alt"]]
|
||||||
@@ -220,7 +223,7 @@ def generateScheduleV3(
|
|||||||
getAvailableCourse = False
|
getAvailableCourse = False
|
||||||
break
|
break
|
||||||
elif len(selectedCourses[cname]["students"]) == emptyClasses[course][cname]["expectedLen"]:
|
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 cname[len(cname)-1] == f"{len(emptyClasses[course])-1}":
|
||||||
if len(alternates) > 0:
|
if len(alternates) > 0:
|
||||||
# Use alternate
|
# Use alternate
|
||||||
@@ -276,7 +279,7 @@ def generateScheduleV3(
|
|||||||
|
|
||||||
while len(allClassRunCounts) > 0:
|
while len(allClassRunCounts) > 0:
|
||||||
# Get highest resource class (most times run)
|
# Get highest resource class (most times run)
|
||||||
index = allClassRunCounts.index(min(allClassRunCounts))
|
index = allClassRunCounts.index(max(allClassRunCounts))
|
||||||
course = list(courseRunInfo)[index]
|
course = list(courseRunInfo)[index]
|
||||||
|
|
||||||
# Tally first and second semester
|
# Tally first and second semester
|
||||||
@@ -324,10 +327,6 @@ def generateScheduleV3(
|
|||||||
semBlocks = []
|
semBlocks = []
|
||||||
offset = 1
|
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 is less than or equal to sem2, add to sem1
|
||||||
if sem1 <= sem2: [semBlocks.append(len(block)) for block in sem1List]
|
if sem1 <= sem2: [semBlocks.append(len(block)) for block in sem1List]
|
||||||
|
|
||||||
@@ -388,12 +387,12 @@ def generateScheduleV3(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if hasConflicts:
|
if hasConflicts:
|
||||||
# Clear student schedule
|
# Clear student schedule to restructure
|
||||||
for block in student["schedule"]:
|
for block in student["schedule"]:
|
||||||
[running[block][cname]["students"].remove(studentData) for cname in student["schedule"][block]]
|
[running[block][cname]["students"].remove(studentData) for cname in student["schedule"][block]]
|
||||||
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 = [], []
|
classes, runCounts = [], []
|
||||||
for block in blocks:
|
for block in blocks:
|
||||||
for cname in block:
|
for cname in block:
|
||||||
@@ -452,7 +451,7 @@ def generateScheduleV3(
|
|||||||
if not solution:
|
if not solution:
|
||||||
# Try alternate
|
# Try alternate
|
||||||
alternates = [alt["CrsNo"] for alt in students[student["studentIndex"]]["remainingAlts"] if alt["CrsNo"] not in flex and alt["CrsNo"] in courseRunInfoCopy]
|
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
|
c_cr_count += 1
|
||||||
criticalCount += 1
|
criticalCount += 1
|
||||||
if not newConflict(student["Pupil #"], "", "Critical", "C-CR", "Couldn't Resolve", conflictLogs): studentsCritical += 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])
|
runCounts.remove(runCounts[index])
|
||||||
|
|
||||||
metSelfRequirements = True if student["classes"] == student["expectedClasses"] else False
|
metSelfRequirements = True if student["classes"] == student["expectedClasses"] else False
|
||||||
while not metSelfRequirements:
|
if not metSelfRequirements:
|
||||||
|
|
||||||
if (student["expectedClasses"] - 2) <= student["classes"] < student["expectedClasses"]:
|
if (student["expectedClasses"] - 2) <= student["classes"] < student["expectedClasses"]:
|
||||||
a_mc_count += 1
|
a_mc_count += 1
|
||||||
acceptableCount += 1
|
acceptableCount += 1
|
||||||
if not newConflict(student["Pupil #"], "", "Acceptable", "A-MC", "Missing 1-2 Classses", conflictLogs): studentsAcceptable += 1
|
if not newConflict(student["Pupil #"], "", "Acceptable", "A-MC", "Missing 1-2 Classses", conflictLogs): studentsAcceptable += 1
|
||||||
break
|
|
||||||
|
|
||||||
elif student["classes"] < (student["expectedClasses"] - 2):
|
elif student["classes"] < (student["expectedClasses"] - 2):
|
||||||
# Difference between classes inserted to and
|
# Difference between classes inserted to and
|
||||||
# expected classes is too great, attempt to fix
|
# expected classes is too great
|
||||||
if student["Pupil #"] in conflictLogs:
|
if student["Pupil #"] in conflictLogs:
|
||||||
c_mc_count += 1
|
c_mc_count += 1
|
||||||
criticalCount += 1
|
criticalCount += 1
|
||||||
if not newConflict(student["Pupil #"], "", "Critical", "C-MC", "Missing too many Classses", conflictLogs): studentsCritical += 1
|
if not newConflict(student["Pupil #"], "", "Critical", "C-MC", "Missing too many Classses", conflictLogs): studentsCritical += 1
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print(f"Fatal error ({getLineNumber()}): Impossible error")
|
print(f"Fatal error ({getLineNumber()}): Impossible error") # Just in case
|
||||||
break
|
|
||||||
|
|
||||||
finalConflictLogs = {
|
finalConflictLogs = {
|
||||||
"Conflicts": conflictLogs,
|
"Conflicts": conflictLogs,
|
||||||
@@ -528,6 +523,7 @@ def generateScheduleV3(
|
|||||||
with open(conflictsDir, "w") as outfile:
|
with open(conflictsDir, "w") as outfile:
|
||||||
json.dump(finalConflictLogs, outfile, indent=2)
|
json.dump(finalConflictLogs, outfile, indent=2)
|
||||||
|
|
||||||
|
# Insert flex (spare) course codes to empty blocks
|
||||||
for student in students:
|
for student in students:
|
||||||
for block in student["schedule"]:
|
for block in student["schedule"]:
|
||||||
if len(student["schedule"][block]) == 0:
|
if len(student["schedule"][block]) == 0:
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ from scheduleGenerator.generator import generateScheduleV3
|
|||||||
|
|
||||||
done = False
|
done = False
|
||||||
|
|
||||||
def processing():
|
def processing(msg: str):
|
||||||
for c in itertools.cycle(['|', '/', '-', '\\']):
|
for c in itertools.cycle(['|', '/', '-', '\\']):
|
||||||
if done: break
|
if done: break
|
||||||
sys.stdout.write(f'\rProcessing {c}')
|
sys.stdout.write(f'\r{msg} {c}')
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
sleep(0.1)
|
sleep(0.1)
|
||||||
|
|
||||||
def errorOutput(students) -> Tuple[PrettyTable, dict, dict]:
|
def errorOutput(students) -> Tuple[PrettyTable, dict, dict]:
|
||||||
# Error Table calulation / output
|
# Error Table calulation / output
|
||||||
@@ -46,17 +46,57 @@ def errorOutput(students) -> Tuple[PrettyTable, dict, dict]:
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
||||||
if len(sys.argv) == 1:
|
if len(sys.argv) == 1:
|
||||||
print()
|
print('\nGenerating...\n')
|
||||||
|
|
||||||
st = time() # Start time
|
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)
|
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 = {}
|
||||||
timetable["Version"] = 3
|
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
|
done = True # End Animation
|
||||||
et = time() # End time
|
et = time() # End time
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ def getSampleCourses(data_dir, log=False) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if log:
|
if log:
|
||||||
with open("./output/realCourses.json", "w") as outfile:
|
with open("./output/courses.json", "w") as outfile:
|
||||||
json.dump(realCourses, outfile, indent=2)
|
json.dump(realCourses, outfile, indent=2)
|
||||||
|
|
||||||
return realCourses
|
return realCourses
|
||||||
@@ -32,5 +32,5 @@ def getSampleCourses(data_dir, log=False) -> dict:
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
courseSet: dict = getSampleCourses("../sample_data/course_selection_data.csv")
|
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)
|
json.dump(courseSet, outfile, indent=2)
|
||||||
Reference in new issue
Block a user