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 # ------ 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"],"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]) # ✅ Unified M.Sc. logic to correctly map years for all subjects if code.startswith(("msc", "msm", "msp", "msb")): if first_digit == 3: return 3 # Represents M.Sc. 1st Year elif first_digit == 4: return 4 # Represents M.Sc. 2nd Year # General fallback for other course codes (e.g., BS-MS) 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: # Strict: two consecutive days and the third after a 1-day gap 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" # 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(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]: # if room is free available_rooms.append(room) row_data.append(", ".join(sorted(available_rooms))) ws.append(row_data) wb.save(output_path) # Optional: Generate a second report (Advanced) if course_df is not None: ws2 = wb.create_sheet("Unused Large Rooms") ws2.append(["Day", "Slot", "Room", "Capacity", "Reason"]) # Build room capacity map room_caps = {} for _, row in course_df.iterrows(): try: students = int(row["Total Students"]) except: continue return # --- Updated schedule_bs_ms_first_year() with detailed unscheduled reasons --- def schedule_bs_ms_first_year( course_df, room_df, room_slots, time_slots, days, output_dir, # Use output_dir directly 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 = [] # Step 1: Schedule Labs # First filter out all 1st-year lab courses 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 ] # Only 4 lab courses needed lab_courses_first_year = lab_courses_first_year[:4] # Use all 5 days to distribute labs across LGs 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): # each LG gets all 4 labs course_row = lab_courses_first_year[i] cname = course_row["Course Name"] subfolder = course_row["Sub folder"] ltpc = course_row["[LTPC]"] # Assign day in a cyclic shifted way day = lab_days[(i + lg_index) % len(lab_days)] # Skip if lab already assigned to another LG on same day 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 # Check slot availability 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 # Room assignment only for IDC labs 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) # Step 2: Schedule Tutorials 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] ]) # Mark lab slots as unavailable for tutorials 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 in LECTURE_SLOTS: #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 # Need 2 rooms for TG-i and TG-i+1 # Assign TG-i and TG-i+1 in parallel room1, room2 = available_rooms[0], available_rooms[1] tg1 = f"TG-{2 * (lg_index) + 1}" tg2 = f"TG-{2 * (lg_index) + 2}" # Mark rooms as used 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) # Record tutorials 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 # Stop trying more slots after assigning both TGs 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})" }) # Step 3: Create a single workbook for 1st Year in output_dir year_folder = os.path.join(output_dir, "1st Year") # Save in a subfolder of output_dir os.makedirs(year_folder, exist_ok=True) # Ensure the subfolder exists first_year_wb = Workbook() first_year_wb.remove(first_year_wb.active) # Remove default sheet for lg_index in range(5): # Iterate through LG indices to create sheets in order 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})" # Create the desired sheet title ws = first_year_wb.create_sheet(title=sheet_title) # Create a sheet with the specified title ws.append(["Day"] + time_slots) for day in days: row = [day] for slot_index, slot in enumerate(time_slots): val = "" # Filter tutorial entries for the current LG and slot tg1_tuts = [f"{c} (Tutorial) @ {r}" for d, s, r, c in tutorial_groups[lg] if d == day and s == slot] tg2_tuts = [] # Need to ensure TG-2 tutorials are also captured for this LG # A more robust way to get tutorials for TG1 and TG2 under this LG for d, s, r, c in tutorial_groups[lg]: if d == day and s == slot: if f"({tg1})" in timetable[1][day][slot]: # Check if TG1 is scheduled in the central timetable for this slot tg1_tuts = [entry for entry in timetable[1][day][slot] if f"({tg1})" in entry] if f"({tg2})" in timetable[1][day][slot]: # Check if TG2 is scheduled tg2_tuts = [entry for entry in timetable[1][day][slot] if f"({tg2})" in entry] for t in tg1_tuts: val += f"{t}\n" # Already includes TG tag from central timetable for t in tg2_tuts: val += f"{t}\n" # Already includes TG tag from central timetable # Filter lab entries for the current LG and slot 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" # Add LG tag for clarity else: val += f"{c} (Lab)\n" # Add LG tag for clarity # Add 1st year lecture entries from the central timetable 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" # Add Lecture tag for clarity 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) # Save the single workbook in the 1st Year subfolder 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): # 2 courses per slot 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)): # Prioritize rooms close to 48 capacity 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: # BS-MS 1st Year: Lectures only between 8–11, no 5–6 on M/W/F if slot not in ["9-10", "10-11","11-12","12-1","5-6"]: return False #if slot == "5-6" and day in ["Monday", "Wednesday", "Friday"]: # return False if year == 2: # Add explicit check for Wednesday 10-11, 11-12, 12-1 block for 2nd year lectures 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 # Also block Monday 10-11, 11-12, 12-1 and Friday 10-11, 11-12, 12-1 as these are tutorial slots for 2nd year if year == 2 and day in ["Monday", "Friday"] and slot in ["10-11", "11-12", "12-1"]: 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_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)" # Header Row: ["Day", "8-9", "9-10", ..., "6-7"] 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) # Wrap text and set column width 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 # 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) } # Track 2nd year tutorial courses separately 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})") # ---------------------------- # Schedule Lectures (L) with Preferred Slots # ---------------------------- if L > 0: print("📘 Scheduling lectures...") lectures_scheduled = 0 # always initialize # Parse preferred slots if available 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): # ⛔️ Global lecture block on Wednesday 10–1 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) ) # Try preferred slots first 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...") # Fallback: original scheduling logic if not all lectures placed if lectures_scheduled < L: # Only 2nd year has lecture blocks (Mon/Wed/Fri 10–1) 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: # Collect all free slots except blocked ones 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)) # Shuffle days so we don't fix consecutive days 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: # Your existing logic for L = 1, 2, 3 (unchanged) 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] # Try consecutive + 1 day gap 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" }) # ---------------------------- # Schedule Tutorials (T) # ---------------------------- if T > 0: print("🧪 Scheduling tutorial...") # Skip tutorials for BS-MS 1st year if year == 1: continue if year == 2: second_year_tutorial_courses.append(cname) continue # Defer scheduling until all second year tutorials are collected 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}" }) # Schedule Labs 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" }) # ✅ Call the dedicated function for BS-MS 2nd year tutorials schedule_bsms_2nd_year_tutorials( second_year_tutorial_courses, room_data, room_slots, timetable ) # Major-wise Timetables for Years 3-5 year_major_courses = defaultdict(lambda: defaultdict(list)) for _, row in course_df.iterrows(): cname = row["Course Name"].strip() codes = cname.split('-') # Determine all years and majors for the course 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 # skip 1st and 2nd year 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) # remove the default "Sheet" # Fixed order of majors → ensures consistent tab naming for major in ["Math", "Physics", "Chemistry", "Biology", "Data Science", "Earth Science"]: if major not in year_major_courses[year]: continue # skip if no courses for this major 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) # Apply colors 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 # Formatting 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) # 2nd Year Combined Timetable 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 # Apply consistent formatting for 2nd year timetable - Increased width for col in range(2, len(time_slots) + 2): # >>>>> MODIFIED LINE FOR 2ND YEAR COLUMN WIDTH <<<<< ws.column_dimensions[get_column_letter(col)].width = 31 # Increased width 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: # Do not save unscheduled courses here, return them to process function pass return first_year_lectures_by_day_slot, unscheduled_courses # --- Final Working Process Function --- 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...") # --- Read Excel Files --- print("📄 Reading course and room files...") course_df = pd.read_excel(course_file.name) room_df = pd.read_excel(room_file.name) # ✅ Rename columns for clarity 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" # Remove separate 1st year dir - This was the issue for path in [output_dir]: # Only remove main output dir if os.path.exists(path): shutil.rmtree(path) os.makedirs(path) # --- Initialize room availability map --- 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 # --- Initialize Central Timetable --- timetable = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) # ✅ Step 1: Generate lectures for 2nd–5th year print("📚 Generating timetable for 2nd–5th years...") # Modified to receive unscheduled_courses from generate_timetable first_year_lectures, unscheduled_from_gen = generate_timetable(course_df, room_df, output_dir, room_slots, timetable) # ✅ Step 2: Schedule BS-MS 1st year labs and tutorials print("👨🔬 Scheduling 1st year labs/tutorials...") # Pass output_dir to schedule_bs_ms_first_year tutorial_groups, lab_groups, unscheduled_1st = schedule_bs_ms_first_year( course_df, room_df, room_slots, time_slots, days, output_dir, # Pass output_dir first_year_lectures_by_day_slot=first_year_lectures, timetable=timetable ) # Combine unscheduled courses from both scheduling steps all_unscheduled = unscheduled_from_gen + unscheduled_1st # ✅ Step 3: Reports 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) # --- Save and Format Unscheduled Report --- 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}") # Ensure the report file is still returned even if formatting fails pass # ✅ Step 4: Zipping folders print("🗂️ Zipping timetables...") zip_path_main = "2nd-5th_Year_Timetable.zip" zip_path_1st = "1st_Year_Timetable.zip" # Define the 1st year zip path with zipfile.ZipFile(zip_path_main, 'w') as zipf: for foldername, _, filenames in os.walk(output_dir): for filename in filenames: # Exclude the 1st Year Timetable and Unscheduled Report from this zip 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) # Create a separate zip for the 1st Year Timetable first_year_excel_path = os.path.join(output_dir, "1st Year", "1st Year Timetable.xlsx") # Look in the 1st Year subfolder 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 # Set to None if the 1st year file wasn't created 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 # --- Gradio UI Launcher --- 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: # 🔼 Banner Image gr.Image( value="https://drive.google.com/uc?id=1UDlI15QVKy0JSkfFCG7yMAR7esv35O-v", height=400, width=8500, show_label=False, container=False ) # 🏷️ Title gr.HTML('