ChaKaGi commited on
Commit
48d920c
·
verified ·
1 Parent(s): 28d83d0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +127 -241
app.py CHANGED
@@ -1,256 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  import pandas as pd
3
- from pulp import LpProblem, LpMinimize, LpVariable, lpSum, LpBinary, LpStatus
4
- import concurrent.futures
5
 
6
- def solve_exam_schedule_mip(exams_text, rooms_text, num_slots, conflicts_text, availability_text, invigilators):
7
  # --- 1. Parsing des données ---
8
- # Ensembles autorisés pour filières et promotions
9
- allowed_filiere = {"IA", "SEIOT", "IM", "SI", "GL"}
10
- allowed_promotions = {"1ere", "2eme", "3eme", "master1", "master2"}
11
-
12
- # Format attendu pour les examens :
13
- # exam_id, nb_etudiants, durée, filière[;...], promotion(s)[;...], reprise (optionnel)
14
- exams = []
15
- for line in exams_text.strip().splitlines():
16
- if not line.strip():
17
- continue
18
  parts = line.split(',')
19
- if len(parts) < 5:
20
- return "Erreur : chaque ligne d'examen doit avoir au moins 5 valeurs : exam_id, nb_etudiants, durée, filière, promotion."
21
- try:
22
- exam_id = int(parts[0].strip())
23
- nb_students = int(parts[1].strip())
24
- duration = int(parts[2].strip())
25
- except ValueError:
26
- return "Erreur : exam_id, nb_etudiants et durée doivent être des nombres."
27
- filiere_set = {f.strip() for f in parts[3].split(';')}
28
- if not filiere_set.issubset(allowed_filiere):
29
- return f"Erreur : filière(s) non autorisée(s). Autorisées : {', '.join(allowed_filiere)}."
30
- promotion_set = {p.strip() for p in parts[4].split(';')}
31
- if not promotion_set.issubset(allowed_promotions):
32
- return f"Erreur : promotion(s) non autorisée(s). Autorisées : {', '.join(allowed_promotions)}."
33
- reprise = 0
34
- if len(parts) >= 6:
35
- try:
36
- reprise = int(parts[5].strip())
37
- except ValueError:
38
- return "Erreur : la reprise doit être un nombre entier."
39
- exams.append((exam_id, nb_students, duration, filiere_set, promotion_set, reprise))
40
- num_exams = len(exams)
41
-
42
- # Mapping exam_id -> index (pour gérer les conflits manuels)
43
- exam_id_to_index = {exam[0]: idx for idx, exam in enumerate(exams)}
44
-
45
- # Lecture des salles : chaque ligne "room_id, capacité"
46
- rooms = []
47
- for line in rooms_text.strip().splitlines():
48
- if not line.strip():
49
- continue
50
  parts = line.split(',')
51
- try:
52
- room_id = int(parts[0].strip())
53
- capacity = int(parts[1].strip())
54
- except ValueError:
55
- return "Erreur : room_id et capacité doivent être des nombres."
56
- rooms.append((room_id, capacity))
57
- num_rooms = len(rooms)
58
-
59
- # Conflits supplémentaires (manuels) : chaque ligne "exam1, exam2"
60
- manual_conflicts = []
61
- for line in conflicts_text.strip().splitlines():
62
- if not line.strip():
63
- continue
64
  parts = line.split(',')
65
- try:
66
- e1 = exam_id_to_index[int(parts[0].strip())]
67
- e2 = exam_id_to_index[int(parts[1].strip())]
68
- manual_conflicts.append((e1, e2))
69
- except (KeyError, ValueError):
70
- return "Erreur dans les conflits manuels : vérifiez que les exam_id existent."
71
-
72
- # Conflits automatiques : deux examens sont en conflit s'ils partagent au moins une filière ou au moins une promotion
73
- auto_conflicts = []
74
- for i in range(num_exams):
75
- for j in range(i+1, num_exams):
76
- if exams[i][3].intersection(exams[j][3]) or exams[i][4].intersection(exams[j][4]):
77
- auto_conflicts.append((i, j))
78
- all_conflicts = set(manual_conflicts + auto_conflicts)
79
-
80
- # Disponibilité des salles : chaque ligne "room_id, slot1, slot2, ...", 1 = disponible, 0 = non disponible
81
- availability = {}
82
- for line in availability_text.strip().splitlines():
83
- if not line.strip():
84
- continue
85
  parts = line.split(',')
86
- try:
87
- room_id = int(parts[0].strip())
88
- slots = [int(s.strip()) for s in parts[1:]]
89
- except:
90
- return "Erreur dans la disponibilité : vérifiez le format."
91
- availability[room_id] = slots
92
- # Construction de la matrice de disponibilité dans l'ordre des salles fournies
93
- avail_matrix = []
94
- for room in rooms:
95
- room_id = room[0]
96
- if room_id in availability:
97
- avail_matrix.append(availability[room_id])
98
- else:
99
- avail_matrix.append([1] * num_slots)
100
-
101
- # --- 2. Formulation MILP avec PuLP ---
102
- prob = LpProblem("Exam_Scheduling", LpMinimize)
103
-
104
- # Variables de décision :
105
- # x[i,j,k] = 1 si l'examen i est programmé au créneau j dans la salle k.
106
- x = {}
107
- for i in range(num_exams):
108
- for j in range(num_slots):
109
- for k in range(num_rooms):
110
- x[(i,j,k)] = LpVariable(f"x_{i}_{j}_{k}", cat=LpBinary)
111
-
112
- # Pour gérer la marge de transition, on définit y[j,k] = 1 si la salle k est utilisée au créneau j.
113
- y = {}
114
- for j in range(num_slots):
115
- for k in range(num_rooms):
116
- y[(j,k)] = LpVariable(f"y_{j}_{k}", cat=LpBinary)
117
-
118
- # Variables T[j] indiquant si le créneau j est utilisé (pour minimiser le nombre de créneaux)
119
- T = {}
120
- for j in range(num_slots):
121
- T[j] = LpVariable(f"T_{j}", cat=LpBinary)
122
-
123
- # Objectif : Minimiser le nombre total de créneaux utilisés
124
- prob += lpSum([T[j] for j in range(num_slots)]), "Minimize_total_slots"
125
-
126
- # Contrainte 1 : chaque examen est programmé exactement une fois
127
- for i in range(num_exams):
128
- prob += lpSum([x[(i,j,k)] for j in range(num_slots) for k in range(num_rooms)]) == 1, f"Exam_{i}_once"
129
-
130
- # Contrainte 2 : capacité de chaque salle à chaque créneau
131
- for j in range(num_slots):
132
- for k in range(num_rooms):
133
- prob += lpSum([(exams[i][1] + exams[i][5]) * x[(i,j,k)] for i in range(num_exams)]) <= rooms[k][1], f"Capacity_slot_{j}_room_{k}"
134
-
135
- # Contrainte 3 : conflits – deux examens en conflit ne peuvent être programmés au même créneau
136
- for (i, l) in all_conflicts:
137
- for j in range(num_slots):
138
- prob += lpSum([x[(i,j,k)] for k in range(num_rooms)]) + lpSum([x[(l,j,k)] for k in range(num_rooms)]) <= 1, f"Conflict_{i}_{l}_slot_{j}"
139
-
140
- # Contrainte 4 : disponibilité des salles
141
- for j in range(num_slots):
142
- for k in range(num_rooms):
143
- if avail_matrix[k][j] == 0:
144
- for i in range(num_exams):
145
- prob += x[(i,j,k)] == 0, f"Avail_room_{k}_slot_{j}_exam_{i}"
146
-
147
- # Contrainte 5 : disponibilité des enseignants (ressources humaines)
148
- for j in range(num_slots):
149
- prob += lpSum([x[(i,j,k)] for i in range(num_exams) for k in range(num_rooms)]) <= invigilators, f"Teachers_slot_{j}"
150
-
151
- # Contrainte 6 : un étudiant (promotion) ne peut avoir deux examens simultanés
152
- for p in allowed_promotions:
153
- for j in range(num_slots):
154
- prob += lpSum([x[(i,j,k)] for i in range(num_exams) if p in exams[i][4] for k in range(num_rooms)]) <= 1, f"Promotion_{p}_slot_{j}"
155
-
156
- # Contrainte 7 : marges de transition dans chaque salle
157
- # On lie d'abord y[j,k] à l'utilisation de la salle
158
- for j in range(num_slots):
159
- for k in range(num_rooms):
160
- prob += y[(j,k)] >= lpSum([x[(i,j,k)] for i in range(num_exams)])/100.0, f"Link_y_lower_{j}_{k}"
161
- prob += y[(j,k)] <= lpSum([x[(i,j,k)] for i in range(num_exams)]), f"Link_y_upper_{j}_{k}"
162
- # Puis, pour chaque salle, deux créneaux consécutifs ne peuvent être utilisés
163
- for k in range(num_rooms):
164
- for j in range(num_slots - 1):
165
- prob += y[(j,k)] + y[(j+1,k)] <= 1, f"Transition_room_{k}_slots_{j}_{j+1}"
166
-
167
- # Contrainte 8 : liaison des variables T[j] : T[j] = 1 si un examen est programmé en slot j
168
- for j in range(num_slots):
169
- prob += T[j] >= lpSum([x[(i,j,k)] for i in range(num_exams) for k in range(num_rooms)])/100.0, f"Link_T_lower_{j}"
170
- prob += T[j] <= lpSum([x[(i,j,k)] for i in range(num_exams) for k in range(num_rooms)]), f"Link_T_upper_{j}"
171
-
172
  # --- 3. Résolution ---
173
- prob.solve()
174
-
175
- if LpStatus[prob.status] in ["Optimal", "Feasible"]:
176
- schedule = []
177
- for i in range(num_exams):
178
- for j in range(num_slots):
179
- for k in range(num_rooms):
180
- if x[(i,j,k)].varValue is not None and x[(i,j,k)].varValue > 0.5:
181
- schedule.append({
182
- "Examen": exams[i][0],
183
- "Filière": ";".join(sorted(list(exams[i][3]))),
184
- "Promotion": ";".join(sorted(list(exams[i][4]))),
185
- "Créneau": j,
186
- "Salle": rooms[k][0],
187
- "Nb Étudiants": exams[i][1],
188
- "Reprise": exams[i][5],
189
- "Durée (h)": exams[i][2]
190
- })
191
- schedule = sorted(schedule, key=lambda s: s["Créneau"])
192
- df = pd.DataFrame(schedule)
193
- return df
194
- else:
195
- return pd.DataFrame([{"Message": "Aucune solution trouvée."}])
196
 
197
- # Exécuter dans un thread séparé pour ne pas bloquer l'interface Gradio
198
- executor = concurrent.futures.ThreadPoolExecutor(max_workers=4)
199
- def solve_in_thread(*args, **kwargs):
200
- future = executor.submit(solve_exam_schedule_mip, *args, **kwargs)
201
- return future.result()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
 
203
- with gr.Blocks(css=".gradio-container {max-width: 900px; margin: auto;}") as demo:
204
- gr.Markdown("# Planification des Examens Multi-Promotions, Filières, avec MIP (PuLP)")
205
- gr.Markdown(
206
- "Format des examens :<br>"
207
- "`exam_id, nb_etudiants, durée, filière[;...], promotion(s)[;...], reprise (optionnel)`<br>"
208
- "Exemples :<br>"
209
- "`0, 30, 2, IA, 1ere, 5`<br>"
210
- "`1, 25, 1, SEIOT;GL, 2eme;3eme`"
211
- )
212
-
213
- with gr.Row():
214
- exams_input = gr.Textbox(
215
- label="Liste des examens",
216
- lines=5,
217
- 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"
218
- )
219
- with gr.Row():
220
- rooms_input = gr.Textbox(
221
- label="Liste des salles (room_id, capacité)",
222
- lines=2,
223
- value="0, 50\n1, 30\n2, 40"
224
- )
225
- with gr.Row():
226
- num_slots_input = gr.Slider(
227
- label="Nombre de créneaux", minimum=1, maximum=10, step=1, value=5
228
- )
229
- with gr.Row():
230
- conflicts_input = gr.Textbox(
231
- label="Conflits supplémentaires (exam1, exam2) [optionnel]",
232
- lines=2,
233
- value="1, 3"
234
- )
235
- with gr.Row():
236
- availability_input = gr.Textbox(
237
- label="Disponibilité des salles (room_id, slot1, slot2, ... avec 1 pour dispo, 0 sinon)",
238
- lines=2,
239
- value="0, 1, 1, 1, 1, 1\n1, 1, 1, 0, 1, 1\n2, 1, 1, 1, 1, 0"
240
- )
241
- with gr.Row():
242
- invigilators_input = gr.Slider(
243
- label="Nombre d'enseignants disponibles par créneau", minimum=1, maximum=10, step=1, value=3
244
- )
245
-
246
- schedule_output = gr.Dataframe(headers=["Examen", "Filière", "Promotion", "Créneau", "Salle", "Nb Étudiants", "Reprise", "Durée (h)"],
247
- label="Calendrier des examens (modifiable)", interactive=True)
248
-
249
  solve_btn = gr.Button("Planifier les examens")
250
  solve_btn.click(
251
- fn=solve_in_thread,
252
- inputs=[exams_input, rooms_input, num_slots_input, conflicts_input, availability_input, invigilators_input],
253
- outputs=schedule_output
254
  )
255
-
256
  demo.launch()
 
1
+ # -*- coding: utf-8 -*-
2
+ """optimisation_examen_PMNE.ipynb
3
+
4
+ Automatically generated by Colab.
5
+
6
+ Original file is located at
7
+ https://colab.research.google.com/drive/1Uewn7e8zZiGZdAAYHqXJtl-3TA7sj5eL
8
+ """
9
+
10
+ #!pip install pulp
11
+ #!pip install gradio
12
+
13
  import gradio as gr
14
  import pandas as pd
15
+ from pulp import LpMinimize, LpProblem, LpVariable, lpSum
 
16
 
17
+ def planifier_examens(examens_text, salles_text, jours_text, creneaux_text, disponibilite_salle_text, conflits_text):
18
  # --- 1. Parsing des données ---
19
+ # Examens
20
+ examens = []
21
+ for line in examens_text.strip().splitlines():
 
 
 
 
 
 
 
22
  parts = line.split(',')
23
+ if len(parts) == 3:
24
+ exam_id, nb_students, duration = parts
25
+ examens.append((exam_id.strip(), int(nb_students.strip()), int(duration.strip())))
26
+
27
+ # Salles et capacités
28
+ salles = {}
29
+ for line in salles_text.strip().splitlines():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  parts = line.split(',')
31
+ if len(parts) == 2:
32
+ room_id, capacity = parts
33
+ salles[room_id.strip()] = int(capacity.strip())
34
+
35
+ # Jours et créneaux
36
+ jours = [int(j.strip()) for j in jours_text.split(',')]
37
+ creneaux = [int(c.strip()) for c in creneaux_text.split(',')]
38
+
39
+ # Disponibilité des salles
40
+ disponibilite_salle = {}
41
+ for line in disponibilite_salle_text.strip().splitlines():
 
 
42
  parts = line.split(',')
43
+ room_id = parts[0].strip()
44
+ disponibilite_salle[room_id] = [int(s) for s in parts[1:]]
45
+
46
+ # Conflits entre examens
47
+ conflits = []
48
+ for line in conflits_text.strip().splitlines():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  parts = line.split(',')
50
+ conflits.append((parts[0].strip(), parts[1].strip()))
51
+
52
+ # --- 2. Modélisation avec PuLP (PMNE) ---
53
+ model = LpProblem("Planification_Examens", LpMinimize)
54
+
55
+ # Variables de décision
56
+ X = {(e, d, c, s): LpVariable(f"X_{e}_{d}_{c}_{s}", cat="Binary")
57
+ for e, _, _ in examens for d in jours for c in creneaux for s in salles}
58
+ Y = {d: LpVariable(f"Y_{d}", cat="Binary") for d in jours}
59
+
60
+ # Contrainte 1 : Chaque examen doit être programmé une seule fois
61
+ for e, _, _ in examens:
62
+ model += lpSum(X[e, d, c, s] for d in jours for c in creneaux for s in salles) == 1
63
+
64
+ # Contrainte 2 : Capacité des salles respectée
65
+ for d in jours:
66
+ for c in creneaux:
67
+ for s in salles:
68
+ model += lpSum(nb_students * X[e, d, c, s] for e, nb_students, _ in examens) <= salles[s]
69
+
70
+ # Contrainte 3 : Une salle ne peut accueillir qu’un seul examen par créneau
71
+ for d in jours:
72
+ for c in creneaux:
73
+ for s in salles:
74
+ model += lpSum(X[e, d, c, s] for e, _, _ in examens) <= 1
75
+
76
+ # Contrainte 4 : Disponibilité des salles
77
+ for e, _, _ in examens:
78
+ for d in jours:
79
+ for c in creneaux:
80
+ for s in salles:
81
+ if disponibilite_salle[s][c - 1] == 0:
82
+ model += X[e, d, c, s] == 0
83
+
84
+ # Contrainte 5 : Conflits entre examens (ne pas être en même temps)
85
+ for e1, e2 in conflits:
86
+ for d in jours:
87
+ for c in creneaux:
88
+ model += lpSum(X[e1, d, c, s] for s in salles) + lpSum(X[e2, d, c, s] for s in salles) <= 1
89
+
90
+ # Contrainte 6 : Activation des jours
91
+ for d in jours:
92
+ for e, _, _ in examens:
93
+ for c in creneaux:
94
+ for s in salles:
95
+ model += Y[d] >= X[e, d, c, s]
96
+
97
+ # Fonction Objectif : Minimiser le nombre de jours utilisés
98
+ model += lpSum(Y[d] for d in jours)
99
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  # --- 3. Résolution ---
101
+ model.solve()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
+ # --- 4. Extraction des résultats ---
104
+ planning = []
105
+ for e, _, _ in examens:
106
+ for d in jours:
107
+ for c in creneaux:
108
+ for s in salles:
109
+ if X[e, d, c, s].varValue == 1:
110
+ planning.append({"Examen": e, "Jour": d, "Créneau": c, "Salle": s})
111
+
112
+ df = pd.DataFrame(planning)
113
+ df.to_csv("planning_examens.csv", index=False)
114
+
115
+ return df, "planning_examens.csv"
116
+
117
+ # Interface avec Gradio
118
+ with gr.Blocks() as demo:
119
+ gr.Markdown("# 📅 Planification des Examens avec PMNE et Gradio")
120
+
121
+ exams_input = gr.Textbox(label="Examens (exam_id, nb_étudiants, durée)", lines=5,
122
+ value="E1, 30, 2\nE2, 25, 1\nE3, 40, 2\nE4, 20, 1")
123
+ rooms_input = gr.Textbox(label="Salles (room_id, capacité)", lines=3,
124
+ value="S1, 50\nS2, 30\nS3, 40")
125
+ jours_input = gr.Textbox(label="Jours disponibles (séparés par des virgules)", value="1, 2, 3")
126
+ creneaux_input = gr.Textbox(label="Créneaux disponibles (séparés par des virgules)", value="1, 2, 3, 4")
127
+ disponibilite_input = gr.Textbox(label="Disponibilité des salles (room_id, slot1, slot2, ...)", lines=4,
128
+ value="S1, 1, 1, 1, 1\nS2, 1, 1, 0, 1\nS3, 1, 1, 1, 0")
129
+ conflits_input = gr.Textbox(label="Conflits entre examens (exam1, exam2)", lines=3,
130
+ value="E1, E3")
131
+
132
+ output_table = gr.Dataframe(headers=["Examen", "Jour", "Créneau", "Salle"], label="Planning des examens")
133
+ output_file = gr.File(label="Télécharger le planning")
134
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  solve_btn = gr.Button("Planifier les examens")
136
  solve_btn.click(
137
+ fn=planifier_examens,
138
+ inputs=[exams_input, rooms_input, jours_input, creneaux_input, disponibilite_input, conflits_input],
139
+ outputs=[output_table, output_file]
140
  )
141
+
142
  demo.launch()