From 1b4cca40f8fa29cf7990d1f6c6bb208d28f96d86 Mon Sep 17 00:00:00 2001 From: ross Date: Sat, 11 Apr 2020 18:33:17 +0100 Subject: [PATCH] continue --- rota.ipynb | 883 +++++++---------------------------------------------- shifts.py | 541 +++++++++++++++++++++++++++++++- workers.py | 53 ++-- 3 files changed, 672 insertions(+), 805 deletions(-) diff --git a/rota.ipynb b/rota.ipynb index b0de0b6..2e151bc 100644 --- a/rota.ipynb +++ b/rota.ipynb @@ -31,7 +31,7 @@ "source": [ "from pyomo.environ import *\n", "from pyomo.opt import SolverFactory\n", - "from shifts import SingleShift, ShiftCollection, days, sites\n", + "from shifts import SingleShift, RotaBuilder, days, sites\n", "from workers import Worker\n", "\n", "import math\n", @@ -59,7 +59,7 @@ "source": [ "#night_balance_offset = 2\n", "#twilight_balance_offset = 1\n", - "weeks_to_rota = 12\n", + "weeks_to_rota = 16\n", "use_test_workers = True" ] }, @@ -70,14 +70,9 @@ "outputs": [], "source": [ "\n", + "Rota = RotaBuilder(weeks_to_rota=weeks_to_rota)\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", + "Rota.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", @@ -89,41 +84,18 @@ " SingleShift((sites), \"night\", 12.5, days, balance_offset=2, balance_weighting=2, workers_required=3, rota_on_nwds=True), \n", ")\n", "\n", + "#Rota2 = RotaBuilder(weeks_to_rota=weeks_to_rota)\n", + "\n", + "#Rota2.add_shifts(\n", + "# SingleShift((\"exeter\",), \"exeter_twilight\", 12.5, days[:5], balance_offset=3), \n", + "# SingleShift((\"exeter\",), \"weekend_exeter\", 12.5, days[5:], balance_offset=3, balance_weighting=1), \n", + "# SingleShift((sites), \"night\", 12.5, days, balance_offset=2, balance_weighting=4, workers_required=3, rota_on_nwds=True), \n", + "#)\n", + "\n", + "#Rota = Rota2\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", - "\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):" + "" ] }, { @@ -132,16 +104,16 @@ "metadata": {}, "outputs": [], "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\", 3, nwd=['Fri', \"Sat\", \"Sun\"]) for i in range(1, 2)])\n", - " workers.extend([Worker(Shifts,i, \"Test Person {}\".format(i), \"truro\", 4, ) for i in range(1, 15)])\n", - " workers.extend([Worker(Shifts,i, \"Test Person {}\".format(i), \"plymouth\", 4) for i in range(15, 30)])\n", - " workers.extend([Worker(Shifts,i, \"Test Person {}\".format(i), \"exeter\", 4) for i in range(30, 45)])\n", - " workers.extend([Worker(Shifts,i, \"Test Person {}\".format(i), \"torquay\", 4) for i in range(45, 60)])\n", + " #workers.extend([Worker(Rota, i, \"Test Person {}\".format(i), \"truro\", 3, nwd=['Fri', \"Sat\", \"Sun\"]) for i in range(1, 2)])\n", + " Rota.add_workers([Worker(Rota,i, \"Test Person {}\".format(i), \"truro\", 4, ) for i in range(1, 15)])\n", + " Rota.add_workers([Worker(Rota,i, \"Test Person {}\".format(i), \"plymouth\", 4) for i in range(15, 30)])\n", + " Rota.add_workers([Worker(Rota,i, \"Test Person {}\".format(i), \"exeter\", 4) for i in range(30, 45)])\n", + " Rota.add_workers([Worker(Rota,i, \"Test Person {}\".format(i), \"torquay\", 4) for i in range(45, 60)])\n", " #workers.extend([workerClass(i, \"Test Person {}\".format(i), 4, 200, \"\") for i in range(48, 49)])\n", + " \n", + " #workers.extend([Worker(Rota,i, \"Test Person {}\".format(i), \"exeter\", 4) for i in range(34, 45)])\n", "else:\n", " # Import trainee data\n", " import csv\n", @@ -164,9 +136,9 @@ " 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", + " Rota.add_worker(Worker(Rota, n, name, site.lower(), int(grade[2]), int(fte), nwds, end_date, oop))\n", + "\n", "\n", - "workers = sorted(workers)\n", "" ] }, @@ -176,640 +148,13 @@ "metadata": {}, "outputs": [], "source": [ - "#[workers.site for workers in workers]\n", - "" + "Rota.build_workers()\n", + "Rota.build_model()" ] }, { "cell_type": "code", "execution_count": 6, - "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": 7, - "metadata": {}, - "outputs": [], - "source": [ - "#Shifts.GetRequiredShiftsAndWorkers()\n", - "#Shifts.GetAllShiftsAsNames()\n", - "#Shifts.GetNotRequiredShifts()" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "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.shift_count = Var(((worker.id, shift) for worker in workers for shift in Shifts.GetShiftNames()), domain=NonNegativeReals, initialize=0)\n", - "\n", - "#model.shift_count_t1 = Var(((worker.id, shift) for worker in workers for shift in Shifts.GetShiftNames()), domain=NonNegativeReals, initialize=0)\n", - "#model.shift_count_t2 = Var(((worker.id, shift) for worker in workers for shift in Shifts.GetShiftNames()), domain=NonNegativeReals, initialize=0)\n", - "model.shift_count_t1 = Var(((worker.id) for worker in workers), domain=NonNegativeReals, initialize=0)\n", - "model.shift_count_t2 = Var(((worker.id) for worker in workers), domain=NonNegativeReals, initialize=0)\n", - "\n", - "model.shift_count_t3 = Var((worker.id for worker in workers), initialize=0)\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 Shifts.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": 9, - "metadata": { - "tags": [ - "outputPrepend", - "outputPrepend", - "outputPrepend", - "outputPrepend", - "outputPrepend", - "outputPrepend", - "outputPrepend", - "outputPrepend", - "outputPrepend", - "outputPrepend", - "outputPrepend", - "outputPrepend", - "outputPrepend", - "outputPrepend", - "outputPrepend", - "outputPrepend", - "outputPrepend", - "outputPrepend", - "outputPrepend", - "outputPrepend" - ] - }, - "outputs": [], - "source": [ - "#for shift in Shifts.shifts:\n", - "# s = []\n", - "# for worker in workers:\n", - "# if worker.site in shift.site:\n", - "# s.append(abs(sum(model.works[worker.id, week, day, shift.name].value for week, day in Shifts.GetAllWeeksDays()) - worker.shift_target_number[shift.name]))\n", - " #f n > 0:\n", - " # s.append(abs(n - worker.shift_target_number[shift.name]))\n", - "\n", - "# print(shift.name,sum(s))" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "#sum(abs(worker.shift_target_number[shift.name] - sum(model.works[worker.id, week, day, shift.name] for week, day in Shifts.GetAllWeeksDays())) for worker in workers for shift in Shifts.shifts)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "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": 12, - "metadata": { - "tags": [ - "outputPrepend" - ] - }, - "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", - " )\n", - "" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "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", - "\n", - "\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": 14, - "metadata": {}, - "outputs": [], - "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", - "\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", - "# 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): # NOTE no longer used - see generic function below\n", - " target_shifts = total_night_shifts / full_time_equivalent * fte_adj\n", - " max_shifts = target_shifts + night_balance_offset\n", - " min_shifts = target_shifts - 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", - "# Balance shifts\n", - "for worker in workers:\n", - " for shift in Shifts.GetShiftOptions():\n", - " if worker.site in shift.site: # Each site specfies which sites workers can fullfill it\n", - "\n", - " #total_shifts = Shifts.rota_days_length * shift.workers_required # One person per site\n", - " total_shifts = len(Shifts.weeks) * len(shift.shift_days) * shift.workers_required\n", - "\n", - " #if shift.balance_by_site:\n", - " full_time_equivalent_joined = sum([full_time_equivalent_sites[i] for i in shift.site])\n", - "\n", - " target_shifts = total_shifts / full_time_equivalent_joined * worker.fte_adj\n", - "\n", - " worker.shift_target_number[shift.name] = target_shifts\n", - "\n", - " max_shifts = target_shifts + shift.balance_offset\n", - " min_shifts = target_shifts - shift.balance_offset\n", - " #print(max_shifts, min_shifts)\n", - " \n", - " model.constraints.add(\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": 15, - "metadata": {}, - "outputs": [], - "source": [ - "for worker in workers:\n", - " # Define shift_count_t1 and shift_count_t2 constraints for the object\n", - " # This bypassing the need for a quadratic solver\n", - " # t1-t2 is the target\n", - " # As the objective is to minimise t1+t2 and t1 and t2 are positive reals \n", - " # t1+t2 approximates the absolute target (which otherwise requires a quadratic solver)\n", - " model.constraints.add(\n", - " #model.shift_count_t3[worker.id] == abs(sum(model.shift_count[worker.id, shift.name] - worker.shift_target_number[shift.name] for shift in Shifts.GetShiftOptions()))\n", - " model.shift_count_t1[worker.id]-model.shift_count_t2[worker.id] == sum((model.shift_count[worker.id, shift.name] - worker.shift_target_number[shift.name]) * shift.balance_weighting for shift in Shifts.GetShiftOptions())\n", - " )\n", - " for shift in Shifts.GetShiftOptions():\n", - " model.constraints.add(\n", - " model.shift_count[worker.id, shift.name] == sum(model.works[worker.id, week, day, shift.name] for week, day in Shifts.GetAllWeeksDays())\n", - " )\n", - " #model.constraints.add( \n", - " # #model.shift_count_t1[worker.id, shift.name]-model.shift_count_t2[worker.id, shift.name] == model.shift_count[worker.id, shift.name]\n", - " #model.shift_count_t1[worker.id, shift.name]-model.shift_count_t2[worker.id, shift.name] == model.shift_count[worker.id, shift.name] - worker.shift_target_number[shift.name]\n", - " #model.shift_count_t3[worker.id] == sum(model.shift_count[worker.id, shift.name] - worker.shift_target_number[shift.name] for shift in Shifts.GetShiftNames())\n", - " #)\n", - "\n", - "\n", - "\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", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "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": 17, - "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\")\n", - "#[[worker.id, shift, worker.shift_target_number[shift] - model.shift_count[(worker.id, shift)].value] for shift in worker.shift_target_number for worker in workers]" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [], - "source": [ - "#[[worker.shift_target_number[1] - model.shift_count[(worker.id, 1)].value for worker in workers]" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": { - "tags": [ - "outputPrepend" - ] - }, - "outputs": [], - "source": [ - "# Define an objective function with model as input, to pass later\n", - "def obj_rule(m):\n", - " c = len(workers)\n", - "\n", - "\n", - " #v = []\n", - " #for shift in Shifts.shifts:\n", - " # s = []\n", - " # for worker in workers:\n", - " # if worker.site in shift.site:\n", - " # s.append(abs(sum(model.works[worker.id, week, day, shift.name] for week, day in Shifts.GetAllWeeksDays()) - worker.shift_target_number[shift.name]))\n", - " # v.append(sum(s))\n", - "\n", - "\n", - " # To balance shifts as much as possible we work with the Coefficient of variation of \n", - " # differences between the target number of shifts and the given number of shifts\n", - " # stats.variation()\n", - " #return 1\n", - " #return sum(prod((model.shift_count_t1[(worker.id, shift)] + model.shift_count_t2[(worker.id, shift)]) for shift in Shifts.GetShiftNames()) for worker in workers)\n", - " return sum(model.shift_count_t1[(worker.id)] + model.shift_count_t2[(worker.id)] for worker in workers)\n", - " #return sum(model.shift_count_t3[worker.id] for worker in workers)\n", - " \n", - " #sum(abs(worker.shift_target_number - sum(model.works[worker.id, week, day, shift.name] for week, day in Shifts.GetAllWeeksDays())) for worker in workess for shift in Shifts.shifts)\n", - "\n", - "\n", - " #return sum(abs(worker.shift_target_number[shift.name] - sum(model.works[worker.id, week, day, shift.name] for week, day in Shifts.GetAllWeeksDays())) for worker in workers for shift in Shifts.shifts)\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": 20, "metadata": { "tags": [ "outputPrepend", @@ -2975,28 +2320,28 @@ { "output_type": "stream", "name": "stdout", - "text": "Welcome to the CBC MILP Solver \nVersion: 2.9.9 \nBuild Date: Aug 7 2019 \n\ncommand line - /usr/bin/cbc -seconds 120 -printingOptions all -import /tmp/tmpyr1b9jcq.pyomo.lp -stat=1 -solve -solu /tmp/tmpyr1b9jcq.pyomo.soln (default strategy 1)\nseconds was changed from 1e+100 to 120\nOption for printingOptions changed from normal to all\nPresolve 14257 (-10891) rows, 26974 (-20404) columns and 180160 (-105283) elements\nStatistics for presolved model\nOriginal problem has 46728 integers (46728 of which binary)\nPresolved problem has 26856 integers (26856 of which binary)\n==== 26856 zero objective 2 different\n26856 variables have objective of 0\n118 variables have objective of 1\n==== absolute objective values 2 different\n26856 variables have objective of 0\n118 variables have objective of 1\n==== for integers 26856 zero objective 1 different\n26856 variables have objective of 0\n==== for integers absolute objective values 1 different\n26856 variables have objective of 0\n===== end objective counts\n\n\nProblem has 14257 rows, 26974 columns (118 with objective) and 180160 elements\nThere are 118 singletons with objective 660 singletons with no objective \nColumn breakdown:\n118 of type 0.0->inf, 0 of type 0.0->up, 0 of type lo->inf, \n0 of type lo->up, 0 of type free, 0 of type fixed, \n0 of type -inf->0.0, 0 of type -inf->up, 26856 of type 0.0->1.0 \nRow breakdown:\n1368 of type E 0.0, 972 of type E 1.0, 0 of type E -1.0, \n215 of type E other, 0 of type G 0.0, 12 of type G 1.0, \n189 of type G other, 0 of type L 0.0, 10572 of type L 1.0, \n929 of type L other, 0 of type Range 0.0->1.0, 0 of type Range other, \n0 of type Free \nContinuous objective value is 0 - 1.28 seconds\nCgl0002I 19824 variables fixed\nCgl0003I 0 fixed, 0 tightened bounds, 10100 strengthened rows, 26196 substitutions\nCgl0003I 0 fixed, 0 tightened bounds, 10502 strengthened rows, 0 substitutions\nCgl0003I 0 fixed, 0 tightened bounds, 6750 strengthened rows, 0 substitutions\nCgl0003I 0 fixed, 0 tightened bounds, 5776 strengthened rows, 0 substitutions\nCgl0003I 0 fixed, 0 tightened bounds, 5336 strengthened rows, 0 substitutions\nCgl0003I 0 fixed, 0 tightened bounds, 4286 strengthened rows, 0 substitutions\nCgl0003I 0 fixed, 0 tightened bounds, 3699 strengthened rows, 0 substitutions\nCgl0003I 0 fixed, 0 tightened bounds, 1069 strengthened rows, 0 substitutions\nCgl0003I 0 fixed, 0 tightened bounds, 650 strengthened rows, 0 substitutions\nCgl0004I processed model has 7928 rows, 13210 columns (13092 integer (13092 of which binary)) and 98818 elements\nCbc0038I Initial state - 635 integers unsatisfied sum - 124.129\nCbc0038I Pass 1: (9.41 seconds) suminf. 13.54383 (62) obj. 93.1564 iterations 2715\nCbc0038I Pass 2: (9.44 seconds) suminf. 11.68652 (52) obj. 89.0441 iterations 250\nCbc0038I Pass 3: (9.46 seconds) suminf. 11.11509 (49) obj. 89.9012 iterations 61\nCbc0038I Pass 4: (9.49 seconds) suminf. 7.97224 (37) obj. 90.2114 iterations 235\nCbc0038I Pass 5: (9.51 seconds) suminf. 7.97224 (37) obj. 90.2114 iterations 9\nCbc0038I Pass 6: (9.53 seconds) suminf. 5.82938 (26) obj. 89.5051 iterations 136\nCbc0038I Pass 7: (9.55 seconds) suminf. 5.82938 (26) obj. 89.5051 iterations 63\nCbc0038I Pass 8: (9.60 seconds) suminf. 3.65763 (16) obj. 93.2746 iterations 200\nCbc0038I Pass 9: (9.61 seconds) suminf. 3.65763 (16) obj. 93.2746 iterations 31\nCbc0038I Pass 10: (9.64 seconds) suminf. 3.65763 (16) obj. 91.6203 iterations 134\nCbc0038I Pass 11: (9.66 seconds) suminf. 3.65763 (16) obj. 91.6203 iterations 21\nCbc0038I Pass 12: (9.68 seconds) suminf. 3.65763 (16) obj. 93.2746 iterations 111\nCbc0038I Pass 13: (9.74 seconds) suminf. 10.34334 (39) obj. 109.802 iterations 651\nCbc0038I Pass 14: (9.80 seconds) suminf. 5.20048 (21) obj. 109.861 iterations 450\nCbc0038I Pass 15: (9.83 seconds) suminf. 4.40048 (18) obj. 106.088 iterations 228\nCbc0038I Pass 16: (9.85 seconds) suminf. 4.40048 (19) obj. 106.088 iterations 27\nCbc0038I Pass 17: (9.88 seconds) suminf. 3.60048 (16) obj. 104.434 iterations 212\nCbc0038I Pass 18: (9.90 seconds) suminf. 3.60048 (17) obj. 104.434 iterations 46\nCbc0038I Pass 19: (9.93 seconds) suminf. 3.60048 (16) obj. 104.947 iterations 177\nCbc0038I Pass 20: (9.96 seconds) suminf. 3.60048 (17) obj. 104.947 iterations 67\nCbc0038I Pass 21: (9.98 seconds) suminf. 3.60048 (16) obj. 107.173 iterations 168\nCbc0038I Pass 22: (10.00 seconds) suminf. 3.60048 (17) obj. 107.173 iterations 28\nCbc0038I Pass 23: (10.03 seconds) suminf. 3.60048 (16) obj. 107.686 iterations 146\nCbc0038I Pass 24: (10.04 seconds) suminf. 3.60048 (17) obj. 107.686 iterations 44\nCbc0038I Pass 25: (10.08 seconds) suminf. 3.60048 (16) obj. 104.434 iterations 176\nCbc0038I Pass 26: (10.10 seconds) suminf. 3.60048 (17) obj. 104.434 iterations 37\nCbc0038I Pass 27: (10.13 seconds) suminf. 3.60048 (16) obj. 104.947 iterations 170\nCbc0038I Pass 28: (10.15 seconds) suminf. 3.60048 (17) obj. 104.947 iterations 53\nCbc0038I Pass 29: (10.18 seconds) suminf. 3.60048 (16) obj. 105.803 iterations 180\nCbc0038I Pass 30: (10.20 seconds) suminf. 3.60048 (17) obj. 105.803 iterations 75\nCbc0038I No solution found this major pass\nCbc0038I Before mini branch and bound, 12254 integers at bound fixed and 38 continuous\nCbc0038I Full problem 7928 rows 13210 columns, reduced to 621 rows 573 columns\nCbc0038I Mini branch and bound did not improve solution (10.94 seconds)\nCbc0038I Full problem 7929 rows 13210 columns, reduced to 7929 rows 13210 columns - too large\nCbc0038I After 12.39 seconds - Feasibility pump exiting - took 3.49 seconds\nCbc0031I 165 added rows had average density of 129.56364\nCbc0013I At root node, 165 cuts changed objective from 0 to 14.542373 in 10 passes\nCbc0014I Cut generator 0 (Probing) - 296 row cuts average 17.7 elements, 0 column cuts (29 active) in 3.877 seconds - new frequency is 1\nCbc0014I Cut generator 1 (Gomory) - 292 row cuts average 302.7 elements, 0 column cuts (0 active) in 1.389 seconds - new frequency is 1\nCbc0014I Cut generator 2 (Knapsack) - 36 row cuts average 15.9 elements, 0 column cuts (0 active) in 0.085 seconds - new frequency is 1\nCbc0014I Cut generator 3 (Clique) - 6 row cuts average 3.5 elements, 0 column cuts (0 active) in 0.057 seconds - new frequency is -100\nCbc0014I Cut generator 4 (MixedIntegerRounding2) - 636 row cuts average 66.2 elements, 0 column cuts (0 active) in 0.203 seconds - new frequency is 1\nCbc0014I Cut generator 5 (FlowCover) - 0 row cuts average 0.0 elements, 0 column cuts (0 active) in 0.038 seconds - new frequency is -100\nCbc0014I Cut generator 6 (TwoMirCuts) - 585 row cuts average 161.8 elements, 0 column cuts (0 active) in 1.274 seconds - new frequency is 1\nCbc0010I After 0 nodes, 1 on tree, 1e+50 best solution, best possible 14.542373 (27.71 seconds)\nCbc0012I Integer solution of 67.681356 found by DiveCoefficient after 13832 iterations and 2 nodes (31.89 seconds)\nCbc0038I Full problem 7928 rows 13210 columns, reduced to 500 rows 445 columns\nCbc0012I Integer solution of 65.420339 found by RINS after 13832 iterations and 2 nodes (37.68 seconds)\nCbc0012I Integer solution of 63.020339 found by DiveCoefficient after 16683 iterations and 7 nodes (42.56 seconds)\nCbc0012I Integer solution of 56.705085 found by DiveCoefficient after 17021 iterations and 8 nodes (43.21 seconds)\nCbc0038I Full problem 7928 rows 13210 columns, reduced to 475 rows 467 columns\nCbc0038I Full problem 7928 rows 13210 columns, reduced to 540 rows 502 columns\nCbc0012I Integer solution of 55.450847 found by RINS after 41604 iterations and 91 nodes (81.02 seconds)\nCbc0010I After 100 nodes, 56 on tree, 55.450847 best solution, best possible 14.542373 (83.41 seconds)\nCbc0038I Full problem 7928 rows 13210 columns, reduced to 489 rows 478 columns\nCbc0020I Exiting on maximum time\nCbc0005I Partial search - best objective 55.450847 (best possible 14.542373), took 66179 iterations and 200 nodes (112.52 seconds)\nCbc0032I Strong branching done 1622 times (15892 iterations), fathomed 0 nodes and fixed 0 variables\nCbc0035I Maximum depth 56, 0 variables fixed on reduced cost\nCuts at root node changed objective from 0 to 14.5424\nProbing was tried 219 times and created 1847 cuts of which 29 were active after adding rounds of cuts (5.587 seconds)\nGomory was tried 219 times and created 386 cuts of which 0 were active after adding rounds of cuts (6.989 seconds)\nKnapsack was tried 219 times and created 55 cuts of which 0 were active after adding rounds of cuts (1.355 seconds)\nClique was tried 10 times and created 6 cuts of which 0 were active after adding rounds of cuts (0.057 seconds)\nMixedIntegerRounding2 was tried 219 times and created 8454 cuts of which 0 were active after adding rounds of cuts (3.958 seconds)\nFlowCover was tried 10 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.038 seconds)\nTwoMirCuts was tried 219 times and created 1370 cuts of which 0 were active after adding rounds of cuts (2.921 seconds)\nImplicationCuts was tried 16 times and created 37 cuts of which 0 were active after adding rounds of cuts (0.042 seconds)\n\nResult - Stopped on time limit\n\nObjective value: 55.45084746\nLower bound: 14.542\nGap: 2.81\nEnumerated nodes: 200\nTotal iterations: 66179\nTime (CPU seconds): 112.89\nTime (Wallclock seconds): 113.89\n\nTotal time (CPU seconds): 113.61 (Wallclock seconds): 114.65\n\nWARNING: Loading a SolverResults object with an 'aborted' status, but\n containing a solution\n" + "text": "Welcome to the CBC MILP Solver \nVersion: 2.9.9 \nBuild Date: Aug 7 2019 \n\ncommand line - /usr/bin/cbc -seconds 600 -allow 25 -printingOptions all -import /tmp/tmpua24r1de.pyomo.lp -stat=1 -solve -solu /tmp/tmpua24r1de.pyomo.soln (default strategy 1)\nseconds was changed from 1e+100 to 600\nallowableGap was changed from 1e-10 to 25\nOption for printingOptions changed from normal to all\nPresolve 8525 (-7679) rows, 33158 (-28852) columns and 138886 (-135111) elements\nStatistics for presolved model\nOriginal problem has 61360 integers (61360 of which binary)\nPresolved problem has 33040 integers (33040 of which binary)\n==== 33040 zero objective 2 different\n33040 variables have objective of 0\n118 variables have objective of 1\n==== absolute objective values 2 different\n33040 variables have objective of 0\n118 variables have objective of 1\n==== for integers 33040 zero objective 1 different\n33040 variables have objective of 0\n==== for integers absolute objective values 1 different\n33040 variables have objective of 0\n===== end objective counts\n\n\nProblem has 8525 rows, 33158 columns (118 with objective) and 138886 elements\nThere are 118 singletons with objective \nColumn breakdown:\n118 of type 0.0->inf, 0 of type 0.0->up, 0 of type lo->inf, \n0 of type lo->up, 0 of type free, 0 of type fixed, \n0 of type -inf->0.0, 0 of type -inf->up, 33040 of type 0.0->1.0 \nRow breakdown:\n0 of type E 0.0, 416 of type E 1.0, 0 of type E -1.0, \n203 of type E other, 0 of type G 0.0, 0 of type G 1.0, \n177 of type G other, 0 of type L 0.0, 6608 of type L 1.0, \n1121 of type L other, 0 of type Range 0.0->1.0, 0 of type Range other, \n0 of type Free \nContinuous objective value is 0 - 0.33 seconds\nCgl0002I 26432 variables fixed\nCgl0004I processed model has 8348 rows, 21350 columns (21232 integer (21232 of which binary)) and 90246 elements\nCbc0038I Initial state - 159 integers unsatisfied sum - 34.8044\nCbc0038I Pass 1: (0.88 seconds) suminf. 9.38095 (36) obj. 40.5517 iterations 464\nCbc0038I Pass 2: (0.88 seconds) suminf. 9.38095 (36) obj. 40.5517 iterations 0\nCbc0038I Pass 3: (0.90 seconds) suminf. 5.00952 (20) obj. 48.6712 iterations 122\nCbc0038I Pass 4: (0.92 seconds) suminf. 4.34286 (19) obj. 49.0712 iterations 31\nCbc0038I Pass 5: (0.94 seconds) suminf. 2.60952 (10) obj. 51.7785 iterations 80\nCbc0038I Pass 6: (0.96 seconds) suminf. 2.60952 (10) obj. 51.7785 iterations 11\nCbc0038I Pass 7: (0.98 seconds) suminf. 2.60952 (10) obj. 52.9747 iterations 60\nCbc0038I Pass 8: (1.00 seconds) suminf. 2.60952 (10) obj. 51.1296 iterations 60\nCbc0038I Pass 9: (1.04 seconds) suminf. 27.94802 (95) obj. 192.581 iterations 537\nCbc0038I Pass 10: (1.07 seconds) suminf. 16.67684 (61) obj. 186.06 iterations 303\nCbc0038I Pass 11: (1.08 seconds) suminf. 15.67119 (61) obj. 186.941 iterations 34\nCbc0038I Pass 12: (1.10 seconds) suminf. 12.72768 (49) obj. 180.727 iterations 106\nCbc0038I Pass 13: (1.12 seconds) suminf. 12.33785 (48) obj. 179.814 iterations 48\nCbc0038I Pass 14: (1.14 seconds) suminf. 12.33785 (48) obj. 179.814 iterations 2\nCbc0038I Pass 15: (1.16 seconds) suminf. 9.84068 (39) obj. 180.049 iterations 116\nCbc0038I Pass 16: (1.18 seconds) suminf. 9.84068 (39) obj. 180.049 iterations 24\nCbc0038I Pass 17: (1.20 seconds) suminf. 7.43874 (31) obj. 180.126 iterations 58\nCbc0038I Pass 18: (1.22 seconds) suminf. 7.43874 (31) obj. 180.126 iterations 26\nCbc0038I Pass 19: (1.24 seconds) suminf. 5.72446 (25) obj. 179.71 iterations 70\nCbc0038I Pass 20: (1.26 seconds) suminf. 5.72446 (25) obj. 179.71 iterations 26\nCbc0038I Pass 21: (1.29 seconds) suminf. 5.85779 (21) obj. 179.251 iterations 110\nCbc0038I Pass 22: (1.30 seconds) suminf. 5.19112 (21) obj. 179.651 iterations 58\nCbc0038I Pass 23: (1.33 seconds) suminf. 5.59112 (21) obj. 175.843 iterations 122\nCbc0038I Pass 24: (1.34 seconds) suminf. 5.19112 (21) obj. 176.243 iterations 43\nCbc0038I Pass 25: (1.36 seconds) suminf. 5.59112 (21) obj. 176.051 iterations 85\nCbc0038I Pass 26: (1.38 seconds) suminf. 5.19112 (21) obj. 176.451 iterations 52\nCbc0038I Pass 27: (1.40 seconds) suminf. 5.59112 (21) obj. 175.843 iterations 75\nCbc0038I Pass 28: (1.42 seconds) suminf. 5.19112 (21) obj. 176.243 iterations 36\nCbc0038I Pass 29: (1.44 seconds) suminf. 5.59112 (21) obj. 177.518 iterations 118\nCbc0038I Pass 30: (1.46 seconds) suminf. 5.19112 (21) obj. 177.918 iterations 42\nCbc0038I No solution found this major pass\nCbc0038I Before mini branch and bound, 20552 integers at bound fixed and 25 continuous\nCbc0038I Full problem 8348 rows 21350 columns, reduced to 251 rows 531 columns\nCbc0038I Mini branch and bound did not improve solution (1.77 seconds)\nCbc0038I Full problem 8349 rows 21350 columns, reduced to 8349 rows 21350 columns - too large\nCbc0038I After 2.19 seconds - Feasibility pump exiting - took 1.40 seconds\nCbc0012I Integer solution of 19.715254 found by DiveCoefficient after 3604 iterations and 0 nodes (6.37 seconds)\nCbc0031I 124 added rows had average density of 258.29032\nCbc0013I At root node, 124 cuts changed objective from 0 to 10.135593 in 10 passes\nCbc0014I Cut generator 0 (Probing) - 0 row cuts average 0.0 elements, 0 column cuts (0 active) in 0.389 seconds - new frequency is -100\nCbc0014I Cut generator 1 (Gomory) - 408 row cuts average 742.9 elements, 0 column cuts (0 active) in 0.874 seconds - new frequency is 1\nCbc0014I Cut generator 2 (Knapsack) - 0 row cuts average 0.0 elements, 0 column cuts (0 active) in 0.049 seconds - new frequency is -100\nCbc0014I Cut generator 3 (Clique) - 0 row cuts average 0.0 elements, 0 column cuts (0 active) in 0.007 seconds - new frequency is -100\nCbc0014I Cut generator 4 (MixedIntegerRounding2) - 322 row cuts average 223.6 elements, 0 column cuts (0 active) in 0.191 seconds - new frequency is 1\nCbc0014I Cut generator 5 (FlowCover) - 0 row cuts average 0.0 elements, 0 column cuts (0 active) in 0.054 seconds - new frequency is -100\nCbc0014I Cut generator 6 (TwoMirCuts) - 274 row cuts average 277.7 elements, 0 column cuts (0 active) in 0.458 seconds - new frequency is 1\nCbc0011I Exiting as integer gap of 9.579661 less than 25 or 0%%\nCbc0001I Search completed - best objective 19.71525423728813, took 3604 iterations and 0 nodes (6.39 seconds)\nCbc0035I Maximum depth 0, 0 variables fixed on reduced cost\nCuts at root node changed objective from 0 to 10.1356\nProbing was tried 10 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.389 seconds)\nGomory was tried 10 times and created 408 cuts of which 0 were active after adding rounds of cuts (0.874 seconds)\nKnapsack was tried 10 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.049 seconds)\nClique was tried 10 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.007 seconds)\nMixedIntegerRounding2 was tried 10 times and created 322 cuts of which 0 were active after adding rounds of cuts (0.191 seconds)\nFlowCover was tried 10 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.054 seconds)\nTwoMirCuts was tried 10 times and created 274 cuts of which 0 were active after adding rounds of cuts (0.458 seconds)\n\nResult - Optimal solution found (within gap tolerance)\n\nObjective value: 19.71525424\nLower bound: 10.136\nGap: 0.95\nEnumerated nodes: 0\nTotal iterations: 3604\nTime (CPU seconds): 6.56\nTime (Wallclock seconds): 6.66\n\nTotal time (CPU seconds): 7.38 (Wallclock seconds): 7.51\n\n" } ], "source": [ "opt = SolverFactory('cbc') # choose a solver\n", "#opt = SolverFactory('ipopt') # choose a solver\n", "\n", - "results = opt.solve(model, tee=True, options={\"seconds\":600}) # solve the model" + "results = opt.solve(Rota.model, tee=True, options={\"seconds\":600, \"allow\": 25}) # solve the model with the, options=\"seconds=60\" selected solver" ] }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 7, "metadata": {}, "outputs": [ { "output_type": "execute_result", "data": { - "text/plain": "EnumValue(, 3, 'aborted')" + "text/plain": "EnumValue(, 0, 'ok')" }, "metadata": {}, - "execution_count": 21 + "execution_count": 7 } ], "source": [ @@ -3005,7 +2350,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 83, "metadata": { "tags": [ "outputPrepend", @@ -3014,42 +2359,42 @@ }, "outputs": [], "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", + "#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", + " week_table = {week :{day: {shift: [] for shift in Rota.GetShiftNames()} for day in days} for week in Rota.weeks}\n", + " for week in Rota.weeks:\n", + " for worker in Rota.workers:\n", + " for day, shift in Rota.GetAllDayShiftAsNames():\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", + " timetable = { worker.name : { week : { day: \"\" for day in days } for week in Rota.weeks } for worker in Rota.workers }\n", + " for worker in Rota.workers:\n", + " for week in Rota.weeks:\n", + " for day, shift in Rota.GetAllDayShiftAsNames():\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", + " timetable.append(\"{:20}\".format(\"Worker\") + \"\".join([7*str(\"{}\".format(w))[-1:] for w in Rota.weeks]))\n", + " for worker in Rota.workers:\n", " w = [\"{:20}\".format(worker.name)]\n", - " for week, day in Shifts.GetAllWeeksDays():\n", + " for week, day in Rota.GetAllWeeksDays():\n", " a = \"-\"\n", - " for shift in Shifts.GetShiftNamesByDay(day):\n", + " for shift in Rota.GetShiftNamesByDay(day):\n", " if works[worker.id, week, day, shift].value == 1:\n", " a = shift[0]\n", " w.append(a)\n", @@ -3058,15 +2403,15 @@ " 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[0] : \"\" for shift in Rota.GetShiftNames() } for worker in Rota.workers }\n", " #timetable = { worker.get_details() : { shift : \"\" for shift in [\"truro_twilight\"] } for worker in workers } \n", " t = []\n", - " for worker in workers:\n", + " for worker in Rota.workers:\n", " l = [worker.get_details()+\": \"]\n", "\n", - " for shift in Shifts.GetShiftNames():\n", + " for shift in Rota.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", + " c = [works[worker.id, week, day, shift] for week in Rota.weeks for day in days].count(1)\n", " if c > 0:\n", " l.append(\"{} ({})\".format(shift,c))\n", " #print(worker.id, shift)\n", @@ -3074,33 +2419,33 @@ " 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", + "#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", + " timetable = { worker.name : { week : { block: \"\" for block in Rota.night_blocks } for week in Rota.weeks } for worker in Rota.workers }\n", + " for worker in Rota.workers:\n", + " for week in Rota.weeks:\n", + " for block in Rota.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)" + "#workers_needed = get_workers_needed(model.needed) # dict with the optimal timetable\n", + "week_table = get_work_table(Rota.model.works) # list with the required workers\n", + "worker_timetable = get_worker_timetable(Rota.model.works)\n", + "worker_timetable_brief = get_worker_timetable_brief(Rota.model.works)\n", + "#workers_no_pref = get_no_preference(Rota.model.no_pref) # list with the non-satisfied workers (work on Sat but not on Sun)\n", + "worker_night_block = get_night_blocks(Rota.model.nights)\n", + "worker_shift_summary = get_shift_summary(Rota.model.works)" ] }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 98, "metadata": {}, "outputs": [], "source": [ @@ -3108,46 +2453,46 @@ " 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", + " l.extend([worker.name for worker in Rota.workers])\n", " wr.writerow(l)\n", "\n", " l2 = [\"Site\"]\n", - " l2.extend([worker.site for worker in workers])\n", + " l2.extend([worker.site for worker in Rota.workers])\n", " wr.writerow(l2)\n", "\n", " l3 = [\"Grade\"]\n", - " l3.extend([worker.grade for worker in workers])\n", + " l3.extend([worker.grade for worker in Rota.workers])\n", " wr.writerow(l3)\n", "\n", " l4 = [\"FTE\"]\n", - " l4.extend([worker.fte for worker in workers])\n", + " l4.extend([worker.fte for worker in Rota.workers])\n", " wr.writerow(l4)\n", "\n", "\n", - " for week, day in Shifts.GetAllWeeksDays():\n", + " for week, day in Rota.GetAllWeeksDays():\n", " d = [\"Week {} Day {}\".format(week, day)]\n", "\n", - " for worker in workers:\n", + " for worker in Rota.workers:\n", " i = \"\"\n", - " for shift in Shifts.GetShiftNamesByDay(day):\n", + " for shift in Rota.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)" + "ExportRotaToCSV(Rota.model.works)" ] }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 99, "metadata": {}, "outputs": [ { "output_type": "stream", "name": "stdout", - "text": "Worker 111111122222223333333444444455555556666666777777788888889999999000000011111112222222\nTest Person 30 ---e--------------------------------------e----------e---------e----w-nnnn----------\nTest Person 31 -------------w----------e-----------e---------nnn---------------------e---nnn-------\nTest Person 32 e----------e--------------w------------e---------------w---e---nnnn-----------------\nTest Person 33 --e-------e---------w--------e-----nnnn-----------e----------------------------e----\nTest Person 34 ----e--nnnn-------e------e--------------w----------e----------w---------------------\nTest Person 35 --------e---------nnn--e---w---e---------------w----------e-------------------------\nTest Person 36 -e----------w--------nnnn------------e-------e--------w-------------------------e---\nTest Person 37 -----w--------e------nnnn-------------------e----------------w---e-----e------------\nTest Person 38 ----nnn----------e----------e------------w----------e--------------nnn--------------\nTest Person 39 -------------------w--------------w----nnn----e---------e----------e-----e----------\nTest Person 40 ----nnn-----------nnn-----------e-----e-------------------------e----------w--------\nTest Person 41 ---------e------e----------------w--------nnnn--------------------------e---w----e--\nTest Person 42 ------w-------nnnn---e---------------------------e----------e-------------e---e-----\nTest Person 43 -------e----------------------e------------e--nnn--------e-----------w-------e-----w\nTest Person 44 ---------------e------e------------e------------w-------nnnn------e---------------w-\nTest Person 15 pp----------w----------------------nnnn---p-----w-------------w---------------p---w-\nTest Person 16 ---p----p-----------w-----w------------p--------------------nnn-------p------p----w-\nTest Person 17 ------w--------p------------nnnn-----------------------w------w---------p---w----p--\nTest Person 18 ----p-----p--------------------p--------w-nnnn----p---w----------p----------w-------\nTest Person 19 --p------p---------w----p----------p---------------p---w------------w-nnnn----------\nTest Person 20 -----w-nnnn----------------w--------p-------p--------p------p--------w---p----------\nTest Person 21 --t--------p----------p---w-----nnn-------------w--------p-------------p-----------w\nTest Person 22 -----------------p-----p---------w----p----------p-------------nnnn--w-----w-------w\nTest Person 23 --------------p------------w-------nnnn----p---w-----------p--------w---------------\nTest Person 24 --------------------w--------p----w-----------------p---nnnn-------p-------w----p---\nTest Person 25 -------------w----p-------------p----p---w-----w-nnnn-------------p-----------------\nTest Person 26 nnnn---p----w------w-----p---------------w----p---------p-------p-------------------\nTest Person 27 -----w----------p-----------p----w-----nnn-----------nnn-----w------------p---------\nTest Person 28 --------------t---------------p---w-----w----p---------------w-nnnn------------p----\nTest Person 29 ------w------w-------p----------nnn-------------------w---p----p---nnn--------------\nTest Person 45 tt---w-----nnn----------t-------------------------------t---t--------w-------------w\nTest Person 46 --t---w---------------------nnnn-----------t-----------w---t--------------------t---\nTest Person 47 -------------w------w----------------t----t---t-----t-----------------nnnn----------\nTest Person 48 ------------w-------------------------t-------nnn--t-------------t--------nnn-------\nTest Person 49 ----------t----t----------w------------t---------nnnn----------t-----------w--------\nTest Person 50 -----------------t---nnnn----t--------------------t---w-------w---------------t-----\nTest Person 51 --------------t-------t-------t---w-------nnnn--------------------t----t------------\nTest Person 52 --------t------------t------nnnn---t----w-----------------t------------------------w\nTest Person 53 -------t-----------w--------t---t--------------w-nnnn-----------t-------------------\nTest Person 54 --------------nnnn---------------w----------t--------t---t---------t----t-----------\nTest Person 55 -----------t---------------w--------t--nnn-------t-----------------nnn--------------\nTest Person 56 ---------t--------t----t--------nnn-------------w-------------------w----t-----t---w\nTest Person 57 ----t--------------------nnn-------------w-----------nnn---------------------t---t--\nTest Person 58 -----------nnn--t--------nnn---t-------------t------------------------------w-------\nTest Person 59 ---t----------nnnn-------t-----------------------------------w--------t---t-------w-\nTest Person 1 -t-----nnnn----------t--------------t-------------t---w---------------------------w-\nTest Person 10 ---------t---------w---------------------------w-----t---t--------------t----nnnn--w\nTest Person 11 ----------t---------------w---t----t-------------------w---t-----------------nnnn---\nTest Person 12 ---t-------nnn-------------------------------t--------------t-------w-----nnn---t---\nTest Person 13 nnnn---------------w--------------------------t-----t-----------t----w---t----------\nTest Person 14 --------------t---------t------------------t----w-------t----w----------------t--nnn\nTest Person 2 --t-nnn-----------t---------t------------w-----------------------t---------------nnn\nTest Person 3 nnnn----------------w------w----t-----------t----t----------------t-------t---------\nTest Person 4 --t-----t----w---t---------------------t----------------nnnn----------t-----w-------\nTest Person 5 ----t------t----------t--nnn-----w--------------------------nnn--------------t------\nTest Person 6 -----w-----------------t-------t-----t---------------nnn-------t-----------------nnn\nTest Person 7 ------w---------------------------w---t---t---------------t-nnn----t-------------t--\nTest Person 8 t--------------t--nnn--------t----------w----------------------------------w---t----\nTest Person 9 -------t----w---t--------t-------------------------t----------w--------t-----nnnn---\n" + "text": "Worker 1111111222222233333334444444555555566666667777777888888899999990000000111111122222223333333444444455555556666666\nTest Person 30 eee------n-------------------------n-e--w---e--------n-w------------w----ne------------------------n------------\nTest Person 31 ---ee------------------e--nn-n----------------n-------w---e-------------------e------n------------------w-----n-\nTest Person 32 -----------n--------ne----------e---------------w----------n----------e-----------w-n--ee--e--n------n----------\nTest Person 33 ------------w--------------------n-e-----------------------ee-------n-------nnn--------n--------------ew---e----\nTest Person 34 -----wwe-----------nn----------n-------e----------------n------n---------------------e-------n------e-------e---\nTest Person 35 -------n-------en--w----------e------n---w------ne--n---e------e-------------------------n----------------------\nTest Person 36 ------------------------e-w---n--------------ee---------------n----e------------------n--w---------nn--n--e-----\nTest Person 37 -n-n----e--------------------------n-------n----------------nn---------e---w----------------------e--e-------e-w\nTest Person 38 -----------e---------------w--n--------n---e--------e------------e---w-ne--n------------n------------n----------\nTest Person 39 --------------e---e---------nen---w---n----------------------ww-e-n------------e-------------------e---n--------\nTest Person 40 --------n---n---e-----------e---------------n-----e------e------n------------e--e-------------n--w------------w-\nTest Person 41 ---------e---w---e---n---n----------e------------------n------n------------------e--------w---en------------n---\nTest Person 42 ----------e-----------e-----n---------e---n----------e-------------------e--w---------n-----e--nw------------n--\nTest Person 43 -------------------------e-------w--------e----w----------------n-e-------n---------e-e---------n-----n----n-n--\nTest Person 44 --------------------w----n-----e-------n-----------e--------n--------n-------------w----nn---e-e---------e------\nTest Person 15 pnppn-w--------p---n------w------w----------------n----w--n----------w------------------------p------p----------\nTest Person 16 ----pwwp----w--------------w----------------------nn-------n----pn-----n--p-------------pw---p------------------\nTest Person 17 --n--w--p-p--w---------np-------n-------w----------------------n--p-------------p-n--------n------------w-------\nTest Person 18 --n--------pww-----------p---n------n--------------p-n------p-w------n-----w---------------p-------------n------\nTest Person 19 -pn-----------p----w-------------------p----------------nn----w---------p-----------n-----------n------w-----pwn\nTest Person 20 ---n------------pppwwp-----wp----------------------------n-----------------w-----n----------n-n-------------n---\nTest Person 21 ----------n----------------------n-------w---p--w------------------n---p-------n---w--------p--p-wp-----nn------\nTest Person 22 ------------nn---------------------------------wwp--n---p--------p-p-wp------------np-----w-------n-------n-----\nTest Person 23 ----------------------p---w--p----wp--nn-wn--n---np-------------------------w--------------------w-p--------p---\nTest Person 24 ------n---------------np-------p--w------n----------p--w-------p--------n-------n-w---------n---w---------p-----\nTest Person 25 ----n----p--------------------p----------------w-----nw------------n--n------np--p-n--------------------w--p--w-\nTest Person 26 ---------------------n----------pn----p----p---------p-------w---n--w-n-----w--------pp--nw-----w---------------\nTest Person 27 ---------------------------------wn-p-----n------n-n------p--w-n-n-------p---------w----------------p----p-----w\nTest Person 28 --------------------w----------------p--------p------------p-------------n--np----w------wnnn---------pw-------n\nTest Person 29 ------n-------------------------n-------w-p-pn--------w--p--n-----n-w---n------p-------p-----------------------w\nTest Person 45 t-t--n-------------w-----t------n-----------------------n----n-----------------------n-------t-t--n----w--------\nTest Person 46 ----t--n----------------n------n------------n---------------t-------------t-----t-------t-w----n------n-------w-\nTest Person 47 -----w---tt------------tn---t------------n--t-t-------n----------------n----w--------n----------n---------------\nTest Person 48 ------w-t--------t---------------------------------t-----n----w---------------------------nn--------n-tnn--t----\nTest Person 49 ------------w----n---t----w-------------n---------n-------------t------------t--nn-----t--n------------------t--\nTest Person 50 --------------nn-----n--t--w------------w-----n-n------n-------t------t-------t-------------------t-------------\nTest Person 51 -t--------------t--------------------------n------t-t-n-----------t-ww----------n-n--t-----------n--------------\nTest Person 52 -----n-t--------n---------------------t---t----w-n-n---------w-----t-------n----------n--------------t----------\nTest Person 53 ---------------n------------n-----------------n--------------n--nt---------------t-------w-ttnt-w---------------\nTest Person 54 --------n--t--------w---n------tt----------------------------------n---t---w--n--------------------t--n-n-------\nTest Person 55 -----------n-w-nn-------------------tn-t-------nw---------t------------------------wt---------------t----t------\nTest Person 56 ------------------nn---------t-------------------t---t-wt------------------nn---------t-n--------w------w---t---\nTest Person 57 ------------------t---t------nt----t-----w-----n------n--t---------------------------------------n--------nn---w\nTest Person 58 ---------n----tt-----------------ww--t-----n---------------t----------n--t----n-----n--n------------------t-----\nTest Person 59 ---t------n------n-----------------------n-t-t--------w----n------------t-n----t-nw-----------------------------\nTest Person 1 n---n--tt-n---t-tt---------w----------t-w---n----------------------------------n---------------------------n----\nTest Person 10 --------------n---------------------------ttt--------t-------w--------------------------t----------nt----n--nnww\nTest Person 11 n-tt-n---t---n---------------------------------n-------n--n-----t-----------------w------------t--------w-------\nTest Person 12 ------------------n--t---nnn--------------------w---------n---------------t---tt------t-------------n--w--t-----\nTest Person 13 -t--------t-----------n---w-------n----------------t----------w-----n--t-------------------t-nt------n----------\nTest Person 14 t---------------------nn-------------------------------w--t-------nt----n---w--n----t-------t--------------t----\nTest Person 2 nn--tww----twn-t--n----n--------------------------------t----------------t----------------------------t---------\nTest Person 3 ---n--------n----------------t--t------------tt-n---t----------t-----n-----w-------w-------------n--------------\nTest Person 4 ------nn-----w------n-t---nn---n----t--------------------------------wt---------t-----------------t-------------\nTest Person 5 --------n-----n--n----------t----w-t-------------t-------t--t-n--------------------------w-------------------tnn\nTest Person 6 -----------n------t------t--------w--n--n------w------w-----------t----------n---t---t----------------------t-n-\nTest Person 7 -----------------------tt---------n-n--------n----------------------w---t-n------------n--------w--t-----tn-----\nTest Person 8 ---------n---------ww---------tt---n-t-tn---------t-------------------------------nn------w--t------------------\nTest Person 9 ------------------------------------n-n--w----------n------t-----t--n----n---t---------t---------wn--t----------\n" } ], "source": [ @@ -3158,13 +2503,13 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 100, "metadata": {}, "outputs": [ { "output_type": "stream", "name": "stdout", - "text": "30 e 4: ,exeter_twilight (4),weekend_exeter (1),night (4)\n31 e 4: ,exeter_twilight (3),weekend_exeter (1),night (6)\n32 e 4: ,exeter_twilight (4),weekend_exeter (2),night (4)\n33 e 4: ,exeter_twilight (5),weekend_exeter (1),night (4)\n34 e 4: ,exeter_twilight (4),weekend_exeter (2),night (4)\n35 e 4: ,exeter_twilight (4),weekend_exeter (2),night (3)\n36 e 4: ,exeter_twilight (4),weekend_exeter (2),night (4)\n37 e 4: ,exeter_twilight (4),weekend_exeter (2),night (4)\n38 e 4: ,exeter_twilight (3),weekend_exeter (1),night (6)\n39 e 4: ,exeter_twilight (4),weekend_exeter (2),night (3)\n40 e 4: ,exeter_twilight (3),weekend_exeter (1),night (6)\n41 e 4: ,exeter_twilight (4),weekend_exeter (2),night (4)\n42 e 4: ,exeter_twilight (5),weekend_exeter (1),night (4)\n43 e 4: ,exeter_twilight (5),weekend_exeter (2),night (3)\n44 e 4: ,exeter_twilight (4),weekend_exeter (2),night (4)\n15 p 4: ,plymouth_twilight (4),weekend_plymouth (4),night (4)\n16 p 4: ,plymouth_twilight (5),weekend_plymouth (3),night (3)\n17 p 4: ,plymouth_twilight (3),weekend_plymouth (4),night (4)\n18 p 4: ,plymouth_twilight (5),weekend_plymouth (3),night (4)\n19 p 4: ,plymouth_twilight (5),weekend_plymouth (3),night (4)\n20 p 4: ,plymouth_twilight (5),weekend_plymouth (3),night (4)\n21 p 4: ,truro_twilight (1),plymouth_twilight (4),weekend_plymouth (3),night (3)\n22 p 4: ,plymouth_twilight (4),weekend_plymouth (4),night (4)\n23 p 4: ,plymouth_twilight (3),weekend_plymouth (3),night (4)\n24 p 4: ,plymouth_twilight (4),weekend_plymouth (3),night (4)\n25 p 4: ,plymouth_twilight (4),weekend_plymouth (3),night (4)\n26 p 4: ,plymouth_twilight (5),weekend_plymouth (3),night (4)\n27 p 4: ,plymouth_twilight (3),weekend_plymouth (3),night (6)\n28 p 4: ,truro_twilight (1),plymouth_twilight (3),weekend_plymouth (3),night (4)\n29 p 4: ,plymouth_twilight (3),weekend_plymouth (3),night (6)\n45 t 4: ,torquay_twilight (5),weekend_torquay (2),weekend_plymouth (1),night (3)\n46 t 4: ,torquay_twilight (4),weekend_torquay (2),night (4)\n47 t 4: ,torquay_twilight (4),weekend_torquay (2),night (4)\n48 t 4: ,torquay_twilight (3),weekend_torquay (1),night (6)\n49 t 4: ,torquay_twilight (4),weekend_torquay (2),night (4)\n50 t 4: ,torquay_twilight (4),weekend_torquay (2),night (4)\n51 t 4: ,torquay_twilight (5),weekend_torquay (1),night (4)\n52 t 4: ,torquay_twilight (4),weekend_torquay (2),night (4)\n53 t 4: ,torquay_twilight (4),weekend_torquay (2),night (4)\n54 t 4: ,torquay_twilight (5),weekend_torquay (1),night (4)\n55 t 4: ,torquay_twilight (3),weekend_torquay (1),night (6)\n56 t 4: ,torquay_twilight (5),weekend_truro (1),weekend_torquay (2),night (3)\n57 t 4: ,torquay_twilight (3),weekend_torquay (1),night (6)\n58 t 4: ,torquay_twilight (3),weekend_torquay (1),night (6)\n59 t 4: ,torquay_twilight (4),weekend_torquay (2),night (4)\n1 t 4: ,truro_twilight (4),weekend_truro (2),night (4)\n10 t 4: ,truro_twilight (4),weekend_exeter (1),weekend_truro (2),night (4)\n11 t 4: ,truro_twilight (4),weekend_truro (2),night (4)\n12 t 4: ,truro_twilight (4),weekend_truro (1),night (6)\n13 t 4: ,truro_twilight (4),weekend_truro (2),night (4)\n14 t 4: ,truro_twilight (5),weekend_truro (2),night (3)\n2 t 4: ,truro_twilight (4),weekend_truro (1),night (6)\n3 t 4: ,truro_twilight (5),weekend_truro (2),night (4)\n4 t 4: ,truro_twilight (4),torquay_twilight (1),weekend_truro (2),night (4)\n5 t 4: ,truro_twilight (4),weekend_truro (1),night (6)\n6 t 4: ,truro_twilight (4),weekend_truro (1),night (6)\n7 t 4: ,truro_twilight (5),weekend_truro (2),night (3)\n8 t 4: ,truro_twilight (4),weekend_truro (2),night (3)\n9 t 4: ,truro_twilight (5),weekend_truro (2),night (4)\n" + "text": "30 e 4: ,exeter_twilight (6),weekend_exeter (3),night (5)\n31 e 4: ,exeter_twilight (5),weekend_exeter (2),night (6)\n32 e 4: ,exeter_twilight (6),weekend_exeter (2),night (6)\n33 e 4: ,exeter_twilight (5),weekend_exeter (2),night (6)\n34 e 4: ,exeter_twilight (5),weekend_exeter (2),night (6)\n35 e 4: ,exeter_twilight (5),weekend_exeter (2),night (6)\n36 e 4: ,exeter_twilight (5),weekend_exeter (2),night (6)\n37 e 4: ,exeter_twilight (5),weekend_exeter (2),night (6)\n38 e 4: ,exeter_twilight (5),weekend_exeter (2),night (6)\n39 e 4: ,exeter_twilight (6),weekend_exeter (3),night (5)\n40 e 4: ,exeter_twilight (6),weekend_exeter (2),night (5)\n41 e 4: ,exeter_twilight (5),weekend_exeter (2),night (6)\n42 e 4: ,exeter_twilight (6),weekend_exeter (2),night (5)\n43 e 4: ,exeter_twilight (5),weekend_exeter (2),night (6)\n44 e 4: ,exeter_twilight (5),weekend_exeter (2),night (6)\n15 p 4: ,plymouth_twilight (6),weekend_plymouth (5),night (5)\n16 p 4: ,plymouth_twilight (6),weekend_plymouth (5),night (5)\n17 p 4: ,plymouth_twilight (5),weekend_plymouth (4),night (6)\n18 p 4: ,plymouth_twilight (5),weekend_plymouth (4),night (6)\n19 p 4: ,plymouth_twilight (5),weekend_plymouth (4),night (6)\n20 p 4: ,plymouth_twilight (5),weekend_plymouth (4),night (6)\n21 p 4: ,plymouth_twilight (5),weekend_plymouth (4),night (6)\n22 p 4: ,plymouth_twilight (6),weekend_plymouth (4),night (6)\n23 p 4: ,plymouth_twilight (6),weekend_plymouth (5),night (5)\n24 p 4: ,plymouth_twilight (5),weekend_plymouth (4),night (6)\n25 p 4: ,plymouth_twilight (5),weekend_plymouth (4),night (6)\n26 p 4: ,plymouth_twilight (6),weekend_plymouth (5),night (5)\n27 p 4: ,plymouth_twilight (5),weekend_plymouth (4),night (6)\n28 p 4: ,plymouth_twilight (5),weekend_plymouth (4),night (6)\n29 p 4: ,plymouth_twilight (5),weekend_plymouth (4),night (6)\n45 t 4: ,torquay_twilight (5),weekend_torquay (2),night (6)\n46 t 4: ,torquay_twilight (5),weekend_torquay (2),night (6)\n47 t 4: ,torquay_twilight (6),weekend_torquay (2),night (6)\n48 t 4: ,torquay_twilight (5),weekend_torquay (2),night (6)\n49 t 4: ,torquay_twilight (5),weekend_torquay (2),night (6)\n50 t 4: ,torquay_twilight (5),weekend_torquay (2),night (6)\n51 t 4: ,torquay_twilight (6),weekend_torquay (2),night (5)\n52 t 4: ,torquay_twilight (5),weekend_torquay (2),night (6)\n53 t 4: ,torquay_twilight (5),weekend_torquay (2),night (6)\n54 t 4: ,torquay_twilight (5),weekend_torquay (2),night (6)\n55 t 4: ,torquay_twilight (6),weekend_torquay (3),night (5)\n56 t 4: ,torquay_twilight (6),weekend_torquay (3),night (5)\n57 t 4: ,torquay_twilight (5),weekend_torquay (2),night (6)\n58 t 4: ,torquay_twilight (6),weekend_torquay (2),night (6)\n59 t 4: ,torquay_twilight (5),weekend_torquay (2),night (6)\n1 t 4: ,truro_twilight (6),weekend_truro (2),night (6)\n10 t 4: ,truro_twilight (6),weekend_truro (3),night (5)\n11 t 4: ,truro_twilight (5),weekend_truro (2),night (6)\n12 t 4: ,truro_twilight (6),weekend_truro (2),night (6)\n13 t 4: ,truro_twilight (6),weekend_truro (2),night (5)\n14 t 4: ,truro_twilight (6),weekend_truro (2),night (5)\n2 t 4: ,truro_twilight (6),weekend_truro (3),night (5)\n3 t 4: ,truro_twilight (6),weekend_truro (2),night (5)\n4 t 4: ,truro_twilight (5),weekend_truro (2),night (6)\n5 t 4: ,truro_twilight (6),weekend_truro (2),night (6)\n6 t 4: ,truro_twilight (6),weekend_truro (3),night (5)\n7 t 4: ,truro_twilight (5),weekend_truro (2),night (6)\n8 t 4: ,truro_twilight (6),weekend_truro (3),night (5)\n9 t 4: ,truro_twilight (5),weekend_truro (2),night (6)\n" } ], "source": [ @@ -3175,16 +2520,16 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 101, "metadata": {}, "outputs": [ { "output_type": "execute_result", "data": { - "text/plain": "light': ['9 t 4'],\n 'torquay_twilight': ['58 t 4'],\n 'plymouth_twilight': ['27 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['42 e 4', '54 t 4', '59 t 4']},\n 'Thu': {'exeter_twilight': ['38 e 4'],\n 'truro_twilight': ['4 t 4'],\n 'torquay_twilight': ['50 t 4'],\n 'plymouth_twilight': ['22 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['42 e 4', '54 t 4', '59 t 4']},\n 'Fri': {'exeter_twilight': ['34 e 4'],\n 'truro_twilight': ['2 t 4'],\n 'torquay_twilight': ['56 t 4'],\n 'plymouth_twilight': ['25 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['35 e 4', '40 e 4', '8 t 4']},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['39 e 4', '10 t 4'],\n 'weekend_truro': ['13 t 4'],\n 'weekend_torquay': ['53 t 4'],\n 'weekend_plymouth': ['19 p 4', '26 p 4'],\n 'night': ['35 e 4', '40 e 4', '8 t 4']},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['33 e 4'],\n 'weekend_truro': ['3 t 4'],\n 'weekend_torquay': ['47 t 4'],\n 'weekend_plymouth': ['16 p 4', '24 p 4'],\n 'night': ['35 e 4', '40 e 4', '8 t 4']}},\n 4: {'Mon': {'exeter_twilight': ['42 e 4'],\n 'truro_twilight': ['1 t 4'],\n 'torquay_twilight': ['52 t 4'],\n 'plymouth_twilight': ['29 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['36 e 4', '37 e 4', '50 t 4']},\n 'Tue': {'exeter_twilight': ['44 e 4'],\n 'truro_twilight': ['5 t 4'],\n 'torquay_twilight': ['51 t 4'],\n 'plymouth_twilight': ['21 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['36 e 4', '37 e 4', '50 t 4']},\n 'Wed': {'exeter_twilight': ['35 e 4'],\n 'truro_twilight': ['6 t 4'],\n 'torquay_twilight': ['56 t 4'],\n 'plymouth_twilight': ['22 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['36 e 4', '37 e 4', '50 t 4']},\n 'Thu': {'exeter_twilight': ['31 e 4'],\n 'truro_twilight': ['14 t 4'],\n 'torquay_twilight': ['45 t 4'],\n 'plymouth_twilight': ['19 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['36 e 4', '37 e 4', '50 t 4']},\n 'Fri': {'exeter_twilight': ['34 e 4'],\n 'truro_twilight': ['9 t 4'],\n 'torquay_twilight': ['59 t 4'],\n 'plymouth_twilight': ['26 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['57 t 4', '58 t 4', '5 t 4']},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['32 e 4'],\n 'weekend_truro': ['11 t 4'],\n 'weekend_torquay': ['49 t 4'],\n 'weekend_plymouth': ['16 p 4', '21 p 4'],\n 'night': ['57 t 4', '58 t 4', '5 t 4']},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['35 e 4'],\n 'weekend_truro': ['3 t 4'],\n 'weekend_torquay': ['55 t 4'],\n 'weekend_plymouth': ['20 p 4', '23 p 4'],\n 'night': ['57 t 4', '58 t 4', '5 t 4']}},\n 5: {'Mon': {'exeter_twilight': ['38 e 4'],\n 'truro_twilight': ['2 t 4'],\n 'torquay_twilight': ['53 t 4'],\n 'plymouth_twilight': ['27 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['17 p 4', '46 t 4', '52 t 4']},\n 'Tue': {'exeter_twilight': ['33 e 4'],\n 'truro_twilight': ['8 t 4'],\n 'torquay_twilight': ['50 t 4'],\n 'plymouth_twilight': ['24 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['17 p 4', '46 t 4', '52 t 4']},\n 'Wed': {'exeter_twilight': ['43 e 4'],\n 'truro_twilight': ['11 t 4'],\n 'torquay_twilight': ['51 t 4'],\n 'plymouth_twilight': ['28 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['17 p 4', '46 t 4', '52 t 4']},\n 'Thu': {'exeter_twilight': ['35 e 4'],\n 'truro_twilight': ['6 t 4'],\n 'torquay_twilight': ['58 t 4'],\n 'plymouth_twilight': ['18 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['17 p 4', '46 t 4', '52 t 4']},\n 'Fri': {'exeter_twilight': ['40 e 4'],\n 'truro_twilight': ['3 t 4'],\n 'torquay_twilight': ['53 t 4'],\n 'plymouth_twilight': ['25 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['21 p 4', '29 p 4', '56 t 4']},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['41 e 4'],\n 'weekend_truro': ['5 t 4'],\n 'weekend_torquay': ['54 t 4'],\n 'weekend_plymouth': ['22 p 4', '27 p 4'],\n 'night': ['21 p 4', '29 p 4', '56 t 4']},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['39 e 4'],\n 'weekend_truro': ['7 t 4'],\n 'weekend_torquay': ['51 t 4'],\n 'weekend_plymouth': ['24 p 4', '28 p 4'],\n 'night': ['21 p 4', '29 p 4', '56 t 4']}},\n 6: {'Mon': {'exeter_twilight': ['44 e 4'],\n 'truro_twilight': ['11 t 4'],\n 'torquay_twilight': ['52 t 4'],\n 'plymouth_twilight': ['19 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['33 e 4', '15 p 4', '23 p 4']},\n 'Tue': {'exeter_twilight': ['31 e 4'],\n 'truro_twilight': ['1 t 4'],\n 'torquay_twilight': ['55 t 4'],\n 'plymouth_twilight': ['20 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['33 e 4', '15 p 4', '23 p 4']},\n 'Wed': {'exeter_twilight': ['36 e 4'],\n 'truro_twilight': ['6 t 4'],\n 'torquay_twilight': ['47 t 4'],\n 'plymouth_twilight': ['25 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['33 e 4', '15 p 4', '23 p 4']},\n 'Thu': {'exeter_twilight': ['40 e 4'],\n 'truro_twilight': ['7 t 4'],\n 'torquay_twilight': ['48 t 4'],\n 'plymouth_twilight': ['22 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['33 e 4', '15 p 4', '23 p 4']},\n 'Fri': {'exeter_twilight': ['32 e 4'],\n 'truro_twilight': ['4 t 4'],\n 'torquay_twilight': ['49 t 4'],\n 'plymouth_twilight': ['16 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['39 e 4', '27 p 4', '55 t 4']},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['34 e 4'],\n 'weekend_truro': ['8 t 4'],\n 'weekend_torquay': ['52 t 4'],\n 'weekend_plymouth': ['18 p 4', '28 p 4'],\n 'night': ['39 e 4', '27 p 4', '55 t 4']},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['38 e 4'],\n 'weekend_truro': ['2 t 4'],\n 'weekend_torquay': ['57 t 4'],\n 'weekend_plymouth': ['25 p 4', '26 p 4'],\n 'night': ['39 e 4', '27 p 4', '55 t 4']}},\n 7: {'Mon': {'exeter_twilight': ['30 e 4'],\n 'truro_twilight': ['7 t 4'],\n 'torquay_twilight': ['47 t 4'],\n 'plymouth_twilight': ['15 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['41 e 4', '18 p 4', '51 t 4']},\n 'Tue': {'exeter_twilight': ['43 e 4'],\n 'truro_twilight': ['14 t 4'],\n 'torquay_twilight': ['46 t 4'],\n 'plymouth_twilight': ['23 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['41 e 4', '18 p 4', '51 t 4']},\n 'Wed': {'exeter_twilight': ['37 e 4'],\n 'truro_twilight': ['3 t 4'],\n 'torquay_twilight': ['54 t 4'],\n 'plymouth_twilight': ['20 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['41 e 4', '18 p 4', '51 t 4']},\n 'Thu': {'exeter_twilight': ['36 e 4'],\n 'truro_twilight': ['12 t 4'],\n 'torquay_twilight': ['58 t 4'],\n 'plymouth_twilight': ['28 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['41 e 4', '18 p 4', '51 t 4']},\n 'Fri': {'exeter_twilight': ['39 e 4'],\n 'truro_twilight': ['13 t 4'],\n 'torquay_twilight': ['47 t 4'],\n 'plymouth_twilight': ['26 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['31 e 4', '43 e 4', '48 t 4']},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['35 e 4'],\n 'weekend_truro': ['10 t 4'],\n 'weekend_torquay': ['53 t 4'],\n 'weekend_plymouth': ['23 p 4', '25 p 4'],\n 'night': ['31 e 4', '43 e 4', '48 t 4']},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['44 e 4'],\n 'weekend_truro': ['14 t 4'],\n 'weekend_torquay': ['56 t 4'],\n 'weekend_plymouth': ['15 p 4', '21 p 4'],\n 'night': ['31 e 4', '43 e 4', '48 t 4']}},\n 8: {'Mon': {'exeter_twilight': ['42 e 4'],\n 'truro_twilight': ['3 t 4'],\n 'torquay_twilight': ['55 t 4'],\n 'plymouth_twilight': ['22 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['25 p 4', '49 t 4', '53 t 4']},\n 'Tue': {'exeter_twilight': ['33 e 4'],\n 'truro_twilight': ['1 t 4'],\n 'torquay_twilight': ['50 t 4'],\n 'plymouth_twilight': ['18 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['25 p 4', '49 t 4', '53 t 4']},\n 'Wed': {'exeter_twilight': ['34 e 4'],\n 'truro_twilight': ['9 t 4'],\n 'torquay_twilight': ['48 t 4'],\n 'plymouth_twilight': ['19 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['25 p 4', '49 t 4', '53 t 4']},\n 'Thu': {'exeter_twilight': ['38 e 4'],\n 'truro_twilight': ['13 t 4'],\n 'torquay_twilight': ['47 t 4'],\n 'plymouth_twilight': ['24 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['25 p 4', '49 t 4', '53 t 4']},\n 'Fri': {'exeter_twilight': ['30 e 4'],\n 'truro_twilight': ['10 t 4'],\n 'torquay_twilight': ['54 t 4'],\n 'plymouth_twilight': ['20 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['27 p 4', '57 t 4', '6 t 4']},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['36 e 4'],\n 'weekend_truro': ['1 t 4'],\n 'weekend_torquay': ['50 t 4'],\n 'weekend_plymouth': ['18 p 4', '29 p 4'],\n 'night': ['27 p 4', '57 t 4', '6 t 4']},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['32 e 4'],\n 'weekend_truro': ['11 t 4'],\n 'weekend_torquay': ['46 t 4'],\n 'weekend_plymouth': ['17 p 4', '19 p 4'],\n 'night': ['27 p 4', '57 t 4', '6 t 4']}},\n 9: {'Mon': {'exeter_twilight': ['39 e 4'],\n 'truro_twilight': ['14 t 4'],\n 'torquay_twilight': ['45 t 4'],\n 'plymouth_twilight': ['26 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['44 e 4', '24 p 4', '4 t 4']},\n 'Tue': {'exeter_twilight': ['43 e 4'],\n 'truro_twilight': ['10 t 4'],\n 'torquay_twilight': ['54 t 4'],\n 'plymouth_twilight': ['21 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['44 e 4', '24 p 4', '4 t 4']},\n 'Wed': {'exeter_twilight': ['35 e 4'],\n 'truro_twilight': ['7 t 4'],\n 'torquay_twilight': ['52 t 4'],\n 'plymouth_twilight': ['29 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['44 e 4', '24 p 4', '4 t 4']},\n 'Thu': {'exeter_twilight': ['32 e 4'],\n 'truro_twilight': ['11 t 4'],\n 'torquay_twilight': ['46 t 4'],\n 'plymouth_twilight': ['23 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['44 e 4', '24 p 4', '4 t 4']},\n 'Fri': {'exeter_twilight': ['42 e 4'],\n 'truro_twilight': ['12 t 4'],\n 'torquay_twilight': ['45 t 4'],\n 'plymouth_twilight': ['20 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['16 p 4', '5 t 4', '7 t 4']},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['37 e 4'],\n 'weekend_truro': ['14 t 4'],\n 'weekend_torquay': ['59 t 4'],\n 'weekend_plymouth': ['27 p 4', '28 p 4'],\n 'night': ['16 p 4', '5 t 4', '7 t 4']},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['34 e 4'],\n 'weekend_truro': ['9 t 4'],\n 'weekend_torquay': ['50 t 4'],\n 'weekend_plymouth': ['15 p 4', '17 p 4'],\n 'night': ['16 p 4', '5 t 4', '7 t 4']}},\n 10: {'Mon': {'exeter_twilight': ['30 e 4'],\n 'truro_twilight': ['6 t 4'],\n 'torquay_twilight': ['49 t 4'],\n 'plymouth_twilight': ['29 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['32 e 4', '22 p 4', '28 p 4']},\n 'Tue': {'exeter_twilight': ['40 e 4'],\n 'truro_twilight': ['13 t 4'],\n 'torquay_twilight': ['53 t 4'],\n 'plymouth_twilight': ['26 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['32 e 4', '22 p 4', '28 p 4']},\n 'Wed': {'exeter_twilight': ['37 e 4'],\n 'truro_twilight': ['2 t 4'],\n 'torquay_twilight': ['48 t 4'],\n 'plymouth_twilight': ['18 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['32 e 4', '22 p 4', '28 p 4']},\n 'Thu': {'exeter_twilight': ['44 e 4'],\n 'truro_twilight': ['3 t 4'],\n 'torquay_twilight': ['51 t 4'],\n 'plymouth_twilight': ['25 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['32 e 4', '22 p 4', '28 p 4']},\n 'Fri': {'exeter_twilight': ['39 e 4'],\n 'truro_twilight': ['7 t 4'],\n 'torquay_twilight': ['54 t 4'],\n 'plymouth_twilight': ['24 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['38 e 4', '29 p 4', '55 t 4']},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['30 e 4'],\n 'weekend_truro': ['12 t 4'],\n 'weekend_torquay': ['56 t 4'],\n 'weekend_plymouth': ['19 p 4', '23 p 4'],\n 'night': ['38 e 4', '29 p 4', '55 t 4']},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['43 e 4'],\n 'weekend_truro': ['13 t 4'],\n 'weekend_torquay': ['45 t 4'],\n 'weekend_plymouth': ['20 p 4', '22 p 4'],\n 'night': ['38 e 4', '29 p 4', '55 t 4']}},\n 11: {'Mon': {'exeter_twilight': ['31 e 4'],\n 'truro_twilight': ['4 t 4'],\n 'torquay_twilight': ['59 t 4'],\n 'plymouth_twilight': ['16 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['30 e 4', '19 p 4', '47 t 4']},\n 'Tue': {'exeter_twilight': ['37 e 4'],\n 'truro_twilight': ['9 t 4'],\n 'torquay_twilight': ['51 t 4'],\n 'plymouth_twilight': ['21 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['30 e 4', '19 p 4', '47 t 4']},\n 'Wed': {'exeter_twilight': ['41 e 4'],\n 'truro_twilight': ['10 t 4'],\n 'torquay_twilight': ['54 t 4'],\n 'plymouth_twilight': ['17 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['30 e 4', '19 p 4', '47 t 4']},\n 'Thu': {'exeter_twilight': ['39 e 4'],\n 'truro_twilight': ['13 t 4'],\n 'torquay_twilight': ['56 t 4'],\n 'plymouth_twilight': ['20 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['30 e 4', '19 p 4', '47 t 4']},\n 'Fri': {'exeter_twilight': ['42 e 4'],\n 'truro_twilight': ['3 t 4'],\n 'torquay_twilight': ['59 t 4'],\n 'plymouth_twilight': ['27 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['31 e 4', '48 t 4', '12 t 4']},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['40 e 4'],\n 'weekend_truro': ['8 t 4'],\n 'weekend_torquay': ['49 t 4'],\n 'weekend_plymouth': ['22 p 4', '24 p 4'],\n 'night': ['31 e 4', '48 t 4', '12 t 4']},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['41 e 4'],\n 'weekend_truro': ['4 t 4'],\n 'weekend_torquay': ['58 t 4'],\n 'weekend_plymouth': ['17 p 4', '18 p 4'],\n 'night': ['31 e 4', '48 t 4', '12 t 4']}},\n 12: {'Mon': {'exeter_twilight': ['43 e 4'],\n 'truro_twilight': ['5 t 4'],\n 'torquay_twilight': ['57 t 4'],\n 'plymouth_twilight': ['16 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['10 t 4', '11 t 4', '9 t 4']},\n 'Tue': {'exeter_twilight': ['42 e 4'],\n 'truro_twilight': ['14 t 4'],\n 'torquay_twilight': ['50 t 4'],\n 'plymouth_twilight': ['15 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['10 t 4', '11 t 4', '9 t 4']},\n 'Wed': {'exeter_twilight': ['33 e 4'],\n 'truro_twilight': ['8 t 4'],\n 'torquay_twilight': ['56 t 4'],\n 'plymouth_twilight': ['28 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['10 t 4', '11 t 4', '9 t 4']},\n 'Thu': {'exeter_twilight': ['36 e 4'],\n 'truro_twilight': ['12 t 4'],\n 'torquay_twilight': ['46 t 4'],\n 'plymouth_twilight': ['24 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['10 t 4', '11 t 4', '9 t 4']},\n 'Fri': {'exeter_twilight': ['41 e 4'],\n 'truro_twilight': ['7 t 4'],\n 'torquay_twilight': ['57 t 4'],\n 'plymouth_twilight': ['17 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['14 t 4', '2 t 4', '6 t 4']},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['44 e 4'],\n 'weekend_truro': ['1 t 4'],\n 'weekend_torquay': ['59 t 4'],\n 'weekend_plymouth': ['15 p 4', '16 p 4'],\n 'night': ['14 t 4', '2 t 4', '6 t 4']},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['43 e 4'],\n 'weekend_truro': ['56 t 4', '10 t 4'],\n 'weekend_torquay': ['52 t 4'],\n 'weekend_plymouth': ['21 p 4', '22 p 4', '45 t 4'],\n 'night': ['14 t 4', '2 t 4', '6 t 4']}}}" + "text/plain": "\n 'truro_twilight': ['10 t 4'],\n 'torquay_twilight': ['47 t 4'],\n 'plymouth_twilight': ['29 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['40 e 4', '46 t 4', '1 t 4']},\n 'Thu': {'exeter_twilight': ['36 e 4'],\n 'truro_twilight': ['3 t 4'],\n 'torquay_twilight': ['59 t 4'],\n 'plymouth_twilight': ['21 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['23 p 4', '29 p 4', '7 t 4']},\n 'Fri': {'exeter_twilight': ['36 e 4'],\n 'truro_twilight': ['3 t 4'],\n 'torquay_twilight': ['47 t 4'],\n 'plymouth_twilight': ['28 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['31 e 4', '50 t 4', '53 t 4']},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['43 e 4'],\n 'weekend_truro': ['6 t 4'],\n 'weekend_torquay': ['52 t 4'],\n 'weekend_plymouth': ['22 p 4', '25 p 4'],\n 'night': ['55 t 4', '57 t 4', '11 t 4']},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['32 e 4'],\n 'weekend_truro': ['12 t 4'],\n 'weekend_torquay': ['55 t 4'],\n 'weekend_plymouth': ['21 p 4', '22 p 4'],\n 'night': ['35 e 4', '50 t 4', '3 t 4']}},\n 8: {'Mon': {'exeter_twilight': ['35 e 4'],\n 'truro_twilight': ['5 t 4'],\n 'torquay_twilight': ['56 t 4'],\n 'plymouth_twilight': ['22 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['23 p 4', '27 p 4', '52 t 4']},\n 'Tue': {'exeter_twilight': ['40 e 4'],\n 'truro_twilight': ['8 t 4'],\n 'torquay_twilight': ['51 t 4'],\n 'plymouth_twilight': ['23 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['15 p 4', '16 p 4', '49 t 4']},\n 'Wed': {'exeter_twilight': ['44 e 4'],\n 'truro_twilight': ['13 t 4'],\n 'torquay_twilight': ['48 t 4'],\n 'plymouth_twilight': ['18 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['16 p 4', '27 p 4', '52 t 4']},\n 'Thu': {'exeter_twilight': ['38 e 4'],\n 'truro_twilight': ['3 t 4'],\n 'torquay_twilight': ['51 t 4'],\n 'plymouth_twilight': ['24 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['35 e 4', '22 p 4', '9 t 4']},\n 'Fri': {'exeter_twilight': ['42 e 4'],\n 'truro_twilight': ['10 t 4'],\n 'torquay_twilight': ['56 t 4'],\n 'plymouth_twilight': ['26 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['30 e 4', '18 p 4', '25 p 4']},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['31 e 4'],\n 'weekend_truro': ['6 t 4'],\n 'weekend_torquay': ['59 t 4'],\n 'weekend_plymouth': ['25 p 4', '29 p 4'],\n 'night': ['47 t 4', '51 t 4', '57 t 4']},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['30 e 4'],\n 'weekend_truro': ['14 t 4'],\n 'weekend_torquay': ['56 t 4'],\n 'weekend_plymouth': ['15 p 4', '24 p 4'],\n 'night': ['41 e 4', '50 t 4', '11 t 4']}},\n 9: {'Mon': {'exeter_twilight': ['35 e 4'],\n 'truro_twilight': ['2 t 4'],\n 'torquay_twilight': ['56 t 4'],\n 'plymouth_twilight': ['22 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['34 e 4', '19 p 4', '45 t 4']},\n 'Tue': {'exeter_twilight': ['40 e 4'],\n 'truro_twilight': ['5 t 4'],\n 'torquay_twilight': ['57 t 4'],\n 'plymouth_twilight': ['29 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['19 p 4', '20 p 4', '48 t 4']},\n 'Wed': {'exeter_twilight': ['31 e 4'],\n 'truro_twilight': ['14 t 4'],\n 'torquay_twilight': ['55 t 4'],\n 'plymouth_twilight': ['27 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['15 p 4', '11 t 4', '12 t 4']},\n 'Thu': {'exeter_twilight': ['33 e 4'],\n 'truro_twilight': ['9 t 4'],\n 'torquay_twilight': ['58 t 4'],\n 'plymouth_twilight': ['28 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['32 e 4', '16 p 4', '59 t 4']},\n 'Fri': {'exeter_twilight': ['33 e 4'],\n 'truro_twilight': ['5 t 4'],\n 'torquay_twilight': ['46 t 4'],\n 'plymouth_twilight': ['18 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['37 e 4', '44 e 4', '29 p 4']},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['39 e 4'],\n 'weekend_truro': ['10 t 4'],\n 'weekend_torquay': ['52 t 4'],\n 'weekend_plymouth': ['26 p 4', '27 p 4'],\n 'night': ['37 e 4', '45 t 4', '53 t 4']},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['39 e 4'],\n 'weekend_truro': ['13 t 4'],\n 'weekend_torquay': ['48 t 4'],\n 'weekend_plymouth': ['18 p 4', '19 p 4'],\n 'night': ['36 e 4', '41 e 4', '5 t 4']}},\n 10: {'Mon': {'exeter_twilight': ['35 e 4'],\n 'truro_twilight': ['3 t 4'],\n 'torquay_twilight': ['50 t 4'],\n 'plymouth_twilight': ['24 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['34 e 4', '17 p 4', '27 p 4']},\n 'Tue': {'exeter_twilight': ['39 e 4'],\n 'truro_twilight': ['11 t 4'],\n 'torquay_twilight': ['49 t 4'],\n 'plymouth_twilight': ['16 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['40 e 4', '43 e 4', '53 t 4']},\n 'Wed': {'exeter_twilight': ['38 e 4'],\n 'truro_twilight': ['9 t 4'],\n 'torquay_twilight': ['53 t 4'],\n 'plymouth_twilight': ['22 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['16 p 4', '26 p 4', '27 p 4']},\n 'Thu': {'exeter_twilight': ['43 e 4'],\n 'truro_twilight': ['6 t 4'],\n 'torquay_twilight': ['51 t 4'],\n 'plymouth_twilight': ['17 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['39 e 4', '29 p 4', '14 t 4']},\n 'Fri': {'exeter_twilight': ['36 e 4'],\n 'truro_twilight': ['14 t 4'],\n 'torquay_twilight': ['52 t 4'],\n 'plymouth_twilight': ['22 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['21 p 4', '25 p 4', '54 t 4']},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['30 e 4'],\n 'weekend_truro': ['7 t 4'],\n 'weekend_torquay': ['51 t 4'],\n 'weekend_plymouth': ['26 p 4', '29 p 4'],\n 'night': ['33 e 4', '13 t 4', '9 t 4']},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['38 e 4'],\n 'weekend_truro': ['4 t 4'],\n 'weekend_torquay': ['51 t 4'],\n 'weekend_plymouth': ['15 p 4', '22 p 4'],\n 'night': ['44 e 4', '18 p 4', '3 t 4']}},\n 11: {'Mon': {'exeter_twilight': ['32 e 4'],\n 'truro_twilight': ['4 t 4'],\n 'torquay_twilight': ['50 t 4'],\n 'plymouth_twilight': ['22 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['25 p 4', '26 p 4', '58 t 4']},\n 'Tue': {'exeter_twilight': ['37 e 4'],\n 'truro_twilight': ['13 t 4'],\n 'torquay_twilight': ['54 t 4'],\n 'plymouth_twilight': ['21 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['38 e 4', '16 p 4', '47 t 4']},\n 'Wed': {'exeter_twilight': ['38 e 4'],\n 'truro_twilight': ['7 t 4'],\n 'torquay_twilight': ['59 t 4'],\n 'plymouth_twilight': ['19 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['24 p 4', '29 p 4', '14 t 4']},\n 'Thu': {'exeter_twilight': ['42 e 4'],\n 'truro_twilight': ['2 t 4'],\n 'torquay_twilight': ['58 t 4'],\n 'plymouth_twilight': ['27 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['30 e 4', '28 p 4', '9 t 4']},\n 'Fri': {'exeter_twilight': ['30 e 4'],\n 'truro_twilight': ['12 t 4'],\n 'torquay_twilight': ['46 t 4'],\n 'plymouth_twilight': ['16 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['43 e 4', '59 t 4', '7 t 4']},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['37 e 4'],\n 'weekend_truro': ['3 t 4'],\n 'weekend_torquay': ['54 t 4'],\n 'weekend_plymouth': ['18 p 4', '20 p 4'],\n 'night': ['38 e 4', '52 t 4', '56 t 4']},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['42 e 4'],\n 'weekend_truro': ['14 t 4'],\n 'weekend_torquay': ['47 t 4'],\n 'weekend_plymouth': ['23 p 4', '26 p 4'],\n 'night': ['33 e 4', '28 p 4', '56 t 4']}},\n 12: {'Mon': {'exeter_twilight': ['40 e 4'],\n 'truro_twilight': ['9 t 4'],\n 'torquay_twilight': ['49 t 4'],\n 'plymouth_twilight': ['28 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['33 e 4', '25 p 4', '6 t 4']},\n 'Tue': {'exeter_twilight': ['31 e 4'],\n 'truro_twilight': ['12 t 4'],\n 'torquay_twilight': ['50 t 4'],\n 'plymouth_twilight': ['25 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['33 e 4', '54 t 4', '58 t 4']},\n 'Wed': {'exeter_twilight': ['39 e 4'],\n 'truro_twilight': ['12 t 4'],\n 'torquay_twilight': ['59 t 4'],\n 'plymouth_twilight': ['29 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['21 p 4', '1 t 4', '14 t 4']},\n 'Thu': {'exeter_twilight': ['40 e 4'],\n 'truro_twilight': ['4 t 4'],\n 'torquay_twilight': ['46 t 4'],\n 'plymouth_twilight': ['17 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['24 p 4', '49 t 4', '51 t 4']},\n 'Fri': {'exeter_twilight': ['41 e 4'],\n 'truro_twilight': ['6 t 4'],\n 'torquay_twilight': ['53 t 4'],\n 'plymouth_twilight': ['25 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['20 p 4', '49 t 4', '59 t 4']},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['32 e 4'],\n 'weekend_truro': ['11 t 4'],\n 'weekend_torquay': ['59 t 4'],\n 'weekend_plymouth': ['24 p 4', '28 p 4'],\n 'night': ['17 p 4', '51 t 4', '8 t 4']},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['44 e 4'],\n 'weekend_truro': ['3 t 4'],\n 'weekend_torquay': ['55 t 4'],\n 'weekend_plymouth': ['21 p 4', '27 p 4'],\n 'night': ['22 p 4', '25 p 4', '8 t 4']}},\n 13: {'Mon': {'exeter_twilight': ['43 e 4'],\n 'truro_twilight': ['14 t 4'],\n 'torquay_twilight': ['55 t 4'],\n 'plymouth_twilight': ['22 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['32 e 4', '19 p 4', '58 t 4']},\n 'Tue': {'exeter_twilight': ['34 e 4'],\n 'truro_twilight': ['6 t 4'],\n 'torquay_twilight': ['51 t 4'],\n 'plymouth_twilight': ['26 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['31 e 4', '45 t 4', '47 t 4']},\n 'Wed': {'exeter_twilight': ['43 e 4'],\n 'truro_twilight': ['12 t 4'],\n 'torquay_twilight': ['56 t 4'],\n 'plymouth_twilight': ['26 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['36 e 4', '42 e 4', '52 t 4']},\n 'Thu': {'exeter_twilight': ['32 e 4'],\n 'truro_twilight': ['9 t 4'],\n 'torquay_twilight': ['49 t 4'],\n 'plymouth_twilight': ['29 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['33 e 4', '58 t 4', '7 t 4']},\n 'Fri': {'exeter_twilight': ['32 e 4'],\n 'truro_twilight': ['10 t 4'],\n 'torquay_twilight': ['46 t 4'],\n 'plymouth_twilight': ['16 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['38 e 4', '44 e 4', '56 t 4']},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['36 e 4'],\n 'weekend_truro': ['5 t 4'],\n 'weekend_torquay': ['53 t 4'],\n 'weekend_plymouth': ['16 p 4', '28 p 4'],\n 'night': ['35 e 4', '44 e 4', '26 p 4']},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['41 e 4'],\n 'weekend_truro': ['8 t 4'],\n 'weekend_torquay': ['46 t 4'],\n 'weekend_plymouth': ['22 p 4', '26 p 4'],\n 'night': ['28 p 4', '48 t 4', '49 t 4']}},\n 14: {'Mon': {'exeter_twilight': ['32 e 4'],\n 'truro_twilight': ['13 t 4'],\n 'torquay_twilight': ['53 t 4'],\n 'plymouth_twilight': ['18 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['17 p 4', '28 p 4', '48 t 4']},\n 'Tue': {'exeter_twilight': ['42 e 4'],\n 'truro_twilight': ['14 t 4'],\n 'torquay_twilight': ['53 t 4'],\n 'plymouth_twilight': ['21 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['20 p 4', '24 p 4', '28 p 4']},\n 'Wed': {'exeter_twilight': ['44 e 4'],\n 'truro_twilight': ['8 t 4'],\n 'torquay_twilight': ['45 t 4'],\n 'plymouth_twilight': ['16 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['34 e 4', '53 t 4', '13 t 4']},\n 'Thu': {'exeter_twilight': ['41 e 4'],\n 'truro_twilight': ['13 t 4'],\n 'torquay_twilight': ['53 t 4'],\n 'plymouth_twilight': ['15 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['32 e 4', '40 e 4', '20 p 4']},\n 'Fri': {'exeter_twilight': ['44 e 4'],\n 'truro_twilight': ['11 t 4'],\n 'torquay_twilight': ['45 t 4'],\n 'plymouth_twilight': ['21 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['41 e 4', '42 e 4', '46 t 4']},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['42 e 4'],\n 'weekend_truro': ['7 t 4'],\n 'weekend_torquay': ['53 t 4'],\n 'weekend_plymouth': ['24 p 4', '26 p 4'],\n 'night': ['43 e 4', '19 p 4', '47 t 4']},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['40 e 4'],\n 'weekend_truro': ['9 t 4'],\n 'weekend_torquay': ['56 t 4'],\n 'weekend_plymouth': ['21 p 4', '23 p 4'],\n 'night': ['51 t 4', '57 t 4', '3 t 4']}},\n 15: {'Mon': {'exeter_twilight': ['37 e 4'],\n 'truro_twilight': ['4 t 4'],\n 'torquay_twilight': ['50 t 4'],\n 'plymouth_twilight': ['21 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['22 p 4', '45 t 4', '9 t 4']},\n 'Tue': {'exeter_twilight': ['39 e 4'],\n 'truro_twilight': ['7 t 4'],\n 'torquay_twilight': ['54 t 4'],\n 'plymouth_twilight': ['23 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['30 e 4', '36 e 4', '10 t 4']},\n 'Wed': {'exeter_twilight': ['34 e 4'],\n 'truro_twilight': ['10 t 4'],\n 'torquay_twilight': ['55 t 4'],\n 'plymouth_twilight': ['27 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['36 e 4', '48 t 4', '12 t 4']},\n 'Thu': {'exeter_twilight': ['37 e 4'],\n 'truro_twilight': ['9 t 4'],\n 'torquay_twilight': ['52 t 4'],\n 'plymouth_twilight': ['15 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['32 e 4', '38 e 4', '13 t 4']},\n 'Fri': {'exeter_twilight': ['33 e 4'],\n 'truro_twilight': ['2 t 4'],\n 'torquay_twilight': ['48 t 4'],\n 'plymouth_twilight': ['28 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['43 e 4', '46 t 4', '54 t 4']},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['33 e 4'],\n 'weekend_truro': ['12 t 4'],\n 'weekend_torquay': ['45 t 4'],\n 'weekend_plymouth': ['19 p 4', '28 p 4'],\n 'night': ['36 e 4', '39 e 4', '48 t 4']},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['31 e 4'],\n 'weekend_truro': ['11 t 4'],\n 'weekend_torquay': ['56 t 4'],\n 'weekend_plymouth': ['17 p 4', '25 p 4'],\n 'night': ['21 p 4', '48 t 4', '54 t 4']}},\n 16: {'Mon': {'exeter_twilight': ['44 e 4'],\n 'truro_twilight': ['7 t 4'],\n 'torquay_twilight': ['55 t 4'],\n 'plymouth_twilight': ['27 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['18 p 4', '21 p 4', '10 t 4']},\n 'Tue': {'exeter_twilight': ['36 e 4'],\n 'truro_twilight': ['12 t 4'],\n 'torquay_twilight': ['58 t 4'],\n 'plymouth_twilight': ['24 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['22 p 4', '57 t 4', '7 t 4']},\n 'Wed': {'exeter_twilight': ['33 e 4'],\n 'truro_twilight': ['14 t 4'],\n 'torquay_twilight': ['48 t 4'],\n 'plymouth_twilight': ['25 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['43 e 4', '57 t 4', '1 t 4']},\n 'Thu': {'exeter_twilight': ['34 e 4'],\n 'truro_twilight': ['6 t 4'],\n 'torquay_twilight': ['56 t 4'],\n 'plymouth_twilight': ['23 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['41 e 4', '20 p 4', '10 t 4']},\n 'Fri': {'exeter_twilight': ['37 e 4'],\n 'truro_twilight': ['5 t 4'],\n 'torquay_twilight': ['49 t 4'],\n 'plymouth_twilight': ['19 p 4'],\n 'weekend_exeter': [],\n 'weekend_truro': [],\n 'weekend_torquay': [],\n 'weekend_plymouth': [],\n 'night': ['42 e 4', '43 e 4', '10 t 4']},\n 'Sat': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['40 e 4'],\n 'weekend_truro': ['10 t 4'],\n 'weekend_torquay': ['46 t 4'],\n 'weekend_plymouth': ['19 p 4', '25 p 4'],\n 'night': ['31 e 4', '5 t 4', '6 t 4']},\n 'Sun': {'exeter_twilight': [],\n 'truro_twilight': [],\n 'torquay_twilight': [],\n 'plymouth_twilight': [],\n 'weekend_exeter': ['37 e 4'],\n 'weekend_truro': ['10 t 4'],\n 'weekend_torquay': ['57 t 4'],\n 'weekend_plymouth': ['27 p 4', '29 p 4'],\n 'night': ['19 p 4', '28 p 4', '5 t 4']}}}" }, "metadata": {}, - "execution_count": 26 + "execution_count": 101 } ], "source": [ @@ -3193,7 +2538,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 102, "metadata": {}, "outputs": [], "source": [ @@ -3202,7 +2547,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 103, "metadata": {}, "outputs": [], "source": [ @@ -3211,7 +2556,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 104, "metadata": {}, "outputs": [ { @@ -3220,7 +2565,7 @@ "text/plain": "1" }, "metadata": {}, - "execution_count": 29 + "execution_count": 104 } ], "source": [ @@ -3229,7 +2574,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 105, "metadata": {}, "outputs": [ { @@ -3238,7 +2583,7 @@ "text/plain": "0" }, "metadata": {}, - "execution_count": 30 + "execution_count": 105 } ], "source": [ @@ -3247,7 +2592,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 106, "metadata": {}, "outputs": [], "source": [ @@ -3258,7 +2603,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 107, "metadata": {}, "outputs": [ { @@ -3268,7 +2613,7 @@ "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\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;32m\u001b[0m in \u001b[0;36m\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" ] } @@ -3279,7 +2624,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 108, "metadata": {}, "outputs": [ { @@ -3288,7 +2633,7 @@ "text/plain": "0" }, "metadata": {}, - "execution_count": 33 + "execution_count": 108 } ], "source": [ @@ -3297,7 +2642,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 109, "metadata": {}, "outputs": [ { @@ -3306,7 +2651,7 @@ "text/plain": "6" }, "metadata": {}, - "execution_count": 34 + "execution_count": 109 } ], "source": [ @@ -3315,16 +2660,19 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 110, "metadata": {}, "outputs": [ { - "output_type": "execute_result", - "data": { - "text/plain": "6.0" - }, - "metadata": {}, - "execution_count": 35 + "output_type": "error", + "ename": "NameError", + "evalue": "name 'model' is not defined", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mmodel\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mshift_count\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m5\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"night\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalue\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mNameError\u001b[0m: name 'model' is not defined" + ] } ], "source": [ @@ -3333,16 +2681,19 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 111, "metadata": {}, "outputs": [ { - "output_type": "execute_result", - "data": { - "text/plain": "[30,\n 31,\n 32,\n 33,\n 34,\n 35,\n 36,\n 37,\n 38,\n 39,\n 40,\n 41,\n 42,\n 43,\n 44,\n 15,\n 16,\n 17,\n 18,\n 19,\n 20,\n 21,\n 22,\n 23,\n 24,\n 25,\n 26,\n 27,\n 28,\n 29,\n 45,\n 46,\n 47,\n 48,\n 49,\n 50,\n 51,\n 52,\n 53,\n 54,\n 55,\n 56,\n 57,\n 58,\n 59,\n 1,\n 10,\n 11,\n 12,\n 13,\n 14,\n 2,\n 3,\n 4,\n 5,\n 6,\n 7,\n 8,\n 9]" - }, - "metadata": {}, - "execution_count": 36 + "output_type": "error", + "ename": "NameError", + "evalue": "name 'workers' is not defined", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0;34m[\u001b[0m\u001b[0mw\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mid\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mw\u001b[0m \u001b[0;32min\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[0m", + "\u001b[0;31mNameError\u001b[0m: name 'workers' is not defined" + ] } ], "source": [ diff --git a/shifts.py b/shifts.py index 1ffd75b..a13a7da 100644 --- a/shifts.py +++ b/shifts.py @@ -2,6 +2,11 @@ import datetime import itertools from typing import List, Tuple +from workers import Worker + +from pyomo.environ import * +from pyomo.opt import SolverFactory + ShiftName = str DayStr = str WeekInt = int @@ -48,7 +53,8 @@ class SingleShift(object): self.total_shifts = length * len(days) * len(shift_days) -class ShiftCollection(object): + +class RotaBuilder(object): """Class to hold and manipulate shifts""" def __init__(self, weeks_to_rota=26): @@ -64,6 +70,507 @@ class ShiftCollection(object): self.unavailable_to_work = set() + self.workers = [] + + self.night_blocks = ["weekday", "weekend", "none"] + + def build_model(self): + # Initialize model + self.model = ConcreteModel() + + # binary variables representing if a worker is scheduled somewhere + # NOTE: this will assign to all possible shift combinations (which we probably don't want) + self.model.works = Var( + ( + (worker.id, week, day, shift) + for worker in self.workers + for week, day in self.get_week_day_combinations() + for shift in self.get_shift_names() + ), + within=Binary, + initialize=0, + ) + + # The nights self.model is used to ensure nights are assigned as a block (and limit the number of workers required) + self.model.nights = Var( + ( + (worker.id, week, block) + for worker in self.workers + for week in self.weeks + for block in self.night_blocks + ), + within=Binary, + initialize=0, + ) + + self.model.shift_count = Var( + ( + (worker.id, shift) + for worker in self.workers + for shift in self.get_shift_names() + ), + domain=NonNegativeReals, + initialize=0, + ) + + # self.model.shift_count_t1 = Var(((worker.id, shift) for worker in self.workers for shift in self.get_shift_names()), domain=NonNegativeReals, initialize=0) + # self.model.shift_count_t2 = Var(((worker.id, shift) for worker in self.workers for shift in self.get_shift_names()), domain=NonNegativeReals, initialize=0) + + self.model.shift_count_t1 = Var( + ((worker.id) for worker in self.workers), + domain=NonNegativeReals, + initialize=0, + ) + self.model.shift_count_t2 = Var( + ((worker.id) for worker in self.workers), + domain=NonNegativeReals, + initialize=0, + ) + + self.model.shift_count_t3 = Var( + (worker.id for worker in self.workers), initialize=0 + ) + # self.model.unavailable = Var(((worker, week, day) for worker in self.workers for week in weeks for day in days), + # within=Binary, initialize=0) + + def availability_init(model, wid, week, day): + if (wid, week, day) in self.unavailable_to_work: + # print((wid, week, day)) + return 0 + return 1 + + self.model.available = Param( + ( + (worker.id, week, day) + for worker in self.workers + for week, day in self.get_week_day_combinations() + ), + initialize=availability_init, + ) + + # binary variables representing if a worker is necessary + # self.model.needed = Var([worker.id for worker in self.workers], within=Binary, initialize=0) + + # binary variables representing if a worker worked on sunday but not on saturday (avoid if possible) + # self.model.no_pref = Var([worker.id for worker in self.workers], within=Binary, initialize=0) + + self.build_model_constraints() + + def build_model_constraints(self): + self.model.constraints = ConstraintList() # Create a set of constraints + + for ( + week, + day, + shift, + self.workers_required, + site_required, + ) in self.get_required_workers_and_site_combinations(): + # print(week, day, shift, self.workers_required, site_required) + self.model.constraints.add( + self.workers_required + == sum( + self.model.works[worker.id, week, day, shift] + for worker in self.workers + if worker.site in site_required + ) + ) + + for week, day, shift in self.get_not_required_shifts(): + self.model.constraints.add( + 0 + == sum( + self.model.works[worker.id, week, day, shift.name] + for worker in self.workers + ) + ) + + # Constraint: total hours worked hours worked + # for worker in self.workers: + # self.model.constraints.add( + # 1200 >= sum(shift_lengths[shift] * self.model.works[worker, week, day, shift] for week in weeks for day in days for shift in days_shifts[day]) + # ) + + # for week in weeks: + # for worker in self.workers: + # self.model.constraints.add( + # 48 >= sum(shift_lengths[shift] * self.model.works[worker, week, day, shift] for day in days for shift in days_shifts[day]) + # ) + + def maxHoursPerWeekRule(model, wid, week): + return 72 >= sum( + shift.length * model.works[wid, week, day, shift.name] + for day, shift in self.get_day_shiftclass_products() + ) + + self.model.max_hours_per_week_constraint = Constraint( + [worker.id for worker in self.workers], self.weeks, rule=maxHoursPerWeekRule + ) + + def maxHoursRule(model, wid): + return 8000 >= sum( + shift.length * model.works[wid, week, day, shift.name] + for week, day, shift in self.get_all_shiftclass_combinations() + ) + + self.model.max_hours_constraint = Constraint( + [worker.id for worker in self.workers], rule=maxHoursRule + ) + + # self should not be allocated on nwds (excepting night shifts) + def nwdRule(model, worker): + print(worker, worker.id, worker.nwd) + wid = worker.id + nwd = worker.nwd + return 0 == sum( + model.works[wid, week, day, shift.name] + for week, day, shift in self.get_all_shiftclass_combinations() + if (not shift.rota_on_nwds and day in nwd) + ) + + # self.model.nwd_constraint = Constraint([(worker) for worker in self.workers if worker.nwd], rule=nwdRule) + + # Set a rule to even twilight shifts: + # total_twilight_shifts = len(weeks) * 5 * 1 # One person per site + + # def trurotwilightShiftNumberRule(self.model, wid, fte_adj, site, shift): + # #site, shift = site_and_shift + # max_shifts = total_twilight_shifts / self.full_time_equivalent_sites[site] * fte_adj + twilight_balance_offset + # min_shifts = total_twilight_shifts / self.full_time_equivalent_sites[site] * fte_adj - twilight_balance_offset + + # return inequality(min_shifts, sum(self.model.works[wid, week, day, shift.GetShift()] for week in weeks for day in days), max_shifts) + # self.model.twilight_shifts_truro_constraint = Constraint([(worker.id, worker.fte_adj, ss[0], ss[1]) for ss in site_and_shifts for worker in self.workers if worker.site == ss[0]], rule=trurotwilightShiftNumberRule) + + # def plymouthtwilightShiftNumberRule(self.model, wid, fte_adj): + # max_shifts = total_twilight_shifts / self.full_time_equivalent_sites["plymouth"] * fte_adj + twilight_balance_offset + # min_shifts = total_twilight_shifts / self.full_time_equivalent_sites["plymouth"] * fte_adj - twilight_balance_offset + # + # return inequality(min_shifts, sum(self.model.works[wid, week, day, "plymouth_twilight"] for week in weeks for day in days), max_shifts) + # self.model.twilight_shifts_plymouth_constraint = Constraint([(worker.id, worker.fte_adj) for worker in self.workers if worker.site == "plymouth"], #rule=plymouthtwilightShiftNumberRule) + + # Limit to 1 ST2 (or below) on a night shift + def nightShiftMaxSTRule(model, week, block): + """Limits to 1 ST2 (or below) per night""" + single_workers = [w for w in self.workers if w.grade < 3] + if not single_workers: + return Constraint.Skip + return sum(model.nights[w.id, week, block] for w in single_workers) <= 1 + + self.model.night_shifts_max_st_constraint = Constraint( + [week for week in self.weeks], + [block for block in ["weekday", "weekend"]], + rule=nightShiftMaxSTRule, + ) + + # Enusre at least 1 ST4+ on a night shift + def nightShiftMinST4Rule(model, week, block): + single_workers = [w for w in self.workers if w.grade > 3] + if not single_workers: + return Constraint.Skip + return sum(model.nights[w.id, week, block] for w in single_workers) >= 1 + + self.model.night_shifts_min_st4_constraint = Constraint( + [week for week in self.weeks], + [block for block in ["weekday", "weekend"]], + rule=nightShiftMinST4Rule, + ) + + # Constraint (def of self.model.needed) + # for worker in self.workers: + # self.model.constraints.add( + # 10000 * self.model.needed[worker.id] >= sum(self.model.works[worker.id, week, day, shift] for week in weeks for day in days for shift in days_shifts[day]) + # ) # if any self.model.works[worker, ·, ·] non-zero, self.model.needed[worker] must be one; else is zero to reduce the obj function + # 10000 is to remark, but 5 was enough since max of 40 hours yields max of 5 shifts, the maximum possible sum + + # Constraint (def of self.model.no_pref) + # for worker in self.workers: + # for week in weeks: + # self.model.constraints.add( + # self.model.no_pref[worker.id] >= sum(self.model.works[worker.id, week, 'Sat', shift] for shift in days_shifts['Sat']) + # - sum(self.model.works[worker.id, week, 'Sun', shift] for shift in days_shifts['Sun']) + # ) # if not working on sunday but working saturday self.model.needed must be 1; else will be zero to reduce the obj function + + # def balanceSiteShiftNumberRule(model, wid, fte_adj, shiftClass): + # # TODO: make helper to retrieve + # total_shifts = len(self.weeks) * len(shiftClass.shift_days) * shiftClass.self.workers_required # One person per site + + # full_time_equivalent_joined = sum([self.full_time_equivalent_sites[i] for i in shiftClass.site]) + + # max_shifts = total_shifts / full_time_equivalent_joined * fte_adj + twilight_balance_offset + # min_shifts = total_shifts / full_time_equivalent_joined * fte_adj - twilight_balance_offset + + # return inequality(min_shifts, sum(self.model.works[wid, week, day, shiftClass.name] for week, day in self.get_week_day_combinations()), max_shifts) + + # #self.model.balance_site_shift_constraint = Constraint([(worker.id, worker.fte_adj, shift) for shift in self.GetShiftOptions() for worker in self.workers if worker.site in shift.site and shift.balance_by_site], rule=balanceSiteShiftNumberRule) + + # # set a rule to even night shifts (could also be done in blocks, which may be quicker?) + # # takes into accound both LTFT, OOP and leaving the training scheme + # #total_night_shifts = self.rota_days_length * self.GetShiftByName("night").self.workers_required + + # def nightShiftNumberRule(model, wid, fte_adj): # NOTE no longer used - see generic function below + # target_shifts = total_night_shifts / self.full_time_equivalent * fte_adj + # max_shifts = target_shifts + night_balance_offset + # min_shifts = target_shifts - night_balance_offset + + # return inequality(min_shifts, sum(model.works[wid, week, day, "night"] for week, day in self.get_week_day_combinations()), max_shifts) + # #self.model.night_shifts_constraint = Constraint([(worker.id, worker.fte_adj) for worker in self.workers], rule=nightShiftNumberRule) + # twilight_constraints = {} + + # Balance shifts + for worker in self.workers: + for shift in self.GetShiftOptions(): + if ( + worker.site in shift.site + ): # Each site specfies which sites self.workers can fullfill it + + # total_shifts = self.rota_days_length * shift.workers_required # One person per site + total_shifts = ( + len(self.weeks) + * len(shift.shift_days) + * shift.workers_required + ) + + # if shift.balance_by_site: + full_time_equivalent_joined = sum( + [self.full_time_equivalent_sites[i] for i in shift.site] + ) + + target_shifts = ( + total_shifts / full_time_equivalent_joined * worker.fte_adj + ) + + worker.shift_target_number[shift.name] = target_shifts + + max_shifts = target_shifts + shift.balance_offset + min_shifts = target_shifts - shift.balance_offset + # print(max_shifts, min_shifts) + + self.model.constraints.add( + inequality( + min_shifts, + sum( + self.model.works[worker.id, week, day, shift.name] + for week, day in self.get_week_day_combinations() + ), + max_shifts, + ) + ) + # return inequality(min_shifts, sum(self.model.works[wid, week, day, shiftClass.name] for week, day in self.get_week_day_combinations()), max_shifts) + + for worker in self.workers: + # Define shift_count_t1 and shift_count_t2 constraints for the object + # This bypassing the need for a quadratic solver + # t1-t2 is the target + # As the objective is to minimise t1+t2 and t1 and t2 are positive reals + # t1+t2 approximates the absolute target (which otherwise requires a quadratic solver) + self.model.constraints.add( + # self.model.shift_count_t3[worker.id] == abs(sum(self.model.shift_count[worker.id, shift.name] - worker.shift_target_number[shift.name] for shift in self.GetShiftOptions())) + self.model.shift_count_t1[worker.id] + - self.model.shift_count_t2[worker.id] + == sum( + ( + self.model.shift_count[worker.id, shift.name] + - worker.shift_target_number[shift.name] + ) + * shift.balance_weighting + for shift in self.GetShiftOptions() + ) + ) + for shift in self.GetShiftOptions(): + self.model.constraints.add( + self.model.shift_count[worker.id, shift.name] + == sum( + self.model.works[worker.id, week, day, shift.name] + for week, day in self.get_week_day_combinations() + ) + ) + # self.model.constraints.add( + # #self.model.shift_count_t1[worker.id, shift.name]-self.model.shift_count_t2[worker.id, shift.name] == self.model.shift_count[worker.id, shift.name] + # self.model.shift_count_t1[worker.id, shift.name]-self.model.shift_count_t2[worker.id, shift.name] == self.model.shift_count[worker.id, shift.name] - worker.shift_target_number[shift.name] + # ) + + if worker.nwd: + for week, day, shift in self.get_all_shiftclass_combinations(): + if not shift.rota_on_nwds and day in worker.nwd: + self.model.constraints.add( + 0 == self.model.works[worker.id, week, day, shift.name] + ) + + # Constraint: rest between two shifts is of 12 hours (i.e., at least two shifts) + weeks_days = self.get_week_day_combinations() + for worker in self.workers: + if "nights" in self.get_shift_names(): + + for week_blocks in self.get_week_block_iterator(2): + # Prevent nights more than once every n weeks + self.model.constraints.add( + 1 + >= sum( + self.model.nights[worker.id, week, block] + for week in week_blocks + for block in ["weekday", "weekend"] + ) + ) + + for week in self.weeks: + if "nights" in self.get_shift_names(): + # Force nights to be assigned in blocks + self.model.constraints.add( + 1 + == sum( + self.model.nights[worker.id, week, block] + for block in self.night_blocks + ) + ) + + # if night block is weekday make sure Mon - Thurs is assigned as nights + self.model.constraints.add( + self.model.nights[worker.id, week, "weekday"] * 4 + == sum( + self.model.works[worker.id, week, day, "night"] + for day in days[:4] + ) + ) + self.model.constraints.add( + self.model.nights[worker.id, week, "weekend"] * 3 + == sum( + self.model.works[worker.id, week, day, "night"] + for day in days[4:] + ) + ) + + # + + for n in range(len(weeks_days)): + + week, day = weeks_days[n] + + p1 = 1 + if n > 0: + pweek, pday = weeks_days[n - 1] + else: + p1 = 0 + pweek, pday = weeks_days[n] + + n1 = 1 + n2 = 1 + try: + nweek, nday = weeks_days[n + 1] + except IndexError: + # print(worker.id, "shit") + nweek, nday = weeks_days[n] + n1 = 0 + + try: + + n2week, n2day = weeks_days[n + 2] + except IndexError: + # print(worker.id, "shit2") + n2week, n2day = weeks_days[n] + n2 = 0 + + # print(week, day, nweek, nday, n2week, n2day, n1, n2) + # Unable to work (hard constraint not preference) + self.model.constraints.add( + self.model.available[worker.id, week, day] + >= sum( + self.model.works[worker.id, week, day, shift] + for shift in self.get_shift_namesByDay(day) + ) + ) + + if "nights" in self.get_shift_names(): + self.model.constraints.add( + self.model.available[worker.id, week, day] + >= self.model.works[worker.id, pweek, pday, "night"] + ) + + # single shift per day + self.model.constraints.add( + 1 + >= sum( + self.model.works[worker.id, week, day, shift] + for shift in self.get_shift_namesByDay(day) + ) + ) + ## if working in evening, until next evening (note that after sunday comes next monday) + # self.model.constraints.add( + # 1 >= sum(self.model.works[worker.id, week, days[j], shift] for shift in ['evening', 'night']) + + # self.model.works[worker.id, nweek, days[(j + 1) % 7], 'truro_twilight'] + # ) + + # if working a night ensure preceeding (1) or subsequent (2) shifts can only be nights + + if "nights" in self.get_shift_names(): + + self.model.constraints.add( + 1 + >= self.model.works[worker.id, week, day, "night"] + + sum( + n1 * self.model.works[worker.id, nweek, nday, shift] + for shift in self.get_shift_namesByDay(nday) + if shift != "night" + ) + + sum( + n2 * self.model.works[worker.id, n2week, n2day, shift] + for shift in self.get_shift_namesByDay(n2day) + if shift != "night" + ) + + sum( + p1 * self.model.works[worker.id, pweek, pday, shift] + for shift in self.get_shift_namesByDay(pday) + if shift != "night" + ) + ) + + self.define_objectives() + + def define_objectives(self): + # Define an objective function with model as input, to pass later + def obj_rule(m): + + #c = len(workers) + + #return 1 + return sum(self.model.shift_count_t1[(worker.id)] + self.model.shift_count_t2[(worker.id)] for worker in self.workers) + #return sum(model.shift_count_t3[worker.id] for worker in workers) + + + + # add objective function to the model. rule (pass function) or expr (pass expression directly) + self.model.obj = Objective(rule=obj_rule, sense=minimize) + + def add_worker(self, worker: Worker): + """Add a worker to the rota + + Args: + worker (Worker): + """ + self.workers.append(worker) + + def add_workers(self, workers: List): + """Add multiple worker to the rota + + Args: + workers (List(Worker)): + """ + self.workers.extend(workers) + + def build_workers(self): + """Process loaded workers + + Must be called prior to attempting to solve + """ + self.workers = sorted(self.workers) + + self.full_time_equivalent = sum([w.fte_adj for w in self.workers]) + self.full_time_equivalent_sites = {} + + for site in sites: + self.full_time_equivalent_sites[site] = sum( + [w.fte_adj for w in self.workers if w.site == site] + ) + def add_shift(self, shift): """Add a shift to the collection @@ -71,7 +578,7 @@ class ShiftCollection(object): """ self.shifts.append(shift) - self.BuildShifts() + self.BuildShift() def add_shifts(self, *shifts: SingleShift): """Add multiple shifts @@ -80,9 +587,9 @@ class ShiftCollection(object): None """ self.shifts.extend(shifts) - self.BuildShifts() + self.BuildShift() - def GetShiftNamesByDay(self, day: DayStr): + def get_shift_namesByDay(self, day: DayStr): """Returns the shifts required for a specific day Returns: @@ -110,7 +617,7 @@ class ShiftCollection(object): """ return self.shifts_by_name[name].length - def BuildShifts(self): + def BuildShift(self): """ """ self.shifts_by_name = {} self.shift_names = [] # type: List[ShiftName] @@ -140,7 +647,7 @@ class ShiftCollection(object): self.day_shift_product.append((day, s.name)) self.day_shiftclass_product.append((day, s)) - def GetAllDayShiftsAsNames(self): + def GetAllDayShiftAsNames(self): """Returns a list of all required day / shift combinations Returns: @@ -148,7 +655,7 @@ class ShiftCollection(object): """ return self.day_shift_product - def GetAllDayShiftsAsClass(self) -> List[Tuple[DayStr, SingleShift]]: + def get_day_shiftclass_products(self) -> List[Tuple[DayStr, SingleShift]]: """Returns a list of all required day / shift combinations Returns: @@ -156,7 +663,7 @@ class ShiftCollection(object): """ return self.day_shiftclass_product - def GetAllShiftsAsNames(self) -> List[Tuple[WeekInt, DayStr, ShiftName]]: + def GetAllShiftAsNames(self) -> List[Tuple[WeekInt, DayStr, ShiftName]]: """Returns a list of all possible week / day / shift combinations Returns: @@ -164,7 +671,7 @@ class ShiftCollection(object): """ return self.week_day_shift_product - def GetAllShiftsAsClass(self) -> List[Tuple[WeekInt, DayStr, SingleShift]]: + def get_all_shiftclass_combinations(self) -> List[Tuple[WeekInt, DayStr, SingleShift]]: """Returns a list of all possible week / day / shift combinations Returns: @@ -172,7 +679,7 @@ class ShiftCollection(object): """ return self.week_day_shiftclass_product - def GetAllWeeksDays(self) -> list: + def get_week_day_combinations(self) -> list: """Returns a list of all week / day tuple combinations Returns: @@ -189,7 +696,7 @@ class ShiftCollection(object): """ return self.shifts - def GetShiftNames(self) -> List[ShiftName]: + def get_shift_names(self) -> List[ShiftName]: """Returns a list of all the registered shift names Returns: @@ -197,7 +704,7 @@ class ShiftCollection(object): """ """ """ return self.shift_names - def GetWeeksInBlocks(self, block_length): + def get_week_block_iterator(self, block_length): """Gets a two dimensional list of week blocks in specified length e.g. block_length = 4 @@ -218,7 +725,7 @@ class ShiftCollection(object): blocks.append(self.weeks[i : i + block_length]) return blocks - def GetRequiredShiftsWorkersAndSites(self): + def get_required_workers_and_site_combinations(self): """Returns a list of all possible shifts, the workers required and site Returns: @@ -227,11 +734,13 @@ class ShiftCollection(object): l = [] for week, day, shift in self.week_day_shiftclass_product: if day in shift.shift_days: - l.append((week, day, shift.name, shift.workers_required, shift.site)) + l.append( + (week, day, shift.name, shift.workers_required, shift.site) + ) return l - def GetNotRequiredShifts(self): + def get_not_required_shifts(self): """Returns a set of all possible shifts combinations that are not required Includes those on days @@ -246,4 +755,4 @@ class ShiftCollection(object): for shift in self.shifts: l.add((week, day, shift)) - return l - set(self.GetAllShiftsAsClass()) + return l - set(self.get_all_shiftclass_combinations()) diff --git a/workers.py b/workers.py index 1020331..a51bdeb 100644 --- a/workers.py +++ b/workers.py @@ -1,11 +1,13 @@ import datetime from collections import defaultdict -from shifts import SingleShift, ShiftCollection, days, sites +#from shifts import RotaBuilder, days, sites class Worker: - def __init__(self, Shifts, 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 + ): self.id = id self.name = name self.site = site @@ -16,48 +18,53 @@ class Worker: self.shift_target_number = defaultdict(int) - days_to_work = Shifts.rota_days_length + 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, '%Y/%m/%d').date() + self.end_date = datetime.datetime.strptime(end_date, "%Y/%m/%d").date() - if self.end_date > Shifts.rota_end_date: - self.end_date = Shifts.rota_end_date + if self.end_date > Rota.rota_end_date: + self.end_date = Rota.rota_end_date else: - days_to_work = (self.end_date - Shifts.start_date).days + days_to_work = (self.end_date - Rota.start_date).days # add unavalabilities - for weeks_days in Shifts.weeks_days_product[days_to_work:]: + for weeks_days in Rota.weeks_days_product[days_to_work:]: week, day = weeks_days - Shifts.unavailable_to_work.add((self.id, week, day)) - + Rota.unavailable_to_work.add((self.id, week, day)) + 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 - if end_oop_date > Shifts.rota_end_date: - end_oop_date = Shifts.rota_end_date - oop_length = (end_oop_date - start_oop_date).days days_to_work = days_to_work - oop_length - days_until_oop = (start_oop_date - Shifts.start_date).days + days_until_oop = (start_oop_date - Rota.start_date).days - for weeks_days in Shifts.weeks_days_product[days_until_oop:days_until_oop+oop_length]: + for weeks_days in Rota.weeks_days_product[ + days_until_oop : days_until_oop + oop_length + ]: week, day = weeks_days - Shifts.unavailable_to_work.add((self.id, week, day)) + Rota.unavailable_to_work.add((self.id, week, day)) - - self.proportion_rota_to_work = days_to_work / Shifts.rota_days_length + 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 self.fte_adj = self.fte * self.proportion_rota_to_work def __lt__(self, other): - return (self.site, self.grade, self.name) < (other.site, other.grade, other.name) - + return (self.site, self.grade, self.name) < ( + other.site, + other.grade, + other.name, + ) + def get_details(self): - return "{} {} {}".format(self.id, self.site[0], self.grade) \ No newline at end of file + return "{} {} {}".format(self.id, self.site[0], self.grade)