Start
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
+704
@@ -0,0 +1,704 @@
|
|||||||
|
{
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 2,
|
||||||
|
"metadata": {
|
||||||
|
"language_info": {
|
||||||
|
"name": "python",
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"version": "3.7.5-final"
|
||||||
|
},
|
||||||
|
"orig_nbformat": 2,
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"npconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": 3,
|
||||||
|
"kernelspec": {
|
||||||
|
"name": "python37564bit5ba0d1a6f89f489d9799b0f9b976a56e",
|
||||||
|
"display_name": "Python 3.7.5 64-bit"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 1,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from pyomo.environ import *\n",
|
||||||
|
"from pyomo.opt import SolverFactory\n",
|
||||||
|
"\n",
|
||||||
|
"import datetime\n",
|
||||||
|
"import itertools"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"night_balance_offset = 3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 2,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"output_type": "error",
|
||||||
|
"ename": "FileNotFoundError",
|
||||||
|
"evalue": "[Errno 2] No such file or directory: 'trainees.csv'",
|
||||||
|
"traceback": [
|
||||||
|
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
|
||||||
|
"\u001b[0;31mFileNotFoundError\u001b[0m Traceback (most recent call last)",
|
||||||
|
"\u001b[0;32m<ipython-input-2-a86d72f54d28>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# Import trainee data\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mcsv\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0;32mwith\u001b[0m \u001b[0mopen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'trainees.csv'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnewline\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m''\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mf\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 4\u001b[0m \u001b[0mlines\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcsv\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mreader\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mf\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdelimiter\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m','\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mquotechar\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'|'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mrow\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mlines\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
|
||||||
|
"\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: 'trainees.csv'"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"# Import trainee data\n",
|
||||||
|
"import csv\n",
|
||||||
|
"with open('trainees.csv', newline='') as f:\n",
|
||||||
|
" lines = csv.reader(f, delimiter=',', quotechar='|')\n",
|
||||||
|
" for row in lines[1:]:\n",
|
||||||
|
" name, site, grade, fte, end_date, nwds = line.split(\",\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 3,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Define days (1 week)\n",
|
||||||
|
"days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']\n",
|
||||||
|
"\n",
|
||||||
|
"weeks = [i for i in range(1,2)]\n",
|
||||||
|
"\n",
|
||||||
|
"weeks_days_product = list(itertools.product(weeks, days))\n",
|
||||||
|
"\n",
|
||||||
|
"# Enter shifts of each day\n",
|
||||||
|
"shifts = ['morning', 'evening', 'night', 'truro_twighlight'] # 3 shifts of 8 hours\n",
|
||||||
|
"shift_len = [8, 8, 12.5, 12.5]\n",
|
||||||
|
"\n",
|
||||||
|
"night_blocks = [\"weekday\", \"weekend\", \"none\"]\n",
|
||||||
|
"\n",
|
||||||
|
"shift_lengths = dict(zip(shifts, shift_len))\n",
|
||||||
|
"\n",
|
||||||
|
"days_shifts = {day: shifts for day in days} # dict with day as key and list of its shifts as value\n",
|
||||||
|
"\n",
|
||||||
|
"start_date = datetime.date(2020,9,7)\n",
|
||||||
|
"rota_days_length = len(weeks) * 7\n",
|
||||||
|
"rota_end_date = start_date + datetime.timedelta(rota_days_length)\n",
|
||||||
|
"\n",
|
||||||
|
"unavailable_to_work = set()\n",
|
||||||
|
"\n",
|
||||||
|
"class workerClass:\n",
|
||||||
|
" def __init__(self, id, name, site, grade, fte=100, nwd=\"\", end_date=None, oop=None):\n",
|
||||||
|
" self.id = id\n",
|
||||||
|
" self.name = name\n",
|
||||||
|
" self.site = site\n",
|
||||||
|
" self.grade = grade\n",
|
||||||
|
" self.fte = fte\n",
|
||||||
|
" self.nwd = nwd\n",
|
||||||
|
" self.proportion_rota_to_work = 1\n",
|
||||||
|
"\n",
|
||||||
|
" days_to_work = rota_days_length\n",
|
||||||
|
"\n",
|
||||||
|
" if end_date is None:\n",
|
||||||
|
" self.end_date = None\n",
|
||||||
|
" else:\n",
|
||||||
|
" y, m, d = [int(i) for i in end_date.split(\"/\")]\n",
|
||||||
|
" self.end_date = datetime.date(y, m, d)\n",
|
||||||
|
"\n",
|
||||||
|
" if self.end_date > rota_end_date:\n",
|
||||||
|
" self.end_date = rota_end_date\n",
|
||||||
|
" else:\n",
|
||||||
|
" days_to_work = (self.end_date - start_date).days\n",
|
||||||
|
"\n",
|
||||||
|
" # add unavalabilities\n",
|
||||||
|
" for weeks_days in weeks_days_product[days_to_work:]:\n",
|
||||||
|
" week, day = weeks_days\n",
|
||||||
|
" unavailable_to_work.add((self.id, week, day))\n",
|
||||||
|
" \n",
|
||||||
|
" if oop is not None:\n",
|
||||||
|
" start_oop, end_oop = oop\n",
|
||||||
|
" start_oop_date = datetime.datetime.strptime(start_oop, '%Y/%m/%d').date()\n",
|
||||||
|
" end_oop_date = datetime.datetime.strptime(end_oop, '%Y/%m/%d').date()\n",
|
||||||
|
"\n",
|
||||||
|
" if end_oop_date > rota_end_date:\n",
|
||||||
|
" end_oop_date = rota_end_date\n",
|
||||||
|
" \n",
|
||||||
|
" oop_length = (end_oop_date - start_oop_date).days\n",
|
||||||
|
" days_to_work = days_to_work - oop_length\n",
|
||||||
|
"\n",
|
||||||
|
" days_until_oop = (start_oop_date - start_date).days\n",
|
||||||
|
"\n",
|
||||||
|
" for weeks_days in weeks_days_product[days_until_oop:days_until_oop+oop_length]:\n",
|
||||||
|
" week, day = weeks_days\n",
|
||||||
|
" unavailable_to_work.add((self.id, week, day))\n",
|
||||||
|
"\n",
|
||||||
|
" \n",
|
||||||
|
" self.proportion_rota_to_work = days_to_work / rota_days_length\n",
|
||||||
|
"\n",
|
||||||
|
" # We had to adjust the full time equivalent for people who CCT / leave the rota early\n",
|
||||||
|
" self.fte_adj = self.fte * self.proportion_rota_to_work\n",
|
||||||
|
" \n",
|
||||||
|
" def GetDetails(self):\n",
|
||||||
|
" return \"{} {} {}\".format(self.name, self.site, self.grade)\n",
|
||||||
|
"\n",
|
||||||
|
"# Enter workers ids (name, number, ...)\n",
|
||||||
|
"workers = [workerClass(i, \"Test Person {}\".format(i), \"truro\", 3) for i in range(1, 10)] \n",
|
||||||
|
"workers.extend([workerClass(i, \"Test Person {}\".format(i), \"plymouth\", 4) for i in range(11, 28)])\n",
|
||||||
|
"#workers.extend([workerClass(i, \"Test Person {}\".format(i), 4, 200, \"\") for i in range(48, 49)])\n",
|
||||||
|
"\n",
|
||||||
|
"\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",
|
||||||
|
"full_time_equivalent = sum([w.fte_adj for w in workers])\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"#for i in range(11,30):"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 5,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"output_type": "execute_result",
|
||||||
|
"data": {
|
||||||
|
"text/plain": "[(1, 'Wed'), (1, 'Thu'), (1, 'Fri'), (1, 'Sat'), (1, 'Sun')]"
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"execution_count": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"weeks_days_product = list(itertools.product(weeks, days))\n",
|
||||||
|
"weeks_days_product[2:]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 7,
|
||||||
|
"metadata": {
|
||||||
|
"tags": [
|
||||||
|
"outputPrepend",
|
||||||
|
"outputPrepend",
|
||||||
|
"outputPrepend",
|
||||||
|
"outputPrepend",
|
||||||
|
"outputPrepend",
|
||||||
|
"outputPrepend",
|
||||||
|
"outputPrepend",
|
||||||
|
"outputPrepend",
|
||||||
|
"outputPrepend"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Initialize model\n",
|
||||||
|
"model = ConcreteModel()\n",
|
||||||
|
"\n",
|
||||||
|
"# binary variables representing if a worker is scheduled somewhere\n",
|
||||||
|
"model.works = Var(((worker.id, week, day, shift) for worker in workers for week in weeks for day in days for shift in days_shifts[day]),\n",
|
||||||
|
" within=Binary, initialize=0)\n",
|
||||||
|
"\n",
|
||||||
|
"model.nights = Var(((worker.id, week, block) for worker in workers for week in weeks for block in night_blocks),\n",
|
||||||
|
" within=Binary, initialize=0)\n",
|
||||||
|
"\n",
|
||||||
|
"#model.unavailable = Var(((worker, week, day) for worker in workers for week in weeks for day in days),\n",
|
||||||
|
"# within=Binary, initialize=0)\n",
|
||||||
|
"def availability_init(model, wid, week, day):\n",
|
||||||
|
" if (wid, week, day) in unavailable_to_work:\n",
|
||||||
|
" print((wid, week, day))\n",
|
||||||
|
" return 0\n",
|
||||||
|
" return 1\n",
|
||||||
|
"model.available = Param(((worker.id, week, day) for worker in workers for week in weeks for day in days), initialize=availability_init) \n",
|
||||||
|
"\n",
|
||||||
|
"# binary variables representing if a worker is necessary\n",
|
||||||
|
"model.needed = Var([worker.id for worker in workers], within=Binary, initialize=0)\n",
|
||||||
|
"\n",
|
||||||
|
"# binary variables representing if a worker worked on sunday but not on saturday (avoid if possible)\n",
|
||||||
|
"model.no_pref = Var([worker.id for worker in workers], within=Binary, initialize=0)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 8,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Define an objective function with model as input, to pass later\n",
|
||||||
|
"def obj_rule(m):\n",
|
||||||
|
" c = len(workers)\n",
|
||||||
|
" return sum(m.no_pref[worker.id] for worker in workers)\n",
|
||||||
|
" #return sum(m.no_pref[worker] for worker in workers) + sum(c * m.needed[worker] for worker in workers)\n",
|
||||||
|
"# we multiply the second term by a constant to make sure that it is the primary objective\n",
|
||||||
|
"# since sum(m.no_prefer) is at most len(workers), len(workers) + 1 is a valid constant.\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"# add objective function to the model. rule (pass function) or expr (pass expression directly)\n",
|
||||||
|
"model.obj = Objective(rule=obj_rule, sense=minimize)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 9,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"model.constraints = ConstraintList() # Create a set of constraints\n",
|
||||||
|
"\n",
|
||||||
|
"# Constraint: all shifts are assigned\n",
|
||||||
|
"for week in weeks:\n",
|
||||||
|
" for day in days:\n",
|
||||||
|
" for shift in days_shifts[day]:\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_twighlight']: \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",
|
||||||
|
" # make sure twighlights are not assigned on a weekend\n",
|
||||||
|
" elif day in days[5:] and shift in ['truro_twighlight']:\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 ['morning', 'evening']:\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_lengths[shift] * model.works[wid, week, day, shift]for day in days for shift in days_shifts[day]))\n",
|
||||||
|
"model.max_hours_per_week_constraint = Constraint([worker.id for worker in workers], weeks, rule=maxHoursPerWeekRule)\n",
|
||||||
|
"\n",
|
||||||
|
"def maxHoursRule(model, wid):\n",
|
||||||
|
" return(800 >= sum(shift_lengths[shift] * model.works[wid, week, day, shift] for week in weeks for day in days for shift in days_shifts[day]))\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, wid, nwds):\n",
|
||||||
|
" if nwds != \"\":\n",
|
||||||
|
" return(1 >= sum(model.works[wid, week, day, shift] for week in weeks for day in nwds.split(\",\") for shift in [\"morning\",\"evening\"]) + 1)\n",
|
||||||
|
" return Constraint.Skip\n",
|
||||||
|
"model.nwd_constraint = Constraint([(worker.id, worker.nwd) for worker in workers], rule=nwdRule)\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 = len(weeks) * 7 * 3\n",
|
||||||
|
"\n",
|
||||||
|
"def nightShiftNumberRule(model, wid, fte_adj):\n",
|
||||||
|
" max_shifts = total_night_shifts / full_time_equivalent * fte_adj + night_balance_offset\n",
|
||||||
|
" min_shifts = total_night_shifts / full_time_equivalent * fte_adj - night_balance_offset\n",
|
||||||
|
" \n",
|
||||||
|
" return inequality(min_shifts, sum(model.works[wid, week, day, \"night\"] for week in weeks for day in days), max_shifts)\n",
|
||||||
|
"model.night_shifts_constraint = Constraint([(worker.id, worker.fte_adj) for worker in workers], rule=nightShiftNumberRule)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"# Limit to 1 ST2 (or below) on a night shift\n",
|
||||||
|
"def nightShiftMaxSTRule(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_max_st_constraint = Constraint([week for week in 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 weeks], [block for block in [\"weekday\", \"weekend\"]], rule=nightShiftMinST4Rule)\n",
|
||||||
|
"\n",
|
||||||
|
"# Constraint: rest between two shifts is of 12 hours (i.e., at least two shifts)\n",
|
||||||
|
"for worker in workers:\n",
|
||||||
|
" for week in 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 j in range(len(days)):\n",
|
||||||
|
" nweek = min(week + (j + 1) // 7, weeks[-1])\n",
|
||||||
|
" pweek = week\n",
|
||||||
|
" if j == 0:\n",
|
||||||
|
" pweek = max(week - 1, 1)\n",
|
||||||
|
"\n",
|
||||||
|
" # Unable to work (hard constraint not preference)\n",
|
||||||
|
" model.constraints.add(\n",
|
||||||
|
" model.available[(worker.id, week, days[j])] >= \\\n",
|
||||||
|
" sum(model.works[worker.id, week, days[j], shift] for shift in days_shifts[days[j]]) \n",
|
||||||
|
" )\n",
|
||||||
|
" model.constraints.add(\n",
|
||||||
|
" model.available[(worker.id, week, days[j])] >= \\\n",
|
||||||
|
" model.works[worker.id, pweek, days[(j - 1) % 7], \"night\"]\n",
|
||||||
|
" )\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
" # if working in morning, cannot work again that day\n",
|
||||||
|
" model.constraints.add(\n",
|
||||||
|
" 1 >= sum(model.works[worker.id, week, days[j], shift] for shift in days_shifts[days[j]])\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], 'morning']\n",
|
||||||
|
" )\n",
|
||||||
|
"\n",
|
||||||
|
" # if working a night ensure preceeding (1) or subsequent (2) shifts can only be nights\n",
|
||||||
|
" model.constraints.add(\n",
|
||||||
|
" 1 >= model.works[worker.id, week, days[j], \"night\"] +\n",
|
||||||
|
" sum(model.works[worker.id, nweek, days[(j + 1) % 7], shift] for shift in ['morning', 'evening']) +\n",
|
||||||
|
" sum(model.works[worker.id, nweek, days[(j + 2) % 7], shift] for shift in ['morning', 'evening']) +\n",
|
||||||
|
" sum(model.works[worker.id, pweek, days[(j - 1) % 7], shift] for shift in ['morning', 'evening'])\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": 10,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"opt = SolverFactory('cbc') # choose a solver\n",
|
||||||
|
"results = opt.solve(model) # solve the model with the selected solver"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 11,
|
||||||
|
"metadata": {
|
||||||
|
"tags": [
|
||||||
|
"outputPrepend",
|
||||||
|
"outputPrepend"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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",
|
||||||
|
"\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 days_shifts[day]} for day in days} for week in weeks}\n",
|
||||||
|
" for week in weeks:\n",
|
||||||
|
" for worker in workers:\n",
|
||||||
|
" for day in days:\n",
|
||||||
|
" for shift in days_shifts[day]:\n",
|
||||||
|
" if works[worker.id, week, day, shift].value == 1:\n",
|
||||||
|
" week_table[week][day][shift].append(worker.GetDetails())\n",
|
||||||
|
" return week_table\n",
|
||||||
|
"\n",
|
||||||
|
"def get_worker_timetable(works):\n",
|
||||||
|
" timetable = { worker.name : { week : { day: \"\" for day in days } for week in weeks } for worker in workers }\n",
|
||||||
|
" for worker in workers:\n",
|
||||||
|
" for week in weeks:\n",
|
||||||
|
" for day in days:\n",
|
||||||
|
" for shift in days_shifts[day]:\n",
|
||||||
|
" if works[worker.id, week, day, shift].value == 1:\n",
|
||||||
|
" timetable[worker.name][week][day] = shift\n",
|
||||||
|
" return timetable\n",
|
||||||
|
"\n",
|
||||||
|
"def get_shift_summary(works):\n",
|
||||||
|
" #timetable = { worker.GetDetails() : { shift : \"\" for shift in shifts } for worker in workers }\n",
|
||||||
|
" timetable = { worker.GetDetails() : { shift : \"\" for shift in [\"truro_twighlight\"] } for worker in workers } \n",
|
||||||
|
" for worker in workers:\n",
|
||||||
|
"\n",
|
||||||
|
" #for shift in shifts:\n",
|
||||||
|
" for shift in [\"truro_twighlight\"]:\n",
|
||||||
|
" timetable[worker.GetDetails()][shift] = [works[worker.id, week, day, shift] for week in weeks for day in days].count(1)\n",
|
||||||
|
" return timetable \n",
|
||||||
|
"\n",
|
||||||
|
"def get_no_preference(no_pref):\n",
|
||||||
|
" \"\"\"Extract to a list the workers not satisfied with their weekend preference.\"\"\"\n",
|
||||||
|
" return [worker.id for worker in workers if no_pref[worker.id].value == 1]\n",
|
||||||
|
"\n",
|
||||||
|
"def get_night_blocks(nights):\n",
|
||||||
|
" timetable = { worker.name : { week : { block: \"\" for block in night_blocks } for week in weeks } for worker in workers }\n",
|
||||||
|
" for worker in workers:\n",
|
||||||
|
" for week in weeks:\n",
|
||||||
|
" for block in night_blocks:\n",
|
||||||
|
" if nights[worker.id, week, block].value == 1:\n",
|
||||||
|
" timetable[worker.name][week][block] = \"true\"\n",
|
||||||
|
" return timetable\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"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",
|
||||||
|
"workers_no_pref = get_no_preference(model.no_pref) # list with the non-satisfied workers (work on Sat but not on Sun)\n",
|
||||||
|
"worker_night_block = get_night_blocks(model.nights)\n",
|
||||||
|
"worker_shift_summary = get_shift_summary(model.works)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 12,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"output_type": "execute_result",
|
||||||
|
"data": {
|
||||||
|
"text/plain": "{'Test Person 1 truro 3': {'truro_twighlight': 0},\n 'Test Person 2 truro 3': {'truro_twighlight': 0},\n 'Test Person 3 truro 3': {'truro_twighlight': 0},\n 'Test Person 4 truro 3': {'truro_twighlight': 3},\n 'Test Person 5 truro 3': {'truro_twighlight': 2},\n 'Test Person 6 truro 3': {'truro_twighlight': 0},\n 'Test Person 7 truro 3': {'truro_twighlight': 0},\n 'Test Person 8 truro 3': {'truro_twighlight': 0},\n 'Test Person 9 truro 3': {'truro_twighlight': 0},\n 'Test Person 11 plymouth 4': {'truro_twighlight': 0},\n 'Test Person 12 plymouth 4': {'truro_twighlight': 0},\n 'Test Person 13 plymouth 4': {'truro_twighlight': 0},\n 'Test Person 14 plymouth 4': {'truro_twighlight': 0},\n 'Test Person 15 plymouth 4': {'truro_twighlight': 0},\n 'Test Person 16 plymouth 4': {'truro_twighlight': 0},\n 'Test Person 17 plymouth 4': {'truro_twighlight': 0},\n 'Test Person 18 plymouth 4': {'truro_twighlight': 0},\n 'Test Person 19 plymouth 4': {'truro_twighlight': 0},\n 'Test Person 20 plymouth 4': {'truro_twighlight': 0},\n 'Test Person 21 plymouth 4': {'truro_twighlight': 0},\n 'Test Person 22 plymouth 4': {'truro_twighlight': 0},\n 'Test Person 23 plymouth 4': {'truro_twighlight': 0},\n 'Test Person 24 plymouth 4': {'truro_twighlight': 0},\n 'Test Person 25 plymouth 4': {'truro_twighlight': 0},\n 'Test Person 26 plymouth 4': {'truro_twighlight': 0},\n 'Test Person 27 plymouth 4': {'truro_twighlight': 0}}"
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"execution_count": 12
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"#worker_timetable\n",
|
||||||
|
"worker_shift_summary"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 13,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"output_type": "execute_result",
|
||||||
|
"data": {
|
||||||
|
"text/plain": "['Test Person 1',\n 'Test Person 2',\n 'Test Person 3',\n 'Test Person 4',\n 'Test Person 5',\n 'Test Person 6',\n 'Test Person 7',\n 'Test Person 8',\n 'Test Person 9',\n 'Test Person 11',\n 'Test Person 12',\n 'Test Person 13',\n 'Test Person 14',\n 'Test Person 15',\n 'Test Person 16',\n 'Test Person 17',\n 'Test Person 18',\n 'Test Person 19',\n 'Test Person 20',\n 'Test Person 21',\n 'Test Person 22',\n 'Test Person 23',\n 'Test Person 24',\n 'Test Person 25',\n 'Test Person 26',\n 'Test Person 27']"
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"execution_count": 13
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"workers_needed"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 14,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"output_type": "execute_result",
|
||||||
|
"data": {
|
||||||
|
"text/plain": "{1: {'Mon': {'morning': ['Test Person 19 plymouth 4'],\n 'evening': ['Test Person 9 truro 3'],\n 'night': ['Test Person 11 plymouth 4',\n 'Test Person 13 plymouth 4',\n 'Test Person 27 plymouth 4'],\n 'truro_twighlight': ['Test Person 5 truro 3']},\n 'Tue': {'morning': ['Test Person 17 plymouth 4'],\n 'evening': ['Test Person 20 plymouth 4'],\n 'night': ['Test Person 11 plymouth 4',\n 'Test Person 13 plymouth 4',\n 'Test Person 27 plymouth 4'],\n 'truro_twighlight': ['Test Person 4 truro 3']},\n 'Wed': {'morning': ['Test Person 12 plymouth 4'],\n 'evening': ['Test Person 23 plymouth 4'],\n 'night': ['Test Person 11 plymouth 4',\n 'Test Person 13 plymouth 4',\n 'Test Person 27 plymouth 4'],\n 'truro_twighlight': ['Test Person 4 truro 3']},\n 'Thu': {'morning': ['Test Person 2 truro 3'],\n 'evening': ['Test Person 7 truro 3'],\n 'night': ['Test Person 11 plymouth 4',\n 'Test Person 13 plymouth 4',\n 'Test Person 27 plymouth 4'],\n 'truro_twighlight': ['Test Person 4 truro 3']},\n 'Fri': {'morning': ['Test Person 18 plymouth 4'],\n 'evening': ['Test Person 15 plymouth 4'],\n 'night': ['Test Person 12 plymouth 4',\n 'Test Person 21 plymouth 4',\n 'Test Person 23 plymouth 4'],\n 'truro_twighlight': ['Test Person 5 truro 3']},\n 'Sat': {'morning': ['Test Person 1 truro 3'],\n 'evening': ['Test Person 6 truro 3'],\n 'night': ['Test Person 12 plymouth 4',\n 'Test Person 21 plymouth 4',\n 'Test Person 23 plymouth 4'],\n 'truro_twighlight': []},\n 'Sun': {'morning': ['Test Person 8 truro 3'],\n 'evening': ['Test Person 5 truro 3'],\n 'night': ['Test Person 12 plymouth 4',\n 'Test Person 21 plymouth 4',\n 'Test Person 23 plymouth 4'],\n 'truro_twighlight': []}}}"
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"execution_count": 14
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"week_table"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 15,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"output_type": "execute_result",
|
||||||
|
"data": {
|
||||||
|
"text/plain": "{'Test Person 1': {1: {'weekday': '', 'weekend': '', 'none': 'true'}},\n 'Test Person 2': {1: {'weekday': '', 'weekend': '', 'none': 'true'}},\n 'Test Person 3': {1: {'weekday': '', 'weekend': '', 'none': 'true'}},\n 'Test Person 4': {1: {'weekday': '', 'weekend': '', 'none': 'true'}},\n 'Test Person 5': {1: {'weekday': '', 'weekend': '', 'none': 'true'}},\n 'Test Person 6': {1: {'weekday': '', 'weekend': '', 'none': 'true'}},\n 'Test Person 7': {1: {'weekday': '', 'weekend': '', 'none': 'true'}},\n 'Test Person 8': {1: {'weekday': '', 'weekend': '', 'none': 'true'}},\n 'Test Person 9': {1: {'weekday': '', 'weekend': '', 'none': 'true'}},\n 'Test Person 11': {1: {'weekday': 'true', 'weekend': '', 'none': ''}},\n 'Test Person 12': {1: {'weekday': '', 'weekend': 'true', 'none': ''}},\n 'Test Person 13': {1: {'weekday': 'true', 'weekend': '', 'none': ''}},\n 'Test Person 14': {1: {'weekday': '', 'weekend': '', 'none': 'true'}},\n 'Test Person 15': {1: {'weekday': '', 'weekend': '', 'none': 'true'}},\n 'Test Person 16': {1: {'weekday': '', 'weekend': '', 'none': 'true'}},\n 'Test Person 17': {1: {'weekday': '', 'weekend': '', 'none': 'true'}},\n 'Test Person 18': {1: {'weekday': '', 'weekend': '', 'none': 'true'}},\n 'Test Person 19': {1: {'weekday': '', 'weekend': '', 'none': 'true'}},\n 'Test Person 20': {1: {'weekday': '', 'weekend': '', 'none': 'true'}},\n 'Test Person 21': {1: {'weekday': '', 'weekend': 'true', 'none': ''}},\n 'Test Person 22': {1: {'weekday': '', 'weekend': '', 'none': 'true'}},\n 'Test Person 23': {1: {'weekday': '', 'weekend': 'true', 'none': ''}},\n 'Test Person 24': {1: {'weekday': '', 'weekend': '', 'none': 'true'}},\n 'Test Person 25': {1: {'weekday': '', 'weekend': '', 'none': 'true'}},\n 'Test Person 26': {1: {'weekday': '', 'weekend': '', 'none': 'true'}},\n 'Test Person 27': {1: {'weekday': 'true', 'weekend': '', 'none': ''}}}"
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"execution_count": 15
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"worker_night_block"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 16,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"output_type": "execute_result",
|
||||||
|
"data": {
|
||||||
|
"text/plain": "[[<__main__.workerClass at 0x7f3a5179cbd0>, 1, 'Mon', 'night'],\n [<__main__.workerClass at 0x7f3a5179cbd0>, 1, 'Tue', 'night'],\n [<__main__.workerClass at 0x7f3a5179cbd0>, 1, 'Wed', 'night'],\n [<__main__.workerClass at 0x7f3a5179cbd0>, 1, 'Thu', 'night'],\n [<__main__.workerClass at 0x7f3a5179cbd0>, 1, 'Fri', 'night'],\n [<__main__.workerClass at 0x7f3a5179cbd0>, 1, 'Sat', 'night'],\n [<__main__.workerClass at 0x7f3a5179cbd0>, 1, 'Sun', 'night']]"
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"execution_count": 16
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"[[worker, week, day, shift] for day in days for shift in [\"night\"]]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 17,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"output_type": "execute_result",
|
||||||
|
"data": {
|
||||||
|
"text/plain": "1"
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"execution_count": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"1 if 4 in [4, 1, 0] else 0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 18,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"output_type": "execute_result",
|
||||||
|
"data": {
|
||||||
|
"text/plain": "0"
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"execution_count": 18
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"(4 + 1) // 7"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 19,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"for worker in workers:\n",
|
||||||
|
" for shift in shifts:\n",
|
||||||
|
" a = [model.works[worker.id, week, day, shift].value for week in weeks for day in days]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 20,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"output_type": "execute_result",
|
||||||
|
"data": {
|
||||||
|
"text/plain": "[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]"
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"execution_count": 20
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"a"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 21,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"output_type": "execute_result",
|
||||||
|
"data": {
|
||||||
|
"text/plain": "0"
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"execution_count": 21
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"0 % 7"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 22,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"output_type": "execute_result",
|
||||||
|
"data": {
|
||||||
|
"text/plain": "6"
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"execution_count": 22
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"-1 % 7"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user