Files
timetable/controller/template_controller.py
Talhadeveloperr 9079ba2739 succesfull testing
2026-09-09 18:55:53 +05:00

146 lines
5.7 KiB
Python

"""
Loads dummy CSV data and exposes a thin Scheduler facade over the solver
engine in controller/solver/ (grid constants, PlacementBoard, and one
module per phase — phase1_general, phase2_labs, phase3_remaining).
Scheduling is done in three sequential, independently-verified phases, each
with its own tailored strategy (see controller/solver/phase*_*.py for the
per-phase logic). Each phase's placements are locked before the next phase
starts: once a (day, slot, room/instructor/batch) combination is marked
busy by an earlier phase, later phases can never reuse it — except Phase
3, which is allowed to backtrack and reopen *its own* earlier batches
(never Phase 1 or Phase 2) when a later batch can't otherwise be placed.
1. General courses (shared across every batch of a session), preferring
Tuesday/Thursday first, self-repaired until clash-free.
2. Labs, scheduled against Phase 1's fixed placements, then self-repaired
for lab-vs-lab clashes within Phase 2 only.
3. Remaining (non-general) theory sessions, scheduled batch by batch in
sorted batch_code order, with backtracking into earlier Phase 3
batches when a later batch can't otherwise fit.
Time grid: 5 normal 90-minute slots, 09:00-16:30, no lunch break. Labs use
180-minute double-slots (09:00-12:00 and 12:00-15:00). A course whose
lecture_duration_minutes is 120 ("2 hours once a week") is instead placed
in a special fixed 08:30-10:30 slot, which overlaps normal slot 0
(09:00-10:30) in wall-clock time and is tracked as clashing with it.
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
from solver.grid import ( # noqa: F401 (re-exported for templates/backward compatibility)
DAYS, TIME_SLOTS, TEACHING_SLOT_INDICES, LAB_SLOT_PAIRS,
LONG_LECTURE_SLOT_INDEX, LONG_LECTURE_START, LONG_LECTURE_END, DISPLAY_ROWS,
)
from solver.placement_board import PlacementBoard
from solver import phase1_general, phase2_labs, phase3_remaining
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "dummy")
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"
c["lecture_duration_minutes"] = int(c.get("lecture_duration_minutes") or 90)
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:
"""
Thin facade over the solver engine: owns a PlacementBoard and
delegates each phase to its dedicated solver/phase*_*.py module.
Keeps the same public surface the rest of the app already relies on
(app/template_generator.py, controller/conflict_checker/*).
"""
def __init__(self, data):
self.data = data
self.board = PlacementBoard(data)
@property
def sessions(self):
return self.board.sessions
def to_state(self):
return self.board.to_state()
@classmethod
def from_state(cls, data, state):
scheduler = cls(data)
scheduler.board = PlacementBoard.from_state(data, state)
return scheduler
def phase_1_general_courses(self):
return phase1_general.run(self.board, self.data)
def phase_2_labs(self):
return phase2_labs.run(self.board, self.data)
def phase_3_remaining_courses(self):
return phase3_remaining.run(self.board, self.data)
def run(self):
self.phase_1_general_courses()
self.phase_2_labs()
self.phase_3_remaining_courses()
return self.board.sessions
def build_schedule():
data = load_data()
scheduler = Scheduler(data)
sessions = scheduler.run()
return data, sessions