Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,700 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
import gradio as gr
|
| 5 |
+
from collections import defaultdict
|
| 6 |
+
from openpyxl import Workbook
|
| 7 |
+
from openpyxl.styles import PatternFill
|
| 8 |
+
import shutil
|
| 9 |
+
import zipfile
|
| 10 |
+
|
| 11 |
+
# ------ CONFIGURATION ------
|
| 12 |
+
days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
|
| 13 |
+
time_slots = ["8-9", "9-10", "10-11", "11-12", "12-1", "1-2", "2-3", "3-4", "4-5", "5-6", "6-7"]
|
| 14 |
+
lunch_slot = "1-2"
|
| 15 |
+
no_class_3rd_4th = {"Tuesday": ["4-5"], "Friday": ["5-6"], "Monday":["6-7"],"Tuesday":["6-7"],"Thursday":["6-7"]}
|
| 16 |
+
seminar_hall_blocked_slots = ["3-4", "4-5", "5-6"]
|
| 17 |
+
|
| 18 |
+
block_3rd = {"Monday", "Wednesday", "Friday"}
|
| 19 |
+
block_3rd_slots = ["2-3", "3-4", "4-5"]
|
| 20 |
+
block_4th = {"Monday", "Wednesday", "Thursday"}
|
| 21 |
+
block_4th_slots = ["10-11", "11-12", "12-1"]
|
| 22 |
+
|
| 23 |
+
restricted_5_6_slots = {
|
| 24 |
+
1: {"Monday", "Wednesday", "Friday"},
|
| 25 |
+
2: {"Monday", "Tuesday", "Wednesday"}
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
bcp_prefixes = ["phy", "msp", "chy", "msc", "bio", "msb", "i2c", "i2p", "i2b"]
|
| 29 |
+
|
| 30 |
+
def ordinal(n):
|
| 31 |
+
mapping = ["First", "Second", "Third", "Fourth", "Fifth"]
|
| 32 |
+
return mapping[n - 1] if 1 <= n <= 5 else f"Year{n or 'Unknown'}"
|
| 33 |
+
|
| 34 |
+
def extract_year_from_code(code):
|
| 35 |
+
code = code.lower()
|
| 36 |
+
numeric = ''.join(filter(str.isdigit, code))
|
| 37 |
+
if not numeric:
|
| 38 |
+
return None
|
| 39 |
+
first_digit = int(numeric[0])
|
| 40 |
+
if code.startswith(("msp", "msb", "msc", "msm")):
|
| 41 |
+
return 1 if first_digit == 3 else 2 if first_digit == 4 else None
|
| 42 |
+
if 1 <= first_digit <= 5:
|
| 43 |
+
return first_digit
|
| 44 |
+
return None
|
| 45 |
+
|
| 46 |
+
def is_bcp_major(code):
|
| 47 |
+
return any(code.startswith(prefix) for prefix in bcp_prefixes)
|
| 48 |
+
|
| 49 |
+
def is_bcp_blocked(code, day, slot):
|
| 50 |
+
if not is_bcp_major(code):
|
| 51 |
+
return False
|
| 52 |
+
year = extract_year_from_code(code)
|
| 53 |
+
if year == 3 and day in block_3rd and slot in block_3rd_slots:
|
| 54 |
+
return True
|
| 55 |
+
if year == 4 and day in block_4th and slot in block_4th_slots:
|
| 56 |
+
return True
|
| 57 |
+
return False
|
| 58 |
+
|
| 59 |
+
def parse_ltpc(ltpc):
|
| 60 |
+
ltpc = str(ltpc).strip()
|
| 61 |
+
if '-' in ltpc:
|
| 62 |
+
parts = list(map(int, ltpc.split('-')))
|
| 63 |
+
while len(parts) < 4:
|
| 64 |
+
parts.append(0)
|
| 65 |
+
return tuple(parts[:4])
|
| 66 |
+
digits = ''.join(filter(str.isdigit, ltpc)).zfill(4)
|
| 67 |
+
return int(digits[0]), int(digits[1]), int(digits[2]), int(digits[3])
|
| 68 |
+
|
| 69 |
+
def find_valid_lecture_days(L, days_available):
|
| 70 |
+
if L == 1:
|
| 71 |
+
return [[d] for d in days_available]
|
| 72 |
+
elif L == 2:
|
| 73 |
+
return [[d1, d2] for d1 in days_available for d2 in days_available if abs(d1 - d2) >= 2 and d1 < d2]
|
| 74 |
+
elif L == 3:
|
| 75 |
+
options = []
|
| 76 |
+
for d1 in days_available:
|
| 77 |
+
for d2 in days_available:
|
| 78 |
+
for d3 in days_available:
|
| 79 |
+
if d1 < d2 < d3:
|
| 80 |
+
if d2 - d1 == 1 and d3 - d2 >= 2:
|
| 81 |
+
options.append([d1, d2, d3])
|
| 82 |
+
elif d3 - d2 == 1 and d2 - d1 >= 2:
|
| 83 |
+
options.append([d1, d2, d3])
|
| 84 |
+
return options
|
| 85 |
+
return []
|
| 86 |
+
|
| 87 |
+
def generate_central_timetable_excel(timetable, tutorial_groups=None, lab_groups=None, output_path="Central_Timetable.xlsx"):
|
| 88 |
+
from openpyxl import Workbook
|
| 89 |
+
from openpyxl.styles import Alignment
|
| 90 |
+
|
| 91 |
+
wb = Workbook()
|
| 92 |
+
ws = wb.active
|
| 93 |
+
ws.title = "Central Timetable"
|
| 94 |
+
|
| 95 |
+
# Header Row
|
| 96 |
+
ws.append(["Day"] + time_slots)
|
| 97 |
+
|
| 98 |
+
for day in days:
|
| 99 |
+
row = [day]
|
| 100 |
+
for slot in time_slots:
|
| 101 |
+
entries = []
|
| 102 |
+
|
| 103 |
+
# Collect normal lecture/tutorial entries (years 1β5)
|
| 104 |
+
for year in sorted(timetable.keys()):
|
| 105 |
+
scheduled = timetable[year][day][slot]
|
| 106 |
+
entries.extend(scheduled)
|
| 107 |
+
|
| 108 |
+
# β
NEW: Append 1st-year tutorial groups with LG tags
|
| 109 |
+
if tutorial_groups:
|
| 110 |
+
for lg, sessions in tutorial_groups.items():
|
| 111 |
+
for d, s, r, cname in sessions:
|
| 112 |
+
if d == day and s == slot:
|
| 113 |
+
entries.append(f"{cname} (Tutorial) @ {r} ({lg})")
|
| 114 |
+
|
| 115 |
+
# β
NEW: Append 1st-year lab groups with LG tags
|
| 116 |
+
if lab_groups:
|
| 117 |
+
for lg, sessions in lab_groups.items():
|
| 118 |
+
for d, slot_block, r, cname in sessions:
|
| 119 |
+
if d == day and slot in slot_block:
|
| 120 |
+
entries.append(f"{cname} (Lab) @ {r} ({lg})")
|
| 121 |
+
|
| 122 |
+
row.append("\n".join(entries))
|
| 123 |
+
ws.append(row)
|
| 124 |
+
|
| 125 |
+
# β
Optional formatting
|
| 126 |
+
for col in ws.columns:
|
| 127 |
+
for cell in col:
|
| 128 |
+
cell.alignment = Alignment(wrapText=True)
|
| 129 |
+
|
| 130 |
+
# Set appropriate column widths
|
| 131 |
+
for i in range(2, len(time_slots) + 2):
|
| 132 |
+
ws.column_dimensions[chr(64 + i)].width = 30
|
| 133 |
+
|
| 134 |
+
wb.save(output_path)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def generate_available_halls_report(room_slots, output_path="Available_Halls.xlsx"):
|
| 139 |
+
from openpyxl import Workbook
|
| 140 |
+
|
| 141 |
+
wb = Workbook()
|
| 142 |
+
ws = wb.active
|
| 143 |
+
ws.title = "Available Halls"
|
| 144 |
+
|
| 145 |
+
# Header row: Slot names
|
| 146 |
+
ws.append(["Day"] + time_slots)
|
| 147 |
+
|
| 148 |
+
for day in days:
|
| 149 |
+
row_data = [day]
|
| 150 |
+
for slot in time_slots:
|
| 151 |
+
available_rooms = []
|
| 152 |
+
for room in room_slots:
|
| 153 |
+
key = f"{day}-{slot}"
|
| 154 |
+
if room_slots[room][key]:
|
| 155 |
+
available_rooms.append(room)
|
| 156 |
+
row_data.append(", ".join(sorted(available_rooms)))
|
| 157 |
+
ws.append(row_data)
|
| 158 |
+
|
| 159 |
+
wb.save(output_path)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def schedule_bs_ms_first_year(
|
| 163 |
+
course_df, room_df, room_slots, time_slots, days, output_dir,
|
| 164 |
+
first_year_lectures_by_day_slot=None,
|
| 165 |
+
timetable=None # β
NEW ARG: pass the main timetable dict (defaultdict)
|
| 166 |
+
):
|
| 167 |
+
from openpyxl import Workbook
|
| 168 |
+
from collections import defaultdict
|
| 169 |
+
import os
|
| 170 |
+
|
| 171 |
+
TUTORIAL_ROOMS = ["LHC 105", "LHC 106", "LHC 107", "LHC 108"]
|
| 172 |
+
LAB_SLOTS = ["2-3", "3-4", "4-5"]
|
| 173 |
+
LECTURE_SLOTS = ["8-9", "9-10", "10-11", "11-12", "12-1"]
|
| 174 |
+
LUNCH_SLOT = "1-2"
|
| 175 |
+
FORBIDDEN_5_6_DAYS = {"Monday", "Wednesday", "Friday"}
|
| 176 |
+
|
| 177 |
+
# Mapping TGs to LGs
|
| 178 |
+
tg_to_lg = {
|
| 179 |
+
"TG-1": "LG-1", "TG-2": "LG-1",
|
| 180 |
+
"TG-3": "LG-2", "TG-4": "LG-2",
|
| 181 |
+
"TG-5": "LG-3", "TG-6": "LG-3",
|
| 182 |
+
"TG-7": "LG-4", "TG-8": "LG-4",
|
| 183 |
+
"TG-9": "LG-5", "TG-10": "LG-5",
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
# Extract valid lab rooms
|
| 187 |
+
lab_rooms = []
|
| 188 |
+
for _, row in room_df.iterrows():
|
| 189 |
+
for col in ["Computer Lab", "Class Room", "Seminar Halls"]:
|
| 190 |
+
room = row.get(col)
|
| 191 |
+
if pd.notna(room) and ("lab" in str(room).lower() or "computer" in str(room).lower()):
|
| 192 |
+
lab_rooms.append(str(room).strip())
|
| 193 |
+
|
| 194 |
+
lab_groups = {f"LG-{i+1}": [] for i in range(5)}
|
| 195 |
+
tutorial_groups = {f"LG-{i+1}": [] for i in range(5)}
|
| 196 |
+
tutorial_slot_count = defaultdict(lambda: defaultdict(int)) # day -> slot -> count
|
| 197 |
+
used_slots_by_lg = defaultdict(set)
|
| 198 |
+
unscheduled = []
|
| 199 |
+
|
| 200 |
+
for _, row in course_df.iterrows():
|
| 201 |
+
cname = row["Course Name"]
|
| 202 |
+
subfolder = row["Sub folder"]
|
| 203 |
+
ltpc = row["[LTPC]"]
|
| 204 |
+
students = int(row["Total Students"])
|
| 205 |
+
L, T, P, C = parse_ltpc(ltpc)
|
| 206 |
+
|
| 207 |
+
codes = cname.split("-")
|
| 208 |
+
years = [extract_year_from_code(c) for c in codes if extract_year_from_code(c) is not None]
|
| 209 |
+
if not years or max(years) != 1:
|
| 210 |
+
continue # Not a 1st year course
|
| 211 |
+
|
| 212 |
+
# --- Schedule Labs ---
|
| 213 |
+
if P > 0:
|
| 214 |
+
for lg_index in range(5):
|
| 215 |
+
lg = f"LG-{lg_index+1}"
|
| 216 |
+
assigned = False
|
| 217 |
+
for day in days:
|
| 218 |
+
for room in lab_rooms:
|
| 219 |
+
if all(room_slots[room][f"{day}-{slot}"] for slot in LAB_SLOTS):
|
| 220 |
+
for slot in LAB_SLOTS:
|
| 221 |
+
room_slots[room][f"{day}-{slot}"] = False
|
| 222 |
+
used_slots_by_lg[lg].add(f"{day}-{slot}")
|
| 223 |
+
# β
NEW: Add to central timetable
|
| 224 |
+
if timetable:
|
| 225 |
+
timetable[1][day][slot].append(f"{cname} (Lab) @ {room} ({lg})")
|
| 226 |
+
lab_groups[lg].append((day, LAB_SLOTS, room, cname))
|
| 227 |
+
assigned = True
|
| 228 |
+
break
|
| 229 |
+
if assigned:
|
| 230 |
+
break
|
| 231 |
+
if not assigned:
|
| 232 |
+
unscheduled.append({
|
| 233 |
+
"Course Name": cname,
|
| 234 |
+
"LTPC": ltpc,
|
| 235 |
+
"Sub folder": subfolder,
|
| 236 |
+
"Reason": f"No available 2β5PM block for {lg}"
|
| 237 |
+
})
|
| 238 |
+
|
| 239 |
+
# --- Schedule Tutorials ---
|
| 240 |
+
if T == 1:
|
| 241 |
+
for lg_index in range(5):
|
| 242 |
+
lg = f"LG-{lg_index+1}"
|
| 243 |
+
tg1 = f"TG-{2*lg_index+1}"
|
| 244 |
+
tg2 = f"TG-{2*lg_index+2}"
|
| 245 |
+
assigned = 0
|
| 246 |
+
|
| 247 |
+
for day in days:
|
| 248 |
+
for slot in time_slots:
|
| 249 |
+
if slot == LUNCH_SLOT or slot in LAB_SLOTS or (slot == "5-6" and day in FORBIDDEN_5_6_DAYS):
|
| 250 |
+
continue
|
| 251 |
+
if slot in used_slots_by_lg[lg]:
|
| 252 |
+
continue
|
| 253 |
+
if tutorial_slot_count[day][slot] >= 3:
|
| 254 |
+
continue
|
| 255 |
+
|
| 256 |
+
available_rooms = [r for r in TUTORIAL_ROOMS if room_slots[r][f"{day}-{slot}"]]
|
| 257 |
+
if not available_rooms:
|
| 258 |
+
continue
|
| 259 |
+
|
| 260 |
+
room = available_rooms[0]
|
| 261 |
+
room_slots[room][f"{day}-{slot}"] = False
|
| 262 |
+
tutorial_slot_count[day][slot] += 1
|
| 263 |
+
tutorial_groups[lg].append((day, slot, room, cname))
|
| 264 |
+
used_slots_by_lg[lg].add(f"{day}-{slot}")
|
| 265 |
+
assigned += 1
|
| 266 |
+
|
| 267 |
+
# β
NEW: Add to central timetable
|
| 268 |
+
if timetable:
|
| 269 |
+
timetable[1][day][slot].append(f"{cname} (Tutorial) @ {room} ({lg})")
|
| 270 |
+
|
| 271 |
+
if assigned == 2:
|
| 272 |
+
break
|
| 273 |
+
if assigned == 2:
|
| 274 |
+
break
|
| 275 |
+
|
| 276 |
+
if assigned < 2:
|
| 277 |
+
unscheduled.append({
|
| 278 |
+
"Course Name": cname,
|
| 279 |
+
"LTPC": ltpc,
|
| 280 |
+
"Sub folder": subfolder,
|
| 281 |
+
"Reason": f"Tutorials for {tg1}, {tg2} (under {lg}) not scheduled"
|
| 282 |
+
})
|
| 283 |
+
|
| 284 |
+
# --- Write LG-wise Excel files with lectures ---
|
| 285 |
+
year_folder = os.path.join(output_dir, "1st Year")
|
| 286 |
+
os.makedirs(year_folder, exist_ok=True)
|
| 287 |
+
|
| 288 |
+
for lg in lab_groups:
|
| 289 |
+
wb = Workbook()
|
| 290 |
+
ws = wb.active
|
| 291 |
+
ws.title = lg
|
| 292 |
+
ws.append(["Day"] + time_slots)
|
| 293 |
+
|
| 294 |
+
for day in days:
|
| 295 |
+
row = [day]
|
| 296 |
+
for slot in time_slots:
|
| 297 |
+
val = ""
|
| 298 |
+
|
| 299 |
+
# Tutorials
|
| 300 |
+
for d, s, r, c in tutorial_groups[lg]:
|
| 301 |
+
if d == day and s == slot:
|
| 302 |
+
val += f"{c} (Tutorial) @ {r}\n"
|
| 303 |
+
|
| 304 |
+
# Labs
|
| 305 |
+
for d, slot_block, r, c in lab_groups[lg]:
|
| 306 |
+
if d == day and slot in slot_block:
|
| 307 |
+
val += f"{c} (Lab) @ {r}\n"
|
| 308 |
+
|
| 309 |
+
# Lectures
|
| 310 |
+
if first_year_lectures_by_day_slot:
|
| 311 |
+
for entry in first_year_lectures_by_day_slot[day][slot]:
|
| 312 |
+
if f"{day}-{slot}" not in used_slots_by_lg[lg]:
|
| 313 |
+
val += f"{entry} (Lecture)\n"
|
| 314 |
+
used_slots_by_lg[lg].add(f"{day}-{slot}")
|
| 315 |
+
|
| 316 |
+
row.append(val.strip())
|
| 317 |
+
ws.append(row)
|
| 318 |
+
|
| 319 |
+
# β
NEW: Rename file with LG-TG mapping
|
| 320 |
+
lg_index = int(lg.split("-")[1])
|
| 321 |
+
tg1 = f"TG-{2 * (lg_index - 1) + 1}"
|
| 322 |
+
tg2 = f"TG-{2 * (lg_index - 1) + 2}"
|
| 323 |
+
filename = f"{lg} ({tg1} & {tg2}).xlsx"
|
| 324 |
+
|
| 325 |
+
wb.save(os.path.join(year_folder, filename))
|
| 326 |
+
|
| 327 |
+
# --- Export Unscheduled ---
|
| 328 |
+
if unscheduled:
|
| 329 |
+
pd.DataFrame(unscheduled).to_excel(os.path.join(output_dir, "Unscheduled_LTP_Report.xlsx"), index=False)
|
| 330 |
+
|
| 331 |
+
return tutorial_groups, lab_groups, unscheduled
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
def is_slot_allowed_for_year(year, day, slot):
|
| 341 |
+
if slot == lunch_slot:
|
| 342 |
+
return False
|
| 343 |
+
if year == 1:
|
| 344 |
+
# BS-MS 1st Year: Lectures only between 8β1, no 5β6 on M/W/F
|
| 345 |
+
if slot not in ["8-9", "9-10", "10-11", "11-12", "12-1"]:
|
| 346 |
+
return False
|
| 347 |
+
if slot == "5-6" and day in ["Monday", "Wednesday", "Friday"]:
|
| 348 |
+
return False
|
| 349 |
+
if year in restricted_5_6_slots and slot == "5-6" and day in restricted_5_6_slots[year]:
|
| 350 |
+
return False
|
| 351 |
+
if year in [3, 4] and slot in no_class_3rd_4th.get(day, []):
|
| 352 |
+
return False
|
| 353 |
+
return True
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
def generate_timetable(course_df, room_df, output_dir, room_slots, timetable):
|
| 357 |
+
from openpyxl import Workbook
|
| 358 |
+
from openpyxl.styles import PatternFill
|
| 359 |
+
from collections import defaultdict
|
| 360 |
+
|
| 361 |
+
course_colors = {}
|
| 362 |
+
course_color_palette = ["FFCCCC", "CCFFCC", "CCCCFF", "FFFFCC", "CCFFFF", "FFCCFF", "F0E68C", "E6E6FA"]
|
| 363 |
+
color_index = 0
|
| 364 |
+
first_year_lectures_by_day_slot = defaultdict(lambda: defaultdict(list))
|
| 365 |
+
unscheduled_courses = []
|
| 366 |
+
|
| 367 |
+
room_data = []
|
| 368 |
+
for i in range(len(room_df)):
|
| 369 |
+
for hall_type in ["Class Room", "Computer Lab", "Seminar Halls"]:
|
| 370 |
+
room = room_df[hall_type][i]
|
| 371 |
+
cap = room_df["Capacity"][i]
|
| 372 |
+
if pd.notna(room):
|
| 373 |
+
room_data.append((room, cap, hall_type))
|
| 374 |
+
|
| 375 |
+
grouped = course_df.groupby("Sub folder")
|
| 376 |
+
|
| 377 |
+
for subfolder, group in grouped:
|
| 378 |
+
sorted_courses = group.sort_values(by="Total Students", ascending=False)
|
| 379 |
+
for _, row in sorted_courses.iterrows():
|
| 380 |
+
cname = row["Course Name"]
|
| 381 |
+
course_group = cname.split("-")
|
| 382 |
+
ltpc = row["[LTPC]"]
|
| 383 |
+
students = int(row["Total Students"])
|
| 384 |
+
years_in_group = [extract_year_from_code(part) for part in course_group]
|
| 385 |
+
valid_years = list(filter(None, years_in_group))
|
| 386 |
+
if not valid_years or max(valid_years) > 5:
|
| 387 |
+
unscheduled_courses.append({"Course Name": cname, "LTPC": ltpc, "Sub folder": subfolder, "Reason": "Invalid or unsupported year in course code"})
|
| 388 |
+
continue
|
| 389 |
+
year = max(valid_years)
|
| 390 |
+
|
| 391 |
+
L, T, P, C = parse_ltpc(ltpc)
|
| 392 |
+
|
| 393 |
+
if cname not in course_colors:
|
| 394 |
+
course_colors[cname] = course_color_palette[color_index % len(course_color_palette)]
|
| 395 |
+
color_index += 1
|
| 396 |
+
|
| 397 |
+
# --- Assign Room ---
|
| 398 |
+
assigned_room = None
|
| 399 |
+
if L > 0:
|
| 400 |
+
for room, cap, hall_type in sorted(room_data, key=lambda x: abs(x[1] - students)):
|
| 401 |
+
if cap >= students and hall_type in ["Class Room", "Seminar Halls"]:
|
| 402 |
+
assigned_room = (room, hall_type)
|
| 403 |
+
break
|
| 404 |
+
else:
|
| 405 |
+
for room, cap, hall_type in sorted(room_data, key=lambda x: abs(x[1] - students)):
|
| 406 |
+
if cap >= students:
|
| 407 |
+
assigned_room = (room, hall_type)
|
| 408 |
+
break
|
| 409 |
+
|
| 410 |
+
if not assigned_room:
|
| 411 |
+
unscheduled_courses.append({"Course Name": cname, "LTPC": ltpc, "Sub folder": subfolder, "Reason": "No suitable room"})
|
| 412 |
+
continue
|
| 413 |
+
|
| 414 |
+
room_name, hall_type = assigned_room
|
| 415 |
+
lecture_day_options = find_valid_lecture_days(L, list(range(len(days))))
|
| 416 |
+
best_assigned = []
|
| 417 |
+
max_assigned = 0
|
| 418 |
+
|
| 419 |
+
for day_indices in lecture_day_options:
|
| 420 |
+
temp_assigned = []
|
| 421 |
+
for idx in day_indices:
|
| 422 |
+
day = days[idx]
|
| 423 |
+
for slot in time_slots:
|
| 424 |
+
if not is_slot_allowed_for_year(year, day, slot):
|
| 425 |
+
continue
|
| 426 |
+
if hall_type == "Seminar Halls" and slot in seminar_hall_blocked_slots:
|
| 427 |
+
continue
|
| 428 |
+
if not room_slots[room_name][f"{day}-{slot}"]:
|
| 429 |
+
continue
|
| 430 |
+
if any(is_bcp_blocked(part, day, slot) for part in course_group):
|
| 431 |
+
continue
|
| 432 |
+
|
| 433 |
+
entry = f"{cname} @ {room_name}"
|
| 434 |
+
timetable[year][day][slot].append(entry)
|
| 435 |
+
room_slots[room_name][f"{day}-{slot}"] = False
|
| 436 |
+
|
| 437 |
+
if year == 1:
|
| 438 |
+
first_year_lectures_by_day_slot[day][slot].append(entry)
|
| 439 |
+
|
| 440 |
+
temp_assigned.append(idx)
|
| 441 |
+
break
|
| 442 |
+
if len(temp_assigned) > max_assigned:
|
| 443 |
+
best_assigned = temp_assigned
|
| 444 |
+
max_assigned = len(temp_assigned)
|
| 445 |
+
if max_assigned == L:
|
| 446 |
+
break
|
| 447 |
+
|
| 448 |
+
if max_assigned < L:
|
| 449 |
+
unscheduled_courses.append({
|
| 450 |
+
"Course Name": cname,
|
| 451 |
+
"LTPC": ltpc,
|
| 452 |
+
"Sub folder": subfolder,
|
| 453 |
+
"Reason": f"Only {max_assigned} of {L} lectures scheduled"
|
| 454 |
+
})
|
| 455 |
+
|
| 456 |
+
# --- Schedule Tutorials for Non-1st Year ---
|
| 457 |
+
if T == 1 and year != 1:
|
| 458 |
+
tutorial_scheduled = False
|
| 459 |
+
for i, day in enumerate(days):
|
| 460 |
+
if i in best_assigned:
|
| 461 |
+
continue
|
| 462 |
+
for slot in time_slots:
|
| 463 |
+
if not is_slot_allowed_for_year(year, day, slot):
|
| 464 |
+
continue
|
| 465 |
+
if not room_slots[room_name][f"{day}-{slot}"]:
|
| 466 |
+
continue
|
| 467 |
+
if any(is_bcp_blocked(part, day, slot) for part in course_group):
|
| 468 |
+
continue
|
| 469 |
+
if hall_type not in ["Computer Lab", "Class Room", "Seminar Halls"]:
|
| 470 |
+
continue
|
| 471 |
+
|
| 472 |
+
timetable[year][day][slot].append(f"{cname} Tutorial @ {room_name}")
|
| 473 |
+
room_slots[room_name][f"{day}-{slot}"] = False
|
| 474 |
+
tutorial_scheduled = True
|
| 475 |
+
break
|
| 476 |
+
if tutorial_scheduled:
|
| 477 |
+
break
|
| 478 |
+
if not tutorial_scheduled:
|
| 479 |
+
unscheduled_courses.append({
|
| 480 |
+
"Course Name": cname,
|
| 481 |
+
"LTPC": ltpc,
|
| 482 |
+
"Sub folder": subfolder,
|
| 483 |
+
"Reason": "Tutorial slot unavailable due to conflicts/constraints"
|
| 484 |
+
})
|
| 485 |
+
|
| 486 |
+
# --- Write Timetables (Years 2β5) ---
|
| 487 |
+
year_major_courses = defaultdict(lambda: defaultdict(list))
|
| 488 |
+
for _, row in course_df.iterrows():
|
| 489 |
+
cname = row["Course Name"]
|
| 490 |
+
years = [extract_year_from_code(code) for code in cname.split("-")]
|
| 491 |
+
valid_years = list(filter(None, years))
|
| 492 |
+
if not valid_years or max(valid_years) > 5:
|
| 493 |
+
continue
|
| 494 |
+
year = max(valid_years)
|
| 495 |
+
for part in cname.split("-"):
|
| 496 |
+
code = part.lower()
|
| 497 |
+
for prefix, major in {
|
| 498 |
+
"mat": "Math", "msm": "Math", "phy": "Physics", "msp": "Physics", "chy": "Chemistry", "msc": "Chemistry",
|
| 499 |
+
"bio": "Biology", "msb": "Biology", "ees": "Earth Science", "dsc": "Data Science", "i2m": "Math",
|
| 500 |
+
"i2p": "Physics", "i2c": "Chemistry", "i2b": "Biology"
|
| 501 |
+
}.items():
|
| 502 |
+
if code.startswith(prefix):
|
| 503 |
+
year_major_courses[year][major].append(cname)
|
| 504 |
+
|
| 505 |
+
for year in timetable:
|
| 506 |
+
if year == 1:
|
| 507 |
+
continue # 1st-year handled separately in LG-wise routine
|
| 508 |
+
|
| 509 |
+
year_folder = os.path.join(output_dir, f"{ordinal(year)} Year")
|
| 510 |
+
os.makedirs(year_folder, exist_ok=True)
|
| 511 |
+
|
| 512 |
+
for major in year_major_courses[year]:
|
| 513 |
+
fname = f"{ordinal(year)} Year, {major} Major.xlsx"
|
| 514 |
+
fpath = os.path.join(year_folder, fname)
|
| 515 |
+
wb = Workbook()
|
| 516 |
+
ws = wb.active
|
| 517 |
+
ws.append(["Day"] + time_slots)
|
| 518 |
+
|
| 519 |
+
for row_idx, day in enumerate(days, start=2):
|
| 520 |
+
row_data = [day]
|
| 521 |
+
for col_idx, slot in enumerate(time_slots, start=2):
|
| 522 |
+
entries = timetable[year][day][slot]
|
| 523 |
+
val = ", ".join(e for e in entries if any(c in e for c in year_major_courses[year][major]))
|
| 524 |
+
row_data.append(val)
|
| 525 |
+
ws.append(row_data)
|
| 526 |
+
|
| 527 |
+
for col_idx, slot in enumerate(time_slots, start=2):
|
| 528 |
+
cell = ws.cell(row=row_idx, column=col_idx)
|
| 529 |
+
entries = timetable[year][day][slot]
|
| 530 |
+
for cname in year_major_courses[year][major]:
|
| 531 |
+
if any(cname in e for e in entries):
|
| 532 |
+
cell.fill = PatternFill(start_color=course_colors[cname], end_color=course_colors[cname], fill_type="solid")
|
| 533 |
+
break
|
| 534 |
+
wb.save(fpath)
|
| 535 |
+
|
| 536 |
+
# β
NEW: Rename output report
|
| 537 |
+
if unscheduled_courses:
|
| 538 |
+
pd.DataFrame(unscheduled_courses).to_excel("Unscheduled_LTP_Report.xlsx", index=False)
|
| 539 |
+
|
| 540 |
+
return first_year_lectures_by_day_slot
|
| 541 |
+
|
| 542 |
+
|
| 543 |
+
|
| 544 |
+
|
| 545 |
+
|
| 546 |
+
|
| 547 |
+
|
| 548 |
+
# -- All other imported functions like: generate_available_halls_report, generate_central_timetable_excel, schedule_bs_ms_first_year, generate_timetable --
|
| 549 |
+
# You already posted them correctly. I won't repeat to save space unless you ask.
|
| 550 |
+
|
| 551 |
+
# --- Final Working Process Function ---
|
| 552 |
+
def process(course_file, room_file):
|
| 553 |
+
import shutil
|
| 554 |
+
import zipfile
|
| 555 |
+
from collections import defaultdict
|
| 556 |
+
|
| 557 |
+
course_df = pd.read_excel(course_file.name)
|
| 558 |
+
room_df = pd.read_excel(room_file.name)
|
| 559 |
+
|
| 560 |
+
output_dir = "generated_routines"
|
| 561 |
+
output_1st_year_dir = "generated_1st_year_LT"
|
| 562 |
+
|
| 563 |
+
# --- Clean old folders ---
|
| 564 |
+
for path in [output_dir, output_1st_year_dir]:
|
| 565 |
+
if os.path.exists(path):
|
| 566 |
+
shutil.rmtree(path)
|
| 567 |
+
os.makedirs(path)
|
| 568 |
+
|
| 569 |
+
# --- Initialize room availability map ---
|
| 570 |
+
room_slots = defaultdict(lambda: defaultdict(lambda: True))
|
| 571 |
+
|
| 572 |
+
# β
BLOCK 2β5 PM for BS-MS 1st Year labs
|
| 573 |
+
for day in days:
|
| 574 |
+
for room in room_slots:
|
| 575 |
+
for slot in ["2-3", "3-4", "4-5"]:
|
| 576 |
+
room_slots[room][f"{day}-{slot}"] = False
|
| 577 |
+
|
| 578 |
+
# β
NEW: Initialize central timetable
|
| 579 |
+
timetable = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
|
| 580 |
+
|
| 581 |
+
# Step 1: Generate lectures (all years) + fill timetable for 2ndβ5th year
|
| 582 |
+
first_year_lectures = generate_timetable(course_df, room_df, output_dir, room_slots, timetable)
|
| 583 |
+
|
| 584 |
+
# Step 2: Schedule 1st year labs + tutorials + lectures (and update timetable)
|
| 585 |
+
tg, lg, unscheduled_1st = schedule_bs_ms_first_year(
|
| 586 |
+
course_df,
|
| 587 |
+
room_df,
|
| 588 |
+
room_slots,
|
| 589 |
+
time_slots,
|
| 590 |
+
days,
|
| 591 |
+
output_1st_year_dir,
|
| 592 |
+
first_year_lectures_by_day_slot=first_year_lectures,
|
| 593 |
+
timetable=timetable # β
NEW ARG
|
| 594 |
+
)
|
| 595 |
+
|
| 596 |
+
# Step 3: Export reports after full timetable is complete
|
| 597 |
+
generate_available_halls_report(room_slots)
|
| 598 |
+
generate_central_timetable_excel(timetable)
|
| 599 |
+
|
| 600 |
+
# Step 4: Zip Final_Timetable (2ndβ5th Year)
|
| 601 |
+
zip_path_main = "Final_Timetable.zip"
|
| 602 |
+
with zipfile.ZipFile(zip_path_main, 'w') as zipf:
|
| 603 |
+
for foldername, _, filenames in os.walk(output_dir):
|
| 604 |
+
for filename in filenames:
|
| 605 |
+
filepath = os.path.join(foldername, filename)
|
| 606 |
+
arcname = os.path.relpath(filepath, output_dir)
|
| 607 |
+
zipf.write(filepath, arcname)
|
| 608 |
+
|
| 609 |
+
# Step 5: Zip 1st_Year_Labs_Tutorials.zip (includes lectures + labs + tutorials)
|
| 610 |
+
zip_path_1st = "1st_Year_Labs_Tutorials.zip"
|
| 611 |
+
with zipfile.ZipFile(zip_path_1st, 'w') as zipf:
|
| 612 |
+
for foldername, _, filenames in os.walk(output_1st_year_dir):
|
| 613 |
+
for filename in filenames:
|
| 614 |
+
filepath = os.path.join(foldername, filename)
|
| 615 |
+
arcname = os.path.relpath(filepath, output_1st_year_dir)
|
| 616 |
+
zipf.write(filepath, arcname)
|
| 617 |
+
|
| 618 |
+
# Step 6: Return final file outputs
|
| 619 |
+
return (
|
| 620 |
+
zip_path_main,
|
| 621 |
+
zip_path_1st,
|
| 622 |
+
("Unscheduled_LTP_Report.xlsx" if os.path.exists("Unscheduled_LTP_Report.xlsx") else None), # β
RENAMED
|
| 623 |
+
("Available_Halls.xlsx" if os.path.exists("Available_Halls.xlsx") else None),
|
| 624 |
+
("Central_Timetable.xlsx" if os.path.exists("Central_Timetable.xlsx") else None)
|
| 625 |
+
)
|
| 626 |
+
|
| 627 |
+
|
| 628 |
+
|
| 629 |
+
|
| 630 |
+
# --- Gradio UI Launcher ---
|
| 631 |
+
import gradio as gr
|
| 632 |
+
|
| 633 |
+
def launch_ui():
|
| 634 |
+
with gr.Blocks(css="""
|
| 635 |
+
.centered-title {
|
| 636 |
+
text-align: center;
|
| 637 |
+
font-size: 2.2em;
|
| 638 |
+
font-weight: bold;
|
| 639 |
+
color: #2b3d4f;
|
| 640 |
+
background: #e0f7fa;
|
| 641 |
+
padding: 10px;
|
| 642 |
+
border-radius: 12px;
|
| 643 |
+
margin-bottom: 20px;
|
| 644 |
+
}
|
| 645 |
+
.highlight-button {
|
| 646 |
+
background-color: #1976D2 !important;
|
| 647 |
+
color: white !important;
|
| 648 |
+
border-radius: 8px;
|
| 649 |
+
padding: 10px 20px;
|
| 650 |
+
}
|
| 651 |
+
""") as iface:
|
| 652 |
+
|
| 653 |
+
# πΌ Banner Image (Header)
|
| 654 |
+
gr.Image(
|
| 655 |
+
value="https://drive.google.com/uc?id=1UDlI15QVKy0JSkfFCG7yMAR7esv35O-v",
|
| 656 |
+
height=400,
|
| 657 |
+
width=8000, # β
Reasonable width
|
| 658 |
+
show_label=False,
|
| 659 |
+
container=False
|
| 660 |
+
)
|
| 661 |
+
|
| 662 |
+
# π·οΈ Styled Title
|
| 663 |
+
gr.HTML('<div class="centered-title">IISER TVM Course Timetable Scheduler</div>')
|
| 664 |
+
|
| 665 |
+
# π File Inputs
|
| 666 |
+
with gr.Row():
|
| 667 |
+
course_input = gr.File(label="π Upload Course Excel File (.xlsx)")
|
| 668 |
+
room_input = gr.File(label="π¬ Upload Room Details Excel File (.xlsx)")
|
| 669 |
+
|
| 670 |
+
# π Styled Button
|
| 671 |
+
run_button = gr.Button("Generate Timetable", elem_classes="highlight-button")
|
| 672 |
+
|
| 673 |
+
# π§Ύ Outputs (Timetable files)
|
| 674 |
+
with gr.Row():
|
| 675 |
+
timetable_output = gr.File(label="π Download Timetable ZIP")
|
| 676 |
+
first_year_output = gr.File(label="π§ͺ 1st Year Labs & Tutorials ZIP")
|
| 677 |
+
|
| 678 |
+
with gr.Row():
|
| 679 |
+
unscheduled_output = gr.File(label="β οΈ Unscheduled L/T/P Report (if any)")
|
| 680 |
+
available_halls_output = gr.File(label="π© Available Halls Report")
|
| 681 |
+
central_output = gr.File(label="π Central Timetable (All Courses)")
|
| 682 |
+
|
| 683 |
+
# π§ Process Trigger
|
| 684 |
+
run_button.click(
|
| 685 |
+
fn=process,
|
| 686 |
+
inputs=[course_input, room_input],
|
| 687 |
+
outputs=[
|
| 688 |
+
timetable_output,
|
| 689 |
+
first_year_output,
|
| 690 |
+
unscheduled_output,
|
| 691 |
+
available_halls_output,
|
| 692 |
+
central_output
|
| 693 |
+
]
|
| 694 |
+
)
|
| 695 |
+
|
| 696 |
+
iface.launch(share=True, debug=True)
|
| 697 |
+
|
| 698 |
+
|
| 699 |
+
if __name__ == "__main__":
|
| 700 |
+
launch_ui()
|