many improvements + restructure

This commit is contained in:
Ross
2022-02-21 13:53:13 +00:00
parent cbe99ea3d8
commit 0c795e518b
17 changed files with 653 additions and 1927 deletions
View File
+118
View File
@@ -0,0 +1,118 @@
import csv
import re
date_re = r"[\d]{1,2}\/[\d]{1,2}\/[\d]{2}"
def load_leave(Rota, use_live_rota):
file_name = "leave.csv"
with open(file_name, newline="") as f:
shifts = Rota.get_shift_names()
workers = {}
reader = csv.reader(f, delimiter=",", quotechar='"')
leave = {}
requests = {}
site_prefs = {}
order = []
n = 0
for row in reader:
row_title = row[0]
r = row[2:]
# header, extract names
if n == 1:
names = r
order = names
for name in names:
workers[name] = {}
workers[name]["leave"] = []
workers[name]["requests"] = []
workers[name]["site_pref"] = []
workers[name]["shift_balance_extra"] = {}
workers[name]["bank_holiday_extra"] = 0
for i in range(len(r)):
try:
a = order[i]
except IndexError:
continue
worker = workers[order[i]]
lower_item = r[i].lower()
if lower_item == "derriford":
lower_item = "plymouth"
if lower_item == "derriford twilights":
lower_item = "plymouth_twilights"
if row_title == "PROC site":
worker["site_pref"] = lower_item
if row_title == "Rotation":
worker["site"] = lower_item
elif row_title == "Grade":
worker["grade"] = lower_item
elif row_title == "%FTE":
worker["fte"] = lower_item
elif row_title == "NWD":
worker["nwd"] = lower_item
elif row_title == "Flexible NWD":
worker["flexible_nwd"] = lower_item
elif row_title == "End Date":
worker["end_date"] = lower_item
elif row_title == "OOP":
worker["oop"] = lower_item
elif row_title == "Group":
worker["group"] = lower_item
elif row_title == "Pair":
if lower_item == "":
worker["pair"] = 0
else:
worker["pair"] = int(lower_item)
elif row_title.startswith("Extra_"):
if lower_item:
shift = row_title[len("Extra_") :]
if shift == "twilight":
shift = f"{worker['site']}_{shift}"
elif shift == "weekend":
shift = f"weekend_{worker['site']}"
worker["shift_balance_extra"][shift] = float(lower_item)
elif row_title == "bank_holidays_worked":
if lower_item:
print("BH", lower_item)
worker["bank_holiday_extra"] = int(lower_item)
elif re.match(date_re, row_title) is not None:
if lower_item != "":
# This may be easier to do as a dict
if lower_item in shifts:
worker["requests"].append((row[0], lower_item))
else:
worker["leave"].append((row[0], lower_item))
n = n + 1
return workers
# load_leave()
+377
View File
@@ -0,0 +1,377 @@
from pyomo.environ import *
from pyomo.opt import SolverFactory
from pyomo.opt.results import SolverStatus
from shifts import SingleShift, RotaBuilder, days
from workers import Worker
import datetime
import sys
import os
use_neos = False
use_cplex = False
solve = True
weeks_to_rota = 13
time_to_run = 258500
# allow = 5000
ratio = 0.001
use_live_rota = False
suspend_on_finish = False
sites = ("truro", "exeter", "torbay", "barnstaple", "plymouth", "proc", "truro no proc")
Rota = RotaBuilder(
# datetime.date(2022, 6, 6),
datetime.date(2022, 3, 7),
# Rota = RotaBuilder(datetime.date(2021, 12, 6),
# Rota = RotaBuilder(datetime.date(2021, 11, 8),
weeks_to_rota=weeks_to_rota,
balance_offset_modifier=1,
max_weekend_frequency=2,
max_night_frequency=2,
use_shift_balance_extra=True,
use_bank_holiday_extra=True,
)
Rota.constraint_options["limit_to_1_st2_on_nights"] = True
Rota.constraint_options["ensure_1_st4_plus_on_nights"] = True
Rota.constraint_options["balance_nights"] = True
#Rota.constraint_options["constrain_time_off_after_nights"] = True
Rota.constraint_options["balance_nights_across_sites"] = True
Rota.constraint_options["balance_blocks"] = True
Rota.constraint_options["balance_weekends"] = True
Rota.constraint_options["balance_shifts_over_workers"] = True
Rota.constraint_options["balance_bank_holidays"] = True
Rota.constraint_options["avoid_st2_first_month"] = False
Rota.constraint_options["hard_constrain_pair_separation"] = True
Rota.constraint_options["max_weekends"] = 7
#Rota.constraint_options["prevent_monday_and_tuesday_after_full_weekends"] = [
# "truro",
# "torbay",
# "exeter",
# "plymouth",
# "plymouth no proc",
# "truro no proc",
#]
#Rota.constraint_options["prevent_monday_after_full_weekends"] = [
# "plymouth",
# "plymouth no proc",
#]
#Rota.constraint_options["prevent_fridays_before_full_weekends"] = [
# "plymouth",
# "plymouth no proc",
#]
#Rota.constraint_options["prevent_thursdays_before_full_weekends"] = [
# "truro",
# "torbay",
# "exeter",
# "truro no proc",
#]
Rota.add_shifts(
SingleShift(
("exeter",),
"exeter_twilight",
12.5,
days[:4],
balance_offset=3,
constraints=["max_2_shifts_per_week"],
),
SingleShift(
("truro", "truro no proc"), "truro_twilight", 12.5, days[:4], balance_offset=3
),
SingleShift(
("torbay", "torbay twilights"),
"torbay_twilight",
12.5,
days[:4],
balance_offset=5,
assign_as_block=True,
),
SingleShift(
("plymouth", "plymouth_twilights"),
"plymouth_twilight",
12.5,
days[:5],
balance_offset=3,
workers_required=1,
constraints=["max_2_shifts_per_week"],
),
SingleShift(
("exeter",),
"weekend_exeter",
12.5,
days[4:],
balance_offset=4,
rota_on_nwds=True,
force_as_block=True,
constraints=["preclear2", "postclear2"],
),
SingleShift(
("truro", "truro no proc"),
"weekend_truro",
12.5,
days[4:],
balance_offset=4,
rota_on_nwds=True,
force_as_block=True,
constraints=["preclear2", "postclear2"],
# force_as_block_unless_nwd=True
),
SingleShift(
("torbay",),
"weekend_torbay",
12.5,
days[4:],
balance_offset=4,
rota_on_nwds=True,
force_as_block=True,
constraints=["preclear2", "postclear2"],
# force_as_block_unless_nwd=True
),
SingleShift(
("plymouth",),
"weekend_plymouth1",
8,
days[5:],
balance_offset=4,
workers_required=1,
rota_on_nwds=True,
force_as_block=True,
constraints=["preclear2", "postclear2"],
),
SingleShift(
("plymouth",),
"weekend_plymouth2",
8,
days[5:],
balance_offset=4,
workers_required=1,
rota_on_nwds=True,
force_as_block=True,
constraints=["preclear2", "postclear2"],
),
SingleShift(
("plymouth", "plymouth_twilights"),
"plymouth_bank_holidays",
8,
days[5:],
balance_offset=2,
workers_required=1,
rota_on_nwds=True,
force_as_block=False,
bank_holidays_only=True,
),
SingleShift(
(sites[:-1]),
"night_weekday",
12.25,
days[:4],
balance_offset=4.9,
balance_weighting=1,
# hard_constrain_shift=False,
workers_required=3,
force_as_block=True,
rota_on_nwds=True,
constraints=["night", "preclear2", "postclear2"],
),
SingleShift(
(sites[:-1]),
"night_weekend",
12.25,
days[4:],
balance_offset=3.9,
balance_weighting=1,
# hard_constrain_shift=False,
workers_required=3,
force_as_block=True,
rota_on_nwds=True,
constraints=["night", "preclear2", "postclear2"],
),
)
use_test_workers = False
load_leave = True
Rota.build_shifts()
if load_leave:
import leave
workers = leave.load_leave(Rota, use_live_rota)
# Import trainee data
if use_test_workers:
# Rota.add_workers([
# Worker(Rota,
# i,
# "Truro {}".format(i),
# "truro",
# 3,
# nwd=['Fri', "Sat", "Sun"]) for i in range(1, 2)
# ])
Rota.add_workers(
[
Worker(
Rota,
"Truro {}".format(i),
"truro",
4,
id=i,
)
for i in range(1, 9)
]
)
Rota.add_workers(
[Worker(Rota, i, "Plym {}".format(i), "plymouth", 4, 50) for i in range(9, 17)]
)
Rota.add_workers(
[
Worker(
Rota, i, "Plym {}".format(i), "plymouth", 4, 80, end_date="2020/10/05"
)
for i in range(17, 19)
]
)
else:
n = 0
for worker in workers:
n = n + 1
worker_name = worker
w = workers[worker]
site = w["site"]
grade = w["grade"]
fte = w["fte"]
nwd = w["nwd"]
end_date = w["end_date"]
oop = w["oop"]
grade = w["grade"]
pair = w["pair"]
# name, site, grade, fte, nwd, end_date, oop, twi, twi_we, night, night_we, pw1, pw2, extra = row
# Ignore trainees if fte == 0 (what are they doing here anyway)
if fte == 0 or fte == "0" or fte == "":
continue
nwds = None
if nwd:
nwds = []
for i in nwd.split(","):
nwd_start_date = Rota.start_date
nwd_end_date = Rota.rota_end_date
if "[" in i:
a, b = i.split("[")[1][:-1].split("-")
nwd_start_date = datetime.datetime.strptime(a, "%d/%m/%y").date()
nwd_end_date = datetime.datetime.strptime(b, "%d/%m/%y").date()
nwds.append(
(
i.split("[")[0].strip()[:3].capitalize(),
nwd_start_date,
nwd_end_date,
)
)
# nwds = [i.strip()[:3].capitalize() for i in nwd.split("/")] if nwd else None
end_date = end_date if end_date else None
oop = oop.split("-") if oop else None
previous_shifts = {}
# if twi:
# worked = twi.split(" ")[0]
# allocated = twi.split(" ")[1][1:-1]
# previous_shifts["{}_twilight".format(site.lower())] = (worked, allocated)
# if twi_we:
# worked = twi_we.split(" ")[0]
# allocated = twi_we.split(" ")[1][1:-1]
# previous_shifts["weekend_{}".format(site.lower())] = (worked, allocated)
# if night:
# worked = night.split(" ")[0]
# allocated = night.split(" ")[1][1:-1]
# previous_shifts["night_weekday".format(site.lower())] = (worked, allocated)
# if night_we:
# worked = night_we.split(" ")[0]
# allocated = night_we.split(" ")[1][1:-1]
# previous_shifts["night_weekend".format(site.lower())] = (worked, allocated)
# if pw1:
# worked = pw1.split(" ")[0]
# allocated = pw1.split(" ")[1][1:-1]
# previous_shifts["weekend_plymouth1"] = (worked, allocated)
# if pw2:
# worked = pw2.split(" ")[0]
# allocated = pw2.split(" ")[1][1:-1]
# previous_shifts["weekend_plymouth2"] = (worked, allocated)
leave_requests = w["leave"]
work_requests = w["requests"]
site_pref = w["site_pref"]
Rota.add_worker(
Worker(
Rota,
worker_name,
site.lower(),
int(grade[2]),
n,
int(fte),
nwds,
end_date,
oop,
not_available_to_work=leave_requests,
work_requests=work_requests,
home_site=site_pref,
previous_shifts=previous_shifts,
pair=pair,
shift_balance_extra=w["shift_balance_extra"],
bank_holiday_extra=w["bank_holiday_extra"],
)
)
print("Building model")
Rota.build_workers()
Rota.sites.add("plymouth_twilights")
Rota.build_model()
print(Rota.get_worker_details())
if not solve:
ResultsHolder = RotaResults(Rota)
ResultsHolder.export_rota_to_html()
sys.exit(0)
solver_options = {"ratio": ratio, "seconds": time_to_run, "threads": 10}
Rota.solve_model(options=solver_options)
# print(Rota.get_worker_details())
# optimizer = SolverFactory('cbc')
# result = optimizer.solve(prob,tee=True)
# result.Solver.Status = SolverStatus.warning
# prob.solutions.load_from(result)
#ResultsHolder = RotaResults(Rota)
#worker_timetable_brief = ResultsHolder.get_worker_timetable_brief(
# show_prefs=False, show_unavailable=False
#)
#
#print(worker_timetable_brief)
Rota.export_rota_to_html()
Rota.export_rota_to_csv("rota")
if suspend_on_finish:
os.system("systemctl suspend")
+464
View File
@@ -0,0 +1,464 @@
from pyomo.environ import *
from pyomo.opt import SolverFactory
from shifts import SingleShift, RotaBuilder, RotaResults, days
from workers import Worker
from copy import deepcopy
import math
import scipy.stats as stats
import statistics
import datetime
import itertools
import operator
use_neos = False
weeks_to_rota = 52
sites = ("truro", "exeter", "torquay", "barnstaple", "plymouth")
Rota = RotaBuilder(
datetime.date(2020, 9, 7),
weeks_to_rota=weeks_to_rota,
balance_offset_modifier=1,
max_weekend_frequency=2,
max_night_frequency=2,
)
rota_collections = {
"current": deepcopy(Rota),
# "extended proc nights only": deepcopy(Rota),
# "no weekends": deepcopy(Rota),
# "no twighlights": deepcopy(Rota),
# "no nights": deepcopy(Rota),
# "nights + proc twighlights3 + normal weekends": deepcopy(Rota),
# "nights + proc twighlights4 + normal weekends": deepcopy(Rota),
# "nights + proc twighlights + proc weekends": deepcopy(Rota),
}
rota_collections["current"].add_shifts(
SingleShift(("exeter",), "exeter_twilight", 12.5, days[:5]),
SingleShift(("truro",), "truro_twilight", 12.5, days[:5]),
SingleShift(("torquay",), "torquay_twilight", 12.5, days[:5]),
SingleShift(("plymouth",), "plymouth_twilight", 12.5, days[:5]),
SingleShift(("exeter",), "weekend_exeter", 12.5, days[5:], assign_as_block=True),
SingleShift(("truro",), "weekend_truro", 12.5, days[5:], assign_as_block=True),
SingleShift(("torquay",), "weekend_torquay", 12.5, days[5:], assign_as_block=True),
SingleShift(
("plymouth",),
"weekend_plymouth",
8,
days[5:],
workers_required=2,
assign_as_block=True,
),
SingleShift(
(sites),
"night_weekday",
12.25,
days[:4],
balance_offset=5,
balance_weighting=1,
workers_required=3,
force_as_block=False,
rota_on_nwds=True,
constraints=["night"],
),
SingleShift(
(sites),
"night_weekend",
12.25,
days[4:],
balance_offset=4,
balance_weighting=1,
workers_required=3,
force_as_block=False,
rota_on_nwds=True,
constraints=["night"],
),
)
# rota_collections['extended proc nights only'].add_shifts(
# #SingleShift((sites), "night", 12.5, days, balance_weighting=1, workers_required=3, rota_on_nwds=True),
# SingleShift((sites),
# "night_weekday",
# 13,
# days[:4],
# balance_offset=5,
# balance_weighting=1,
# workers_required=3,
# force_as_block=False,
# rota_on_nwds=True,
# constraints=["night"]),
# SingleShift((sites),
# "night_weekend",
# 13,
# days[4:],
# balance_offset=4,
# balance_weighting=1,
# workers_required=3,
# force_as_block=False,
# rota_on_nwds=True,
# constraints=["night"]),
# )
# rota_collections['no weekends'].add_shifts(
# SingleShift(("exeter", ), "exeter_twilight", 12.5, days[:5]),
# SingleShift(("truro", ), "truro_twilight", 12.5, days[:5]),
# SingleShift(("torquay", ), "torquay_twilight", 12.5, days[:5]),
# SingleShift(("plymouth", ), "plymouth_twilight", 12.5, days[:5]),
# #SingleShift((sites), "night", 12.5, days, balance_weighting=1, workers_required=3, rota_on_nwds=True),
# SingleShift((sites),
# "night_weekday",
# 13,
# days[:4],
# balance_offset=5,
# balance_weighting=1,
# workers_required=3,
# force_as_block=False,
# rota_on_nwds=True,
# constraints=["night"]),
# SingleShift((sites),
# "night_weekend",
# 13,
# days[4:],
# balance_offset=4,
# balance_weighting=1,
# workers_required=3,
# force_as_block=False,
# rota_on_nwds=True,
# constraints=["night"]),
# )
# rota_collections['no twighlights'].add_shifts(
# SingleShift(("exeter", ),
# "weekend_exeter",
# 12.5,
# days[5:],
# assign_as_block=True),
# SingleShift(("truro", ),
# "weekend_truro",
# 12.5,
# days[5:],
# assign_as_block=True),
# SingleShift(("torquay", ),
# "weekend_torquay",
# 12.5,
# days[5:],
# assign_as_block=True),
# SingleShift(("plymouth", ),
# "weekend_plymouth",
# 8,
# days[5:],
# workers_required=2,
# assign_as_block=True),
# #SingleShift((sites), "night", 12.5, days, balance_weighting=1, workers_required=3, rota_on_nwds=True),
# SingleShift((sites),
# "night_weekday",
# 13,
# days[:4],
# balance_offset=5,
# balance_weighting=1,
# workers_required=3,
# force_as_block=False,
# rota_on_nwds=True,
# constraints=["night"]),
# SingleShift((sites),
# "night_weekend",
# 13,
# days[4:],
# balance_offset=4,
# balance_weighting=1,
# workers_required=3,
# force_as_block=False,
# rota_on_nwds=True,
# constraints=["night"]),
# )
# rota_collections['no nights'].add_shifts(
# SingleShift(("exeter", ), "exeter_twilight", 12.5, days[:5]),
# SingleShift(("truro", ), "truro_twilight", 12.5, days[:5]),
# SingleShift(("torquay", ), "torquay_twilight", 12.5, days[:5]),
# SingleShift(("plymouth", ), "plymouth_twilight", 12.5, days[:5]),
# SingleShift(("exeter", ),
# "weekend_exeter",
# 12.5,
# days[5:],
# assign_as_block=True),
# SingleShift(("truro", ),
# "weekend_truro",
# 12.5,
# days[5:],
# assign_as_block=True),
# SingleShift(("torquay", ),
# "weekend_torquay",
# 12.5,
# days[5:],
# assign_as_block=True),
# SingleShift(("plymouth", ),
# "weekend_plymouth",
# 8,
# days[5:],
# workers_required=2,
# assign_as_block=True),
# #SingleShift((sites), "night", 12.5, days, balance_weighting=1, workers_required=3, rota_on_nwds=True),
# )
# rota_collections['nights + proc twighlights4 + normal weekends'].add_shifts(
# SingleShift((sites), "proc_twilight", 12.5, days[:5], workers_required=4),
# SingleShift(("exeter", ),
# "weekend_exeter",
# 12.5,
# days[5:],
# assign_as_block=True),
# SingleShift(("truro", ),
# "weekend_truro",
# 12.5,
# days[5:],
# assign_as_block=True),
# SingleShift(("torquay", ),
# "weekend_torquay",
# 12.5,
# days[5:],
# assign_as_block=True),
# SingleShift(("plymouth", ),
# "weekend_plymouth",
# 8,
# days[5:],
# workers_required=2,
# assign_as_block=True),
# #SingleShift((sites), "night", 12.5, days, balance_weighting=1, workers_required=3, rota_on_nwds=True),
# SingleShift((sites),
# "night_weekday",
# 12.25,
# days[:4],
# balance_offset=5,
# balance_weighting=1,
# workers_required=3,
# force_as_block=False,
# rota_on_nwds=True,
# constraints=["night"]),
# SingleShift((sites),
# "night_weekend",
# 12.25,
# days[4:],
# balance_offset=4,
# balance_weighting=1,
# workers_required=3,
# force_as_block=False,
# rota_on_nwds=True,
# constraints=["night"]),
# )
# rota_collections['nights + proc twighlights3 + normal weekends'].add_shifts(
# SingleShift((sites), "proc_twilight", 12.5, days[:5], workers_required=3),
# SingleShift(("exeter", ),
# "weekend_exeter",
# 12.5,
# days[5:],
# assign_as_block=True),
# SingleShift(("truro", ),
# "weekend_truro",
# 12.5,
# days[5:],
# assign_as_block=True),
# SingleShift(("torquay", ),
# "weekend_torquay",
# 12.5,
# days[5:],
# assign_as_block=True),
# SingleShift(("plymouth", ),
# "weekend_plymouth",
# 8,
# days[5:],
# workers_required=2,
# assign_as_block=True),
# #SingleShift((sites), "night", 12.5, days, balance_weighting=1, workers_required=3, rota_on_nwds=True),
# SingleShift((sites),
# "night_weekday",
# 12.25,
# days[:4],
# balance_offset=5,
# balance_weighting=1,
# workers_required=3,
# force_as_block=False,
# rota_on_nwds=True,
# constraints=["night"]),
# SingleShift((sites),
# "night_weekend",
# 12.25,
# days[4:],
# balance_offset=4,
# balance_weighting=1,
# workers_required=3,
# force_as_block=False,
# rota_on_nwds=True,
# constraints=["night"]),
# )
# rota_collections['nights + proc twighlights + proc weekends'].add_shifts(
# SingleShift((sites), "proc_twilight", 12.5, days[:5], workers_required=3),
# SingleShift((sites), "proc_weekends", 12.5, days[5:], workers_required=3),
# #SingleShift((sites), "night", 12.5, days, balance_weighting=1, workers_required=3, rota_on_nwds=True),
# SingleShift((sites),
# "night_weekday",
# 12.25,
# days[:4],
# balance_offset=5,
# balance_weighting=1,
# workers_required=3,
# force_as_block=False,
# rota_on_nwds=True,
# constraints=["night"]),
# SingleShift((sites),
# "night_weekend",
# 12.25,
# days[4:],
# balance_offset=4,
# balance_weighting=1,
# workers_required=3,
# force_as_block=False,
# rota_on_nwds=True,
# constraints=["night"]),
# )
load_leave = True
if load_leave:
import leave
leave = leave.load_leave()
# Import trainee data
import csv
with open("trainees.csv", newline="") as f:
reader = csv.reader(f, delimiter=",", quotechar="|")
next(reader) # skip header
n = 0
for row in reader:
n = n + 1
# print(row)
name, site, grade, fte, nwd, end_date, oop, extra = row
# Ignore trainees if fte == 0 (what are they doing here anyway)
if fte == "0":
continue
nwds = nwd.split("/") if nwd else None
end_date = end_date if end_date else None
oop = oop.split("-") if oop else None
if load_leave:
if name not in leave:
raise ValueError("{} not found it leave requests".format(name))
leave_requests = [i[0] for i in leave[name] if i[0] != ""]
# print(nwds, end_date, oop)
for rot in rota_collections:
Rota = rota_collections[rot]
Rota.add_worker(
Worker(
Rota,
n,
name,
site.lower(),
int(grade[2]),
int(fte),
nwds,
end_date,
oop,
not_available_to_work=leave_requests,
)
)
with open("rotas.html", "w") as f:
f.write(
"""<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="timetable.js" defer></script>
</head>
<body>"""
)
for rot in rota_collections:
print("Running rota: ", rot)
Rota = rota_collections[rot]
Rota.build_workers()
print("-Building model")
Rota.build_model()
print("-Building model: complete")
opt = SolverFactory("cbc") # choose a solver
# opt = SolverFactory('ipopt') # choose a solver
if use_neos:
print("-Solving with neos")
solver_manager = SolverManagerFactory("neos") # Solve in neos server
results = solver_manager.solve(
Rota.model, opt=opt, logfile="{}.log".format(rot)
)
else:
print("-Solving with local cbc")
results = opt.solve(
Rota.model,
tee=True,
options={
"seconds": 10200,
"allow": 2000,
},
logfile="{}.log".format(rot),
) # solve the model with the, options="seconds=60" selected solver
results.solver.status
ResultsHolder = RotaResults(Rota, results)
week_table = ResultsHolder.get_work_table() # list with the required workers
# worker_timetable = ResultsHolder.get_worker_timetable()
worker_timetable_brief = ResultsHolder.get_worker_timetable_brief(show_prefs=False)
worker_shift_summary = ResultsHolder.get_shift_summary()
ResultsHolder.export_rota_to_csv("rotas/" + rot)
with open("rotas.html", "a") as f:
f.write("<hr>")
f.write("<h2>{}</h2>".format(rot))
# f.write("----------------------------")
f.write("\n\n")
for s in Rota.shifts:
f.write(
"<div id='{}-{}-shift-summary' class='shift-summary'>{}</div>".format(
rot, s.name, s.get_shift_summary()
)
)
f.write("\n")
f.write("\n\n")
f.write(ResultsHolder.get_worker_timetable_html(table_name=rot))
f.write("\n\n")
f.write(
"<pre class='worker-shift-summary'>{}</pre>".format(worker_shift_summary)
)
f.write("\n\n\n")
# workers_no_pref = ResultsHolder.get_no_preference(Rota.model.no_pref) # list with the non-satisfied workers (work on Sat but not on Sun)
# worker_night_block = ResultsHolder.get_night_blocks()
# multi = ResultsHolder.get_multinight()
# break
with open("rotas.html", "a") as f:
f.write("</body></html>")
+373
View File
@@ -0,0 +1,373 @@
from pyomo.environ import *
from pyomo.opt import SolverFactory
from pyomo.opt.results import SolverStatus
from shifts import SingleShift, RotaBuilder, RotaResults, days
from workers import Worker
import datetime
import sys
import os
use_neos = False
use_cplex = False
solve = True
weeks_to_rota = 26
time_to_run = 108500
# allow = 5000
ratio = 0.001
use_live_rota = False
suspend_on_finish = True
sites = ("truro", "exeter", "torbay", "barnstaple", "plymouth", "proc", "truro no proc")
Rota = RotaBuilder(
# datetime.date(2022, 6, 6),
datetime.date(2022, 3, 7),
# Rota = RotaBuilder(datetime.date(2021, 12, 6),
# Rota = RotaBuilder(datetime.date(2021, 11, 8),
weeks_to_rota=weeks_to_rota,
balance_offset_modifier=1,
max_weekend_frequency=2,
max_night_frequency=2,
use_shift_balance_extra=True,
use_bank_holiday_extra=True,
)
Rota.constraint_options["limit_to_1_st2_on_nights"] = True
Rota.constraint_options["ensure_1_st4_plus_on_nights"] = True
Rota.constraint_options["balance_nights"] = True
Rota.constraint_options["constrain_time_off_after_nights"] = True
Rota.constraint_options["balance_nights_across_sites"] = True
Rota.constraint_options["balance_blocks"] = True
Rota.constraint_options["balance_weekends"] = True
Rota.constraint_options["balance_shifts_over_workers"] = True
Rota.constraint_options["balance_bank_holidays"] = True
Rota.constraint_options["avoid_st2_first_month"] = False
Rota.constraint_options["hard_constrain_pair_separation"] = True
Rota.constraint_options["max_weekends"] = 7
Rota.constraint_options["prevent_monday_and_tuesday_after_full_weekends"] = [
"truro",
"torbay",
"exeter",
"plymouth",
"plymouth no proc",
"truro no proc",
]
Rota.constraint_options["prevent_monday_after_full_weekends"] = [
"plymouth",
"plymouth no proc",
]
Rota.constraint_options["prevent_fridays_before_full_weekends"] = [
"plymouth",
"plymouth no proc",
]
Rota.constraint_options["prevent_thursdays_before_full_weekends"] = [
"truro",
"torbay",
"exeter",
"truro no proc",
]
Rota.constraint_options["require_presence_at_site_overnight"] = ["plymouth"]
Rota.add_shifts(
SingleShift(
("exeter",),
"exeter_twilight",
12.5,
days[:4],
balance_offset=3,
constraints=["max_2_shifts_per_week"],
),
SingleShift(
("truro", "truro no proc"), "truro_twilight", 12.5, days[:4], balance_offset=3
),
SingleShift(
("torbay", "torbay twilights"),
"torbay_twilight",
12.5,
days[:4],
balance_offset=3,
assign_as_block=True,
),
SingleShift(
("plymouth", "plymouth_twilights"),
"plymouth_twilight",
12.5,
days[:5],
balance_offset=3,
workers_required=1,
constraints=["max_2_shifts_per_week"],
),
SingleShift(
("exeter",),
"weekend_exeter",
12.5,
days[4:],
balance_offset=4,
rota_on_nwds=True,
force_as_block=True,
),
SingleShift(
("truro", "truro no proc"),
"weekend_truro",
12.5,
days[4:],
balance_offset=4,
rota_on_nwds=True,
force_as_block=True,
# force_as_block_unless_nwd=True
),
SingleShift(
("torbay",),
"weekend_torbay",
12.5,
days[4:],
balance_offset=4,
rota_on_nwds=True,
force_as_block=True,
# force_as_block_unless_nwd=True
),
SingleShift(
("plymouth",),
"weekend_plymouth1",
8,
days[5:],
balance_offset=4,
workers_required=1,
rota_on_nwds=True,
force_as_block=True,
),
SingleShift(
("plymouth",),
"weekend_plymouth2",
8,
days[5:],
balance_offset=4,
workers_required=1,
rota_on_nwds=True,
force_as_block=True,
),
SingleShift(
("plymouth", "plymouth_twilights"),
"plymouth_bank_holidays",
8,
days[5:],
balance_offset=2,
workers_required=1,
rota_on_nwds=True,
force_as_block=False,
bank_holidays_only=True,
),
SingleShift(
(sites[:-1]),
"night_weekday",
12.25,
days[:4],
balance_offset=4,
balance_weighting=1,
# hard_constrain_shift=False,
workers_required=3,
force_as_block=False,
rota_on_nwds=True,
constraints=["night"],
),
SingleShift(
(sites[:-1]),
"night_weekend",
12.25,
days[4:],
balance_offset=3,
balance_weighting=1,
# hard_constrain_shift=False,
workers_required=3,
force_as_block=False,
rota_on_nwds=True,
constraints=["night"],
),
)
use_test_workers = False
load_leave = True
Rota.build_shifts()
if load_leave:
import leave
workers = leave.load_leave(Rota, use_live_rota)
# Import trainee data
if use_test_workers:
# Rota.add_workers([
# Worker(Rota,
# i,
# "Truro {}".format(i),
# "truro",
# 3,
# nwd=['Fri', "Sat", "Sun"]) for i in range(1, 2)
# ])
Rota.add_workers(
[
Worker(
Rota,
"Truro {}".format(i),
"truro",
4,
id=i,
)
for i in range(1, 9)
]
)
Rota.add_workers(
[Worker(Rota, i, "Plym {}".format(i), "plymouth", 4, 50) for i in range(9, 17)]
)
Rota.add_workers(
[
Worker(
Rota, i, "Plym {}".format(i), "plymouth", 4, 80, end_date="2020/10/05"
)
for i in range(17, 19)
]
)
else:
n = 0
for worker in workers:
n = n + 1
worker_name = worker
w = workers[worker]
site = w["site"]
grade = w["grade"]
fte = w["fte"]
nwd = w["nwd"]
end_date = w["end_date"]
oop = w["oop"]
grade = w["grade"]
pair = w["pair"]
# name, site, grade, fte, nwd, end_date, oop, twi, twi_we, night, night_we, pw1, pw2, extra = row
# Ignore trainees if fte == 0 (what are they doing here anyway)
if fte == 0 or fte == "0" or fte == "":
continue
nwds = None
if nwd:
nwds = []
for i in nwd.split(","):
nwd_start_date = Rota.start_date
nwd_end_date = Rota.rota_end_date
if "[" in i:
a, b = i.split("[")[1][:-1].split("-")
nwd_start_date = datetime.datetime.strptime(a, "%d/%m/%y").date()
nwd_end_date = datetime.datetime.strptime(b, "%d/%m/%y").date()
nwds.append(
(
i.split("[")[0].strip()[:3].capitalize(),
nwd_start_date,
nwd_end_date,
)
)
# nwds = [i.strip()[:3].capitalize() for i in nwd.split("/")] if nwd else None
end_date = end_date if end_date else None
oop = oop.split("-") if oop else None
previous_shifts = {}
# if twi:
# worked = twi.split(" ")[0]
# allocated = twi.split(" ")[1][1:-1]
# previous_shifts["{}_twilight".format(site.lower())] = (worked, allocated)
# if twi_we:
# worked = twi_we.split(" ")[0]
# allocated = twi_we.split(" ")[1][1:-1]
# previous_shifts["weekend_{}".format(site.lower())] = (worked, allocated)
# if night:
# worked = night.split(" ")[0]
# allocated = night.split(" ")[1][1:-1]
# previous_shifts["night_weekday".format(site.lower())] = (worked, allocated)
# if night_we:
# worked = night_we.split(" ")[0]
# allocated = night_we.split(" ")[1][1:-1]
# previous_shifts["night_weekend".format(site.lower())] = (worked, allocated)
# if pw1:
# worked = pw1.split(" ")[0]
# allocated = pw1.split(" ")[1][1:-1]
# previous_shifts["weekend_plymouth1"] = (worked, allocated)
# if pw2:
# worked = pw2.split(" ")[0]
# allocated = pw2.split(" ")[1][1:-1]
# previous_shifts["weekend_plymouth2"] = (worked, allocated)
leave_requests = w["leave"]
work_requests = w["requests"]
site_pref = w["site_pref"]
Rota.add_worker(
Worker(
Rota,
worker_name,
site.lower(),
int(grade[2]),
n,
int(fte),
nwds,
end_date,
oop,
not_available_to_work=leave_requests,
work_requests=work_requests,
home_site=site_pref,
previous_shifts=previous_shifts,
pair=pair,
shift_balance_extra=w["shift_balance_extra"],
bank_holiday_extra=w["bank_holiday_extra"],
)
)
print("Building model")
Rota.build_workers()
Rota.sites.add("plymouth_twilights")
Rota.build_model()
print(Rota.get_worker_details())
if not solve:
ResultsHolder = RotaResults(Rota)
ResultsHolder.export_rota_to_html()
sys.exit(0)
solver_options = {"ratio": ratio, "seconds": time_to_run, "threads": 10}
Rota.solve_model(options=solver_options)
# print(Rota.get_worker_details())
# optimizer = SolverFactory('cbc')
# result = optimizer.solve(prob,tee=True)
# result.Solver.Status = SolverStatus.warning
# prob.solutions.load_from(result)
ResultsHolder = RotaResults(Rota)
worker_timetable_brief = ResultsHolder.get_worker_timetable_brief(
show_prefs=False, show_unavailable=False
)
print(worker_timetable_brief)
ResultsHolder.export_rota_to_html("fixnights")
ResultsHolder.export_rota_to_csv("fixnights")
if suspend_on_finish:
os.system("systemctl suspend")
+2780
View File
File diff suppressed because it is too large Load Diff
+186
View File
@@ -0,0 +1,186 @@
import datetime
from collections import defaultdict
# from shifts import RotaBuilder, days, sites
import uuid
class Worker:
def __init__(
self,
Rota, # Aim to remove (workers should be independent of rotas)
name: str,
site: str,
grade: int,
id=None,
fte: int = 100,
nwd=None,
end_date=None,
oop=None,
not_available_to_work=None,
pref_not_to_work=None,
work_requests=None,
home_site: str = "plymouth", # We set a default proc_site
previous_shifts: dict = {},
shift_balance_extra: dict = {},
bank_holiday_extra: int = 0,
pair: int = 0,
):
# We can either have a user generated ID
if id is not None:
self.id = id
else:
self.id = uuid.uuid4()
self.name = name
self.site = site
# Grade are equivalent to roles, by keeping them integer defining model
# rules is easier.
self.grade = grade
self.fte = fte
self.nwd = nwd
self.proportion_rota_to_work = 1
self.pair = pair
self.shift_balance_extra = shift_balance_extra
self.bank_holiday_extra = bank_holiday_extra
self.home_site = home_site
if home_site == "plymouth" or home_site == "":
self.night_at_derriford = 1
else:
self.night_at_derriford = 0
self.previous_shifts = previous_shifts
self.shift_target_number = defaultdict(int)
days_to_work = Rota.rota_days_length
if end_date is None:
self.end_date = None
else:
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
else:
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"
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()
# ignore oops if they finish before the rota start date
print(end_oop_date, Rota.start_date)
if end_oop_date > Rota.start_date:
if start_oop_date > Rota.rota_end_date:
pass
else:
if end_oop_date > Rota.rota_end_date:
end_oop_date = Rota.rota_end_date
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
]:
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)
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
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
)
if not_available_to_work is not None:
# 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
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
week = days_from_start // 7 + 1
day = Rota.days[(days_from_start % 7)]
Rota.work_requests.add((self.id, week, day, shift))
# Calculate the proportion of the rota that is being worked
self.proportion_rota_to_work = days_to_work / Rota.rota_days_length
self.days_to_work = days_to_work
# 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))
# print(self.name, self.nwd)
def __lt__(self, other) -> bool:
return (self.site, self.grade, self.fte_adj, self.name) < (
other.site,
other.grade,
other.fte_adj,
other.name,
)
def __str__(self) -> str:
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.home_site,
self.night_at_derriford,
)
def get_details(self) -> str:
return "{} {} {}".format(self.id, self.site[0], self.grade)
def get_full_details(self) -> str:
return "{}/{}/{}".format(self.site, self.grade, self.name)
def get_shift_targets(self) -> dict:
return self.shift_target_number