""" 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. Each general offering gets `lectures_per_week` sessions. 2. Lab courses are placed next: each offering gets `labs_per_week` 180-minute / double-slot sessions, in lab rooms only, on whichever day/slot-pair is free for the batch, instructor, and room. 3. Remaining theory sessions (`lectures_per_week` per non-general offering) fill the rest of the week (Mon/Tue/Wed/Thu/Fri), one 90-minute slot at a time, avoiding room / instructor / batch clashes. Room selection always tries a course's preferred lecture_room_id / lab_room_id first, falling back to another same-type room only if the preferred one is busy for that day/slot. """ 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 rooms_by_id = {r["room_id"]: r for r in rooms} instructor_names_by_code = {i["instructor_code"]: i["instructor_name"] for i in instructors} batches_by_code = {b["batch_code"]: b for b in batches} # normalize courses: numeric fields + a lookup by id courses_by_id = {} for c in courses: c["credit_hours"] = int(c["credit_hours"]) c["lectures_per_week"] = int(c["lectures_per_week"]) c["labs_per_week"] = int(c["labs_per_week"]) c["is_general"] = c["is_general"].lower() == "yes" courses_by_id[c["course_id"]] = c # normalize offerings: expand "batch1|batch2" section batch groups into # a list of batch codes, and attach the course record for convenience for off in offerings: off["batch_codes"] = off["batch_codes"].split("|") off["course"] = courses_by_id[off["course_id"]] off["instructor_name"] = instructor_names_by_code.get(off["instructor_code"], off["instructor_code"]) return { "rooms": rooms, "rooms_by_id": rooms_by_id, "blocks": blocks, "instructors": instructors, "instructor_names_by_code": instructor_names_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 _room_candidates(self, preferred_room_id, room_pool): """Preferred room first, then the rest of the same-type pool.""" preferred = self.data["rooms_by_id"].get(preferred_room_id) rest = [r for r in room_pool if r["room_id"] != preferred_room_id] return ([preferred] if preferred else []) + rest def _add_session(self, offering, day, slot_indices, room): course = offering["course"] start = TIME_SLOTS[slot_indices[0]]["start"] end = TIME_SLOTS[slot_indices[-1]]["end"] session_type = "lab" if len(slot_indices) > 1 else "theory" 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": session_type, "is_general": course["is_general"], "section_id": offering["section_id"], "instructor_code": offering["instructor_code"], "instructor_name": offering["instructor_name"], "room_id": room["room_id"], "room_name": room["room_name"], "batch_codes": offering["batch_codes"], }) def schedule_general_courses(self): general = [o for o in self.data["offerings"] if o["course"]["is_general"]] for offering in general: course = offering["course"] room_candidates = self._room_candidates(course["lecture_room_id"], self.classrooms) for _ in range(course["lectures_per_week"]): placed = False # Tuesday first, then spill to the rest of the week if a # session can't fit there. for day in ["Tuesday"] + [d for d in DAYS if d != "Tuesday"]: for slot_index in TEACHING_SLOT_INDICES: for room in room_candidates: 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 a lecture for general course {offering['course_id']} section {offering['section_id']}") def schedule_lab_courses(self): offerings = [o for o in self.data["offerings"] if not o["course"]["is_general"] and o["course"]["labs_per_week"] > 0] for offering in offerings: course = offering["course"] room_candidates = self._room_candidates(course["lab_room_id"], self.labs) for _ in range(course["labs_per_week"]): placed = False for day in DAYS: for slot_a, slot_b in LAB_SLOT_PAIRS: for room in room_candidates: 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 a lab for course {offering['course_id']} section {offering['section_id']} ({offering['batch_codes']})") def schedule_theory_courses(self): offerings = [o for o in self.data["offerings"] if not o["course"]["is_general"]] for offering in offerings: course = offering["course"] room_candidates = self._room_candidates(course["lecture_room_id"], self.classrooms) for _ in range(course["lectures_per_week"]): placed = False for day in DAYS: for slot_index in TEACHING_SLOT_INDICES: for room in room_candidates: 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 a lecture for course {offering['course_id']} section {offering['section_id']} ({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