update
This commit is contained in:
@@ -119,6 +119,8 @@ class RotaBuilder(object):
|
||||
"balance_blocks": True,
|
||||
"balance_shifts": True,
|
||||
"balance_weekends": True,
|
||||
"prevent_monday_after_full_weekends": False,
|
||||
"prevent_fridays_before_full_weekends": True,
|
||||
}
|
||||
|
||||
def build_model(self):
|
||||
@@ -209,17 +211,19 @@ class RotaBuilder(object):
|
||||
initialize=0,
|
||||
)
|
||||
|
||||
self.model.shift_count_t1 = Var(
|
||||
((worker.id) for worker in self.workers),
|
||||
domain=NonNegativeReals,
|
||||
initialize=0,
|
||||
)
|
||||
if self.constraint_options["balance_shifts"]:
|
||||
|
||||
self.model.shift_count_t2 = Var(
|
||||
((worker.id) for worker in self.workers),
|
||||
domain=NonNegativeReals,
|
||||
initialize=0,
|
||||
)
|
||||
self.model.shift_count_t1 = Var(
|
||||
((worker.id) for worker in self.workers),
|
||||
domain=NonNegativeReals,
|
||||
initialize=0,
|
||||
)
|
||||
|
||||
self.model.shift_count_t2 = Var(
|
||||
((worker.id) for worker in self.workers),
|
||||
domain=NonNegativeReals,
|
||||
initialize=0,
|
||||
)
|
||||
|
||||
if self.constraint_options["balance_nights"]:
|
||||
# We also try to even out the night shifts seperately
|
||||
@@ -284,7 +288,7 @@ class RotaBuilder(object):
|
||||
|
||||
self.model.works_weekend = Var(
|
||||
((worker.id, week) for worker in self.workers
|
||||
for week in self.weeks),
|
||||
for week in self.weeks),
|
||||
domain=Binary,
|
||||
initialize=0,
|
||||
)
|
||||
@@ -295,7 +299,6 @@ class RotaBuilder(object):
|
||||
initialize=0,
|
||||
)
|
||||
|
||||
|
||||
self.model.weekend_shift_count_t1 = Var(
|
||||
((worker.id) for worker in self.workers),
|
||||
domain=NonNegativeReals,
|
||||
@@ -331,10 +334,12 @@ 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
|
||||
|
||||
#print(self.unavailable_to_work)
|
||||
|
||||
self.model.available = Param(
|
||||
((worker.id, week, day) for worker in self.workers
|
||||
for week, day in self.get_week_day_combinations()),
|
||||
@@ -545,9 +550,6 @@ 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)
|
||||
|
||||
if shift.hard_constrain_shift:
|
||||
adjusted_balance_offset = shift.balance_offset
|
||||
@@ -590,12 +592,14 @@ class RotaBuilder(object):
|
||||
# t1-t2 is the target
|
||||
# As the objective is to minimise t1+t2 and t1 and t2 are positive reals
|
||||
# t1+t2 approximates the absolute target (which otherwise requires a quadratic solver)
|
||||
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 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))
|
||||
|
||||
if self.constraint_options["balance_nights"]:
|
||||
@@ -603,15 +607,15 @@ class RotaBuilder(object):
|
||||
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
|
||||
# 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(
|
||||
# inequality(
|
||||
# min_shifts,
|
||||
# self.model.night_shift_count[worker.id],
|
||||
# max_shifts,
|
||||
# ))
|
||||
|
||||
self.model.constraints.add(
|
||||
self.model.night_shift_count_t1[worker.id] -
|
||||
@@ -619,6 +623,7 @@ class RotaBuilder(object):
|
||||
self.model.night_shift_count[worker.id] -
|
||||
night_shift_target_number)
|
||||
|
||||
# This may need to be updated
|
||||
xU = 6
|
||||
xL = 1
|
||||
self.model.constraints.add(
|
||||
@@ -644,6 +649,52 @@ class RotaBuilder(object):
|
||||
# self.model.constraints.add(
|
||||
# self.model.night_shift_count_w[worker.id] >= 0)
|
||||
|
||||
# We use a similar method to balance the number of weekends worked
|
||||
# I'm not entirely sure how split shifts will affect this
|
||||
if self.constraint_options["balance_weekends"]:
|
||||
weekend_shift_target_number = sum(
|
||||
worker.shift_target_number[shift.name]
|
||||
for shift in self.get_shifts()
|
||||
if not set(("Sat", "Sun")).isdisjoint(shift.shift_days))
|
||||
|
||||
# min_shifts = weekend_shift_target_number - 20
|
||||
# max_shifts = weekend_shift_target_number + 20
|
||||
|
||||
# self.model.constraints.add(
|
||||
# inequality(
|
||||
# min_shifts,
|
||||
# self.model.worker_weekend_count[worker.id],
|
||||
# max_shifts,
|
||||
# ))
|
||||
|
||||
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)
|
||||
|
||||
xU = 10
|
||||
xL = 1
|
||||
self.model.constraints.add(
|
||||
inequality(
|
||||
xL,
|
||||
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.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)
|
||||
|
||||
# Ensure worker is not allocated shifts on non working days
|
||||
if worker.nwd:
|
||||
for week, day, shift in self.get_all_shiftclass_combinations():
|
||||
@@ -652,24 +703,56 @@ class RotaBuilder(object):
|
||||
0 == self.model.works[worker.id, week, day,
|
||||
shift.name])
|
||||
|
||||
# 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")))
|
||||
if self.constraint_options["balance_blocks"]:
|
||||
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_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:
|
||||
if self.constraint_options[
|
||||
"prevent_monday_after_full_weekends"]:
|
||||
# Ignore last week
|
||||
if week + 1 in self.weeks:
|
||||
self.model.constraints.add(
|
||||
2 >= 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 self.constraint_options[
|
||||
"prevent_fridays_before_full_weekends"]:
|
||||
self.model.constraints.add(
|
||||
2 >= sum(self.model.works[worker.id, week, "Sat",
|
||||
shift.name]
|
||||
for shift in self.get_shifts()
|
||||
if shift.name not in ("night_weekend")) +
|
||||
sum(self.model.works[worker.id, week, "Sun",
|
||||
shift.name]
|
||||
for shift in self.get_shifts() if shift.name not in
|
||||
("night_weekend")) +
|
||||
sum(self.model.works[worker.id, week, "Fri",
|
||||
shift.name]
|
||||
for shift in self.get_shifts() if shift.name not in
|
||||
("night_weekend")))
|
||||
|
||||
# # 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
|
||||
@@ -755,6 +838,12 @@ class RotaBuilder(object):
|
||||
n2week, n2day = weeks_days[n]
|
||||
n2 = 0
|
||||
|
||||
try:
|
||||
n3week, n3day = weeks_days[n + 3]
|
||||
except IndexError:
|
||||
n3week, n3day = weeks_days[n]
|
||||
n3 = 0
|
||||
|
||||
if self.get_shift_names_by_day(day):
|
||||
# Unable to work (hard constraint not preference)
|
||||
self.model.constraints.add(
|
||||
@@ -793,6 +882,13 @@ class RotaBuilder(object):
|
||||
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(n3 * self.model.works[worker.id, n3week, n3day,
|
||||
# shift]
|
||||
# for shift in self.get_shift_names_by_day(n3day)
|
||||
# if shift != constraint_shift.name))
|
||||
self.model.constraints.add(
|
||||
1 >= self.model.works[worker.id, week, day,
|
||||
constraint_shift.name] +
|
||||
@@ -816,7 +912,7 @@ class RotaBuilder(object):
|
||||
|
||||
# c = len(workers)
|
||||
|
||||
balance_modifier_constant = 500
|
||||
balance_modifier_constant = 1
|
||||
|
||||
if self.constraint_options["balance_shifts"]:
|
||||
shift_balancing = sum(balance_modifier_constant *
|
||||
@@ -827,7 +923,7 @@ class RotaBuilder(object):
|
||||
shift_balancing = 0
|
||||
|
||||
if self.constraint_options["balance_nights"]:
|
||||
night_balance_modifier_constant = 1
|
||||
night_balance_modifier_constant = 10
|
||||
night_shift_balancing = sum(
|
||||
night_balance_modifier_constant *
|
||||
self.model.night_shift_count_w[(worker.id)]
|
||||
@@ -837,6 +933,15 @@ class RotaBuilder(object):
|
||||
else:
|
||||
night_shift_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)
|
||||
else:
|
||||
weekend_shift_balancing = 0
|
||||
|
||||
preference_constant = 10
|
||||
# Preferences
|
||||
preferences = sum(
|
||||
@@ -860,16 +965,16 @@ class RotaBuilder(object):
|
||||
|
||||
if self.constraint_options["balance_blocks"]:
|
||||
blocks_balancing = sum(
|
||||
100 * self.model.blocks_assigned[week, shift]
|
||||
1 * 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 night_shift_balancing
|
||||
#return weekend_shift_balancing + blocks_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
|
||||
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)
|
||||
@@ -1120,6 +1225,20 @@ class RotaBuilder(object):
|
||||
def get_workers_total_fte(self):
|
||||
return self.full_time_equivalent
|
||||
|
||||
def get_worker_details(self):
|
||||
|
||||
w = defaultdict(list)
|
||||
for worker in self.workers:
|
||||
w[worker.site].append(worker)
|
||||
|
||||
l = []
|
||||
for site in w:
|
||||
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)
|
||||
|
||||
|
||||
class RotaResults(object):
|
||||
def __init__(self, rota, results):
|
||||
@@ -1193,9 +1312,9 @@ class RotaResults(object):
|
||||
return timetable
|
||||
|
||||
def get_worker_timetable_brief(self,
|
||||
show_prefs=True,
|
||||
marker_every=19,
|
||||
show_unavailable=True):
|
||||
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])
|
||||
@@ -1253,6 +1372,112 @@ class RotaResults(object):
|
||||
timetable.insert(0, days_string)
|
||||
return "\n".join(timetable)
|
||||
|
||||
def get_worker_timetable_html(self,
|
||||
include_html_tag=False,
|
||||
table_name="Test"):
|
||||
model = self.rota.model
|
||||
|
||||
timetable = []
|
||||
|
||||
date_row = ["<th class='worker'></th>"]
|
||||
|
||||
n = 0
|
||||
for week, day in self.rota.get_week_day_combinations():
|
||||
d = self.rota.start_date + datetime.timedelta(n)
|
||||
date_row.append("<th title='{}'>Week {}: {}</th>".format(
|
||||
d, week, day))
|
||||
n = n + 1
|
||||
timetable.append("<tr class='data-row'>{}</tr>".format(
|
||||
"".join(date_row)))
|
||||
|
||||
for worker in self.rota.workers:
|
||||
shifts = []
|
||||
if worker.nwd is not None:
|
||||
nwds = ", ".join(worker.nwd)
|
||||
else:
|
||||
nwds = None
|
||||
|
||||
w = [
|
||||
"<td title='Site: {}' class='worker {}' data-nwds='{}' data-site='{}' data-worker='{}' data-fte='{}' data-fte_adj='{}' data-end_date='{}'>{} ({}) [{}]</td>"
|
||||
.format(worker.site, worker.site, nwds, worker.site,
|
||||
worker.name, worker.fte, worker.fte_adj,
|
||||
worker.end_date, worker.name, worker.grade, worker.fte)
|
||||
]
|
||||
n = 0
|
||||
for week, day in self.rota.get_week_day_combinations():
|
||||
d = self.rota.start_date + datetime.timedelta(n)
|
||||
n = n + 1
|
||||
a = "-"
|
||||
shift_name = ""
|
||||
for shift in self.rota.get_shift_names_by_day(day):
|
||||
if model.works[worker.id, week, day, shift].value == 1:
|
||||
shifts.append(shift)
|
||||
a = shift[0]
|
||||
shift_name = shift
|
||||
break
|
||||
css_class = ""
|
||||
if model.available[worker.id, week, day] > 0:
|
||||
available = True
|
||||
else:
|
||||
available = False
|
||||
css_class = "unavailable"
|
||||
if worker.name == "Delilah Trimmer":
|
||||
pass
|
||||
w.append(
|
||||
"<td title='{} ({})' class='rota-day {}' data-shift='{}' data-available='{}' data-date='{}' data-week='{}' data-day='{}'>{}</td>"
|
||||
.format(shift_name, d, css_class, shift_name, available, d,
|
||||
week, day, a))
|
||||
|
||||
shift_count = ""
|
||||
for s in set(shifts):
|
||||
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("<tr class='worker-row'>{}</tr>".format(
|
||||
"".join(w)))
|
||||
#timetable.append("<tr>{}</tr>".format("".join(w), shift_count))
|
||||
|
||||
# if show_prefs:
|
||||
# # prefs
|
||||
# w = ["{:20}".format("Preferences")]
|
||||
# for week, day in self.rota.get_week_day_combinations():
|
||||
# if model.pref_not_to_work[worker.id, week, day] > 0:
|
||||
# w.append("Y")
|
||||
# else:
|
||||
# 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))
|
||||
|
||||
html = """
|
||||
<body>
|
||||
<div class="table-div" id="{}">
|
||||
<table>{}</table>
|
||||
</div>
|
||||
</body>
|
||||
""".format(table_name, "\n".join(timetable))
|
||||
|
||||
if include_html_tag:
|
||||
html = """<html>
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="timetable.css">
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||
<script src="s.js" defer></script>
|
||||
</head>
|
||||
{}</html>""".format(html)
|
||||
|
||||
return html
|
||||
|
||||
def get_shift_summary(self):
|
||||
works = self.rota.model.works
|
||||
timetable = {
|
||||
@@ -1282,6 +1507,35 @@ class RotaResults(object):
|
||||
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()}
|
||||
for worker in self.rota.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"]:
|
||||
c = [
|
||||
works[worker.id, week, day, shift]
|
||||
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)
|
||||
timetable[worker.get_details()][shift[0]] = c
|
||||
t.append("{} [{}]: {}".format(worker.get_full_details(),
|
||||
total_shifts, ", ".join(l)))
|
||||
return "\n".join(t)
|
||||
|
||||
#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]
|
||||
@@ -1332,4 +1586,24 @@ class RotaResults(object):
|
||||
1),
|
||||
"balance: {},".format(
|
||||
self.rota.model.night_shift_count_w[worker.id].value),
|
||||
)
|
||||
)
|
||||
|
||||
def get_weekend_details(self):
|
||||
for worker in self.rota.workers:
|
||||
print(
|
||||
"{:20}".format(worker.name),
|
||||
"worked: {},".format(
|
||||
self.rota.model.worker_weekend_count[(worker.id)].value),
|
||||
"target: {},".format(
|
||||
sum([
|
||||
worker.shift_target_number[shift.name]
|
||||
for shift in self.rota.get_shifts()
|
||||
if not set(("Sat", "Sun")).isdisjoint(shift.shift_days)
|
||||
])),
|
||||
"target_diff: {},".format(
|
||||
self.rota.model.weekend_shift_count_t1[(worker.id)].value +
|
||||
self.rota.model.weekend_shift_count_t2[(worker.id)].value +
|
||||
1),
|
||||
"balance: {},".format(
|
||||
self.rota.model.weekend_shift_count_w[worker.id].value),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user