From bd3b9b9afc07719202f3c1a159db4443ac005b06 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 6 Sep 2021 12:25:18 +0100 Subject: [PATCH 1/2] trial switching to generic night site --- shifts.py | 1401 ++++++++++++++++++++++++++++++++-------------------- test.py | 3 +- workers.py | 77 +-- 3 files changed, 918 insertions(+), 563 deletions(-) diff --git a/shifts.py b/shifts.py index b353a8b..cd3f60c 100644 --- a/shifts.py +++ b/shifts.py @@ -26,16 +26,18 @@ days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] full_weekend_count = 2 +# Load uk based bank holidays from govuk_bank_holidays.bank_holidays import BankHolidays bank_holidays = BankHolidays() bank_holiday_map = {} for bank_holiday in bank_holidays.get_holidays(): - bank_holiday_map[bank_holiday['date']] = bank_holiday['title'] + bank_holiday_map[bank_holiday["date"]] = bank_holiday["title"] class SingleShift(object): """Class to hold all details for a shift""" + def __init__( self, sites, @@ -74,13 +76,13 @@ class SingleShift(object): self.force_as_block = force_as_block self.force_as_block_unless_nwd = force_as_block_unless_nwd self.constraints = constraints - + self.bank_holidays_only = bank_holidays_only - #self.total_shifts = length * len(days) * len(shift_days) + # self.total_shifts = length * len(days) * len(shift_days) def __str__(self): - return f"name: {self.name}, site: {self.site}, length: {self.length}, balance weighting: {self.balance_weighting}, balance offset: {self.balance_offset}, hard_constrain: {self.hard_constrain_shift}, workers required : {self.workers_required}, rota on nwds {self.rota_on_nwds}, assign as block: {self.assign_as_block}, force as block: {self.force_as_block}, constraints: {self.constraints}"#, total shifts: {self.total_shifts}" + return f"name: {self.name}, site: {self.site}, length: {self.length}, balance weighting: {self.balance_weighting}, balance offset: {self.balance_offset}, hard_constrain: {self.hard_constrain_shift}, workers required : {self.workers_required}, rota on nwds {self.rota_on_nwds}, assign as block: {self.assign_as_block}, force as block: {self.force_as_block}, constraints: {self.constraints}" # , total shifts: {self.total_shifts}" def get_shift_summary(self): """ @@ -88,21 +90,26 @@ class SingleShift(object): """ return "{}: {} workers for sites ({}) on days ({})".format( - self.name, self.workers_required, ", ".join(self.site), - ", ".join(self.shift_days)) + self.name, + self.workers_required, + ", ".join(self.site), + ", ".join(self.shift_days), + ) class RotaBuilder(object): """Class to hold and manipulate shifts""" - def __init__(self, - start_date, - weeks_to_rota=26, - balance_offset_modifier=1, - ltft_balance_offset=2, - max_night_frequency=2, - max_weekend_frequency=2, - use_previous_shifts=False, - ): + + def __init__( + self, + start_date, + weeks_to_rota=26, + balance_offset_modifier=1, + ltft_balance_offset=2, + max_night_frequency=2, + max_weekend_frequency=2, + use_previous_shifts=False, + ): print("Start time: {}".format(datetime.datetime.now())) print("Weeks to rota: {}".format(weeks_to_rota)) @@ -119,13 +126,15 @@ class RotaBuilder(object): self.use_previous_shifts = use_previous_shifts if start_date.weekday() != 0: - raise ValueError("Start date {} must be a Mon (not a {})".format( - start_date.isoformat(), days[start_date.weekday()])) + raise ValueError( + "Start date {} must be a Mon (not a {})".format( + start_date.isoformat(), days[start_date.weekday()] + ) + ) self.start_date = start_date self.rota_days_length = len(self.weeks) * 7 - self.rota_end_date = self.start_date + datetime.timedelta( - self.rota_days_length) + self.rota_end_date = self.start_date + datetime.timedelta(self.rota_days_length) self.unavailable_to_work = set() self.unavailable_to_work_reason = {} @@ -165,6 +174,8 @@ class RotaBuilder(object): "prevent_monday_and_tuesday_after_full_weekends": [], "prevent_fridays_before_full_weekends": [], "prevent_thursdays_before_full_weekends": [], + # This may be better defined on the shift? + "require_presence_at_site_overnight": [], "avoid_st2_first_month": False, "hard_constrain_pair_separation": False, } @@ -178,24 +189,26 @@ class RotaBuilder(object): self.week_day_date_map[(week, day)] = d n = n + 1 - def solve_model(self, solver='cbc', use_neos=False, options={}): + def solve_model(self, solver="cbc", use_neos=False, options={}): print("Setting up solver") - self.opt = SolverFactory(solver) + self.opt = SolverFactory(solver) print("Solving") if use_neos: - solver_manager = SolverManagerFactory('neos') # Solve in neos server - #results = solver_manager.solve(Rota.model, opt=opt, logfile="test.log") - results = solver_manager.solve(self.model, keepfiles=True, tee=True, opt=self.opt, logfile="test.log") + solver_manager = SolverManagerFactory("neos") # Solve in neos server + # results = solver_manager.solve(Rota.model, opt=opt, logfile="test.log") + results = solver_manager.solve( + self.model, keepfiles=True, tee=True, opt=self.opt, logfile="test.log" + ) else: results = self.opt.solve( self.model, tee=True, options=options, - #options={ + # options={ # "threads": 10, - #}, - logfile="test.log" + # }, + logfile="test.log", ) self.results = results @@ -205,7 +218,6 @@ class RotaBuilder(object): if not results.solver.status: sys.exit(0) - def build_model(self): # Initialize model self.model = ConcreteModel() @@ -213,9 +225,12 @@ class RotaBuilder(object): # binary variables representing if a worker is scheduled somewhere # NOTE: this will assign to all possible shift combinations (which we probably don't want) self.model.works = Var( - ((worker.id, week, day, shift) for worker in self.workers - for week, day in self.get_week_day_combinations() - for shift in self.get_shift_names()), + ( + (worker.id, week, day, shift) + for worker in self.workers + for week, day in self.get_week_day_combinations() + for shift in self.get_shift_names() + ), within=Binary, initialize=0, ) @@ -230,31 +245,43 @@ class RotaBuilder(object): # The nights self.model is used to ensure nights are assigned as a block (and limit the number of workers required) self.model.shift_week_worker_assigned = Var( - ((shift, week, worker.id) for worker in self.workers - for week in self.weeks for shift in self.get_shift_names()), + ( + (shift, week, worker.id) + for worker in self.workers + for week in self.weeks + for shift in self.get_shift_names() + ), within=Binary, initialize=0, ) self.model.worker_block_shift_split = Var( - ((week, shift) for week in self.weeks - for shift in self.shifts_to_assign_or_force_as_blocks()), + ( + (week, shift) + for week in self.weeks + for shift in self.shifts_to_assign_or_force_as_blocks() + ), within=Binary, initialize=0, ) self.model.blocks_worker_shift_assigned = Var( - ((worker.id, week, shift) for worker in self.workers - for week in self.weeks - for shift in self.shifts_to_assign_or_force_as_blocks()), + ( + (worker.id, week, shift) + for worker in self.workers + for week in self.weeks + for shift in self.shifts_to_assign_or_force_as_blocks() + ), within=Binary, initialize=0, ) - self.model.blocks_assigned = Var( - ((week, shift) for week in self.weeks - for shift in self.shifts_to_assign_or_force_as_blocks()), + ( + (week, shift) + for week in self.weeks + for shift in self.shifts_to_assign_or_force_as_blocks() + ), within=NonNegativeIntegers, initialize=0, ) @@ -284,32 +311,44 @@ class RotaBuilder(object): # from a single site if self.constraint_options["balance_nights_across_sites"]: self.model.night_per_site = Var( - ((week, shift.name, site) for site in self.sites - for week in self.weeks - for shift in self.get_shifts_with_constraint("night")), - #within=Binary, + ( + (week, shift.name, site) + for site in self.sites + for week in self.weeks + for shift in self.get_shifts_with_constraint("night") + ), + # within=Binary, within=NonNegativeIntegers, initialize=0, ) self.model.night_per_site_t1 = Var( - ((week, shift.name, site) for site in self.sites - for week in self.weeks - for shift in self.get_shifts_with_constraint("night")), + ( + (week, shift.name, site) + for site in self.sites + for week in self.weeks + for shift in self.get_shifts_with_constraint("night") + ), domain=NonNegativeIntegers, initialize=0, ) self.model.night_per_site_t2 = Var( - ((week, shift.name, site) for site in self.sites - for week in self.weeks - for shift in self.get_shifts_with_constraint("night")), + ( + (week, shift.name, site) + for site in self.sites + for week in self.weeks + for shift in self.get_shifts_with_constraint("night") + ), domain=NonNegativeIntegers, initialize=0, ) self.model.shift_count = Var( - ((worker.id, shift) for worker in self.workers - for shift in self.get_shift_names()), + ( + (worker.id, shift) + for worker in self.workers + for shift in self.get_shift_names() + ), domain=NonNegativeReals, initialize=0, ) @@ -390,8 +429,7 @@ class RotaBuilder(object): if self.constraint_options["balance_weekends"]: self.model.works_weekend = Var( - ((worker.id, week) for worker in self.workers - for week in self.weeks), + ((worker.id, week) for worker in self.workers for week in self.weeks), domain=Binary, initialize=0, ) @@ -437,13 +475,16 @@ class RotaBuilder(object): def availability_init(model, wid, week, day): if (wid, week, day) in self.unavailable_to_work: - #print((wid, week, day)) + # print((wid, week, day)) return 0 return 1 self.model.available = Param( - ((worker.id, week, day) for worker in self.workers - for week, day in self.get_week_day_combinations()), + ( + (worker.id, week, day) + for worker in self.workers + for week, day in self.get_week_day_combinations() + ), initialize=availability_init, ) @@ -454,8 +495,11 @@ class RotaBuilder(object): return 0 self.model.work_requests = Param( - ((worker.id, week, day, shift) for worker in self.workers - for week, day, shift in self.get_all_shiftname_combinations()), + ( + (worker.id, week, day, shift) + for worker in self.workers + for week, day, shift in self.get_all_shiftname_combinations() + ), initialize=work_request_init, ) @@ -468,8 +512,11 @@ class RotaBuilder(object): # which would allow more weight to be given if the whole block # is not allocated self.model.pref_not_to_work = Param( - ((worker.id, week, day) for worker in self.workers - for week, day in self.get_week_day_combinations()), + ( + (worker.id, week, day) + for worker in self.workers + for week, day in self.get_week_day_combinations() + ), initialize=pref_init, ) @@ -482,25 +529,34 @@ class RotaBuilder(object): self.build_model_constraints() def build_model_constraints(self): - self.model.constraints = ConstraintList( - ) # Create a set of constraints + self.model.constraints = ConstraintList() # Create a set of constraints # Ensure each shift has the requisit number of workers assigned for ( - week, - day, - shift, - workers_required, - site_required, + week, + day, + shift, + workers_required, + site_required, ) in self.get_required_workers_and_site_combinations(): print(week, day, shift, workers_required, site_required) - self.model.constraints.add(workers_required == sum( - self.model.works[worker.id, week, day, shift] - for worker in self.workers if worker.site in site_required)) + self.model.constraints.add( + workers_required + == sum( + self.model.works[worker.id, week, day, shift] + for worker in self.workers + if worker.site in site_required + ) + ) # And it is not assigned if the worker is from the wrong site - self.model.constraints.add(0 == sum( - self.model.works[worker.id, week, day, shift] - for worker in self.workers if worker.site not in site_required)) + self.model.constraints.add( + 0 + == sum( + self.model.works[worker.id, week, day, shift] + for worker in self.workers + if worker.site not in site_required + ) + ) # # Ensure shifts are only assigned by workers from the allowed sites # self.model.constraints.add(0 == sum( # self.model.works[worker.id, week, day, shift] @@ -508,10 +564,13 @@ class RotaBuilder(object): # Ensure no workers are assigned to shifts that are not required for week, day, shift in self.get_not_required_shifts(): - self.model.constraints.add(0 == sum(self.model.works[worker.id, - week, day, - shift.name] - for worker in self.workers)) + self.model.constraints.add( + 0 + == sum( + self.model.works[worker.id, week, day, shift.name] + for worker in self.workers + ) + ) # Constraint: total hours worked hours worked # for worker in self.workers: @@ -527,25 +586,25 @@ class RotaBuilder(object): # Define a maximum number hours that can be worked per week # NOTE: this works on a calander week not a 7 day period - + # TODO: consider fixing - #def maxHoursPerWeekRule(model, wid, week): + # def maxHoursPerWeekRule(model, wid, week): # return 72 >= sum( # shift.length * model.works[wid, week, day, shift.name] # for day, shift in self.get_day_shiftclass_combinations()) - #self.model.max_hours_per_week_constraint = Constraint( + # self.model.max_hours_per_week_constraint = Constraint( # [worker.id for worker in self.workers], # self.weeks, # rule=maxHoursPerWeekRule) # Define an upper limit for hours worked over the rota period - #def maxHoursRule(model, wid): + # def maxHoursRule(model, wid): # return 8000 >= sum( # shift.length * model.works[wid, week, day, shift.name] # for week, day, shift in self.get_all_shiftclass_combinations()) - #self.model.max_hours_constraint = Constraint( + # self.model.max_hours_constraint = Constraint( # [worker.id for worker in self.workers], rule=maxHoursRule) # Limit to 1 ST2 (or below) on a night shift @@ -554,16 +613,18 @@ class RotaBuilder(object): single_workers = [w for w in self.workers if w.grade < 3] if not single_workers: return Constraint.Skip - return sum(model.shift_week_worker_assigned[shift, week, w.id] - for w in single_workers) <= 1 + return ( + sum( + model.shift_week_worker_assigned[shift, week, w.id] + for w in single_workers + ) + <= 1 + ) if self.constraint_options["limit_to_1_st2_on_nights"]: self.model.night_shifts_max_st_constraint = Constraint( [week for week in self.weeks], - [ - shift.name - for shift in self.get_shifts_with_constraint("night") - ], + [shift.name for shift in self.get_shifts_with_constraint("night")], rule=nightShiftMaxSTRule, ) @@ -571,36 +632,63 @@ class RotaBuilder(object): single_workers = [w for w in self.workers if w.grade >= 4] if not single_workers: return Constraint.Skip - return sum(model.shift_week_worker_assigned[shift, week, w.id] - for w in single_workers) >= 1 + return ( + sum( + model.shift_week_worker_assigned[shift, week, w.id] + for w in single_workers + ) + >= 1 + ) if self.constraint_options["ensure_1_st4_plus_on_nights"]: self.model.night_shifts_min_st4_constraint = Constraint( [week for week in self.weeks], - [ - shift.name - for shift in self.get_shifts_with_constraint("night") - ], + [shift.name for shift in self.get_shifts_with_constraint("night")], rule=nightShiftMinST4Rule, ) - def nightShiftDerrifordRule(model, week, shift): - derriford_workers = [w for w in self.workers if w.night_at_derriford >= 1] - #if not derriford_workers: - #return Constraint.Skip - return sum(model.shift_week_worker_assigned[shift, week, w.id] - for w in derriford_workers) >= 1 + # def nightShiftDerrifordRule(model, week, shift): + # derriford_workers = [w for w in self.workers if w.night_at_derriford >= 1] + # # if not derriford_workers: + # # return Constraint.Skip + # return ( + # sum( + # model.shift_week_worker_assigned[shift, week, w.id] + # for w in derriford_workers + # ) + # >= 1 + # ) - if self.constraint_options["ensure_derriford_reg_for_nights"]: - self.model.night_shifts_derriford_rule = Constraint( - [week for week in self.weeks], - [ - shift.name - for shift in self.get_shifts_with_constraint("night") - ], - rule=nightShiftDerrifordRule, + # if self.constraint_options["ensure_derriford_reg_for_nights"]: + # self.model.night_shifts_derriford_rule = Constraint( + # [week for week in self.weeks], + # [shift.name for shift in self.get_shifts_with_constraint("night")], + # rule=nightShiftDerrifordRule, + # ) + + def presenceAtSiteOvernight(model, week, shift, required_site): + required_site_workers = [ + w for w in self.workers if required_site == w.night_site + ] + + return ( + sum( + model.shift_week_worker_assigned[shift, week, w.id] + for w in required_site_workers + ) + >= 1 ) + if self.constraint_options["require_presence_at_site_overnight"]: + + for site in self.constraint_options["require_presence_at_site_overnight"]: + self.model.require_presence_at_site_overnight_rule = Constraint( + [week for week in self.weeks], + [shift.name for shift in self.get_shifts_with_constraint("night")], + site, + rule=presenceAtSiteOvernight, + ) + # # Count the number of workers from each site on each night shift # # As 1 or 0 is optimum we can simply subtract 1 from the number if self.constraint_options["balance_nights_across_sites"]: @@ -609,17 +697,21 @@ class RotaBuilder(object): block = shift.name for week in self.weeks: self.model.constraints.add( - self.model.night_per_site[week, block, site] == - sum(self.model.shift_week_worker_assigned[ - block, week, worker.id] + self.model.night_per_site[week, block, site] + == sum( + self.model.shift_week_worker_assigned[ + block, week, worker.id + ] for worker in self.workers - if worker.site == site) - #1 >= sum(self.model.shift_week_worker_assigned[shift.name, week, worker.id] for worker in self.workers if worker.site == site) + if worker.site == site + ) + # 1 >= sum(self.model.shift_week_worker_assigned[shift.name, week, worker.id] for worker in self.workers if worker.site == site) ) self.model.constraints.add( - self.model.night_per_site_t1[week, block, site] - - self.model.night_per_site_t2[week, block, site] == - self.model.night_per_site[week, block, site] - 1) + self.model.night_per_site_t1[week, block, site] + - self.model.night_per_site_t2[week, block, site] + == self.model.night_per_site[week, block, site] - 1 + ) weeks_days = self.get_week_day_combinations() @@ -628,27 +720,33 @@ class RotaBuilder(object): shift = self.get_shift_by_name(shift_name) for worker in self.get_workers_for_shift(shift): self.model.constraints.add( - 8 * self.model.blocks_worker_shift_assigned[ - worker.id, week, shift_name] >= sum( - self.model.works[worker.id, week, day, - shift_name] - for day in shift.shift_days)) + 8 + * self.model.blocks_worker_shift_assigned[ + worker.id, week, shift_name + ] + >= sum( + self.model.works[worker.id, week, day, shift_name] + for day in shift.shift_days + ) + ) self.model.constraints.add( - self.model.blocks_assigned[week, shift_name] == sum( - self.model.blocks_worker_shift_assigned[worker.id, - week, - shift_name] - for worker in self.workers)) + self.model.blocks_assigned[week, shift_name] + == sum( + self.model.blocks_worker_shift_assigned[ + worker.id, week, shift_name + ] + for worker in self.workers + ) + ) if shift.force_as_block: - if shift.force_as_block_unless_nwd: workers = self.get_workers_for_shift(shift) # Get workers who have a nwd on the shift nwd_workers = [] full_workers = [] - + for w in workers: if w.nwd is not None: l = [] @@ -661,142 +759,186 @@ class RotaBuilder(object): nwd_workers.append(w) else: full_workers.append(w) - + else: full_workers.append(w) print(shift.name, [n.name for n in nwd_workers]) - - if not nwd_workers: # Just do the usual + + if not nwd_workers: # Just do the usual self.model.constraints.add( - sum(self.model.blocks_worker_shift_assigned[worker.id, - week, - shift.name] - for worker in self.workers) <= 1) - + sum( + self.model.blocks_worker_shift_assigned[ + worker.id, week, shift.name + ] + for worker in self.workers + ) + <= 1 + ) + else: print(shift.name) self.model.constraints.add( - sum(self.model.blocks_worker_shift_assigned[worker.id, - week, - shift.name] - for worker in full_workers) <= 1) - + sum( + self.model.blocks_worker_shift_assigned[ + worker.id, week, shift.name + ] + for worker in full_workers + ) + <= 1 + ) + self.model.constraints.add( - sum(self.model.blocks_worker_shift_assigned[worker.id, - week, - shift.name] - for worker in workers) <= 2) - - - #self.model.constraints.add( + sum( + self.model.blocks_worker_shift_assigned[ + worker.id, week, shift.name + ] + for worker in workers + ) + <= 2 + ) + + # self.model.constraints.add( # self.model.worker_block_shift_split[week, shift.name] <= 2 - sum(self.model.blocks_worker_shift_assigned[worker.id, # week, # shift.name] # for worker in workers)) - # - #self.model.constraints.add( + # + # self.model.constraints.add( # sum(self.model.works[w.id, week, day,shift.name] # for w in nwd_workers for day in shift.shift_days) - sum(self.model.works[w.id, week, day,shift.name] # for w in full_workers for day in shift.shift_days) >= self.model.worker_block_shift_split[week, shift.name] ) - + else: self.model.constraints.add( - sum(self.model.blocks_worker_shift_assigned[worker.id, - week, - shift.name] - for worker in self.workers) <= 1) + sum( + self.model.blocks_worker_shift_assigned[ + worker.id, week, shift.name + ] + for worker in self.workers + ) + <= 1 + ) # Most of our constraints apply per worker for worker in self.workers: - for week_blocks in self.get_week_block_iterator(4): # Prevent weekend shifts more than once every n weeks self.model.constraints.add( - self.constraint_options["max_shifts_per_month"] >= sum( - self.model.works[worker.id, week, day, shift.name] for week in week_blocks - for day in self.days for shift in self.get_shifts())) + self.constraint_options["max_shifts_per_month"] + >= sum( + self.model.works[worker.id, week, day, shift.name] + for week in week_blocks + for day in self.days + for shift in self.get_shifts() + ) + ) - - if self.constraint_options[ - "avoid_st2_first_month"] and worker.grade == 2: + if self.constraint_options["avoid_st2_first_month"] and worker.grade == 2: # Avoid ST1s on the first month try: - self.model.constraints.add(0 == sum( - self.model.works[worker.id, week, day, shift] for week, - day, shift in self.get_all_shiftname_combinations() - if week in (1, 2, 3, 4) and shift in ("night_weekday", "night_weekend"))) + self.model.constraints.add( + 0 + == sum( + self.model.works[worker.id, week, day, shift] + for week, day, shift in self.get_all_shiftname_combinations() + if week in (1, 2, 3, 4) + and shift in ("night_weekday", "night_weekend") + ) + ) except ValueError as e: print("Failure setting constraint", "avoid_st2_first_month") print(e) # Count number of weekends an worker works if self.constraint_options["balance_weekends"]: self.model.constraints.add( - self.model.worker_weekend_count[worker.id] == sum( - self.model.works_weekend[worker.id, week] - for week in self.weeks)) + self.model.worker_weekend_count[worker.id] + == sum( + self.model.works_weekend[worker.id, week] for week in self.weeks + ) + ) if self.constraint_options["max_weekends"]: self.model.constraints.add( - self.constraint_options["max_weekends"] >= - self.model.worker_weekend_count[worker.id]) + self.constraint_options["max_weekends"] + >= self.model.worker_weekend_count[worker.id] + ) # Balance shifts within required limits (set by balance_offset) # If balance_offset is too restrictive (for the rota length) # no solution will be possible for shift in self.get_shifts(): if ( - worker.site in shift.site + worker.site in shift.site ): # Each site specfies which sites self.workers can fullfill it # calaculate the total number of shifts that need assigning over the rota - #total_shifts = (len(self.weeks) * len(shift.shift_days) * + # total_shifts = (len(self.weeks) * len(shift.shift_days) * total_shifts = self.shift_counts[shift] * shift.workers_required - full_time_equivalent_joined = sum([ - self.full_time_equivalent_sites[i] for i in shift.site - ]) + full_time_equivalent_joined = sum( + [self.full_time_equivalent_sites[i] for i in shift.site] + ) - target_shifts = (total_shifts / - full_time_equivalent_joined * - worker.fte_adj) + target_shifts = ( + total_shifts / full_time_equivalent_joined * worker.fte_adj + ) if self.use_previous_shifts: if shift.name in worker.previous_shifts: worked, allocated = worker.previous_shifts[shift.name] - target_shifts = target_shifts + float(allocated) - float(worked) + target_shifts = ( + target_shifts + float(allocated) - float(worked) + ) - #print(worker.name, shift.name, target_shifts) + # print(worker.name, shift.name, target_shifts) worker.shift_target_number[shift.name] = target_shifts if shift.hard_constrain_shift: adjusted_balance_offset = shift.balance_offset if worker.fte_adj < 100: - adjusted_balance_offset = adjusted_balance_offset * self.ltft_balance_offset + adjusted_balance_offset = ( + adjusted_balance_offset * self.ltft_balance_offset + ) - max_shifts = target_shifts + adjusted_balance_offset * self.balance_offset_modifier - min_shifts = target_shifts - adjusted_balance_offset * self.balance_offset_modifier + max_shifts = ( + target_shifts + + adjusted_balance_offset * self.balance_offset_modifier + ) + min_shifts = ( + target_shifts + - adjusted_balance_offset * self.balance_offset_modifier + ) self.model.constraints.add( inequality( min_shifts, - sum(self.model.works[worker.id, week, day, - shift.name] for week, day - in self.get_week_day_combinations()), + sum( + self.model.works[worker.id, week, day, shift.name] + for week, day in self.get_week_day_combinations() + ), max_shifts, - )) + ) + ) else: # Make sure shifts aren't assigned to those from other sites - self.model.constraints.add(0 == sum( - self.model.works[worker.id, week, day, shift.name] - for week, day in self.get_week_day_combinations())) + self.model.constraints.add( + 0 + == sum( + self.model.works[worker.id, week, day, shift.name] + for week, day in self.get_week_day_combinations() + ) + ) # model.shift_count stores the number of shifts that a worker is assigned to worker by shift # this is used (by the objective) to balance the total number of shifts worked self.model.constraints.add( - self.model.shift_count[worker.id, shift.name] == sum( + self.model.shift_count[worker.id, shift.name] + == sum( self.model.works[worker.id, week, day, shift.name] - for week, day in self.get_week_day_combinations())) + for week, day in self.get_week_day_combinations() + ) + ) # Define shift_count_t1 and shift_count_t2 constraints for the object # Thus bypassing the need for a quadratic solver @@ -805,21 +947,28 @@ class RotaBuilder(object): # t1+t2 approximates the absolute target (which otherwise requires a quadratic solver) if self.constraint_options["balance_shifts"]: self.model.constraints.add( - self.model.shift_count_t1[worker.id] - - self.model.shift_count_t2[worker.id] == sum( - (self.model.shift_count[worker.id, shift.name] - - worker.shift_target_number[shift.name]) * - shift.balance_weighting - for shift in self.get_shifts())) - #if "night" not in shift.constraints)) + self.model.shift_count_t1[worker.id] + - self.model.shift_count_t2[worker.id] + == sum( + ( + self.model.shift_count[worker.id, shift.name] + - worker.shift_target_number[shift.name] + ) + * shift.balance_weighting + for shift in self.get_shifts() + ) + ) + # if "night" not in shift.constraints)) if self.constraint_options["balance_bank_holidays"]: self.model.constraints.add( - self.model.bank_holiday_count[worker.id] == sum( - self.model.works[worker.id, week, day, shift] for week, - day, shift in self.get_all_shiftname_combinations() - if self.week_day_date_map[(week, - day)] in bank_holiday_map)) + self.model.bank_holiday_count[worker.id] + == sum( + self.model.works[worker.id, week, day, shift] + for week, day, shift in self.get_all_shiftname_combinations() + if self.week_day_date_map[(week, day)] in bank_holiday_map + ) + ) xU = len(self.get_bank_holiday_week_days()) + 1 xL = 1 @@ -828,28 +977,33 @@ class RotaBuilder(object): xL, self.model.bank_holiday_count[worker.id] + 1, xU, - )) + ) + ) self.model.constraints.add( - self.model.bank_holiday_count_w[worker.id] >= xL * - (self.model.bank_holiday_count[worker.id] + 1) * 2 - - xL * xL) + self.model.bank_holiday_count_w[worker.id] + >= xL * (self.model.bank_holiday_count[worker.id] + 1) * 2 - xL * xL + ) self.model.constraints.add( - self.model.bank_holiday_count_w[worker.id] >= xU * - (self.model.bank_holiday_count[worker.id] + 1) * 2 - - xU * xU) + self.model.bank_holiday_count_w[worker.id] + >= xU * (self.model.bank_holiday_count[worker.id] + 1) * 2 - xU * xU + ) if self.constraint_options["balance_nights"]: self.model.constraints.add( - self.model.night_shift_count[worker.id] == sum( + self.model.night_shift_count[worker.id] + == sum( self.model.works[worker.id, week, day, shift.name] for week, day in self.get_week_day_combinations() - for shift in self.get_shifts_with_constraint("night"))) + for shift in self.get_shifts_with_constraint("night") + ) + ) night_shift_target_number = sum( worker.shift_target_number[shift.name] - for shift in self.get_shifts_with_constraint("night")) + for shift in self.get_shifts_with_constraint("night") + ) # min_shifts = night_shift_target_number - 20 # max_shifts = night_shift_target_number + 20 @@ -862,10 +1016,11 @@ class RotaBuilder(object): # )) self.model.constraints.add( - self.model.night_shift_count_t1[worker.id] - - self.model.night_shift_count_t2[worker.id] == - self.model.night_shift_count[worker.id] - - night_shift_target_number) + self.model.night_shift_count_t1[worker.id] + - self.model.night_shift_count_t2[worker.id] + == self.model.night_shift_count[worker.id] + - night_shift_target_number + ) # This may need to be updated xU = 6 @@ -873,22 +1028,36 @@ class RotaBuilder(object): self.model.constraints.add( inequality( xL, - self.model.night_shift_count_t1[worker.id] + - self.model.night_shift_count_t2[worker.id] + 1, + self.model.night_shift_count_t1[worker.id] + + self.model.night_shift_count_t2[worker.id] + + 1, xU, - )) + ) + ) self.model.constraints.add( - self.model.night_shift_count_w[worker.id] >= xL * - (self.model.night_shift_count_t1[worker.id] + - self.model.night_shift_count_t2[worker.id] + 1) * 2 - - xL * xL) + self.model.night_shift_count_w[worker.id] + >= xL + * ( + self.model.night_shift_count_t1[worker.id] + + self.model.night_shift_count_t2[worker.id] + + 1 + ) + * 2 + - xL * xL + ) self.model.constraints.add( - self.model.night_shift_count_w[worker.id] >= xU * - (self.model.night_shift_count_t1[worker.id] + - self.model.night_shift_count_t2[worker.id] + 1) * 2 - - xU * xU) + self.model.night_shift_count_w[worker.id] + >= xU + * ( + self.model.night_shift_count_t1[worker.id] + + self.model.night_shift_count_t2[worker.id] + + 1 + ) + * 2 + - xU * xU + ) # self.model.constraints.add( # self.model.night_shift_count_w[worker.id] >= 0) @@ -897,9 +1066,10 @@ class RotaBuilder(object): # This works as long as weekend shifts are assigned as blocks! if self.constraint_options["balance_weekends"]: weekend_shift_target_number = sum( - worker.shift_target_number[shift.name] / - len(shift.shift_days) for shift in self.get_shifts() - if "Sat" in shift.shift_days or "Sun" in shift.shift_days) + worker.shift_target_number[shift.name] / len(shift.shift_days) + for shift in self.get_shifts() + if "Sat" in shift.shift_days or "Sun" in shift.shift_days + ) worker.weekend_shift_target_number = weekend_shift_target_number @@ -914,10 +1084,11 @@ class RotaBuilder(object): # )) self.model.constraints.add( - self.model.weekend_shift_count_t1[worker.id] - - self.model.weekend_shift_count_t2[worker.id] == - self.model.worker_weekend_count[worker.id] - - weekend_shift_target_number) + self.model.weekend_shift_count_t1[worker.id] + - self.model.weekend_shift_count_t2[worker.id] + == self.model.worker_weekend_count[worker.id] + - weekend_shift_target_number + ) xU = self.constraint_options["max_weekends"] @@ -925,127 +1096,192 @@ class RotaBuilder(object): self.model.constraints.add( inequality( xL, - self.model.weekend_shift_count_t1[worker.id] + - self.model.weekend_shift_count_t2[worker.id] + 1, + self.model.weekend_shift_count_t1[worker.id] + + self.model.weekend_shift_count_t2[worker.id] + + 1, xU, - )) + ) + ) self.model.constraints.add( - self.model.weekend_shift_count_w[worker.id] >= xL * - (self.model.weekend_shift_count_t1[worker.id] + - self.model.weekend_shift_count_t2[worker.id] + 1) * 2 - - xL * xL) + self.model.weekend_shift_count_w[worker.id] + >= xL + * ( + self.model.weekend_shift_count_t1[worker.id] + + self.model.weekend_shift_count_t2[worker.id] + + 1 + ) + * 2 + - xL * xL + ) self.model.constraints.add( - self.model.weekend_shift_count_w[worker.id] >= xU * - (self.model.weekend_shift_count_t1[worker.id] + - self.model.weekend_shift_count_t2[worker.id] + 1) * 2 - - xU * xU) + self.model.weekend_shift_count_w[worker.id] + >= xU + * ( + self.model.weekend_shift_count_t1[worker.id] + + self.model.weekend_shift_count_t2[worker.id] + + 1 + ) + * 2 + - xU * xU + ) # Ensure worker is not allocated shifts on non working days if worker.nwd: for week, day, shift in self.get_all_shiftclass_combinations(): for n, start, end in worker.nwd: if not shift.rota_on_nwds and day == n: - #print(start, self.week_day_date_map[(week, day)], end) + # print(start, self.week_day_date_map[(week, day)], end) if start <= self.week_day_date_map[(week, day)] <= end: self.model.constraints.add( - 0 == self.model.works[worker.id, week, day, - shift.name]) + 0 + == self.model.works[ + worker.id, week, day, shift.name + ] + ) if self.constraint_options["balance_blocks"]: for week_blocks in self.get_week_block_iterator( - self.max_night_frequency): + self.max_night_frequency + ): if self.get_shifts_with_constraint("night"): # Prevent nights more than once every n weeks - self.model.constraints.add(1 >= sum( - self.model.shift_week_worker_assigned[shift.name, - week, - worker.id] - for week in week_blocks - for shift in self.get_shifts_with_constraint( - "night"))) + self.model.constraints.add( + 1 + >= sum( + self.model.shift_week_worker_assigned[ + shift.name, week, worker.id + ] + for week in week_blocks + for shift in self.get_shifts_with_constraint("night") + ) + ) for week_blocks in self.get_week_block_iterator( - self.max_weekend_frequency): + self.max_weekend_frequency + ): # Prevent weekend shifts more than once every n weeks self.model.constraints.add( - 1 >= sum(self.model.works_weekend[worker.id, week] - for week in week_blocks)) + 1 + >= sum( + self.model.works_weekend[worker.id, week] + for week in week_blocks + ) + ) for week in self.weeks: - self.model.constraints.add(self.constraint_options["max_shifts_per_week"] >= sum( - self.model.works[worker.id, week, day, shift.name] - for day in self.days for shift in self.get_shifts())) - - for shift in self.get_shifts_with_constraint( - "max_2_shifts_per_week"): - self.model.constraints.add(2 >= sum( + self.model.constraints.add( + self.constraint_options["max_shifts_per_week"] + >= sum( self.model.works[worker.id, week, day, shift.name] - for day in self.days)) + for day in self.days + for shift in self.get_shifts() + ) + ) - if worker.site in self.constraint_options[ - "prevent_monday_after_full_weekends"]: + for shift in self.get_shifts_with_constraint("max_2_shifts_per_week"): + self.model.constraints.add( + 2 + >= sum( + self.model.works[worker.id, week, day, shift.name] + for day in self.days + ) + ) + + if ( + worker.site + in self.constraint_options["prevent_monday_after_full_weekends"] + ): # Ignore last week if week + 1 in self.weeks: self.model.constraints.add( - full_weekend_count >= sum(self.model.works[worker.id, week, "Sat", - shift.name] - for shift in self.get_shifts()) + - sum(self.model.works[worker.id, week, "Sun", - shift.name] - for shift in self.get_shifts()) + - sum(self.model.works[worker.id, week + 1, "Mon", - shift.name] - for shift in self.get_shifts())) - if worker.site in self.constraint_options[ - "prevent_monday_and_tuesday_after_full_weekends"]: + full_weekend_count + >= sum( + self.model.works[worker.id, week, "Sat", shift.name] + for shift in self.get_shifts() + ) + + sum( + self.model.works[worker.id, week, "Sun", shift.name] + for shift in self.get_shifts() + ) + + sum( + self.model.works[worker.id, week + 1, "Mon", shift.name] + for shift in self.get_shifts() + ) + ) + if ( + worker.site + in self.constraint_options[ + "prevent_monday_and_tuesday_after_full_weekends" + ] + ): # Ignore last week if week + 1 in self.weeks: self.model.constraints.add( - full_weekend_count >= sum(self.model.works[worker.id, week, "Sat", - shift.name] - for shift in self.get_shifts()) + - sum(self.model.works[worker.id, week, "Sun", - shift.name] - for shift in self.get_shifts()) + - sum(self.model.works[worker.id, week + 1, "Mon", - shift.name] - for shift in self.get_shifts()) + - sum(self.model.works[worker.id, week + 1, "Tue", - shift.name] - for shift in self.get_shifts())) - if worker.site in self.constraint_options[ - "prevent_fridays_before_full_weekends"]: + full_weekend_count + >= sum( + self.model.works[worker.id, week, "Sat", shift.name] + for shift in self.get_shifts() + ) + + sum( + self.model.works[worker.id, week, "Sun", shift.name] + for shift in self.get_shifts() + ) + + sum( + self.model.works[worker.id, week + 1, "Mon", shift.name] + for shift in self.get_shifts() + ) + + sum( + self.model.works[worker.id, week + 1, "Tue", shift.name] + for shift in self.get_shifts() + ) + ) + if ( + worker.site + in self.constraint_options["prevent_fridays_before_full_weekends"] + ): self.model.constraints.add( - full_weekend_count >= sum(self.model.works[worker.id, week, "Sat", - shift.name] - for shift in self.get_shifts() - if "night" not in shift.constraints) + - sum(self.model.works[worker.id, week, "Sun", - shift.name] + full_weekend_count + >= sum( + self.model.works[worker.id, week, "Sat", shift.name] for shift in self.get_shifts() - if "night" not in shift.constraints) + - sum(self.model.works[worker.id, week, "Fri", - shift.name] + if "night" not in shift.constraints + ) + + sum( + self.model.works[worker.id, week, "Sun", shift.name] for shift in self.get_shifts() - if "night" not in shift.constraints)) - if worker.site in self.constraint_options[ - "prevent_thursdays_before_full_weekends"]: + if "night" not in shift.constraints + ) + + sum( + self.model.works[worker.id, week, "Fri", shift.name] + for shift in self.get_shifts() + if "night" not in shift.constraints + ) + ) + if ( + worker.site + in self.constraint_options["prevent_thursdays_before_full_weekends"] + ): self.model.constraints.add( - full_weekend_count >= sum(self.model.works[worker.id, week, "Sat", - shift.name] - for shift in self.get_shifts() - if "night" not in shift.constraints) + - sum(self.model.works[worker.id, week, "Sun", - shift.name] + full_weekend_count + >= sum( + self.model.works[worker.id, week, "Sat", shift.name] for shift in self.get_shifts() - if "night" not in shift.constraints) + - sum(self.model.works[worker.id, week, "Thu", - shift.name] + if "night" not in shift.constraints + ) + + sum( + self.model.works[worker.id, week, "Sun", shift.name] for shift in self.get_shifts() - if "night" not in shift.constraints)) + if "night" not in shift.constraints + ) + + sum( + self.model.works[worker.id, week, "Thu", shift.name] + for shift in self.get_shifts() + if "night" not in shift.constraints + ) + ) # # model.weekend_count stores the number of weekend shifts that a worker is assigned to wor # # this is used (by the objective) to balance the total number of weekend shifts worked @@ -1062,15 +1298,22 @@ class RotaBuilder(object): # for shift in self.get_shifts()) / 2) if self.constraint_options["balance_weekends"]: self.model.constraints.add( - self.model.works_weekend[worker.id, week] >= sum( + self.model.works_weekend[worker.id, week] + >= sum( self.model.works[worker.id, week, day, shift.name] for day in self.days[5:] - for shift in self.get_shifts()) / 2) + for shift in self.get_shifts() + ) + / 2 + ) self.model.constraints.add( - self.model.works_weekend[worker.id, week] <= sum( + self.model.works_weekend[worker.id, week] + <= sum( self.model.works[worker.id, week, day, shift.name] for day in self.days[5:] - for shift in self.get_shifts())) + for shift in self.get_shifts() + ) + ) # self.model.constraints.add( # self.model.works_saturday[worker.id, week] == sum( # self.model.works[worker.id, week, "Sat", shift.name] @@ -1091,11 +1334,15 @@ class RotaBuilder(object): # Force night shifts to be assigned in blocks self.model.constraints.add( - self.model.shift_week_worker_assigned[shift.name, week, - worker.id] * - len(shift.shift_days) == sum( + self.model.shift_week_worker_assigned[ + shift.name, week, worker.id + ] + * len(shift.shift_days) + == sum( self.model.works[worker.id, week, day, shift.name] - for day in shift.shift_days)) + for day in shift.shift_days + ) + ) # @@ -1140,29 +1387,46 @@ class RotaBuilder(object): if self.get_shift_names_by_week_day(week, day): # Unable to work (hard constraint not preference) self.model.constraints.add( - self.model.available[worker.id, week, day] >= sum( + self.model.available[worker.id, week, day] + >= sum( self.model.works[worker.id, week, day, shift] - for shift in self.get_shift_names_by_week_day(week, day))) + for shift in self.get_shift_names_by_week_day(week, day) + ) + ) # single shift per day - self.model.constraints.add(1 >= sum( - self.model.works[worker.id, week, day, shift] - for shift in self.get_shift_names_by_week_day(week, day))) + self.model.constraints.add( + 1 + >= sum( + self.model.works[worker.id, week, day, shift] + for shift in self.get_shift_names_by_week_day(week, day) + ) + ) if self.constraint_options["hard_constrain_pair_separation"]: for worker_pairs in self.worker_pairs: if worker_pairs[0] == worker: - self.model.constraints.add(1 >= sum(self.model.works[w.id, week, day, shift] for shift in self.get_shift_names_by_week_day(week, day) for w in worker_pairs)) + self.model.constraints.add( + 1 + >= sum( + self.model.works[w.id, week, day, shift] + for shift in self.get_shift_names_by_week_day( + week, day + ) + for w in worker_pairs + ) + ) # if working a night ensure preceeding (1) or subsequent (2) shifts can only be nights if self.constraint_options["constrain_time_off_after_nights"]: - for constraint_shift in self.get_shifts_with_constraint( - "night"): + for constraint_shift in self.get_shifts_with_constraint("night"): # Ensure night prior to unavalibity is not assigned self.model.constraints.add( - self.model.available[worker.id, week, day] >= - self.model.works[worker.id, pweek, pday, - constraint_shift.name]) + self.model.available[worker.id, week, day] + >= self.model.works[ + worker.id, pweek, pday, constraint_shift.name + ] + ) # IF paired we check the following against both workers workers = [worker] @@ -1171,24 +1435,37 @@ class RotaBuilder(object): if worker_pairs[0] == worker: workers = worker_pairs - #print("Workers", workers) - + # print("Workers", workers) self.model.constraints.add( - 1 >= self.model.works[worker.id, week, day, - constraint_shift.name] + - sum(n1 * - self.model.works[w.id, nweek, nday, shift] - for shift in self.get_shift_names_by_week_day(nweek, nday) - if shift != constraint_shift.name for w in workers)) + 1 + >= self.model.works[ + worker.id, week, day, constraint_shift.name + ] + + sum( + n1 * self.model.works[w.id, nweek, nday, shift] + for shift in self.get_shift_names_by_week_day( + nweek, nday + ) + if shift != constraint_shift.name + for w in workers + ) + ) self.model.constraints.add( - 1 >= self.model.works[worker.id, week, day, - constraint_shift.name] + - sum(n2 * self.model.works[w.id, n2week, n2day, - shift] - for shift in self.get_shift_names_by_week_day(n2week, n2day) - if shift != constraint_shift.name for w in workers)) + 1 + >= self.model.works[ + worker.id, week, day, constraint_shift.name + ] + + sum( + n2 * self.model.works[w.id, n2week, n2day, shift] + for shift in self.get_shift_names_by_week_day( + n2week, n2day + ) + if shift != constraint_shift.name + for w in workers + ) + ) # self.model.constraints.add( # 1 >= self.model.works[worker.id, week, day, # constraint_shift.name] + @@ -1197,19 +1474,33 @@ class RotaBuilder(object): # for shift in self.get_shift_names_by_week_day(week, n3day) # if shift != constraint_shift.name)) self.model.constraints.add( - 1 >= self.model.works[worker.id, week, day, - constraint_shift.name] + - sum(p1 * - self.model.works[w.id, pweek, pday, shift] - for shift in self.get_shift_names_by_week_day(pweek, pday) - if shift != constraint_shift.name for w in workers)) + 1 + >= self.model.works[ + worker.id, week, day, constraint_shift.name + ] + + sum( + p1 * self.model.works[w.id, pweek, pday, shift] + for shift in self.get_shift_names_by_week_day( + pweek, pday + ) + if shift != constraint_shift.name + for w in workers + ) + ) self.model.constraints.add( - 1 >= self.model.works[worker.id, week, day, - constraint_shift.name] + - sum(p2 * self.model.works[w.id, p2week, p2day, - shift] - for shift in self.get_shift_names_by_week_day(p2week, p2day) - if shift != constraint_shift.name for w in workers)) + 1 + >= self.model.works[ + worker.id, week, day, constraint_shift.name + ] + + sum( + p2 * self.model.works[w.id, p2week, p2day, shift] + for shift in self.get_shift_names_by_week_day( + p2week, p2day + ) + if shift != constraint_shift.name + for w in workers + ) + ) self.define_objectives() @@ -1222,70 +1513,85 @@ class RotaBuilder(object): balance_modifier_constant = 1 if self.constraint_options["balance_shifts"]: - shift_balancing = sum(balance_modifier_constant * - (self.model.shift_count_t1[(worker.id)] + - self.model.shift_count_t2[(worker.id)]) - for worker in self.workers) + shift_balancing = sum( + balance_modifier_constant + * ( + self.model.shift_count_t1[(worker.id)] + + self.model.shift_count_t2[(worker.id)] + ) + for worker in self.workers + ) else: shift_balancing = 0 if self.constraint_options["balance_nights"]: night_balance_modifier_constant = 10 night_shift_balancing = sum( - night_balance_modifier_constant * - self.model.night_shift_count_w[(worker.id)] + night_balance_modifier_constant + * self.model.night_shift_count_w[(worker.id)] # (self.model.night_shift_count_t1[(worker.id)] + # self.model.night_shift_count_t2[(worker.id)]) - for worker in self.workers) + for worker in self.workers + ) else: night_shift_balancing = 0 if self.constraint_options["balance_bank_holidays"]: bank_holiday_balance_modifier_constant = 1000 bank_holiday_balancing = sum( - bank_holiday_balance_modifier_constant * - (self.model.bank_holiday_count_w[(worker.id)] - 1) + bank_holiday_balance_modifier_constant + * (self.model.bank_holiday_count_w[(worker.id)] - 1) # (self.model.night_shift_count_t1[(worker.id)] + # self.model.night_shift_count_t2[(worker.id)]) - for worker in self.workers) + for worker in self.workers + ) else: bank_holiday_balancing = 0 if self.constraint_options["balance_weekends"]: weekend_balance_modifier_constant = 5 weekend_shift_balancing = sum( - weekend_balance_modifier_constant * - self.model.weekend_shift_count_w[(worker.id)] - for worker in self.workers) + weekend_balance_modifier_constant + * self.model.weekend_shift_count_w[(worker.id)] + for worker in self.workers + ) else: weekend_shift_balancing = 0 preference_constant = 10 # Preferences (not to work) preferences = sum( - self.model.pref_not_to_work[worker.id, week, day] * - self.model.works[worker.id, week, day, shift] * - preference_constant for worker in self.workers - for week, day, shift in self.get_all_shiftname_combinations()) + self.model.pref_not_to_work[worker.id, week, day] + * self.model.works[worker.id, week, day, shift] + * preference_constant + for worker in self.workers + for week, day, shift in self.get_all_shiftname_combinations() + ) # Work requsets work_request_constant = 10000 # Needs to be very big to override loss from bank holiday requests work_requests = sum( - self.model.work_requests[worker.id, week, day, shift] * - self.model.works[worker.id, week, day, shift] * - work_request_constant for worker in self.workers - for week, day, shift in self.get_all_shiftname_combinations()) + self.model.work_requests[worker.id, week, day, shift] + * self.model.works[worker.id, week, day, shift] + * work_request_constant + for worker in self.workers + for week, day, shift in self.get_all_shiftname_combinations() + ) # # Spread nights if self.constraint_options["balance_nights_across_sites"]: nights_site_balancing = sum( - (self.model.night_per_site_t1[week, shift.name, site] + - self.model.night_per_site_t2[week, shift.name, site]) * 20 - #self.model.night_per_site2[week, block, site] + ( + self.model.night_per_site_t1[week, shift.name, site] + + self.model.night_per_site_t2[week, shift.name, site] + ) + * 20 + # self.model.night_per_site2[week, block, site] for week in self.weeks for shift in self.get_shifts_with_constraint("night") - for site in self.sites) + for site in self.sites + ) else: nights_site_balancing = 0 @@ -1293,25 +1599,34 @@ class RotaBuilder(object): blocks_balancing = sum( 1 * self.model.blocks_assigned[week, shift] for week in self.weeks - for shift in self.shifts_to_assign_as_blocks()) + for shift in self.shifts_to_assign_as_blocks() + ) else: blocks_balancing = 0 # Quadratic :( - #worker_pairs_balancing = 0 - #worker_pairs_constant = 100000 - #if self.worker_pairs: + # worker_pairs_balancing = 0 + # worker_pairs_constant = 100000 + # if self.worker_pairs: # for worker_a, worker_b in self.worker_pairs: # print(worker_a, worker_b) # for week, day in self.get_week_day_combinations(): - # worker_pairs_balancing = worker_pairs_balancing + sum(self.model.works[worker_a.id, week, day, shift] for shift in self.get_shift_names_by_week_day(week, day)) * sum(self.model.works[worker_b.id, week, day, shift] for shift in self.get_shift_names_by_week_day(week, day)) * worker_pairs_constant + # worker_pairs_balancing = worker_pairs_balancing + sum(self.model.works[worker_a.id, week, day, shift] for shift in self.get_shift_names_by_week_day(week, day)) * sum(self.model.works[worker_b.id, week, day, shift] for shift in self.get_shift_names_by_week_day(week, day)) * worker_pairs_constant - - #return weekend_shift_balancing + blocks_balancing - #return bank_holiday_balancing - #return shift_balancing + preferences + blocks_balancing - #return shift_balancing + preferences + nights_site_balancing + blocks_balancing - return weekend_shift_balancing + shift_balancing + night_shift_balancing + preferences + nights_site_balancing + bank_holiday_balancing + blocks_balancing - work_requests + # return weekend_shift_balancing + blocks_balancing + # return bank_holiday_balancing + # return shift_balancing + preferences + blocks_balancing + # return shift_balancing + preferences + nights_site_balancing + blocks_balancing + return ( + weekend_shift_balancing + + shift_balancing + + night_shift_balancing + + preferences + + nights_site_balancing + + bank_holiday_balancing + + blocks_balancing + - work_requests + ) # add objective function to the model. rule (pass function) or expr (pass expression directly) self.model.obj = Objective(rule=obj_rule, sense=minimize) @@ -1337,7 +1652,7 @@ class RotaBuilder(object): Must be called prior to attempting to solve """ - #self.build_shifts() + # self.build_shifts() self.workers = sorted(self.workers) @@ -1348,7 +1663,8 @@ class RotaBuilder(object): for site in self.sites: self.workers_at_sites[site] = [w for w in self.workers if w.site == site] self.full_time_equivalent_sites[site] = sum( - [w.fte_adj for w in self.workers if w.site == site]) + [w.fte_adj for w in self.workers if w.site == site] + ) pairs = defaultdict(list) for w in self.workers: @@ -1358,7 +1674,6 @@ class RotaBuilder(object): for p in pairs: self.worker_pairs.append(tuple(pairs[p])) - def add_shift(self, shift): """Add a shift to the collection @@ -1369,7 +1684,7 @@ class RotaBuilder(object): def add_shifts(self, *shifts: SingleShift): """Add multiple shifts - + Returns: None """ @@ -1387,17 +1702,17 @@ class RotaBuilder(object): def get_shift_by_name(self, name): """ - :param name: + :param name: """ return self.shifts_by_name[name] def get_shift_length_by_name(self, name): """Get the length of a shift - + Args: name (str): Name of a shift - + Returns: int: Length of the shift """ @@ -1408,14 +1723,13 @@ class RotaBuilder(object): self.shifts_by_name = {} self.shift_names = [] # type: List[ShiftName] - #self.week_day_shifts_dict = {(week, day): set() for week in self.weeks for day in days} + # self.week_day_shifts_dict = {(week, day): set() for week in self.weeks for day in days} self.week_day_shifts_dict = {} self.sites = set() - self.week_day_shift_product = [] self.week_day_shiftclass_product = [] - + self.shift_counts = defaultdict(int) for week, day in self.weeks_days_product: self.week_day_shifts_dict[(week, day)] = set() @@ -1432,17 +1746,17 @@ class RotaBuilder(object): self.week_day_shiftclass_product.append((week, day, s)) self.week_day_shifts_dict[(week, day)].add(s.name) self.shift_counts[s] = self.shift_counts[s] + 1 - #print(self.week_day_shift_product) + # print(self.week_day_shift_product) for s in self.shifts: self.shifts_by_name[s.name] = s self.shift_names.append(s.name) - #for day in s.shift_days: + # for day in s.shift_days: # self.week_day_shifts_dict[(week, day)].add(s.name) for site in s.site: - self.sites.add(site) - + self.sites.add(site) + # Todo replace with week_day..... self.day_shift_product = [] self.day_shiftclass_product = [] @@ -1454,34 +1768,33 @@ class RotaBuilder(object): def get_day_shiftname_combinations(self): """Returns a list of all required day / shift combinations - + Returns: list: list of in the following format (str->day, str->shift) """ return self.day_shift_product - def get_day_shiftclass_combinations( - self) -> List[Tuple[DayStr, SingleShift]]: + def get_day_shiftclass_combinations(self) -> List[Tuple[DayStr, SingleShift]]: """Returns a list of all required day / shift combinations - + Returns: list: list of in the following format (str->day, shiftObject->shift) """ return self.day_shiftclass_product - def get_all_shiftname_combinations( - self) -> List[Tuple[WeekInt, DayStr, ShiftName]]: + def get_all_shiftname_combinations(self) -> List[Tuple[WeekInt, DayStr, ShiftName]]: """Returns a list of all possible week / day / shift combinations - + Returns: list: contains tuple of all possible week / day / shift combinations """ return self.week_day_shift_product def get_all_shiftclass_combinations( - self, ) -> List[Tuple[WeekInt, DayStr, SingleShift]]: + self, + ) -> List[Tuple[WeekInt, DayStr, SingleShift]]: """Returns a list of all possible week / day / shift combinations - + Returns: list: contains tuple of all possible week / day / shift combinations """ @@ -1498,16 +1811,14 @@ class RotaBuilder(object): def get_shifts(self) -> List[SingleShift]: """Returns a list of all the registered shifts - + Returns: List[SingleShift]: list of registered shifts (as SingleShift) """ return self.shifts def get_shifts_with_constraint(self, constraint) -> List[SingleShift]: - return [ - shift for shift in self.shifts if constraint in shift.constraints - ] + return [shift for shift in self.shifts if constraint in shift.constraints] def get_shift_names(self) -> List[ShiftName]: """Returns a list of all the registered shift names @@ -1526,29 +1837,28 @@ class RotaBuilder(object): [ 2, 3, 4, 5], ... ] - + Args: block_length (int): length of blocks to create - + Returns: list: two dimensional list containing weeks in blocks """ blocks = [] for i in range(len(self.weeks)): - blocks.append(self.weeks[i:i + block_length]) + blocks.append(self.weeks[i : i + block_length]) return blocks def get_required_workers_and_site_combinations(self): """Returns a list of all possible shifts, the workers required and site - + Returns: list: list (week, day, shift name, workers required, shift site) """ l = [] for week, day, shift in self.week_day_shiftclass_product: - #if day in shift.shift_days: - l.append((week, day, shift.name, shift.workers_required, - shift.site)) + # if day in shift.shift_days: + l.append((week, day, shift.name, shift.workers_required, shift.site)) return l @@ -1556,7 +1866,7 @@ class RotaBuilder(object): """Returns a set of all possible shifts combinations that are not required Includes those on days - + Returns: list: (week, day, shift) """ @@ -1570,20 +1880,16 @@ class RotaBuilder(object): return l - set(self.get_all_shiftclass_combinations()) def shifts_to_assign_as_blocks(self): - return [ - shift.name for shift in self.get_shifts() if shift.assign_as_block - ] + return [shift.name for shift in self.get_shifts() if shift.assign_as_block] def shifts_to_force_as_blocks(self): - return [ - shift.name for shift in self.get_shifts() if shift.force_as_block - ] + return [shift.name for shift in self.get_shifts() if shift.force_as_block] def shifts_to_assign_or_force_as_blocks(self): s = self.shifts_to_assign_as_blocks() s.extend(self.shifts_to_force_as_blocks()) return s - + def get_workers_for_shift(self, shift): return [worker for worker in self.workers if worker.site in shift.site] @@ -1594,20 +1900,23 @@ class RotaBuilder(object): w = defaultdict(list) for worker in self.workers: - #print(worker) + # print(worker) w[worker.site].append(worker) l = [] for site in w: - l.append("{}: {}".format( - site, - sum([worker.fte_adj for worker in w[site]]) / 100)) + l.append( + "{}: {}".format(site, sum([worker.fte_adj for worker in w[site]]) / 100) + ) t = "\n".join(l) return "Full time equivalent trainees by site:\n{}".format(t) def get_bank_holiday_week_days(self): - return ([(week, day) for week, day in self.get_week_day_combinations() - if self.week_day_date_map[(week, day)] in bank_holiday_map]) + return [ + (week, day) + for week, day in self.get_week_day_combinations() + if self.week_day_date_map[(week, day)] in bank_holiday_map + ] class RotaResults(object): @@ -1621,7 +1930,7 @@ class RotaResults(object): def export_rota_to_csv(self, filename="rota"): works = self.rota.model.works - with open("{}.csv".format(filename), 'w', newline='') as f: + with open("{}.csv".format(filename), "w", newline="") as f: wr = csv.writer(f, quoting=csv.QUOTE_ALL) l = ["Name"] l.extend([worker.name for worker in self.rota.workers]) @@ -1656,8 +1965,7 @@ class RotaResults(object): works = self.rota.model.works week_table = { week: { - day: {shift: [] - for shift in self.rota.get_shift_names()} + day: {shift: [] for shift in self.rota.get_shift_names()} for day in days } for week in self.rota.weeks @@ -1666,17 +1974,13 @@ class RotaResults(object): for worker in self.rota.workers: for day, shift in self.rota.get_day_shiftname_combinations(): if works[worker.id, week, day, shift].value == 1: - week_table[week][day][shift].append( - worker.get_details()) + week_table[week][day][shift].append(worker.get_details()) return week_table def get_worker_timetable(self): works = self.rota.model.works timetable = { - worker.name: - {week: {day: "" - for day in days} - for week in self.rota.weeks} + worker.name: {week: {day: "" for day in days} for week in self.rota.weeks} for worker in self.rota.workers } for worker in self.rota.workers: @@ -1686,15 +1990,16 @@ class RotaResults(object): timetable[worker.name][week][day] = shift return timetable - def get_worker_timetable_brief(self, - show_prefs=False, - marker_every=30, - show_unavailable=False): + def get_worker_timetable_brief( + self, show_prefs=False, marker_every=30, show_unavailable=False + ): model = self.rota.model week_string = "{:20}".format("-Week-") + "".join( - [7 * str("{}".format(w))[-1:] for w in self.rota.weeks]) + [7 * str("{}".format(w))[-1:] for w in self.rota.weeks] + ) days_string = "{:20}".format("-Day-") + "".join( - "MTWTFSS" * len(self.rota.weeks)) + "MTWTFSS" * len(self.rota.weeks) + ) timetable = [] for worker in self.rota.workers: shifts = [] @@ -1709,11 +2014,11 @@ class RotaResults(object): shift_count = "" for s in set(shifts): - shift_count = shift_count + "{}: {}, ".format( - s, shifts.count(s)) + shift_count = shift_count + "{}: {}, ".format(s, shifts.count(s)) shift_count = shift_count + "#weekends_worked: {}\\#".format( - model.worker_weekend_count[worker.id].value) + model.worker_weekend_count[worker.id].value + ) timetable.append("{} {}".format("".join(w), shift_count)) if show_prefs: @@ -1747,32 +2052,40 @@ class RotaResults(object): timetable.insert(0, days_string) return "\n".join(timetable) - def get_worker_timetable_html(self, - include_html_tag=False, - table_name="rota-table"): + def get_worker_timetable_html( + self, include_html_tag=False, table_name="rota-table" + ): model = self.rota.model timetable = [] - timetable.append("

Rota start date: {} ({} weeks)

".format(self.rota.start_date.isoformat(), self.rota.weeks[-1])) + timetable.append( + "

Rota start date: {} ({} weeks)

".format( + self.rota.start_date.isoformat(), self.rota.weeks[-1] + ) + ) date_row = [""] n = 0 for week, day in self.rota.get_week_day_combinations(): d = self.rota.start_date + datetime.timedelta(n) - date_row.append("Week {}: {}".format( - d, week, day)) + date_row.append("Week {}: {}".format(d, week, day)) n = n + 1 - timetable.append("{}".format( - "".join(date_row))) + timetable.append("{}".format("".join(date_row))) current_site = "" for worker in self.rota.workers: if worker.site != current_site: try: - timetable.append("{} n={} fte={}".format(worker.site, len(self.rota.workers_at_sites[worker.site]), self.rota.full_time_equivalent_sites[worker.site])) + timetable.append( + "{} n={} fte={}".format( + worker.site, + len(self.rota.workers_at_sites[worker.site]), + self.rota.full_time_equivalent_sites[worker.site], + ) + ) except KeyError as e: print(e) current_site = worker.site @@ -1813,7 +2126,9 @@ class RotaResults(object): available = False css_class = "unavailable" try: - unavailable_reason = self.rota.unavailable_to_work_reason[(worker.id, week, day)] + unavailable_reason = self.rota.unavailable_to_work_reason[ + (worker.id, week, day) + ] except KeyError: print("Error getting reason") print(worker.id, worker.name) @@ -1824,20 +2139,31 @@ class RotaResults(object): bank_holiday = "" if d in bank_holiday_map: css_class = " ".join((css_class, "bank-holiday")) - bank_holiday = " data-bank-holiday='{}'".format( - bank_holiday_map[d]) + bank_holiday = " data-bank-holiday='{}'".format(bank_holiday_map[d]) requests = "" if (worker.id, week, day) in self.rota.work_requests_map: css_class = " ".join((css_class, "shift-requested")) title = " ".join((title, "[REQUESTED]")) requests = " data-shift-request='{}'".format( - self.rota.work_requests_map[(worker.id, week, day)]) + self.rota.work_requests_map[(worker.id, week, day)] + ) shift_tds.append( - "{}" - .format(title, css_class, shift_name, available, unavailable_reason, d, week, - day, requests, bank_holiday, a)) + "{}".format( + title, + css_class, + shift_name, + available, + unavailable_reason, + d, + week, + day, + requests, + bank_holiday, + a, + ) + ) shift_count = "" shift_count_dict = {} @@ -1847,22 +2173,31 @@ class RotaResults(object): shift_count = shift_count + "{}: {}, ".format(s, c) worker_td = "{name} ({grade}) [{fte}]".format( - site=worker.site, nwds=nwds, name=worker.name, - fte=worker.fte, fte_adj=worker.fte_adj, end_date=worker.end_date, targets=worker_targets, + site=worker.site, + nwds=nwds, + name=worker.name, + fte=worker.fte, + fte_adj=worker.fte_adj, + end_date=worker.end_date, + targets=worker_targets, shift_counts=json.dumps(shift_count_dict), - weekend_target=worker.weekend_shift_target_number, nights_at_derriford=worker.night_at_derriford, grade=worker.grade, pair=worker.pair - ) + weekend_target=worker.weekend_shift_target_number, + nights_at_derriford=worker.night_at_derriford, + grade=worker.grade, + pair=worker.pair, + ) shift_count = shift_count + "#weekends_worked: {}\\#".format( - model.worker_weekend_count[worker.id].value) + model.worker_weekend_count[worker.id].value + ) bank_holiday_count = model.bank_holiday_count[worker.id].value - bank_holiday_count_w = model.bank_holiday_count_w[ - worker.id].value - 1 - #print(worker.name, bank_holiday_count, bank_holiday_count_w) - timetable.append("{}{}".format( - worker_td, "".join(shift_tds))) - #timetable.append("{}".format("".join(w), shift_count)) + bank_holiday_count_w = model.bank_holiday_count_w[worker.id].value - 1 + # print(worker.name, bank_holiday_count, bank_holiday_count_w) + timetable.append( + "{}{}".format(worker_td, "".join(shift_tds)) + ) + # timetable.append("{}".format("".join(w), shift_count)) # if show_prefs: # # prefs @@ -1884,7 +2219,6 @@ class RotaResults(object): # w.append("U") # timetable.append("".join(w)) - result_stream = StringIO() self.results.write(ostream=result_stream) @@ -1917,7 +2251,13 @@ class RotaResults(object):
- """.format(table_name, "\n".join(timetable), json.dumps(self.rota.constraint_options, indent=4), "
".join(str(i) for i in self.rota.shifts), result_string) + """.format( + table_name, + "\n".join(timetable), + json.dumps(self.rota.constraint_options, indent=4), + "
".join(str(i) for i in self.rota.shifts), + result_string, + ) if include_html_tag: html = """ @@ -1926,69 +2266,79 @@ class RotaResults(object): - {}""".format(html) + {}""".format( + html + ) return html def get_shift_summary(self): works = self.rota.model.works timetable = { - worker.get_details(): - {shift[0]: "" - for shift in self.rota.get_shift_names()} + worker.get_details(): { + shift[0]: "" for shift in self.rota.get_shift_names() + } for worker in self.rota.workers } - #timetable = { worker.get_details() : { shift : "" for shift in ["truro_twilight"] } for worker in workers } + # timetable = { worker.get_details() : { shift : "" for shift in ["truro_twilight"] } for worker in workers } t = [] for worker in self.rota.workers: l = [] total_shifts = 0 for shift in self.rota.get_shift_names(): - #for shift in ["truro_twilight"]: + # for shift in ["truro_twilight"]: c = [ works[worker.id, week, day, shift] - for week in self.rota.weeks for day in days + for week in self.rota.weeks + for day in days ].count(1) if c > 0: l.append("{} ({})".format(shift, c)) total_shifts = total_shifts + c - #print(worker.id, shift) + # print(worker.id, shift) timetable[worker.get_details()][shift[0]] = c - t.append("{} [{}]: {}".format(worker.get_full_details(), - total_shifts, ", ".join(l))) + t.append( + "{} [{}]: {}".format( + worker.get_full_details(), total_shifts, ", ".join(l) + ) + ) return "\n".join(t) def get_shift_summary_html(self): works = self.rota.model.works timetable = { - worker.get_details(): - {shift[0]: "" - for shift in self.rota.get_shift_names()} + worker.get_details(): { + shift[0]: "" for shift in self.rota.get_shift_names() + } for worker in self.rota.workers } - #timetable = { worker.get_details() : { shift : "" for shift in ["truro_twilight"] } for worker in workers } + # timetable = { worker.get_details() : { shift : "" for shift in ["truro_twilight"] } for worker in workers } t = [] for worker in self.rota.workers: l = [] total_shifts = 0 for shift in self.rota.get_shift_names(): - #for shift in ["truro_twilight"]: + # for shift in ["truro_twilight"]: c = [ works[worker.id, week, day, shift] - for week in self.rota.weeks for day in days + for week in self.rota.weeks + for day in days ].count(1) if c > 0: l.append("{} ({})".format(shift, c)) total_shifts = total_shifts + c - #print(worker.id, shift) + # print(worker.id, shift) timetable[worker.get_details()][shift[0]] = c - t.append("{} [{}]: {}".format(worker.get_full_details(), - total_shifts, ", ".join(l))) + t.append( + "{} [{}]: {}".format( + worker.get_full_details(), total_shifts, ", ".join(l) + ) + ) return "\n".join(t) - #def get_no_preference(no_pref): + # def get_no_preference(no_pref): # """Extract to a list the workers not satisfied with their weekend preference.""" # return [worker.id for worker in workers if no_pref[worker.id].value == 1] @@ -2015,13 +2365,14 @@ class RotaResults(object): block = shift.name for week in self.rota.weeks: print( - site, week, block, - self.rota.model.night_per_site[week, block, - site].value, - self.rota.model.night_per_site_t1[week, block, - site].value, - self.rota.model.night_per_site_t2[week, block, - site].value) + site, + week, + block, + self.rota.model.night_per_site[week, block, site].value, + self.rota.model.night_per_site_t1[week, block, site].value, + self.rota.model.night_per_site_t2[week, block, site].value, + ) + """ def get_night_details(self): for worker in self.rota.workers: @@ -2059,4 +2410,4 @@ class RotaResults(object): self.rota.model.weekend_shift_count_w[worker.id].value), ) - """ \ No newline at end of file + """ diff --git a/test.py b/test.py index 508bb1f..31a5c89 100644 --- a/test.py +++ b/test.py @@ -63,6 +63,7 @@ Rota.constraint_options["prevent_thursdays_before_full_weekends"] = [ "exeter", "truro no proc", ] +Rota.constraint_options["require_presence_at_site_overnight"] = ["plymouth"] Rota.add_shifts( SingleShift( @@ -327,7 +328,7 @@ else: oop, not_available_to_work=leave_requests, work_requests=work_requests, - proc_site=site_pref, + night_site=site_pref, previous_shifts=previous_shifts, pair=pair, ) diff --git a/workers.py b/workers.py index 4c91901..eb5f29e 100644 --- a/workers.py +++ b/workers.py @@ -1,7 +1,7 @@ import datetime from collections import defaultdict -#from shifts import RotaBuilder, days, sites +# from shifts import RotaBuilder, days, sites class Worker: @@ -19,9 +19,9 @@ class Worker: not_available_to_work=None, pref_not_to_work=None, work_requests=None, - proc_site="plymouth", + night_site="plymouth", # We set a default night_site previous_shifts={}, - pair=0 + pair=0, ): self.id = id self.name = name @@ -33,12 +33,8 @@ class Worker: self.nwd = nwd self.proportion_rota_to_work = 1 self.pair = pair - - self.proc_site = proc_site - if proc_site == "plymouth" or proc_site == "": - self.night_at_derriford = 1 - else: - self.night_at_derriford = 0 + + self.night_site = night_site self.previous_shifts = previous_shifts @@ -49,8 +45,7 @@ class Worker: if end_date is None: self.end_date = None else: - self.end_date = datetime.datetime.strptime(end_date, - "%d/%m/%y").date() + self.end_date = datetime.datetime.strptime(end_date, "%d/%m/%y").date() if self.end_date > Rota.rota_end_date: self.end_date = Rota.rota_end_date @@ -58,21 +53,19 @@ class Worker: if self.end_date > Rota.start_date: days_to_work = (self.end_date - Rota.start_date).days - # add unavalabilities for weeks_days in Rota.weeks_days_product[days_to_work:]: week, day = weeks_days Rota.unavailable_to_work.add((self.id, week, day)) - Rota.unavailable_to_work_reason[(self.id, week, day)] = "END DATE" + Rota.unavailable_to_work_reason[ + (self.id, week, day) + ] = "END DATE" if oop is not None: start_oop, end_oop = oop - start_oop_date = datetime.datetime.strptime(start_oop, - "%d/%m/%y").date() - end_oop_date = datetime.datetime.strptime(end_oop, - "%d/%m/%y").date() + start_oop_date = datetime.datetime.strptime(start_oop, "%d/%m/%y").date() + end_oop_date = datetime.datetime.strptime(end_oop, "%d/%m/%y").date() - # ignore oops if they finish before the rota start date print(end_oop_date, Rota.start_date) if end_oop_date > Rota.start_date: @@ -85,62 +78,67 @@ class Worker: if start_oop_date < Rota.start_date: start_oop_date = Rota.start_date - oop_length = (end_oop_date - start_oop_date).days days_to_work = days_to_work - oop_length days_until_oop = (start_oop_date - Rota.start_date).days for weeks_days in Rota.weeks_days_product[ - days_until_oop:days_until_oop + oop_length]: + days_until_oop : days_until_oop + oop_length + ]: week, day = weeks_days Rota.unavailable_to_work.add((self.id, week, day)) - Rota.unavailable_to_work_reason[(self.id, week, day)] = "OOP ()".format(oop) + Rota.unavailable_to_work_reason[ + (self.id, week, day) + ] = "OOP ()".format(oop) if pref_not_to_work is not None: # loop throught dates converting to week / day combination for date in pref_not_to_work: days_from_start = ( - datetime.datetime.strptime(date, "%d/%m/%y").date() - - Rota.start_date).days + datetime.datetime.strptime(date, "%d/%m/%y").date() + - Rota.start_date + ).days week = days_from_start // 7 + 1 day = Rota.days[(days_from_start % 7)] # Weight the value to take into account the number of preferences # 1 is added to the total number of requests to ensure they do not outweight # a single (or fewer) request(s) - Rota.pref_not_to_work[(self.id, week, - day)] = 1 / (len(pref_not_to_work) + 1) + Rota.pref_not_to_work[(self.id, week, day)] = 1 / ( + len(pref_not_to_work) + 1 + ) if not_available_to_work is not None: - #print(not_available_to_work) + # print(not_available_to_work) # loop throught dates converting to week / day combination for date, reason in not_available_to_work: days_from_start = ( - datetime.datetime.strptime(date, "%d/%m/%y").date() - - Rota.start_date).days + datetime.datetime.strptime(date, "%d/%m/%y").date() + - Rota.start_date + ).days week = days_from_start // 7 + 1 day = Rota.days[(days_from_start % 7)] Rota.unavailable_to_work.add((self.id, week, day)) Rota.unavailable_to_work_reason[(self.id, week, day)] = reason - if work_requests is not None: for date, shift in work_requests: days_from_start = ( - datetime.datetime.strptime(date, "%d/%m/%y").date() - - Rota.start_date).days + datetime.datetime.strptime(date, "%d/%m/%y").date() + - Rota.start_date + ).days week = days_from_start // 7 + 1 day = Rota.days[(days_from_start % 7)] Rota.work_requests.add((self.id, week, day, shift)) - print(days_to_work, Rota.rota_days_length) + # Calculate the proportion of the rota that is being worked self.proportion_rota_to_work = days_to_work / Rota.rota_days_length - # We had to adjust the full time equivalent for people who CCT / leave the rota early + # We have to adjust the full time equivalent for people who CCT / leave the rota early self.fte_adj = self.fte * self.proportion_rota_to_work if self.fte_adj > 100: - raise ValueError("{} : fte_ajd = {}".format(self.name, self.fte_adj)) + raise ValueError("{} : fte_ajd = {}".format(self.name, self.fte_adj)) # print(self.name, self.nwd) @@ -151,11 +149,16 @@ class Worker: other.fte_adj, other.name, ) - + def __str__(self): - + nwd = ", ".join([str(i) for i in self.nwd]) if self.nwd is not None else "" - return "{} {} [{}] PROC SITE:{} ({})".format(self.name, (self.site, self.grade, self.fte_adj), nwd, self.proc_site, self.night_at_derriford) + return "{} {} [{}] PROC SITE:{} ({})".format( + self.name, + (self.site, self.grade, self.fte_adj), + nwd, + self.night_site, + ) def get_details(self): return "{} {} {}".format(self.id, self.site[0], self.grade) From 68e39b08ae631e36980fd5bf64ecb26b0a1a765e Mon Sep 17 00:00:00 2001 From: Ross Date: Sat, 15 Jan 2022 18:23:29 +0000 Subject: [PATCH 2/2] . --- shifts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shifts.py b/shifts.py index cd3f60c..a58db1a 100644 --- a/shifts.py +++ b/shifts.py @@ -685,7 +685,7 @@ class RotaBuilder(object): self.model.require_presence_at_site_overnight_rule = Constraint( [week for week in self.weeks], [shift.name for shift in self.get_shifts_with_constraint("night")], - site, + [site], rule=presenceAtSiteOvernight, )