allow more variable changes
This commit is contained in:
3 files changed
+110
-71
No files matched your search
@@ -3,6 +3,7 @@ import json
|
||||
import random
|
||||
from inspect import currentframe
|
||||
from string import hexdigits
|
||||
from dataclasses import dataclass
|
||||
|
||||
# Import from custom utilities
|
||||
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
|
||||
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
|
||||
# Returns if the particular student has a previous error
|
||||
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
|
||||
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
|
||||
# Semester 1 and 2
|
||||
flex = ("XAT--12A-S", "XAT--12B-S")
|
||||
@@ -90,13 +86,28 @@ 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, # Refer to ../util/mockStudents.py to see the students list structure
|
||||
courses: dict, # Reger to ../util/generateCourses.py to see the courses dictionary 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
|
||||
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
|
||||
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",
|
||||
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
|
||||
q,r = divmod(sum(l),len(l))
|
||||
@@ -276,14 +287,19 @@ def generateScheduleV3(
|
||||
|
||||
# Step 4 - Attempt to fit classes into timetable
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
courseRunInfoCopy = dict(courseRunInfo)
|
||||
@@ -294,15 +310,12 @@ def generateScheduleV3(
|
||||
course = list(courseRunInfo)[index]
|
||||
|
||||
# Tally first and second semester
|
||||
sem1 = sum(len(running[f'block{i}']) for i in range(1, 6))
|
||||
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)]
|
||||
allSemBlockLens = [len(running[f'block{i}']) for i in range(1, totalBlocks + 1)]
|
||||
|
||||
# If there is more than one class Running
|
||||
if allClassRunCounts[index] > 1:
|
||||
blockIndex = allSemBlockLens.index(min(allSemBlockLens))
|
||||
startSem = 1 if blockIndex < 5 else 2
|
||||
stepType = 0 if blockIndex < 5 else 1
|
||||
stepType = 0 if blockIndex < blockPerSem else 1
|
||||
offset = 0
|
||||
|
||||
# Spread classes throughout both semesters
|
||||
@@ -310,7 +323,6 @@ def generateScheduleV3(
|
||||
cname = f"{course}-{hexdigits[i]}"
|
||||
classInserted = False
|
||||
while not classInserted:
|
||||
|
||||
blockIndex += offset
|
||||
if len(running[f'block{blockIndex+1}']) < blockClassLimit:
|
||||
running[list(running)[blockIndex]][cname] = {
|
||||
@@ -323,8 +335,8 @@ def generateScheduleV3(
|
||||
|
||||
offset = stepIndex(offset, stepType)
|
||||
|
||||
if blockIndex >= 9:
|
||||
blockIndex = 0 if stepType == 0 else 5
|
||||
if blockIndex >= (totalBlocks - 1):
|
||||
blockIndex = 0 if stepType == 0 else blockPerSem
|
||||
offset = 0
|
||||
|
||||
# If the class only runs once, place in semester with least classes
|
||||
@@ -348,10 +360,6 @@ def generateScheduleV3(
|
||||
allClassRunCounts.remove(allClassRunCounts[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
|
||||
for block in running:
|
||||
for cname in running[block]:
|
||||
@@ -367,12 +375,9 @@ def generateScheduleV3(
|
||||
studentsCritical, studentsAcceptable = 0, 0
|
||||
|
||||
for student in students:
|
||||
# if student["Pupil #"] == "772554": debug("772554")
|
||||
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
|
||||
# if student["Pupil #"] == "772554": debug(f'Has conflict? {hasConflicts}')
|
||||
|
||||
|
||||
# If there is no conflicts
|
||||
# and classes inserted to is equal to expectedClasses
|
||||
# or classes the student is inserted to is missing
|
||||
@@ -407,7 +412,7 @@ def generateScheduleV3(
|
||||
runCounts.append(courseRunInfoCopy[cname[:-2]]["Total"])
|
||||
|
||||
# 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:
|
||||
index = runCounts.index(min(runCounts)) # Get class least run
|
||||
found = False
|
||||
@@ -583,11 +588,12 @@ def generateScheduleV3(
|
||||
if len(student["schedule"][block]) == 0:
|
||||
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:
|
||||
json.dump(students, outfile, indent=2)
|
||||
|
||||
return running
|
||||
return (running, None)
|
||||
|
||||
|
||||
def main():
|
||||
print("Processing...")
|
||||
@@ -596,7 +602,9 @@ def main():
|
||||
samplemockCourses = getSampleCourses(True)
|
||||
timetable = {}
|
||||
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:
|
||||
json.dump(timetable, outfile, indent=2)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python3.113.11
|
||||
#!/usr/bin/env python3.11
|
||||
from os.path import exists as file_exists
|
||||
from prettytable import PrettyTable
|
||||
from typing import Tuple
|
||||
@@ -20,12 +20,12 @@ from scheduleGenerator.generator import generateScheduleV3
|
||||
|
||||
done = False
|
||||
|
||||
# consts
|
||||
blockClassLimit = 40
|
||||
# constants
|
||||
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
|
||||
MIN_REQ = 18 # this is minumum number of requests to allow a course to run
|
||||
CLASS_CAP = 30 # this is the max number of students per class
|
||||
|
||||
# print()
|
||||
# debug(f'Current blockClassLimit = {blockClassLimit}')
|
||||
|
||||
def processing(msg: str):
|
||||
for c in itertools.cycle(['|', '/', '-', '\\']):
|
||||
if done: break
|
||||
@@ -66,15 +66,26 @@ def main():
|
||||
st = time() # Start time
|
||||
|
||||
if noAnim:
|
||||
sampleStudents = getSampleStudents("./sample_data/course_selection_data.csv", True)
|
||||
sampleCourses = getSampleCourses("./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", log=True)
|
||||
timetable = {}
|
||||
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:
|
||||
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", log=True, totalBlocks=TOTAL_BLOCKS)
|
||||
done = True # End Animation
|
||||
print('\nStudent list generated.\n')
|
||||
|
||||
@@ -82,7 +93,7 @@ def main():
|
||||
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)
|
||||
sampleCourses = getSampleCourses("./sample_data/course_selection_data.csv", log=True)
|
||||
done = True # End Animation
|
||||
print('\nCourse Information Collected.\n')
|
||||
|
||||
@@ -92,9 +103,18 @@ def main():
|
||||
t.start() # Start animation
|
||||
timetable = {}
|
||||
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
|
||||
if err is not None: raise SystemExit(f'{err.Title} : {err.Description}')
|
||||
|
||||
et = time() # End time
|
||||
elapsed_time = round((et - st), 3) # Execution time
|
||||
@@ -127,15 +147,36 @@ def main():
|
||||
if noAnim:
|
||||
timetable = {}
|
||||
timetable["Version"] = 3
|
||||
timetable["timetable"] = generateScheduleV3(sampleStudents, sampleCourses, blockClassLimit, "./output/students.json", "./output/conflicts.json")
|
||||
else :
|
||||
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:
|
||||
t = threading.Thread(target=processing, args=('Processing',))
|
||||
t.start() # Start animation
|
||||
timetable = {}
|
||||
timetable["Version"] = 3
|
||||
timetable["timetable"] = generateScheduleV3(sampleStudents, sampleCourses, blockClassLimit, "./output/students.json", "./output/conflicts.json")
|
||||
done = True # End Animation
|
||||
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
|
||||
if err is not None: raise SystemExit(f'{err.Title} : {err.Description}')
|
||||
|
||||
et = time() # End time
|
||||
elapsed_time = round((et - st), 3) # Execution time
|
||||
print(f'\n\nDone - Finished in {elapsed_time} seconds\n')
|
||||
|
||||
+4
-14
@@ -8,7 +8,7 @@ flex= ("XAT--12A-S", "XAT--12B-S")
|
||||
mockStudents: list[dict] = []
|
||||
|
||||
# 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:
|
||||
reader = csv.DictReader(csvfile)
|
||||
for row in reader:
|
||||
@@ -18,7 +18,7 @@ def getSampleStudents(data_dir: str, log: bool = False) -> list[dict]:
|
||||
if exists: break
|
||||
alternate = True if row["Alternate?"] == 'TRUE' else False
|
||||
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({
|
||||
"CrsNo": row["CrsNo"],
|
||||
"Description": row["Description"],
|
||||
@@ -34,23 +34,13 @@ def getSampleStudents(data_dir: str, log: bool = False) -> list[dict]:
|
||||
"Description": row["Description"],
|
||||
"alt": alternate
|
||||
}],
|
||||
"schedule": {
|
||||
"block1": [],
|
||||
"block2": [],
|
||||
"block3": [],
|
||||
"block4": [],
|
||||
"block5": [],
|
||||
"block6": [],
|
||||
"block7": [],
|
||||
"block8": [],
|
||||
"block9": [],
|
||||
"block10": []
|
||||
},
|
||||
"schedule": {},
|
||||
"expectedClasses": 1,
|
||||
"classes": 0,
|
||||
"remainingAlts": [],
|
||||
"studentIndex": len(mockStudents)
|
||||
}
|
||||
for i in range(1, totalBlocks+1): newStudent["schedule"][f'block{i}'] = []
|
||||
mockStudents.append(newStudent)
|
||||
|
||||
# Estimate student grades
|
||||
|
||||
Reference in new issue
Block a user