saptak21 commited on
Commit
e993a5c
Β·
verified Β·
1 Parent(s): 06ce407

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +700 -0
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()