| import os |
| import pandas as pd |
| import numpy as np |
| import gradio as gr |
| from collections import defaultdict |
| from openpyxl import Workbook |
| from openpyxl.styles import PatternFill |
| from openpyxl.styles import PatternFill, Alignment |
| import shutil |
| import zipfile |
|
|
| |
| days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"] |
| time_slots = ["8-9", "9-10", "10-11", "11-12", "12-1", "1-2", "2-3", "3-4", "4-5", "5-6", "6-7"] |
| lunch_slot = "1-2" |
| no_class_3rd_4th = {"Tuesday": ["4-5"], "Friday": ["5-6"], "Monday":["6-7"],"Thursday":["6-7"]} |
| seminar_hall_blocked_slots = ["3-4", "4-5", "5-6"] |
|
|
| block_3rd = {"Monday", "Wednesday", "Friday"} |
| block_3rd_slots = ["2-3", "3-4", "4-5"] |
| block_4th = {"Monday", "Wednesday", "Thursday"} |
| block_4th_slots = ["10-11", "11-12", "12-1"] |
|
|
| |
| |
| |
| |
|
|
| bcp_prefixes = ["phy", "msp", "chy", "msc", "bio", "msb", "i2c", "i2p", "i2b"] |
|
|
| def ordinal(n): |
| mapping = ["First", "Second", "Third", "Fourth", "Fifth"] |
| return mapping[n - 1] if 1 <= n <= 5 else f"Year{n or 'Unknown'}" |
|
|
| def extract_year_from_code(code): |
| code = code.lower() |
| numeric = ''.join(filter(str.isdigit, code)) |
| if not numeric: |
| return None |
| first_digit = int(numeric[0]) |
|
|
| |
| if code.startswith(("msc", "msm", "msp", "msb")): |
| if first_digit == 3: |
| return 3 |
| elif first_digit == 4: |
| return 4 |
|
|
| |
| if 1 <= first_digit <= 5: |
| return first_digit |
| return None |
|
|
| def is_bcp_major(code): |
| return any(code.startswith(prefix) for prefix in bcp_prefixes) |
|
|
| def is_bcp_blocked(code, day, slot): |
| if not is_bcp_major(code): |
| return False |
| year = extract_year_from_code(code) |
| if year == 3 and day in block_3rd and slot in block_3rd_slots: |
| return True |
| if year == 4 and day in block_4th and slot in block_4th_slots: |
| return True |
| return False |
|
|
| def parse_ltpc(ltpc): |
| ltpc = str(ltpc).strip() |
| if '-' in ltpc: |
| parts = list(map(int, ltpc.split('-'))) |
| while len(parts) < 4: |
| parts.append(0) |
| return tuple(parts[:4]) |
| digits = ''.join(filter(str.isdigit, ltpc)).zfill(4) |
| return int(digits[0]), int(digits[1]), int(digits[2]), int(digits[3]) |
|
|
| def find_valid_lecture_days(L, days_available, year=None): |
| if L == 1: |
| return [[d] for d in days_available] |
|
|
| elif L == 2: |
| return [[d1, d2] for d1 in days_available for d2 in days_available if abs(d1 - d2) >= 2 and d1 < d2] |
|
|
| elif L == 3: |
| options = [] |
| for d1 in days_available: |
| for d2 in days_available: |
| for d3 in days_available: |
| if d1 < d2 < d3: |
| |
| if (d2 - d1 == 1 and d3 - d2 == 2) or (d2 - d1 == 2 and d3 - d2 == 1): |
| options.append([d1, d2, d3]) |
| return options |
|
|
| return [] |
|
|
|
|
| def generate_central_timetable_excel(timetable, tutorial_groups=None, lab_groups=None, output_path="Central_Timetable.xlsx"): |
| from openpyxl import Workbook |
| from openpyxl.styles import Alignment |
|
|
| wb = Workbook() |
| ws = wb.active |
| ws.title = "Central Timetable" |
|
|
| |
| ws.append(["Day"] + time_slots) |
|
|
| for day in days: |
| row = [day] |
| for slot in time_slots: |
| entries = [] |
|
|
| |
| for year in sorted(timetable.keys()): |
| scheduled = timetable[year][day][slot] |
| entries.extend(scheduled) |
|
|
| |
| if tutorial_groups: |
| for lg, sessions in tutorial_groups.items(): |
| for d, s, r, cname in sessions: |
| if d == day and s == slot: |
| entries.append(f"{cname} (Tutorial) @ {r} ({lg})") |
|
|
| |
| if lab_groups: |
| for lg, sessions in lab_groups.items(): |
| for d, slot_block, r, cname in sessions: |
| if d == day and slot in slot_block: |
| entries.append(f"{cname} (Lab) @ {r} ({lg})") |
|
|
| row.append("\n".join(entries)) |
| ws.append(row) |
|
|
| |
| for col in ws.columns: |
| for cell in col: |
| cell.alignment = Alignment(wrapText=True) |
|
|
| |
| for i in range(2, len(time_slots) + 2): |
| ws.column_dimensions[chr(64 + i)].width = 30 |
|
|
| wb.save(output_path) |
|
|
|
|
|
|
| def generate_available_halls_report(course_df, room_slots, output_path="Available_Halls.xlsx"): |
| from openpyxl import Workbook |
|
|
| wb = Workbook() |
| ws = wb.active |
| ws.title = "Available Halls" |
|
|
| ws.append(["Day"] + time_slots) |
|
|
| for day in days: |
| row_data = [day] |
| for slot in time_slots: |
| available_rooms = [] |
| for room in room_slots: |
| key = f"{day}-{slot}" |
| if room_slots[room][key]: |
| available_rooms.append(room) |
| row_data.append(", ".join(sorted(available_rooms))) |
| ws.append(row_data) |
|
|
| wb.save(output_path) |
|
|
| |
| if course_df is not None: |
| ws2 = wb.create_sheet("Unused Large Rooms") |
| ws2.append(["Day", "Slot", "Room", "Capacity", "Reason"]) |
|
|
| |
| room_caps = {} |
| for _, row in course_df.iterrows(): |
| try: |
| students = int(row["Total Students"]) |
| except: |
| continue |
|
|
|
|
|
|
| return |
|
|
|
|
| |
| def schedule_bs_ms_first_year( |
| course_df, room_df, room_slots, time_slots, days, output_dir, |
| first_year_lectures_by_day_slot=None, |
| timetable=None |
| ): |
| from openpyxl import Workbook |
| from openpyxl.styles import Alignment |
| from openpyxl.utils import get_column_letter |
| from collections import defaultdict |
| import os |
| import pandas as pd |
|
|
| TUTORIAL_ROOMS = ["LHC 105", "LHC 106", "LHC 107", "LHC 108"] |
| LAB_SLOTS = ["2-3", "3-4", "4-5"] |
| LECTURE_SLOTS = ["8-9", "9-10", "10-11"] |
| LUNCH_SLOT = "1-2" |
| FORBIDDEN_5_6_DAYS = {"Monday", "Wednesday", "Friday"} |
|
|
| scheduled_labs_by_day = defaultdict(set) |
| tg_to_lg = { |
| "TG-1": "LG-1", "TG-2": "LG-1", |
| "TG-3": "LG-2", "TG-4": "LG-2", |
| "TG-5": "LG-3", "TG-6": "LG-3", |
| "TG-7": "LG-4", "TG-8": "LG-4", |
| "TG-9": "LG-5", "TG-10": "LG-5", |
| } |
|
|
| lab_rooms = [] |
| for _, row in room_df.iterrows(): |
| for col in ["Computer Lab", "Class Room", "Seminar Halls"]: |
| room = row.get(col) |
| if pd.notna(room) and ("lab" in str(room).lower() or "computer" in str(room).lower()): |
| lab_rooms.append(str(room).strip()) |
|
|
| lab_groups = {f"LG-{i+1}": [] for i in range(5)} |
| tutorial_groups = {f"LG-{i+1}": [] for i in range(5)} |
| tutorial_slot_count = defaultdict(lambda: defaultdict(int)) |
| used_slots_by_lg = defaultdict(set) |
| unscheduled = [] |
|
|
| |
| |
| lab_courses_first_year = [ |
| row for _, row in course_df.iterrows() |
| if parse_ltpc(row["[LTPC]"])[2] > 0 and |
| max([extract_year_from_code(c) for c in row["Course Name"].split("-") if extract_year_from_code(c)]) == 1 |
| ] |
|
|
| |
| lab_courses_first_year = lab_courses_first_year[:4] |
|
|
| |
| lab_days = days[:5] |
| num_labs = len(lab_courses_first_year) |
|
|
| for lg_index in range(5): |
| lg = f"LG-{lg_index+1}" |
| for i in range(num_labs): |
| course_row = lab_courses_first_year[i] |
| cname = course_row["Course Name"] |
| subfolder = course_row["Sub folder"] |
| ltpc = course_row["[LTPC]"] |
|
|
| |
| day = lab_days[(i + lg_index) % len(lab_days)] |
|
|
| |
| if cname in scheduled_labs_by_day[day]: |
| unscheduled.append({ |
| "Course Name": cname, |
| "LTPC": ltpc, |
| "Sub folder": subfolder, |
| "Reason": f"{cname} already scheduled on {day} for another LG" |
| }) |
| continue |
|
|
| |
| available = all(f"{day}-{slot}" not in used_slots_by_lg[lg] for slot in LAB_SLOTS) |
| if not available: |
| unscheduled.append({ |
| "Course Name": cname, |
| "LTPC": ltpc, |
| "Sub folder": subfolder, |
| "Reason": f"Slots not free for {lg} on {day}" |
| }) |
| continue |
|
|
| |
| if "IDC112" in cname.upper(): |
| room_found = False |
| for room in lab_rooms: |
| if all(room_slots[room][f"{day}-{slot}"] for slot in LAB_SLOTS): |
| for slot in LAB_SLOTS: |
| room_slots[room][f"{day}-{slot}"] = False |
| used_slots_by_lg[lg].add(f"{day}-{slot}") |
| if timetable: |
| timetable[1][day][slot].append(f"{cname} (Lab) @ {room} ({lg})") |
| lab_groups[lg].append((day, LAB_SLOTS, room, cname)) |
| scheduled_labs_by_day[day].add(cname) |
| room_found = True |
| break |
| if not room_found: |
| unscheduled.append({ |
| "Course Name": cname, |
| "LTPC": ltpc, |
| "Sub folder": subfolder, |
| "Reason": f"No room available for {cname} on {day} for {lg}" |
| }) |
| else: |
| for slot in LAB_SLOTS: |
| used_slots_by_lg[lg].add(f"{day}-{slot}") |
| if timetable: |
| timetable[1][day][slot].append(f"{cname} (Lab) ({lg})") |
| lab_groups[lg].append((day, LAB_SLOTS, None, cname)) |
| scheduled_labs_by_day[day].add(cname) |
|
|
| |
| for _, row in course_df.iterrows(): |
| cname = row["Course Name"] |
| subfolder = row["Sub folder"] |
| ltpc = row["[LTPC]"] |
| students = int(row["Total Students"]) |
| L, T, P, C = parse_ltpc(ltpc) |
|
|
| codes = cname.split("-") |
| years = [extract_year_from_code(c) for c in codes if extract_year_from_code(c) is not None] |
| if not years or max(years) != 1: |
| continue |
|
|
| if T == 1: |
| for lg_index in range(5): |
| lg = f"LG-{lg_index+1}" |
| assigned = 0 |
| used_tut_slots = set(slot for d, slot in [ |
| (d, s) for d, s, _, _ in tutorial_groups[lg] |
| ]) |
|
|
| |
| lab_blocked_slots = set() |
| for d, slot_list, _, _ in lab_groups[lg]: |
| for s in slot_list: |
| lab_blocked_slots.add(f"{d}-{s}") |
|
|
| for day in days: |
| for slot in time_slots: |
| slot_key = f"{day}-{slot}" |
|
|
| if slot == LUNCH_SLOT: |
| continue |
| |
| |
| if slot == "5-6" and day in FORBIDDEN_5_6_DAYS: |
| continue |
| if slot_key in lab_blocked_slots: |
| continue |
| if f"{day}-{slot}" in used_slots_by_lg[lg]: |
| continue |
| if tutorial_slot_count[day][slot] >= len(TUTORIAL_ROOMS): |
| continue |
| if first_year_lectures_by_day_slot: |
| if first_year_lectures_by_day_slot.get(day, {}).get(slot, []): |
| continue |
|
|
| available_rooms = [r for r in TUTORIAL_ROOMS if room_slots[r][slot_key]] |
| if len(available_rooms) < 2: |
| continue |
|
|
| |
| room1, room2 = available_rooms[0], available_rooms[1] |
| tg1 = f"TG-{2 * (lg_index) + 1}" |
| tg2 = f"TG-{2 * (lg_index) + 2}" |
|
|
| |
| room_slots[room1][slot_key] = False |
| room_slots[room2][slot_key] = False |
| tutorial_slot_count[day][slot] += 2 |
| used_slots_by_lg[lg].add(slot_key) |
|
|
| |
| tutorial_groups[lg].append((day, slot, room1, f"{cname}")) |
| tutorial_groups[lg].append((day, slot, room2, f"{cname}")) |
|
|
| if timetable: |
| timetable[1][day][slot].append(f"{cname} (Tutorial) @ {room1} ({tg1})") |
| timetable[1][day][slot].append(f"{cname} (Tutorial) @ {room2} ({tg2})") |
|
|
| assigned = 2 |
| break |
| if assigned == 2: |
| break |
|
|
| if assigned < 2: |
| tg1 = f"TG-{2 * (lg_index) + 1}" |
| tg2 = f"TG-{2 * (lg_index) + 2}" |
| unscheduled.append({ |
| "Course Name": cname, |
| "LTPC": ltpc, |
| "Sub folder": subfolder, |
| "Reason": f"Could not schedule tutorial for {tg1} and {tg2} (under {lg})" |
| }) |
|
|
|
|
| |
| year_folder = os.path.join(output_dir, "1st Year") |
| os.makedirs(year_folder, exist_ok=True) |
| first_year_wb = Workbook() |
| first_year_wb.remove(first_year_wb.active) |
|
|
| for lg_index in range(5): |
| lg = f"LG-{lg_index+1}" |
| tg1 = f"TG-{2 * lg_index + 1}" |
| tg2 = f"TG-{2 * lg_index + 2}" |
| sheet_title = f"{lg} ({tg1} & {tg2})" |
| ws = first_year_wb.create_sheet(title=sheet_title) |
| ws.append(["Day"] + time_slots) |
|
|
| for day in days: |
| row = [day] |
| for slot_index, slot in enumerate(time_slots): |
| val = "" |
|
|
| |
| tg1_tuts = [f"{c} (Tutorial) @ {r}" for d, s, r, c in tutorial_groups[lg] if d == day and s == slot] |
| tg2_tuts = [] |
|
|
| |
| for d, s, r, c in tutorial_groups[lg]: |
| if d == day and s == slot: |
|
|
| if f"({tg1})" in timetable[1][day][slot]: |
| tg1_tuts = [entry for entry in timetable[1][day][slot] if f"({tg1})" in entry] |
| if f"({tg2})" in timetable[1][day][slot]: |
| tg2_tuts = [entry for entry in timetable[1][day][slot] if f"({tg2})" in entry] |
|
|
|
|
| for t in tg1_tuts: |
| val += f"{t}\n" |
| for t in tg2_tuts: |
| val += f"{t}\n" |
|
|
|
|
| |
| for d, slot_block, r, c in lab_groups[lg]: |
| if d == day and slot in slot_block: |
| if r: |
| val += f"{c} (Lab) @ {r}\n" |
| else: |
| val += f"{c} (Lab)\n" |
|
|
|
|
| |
| if first_year_lectures_by_day_slot: |
| for entry in first_year_lectures_by_day_slot.get(day, {}).get(slot, []): |
| val += f"{entry} (Lecture)\n" |
|
|
|
|
| row.append(val.strip()) |
| ws.append(row) |
|
|
| for col in range(2, len(time_slots) + 2): |
| ws.column_dimensions[get_column_letter(col)].width = 31 |
| for row_cells in ws.iter_rows(min_row=2, min_col=2): |
| for cell in row_cells: |
| cell.alignment = Alignment(wrap_text=True) |
|
|
|
|
| |
| filename = os.path.join(year_folder, "1st Year Timetable.xlsx") |
| first_year_wb.save(filename) |
|
|
|
|
| return tutorial_groups, lab_groups, unscheduled |
|
|
|
|
|
|
|
|
|
|
| def schedule_bsms_2nd_year_tutorials(second_year_tutorial_courses, room_data, room_slots, timetable): |
| """ |
| Schedule BS-MS 2nd Year Tutorials for TG-1/2 on Monday, TG-3/4 on Friday. |
| Each slot (10-11, 11-12, 12-1) handles 2 courses Γ 2 groups = 4 rooms. |
| """ |
| bsms_2nd_year_tutorial_slots = { |
| "Monday": ["10-11", "11-12", "12-1"], |
| "Friday": ["10-11", "11-12", "12-1"] |
| } |
| tg_map = { |
| "Monday": ["TG-1", "TG-2"], |
| "Friday": ["TG-3", "TG-4"] |
| } |
|
|
| for day in ["Monday", "Friday"]: |
| course_idx = 0 |
| for slot in bsms_2nd_year_tutorial_slots[day]: |
| for _ in range(2): |
| if course_idx >= len(second_year_tutorial_courses): |
| break |
| cname = second_year_tutorial_courses[course_idx] |
| for tg in tg_map[day]: |
| assigned = False |
| for room, cap, hall_type in sorted(room_data, key=lambda x: abs(x[1] - 48)): |
| if cap >= 48 and room_slots[room][f"{day}-{slot}"]: |
| timetable[2][day][slot].append(f"{cname} {tg} Tutorial @ {room}") |
| room_slots[room][f"{day}-{slot}"] = False |
| assigned = True |
| break |
| if not assigned: |
| print(f"β Could not assign {cname} {tg} on {day} {slot}") |
| course_idx += 1 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| def is_slot_allowed_for_year(year, day, slot): |
| if slot == lunch_slot: |
| return False |
| if year == 1: |
| |
| if slot not in ["9-10", "10-11","11-12","12-1","5-6"]: |
| return False |
| |
| |
| if year == 2: |
| |
|
|
| if day == "Wednesday" and slot in ["10-11", "11-12", "12-1"]: |
| return False |
| if (day == "Tuesday" and slot == "4-5") or (day == "Friday" and slot == "5-6"): |
| return False |
| |
| if year == 2 and day in ["Monday", "Friday"] and slot in ["10-11", "11-12", "12-1"]: |
| return False |
| |
| |
| if year in [3, 4] and slot in no_class_3rd_4th.get(day, []): |
| return False |
| return True |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| def generate_unused_halls_report(room_slots, output_path="Unused_Halls_Report.xlsx"): |
| from openpyxl import Workbook |
| from openpyxl.styles import Alignment |
|
|
| wb = Workbook() |
| ws = wb.active |
| ws.title = "Unused Halls (Matrix Format)" |
|
|
| |
| ws.append(["Day"] + time_slots) |
|
|
| for day in days: |
| row = [day] |
| for slot in time_slots: |
| free_rooms = [] |
| for room in room_slots: |
| key = f"{day}-{slot}" |
| if room_slots[room][key]: |
| free_rooms.append(room) |
| row.append(", ".join(sorted(free_rooms))) |
| ws.append(row) |
|
|
| |
| for col in ws.columns: |
| for cell in col: |
| cell.alignment = Alignment(wrapText=True) |
|
|
| for i in range(2, len(time_slots) + 2): |
| ws.column_dimensions[chr(64 + i)].width = 25 |
|
|
| wb.save(output_path) |
|
|
|
|
|
|
|
|
| import random |
|
|
| def generate_timetable(course_df, room_df, output_dir, room_slots, timetable): |
| from openpyxl.styles import PatternFill |
| from collections import defaultdict |
| import os |
| import pandas as pd |
| from openpyxl.utils import get_column_letter |
|
|
| course_df["Course Name"] = course_df["Course Name"].astype(str) |
| course_df["Course Name"] = course_df["Course Name"].str.replace(r'^\s+|\s+$', '', regex=True) |
|
|
|
|
| course_colors = {} |
| course_color_palette = ["FFCCCC", "CCFFCC", "CCCCFF", "FFFFCC", "CCFFFF", "FFCCFF", "F0E68C", "E6E6FA"] |
| color_index = 0 |
| first_year_lectures_by_day_slot = defaultdict(lambda: defaultdict(list)) |
| unscheduled_courses = [] |
|
|
| room_data = [] |
| for _, row in room_df.iterrows(): |
| for hall_type, room_col, cap_col in [ |
| ("Class Room", "Class Room", "Class Capacity"), |
| ("Computer Lab", "Computer Lab", "Computer Capacity"), |
| ("Seminar Halls", "Seminar Halls", "Seminar Capacity") |
| ]: |
| room = row[room_col] |
| cap = row[cap_col] |
| if pd.notna(room) and pd.notna(cap): |
| room_data.append((str(room).strip(), int(cap), hall_type)) |
|
|
| grouped = course_df.groupby("Sub folder") |
|
|
| bsms_2nd_year_tutorial_slots = { |
| "Monday": ["10-11", "11-12", "12-1"], |
| "Friday": ["10-11", "11-12", "12-1"] |
| } |
| bsms_2nd_year_tutorial_schedule = { |
| "Monday": defaultdict(list), |
| "Friday": defaultdict(list) |
| } |
|
|
| |
| second_year_tutorial_courses = [] |
|
|
| for subfolder, group in grouped: |
| sorted_courses = group.sort_values(by="Total Students", ascending=False) |
|
|
| for _, row in sorted_courses.iterrows(): |
| cname = row["Course Name"].strip() |
| course_group = cname.split("-") |
| ltpc = row["[LTPC]"] |
| students = int(row["Total Students"]) |
|
|
| years_in_group = [extract_year_from_code(part) for part in course_group] |
| valid_years = list(filter(None, years_in_group)) |
|
|
| print(f"\n--- Scheduling {cname} (LTPC: {ltpc}, Students: {students}) ---") |
|
|
| if not valid_years: |
| if 'msc' in cname.lower(): |
| print(f"β οΈ Warning: M.Sc. course {cname} has no valid year. Defaulting to year 3 or 4 based on code.") |
| year_from_code = extract_year_from_code(cname) |
| if year_from_code: |
| valid_years = [year_from_code] |
| else: |
| unscheduled_courses.append({ |
| "Course Name": cname, |
| "LTPC": ltpc, |
| "Sub folder": subfolder, |
| "Reason": "Invalid or unsupported year in course code" |
| }) |
| continue |
| else: |
| unscheduled_courses.append({ |
| "Course Name": cname, |
| "LTPC": ltpc, |
| "Sub folder": subfolder, |
| "Reason": "Invalid or unsupported year in course code" |
| }) |
| continue |
|
|
| year = max(valid_years) |
| L, T, P, C = parse_ltpc(ltpc) |
|
|
| if cname not in course_colors: |
| course_colors[cname] = course_color_palette[color_index % len(course_color_palette)] |
| color_index += 1 |
|
|
| assigned_room = None |
| if L > 0: |
| for room, cap, hall_type in sorted(room_data, key=lambda x: abs(x[1] - students)): |
| if cap >= students and hall_type in ["Class Room", "Seminar Halls"]: |
| assigned_room = (room, hall_type) |
| break |
| else: |
| for room, cap, hall_type in sorted(room_data, key=lambda x: abs(x[1] - students)): |
| if cap >= students: |
| assigned_room = (room, hall_type) |
| break |
|
|
| if not assigned_room: |
| print("β Failed: No suitable room") |
| unscheduled_courses.append({ |
| "Course Name": cname, |
| "LTPC": ltpc, |
| "Sub folder": subfolder, |
| "Reason": "No suitable room" |
| }) |
| continue |
|
|
| room_name, hall_type = assigned_room |
| print(f"β
Assigned room: {room_name} ({hall_type})") |
|
|
| |
| |
| |
| if L > 0: |
| print("π Scheduling lectures...") |
|
|
| lectures_scheduled = 0 |
|
|
| |
| preferred_raw = row.get("Preferred Slots", "") |
| preferred_slots = [] |
| if pd.notna(preferred_raw) and isinstance(preferred_raw, str) and preferred_raw.strip(): |
| try: |
| preferred_slots = [ |
| (part.split()[0], part.split()[1]) |
| for part in [s.strip() for s in preferred_raw.split(",")] |
| if len(part.split()) == 2 |
| ] |
| except Exception as e: |
| print(f"β οΈ Failed to parse preferred slots for {cname}: {e}") |
|
|
| def can_schedule(day, slot): |
| |
| if day == "Wednesday" and slot in ["10-11", "11-12", "12-1"]: |
| return False |
|
|
| if hall_type == "Seminar Halls" and day in ["Wednesday", "Friday"] and slot in seminar_hall_blocked_slots: |
| return False |
|
|
|
|
| return ( |
| is_slot_allowed_for_year(year, day, slot) |
| and room_slots[room_name][f"{day}-{slot}"] |
| and not any(is_bcp_blocked(part, day, slot) for part in course_group) |
| ) |
|
|
| |
| if preferred_slots: |
| chosen_slots = [] |
| used_days = set() |
| for day, slot in preferred_slots: |
| if can_schedule(day, slot) and day not in used_days: |
| chosen_slots.append((day, slot)) |
| used_days.add(day) |
| if len(chosen_slots) == L: |
| break |
|
|
| if len(chosen_slots) == L: |
| for day, slot in chosen_slots: |
| timetable[year][day][slot].append(f"{cname} @ {room_name}") |
| room_slots[room_name][f"{day}-{slot}"] = False |
| if year == 1: |
| first_year_lectures_by_day_slot[day][slot].append(f"{cname} @ {room_name}") |
| lectures_scheduled = L |
| print(f"β
Scheduled {L} lectures using preferred slots: {', '.join(day for day, _ in chosen_slots)}") |
| else: |
| print(f"β οΈ Could not schedule all {L} lectures in preferred slots, falling back to default logic...") |
|
|
| |
| if lectures_scheduled < L: |
| |
| if year == 2: |
| blocked_slots = { |
| ("Monday", "10-11"), ("Monday", "11-12"), ("Monday", "12-1"), |
| ("Wednesday", "10-11"), ("Wednesday", "11-12"), ("Wednesday", "12-1"), |
| ("Friday", "10-11"), ("Friday", "11-12"), ("Friday", "12-1"), |
| } |
| else: |
| blocked_slots = set() |
|
|
| if year == 2 and L == 3: |
| |
| available_slots = [] |
| for day in days: |
| for slot in time_slots: |
| if not is_slot_allowed_for_year(year, day, slot): |
| continue |
| if hall_type == "Seminar Halls" and slot in seminar_hall_blocked_slots: |
| continue |
| if not room_slots[room_name][f"{day}-{slot}"]: |
| continue |
| if any(is_bcp_blocked(part, day, slot) for part in course_group): |
| continue |
| if (day, slot) in blocked_slots: |
| continue |
| available_slots.append((day, slot)) |
|
|
| |
| random.shuffle(available_slots) |
|
|
| chosen_slots = [] |
| used_days = set() |
|
|
| for day, slot in available_slots: |
| if day not in used_days: |
| chosen_slots.append((day, slot)) |
| used_days.add(day) |
| if len(chosen_slots) == 3: |
| break |
|
|
| if len(chosen_slots) < 3: |
| for day, slot in available_slots: |
| if (day, slot) not in chosen_slots: |
| chosen_slots.append((day, slot)) |
| if len(chosen_slots) == 3: |
| break |
|
|
| if len(chosen_slots) == 3: |
| for day, slot in chosen_slots: |
| timetable[year][day][slot].append(f"{cname} @ {room_name}") |
| room_slots[room_name][f"{day}-{slot}"] = False |
| if year == 1: |
| first_year_lectures_by_day_slot[day][slot].append(f"{cname} @ {room_name}") |
| lectures_scheduled = 3 |
| print(f"β
Scheduled 3 lectures on {', '.join(day for day, _ in chosen_slots)}") |
| else: |
| print("β Could not schedule all 3 lectures") |
| unscheduled_courses.append({ |
| "Course Name": cname, |
| "LTPC": ltpc, |
| "Sub folder": subfolder, |
| "Reason": f"Could not schedule {3 - len(chosen_slots)} out of 3 lectures" |
| }) |
| else: |
| |
| if L == 1: |
| for day in days: |
| for slot in time_slots: |
| if not is_slot_allowed_for_year(year, day, slot): continue |
| if hall_type == "Seminar Halls" and slot in seminar_hall_blocked_slots: continue |
| if not room_slots[room_name][f"{day}-{slot}"]: continue |
| if any(is_bcp_blocked(part, day, slot) for part in course_group): continue |
| timetable[year][day][slot].append(f"{cname} @ {room_name}") |
| room_slots[room_name][f"{day}-{slot}"] = False |
| if year == 1: |
| first_year_lectures_by_day_slot[day][slot].append(f"{cname} @ {room_name}") |
| lectures_scheduled = 1 |
| print(f"β
Scheduled 1-hour lecture on {day} at {slot}") |
| break |
| if lectures_scheduled == 1: break |
|
|
| elif L == 2: |
| for i in range(len(days)): |
| for j in range(i + 2, len(days)): |
| day1, day2 = days[i], days[j] |
| chosen_slots = [] |
| valid = True |
| for day in [day1, day2]: |
| for slot in time_slots: |
| if not is_slot_allowed_for_year(year, day, slot): continue |
| if hall_type == "Seminar Halls" and slot in seminar_hall_blocked_slots: continue |
| if not room_slots[room_name][f"{day}-{slot}"]: continue |
| if any(is_bcp_blocked(part, day, slot) for part in course_group): continue |
| chosen_slots.append((day, slot)) |
| break |
| else: |
| valid = False |
| break |
| if valid: |
| for day, slot in chosen_slots: |
| timetable[year][day][slot].append(f"{cname} @ {room_name}") |
| room_slots[room_name][f"{day}-{slot}"] = False |
| if year == 1: |
| first_year_lectures_by_day_slot[day][slot].append(f"{cname} @ {room_name}") |
| lectures_scheduled = 2 |
| print(f"β
Scheduled 2 lectures on {day1}, {day2}") |
| break |
| if lectures_scheduled == 2: break |
|
|
| elif L == 3: |
| for i in range(len(days) - 2): |
| for k in range(i + 3, len(days)): |
| day1, day2, day3 = days[i], days[i + 1], days[k] |
| candidate_days = [day1, day2, day3] |
| chosen_slots = [] |
| valid = True |
| for day in candidate_days: |
| for slot in time_slots: |
| if not is_slot_allowed_for_year(year, day, slot): continue |
| if hall_type == "Seminar Halls" and slot in seminar_hall_blocked_slots: continue |
| if not room_slots[room_name][f"{day}-{slot}"]: continue |
| if any(is_bcp_blocked(part, day, slot) for part in course_group): continue |
| chosen_slots.append((day, slot)) |
| break |
| else: |
| valid = False |
| break |
| if valid: |
| for day, slot in chosen_slots: |
| timetable[year][day][slot].append(f"{cname} @ {room_name}") |
| room_slots[room_name][f"{day}-{slot}"] = False |
| if year == 1: |
| first_year_lectures_by_day_slot[day][slot].append(f"{cname} @ {room_name}") |
| lectures_scheduled = 3 |
| print(f"β
Scheduled 3 lectures on {day1}, {day2}, {day3}") |
| break |
| if lectures_scheduled == 3: break |
| if lectures_scheduled < L: |
| unscheduled_courses.append({ |
| "Course Name": cname, |
| "LTPC": ltpc, |
| "Sub folder": subfolder, |
| "Reason": f"Could not schedule {L - lectures_scheduled} out of {L} lectures" |
| }) |
| |
| |
| |
| if T > 0: |
| print("π§ͺ Scheduling tutorial...") |
|
|
| |
| if year == 1: |
| continue |
|
|
| if year == 2: |
| second_year_tutorial_courses.append(cname) |
| continue |
|
|
| tutorial_scheduled = False |
| for day in days: |
| for slot in time_slots: |
| if not is_slot_allowed_for_year(year, day, slot): continue |
| if not room_slots[room_name][f"{day}-{slot}"]: continue |
| if any(is_bcp_blocked(part, day, slot) for part in course_group): continue |
| timetable[year][day][slot].append(f"{cname} Tutorial @ {room_name}") |
| room_slots[room_name][f"{day}-{slot}"] = False |
| tutorial_scheduled = True |
| print(f"β
Tutorial scheduled on {day} at {slot}") |
| break |
| if tutorial_scheduled: break |
|
|
| if not tutorial_scheduled: |
| print("β Tutorial not scheduled") |
| unscheduled_courses.append({ |
| "Course Name": cname, |
| "LTPC": ltpc, |
| "Sub folder": subfolder, |
| "Reason": f"Could not schedule 1 tutorial out of {T}" |
| }) |
|
|
|
|
| |
| lab_scheduled_blocks = 0 |
| if P in [2, 3]: |
| print(f"π§ͺ Scheduling {P}-hour lab...") |
| lab_slots_needed = P |
| for day in days: |
| for i in range(len(time_slots) - lab_slots_needed + 1): |
| slot_block = time_slots[i:i+lab_slots_needed] |
| if all(is_slot_allowed_for_year(year, day, s) and room_slots[room_name][f"{day}-{s}"] and not any(is_bcp_blocked(part, day, s) for part in course_group) for s in slot_block): |
| for s in slot_block: |
| timetable[year][day][s].append(f"{cname} Lab ({P} hours) @ {room_name}") |
| room_slots[room_name][f"{day}-{s}"] = False |
| lab_scheduled_blocks = 1 |
| print(f"β
Lab scheduled on {day} at {slot_block[0]}-{slot_block[-1]}") |
| break |
| if lab_scheduled_blocks == 1: break |
| elif P == 6: |
| print("π§ͺ Scheduling 6-hour lab over 2 days...") |
| for day in days: |
| for slot in time_slots: |
| if not is_slot_allowed_for_year(year, day, slot): continue |
| if not room_slots[room_name][f"{day}-{slot}"]: continue |
| if any(is_bcp_blocked(part, day, slot) for part in course_group): continue |
| timetable[year][day][slot].append(f"{cname} Lab (3 hours) @ {room_name}") |
| room_slots[room_name][f"{day}-{slot}"] = False |
| lab_scheduled_blocks += 1 |
| print(f"β
Half of 6-hour lab scheduled on {day} at {slot}") |
| if lab_scheduled_blocks == 2: break |
| if lab_scheduled_blocks == 2: break |
|
|
| expected_blocks = 1 if P in [2,3] else P // 3 if P else 0 |
| if expected_blocks > lab_scheduled_blocks: |
| unscheduled_courses.append({ |
| "Course Name": cname, |
| "LTPC": ltpc, |
| "Sub folder": subfolder, |
| "Reason": f"Could not schedule {expected_blocks - lab_scheduled_blocks} out of {expected_blocks} labs" |
| }) |
|
|
|
|
| |
| schedule_bsms_2nd_year_tutorials( |
| second_year_tutorial_courses, |
| room_data, |
| room_slots, |
| timetable |
| ) |
|
|
| |
| year_major_courses = defaultdict(lambda: defaultdict(list)) |
| for _, row in course_df.iterrows(): |
| cname = row["Course Name"].strip() |
| codes = cname.split('-') |
|
|
| |
| course_years = set() |
| course_majors = set() |
|
|
| for code in codes: |
| year = extract_year_from_code(code) |
| if year: |
| course_years.add(year) |
|
|
| major_found = False |
| for prefix, major in { |
| "mat": "Math", "msm": "Math", "phy": "Physics", "msp": "Physics", "chy": "Chemistry", "msc": "Chemistry", |
| "bio": "Biology", "msb": "Biology", "ees": "Earth Science", "dsc": "Data Science", "i2m": "Math", |
| "i2p": "Physics", "i2c": "Chemistry", "i2b": "Biology" |
| }.items(): |
| if code.lower().startswith(prefix): |
| course_majors.add(major) |
| major_found = True |
|
|
|
|
|
|
| for year in course_years: |
| for major in course_majors: |
| if cname not in year_major_courses[year][major]: |
| year_major_courses[year][major].append(cname) |
|
|
|
|
| for year in timetable: |
| if year in [1, 2]: |
| continue |
|
|
| year_folder = os.path.join(output_dir, f"{ordinal(year)} Year") |
| os.makedirs(year_folder, exist_ok=True) |
| fpath = os.path.join(year_folder, f"{ordinal(year)} Year Timetable.xlsx") |
|
|
| wb = Workbook() |
| wb.remove(wb.active) |
|
|
| |
| for major in ["Math", "Physics", "Chemistry", "Biology", "Data Science", "Earth Science"]: |
| if major not in year_major_courses[year]: |
| continue |
|
|
| ws = wb.create_sheet(title=major) |
| ws.append(["Day"] + time_slots) |
|
|
| for row_idx, day in enumerate(days, start=2): |
| row_data = [day] |
| for slot in time_slots: |
| entries = timetable[year][day][slot] |
| val = ", ".join( |
| e for e in entries if any(c in e for c in year_major_courses[year][major]) |
| ) |
| row_data.append(val) |
| ws.append(row_data) |
|
|
| |
| for col_idx, slot in enumerate(time_slots, start=2): |
| cell = ws.cell(row=row_idx, column=col_idx) |
| entries = timetable[year][day][slot] |
| for cname in year_major_courses[year][major]: |
| if any(cname in e for e in entries): |
| if cname in course_colors: |
| cell.fill = PatternFill( |
| start_color=course_colors[cname], |
| end_color=course_colors[cname], |
| fill_type="solid" |
| ) |
| break |
|
|
| |
| for col in range(2, len(time_slots) + 2): |
| ws.column_dimensions[get_column_letter(col)].width = 35 |
| for row_cells in ws.iter_rows(min_row=2, min_col=2): |
| for cell in row_cells: |
| cell.alignment = Alignment(wrap_text=True, vertical='top') |
|
|
| wb.save(fpath) |
|
|
|
|
|
|
| |
| bsms_2nd_year_codes = {"mat201","mat202", "phy211","phy212", "chy211", "chy212","ees212","ees211", "dsc212","dsc211", "bio211","bio212","hum211",} |
| bsms_2nd_year_courses = { |
| cname.strip() for cname in course_df["Course Name"].unique() |
| if cname.strip().split()[0].lower() in bsms_2nd_year_codes |
| } |
|
|
| if 2 in timetable: |
| combined_wb = Workbook() |
| ws = combined_wb.active |
| ws.title = "2nd Year BS-MS" |
| ws.append(["Day"] + time_slots) |
|
|
| for row_idx, day in enumerate(days, start=2): |
| row_data = [day] |
| for slot in time_slots: |
| entries = timetable[2][day][slot] |
| filtered = [entry for entry in entries if any(course in entry for course in bsms_2nd_year_courses)] |
| row_data.append(", ".join(filtered)) |
| ws.append(row_data) |
|
|
| for col_idx, slot in enumerate(time_slots, start=2): |
| cell = ws.cell(row=row_idx, column=col_idx) |
| entries = timetable[2][day][slot] |
| for cname in bsms_2nd_year_courses: |
| if any(cname in e for e in entries): |
| if cname in course_colors: |
| cell.fill = PatternFill(start_color=course_colors[cname], end_color=course_colors[cname], fill_type="solid") |
| break |
|
|
| |
| for col in range(2, len(time_slots) + 2): |
| |
| ws.column_dimensions[get_column_letter(col)].width = 31 |
| for row_cells in ws.iter_rows(min_row=2, min_col=2): |
| for cell in row_cells: |
| cell.alignment = Alignment(wrap_text=True) |
|
|
| combined_path = os.path.join(output_dir, "2nd Year", "2nd Year All Courses.xlsx") |
| os.makedirs(os.path.dirname(combined_path), exist_ok=True) |
| combined_wb.save(combined_path) |
|
|
|
|
|
|
|
|
|
|
| if unscheduled_courses: |
| |
| pass |
|
|
| return first_year_lectures_by_day_slot, unscheduled_courses |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
| def process(course_file, room_file): |
| import traceback |
| import shutil |
| import zipfile |
| from collections import defaultdict |
| import pandas as pd |
| import os |
| from openpyxl import load_workbook |
| from openpyxl.utils import get_column_letter |
|
|
|
|
| try: |
| print("β
Starting process...") |
|
|
| |
| print("π Reading course and room files...") |
| course_df = pd.read_excel(course_file.name) |
| room_df = pd.read_excel(room_file.name) |
|
|
| |
| room_df.columns = [ |
| "Class Room", "Class Capacity", |
| "Computer Lab", "Computer Capacity", |
| "Seminar Halls", "Seminar Capacity" |
| ] |
|
|
| |
| output_dir = "generated_routines" |
| |
|
|
| for path in [output_dir]: |
| if os.path.exists(path): |
| shutil.rmtree(path) |
| os.makedirs(path) |
|
|
| |
| room_slots = defaultdict(lambda: defaultdict(lambda: True)) |
|
|
| for day in days: |
| for room in room_slots: |
| for slot in ["2-3", "3-4", "4-5"]: |
| room_slots[room][f"{day}-{slot}"] = False |
|
|
| |
| timetable = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) |
|
|
| |
| print("π Generating timetable for 2ndβ5th years...") |
| |
| first_year_lectures, unscheduled_from_gen = generate_timetable(course_df, room_df, output_dir, room_slots, timetable) |
|
|
| |
| print("π¨βπ¬ Scheduling 1st year labs/tutorials...") |
| |
| tutorial_groups, lab_groups, unscheduled_1st = schedule_bs_ms_first_year( |
| course_df, |
| room_df, |
| room_slots, |
| time_slots, |
| days, |
| output_dir, |
| first_year_lectures_by_day_slot=first_year_lectures, |
| timetable=timetable |
| ) |
|
|
| |
| all_unscheduled = unscheduled_from_gen + unscheduled_1st |
|
|
| |
| print("π Generating reports...") |
| generate_available_halls_report(course_df, room_slots) |
| generate_central_timetable_excel(timetable) |
| unused_hall_path = "Unused_Halls_Report.xlsx" |
| generate_unused_halls_report(room_slots, output_path=unused_hall_path) |
|
|
| |
| unscheduled_report_path = os.path.join(output_dir, "Unscheduled_LTP_Report.xlsx") |
| if all_unscheduled: |
| df_all_unscheduled = pd.DataFrame(all_unscheduled) |
| df_all_unscheduled.to_excel(unscheduled_report_path, index=False) |
|
|
| try: |
| wb_unscheduled = load_workbook(unscheduled_report_path) |
| ws_unscheduled = wb_unscheduled.active |
|
|
| header = [cell.value for cell in ws_unscheduled[1]] |
| try: |
| course_name_col_idx = header.index("Course Name") + 1 |
| reason_col_idx = header.index("Reason") + 1 |
|
|
| ws_unscheduled.column_dimensions[get_column_letter(course_name_col_idx)].width = 35 |
| ws_unscheduled.column_dimensions[get_column_letter(reason_col_idx)].width = 50 |
|
|
| for row in ws_unscheduled.iter_rows(min_row=2): |
| for cell in row: |
| cell.alignment = Alignment(wrapText=True, vertical='top') |
|
|
| wb_unscheduled.save(unscheduled_report_path) |
| print("β
Unscheduled Report formatted successfully.") |
| except ValueError: |
| print("Warning: 'Course Name' or 'Reason' column not found in Unscheduled Report for formatting.") |
|
|
| except Exception as e: |
| print(f"Error formatting Unscheduled Report: {e}") |
| |
| pass |
|
|
|
|
| |
| print("ποΈ Zipping timetables...") |
| zip_path_main = "2nd-5th_Year_Timetable.zip" |
| zip_path_1st = "1st_Year_Timetable.zip" |
|
|
| with zipfile.ZipFile(zip_path_main, 'w') as zipf: |
| for foldername, _, filenames in os.walk(output_dir): |
| for filename in filenames: |
| |
| if filename == "Unscheduled_LTP_Report.xlsx" or filename == "1st Year Timetable.xlsx": |
| continue |
| filepath = os.path.join(foldername, filename) |
| arcname = os.path.relpath(filepath, output_dir) |
| zipf.write(filepath, arcname) |
|
|
|
|
| |
| first_year_excel_path = os.path.join(output_dir, "1st Year", "1st Year Timetable.xlsx") |
| if os.path.exists(first_year_excel_path): |
| with zipfile.ZipFile(zip_path_1st, 'w') as zipf: |
| zipf.write(first_year_excel_path, os.path.basename(first_year_excel_path)) |
| else: |
| zip_path_1st = None |
|
|
|
|
| print("β
All done successfully.") |
|
|
| return ( |
| zip_path_main, |
| zip_path_1st, |
| unscheduled_report_path if os.path.exists(unscheduled_report_path) else None, |
| "Available_Halls.xlsx" if os.path.exists("Available_Halls.xlsx") else None, |
| "Central_Timetable.xlsx" if os.path.exists("Central_Timetable.xlsx") else None, |
| unused_hall_path if os.path.exists(unused_hall_path) else None, |
| "" |
| ) |
|
|
| except Exception as e: |
| err_msg = f"π¨ Error: {str(e)}\n\n" + traceback.format_exc() |
| print(err_msg) |
| return None, None, None, None, None, None, err_msg |
|
|
| |
| import gradio as gr |
| import gradio as gr |
|
|
| def launch_ui(): |
| with gr.Blocks(css=""" |
| .centered-title { |
| text-align: center; |
| font-size: 2.4em; |
| font-weight: bold; |
| color: #2b3d4f; |
| background: #e0f7fa; |
| padding: 14px; |
| border-radius: 14px; |
| margin-bottom: 25px; |
| } |
| .section-card { |
| background-color: #f7f9fc; |
| border: 1px solid #d0d7de; |
| border-radius: 14px; |
| padding: 20px; |
| margin-bottom: 25px; |
| box-shadow: 0 2px 6px rgba(0,0,0,0.06); |
| } |
| .highlight-button { |
| background-color: #1976D2 !important; |
| color: white !important; |
| border-radius: 8px; |
| padding: 10px 20px; |
| } |
| """) as iface: |
|
|
| |
| gr.Image( |
| value="https://drive.google.com/uc?id=1UDlI15QVKy0JSkfFCG7yMAR7esv35O-v", |
| height=400, |
| width=8500, |
| show_label=False, |
| container=False |
| ) |
|
|
| |
| gr.HTML('<div class="centered-title">ποΈπ IISER TVM Course Timetable Scheduler</div>') |
|
|
| |
| with gr.Tabs(): |
| |
| with gr.TabItem("π Upload Input Data"): |
|
|
| gr.Markdown("### Upload Files") |
| with gr.Row(): |
| course_input = gr.File(label="π Upload Course Details Excel File (.xlsx)", file_types=[".xlsx"]) |
| room_input = gr.File(label="π¬ Upload Room Information Excel File (.xlsx)", file_types=[".xlsx"]) |
| gr.HTML('</div>') |
|
|
|
|
| gr.Markdown("### Generate Timetable") |
| run_button = gr.Button("π Generate Timetable", elem_classes="highlight-button") |
| gr.HTML('</div>') |
|
|
| |
| with gr.TabItem("β¬οΈ View & Download Outputs"): |
|
|
| gr.Markdown("### π¦ Downloadable Timetables") |
| with gr.Row(): |
| timetable_output = gr.File(label="π 2nd-5th year Timetable") |
| first_year_output = gr.File(label="π§ͺ 1st Year Timetable") |
| gr.HTML('</div>') |
|
|
|
|
| gr.Markdown("### π Reports & Summary") |
| with gr.Row(): |
| unscheduled_output = gr.File(label="β οΈ Unscheduled Report") |
| available_halls_output = gr.File(label="π© Available Halls Report") |
| central_output = gr.File(label="π Central Timetable") |
| unused_output = gr.File(label="π Unused Halls Report") |
| gr.HTML('</div>') |
|
|
|
|
| |
| with gr.TabItem("π Reset"): |
| reset_btn = gr.Button("β»οΈ Clear All Data") |
|
|
| |
| run_button.click( |
| fn=process, |
| inputs=[course_input, room_input], |
| outputs=[ |
| timetable_output, |
| first_year_output, |
| unscheduled_output, |
| available_halls_output, |
| central_output, |
| unused_output |
| ] |
| ) |
|
|
| iface.launch(debug=True) |
|
|
| if __name__ == "__main__": |
| launch_ui() |