it works?
This commit is contained in:
@@ -37,6 +37,7 @@ class SingleShift(object):
|
||||
rota_on_nwds=False,
|
||||
assign_as_block=False,
|
||||
force_as_block=False,
|
||||
hard_constrain_shift=True,
|
||||
constraints=[],
|
||||
):
|
||||
self.site = sites
|
||||
@@ -51,6 +52,7 @@ class SingleShift(object):
|
||||
|
||||
# weight the shift for balancing, default is 1
|
||||
self.balance_weighting = balance_weighting
|
||||
self.hard_constrain_shift = hard_constrain_shift
|
||||
|
||||
self.workers_required = workers_required
|
||||
self.rota_on_nwds = rota_on_nwds
|
||||
@@ -75,7 +77,10 @@ class RotaBuilder(object):
|
||||
def __init__(self,
|
||||
start_date,
|
||||
weeks_to_rota=26,
|
||||
balance_offset_modifier=1):
|
||||
balance_offset_modifier=1,
|
||||
ltft_balance_offset=4,
|
||||
max_night_frequency=2,
|
||||
max_weekend_frequency=2):
|
||||
self.shifts = [] # type List[SingleShift]
|
||||
|
||||
self.days = days
|
||||
@@ -99,6 +104,22 @@ class RotaBuilder(object):
|
||||
self.sites = None
|
||||
|
||||
self.balance_offset_modifier = balance_offset_modifier
|
||||
self.ltft_balance_offset = ltft_balance_offset
|
||||
|
||||
# Don't assign multiple shifts every (n) weeks
|
||||
self.max_night_frequency = max_night_frequency
|
||||
self.max_weekend_frequency = max_weekend_frequency
|
||||
|
||||
self.constraint_options = {
|
||||
"limit_to_1_st1_on_nights": True,
|
||||
"ensure_1_st4_plus_on_nights": True,
|
||||
"balance_nights": True,
|
||||
"constrain_time_off_after_nights": True,
|
||||
"balance_nights_across_sites": True,
|
||||
"balance_blocks": True,
|
||||
"balance_shifts": True,
|
||||
"balance_weekends": True,
|
||||
}
|
||||
|
||||
def build_model(self):
|
||||
# Initialize model
|
||||
@@ -115,12 +136,12 @@ 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.nights = Var(
|
||||
((worker.id, week, block) for worker in self.workers
|
||||
for week in self.weeks for block in self.night_blocks),
|
||||
within=Binary,
|
||||
initialize=0,
|
||||
)
|
||||
# self.model.nights = Var(
|
||||
# ((worker.id, week, block) for worker in self.workers
|
||||
# for week in self.weeks for block in self.night_blocks),
|
||||
# within=Binary,
|
||||
# initialize=0,
|
||||
# )
|
||||
|
||||
# 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(
|
||||
@@ -156,26 +177,30 @@ class RotaBuilder(object):
|
||||
# Used to limit number of workers on night shift per site
|
||||
# if we force a binary it will in effect hard constrain to < 2
|
||||
# from a single site
|
||||
self.model.night_per_site = Var(
|
||||
((week, block, site) for site in self.sites for week in self.weeks
|
||||
for block in ["weekday", "weekend"]),
|
||||
#within=Binary,
|
||||
within=NonNegativeIntegers,
|
||||
initialize=0,
|
||||
)
|
||||
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,
|
||||
within=NonNegativeIntegers,
|
||||
initialize=0,
|
||||
)
|
||||
|
||||
self.model.night_per_site_t1 = Var(
|
||||
((week, block, site) for site in self.sites for week in self.weeks
|
||||
for block in ["weekday", "weekend"]),
|
||||
domain=NonNegativeIntegers,
|
||||
initialize=0,
|
||||
)
|
||||
self.model.night_per_site_t2 = Var(
|
||||
((week, block, site) for site in self.sites for week in self.weeks
|
||||
for block in ["weekday", "weekend"]),
|
||||
domain=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")),
|
||||
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")),
|
||||
domain=NonNegativeIntegers,
|
||||
initialize=0,
|
||||
)
|
||||
|
||||
self.model.shift_count = Var(
|
||||
((worker.id, shift) for worker in self.workers
|
||||
@@ -196,6 +221,84 @@ class RotaBuilder(object):
|
||||
initialize=0,
|
||||
)
|
||||
|
||||
if self.constraint_options["balance_nights"]:
|
||||
# We also try to even out the night shifts seperately
|
||||
self.model.night_shift_count = Var(
|
||||
((worker.id) for worker in self.workers),
|
||||
domain=NonNegativeReals,
|
||||
initialize=0,
|
||||
)
|
||||
|
||||
self.model.night_shift_count_t1 = Var(
|
||||
((worker.id) for worker in self.workers),
|
||||
domain=NonNegativeReals,
|
||||
initialize=0,
|
||||
)
|
||||
|
||||
self.model.night_shift_count_t2 = Var(
|
||||
((worker.id) for worker in self.workers),
|
||||
domain=NonNegativeReals,
|
||||
initialize=0,
|
||||
)
|
||||
|
||||
self.model.worker_weekend_assigned = Var(
|
||||
((worker.id, week) for worker in self.workers
|
||||
for week in self.weeks),
|
||||
domain=NonNegativeReals,
|
||||
initialize=0,
|
||||
)
|
||||
|
||||
self.model.works_weekend_count = Var(
|
||||
((worker.id, week) for worker in self.workers
|
||||
for week in self.weeks),
|
||||
domain=NonNegativeIntegers,
|
||||
initialize=0,
|
||||
)
|
||||
# self.model.works_saturday = Var(
|
||||
# ((worker.id, week) for worker in self.workers
|
||||
# for week in self.weeks),
|
||||
# domain=Binary,
|
||||
# initialize=0,
|
||||
# )
|
||||
# self.model.works_sunday = Var(
|
||||
# ((worker.id, week) for worker in self.workers
|
||||
# for week in self.weeks),
|
||||
# domain=Binary,
|
||||
# initialize=0,
|
||||
# )
|
||||
|
||||
# self.model.works_whole_weekend = Var(
|
||||
# ((worker.id, week) for worker in self.workers
|
||||
# for week in self.weeks),
|
||||
# domain=Binary,
|
||||
# initialize=0,
|
||||
# )
|
||||
|
||||
self.model.works_weekend = Var(
|
||||
((worker.id, week) for worker in self.workers
|
||||
for week in self.weeks),
|
||||
domain=Binary,
|
||||
initialize=0,
|
||||
)
|
||||
|
||||
self.model.worker_weekend_count = Var(
|
||||
((worker.id) for worker in self.workers),
|
||||
domain=NonNegativeIntegers,
|
||||
initialize=0,
|
||||
)
|
||||
|
||||
# self.model.weekend_count_t1 = Var(
|
||||
# ((worker.id) for worker in self.workers),
|
||||
# domain=NonNegativeReals,
|
||||
# initialize=0,
|
||||
# )
|
||||
|
||||
# self.model.weekend_count_t2 = Var(
|
||||
# ((worker.id) for worker in self.workers),
|
||||
# domain=NonNegativeReals,
|
||||
# initialize=0,
|
||||
# )
|
||||
|
||||
# self.model.unavailable = Var(((worker, week, day) for worker in self.workers for week in weeks for day in days),
|
||||
# within=Binary, initialize=0)
|
||||
|
||||
@@ -295,86 +398,62 @@ class RotaBuilder(object):
|
||||
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
|
||||
# def nightShiftMaxSTRule(model, week, block):
|
||||
# """Limits to 1 ST2 (or below) per night"""
|
||||
# single_workers = [w for w in self.workers if w.grade < 3]
|
||||
# if not single_workers:
|
||||
# return Constraint.Skip
|
||||
# return sum(model.nights[w.id, week, block]
|
||||
# for w in single_workers) <= 1
|
||||
|
||||
# # self.model.night_shifts_max_st_constraint = Constraint(
|
||||
# # [week for week in self.weeks],
|
||||
# # [block for block in ["weekday", "weekend"]],
|
||||
# # rule=nightShiftMaxSTRule,
|
||||
# # )
|
||||
|
||||
# Limit to 1 ST2 (or below) on a night shift
|
||||
def nightShiftMaxSTRule_new(model, week, day, shift):
|
||||
def nightShiftMaxSTRule(model, week, shift):
|
||||
"""Limits to 1 ST2 (or below) per night"""
|
||||
single_workers = [w for w in self.workers if w.grade < 3]
|
||||
if not single_workers:
|
||||
return Constraint.Skip
|
||||
return sum(model.works[w.id, week, day, shift]
|
||||
return sum(model.shift_week_worker_assigned[shift, week, w.id]
|
||||
for w in single_workers) <= 1
|
||||
|
||||
self.model.night_shifts_max_st_constraint = Constraint(
|
||||
[week for week in self.weeks],
|
||||
[day for day in self.days],
|
||||
[shift.name for shift in self.get_shifts_with_constraint("night")],
|
||||
rule=nightShiftMaxSTRule_new,
|
||||
)
|
||||
if self.constraint_options["limit_to_1_st1_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")
|
||||
],
|
||||
rule=nightShiftMaxSTRule,
|
||||
)
|
||||
|
||||
# Enusre at least 1 ST4+ on a night shift
|
||||
def nightShiftMinST4Rule(model, week, block):
|
||||
def nightShiftMinST4Rule(model, week, shift):
|
||||
single_workers = [w for w in self.workers if w.grade > 3]
|
||||
if not single_workers:
|
||||
print(single_workers)
|
||||
return Constraint.Skip
|
||||
return sum(model.nights[w.id, week, block]
|
||||
for w in single_workers) >= 1
|
||||
return sum(model.shift_week_worker_assigned[shift, week, w.id]
|
||||
for w in single_workers) >= 2
|
||||
|
||||
self.model.night_shifts_min_st4_constraint = Constraint(
|
||||
[week for week in self.weeks],
|
||||
[block for block in ["weekday", "weekend"]],
|
||||
rule=nightShiftMinST4Rule,
|
||||
)
|
||||
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")
|
||||
],
|
||||
rule=nightShiftMinST4Rule,
|
||||
)
|
||||
|
||||
# #Constraint (def of self.model.needed)
|
||||
# for worker in self.workers:
|
||||
# self.model.constraints.add(
|
||||
# 10000 * self.model.needed[worker.id] >= sum(
|
||||
# self.model.works[worker.id, week, day, shift] for week,
|
||||
# day, shift in self.get_all_shiftname_combinations())
|
||||
# ) # if any self.model.works[worker, ·, ·] non-zero, self.model.needed[worker] must be one; else is zero to reduce the obj function
|
||||
# #10000 is to remark, but 5 was enough since max of 40 hours yields max of 5 shifts, the maximum possible sum
|
||||
|
||||
# #Constraint (def of self.model.no_pref)
|
||||
# for worker in self.workers:
|
||||
# for week in weeks:
|
||||
# self.model.constraints.add(
|
||||
# self.model.no_pref[worker.id] >=
|
||||
# sum(self.model.works[worker.id, week, 'Sat', shift]
|
||||
# for shift in self.get_shift_names_by_day("Sat")) -
|
||||
# sum(self.model.works[worker.id, week, 'Sun', shift]
|
||||
# for shift in self.get_shift_names_by_day("Sun"))
|
||||
# ) # if not working on sunday but working saturday self.model.needed must be 1; else will be zero to reduce the obj function
|
||||
|
||||
# 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
|
||||
for site in self.sites:
|
||||
for block in ("weekend", "weekday"):
|
||||
for week in self.weeks:
|
||||
self.model.constraints.add(
|
||||
self.model.night_per_site[week, block, site] == sum(
|
||||
self.model.nights[worker.id, week, block]
|
||||
for worker in self.workers if worker.site == site)
|
||||
#1 >= sum(self.model.nights[worker.id, week, block] 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)
|
||||
# # 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"]:
|
||||
for site in self.sites:
|
||||
for shift in self.get_shifts_with_constraint("night"):
|
||||
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]
|
||||
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)
|
||||
)
|
||||
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)
|
||||
|
||||
weeks_days = self.get_week_day_combinations()
|
||||
|
||||
@@ -396,7 +475,6 @@ class RotaBuilder(object):
|
||||
for worker in self.workers))
|
||||
|
||||
if shift.force_as_block:
|
||||
# Force nights to be assigned in blocks
|
||||
self.model.constraints.add(
|
||||
self.model.worker_block_shift_counts[worker.id, week,
|
||||
shift.name] ==
|
||||
@@ -412,6 +490,13 @@ class RotaBuilder(object):
|
||||
# Most of our constraints apply per worker
|
||||
for worker in self.workers:
|
||||
|
||||
# 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))
|
||||
|
||||
# Balance shifts within required limits (set by balance_offset)
|
||||
# If balance_offset is too restrictive (for the rota length)
|
||||
# no solution will be possible
|
||||
@@ -433,18 +518,26 @@ class RotaBuilder(object):
|
||||
worker.fte_adj)
|
||||
|
||||
worker.shift_target_number[shift.name] = target_shifts
|
||||
if worker.name == "Jean Sukumar":
|
||||
print(worker.name, shift.name, target_shifts,
|
||||
total_shifts, worker.fte_adj)
|
||||
|
||||
max_shifts = target_shifts + shift.balance_offset * self.balance_offset_modifier
|
||||
min_shifts = target_shifts - shift.balance_offset * self.balance_offset_modifier
|
||||
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
|
||||
|
||||
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()),
|
||||
max_shifts,
|
||||
))
|
||||
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()),
|
||||
max_shifts,
|
||||
))
|
||||
else:
|
||||
# Make sure shifts aren't assigned to those from other sites
|
||||
self.model.constraints.add(0 == sum(
|
||||
@@ -458,6 +551,13 @@ class RotaBuilder(object):
|
||||
self.model.works[worker.id, week, day, shift.name]
|
||||
for week, day in self.get_week_day_combinations()))
|
||||
|
||||
if self.constraint_options["balance_nights"]:
|
||||
self.model.constraints.add(
|
||||
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")))
|
||||
|
||||
# Define shift_count_t1 and shift_count_t2 constraints for the object
|
||||
# Thus bypassing the need for a quadratic solver
|
||||
# t1-t2 is the target
|
||||
@@ -469,6 +569,28 @@ class RotaBuilder(object):
|
||||
(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_nights"]:
|
||||
night_shift_target_number = sum(
|
||||
worker.shift_target_number[shift.name]
|
||||
for shift in self.get_shifts_with_constraint("night"))
|
||||
|
||||
min_shifts = night_shift_target_number - 20
|
||||
max_shifts = night_shift_target_number + 20
|
||||
|
||||
self.model.constraints.add(
|
||||
inequality(
|
||||
min_shifts,
|
||||
self.model.night_shift_count[worker.id],
|
||||
max_shifts,
|
||||
))
|
||||
|
||||
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)
|
||||
|
||||
# Ensure worker is not allocated shifts on non working days
|
||||
if worker.nwd:
|
||||
@@ -478,37 +600,74 @@ class RotaBuilder(object):
|
||||
0 == self.model.works[worker.id, week, day,
|
||||
shift.name])
|
||||
|
||||
if "night" in self.get_shift_names():
|
||||
for week_blocks in self.get_week_block_iterator(2):
|
||||
# Prevent nights more than once every n weeks
|
||||
self.model.constraints.add(
|
||||
1 >= sum(self.model.nights[worker.id, week, block]
|
||||
for week in week_blocks
|
||||
for block in ["weekday", "weekend"]))
|
||||
# for week_blocks in self.get_week_block_iterator(
|
||||
# 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")))
|
||||
|
||||
# for week_blocks in self.get_week_block_iterator(
|
||||
# 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))
|
||||
|
||||
for week in self.weeks:
|
||||
|
||||
# # 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
|
||||
# self.model.constraints.add(
|
||||
# # We could use a helper to get required shifts to check
|
||||
# self.model.works_whole_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()) - 1)
|
||||
# self.model.constraints.add(
|
||||
# self.model.works_whole_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)
|
||||
if self.constraint_options["balance_weekends"]:
|
||||
self.model.constraints.add(
|
||||
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)
|
||||
self.model.constraints.add(
|
||||
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()))
|
||||
# self.model.constraints.add(
|
||||
# self.model.works_saturday[worker.id, week] == sum(
|
||||
# self.model.works[worker.id, week, "Sat", shift.name]
|
||||
# for shift in self.get_shifts()))
|
||||
# self.model.constraints.add(
|
||||
# self.model.works_sunday[worker.id, week] == sum(
|
||||
# self.model.works[worker.id, week, "Sun", shift.name]
|
||||
# for shift in self.get_shifts()))
|
||||
|
||||
# self.model.constraints.add(1 == sum(
|
||||
# self.model.shift_week_worker_assigned[s.name, week, worker.id]
|
||||
# for s in self.get_shifts_with_constraint("night")))
|
||||
|
||||
if "night" in self.get_shift_names():
|
||||
for shift in self.get_shifts_with_constraint("night"):
|
||||
# Force nights to be assigned in blocks
|
||||
self.model.constraints.add(1 == sum(
|
||||
self.model.nights[worker.id, week, block]
|
||||
for block in self.night_blocks))
|
||||
# self.model.constraints.add(8* self.model.shift_week_worker_assigned[shift.name, week, worker.id] >= sum(self.model.works[worker.id, week, day, shift.name] for day in self.days)
|
||||
# )
|
||||
|
||||
# if night block is weekday make sure Mon - Thurs is assigned as nights
|
||||
# Force night shifts to be assigned in blocks
|
||||
self.model.constraints.add(
|
||||
self.model.nights[worker.id, week, "weekday"] *
|
||||
4 == sum(self.model.works[worker.id, week, day,
|
||||
"night"]
|
||||
for day in days[:4]))
|
||||
self.model.constraints.add(
|
||||
self.model.nights[worker.id, week, "weekend"] *
|
||||
3 == sum(self.model.works[worker.id, week, day,
|
||||
"night"]
|
||||
for day in days[4:]))
|
||||
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))
|
||||
|
||||
#
|
||||
|
||||
@@ -528,7 +687,7 @@ class RotaBuilder(object):
|
||||
p2week, p2day = weeks_days[n - 2]
|
||||
except IndexError:
|
||||
p2week, p2day = weeks_days[n]
|
||||
n2 = 0
|
||||
p2 = 0
|
||||
|
||||
n1 = 1
|
||||
n2 = 1
|
||||
@@ -544,50 +703,58 @@ class RotaBuilder(object):
|
||||
n2week, n2day = weeks_days[n]
|
||||
n2 = 0
|
||||
|
||||
# Unable to work (hard constraint not preference)
|
||||
self.model.constraints.add(
|
||||
self.model.available[worker.id, week, day] >= sum(
|
||||
if self.get_shift_names_by_day(day):
|
||||
# Unable to work (hard constraint not preference)
|
||||
self.model.constraints.add(
|
||||
self.model.available[worker.id, week, day] >= sum(
|
||||
self.model.works[worker.id, week, day, shift]
|
||||
for shift in self.get_shift_names_by_day(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_day(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_day(day)))
|
||||
|
||||
# if working a night ensure preceeding (1) or subsequent (2) shifts can only be nights
|
||||
|
||||
if "night" in self.get_shift_names():
|
||||
# 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, "night"])
|
||||
if self.constraint_options["constrain_time_off_after_nights"]:
|
||||
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.constraints.add(
|
||||
1 >= self.model.works[worker.id, week, day, "night"] +
|
||||
sum(n1 *
|
||||
self.model.works[worker.id, nweek, nday, shift]
|
||||
for shift in self.get_shift_names_by_day(nday)
|
||||
if shift != "night"))
|
||||
self.model.constraints.add(
|
||||
1 >= self.model.works[worker.id, week, day,
|
||||
constraint_shift.name] +
|
||||
sum(n1 *
|
||||
self.model.works[worker.id, nweek, nday, shift]
|
||||
for shift in self.get_shift_names_by_day(nday)
|
||||
if shift != constraint_shift.name))
|
||||
|
||||
self.model.constraints.add(
|
||||
1 >= self.model.works[worker.id, week, day, "night"] +
|
||||
sum(n2 *
|
||||
self.model.works[worker.id, n2week, n2day, shift]
|
||||
for shift in self.get_shift_names_by_day(n2day)
|
||||
if shift != "night"))
|
||||
self.model.constraints.add(
|
||||
1 >= self.model.works[worker.id, week, day, "night"] +
|
||||
sum(p1 *
|
||||
self.model.works[worker.id, pweek, pday, shift]
|
||||
for shift in self.get_shift_names_by_day(pday)
|
||||
if shift != "night"))
|
||||
self.model.constraints.add(
|
||||
1 >= self.model.works[worker.id, week, day, "night"] +
|
||||
sum(p2 *
|
||||
self.model.works[worker.id, p2week, p2day, shift]
|
||||
for shift in self.get_shift_names_by_day(p2day)
|
||||
if shift != "night"))
|
||||
self.model.constraints.add(
|
||||
1 >= self.model.works[worker.id, week, day,
|
||||
constraint_shift.name] +
|
||||
sum(n2 * self.model.works[worker.id, n2week, n2day,
|
||||
shift]
|
||||
for shift in self.get_shift_names_by_day(n2day)
|
||||
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[worker.id, pweek, pday, shift]
|
||||
for shift in self.get_shift_names_by_day(pday)
|
||||
if shift != constraint_shift.name))
|
||||
self.model.constraints.add(
|
||||
1 >= self.model.works[worker.id, week, day,
|
||||
constraint_shift.name] +
|
||||
sum(p2 * self.model.works[worker.id, p2week, p2day,
|
||||
shift]
|
||||
for shift in self.get_shift_names_by_day(p2day)
|
||||
if shift != constraint_shift.name))
|
||||
|
||||
self.define_objectives()
|
||||
|
||||
@@ -597,12 +764,25 @@ class RotaBuilder(object):
|
||||
|
||||
# c = len(workers)
|
||||
|
||||
balance_modifier_constant = 1000
|
||||
balance_modifier_constant = 500
|
||||
|
||||
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)
|
||||
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)
|
||||
else:
|
||||
shift_balancing = 0
|
||||
|
||||
if self.constraint_options["balance_nights"]:
|
||||
night_balance_modifier_constant = 2000
|
||||
night_shift_balancing = sum(
|
||||
night_balance_modifier_constant *
|
||||
(self.model.night_shift_count_t1[(worker.id)] +
|
||||
self.model.night_shift_count_t2[(worker.id)])
|
||||
for worker in self.workers)
|
||||
else:
|
||||
night_shift_balancing = 0
|
||||
|
||||
preference_constant = 10
|
||||
# Preferences
|
||||
@@ -614,19 +794,29 @@ class RotaBuilder(object):
|
||||
|
||||
# # Spread nights
|
||||
|
||||
nights_balancing = sum(
|
||||
self.model.night_per_site_t1[week, block, site] * 20
|
||||
#self.model.night_per_site2[week, block, site]
|
||||
for week in self.weeks for block in ("weekday", "weekend")
|
||||
for site in self.sites)
|
||||
#nights_balancing = 0
|
||||
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]
|
||||
for week in self.weeks
|
||||
for shift in self.get_shifts_with_constraint("night")
|
||||
for site in self.sites)
|
||||
else:
|
||||
nights_site_balancing = 0
|
||||
|
||||
blocks_balancing = sum(
|
||||
100 * self.model.blocks_assigned[week, shift]
|
||||
for week in self.weeks
|
||||
for shift in self.shifts_to_assign_as_blocks())
|
||||
if self.constraint_options["balance_blocks"]:
|
||||
blocks_balancing = sum(
|
||||
100 * self.model.blocks_assigned[week, shift]
|
||||
for week in self.weeks
|
||||
for shift in self.shifts_to_assign_as_blocks())
|
||||
else:
|
||||
blocks_balancing = 0
|
||||
|
||||
return shift_balancing + preferences + nights_balancing + blocks_balancing
|
||||
return night_shift_balancing
|
||||
#return shift_balancing + preferences + blocks_balancing
|
||||
#return shift_balancing + preferences + nights_site_balancing + blocks_balancing
|
||||
return shift_balancing + night_shift_balancing + preferences + nights_site_balancing + blocks_balancing
|
||||
|
||||
# add objective function to the model. rule (pass function) or expr (pass expression directly)
|
||||
self.model.obj = Objective(rule=obj_rule, sense=minimize)
|
||||
@@ -874,6 +1064,9 @@ class RotaBuilder(object):
|
||||
s.extend(self.shifts_to_force_as_blocks())
|
||||
return s
|
||||
|
||||
def get_workers_total_fte(self):
|
||||
return self.full_time_equivalent
|
||||
|
||||
|
||||
class RotaResults(object):
|
||||
def __init__(self, rota, results):
|
||||
@@ -946,7 +1139,10 @@ class RotaResults(object):
|
||||
timetable[worker.name][week][day] = shift
|
||||
return timetable
|
||||
|
||||
def get_worker_timetable_brief(self, show_prefs=True, marker_every=19):
|
||||
def get_worker_timetable_brief(self,
|
||||
show_prefs=True,
|
||||
marker_every=19,
|
||||
show_unavailable=True):
|
||||
model = self.rota.model
|
||||
week_string = "{:20}".format("-Week-") + "".join(
|
||||
[7 * str("{}".format(w))[-1:] for w in self.rota.weeks])
|
||||
@@ -966,8 +1162,11 @@ class RotaResults(object):
|
||||
|
||||
shift_count = ""
|
||||
for s in set(shifts):
|
||||
shift_count = shift_count + "{}: {},".format(
|
||||
shift_count = shift_count + "{}: {}, ".format(
|
||||
s, shifts.count(s))
|
||||
|
||||
shift_count = shift_count + "#weekends_worked: {}\\#".format(
|
||||
model.worker_weekend_count[worker.id].value)
|
||||
timetable.append("{} {}".format("".join(w), shift_count))
|
||||
|
||||
if show_prefs:
|
||||
@@ -980,6 +1179,16 @@ class RotaResults(object):
|
||||
w.append("N")
|
||||
timetable.append("".join(w))
|
||||
|
||||
if show_unavailable:
|
||||
# prefs
|
||||
w = ["{:20}".format("Unavailable")]
|
||||
for week, day in self.rota.get_week_day_combinations():
|
||||
if model.available[worker.id, week, day] > 0:
|
||||
w.append("A")
|
||||
else:
|
||||
w.append("U")
|
||||
timetable.append("".join(w))
|
||||
|
||||
i = marker_every
|
||||
while i < len(timetable):
|
||||
timetable.insert(i, week_string)
|
||||
@@ -1024,26 +1233,27 @@ class RotaResults(object):
|
||||
# """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]
|
||||
|
||||
def get_night_blocks(self):
|
||||
nights = self.rota.model.nights
|
||||
timetable = {
|
||||
worker.name: {
|
||||
week: {block: ""
|
||||
for block in self.rota.night_blocks}
|
||||
for week in self.rota.weeks
|
||||
}
|
||||
for worker in self.rota.workers
|
||||
}
|
||||
for worker in self.rota.workers:
|
||||
for week in self.rota.weeks:
|
||||
for block in self.rota.night_blocks:
|
||||
if nights[worker.id, week, block].value == 1:
|
||||
timetable[worker.name][week][block] = "true"
|
||||
return timetable
|
||||
# def get_night_blocks(self):
|
||||
# nights = self.rota.model.nights
|
||||
# timetable = {
|
||||
# worker.name: {
|
||||
# week: {block: ""
|
||||
# for block in self.rota.night_blocks}
|
||||
# for week in self.rota.weeks
|
||||
# }
|
||||
# for worker in self.rota.workers
|
||||
# }
|
||||
# for worker in self.rota.workers:
|
||||
# for week in self.rota.weeks:
|
||||
# for block in self.rota.night_blocks:
|
||||
# if nights[worker.id, week, block].value == 1:
|
||||
# timetable[worker.name][week][block] = "true"
|
||||
# return timetable
|
||||
|
||||
def get_multinight(self):
|
||||
for site in self.rota.sites:
|
||||
for block in ("weekend", "weekday"):
|
||||
for shift in self.get_shifts_with_constraint("night"):
|
||||
block = shift.name
|
||||
for week in self.rota.weeks:
|
||||
print(
|
||||
site, week, block,
|
||||
@@ -1053,3 +1263,13 @@ class RotaResults(object):
|
||||
site].value,
|
||||
self.rota.model.night_per_site_t2[week, block,
|
||||
site].value)
|
||||
|
||||
def get_night_details(self):
|
||||
for worker in self.rota.workers:
|
||||
print(
|
||||
worker.name,
|
||||
self.rota.model.night_shift_count[(worker.id)].value,
|
||||
self.rota.model.night_shift_count_t1[(worker.id)].value,
|
||||
self.rota.model.night_shift_count_t2[(worker.id)].value,
|
||||
worker.shift_target_number["night_weekday"] +
|
||||
worker.shift_target_number["night_weekend"])
|
||||
|
||||
Reference in New Issue
Block a user