Files
proc-rota/rota.ipynb
T
2020-03-31 23:17:13 +01:00

1054 lines
64 KiB
Plaintext

{
"nbformat": 4,
"nbformat_minor": 2,
"metadata": {
"language_info": {
"name": "python",
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"version": "3.7.5-final"
},
"orig_nbformat": 2,
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"npconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": 3,
"kernelspec": {
"name": "python37564bit5ba0d1a6f89f489d9799b0f9b976a56e",
"display_name": "Python 3.7.5 64-bit"
}
},
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from pyomo.environ import *\n",
"from pyomo.opt import SolverFactory\n",
"from shifts import SingleShift, ShiftCollection, days, sites\n",
"from workers import Worker\n",
"\n",
"import datetime\n",
"import itertools"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"night_balance_offset = 2\n",
"twilight_balance_offset = 1\n",
"weeks_to_rota = 8\n",
"use_test_workers = False"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"\n",
"\n",
"#weeks = [i for i in range(1,weeks_to_rota+1)]\n",
"\n",
"#weeks_days_product = list(itertools.product(weeks, days))\n",
"\n",
"Shifts = ShiftCollection(weeks_to_rota=weeks_to_rota)\n",
"\n",
"Shifts.add_shifts(\n",
" SingleShift((\"exeter\",), \"exeter_twilight\", 12.5, days[:5]),\n",
" SingleShift((\"truro\",), \"truro_twilight\", 12.5, days[:5]),\n",
" SingleShift((\"torquay\",), \"torquay_twilight\", 12.5, days[:5]),\n",
" SingleShift((\"plymouth\",), \"plymouth_twilight\", 12.5, days[:5]), \n",
" SingleShift((\"exeter\",), \"weekend_exeter\", 12.5, days[5:]), \n",
" SingleShift((\"truro\",), \"weekend_truro\", 12.5, days[5:]), \n",
" SingleShift((\"torquay\",), \"weekend_torquay\", 12.5, days[5:]), \n",
" SingleShift((\"plymouth\",), \"weekend_plymouth\", 8, days[5:], workers_required=2), \n",
" SingleShift((sites), \"night\", 12.5, days, balance_by_site=False, workers_required=3, rota_on_nwds=True), \n",
")\n",
"\n",
"# Enter shifts of each day\n",
"#shifts = ['morning', 'evening', 'night', 'truro_twilight'] # 3 shifts of 8 hours\n",
"#shifts = ['night', 'truro_twilight', 'plymouth_twilight'] # 3 shifts of 8 hours\n",
"\n",
"# Generate\n",
"\n",
"#site_and_shifts_to_balance = ((\"truro\", \"truro_twilight\"),\n",
"# (\"plymouth\", \"plymouth_twilight\"))\n",
"\n",
"#shift_len = [12.5, 12.5, 12.5]\n",
"\n",
"night_blocks = [\"weekday\", \"weekend\", \"none\"]\n",
"\n",
"#shift_lengths = {}\n",
"#for shift in shifts:\n",
"# shift_\n",
"#days_shifts\n",
"#lengths[shift.shift] = shift.length\n",
"\n",
"\n",
"#days_shifts = {day: shifts.shifts for day in days} # dict with day as key and list of its shifts as value\n",
"\n",
"#start_date = datetime.date(2020,9,7)\n",
"#rota_days_length = len(weeks) * 7\n",
"#rota_end_date = start_date + datetime.timedelta(rota_days_length)\n",
"\n",
"unavailable_to_work = set()\n",
"\n",
"\n",
"#Shifts.GetAllDayShiftsAsClass()\n",
"#unavailable_to_work.update([(\"W1\", 1, \"Mon\"),(\"W1\", 1, \"Fri\"),(\"W1\", 1, \"Thu\"),(\"W3\", 1, \"Mon\"),(\"W2\", 1, \"Mon\"),(\"W2\", 1, \"Tue\"),(\"W2\", 1, \"Wed\"),(\"W6\", 1, \"Thu\"),(\"W5\", 1, \"Fri\"),(\"W9\", 1, \"Fri\"),(\"W14\", 1, \"Fri\"),(\"W21\", 1, \"Fri\")])\n",
"\n",
"#for i in range(11,30):"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "['Wijhdan Abusrewil', 'Exeter', 'ST2', '100', '', '', '', '']\n['Seren Peters', 'Plymouth', 'ST2', '100', '', '', '', '']\n['Hannah Lewis', 'Plymouth', 'ST2', '100', '', '', '', '']\n['Christian Greer', 'Torquay', 'ST2', '100', '', '', '', '']\n['Nicolas Dziad', 'Truro', 'ST2', '100', '', '', '', '']\n['Michal Tomek', 'Exeter', 'ST2', '100', '', '', '', '']\n['Alex Wood', 'Exeter', 'ST2', '100', '', '', '', '']\n['Alexander Sanchez- Cabello', 'Plymouth', 'ST2', '100', '', '', '', '']\n['Max Ireland', 'Torquay', 'ST2', '100', '', '', '', '']\n['Vilim Kalamar', 'Truro', 'ST2', '100', '', '', '', '']\n['Salma Aslam', 'Exeter', 'ST3', '0', '', '', '', 'IDT']\n['Adeola Omotade', 'Plymouth', 'ST3', '0', '', '', '', 'IDT']\n['Amina Odeh', 'Plymouth', 'ST3', '100', '', '', '', '']\n['Charles Finan', 'Torquay', 'ST3', '100', '', '', '', '']\n['Alan Eccles', 'Torquay', 'ST3', '80', '', '', '', '']\n['Sarath Vennam', 'Truro', 'ST3', '100', '', '', '', '']\n['Paul Ward', 'Truro', 'ST3', '100', '', '', '', '']\n['Sayed Alqarooni', 'Exeter', 'ST3', '100', '', '', '', '']\n['Jean Sukumar', 'Plymouth', 'ST3', '60', '', '', '', '']\n['Jenn Fong', 'Plymouth', 'ST3', '100', '', '', '', '']\n['Chong Ng', 'Torquay', 'ST3', '100', '', '', '', '']\n['Matthew Thorley', 'Torquay', 'ST3', '100', '', '', '', '']\n['Jenna Millington', 'Truro', 'ST3', '60', '', '', '', '']\n['Chi Leung', 'Truro', 'ST3', '100', '', '', '', '']\n['Tom Welsh', 'Exeter', 'ST4', '100', '', '', '', '']\n['Rebecca Murphy', 'Exeter', 'ST4', '100', '', '', '', '']\n['Delilah Trimmer', 'Exeter', 'ST4', '50', '', '', '', 'Back Nov 20']\n['Sophie McGlade', 'Plymouth', 'ST4', '100', '', '', '', '']\n['S Ghauri', 'Plymouth', 'ST4', '100', '', '', '', '']\n['Khalil Madbak', 'Plymouth', 'ST4', '100', '', '', '', '']\n['Natasha Hougham', 'Plymouth', 'ST4', '60', '', '', '', '']\n['Richard Chaytor', 'Plymouth', 'ST4', '100', '', '', '', '']\n['Safi Rahman', 'Plymouth', 'ST4', '0', '', '', '', 'IDT']\n['George Edwards', 'Plymouth', 'ST4', '100', '', '', '', '']\n['Neil McIntyre', 'Torquay', 'ST4', '100', '', '', '', '']\n['Mo Babsail', 'Torquay', 'ST4', '100', '', '', '', '']\n['Paul Jenkins', 'Torquay', 'ST4', '100', '', '', '', '']\n['Ross Kruger', 'Truro', 'ST4', '100', '', '', '', '']\n['Amoolya Mannava', 'Truro', 'ST4', '100', '', '', '', '']\n['Tom Davies', 'Exeter', 'ST5', '42', '', '', '2020/09/01-2021/04/12', '']\n"
},
{
"output_type": "error",
"ename": "NameError",
"evalue": "name 'unavailable_to_work' is not defined",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m<ipython-input-4-f616e02611f2>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m 30\u001b[0m \u001b[0moop\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0moop\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msplit\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"-\"\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0moop\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 31\u001b[0m \u001b[0;31m#print(nwds, end_date, oop)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 32\u001b[0;31m \u001b[0mworkers\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mWorker\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mShifts\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mn\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mname\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msite\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlower\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mgrade\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfte\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnwds\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mend_date\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0moop\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 33\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 34\u001b[0m \u001b[0mworkers\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msorted\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mworkers\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;32m~/scripts/rota/workers.py\u001b[0m in \u001b[0;36m__init__\u001b[0;34m(self, Shifts, id, name, site, grade, fte, nwd, end_date, oop)\u001b[0m\n\u001b[1;32m 46\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mweeks_days\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mShifts\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mweeks_days_product\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mdays_until_oop\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0mdays_until_oop\u001b[0m\u001b[0;34m+\u001b[0m\u001b[0moop_length\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 47\u001b[0m \u001b[0mweek\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mday\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mweeks_days\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 48\u001b[0;31m \u001b[0munavailable_to_work\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mid\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mweek\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mday\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 49\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 50\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;31mNameError\u001b[0m: name 'unavailable_to_work' is not defined"
]
}
],
"source": [
"workers = []\n",
"\n",
"if use_test_workers:\n",
" # Enter workers ids (name, number, ...)\n",
" workers.extend([Worker(Shifts, i, \"Test Person {}\".format(i), \"truro\", 2, nwd=['Fri', \"Sat\", \"Sun\"]) for i in range(1, 2)])\n",
" workers.extend([Worker(Shifts,i, \"Test Person {}\".format(i), \"truro\", 2, nwd=[\"Sat\", \"Sun\"]) for i in range(2, 10)])\n",
" workers.extend([Worker(Shifts,i, \"Test Person {}\".format(i), \"plymouth\", 4) for i in range(10, 20)])\n",
" workers.extend([Worker(Shifts,i, \"Test Person {}\".format(i), \"exeter\", 4) for i in range(20, 30)])\n",
" #workers.extend([workerClass(i, \"Test Person {}\".format(i), 4, 200, \"\") for i in range(48, 49)])\n",
"else:\n",
" # Import trainee data\n",
" import csv\n",
"\n",
" with open('trainees.csv', newline='') as f:\n",
" reader = csv.reader(f, delimiter=',', quotechar='|')\n",
" next(reader) # skip header\n",
"\n",
" n = 0\n",
" for row in reader:\n",
" n = n + 1\n",
" print(row)\n",
" name, site, grade, fte, nwd, end_date, oop, extra = row\n",
"\n",
" # Ignore trainees if fte == 0 (what are they doing here anyway)\n",
" if fte == 0:\n",
" continue\n",
"\n",
" nwds = nwd.split(\",\").title() if nwd else None\n",
" end_date = end_date if end_date else None\n",
" oop = oop.split(\"-\") if oop else None\n",
" #print(nwds, end_date, oop)\n",
" workers.append(Worker(Shifts, n, name, site.lower(), int(grade[2]), int(fte), nwds, end_date, oop))\n",
"\n",
"workers = sorted(workers)\n",
""
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"full_time_equivalent = sum([w.fte_adj for w in workers])\n",
"full_time_equivalent_sites = {}\n",
"\n",
"for site in sites:\n",
" full_time_equivalent_sites[site] = sum([w.fte_adj for w in workers if w.site == site])\n",
""
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"#Shifts.GetRequiredShiftsAndWorkers()\n",
"#Shifts.GetAllShiftsAsNames()\n",
"#Shifts.GetNotRequiredShifts()"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"tags": [
"outputPrepend",
"outputPrepend",
"outputPrepend"
]
},
"outputs": [],
"source": [
"# Initialize model\n",
"model = ConcreteModel()\n",
"\n",
"# binary variables representing if a worker is scheduled somewhere\n",
"# NOTE: this will assign to all possible shift combinations (which we probably don't want)\n",
"model.works = Var(((worker.id, week, day, shift) for worker in workers for week, day in Shifts.GetAllWeeksDays() for shift in Shifts.GetShiftNames()),\n",
" within=Binary, initialize=0)\n",
"\n",
"# The nights model is used to ensure nights are assigned as a block (and limit the number of workers required)\n",
"model.nights = Var(((worker.id, week, block) for worker in workers for week in Shifts.weeks for block in night_blocks),\n",
" within=Binary, initialize=0)\n",
"\n",
"#model.unavailable = Var(((worker, week, day) for worker in workers for week in weeks for day in days),\n",
"# within=Binary, initialize=0)\n",
"\n",
"def availability_init(model, wid, week, day):\n",
" if (wid, week, day) in unavailable_to_work:\n",
" #print((wid, week, day))\n",
" return 0\n",
" return 1\n",
"model.available = Param(((worker.id, week, day) for worker in workers for week, day in Shifts.GetAllWeeksDays()), initialize=availability_init) \n",
"\n",
"# binary variables representing if a worker is necessary\n",
"model.needed = Var([worker.id for worker in workers], within=Binary, initialize=0)\n",
"\n",
"# binary variables representing if a worker worked on sunday but not on saturday (avoid if possible)\n",
"model.no_pref = Var([worker.id for worker in workers], within=Binary, initialize=0)"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
"# Define an objective function with model as input, to pass later\n",
"def obj_rule(m):\n",
" c = len(workers)\n",
" return 1\n",
" #return sum(m.no_pref[worker.id] for worker in workers)\n",
" #return sum(m.no_pref[worker] for worker in workers) + sum(c * m.needed[worker] for worker in workers)\n",
"# we multiply the second term by a constant to make sure that it is the primary objective\n",
"# since sum(m.no_prefer) is at most len(workers), len(workers) + 1 is a valid constant.\n",
"\n",
"\n",
"# add objective function to the model. rule (pass function) or expr (pass expression directly)\n",
"model.obj = Objective(rule=obj_rule, sense=minimize)"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [],
"source": [
"#[(worker.id, worker.fte_adj, ss) for ss in site_and_shifts for worker in workers if worker.site == ss[0]]\n",
"#[(worker.id, worker.fte_adj, shift.shift) for shift in shifts for worker in workers if worker.site == shift.site and shift.balance_by_site]\n",
"#[1, week, day, shift.name] for week, day, shift in Shifts.GetAllShiftsAsClass()]\n",
"#hifts.GetAllShiftsAsClass()\n",
"#[[1, week, day, shift.name] for week, day, shift in Shifts.GetAllShiftsAsClass() if (shift.rota_on_nwds or day not in nwds)]\n",
"#[(week, day, shift) for week, day in Shifts.GetAllWeeksDays() for shift in Shifts.GetShiftNames()]"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [],
"source": [
"model.constraints = ConstraintList() # Create a set of constraints\n",
"\n",
"for week, day, shift, workers_required, site_required in Shifts.GetRequiredShiftsWorkersAndSites():\n",
" #print(week, day, shift, workers_required, site_required)\n",
" model.constraints.add(\n",
" workers_required == sum(model.works[worker.id, week, day, shift] for worker in workers if worker.site in site_required)\n",
" )\n",
"\n",
"for week, day, shift in Shifts.GetNotRequiredShifts():\n",
" model.constraints.add(\n",
" 0 == sum(model.works[worker.id, week, day, shift.name] for worker in workers)\n",
" )"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {
"tags": [
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend"
]
},
"outputs": [],
"source": [
"\n",
"\n",
"# Constraint: all shifts are assigned\n",
"#for week, day, shift in Shifts.GetAllShiftsAsNames():\n",
"# # 3 people per night\n",
"# if shift in ['night']:\n",
"# model.constraints.add( # to add a constraint to model.constraints set\n",
"# 3 == sum(model.works[worker.id, week, day, shift] for worker in workers)\n",
"# )\n",
"# elif day in days[:5] and shift in ['truro_twilight']: \n",
"# model.constraints.add( # to add a constraint to model.constraints set\n",
"# 1 == sum(model.works[worker.id, week, day, shift] for worker in workers if worker.site == \"truro\")\n",
"# )\n",
"# elif day in days[:5] and shift in ['plymouth_twilight']: \n",
"# model.constraints.add( # to add a constraint to model.constraints set\n",
"# 1 == sum(model.works[worker.id, week, day, shift] for worker in workers if worker.site == \"plymouth\")\n",
"# ) \n",
"# # make sure twilights are not assigned on a weekend\n",
"# elif day in days[5:] and shift in ['truro_twilight', 'plymouth_twilight']:\n",
"# model.constraints.add( # to add a constraint to model.constraints set\n",
"# 0 == sum(model.works[worker.id, week, day, shift] for worker in workers)\n",
"# )\n",
" \n",
" #elif day in days[:-1] and shift in ['truro_twilight', 'plymouth_twilight']:\n",
" # # weekdays' and Saturdays' day shifts have exactly two workers\n",
" # model.constraints.add( # to add a constraint to model.constraints set\n",
" # 1 == sum(model.works[worker.id, week, day, shift] for worker in workers)\n",
" # )\n",
" #else:\n",
" # # Sunday shifts have exactly one worker\n",
" # model.constraints.add(\n",
" # 1 == sum(model.works[worker.id, week, day, shift] for worker in workers)\n",
" # )\n",
"\n",
"# Constraint: total hours worked hours worked\n",
"#for worker in workers:\n",
"# model.constraints.add(\n",
"# 1200 >= sum(shift_lengths[shift] * model.works[worker, week, day, shift] for week in weeks for day in days for shift in days_shifts[day])\n",
"# )\n",
"\n",
"#for week in weeks:\n",
"# for worker in workers:\n",
"# model.constraints.add(\n",
"# 48 >= sum(shift_lengths[shift] * model.works[worker, week, day, shift] for day in days for shift in days_shifts[day])\n",
"# )\n",
"\n",
"def maxHoursPerWeekRule(model, wid, week):\n",
" return(72 >= sum(shift.length * model.works[wid, week, day, shift.name] for day, shift in Shifts.GetAllDayShiftsAsClass()))\n",
"model.max_hours_per_week_constraint = Constraint([worker.id for worker in workers], Shifts.weeks, rule=maxHoursPerWeekRule)\n",
"\n",
"def maxHoursRule(model, wid):\n",
" return(8000 >= sum(shift.length * model.works[wid, week, day, shift.name] for week, day, shift in Shifts.GetAllShiftsAsClass()))\n",
"model.max_hours_constraint = Constraint([worker.id for worker in workers], rule=maxHoursRule)\n",
"\n",
"# Shifts should not be allocated on nwds (excepting night shifts)\n",
"def nwdRule(model, worker):\n",
" print(worker, worker.id, worker.nwd)\n",
" wid = worker.id\n",
" nwd = worker.nwd\n",
" return(0 == sum(model.works[wid, week, day, shift.name] for week, day, shift in Shifts.GetAllShiftsAsClass() if (not shift.rota_on_nwds and day in nwd)))\n",
"\n",
"#model.nwd_constraint = Constraint([(worker) for worker in workers if worker.nwd], rule=nwdRule)\n",
"\n",
"for worker in workers:\n",
" if worker.nwd:\n",
" for week, day, shift in Shifts.GetAllShiftsAsClass():\n",
" if not shift.rota_on_nwds and day in worker.nwd:\n",
" model.constraints.add(\n",
" 0 == model.works[worker.id, week, day, shift.name]\n",
" )\n",
"\n",
"# set a rule to even night shifts (could also be done in blocks, which may be quicker?)\n",
"# takes into accound both LTFT, OOP and leaving the training scheme\n",
"total_night_shifts = Shifts.rota_days_length * Shifts.GetShiftByName(\"night\").workers_required\n",
"\n",
"def nightShiftNumberRule(model, wid, fte_adj):\n",
" \n",
" max_shifts = total_night_shifts / full_time_equivalent * fte_adj + night_balance_offset\n",
" min_shifts = total_night_shifts / full_time_equivalent * fte_adj - night_balance_offset\n",
"\n",
" return inequality(min_shifts, sum(model.works[wid, week, day, \"night\"] for week, day in Shifts.GetAllWeeksDays()), max_shifts)\n",
"model.night_shifts_constraint = Constraint([(worker.id, worker.fte_adj) for worker in workers], rule=nightShiftNumberRule)\n",
"\n",
"\n",
"twilight_constraints = {}\n",
"\n",
"# Set a rule to even twilight shifts:\n",
"#total_twilight_shifts = len(weeks) * 5 * 1 # One person per site\n",
"\n",
"#def trurotwilightShiftNumberRule(model, wid, fte_adj, site, shift):\n",
"# #site, shift = site_and_shift\n",
"# max_shifts = total_twilight_shifts / full_time_equivalent_sites[site] * fte_adj + twilight_balance_offset\n",
"# min_shifts = total_twilight_shifts / full_time_equivalent_sites[site] * fte_adj - twilight_balance_offset\n",
" \n",
"# return inequality(min_shifts, sum(model.works[wid, week, day, shift.GetShift()] for week in weeks for day in days), max_shifts)\n",
"#model.twilight_shifts_truro_constraint = Constraint([(worker.id, worker.fte_adj, ss[0], ss[1]) for ss in site_and_shifts for worker in workers if worker.site == ss[0]], rule=trurotwilightShiftNumberRule)\n",
"\n",
"\n",
"\n",
"#def plymouthtwilightShiftNumberRule(model, wid, fte_adj):\n",
"# max_shifts = total_twilight_shifts / full_time_equivalent_sites[\"plymouth\"] * fte_adj + twilight_balance_offset\n",
"# min_shifts = total_twilight_shifts / full_time_equivalent_sites[\"plymouth\"] * fte_adj - twilight_balance_offset\n",
"# \n",
"# return inequality(min_shifts, sum(model.works[wid, week, day, \"plymouth_twilight\"] for week in weeks for day in days), max_shifts)\n",
"#model.twilight_shifts_plymouth_constraint = Constraint([(worker.id, worker.fte_adj) for worker in workers if worker.site == \"plymouth\"], #rule=plymouthtwilightShiftNumberRule)\n",
"\n",
"\n",
"# Limit to 1 ST2 (or below) on a night shift\n",
"def nightShiftMaxSTRule(model, week, block):\n",
" \"\"\"Limits to 1 ST2 (or below) per night\"\"\"\n",
" single_workers = [w for w in workers if w.grade < 3]\n",
" if not single_workers:\n",
" return Constraint.Skip\n",
" return (sum(model.nights[w.id, week, block] for w in single_workers) <= 1)\n",
"model.night_shifts_max_st_constraint = Constraint([week for week in Shifts.weeks], [block for block in [\"weekday\", \"weekend\"]], rule=nightShiftMaxSTRule)\n",
"\n",
"# Enusre at least 1 ST4+ on a night shift\n",
"def nightShiftMinST4Rule(model, week, block):\n",
" single_workers = [w for w in workers if w.grade > 3]\n",
" if not single_workers:\n",
" return Constraint.Skip\n",
" return (sum(model.nights[w.id, week, block] for w in single_workers) >= 1)\n",
"model.night_shifts_min_st4_constraint = Constraint([week for week in Shifts.weeks], [block for block in [\"weekday\", \"weekend\"]], rule=nightShiftMinST4Rule)\n",
"\n",
" \n",
"# Constraint (def of model.needed)\n",
"#for worker in workers:\n",
"# model.constraints.add(\n",
"# 10000 * model.needed[worker.id] >= sum(model.works[worker.id, week, day, shift] for week in weeks for day in days for shift in days_shifts[day])\n",
"# ) # if any model.works[worker, ·, ·] non-zero, model.needed[worker] must be one; else is zero to reduce the obj function\n",
" # 10000 is to remark, but 5 was enough since max of 40 hours yields max of 5 shifts, the maximum possible sum\n",
"\n",
"# Constraint (def of model.no_pref)\n",
"#for worker in workers:\n",
"# for week in weeks:\n",
"# model.constraints.add(\n",
"# model.no_pref[worker.id] >= sum(model.works[worker.id, week, 'Sat', shift] for shift in days_shifts['Sat'])\n",
"# - sum(model.works[worker.id, week, 'Sun', shift] for shift in days_shifts['Sun'])\n",
"# ) # if not working on sunday but working saturday model.needed must be 1; else will be zero to reduce the obj function\n",
""
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "4.076923076923077 2.076923076923077\n2.230769230769231 0.23076923076923084\n2.7857142857142856 0.7857142857142856\n2.428571428571429 0.4285714285714286\n2.7857142857142856 0.7857142857142856\n2.428571428571429 0.4285714285714286\n3.272727272727273 1.272727272727273\n1.9090909090909092 -0.09090909090909094\n3.631578947368421 1.6315789473684208\n2.052631578947368 0.05263157894736836\n4.076923076923077 2.076923076923077\n2.230769230769231 0.23076923076923084\n4.076923076923077 2.076923076923077\n2.230769230769231 0.23076923076923084\n2.7857142857142856 0.7857142857142856\n2.428571428571429 0.4285714285714286\n3.272727272727273 1.272727272727273\n1.9090909090909092 -0.09090909090909094\n3.631578947368421 1.6315789473684208\n2.052631578947368 0.05263157894736836\n1.0 -1.0\n1.0 -1.0\n1.0 -1.0\n1.0 -1.0\n2.7857142857142856 0.7857142857142856\n2.428571428571429 0.4285714285714286\n3.272727272727273 1.272727272727273\n1.9090909090909092 -0.09090909090909094\n2.8181818181818183 0.8181818181818183\n1.7272727272727273 -0.2727272727272727\n3.631578947368421 1.6315789473684208\n2.052631578947368 0.05263157894736836\n3.631578947368421 1.6315789473684208\n2.052631578947368 0.05263157894736836\n4.076923076923077 2.076923076923077\n2.230769230769231 0.23076923076923084\n2.071428571428571 0.0714285714285714\n1.8571428571428572 -0.1428571428571429\n2.7857142857142856 0.7857142857142856\n2.428571428571429 0.4285714285714286\n3.272727272727273 1.272727272727273\n1.9090909090909092 -0.09090909090909094\n3.272727272727273 1.272727272727273\n1.9090909090909092 -0.09090909090909094\n2.5789473684210527 0.5789473684210527\n1.631578947368421 -0.368421052631579\n3.631578947368421 1.6315789473684208\n2.052631578947368 0.05263157894736836\n4.076923076923077 2.076923076923077\n2.230769230769231 0.23076923076923084\n4.076923076923077 2.076923076923077\n2.230769230769231 0.23076923076923084\n2.5384615384615383 0.5384615384615385\n1.6153846153846154 -0.3846153846153846\n2.7857142857142856 0.7857142857142856\n2.428571428571429 0.4285714285714286\n2.7857142857142856 0.7857142857142856\n2.428571428571429 0.4285714285714286\n2.7857142857142856 0.7857142857142856\n2.428571428571429 0.4285714285714286\n2.071428571428571 0.0714285714285714\n1.8571428571428572 -0.1428571428571429\n2.7857142857142856 0.7857142857142856\n2.428571428571429 0.4285714285714286\n1.0 -1.0\n1.0 -1.0\n2.7857142857142856 0.7857142857142856\n2.428571428571429 0.4285714285714286\n3.272727272727273 1.272727272727273\n1.9090909090909092 -0.09090909090909094\n3.272727272727273 1.272727272727273\n1.9090909090909092 -0.09090909090909094\n3.272727272727273 1.272727272727273\n1.9090909090909092 -0.09090909090909094\n3.631578947368421 1.6315789473684208\n2.052631578947368 0.05263157894736836\n3.631578947368421 1.6315789473684208\n2.052631578947368 0.05263157894736836\n"
}
],
"source": [
"def balanceSiteShiftNumberRule(model, wid, fte_adj, shiftClass):\n",
" # TODO: make helper to retrieve\n",
" total_shifts = len(Shifts.weeks) * len(shiftClass.shift_days) * shiftClass.workers_required # One person per site\n",
"\n",
" full_time_equivalent_joined = sum([full_time_equivalent_sites[i] for i in shiftClass.site])\n",
"\n",
"\n",
" max_shifts = total_shifts / full_time_equivalent_joined * fte_adj + twilight_balance_offset\n",
" min_shifts = total_shifts / full_time_equivalent_joined * fte_adj - twilight_balance_offset\n",
" print(max_shifts, min_shifts)\n",
"\n",
" return inequality(min_shifts, sum(model.works[wid, week, day, shiftClass.name] for week, day in Shifts.GetAllWeeksDays()), max_shifts)\n",
"\n",
"#model.balance_site_shift_constraint = Constraint([(worker.id, worker.fte_adj, shift) for shift in Shifts.GetShiftOptions() for worker in workers if worker.site in shift.site and shift.balance_by_site], rule=balanceSiteShiftNumberRule)\n",
"\n",
"for worker in workers:\n",
" for shift in Shifts.GetShiftOptions():\n",
" if worker.site in shift.site and shift.balance_by_site:\n",
" total_shifts = len(Shifts.weeks) * len(shift.shift_days) * shift.workers_required # One person per site\n",
"\n",
" full_time_equivalent_joined = sum([full_time_equivalent_sites[i] for i in shift.site])\n",
"\n",
"\n",
" max_shifts = total_shifts / full_time_equivalent_joined * worker.fte_adj + twilight_balance_offset\n",
" min_shifts = total_shifts / full_time_equivalent_joined * worker.fte_adj - twilight_balance_offset\n",
" print(max_shifts, min_shifts)\n",
" \n",
" model.constraints.add(\n",
" #min_shifts <= sum(model.works[worker.id, week, day, shift.name] for week, day in Shifts.GetAllWeeksDays()) <= max_shifts\n",
" inequality(min_shifts, sum(model.works[worker.id, week, day, shift.name] for week, day in Shifts.GetAllWeeksDays()), max_shifts)\n",
" )\n",
" #return inequality(min_shifts, sum(model.works[wid, week, day, shiftClass.name] for week, day in Shifts.GetAllWeeksDays()), max_shifts)"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": "['exeter_twilight',\n 'truro_twilight',\n 'torquay_twilight',\n 'plymouth_twilight',\n 'weekend_exeter',\n 'weekend_truro',\n 'weekend_torquay',\n 'weekend_plymouth',\n 'night']"
},
"metadata": {},
"execution_count": 13
}
],
"source": [
"[s.name for s in Shifts.GetShiftOptions() ]"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": "[1, 6, 7, 11, 18, 25, 26, 27]"
},
"metadata": {},
"execution_count": 14
}
],
"source": [
"#[[week, day, shift.name] for week, day, shift in Shifts.GetAllShiftsAsClass() if (not shift.rota_on_nwds and day in [\"Fri\", \"Wed\"])]\n",
"[worker.id for worker in workers if worker.site in (\"exeter\")]\n",
""
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {
"tags": [
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend",
"outputPrepend"
]
},
"outputs": [],
"source": [
"# Constraint: rest between two shifts is of 12 hours (i.e., at least two shifts)\n",
"weeks_days = Shifts.GetAllWeeksDays()\n",
"for worker in workers:\n",
" for week_blocks in Shifts.GetWeeksInBlocks(2):\n",
" # Prevent nights more than once every n weeks\n",
" model.constraints.add(\n",
" 1 >= sum(model.nights[worker.id, week, block] for week in week_blocks for block in [\"weekday\", \"weekend\"])\n",
" )\n",
"\n",
" for week in Shifts.weeks:\n",
" #model.constraints.add(\n",
" #0 == sum(model.works[worker, week, day, shift] for day in days for shift in [\"night\"]) * (sum(model.works[worker, week, day, shift] for day in days for shift in [\"night\"]) - 4) * (sum(model.works[worker, week, day, shift] for day in days for shift in [\"night\"]) - 3)\n",
" # 0 == (sum(model.works[worker, week, day, shift] for day in days for shift in [\"night\"]) - 4) * (sum(model.works[worker, week, day, shift] for day in days for shift in [\"night\"]) - 3)\n",
" # )\n",
"\n",
" # Force nights to be assigned in blocks\n",
" model.constraints.add(\n",
" 1 == sum(model.nights[worker.id, week, block] for block in night_blocks)\n",
" )\n",
" \n",
" # if night block is weekday make sure Mon - Thurs is assigned as nights\n",
" model.constraints.add(\n",
" model.nights[worker.id, week, \"weekday\"] * 4 == sum(model.works[worker.id, week, day, \"night\"] for day in days[:4])\n",
" )\n",
" model.constraints.add(\n",
" model.nights[worker.id, week, \"weekend\"] * 3 == sum(model.works[worker.id, week, day, \"night\"] for day in days[4:])\n",
" )\n",
"\n",
" # \n",
" \n",
" for n in range(len(weeks_days)):\n",
"\n",
" week, day = weeks_days[n]\n",
"\n",
" p1 = 1\n",
" if n > 0:\n",
" pweek, pday = weeks_days[n-1]\n",
" else:\n",
" p1 = 0\n",
" pweek, pday = weeks_days[n]\n",
"\n",
" n1 = 1\n",
" n2 = 1\n",
" try:\n",
" nweek, nday = weeks_days[n+1]\n",
" except IndexError:\n",
" #print(worker.id, \"shit\")\n",
" nweek, nday = weeks_days[n]\n",
" n1 = 0\n",
" \n",
" try:\n",
" \n",
" n2week, n2day = weeks_days[n+2]\n",
" except IndexError:\n",
" #print(worker.id, \"shit2\")\n",
" n2week, n2day = weeks_days[n]\n",
" n2 = 0\n",
"\n",
" #print(week, day, nweek, nday, n2week, n2day, n1, n2)\n",
" # Unable to work (hard constraint not preference)\n",
" model.constraints.add(\n",
" model.available[worker.id, week, day] >= \\\n",
" sum(model.works[worker.id, week, day, shift] for shift in Shifts.GetShiftNamesByDay(day)) \n",
" )\n",
" model.constraints.add(\n",
" model.available[worker.id, week, day] >= \\\n",
" model.works[worker.id, pweek, pday, \"night\"]\n",
" )\n",
"\n",
"\n",
" # single shift per day\n",
" model.constraints.add(\n",
" 1 >= sum(model.works[worker.id, week, day, shift] for shift in Shifts.GetShiftNamesByDay(day))\n",
" )\n",
" ## if working in evening, until next evening (note that after sunday comes next monday)\n",
" #model.constraints.add(\n",
" # 1 >= sum(model.works[worker.id, week, days[j], shift] for shift in ['evening', 'night']) +\n",
" # model.works[worker.id, nweek, days[(j + 1) % 7], 'truro_twilight']\n",
" #)\n",
"\n",
" # if working a night ensure preceeding (1) or subsequent (2) shifts can only be nights\n",
"\n",
"\n",
" model.constraints.add(\n",
" 1 >= model.works[worker.id, week, day, \"night\"] +\n",
" sum(n1 * model.works[worker.id, nweek, nday, shift] for shift in Shifts.GetShiftNamesByDay(nday) if shift != \"night\") +\n",
" sum(n2 * model.works[worker.id, n2week, n2day, shift] for shift in Shifts.GetShiftNamesByDay(n2day) if shift != \"night\") +\n",
" sum(p1 * model.works[worker.id, pweek, pday, shift] for shift in Shifts.GetShiftNamesByDay(pday) if shift != \"night\")\n",
" )"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [],
"source": [
"#print(model.works[18,6, \"Fri\", \"night\"].value)\n",
"#print(model.works[18,7, \"Mon\", \"truro_twilight\"].value * 0)\n",
"#Shifts.GetShiftNamesByDay(\"Mon\")"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "WARNING: Constant objective detected, replacing with a placeholder to prevent\n solver failure.\nWARNING: Loading a SolverResults object with a warning status into\n model=unknown;\n message from solver=<undefined>\n"
}
],
"source": [
"opt = SolverFactory('cbc') # choose a solver\n",
"results = opt.solve(model) # solve the model with the selected solver"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {
"tags": [
"outputPrepend",
"outputPrepend"
]
},
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "1 exeter_twilight\n1 truro_twilight\n1 torquay_twilight\n1 plymouth_twilight\n1 weekend_exeter\n1 weekend_truro\n1 weekend_torquay\n1 weekend_plymouth\n1 night\n2 exeter_twilight\n2 truro_twilight\n2 torquay_twilight\n2 plymouth_twilight\n2 weekend_exeter\n2 weekend_truro\n2 weekend_torquay\n2 weekend_plymouth\n2 night\n3 exeter_twilight\n3 truro_twilight\n3 torquay_twilight\n3 plymouth_twilight\n3 weekend_exeter\n3 weekend_truro\n3 weekend_torquay\n3 weekend_plymouth\n3 night\n4 exeter_twilight\n4 truro_twilight\n4 torquay_twilight\n4 plymouth_twilight\n4 weekend_exeter\n4 weekend_truro\n4 weekend_torquay\n4 weekend_plymouth\n4 night\n5 exeter_twilight\n5 truro_twilight\n5 torquay_twilight\n5 plymouth_twilight\n5 weekend_exeter\n5 weekend_truro\n5 weekend_torquay\n5 weekend_plymouth\n5 night\n6 exeter_twilight\n6 truro_twilight\n6 torquay_twilight\n6 plymouth_twilight\n6 weekend_exeter\n6 weekend_truro\n6 weekend_torquay\n6 weekend_plymouth\n6 night\n7 exeter_twilight\n7 truro_twilight\n7 torquay_twilight\n7 plymouth_twilight\n7 weekend_exeter\n7 weekend_truro\n7 weekend_torquay\n7 weekend_plymouth\n7 night\n8 exeter_twilight\n8 truro_twilight\n8 torquay_twilight\n8 plymouth_twilight\n8 weekend_exeter\n8 weekend_truro\n8 weekend_torquay\n8 weekend_plymouth\n8 night\n9 exeter_twilight\n9 truro_twilight\n9 torquay_twilight\n9 plymouth_twilight\n9 weekend_exeter\n9 weekend_truro\n9 weekend_torquay\n9 weekend_plymouth\n9 night\n10 exeter_twilight\n10 truro_twilight\n10 torquay_twilight\n10 plymouth_twilight\n10 weekend_exeter\n10 weekend_truro\n10weekend_torquay\n10 weekend_plymouth\n10 night\n11 exeter_twilight\n11 truro_twilight\n11 torquay_twilight\n11 plymouth_twilight\n11 weekend_exeter\n11 weekend_truro\n11 weekend_torquay\n11 weekend_plymouth\n11 night\n12 exeter_twilight\n12 truro_twilight\n12 torquay_twilight\n12 plymouth_twilight\n12 weekend_exeter\n12 weekend_truro\n12 weekend_torquay\n12 weekend_plymouth\n12 night\n13 exeter_twilight\n13 truro_twilight\n13 torquay_twilight\n13 plymouth_twilight\n13 weekend_exeter\n13 weekend_truro\n13 weekend_torquay\n13 weekend_plymouth\n13 night\n14 exeter_twilight\n14 truro_twilight\n14 torquay_twilight\n14 plymouth_twilight\n14 weekend_exeter\n14 weekend_truro\n14 weekend_torquay\n14 weekend_plymouth\n14 night\n15 exeter_twilight\n15 truro_twilight\n15 torquay_twilight\n15 plymouth_twilight\n15 weekend_exeter\n15 weekend_truro\n15 weekend_torquay\n15 weekend_plymouth\n15 night\n16 exeter_twilight\n16 truro_twilight\n16 torquay_twilight\n16 plymouth_twilight\n16 weekend_exeter\n16 weekend_truro\n16 weekend_torquay\n16 weekend_plymouth\n16 night\n17 exeter_twilight\n17 truro_twilight\n17 torquay_twilight\n17 plymouth_twilight\n17 weekend_exeter\n17 weekend_truro\n17 weekend_torquay\n17 weekend_plymouth\n17 night\n18 exeter_twilight\n18 truro_twilight\n18 torquay_twilight\n18 plymouth_twilight\n18 weekend_exeter\n18 weekend_truro\n18 weekend_torquay\n18 weekend_plymouth\n18 night\n19 exeter_twilight\n19 truro_twilight\n19 torquay_twilight\n19 plymouth_twilight\n19 weekend_exeter\n19 weekend_truro\n19 weekend_torquay\n19 weekend_plymouth\n19 night\n20 exeter_twilight\n20 truro_twilight\n20 torquay_twilight\n20 plymouth_twilight\n20 weekend_exeter\n20 weekend_truro\n20 weekend_torquay\n20 weekend_plymouth\n20 night\n21 exeter_twilight\n21 truro_twilight\n21 torquay_twilight\n21 plymouth_twilight\n21 weekend_exeter\n21 weekend_truro\n21 weekend_torquay\n21 weekend_plymouth\n21 night\n22 exeter_twilight\n22 truro_twilight\n22 torquay_twilight\n22 plymouth_twilight\n22 weekend_exeter\n22 weekend_truro\n22 weekend_torquay\n22 weekend_plymouth\n22 night\n23 exeter_twilight\n23 truro_twilight\n23 torquay_twilight\n23 plymouth_twilight\n23 weekend_exeter\n23 weekend_truro\n23 weekend_torquay\n23 weekend_plymouth\n23 night\n24 exeter_twilight\n24 truro_twilight\n24 torquay_twilight\n24 plymouth_twilight\n24 weekend_exeter\n24 weekend_truro\n24 weekend_torquay\n24 weekend_plymouth\n24 night\n25 exeter_twilight\n25 truro_twilight\n25 torquay_twilight\n25 plymouth_twilight\n25 weekend_exeter\n25 weekend_truro\n25 weekend_torquay\n25 weekend_plymouth\n25 night\n26 exeter_twilight\n26 truro_twilight\n26 torquay_twilight\n26 plymouth_twilight\n26 weekend_exeter\n26 weekend_truro\n26 weekend_torquay\n26 weekend_plymouth\n26 night\n27 exeter_twilight\n27 truro_twilight\n27 torquay_twilight\n27 plymouth_twilight\n27 weekend_exeter\n27 weekend_truro\n27 weekend_torquay\n27 weekend_plymouth\n27 night\n28 exeter_twilight\n28 truro_twilight\n28 torquay_twilight\n28 plymouth_twilight\n28 weekend_exeter\n28 weekend_truro\n28 weekend_torquay\n28 weekend_plymouth\n28 night\n29 exeter_twilight\n29 truro_twilight\n29 torquay_twilight\n29 plymouth_twilight\n29 weekend_exeter\n29 weekend_truro\n29 weekend_torquay\n29 weekend_plymouth\n29 night\n30 exeter_twilight\n30 truro_twilight\n30 torquay_twilight\n30 plymouth_twilight\n30 weekend_exeter\n30 weekend_truro\n30 weekend_torquay\n30 weekend_plymouth\n30 night\n31 exeter_twilight\n31 truro_twilight\n31 torquay_twilight\n31 plymouth_twilight\n31 weekend_exeter\n31 weekend_truro\n31 weekend_torquay\n31 weekend_plymouth\n31 night\n32 exeter_twilight\n32 truro_twilight\n32 torquay_twilight\n32 plymouth_twilight\n32 weekend_exeter\n32 weekend_truro\n32 weekend_torquay\n32 weekend_plymouth\n32 night\n33 exeter_twilight\n33 truro_twilight\n33 torquay_twilight\n33 plymouth_twilight\n33 weekend_exeter\n33 weekend_truro\n33 weekend_torquay\n33 weekend_plymouth\n33 night\n34 exeter_twilight\n34 truro_twilight\n34 torquay_twilight\n34 plymouth_twilight\n34 weekend_exeter\n34 weekend_truro\n34 weekend_torquay\n34 weekend_plymouth\n34 night\n35 exeter_twilight\n35 truro_twilight\n35 torquay_twilight\n35 plymouth_twilight\n35 weekend_exeter\n35 weekend_truro\n35 weekend_torquay\n35 weekend_plymouth\n35 night\n36 exeter_twilight\n36 truro_twilight\n36 torquay_twilight\n36 plymouth_twilight\n36 weekend_exeter\n36 weekend_truro\n36 weekend_torquay\n36 weekend_plymouth\n36 night\n37 exeter_twilight\n37 truro_twilight\n37 torquay_twilight\n37 plymouth_twilight\n37 weekend_exeter\n37 weekend_truro\n37 weekend_torquay\n37 weekend_plymouth\n37 night\n38 exeter_twilight\n38 truro_twilight\n38 torquay_twilight\n38 plymouth_twilight\n38 weekend_exeter\n38 weekend_truro\n38 weekend_torquay\n38 weekend_plymouth\n38 night\n39 exeter_twilight\n39 truro_twilight\n39 torquay_twilight\n39 plymouth_twilight\n39 weekend_exeter\n39 weekend_truro\n39 weekend_torquay\n39 weekend_plymouth\n39 night\n"
}
],
"source": [
"def get_workers_needed(needed):\n",
" \"\"\"Extract to a list the needed workers for the optimal solution.\"\"\"\n",
" workers_needed = []\n",
" for worker in workers:\n",
" if needed[worker.id].value == 1:\n",
" workers_needed.append(worker.name)\n",
" return workers_needed\n",
"\n",
"\n",
"def get_work_table(works):\n",
" \"\"\"Build a timetable of the week as a dictionary from the model's optimal solution.\"\"\"\n",
" week_table = {week :{day: {shift: [] for shift in Shifts.GetShiftNames()} for day in days} for week in Shifts.weeks}\n",
" for week in Shifts.weeks:\n",
" for worker in workers:\n",
" for day, shift in Shifts.GetAllDayShiftsAsNames():\n",
" if works[worker.id, week, day, shift].value == 1:\n",
" week_table[week][day][shift].append(worker.get_details())\n",
" return week_table\n",
"\n",
"def get_worker_timetable(works):\n",
" timetable = { worker.name : { week : { day: \"\" for day in days } for week in Shifts.weeks } for worker in workers }\n",
" for worker in workers:\n",
" for week in Shifts.weeks:\n",
" for day, shift in Shifts.GetAllDayShiftsAsNames():\n",
" if works[worker.id, week, day, shift].value == 1:\n",
" timetable[worker.name][week][day] = shift\n",
" return timetable\n",
"\n",
"def get_worker_timetable_brief(works):\n",
" timetable = []\n",
" timetable.append(\"{:20}\".format(\"Worker\") + \"\".join([7*str(\"{}\".format(w))[-1:] for w in Shifts.weeks]))\n",
" for worker in workers:\n",
" w = [\"{:20}\".format(worker.name)]\n",
" for week, day in Shifts.GetAllWeeksDays():\n",
" a = \"-\"\n",
" for shift in Shifts.GetShiftNamesByDay(day):\n",
" if works[worker.id, week, day, shift].value == 1:\n",
" a = shift[0]\n",
" w.append(a)\n",
" timetable.append(\"\".join(w))\n",
" \n",
" return \"\\n\".join(timetable)\n",
"\n",
"def get_shift_summary(works):\n",
" timetable = { worker.get_details() : { shift[0] : \"\" for shift in Shifts.GetShiftNames() } for worker in workers }\n",
" #timetable = { worker.get_details() : { shift : \"\" for shift in [\"truro_twilight\"] } for worker in workers } \n",
" t = []\n",
" for worker in workers:\n",
" l = [worker.get_details()+\": \"]\n",
"\n",
" for shift in Shifts.GetShiftNames():\n",
" #for shift in [\"truro_twilight\"]:\n",
" c = [works[worker.id, week, day, shift] for week in Shifts.weeks for day in days].count(1)\n",
" if c > 0:\n",
" l.append(\"{} ({})\".format(shift,c))\n",
" print(worker.id, shift)\n",
" timetable[worker.get_details()][shift[0]] = c\n",
" t.append(\",\".join(l))\n",
" return \"\\n\".join(t) \n",
"\n",
"def get_no_preference(no_pref):\n",
" \"\"\"Extract to a list the workers not satisfied with their weekend preference.\"\"\"\n",
" return [worker.id for worker in workers if no_pref[worker.id].value == 1]\n",
"\n",
"def get_night_blocks(nights):\n",
" timetable = { worker.name : { week : { block: \"\" for block in night_blocks } for week in Shifts.weeks } for worker in workers }\n",
" for worker in workers:\n",
" for week in Shifts.weeks:\n",
" for block in night_blocks:\n",
" if nights[worker.id, week, block].value == 1:\n",
" timetable[worker.name][week][block] = \"true\"\n",
" return timetable\n",
"\n",
"\n",
"\n",
"workers_needed = get_workers_needed(model.needed) # dict with the optimal timetable\n",
"week_table = get_work_table(model.works) # list with the required workers\n",
"worker_timetable = get_worker_timetable(model.works)\n",
"worker_timetable_brief = get_worker_timetable_brief(model.works)\n",
"workers_no_pref = get_no_preference(model.no_pref) # list with the non-satisfied workers (work on Sat but not on Sun)\n",
"worker_night_block = get_night_blocks(model.nights)\n",
"worker_shift_summary = get_shift_summary(model.works)"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {},
"outputs": [],
"source": [
"def ExportRotaToCSV(works):\n",
" with open(\"rota.csv\", 'w', newline='') as f:\n",
" wr = csv.writer(f, quoting=csv.QUOTE_ALL)\n",
" l = [\"Name\"]\n",
" l.extend([worker.name for worker in workers])\n",
" wr.writerow(l)\n",
"\n",
" l2 = [\"Site\"]\n",
" l2.extend([worker.site for worker in workers])\n",
" wr.writerow(l2)\n",
"\n",
" l3 = [\"Grade\"]\n",
" l3.extend([worker.grade for worker in workers])\n",
" wr.writerow(l3)\n",
"\n",
" l4 = [\"FTE\"]\n",
" l4.extend([worker.fte for worker in workers])\n",
" wr.writerow(l4)\n",
"\n",
"\n",
" for week, day in Shifts.GetAllWeeksDays():\n",
" d = [\"Week {} Day {}\".format(week, day)]\n",
"\n",
" for worker in workers:\n",
" i = \"\"\n",
" for shift in Shifts.GetShiftNamesByDay(day):\n",
" if works[worker.id, week, day, shift].value == 1:\n",
" i = shift\n",
"\n",
" d.append(i)\n",
" wr.writerow(d)\n",
"\n",
"ExportRotaToCSV(model.works)"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {},
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "Worker 1111111222222233333334444444\nWijhdan Abusrewil ----------------------------\nSeren Peters ----------------------------\nHannah Lewis ----------------------------\nChristian Greer ----------------------------\nNicolas Dziad ----------------------------\nMichal Tomek ----------------------------\nAlex Wood ----------------------------\nAlexander Sanchez- Cabello----------------------------\nMax Ireland ----------------------------\nVilim Kalamar ----------------------------\nSalma Aslam ----------------------------\nAdeola Omotade ----------------------------\nAmina Odeh ----------------------------\nCharles Finan ----------------------------\nAlan Eccles ----------------------------\nSarath Vennam ----------------------------\nPaul Ward ----------------------------\nSayed Alqarooni ----------------------------\nJean Sukumar ----------------------------\nJenn Fong ----------------------------\nChong Ng ----------------------------\nMatthew Thorley ----------------------------\nJenna Millington ----------------------------\nChi Leung ----------------------------\nTom Welsh ----------------------------\nRebecca Murphy ----------------------------\nDelilah Trimmer ----------------------------\nSophie McGlade ----------------------------\nS Ghauri ----------------------------\nKhalil Madbak ----------------------------\nNatasha Hougham ----------------------------\nRichard Chaytor ----------------------------\nSafi Rahman ----------------------------\nGeorge Edwards ----------------------------\nNeil McIntyre ----------------------------\nMo Babsail ----------------------------\nPaul Jenkins ----------------------------\nRoss Kruger ----------------------------\nAmoolya Mannava ----------------------------\n1 e 2: \n2 p 2: \n3 p 2: \n4 t 2: \n5 t 2: \n6 e 2: \n7 e 2: \n8 p 2: \n9 t 2: \n10 t 2: \n11 e 3: \n12 p 3: \n13 p 3: \n14 t 3: \n15 t 3: \n16 t 3: \n17 t 3: \n18 e 3: \n19 p 3: \n20 p 3: \n21 t 3: \n22 t 3: \n23 t 3: \n24 t 3: \n25 e 4: \n26 e 4: \n27 e 4: \n28 p 4: \n29 p 4: \n30 p 4: \n31 p 4: \n32 p 4: \n33 p 4: \n34 p 4: \n35 t 4: \n36 t 4: \n37 t 4: \n38 t 4: \n39 t 4: \n"
}
],
"source": [
"\n",
"print(worker_timetable_brief)\n",
"print(worker_shift_summary)"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {},
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": "[('Mon', 'exeter_twilight'),\n ('Tue', 'exeter_twilight'),\n ('Wed', 'exeter_twilight'),\n ('Thu', 'exeter_twilight'),\n ('Fri', 'exeter_twilight'),\n ('Mon', 'truro_twilight'),\n ('Tue', 'truro_twilight'),\n ('Wed', 'truro_twilight'),\n ('Thu', 'truro_twilight'),\n ('Fri', 'truro_twilight'),\n ('Mon', 'torquay_twilight'),\n ('Tue', 'torquay_twilight'),\n ('Wed', 'torquay_twilight'),\n ('Thu', 'torquay_twilight'),\n ('Fri', 'torquay_twilight'),\n ('Mon', 'plymouth_twilight'),\n ('Tue', 'plymouth_twilight'),\n ('Wed', 'plymouth_twilight'),\n ('Thu', 'plymouth_twilight'),\n ('Fri', 'plymouth_twilight'),\n ('Sat', 'weekend_exeter'),\n ('Sun', 'weekend_exeter'),\n ('Sat', 'weekend_truro'),\n ('Sun', 'weekend_truro'),\n ('Sat', 'weekend_torquay'),\n ('Sun', 'weekend_torquay'),\n ('Sat', 'weekend_plymouth'),\n ('Sun', 'weekend_plymouth'),\n ('Mon', 'night'),\n ('Tue', 'night'),\n ('Wed', 'night'),\n ('Thu', 'night'),\n ('Fri', 'night'),\n ('Sat', 'night'),\n ('Sun', 'night')]"
},
"metadata": {},
"execution_count": 21
}
],
"source": [
"#workers_needed\n",
"Shifts.GetAllDayShiftsAsNames()"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {},
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": "{1: {'Mon': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Tue': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Wed': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Thu': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Fri': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []}},\n 2: {'Mon': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Tue': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Wed': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Thu': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Fri': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []}},\n 3: {'Mon': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Tue': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Wed': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Thu': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Fri': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []}},\n 4: {'Mon': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Tue': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Wed': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Thu': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Fri': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': []}}}"
},
"metadata": {},
"execution_count": 22
}
],
"source": [
"week_table"
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {},
"outputs": [],
"source": [
"#worker_night_block"
]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {},
"outputs": [],
"source": [
"#[[worker, week, day, shift] for day in days for shift in [\"night\"]]"
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {},
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": "1"
},
"metadata": {},
"execution_count": 25
}
],
"source": [
"1 if 4 in [4, 1, 0] else 0"
]
},
{
"cell_type": "code",
"execution_count": 26,
"metadata": {},
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": "0"
},
"metadata": {},
"execution_count": 26
}
],
"source": [
"(4 + 1) // 7"
]
},
{
"cell_type": "code",
"execution_count": 27,
"metadata": {},
"outputs": [],
"source": [
"#for worker in workers:\n",
"# for shift in shifts:\n",
"# a = [model.works[worker.id, week, day, shift].value for week in weeks for day in days]"
]
},
{
"cell_type": "code",
"execution_count": 28,
"metadata": {},
"outputs": [
{
"output_type": "error",
"ename": "NameError",
"evalue": "name 'a' is not defined",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m<ipython-input-28-3f786850e387>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0ma\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;31mNameError\u001b[0m: name 'a' is not defined"
]
}
],
"source": [
"a"
]
},
{
"cell_type": "code",
"execution_count": 29,
"metadata": {},
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": "0"
},
"metadata": {},
"execution_count": 29
}
],
"source": [
"0 % 7"
]
},
{
"cell_type": "code",
"execution_count": 30,
"metadata": {},
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": "6"
},
"metadata": {},
"execution_count": 30
}
],
"source": [
"-1 % 7"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
]
}