ChaKaGi commited on
Commit
98aaac2
·
verified ·
1 Parent(s): 76c18be

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +86 -39
app.py CHANGED
@@ -4,90 +4,133 @@ 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
  allowed_filiere = {"IA", "SEIOT", "IM", "SI", "GL"}
 
 
 
 
8
  exams = []
9
  for line in exams_text.strip().splitlines():
10
- if line.strip() == "":
11
  continue
12
  parts = line.split(',')
13
- if len(parts) < 4:
14
- return "Erreur dans la saisie des examens. Chaque ligne doit comporter 4 valeurs : exam_id, nb_etudiants, durée, filière."
15
- exam_id = int(parts[0].strip())
16
- nb_students = int(parts[1].strip())
17
- duration = int(parts[2].strip())
18
- filiere = parts[3].strip()
19
- if filiere not in allowed_filiere:
20
- return f"Erreur : la filière '{filiere}' n'est pas autorisée. Les filières autorisées sont : {', '.join(allowed_filiere)}."
21
- exams.append((exam_id, nb_students, duration, filiere))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  num_exams = len(exams)
23
 
 
24
  exam_id_to_index = {exam[0]: idx for idx, exam in enumerate(exams)}
25
 
 
26
  rooms = []
27
  for line in rooms_text.strip().splitlines():
28
- if line.strip() == "":
29
  continue
30
  parts = line.split(',')
31
- room_id = int(parts[0].strip())
32
- capacity = int(parts[1].strip())
 
 
 
33
  rooms.append((room_id, capacity))
34
  num_rooms = len(rooms)
35
 
 
36
  user_conflicts = []
37
  for line in conflicts_text.strip().splitlines():
38
- if line.strip() == "":
39
  continue
40
  parts = line.split(',')
41
  try:
42
  e1 = exam_id_to_index[int(parts[0].strip())]
43
  e2 = exam_id_to_index[int(parts[1].strip())]
44
  user_conflicts.append((e1, e2))
45
- except KeyError:
46
- return "Erreur dans les conflits. Vérifiez que les exam_id existent dans la liste des examens."
47
 
 
48
  auto_conflicts = []
49
  for i in range(num_exams):
50
  for j in range(i + 1, num_exams):
51
- if exams[i][3] == exams[j][3]:
52
  auto_conflicts.append((i, j))
53
 
 
54
  all_conflicts = set(user_conflicts + auto_conflicts)
55
 
 
56
  availability = {}
57
  for line in availability_text.strip().splitlines():
58
- if line.strip() == "":
59
  continue
60
  parts = line.split(',')
61
- room_id = int(parts[0].strip())
62
- slots = [int(s.strip()) for s in parts[1:]]
 
 
 
 
 
 
 
 
63
  availability[room_id] = slots
 
64
  avail_matrix = []
65
  for room in rooms:
66
  room_id = room[0]
67
  if room_id in availability:
68
  avail_matrix.append(availability[room_id])
69
  else:
70
- avail_matrix.append([1]*num_slots)
71
 
72
  # --- 2. Modélisation avec OR-Tools ---
73
  model = cp_model.CpModel()
74
 
 
 
75
  x = {}
76
  for i in range(num_exams):
77
  for j in range(num_slots):
78
  for k in range(num_rooms):
79
  x[(i, j, k)] = model.NewBoolVar(f'x_{i}_{j}_{k}')
80
 
 
81
  for i in range(num_exams):
82
  model.Add(sum(x[(i, j, k)] for j in range(num_slots) for k in range(num_rooms)) == 1)
83
 
 
 
84
  for j in range(num_slots):
85
  for k in range(num_rooms):
86
  model.Add(
87
- sum(exams[i][1] * x[(i, j, k)] for i in range(num_exams))
88
  <= rooms[k][1]
89
  )
90
 
 
91
  for (i, l) in all_conflicts:
92
  for j in range(num_slots):
93
  model.Add(
@@ -96,12 +139,14 @@ def solve_exam_schedule(exams_text, rooms_text, num_slots, conflicts_text, avail
96
  <= 1
97
  )
98
 
 
99
  for j in range(num_slots):
100
  for k in range(num_rooms):
101
  if avail_matrix[k][j] == 0:
102
  for i in range(num_exams):
103
  model.Add(x[(i, j, k)] == 0)
104
 
 
105
  T_max = model.NewIntVar(0, num_slots - 1, 'T_max')
106
  y = {}
107
  for i in range(num_exams):
@@ -115,9 +160,9 @@ def solve_exam_schedule(exams_text, rooms_text, num_slots, conflicts_text, avail
115
 
116
  model.Minimize(T_max)
117
 
 
118
  solver = cp_model.CpSolver()
119
- # Limite de temps pour éviter un blocage trop long
120
- solver.parameters.max_time_in_seconds = 10
121
  status = solver.Solve(model)
122
 
123
  if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
@@ -129,22 +174,26 @@ def solve_exam_schedule(exams_text, rooms_text, num_slots, conflicts_text, avail
129
  exam_id = exams[i][0]
130
  nb_students = exams[i][1]
131
  duration = exams[i][2]
132
- filiere = exams[i][3]
 
 
133
  room_id = rooms[k][0]
134
  schedule.append({
135
  "Examen": exam_id,
136
  "Filière": filiere,
 
137
  "Créneau": j,
138
  "Salle": room_id,
139
  "Nb Étudiants": nb_students,
 
140
  "Durée (h)": duration
141
  })
142
  schedule = sorted(schedule, key=lambda item: item["Créneau"])
143
  output_text = "### Calendrier des Examens\n"
144
  for item in schedule:
145
  output_text += (
146
- f"**Examen {item['Examen']}** | Filière: {item['Filière']} | Créneau: {item['Créneau']} | "
147
- f"Salle: {item['Salle']} | Étudiants: {item['Nb Étudiants']} | Durée: {item['Durée (h)']}h\n\n"
148
  )
149
  return output_text
150
  else:
@@ -152,29 +201,29 @@ def solve_exam_schedule(exams_text, rooms_text, num_slots, conflicts_text, avail
152
 
153
  # Utilisation d'un ThreadPoolExecutor pour exécuter la fonction dans un thread séparé
154
  executor = concurrent.futures.ThreadPoolExecutor(max_workers=4)
155
-
156
  def solve_in_thread(*args, **kwargs):
157
  future = executor.submit(solve_exam_schedule, *args, **kwargs)
158
  return future.result()
159
 
160
  with gr.Blocks() as demo:
161
- gr.Markdown("# Planification des Examens avec Filières (IA, SEIOT, IM, SI, GL)")
162
  gr.Markdown(
163
- "Remplissez les champs ci-dessous pour définir le problème de planification des examens. "
164
- "Pour les examens, chaque ligne doit être sous la forme :<br>"
165
- "`exam_id, nb_etudiants, durée, filière`<br>"
166
- "Exemple: `0, 30, 2, IA`"
 
167
  )
168
 
169
  with gr.Row():
170
  exams_input = gr.Textbox(
171
  label="Liste des examens",
172
  lines=5,
173
- value="0, 30, 2, IA\n1, 25, 1, SEIOT\n2, 40, 2, IA\n3, 20, 1, SI"
174
  )
175
  with gr.Row():
176
  rooms_input = gr.Textbox(
177
- label="Liste des salles (chaque ligne: room_id, capacité)",
178
  lines=2,
179
  value="0, 50\n1, 30"
180
  )
@@ -184,20 +233,18 @@ with gr.Blocks() as demo:
184
  )
185
  with gr.Row():
186
  conflicts_input = gr.Textbox(
187
- label="Conflits supplémentaires (chaque ligne: exam1, exam2) [optionnel]",
188
  lines=2,
189
  value="1, 3"
190
  )
191
  with gr.Row():
192
  availability_input = gr.Textbox(
193
- label=("Disponibilité des salles (chaque ligne: room_id, slot1, slot2, ... "
194
- "avec 1 pour dispo, 0 sinon)"),
195
  lines=2,
196
  value="0, 1, 1, 1, 1\n1, 1, 1, 0, 1"
197
  )
198
 
199
  output = gr.Markdown(label="Calendrier des examens")
200
-
201
  solve_btn = gr.Button("Planifier les examens")
202
  solve_btn.click(
203
  fn=solve_in_thread,
 
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():
16
  continue
17
  parts = line.split(',')
18
+ if len(parts) < 5:
19
+ return "Erreur : chaque ligne d'examen doit avoir au moins 5 valeurs : exam_id, nb_etudiants, durée, filière, promotion."
20
+ try:
21
+ exam_id = int(parts[0].strip())
22
+ nb_students = int(parts[1].strip())
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):
33
+ return f"Erreur : promotion(s) non autorisée(s). Autorisées : {', '.join(allowed_promotions)}."
34
+ reprise = 0
35
+ if len(parts) >= 6:
36
+ try:
37
+ reprise = int(parts[5].strip())
38
+ except ValueError:
39
+ return "Erreur : la reprise doit être un nombre entier."
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é"
47
  rooms = []
48
  for line in rooms_text.strip().splitlines():
49
+ if not line.strip():
50
  continue
51
  parts = line.split(',')
52
+ try:
53
+ room_id = int(parts[0].strip())
54
+ capacity = int(parts[1].strip())
55
+ except ValueError:
56
+ return "Erreur : room_id et capacité doivent être des nombres."
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():
64
  continue
65
  parts = line.split(',')
66
  try:
67
  e1 = exam_id_to_index[int(parts[0].strip())]
68
  e2 = exam_id_to_index[int(parts[1].strip())]
69
  user_conflicts.append((e1, e2))
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():
87
  continue
88
  parts = line.split(',')
89
+ try:
90
+ room_id = int(parts[0].strip())
91
+ except ValueError:
92
+ return "Erreur : room_id dans la disponibilité doit être un nombre."
93
+ slots = []
94
+ for s in parts[1:]:
95
+ try:
96
+ slots.append(int(s.strip()))
97
+ except ValueError:
98
+ return "Erreur : les valeurs de disponibilité doivent être 0 ou 1."
99
  availability[room_id] = slots
100
+ # Construction de la matrice de disponibilité dans l'ordre des salles
101
  avail_matrix = []
102
  for room in rooms:
103
  room_id = room[0]
104
  if room_id in availability:
105
  avail_matrix.append(availability[room_id])
106
  else:
107
+ avail_matrix.append([1] * num_slots)
108
 
109
  # --- 2. Modélisation avec OR-Tools ---
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(
129
+ sum((exams[i][1] + exams[i][5]) * x[(i, j, k)] for i in range(num_exams))
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
  <= 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 = {}
152
  for i in range(num_exams):
 
160
 
161
  model.Minimize(T_max)
162
 
163
+ # --- 3. Résolution ---
164
  solver = cp_model.CpSolver()
165
+ solver.parameters.max_time_in_seconds = 10 # Limite de temps pour éviter un blocage trop long
 
166
  status = solver.Solve(model)
167
 
168
  if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
 
174
  exam_id = exams[i][0]
175
  nb_students = exams[i][1]
176
  duration = exams[i][2]
177
+ filiere = ";".join(sorted(list(exams[i][3])))
178
+ promotions = ";".join(sorted(list(exams[i][4])))
179
+ reprise = exams[i][5]
180
  room_id = rooms[k][0]
181
  schedule.append({
182
  "Examen": exam_id,
183
  "Filière": filiere,
184
+ "Promotion": promotions,
185
  "Créneau": j,
186
  "Salle": room_id,
187
  "Nb Étudiants": nb_students,
188
+ "Reprise": reprise,
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:
 
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():
219
  exams_input = gr.Textbox(
220
  label="Liste des examens",
221
  lines=5,
222
+ 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"
223
  )
224
  with gr.Row():
225
  rooms_input = gr.Textbox(
226
+ label="Liste des salles (room_id, capacité)",
227
  lines=2,
228
  value="0, 50\n1, 30"
229
  )
 
233
  )
234
  with gr.Row():
235
  conflicts_input = gr.Textbox(
236
+ label="Conflits supplémentaires (exam1, exam2) [optionnel]",
237
  lines=2,
238
  value="1, 3"
239
  )
240
  with gr.Row():
241
  availability_input = gr.Textbox(
242
+ label="Disponibilité des salles (room_id, slot1, slot2, ... avec 1 pour dispo, 0 sinon)",
 
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,