|
|
import gradio as gr |
|
|
import pandas as pd |
|
|
from ortools.sat.python import cp_model |
|
|
import concurrent.futures |
|
|
|
|
|
def solve_exam_schedule(exams_text, rooms_text, num_slots, conflicts_text, availability_text, invigilators): |
|
|
|
|
|
|
|
|
allowed_filiere = {"IA", "SEIOT", "IM", "SI", "GL"} |
|
|
allowed_promotions = {"1ere", "2eme", "3eme", "master1", "master2"} |
|
|
|
|
|
|
|
|
|
|
|
exams = [] |
|
|
for line in exams_text.strip().splitlines(): |
|
|
if not line.strip(): |
|
|
continue |
|
|
parts = line.split(',') |
|
|
if len(parts) < 5: |
|
|
return "Erreur : chaque ligne d'examen doit avoir au moins 5 valeurs : exam_id, nb_etudiants, durée, filière, promotion." |
|
|
try: |
|
|
exam_id = int(parts[0].strip()) |
|
|
nb_students = int(parts[1].strip()) |
|
|
duration = int(parts[2].strip()) |
|
|
except ValueError: |
|
|
return "Erreur : exam_id, nb_etudiants et durée doivent être des nombres." |
|
|
|
|
|
filiere_str = parts[3].strip() |
|
|
filiere_set = {f.strip() for f in filiere_str.split(';')} |
|
|
if not filiere_set.issubset(allowed_filiere): |
|
|
return f"Erreur : filière(s) non autorisée(s). Autorisées : {', '.join(allowed_filiere)}." |
|
|
|
|
|
promotion_str = parts[4].strip() |
|
|
promotion_set = {p.strip() for p in promotion_str.split(';')} |
|
|
if not promotion_set.issubset(allowed_promotions): |
|
|
return f"Erreur : promotion(s) non autorisée(s). Autorisées : {', '.join(allowed_promotions)}." |
|
|
reprise = 0 |
|
|
if len(parts) >= 6: |
|
|
try: |
|
|
reprise = int(parts[5].strip()) |
|
|
except ValueError: |
|
|
return "Erreur : la reprise doit être un nombre entier." |
|
|
exams.append((exam_id, nb_students, duration, filiere_set, promotion_set, reprise)) |
|
|
num_exams = len(exams) |
|
|
|
|
|
|
|
|
exam_id_to_index = {exam[0]: idx for idx, exam in enumerate(exams)} |
|
|
|
|
|
|
|
|
rooms = [] |
|
|
for line in rooms_text.strip().splitlines(): |
|
|
if not line.strip(): |
|
|
continue |
|
|
parts = line.split(',') |
|
|
try: |
|
|
room_id = int(parts[0].strip()) |
|
|
capacity = int(parts[1].strip()) |
|
|
except ValueError: |
|
|
return "Erreur : room_id et capacité doivent être des nombres." |
|
|
rooms.append((room_id, capacity)) |
|
|
num_rooms = len(rooms) |
|
|
|
|
|
|
|
|
user_conflicts = [] |
|
|
for line in conflicts_text.strip().splitlines(): |
|
|
if not line.strip(): |
|
|
continue |
|
|
parts = line.split(',') |
|
|
try: |
|
|
e1 = exam_id_to_index[int(parts[0].strip())] |
|
|
e2 = exam_id_to_index[int(parts[1].strip())] |
|
|
user_conflicts.append((e1, e2)) |
|
|
except (KeyError, ValueError): |
|
|
return "Erreur dans les conflits : vérifiez que les exam_id existent et sont valides." |
|
|
|
|
|
|
|
|
|
|
|
auto_conflicts = [] |
|
|
for i in range(num_exams): |
|
|
for j in range(i + 1, num_exams): |
|
|
if exams[i][3].intersection(exams[j][3]) or exams[i][4].intersection(exams[j][4]): |
|
|
auto_conflicts.append((i, j)) |
|
|
all_conflicts = set(user_conflicts + auto_conflicts) |
|
|
|
|
|
|
|
|
availability = {} |
|
|
for line in availability_text.strip().splitlines(): |
|
|
if not line.strip(): |
|
|
continue |
|
|
parts = line.split(',') |
|
|
try: |
|
|
room_id = int(parts[0].strip()) |
|
|
except ValueError: |
|
|
return "Erreur : room_id dans la disponibilité doit être un nombre." |
|
|
slots = [] |
|
|
for s in parts[1:]: |
|
|
try: |
|
|
slots.append(int(s.strip())) |
|
|
except ValueError: |
|
|
return "Erreur : les valeurs de disponibilité doivent être 0 ou 1." |
|
|
availability[room_id] = slots |
|
|
|
|
|
avail_matrix = [] |
|
|
for room in rooms: |
|
|
room_id = room[0] |
|
|
if room_id in availability: |
|
|
avail_matrix.append(availability[room_id]) |
|
|
else: |
|
|
avail_matrix.append([1] * num_slots) |
|
|
|
|
|
|
|
|
model = cp_model.CpModel() |
|
|
|
|
|
|
|
|
|
|
|
x = {} |
|
|
for i in range(num_exams): |
|
|
for j in range(num_slots): |
|
|
for k in range(num_rooms): |
|
|
x[(i, j, k)] = model.NewBoolVar(f'x_{i}_{j}_{k}') |
|
|
|
|
|
|
|
|
for i in range(num_exams): |
|
|
model.Add(sum(x[(i, j, k)] for j in range(num_slots) for k in range(num_rooms)) == 1) |
|
|
|
|
|
|
|
|
|
|
|
for j in range(num_slots): |
|
|
for k in range(num_rooms): |
|
|
model.Add( |
|
|
sum((exams[i][1] + exams[i][5]) * x[(i, j, k)] for i in range(num_exams)) |
|
|
<= rooms[k][1] |
|
|
) |
|
|
|
|
|
|
|
|
for (i, l) in all_conflicts: |
|
|
for j in range(num_slots): |
|
|
model.Add( |
|
|
sum(x[(i, j, k)] for k in range(num_rooms)) + |
|
|
sum(x[(l, j, k)] for k in range(num_rooms)) |
|
|
<= 1 |
|
|
) |
|
|
|
|
|
|
|
|
for j in range(num_slots): |
|
|
for k in range(num_rooms): |
|
|
if avail_matrix[k][j] == 0: |
|
|
for i in range(num_exams): |
|
|
model.Add(x[(i, j, k)] == 0) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for j in range(num_slots): |
|
|
model.Add(sum(x[(i, j, k)] for i in range(num_exams) for k in range(num_rooms)) <= invigilators) |
|
|
|
|
|
|
|
|
|
|
|
for p in allowed_promotions: |
|
|
for j in range(num_slots): |
|
|
model.Add(sum(x[(i, j, k)] for i in range(num_exams) if p in exams[i][4] for k in range(num_rooms)) <= 1) |
|
|
|
|
|
|
|
|
|
|
|
for k in range(num_rooms): |
|
|
for j in range(num_slots - 1): |
|
|
model.Add( |
|
|
sum(x[(i, j, k)] for i in range(num_exams)) + |
|
|
sum(x[(i, j+1, k)] for i in range(num_exams)) |
|
|
<= 1 |
|
|
) |
|
|
|
|
|
|
|
|
T_max = model.NewIntVar(0, num_slots - 1, 'T_max') |
|
|
y = {} |
|
|
for i in range(num_exams): |
|
|
for j in range(num_slots): |
|
|
y[(i, j)] = model.NewBoolVar(f'y_{i}_{j}') |
|
|
model.Add(sum(x[(i, j, k)] for k in range(num_rooms)) == y[(i, j)]) |
|
|
|
|
|
for i in range(num_exams): |
|
|
for j in range(num_slots): |
|
|
model.Add(T_max >= j).OnlyEnforceIf(y[(i, j)]) |
|
|
|
|
|
model.Minimize(T_max) |
|
|
|
|
|
|
|
|
solver = cp_model.CpSolver() |
|
|
solver.parameters.max_time_in_seconds = 10 |
|
|
status = solver.Solve(model) |
|
|
|
|
|
if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: |
|
|
schedule = [] |
|
|
for i in range(num_exams): |
|
|
for j in range(num_slots): |
|
|
for k in range(num_rooms): |
|
|
if solver.Value(x[(i, j, k)]) == 1: |
|
|
exam_id = exams[i][0] |
|
|
nb_students = exams[i][1] |
|
|
duration = exams[i][2] |
|
|
filiere = ";".join(sorted(list(exams[i][3]))) |
|
|
promotions = ";".join(sorted(list(exams[i][4]))) |
|
|
reprise = exams[i][5] |
|
|
room_id = rooms[k][0] |
|
|
schedule.append({ |
|
|
"Examen": exam_id, |
|
|
"Filière": filiere, |
|
|
"Promotion": promotions, |
|
|
"Créneau": j, |
|
|
"Salle": room_id, |
|
|
"Nb Étudiants": nb_students, |
|
|
"Reprise": reprise, |
|
|
"Durée (h)": duration |
|
|
}) |
|
|
schedule = sorted(schedule, key=lambda item: item["Créneau"]) |
|
|
df = pd.DataFrame(schedule) |
|
|
return df |
|
|
else: |
|
|
|
|
|
return pd.DataFrame([{"Message": "Aucune solution trouvée."}]) |
|
|
|
|
|
|
|
|
executor = concurrent.futures.ThreadPoolExecutor(max_workers=4) |
|
|
def solve_in_thread(*args, **kwargs): |
|
|
future = executor.submit(solve_exam_schedule, *args, **kwargs) |
|
|
return future.result() |
|
|
|
|
|
|
|
|
with gr.Blocks(css=".gradio-container {max-width: 900px; margin: auto;}") as demo: |
|
|
gr.Markdown("# Planification des Examens Multi-Promotions, Filières et Contraintes de Ressources") |
|
|
gr.Markdown( |
|
|
"Format des examens :<br>" |
|
|
"`exam_id, nb_etudiants, durée, filière[;...], promotion(s)[;...], reprise (optionnel)`<br>" |
|
|
"Exemples :<br>" |
|
|
"`0, 30, 2, IA, 1ere, 5`<br>" |
|
|
"`1, 25, 1, SEIOT;GL, 2eme;3eme`<br><br>" |
|
|
"Vous pouvez modifier directement le planning dans le tableau ci-dessous une fois généré." |
|
|
) |
|
|
|
|
|
with gr.Row(): |
|
|
exams_input = gr.Textbox( |
|
|
label="Liste des examens", |
|
|
lines=5, |
|
|
value="0, 30, 2, IA, 1ere, 5\n1, 25, 1, SEIOT, 2eme, 0\n2, 40, 2, IA;GL, 3eme, 0\n3, 20, 1, SI, master1, 0" |
|
|
) |
|
|
with gr.Row(): |
|
|
rooms_input = gr.Textbox( |
|
|
label="Liste des salles (room_id, capacité)", |
|
|
lines=2, |
|
|
value="0, 50\n1, 30" |
|
|
) |
|
|
with gr.Row(): |
|
|
num_slots_input = gr.Slider( |
|
|
label="Nombre de créneaux", minimum=1, maximum=10, step=1, value=4 |
|
|
) |
|
|
with gr.Row(): |
|
|
conflicts_input = gr.Textbox( |
|
|
label="Conflits supplémentaires (exam1, exam2) [optionnel]", |
|
|
lines=2, |
|
|
value="1, 3" |
|
|
) |
|
|
with gr.Row(): |
|
|
availability_input = gr.Textbox( |
|
|
label="Disponibilité des salles (room_id, slot1, slot2, ... avec 1 pour dispo, 0 sinon)", |
|
|
lines=2, |
|
|
value="0, 1, 1, 1, 1\n1, 1, 1, 0, 1" |
|
|
) |
|
|
with gr.Row(): |
|
|
invigilators_input = gr.Slider( |
|
|
label="Nombre d'enseignants disponibles par créneau", minimum=1, maximum=10, step=1, value=3 |
|
|
) |
|
|
|
|
|
|
|
|
schedule_output = gr.Dataframe(headers=["Examen", "Filière", "Promotion", "Créneau", "Salle", "Nb Étudiants", "Reprise", "Durée (h)"], |
|
|
label="Calendrier des examens (modifiable)", interactive=True) |
|
|
|
|
|
solve_btn = gr.Button("Planifier les examens") |
|
|
solve_btn.click( |
|
|
fn=solve_in_thread, |
|
|
inputs=[exams_input, rooms_input, num_slots_input, conflicts_input, availability_input, invigilators_input], |
|
|
outputs=schedule_output |
|
|
) |
|
|
|
|
|
demo.launch() |
|
|
|