From b38c15034bfcac54a1862af3bda13a357d4c9a67 Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Sat, 5 Nov 2022 22:58:14 -0700 Subject: [PATCH] allow more variable changes --- scheduleGenerator/generator.py | 88 ++++++++++++++++++---------------- tinker.py | 75 ++++++++++++++++++++++------- util/mockStudents.py | 18 ++----- 3 files changed, 110 insertions(+), 71 deletions(-) diff --git a/scheduleGenerator/generator.py b/scheduleGenerator/generator.py index 6e9f305..ca27d51 100755 --- a/scheduleGenerator/generator.py +++ b/scheduleGenerator/generator.py @@ -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) diff --git a/tinker.py b/tinker.py index ccbbb48..abb7112 100755 --- a/tinker.py +++ b/tinker.py @@ -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') diff --git a/util/mockStudents.py b/util/mockStudents.py index 532c016..24ebb8d 100755 --- a/util/mockStudents.py +++ b/util/mockStudents.py @@ -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