Files
timetable/controller/template_controller.py
Talhadeveloperr a7bb2f3dae initial project
2026-09-09 17:00:54 +05:00

207 lines
9.0 KiB
Python

"""
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