phases initial
This commit is contained in:
@@ -22,11 +22,11 @@ sys.path.insert(0, os.path.join(ROOT_DIR, "templates", "room-wise"))
|
|||||||
sys.path.insert(0, os.path.join(ROOT_DIR, "templates", "student-wise"))
|
sys.path.insert(0, os.path.join(ROOT_DIR, "templates", "student-wise"))
|
||||||
sys.path.insert(0, os.path.join(ROOT_DIR, "templates", "teacher-wise"))
|
sys.path.insert(0, os.path.join(ROOT_DIR, "templates", "teacher-wise"))
|
||||||
|
|
||||||
from template_controller import build_schedule # noqa: E402
|
from template_controller import load_data, Scheduler # noqa: E402
|
||||||
from conflict_checker import check_all_conflicts # noqa: E402
|
from conflict_checker import check_all_conflicts # noqa: E402
|
||||||
from conflict_checker.student_conflict_checker import format_student_conflicts # noqa: E402
|
from conflict_checker.student_conflict_checker import format_student_conflicts # noqa: E402
|
||||||
from conflict_checker.teacher_conflict_checker import format_teacher_conflicts # noqa: E402
|
from conflict_checker.teacher_conflict_checker import format_teacher_conflicts # noqa: E402
|
||||||
from template_logger import reset_log, log_error, log_info # noqa: E402
|
from template_logger import reset_log, log_error, log_info, log_warning # noqa: E402
|
||||||
|
|
||||||
from batch_wise_template import generate_batch_wise_templates # noqa: E402
|
from batch_wise_template import generate_batch_wise_templates # noqa: E402
|
||||||
from department_wise_template import generate_department_wise_templates # noqa: E402
|
from department_wise_template import generate_department_wise_templates # noqa: E402
|
||||||
@@ -38,6 +38,23 @@ RESULT_DIR = os.path.join(ROOT_DIR, "result", "templates")
|
|||||||
VIEW_NAMES = ["batch-wise", "department-wise", "room-wise", "student-wise", "teacher-wise"]
|
VIEW_NAMES = ["batch-wise", "department-wise", "room-wise", "student-wise", "teacher-wise"]
|
||||||
|
|
||||||
|
|
||||||
|
def _check_and_log(sessions, label):
|
||||||
|
"""Runs student + teacher conflict checks over `sessions` and logs the result."""
|
||||||
|
report = check_all_conflicts(sessions)
|
||||||
|
if report["has_conflicts"]:
|
||||||
|
for line in format_student_conflicts(report["student_conflicts"]):
|
||||||
|
log_error(f"[{label}] STUDENT CONFLICT: {line}")
|
||||||
|
for line in format_teacher_conflicts(report["teacher_conflicts"]):
|
||||||
|
log_error(f"[{label}] TEACHER CONFLICT: {line}")
|
||||||
|
log_warning(
|
||||||
|
f"[{label}] Found {len(report['student_conflicts'])} student conflict(s) and "
|
||||||
|
f"{len(report['teacher_conflicts'])} teacher conflict(s)."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log_info(f"[{label}] No conflicts found — clash-free.")
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
def clear_result_dir():
|
def clear_result_dir():
|
||||||
"""Removes every previous run's output so stale files never linger."""
|
"""Removes every previous run's output so stale files never linger."""
|
||||||
for view_name in VIEW_NAMES:
|
for view_name in VIEW_NAMES:
|
||||||
@@ -59,23 +76,32 @@ def main():
|
|||||||
generate_dummy_data()
|
generate_dummy_data()
|
||||||
print()
|
print()
|
||||||
|
|
||||||
print("Loading data and building clash-free schedule...")
|
log_info("Loading data...")
|
||||||
data, sessions = build_schedule()
|
data = load_data()
|
||||||
print(f"Scheduled {len(sessions)} sessions.\n")
|
scheduler = Scheduler(data)
|
||||||
|
|
||||||
log_info("Running conflict checks (student/batch and teacher)...")
|
# --- Phase 1: general courses (Tue/Thu preferred), self-repaired until clash-free ---
|
||||||
report = check_all_conflicts(sessions)
|
log_info("Phase 1/3: scheduling general courses...")
|
||||||
if report["has_conflicts"]:
|
phase1_sessions = scheduler.phase_1_general_courses()
|
||||||
for line in format_student_conflicts(report["student_conflicts"]):
|
_check_and_log(phase1_sessions, "Phase 1 (general courses)")
|
||||||
log_error(f"STUDENT CONFLICT: {line}")
|
|
||||||
for line in format_teacher_conflicts(report["teacher_conflicts"]):
|
# --- Phase 2: labs, scheduled against Phase 1's fixed placements ---
|
||||||
log_error(f"TEACHER CONFLICT: {line}")
|
log_info("Phase 2/3: scheduling labs...")
|
||||||
log_error(
|
phase2_sessions = scheduler.phase_2_labs()
|
||||||
f"Found {len(report['student_conflicts'])} student conflict(s) and "
|
_check_and_log(phase1_sessions + phase2_sessions, "Phase 2 (general courses + labs)")
|
||||||
f"{len(report['teacher_conflicts'])} teacher conflict(s)."
|
|
||||||
)
|
# --- Phase 3: remaining theory courses, batch by batch ---
|
||||||
else:
|
log_info("Phase 3/3: scheduling remaining courses (batch by batch)...")
|
||||||
log_info("No conflicts found — schedule is clash-free for students and teachers.")
|
phase3_sessions = scheduler.phase_3_remaining_courses()
|
||||||
|
|
||||||
|
sessions = phase1_sessions + phase2_sessions + phase3_sessions
|
||||||
|
log_info(
|
||||||
|
f"Scheduling complete: {len(phase1_sessions)} general, {len(phase2_sessions)} lab, "
|
||||||
|
f"{len(phase3_sessions)} remaining = {len(sessions)} total session(s)."
|
||||||
|
)
|
||||||
|
|
||||||
|
log_info("Running final conflict check (student/batch and teacher) over the full schedule...")
|
||||||
|
_check_and_log(sessions, "Final (full schedule)")
|
||||||
|
|
||||||
generators = [
|
generators = [
|
||||||
("batch-wise", generate_batch_wise_templates),
|
("batch-wise", generate_batch_wise_templates),
|
||||||
|
|||||||
@@ -2,17 +2,23 @@
|
|||||||
Loads dummy CSV data and builds a clash-free schedule of sessions that all
|
Loads dummy CSV data and builds a clash-free schedule of sessions that all
|
||||||
five templates/*-wise template modules render from.
|
five templates/*-wise template modules render from.
|
||||||
|
|
||||||
Scheduling rules (dummy/demo scheduler, not a full CSP solver):
|
Scheduling is done in three sequential, independently-verified phases (see
|
||||||
1. General courses (shared across every batch of a session) are placed
|
app/template_generator.py for the orchestration + per-phase conflict
|
||||||
first, only on Tuesday, so those slots are locked in before anything
|
checks). Each phase's placements are locked before the next phase starts:
|
||||||
else and never clash across the batches sharing them. Each general
|
once a (day, slot, room/instructor/batch) combination is marked busy by an
|
||||||
offering gets `lectures_per_week` sessions.
|
earlier phase, later phases can never reuse it.
|
||||||
2. Lab courses are placed next: each offering gets `labs_per_week`
|
|
||||||
180-minute / double-slot sessions, in lab rooms only, on whichever
|
1. General courses (shared across every batch of a session), preferring
|
||||||
day/slot-pair is free for the batch, instructor, and room.
|
Tuesday/Thursday first. Each offering gets `lectures_per_week`
|
||||||
3. Remaining theory sessions (`lectures_per_week` per non-general
|
sessions. If two general courses still land in the same slot for a
|
||||||
offering) fill the rest of the week (Mon/Tue/Wed/Thu/Fri), one
|
batch, the losing session is moved to another day/slot until this
|
||||||
90-minute slot at a time, avoiding room / instructor / batch clashes.
|
phase is clash-free (see `phase_1_general_courses`).
|
||||||
|
2. Labs: each offering gets `labs_per_week` 180-minute / double-slot
|
||||||
|
sessions, in lab rooms only, scheduled against Phase 1's fixed
|
||||||
|
placements plus labs placed so far this phase.
|
||||||
|
3. Remaining (non-general) theory sessions, scheduled batch by batch in
|
||||||
|
sorted batch_code order: for each batch, every one of its remaining
|
||||||
|
offerings is placed and locked before the next batch is considered.
|
||||||
|
|
||||||
Room selection always tries a course's preferred lecture_room_id /
|
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
|
lab_room_id first, falling back to another same-type room only if the
|
||||||
@@ -131,6 +137,13 @@ class Scheduler:
|
|||||||
self.instructor_busy.setdefault(key, set()).add(instructor_code)
|
self.instructor_busy.setdefault(key, set()).add(instructor_code)
|
||||||
self.batch_busy.setdefault(key, set()).update(batch_codes)
|
self.batch_busy.setdefault(key, set()).update(batch_codes)
|
||||||
|
|
||||||
|
def _unmark_busy(self, day, slot_index, room_id, instructor_code, batch_codes):
|
||||||
|
key = (day, slot_index)
|
||||||
|
self.room_busy.get(key, set()).discard(room_id)
|
||||||
|
self.instructor_busy.get(key, set()).discard(instructor_code)
|
||||||
|
for b in batch_codes:
|
||||||
|
self.batch_busy.get(key, set()).discard(b)
|
||||||
|
|
||||||
def _room_candidates(self, preferred_room_id, room_pool):
|
def _room_candidates(self, preferred_room_id, room_pool):
|
||||||
"""Preferred room first, then the rest of the same-type pool."""
|
"""Preferred room first, then the rest of the same-type pool."""
|
||||||
preferred = self.data["rooms_by_id"].get(preferred_room_id)
|
preferred = self.data["rooms_by_id"].get(preferred_room_id)
|
||||||
@@ -142,7 +155,7 @@ class Scheduler:
|
|||||||
start = TIME_SLOTS[slot_indices[0]]["start"]
|
start = TIME_SLOTS[slot_indices[0]]["start"]
|
||||||
end = TIME_SLOTS[slot_indices[-1]]["end"]
|
end = TIME_SLOTS[slot_indices[-1]]["end"]
|
||||||
session_type = "lab" if len(slot_indices) > 1 else "theory"
|
session_type = "lab" if len(slot_indices) > 1 else "theory"
|
||||||
self.sessions.append({
|
session = {
|
||||||
"day": day,
|
"day": day,
|
||||||
"slot_indices": slot_indices,
|
"slot_indices": slot_indices,
|
||||||
"start": start,
|
"start": start,
|
||||||
@@ -157,34 +170,84 @@ class Scheduler:
|
|||||||
"room_id": room["room_id"],
|
"room_id": room["room_id"],
|
||||||
"room_name": room["room_name"],
|
"room_name": room["room_name"],
|
||||||
"batch_codes": offering["batch_codes"],
|
"batch_codes": offering["batch_codes"],
|
||||||
})
|
}
|
||||||
|
self.sessions.append(session)
|
||||||
|
return session
|
||||||
|
|
||||||
def schedule_general_courses(self):
|
def _place_theory_session(self, offering, room_candidates, day_order):
|
||||||
|
"""Finds and locks a free single-slot session; returns the session dict or None."""
|
||||||
|
for day in day_order:
|
||||||
|
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"])
|
||||||
|
return self._add_session(offering, day, [slot_index], room)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def phase_1_general_courses(self, preferred_days=("Tuesday", "Thursday"), max_repair_attempts=20):
|
||||||
|
"""
|
||||||
|
Places every general-course offering's weekly lectures, preferring
|
||||||
|
`preferred_days` first. Afterwards, self-repairs: any batch left
|
||||||
|
with two general-course sessions in the same slot has the losing
|
||||||
|
session unmarked and re-placed (scanning every day) until this
|
||||||
|
phase's own sessions are clash-free. Returns this phase's sessions.
|
||||||
|
"""
|
||||||
general = [o for o in self.data["offerings"] if o["course"]["is_general"]]
|
general = [o for o in self.data["offerings"] if o["course"]["is_general"]]
|
||||||
|
preferred_order = list(preferred_days) + [d for d in DAYS if d not in preferred_days]
|
||||||
|
phase_start = len(self.sessions)
|
||||||
|
|
||||||
for offering in general:
|
for offering in general:
|
||||||
course = offering["course"]
|
course = offering["course"]
|
||||||
room_candidates = self._room_candidates(course["lecture_room_id"], self.classrooms)
|
room_candidates = self._room_candidates(course["lecture_room_id"], self.classrooms)
|
||||||
|
|
||||||
for _ in range(course["lectures_per_week"]):
|
for _ in range(course["lectures_per_week"]):
|
||||||
placed = False
|
session = self._place_theory_session(offering, room_candidates, preferred_order)
|
||||||
# Tuesday first, then spill to the rest of the week if a
|
if session is None:
|
||||||
# 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']}")
|
print(f"WARNING: could not place a lecture for general course {offering['course_id']} section {offering['section_id']}")
|
||||||
|
|
||||||
def schedule_lab_courses(self):
|
phase_sessions = self.sessions[phase_start:]
|
||||||
|
self._repair_general_conflicts(phase_sessions, preferred_order, max_repair_attempts)
|
||||||
|
return phase_sessions
|
||||||
|
|
||||||
|
def _repair_general_conflicts(self, phase_sessions, day_order, max_attempts):
|
||||||
|
"""Moves general-course sessions that clash for a batch to a free slot on any day."""
|
||||||
|
from conflict_checker.student_conflict_checker import check_student_conflicts
|
||||||
|
|
||||||
|
any_day_order = list(DAYS)
|
||||||
|
for _ in range(max_attempts):
|
||||||
|
conflicts = check_student_conflicts(phase_sessions)
|
||||||
|
if not conflicts:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Re-place the last session involved in the first conflict; the
|
||||||
|
# other session(s) in that cell stay put.
|
||||||
|
losing_session = conflicts[0]["sessions"][-1]
|
||||||
|
offering = next(
|
||||||
|
o for o in self.data["offerings"]
|
||||||
|
if o["course_id"] == losing_session["course_id"] and o["section_id"] == losing_session["section_id"]
|
||||||
|
)
|
||||||
|
self._unmark_busy(
|
||||||
|
losing_session["day"], losing_session["slot_indices"][0],
|
||||||
|
losing_session["room_id"], losing_session["instructor_code"], losing_session["batch_codes"],
|
||||||
|
)
|
||||||
|
self.sessions.remove(losing_session)
|
||||||
|
phase_sessions.remove(losing_session)
|
||||||
|
|
||||||
|
room_candidates = self._room_candidates(offering["course"]["lecture_room_id"], self.classrooms)
|
||||||
|
new_session = self._place_theory_session(offering, room_candidates, any_day_order)
|
||||||
|
if new_session is None:
|
||||||
|
print(f"WARNING: could not repair general-course conflict for {offering['course_id']} section {offering['section_id']}")
|
||||||
|
return
|
||||||
|
phase_sessions.append(new_session)
|
||||||
|
|
||||||
|
print("WARNING: general-course conflicts remained unresolved after max repair attempts")
|
||||||
|
|
||||||
|
def phase_2_labs(self):
|
||||||
|
"""
|
||||||
|
Places every lab offering's weekly labs into lab rooms, treating
|
||||||
|
Phase 1's placements (already marked busy) as fixed. Returns this
|
||||||
|
phase's sessions.
|
||||||
|
"""
|
||||||
|
phase_start = len(self.sessions)
|
||||||
offerings = [o for o in self.data["offerings"] if not o["course"]["is_general"] and o["course"]["labs_per_week"] > 0]
|
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:
|
for offering in offerings:
|
||||||
course = offering["course"]
|
course = offering["course"]
|
||||||
@@ -209,33 +272,37 @@ class Scheduler:
|
|||||||
if not placed:
|
if not placed:
|
||||||
print(f"WARNING: could not place a lab for course {offering['course_id']} section {offering['section_id']} ({offering['batch_codes']})")
|
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):
|
return self.sessions[phase_start:]
|
||||||
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"]):
|
def phase_3_remaining_courses(self):
|
||||||
placed = False
|
"""
|
||||||
for day in DAYS:
|
Places every remaining (non-general) theory offering's weekly
|
||||||
for slot_index in TEACHING_SLOT_INDICES:
|
lectures, batch by batch in sorted batch_code order: all of one
|
||||||
for room in room_candidates:
|
batch's offerings are placed and locked before the next batch is
|
||||||
if self._is_free(day, slot_index, room["room_id"], offering["instructor_code"], offering["batch_codes"]):
|
considered. Treats Phases 1+2 (already marked busy) as fixed.
|
||||||
self._mark_busy(day, slot_index, room["room_id"], offering["instructor_code"], offering["batch_codes"])
|
Returns this phase's sessions.
|
||||||
self._add_session(offering, day, [slot_index], room)
|
"""
|
||||||
placed = True
|
phase_start = len(self.sessions)
|
||||||
break
|
offerings = [o for o in self.data["offerings"] if not o["course"]["is_general"]]
|
||||||
if placed:
|
|
||||||
break
|
for batch in sorted(self.data["batches"], key=lambda b: b["batch_code"]):
|
||||||
if placed:
|
batch_code = batch["batch_code"]
|
||||||
break
|
batch_offerings = [o for o in offerings if batch_code in o["batch_codes"]]
|
||||||
if not placed:
|
|
||||||
print(f"WARNING: could not place a lecture for course {offering['course_id']} section {offering['section_id']} ({offering['batch_codes']})")
|
for offering in batch_offerings:
|
||||||
|
course = offering["course"]
|
||||||
|
room_candidates = self._room_candidates(course["lecture_room_id"], self.classrooms)
|
||||||
|
for _ in range(course["lectures_per_week"]):
|
||||||
|
session = self._place_theory_session(offering, room_candidates, DAYS)
|
||||||
|
if session is None:
|
||||||
|
print(f"WARNING: could not place a lecture for course {offering['course_id']} section {offering['section_id']} ({offering['batch_codes']})")
|
||||||
|
|
||||||
|
return self.sessions[phase_start:]
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.schedule_general_courses()
|
self.phase_1_general_courses()
|
||||||
self.schedule_lab_courses()
|
self.phase_2_labs()
|
||||||
self.schedule_theory_courses()
|
self.phase_3_remaining_courses()
|
||||||
return self.sessions
|
return self.sessions
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
Timetable generation log - started 2026-09-09T17:48:16.013663
|
Timetable generation log - started 2026-09-09T17:59:37.068314
|
||||||
================================================================================
|
================================================================================
|
||||||
[17:48:16] INFO: Running conflict checks (student/batch and teacher)...
|
[17:59:37] INFO: Loading data...
|
||||||
[17:48:16] INFO: No conflicts found — schedule is clash-free for students and teachers.
|
[17:59:37] INFO: Phase 1/3: scheduling general courses...
|
||||||
|
[17:59:37] INFO: [Phase 1 (general courses)] No conflicts found — clash-free.
|
||||||
|
[17:59:37] INFO: Phase 2/3: scheduling labs...
|
||||||
|
[17:59:37] INFO: [Phase 2 (general courses + labs)] No conflicts found — clash-free.
|
||||||
|
[17:59:37] INFO: Phase 3/3: scheduling remaining courses (batch by batch)...
|
||||||
|
[17:59:37] INFO: Scheduling complete: 6 general, 10 lab, 20 remaining = 36 total session(s).
|
||||||
|
[17:59:37] INFO: Running final conflict check (student/batch and teacher) over the full schedule...
|
||||||
|
[17:59:37] INFO: [Final (full schedule)] No conflicts found — clash-free.
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user