247 lines
11 KiB
Python
247 lines
11 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. 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
|