IISER / app.py
saptak21's picture
Update app.py
42fc98b verified
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('<div class="centered-title">πŸ—“οΈπŸ“˜ IISER TVM Course Timetable Scheduler</div>')
# πŸ”€ Tabs
with gr.Tabs():
# 🟒 Tab 1 – Upload Section
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>')
# 🟣 Tab 2 – Output Downloads
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>')
# πŸ”„ Reset Tab
with gr.TabItem("πŸ” Reset"):
reset_btn = gr.Button("♻️ Clear All Data")
# 🧠 Trigger logic
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()