├── README.md
└── pyomo-linear-foodstore-shift.ipynb
/README.md:
--------------------------------------------------------------------------------
1 | # Linear optimization solutions for job scheduling
2 |
3 | Existing products, sample implementation guidelines
4 | 1. (VIDEO)
5 | https://fieldsquared.com/video-field-workforce-management-mobility/
6 | https://en.praxedo.com/features/guided-tour/dispatcher/scheduling/optimise-routes-with-smartscheduler/
7 | 2. (DOCUMENT)
8 | https://help.salesforce.com/articleView?id=pfs_optimization_theory_definitions.htm&type=5
9 |
10 |
11 |
12 | Models (Implementation)
13 | * Python Gurobi lib (mixed-integer linear/quad optimization)
14 | http://www.matthewdgilbert.com/blog/introduction-to-gurobipy.html
15 | https://towardsdatascience.com/scheduling-with-ease-cost-optimization-tutorial-for-python-c05a5910ee0d
16 | * Python Taskpacker
17 | https://github.com/Edinburgh-Genome-Foundry/Taskpacker
18 | * Python pyomo
19 | https://towardsdatascience.com/modeling-and-optimization-of-a-weekly-workforce-with-python-and-pyomo-29484ba065bb
20 | https://github.com/ccarballolozano/blog-post-codes/blob/master/Modeling-and-optimization-of-a-weekly-workforce-with-Python-and-Pyomo/Modeling%20and%20optimization%20of%20a%20weekly%20workforce%20with%20Python%20and%20Pyomo.ipynb
21 | * Ortools python (cp_model)
22 | https://developers.google.com/optimization/scheduling/job_shop
23 | * IBM Decision Optimization CPLEX Modeling for Python (Apache 2.0 Open src) (Feature by feature chapter)
24 | https://pypi.org/project/docplex/
25 | https://ibmdecisionoptimization.github.io/tutorials/html/Scheduling_Tutorial.html
26 | * Python Whiteboard library
27 | https://medium.freecodecamp.org/introducing-timeboard-a-python-business-calendar-package-a2335898c697
28 | * Genetic algorithm (Schedule optimization implementations)
29 | (Number of workers, Time of task, Priority of task) http://theisensanders.com/genetic_task_scheduling_algorithm/
30 | (Conference talks optimization - Multiple constraints) https://medium.com/@filiph/using-a-genetic-algorithm-to-optimize-developer-conference-schedules-27f13d97fa9a
31 | (Job-shop schedule optimization) http://mat.uab.cat/~alseda/MasterOpt/p11-31.pdf
32 | * Integer linear programming
33 | (Nurse job scheduling - Minimize staff cost given constraints related to time/shifts) https://blogs.mathworks.com/loren/2016/01/06/generating-an-optimal-employee-work-schedule-using-integer-linear-programming/
34 | https://towardsdatascience.com/integer-programming-in-python-1cbdfa240df2
35 | * PySwarm (Particle swarm optimization)
36 | https://pythonhosted.org/pyswarm/
37 | * Uber ride optimization data set:
38 | https://towardsdatascience.com/uber-driver-schedule-optimization-62879ea41658
39 | * Multiple algos implementation
40 | (Job-Shop) https://github.com/oahziur/animated-archer
41 |
--------------------------------------------------------------------------------
/pyomo-linear-foodstore-shift.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 2,
6 | "metadata": {},
7 | "outputs": [
8 | {
9 | "name": "stdout",
10 | "output_type": "stream",
11 | "text": [
12 | "Collecting pyomo\n",
13 | " Downloading https://files.pythonhosted.org/packages/e4/df/3f4a54d494d429102c035308168bfd71aa0fac31832385ab356cb44560df/Pyomo-5.6.1-py3-none-any.whl (2.1MB)\n",
14 | "Requirement already satisfied: six>=1.4 in c:\\users\\jaylohokare\\anaconda3\\lib\\site-packages (from pyomo) (1.12.0)\n",
15 | "Collecting PyUtilib>=5.6.5 (from pyomo)\n",
16 | " Downloading https://files.pythonhosted.org/packages/43/04/9174c992ab7b5d5c9c29c0dce3ffbe2440dcfaf054a83b536f8253ce8384/PyUtilib-5.6.5-py2.py3-none-any.whl (250kB)\n",
17 | "Requirement already satisfied: ply in c:\\users\\jaylohokare\\anaconda3\\lib\\site-packages (from pyomo) (3.11)\n",
18 | "Collecting appdirs (from pyomo)\n",
19 | " Downloading https://files.pythonhosted.org/packages/56/eb/810e700ed1349edde4cbdc1b2a21e28cdf115f9faf263f6bbf8447c1abf3/appdirs-1.4.3-py2.py3-none-any.whl\n",
20 | "Requirement already satisfied: nose in c:\\users\\jaylohokare\\anaconda3\\lib\\site-packages (from PyUtilib>=5.6.5->pyomo) (1.3.7)\n",
21 | "Installing collected packages: PyUtilib, appdirs, pyomo\n",
22 | "Successfully installed PyUtilib-5.6.5 appdirs-1.4.3 pyomo-5.6.1\n"
23 | ]
24 | }
25 | ],
26 | "source": [
27 | "!pip install pyomo"
28 | ]
29 | },
30 | {
31 | "cell_type": "markdown",
32 | "metadata": {},
33 | "source": [
34 | "# Problem Statement\n",
35 | "\n",
36 | "https://towardsdatascience.com/modeling-and-optimization-of-a-weekly-workforce-with-python-and-pyomo-29484ba065bb\n",
37 | "\n",
38 | "A new food store has been opened at the University Campus which will be open 24 hours a day, 7 days a week. Each day, there are three eight-hour shifts. Morning shift is from 6:00 to 14:00, evening shift is from 14:00 to 22:00 and night shift is from 22:00 to 6:00 of the next day.\n",
39 | "\n",
40 | "During the night there is only one worker while during the day there are two, except on Sunday that there is only one for each shift. Each worker will not exceed a maximum of 40 hours per week and have to rest for 12 hours between two shifts.\n",
41 | "\n",
42 | "As for the weekly rest days, an employee who rests one Sunday will also prefer to do the same that Saturday.\n",
43 | "\n",
44 | "In principle, there are available ten employees, which is clearly over-sized. The less the workers are needed, the more the resources for other stores."
45 | ]
46 | },
47 | {
48 | "cell_type": "code",
49 | "execution_count": 3,
50 | "metadata": {},
51 | "outputs": [],
52 | "source": [
53 | "from pyomo.environ import *\n",
54 | "from pyomo.opt import SolverFactory"
55 | ]
56 | },
57 | {
58 | "cell_type": "code",
59 | "execution_count": 11,
60 | "metadata": {},
61 | "outputs": [],
62 | "source": [
63 | "# Define days (1 week)\n",
64 | "days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']\n",
65 | "\n",
66 | "# Enter shifts of each day\n",
67 | "shifts = ['morning', 'evening', 'night'] # 3 shifts of 8 hours\n",
68 | "days_shifts = {day: shifts for day in days} # dict with day as key and list of its shifts as value\n",
69 | "\n",
70 | "# Enter workers ids (name, number, ...)\n",
71 | "workers = ['W' + str(i) for i in range(1, 11)] # 10 workers available, more than needed"
72 | ]
73 | },
74 | {
75 | "cell_type": "code",
76 | "execution_count": 6,
77 | "metadata": {},
78 | "outputs": [],
79 | "source": [
80 | "# Initialize model\n",
81 | "model = ConcreteModel()\n",
82 | "\n",
83 | "# binary variables representing if a worker is scheduled somewhere\n",
84 | "model.works = Var(((worker, day, shift) for worker in workers for day in days for shift in days_shifts[day]),\n",
85 | " within=Binary, initialize=0)\n",
86 | "\n",
87 | "# binary variables representing if a worker is necessary\n",
88 | "model.needed = Var(workers, within=Binary, initialize=0)\n",
89 | "\n",
90 | "# binary variables representing if a worker worked on sunday but not on saturday (avoid if possible)\n",
91 | "model.no_pref = Var(workers, within=Binary, initialize=0)"
92 | ]
93 | },
94 | {
95 | "cell_type": "code",
96 | "execution_count": 14,
97 | "metadata": {},
98 | "outputs": [],
99 | "source": [
100 | "\n",
101 | "# Define an objective function with model as input, to pass later\n",
102 | "def obj_rule(m):\n",
103 | " c = len(workers)\n",
104 | " return sum(m.no_pref[worker] for worker in workers) + sum(c * m.needed[worker] for worker in workers)\n",
105 | "# we multiply the second term by a constant to make sure that it is the primary objective\n",
106 | "# since sum(m.no_prefer) is at most len(workers), len(workers) + 1 is a valid constant.\n",
107 | "\n",
108 | "\n",
109 | "# add objective function to the model. rule (pass function) or expr (pass expression directly)\n",
110 | "model.obj = Objective(rule=obj_rule, sense=minimize)"
111 | ]
112 | },
113 | {
114 | "cell_type": "code",
115 | "execution_count": null,
116 | "metadata": {},
117 | "outputs": [],
118 | "source": [
119 | "model.constraints = ConstraintList() # Create a set of constraints\n",
120 | "\n",
121 | "# Constraint: all shifts are assigned\n",
122 | "for day in days:\n",
123 | " for shift in days_shifts[day]:\n",
124 | " if day in days[:-1] and shift in ['morning', 'evening']:\n",
125 | " # weekdays' and Saturdays' day shifts have exactly two workers\n",
126 | " model.constraints.add( # to add a constraint to model.constraints set\n",
127 | " 2 == sum(model.works[worker, day, shift] for worker in workers)\n",
128 | " )\n",
129 | " else:\n",
130 | " # Sundays' and nights' shifts have exactly one worker\n",
131 | " model.constraints.add(\n",
132 | " 1 == sum(model.works[worker, day, shift] for worker in workers)\n",
133 | " )\n",
134 | "\n",
135 | "# Constraint: no more than 40 hours worked\n",
136 | "for worker in workers:\n",
137 | " model.constraints.add(\n",
138 | " 40 >= sum(8 * model.works[worker, day, shift] for day in days for shift in days_shifts[day])\n",
139 | " )\n",
140 | "\n",
141 | "# Constraint: rest between two shifts is of 12 hours (i.e., at least two shifts)\n",
142 | "for worker in workers:\n",
143 | " for j in range(len(days)):\n",
144 | " # if working in morning, cannot work again that day\n",
145 | " model.constraints.add(\n",
146 | " 1 >= sum(model.works[worker, days[j], shift] for shift in days_shifts[days[j]])\n",
147 | " )\n",
148 | " # if working in evening, until next evening (note that after sunday comes next monday)\n",
149 | " model.constraints.add(\n",
150 | " 1 >= sum(model.works[worker, days[j], shift] for shift in ['evening', 'night']) +\n",
151 | " model.works[worker, days[(j + 1) % 7], 'morning']\n",
152 | " )\n",
153 | " # if working in night, until next night\n",
154 | " model.constraints.add(\n",
155 | " 1 >= model.works[worker, days[j], 'night'] +\n",
156 | " sum(model.works[worker, days[(j + 1) % 7], shift] for shift in ['morning', 'evening'])\n",
157 | " )\n",
158 | "\n",
159 | "# Constraint (def of model.needed)\n",
160 | "for worker in workers:\n",
161 | " model.constraints.add(\n",
162 | " 10000 * model.needed[worker] >= sum(model.works[worker, day, shift] for day in days for shift in days_shifts[day])\n",
163 | " ) # if any model.works[worker, ·, ·] non-zero, model.needed[worker] must be one; else is zero to reduce the obj function\n",
164 | " # 10000 is to remark, but 5 was enough since max of 40 hours yields max of 5 shifts, the maximum possible sum\n",
165 | "\n",
166 | "# Constraint (def of model.no_pref)\n",
167 | "for worker in workers:\n",
168 | " model.constraints.add(\n",
169 | " model.no_pref[worker] >= sum(model.works[worker, 'Sat', shift] for shift in days_shifts['Sat'])\n",
170 | " - sum(model.works[worker, 'Sun', shift] for shift in days_shifts['Sun'])\n",
171 | " ) # if not working on sunday but working saturday model.needed must be 1; else will be zero to reduce the obj function"
172 | ]
173 | },
174 | {
175 | "cell_type": "code",
176 | "execution_count": 16,
177 | "metadata": {},
178 | "outputs": [
179 | {
180 | "name": "stdout",
181 | "output_type": "stream",
182 | "text": [
183 | "WARNING: Could not locate the 'cbc' executable, which is required for solver\n",
184 | " cbc\n"
185 | ]
186 | },
187 | {
188 | "ename": "ApplicationError",
189 | "evalue": "No executable found for solver 'cbc'",
190 | "output_type": "error",
191 | "traceback": [
192 | "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
193 | "\u001b[1;31mApplicationError\u001b[0m Traceback (most recent call last)",
194 | "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[1;31m#Download model here - https://ampl.com/products/solvers/open-source/#cbc\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 2\u001b[0m \u001b[0mopt\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mSolverFactory\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'cbc'\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;31m# choose a solver\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 3\u001b[1;33m \u001b[0mresults\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mopt\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msolve\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mmodel\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;31m# solve the model with the selected solver\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m",
195 | "\u001b[1;32m~\\Anaconda3\\lib\\site-packages\\pyomo\\opt\\base\\solvers.py\u001b[0m in \u001b[0;36msolve\u001b[1;34m(self, *args, **kwds)\u001b[0m\n\u001b[0;32m 510\u001b[0m \u001b[1;34m\"\"\" Solve the problem \"\"\"\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 511\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 512\u001b[1;33m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mavailable\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mexception_flag\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;32mTrue\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 513\u001b[0m \u001b[1;31m#\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 514\u001b[0m \u001b[1;31m# If the inputs are models, then validate that they have been\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
196 | "\u001b[1;32m~\\Anaconda3\\lib\\site-packages\\pyomo\\opt\\solver\\shellcmd.py\u001b[0m in \u001b[0;36mavailable\u001b[1;34m(self, exception_flag)\u001b[0m\n\u001b[0;32m 124\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mexception_flag\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 125\u001b[0m \u001b[0mmsg\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;34m\"No executable found for solver '%s'\"\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 126\u001b[1;33m \u001b[1;32mraise\u001b[0m \u001b[0mApplicationError\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mmsg\u001b[0m \u001b[1;33m%\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mname\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 127\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[1;32mFalse\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 128\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[1;32mTrue\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
197 | "\u001b[1;31mApplicationError\u001b[0m: No executable found for solver 'cbc'"
198 | ]
199 | }
200 | ],
201 | "source": [
202 | "#Download model here - https://ampl.com/products/solvers/open-source/#cbc\n",
203 | "opt = SolverFactory('cbc') # choose a solver\n",
204 | "results = opt.solve(model) # solve the model with the selected solver"
205 | ]
206 | },
207 | {
208 | "cell_type": "code",
209 | "execution_count": null,
210 | "metadata": {},
211 | "outputs": [],
212 | "source": []
213 | },
214 | {
215 | "cell_type": "code",
216 | "execution_count": null,
217 | "metadata": {},
218 | "outputs": [],
219 | "source": []
220 | }
221 | ],
222 | "metadata": {
223 | "kernelspec": {
224 | "display_name": "Python 3",
225 | "language": "python",
226 | "name": "python3"
227 | },
228 | "language_info": {
229 | "codemirror_mode": {
230 | "name": "ipython",
231 | "version": 3
232 | },
233 | "file_extension": ".py",
234 | "mimetype": "text/x-python",
235 | "name": "python",
236 | "nbconvert_exporter": "python",
237 | "pygments_lexer": "ipython3",
238 | "version": "3.7.3"
239 | }
240 | },
241 | "nbformat": 4,
242 | "nbformat_minor": 2
243 | }
244 |
--------------------------------------------------------------------------------