""" Loads dummy CSV data and builds a clash-free schedule of sessions that all five templates/*-wise template modules render from. Scheduling rules (dummy/demo scheduler, not a full CSP solver): 1. General courses (shared across every batch of a session) are placed first, only on Tuesday, so those slots are locked in before anything else and never clash across the batches sharing them. 2. Lab courses (180-minute / double-slot) are placed next, in lab rooms only, on whichever day/slot-pair is free for the batch, instructor, and room. 3. Remaining theory courses fill the rest of the week (Mon/Tue/Wed/Thu/Fri), one 90-minute slot at a time, avoiding room / instructor / batch clashes. """ import csv import os DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "dummy") DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"] # Theory grid: 5 x 90-minute slots with a lunch break, 08:30-16:30 TIME_SLOTS = [ {"label": "S1", "start": "08:30", "end": "10:00", "is_lunch": False}, {"label": "S2", "start": "10:00", "end": "11:30", "is_lunch": False}, {"label": "S3", "start": "11:30", "end": "13:00", "is_lunch": False}, {"label": "LUNCH", "start": "13:00", "end": "13:30", "is_lunch": True}, {"label": "S4", "start": "13:30", "end": "15:00", "is_lunch": False}, {"label": "S5", "start": "15:00", "end": "16:30", "is_lunch": False}, ] # Indices into TIME_SLOTS that are real teaching slots (skip lunch) TEACHING_SLOT_INDICES = [i for i, s in enumerate(TIME_SLOTS) if not s["is_lunch"]] # Consecutive pairs of teaching slots usable as a 180-minute lab double-slot LAB_SLOT_PAIRS = [ (a, b) for a, b in zip(TEACHING_SLOT_INDICES, TEACHING_SLOT_INDICES[1:]) if TIME_SLOTS[b]["start"] == TIME_SLOTS[a]["end"] ] def _read_csv(relative_path): path = os.path.join(DATA_DIR, relative_path) with open(path, newline="", encoding="utf-8") as f: return list(csv.DictReader(f)) def load_data(): """Loads all dummy CSVs into plain dict/list structures.""" rooms = _read_csv(os.path.join("blocks", "rooms.csv")) blocks = _read_csv(os.path.join("blocks", "blocks.csv")) instructors = _read_csv(os.path.join("instructor", "instructors.csv")) courses = _read_csv(os.path.join("courses", "courses.csv")) offerings = _read_csv(os.path.join("courses", "course_offering.csv")) students = _read_csv(os.path.join("roll_no", "students.csv")) batches_25 = _read_csv(os.path.join("batch_codes", "25", "batches.csv")) batches_26 = _read_csv(os.path.join("batch_codes", "26", "batches.csv")) batches = batches_25 + batches_26 courses_by_id = {c["course_id"]: c for c in courses} instructors_by_code = {i["instructor_code"]: i for i in instructors} batches_by_code = {b["batch_code"]: b for b in batches} # normalize offerings: expand "batch1|batch2" general offerings into a # list of batch codes on each offering record for off in offerings: off["batch_codes"] = off["batch_code"].split("|") off["is_general"] = courses_by_id[off["course_id"]]["is_general"].lower() == "yes" off["course_type"] = courses_by_id[off["course_id"]]["course_type"] return { "rooms": rooms, "blocks": blocks, "instructors": instructors, "instructors_by_code": instructors_by_code, "courses": courses, "courses_by_id": courses_by_id, "offerings": offerings, "students": students, "batches": batches, "batches_by_code": batches_by_code, } class Scheduler: """Greedy clash-free scheduler over rooms / instructors / batches.""" def __init__(self, data): self.data = data self.classrooms = [r for r in data["rooms"] if r["room_type"] == "classroom"] self.labs = [r for r in data["rooms"] if r["room_type"] == "lab"] # busy[(day, slot_index)] -> set of keys already occupied self.room_busy = {} self.instructor_busy = {} self.batch_busy = {} self.sessions = [] def _is_free(self, day, slot_index, room_id, instructor_code, batch_codes): key = (day, slot_index) if room_id in self.room_busy.get(key, set()): return False if instructor_code in self.instructor_busy.get(key, set()): return False if any(b in self.batch_busy.get(key, set()) for b in batch_codes): return False return True def _mark_busy(self, day, slot_index, room_id, instructor_code, batch_codes): key = (day, slot_index) self.room_busy.setdefault(key, set()).add(room_id) self.instructor_busy.setdefault(key, set()).add(instructor_code) self.batch_busy.setdefault(key, set()).update(batch_codes) def _add_session(self, offering, day, slot_indices, room): course = self.data["courses_by_id"][offering["course_id"]] start = TIME_SLOTS[slot_indices[0]]["start"] end = TIME_SLOTS[slot_indices[-1]]["end"] self.sessions.append({ "day": day, "slot_indices": slot_indices, "start": start, "end": end, "course_id": offering["course_id"], "course_name": course["course_name"], "course_type": course["course_type"], "is_general": offering["is_general"], "instructor_code": offering["instructor_code"], "instructor_name": self.data["instructors_by_code"][offering["instructor_code"]]["instructor_name"], "room_id": room["room_id"], "room_name": room["room_name"], "batch_codes": offering["batch_codes"], }) def schedule_general_courses(self): day = "Tuesday" general = [o for o in self.data["offerings"] if o["is_general"]] for offering in general: placed = False for slot_index in TEACHING_SLOT_INDICES: for room in self.classrooms: if self._is_free(day, slot_index, room["room_id"], offering["instructor_code"], offering["batch_codes"]): self._mark_busy(day, slot_index, room["room_id"], offering["instructor_code"], offering["batch_codes"]) self._add_session(offering, day, [slot_index], room) placed = True break if placed: break if not placed: print(f"WARNING: could not place general course {offering['course_id']}") def schedule_lab_courses(self): labs_offerings = [o for o in self.data["offerings"] if o["course_type"] == "lab" and not o["is_general"]] for offering in labs_offerings: placed = False for day in DAYS: for slot_a, slot_b in LAB_SLOT_PAIRS: for room in self.labs: if self._is_free(day, slot_a, room["room_id"], offering["instructor_code"], offering["batch_codes"]) and \ self._is_free(day, slot_b, room["room_id"], offering["instructor_code"], offering["batch_codes"]): self._mark_busy(day, slot_a, room["room_id"], offering["instructor_code"], offering["batch_codes"]) self._mark_busy(day, slot_b, room["room_id"], offering["instructor_code"], offering["batch_codes"]) self._add_session(offering, day, [slot_a, slot_b], room) placed = True break if placed: break if placed: break if not placed: print(f"WARNING: could not place lab course {offering['course_id']} for {offering['batch_codes']}") def schedule_theory_courses(self): theory_offerings = [o for o in self.data["offerings"] if o["course_type"] == "theory" and not o["is_general"]] for offering in theory_offerings: placed = False for day in DAYS: for slot_index in TEACHING_SLOT_INDICES: for room in self.classrooms: if self._is_free(day, slot_index, room["room_id"], offering["instructor_code"], offering["batch_codes"]): self._mark_busy(day, slot_index, room["room_id"], offering["instructor_code"], offering["batch_codes"]) self._add_session(offering, day, [slot_index], room) placed = True break if placed: break if placed: break if not placed: print(f"WARNING: could not place theory course {offering['course_id']} for {offering['batch_codes']}") def run(self): self.schedule_general_courses() self.schedule_lab_courses() self.schedule_theory_courses() return self.sessions def build_schedule(): data = load_data() scheduler = Scheduler(data) sessions = scheduler.run() return data, sessions