11import logging
22from datetime import timedelta
3+ from typing import cast
34
4- from ortools .sat .python .cp_model import CpModel
5+ from ortools .sat .python .cp_model import CpModel , IntVar , LinearExpr
56
67from src .day import Day
78from src .employee import Employee
8- from src .shift import Shift
99
1010from ..variables import EmployeeWorksOnDayVariables , ShiftAssignmentVariables
11- from .constraint import Constraint
11+ from .objective import Objective
1212
1313
14- class EverySecondWeekendFreeConstraint ( Constraint ):
14+ class EverySecondWeekendFreeObjective ( Objective ):
1515 @property
1616 def KEY (self ) -> str :
1717 return "every-second-weekend-free"
1818
1919 def __init__ (
2020 self ,
21+ weight : float ,
2122 employees : list [Employee ],
2223 days : list [Day ],
23- shifts : list [Shift ],
2424 ):
2525 """
26- Initializes the constraint that enforces alternating free weekends.
26+ Initializes the objective that encourages alternating free weekends.
2727 A weekend is defined as Saturday and Sunday, both days must be free.
2828 """
29- super ().__init__ (employees , days , shifts )
29+ super ().__init__ (weight , employees , days , [] )
3030
3131 def create (
3232 self ,
3333 model : CpModel ,
3434 shift_assignment_variables : ShiftAssignmentVariables ,
3535 employee_works_on_day_variables : EmployeeWorksOnDayVariables ,
36- ) -> None :
36+ ) -> LinearExpr :
37+ penalties : list [IntVar ] = []
38+
3739 # Collect all complete weekends (Saturday-Sunday pairs) in the planning period
3840 weekends : list [tuple [Day , Day ]] = []
3941
@@ -57,21 +59,17 @@ def create(
5759 logging .info (f"Found { len (weekends )} complete weekends in the planning period" )
5860
5961 for employee in self ._employees :
60- if employee .hidden :
61- continue
62-
63- # For each pair of consecutive weekends, enforce alternating pattern
62+ # For each pair of consecutive weekends, penalize if both are free or both have work
6463 for i in range (len (weekends ) - 1 ):
6564 # Get two consecutive weekends
6665 weekend1_sat , weekend1_sun = weekends [i ]
6766 weekend2_sat , weekend2_sun = weekends [i + 1 ]
68-
6967 w1_sat_var = employee_works_on_day_variables [employee ][weekend1_sat ]
7068 w1_sun_var = employee_works_on_day_variables [employee ][weekend1_sun ]
7169 w2_sat_var = employee_works_on_day_variables [employee ][weekend2_sat ]
7270 w2_sun_var = employee_works_on_day_variables [employee ][weekend2_sun ]
7371
74- # Create boolean variables for weekend status
72+ # Check if weekends are free (both days must be free)
7573 w1_free = model .new_bool_var (f"w1_free_e:{ employee .get_key ()} _i:{ i } " )
7674 w2_free = model .new_bool_var (f"w2_free_e:{ employee .get_key ()} _i:{ i } " )
7775
@@ -81,7 +79,16 @@ def create(
8179
8280 model .add (w2_sat_var + w2_sun_var == 0 ).only_enforce_if (w2_free )
8381 model .add (w2_sat_var + w2_sun_var >= 1 ).only_enforce_if (w2_free .Not ())
82+ same_status_penalty = model .new_bool_var (f"same_status_penalty_e:{ employee .get_key ()} _i:{ i } " )
83+
84+ # Penalty = 1 if (w1_free AND w2_free) OR (NOT w1_free AND NOT w2_free)
85+ model .add (same_status_penalty == 1 ).only_enforce_if ([w1_free , w2_free ])
86+ model .add (same_status_penalty == 1 ).only_enforce_if ([w1_free .Not (), w2_free .Not ()])
87+
88+ # these two penalties seem useless
89+ model .add (same_status_penalty == 0 ).only_enforce_if ([w1_free , w2_free .Not ()])
90+ model .add (same_status_penalty == 0 ).only_enforce_if ([w1_free .Not (), w2_free ])
91+
92+ penalties .append (same_status_penalty )
8493
85- # Hard constraint: two consecutive weekends may not both be worked
86- # At least one of the two consecutive weekends must be free
87- model .add_bool_or ([w1_free , w2_free ])
94+ return cast (LinearExpr , sum (penalties ) * self .weight )
0 commit comments