This commit is contained in:
Talhadeveloperr
2026-09-09 18:07:19 +05:00
parent 3be641a36b
commit 1eed747639
53 changed files with 1213 additions and 56 deletions

View File

@@ -1,12 +1,20 @@
"""
Main entry point: builds a clash-free dummy schedule and renders all five
timetable views (batch/department/room/student/teacher -wise) as .xlsx
files under result/templates/<view>/.
Main entry point: runs the three scheduling phases (general courses, labs,
remaining courses) and renders all five timetable views (batch/department/
room/student/teacher -wise) as .xlsx files under result/templates/<view>/.
Run:
python app/template_generator.py
Each phase is its own subcommand so it can be run and inspected as a
separate step; each phase's placements are persisted to
result/schedule_state.json and loaded back in by the next phase's command.
Usage:
python app/template_generator.py phase_1 # general courses (Tue/Thu preferred)
python app/template_generator.py phase_2 # labs, on top of phase_1
python app/template_generator.py phase_3 # remaining courses, batch by batch
python app/template_generator.py clear_all # wipe state/results/log to start over
"""
import json
import os
import shutil
import sys
@@ -35,8 +43,43 @@ from student_wise_template import generate_student_wise_templates # noqa: E402
from teacher_wise_template import generate_teacher_wise_templates # noqa: E402
RESULT_DIR = os.path.join(ROOT_DIR, "result", "templates")
STATE_FILE = os.path.join(ROOT_DIR, "result", "schedule_state.json")
VIEW_NAMES = ["batch-wise", "department-wise", "room-wise", "student-wise", "teacher-wise"]
PHASE_ORDER = ["phase_1", "phase_2", "phase_3"]
GENERATORS = [
("batch-wise", generate_batch_wise_templates),
("department-wise", generate_department_wise_templates),
("room-wise", generate_room_wise_templates),
("student-wise", generate_student_wise_templates),
("teacher-wise", generate_teacher_wise_templates),
]
# ---------------------------------------------------------------------------
# State persistence
# ---------------------------------------------------------------------------
def load_state():
"""Returns the saved schedule state dict, or None if none exists yet."""
if not os.path.exists(STATE_FILE):
return None
with open(STATE_FILE, encoding="utf-8") as f:
return json.load(f)
def save_state(scheduler, phases_completed):
state = scheduler.to_state()
state["phases_completed"] = phases_completed
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
with open(STATE_FILE, "w", encoding="utf-8") as f:
json.dump(state, f, indent=2)
# ---------------------------------------------------------------------------
# Shared helpers
# ---------------------------------------------------------------------------
def _check_and_log(sessions, label):
"""Runs student + teacher conflict checks over `sessions` and logs the result."""
@@ -64,10 +107,7 @@ def clear_result_dir():
os.makedirs(output_dir, exist_ok=True)
def main():
reset_log()
clear_result_dir()
def ensure_dummy_data():
data_csv_marker = os.path.join(ROOT_DIR, "data", "dummy", "blocks", "rooms.csv")
if not os.path.exists(data_csv_marker):
print("Dummy data not found — generating it first...")
@@ -76,49 +116,101 @@ def main():
generate_dummy_data()
print()
log_info("Loading data...")
data = load_data()
scheduler = Scheduler(data)
# --- Phase 1: general courses (Tue/Thu preferred), self-repaired until clash-free ---
log_info("Phase 1/3: scheduling general courses...")
phase1_sessions = scheduler.phase_1_general_courses()
_check_and_log(phase1_sessions, "Phase 1 (general courses)")
# --- Phase 2: labs, scheduled against Phase 1's fixed placements ---
log_info("Phase 2/3: scheduling labs...")
phase2_sessions = scheduler.phase_2_labs()
_check_and_log(phase1_sessions + phase2_sessions, "Phase 2 (general courses + labs)")
# --- Phase 3: remaining theory courses, batch by batch ---
log_info("Phase 3/3: scheduling remaining courses (batch by batch)...")
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 = [
("batch-wise", generate_batch_wise_templates),
("department-wise", generate_department_wise_templates),
("room-wise", generate_room_wise_templates),
("student-wise", generate_student_wise_templates),
("teacher-wise", generate_teacher_wise_templates),
]
def regenerate_templates(sessions, data):
clear_result_dir()
total_files = 0
for view_name, generate_fn in generators:
for view_name, generate_fn in GENERATORS:
output_dir = os.path.join(RESULT_DIR, view_name)
written = generate_fn(sessions, data, output_dir)
total_files += len(written)
print(f"[{view_name}] generated {len(written)} file(s) -> {output_dir}")
print(f"\n{total_files} timetable file(s) written under {RESULT_DIR}")
print(f"\nDone. {total_files} timetable file(s) written under {RESULT_DIR}")
# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------
def cmd_clear_all():
if os.path.exists(STATE_FILE):
os.remove(STATE_FILE)
clear_result_dir()
reset_log()
log_info("clear_all: schedule state, result templates, and log have been reset. Run phase_1 to start over.")
def cmd_phase(phase_name):
ensure_dummy_data()
data = load_data()
state = load_state()
completed = state["phases_completed"] if state else []
if phase_name in completed:
log_error(
f"{phase_name} has already been run (completed phases: {completed}). "
f"Run 'python app/template_generator.py clear_all' first if you want to redo it."
)
return
phase_index = PHASE_ORDER.index(phase_name)
required_prior = PHASE_ORDER[:phase_index]
missing = [p for p in required_prior if p not in completed]
if missing:
log_error(
f"Cannot run {phase_name} yet — missing prior phase(s) {missing}. "
f"Run them in order: {', '.join(PHASE_ORDER)} (or 'clear_all' to start over)."
)
return
if state is None:
scheduler = Scheduler(data)
else:
scheduler = Scheduler.from_state(data, state)
if phase_name == "phase_1":
log_info("Phase 1/3: scheduling general courses (Tue/Thu preferred)...")
new_sessions = scheduler.phase_1_general_courses()
elif phase_name == "phase_2":
log_info("Phase 2/3: scheduling labs (on top of phase_1's fixed placements)...")
new_sessions = scheduler.phase_2_labs()
else:
log_info("Phase 3/3: scheduling remaining courses (batch by batch)...")
new_sessions = scheduler.phase_3_remaining_courses()
label = {
"phase_1": "Phase 1 (general courses)",
"phase_2": "Phase 2 (general courses + labs)",
"phase_3": "Phase 3 (full schedule)",
}[phase_name]
_check_and_log(scheduler.sessions, label)
completed = completed + [phase_name]
save_state(scheduler, completed)
log_info(
f"{phase_name} complete: {len(new_sessions)} session(s) added this phase, "
f"{len(scheduler.sessions)} total so far. State saved to {STATE_FILE}."
)
regenerate_templates(scheduler.sessions, data)
def print_usage():
print(__doc__)
def main():
args = sys.argv[1:]
if not args or args[0] not in ("phase_1", "phase_2", "phase_3", "clear_all"):
print_usage()
return
command = args[0]
if command == "clear_all":
cmd_clear_all()
else:
cmd_phase(command)
if __name__ == "__main__":

View File

@@ -40,8 +40,19 @@ def log_info(message):
def _write(level, message):
global _log_path
if _log_path is None:
reset_log()
os.makedirs(LOGS_DIR, exist_ok=True)
_log_path = LOG_FILE
if not os.path.exists(_log_path):
# First-ever write with no prior reset_log() call in this
# process (e.g. a phase_N command run without a preceding
# clear_all): create the file fresh, but never truncate an
# existing log — separate process invocations of phase_1/2/3
# must all append to the same ongoing log.
with open(_log_path, "w", encoding="utf-8") as f:
f.write(f"Timetable generation log - started {datetime.datetime.now().isoformat()}\n")
f.write("=" * 80 + "\n")
line = f"[{datetime.datetime.now().strftime('%H:%M:%S')}] {level}: {message}"
with open(_log_path, "a", encoding="utf-8") as f:
f.write(line + "\n")

View File

@@ -121,6 +121,39 @@ class Scheduler:
self.sessions = []
def to_state(self):
"""
Serializes this scheduler's accumulated sessions and busy-maps to a
JSON-able dict, so a later process can resume exactly where this
one left off (used to run each phase as a separate CLI command).
"""
def _dump_busy(busy_map):
return {f"{day}|{slot_index}": sorted(values) for (day, slot_index), values in busy_map.items()}
return {
"sessions": self.sessions,
"room_busy": _dump_busy(self.room_busy),
"instructor_busy": _dump_busy(self.instructor_busy),
"batch_busy": _dump_busy(self.batch_busy),
}
@classmethod
def from_state(cls, data, state):
"""Rebuilds a Scheduler from a dict previously produced by to_state()."""
def _load_busy(dumped):
busy = {}
for key, values in dumped.items():
day, slot_index = key.rsplit("|", 1)
busy[(day, int(slot_index))] = set(values)
return busy
scheduler = cls(data)
scheduler.sessions = list(state["sessions"])
scheduler.room_busy = _load_busy(state["room_busy"])
scheduler.instructor_busy = _load_busy(state["instructor_busy"])
scheduler.batch_busy = _load_busy(state["batch_busy"])
return scheduler
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()):

View File

@@ -1,11 +1,5 @@
Timetable generation log - started 2026-09-09T18:02:42.384468
Timetable generation log - started 2026-09-09T18:06:49.239332
================================================================================
[18:02:42] INFO: Loading data...
[18:02:42] INFO: Phase 1/3: scheduling general courses...
[18:02:42] INFO: [Phase 1 (general courses)] No conflicts found — clash-free.
[18:02:42] INFO: Phase 2/3: scheduling labs...
[18:02:42] INFO: [Phase 2 (general courses + labs)] No conflicts found — clash-free.
[18:02:42] INFO: Phase 3/3: scheduling remaining courses (batch by batch)...
[18:02:42] INFO: Scheduling complete: 6 general, 10 lab, 20 remaining = 36 total session(s).
[18:02:42] INFO: Running final conflict check (student/batch and teacher) over the full schedule...
[18:02:42] INFO: [Final (full schedule)] No conflicts found — clash-free.
[18:06:49] INFO: Phase 3/3: scheduling remaining courses (batch by batch)...
[18:06:49] INFO: [Phase 3 (full schedule)] No conflicts found — clash-free.
[18:06:49] INFO: phase_3 complete: 20 session(s) added this phase, 36 total so far. State saved to D:\projects\UORM\timetable\result\schedule_state.json.

1027
result/schedule_state.json Normal file

File diff suppressed because it is too large Load Diff