KindAlien commited on
Commit
81bc3c3
·
verified ·
1 Parent(s): 6dfe51d

Update objective_engine.py

Browse files
Files changed (1) hide show
  1. objective_engine.py +139 -7
objective_engine.py CHANGED
@@ -52,6 +52,8 @@ class ObjectiveEngine:
52
  "morning_core": 5,
53
  "late_heavy": 5,
54
  "faculty_gaps": 2,
 
 
55
  "campus_movement": 3,
56
  "faculty_load_balance": 1,
57
  "no_first_hour_free": 20
@@ -69,13 +71,11 @@ class ObjectiveEngine:
69
  self._prioritize_morning_core_subjects()
70
  self._avoid_late_heavy_subjects()
71
 
72
- # Disable highly expensive constraints for massive math (large datasets)
73
- if len(self.tasks) <= 100:
74
- self._minimize_faculty_gaps()
75
- self._minimize_campus_movement()
76
- self._penalize_first_hour_free()
77
- else:
78
- print(f"Skipping expensive soft constraints due to massive math (Tasks: {len(self.tasks)})")
79
 
80
  # Summation of all penalties
81
  if self.penalties:
@@ -253,6 +253,138 @@ class ObjectiveEngine:
253
 
254
  self.penalties.append(idle_time * weight)
255
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  def _minimize_campus_movement(self):
257
  """
258
  Penalizes consecutive tasks for a section that are in different buildings.
 
52
  "morning_core": 5,
53
  "late_heavy": 5,
54
  "faculty_gaps": 2,
55
+ "student_gaps": 10,
56
+ "isolated_afternoon": 10,
57
  "campus_movement": 3,
58
  "faculty_load_balance": 1,
59
  "no_first_hour_free": 20
 
71
  self._prioritize_morning_core_subjects()
72
  self._avoid_late_heavy_subjects()
73
 
74
+ self._minimize_faculty_gaps()
75
+ self._minimize_student_gaps()
76
+ self._penalize_isolated_afternoon_classes()
77
+ self._minimize_campus_movement()
78
+ self._penalize_first_hour_free()
 
 
79
 
80
  # Summation of all penalties
81
  if self.penalties:
 
253
 
254
  self.penalties.append(idle_time * weight)
255
 
256
+ def _minimize_student_gaps(self):
257
+ """
258
+ Penalizes 'idle spans' for students (sections).
259
+ """
260
+ weight = self.weights.get("student_gaps", 10)
261
+ if weight == 0: return
262
+
263
+ tasks_by_section = defaultdict(list)
264
+ for task in self.tasks:
265
+ tasks_by_section[task.section.section_id].append(task)
266
+
267
+ for sec_id, s_tasks in tasks_by_section.items():
268
+ if not s_tasks:
269
+ continue
270
+
271
+ for day in range(const.NUM_WORKING_DAYS):
272
+ day_offset_start = day * const.NUM_TEACHING_SLOTS_PER_DAY
273
+ day_offset_end = (day + 1) * const.NUM_TEACHING_SLOTS_PER_DAY
274
+
275
+ day_active = self.model.NewBoolVar(f"sec_active_{sec_id}_{day}")
276
+ day_start = self.model.NewIntVar(day_offset_start, day_offset_end, f"sec_start_{sec_id}_{day}")
277
+ day_end = self.model.NewIntVar(day_offset_start, day_offset_end, f"sec_end_{sec_id}_{day}")
278
+
279
+ task_on_day_lits = []
280
+ total_duration_on_day = self.model.NewIntVar(0, const.NUM_TEACHING_SLOTS_PER_DAY, f"sec_dur_{sec_id}_{day}")
281
+
282
+ durations_sum = []
283
+
284
+ for task in s_tasks:
285
+ t_start = self.ce.task_vars[task.task_id][0]
286
+ t_end = self.ce.task_vars[task.task_id][1]
287
+
288
+ is_on_day = self.model.NewBoolVar(f"sec_{task.task_id}_on_day_{day}")
289
+
290
+ t_day = self.model.NewIntVar(0, const.NUM_WORKING_DAYS - 1, f"sec_t_day_{task.task_id}_{day}")
291
+ self.model.AddDivisionEquality(t_day, t_start, const.NUM_TEACHING_SLOTS_PER_DAY)
292
+
293
+ self.model.Add(t_day == day).OnlyEnforceIf(is_on_day)
294
+ self.model.Add(t_day != day).OnlyEnforceIf(is_on_day.Not())
295
+
296
+ task_on_day_lits.append(is_on_day)
297
+
298
+ # Update min start and max end for the day ONLY if task is on this day
299
+ self.model.Add(day_start <= t_start).OnlyEnforceIf(is_on_day)
300
+ self.model.Add(day_end >= t_end).OnlyEnforceIf(is_on_day)
301
+
302
+ dur_term = self.model.NewIntVar(0, task.duration, f"sec_dur_term_{task.task_id}_{day}")
303
+ self.model.Add(dur_term == task.duration).OnlyEnforceIf(is_on_day)
304
+ self.model.Add(dur_term == 0).OnlyEnforceIf(is_on_day.Not())
305
+ durations_sum.append(dur_term)
306
+
307
+ self.model.Add(sum(task_on_day_lits) > 0).OnlyEnforceIf(day_active)
308
+ self.model.Add(sum(task_on_day_lits) == 0).OnlyEnforceIf(day_active.Not())
309
+
310
+ self.model.Add(total_duration_on_day == sum(durations_sum))
311
+
312
+ span = self.model.NewIntVar(0, const.NUM_TEACHING_SLOTS_PER_DAY, f"sec_span_{sec_id}_{day}")
313
+ self.model.Add(span == day_end - day_start).OnlyEnforceIf(day_active)
314
+ self.model.Add(span == 0).OnlyEnforceIf(day_active.Not())
315
+
316
+ idle_time = self.model.NewIntVar(0, const.NUM_TEACHING_SLOTS_PER_DAY, f"sec_idle_{sec_id}_{day}")
317
+ self.model.Add(idle_time == span - total_duration_on_day).OnlyEnforceIf(day_active)
318
+ self.model.Add(idle_time == 0).OnlyEnforceIf(day_active.Not())
319
+
320
+ self.penalties.append(idle_time * weight)
321
+
322
+ def _penalize_isolated_afternoon_classes(self):
323
+ """
324
+ Penalizes sections having only 1 or 2 classes (slots) after lunch.
325
+ Students would rather have either no afternoon classes or a full afternoon.
326
+ """
327
+ weight = self.weights.get("isolated_afternoon", 10)
328
+ if weight == 0: return
329
+
330
+ afternoon_start_index = 4 # Index for slots after lunch
331
+
332
+ tasks_by_section = defaultdict(list)
333
+ for task in self.tasks:
334
+ tasks_by_section[task.section.section_id].append(task)
335
+
336
+ for sec_id, s_tasks in tasks_by_section.items():
337
+ for day in range(const.NUM_WORKING_DAYS):
338
+ afternoon_duration_sum = []
339
+
340
+ for task in s_tasks:
341
+ start_var = self.ce.task_vars[task.task_id][0]
342
+
343
+ daily_slot = self.model.NewIntVar(0, const.NUM_TEACHING_SLOTS_PER_DAY - 1, f"daily_slot_{task.task_id}_{day}")
344
+ self.model.AddModuloEquality(daily_slot, start_var, const.NUM_TEACHING_SLOTS_PER_DAY)
345
+
346
+ t_day = self.model.NewIntVar(0, const.NUM_WORKING_DAYS - 1, f"t_day_{task.task_id}_aft_{day}")
347
+ self.model.AddDivisionEquality(t_day, start_var, const.NUM_TEACHING_SLOTS_PER_DAY)
348
+
349
+ is_on_day = self.model.NewBoolVar(f"is_on_day_{task.task_id}_{day}_aft")
350
+ self.model.Add(t_day == day).OnlyEnforceIf(is_on_day)
351
+ self.model.Add(t_day != day).OnlyEnforceIf(is_on_day.Not())
352
+
353
+ is_afternoon = self.model.NewBoolVar(f"is_afternoon_{task.task_id}_{day}_aft")
354
+ self.model.Add(daily_slot >= afternoon_start_index).OnlyEnforceIf(is_afternoon)
355
+ self.model.Add(daily_slot < afternoon_start_index).OnlyEnforceIf(is_afternoon.Not())
356
+
357
+ # Task is on this day AND in the afternoon
358
+ is_on_day_and_afternoon = self.model.NewBoolVar(f"is_on_day_and_afternoon_{task.task_id}_{day}")
359
+ self.model.AddBoolAnd([is_on_day, is_afternoon]).OnlyEnforceIf(is_on_day_and_afternoon)
360
+
361
+ dur_term = self.model.NewIntVar(0, task.duration, f"aft_dur_{task.task_id}_{day}")
362
+ self.model.Add(dur_term == task.duration).OnlyEnforceIf(is_on_day_and_afternoon)
363
+ self.model.Add(dur_term == 0).OnlyEnforceIf(is_on_day_and_afternoon.Not())
364
+
365
+ afternoon_duration_sum.append(dur_term)
366
+
367
+ total_afternoon_dur = self.model.NewIntVar(0, const.NUM_TEACHING_SLOTS_PER_DAY, f"tot_aft_dur_{sec_id}_{day}")
368
+ if afternoon_duration_sum:
369
+ self.model.Add(total_afternoon_dur == sum(afternoon_duration_sum))
370
+ else:
371
+ self.model.Add(total_afternoon_dur == 0)
372
+
373
+ # We want to penalize if total_afternoon_dur is 1 or 2.
374
+ is_dur_1 = self.model.NewBoolVar(f"is_dur_1_{sec_id}_{day}")
375
+ self.model.Add(total_afternoon_dur == 1).OnlyEnforceIf(is_dur_1)
376
+ self.model.Add(total_afternoon_dur != 1).OnlyEnforceIf(is_dur_1.Not())
377
+
378
+ is_dur_2 = self.model.NewBoolVar(f"is_dur_2_{sec_id}_{day}")
379
+ self.model.Add(total_afternoon_dur == 2).OnlyEnforceIf(is_dur_2)
380
+ self.model.Add(total_afternoon_dur != 2).OnlyEnforceIf(is_dur_2.Not())
381
+
382
+ is_isolated = self.model.NewBoolVar(f"is_isolated_{sec_id}_{day}")
383
+ self.model.AddBoolOr([is_dur_1, is_dur_2]).OnlyEnforceIf(is_isolated)
384
+ self.model.AddBoolAnd([is_dur_1.Not(), is_dur_2.Not()]).OnlyEnforceIf(is_isolated.Not())
385
+
386
+ self.penalties.append(is_isolated * weight)
387
+
388
  def _minimize_campus_movement(self):
389
  """
390
  Penalizes consecutive tasks for a section that are in different buildings.