Start rewriting nights

This commit is contained in:
Ross
2020-05-09 23:48:46 +01:00
parent 1b4cca40f8
commit 0ac1990cfd
7 changed files with 1358 additions and 2616 deletions
+2
View File
@@ -1,3 +1,5 @@
rota.csv
trainees.csv
sample.txt
*.csv
*.html
+114
View File
@@ -0,0 +1,114 @@
# The default ``config.py``
# flake8: noqa
def set_prefs(prefs):
"""This function is called before opening the project"""
# Specify which files and folders to ignore in the project.
# Changes to ignored resources are not added to the history and
# VCSs. Also they are not returned in `Project.get_files()`.
# Note that ``?`` and ``*`` match all characters but slashes.
# '*.pyc': matches 'test.pyc' and 'pkg/test.pyc'
# 'mod*.pyc': matches 'test/mod1.pyc' but not 'mod/1.pyc'
# '.svn': matches 'pkg/.svn' and all of its children
# 'build/*.o': matches 'build/lib.o' but not 'build/sub/lib.o'
# 'build//*.o': matches 'build/lib.o' and 'build/sub/lib.o'
prefs['ignored_resources'] = ['*.pyc', '*~', '.ropeproject',
'.hg', '.svn', '_svn', '.git', '.tox']
# Specifies which files should be considered python files. It is
# useful when you have scripts inside your project. Only files
# ending with ``.py`` are considered to be python files by
# default.
# prefs['python_files'] = ['*.py']
# Custom source folders: By default rope searches the project
# for finding source folders (folders that should be searched
# for finding modules). You can add paths to that list. Note
# that rope guesses project source folders correctly most of the
# time; use this if you have any problems.
# The folders should be relative to project root and use '/' for
# separating folders regardless of the platform rope is running on.
# 'src/my_source_folder' for instance.
# prefs.add('source_folders', 'src')
# You can extend python path for looking up modules
# prefs.add('python_path', '~/python/')
# Should rope save object information or not.
prefs['save_objectdb'] = True
prefs['compress_objectdb'] = False
# If `True`, rope analyzes each module when it is being saved.
prefs['automatic_soa'] = True
# The depth of calls to follow in static object analysis
prefs['soa_followed_calls'] = 0
# If `False` when running modules or unit tests "dynamic object
# analysis" is turned off. This makes them much faster.
prefs['perform_doa'] = True
# Rope can check the validity of its object DB when running.
prefs['validate_objectdb'] = True
# How many undos to hold?
prefs['max_history_items'] = 32
# Shows whether to save history across sessions.
prefs['save_history'] = True
prefs['compress_history'] = False
# Set the number spaces used for indenting. According to
# :PEP:`8`, it is best to use 4 spaces. Since most of rope's
# unit-tests use 4 spaces it is more reliable, too.
prefs['indent_size'] = 4
# Builtin and c-extension modules that are allowed to be imported
# and inspected by rope.
prefs['extension_modules'] = []
# Add all standard c-extensions to extension_modules list.
prefs['import_dynload_stdmods'] = True
# If `True` modules with syntax errors are considered to be empty.
# The default value is `False`; When `False` syntax errors raise
# `rope.base.exceptions.ModuleSyntaxError` exception.
prefs['ignore_syntax_errors'] = False
# If `True`, rope ignores unresolvable imports. Otherwise, they
# appear in the importing namespace.
prefs['ignore_bad_imports'] = False
# If `True`, rope will insert new module imports as
# `from <package> import <module>` by default.
prefs['prefer_module_from_imports'] = False
# If `True`, rope will transform a comma list of imports into
# multiple separate import statements when organizing
# imports.
prefs['split_imports'] = False
# If `True`, rope will remove all top-level import statements and
# reinsert them at the top of the module when making changes.
prefs['pull_imports_to_top'] = True
# If `True`, rope will sort imports alphabetically by module name instead
# of alphabetically by import statement, with from imports after normal
# imports.
prefs['sort_imports_alphabetically'] = False
# Location of implementation of
# rope.base.oi.type_hinting.interfaces.ITypeHintingFactory In general
# case, you don't have to change this value, unless you're an rope expert.
# Change this value to inject you own implementations of interfaces
# listed in module rope.base.oi.type_hinting.providers.interfaces
# For example, you can add you own providers for Django Models, or disable
# the search type-hinting in a class hierarchy, etc.
prefs['type_hinting_factory'] = (
'rope.base.oi.type_hinting.factory.default_type_hinting_factory')
def project_opened(project):
"""This function is called after opening the project"""
# Do whatever you like here!
+126 -2316
View File
File diff suppressed because one or more lines are too long
+294
View File
@@ -0,0 +1,294 @@
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
weeks_to_rota = 26
sites = ("truro", "exeter", "torquay", "barnstaple", "plymouth")
Rota = RotaBuilder(datetime.date(2020, 9, 7),
weeks_to_rota=weeks_to_rota,
balance_offset_modifier=1)
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 twighlights + 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", 12.5, days, balance_weighting=1, workers_required=3, rota_on_nwds=True),
SingleShift((sites),
"night",
12.25,
days,
balance_offset=2,
balance_weighting=1,
workers_required=3,
rota_on_nwds=True),
)
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",
13,
days,
balance_offset=2,
balance_weighting=1,
workers_required=3,
rota_on_nwds=True), )
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",
13,
days,
balance_offset=2,
balance_weighting=1,
workers_required=3,
rota_on_nwds=True),
)
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",
13,
days,
balance_offset=2,
balance_weighting=1,
workers_required=3,
rota_on_nwds=True),
)
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 twighlights + 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",
12.25,
days,
balance_offset=2,
balance_weighting=1,
workers_required=3,
rota_on_nwds=True),
)
rota_collections['nights + proc twighlights + proc weekends'].add_shifts(
SingleShift((sites),
"proc_twilight_and_weekends",
12.5,
days,
workers_required=3),
#SingleShift((sites), "night", 12.5, days, balance_weighting=1, workers_required=3, rota_on_nwds=True),
SingleShift((sites),
"night",
12.25,
days,
balance_offset=2,
balance_weighting=1,
workers_required=3,
rota_on_nwds=True),
)
# 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(",").title() if nwd else None
end_date = end_date if end_date else None
oop = oop.split("-") if oop else None
#print(nwds, end_date, oop)
for rot in rota_collections:
rota_collections[rot].add_worker(
Worker(Rota, n, name, site.lower(), int(grade[2]), int(fte),
nwds, end_date, oop))
with open("rotas.html", "w") as f:
f.write("<html><body>")
for rot in rota_collections:
print("Running rota: ", rot)
Rota = rota_collections[rot]
Rota.build_shifts_and_workers()
Rota.build_model()
opt = SolverFactory('cbc') # choose a solver
#opt = SolverFactory('ipopt') # choose a solver
results = opt.solve(
Rota.model, tee=True, options={
"seconds": 1200,
"allow": 3500
}) # 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("----------------------------")
f.write("<h2>{}</h2>".format(rot))
f.write("----------------------------")
f.write("\n\n")
for s in Rota.shifts:
f.write("<pre>{}</pre>".format(s.get_shift_summary()))
f.write("\n")
f.write("\n\n")
f.write("<pre>{}</pre>".format(worker_timetable_brief))
f.write("\n\n")
f.write("<pre>{}</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>")
+589 -292
View File
File diff suppressed because it is too large Load Diff
+198
View File
@@ -0,0 +1,198 @@
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
weeks_to_rota = 16
sites = ("truro", "exeter", "torquay", "barnstaple", "plymouth")
Rota = RotaBuilder(datetime.date(2020, 9, 7),
weeks_to_rota=weeks_to_rota,
balance_offset_modifier=1)
Rota.add_shifts(
SingleShift(("exeter", ), "exeter_twilight", 12.5, days[:5]),
SingleShift(("truro", ),
"truro_twilight",
12.5,
days[:5],
assign_as_block=False),
#SingleShift(("torquay", ), "torquay_twilight", 12.5, days[:5]),
#SingleShift(("plymouth", ), "plymouth_twilight", 12.5, days[:5]),
SingleShift(("exeter", ),
"weekend_exeter",
12.5,
days[5:],
force_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.25,
days,
balance_offset=2,
balance_weighting=1,
workers_required=3,
rota_on_nwds=True,
constraints=["night"]),
)
use_test_workers = True
# 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,
i,
"Truro {}".format(i),
"truro",
4,
) for i in range(2, 14)
])
# Rota.add_workers([
# Worker(Rota, i, "Plym {}".format(i), "plymouth", 4)
# for i in range(16, 30)
# ])
Rota.add_workers([
Worker(
Rota,
i,
"Exe {}".format(i),
"exeter",
4,
) for i in range(30, 40)
])
Rota.add_workers([
Worker(Rota,
i,
"Torq {}".format(i),
"torquay",
2,
pref_not_to_work=(("2020/09/07"), )) for i in range(50, 60)
])
else:
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(",").title() if nwd else None
end_date = end_date if end_date else None
oop = oop.split("-") if oop else None
#print(nwds, end_date, oop)
Rota.add_worker(
Worker(Rota, n, name, site.lower(), int(grade[2]), int(fte),
nwds, end_date, oop))
print(0)
Rota.build_shifts_and_workers()
Rota.build_model()
print(1)
opt = SolverFactory('cbc') # choose a solver
#opt = SolverFactory('ipopt') # choose a solver
print(2)
results = opt.solve(
Rota.model, tee=True, options={
"seconds": 1200,
"allow": 70000
}) # solve the model with the, options="seconds=60" selected solver
print(results)
results.solver.status
if not results.solver.status:
sys.exit(0)
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)
print(worker_timetable_brief)
# for i in ResultsHolder.rota.model.blocks_worker_shift_count:
# n = ResultsHolder.rota.model.blocks_worker_shift_count[i].value
# if n > 0:
# print(i, n)
# #print(ResultsHolder.rota.model.blocks_assigned[i].value)
# else:
# pass
# #print("SHIT")
# for i in ResultsHolder.rota.model.blocks_assigned:
# n = ResultsHolder.rota.model.blocks_assigned[i].value
# if n:
# print(i, n)
# #print(ResultsHolder.rota.model.blocks_assigned[i].value)
# else:
# pass
# ResultsHolder.export_rota_to_csv("rotas/"+rot)
# with open("cest", "a") as f:
# f.write(rot)
# f.write("\n\n")
# for s in Rota.shifts:
# f.write(s.get_shift_summary())
# f.write("\n")
# f.write("\n\n")
# f.write(worker_timetable_brief)
# 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()
# worker_shift_summary = ResultsHolder.get_shift_summary()
# multi = ResultsHolder.get_multinight()
#break
+35 -8
View File
@@ -5,12 +5,22 @@ from collections import defaultdict
class Worker:
def __init__(
self, Rota, id, name, site, grade, fte=100, nwd=None, end_date=None, oop=None
):
def __init__(self,
Rota,
id,
name,
site,
grade,
fte=100,
nwd=None,
end_date=None,
oop=None,
pref_not_to_work=None):
self.id = id
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
@@ -23,7 +33,8 @@ class Worker:
if end_date is None:
self.end_date = None
else:
self.end_date = datetime.datetime.strptime(end_date, "%Y/%m/%d").date()
self.end_date = datetime.datetime.strptime(end_date,
"%Y/%m/%d").date()
if self.end_date > Rota.rota_end_date:
self.end_date = Rota.rota_end_date
@@ -37,8 +48,10 @@ class Worker:
if oop is not None:
start_oop, end_oop = oop
start_oop_date = datetime.datetime.strptime(start_oop, "%Y/%m/%d").date()
end_oop_date = datetime.datetime.strptime(end_oop, "%Y/%m/%d").date()
start_oop_date = datetime.datetime.strptime(start_oop,
"%Y/%m/%d").date()
end_oop_date = datetime.datetime.strptime(end_oop,
"%Y/%m/%d").date()
if end_oop_date > Rota.rota_end_date:
end_oop_date = Rota.rota_end_date
@@ -49,11 +62,22 @@ class Worker:
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))
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, "%Y/%m/%d").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)
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
@@ -68,3 +92,6 @@ class Worker:
def get_details(self):
return "{} {} {}".format(self.id, self.site[0], self.grade)
def get_full_details(self):
return "{}/{}/{}".format(self.site, self.grade, self.name)