KindAlien commited on
Commit
2477400
·
verified ·
1 Parent(s): 608ea8f

Update objective_engine.py

Browse files
Files changed (1) hide show
  1. objective_engine.py +42 -54
objective_engine.py CHANGED
@@ -108,7 +108,7 @@ class ObjectiveEngine:
108
  self._avoid_late_heavy_subjects()
109
  self._minimize_faculty_gaps()
110
  self._minimize_campus_movement()
111
- self._penalize_first_hour_free()
112
  self._minimize_student_gaps()
113
 
114
  # Summation of all penalties
@@ -457,63 +457,51 @@ class ObjectiveEngine:
457
  self.penalties.append(both_occupied * max(1, weight // 2))
458
 
459
  def _penalize_first_hour_free(self):
 
460
  """
461
- Strongly penalizes having the first hour (period index 0) free for any
462
- section on any day. The solver will avoid this unless there is genuinely
463
- no other feasible assignment.
464
-
465
- Rewritten to reuse cached day booleans and daily slot vars for efficiency.
466
  """
467
- weight = self.weights.get("no_first_hour_free", 20)
468
- if weight == 0:
469
- return
470
 
471
- # Group tasks by parent section (merge batches into parent)
472
- tasks_by_parent = defaultdict(list)
473
  for task in self.tasks:
474
- sid = task.section.section_id
475
- parent = sid.split('-')[0] if '-' in sid else sid
476
- tasks_by_parent[parent].append(task)
477
 
478
- for sec_id, sec_tasks in tasks_by_parent.items():
479
- # Ensure day booleans exist
480
- for task in sec_tasks:
481
- self._ensure_day_bools(task)
482
 
483
  for day in range(const.NUM_WORKING_DAYS):
484
- # The absolute slot index for period 0 of this day
485
- first_slot = day * const.NUM_TEACHING_SLOTS_PER_DAY
486
-
487
- # For each task, create a bool: does it start at exactly first_slot?
488
- starts_at_first = []
489
- for task in sec_tasks:
490
- start_var = self.ce.task_vars[task.task_id][0]
491
-
492
- at_first = self.model.NewBoolVar(
493
- f"at1st_{task.task_id}_d{day}")
494
- self.model.Add(
495
- start_var == first_slot).OnlyEnforceIf(at_first)
496
- self.model.Add(
497
- start_var != first_slot).OnlyEnforceIf(at_first.Not())
498
- starts_at_first.append(at_first)
499
-
500
- if not starts_at_first:
501
- continue
502
-
503
- # any_at_first = True if at least one task starts at period 0
504
- any_at_first = self.model.NewBoolVar(
505
- f"any_at1st_{sec_id}_d{day}")
506
- self.model.AddBoolOr(
507
- starts_at_first).OnlyEnforceIf(any_at_first)
508
- for lit in starts_at_first:
509
- self.model.AddImplication(any_at_first.Not(), lit.Not())
510
-
511
- # Penalty when the first hour IS free (no task at period 0)
512
- first_free = self.model.NewBoolVar(
513
- f"first_free_{sec_id}_d{day}")
514
- self.model.Add(
515
- first_free == 1).OnlyEnforceIf(any_at_first.Not())
516
- self.model.Add(
517
- first_free == 0).OnlyEnforceIf(any_at_first)
518
-
519
- self.penalties.append(first_free * weight)
 
108
  self._avoid_late_heavy_subjects()
109
  self._minimize_faculty_gaps()
110
  self._minimize_campus_movement()
111
+ self._pack_morning_session()
112
  self._minimize_student_gaps()
113
 
114
  # Summation of all penalties
 
457
  self.penalties.append(both_occupied * max(1, weight // 2))
458
 
459
  def _penalize_first_hour_free(self):
460
+ def _pack_morning_session(self):
461
  """
462
+ Penalizes any free slot in the first 4 hours (morning session) for any section.
463
+ This strongly forces the solver to pack the morning tightly, leaving afternoons free if necessary,
464
+ and completely eliminating gaps in the first 4 hours.
 
 
465
  """
466
+ weight = self.weights.get("pack_morning", 500)
467
+ if weight == 0: return
 
468
 
469
+ from collections import defaultdict
470
+ tasks_by_sec = defaultdict(list)
471
  for task in self.tasks:
472
+ tasks_by_sec[task.section.section_id].append(task)
 
 
473
 
474
+ for sec_id, sec_tasks in tasks_by_sec.items():
475
+ if not sec_tasks: continue
 
 
476
 
477
  for day in range(const.NUM_WORKING_DAYS):
478
+ for slot in range(4): # First 4 hours
479
+ abs_slot = day * const.NUM_TEACHING_SLOTS_PER_DAY + slot
480
+ slot_covs = []
481
+
482
+ for task in sec_tasks:
483
+ start_var = self.ce.task_vars[task.task_id][0]
484
+ cov = self.model.NewBoolVar(f"morn_cov_{task.task_id}_s{abs_slot}")
485
+ b1 = self.model.NewBoolVar(f"mb1_{task.task_id}_s{abs_slot}")
486
+ b2 = self.model.NewBoolVar(f"mb2_{task.task_id}_s{abs_slot}")
487
+
488
+ self.model.Add(start_var <= abs_slot).OnlyEnforceIf(b1)
489
+ self.model.Add(start_var > abs_slot).OnlyEnforceIf(b1.Not())
490
+ self.model.Add(start_var + task.duration > abs_slot).OnlyEnforceIf(b2)
491
+ self.model.Add(start_var + task.duration <= abs_slot).OnlyEnforceIf(b2.Not())
492
+
493
+ self.model.AddBoolAnd([b1, b2]).OnlyEnforceIf(cov)
494
+ self.model.AddBoolOr([b1.Not(), b2.Not()]).OnlyEnforceIf(cov.Not())
495
+ slot_covs.append(cov)
496
+
497
+ is_active = self.model.NewBoolVar(f"morn_act_{sec_id}_d{day}_s{slot}")
498
+ self.model.AddBoolOr(slot_covs).OnlyEnforceIf(is_active)
499
+ for c in slot_covs:
500
+ self.model.AddImplication(is_active.Not(), c.Not())
501
+
502
+ # We penalize if is_active is False
503
+ is_free = self.model.NewBoolVar(f"morn_free_{sec_id}_d{day}_s{slot}")
504
+ self.model.Add(is_active == 0).OnlyEnforceIf(is_free)
505
+ self.model.Add(is_active == 1).OnlyEnforceIf(is_free.Not())
506
+
507
+ self.penalties.append(is_free * weight)