| | |
| | """ |
| | Improved Automatic Time Table Generation Agent (Genetic Algorithm) |
| | - Gradio interface (offline) |
| | - Adaptive mutation rate, better crossover, visualizations, exports |
| | """ |
| |
|
| | import io |
| | import random |
| | import math |
| | from typing import List, Dict, Tuple, Optional |
| | import tempfile |
| | import datetime |
| |
|
| | import numpy as np |
| | import pandas as pd |
| | import gradio as gr |
| | import plotly.graph_objects as go |
| | import plotly.express as px |
| |
|
| | |
| | |
| | |
| | def parse_lines(text: str) -> List[str]: |
| | return [line.strip() for line in (text or "").splitlines() if line.strip()] |
| |
|
| | def parse_teacher_unavailability(text: str) -> Dict[str, List[Tuple[str,str]]]: |
| | d = {} |
| | for ln in (text or "").splitlines(): |
| | ln = ln.strip() |
| | if not ln: continue |
| | parts = [p.strip() for p in ln.split(",")] |
| | if len(parts) >= 3: |
| | teacher, day, slot = parts[0], parts[1], parts[2] |
| | d.setdefault(teacher, []).append((day, slot)) |
| | return d |
| |
|
| | def parse_course_teacher_pref(text: str) -> Dict[str, List[str]]: |
| | d = {} |
| | for ln in (text or "").splitlines(): |
| | ln = ln.strip() |
| | if not ln: continue |
| | if ":" in ln: |
| | course, rest = ln.split(":", 1) |
| | teachers = [t.strip() for t in rest.split(",") if t.strip()] |
| | if teachers: |
| | d[course.strip()] = teachers |
| | return d |
| |
|
| | def parse_room_constraints(text: str) -> Dict[str, List[str]]: |
| | d = {} |
| | for ln in (text or "").splitlines(): |
| | ln = ln.strip() |
| | if not ln: continue |
| | if ":" in ln: |
| | course, rest = ln.split(":", 1) |
| | rooms = [r.strip() for r in rest.split(",") if r.strip()] |
| | if rooms: |
| | d[course.strip()] = rooms |
| | return d |
| |
|
| | |
| | |
| | |
| | class TimetableGA: |
| | def __init__( |
| | self, |
| | courses: List[str], |
| | teachers: List[str], |
| | rooms: List[str], |
| | days: List[str], |
| | slots: List[str], |
| | teacher_unavailable: Dict[str, List[Tuple[str,str]]], |
| | course_teacher_pref: Dict[str, List[str]], |
| | room_constraints: Dict[str, List[str]], |
| | population_size: int = 80, |
| | generations: int = 350, |
| | mutation_rate: float = 0.06, |
| | elitism: int = 2, |
| | seed: Optional[int] = None, |
| | ): |
| | self.courses = courses |
| | self.teachers = teachers |
| | self.rooms = rooms |
| | self.days = days |
| | self.slots = slots |
| | self.times = [(d, s) for d in days for s in slots] |
| | self.num_periods = len(self.times) |
| | self.num_courses = len(courses) |
| |
|
| | self.teacher_unavailable = teacher_unavailable |
| | self.course_teacher_pref = course_teacher_pref |
| | self.room_constraints = room_constraints |
| |
|
| | self.population_size = max(10, int(population_size)) |
| | self.generations = max(1, int(generations)) |
| | self.base_mutation_rate = float(mutation_rate) |
| | self.mutation_rate = float(mutation_rate) |
| | self.elitism = max(0, int(elitism)) |
| |
|
| | if seed is not None: |
| | random.seed(int(seed)) |
| | np.random.seed(int(seed)) |
| |
|
| | def _random_individual(self): |
| | |
| | if self.num_courses <= self.num_periods: |
| | period_indices = np.random.choice(self.num_periods, size=self.num_courses, replace=False) |
| | else: |
| | period_indices = np.random.randint(0, self.num_periods, size=self.num_courses) |
| | room_indices = np.random.randint(0, len(self.rooms), size=self.num_courses) |
| | teacher_indices = np.zeros(self.num_courses, dtype=int) |
| | for i, c in enumerate(self.courses): |
| | prefs = self.course_teacher_pref.get(c) |
| | if prefs: |
| | |
| | teacher_indices[i] = self.teachers.index(random.choice(prefs)) |
| | else: |
| | teacher_indices[i] = np.random.randint(0, len(self.teachers)) |
| | return (period_indices.astype(int), room_indices.astype(int), teacher_indices.astype(int)) |
| |
|
| | def _fitness(self, individual) -> float: |
| | p, r, t = individual |
| | penalties = 0.0 |
| |
|
| | |
| | teacher_slot = {} |
| | for i in range(self.num_courses): |
| | key = (int(t[i]), int(p[i])) |
| | teacher_slot.setdefault(key, 0) |
| | teacher_slot[key] += 1 |
| | teacher_conflicts = sum(max(0, c-1) for c in teacher_slot.values()) |
| | penalties += teacher_conflicts * 250.0 |
| |
|
| | |
| | room_slot = {} |
| | for i in range(self.num_courses): |
| | key = (int(r[i]), int(p[i])) |
| | room_slot.setdefault(key, 0) |
| | room_slot[key] += 1 |
| | room_conflicts = sum(max(0, c-1) for c in room_slot.values()) |
| | penalties += room_conflicts * 180.0 |
| |
|
| | |
| | unavail = 0 |
| | for i in range(self.num_courses): |
| | teacher = self.teachers[int(t[i])] |
| | period = self.times[int(p[i])] |
| | if teacher in self.teacher_unavailable and period in self.teacher_unavailable[teacher]: |
| | unavail += 1 |
| | penalties += unavail * 300.0 |
| |
|
| | |
| | pref_violations = 0 |
| | for i, c in enumerate(self.courses): |
| | prefs = self.course_teacher_pref.get(c) |
| | if prefs: |
| | chosen = self.teachers[int(t[i])] |
| | if chosen not in prefs: |
| | pref_violations += 1 |
| | penalties += pref_violations * 8.0 |
| |
|
| | |
| | room_viol = 0 |
| | for i, c in enumerate(self.courses): |
| | allowed = self.room_constraints.get(c) |
| | if allowed: |
| | chosen_room = self.rooms[int(r[i])] |
| | if chosen_room not in allowed: |
| | room_viol += 1 |
| | penalties += room_viol * 12.0 |
| |
|
| | |
| | teacher_workload = {} |
| | for i in range(self.num_courses): |
| | teacher_workload.setdefault(int(t[i]), 0) |
| | teacher_workload[int(t[i])] += 1 |
| | |
| | workloads = np.array(list(teacher_workload.values()), dtype=float) if teacher_workload else np.array([0.0]) |
| | if workloads.size > 1: |
| | variance = float(np.var(workloads)) |
| | penalties += variance * 5.0 |
| |
|
| | base = 20000.0 |
| | score = base - penalties |
| | return float(score) |
| |
|
| | def _crossover(self, a, b): |
| | |
| | a_p, a_r, a_t = a |
| | b_p, b_r, b_t = b |
| | if self.num_courses <= 2: |
| | return a, b |
| | i1 = np.random.randint(1, self.num_courses - 1) |
| | i2 = np.random.randint(i1, self.num_courses) |
| | def mix(x, y): |
| | child = x.copy() |
| | child[i1:i2] = y[i1:i2] |
| | return child |
| | c1 = (mix(a_p, b_p).copy(), mix(a_r, b_r).copy(), mix(a_t, b_t).copy()) |
| | c2 = (mix(b_p, a_p).copy(), mix(b_r, a_r).copy(), mix(b_t, a_t).copy()) |
| | return c1, c2 |
| |
|
| | def _mutate(self, ind, mutate_rate): |
| | p, r, t = ind |
| | for i in range(self.num_courses): |
| | if random.random() < mutate_rate: |
| | |
| | p[i] = random.randint(0, self.num_periods - 1) |
| | if random.random() < mutate_rate: |
| | |
| | r[i] = random.randint(0, len(self.rooms) - 1) |
| | if random.random() < mutate_rate: |
| | |
| | prefs = self.course_teacher_pref.get(self.courses[i]) |
| | if prefs: |
| | t[i] = self.teachers.index(random.choice(prefs)) |
| | else: |
| | t[i] = random.randint(0, len(self.teachers) - 1) |
| | return (p, r, t) |
| |
|
| | def run(self, verbose=False, progress_callback=None): |
| | |
| | population = [self._random_individual() for _ in range(self.population_size)] |
| | fitnesses = [self._fitness(ind) for ind in population] |
| | best_idx = int(np.argmax(fitnesses)) |
| | best = population[best_idx] |
| | best_score = fitnesses[best_idx] |
| | stagnation = 0 |
| | last_improve_gen = 0 |
| |
|
| | for gen in range(self.generations): |
| | |
| | self.mutation_rate = self.base_mutation_rate * (0.98 ** gen) |
| | if gen - last_improve_gen > max(10, self.generations // 40): |
| | |
| | self.mutation_rate = min(0.5, self.mutation_rate * 1.6) |
| |
|
| | ranked = sorted(zip(fitnesses, population), key=lambda x: x[0], reverse=True) |
| | new_pop = [p for _, p in ranked[:self.elitism]] |
| |
|
| | |
| | while len(new_pop) < self.population_size: |
| | |
| | i1, i2 = random.randrange(self.population_size), random.randrange(self.population_size) |
| | parent1 = population[i1] if fitnesses[i1] > fitnesses[i2] else population[i2] |
| | i3, i4 = random.randrange(self.population_size), random.randrange(self.population_size) |
| | parent2 = population[i3] if fitnesses[i3] > fitnesses[i4] else population[i4] |
| | c1, c2 = self._crossover(parent1, parent2) |
| | c1 = self._mutate(c1, self.mutation_rate) |
| | c2 = self._mutate(c2, self.mutation_rate) |
| | new_pop.extend([c1, c2]) |
| |
|
| | population = new_pop[:self.population_size] |
| | fitnesses = [self._fitness(ind) for ind in population] |
| |
|
| | gen_best = max(fitnesses) |
| | if gen_best > best_score: |
| | best_score = gen_best |
| | best = population[int(np.argmax(fitnesses))] |
| | last_improve_gen = gen |
| |
|
| | |
| | if progress_callback is not None: |
| | try: |
| | progress_callback(gen + 1, self.generations, best_score) |
| | except Exception: |
| | pass |
| |
|
| | if verbose and (gen % max(1, self.generations // 10) == 0): |
| | print(f"Gen {gen} best {best_score:.2f} mut_rate {self.mutation_rate:.4f}") |
| |
|
| | |
| | if best_score >= 19990.0: |
| | break |
| |
|
| | return {"best": best, "score": best_score, "times": self.times, "generations": gen + 1} |
| |
|
| | |
| | |
| | |
| | def individual_to_dataframe(individual, courses, teachers, rooms, times): |
| | p, r, t = individual |
| | rows = [] |
| | for i, course in enumerate(courses): |
| | idx = int(p[i]) |
| | day, slot = times[idx] |
| | rows.append({ |
| | "Course": course, |
| | "Teacher": teachers[int(t[i])], |
| | "Room": rooms[int(r[i])], |
| | "Day": day, |
| | "Slot": slot |
| | }) |
| | df = pd.DataFrame(rows) |
| | |
| | day_order = {d:i for i,d in enumerate([d for d,_ in times])} |
| | df["Day_order"] = df["Day"].map(day_order) |
| | df = df.sort_values(["Day_order","Slot"]).reset_index(drop=True).drop(columns=["Day_order"]) |
| | return df |
| |
|
| | def dataframe_to_csv_bytes(df: pd.DataFrame) -> bytes: |
| | buf = io.StringIO() |
| | df.to_csv(buf, index=False) |
| | return buf.getvalue().encode("utf-8") |
| |
|
| | def dataframe_to_xlsx_bytes(df: pd.DataFrame) -> bytes: |
| | buf = io.BytesIO() |
| | with pd.ExcelWriter(buf, engine="openpyxl") as writer: |
| | df.to_excel(writer, index=False, sheet_name="Timetable") |
| | buf.seek(0) |
| | return buf.read() |
| |
|
| | |
| | |
| | |
| | def compute_conflicts(df: pd.DataFrame): |
| | tconf = df.groupby(["Teacher","Day","Slot"]).size().reset_index(name="count") |
| | tconf = tconf[tconf["count"]>1].copy() |
| | rconf = df.groupby(["Room","Day","Slot"]).size().reset_index(name="count") |
| | rconf = rconf[rconf["count"]>1].copy() |
| | return tconf, rconf |
| |
|
| | def make_week_grid_plot(df: pd.DataFrame, days: List[str], slots: List[str]): |
| | |
| | grid = [["" for _ in slots] for _ in days] |
| | for _, row in df.iterrows(): |
| | try: |
| | d_idx = days.index(row["Day"]) |
| | s_idx = slots.index(row["Slot"]) |
| | grid[d_idx][s_idx] = f"{row['Course']}\n({row['Teacher']})" |
| | except ValueError: |
| | continue |
| | |
| | fig = go.Figure() |
| | fig.add_trace(go.Table( |
| | header=dict(values=["Day/Slot"] + slots, align="center"), |
| | cells=dict(values=[[d] for d in days] + list(map(list, zip(*grid))), align="left", height=40) |
| | )) |
| | fig.update_layout(margin=dict(l=5,r=5,t=20,b=5), height=400 + 30*len(days)) |
| | return fig |
| |
|
| | def make_conflict_heatmap(df: pd.DataFrame, days: List[str], slots: List[str], teachers: List[str], rooms: List[str]): |
| | |
| | time_labels = [f"{d}\n{s}" for d in days for s in slots] |
| | teacher_grid = np.zeros((len(teachers), len(time_labels)), dtype=int) |
| | for _, row in df.iterrows(): |
| | teacher_idx = teachers.index(row["Teacher"]) |
| | time_idx = days.index(row["Day"]) * len(slots) + slots.index(row["Slot"]) |
| | teacher_grid[teacher_idx, time_idx] += 1 |
| | |
| | teacher_fig = px.imshow(teacher_grid, labels=dict(x="Time", y="Teacher", color="Count"), |
| | x=time_labels, y=teachers, aspect="auto") |
| | teacher_fig.update_layout(title="Teacher assignment heatmap", height=350) |
| | room_grid = np.zeros((len(rooms), len(time_labels)), dtype=int) |
| | for _, row in df.iterrows(): |
| | room_idx = rooms.index(row["Room"]) |
| | time_idx = days.index(row["Day"]) * len(slots) + slots.index(row["Slot"]) |
| | room_grid[room_idx, time_idx] += 1 |
| | room_fig = px.imshow(room_grid, labels=dict(x="Time", y="Room", color="Count"), |
| | x=time_labels, y=rooms, aspect="auto") |
| | room_fig.update_layout(title="Room booking heatmap", height=350) |
| | return teacher_fig, room_fig |
| |
|
| | |
| | |
| | |
| | def assistant_reply(df: Optional[pd.DataFrame], query: str) -> str: |
| | if df is None or df.empty: |
| | return "No timetable available. Generate a timetable first." |
| |
|
| | q = (query or "").strip().lower() |
| | if not q: |
| | return "Try: 'show conflicts', 'schedule for T1_Ali', 'when is C2_Physics', or 'summary'." |
| |
|
| | |
| | if "conflict" in q or "clash" in q or "problem" in q: |
| | tconf, rconf = compute_conflicts(df) |
| | lines = [] |
| | if not tconf.empty: |
| | lines.append("Teacher conflicts:") |
| | for _, r in tconf.iterrows(): |
| | lines.append(f"- {r['Teacher']} has {int(r['count'])} classes at {r['Day']} {r['Slot']}") |
| | else: |
| | lines.append("No teacher conflicts detected.") |
| | if not rconf.empty: |
| | lines.append("Room conflicts:") |
| | for _, r in rconf.iterrows(): |
| | lines.append(f"- {r['Room']} has {int(r['count'])} bookings at {r['Day']} {r['Slot']}") |
| | else: |
| | lines.append("No room conflicts detected.") |
| | lines.append("Fix ideas: 1) reassign one of the conflicting classes to a different slot/room; 2) allow alternate teacher; 3) relax room constraints.") |
| | return "\n".join(lines) |
| |
|
| | |
| | if "schedule for" in q or q.startswith("show schedule") or q.startswith("show for"): |
| | |
| | words = q.replace("schedule for", "").replace("show schedule for", "").replace("show for", "").strip() |
| | if not words: |
| | return "Specify teacher, e.g., 'Schedule for T2_Sara'" |
| | |
| | cand = None |
| | for t in sorted(df["Teacher"].unique(), key=len, reverse=True): |
| | if words in t.lower() or words.replace(" ", "_") in t.lower(): |
| | cand = t |
| | break |
| | if cand: |
| | sub = df[df["Teacher"] == cand].sort_values(["Day","Slot"]) |
| | return f"Schedule for {cand}:\n" + sub.to_string(index=False) |
| | else: |
| | return "Couldn't find that teacher. Try exact teacher name like 'T1_Ali' or 'T2_Sara'." |
| |
|
| | |
| | if "when is" in q or "when" in q and any(k in q for k in ["course", "c1", "c2", "when is"]): |
| | |
| | for c in df["Course"].unique(): |
| | if c.lower() in q: |
| | sub = df[df["Course"] == c] |
| | if sub.empty: |
| | continue |
| | rows = [] |
| | for _, r in sub.iterrows(): |
| | rows.append(f"- {r['Course']}: {r['Day']} {r['Slot']} with {r['Teacher']} in {r['Room']}") |
| | return "\n".join(rows) |
| | return "Mention the exact course name, e.g., 'When is C2_Physics scheduled?'" |
| |
|
| | |
| | if "summary" in q or "overview" in q or "stats" in q: |
| | tconf, rconf = compute_conflicts(df) |
| | total = len(df) |
| | unique_teachers = df["Teacher"].nunique() |
| | unique_rooms = df["Room"].nunique() |
| | lines = [ |
| | f"Rows: {total}", |
| | f"Teachers used: {unique_teachers}", |
| | f"Rooms used: {unique_rooms}", |
| | f"Teacher conflict count: {len(tconf)}", |
| | f"Room conflict count: {len(rconf)}" |
| | ] |
| | return "\n".join(lines) |
| |
|
| | return "I didn't understand. Try: 'show conflicts', 'schedule for T1_Ali', 'when is C2_Physics', or 'summary'." |
| |
|
| | |
| | |
| | |
| | title = "Automatic Time Table Generation Agent (Improved)" |
| | desc = "Improved GA + visualizer + assistant. Generate, inspect conflicts, visualize and export." |
| |
|
| | with gr.Blocks(title=title, css=""" |
| | .gradio-container { max-width: 1200px; margin: auto; } |
| | """) as demo: |
| | gr.Markdown(f"# {title}") |
| | gr.Markdown(desc) |
| |
|
| | with gr.Row(): |
| | with gr.Column(scale=1, min_width=380): |
| | gr.Markdown("## Inputs") |
| | courses_in = gr.Textbox(label="Courses (one per line)", value="C1_Math\nC2_Physics\nC3_Chemistry\nC4_English", lines=6) |
| | teachers_in = gr.Textbox(label="Teachers (one per line)", value="T1_Ali\nT2_Sara\nT3_Omar", lines=4) |
| | rooms_in = gr.Textbox(label="Rooms (one per line)", value="R1\nR2\nR3", lines=4) |
| | days_in = gr.Textbox(label="Days (one per line)", value="Monday\nTuesday\nWednesday\nThursday\nFriday", lines=5) |
| | slots_in = gr.Textbox(label="Slots (one per line)", value="Slot1\nSlot2\nSlot3\nSlot4\nSlot5\nSlot6", lines=6) |
| |
|
| | with gr.Accordion("Optional constraints (click to expand)", open=False): |
| | teacher_unavail_in = gr.Textbox(label="Teacher unavailability (Teacher,Day,Slot per line)", value="", lines=4) |
| | course_teacher_pref_in = gr.Textbox(label="Course -> allowed teachers (Course: T1,T2)", value="", lines=4) |
| | room_constraints_in = gr.Textbox(label="Course -> allowed rooms (Course: R1,R2)", value="", lines=4) |
| |
|
| | with gr.Accordion("GA parameters (advanced)", open=False): |
| | pop_in = gr.Slider(label="Population size", minimum=10, maximum=1000, value=120, step=10) |
| | gen_in = gr.Slider(label="Generations", minimum=10, maximum=3000, value=600, step=10) |
| | mut_in = gr.Slider(label="Base mutation rate", minimum=0.0, maximum=0.5, value=0.06, step=0.01) |
| | elitism_in = gr.Slider(label="Elitism (keep top N)", minimum=0, maximum=20, value=3, step=1) |
| | seed_in = gr.Number(label="Random seed (optional)", value=42) |
| | run_btn = gr.Button("Run Generator", variant="primary") |
| |
|
| | with gr.Column(scale=1, min_width=420): |
| | gr.Markdown("## Results & Tools") |
| | summary_out = gr.Textbox(label="Summary", lines=3) |
| | table_out = gr.Dataframe(headers=["Course","Teacher","Room","Day","Slot"], interactive=False) |
| | with gr.Row(): |
| | csv_btn = gr.File(label="Download CSV (generated)") |
| | xlsx_btn = gr.File(label="Download XLSX (generated)") |
| |
|
| | with gr.Tabs(): |
| | with gr.TabItem("Timetable Grid"): |
| | grid_plot = gr.Plot(label="Weekly timetable grid") |
| | download_grid_png = gr.Button("Download timetable PNG") |
| | with gr.TabItem("Conflicts / Heatmaps"): |
| | teacher_heat = gr.Plot(label="Teacher heatmap") |
| | room_heat = gr.Plot(label="Room heatmap") |
| | conflict_table = gr.Dataframe(headers=["Type","Entity","Day","Slot","Count"], interactive=False) |
| |
|
| | gen_progress = gr.Number(label="Generations run", value=0) |
| | best_score_box = gr.Number(label="Best fitness score", value=0) |
| |
|
| | gr.Markdown("## Assistant (Ask about the timetable)") |
| | assistant_input = gr.Textbox(label="Ask a question", placeholder="e.g., 'Show conflicts' or 'Schedule for T2_Sara'") |
| | assistant_output = gr.Textbox(label="Assistant response", lines=8) |
| |
|
| | |
| | state_best = gr.State() |
| | state_df = gr.State() |
| | state_csv = gr.State() |
| | state_xlsx = gr.State() |
| | state_grid_png = gr.State() |
| |
|
| | |
| | def _progress_cb(gen, total, best_score): |
| | |
| | return |
| |
|
| | def run_ga_and_prepare_download( |
| | courses_text, teachers_text, rooms_text, days_text, slots_text, |
| | teacher_unavail_text, course_teacher_pref_text, room_constraints_text, |
| | pop_size, gens, mut_rate, elitism, seed |
| | ): |
| | courses = parse_lines(courses_text) |
| | teachers = parse_lines(teachers_text) |
| | rooms = parse_lines(rooms_text) |
| | days = parse_lines(days_text) |
| | slots = parse_lines(slots_text) |
| | if not (courses and teachers and rooms and days and slots): |
| | return "Please provide courses, teachers, rooms, days and slots.", None, None, None, None, None, None, None |
| |
|
| | teacher_unavail = parse_teacher_unavailability(teacher_unavail_text) |
| | course_teacher_pref = parse_course_teacher_pref(course_teacher_pref_text) |
| | room_constraints = parse_room_constraints(room_constraints_text) |
| |
|
| | ga = TimetableGA( |
| | courses=courses, teachers=teachers, rooms=rooms, days=days, slots=slots, |
| | teacher_unavailable=teacher_unavail, |
| | course_teacher_pref=course_teacher_pref, |
| | room_constraints=room_constraints, |
| | population_size=pop_size, generations=gens, mutation_rate=mut_rate, |
| | elitism=elitism, seed=seed if seed is not None else None |
| | ) |
| |
|
| | |
| | res = ga.run(verbose=False, progress_callback=None) |
| | best = res["best"] |
| | score = res["score"] |
| | generations_ran = res.get("generations", gens) |
| | times = res["times"] |
| | df = individual_to_dataframe(best, courses, teachers, rooms, times) |
| | csv_bytes = dataframe_to_csv_bytes(df) |
| | xlsx_bytes = dataframe_to_xlsx_bytes(df) |
| | summary = f"Generator finished. Best fitness score: {score:.2f}. Rows: {len(df)}. Generations run: {generations_ran}" |
| | |
| | csv_file = io.BytesIO(csv_bytes); csv_file.name = "timetable.csv" |
| | xlsx_file = io.BytesIO(xlsx_bytes); xlsx_file.name = "timetable.xlsx" |
| | return summary, df, csv_file, xlsx_file, generations_ran, score, csv_bytes, xlsx_bytes |
| |
|
| | def make_visuals(df, days_text, slots_text, teachers_text, rooms_text): |
| | days = parse_lines(days_text) |
| | slots = parse_lines(slots_text) |
| | teachers = parse_lines(teachers_text) |
| | rooms = parse_lines(rooms_text) |
| | if df is None or df.empty: |
| | return None, None, None, None, None |
| | grid_fig = make_week_grid_plot(df, days, slots) |
| | teacher_fig, room_fig = make_conflict_heatmap(df, days, slots, teachers, rooms) |
| | tconf, rconf = compute_conflicts(df) |
| | |
| | rows = [] |
| | for _, r in tconf.iterrows(): |
| | rows.append(["Teacher", r["Teacher"], r["Day"], r["Slot"], int(r["count"])]) |
| | for _, r in rconf.iterrows(): |
| | rows.append(["Room", r["Room"], r["Day"], r["Slot"], int(r["count"])]) |
| | conflict_df = pd.DataFrame(rows, columns=["Type","Entity","Day","Slot","Count"]) |
| | return grid_fig, teacher_fig, room_fig, conflict_df, grid_fig.to_image(format="png", width=1000, height=600) |
| |
|
| | run_btn.click( |
| | run_ga_and_prepare_download, |
| | inputs=[courses_in, teachers_in, rooms_in, days_in, slots_in, |
| | teacher_unavail_in, course_teacher_pref_in, room_constraints_in, |
| | pop_in, gen_in, mut_in, elitism_in, seed_in], |
| | outputs=[summary_out, table_out, csv_btn, xlsx_btn, gen_progress, best_score_box, state_csv, state_xlsx], |
| | show_progress=True |
| | ) |
| |
|
| | |
| | def on_table_change(df, days_text, slots_text, teachers_text, rooms_text): |
| | grid_fig, teacher_fig, room_fig, conflict_df, png_bytes = make_visuals(df, days_text, slots_text, teachers_text, rooms_text) |
| | |
| | png_file = None |
| | if png_bytes is not None: |
| | png_file = io.BytesIO(png_bytes) |
| | png_file.name = f"timetable_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.png" |
| | return grid_fig, teacher_fig, room_fig, conflict_df, png_file |
| |
|
| | table_out.change( |
| | on_table_change, |
| | inputs=[table_out, days_in, slots_in, teachers_in, rooms_in], |
| | outputs=[grid_plot, teacher_heat, room_heat, conflict_table, state_grid_png] |
| | ) |
| |
|
| | |
| | def download_png(png_state): |
| | if png_state is None: |
| | return None |
| | return png_state |
| |
|
| | download_grid_png.click(download_png, inputs=[state_grid_png], outputs=[csv_btn]) |
| |
|
| | |
| | assistant_input.submit(lambda q, df: assistant_reply(df, q), inputs=[assistant_input, table_out], outputs=[assistant_output]) |
| | assistant_input.change(lambda q, df: assistant_reply(df, q), inputs=[assistant_input, table_out], outputs=[assistant_output]) |
| |
|
| | |
| | gr.Markdown("**Exports:** CSV and XLSX. **Visuals:** Table grid & heatmaps. Assistant is local and rule-based.") |
| |
|
| | if __name__ == "__main__": |
| | demo.launch(server_name="0.0.0.0", share=False) |
| |
|