Spaces:
Sleeping
Sleeping
Update objective_engine.py
Browse files- 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 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 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.
|