KindAlien commited on
Commit
275ea78
·
verified ·
1 Parent(s): d9286d1

Update objective_engine.py

Browse files
Files changed (1) hide show
  1. objective_engine.py +183 -144
objective_engine.py CHANGED
@@ -56,7 +56,9 @@ class ObjectiveEngine:
56
  "isolated_afternoon": 100,
57
  "campus_movement": 3,
58
  "faculty_load_balance": 1,
59
- "no_first_hour_free": 50
 
 
60
  }
61
 
62
  self.penalties: List[cp_model.IntVar] = []
@@ -76,6 +78,9 @@ class ObjectiveEngine:
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:
@@ -177,14 +182,9 @@ class ObjectiveEngine:
177
  self.penalties.append(is_last_slot * weight)
178
 
179
  def _minimize_faculty_gaps(self):
180
- """
181
- Penalizes 'idle spans' for faculty.
182
- We approximate this by minimizing (Daily End Time - Daily Start Time - Total Teaching Duration).
183
- """
184
- weight = self.weights.get("faculty_gaps", 0)
185
  if weight == 0: return
186
 
187
- # Group tasks by faculty
188
  tasks_by_faculty = {f.id: [] for f in self.faculties}
189
  faculty_ids_set = {f.id for f in self.faculties}
190
  for task in self.tasks:
@@ -195,69 +195,58 @@ class ObjectiveEngine:
195
  tasks_by_faculty[fid].append(task)
196
 
197
  for faculty_id, f_tasks in tasks_by_faculty.items():
198
- if not f_tasks:
199
- continue
200
 
201
  for day in range(const.NUM_WORKING_DAYS):
202
- day_offset_start = day * const.NUM_TEACHING_SLOTS_PER_DAY
203
- day_offset_end = (day + 1) * const.NUM_TEACHING_SLOTS_PER_DAY
204
-
205
- # Variables to track if faculty is active on this day, and their start/end
206
- day_active = self.model.NewBoolVar(f"active_{faculty_id}_{day}")
207
- day_start = self.model.NewIntVar(day_offset_start, day_offset_end, f"start_{faculty_id}_{day}")
208
- day_end = self.model.NewIntVar(day_offset_start, day_offset_end, f"end_{faculty_id}_{day}")
209
-
210
- task_on_day_lits = []
211
- total_duration_on_day = self.model.NewIntVar(0, const.NUM_TEACHING_SLOTS_PER_DAY, f"dur_{faculty_id}_{day}")
212
-
213
- durations_sum = []
214
-
215
- for task in f_tasks:
216
- t_start = self.ce.task_vars[task.task_id][0]
217
- t_end = self.ce.task_vars[task.task_id][1]
218
 
219
- is_on_day = self.model.NewBoolVar(f"{task.task_id}_on_day_{day}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
 
221
- t_day = self.model.NewIntVar(0, const.NUM_WORKING_DAYS - 1, f"t_day_{task.task_id}_{day}")
222
- self.model.AddDivisionEquality(t_day, t_start, const.NUM_TEACHING_SLOTS_PER_DAY)
 
 
 
 
 
 
 
 
 
 
 
223
 
224
- self.model.Add(t_day == day).OnlyEnforceIf(is_on_day)
225
- self.model.Add(t_day != day).OnlyEnforceIf(is_on_day.Not())
226
-
227
- task_on_day_lits.append(is_on_day)
228
-
229
- # Update min start and max end for the day ONLY if task is on this day
230
- self.model.Add(day_start <= t_start).OnlyEnforceIf(is_on_day)
231
- self.model.Add(day_end >= t_end).OnlyEnforceIf(is_on_day)
232
-
233
- # Accumulate duration
234
- dur_term = self.model.NewIntVar(0, task.duration, f"dur_term_{task.task_id}_{day}")
235
- self.model.Add(dur_term == task.duration).OnlyEnforceIf(is_on_day)
236
- self.model.Add(dur_term == 0).OnlyEnforceIf(is_on_day.Not())
237
- durations_sum.append(dur_term)
238
-
239
- # If no tasks on this day, force active to false
240
- self.model.Add(sum(task_on_day_lits) > 0).OnlyEnforceIf(day_active)
241
- self.model.Add(sum(task_on_day_lits) == 0).OnlyEnforceIf(day_active.Not())
242
-
243
- # --- FIX: Use Python sum() inside Add() instead of self.model.Sum() ---
244
- self.model.Add(total_duration_on_day == sum(durations_sum))
245
-
246
- span = self.model.NewIntVar(0, const.NUM_TEACHING_SLOTS_PER_DAY, f"span_{faculty_id}_{day}")
247
- self.model.Add(span == day_end - day_start).OnlyEnforceIf(day_active)
248
- self.model.Add(span == 0).OnlyEnforceIf(day_active.Not())
249
-
250
- idle_time = self.model.NewIntVar(0, const.NUM_TEACHING_SLOTS_PER_DAY, f"idle_{faculty_id}_{day}")
251
- self.model.Add(idle_time == span - total_duration_on_day).OnlyEnforceIf(day_active)
252
- self.model.Add(idle_time == 0).OnlyEnforceIf(day_active.Not())
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)
@@ -265,59 +254,55 @@ class ObjectiveEngine:
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
  """
@@ -435,47 +420,101 @@ class ObjectiveEngine:
435
  self.penalties.append(penalty_active * weight)
436
 
437
  def _penalize_first_hour_free(self):
438
- """
439
- Strongly penalizes having the first hour (period index 0) free for any
440
- section on any day. The solver will avoid this unless there is genuinely
441
- no other feasible assignment.
442
 
443
- Since tasks never cross day boundaries, a task covers period 0 of a day
444
- if and only if its start_var equals that day's first absolute slot index.
445
  """
446
- weight = self.weights.get("no_first_hour_free", 20)
447
- if weight == 0:
448
- return
 
 
449
 
450
- # Group tasks by section
451
  tasks_by_section = defaultdict(list)
452
  for task in self.tasks:
453
  tasks_by_section[task.section.section_id].append(task)
454
 
455
- for sec_id, sec_tasks in tasks_by_section.items():
456
- for day in range(const.NUM_WORKING_DAYS):
457
- # The absolute slot index for period 0 of this day
458
- first_slot = day * const.NUM_TEACHING_SLOTS_PER_DAY
459
-
460
- # For each task, create a bool: does it start at exactly first_slot?
461
- starts_at_first = []
462
- for task in sec_tasks:
463
- start_var = self.ce.task_vars[task.task_id][0]
464
 
465
- at_first = self.model.NewBoolVar(f"at1st_{task.task_id}_d{day}")
466
- self.model.Add(start_var == first_slot).OnlyEnforceIf(at_first)
467
- self.model.Add(start_var != first_slot).OnlyEnforceIf(at_first.Not())
468
- starts_at_first.append(at_first)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
 
470
- # any_at_first = True if at least one task starts at period 0
471
- any_at_first = self.model.NewBoolVar(f"any_at1st_{sec_id}_d{day}")
472
- self.model.AddBoolOr(starts_at_first).OnlyEnforceIf(any_at_first)
473
- for lit in starts_at_first:
474
- self.model.AddImplication(any_at_first.Not(), lit.Not())
475
 
476
- # Penalty when the first hour IS free (no task at period 0)
477
- first_free = self.model.NewBoolVar(f"first_free_{sec_id}_d{day}")
478
- self.model.Add(first_free == 1).OnlyEnforceIf(any_at_first.Not())
479
- self.model.Add(first_free == 0).OnlyEnforceIf(any_at_first)
480
 
481
- self.penalties.append(first_free * weight)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  "isolated_afternoon": 100,
57
  "campus_movement": 3,
58
  "faculty_load_balance": 1,
59
+ "no_first_hour_free": 0, # Disabled intentionally
60
+ "pack_morning": 50,
61
+ "avoid_late_afternoon": 40
62
  }
63
 
64
  self.penalties: List[cp_model.IntVar] = []
 
78
  self._penalize_isolated_afternoon_classes()
79
  self._minimize_campus_movement()
80
  self._penalize_first_hour_free()
81
+
82
+ self._penalize_empty_morning_slots()
83
+ self._penalize_late_afternoon_slots()
84
 
85
  # Summation of all penalties
86
  if self.penalties:
 
182
  self.penalties.append(is_last_slot * weight)
183
 
184
  def _minimize_faculty_gaps(self):
185
+ weight = self.weights.get("faculty_gaps", 30)
 
 
 
 
186
  if weight == 0: return
187
 
 
188
  tasks_by_faculty = {f.id: [] for f in self.faculties}
189
  faculty_ids_set = {f.id for f in self.faculties}
190
  for task in self.tasks:
 
195
  tasks_by_faculty[fid].append(task)
196
 
197
  for faculty_id, f_tasks in tasks_by_faculty.items():
198
+ if not f_tasks: continue
 
199
 
200
  for day in range(const.NUM_WORKING_DAYS):
201
+ slot_occupied = []
202
+ for s in range(const.NUM_TEACHING_SLOTS_PER_DAY):
203
+ abs_slot = day * const.NUM_TEACHING_SLOTS_PER_DAY + s
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
205
+ task_covers = []
206
+ for task in f_tasks:
207
+ covers = self.model.NewBoolVar(f"fac_{faculty_id}_covers_{abs_slot}_{task.task_id}")
208
+ start_var = self.ce.task_vars[task.task_id][0]
209
+ min_start = max(abs_slot - task.duration + 1, day * const.NUM_TEACHING_SLOTS_PER_DAY)
210
+ max_start = abs_slot
211
+
212
+ eq_lits = []
213
+ for p in range(min_start, max_start + 1):
214
+ is_p = self.model.NewBoolVar(f"fac_{faculty_id}_t_{task.task_id}_s_{p}")
215
+ self.model.Add(start_var == p).OnlyEnforceIf(is_p)
216
+ self.model.Add(start_var != p).OnlyEnforceIf(is_p.Not())
217
+ eq_lits.append(is_p)
218
+
219
+ self.model.AddBoolOr(eq_lits).OnlyEnforceIf(covers)
220
+ for lit in eq_lits:
221
+ self.model.AddImplication(covers.Not(), lit.Not())
222
+
223
+ task_covers.append(covers)
224
+
225
+ is_occupied = self.model.NewBoolVar(f"fac_{faculty_id}_occ_{abs_slot}")
226
+ self.model.AddBoolOr(task_covers).OnlyEnforceIf(is_occupied)
227
+ for lit in task_covers:
228
+ self.model.AddImplication(is_occupied.Not(), lit.Not())
229
+
230
+ slot_occupied.append(is_occupied)
231
 
232
+ for s in range(1, const.NUM_TEACHING_SLOTS_PER_DAY - 1):
233
+ has_before = self.model.NewBoolVar(f"fac_{faculty_id}_before_{day}_{s}")
234
+ self.model.AddBoolOr(slot_occupied[:s]).OnlyEnforceIf(has_before)
235
+ for lit in slot_occupied[:s]:
236
+ self.model.AddImplication(has_before.Not(), lit.Not())
237
+
238
+ has_after = self.model.NewBoolVar(f"fac_{faculty_id}_after_{day}_{s}")
239
+ self.model.AddBoolOr(slot_occupied[s+1:]).OnlyEnforceIf(has_after)
240
+ for lit in slot_occupied[s+1:]:
241
+ self.model.AddImplication(has_after.Not(), lit.Not())
242
+
243
+ is_gap = self.model.NewBoolVar(f"fac_{faculty_id}_is_gap_{day}_{s}")
244
+ self.model.AddBoolAnd([slot_occupied[s].Not(), has_before, has_after]).OnlyEnforceIf(is_gap)
245
 
246
+ self.penalties.append(is_gap * weight)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
  def _minimize_student_gaps(self):
249
+ weight = self.weights.get("student_gaps", 100)
 
 
 
250
  if weight == 0: return
251
 
252
  tasks_by_section = defaultdict(list)
 
254
  tasks_by_section[task.section.section_id].append(task)
255
 
256
  for sec_id, s_tasks in tasks_by_section.items():
257
+ if not s_tasks: continue
 
258
 
259
  for day in range(const.NUM_WORKING_DAYS):
260
+ slot_occupied = []
261
+ for s in range(const.NUM_TEACHING_SLOTS_PER_DAY):
262
+ abs_slot = day * const.NUM_TEACHING_SLOTS_PER_DAY + s
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
 
264
+ task_covers = []
265
+ for task in s_tasks:
266
+ covers = self.model.NewBoolVar(f"sec_{sec_id}_covers_{abs_slot}_{task.task_id}")
267
+ start_var = self.ce.task_vars[task.task_id][0]
268
+ min_start = max(abs_slot - task.duration + 1, day * const.NUM_TEACHING_SLOTS_PER_DAY)
269
+ max_start = abs_slot
270
+
271
+ eq_lits = []
272
+ for p in range(min_start, max_start + 1):
273
+ is_p = self.model.NewBoolVar(f"sec_{sec_id}_t_{task.task_id}_s_{p}")
274
+ self.model.Add(start_var == p).OnlyEnforceIf(is_p)
275
+ self.model.Add(start_var != p).OnlyEnforceIf(is_p.Not())
276
+ eq_lits.append(is_p)
277
+
278
+ self.model.AddBoolOr(eq_lits).OnlyEnforceIf(covers)
279
+ for lit in eq_lits:
280
+ self.model.AddImplication(covers.Not(), lit.Not())
281
+
282
+ task_covers.append(covers)
283
+
284
+ is_occupied = self.model.NewBoolVar(f"sec_{sec_id}_occ_{abs_slot}")
285
+ self.model.AddBoolOr(task_covers).OnlyEnforceIf(is_occupied)
286
+ for lit in task_covers:
287
+ self.model.AddImplication(is_occupied.Not(), lit.Not())
288
+
289
+ slot_occupied.append(is_occupied)
290
 
291
+ for s in range(1, const.NUM_TEACHING_SLOTS_PER_DAY - 1):
292
+ has_before = self.model.NewBoolVar(f"sec_{sec_id}_before_{day}_{s}")
293
+ self.model.AddBoolOr(slot_occupied[:s]).OnlyEnforceIf(has_before)
294
+ for lit in slot_occupied[:s]:
295
+ self.model.AddImplication(has_before.Not(), lit.Not())
296
+
297
+ has_after = self.model.NewBoolVar(f"sec_{sec_id}_after_{day}_{s}")
298
+ self.model.AddBoolOr(slot_occupied[s+1:]).OnlyEnforceIf(has_after)
299
+ for lit in slot_occupied[s+1:]:
300
+ self.model.AddImplication(has_after.Not(), lit.Not())
301
+
302
+ is_gap = self.model.NewBoolVar(f"sec_{sec_id}_is_gap_{day}_{s}")
303
+ self.model.AddBoolAnd([slot_occupied[s].Not(), has_before, has_after]).OnlyEnforceIf(is_gap)
304
 
305
+ self.penalties.append(is_gap * weight)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
 
307
  def _penalize_isolated_afternoon_classes(self):
308
  """
 
420
  self.penalties.append(penalty_active * weight)
421
 
422
  def _penalize_first_hour_free(self):
423
+ # DISABLED: This constraint forces the solver to place classes at 8:45 every day to avoid a penalty.
424
+ # Since sections don't always have enough classes to fill all 5 days completely, forcing a class
425
+ # at the start of every day causes the remaining classes to be scattered, leading to unavoidable gaps!
426
+ pass
427
 
428
+ def _penalize_empty_morning_slots(self):
 
429
  """
430
+ Penalizes any empty slot in the first 4 hours (slots 0, 1, 2, 3) for students.
431
+ This forces the solver to pack classes tightly into the morning.
432
+ """
433
+ weight = self.weights.get("pack_morning", 50)
434
+ if weight == 0: return
435
 
 
436
  tasks_by_section = defaultdict(list)
437
  for task in self.tasks:
438
  tasks_by_section[task.section.section_id].append(task)
439
 
440
+ for sec_id, s_tasks in tasks_by_section.items():
441
+ if not s_tasks: continue
 
 
 
 
 
 
 
442
 
443
+ for day in range(const.NUM_WORKING_DAYS):
444
+ for s in range(4): # 0, 1, 2, 3
445
+ abs_slot = day * const.NUM_TEACHING_SLOTS_PER_DAY + s
446
+
447
+ task_covers = []
448
+ for task in s_tasks:
449
+ covers = self.model.NewBoolVar(f"sec_{sec_id}_m_covers_{abs_slot}_{task.task_id}")
450
+ start_var = self.ce.task_vars[task.task_id][0]
451
+ min_start = max(abs_slot - task.duration + 1, day * const.NUM_TEACHING_SLOTS_PER_DAY)
452
+ max_start = abs_slot
453
+
454
+ eq_lits = []
455
+ for p in range(min_start, max_start + 1):
456
+ is_p = self.model.NewBoolVar(f"sec_{sec_id}_m_t_{task.task_id}_s_{p}")
457
+ self.model.Add(start_var == p).OnlyEnforceIf(is_p)
458
+ self.model.Add(start_var != p).OnlyEnforceIf(is_p.Not())
459
+ eq_lits.append(is_p)
460
+
461
+ self.model.AddBoolOr(eq_lits).OnlyEnforceIf(covers)
462
+ for lit in eq_lits:
463
+ self.model.AddImplication(covers.Not(), lit.Not())
464
+
465
+ task_covers.append(covers)
466
+
467
+ is_occupied = self.model.NewBoolVar(f"sec_{sec_id}_m_occ_{abs_slot}")
468
+ self.model.AddBoolOr(task_covers).OnlyEnforceIf(is_occupied)
469
+ for lit in task_covers:
470
+ self.model.AddImplication(is_occupied.Not(), lit.Not())
471
+
472
+ # Penalize if NOT occupied
473
+ self.penalties.append(is_occupied.Not() * weight)
474
+
475
+ def _penalize_late_afternoon_slots(self):
476
+ """
477
+ Penalizes any class scheduled in the last 2 slots of the day (slots 6, 7).
478
+ This encourages classes to end by 3:30 PM (after slot 5).
479
+ """
480
+ weight = self.weights.get("avoid_late_afternoon", 40)
481
+ if weight == 0: return
482
 
483
+ tasks_by_section = defaultdict(list)
484
+ for task in self.tasks:
485
+ tasks_by_section[task.section.section_id].append(task)
 
 
486
 
487
+ for sec_id, s_tasks in tasks_by_section.items():
488
+ if not s_tasks: continue
 
 
489
 
490
+ for day in range(const.NUM_WORKING_DAYS):
491
+ for s in range(6, const.NUM_TEACHING_SLOTS_PER_DAY): # 6, 7
492
+ abs_slot = day * const.NUM_TEACHING_SLOTS_PER_DAY + s
493
+
494
+ task_covers = []
495
+ for task in s_tasks:
496
+ covers = self.model.NewBoolVar(f"sec_{sec_id}_la_covers_{abs_slot}_{task.task_id}")
497
+ start_var = self.ce.task_vars[task.task_id][0]
498
+ min_start = max(abs_slot - task.duration + 1, day * const.NUM_TEACHING_SLOTS_PER_DAY)
499
+ max_start = abs_slot
500
+
501
+ eq_lits = []
502
+ for p in range(min_start, max_start + 1):
503
+ is_p = self.model.NewBoolVar(f"sec_{sec_id}_la_t_{task.task_id}_s_{p}")
504
+ self.model.Add(start_var == p).OnlyEnforceIf(is_p)
505
+ self.model.Add(start_var != p).OnlyEnforceIf(is_p.Not())
506
+ eq_lits.append(is_p)
507
+
508
+ self.model.AddBoolOr(eq_lits).OnlyEnforceIf(covers)
509
+ for lit in eq_lits:
510
+ self.model.AddImplication(covers.Not(), lit.Not())
511
+
512
+ task_covers.append(covers)
513
+
514
+ is_occupied = self.model.NewBoolVar(f"sec_{sec_id}_la_occ_{abs_slot}")
515
+ self.model.AddBoolOr(task_covers).OnlyEnforceIf(is_occupied)
516
+ for lit in task_covers:
517
+ self.model.AddImplication(is_occupied.Not(), lit.Not())
518
+
519
+ # Penalize if OCCUPIED
520
+ self.penalties.append(is_occupied * weight)