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 import shutil import zipfile # ------ CONFIGURATION ------ 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"],"Tuesday":["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"] restricted_5_6_slots = { 1: {"Monday", "Wednesday", "Friday"}, 2: {"Monday", "Tuesday", "Wednesday"} } 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(("msp", "msb", "msc", "msm")): return 1 if first_digit == 3 else 2 if first_digit == 4 else None 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): 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: options.append([d1, d2, d3]) elif d3 - d2 == 1 and d2 - d1 >= 2: 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" # Header Row ws.append(["Day"] + time_slots) for day in days: row = [day] for slot in time_slots: entries = [] # Collect normal lecture/tutorial entries (years 1–5) for year in sorted(timetable.keys()): scheduled = timetable[year][day][slot] entries.extend(scheduled) # ✅ NEW: Append 1st-year tutorial groups with LG tags 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})") # ✅ NEW: Append 1st-year lab groups with LG tags 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) # ✅ Optional formatting for col in ws.columns: for cell in col: cell.alignment = Alignment(wrapText=True) # Set appropriate column widths 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(room_slots, output_path="Available_Halls.xlsx"): from openpyxl import Workbook wb = Workbook() ws = wb.active ws.title = "Available Halls" # Header row: Slot names 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) 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 # ✅ NEW ARG: pass the main timetable dict (defaultdict) ): from openpyxl import Workbook from collections import defaultdict import os 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", "11-12", "12-1"] LUNCH_SLOT = "1-2" FORBIDDEN_5_6_DAYS = {"Monday", "Wednesday", "Friday"} # Mapping TGs to LGs 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", } # Extract valid lab rooms 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)) # day -> slot -> count used_slots_by_lg = defaultdict(set) unscheduled = [] 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 # Not a 1st year course # --- Schedule Labs --- if P > 0: for lg_index in range(5): lg = f"LG-{lg_index+1}" assigned = False for day in days: 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}") # ✅ NEW: Add to central timetable if timetable: timetable[1][day][slot].append(f"{cname} (Lab) @ {room} ({lg})") lab_groups[lg].append((day, LAB_SLOTS, room, cname)) assigned = True break if assigned: break if not assigned: unscheduled.append({ "Course Name": cname, "LTPC": ltpc, "Sub folder": subfolder, "Reason": f"No available 2–5PM block for {lg}" }) # --- Schedule Tutorials --- if T == 1: 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}" assigned = 0 for day in days: for slot in time_slots: if slot == LUNCH_SLOT or slot in LAB_SLOTS or (slot == "5-6" and day in FORBIDDEN_5_6_DAYS): continue if slot in used_slots_by_lg[lg]: continue if tutorial_slot_count[day][slot] >= 3: continue available_rooms = [r for r in TUTORIAL_ROOMS if room_slots[r][f"{day}-{slot}"]] if not available_rooms: continue room = available_rooms[0] room_slots[room][f"{day}-{slot}"] = False tutorial_slot_count[day][slot] += 1 tutorial_groups[lg].append((day, slot, room, cname)) used_slots_by_lg[lg].add(f"{day}-{slot}") assigned += 1 # ✅ NEW: Add to central timetable if timetable: timetable[1][day][slot].append(f"{cname} (Tutorial) @ {room} ({lg})") if assigned == 2: break if assigned == 2: break if assigned < 2: unscheduled.append({ "Course Name": cname, "LTPC": ltpc, "Sub folder": subfolder, "Reason": f"Tutorials for {tg1}, {tg2} (under {lg}) not scheduled" }) # --- Write LG-wise Excel files with lectures --- year_folder = os.path.join(output_dir, "1st Year") os.makedirs(year_folder, exist_ok=True) for lg in lab_groups: wb = Workbook() ws = wb.active ws.title = lg ws.append(["Day"] + time_slots) for day in days: row = [day] for slot in time_slots: val = "" # Tutorials for d, s, r, c in tutorial_groups[lg]: if d == day and s == slot: val += f"{c} (Tutorial) @ {r}\n" # Labs for d, slot_block, r, c in lab_groups[lg]: if d == day and slot in slot_block: val += f"{c} (Lab) @ {r}\n" # Lectures if first_year_lectures_by_day_slot: for entry in first_year_lectures_by_day_slot[day][slot]: if f"{day}-{slot}" not in used_slots_by_lg[lg]: val += f"{entry} (Lecture)\n" used_slots_by_lg[lg].add(f"{day}-{slot}") row.append(val.strip()) ws.append(row) # ✅ NEW: Rename file with LG-TG mapping lg_index = int(lg.split("-")[1]) tg1 = f"TG-{2 * (lg_index - 1) + 1}" tg2 = f"TG-{2 * (lg_index - 1) + 2}" filename = f"{lg} ({tg1} & {tg2}).xlsx" wb.save(os.path.join(year_folder, filename)) # --- Export Unscheduled --- if unscheduled: pd.DataFrame(unscheduled).to_excel(os.path.join(output_dir, "Unscheduled_LTP_Report.xlsx"), index=False) return tutorial_groups, lab_groups, unscheduled def is_slot_allowed_for_year(year, day, slot): if slot == lunch_slot: return False if year == 1: # BS-MS 1st Year: Lectures only between 8–1, no 5–6 on M/W/F if slot not in ["8-9", "9-10", "10-11", "11-12", "12-1"]: return False if slot == "5-6" and day in ["Monday", "Wednesday", "Friday"]: return False if year in restricted_5_6_slots and slot == "5-6" and day in restricted_5_6_slots[year]: return False if year in [3, 4] and slot in no_class_3rd_4th.get(day, []): return False return True def generate_timetable(course_df, room_df, output_dir, room_slots, timetable): from openpyxl import Workbook from openpyxl.styles import PatternFill from collections import defaultdict 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 = [] # ✅ Build usable room list (merged from all columns) room_data = [] for i in range(len(room_df)): 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 = room_df[room_col].iloc[i] cap = room_df[cap_col].iloc[i] if pd.notna(room) and pd.notna(cap): room_data.append((str(room).strip(), int(cap), hall_type)) grouped = course_df.groupby("Sub folder") 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"] 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)) if not valid_years or max(valid_years) > 5: 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) # Assign course color if cname not in course_colors: course_colors[cname] = course_color_palette[color_index % len(course_color_palette)] color_index += 1 # --- Assign suitable room --- assigned_room = None if L > 0: # For lectures, prefer Class Rooms or Seminar Halls 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 tutorials (without lectures) 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: unscheduled_courses.append({"Course Name": cname, "LTPC": ltpc, "Sub folder": subfolder, "Reason": "No suitable room"}) continue room_name, hall_type = assigned_room # --- Assign lecture slots --- lecture_day_options = find_valid_lecture_days(L, list(range(len(days)))) best_assigned = [] max_assigned = 0 for day_indices in lecture_day_options: temp_assigned = [] for idx in day_indices: day = days[idx] 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 entry = f"{cname} @ {room_name}" timetable[year][day][slot].append(entry) room_slots[room_name][f"{day}-{slot}"] = False if year == 1: first_year_lectures_by_day_slot[day][slot].append(entry) temp_assigned.append(idx) break if len(temp_assigned) > max_assigned: best_assigned = temp_assigned max_assigned = len(temp_assigned) if max_assigned == L: break if max_assigned < L: unscheduled_courses.append({ "Course Name": cname, "LTPC": ltpc, "Sub folder": subfolder, "Reason": f"Only {max_assigned} of {L} lectures scheduled" }) # --- Schedule Tutorial for non-1st Year --- if T == 1 and year != 1: tutorial_scheduled = False for i, day in enumerate(days): if i in best_assigned: continue 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 break if tutorial_scheduled: break if not tutorial_scheduled: unscheduled_courses.append({ "Course Name": cname, "LTPC": ltpc, "Sub folder": subfolder, "Reason": "Tutorial slot unavailable due to conflicts/constraints" }) # --- Write Timetables (Years 2–5) --- year_major_courses = defaultdict(lambda: defaultdict(list)) for _, row in course_df.iterrows(): cname = row["Course Name"] years = [extract_year_from_code(code) for code in cname.split("-")] valid_years = list(filter(None, years)) if not valid_years or max(valid_years) > 5: continue year = max(valid_years) for part in cname.split("-"): code = part.lower() 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.startswith(prefix): year_major_courses[year][major].append(cname) for year in timetable: if year == 1: continue # 1st-year handled separately year_folder = os.path.join(output_dir, f"{ordinal(year)} Year") os.makedirs(year_folder, exist_ok=True) for major in year_major_courses[year]: fname = f"{ordinal(year)} Year, {major} Major.xlsx" fpath = os.path.join(year_folder, fname) wb = Workbook() ws = wb.active ws.append(["Day"] + time_slots) for row_idx, day in enumerate(days, start=2): row_data = [day] for col_idx, slot in enumerate(time_slots, start=2): 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): cell.fill = PatternFill(start_color=course_colors[cname], end_color=course_colors[cname], fill_type="solid") break wb.save(fpath) # ✅ Save Unscheduled Report if unscheduled_courses: pd.DataFrame(unscheduled_courses).to_excel("Unscheduled_LTP_Report.xlsx", index=False) return first_year_lectures_by_day_slot # -- All other imported functions like: generate_available_halls_report, generate_central_timetable_excel, schedule_bs_ms_first_year, generate_timetable -- # You already posted them correctly. I won't repeat to save space unless you ask. # --- Final Working Process Function --- def process(course_file, room_file): import shutil import zipfile from collections import defaultdict import pandas as pd import os # --- Read Excel Files --- course_df = pd.read_excel(course_file.name) room_df = pd.read_excel(room_file.name) # ✅ Rename columns for clarity (based on your Excel screenshot) room_df.columns = [ "Class Room", "Class Capacity", "Computer Lab", "Computer Capacity", "Seminar Halls", "Seminar Capacity" ] # --- Create output directories --- output_dir = "generated_routines" output_1st_year_dir = "generated_1st_year_LT" for path in [output_dir, output_1st_year_dir]: if os.path.exists(path): shutil.rmtree(path) os.makedirs(path) # --- Initialize room availability --- room_slots = defaultdict(lambda: defaultdict(lambda: True)) # ✅ BLOCK 2–5 PM for BS-MS 1st Year labs 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 # ✅ Build room_data (now includes Seminar Halls correctly) room_data = [] for i in range(len(room_df)): # Class Room if pd.notna(room_df["Class Room"].iloc[i]) and pd.notna(room_df["Class Capacity"].iloc[i]): room_data.append(( str(room_df["Class Room"].iloc[i]).strip(), int(room_df["Class Capacity"].iloc[i]), "Class Room" )) # Computer Lab if pd.notna(room_df["Computer Lab"].iloc[i]) and pd.notna(room_df["Computer Capacity"].iloc[i]): room_data.append(( str(room_df["Computer Lab"].iloc[i]).strip(), int(room_df["Computer Capacity"].iloc[i]), "Computer Lab" )) # Seminar Hall if pd.notna(room_df["Seminar Halls"].iloc[i]) and pd.notna(room_df["Seminar Capacity"].iloc[i]): room_data.append(( str(room_df["Seminar Halls"].iloc[i]).strip(), int(room_df["Seminar Capacity"].iloc[i]), "Seminar Halls" )) # ✅ Initialize central timetable timetable = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) # Step 1: Generate lectures (Years 2–5) + collect 1st year lectures first_year_lectures = generate_timetable(course_df, room_df, output_dir, room_slots, timetable) # Step 2: Schedule 1st year labs + tutorials tg, lg, unscheduled_1st = schedule_bs_ms_first_year( course_df, room_df, room_slots, time_slots, days, output_1st_year_dir, first_year_lectures_by_day_slot=first_year_lectures, timetable=timetable ) # Step 3: Export reports generate_available_halls_report(room_slots) generate_central_timetable_excel(timetable) # Step 4: Zip Final_Timetable (2nd–5th Year) zip_path_main = "Final_Timetable.zip" with zipfile.ZipFile(zip_path_main, 'w') as zipf: for foldername, _, filenames in os.walk(output_dir): for filename in filenames: filepath = os.path.join(foldername, filename) arcname = os.path.relpath(filepath, output_dir) zipf.write(filepath, arcname) # Step 5: Zip 1st_Year_Labs_Tutorials.zip (includes lectures + labs + tutorials) zip_path_1st = "1st_Year_Labs_Tutorials.zip" with zipfile.ZipFile(zip_path_1st, 'w') as zipf: for foldername, _, filenames in os.walk(output_1st_year_dir): for filename in filenames: filepath = os.path.join(foldername, filename) arcname = os.path.relpath(filepath, output_1st_year_dir) zipf.write(filepath, arcname) # Step 6: Return final file outputs return ( zip_path_main, zip_path_1st, ("Unscheduled_LTP_Report.xlsx" if os.path.exists("Unscheduled_LTP_Report.xlsx") 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) ) # --- Gradio UI Launcher --- import gradio as gr def launch_ui(): with gr.Blocks(css=""" .centered-title { text-align: center; font-size: 2.2em; font-weight: bold; color: #2b3d4f; background: #e0f7fa; padding: 10px; border-radius: 12px; margin-bottom: 20px; } .highlight-button { background-color: #1976D2 !important; color: white !important; border-radius: 8px; padding: 10px 20px; } """) as iface: # 🔼 Banner Image (Header) gr.Image( value="https://drive.google.com/uc?id=1UDlI15QVKy0JSkfFCG7yMAR7esv35O-v", height=400, width=8000, # ✅ Reasonable width show_label=False, container=False ) # 🏷️ Styled Title gr.HTML('
IISER TVM Course Timetable Scheduler
') # 📁 File Inputs with gr.Row(): course_input = gr.File(label="📘 Upload Course Excel File (.xlsx)") room_input = gr.File(label="🏬 Upload Room Details Excel File (.xlsx)") # 🚀 Styled Button run_button = gr.Button("Generate Timetable", elem_classes="highlight-button") # 🧾 Outputs (Timetable files) with gr.Row(): timetable_output = gr.File(label="📁 Download Timetable ZIP") first_year_output = gr.File(label="🧪 1st Year Labs & Tutorials ZIP") with gr.Row(): unscheduled_output = gr.File(label="⚠️ Unscheduled L/T/P Report (if any)") available_halls_output = gr.File(label="🏩 Available Halls Report") central_output = gr.File(label="📘 Central Timetable (All Courses)") # 🧠 Process Trigger run_button.click( fn=process, inputs=[course_input, room_input], outputs=[ timetable_output, first_year_output, unscheduled_output, available_halls_output, central_output ] ) iface.launch(share=True, debug=True) if __name__ == "__main__": launch_ui()