@@ -366,6 +366,16 @@ def generate_planning(
366366 agents , vacations , week_schedule , dayOff , previous_week_schedule , initial_shifts
367367):
368368 model = cp_model .CpModel ()
369+ solver_config = config .get ("solver" , {})
370+ max_time_seconds = int (solver_config .get ("max_time_seconds" , 600 ))
371+ global_max_gap = int (solver_config .get ("global_max_gap" , 240 ))
372+ period_max_gap = int (solver_config .get ("period_max_gap" , 240 ))
373+ relative_gap_limit = float (solver_config .get ("relative_gap_limit" , 0.10 ))
374+ num_search_workers = int (solver_config .get ("num_search_workers" , 0 ))
375+ optimize_period_balance = bool (
376+ solver_config .get ("optimize_period_balance" , False )
377+ )
378+ period_balance_weight = int (solver_config .get ("period_balance_weight" , 2 ))
369379
370380 weeks_split = split_into_weeks (week_schedule ) # Divide week_schedule into weeks
371381
@@ -529,14 +539,10 @@ def generate_planning(
529539 ########################################################
530540 # Staff leave
531541 date_format_full = "%d-%m-%Y"
532- total_hours = {}
542+ leave_paid_hours_by_day = defaultdict ( int )
533543
534544 for agent in agents :
535545 agent_name = agent ["name" ]
536- total_hours [agent_name ] = (
537- 0 # Initialise the total number of hours for the agent
538- )
539-
540546 # Retrieve leave information if available
541547 if "vacations" in agent and isinstance (agent ["vacations" ], list ):
542548 for vac in agent ["vacations" ]:
@@ -566,9 +572,11 @@ def generate_planning(
566572 planning [(agent_name , day_str , vacation )] == 0
567573 )
568574
569- # Calculating hours for days off (7 hours from Monday to Saturday)
575+ # Paid leave hours ( Monday to Saturday): 7h * 10
570576 if day_date .weekday () < 6 : # Monday (0) to Saturday (5)
571- total_hours [agent_name ] += 70 # 7 hours * 10
577+ leave_paid_hours_by_day [(agent_name , day_str )] = (
578+ conge_duration
579+ )
572580
573581 # If the leave starts on a Monday, add the unavailability from the previous weekend
574582 if vacation_start .weekday () == 0 : # Monday (0)
@@ -769,22 +777,19 @@ def generate_planning(
769777 # (Put here all the [soft] constraints that influence the overall objective)
770778
771779 ########################################################
772- # Balance constraint: all agents must have a similar volume of working hours
773- workload_hours = {}
780+ # Balance constraint: all agents must have a similar volume of paid hours
781+ # (worked shifts + paid leave)
782+ paid_hours = {}
774783 for agent in agents :
775784 agent_name = agent ["name" ]
776- workload_hours [agent_name ] = sum (
785+ paid_hours [agent_name ] = cp_model .LinearExpr .Sum (
786+ list (
777787 planning [(agent_name , day , "Jour" )] * jour_duration
778788 + planning [(agent_name , day , "Nuit" )] * nuit_duration
779789 + planning [(agent_name , day , "CDP" )] * cdp_duration
780- +
781- # Leave duration added for each day of leave detected
782- (
783- conge_duration
784- if is_vacation_day (agent_name , day , dayOff ) and not is_weekend (day )
785- else 0
786- )
790+ + leave_paid_hours_by_day [(agent_name , day )]
787791 for day in week_schedule
792+ )
788793 )
789794
790795 # Impose a limit on the difference between the minimum and maximum hours worked by employees
@@ -795,13 +800,13 @@ def generate_planning(
795800 0 , 10000 , "max_hours"
796801 ) # Upper limit - Adjust terminals (*10) if necessary
797802
798- # Constraint on balancing hours worked between agents
799- for agent_name in total_hours :
800- model .Add (min_hours <= total_hours [agent_name ])
801- model .Add (total_hours [agent_name ] <= max_hours )
803+ # Constraint on balancing paid hours between agents
804+ for agent_name in paid_hours :
805+ model .Add (min_hours <= paid_hours [agent_name ])
806+ model .Add (paid_hours [agent_name ] <= max_hours )
802807
803808 # Constraining the difference between max_hours and min_hours for global equilibrium
804- model .Add (max_hours - min_hours <= 240 ) # Adjust flexibility if necessary (*10 )
809+ model .Add (max_hours - min_hours <= global_max_gap )
805810 ########################################################
806811
807812 ########################################################
@@ -810,36 +815,38 @@ def generate_planning(
810815 # Split week_schedule into monthly or single periods
811816 periods = split_by_month_or_period (week_schedule )
812817
813- for period in periods :
818+ period_balancing_terms = []
819+ for period_idx , period in enumerate (periods ):
814820 period_total_hours = {}
815821 for agent in agents :
816822 agent_name = agent ["name" ]
817- # Calculation of total hours per agent over the period
823+ # Calculation of total paid hours per agent over the period
818824 period_total_hours [agent_name ] = cp_model .LinearExpr .Sum (
819825 list (
820826 planning [(agent_name , day , "Jour" )] * jour_duration
821827 + planning [(agent_name , day , "Nuit" )] * nuit_duration
822828 + planning [(agent_name , day , "CDP" )] * cdp_duration
829+ + leave_paid_hours_by_day [(agent_name , day )]
823830 for day in period
824831 )
825832 )
826833
827- # Define min_hours and max_hours for balancing
828- min_hours = model . NewIntVar (
829- 0 , 100000 , "min_hours_period"
830- ) # Lower limit - Adjust terminals (*10) if necessary
831- max_hours = model . NewIntVar (
832- 0 , 100000 , "max_hours_period"
833- ) # Upper limit - Adjust terminals (*10) if necessary
834-
835- # Add constraints for each agent
836- for agent_name in total_hours :
837- model .Add ( min_hours <= total_hours [ agent_name ] )
838- model .Add (total_hours [ agent_name ] <= max_hours )
839-
840- # Limiting the time difference between agents
841- max_difference = 240 # Adjust flexibility if necessary (*10)
842- model . Add ( max_hours - min_hours <= max_difference )
834+ min_period_hours = model . NewIntVar (
835+ 0 , 100000 , f"min_hours_period_ { period_idx } "
836+ )
837+ max_period_hours = model . NewIntVar (
838+ 0 , 100000 , f"max_hours_period_ { period_idx } "
839+ )
840+ for agent in agents :
841+ agent_name = agent [ "name" ]
842+ model . Add ( min_period_hours <= period_total_hours [ agent_name ])
843+ model . Add ( period_total_hours [ agent_name ] <= max_period_hours )
844+ period_gap = model .NewIntVar ( 0 , 100000 , f"period_gap_ { period_idx } " )
845+ model .Add (period_gap == max_period_hours - min_period_hours )
846+ model . Add ( period_gap <= period_max_gap )
847+ period_balancing_terms . append ( period_gap )
848+
849+ period_balancing_objective = cp_model . LinearExpr . Sum ( period_balancing_terms )
843850 ########################################################
844851
845852 ########################################################
@@ -1125,18 +1132,25 @@ def generate_planning(
11251132 )
11261133 )
11271134
1128- # Maximize the overall objective, with a strong preference for preferred shifts and balancing hours
1129- model . Maximize (
1135+ # Maximize the overall objective, with a strong preference for preferred shifts.
1136+ objective = (
11301137 objective_preferred_vacations
11311138 + objective_other_vacations
11321139 + penalized_vacations
11331140 - weekend_balancing_objective
1134- # (variance_weight * total_variance) # ! Temporarily suspended similar hours constraint
11351141 )
1142+ if optimize_period_balance :
1143+ objective -= period_balance_weight * period_balancing_objective
1144+ # (variance_weight * total_variance) # ! Temporarily suspended similar hours constraint
1145+ model .Maximize (objective )
11361146
11371147 # Solver
11381148 solver = cp_model .CpSolver ()
1139- solver .parameters .max_time_in_seconds = 600 # Limit of resolution time in seconds
1149+ if num_search_workers > 0 :
1150+ solver .parameters .num_search_workers = num_search_workers
1151+ if relative_gap_limit > 0 :
1152+ solver .parameters .relative_gap_limit = relative_gap_limit
1153+ solver .parameters .max_time_in_seconds = max_time_seconds
11401154 status = solver .Solve (model )
11411155 # Log the solution
11421156 print (
@@ -1146,6 +1160,8 @@ def generate_planning(
11461160 solver .NumConflicts (),
11471161 " branches :" ,
11481162 solver .NumBranches (),
1163+ " execution time :" ,
1164+ f"{ solver .WallTime ():.4f} seconds" ,
11491165 )
11501166
11511167 # Results
0 commit comments