allow more variable changes

This commit is contained in:
SowinskiBraeden committed 2022-11-05 22:58:14 -07:00
1 parent 8ec7c1befb
commit b38c15034b
3 files changed
+107 -68

No files matched your search

+47 -39
View File
@@ -3,6 +3,7 @@ import json
import random import random
from inspect import currentframe from inspect import currentframe
from string import hexdigits from string import hexdigits
from dataclasses import dataclass
# Import from custom utilities # Import from custom utilities
from util.mockStudents import getSampleStudents from util.mockStudents import getSampleStudents
@@ -44,6 +45,15 @@ from util.debug import debug
exists = lambda n : True if n not in ('', None) else False exists = lambda n : True if n not in ('', None) else False
getLineNumber = lambda : currentframe().f_back.f_lineno getLineNumber = lambda : currentframe().f_back.f_lineno
@dataclass
class Error:
Title: str
Description: str
def __init__(self, title: str, description: str):
self.Title = title
self.Description = description
# Takes in information to create or add a new conflict # Takes in information to create or add a new conflict
# Returns if the particular student has a previous error # Returns if the particular student has a previous error
def newConflict(pupilNum: str, email: str, conflictType: str, code: str, description: str, logs: dict) -> bool: def newConflict(pupilNum: str, email: str, conflictType: str, code: str, description: str, logs: dict) -> bool:
@@ -66,20 +76,6 @@ def insertConflictSolutions(pupilNum: str, logs: dict, data: dict) -> None:
conflict["Missing"] = data conflict["Missing"] = data
break break
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 # These are the codes for Flex (spare) blocks
# Semester 1 and 2 # Semester 1 and 2
flex = ("XAT--12A-S", "XAT--12B-S") flex = ("XAT--12A-S", "XAT--12B-S")
@@ -90,12 +86,27 @@ 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, # Refer to ../util/mockStudents.py to see the students list structure 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 courses: dict, # Reger to /util/generateCourses.py to see the courses dictionary structure
minReq: int=18, # minimum requests for a class to run
classCap: int=30, # maximum students per class
blockClassLimit: int=40, # Block class limit is the number of classrooms available per block. Default 40 classes per block blockClassLimit: int=40, # Block class limit is the number of classrooms available per block. Default 40 classes per block
totalBlocks: int=10, # total blocks between two semesters -> default is 10 for 5 per semester... or this can be 8 for 4 blocks per semester
studentsDir: str="../output/students.json", studentsDir: str="../output/students.json",
conflictsDir: str="../output/conflicts.json" conflictsDir: str="../output/conflicts.json"
) -> dict[str, dict]: # Returns the completed 'running' dictionary from above ) -> tuple[dict, Error]: # Returns the completed 'running' dictionary from above
# Return error that totalBlocks is invalid
if totalBlocks not in (10, 8):
totalBlockError = Error('Invalid totalBlocks', 'An invalid \'totalBlocks\' value was provided -> must be 10 or 8')
return (None, totalBlockError) # return none an signal failure
# First we need to setup some values
median = (minReq + classCap) // 2
blockPerSem = int(totalBlocks / 2)
running = {}
for i in range(1, totalBlocks + 1):
running[f'block{i}'] = {}
def equal(l: list) -> list: # Used to equalize list of numbers def equal(l: list) -> list: # Used to equalize list of numbers
@@ -276,14 +287,19 @@ def generateScheduleV3(
# Step 4 - Attempt to fit classes into timetable # Step 4 - Attempt to fit classes into timetable
def stepIndex(offset: int, stepType: int) -> int: def stepIndex(offset: int, stepType: int) -> int:
# ALl this logic handles taking the # of blocks per semester, 4 or 5 and calculating
# the number used to step between the index of the first or second semester
# stepType 0 is for stepping between first and second semester # stepType 0 is for stepping between first and second semester
if stepType == 0: return 5 if offset in (0, -4) else -4 if stepType == 0: return blockPerSem if offset in (0, (-1 * (blockPerSem - 1))) else (-1 * (blockPerSem - 1))
# stepType 1 is for stepping between second and first semester # stepType 1 is for stepping between second and first semester
elif stepType == 1: return -5 if offset in (0, 6) else 6 elif stepType == 1: return (-1 * blockPerSem) if offset in (0, (blockPerSem + 1)) else (blockPerSem + 1)
# Return Error if code is altered to cause error # Return Error if code is altered to cause error
else: raise SystemExit(f"Invalid 'stepType' in func 'stepIndex' line {getLineNumber()}") else:
invalidStepTypeError = Error('Invalid stepType', 'An invalid \'stepType\' was passed to func \'stepIndex\'')
return (None, invalidStepTypeError)
# Create copy for step 6 # Create copy for step 6
courseRunInfoCopy = dict(courseRunInfo) courseRunInfoCopy = dict(courseRunInfo)
@@ -294,15 +310,12 @@ def generateScheduleV3(
course = list(courseRunInfo)[index] course = list(courseRunInfo)[index]
# Tally first and second semester # Tally first and second semester
sem1 = sum(len(running[f'block{i}']) for i in range(1, 6)) allSemBlockLens = [len(running[f'block{i}']) for i in range(1, totalBlocks + 1)]
sem2 = sum(len(running[f'block{i}']) for i in range(6, 11))
allSemBlockLens = [len(running[f'block{i}']) for i in range(1, 11)]
# If there is more than one class Running # If there is more than one class Running
if allClassRunCounts[index] > 1: if allClassRunCounts[index] > 1:
blockIndex = allSemBlockLens.index(min(allSemBlockLens)) blockIndex = allSemBlockLens.index(min(allSemBlockLens))
startSem = 1 if blockIndex < 5 else 2 stepType = 0 if blockIndex < blockPerSem else 1
stepType = 0 if blockIndex < 5 else 1
offset = 0 offset = 0
# Spread classes throughout both semesters # Spread classes throughout both semesters
@@ -310,7 +323,6 @@ def generateScheduleV3(
cname = f"{course}-{hexdigits[i]}" cname = f"{course}-{hexdigits[i]}"
classInserted = False classInserted = False
while not classInserted: while not classInserted:
blockIndex += offset blockIndex += offset
if len(running[f'block{blockIndex+1}']) < blockClassLimit: if len(running[f'block{blockIndex+1}']) < blockClassLimit:
running[list(running)[blockIndex]][cname] = { running[list(running)[blockIndex]][cname] = {
@@ -323,8 +335,8 @@ def generateScheduleV3(
offset = stepIndex(offset, stepType) offset = stepIndex(offset, stepType)
if blockIndex >= 9: if blockIndex >= (totalBlocks - 1):
blockIndex = 0 if stepType == 0 else 5 blockIndex = 0 if stepType == 0 else blockPerSem
offset = 0 offset = 0
# If the class only runs once, place in semester with least classes # If the class only runs once, place in semester with least classes
@@ -348,10 +360,6 @@ def generateScheduleV3(
allClassRunCounts.remove(allClassRunCounts[index]) allClassRunCounts.remove(allClassRunCounts[index])
courseRunInfo.pop(list(courseRunInfo)[index]) courseRunInfo.pop(list(courseRunInfo)[index])
# # Debug step 4 output
# for i in range(1, 6):
# debug(f'block{i} - {len(running[f"block{i}"])} | block{i+5} - {len(running[f"block{i+5}"])}')
# Step 5 - Fill student schedule # Step 5 - Fill student schedule
for block in running: for block in running:
for cname in running[block]: for cname in running[block]:
@@ -367,11 +375,8 @@ def generateScheduleV3(
studentsCritical, studentsAcceptable = 0, 0 studentsCritical, studentsAcceptable = 0, 0
for student in students: for student in students:
# if student["Pupil #"] == "772554": debug("772554")
blocks = [student["schedule"][block] for block in student["schedule"]] blocks = [student["schedule"][block] for block in student["schedule"]]
# if student["Pupil #"] == "772554": debug(blocks)
hasConflicts = True if sum(1 for b in blocks if len(b)>1) > 0 else False hasConflicts = True if sum(1 for b in blocks if len(b)>1) > 0 else False
# if student["Pupil #"] == "772554": debug(f'Has conflict? {hasConflicts}')
# If there is no conflicts # If there is no conflicts
# and classes inserted to is equal to expectedClasses # and classes inserted to is equal to expectedClasses
@@ -407,7 +412,7 @@ def generateScheduleV3(
runCounts.append(courseRunInfoCopy[cname[:-2]]["Total"]) runCounts.append(courseRunInfoCopy[cname[:-2]]["Total"])
# Rebuild student schedule # Rebuild student schedule
availableBlocks = [f'block{i}' for i in range(1, 11)] availableBlocks = [f'block{i}' for i in range(1, totalBlocks + 1)]
while len(classes) > 0: while len(classes) > 0:
index = runCounts.index(min(runCounts)) # Get class least run index = runCounts.index(min(runCounts)) # Get class least run
found = False found = False
@@ -583,11 +588,12 @@ def generateScheduleV3(
if len(student["schedule"][block]) == 0: if len(student["schedule"][block]) == 0:
student["schedule"][block].append(flex[0]) if int(block[5:]) <= 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 # Update/log Student records
with open(studentsDir, "w") as outfile: with open(studentsDir, "w") as outfile:
json.dump(students, outfile, indent=2) json.dump(students, outfile, indent=2)
return running return (running, None)
def main(): def main():
print("Processing...") print("Processing...")
@@ -596,7 +602,9 @@ def main():
samplemockCourses = getSampleCourses(True) samplemockCourses = getSampleCourses(True)
timetable = {} timetable = {}
timetable["Version"] = 3 timetable["Version"] = 3
timetable["timetable"] = generateScheduleV3(sampleStudents, samplemockCourses) timetable["timetable"], err = generateScheduleV3(sampleStudents, samplemockCourses)
if err is not None: raise SystemExit(f'{err.Title} : {err.Description}')
with open("../output/timetable.json", "w") as outfile: with open("../output/timetable.json", "w") as outfile:
json.dump(timetable, outfile, indent=2) json.dump(timetable, outfile, indent=2)
+56 -15
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python3.113.11 #!/usr/bin/env python3.11
from os.path import exists as file_exists from os.path import exists as file_exists
from prettytable import PrettyTable from prettytable import PrettyTable
from typing import Tuple from typing import Tuple
@@ -20,11 +20,11 @@ from scheduleGenerator.generator import generateScheduleV3
done = False done = False
# consts # constants
blockClassLimit = 40 TOTAL_BLOCKS = 10 # this can also be 8 for 4 blocks per semester
BLOCK_CLASS_LIMIT = 40 # this is number of classrooms available per block
# print() MIN_REQ = 18 # this is minumum number of requests to allow a course to run
# debug(f'Current blockClassLimit = {blockClassLimit}') CLASS_CAP = 30 # this is the max number of students per class
def processing(msg: str): def processing(msg: str):
for c in itertools.cycle(['|', '/', '-', '\\']): for c in itertools.cycle(['|', '/', '-', '\\']):
@@ -66,15 +66,26 @@ def main():
st = time() # Start time st = time() # Start time
if noAnim: if noAnim:
sampleStudents = getSampleStudents("./sample_data/course_selection_data.csv", True) sampleStudents = getSampleStudents("./sample_data/course_selection_data.csv", log=True, totalBlocks=TOTAL_BLOCKS)
sampleCourses = getSampleCourses("./sample_data/course_selection_data.csv", True) sampleCourses = getSampleCourses("./sample_data/course_selection_data.csv", log=True)
timetable = {} timetable = {}
timetable["Version"] = 3 timetable["Version"] = 3
timetable["timetable"] = generateScheduleV3(sampleStudents, sampleCourses, blockClassLimit, "./output/students.json", "./output/conflicts.json") timetable["timetable"], err = generateScheduleV3(
sampleStudents,
sampleCourses,
minReq=MIN_REQ,
blockClassLimit=BLOCK_CLASS_LIMIT,
classCap=CLASS_CAP,
totalBlocks=TOTAL_BLOCKS,
studentsDir="./output/students.json",
conflictsDir="./output/conflicts.json")
if err is not None: raise SystemExit(f'{err.Title} : {err.Description}')
else: else:
t = threading.Thread(target=processing, args=('Collection student requests',)) t = threading.Thread(target=processing, args=('Collection student requests',))
t.start() # Start animation t.start() # Start animation
sampleStudents = getSampleStudents("./sample_data/course_selection_data.csv", True) sampleStudents = getSampleStudents("./sample_data/course_selection_data.csv", log=True, totalBlocks=TOTAL_BLOCKS)
done = True # End Animation done = True # End Animation
print('\nStudent list generated.\n') print('\nStudent list generated.\n')
@@ -82,7 +93,7 @@ def main():
t = threading.Thread(target=processing, args=('Collection Course Information',)) t = threading.Thread(target=processing, args=('Collection Course Information',))
done = False # reset animation done = False # reset animation
t.start() # Start animation t.start() # Start animation
sampleCourses = getSampleCourses("./sample_data/course_selection_data.csv", True) sampleCourses = getSampleCourses("./sample_data/course_selection_data.csv", log=True)
done = True # End Animation done = True # End Animation
print('\nCourse Information Collected.\n') print('\nCourse Information Collected.\n')
@@ -92,9 +103,18 @@ def main():
t.start() # Start animation t.start() # Start animation
timetable = {} timetable = {}
timetable["Version"] = 3 timetable["Version"] = 3
timetable["timetable"] = generateScheduleV3(sampleStudents, sampleCourses, blockClassLimit, "./output/students.json", "./output/conflicts.json") timetable["timetable"], err = generateScheduleV3(
sampleStudents,
sampleCourses,
minReq=MIN_REQ,
blockClassLimit=BLOCK_CLASS_LIMIT,
classCap=CLASS_CAP,
totalBlocks=TOTAL_BLOCKS,
studentsDir="./output/students.json",
conflictsDir="./output/conflicts.json")
done = True # End Animation done = True # End Animation
if err is not None: raise SystemExit(f'{err.Title} : {err.Description}')
et = time() # End time et = time() # End time
elapsed_time = round((et - st), 3) # Execution time elapsed_time = round((et - st), 3) # Execution time
@@ -127,14 +147,35 @@ def main():
if noAnim: if noAnim:
timetable = {} timetable = {}
timetable["Version"] = 3 timetable["Version"] = 3
timetable["timetable"] = generateScheduleV3(sampleStudents, sampleCourses, blockClassLimit, "./output/students.json", "./output/conflicts.json") timetable["timetable"], err = generateScheduleV3(
else : sampleStudents,
sampleCourses,
minReq=MIN_REQ,
blockClassLimit=BLOCK_CLASS_LIMIT,
classCap=CLASS_CAP,
totalBlocks=TOTAL_BLOCKS,
studentsDir="./output/students.json",
conflictsDir="./output/conflicts.json")
if err is not None: raise SystemExit(f'{err.Title} : {err.Description}')
else:
t = threading.Thread(target=processing, args=('Processing',)) t = threading.Thread(target=processing, args=('Processing',))
t.start() # Start animation t.start() # Start animation
timetable = {} timetable = {}
timetable["Version"] = 3 timetable["Version"] = 3
timetable["timetable"] = generateScheduleV3(sampleStudents, sampleCourses, blockClassLimit, "./output/students.json", "./output/conflicts.json") timetable["timetable"], err = generateScheduleV3(
sampleStudents,
sampleCourses,
minReq=MIN_REQ,
blockClassLimit=BLOCK_CLASS_LIMIT,
classCap=CLASS_CAP,
totalBlocks=TOTAL_BLOCKS,
studentsDir="./output/students.json",
conflictsDir="./output/conflicts.json")
done = True # End Animation done = True # End Animation
if err is not None: raise SystemExit(f'{err.Title} : {err.Description}')
et = time() # End time et = time() # End time
elapsed_time = round((et - st), 3) # Execution time elapsed_time = round((et - st), 3) # Execution time
+4 -14
View File
@@ -8,7 +8,7 @@ flex= ("XAT--12A-S", "XAT--12B-S")
mockStudents: list[dict] = [] mockStudents: list[dict] = []
# sort real sample data into usable dictionary # sort real sample data into usable dictionary
def getSampleStudents(data_dir: str, log: bool = False) -> list[dict]: def getSampleStudents(data_dir: str, log: bool = False, totalBlocks: int = 10) -> list[dict]:
with open(data_dir, newline='') as csvfile: with open(data_dir, newline='') as csvfile:
reader = csv.DictReader(csvfile) reader = csv.DictReader(csvfile)
for row in reader: for row in reader:
@@ -18,7 +18,7 @@ def getSampleStudents(data_dir: str, log: bool = False) -> list[dict]:
if exists: break if exists: break
alternate = True if row["Alternate?"] == 'TRUE' else False alternate = True if row["Alternate?"] == 'TRUE' else False
if exists: if exists:
if len(mockStudents[student["studentIndex"]]["requests"]) >= 10 and not alternate and row["CrsNo"] not in flex: alternate = True if len(mockStudents[student["studentIndex"]]["requests"]) >= totalBlocks and not alternate and row["CrsNo"] not in flex: alternate = True
mockStudents[student["studentIndex"]]["requests"].append({ mockStudents[student["studentIndex"]]["requests"].append({
"CrsNo": row["CrsNo"], "CrsNo": row["CrsNo"],
"Description": row["Description"], "Description": row["Description"],
@@ -34,23 +34,13 @@ def getSampleStudents(data_dir: str, log: bool = False) -> list[dict]:
"Description": row["Description"], "Description": row["Description"],
"alt": alternate "alt": alternate
}], }],
"schedule": { "schedule": {},
"block1": [],
"block2": [],
"block3": [],
"block4": [],
"block5": [],
"block6": [],
"block7": [],
"block8": [],
"block9": [],
"block10": []
},
"expectedClasses": 1, "expectedClasses": 1,
"classes": 0, "classes": 0,
"remainingAlts": [], "remainingAlts": [],
"studentIndex": len(mockStudents) "studentIndex": len(mockStudents)
} }
for i in range(1, totalBlocks+1): newStudent["schedule"][f'block{i}'] = []
mockStudents.append(newStudent) mockStudents.append(newStudent)
# Estimate student grades # Estimate student grades