ChaKaGi commited on
Commit
4e51dd0
·
verified ·
1 Parent(s): 98aaac2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +60 -30
app.py CHANGED
@@ -1,15 +1,16 @@
1
  import gradio as gr
 
2
  from ortools.sat.python import cp_model
3
  import concurrent.futures
4
 
5
- def solve_exam_schedule(exams_text, rooms_text, num_slots, conflicts_text, availability_text):
6
  # --- 1. Parsing des données ---
7
- # Ensembles des filières et promotions autorisées
8
  allowed_filiere = {"IA", "SEIOT", "IM", "SI", "GL"}
9
  allowed_promotions = {"1ere", "2eme", "3eme", "master1", "master2"}
10
 
11
- # Chaque ligne des examens doit être sous la forme :
12
- # exam_id, nb_etudiants, durée, filière[;filière2...], promotion(s)[;promotion2...], reprise (optionnel)
13
  exams = []
14
  for line in exams_text.strip().splitlines():
15
  if not line.strip():
@@ -23,10 +24,12 @@ def solve_exam_schedule(exams_text, rooms_text, num_slots, conflicts_text, avail
23
  duration = int(parts[2].strip())
24
  except ValueError:
25
  return "Erreur : exam_id, nb_etudiants et durée doivent être des nombres."
 
26
  filiere_str = parts[3].strip()
27
  filiere_set = {f.strip() for f in filiere_str.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_str = parts[4].strip()
31
  promotion_set = {p.strip() for p in promotion_str.split(';')}
32
  if not promotion_set.issubset(allowed_promotions):
@@ -40,7 +43,7 @@ def solve_exam_schedule(exams_text, rooms_text, num_slots, conflicts_text, avail
40
  exams.append((exam_id, nb_students, duration, filiere_set, promotion_set, reprise))
41
  num_exams = len(exams)
42
 
43
- # Création d'un mapping exam_id -> index (pour les conflits saisis manuellement)
44
  exam_id_to_index = {exam[0]: idx for idx, exam in enumerate(exams)}
45
 
46
  # Lecture des salles : chaque ligne "room_id, capacité"
@@ -57,7 +60,7 @@ def solve_exam_schedule(exams_text, rooms_text, num_slots, conflicts_text, avail
57
  rooms.append((room_id, capacity))
58
  num_rooms = len(rooms)
59
 
60
- # Conflits supplémentaires (saisie manuelle, chaque ligne : exam1, exam2)
61
  user_conflicts = []
62
  for line in conflicts_text.strip().splitlines():
63
  if not line.strip():
@@ -70,17 +73,16 @@ def solve_exam_schedule(exams_text, rooms_text, num_slots, conflicts_text, avail
70
  except (KeyError, ValueError):
71
  return "Erreur dans les conflits : vérifiez que les exam_id existent et sont valides."
72
 
73
- # Conflits automatiques : deux examens sont en conflit s'ils partagent au moins une filière ou une promotion
 
74
  auto_conflicts = []
75
  for i in range(num_exams):
76
  for j in range(i + 1, num_exams):
77
  if exams[i][3].intersection(exams[j][3]) or exams[i][4].intersection(exams[j][4]):
78
  auto_conflicts.append((i, j))
79
-
80
- # Union des conflits
81
  all_conflicts = set(user_conflicts + auto_conflicts)
82
 
83
- # Disponibilité des salles : chaque ligne "room_id, slot1, slot2, ..." (1 = disponible, 0 = non disponible)
84
  availability = {}
85
  for line in availability_text.strip().splitlines():
86
  if not line.strip():
@@ -110,19 +112,19 @@ def solve_exam_schedule(exams_text, rooms_text, num_slots, conflicts_text, avail
110
  model = cp_model.CpModel()
111
 
112
  # Variable de décision :
113
- # x[i, j, k] = 1 si l'examen i est programmé au créneau j dans la salle k
114
  x = {}
115
  for i in range(num_exams):
116
  for j in range(num_slots):
117
  for k in range(num_rooms):
118
  x[(i, j, k)] = model.NewBoolVar(f'x_{i}_{j}_{k}')
119
 
120
- # Chaque examen doit être programmé exactement une fois
121
  for i in range(num_exams):
122
  model.Add(sum(x[(i, j, k)] for j in range(num_slots) for k in range(num_rooms)) == 1)
123
 
124
  # Contrainte de capacité :
125
- # La somme (nb d'étudiants + reprise) pour les examens programmés dans une salle à un créneau doit être ≤ capacité de la salle
126
  for j in range(num_slots):
127
  for k in range(num_rooms):
128
  model.Add(
@@ -130,7 +132,7 @@ def solve_exam_schedule(exams_text, rooms_text, num_slots, conflicts_text, avail
130
  <= rooms[k][1]
131
  )
132
 
133
- # Contrainte de conflits : deux examens en conflit (défini par les conflits manuels et automatiques) ne peuvent être au même créneau
134
  for (i, l) in all_conflicts:
135
  for j in range(num_slots):
136
  model.Add(
@@ -139,13 +141,36 @@ def solve_exam_schedule(exams_text, rooms_text, num_slots, conflicts_text, avail
139
  <= 1
140
  )
141
 
142
- # Contrainte de disponibilité des salles
143
  for j in range(num_slots):
144
  for k in range(num_rooms):
145
  if avail_matrix[k][j] == 0:
146
  for i in range(num_exams):
147
  model.Add(x[(i, j, k)] == 0)
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  # Objectif : Minimiser le dernier créneau utilisé (T_max)
150
  T_max = model.NewIntVar(0, num_slots - 1, 'T_max')
151
  y = {}
@@ -189,30 +214,28 @@ def solve_exam_schedule(exams_text, rooms_text, num_slots, conflicts_text, avail
189
  "Durée (h)": duration
190
  })
191
  schedule = sorted(schedule, key=lambda item: item["Créneau"])
192
- output_text = "### Calendrier des Examens\n"
193
- for item in schedule:
194
- output_text += (
195
- f"**Examen {item['Examen']}** | Filière: {item['Filière']} | Promotion: {item['Promotion']} | Créneau: {item['Créneau']} | "
196
- f"Salle: {item['Salle']} | Étudiants: {item['Nb Étudiants']} | Reprise: {item['Reprise']} | Durée: {item['Durée (h)']}h\n\n"
197
- )
198
- return output_text
199
  else:
200
- return "Aucune solution trouvée."
 
201
 
202
- # Utilisation d'un ThreadPoolExecutor pour exécuter la fonction dans un thread séparé
203
  executor = concurrent.futures.ThreadPoolExecutor(max_workers=4)
204
  def solve_in_thread(*args, **kwargs):
205
  future = executor.submit(solve_exam_schedule, *args, **kwargs)
206
  return future.result()
207
 
208
- with gr.Blocks() as demo:
209
- gr.Markdown("# Planification des Examens Multi-Promotions et Filières")
 
210
  gr.Markdown(
211
  "Format des examens :<br>"
212
  "`exam_id, nb_etudiants, durée, filière[;...], promotion(s)[;...], reprise (optionnel)`<br>"
213
  "Exemples :<br>"
214
  "`0, 30, 2, IA, 1ere, 5`<br>"
215
- "`1, 25, 1, SEIOT;GL, 2eme;3eme`"
 
216
  )
217
 
218
  with gr.Row():
@@ -243,13 +266,20 @@ with gr.Blocks() as demo:
243
  lines=2,
244
  value="0, 1, 1, 1, 1\n1, 1, 1, 0, 1"
245
  )
 
 
 
 
 
 
 
 
246
 
247
- output = gr.Markdown(label="Calendrier des examens")
248
  solve_btn = gr.Button("Planifier les examens")
249
  solve_btn.click(
250
  fn=solve_in_thread,
251
- inputs=[exams_input, rooms_input, num_slots_input, conflicts_input, availability_input],
252
- outputs=output
253
  )
254
 
255
  demo.launch()
 
1
  import gradio as gr
2
+ import pandas as pd
3
  from ortools.sat.python import cp_model
4
  import concurrent.futures
5
 
6
+ def solve_exam_schedule(exams_text, rooms_text, num_slots, conflicts_text, availability_text, invigilators):
7
  # --- 1. Parsing des données ---
8
+ # Ensembles autorisés
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():
 
24
  duration = int(parts[2].strip())
25
  except ValueError:
26
  return "Erreur : exam_id, nb_etudiants et durée doivent être des nombres."
27
+ # Extraction et vérification des filières
28
  filiere_str = parts[3].strip()
29
  filiere_set = {f.strip() for f in filiere_str.split(';')}
30
  if not filiere_set.issubset(allowed_filiere):
31
  return f"Erreur : filière(s) non autorisée(s). Autorisées : {', '.join(allowed_filiere)}."
32
+ # Extraction et vérification des promotions
33
  promotion_str = parts[4].strip()
34
  promotion_set = {p.strip() for p in promotion_str.split(';')}
35
  if not promotion_set.issubset(allowed_promotions):
 
43
  exams.append((exam_id, nb_students, duration, filiere_set, promotion_set, reprise))
44
  num_exams = len(exams)
45
 
46
+ # Mapping exam_id -> index (pour conflits manuels)
47
  exam_id_to_index = {exam[0]: idx for idx, exam in enumerate(exams)}
48
 
49
  # Lecture des salles : chaque ligne "room_id, capacité"
 
60
  rooms.append((room_id, capacity))
61
  num_rooms = len(rooms)
62
 
63
+ # Conflits supplémentaires (manuels) : chaque ligne "exam1, exam2"
64
  user_conflicts = []
65
  for line in conflicts_text.strip().splitlines():
66
  if not line.strip():
 
73
  except (KeyError, ValueError):
74
  return "Erreur dans les conflits : vérifiez que les exam_id existent et sont valides."
75
 
76
+ # Conflits automatiques :
77
+ # Deux examens sont en conflit s'ils partagent au moins une filière ou au moins une promotion.
78
  auto_conflicts = []
79
  for i in range(num_exams):
80
  for j in range(i + 1, num_exams):
81
  if exams[i][3].intersection(exams[j][3]) or exams[i][4].intersection(exams[j][4]):
82
  auto_conflicts.append((i, j))
 
 
83
  all_conflicts = set(user_conflicts + auto_conflicts)
84
 
85
+ # Disponibilité des salles : chaque ligne "room_id, slot1, slot2, ..." (1 = dispo, 0 = non dispo)
86
  availability = {}
87
  for line in availability_text.strip().splitlines():
88
  if not line.strip():
 
112
  model = cp_model.CpModel()
113
 
114
  # Variable de décision :
115
+ # x[i, j, k] = 1 si l'examen i est programmé au créneau j dans la salle k.
116
  x = {}
117
  for i in range(num_exams):
118
  for j in range(num_slots):
119
  for k in range(num_rooms):
120
  x[(i, j, k)] = model.NewBoolVar(f'x_{i}_{j}_{k}')
121
 
122
+ # Chaque examen doit être programmé exactement une fois.
123
  for i in range(num_exams):
124
  model.Add(sum(x[(i, j, k)] for j in range(num_slots) for k in range(num_rooms)) == 1)
125
 
126
  # Contrainte de capacité :
127
+ # Le nombre total d'étudiants (inscrits + reprises) pour les examens programmés dans une salle à un créneau ≤ capacité de la salle.
128
  for j in range(num_slots):
129
  for k in range(num_rooms):
130
  model.Add(
 
132
  <= rooms[k][1]
133
  )
134
 
135
+ # Contrainte de 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
  model.Add(
 
141
  <= 1
142
  )
143
 
144
+ # Contrainte de disponibilité des salles.
145
  for j in range(num_slots):
146
  for k in range(num_rooms):
147
  if avail_matrix[k][j] == 0:
148
  for i in range(num_exams):
149
  model.Add(x[(i, j, k)] == 0)
150
 
151
+ # --- Nouvelles contraintes supplémentaires ---
152
+
153
+ # 1. Disponibilité des ressources humaines (enseignants surveillants)
154
+ # Chaque examen requiert un enseignant, donc le nombre total d'examens simultanés ≤ nombre d'enseignants disponibles.
155
+ for j in range(num_slots):
156
+ model.Add(sum(x[(i, j, k)] for i in range(num_exams) for k in range(num_rooms)) <= invigilators)
157
+
158
+ # 2. Un étudiant (promotion) ne peut être inscrit à deux examens simultanément
159
+ # Pour chaque promotion et chaque créneau, au plus un examen (contenant cette promotion) peut être programmé.
160
+ for p in allowed_promotions:
161
+ for j in range(num_slots):
162
+ 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)
163
+
164
+ # 3. Marges de transition dans chaque salle
165
+ # Si une salle est utilisée au créneau j, elle doit rester libre au créneau j+1.
166
+ for k in range(num_rooms):
167
+ for j in range(num_slots - 1):
168
+ model.Add(
169
+ sum(x[(i, j, k)] for i in range(num_exams)) +
170
+ sum(x[(i, j+1, k)] for i in range(num_exams))
171
+ <= 1
172
+ )
173
+
174
  # Objectif : Minimiser le dernier créneau utilisé (T_max)
175
  T_max = model.NewIntVar(0, num_slots - 1, 'T_max')
176
  y = {}
 
214
  "Durée (h)": duration
215
  })
216
  schedule = sorted(schedule, key=lambda item: item["Créneau"])
217
+ df = pd.DataFrame(schedule)
218
+ return df
 
 
 
 
 
219
  else:
220
+ # En cas d'erreur, on retourne un DataFrame avec un message
221
+ return pd.DataFrame([{"Message": "Aucune solution trouvée."}])
222
 
223
+ # Exécution dans un thread séparé
224
  executor = concurrent.futures.ThreadPoolExecutor(max_workers=4)
225
  def solve_in_thread(*args, **kwargs):
226
  future = executor.submit(solve_exam_schedule, *args, **kwargs)
227
  return future.result()
228
 
229
+ # Interface Gradio
230
+ with gr.Blocks(css=".gradio-container {max-width: 900px; margin: auto;}") as demo:
231
+ gr.Markdown("# Planification des Examens Multi-Promotions, Filières et Contraintes de Ressources")
232
  gr.Markdown(
233
  "Format des examens :<br>"
234
  "`exam_id, nb_etudiants, durée, filière[;...], promotion(s)[;...], reprise (optionnel)`<br>"
235
  "Exemples :<br>"
236
  "`0, 30, 2, IA, 1ere, 5`<br>"
237
+ "`1, 25, 1, SEIOT;GL, 2eme;3eme`<br><br>"
238
+ "Vous pouvez modifier directement le planning dans le tableau ci-dessous une fois généré."
239
  )
240
 
241
  with gr.Row():
 
266
  lines=2,
267
  value="0, 1, 1, 1, 1\n1, 1, 1, 0, 1"
268
  )
269
+ with gr.Row():
270
+ invigilators_input = gr.Slider(
271
+ label="Nombre d'enseignants disponibles par créneau", minimum=1, maximum=10, step=1, value=3
272
+ )
273
+
274
+ # Composant DataFrame interactif pour afficher et modifier le planning
275
+ schedule_output = gr.Dataframe(headers=["Examen", "Filière", "Promotion", "Créneau", "Salle", "Nb Étudiants", "Reprise", "Durée (h)"],
276
+ label="Calendrier des examens (modifiable)", interactive=True)
277
 
 
278
  solve_btn = gr.Button("Planifier les examens")
279
  solve_btn.click(
280
  fn=solve_in_thread,
281
+ inputs=[exams_input, rooms_input, num_slots_input, conflicts_input, availability_input, invigilators_input],
282
+ outputs=schedule_output
283
  )
284
 
285
  demo.launch()