commands
This commit is contained in:
@@ -1,12 +1,20 @@
|
|||||||
"""
|
"""
|
||||||
Main entry point: builds a clash-free dummy schedule and renders all five
|
Main entry point: runs the three scheduling phases (general courses, labs,
|
||||||
timetable views (batch/department/room/student/teacher -wise) as .xlsx
|
remaining courses) and renders all five timetable views (batch/department/
|
||||||
files under result/templates/<view>/.
|
room/student/teacher -wise) as .xlsx files under result/templates/<view>/.
|
||||||
|
|
||||||
Run:
|
Each phase is its own subcommand so it can be run and inspected as a
|
||||||
python app/template_generator.py
|
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 os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
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
|
from teacher_wise_template import generate_teacher_wise_templates # noqa: E402
|
||||||
|
|
||||||
RESULT_DIR = os.path.join(ROOT_DIR, "result", "templates")
|
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"]
|
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):
|
def _check_and_log(sessions, label):
|
||||||
"""Runs student + teacher conflict checks over `sessions` and logs the result."""
|
"""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)
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def ensure_dummy_data():
|
||||||
reset_log()
|
|
||||||
clear_result_dir()
|
|
||||||
|
|
||||||
data_csv_marker = os.path.join(ROOT_DIR, "data", "dummy", "blocks", "rooms.csv")
|
data_csv_marker = os.path.join(ROOT_DIR, "data", "dummy", "blocks", "rooms.csv")
|
||||||
if not os.path.exists(data_csv_marker):
|
if not os.path.exists(data_csv_marker):
|
||||||
print("Dummy data not found — generating it first...")
|
print("Dummy data not found — generating it first...")
|
||||||
@@ -76,49 +116,101 @@ def main():
|
|||||||
generate_dummy_data()
|
generate_dummy_data()
|
||||||
print()
|
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
|
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)
|
output_dir = os.path.join(RESULT_DIR, view_name)
|
||||||
written = generate_fn(sessions, data, output_dir)
|
written = generate_fn(sessions, data, output_dir)
|
||||||
total_files += len(written)
|
total_files += len(written)
|
||||||
print(f"[{view_name}] generated {len(written)} file(s) -> {output_dir}")
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -40,8 +40,19 @@ def log_info(message):
|
|||||||
|
|
||||||
|
|
||||||
def _write(level, message):
|
def _write(level, message):
|
||||||
|
global _log_path
|
||||||
if _log_path is None:
|
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}"
|
line = f"[{datetime.datetime.now().strftime('%H:%M:%S')}] {level}: {message}"
|
||||||
with open(_log_path, "a", encoding="utf-8") as f:
|
with open(_log_path, "a", encoding="utf-8") as f:
|
||||||
f.write(line + "\n")
|
f.write(line + "\n")
|
||||||
|
|||||||
@@ -121,6 +121,39 @@ class Scheduler:
|
|||||||
|
|
||||||
self.sessions = []
|
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):
|
def _is_free(self, day, slot_index, room_id, instructor_code, batch_codes):
|
||||||
key = (day, slot_index)
|
key = (day, slot_index)
|
||||||
if room_id in self.room_busy.get(key, set()):
|
if room_id in self.room_busy.get(key, set()):
|
||||||
|
|||||||
@@ -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:06:49] INFO: Phase 3/3: scheduling remaining courses (batch by batch)...
|
||||||
[18:02:42] INFO: Phase 1/3: scheduling general courses...
|
[18:06:49] INFO: [Phase 3 (full schedule)] No conflicts found — clash-free.
|
||||||
[18:02:42] INFO: [Phase 1 (general courses)] 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.
|
||||||
[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.
|
|
||||||
|
|||||||
1027
result/schedule_state.json
Normal file
1027
result/schedule_state.json
Normal file
File diff suppressed because it is too large
Load Diff
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