From c8a21216e0358e348452a7fcb57edcf798efa45f Mon Sep 17 00:00:00 2001 From: Hgamo <73846452+Hgamo@users.noreply.github.com> Date: Thu, 15 Jan 2026 13:16:32 +0100 Subject: [PATCH] =?UTF-8?q?Revert=20"Add=20EverySecondWeekendFreeConstrain?= =?UTF-8?q?t=20and=20remove=20objective=20implementat=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit ed802d94eea6f86f983e3acbdfaa00a12cfb585f. --- src/cp/__init__.py | 6 +-- src/cp/constraints/__init__.py | 3 -- src/cp/objectives/__init__.py | 3 ++ .../every_second_weekend_free.py | 41 +++++++++++-------- src/solve.py | 5 ++- tests/cp/constraints/test_all_constraints.py | 2 + 6 files changed, 35 insertions(+), 25 deletions(-) rename src/cp/{constraints => objectives}/every_second_weekend_free.py (67%) diff --git a/src/cp/__init__.py b/src/cp/__init__.py index d2d4a3fb..bca04710 100644 --- a/src/cp/__init__.py +++ b/src/cp/__init__.py @@ -1,9 +1,6 @@ from .constraints import ( Constraint as Constraint, ) -from .constraints import ( - EverySecondWeekendFreeConstraint as EverySecondWeekendFreeConstraint, -) from .constraints import ( FreeDayAfterNightShiftPhaseConstraint as FreeDayAfterNightShiftPhaseConstraint, ) @@ -32,6 +29,9 @@ VacationDaysAndShiftsConstraint as VacationDaysAndShiftsConstraint, ) from .model import Model as Model +from .objectives import ( + EverySecondWeekendFreeObjective as EverySecondWeekendFreeObjective, +) from .objectives import ( FreeDaysAfterNightShiftPhaseObjective as FreeDaysAfterNightShiftPhaseObjective, ) diff --git a/src/cp/constraints/__init__.py b/src/cp/constraints/__init__.py index 39fc6105..95a85d17 100644 --- a/src/cp/constraints/__init__.py +++ b/src/cp/constraints/__init__.py @@ -1,7 +1,4 @@ from .constraint import Constraint as Constraint -from .every_second_weekend_free import ( - EverySecondWeekendFreeConstraint as EverySecondWeekendFreeConstraint, -) from .free_day_after_night_shift_phase import ( FreeDayAfterNightShiftPhaseConstraint as FreeDayAfterNightShiftPhaseConstraint, ) diff --git a/src/cp/objectives/__init__.py b/src/cp/objectives/__init__.py index b6897a3e..b9c5d629 100644 --- a/src/cp/objectives/__init__.py +++ b/src/cp/objectives/__init__.py @@ -1,3 +1,6 @@ +from .every_second_weekend_free import ( + EverySecondWeekendFreeObjective as EverySecondWeekendFreeObjective, +) from .free_days_after_night_shift_phase import ( FreeDaysAfterNightShiftPhaseObjective as FreeDaysAfterNightShiftPhaseObjective, ) diff --git a/src/cp/constraints/every_second_weekend_free.py b/src/cp/objectives/every_second_weekend_free.py similarity index 67% rename from src/cp/constraints/every_second_weekend_free.py rename to src/cp/objectives/every_second_weekend_free.py index c187f2cc..b7bd401c 100644 --- a/src/cp/constraints/every_second_weekend_free.py +++ b/src/cp/objectives/every_second_weekend_free.py @@ -1,39 +1,41 @@ import logging from datetime import timedelta +from typing import cast -from ortools.sat.python.cp_model import CpModel +from ortools.sat.python.cp_model import CpModel, IntVar, LinearExpr from src.day import Day from src.employee import Employee -from src.shift import Shift from ..variables import EmployeeWorksOnDayVariables, ShiftAssignmentVariables -from .constraint import Constraint +from .objective import Objective -class EverySecondWeekendFreeConstraint(Constraint): +class EverySecondWeekendFreeObjective(Objective): @property def KEY(self) -> str: return "every-second-weekend-free" def __init__( self, + weight: float, employees: list[Employee], days: list[Day], - shifts: list[Shift], ): """ - Initializes the constraint that enforces alternating free weekends. + Initializes the objective that encourages alternating free weekends. A weekend is defined as Saturday and Sunday, both days must be free. """ - super().__init__(employees, days, shifts) + super().__init__(weight, employees, days, []) def create( self, model: CpModel, shift_assignment_variables: ShiftAssignmentVariables, employee_works_on_day_variables: EmployeeWorksOnDayVariables, - ) -> None: + ) -> LinearExpr: + penalties: list[IntVar] = [] + # Collect all complete weekends (Saturday-Sunday pairs) in the planning period weekends: list[tuple[Day, Day]] = [] @@ -57,21 +59,17 @@ def create( logging.info(f"Found {len(weekends)} complete weekends in the planning period") for employee in self._employees: - if employee.hidden: - continue - - # For each pair of consecutive weekends, enforce alternating pattern + # For each pair of consecutive weekends, penalize if both are free or both have work for i in range(len(weekends) - 1): # Get two consecutive weekends weekend1_sat, weekend1_sun = weekends[i] weekend2_sat, weekend2_sun = weekends[i + 1] - w1_sat_var = employee_works_on_day_variables[employee][weekend1_sat] w1_sun_var = employee_works_on_day_variables[employee][weekend1_sun] w2_sat_var = employee_works_on_day_variables[employee][weekend2_sat] w2_sun_var = employee_works_on_day_variables[employee][weekend2_sun] - # Create boolean variables for weekend status + # Check if weekends are free (both days must be free) w1_free = model.new_bool_var(f"w1_free_e:{employee.get_key()}_i:{i}") w2_free = model.new_bool_var(f"w2_free_e:{employee.get_key()}_i:{i}") @@ -81,7 +79,16 @@ def create( model.add(w2_sat_var + w2_sun_var == 0).only_enforce_if(w2_free) model.add(w2_sat_var + w2_sun_var >= 1).only_enforce_if(w2_free.Not()) + same_status_penalty = model.new_bool_var(f"same_status_penalty_e:{employee.get_key()}_i:{i}") + + # Penalty = 1 if (w1_free AND w2_free) OR (NOT w1_free AND NOT w2_free) + model.add(same_status_penalty == 1).only_enforce_if([w1_free, w2_free]) + model.add(same_status_penalty == 1).only_enforce_if([w1_free.Not(), w2_free.Not()]) + + # these two penalties seem useless + model.add(same_status_penalty == 0).only_enforce_if([w1_free, w2_free.Not()]) + model.add(same_status_penalty == 0).only_enforce_if([w1_free.Not(), w2_free]) + + penalties.append(same_status_penalty) - # Hard constraint: two consecutive weekends may not both be worked - # At least one of the two consecutive weekends must be free - model.add_bool_or([w1_free, w2_free]) + return cast(LinearExpr, sum(penalties) * self.weight) diff --git a/src/solve.py b/src/solve.py index bbc9cab0..9a1496cb 100644 --- a/src/solve.py +++ b/src/solve.py @@ -3,7 +3,7 @@ from datetime import date from src.cp import ( - EverySecondWeekendFreeConstraint, + EverySecondWeekendFreeObjective, FreeDayAfterNightShiftPhaseConstraint, FreeDaysAfterNightShiftPhaseObjective, FreeDaysNearWeekendObjective, @@ -53,6 +53,7 @@ def main( "rotate": 1, "wishes": 3, "after_night": 3, + "second_weekend": 1, } logging.info("General information:") @@ -75,7 +76,6 @@ def main( VacationDaysAndShiftsConstraint(employees, days, shifts), HierarchyOfIntermediateShiftsConstraint(employees, days, shifts), PlannedShiftsConstraint(employees, days, shifts), - EverySecondWeekendFreeConstraint(employees, days, shifts), ] objectives = [ FreeDaysNearWeekendObjective(weights["free_weekend"], employees, days), @@ -86,6 +86,7 @@ def main( RotateShiftsForwardObjective(weights["rotate"], employees, days, shifts), MaximizeEmployeeWishesObjective(weights["wishes"], employees, days, shifts), FreeDaysAfterNightShiftPhaseObjective(weights["after_night"], employees, days, shifts), + EverySecondWeekendFreeObjective(weights["second_weekend"], employees, days), ] model = Model(employees, days, shifts) diff --git a/tests/cp/constraints/test_all_constraints.py b/tests/cp/constraints/test_all_constraints.py index 283508aa..331d713b 100644 --- a/tests/cp/constraints/test_all_constraints.py +++ b/tests/cp/constraints/test_all_constraints.py @@ -26,6 +26,7 @@ ) from src.cp.model import Model from src.cp.objectives import ( + EverySecondWeekendFreeObjective, FreeDaysAfterNightShiftPhaseObjective, FreeDaysNearWeekendObjective, MaximizeEmployeeWishesObjective, @@ -114,6 +115,7 @@ def test_all_constraints_mass_case( model.add_objective(RotateShiftsForwardObjective(1.0, employees, days, shifts)) model.add_objective(MaximizeEmployeeWishesObjective(3.0, employees, days, shifts)) model.add_objective(FreeDaysAfterNightShiftPhaseObjective(3.0, employees, days, shifts)) + model.add_objective(EverySecondWeekendFreeObjective(1.0, employees, days)) model.add_constraint(FreeDayAfterNightShiftPhaseConstraint(employees, days, shifts)) model.add_constraint(HierarchyOfIntermediateShiftsConstraint(employees, days, shifts))