├── .gitignore ├── Introduction to Linear Programming with Python - Part 1.ipynb ├── Introduction to Linear Programming with Python - Part 2.ipynb ├── Introduction to Linear Programming with Python - Part 3.ipynb ├── Introduction to Linear Programming with Python - Part 4.ipynb ├── Introduction to Linear Programming with Python - Part 5.ipynb ├── Introduction to Linear Programming with Python - Part 6.ipynb ├── LaTeX_formatted_ipynb_files ├── Introduction to Linear Programming with Python - Part 1.ipynb ├── Introduction to Linear Programming with Python - Part 2.ipynb ├── Introduction to Linear Programming with Python - Part 3.ipynb ├── Introduction to Linear Programming with Python - Part 4.ipynb ├── Introduction to Linear Programming with Python - Part 5.ipynb ├── Introduction to Linear Programming with Python - Part 6.ipynb ├── part1.html ├── part2.html ├── part3.html ├── part4.html ├── part5.html └── part6.html ├── README.md └── csv ├── factory_variables.csv └── monthly_demand.csv /.gitignore: -------------------------------------------------------------------------------- 1 | # OS generated files # 2 | ###################### 3 | .DS_Store 4 | .DS_Store? 5 | ._* 6 | .Spotlight-V100 7 | .Trashes 8 | ehthumbs.db 9 | Thumbs.db 10 | 11 | # ipynb checkpoints # 12 | ###################### 13 | .ipynb_checkpoints -------------------------------------------------------------------------------- /Introduction to Linear Programming with Python - Part 1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Introduction to Linear Programming with Python - Part 1\n", 8 | "## Introduction to Linear Programming" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "In this set of notebooks we will be looking at some linear programming problems and how we can construct and solve these problems using the python linear programming package [PuLP](http://pythonhosted.org/PuLP/).\n", 16 | "\n", 17 | "Let's start with a simple example:\n", 18 | "\n", 19 | "We want to find the maximum solution to:\n", 20 | "\n", 21 | "Z = 4x + 3y\n", 22 | "\n", 23 | "This is known as our objective function. x and y in this equation are our decision variables.\n", 24 | "\n", 25 | "In this example, the objective function is subject to the following constraints:\n", 26 | "\n", 27 | "\n", 28 | " x ≥ 0 \n", 29 | " y ≥ 2 \n", 30 | " 2y ≤ 25 - x \n", 31 | " 4y ≥ 2x - 8 \n", 32 | " y ≤ 2x - 5 \n", 33 | "\n", 34 | "We'll begin by graphing this problem" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": 1, 40 | "metadata": { 41 | "collapsed": false 42 | }, 43 | "outputs": [ 44 | { 45 | "data": { 46 | "text/plain": [ 47 | "" 48 | ] 49 | }, 50 | "execution_count": 1, 51 | "metadata": {}, 52 | "output_type": "execute_result" 53 | }, 54 | { 55 | "data": { 56 | "image/png": "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\n2In/D4WFHFLXaYNSAAAAAElFTkSuQmCC\n", 57 | "text/plain": [ 58 | "" 59 | ] 60 | }, 61 | "metadata": {}, 62 | "output_type": "display_data" 63 | } 64 | ], 65 | "source": [ 66 | "import numpy as np\n", 67 | "import matplotlib.pyplot as plt\n", 68 | "%matplotlib inline\n", 69 | "\n", 70 | "# Construct lines\n", 71 | "# x > 0\n", 72 | "x = np.linspace(0, 20, 2000)\n", 73 | "# y >= 2\n", 74 | "y1 = (x*0) + 2\n", 75 | "# 2y <= 25 - x\n", 76 | "y2 = (25-x)/2.0\n", 77 | "# 4y >= 2x - 8 \n", 78 | "y3 = (2*x-8)/4.0\n", 79 | "# y <= 2x - 5 \n", 80 | "y4 = 2 * x -5\n", 81 | "\n", 82 | "# Make plot\n", 83 | "plt.plot(x, y1, label=r'$y\\geq2$')\n", 84 | "plt.plot(x, y2, label=r'$2y\\leq25-x$')\n", 85 | "plt.plot(x, y3, label=r'$4y\\geq 2x - 8$')\n", 86 | "plt.plot(x, y4, label=r'$y\\leq 2x-5$')\n", 87 | "plt.xlim((0, 16))\n", 88 | "plt.ylim((0, 11))\n", 89 | "plt.xlabel(r'$x$')\n", 90 | "plt.ylabel(r'$y$')\n", 91 | "\n", 92 | "# Fill feasible region\n", 93 | "y5 = np.minimum(y2, y4)\n", 94 | "y6 = np.maximum(y1, y3)\n", 95 | "plt.fill_between(x, y5, y6, where=y5>y6, color='grey', alpha=0.5)\n", 96 | "plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "metadata": {}, 102 | "source": [ 103 | "Our solution lies somewhere in the grey feasible region in the graph above.\n", 104 | "\n", 105 | "It has been proven that the minima and maxima of linear programming problems lie at the vertices of the feasible region. In this example, there are only 4 corners to our feasible region, so we can find the solutions for each corner to find our maximum.\n", 106 | "\n", 107 | "The four corners are between the lines:\n", 108 | "\n", 109 | "| Line 1 | Line 2 | \n", 110 | "| ------------- |:-------------:| \n", 111 | "| y ≥ 2 | 4y ≥ 2x - 8 | \n", 112 | "| 2y ≤ 25 - x | y ≤ 2x - 5 | \n", 113 | "| 2y ≤ 25 - x | 4y ≥ 2x - 8 |\n", 114 | "| y ≥ 2 | y ≤ 2x - 5 |\n", 115 | "\n", 116 | "So keeping in mind that:\n", 117 | "\n", 118 | "Z = 4x + 3y\n", 119 | "\n", 120 | "We can calculate Z for each corner:" 121 | ] 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "metadata": {}, 126 | "source": [ 127 | "1)\n", 128 | "\n", 129 | " y ≥ 2 and 4y ≥ 2x - 8 \n", 130 | " 2 = (2x - 8) / 4 \n", 131 | " x = 8 \n", 132 | " y = 2 \n", 133 | " Z = 38 \n", 134 | "\n", 135 | "\n", 136 | "2)\n", 137 | "\n", 138 | " 2y ≤ 25 - x and y ≤ 2x - 5 \n", 139 | " (25 - x) / 2 = (2x - 5) \n", 140 | " x = 7 \n", 141 | " y = 9 \n", 142 | " Z = 55 \n", 143 | "\n", 144 | "\n", 145 | "3)\n", 146 | "\n", 147 | " 2y ≤ 25 - x and 4y ≥ 2x - 8 \n", 148 | " (25 - x) / 2 = (2x - 8) / 4 \n", 149 | " x = 14.5 \n", 150 | " y = 5.25 \n", 151 | " Z = 73.75 \n", 152 | "\n", 153 | "4)\n", 154 | "\n", 155 | " y ≥ 2 and y ≤ 2x - 5 \n", 156 | " 2 = 2x-5 \n", 157 | " x = 3.5 \n", 158 | " y = 2 \n", 159 | " Z = 20 " 160 | ] 161 | }, 162 | { 163 | "cell_type": "markdown", 164 | "metadata": { 165 | "collapsed": true 166 | }, 167 | "source": [ 168 | "We have successfully calculated that the maximum value for Z is 73.75, when x is 14.5 and y is 5.25.\n", 169 | "\n", 170 | "This method of testing every vertex is only feasible for a small number of variables and constraints.\n", 171 | "As the numbers of constraints and variables increase, it becomes far more difficult to graph these problems and work out all the vertices.\n", 172 | "For example, if there were a third variable:\n", 173 | "\n", 174 | "Z = Ax + By + Cz\n", 175 | "\n", 176 | "We would have to graph in three dimensions (x, y and z).\n", 177 | "\n", 178 | "In the next few notebooks, we'll take a look at how we can use python and the PuLP package to solve this linear programming problem, as well as some more complex problems" 179 | ] 180 | } 181 | ], 182 | "metadata": { 183 | "kernelspec": { 184 | "display_name": "Python 2", 185 | "language": "python", 186 | "name": "python2" 187 | }, 188 | "language_info": { 189 | "codemirror_mode": { 190 | "name": "ipython", 191 | "version": 2 192 | }, 193 | "file_extension": ".py", 194 | "mimetype": "text/x-python", 195 | "name": "python", 196 | "nbconvert_exporter": "python", 197 | "pygments_lexer": "ipython2", 198 | "version": "2.7.10" 199 | } 200 | }, 201 | "nbformat": 4, 202 | "nbformat_minor": 0 203 | } 204 | -------------------------------------------------------------------------------- /Introduction to Linear Programming with Python - Part 2.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Introduction to Linear Programming with Python - Part 2\n", 8 | "## Introduction to PuLP" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "[PuLP](http://pythonhosted.org/PuLP/) is an open source linear programming package for python. PuLP can be installed using pip, instructions [here](http://pythonhosted.org/PuLP/main/installing_pulp_at_home.html).\n", 16 | "\n", 17 | "In this notebook, we'll explore how to construct and solve the linear programming problem described in Part 1 using PuLP.\n", 18 | "\n", 19 | "A brief reminder of our linear programming problem:\n", 20 | "\n", 21 | "We want to find the maximum solution to the objective function:\n", 22 | "\n", 23 | "Z = 4x + 3y\n", 24 | "\n", 25 | "Subject to the following constraints:\n", 26 | "\n", 27 | " x ≥ 0 \n", 28 | " y ≥ 2 \n", 29 | " 2y ≤ 25 - x \n", 30 | " 4y ≥ 2x - 8 \n", 31 | " y ≤ 2x - 5 " 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "We'll begin by importing PuLP" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": 1, 44 | "metadata": { 45 | "collapsed": true 46 | }, 47 | "outputs": [], 48 | "source": [ 49 | "import pulp" 50 | ] 51 | }, 52 | { 53 | "cell_type": "markdown", 54 | "metadata": {}, 55 | "source": [ 56 | "Then instantiate a problem class, we'll name it \"My LP problem\" and we're looking for an optimal maximum so we use LpMaximize" 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": 2, 62 | "metadata": { 63 | "collapsed": false 64 | }, 65 | "outputs": [], 66 | "source": [ 67 | "my_lp_problem = pulp.LpProblem(\"My LP Problem\", pulp.LpMaximize)" 68 | ] 69 | }, 70 | { 71 | "cell_type": "markdown", 72 | "metadata": {}, 73 | "source": [ 74 | "We then model our decision variables using the LpVariable class. In our example, x had a lower bound of 0 and y had a lower bound of 2.\n", 75 | "\n", 76 | "Upper bounds can be assigned using the upBound parameter." 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": 3, 82 | "metadata": { 83 | "collapsed": false 84 | }, 85 | "outputs": [], 86 | "source": [ 87 | "x = pulp.LpVariable('x', lowBound=0, cat='Continuous')\n", 88 | "y = pulp.LpVariable('y', lowBound=2, cat='Continuous')" 89 | ] 90 | }, 91 | { 92 | "cell_type": "markdown", 93 | "metadata": {}, 94 | "source": [ 95 | "The objective function and constraints are added using the += operator to our model. \n", 96 | "\n", 97 | "The objective function is added first, then the individual constraints." 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": 4, 103 | "metadata": { 104 | "collapsed": true 105 | }, 106 | "outputs": [], 107 | "source": [ 108 | "# Objective function\n", 109 | "my_lp_problem += 4 * x + 3 * y, \"Z\"\n", 110 | "\n", 111 | "# Constraints\n", 112 | "my_lp_problem += 2 * y <= 25 - x\n", 113 | "my_lp_problem += 4 * y >= 2 * x - 8\n", 114 | "my_lp_problem += y <= 2 * x - 5" 115 | ] 116 | }, 117 | { 118 | "cell_type": "markdown", 119 | "metadata": {}, 120 | "source": [ 121 | "We have now constructed our problem and can have a look at it." 122 | ] 123 | }, 124 | { 125 | "cell_type": "code", 126 | "execution_count": 5, 127 | "metadata": { 128 | "collapsed": false 129 | }, 130 | "outputs": [ 131 | { 132 | "data": { 133 | "text/plain": [ 134 | "My LP Problem:\n", 135 | "MAXIMIZE\n", 136 | "4*x + 3*y + 0\n", 137 | "SUBJECT TO\n", 138 | "_C1: x + 2 y <= 25\n", 139 | "\n", 140 | "_C2: - 2 x + 4 y >= -8\n", 141 | "\n", 142 | "_C3: - 2 x + y <= -5\n", 143 | "\n", 144 | "VARIABLES\n", 145 | "x Continuous\n", 146 | "2 <= y Continuous" 147 | ] 148 | }, 149 | "execution_count": 5, 150 | "metadata": {}, 151 | "output_type": "execute_result" 152 | } 153 | ], 154 | "source": [ 155 | "my_lp_problem" 156 | ] 157 | }, 158 | { 159 | "cell_type": "markdown", 160 | "metadata": {}, 161 | "source": [ 162 | "PuLP supports open source linear programming solvers such as CBC and GLPK, as well as commercial solvers such as Gurobi and IBM's CPLEX.\n", 163 | "\n", 164 | "The default solver is CBC, which comes packaged with PuLP upon installation.\n", 165 | "\n", 166 | "For most applications, the open source CBC from [COIN-OR](http://www.coin-or.org/) will be enough for most simple linear programming optimisation algorithms." 167 | ] 168 | }, 169 | { 170 | "cell_type": "code", 171 | "execution_count": 6, 172 | "metadata": { 173 | "collapsed": false 174 | }, 175 | "outputs": [ 176 | { 177 | "data": { 178 | "text/plain": [ 179 | "'Optimal'" 180 | ] 181 | }, 182 | "execution_count": 6, 183 | "metadata": {}, 184 | "output_type": "execute_result" 185 | } 186 | ], 187 | "source": [ 188 | "my_lp_problem.solve()\n", 189 | "pulp.LpStatus[my_lp_problem.status]" 190 | ] 191 | }, 192 | { 193 | "cell_type": "markdown", 194 | "metadata": {}, 195 | "source": [ 196 | "We have also checked the status of the solver, there are 5 status codes:\n", 197 | "* **Not Solved**: Status prior to solving the problem.\n", 198 | "* **Optimal**: An optimal solution has been found.\n", 199 | "* **Infeasible**: There are no feasible solutions (e.g. if you set the constraints x <= 1 and x >=2).\n", 200 | "* **Unbounded**: The constraints are not bounded, maximising the solution will tend towards infinity (e.g. if the only constraint was x >= 3).\n", 201 | "* **Undefined**: The optimal solution may exist but may not have been found." 202 | ] 203 | }, 204 | { 205 | "cell_type": "markdown", 206 | "metadata": {}, 207 | "source": [ 208 | "We can now view our maximal variable values and the maximum value of Z. \n", 209 | "\n", 210 | "We can use the varValue method to retrieve the values of our variables x and y, and the pulp.value function to view the maximum value of the objective function." 211 | ] 212 | }, 213 | { 214 | "cell_type": "code", 215 | "execution_count": 7, 216 | "metadata": { 217 | "collapsed": false 218 | }, 219 | "outputs": [ 220 | { 221 | "name": "stdout", 222 | "output_type": "stream", 223 | "text": [ 224 | "x = 14.5\n", 225 | "y = 5.25\n" 226 | ] 227 | } 228 | ], 229 | "source": [ 230 | "for variable in my_lp_problem.variables():\n", 231 | " print \"{} = {}\".format(variable.name, variable.varValue)" 232 | ] 233 | }, 234 | { 235 | "cell_type": "code", 236 | "execution_count": 8, 237 | "metadata": { 238 | "collapsed": false 239 | }, 240 | "outputs": [ 241 | { 242 | "name": "stdout", 243 | "output_type": "stream", 244 | "text": [ 245 | "73.75\n" 246 | ] 247 | } 248 | ], 249 | "source": [ 250 | "print pulp.value(my_lp_problem.objective)" 251 | ] 252 | }, 253 | { 254 | "cell_type": "markdown", 255 | "metadata": {}, 256 | "source": [ 257 | " Same values as our manual calculations in part 1.\n", 258 | " \n", 259 | " In the next part we'll be looking at a more real world problem." 260 | ] 261 | } 262 | ], 263 | "metadata": { 264 | "kernelspec": { 265 | "display_name": "Python 2", 266 | "language": "python", 267 | "name": "python2" 268 | }, 269 | "language_info": { 270 | "codemirror_mode": { 271 | "name": "ipython", 272 | "version": 2 273 | }, 274 | "file_extension": ".py", 275 | "mimetype": "text/x-python", 276 | "name": "python", 277 | "nbconvert_exporter": "python", 278 | "pygments_lexer": "ipython2", 279 | "version": "2.7.10" 280 | } 281 | }, 282 | "nbformat": 4, 283 | "nbformat_minor": 0 284 | } 285 | -------------------------------------------------------------------------------- /Introduction to Linear Programming with Python - Part 3.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Introduction to Linear Programming with Python - Part 3\n", 8 | "## Real world examples - Resourcing Problem" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "We'll now look at 2 more real world examples. \n", 16 | "\n", 17 | "The first is a resourcing problem and the second is a blending problem.\n", 18 | "\n", 19 | "\n", 20 | "### Resourcing Problem" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "metadata": {}, 26 | "source": [ 27 | "We're consulting for a boutique car manufacturer, producing luxury cars.\n", 28 | "\n", 29 | "They run on one month (30 days) cycles, we have one cycle to show we can provide value.\n", 30 | "\n", 31 | "There is one robot, 2 engineers and one detailer in the factory. The detailer has some holiday off, so only has 21 days available.\n", 32 | "\n", 33 | "The 2 cars need different time with each resource:\n", 34 | "\n", 35 | "**Robot time:** Car A - 3 days; Car B - 4 days.\n", 36 | "\n", 37 | "**Engineer time:** Car A - 5 days; Car B - 6 days.\n", 38 | "\n", 39 | "**Detailer time:** Car A - 1.5 days; Car B - 3 days.\n", 40 | "\n", 41 | "Car A provides €30,000 profit, whilst Car B offers €45,000 profit.\n", 42 | "\n", 43 | "At the moment, they produce 4 of each cars per month, for €300,000 profit. Not bad at all, but we think we can do better for them." 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "metadata": {}, 49 | "source": [ 50 | "This can be modelled as follows:\n", 51 | "\n", 52 | "Maximise\n", 53 | "\n", 54 | "Profit = 30,000A + 45,000B\n", 55 | "\n", 56 | "Subject to:\n", 57 | "\n", 58 | "A ≥ 0\n", 59 | "\n", 60 | "B ≥ 0\n", 61 | "\n", 62 | "3A + 4B ≤ 30\n", 63 | "\n", 64 | "5A + 6B ≤ 60\n", 65 | "\n", 66 | "1.5A + 3B ≤ 21" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 1, 72 | "metadata": { 73 | "collapsed": true 74 | }, 75 | "outputs": [], 76 | "source": [ 77 | "import pulp" 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": 2, 83 | "metadata": { 84 | "collapsed": true 85 | }, 86 | "outputs": [], 87 | "source": [ 88 | "# Instantiate our problem class\n", 89 | "model = pulp.LpProblem(\"Profit maximising problem\", pulp.LpMaximize)" 90 | ] 91 | }, 92 | { 93 | "cell_type": "markdown", 94 | "metadata": {}, 95 | "source": [ 96 | "Unlike our previous problem, the decision variables in this case won't be continuous (We can't sell half a car!), so the category is integer." 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": 3, 102 | "metadata": { 103 | "collapsed": true 104 | }, 105 | "outputs": [], 106 | "source": [ 107 | "A = pulp.LpVariable('A', lowBound=0, cat='Integer')\n", 108 | "B = pulp.LpVariable('B', lowBound=0, cat='Integer')" 109 | ] 110 | }, 111 | { 112 | "cell_type": "code", 113 | "execution_count": 4, 114 | "metadata": { 115 | "collapsed": true 116 | }, 117 | "outputs": [], 118 | "source": [ 119 | "# Objective function\n", 120 | "model += 30000 * A + 45000 * B, \"Profit\"\n", 121 | "\n", 122 | "# Constraints\n", 123 | "model += 3 * A + 4 * B <= 30\n", 124 | "model += 5 * A + 6 * B <= 60\n", 125 | "model += 1.5 * A + 3 * B <= 21" 126 | ] 127 | }, 128 | { 129 | "cell_type": "code", 130 | "execution_count": 5, 131 | "metadata": { 132 | "collapsed": false 133 | }, 134 | "outputs": [ 135 | { 136 | "data": { 137 | "text/plain": [ 138 | "'Optimal'" 139 | ] 140 | }, 141 | "execution_count": 5, 142 | "metadata": {}, 143 | "output_type": "execute_result" 144 | } 145 | ], 146 | "source": [ 147 | "# Solve our problem\n", 148 | "model.solve()\n", 149 | "pulp.LpStatus[model.status]" 150 | ] 151 | }, 152 | { 153 | "cell_type": "code", 154 | "execution_count": 6, 155 | "metadata": { 156 | "collapsed": false 157 | }, 158 | "outputs": [ 159 | { 160 | "name": "stdout", 161 | "output_type": "stream", 162 | "text": [ 163 | "Production of Car A = 2.0\n", 164 | "Production of Car B = 6.0\n" 165 | ] 166 | } 167 | ], 168 | "source": [ 169 | "# Print our decision variable values\n", 170 | "print \"Production of Car A = {}\".format(A.varValue)\n", 171 | "print \"Production of Car B = {}\".format(B.varValue)" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": 7, 177 | "metadata": { 178 | "collapsed": false 179 | }, 180 | "outputs": [ 181 | { 182 | "name": "stdout", 183 | "output_type": "stream", 184 | "text": [ 185 | "330000.0\n" 186 | ] 187 | } 188 | ], 189 | "source": [ 190 | "# Print our objective function value\n", 191 | "print pulp.value(model.objective)" 192 | ] 193 | }, 194 | { 195 | "cell_type": "markdown", 196 | "metadata": {}, 197 | "source": [ 198 | "So that's €330,000 monthly profit, compared to their original monthly profit of €300,000\n", 199 | "\n", 200 | "By producing 2 cars of Car A and 4 cars of Car B, we bolster the profits at the factory by €30,000 per month.\n", 201 | "\n", 202 | "We take our consultancy fee and leave the company with €360,000 extra profit for the factory every year.\n", 203 | "\n", 204 | "In the next part, we'll be making some sausages!" 205 | ] 206 | } 207 | ], 208 | "metadata": { 209 | "kernelspec": { 210 | "display_name": "Python 2", 211 | "language": "python", 212 | "name": "python2" 213 | }, 214 | "language_info": { 215 | "codemirror_mode": { 216 | "name": "ipython", 217 | "version": 2 218 | }, 219 | "file_extension": ".py", 220 | "mimetype": "text/x-python", 221 | "name": "python", 222 | "nbconvert_exporter": "python", 223 | "pygments_lexer": "ipython2", 224 | "version": "2.7.10" 225 | } 226 | }, 227 | "nbformat": 4, 228 | "nbformat_minor": 0 229 | } 230 | -------------------------------------------------------------------------------- /Introduction to Linear Programming with Python - Part 4.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Introduction to Linear Programming with Python - Part 4\n", 8 | "## Real world examples - Blending Problem" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "We're going to make some sausages!\n", 16 | "\n", 17 | "We have the following ingredients available to us:\n", 18 | "\n", 19 | "| Ingredient | Cost (€/kg) | Availability (kg) |\n", 20 | "|------------|--------------|-------------------|\n", 21 | "| Pork | 4.32 | 30 |\n", 22 | "| Wheat | 2.46 | 20 |\n", 23 | "| Starch | 1.86 | 17 |\n", 24 | "\n", 25 | "We'll make 2 types of sausage:\n", 26 | "* Economy (>40% Pork)\n", 27 | "* Premium (>60% Pork)\n", 28 | "\n", 29 | "One sausage is 50 grams (0.05 kg)\n", 30 | "\n", 31 | "According to government regulations, the most starch we can use in our sausages is 25%\n", 32 | "\n", 33 | "We have a contract with a butcher, and have already purchased 23 kg pork, that will go bad if it's not used.\n", 34 | "\n", 35 | "We have a demand for 350 economy sausages and 500 premium sausages.\n", 36 | "\n", 37 | "We need to figure out how to most cost effectively blend our sausages." 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "metadata": {}, 43 | "source": [ 44 | "Let's model our problem\n", 45 | "\n", 46 | " *pe* = Pork in the economy sausages (kg) \n", 47 | " *we* = Wheat in the economy sausages (kg) \n", 48 | " *se* = Starch in the economy sausages (kg) \n", 49 | " *pp* = Pork in the premium sausages (kg) \n", 50 | " *wp* = Wheat in the premium sausages (kg) \n", 51 | " *sp* = Starch in the premium sausages (kg) \n", 52 | "\n", 53 | "We want to minimise costs such that:\n", 54 | "\n", 55 | "Cost = 0.72(*pe* + *pp*) + 0.41(*we* + *wp*) + 0.31(*se* + *sp*)\n", 56 | "\n", 57 | "\n", 58 | "With the following constraints:\n", 59 | "\n", 60 | " *pe* + *we* + *se* = 350 \\* 0.05 \n", 61 | " *pp* + *wp* + *sp* = 500 \\* 0.05 \n", 62 | " *pe* ≥ 0.4(*pe* + *we* + *se*) \n", 63 | " *pp* ≥ 0.6(*pp* + *wp* + *sp*) \n", 64 | " *se* ≤ 0.25(*pe* + *we* + *se*) \n", 65 | " *sp* ≤ 0.25(*pp* + *wp* + *sp*) \n", 66 | " *pe* + *pp* ≤ 30 \n", 67 | " *we* + *wp* ≤ 20 \n", 68 | " *se* + *sp* ≤ 17 \n", 69 | " *pe* + *pp* ≥ 23" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": 1, 75 | "metadata": { 76 | "collapsed": true 77 | }, 78 | "outputs": [], 79 | "source": [ 80 | "import pulp" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": 2, 86 | "metadata": { 87 | "collapsed": true 88 | }, 89 | "outputs": [], 90 | "source": [ 91 | "# Instantiate our problem class\n", 92 | "model = pulp.LpProblem(\"Cost minimising blending problem\", pulp.LpMinimize)" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "metadata": {}, 98 | "source": [ 99 | "Here we have 6 decision variables, we could name them individually but this wouldn't scale up if we had hundreds/thousands of variables (you don't want to be entering all of these by hand multiple times). \n", 100 | "\n", 101 | "We'll create a couple of lists from which we can create tuple indices." 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": 3, 107 | "metadata": { 108 | "collapsed": false 109 | }, 110 | "outputs": [], 111 | "source": [ 112 | "# Construct our decision variable lists\n", 113 | "sausage_types = ['economy', 'premium']\n", 114 | "ingredients = ['pork', 'wheat', 'starch']" 115 | ] 116 | }, 117 | { 118 | "cell_type": "markdown", 119 | "metadata": {}, 120 | "source": [ 121 | "Each of these decision variables will have similar characteristics (lower bound of 0, continuous variables). Therefore we can use PuLP's LpVariable object's dict functionality, we can provide our tuple indices.\n", 122 | "\n", 123 | "These tuples will be keys for the ing_weight dict of decision variables" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": 4, 129 | "metadata": { 130 | "collapsed": false 131 | }, 132 | "outputs": [], 133 | "source": [ 134 | "ing_weight = pulp.LpVariable.dicts(\"weight kg\",\n", 135 | " ((i, j) for i in sausage_types for j in ingredients),\n", 136 | " lowBound=0,\n", 137 | " cat='Continuous')" 138 | ] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "metadata": {}, 143 | "source": [ 144 | "PuLP provides an lpSum vector calculation for the sum of a list of linear expressions.\n", 145 | "\n", 146 | "Whilst we only have 6 decision variables, I will demonstrate how the problem would be constructed in a way that could be scaled up to many variables using list comprehensions." 147 | ] 148 | }, 149 | { 150 | "cell_type": "code", 151 | "execution_count": 5, 152 | "metadata": { 153 | "collapsed": false 154 | }, 155 | "outputs": [], 156 | "source": [ 157 | "# Objective Function\n", 158 | "model += (\n", 159 | " pulp.lpSum([\n", 160 | " 4.32 * ing_weight[(i, 'pork')]\n", 161 | " + 2.46 * ing_weight[(i, 'wheat')]\n", 162 | " + 1.86 * ing_weight[(i, 'starch')]\n", 163 | " for i in sausage_types])\n", 164 | ")" 165 | ] 166 | }, 167 | { 168 | "cell_type": "markdown", 169 | "metadata": {}, 170 | "source": [ 171 | "Now we add our constraints, bear in mind again here how the use of list comprehensions allows for scaling up to many ingredients or sausage types" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": 6, 177 | "metadata": { 178 | "collapsed": true 179 | }, 180 | "outputs": [], 181 | "source": [ 182 | "# Constraints\n", 183 | "# 350 economy and 500 premium sausages at 0.05 kg\n", 184 | "model += pulp.lpSum([ing_weight['economy', j] for j in ingredients]) == 350 * 0.05\n", 185 | "model += pulp.lpSum([ing_weight['premium', j] for j in ingredients]) == 500 * 0.05\n", 186 | "\n", 187 | "# Economy has >= 40% pork, premium >= 60% pork\n", 188 | "model += ing_weight['economy', 'pork'] >= (\n", 189 | " 0.4 * pulp.lpSum([ing_weight['economy', j] for j in ingredients]))\n", 190 | "\n", 191 | "model += ing_weight['premium', 'pork'] >= (\n", 192 | " 0.6 * pulp.lpSum([ing_weight['premium', j] for j in ingredients]))\n", 193 | "\n", 194 | "# Sausages must be <= 25% starch\n", 195 | "model += ing_weight['economy', 'starch'] <= (\n", 196 | " 0.25 * pulp.lpSum([ing_weight['economy', j] for j in ingredients]))\n", 197 | "\n", 198 | "model += ing_weight['premium', 'starch'] <= (\n", 199 | " 0.25 * pulp.lpSum([ing_weight['premium', j] for j in ingredients]))\n", 200 | "\n", 201 | "# We have at most 30 kg of pork, 20 kg of wheat and 17 kg of starch available\n", 202 | "model += pulp.lpSum([ing_weight[i, 'pork'] for i in sausage_types]) <= 30\n", 203 | "model += pulp.lpSum([ing_weight[i, 'wheat'] for i in sausage_types]) <= 20\n", 204 | "model += pulp.lpSum([ing_weight[i, 'starch'] for i in sausage_types]) <= 17\n", 205 | "\n", 206 | "# We have at least 23 kg of pork to use up\n", 207 | "model += pulp.lpSum([ing_weight[i, 'pork'] for i in sausage_types]) >= 23" 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": 7, 213 | "metadata": { 214 | "collapsed": false 215 | }, 216 | "outputs": [ 217 | { 218 | "data": { 219 | "text/plain": [ 220 | "'Optimal'" 221 | ] 222 | }, 223 | "execution_count": 7, 224 | "metadata": {}, 225 | "output_type": "execute_result" 226 | } 227 | ], 228 | "source": [ 229 | "# Solve our problem\n", 230 | "model.solve()\n", 231 | "pulp.LpStatus[model.status]" 232 | ] 233 | }, 234 | { 235 | "cell_type": "code", 236 | "execution_count": 8, 237 | "metadata": { 238 | "collapsed": false 239 | }, 240 | "outputs": [ 241 | { 242 | "name": "stdout", 243 | "output_type": "stream", 244 | "text": [ 245 | "The weight of starch in premium sausages is 6.25 kg\n", 246 | "The weight of starch in economy sausages is 4.375 kg\n", 247 | "The weight of wheat in economy sausages is 6.125 kg\n", 248 | "The weight of wheat in premium sausages is 2.75 kg\n", 249 | "The weight of pork in economy sausages is 7.0 kg\n", 250 | "The weight of pork in premium sausages is 16.0 kg\n" 251 | ] 252 | } 253 | ], 254 | "source": [ 255 | "for var in ing_weight:\n", 256 | " var_value = ing_weight[var].varValue\n", 257 | " print \"The weight of {0} in {1} sausages is {2} kg\".format(var[1], var[0], var_value)" 258 | ] 259 | }, 260 | { 261 | "cell_type": "code", 262 | "execution_count": 9, 263 | "metadata": { 264 | "collapsed": false 265 | }, 266 | "outputs": [ 267 | { 268 | "name": "stdout", 269 | "output_type": "stream", 270 | "text": [ 271 | "The total cost is €140.96 for 350 economy sausages and 500 premium sausages\n" 272 | ] 273 | } 274 | ], 275 | "source": [ 276 | "total_cost = pulp.value(model.objective)\n", 277 | "\n", 278 | "print \"The total cost is €{} for 350 economy sausages and 500 premium sausages\".format(round(total_cost, 2))" 279 | ] 280 | } 281 | ], 282 | "metadata": { 283 | "kernelspec": { 284 | "display_name": "Python 2", 285 | "language": "python", 286 | "name": "python2" 287 | }, 288 | "language_info": { 289 | "codemirror_mode": { 290 | "name": "ipython", 291 | "version": 2 292 | }, 293 | "file_extension": ".py", 294 | "mimetype": "text/x-python", 295 | "name": "python", 296 | "nbconvert_exporter": "python", 297 | "pygments_lexer": "ipython2", 298 | "version": "2.7.10" 299 | } 300 | }, 301 | "nbformat": 4, 302 | "nbformat_minor": 0 303 | } 304 | -------------------------------------------------------------------------------- /Introduction to Linear Programming with Python - Part 5.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Introduction to Linear Programming with Python - Part 5\n", 8 | "## Using PuLP with pandas and binary constraints to solve a scheduling problem" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "In this example, we'll be solving a scheduling problem. We have 2 offshore production plants in 2 locations and an estimated demand for our products. \n", 16 | "\n", 17 | "We want to produce a schedule of production from both plants that meets our demand with the lowest cost.\n", 18 | "\n", 19 | "A factory can be in 2 states:\n", 20 | "* Off - Producing zero units\n", 21 | "* On - Producing between its minimum and maximum production capacities\n", 22 | "\n", 23 | "Both factories have fixed costs, that are incurred as long as the factory is on, and variable costs, a cost per unit of production. These vary month by month. \n", 24 | "\n", 25 | "We also know that factory B is down for maintenance in month 5.\n", 26 | "\n", 27 | "We'll start by importing our data. \n", 28 | "\n", 29 | "The data is imported into a multi-index pandas DataFrame using 'Month' and 'Factory' as our index columns." 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": 1, 35 | "metadata": { 36 | "collapsed": false 37 | }, 38 | "outputs": [], 39 | "source": [ 40 | "import pandas as pd\n", 41 | "import pulp" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": 2, 47 | "metadata": { 48 | "collapsed": false 49 | }, 50 | "outputs": [ 51 | { 52 | "data": { 53 | "text/html": [ 54 | "
\n", 55 | "\n", 56 | " \n", 57 | " \n", 58 | " \n", 59 | " \n", 60 | " \n", 61 | " \n", 62 | " \n", 63 | " \n", 64 | " \n", 65 | " \n", 66 | " \n", 67 | " \n", 68 | " \n", 69 | " \n", 70 | " \n", 71 | " \n", 72 | " \n", 73 | " \n", 74 | " \n", 75 | " \n", 76 | " \n", 77 | " \n", 78 | " \n", 79 | " \n", 80 | " \n", 81 | " \n", 82 | " \n", 83 | " \n", 84 | " \n", 85 | " \n", 86 | " \n", 87 | " \n", 88 | " \n", 89 | " \n", 90 | " \n", 91 | " \n", 92 | " \n", 93 | " \n", 94 | " \n", 95 | " \n", 96 | " \n", 97 | " \n", 98 | " \n", 99 | " \n", 100 | " \n", 101 | " \n", 102 | " \n", 103 | " \n", 104 | " \n", 105 | " \n", 106 | " \n", 107 | " \n", 108 | " \n", 109 | " \n", 110 | " \n", 111 | " \n", 112 | " \n", 113 | " \n", 114 | " \n", 115 | " \n", 116 | " \n", 117 | " \n", 118 | " \n", 119 | " \n", 120 | " \n", 121 | " \n", 122 | " \n", 123 | " \n", 124 | " \n", 125 | " \n", 126 | " \n", 127 | " \n", 128 | " \n", 129 | " \n", 130 | " \n", 131 | " \n", 132 | " \n", 133 | " \n", 134 | " \n", 135 | " \n", 136 | " \n", 137 | " \n", 138 | " \n", 139 | " \n", 140 | " \n", 141 | " \n", 142 | " \n", 143 | " \n", 144 | " \n", 145 | " \n", 146 | " \n", 147 | " \n", 148 | " \n", 149 | " \n", 150 | " \n", 151 | " \n", 152 | " \n", 153 | " \n", 154 | " \n", 155 | " \n", 156 | " \n", 157 | " \n", 158 | " \n", 159 | " \n", 160 | " \n", 161 | " \n", 162 | " \n", 163 | " \n", 164 | " \n", 165 | " \n", 166 | " \n", 167 | " \n", 168 | " \n", 169 | " \n", 170 | " \n", 171 | " \n", 172 | " \n", 173 | " \n", 174 | " \n", 175 | " \n", 176 | " \n", 177 | " \n", 178 | " \n", 179 | " \n", 180 | " \n", 181 | " \n", 182 | " \n", 183 | " \n", 184 | " \n", 185 | " \n", 186 | " \n", 187 | " \n", 188 | " \n", 189 | " \n", 190 | " \n", 191 | " \n", 192 | " \n", 193 | " \n", 194 | " \n", 195 | " \n", 196 | " \n", 197 | " \n", 198 | " \n", 199 | " \n", 200 | " \n", 201 | " \n", 202 | " \n", 203 | " \n", 204 | " \n", 205 | " \n", 206 | " \n", 207 | " \n", 208 | " \n", 209 | " \n", 210 | " \n", 211 | " \n", 212 | " \n", 213 | " \n", 214 | " \n", 215 | " \n", 216 | " \n", 217 | " \n", 218 | " \n", 219 | " \n", 220 | " \n", 221 | " \n", 222 | " \n", 223 | " \n", 224 | " \n", 225 | " \n", 226 | " \n", 227 | " \n", 228 | " \n", 229 | " \n", 230 | " \n", 231 | " \n", 232 | " \n", 233 | " \n", 234 | " \n", 235 | " \n", 236 | " \n", 237 | " \n", 238 | " \n", 239 | " \n", 240 | " \n", 241 | " \n", 242 | " \n", 243 | " \n", 244 | " \n", 245 | " \n", 246 | " \n", 247 | " \n", 248 | " \n", 249 | " \n", 250 | " \n", 251 | " \n", 252 | " \n", 253 | " \n", 254 | " \n", 255 | " \n", 256 | "
Max_CapacityMin_CapacityVariable_CostsFixed_Costs
MonthFactory
1A1000002000010500
B50000200005600
2A1100002000011500
B55000200004600
3A1200002000012500
B60000200003600
4A145000200009500
B100000200005600
5A160000200008500
B0000
6A140000200008500
B70000200006600
7A155000200005500
B60000200004600
8A200000200007500
B100000200006600
9A210000200009500
B100000200008600
10A1970002000010500
B1000002000011600
11A80000200008500
B1200002000010600
12A150000200008500
B1500002000012600
\n", 257 | "
" 258 | ], 259 | "text/plain": [ 260 | " Max_Capacity Min_Capacity Variable_Costs Fixed_Costs\n", 261 | "Month Factory \n", 262 | "1 A 100000 20000 10 500\n", 263 | " B 50000 20000 5 600\n", 264 | "2 A 110000 20000 11 500\n", 265 | " B 55000 20000 4 600\n", 266 | "3 A 120000 20000 12 500\n", 267 | " B 60000 20000 3 600\n", 268 | "4 A 145000 20000 9 500\n", 269 | " B 100000 20000 5 600\n", 270 | "5 A 160000 20000 8 500\n", 271 | " B 0 0 0 0\n", 272 | "6 A 140000 20000 8 500\n", 273 | " B 70000 20000 6 600\n", 274 | "7 A 155000 20000 5 500\n", 275 | " B 60000 20000 4 600\n", 276 | "8 A 200000 20000 7 500\n", 277 | " B 100000 20000 6 600\n", 278 | "9 A 210000 20000 9 500\n", 279 | " B 100000 20000 8 600\n", 280 | "10 A 197000 20000 10 500\n", 281 | " B 100000 20000 11 600\n", 282 | "11 A 80000 20000 8 500\n", 283 | " B 120000 20000 10 600\n", 284 | "12 A 150000 20000 8 500\n", 285 | " B 150000 20000 12 600" 286 | ] 287 | }, 288 | "execution_count": 2, 289 | "metadata": {}, 290 | "output_type": "execute_result" 291 | } 292 | ], 293 | "source": [ 294 | "factories = pd.DataFrame.from_csv('csv/factory_variables.csv', index_col=['Month', 'Factory'])\n", 295 | "factories" 296 | ] 297 | }, 298 | { 299 | "cell_type": "markdown", 300 | "metadata": {}, 301 | "source": [ 302 | "We'll also import our demand data" 303 | ] 304 | }, 305 | { 306 | "cell_type": "code", 307 | "execution_count": 3, 308 | "metadata": { 309 | "collapsed": true 310 | }, 311 | "outputs": [], 312 | "source": [ 313 | "demand = pd.DataFrame.from_csv('csv/monthly_demand.csv', index_col=['Month'])" 314 | ] 315 | }, 316 | { 317 | "cell_type": "code", 318 | "execution_count": 4, 319 | "metadata": { 320 | "collapsed": false, 321 | "scrolled": true 322 | }, 323 | "outputs": [ 324 | { 325 | "data": { 326 | "text/html": [ 327 | "
\n", 328 | "\n", 329 | " \n", 330 | " \n", 331 | " \n", 332 | " \n", 333 | " \n", 334 | " \n", 335 | " \n", 336 | " \n", 337 | " \n", 338 | " \n", 339 | " \n", 340 | " \n", 341 | " \n", 342 | " \n", 343 | " \n", 344 | " \n", 345 | " \n", 346 | " \n", 347 | " \n", 348 | " \n", 349 | " \n", 350 | " \n", 351 | " \n", 352 | " \n", 353 | " \n", 354 | " \n", 355 | " \n", 356 | " \n", 357 | " \n", 358 | " \n", 359 | " \n", 360 | " \n", 361 | " \n", 362 | " \n", 363 | " \n", 364 | " \n", 365 | " \n", 366 | " \n", 367 | " \n", 368 | " \n", 369 | " \n", 370 | " \n", 371 | " \n", 372 | " \n", 373 | " \n", 374 | " \n", 375 | " \n", 376 | " \n", 377 | " \n", 378 | " \n", 379 | " \n", 380 | " \n", 381 | " \n", 382 | " \n", 383 | " \n", 384 | " \n", 385 | " \n", 386 | " \n", 387 | " \n", 388 | " \n", 389 | "
Demand
Month
1120000
2100000
3130000
4130000
5140000
6130000
7150000
8170000
9200000
10190000
11140000
12100000
\n", 390 | "
" 391 | ], 392 | "text/plain": [ 393 | " Demand\n", 394 | "Month \n", 395 | "1 120000\n", 396 | "2 100000\n", 397 | "3 130000\n", 398 | "4 130000\n", 399 | "5 140000\n", 400 | "6 130000\n", 401 | "7 150000\n", 402 | "8 170000\n", 403 | "9 200000\n", 404 | "10 190000\n", 405 | "11 140000\n", 406 | "12 100000" 407 | ] 408 | }, 409 | "execution_count": 4, 410 | "metadata": {}, 411 | "output_type": "execute_result" 412 | } 413 | ], 414 | "source": [ 415 | "demand" 416 | ] 417 | }, 418 | { 419 | "cell_type": "markdown", 420 | "metadata": {}, 421 | "source": [ 422 | "As we have fixed costs and variable costs, we'll need to model both production and the status of the factory i.e. whether it is on or off.\n", 423 | "\n", 424 | "Production is modelled as an integer variable. \n", 425 | "\n", 426 | "We have a value for production for each month for each factory, this is given by the tuples of our multi-index pandas DataFrame index." 427 | ] 428 | }, 429 | { 430 | "cell_type": "code", 431 | "execution_count": 5, 432 | "metadata": { 433 | "collapsed": false 434 | }, 435 | "outputs": [], 436 | "source": [ 437 | "production = pulp.LpVariable.dicts(\"production\",\n", 438 | " ((month, factory) for month, factory in factories.index),\n", 439 | " lowBound=0,\n", 440 | " cat='Integer')" 441 | ] 442 | }, 443 | { 444 | "cell_type": "markdown", 445 | "metadata": {}, 446 | "source": [ 447 | "Factory status is modelled as a binary variable. It will have a value of 1 if the factory is on and a value of 0 when the factory is off.\n", 448 | "\n", 449 | "Binary variables are the same as integer variables but constrained to be >= 0 and <=1\n", 450 | "\n", 451 | "Again this has a value for each month for each factory, again given by the index of our DataFrame" 452 | ] 453 | }, 454 | { 455 | "cell_type": "code", 456 | "execution_count": 6, 457 | "metadata": { 458 | "collapsed": false 459 | }, 460 | "outputs": [], 461 | "source": [ 462 | "factory_status = pulp.LpVariable.dicts(\"factory_status\",\n", 463 | " ((month, factory) for month, factory in factories.index),\n", 464 | " cat='Binary')" 465 | ] 466 | }, 467 | { 468 | "cell_type": "markdown", 469 | "metadata": {}, 470 | "source": [ 471 | "We instantiate our model and use LpMinimize as the aim is to minimise costs." 472 | ] 473 | }, 474 | { 475 | "cell_type": "code", 476 | "execution_count": 7, 477 | "metadata": { 478 | "collapsed": false 479 | }, 480 | "outputs": [], 481 | "source": [ 482 | "model = pulp.LpProblem(\"Cost minimising scheduling problem\", pulp.LpMinimize)" 483 | ] 484 | }, 485 | { 486 | "cell_type": "markdown", 487 | "metadata": {}, 488 | "source": [ 489 | "In our objective function we include our 2 costs: \n", 490 | "* Our variable costs is the product of the variable costs per unit and production\n", 491 | "* Our fixed costs is the factory status - 1 (on) or 0 (off) - multiplied by the fixed cost of production" 492 | ] 493 | }, 494 | { 495 | "cell_type": "code", 496 | "execution_count": 8, 497 | "metadata": { 498 | "collapsed": false 499 | }, 500 | "outputs": [], 501 | "source": [ 502 | "model += pulp.lpSum(\n", 503 | " [production[month, factory] * factories.loc[(month, factory), 'Variable_Costs'] for month, factory in factories.index]\n", 504 | " + [factory_status[month, factory] * factories.loc[(month, factory), 'Fixed_Costs'] for month, factory in factories.index]\n", 505 | ")" 506 | ] 507 | }, 508 | { 509 | "cell_type": "markdown", 510 | "metadata": {}, 511 | "source": [ 512 | "We build up our constraints" 513 | ] 514 | }, 515 | { 516 | "cell_type": "code", 517 | "execution_count": 9, 518 | "metadata": { 519 | "collapsed": false 520 | }, 521 | "outputs": [], 522 | "source": [ 523 | "# Production in any month must be equal to demand\n", 524 | "months = demand.index\n", 525 | "for month in months:\n", 526 | " model += production[(month, 'A')] + production[(month, 'B')] == demand.loc[month, 'Demand']" 527 | ] 528 | }, 529 | { 530 | "cell_type": "markdown", 531 | "metadata": {}, 532 | "source": [ 533 | "An issue we run into here is that in linear programming we can't use conditional constraints. \n", 534 | "\n", 535 | "For example we can't add to our model that if the factory is off factory status must be 0, and if it is on factory status must be 1. Before we've solved our model though, we don't know if the factory will be on or off in a given month.\n", 536 | "\n", 537 | "In this case construct constraints that have minimum and maximum capacities that are constant variables, which we multiply by the factory status.\n", 538 | "\n", 539 | "Now, either factory status is 0 and:\n", 540 | "* min_production >= 0\n", 541 | "* max_production <= 0\n", 542 | "\n", 543 | "Or factory status is 1 and:\n", 544 | "* min_production >= min_capacity\n", 545 | "* max_production <= max_capacity\n", 546 | "\n", 547 | "*(In some cases we can use linear constraints to model conditional statements, we'll explore this in part 6)*" 548 | ] 549 | }, 550 | { 551 | "cell_type": "code", 552 | "execution_count": 10, 553 | "metadata": { 554 | "collapsed": true 555 | }, 556 | "outputs": [], 557 | "source": [ 558 | "# Production in any month must be between minimum and maximum capacity, or zero.\n", 559 | "for month, factory in factories.index:\n", 560 | " min_production = factories.loc[(month, factory), 'Min_Capacity']\n", 561 | " max_production = factories.loc[(month, factory), 'Max_Capacity']\n", 562 | " model += production[(month, factory)] >= min_production * factory_status[month, factory]\n", 563 | " model += production[(month, factory)] <= max_production * factory_status[month, factory]" 564 | ] 565 | }, 566 | { 567 | "cell_type": "code", 568 | "execution_count": 11, 569 | "metadata": { 570 | "collapsed": true 571 | }, 572 | "outputs": [], 573 | "source": [ 574 | "# Factory B is off in May\n", 575 | "model += factory_status[5, 'B'] == 0\n", 576 | "model += production[5, 'B'] == 0" 577 | ] 578 | }, 579 | { 580 | "cell_type": "markdown", 581 | "metadata": {}, 582 | "source": [ 583 | "We then solve the model" 584 | ] 585 | }, 586 | { 587 | "cell_type": "code", 588 | "execution_count": 12, 589 | "metadata": { 590 | "collapsed": false 591 | }, 592 | "outputs": [ 593 | { 594 | "data": { 595 | "text/plain": [ 596 | "'Optimal'" 597 | ] 598 | }, 599 | "execution_count": 12, 600 | "metadata": {}, 601 | "output_type": "execute_result" 602 | } 603 | ], 604 | "source": [ 605 | "model.solve()\n", 606 | "pulp.LpStatus[model.status]" 607 | ] 608 | }, 609 | { 610 | "cell_type": "markdown", 611 | "metadata": {}, 612 | "source": [ 613 | "Let's take a look at the optimal production schedule output for each month from each factory. For ease of viewing we'll output the data to a pandas DataFrame." 614 | ] 615 | }, 616 | { 617 | "cell_type": "code", 618 | "execution_count": 13, 619 | "metadata": { 620 | "collapsed": false 621 | }, 622 | "outputs": [ 623 | { 624 | "data": { 625 | "text/html": [ 626 | "
\n", 627 | "\n", 628 | " \n", 629 | " \n", 630 | " \n", 631 | " \n", 632 | " \n", 633 | " \n", 634 | " \n", 635 | " \n", 636 | " \n", 637 | " \n", 638 | " \n", 639 | " \n", 640 | " \n", 641 | " \n", 642 | " \n", 643 | " \n", 644 | " \n", 645 | " \n", 646 | " \n", 647 | " \n", 648 | " \n", 649 | " \n", 650 | " \n", 651 | " \n", 652 | " \n", 653 | " \n", 654 | " \n", 655 | " \n", 656 | " \n", 657 | " \n", 658 | " \n", 659 | " \n", 660 | " \n", 661 | " \n", 662 | " \n", 663 | " \n", 664 | " \n", 665 | " \n", 666 | " \n", 667 | " \n", 668 | " \n", 669 | " \n", 670 | " \n", 671 | " \n", 672 | " \n", 673 | " \n", 674 | " \n", 675 | " \n", 676 | " \n", 677 | " \n", 678 | " \n", 679 | " \n", 680 | " \n", 681 | " \n", 682 | " \n", 683 | " \n", 684 | " \n", 685 | " \n", 686 | " \n", 687 | " \n", 688 | " \n", 689 | " \n", 690 | " \n", 691 | " \n", 692 | " \n", 693 | " \n", 694 | " \n", 695 | " \n", 696 | " \n", 697 | " \n", 698 | " \n", 699 | " \n", 700 | " \n", 701 | " \n", 702 | " \n", 703 | " \n", 704 | " \n", 705 | " \n", 706 | " \n", 707 | " \n", 708 | " \n", 709 | " \n", 710 | " \n", 711 | " \n", 712 | " \n", 713 | " \n", 714 | " \n", 715 | " \n", 716 | " \n", 717 | " \n", 718 | " \n", 719 | " \n", 720 | " \n", 721 | " \n", 722 | " \n", 723 | " \n", 724 | " \n", 725 | " \n", 726 | " \n", 727 | " \n", 728 | " \n", 729 | " \n", 730 | " \n", 731 | " \n", 732 | " \n", 733 | " \n", 734 | " \n", 735 | " \n", 736 | " \n", 737 | " \n", 738 | " \n", 739 | " \n", 740 | " \n", 741 | " \n", 742 | " \n", 743 | " \n", 744 | " \n", 745 | " \n", 746 | " \n", 747 | " \n", 748 | " \n", 749 | " \n", 750 | " \n", 751 | " \n", 752 | " \n", 753 | " \n", 754 | " \n", 755 | " \n", 756 | " \n", 757 | " \n", 758 | " \n", 759 | " \n", 760 | " \n", 761 | " \n", 762 | " \n", 763 | " \n", 764 | " \n", 765 | " \n", 766 | " \n", 767 | " \n", 768 | " \n", 769 | " \n", 770 | " \n", 771 | " \n", 772 | " \n", 773 | " \n", 774 | " \n", 775 | " \n", 776 | "
Factory StatusProduction
MonthFactory
1A170000
B150000
2A145000
B155000
3A170000
B160000
4A130000
B1100000
5A1140000
B00
6A160000
B170000
7A190000
B160000
8A170000
B1100000
9A1100000
B1100000
10A1190000
B00
11A180000
B160000
12A1100000
B00
\n", 777 | "
" 778 | ], 779 | "text/plain": [ 780 | " Factory Status Production\n", 781 | "Month Factory \n", 782 | "1 A 1 70000\n", 783 | " B 1 50000\n", 784 | "2 A 1 45000\n", 785 | " B 1 55000\n", 786 | "3 A 1 70000\n", 787 | " B 1 60000\n", 788 | "4 A 1 30000\n", 789 | " B 1 100000\n", 790 | "5 A 1 140000\n", 791 | " B 0 0\n", 792 | "6 A 1 60000\n", 793 | " B 1 70000\n", 794 | "7 A 1 90000\n", 795 | " B 1 60000\n", 796 | "8 A 1 70000\n", 797 | " B 1 100000\n", 798 | "9 A 1 100000\n", 799 | " B 1 100000\n", 800 | "10 A 1 190000\n", 801 | " B 0 0\n", 802 | "11 A 1 80000\n", 803 | " B 1 60000\n", 804 | "12 A 1 100000\n", 805 | " B 0 0" 806 | ] 807 | }, 808 | "execution_count": 13, 809 | "metadata": {}, 810 | "output_type": "execute_result" 811 | } 812 | ], 813 | "source": [ 814 | "output = []\n", 815 | "for month, factory in production:\n", 816 | " var_output = {\n", 817 | " 'Month': month,\n", 818 | " 'Factory': factory,\n", 819 | " 'Production': production[(month, factory)].varValue,\n", 820 | " 'Factory Status': factory_status[(month, factory)].varValue\n", 821 | " }\n", 822 | " output.append(var_output)\n", 823 | "output_df = pd.DataFrame.from_records(output).sort_values(['Month', 'Factory'])\n", 824 | "output_df.set_index(['Month', 'Factory'], inplace=True)\n", 825 | "output_df" 826 | ] 827 | }, 828 | { 829 | "cell_type": "markdown", 830 | "metadata": {}, 831 | "source": [ 832 | "Notice above that the factory status is 0 when not producing and 1 when it is producing" 833 | ] 834 | }, 835 | { 836 | "cell_type": "code", 837 | "execution_count": 14, 838 | "metadata": { 839 | "collapsed": false 840 | }, 841 | "outputs": [ 842 | { 843 | "name": "stdout", 844 | "output_type": "stream", 845 | "text": [ 846 | "12906400.0\n" 847 | ] 848 | } 849 | ], 850 | "source": [ 851 | "# Print our objective function value (Total Costs)\n", 852 | "print pulp.value(model.objective)" 853 | ] 854 | } 855 | ], 856 | "metadata": { 857 | "kernelspec": { 858 | "display_name": "Python 2", 859 | "language": "python", 860 | "name": "python2" 861 | }, 862 | "language_info": { 863 | "codemirror_mode": { 864 | "name": "ipython", 865 | "version": 2 866 | }, 867 | "file_extension": ".py", 868 | "mimetype": "text/x-python", 869 | "name": "python", 870 | "nbconvert_exporter": "python", 871 | "pygments_lexer": "ipython2", 872 | "version": "2.7.10" 873 | } 874 | }, 875 | "nbformat": 4, 876 | "nbformat_minor": 0 877 | } 878 | -------------------------------------------------------------------------------- /LaTeX_formatted_ipynb_files/Introduction to Linear Programming with Python - Part 1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Introduction to Linear Programming with Python - Part 1\n", 8 | "## Introduction to Linear Programming" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "In this set of notebooks we will be looking at some linear programming problems and how we can construct and solve these problems using the python linear programming package [PuLP](http://pythonhosted.org/PuLP/).\n", 16 | "\n", 17 | "Let's start with a simple example:\n", 18 | "\n", 19 | "We want to find the maximum solution to:\n", 20 | "\n", 21 | "$Z = 4x + 3y$\n", 22 | "\n", 23 | "This is known as our objective function. x and y in this equation are our decision variables.\n", 24 | "\n", 25 | "In this example, the objective function is subject to the following constraints:\n", 26 | "\n", 27 | "$\n", 28 | "x \\geq 0 \\\\\n", 29 | "y \\geq 2 \\\\\n", 30 | "2y \\leq 25 - x \\\\\n", 31 | "4y \\geq 2x - 8 \\\\\n", 32 | "y \\leq 2x -5 \\\\\n", 33 | "$\n", 34 | "\n", 35 | "We'll begin by graphing this problem" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": 1, 41 | "metadata": { 42 | "collapsed": false 43 | }, 44 | "outputs": [ 45 | { 46 | "data": { 47 | "text/plain": [ 48 | "" 49 | ] 50 | }, 51 | "execution_count": 1, 52 | "metadata": {}, 53 | "output_type": "execute_result" 54 | }, 55 | { 56 | "data": { 57 | "image/png": "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\n2In/D4WFHFLXaYNSAAAAAElFTkSuQmCC\n", 58 | "text/plain": [ 59 | "" 60 | ] 61 | }, 62 | "metadata": {}, 63 | "output_type": "display_data" 64 | } 65 | ], 66 | "source": [ 67 | "import numpy as np\n", 68 | "import matplotlib.pyplot as plt\n", 69 | "%matplotlib inline\n", 70 | "\n", 71 | "# Construct lines\n", 72 | "# x > 0\n", 73 | "x = np.linspace(0, 20, 2000)\n", 74 | "# y >= 2\n", 75 | "y1 = (x*0) + 2\n", 76 | "# 2y <= 25 - x\n", 77 | "y2 = (25-x)/2.0\n", 78 | "# 4y >= 2x - 8 \n", 79 | "y3 = (2*x-8)/4.0\n", 80 | "# y <= 2x - 5 \n", 81 | "y4 = 2 * x -5\n", 82 | "\n", 83 | "# Make plot\n", 84 | "plt.plot(x, y1, label=r'$y\\geq2$')\n", 85 | "plt.plot(x, y2, label=r'$2y\\leq25-x$')\n", 86 | "plt.plot(x, y3, label=r'$4y\\geq 2x - 8$')\n", 87 | "plt.plot(x, y4, label=r'$y\\leq 2x-5$')\n", 88 | "plt.xlim((0, 16))\n", 89 | "plt.ylim((0, 11))\n", 90 | "plt.xlabel(r'$x$')\n", 91 | "plt.ylabel(r'$y$')\n", 92 | "\n", 93 | "# Fill feasible region\n", 94 | "y5 = np.minimum(y2, y4)\n", 95 | "y6 = np.maximum(y1, y3)\n", 96 | "plt.fill_between(x, y5, y6, where=y5>y6, color='grey', alpha=0.5)\n", 97 | "plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)" 98 | ] 99 | }, 100 | { 101 | "cell_type": "markdown", 102 | "metadata": {}, 103 | "source": [ 104 | "Our solution lies somewhere in the grey feasible region in the graph above.\n", 105 | "\n", 106 | "It has been proven that the minima and maxima of linear programming problems lie at the vertices of the feasible region. In this example, there are only 4 corners to our feasible region, so we can find the solutions for each corner to find our maximum.\n", 107 | "\n", 108 | "The four corners are between the lines:\n", 109 | "\n", 110 | "| Line 1 | Line 2 | \n", 111 | "| ------------- |:-------------:| \n", 112 | "| y ≥ 2 | 4y ≥ 2x - 8 | \n", 113 | "| 2y ≤ 25 - x | y ≤ 2x - 5 | \n", 114 | "| 2y ≤ 25 - x | 4y ≥ 2x - 8 |\n", 115 | "| y ≥ 2 | y ≤ 2x - 5 |\n", 116 | "\n", 117 | "So keeping in mind that:\n", 118 | "\n", 119 | "$Z = 4x + 3y$\n", 120 | "\n", 121 | "We can calculate Z for each corner:" 122 | ] 123 | }, 124 | { 125 | "cell_type": "markdown", 126 | "metadata": {}, 127 | "source": [ 128 | "1)\n", 129 | "\n", 130 | " $\n", 131 | " y \\geq 2 \\text{ and } 4y \\geq 2x - 8 \\\\\n", 132 | " 2 = \\dfrac{2x - 8}{4} \\\\\n", 133 | " x = 8 \\\\\n", 134 | " y = 2 \\\\\n", 135 | " Z = (4 \\times 8 + 3 \\times 2) = 38 \\\\\n", 136 | " $\n", 137 | "\n", 138 | "\n", 139 | "2)\n", 140 | "\n", 141 | " $\n", 142 | " 2y \\leq 25 - x \\text{ and } y \\leq 2x - 5 \\\\\n", 143 | " \\dfrac{25 - x}{2} = 2x - 5 \\\\\n", 144 | " x = 7 \\\\ \n", 145 | " y = 9 \\\\\n", 146 | " Z = (4 \\times 7 + 3 \\times 9) 55 \\\\\n", 147 | " $\n", 148 | "\n", 149 | "\n", 150 | "3)\n", 151 | "\n", 152 | " $\n", 153 | " 2y \\leq 25 - x \\text{ and } 4y \\geq 2x - 8 \\\\\n", 154 | " \\dfrac{25 - x}{2} = \\dfrac{2x - 8}{4} \\\\ \n", 155 | " x = 14.5 \\\\\n", 156 | " y = 5.25 \\\\\n", 157 | " Z = (4 \\times 14.5 + 3 \\times 5.25) = 73.75 \\\\\n", 158 | " $\n", 159 | "\n", 160 | "4)\n", 161 | "\n", 162 | " $\n", 163 | " y \\geq 2 \\text{ and } y \\leq 2x - 5 \\\\\n", 164 | " 2 = 2x-5 \\\\\n", 165 | " x = 3.5 \\\\\n", 166 | " y = 2 \\\\\n", 167 | " Z = (4 \\times 3.5 + 3 \\times 2) = 20\n", 168 | " $" 169 | ] 170 | }, 171 | { 172 | "cell_type": "markdown", 173 | "metadata": { 174 | "collapsed": true 175 | }, 176 | "source": [ 177 | "We have successfully calculated that the maximum value for Z is 73.75, when x is 14.5 and y is 5.25.\n", 178 | "\n", 179 | "This method of testing every vertex is only feasible for a small number of variables and constraints.\n", 180 | "As the numbers of constraints and variables increase, it becomes far more difficult to graph these problems and work out all the vertices.\n", 181 | "For example, if there were a third variable:\n", 182 | "\n", 183 | "$Z = Ax + By + Cz$\n", 184 | "\n", 185 | "We would have to graph in three dimensions (x, y and z).\n", 186 | "\n", 187 | "In the next few notebooks, we'll take a look at how we can use python and the PuLP package to solve this linear programming problem, as well as some more complex problems" 188 | ] 189 | } 190 | ], 191 | "metadata": { 192 | "kernelspec": { 193 | "display_name": "Python 2", 194 | "language": "python", 195 | "name": "python2" 196 | }, 197 | "language_info": { 198 | "codemirror_mode": { 199 | "name": "ipython", 200 | "version": 2 201 | }, 202 | "file_extension": ".py", 203 | "mimetype": "text/x-python", 204 | "name": "python", 205 | "nbconvert_exporter": "python", 206 | "pygments_lexer": "ipython2", 207 | "version": "2.7.10" 208 | } 209 | }, 210 | "nbformat": 4, 211 | "nbformat_minor": 0 212 | } 213 | -------------------------------------------------------------------------------- /LaTeX_formatted_ipynb_files/Introduction to Linear Programming with Python - Part 2.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Introduction to Linear Programming with Python - Part 2\n", 8 | "## Introduction to PuLP" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "[PuLP](http://pythonhosted.org/PuLP/) is an open source linear programming package for python. PuLP can be installed using pip, instructions [here](http://pythonhosted.org/PuLP/main/installing_pulp_at_home.html).\n", 16 | "\n", 17 | "In this notebook, we'll explore how to construct and solve the linear programming problem described in Part 1 using PuLP.\n", 18 | "\n", 19 | "A brief reminder of our linear programming problem:\n", 20 | "\n", 21 | "We want to find the maximum solution to the objective function:\n", 22 | "\n", 23 | "$Z = 4x + 3y$\n", 24 | "\n", 25 | "Subject to the following constraints:\n", 26 | "\n", 27 | "$\n", 28 | "x \\geq 0 \\\\\n", 29 | "y \\geq 2 \\\\\n", 30 | "2y \\leq 25 - x \\\\\n", 31 | "4y \\geq 2x - 8 \\\\\n", 32 | "y \\leq 2x -5 \\\\\n", 33 | "$" 34 | ] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "metadata": {}, 39 | "source": [ 40 | "We'll begin by importing PuLP" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 1, 46 | "metadata": { 47 | "collapsed": true 48 | }, 49 | "outputs": [], 50 | "source": [ 51 | "import pulp" 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "metadata": {}, 57 | "source": [ 58 | "Then instantiate a problem class, we'll name it \"My LP problem\" and we're looking for an optimal maximum so we use LpMaximize" 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": 2, 64 | "metadata": { 65 | "collapsed": false 66 | }, 67 | "outputs": [], 68 | "source": [ 69 | "my_lp_problem = pulp.LpProblem(\"My LP Problem\", pulp.LpMaximize)" 70 | ] 71 | }, 72 | { 73 | "cell_type": "markdown", 74 | "metadata": {}, 75 | "source": [ 76 | "We then model our decision variables using the LpVariable class. In our example, x had a lower bound of 0 and y had a lower bound of 2.\n", 77 | "\n", 78 | "Upper bounds can be assigned using the upBound parameter." 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": 3, 84 | "metadata": { 85 | "collapsed": false 86 | }, 87 | "outputs": [], 88 | "source": [ 89 | "x = pulp.LpVariable('x', lowBound=0, cat='Continuous')\n", 90 | "y = pulp.LpVariable('y', lowBound=2, cat='Continuous')" 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "metadata": {}, 96 | "source": [ 97 | "The objective function and constraints are added using the += operator to our model. \n", 98 | "\n", 99 | "The objective function is added first, then the individual constraints." 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": 4, 105 | "metadata": { 106 | "collapsed": true 107 | }, 108 | "outputs": [], 109 | "source": [ 110 | "# Objective function\n", 111 | "my_lp_problem += 4 * x + 3 * y, \"Z\"\n", 112 | "\n", 113 | "# Constraints\n", 114 | "my_lp_problem += 2 * y <= 25 - x\n", 115 | "my_lp_problem += 4 * y >= 2 * x - 8\n", 116 | "my_lp_problem += y <= 2 * x - 5" 117 | ] 118 | }, 119 | { 120 | "cell_type": "markdown", 121 | "metadata": {}, 122 | "source": [ 123 | "We have now constructed our problem and can have a look at it." 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": 5, 129 | "metadata": { 130 | "collapsed": false 131 | }, 132 | "outputs": [ 133 | { 134 | "data": { 135 | "text/plain": [ 136 | "My LP Problem:\n", 137 | "MAXIMIZE\n", 138 | "4*x + 3*y + 0\n", 139 | "SUBJECT TO\n", 140 | "_C1: x + 2 y <= 25\n", 141 | "\n", 142 | "_C2: - 2 x + 4 y >= -8\n", 143 | "\n", 144 | "_C3: - 2 x + y <= -5\n", 145 | "\n", 146 | "VARIABLES\n", 147 | "x Continuous\n", 148 | "2 <= y Continuous" 149 | ] 150 | }, 151 | "execution_count": 5, 152 | "metadata": {}, 153 | "output_type": "execute_result" 154 | } 155 | ], 156 | "source": [ 157 | "my_lp_problem" 158 | ] 159 | }, 160 | { 161 | "cell_type": "markdown", 162 | "metadata": {}, 163 | "source": [ 164 | "PuLP supports open source linear programming solvers such as CBC and GLPK, as well as commercial solvers such as Gurobi and IBM's CPLEX.\n", 165 | "\n", 166 | "The default solver is CBC, which comes packaged with PuLP upon installation.\n", 167 | "\n", 168 | "For most applications, the open source CBC from [COIN-OR](http://www.coin-or.org/) will be enough for most simple linear programming optimisation algorithms." 169 | ] 170 | }, 171 | { 172 | "cell_type": "code", 173 | "execution_count": 6, 174 | "metadata": { 175 | "collapsed": false 176 | }, 177 | "outputs": [ 178 | { 179 | "data": { 180 | "text/plain": [ 181 | "'Optimal'" 182 | ] 183 | }, 184 | "execution_count": 6, 185 | "metadata": {}, 186 | "output_type": "execute_result" 187 | } 188 | ], 189 | "source": [ 190 | "my_lp_problem.solve()\n", 191 | "pulp.LpStatus[my_lp_problem.status]" 192 | ] 193 | }, 194 | { 195 | "cell_type": "markdown", 196 | "metadata": {}, 197 | "source": [ 198 | "We have also checked the status of the solver, there are 5 status codes:\n", 199 | "* **Not Solved**: Status prior to solving the problem.\n", 200 | "* **Optimal**: An optimal solution has been found.\n", 201 | "* **Infeasible**: There are no feasible solutions (e.g. if you set the constraints x <= 1 and x >=2).\n", 202 | "* **Unbounded**: The constraints are not bounded, maximising the solution will tend towards infinity (e.g. if the only constraint was x >= 3).\n", 203 | "* **Undefined**: The optimal solution may exist but may not have been found." 204 | ] 205 | }, 206 | { 207 | "cell_type": "markdown", 208 | "metadata": {}, 209 | "source": [ 210 | "We can now view our maximal variable values and the maximum value of Z. \n", 211 | "\n", 212 | "We can use the varValue method to retrieve the values of our variables x and y, and the pulp.value function to view the maximum value of the objective function." 213 | ] 214 | }, 215 | { 216 | "cell_type": "code", 217 | "execution_count": 7, 218 | "metadata": { 219 | "collapsed": false 220 | }, 221 | "outputs": [ 222 | { 223 | "name": "stdout", 224 | "output_type": "stream", 225 | "text": [ 226 | "x = 14.5\n", 227 | "y = 5.25\n" 228 | ] 229 | } 230 | ], 231 | "source": [ 232 | "for variable in my_lp_problem.variables():\n", 233 | " print \"{} = {}\".format(variable.name, variable.varValue)" 234 | ] 235 | }, 236 | { 237 | "cell_type": "code", 238 | "execution_count": 8, 239 | "metadata": { 240 | "collapsed": false 241 | }, 242 | "outputs": [ 243 | { 244 | "name": "stdout", 245 | "output_type": "stream", 246 | "text": [ 247 | "73.75\n" 248 | ] 249 | } 250 | ], 251 | "source": [ 252 | "print pulp.value(my_lp_problem.objective)" 253 | ] 254 | }, 255 | { 256 | "cell_type": "markdown", 257 | "metadata": {}, 258 | "source": [ 259 | " Same values as our manual calculations in part 1.\n", 260 | " \n", 261 | " In the next part we'll be looking at a more real world problem." 262 | ] 263 | } 264 | ], 265 | "metadata": { 266 | "kernelspec": { 267 | "display_name": "Python 2", 268 | "language": "python", 269 | "name": "python2" 270 | }, 271 | "language_info": { 272 | "codemirror_mode": { 273 | "name": "ipython", 274 | "version": 2 275 | }, 276 | "file_extension": ".py", 277 | "mimetype": "text/x-python", 278 | "name": "python", 279 | "nbconvert_exporter": "python", 280 | "pygments_lexer": "ipython2", 281 | "version": "2.7.10" 282 | } 283 | }, 284 | "nbformat": 4, 285 | "nbformat_minor": 0 286 | } 287 | -------------------------------------------------------------------------------- /LaTeX_formatted_ipynb_files/Introduction to Linear Programming with Python - Part 3.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Introduction to Linear Programming with Python - Part 3\n", 8 | "## Real world examples - Resourcing Problem" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "We'll now look at 2 more real world examples. \n", 16 | "\n", 17 | "The first is a resourcing problem and the second is a blending problem.\n", 18 | "\n", 19 | "\n", 20 | "### Resourcing Problem" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "metadata": {}, 26 | "source": [ 27 | "We're consulting for a boutique car manufacturer, producing luxury cars.\n", 28 | "\n", 29 | "They run on one month (30 days) cycles, we have one cycle to show we can provide value.\n", 30 | "\n", 31 | "There is one robot, 2 engineers and one detailer in the factory. The detailer has some holiday off, so only has 21 days available.\n", 32 | "\n", 33 | "The 2 cars need different time with each resource:\n", 34 | "\n", 35 | "**Robot time:** Car A - 3 days; Car B - 4 days.\n", 36 | "\n", 37 | "**Engineer time:** Car A - 5 days; Car B - 6 days.\n", 38 | "\n", 39 | "**Detailer time:** Car A - 1.5 days; Car B - 3 days.\n", 40 | "\n", 41 | "Car A provides €30,000 profit, whilst Car B offers €45,000 profit.\n", 42 | "\n", 43 | "At the moment, they produce 4 of each cars per month, for €300,000 profit. Not bad at all, but we think we can do better for them." 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "metadata": {}, 49 | "source": [ 50 | "This can be modelled as follows:\n", 51 | "\n", 52 | "Maximise\n", 53 | "\n", 54 | "$\\text{Profit} = 30,000A + 45,000B$\n", 55 | "\n", 56 | "Subject to:\n", 57 | "\n", 58 | "$\n", 59 | "A \\geq 0 \\\\\n", 60 | "B \\geq 0 \\\\\n", 61 | "3A + 4B \\leq 30 \\\\\n", 62 | "5A + 6B \\leq 60 \\\\\n", 63 | "1.5A + 3B \\leq 21 \\\\\n", 64 | "$" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 1, 70 | "metadata": { 71 | "collapsed": true 72 | }, 73 | "outputs": [], 74 | "source": [ 75 | "import pulp" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": 2, 81 | "metadata": { 82 | "collapsed": true 83 | }, 84 | "outputs": [], 85 | "source": [ 86 | "# Instantiate our problem class\n", 87 | "model = pulp.LpProblem(\"Profit maximising problem\", pulp.LpMaximize)" 88 | ] 89 | }, 90 | { 91 | "cell_type": "markdown", 92 | "metadata": {}, 93 | "source": [ 94 | "Unlike our previous problem, the decision variables in this case won't be continuous (We can't sell half a car!), so the category is integer." 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": 3, 100 | "metadata": { 101 | "collapsed": true 102 | }, 103 | "outputs": [], 104 | "source": [ 105 | "A = pulp.LpVariable('A', lowBound=0, cat='Integer')\n", 106 | "B = pulp.LpVariable('B', lowBound=0, cat='Integer')" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": 4, 112 | "metadata": { 113 | "collapsed": true 114 | }, 115 | "outputs": [], 116 | "source": [ 117 | "# Objective function\n", 118 | "model += 30000 * A + 45000 * B, \"Profit\"\n", 119 | "\n", 120 | "# Constraints\n", 121 | "model += 3 * A + 4 * B <= 30\n", 122 | "model += 5 * A + 6 * B <= 60\n", 123 | "model += 1.5 * A + 3 * B <= 21" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": 5, 129 | "metadata": { 130 | "collapsed": false 131 | }, 132 | "outputs": [ 133 | { 134 | "data": { 135 | "text/plain": [ 136 | "'Optimal'" 137 | ] 138 | }, 139 | "execution_count": 5, 140 | "metadata": {}, 141 | "output_type": "execute_result" 142 | } 143 | ], 144 | "source": [ 145 | "# Solve our problem\n", 146 | "model.solve()\n", 147 | "pulp.LpStatus[model.status]" 148 | ] 149 | }, 150 | { 151 | "cell_type": "code", 152 | "execution_count": 6, 153 | "metadata": { 154 | "collapsed": false 155 | }, 156 | "outputs": [ 157 | { 158 | "name": "stdout", 159 | "output_type": "stream", 160 | "text": [ 161 | "Production of Car A = 2.0\n", 162 | "Production of Car B = 6.0\n" 163 | ] 164 | } 165 | ], 166 | "source": [ 167 | "# Print our decision variable values\n", 168 | "print \"Production of Car A = {}\".format(A.varValue)\n", 169 | "print \"Production of Car B = {}\".format(B.varValue)" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": 7, 175 | "metadata": { 176 | "collapsed": false 177 | }, 178 | "outputs": [ 179 | { 180 | "name": "stdout", 181 | "output_type": "stream", 182 | "text": [ 183 | "330000.0\n" 184 | ] 185 | } 186 | ], 187 | "source": [ 188 | "# Print our objective function value\n", 189 | "print pulp.value(model.objective)" 190 | ] 191 | }, 192 | { 193 | "cell_type": "markdown", 194 | "metadata": {}, 195 | "source": [ 196 | "So that's €330,000 monthly profit, compared to their original monthly profit of €300,000\n", 197 | "\n", 198 | "By producing 2 cars of Car A and 4 cars of Car B, we bolster the profits at the factory by €30,000 per month.\n", 199 | "\n", 200 | "We take our consultancy fee and leave the company with €360,000 extra profit for the factory every year.\n", 201 | "\n", 202 | "In the next part, we'll be making some sausages!" 203 | ] 204 | } 205 | ], 206 | "metadata": { 207 | "kernelspec": { 208 | "display_name": "Python 2", 209 | "language": "python", 210 | "name": "python2" 211 | }, 212 | "language_info": { 213 | "codemirror_mode": { 214 | "name": "ipython", 215 | "version": 2 216 | }, 217 | "file_extension": ".py", 218 | "mimetype": "text/x-python", 219 | "name": "python", 220 | "nbconvert_exporter": "python", 221 | "pygments_lexer": "ipython2", 222 | "version": "2.7.10" 223 | } 224 | }, 225 | "nbformat": 4, 226 | "nbformat_minor": 0 227 | } 228 | -------------------------------------------------------------------------------- /LaTeX_formatted_ipynb_files/Introduction to Linear Programming with Python - Part 4.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Introduction to Linear Programming with Python - Part 4\n", 8 | "## Real world examples - Blending Problem" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "We're going to make some sausages!\n", 16 | "\n", 17 | "We have the following ingredients available to us:\n", 18 | "\n", 19 | "| Ingredient | Cost (€/kg) | Availability (kg) |\n", 20 | "|------------|--------------|-------------------|\n", 21 | "| Pork | 4.32 | 30 |\n", 22 | "| Wheat | 2.46 | 20 |\n", 23 | "| Starch | 1.86 | 17 |\n", 24 | "\n", 25 | "We'll make 2 types of sausage:\n", 26 | "* Economy (>40% Pork)\n", 27 | "* Premium (>60% Pork)\n", 28 | "\n", 29 | "One sausage is 50 grams (0.05 kg)\n", 30 | "\n", 31 | "According to government regulations, the most starch we can use in our sausages is 25%\n", 32 | "\n", 33 | "We have a contract with a butcher, and have already purchased 23 kg pork, that must go in our sausages.\n", 34 | "\n", 35 | "We have a demand for 350 economy sausages and 500 premium sausages.\n", 36 | "\n", 37 | "We need to figure out how to most cost effectively blend our sausages." 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "metadata": {}, 43 | "source": [ 44 | "Let's model our problem:\n", 45 | "\n", 46 | "$\n", 47 | " p_e = \\text{Pork in the economy sausages (kg)} \\\\\n", 48 | " w_e = \\text{Wheat in the economy sausages (kg)} \\\\ \n", 49 | " s_e = \\text{Starch in the economy sausages (kg)} \\\\\n", 50 | " p_p = \\text{Pork in the premium sausages (kg)} \\\\\n", 51 | " w_p = \\text{Wheat in the premium sausages (kg)} \\\\\n", 52 | " s_p = \\text{Starch in the premium sausages (kg)} \\\\\n", 53 | "$\n", 54 | "\n", 55 | "We want to minimise costs such that:\n", 56 | "\n", 57 | "$\\text{Cost} = 0.72(p_e + p_p) + 0.41(w_e + w_p) + 0.31(s_e + s_p)$\n", 58 | "\n", 59 | "\n", 60 | "With the following constraints:\n", 61 | "\n", 62 | " $\n", 63 | " p_e + w_e + s_e = 350 \\times 0.05 \\\\\n", 64 | " p_p + w_p + s_p = 500 \\times 0.05 \\\\\n", 65 | " p_e \\geq 0.4(p_e + w_e + s_e) \\\\\n", 66 | " p_p \\geq 0.6(p_p + w_p + s_p) \\\\ \n", 67 | " s_e \\leq 0.25(p_e + w_e + s_e) \\\\\n", 68 | " s_p \\leq 0.25(p_p + w_p + s_p) \\\\\n", 69 | " p_e + p_p \\leq 30 \\\\\n", 70 | " w_e + w_p \\leq 20 \\\\\n", 71 | " s_e + s_p \\leq 17 \\\\\n", 72 | " p_e + p_p \\geq 23 \\\\\n", 73 | " $" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 1, 79 | "metadata": { 80 | "collapsed": true 81 | }, 82 | "outputs": [], 83 | "source": [ 84 | "import pulp" 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": 2, 90 | "metadata": { 91 | "collapsed": true 92 | }, 93 | "outputs": [], 94 | "source": [ 95 | "# Instantiate our problem class\n", 96 | "model = pulp.LpProblem(\"Cost minimising blending problem\", pulp.LpMinimize)" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "metadata": {}, 102 | "source": [ 103 | "Here we have 6 decision variables, we could name them individually but this wouldn't scale up if we had hundreds/thousands of variables (you don't want to be entering all of these by hand multiple times). \n", 104 | "\n", 105 | "We'll create a couple of lists from which we can create tuple indices." 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": 3, 111 | "metadata": { 112 | "collapsed": false 113 | }, 114 | "outputs": [], 115 | "source": [ 116 | "# Construct our decision variable lists\n", 117 | "sausage_types = ['economy', 'premium']\n", 118 | "ingredients = ['pork', 'wheat', 'starch']" 119 | ] 120 | }, 121 | { 122 | "cell_type": "markdown", 123 | "metadata": {}, 124 | "source": [ 125 | "Each of these decision variables will have similar characteristics (lower bound of 0, continuous variables). Therefore we can use PuLP's LpVariable object's dict functionality, we can provide our tuple indices.\n", 126 | "\n", 127 | "These tuples will be keys for the ing_weight dict of decision variables" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": 4, 133 | "metadata": { 134 | "collapsed": false 135 | }, 136 | "outputs": [], 137 | "source": [ 138 | "ing_weight = pulp.LpVariable.dicts(\"weight kg\",\n", 139 | " ((i, j) for i in sausage_types for j in ingredients),\n", 140 | " lowBound=0,\n", 141 | " cat='Continuous')" 142 | ] 143 | }, 144 | { 145 | "cell_type": "markdown", 146 | "metadata": {}, 147 | "source": [ 148 | "PuLP provides an lpSum vector calculation for the sum of a list of linear expressions.\n", 149 | "\n", 150 | "Whilst we only have 6 decision variables, I will demonstrate how the problem would be constructed in a way that could be scaled up to many variables using list comprehensions." 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": 5, 156 | "metadata": { 157 | "collapsed": false 158 | }, 159 | "outputs": [], 160 | "source": [ 161 | "# Objective Function\n", 162 | "model += (\n", 163 | " pulp.lpSum([\n", 164 | " 4.32 * ing_weight[(i, 'pork')]\n", 165 | " + 2.46 * ing_weight[(i, 'wheat')]\n", 166 | " + 1.86 * ing_weight[(i, 'starch')]\n", 167 | " for i in sausage_types])\n", 168 | ")" 169 | ] 170 | }, 171 | { 172 | "cell_type": "markdown", 173 | "metadata": {}, 174 | "source": [ 175 | "Now we add our constraints, bear in mind again here how the use of list comprehensions allows for scaling up to many ingredients or sausage types" 176 | ] 177 | }, 178 | { 179 | "cell_type": "code", 180 | "execution_count": 6, 181 | "metadata": { 182 | "collapsed": true 183 | }, 184 | "outputs": [], 185 | "source": [ 186 | "# Constraints\n", 187 | "# 350 economy and 500 premium sausages at 0.05 kg\n", 188 | "model += pulp.lpSum([ing_weight['economy', j] for j in ingredients]) == 350 * 0.05\n", 189 | "model += pulp.lpSum([ing_weight['premium', j] for j in ingredients]) == 500 * 0.05\n", 190 | "\n", 191 | "# Economy has >= 40% pork, premium >= 60% pork\n", 192 | "model += ing_weight['economy', 'pork'] >= (\n", 193 | " 0.4 * pulp.lpSum([ing_weight['economy', j] for j in ingredients]))\n", 194 | "\n", 195 | "model += ing_weight['premium', 'pork'] >= (\n", 196 | " 0.6 * pulp.lpSum([ing_weight['premium', j] for j in ingredients]))\n", 197 | "\n", 198 | "# Sausages must be <= 25% starch\n", 199 | "model += ing_weight['economy', 'starch'] <= (\n", 200 | " 0.25 * pulp.lpSum([ing_weight['economy', j] for j in ingredients]))\n", 201 | "\n", 202 | "model += ing_weight['premium', 'starch'] <= (\n", 203 | " 0.25 * pulp.lpSum([ing_weight['premium', j] for j in ingredients]))\n", 204 | "\n", 205 | "# We have at most 30 kg of pork, 20 kg of wheat and 17 kg of starch available\n", 206 | "model += pulp.lpSum([ing_weight[i, 'pork'] for i in sausage_types]) <= 30\n", 207 | "model += pulp.lpSum([ing_weight[i, 'wheat'] for i in sausage_types]) <= 20\n", 208 | "model += pulp.lpSum([ing_weight[i, 'starch'] for i in sausage_types]) <= 17\n", 209 | "\n", 210 | "# We have at least 23 kg of pork to use up\n", 211 | "model += pulp.lpSum([ing_weight[i, 'pork'] for i in sausage_types]) >= 23" 212 | ] 213 | }, 214 | { 215 | "cell_type": "code", 216 | "execution_count": 7, 217 | "metadata": { 218 | "collapsed": false 219 | }, 220 | "outputs": [ 221 | { 222 | "data": { 223 | "text/plain": [ 224 | "'Optimal'" 225 | ] 226 | }, 227 | "execution_count": 7, 228 | "metadata": {}, 229 | "output_type": "execute_result" 230 | } 231 | ], 232 | "source": [ 233 | "# Solve our problem\n", 234 | "model.solve()\n", 235 | "pulp.LpStatus[model.status]" 236 | ] 237 | }, 238 | { 239 | "cell_type": "code", 240 | "execution_count": 8, 241 | "metadata": { 242 | "collapsed": false 243 | }, 244 | "outputs": [ 245 | { 246 | "name": "stdout", 247 | "output_type": "stream", 248 | "text": [ 249 | "The weight of starch in premium sausages is 6.25 kg\n", 250 | "The weight of starch in economy sausages is 4.375 kg\n", 251 | "The weight of wheat in economy sausages is 6.125 kg\n", 252 | "The weight of wheat in premium sausages is 2.75 kg\n", 253 | "The weight of pork in economy sausages is 7.0 kg\n", 254 | "The weight of pork in premium sausages is 16.0 kg\n" 255 | ] 256 | } 257 | ], 258 | "source": [ 259 | "for var in ing_weight:\n", 260 | " var_value = ing_weight[var].varValue\n", 261 | " print \"The weight of {0} in {1} sausages is {2} kg\".format(var[1], var[0], var_value)" 262 | ] 263 | }, 264 | { 265 | "cell_type": "code", 266 | "execution_count": 9, 267 | "metadata": { 268 | "collapsed": false 269 | }, 270 | "outputs": [ 271 | { 272 | "name": "stdout", 273 | "output_type": "stream", 274 | "text": [ 275 | "The total cost is €140.96 for 350 economy sausages and 500 premium sausages\n" 276 | ] 277 | } 278 | ], 279 | "source": [ 280 | "total_cost = pulp.value(model.objective)\n", 281 | "\n", 282 | "print \"The total cost is €{} for 350 economy sausages and 500 premium sausages\".format(round(total_cost, 2))" 283 | ] 284 | } 285 | ], 286 | "metadata": { 287 | "kernelspec": { 288 | "display_name": "Python 2", 289 | "language": "python", 290 | "name": "python2" 291 | }, 292 | "language_info": { 293 | "codemirror_mode": { 294 | "name": "ipython", 295 | "version": 2 296 | }, 297 | "file_extension": ".py", 298 | "mimetype": "text/x-python", 299 | "name": "python", 300 | "nbconvert_exporter": "python", 301 | "pygments_lexer": "ipython2", 302 | "version": "2.7.10" 303 | } 304 | }, 305 | "nbformat": 4, 306 | "nbformat_minor": 0 307 | } 308 | -------------------------------------------------------------------------------- /LaTeX_formatted_ipynb_files/Introduction to Linear Programming with Python - Part 5.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Introduction to Linear Programming with Python - Part 5\n", 8 | "## Using PuLP with pandas and binary constraints to solve a scheduling problem" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "In this example, we'll be solving a scheduling problem. We have 2 offshore production plants in 2 locations and an estimated demand for our products. \n", 16 | "\n", 17 | "We want to produce a schedule of production from both plants that meets our demand with the lowest cost.\n", 18 | "\n", 19 | "A factory can be in 2 states:\n", 20 | "* Off - Producing zero units\n", 21 | "* On - Producing between its minimum and maximum production capacities\n", 22 | "\n", 23 | "Both factories have fixed costs, that are incurred as long as the factory is on, and variable costs, a cost per unit of production. These vary month by month. \n", 24 | "\n", 25 | "We also know that factory B is down for maintenance in month 5.\n", 26 | "\n", 27 | "We'll start by importing our data." 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": 1, 33 | "metadata": { 34 | "collapsed": false 35 | }, 36 | "outputs": [], 37 | "source": [ 38 | "import pandas as pd\n", 39 | "import pulp" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": 2, 45 | "metadata": { 46 | "collapsed": false 47 | }, 48 | "outputs": [ 49 | { 50 | "data": { 51 | "text/html": [ 52 | "
\n", 53 | "\n", 54 | " \n", 55 | " \n", 56 | " \n", 57 | " \n", 58 | " \n", 59 | " \n", 60 | " \n", 61 | " \n", 62 | " \n", 63 | " \n", 64 | " \n", 65 | " \n", 66 | " \n", 67 | " \n", 68 | " \n", 69 | " \n", 70 | " \n", 71 | " \n", 72 | " \n", 73 | " \n", 74 | " \n", 75 | " \n", 76 | " \n", 77 | " \n", 78 | " \n", 79 | " \n", 80 | " \n", 81 | " \n", 82 | " \n", 83 | " \n", 84 | " \n", 85 | " \n", 86 | " \n", 87 | " \n", 88 | " \n", 89 | " \n", 90 | " \n", 91 | " \n", 92 | " \n", 93 | " \n", 94 | " \n", 95 | " \n", 96 | " \n", 97 | " \n", 98 | " \n", 99 | " \n", 100 | " \n", 101 | " \n", 102 | " \n", 103 | " \n", 104 | " \n", 105 | " \n", 106 | " \n", 107 | " \n", 108 | " \n", 109 | " \n", 110 | " \n", 111 | " \n", 112 | " \n", 113 | " \n", 114 | " \n", 115 | " \n", 116 | " \n", 117 | " \n", 118 | " \n", 119 | " \n", 120 | " \n", 121 | " \n", 122 | " \n", 123 | " \n", 124 | " \n", 125 | " \n", 126 | " \n", 127 | " \n", 128 | " \n", 129 | " \n", 130 | " \n", 131 | " \n", 132 | " \n", 133 | " \n", 134 | " \n", 135 | " \n", 136 | " \n", 137 | " \n", 138 | " \n", 139 | " \n", 140 | " \n", 141 | " \n", 142 | " \n", 143 | " \n", 144 | " \n", 145 | " \n", 146 | " \n", 147 | " \n", 148 | " \n", 149 | " \n", 150 | " \n", 151 | " \n", 152 | " \n", 153 | " \n", 154 | " \n", 155 | " \n", 156 | " \n", 157 | " \n", 158 | " \n", 159 | " \n", 160 | " \n", 161 | " \n", 162 | " \n", 163 | " \n", 164 | " \n", 165 | " \n", 166 | " \n", 167 | " \n", 168 | " \n", 169 | " \n", 170 | " \n", 171 | " \n", 172 | " \n", 173 | " \n", 174 | " \n", 175 | " \n", 176 | " \n", 177 | " \n", 178 | " \n", 179 | " \n", 180 | " \n", 181 | " \n", 182 | " \n", 183 | " \n", 184 | " \n", 185 | " \n", 186 | " \n", 187 | " \n", 188 | " \n", 189 | " \n", 190 | " \n", 191 | " \n", 192 | " \n", 193 | " \n", 194 | " \n", 195 | " \n", 196 | " \n", 197 | " \n", 198 | " \n", 199 | " \n", 200 | " \n", 201 | " \n", 202 | " \n", 203 | " \n", 204 | " \n", 205 | " \n", 206 | " \n", 207 | " \n", 208 | " \n", 209 | " \n", 210 | " \n", 211 | " \n", 212 | " \n", 213 | " \n", 214 | " \n", 215 | " \n", 216 | " \n", 217 | " \n", 218 | " \n", 219 | " \n", 220 | " \n", 221 | " \n", 222 | " \n", 223 | " \n", 224 | " \n", 225 | " \n", 226 | " \n", 227 | " \n", 228 | " \n", 229 | " \n", 230 | " \n", 231 | " \n", 232 | " \n", 233 | " \n", 234 | " \n", 235 | " \n", 236 | " \n", 237 | " \n", 238 | " \n", 239 | " \n", 240 | " \n", 241 | " \n", 242 | " \n", 243 | " \n", 244 | " \n", 245 | " \n", 246 | " \n", 247 | " \n", 248 | " \n", 249 | " \n", 250 | " \n", 251 | " \n", 252 | " \n", 253 | " \n", 254 | "
Max_CapacityMin_CapacityVariable_CostsFixed_Costs
MonthFactory
1A1000002000010500
B50000200005600
2A1100002000011500
B55000200004600
3A1200002000012500
B60000200003600
4A145000200009500
B100000200005600
5A160000200008500
B0000
6A140000200008500
B70000200006600
7A155000200005500
B60000200004600
8A200000200007500
B100000200006600
9A210000200009500
B100000200008600
10A1970002000010500
B1000002000011600
11A80000200008500
B1200002000010600
12A150000200008500
B1500002000012600
\n", 255 | "
" 256 | ], 257 | "text/plain": [ 258 | " Max_Capacity Min_Capacity Variable_Costs Fixed_Costs\n", 259 | "Month Factory \n", 260 | "1 A 100000 20000 10 500\n", 261 | " B 50000 20000 5 600\n", 262 | "2 A 110000 20000 11 500\n", 263 | " B 55000 20000 4 600\n", 264 | "3 A 120000 20000 12 500\n", 265 | " B 60000 20000 3 600\n", 266 | "4 A 145000 20000 9 500\n", 267 | " B 100000 20000 5 600\n", 268 | "5 A 160000 20000 8 500\n", 269 | " B 0 0 0 0\n", 270 | "6 A 140000 20000 8 500\n", 271 | " B 70000 20000 6 600\n", 272 | "7 A 155000 20000 5 500\n", 273 | " B 60000 20000 4 600\n", 274 | "8 A 200000 20000 7 500\n", 275 | " B 100000 20000 6 600\n", 276 | "9 A 210000 20000 9 500\n", 277 | " B 100000 20000 8 600\n", 278 | "10 A 197000 20000 10 500\n", 279 | " B 100000 20000 11 600\n", 280 | "11 A 80000 20000 8 500\n", 281 | " B 120000 20000 10 600\n", 282 | "12 A 150000 20000 8 500\n", 283 | " B 150000 20000 12 600" 284 | ] 285 | }, 286 | "execution_count": 2, 287 | "metadata": {}, 288 | "output_type": "execute_result" 289 | } 290 | ], 291 | "source": [ 292 | "factories = pd.DataFrame.from_csv('csv/factory_variables.csv', index_col=['Month', 'Factory'])\n", 293 | "factories" 294 | ] 295 | }, 296 | { 297 | "cell_type": "markdown", 298 | "metadata": {}, 299 | "source": [ 300 | "We'll also import our demand data" 301 | ] 302 | }, 303 | { 304 | "cell_type": "code", 305 | "execution_count": 3, 306 | "metadata": { 307 | "collapsed": true 308 | }, 309 | "outputs": [], 310 | "source": [ 311 | "demand = pd.DataFrame.from_csv('csv/monthly_demand.csv', index_col=['Month'])" 312 | ] 313 | }, 314 | { 315 | "cell_type": "code", 316 | "execution_count": 4, 317 | "metadata": { 318 | "collapsed": false, 319 | "scrolled": true 320 | }, 321 | "outputs": [ 322 | { 323 | "data": { 324 | "text/html": [ 325 | "
\n", 326 | "\n", 327 | " \n", 328 | " \n", 329 | " \n", 330 | " \n", 331 | " \n", 332 | " \n", 333 | " \n", 334 | " \n", 335 | " \n", 336 | " \n", 337 | " \n", 338 | " \n", 339 | " \n", 340 | " \n", 341 | " \n", 342 | " \n", 343 | " \n", 344 | " \n", 345 | " \n", 346 | " \n", 347 | " \n", 348 | " \n", 349 | " \n", 350 | " \n", 351 | " \n", 352 | " \n", 353 | " \n", 354 | " \n", 355 | " \n", 356 | " \n", 357 | " \n", 358 | " \n", 359 | " \n", 360 | " \n", 361 | " \n", 362 | " \n", 363 | " \n", 364 | " \n", 365 | " \n", 366 | " \n", 367 | " \n", 368 | " \n", 369 | " \n", 370 | " \n", 371 | " \n", 372 | " \n", 373 | " \n", 374 | " \n", 375 | " \n", 376 | " \n", 377 | " \n", 378 | " \n", 379 | " \n", 380 | " \n", 381 | " \n", 382 | " \n", 383 | " \n", 384 | " \n", 385 | " \n", 386 | " \n", 387 | "
Demand
Month
1120000
2100000
3130000
4130000
5140000
6130000
7150000
8170000
9200000
10190000
11140000
12100000
\n", 388 | "
" 389 | ], 390 | "text/plain": [ 391 | " Demand\n", 392 | "Month \n", 393 | "1 120000\n", 394 | "2 100000\n", 395 | "3 130000\n", 396 | "4 130000\n", 397 | "5 140000\n", 398 | "6 130000\n", 399 | "7 150000\n", 400 | "8 170000\n", 401 | "9 200000\n", 402 | "10 190000\n", 403 | "11 140000\n", 404 | "12 100000" 405 | ] 406 | }, 407 | "execution_count": 4, 408 | "metadata": {}, 409 | "output_type": "execute_result" 410 | } 411 | ], 412 | "source": [ 413 | "demand" 414 | ] 415 | }, 416 | { 417 | "cell_type": "markdown", 418 | "metadata": {}, 419 | "source": [ 420 | "As we have fixed costs and variable costs, we'll need to model both production and the status of the factory i.e. whether it is on or off.\n", 421 | "\n", 422 | "Production is modelled as an integer variable. \n", 423 | "\n", 424 | "We have a value for production for each month for each factory, this is given by the tuples of our multi-index pandas DataFrame index." 425 | ] 426 | }, 427 | { 428 | "cell_type": "code", 429 | "execution_count": 5, 430 | "metadata": { 431 | "collapsed": false 432 | }, 433 | "outputs": [], 434 | "source": [ 435 | "production = pulp.LpVariable.dicts(\"production\",\n", 436 | " ((month, factory) for month, factory in factories.index),\n", 437 | " lowBound=0,\n", 438 | " cat='Integer')" 439 | ] 440 | }, 441 | { 442 | "cell_type": "markdown", 443 | "metadata": {}, 444 | "source": [ 445 | "Factory status is modelled as a binary variable. It will have a value of 1 if the factory is on and a value of 0 when the factory is off.\n", 446 | "\n", 447 | "Binary variables are the same as integer variables but constrained to be >= 0 and <=1\n", 448 | "\n", 449 | "Again this has a value for each month for each factory, again given by the index of our DataFrame" 450 | ] 451 | }, 452 | { 453 | "cell_type": "code", 454 | "execution_count": 6, 455 | "metadata": { 456 | "collapsed": false 457 | }, 458 | "outputs": [], 459 | "source": [ 460 | "factory_status = pulp.LpVariable.dicts(\"factory_status\",\n", 461 | " ((month, factory) for month, factory in factories.index),\n", 462 | " cat='Binary')" 463 | ] 464 | }, 465 | { 466 | "cell_type": "markdown", 467 | "metadata": {}, 468 | "source": [ 469 | "We instantiate our model and use LpMinimize as the aim is to minimise costs." 470 | ] 471 | }, 472 | { 473 | "cell_type": "code", 474 | "execution_count": 7, 475 | "metadata": { 476 | "collapsed": false 477 | }, 478 | "outputs": [], 479 | "source": [ 480 | "model = pulp.LpProblem(\"Cost minimising scheduling problem\", pulp.LpMinimize)" 481 | ] 482 | }, 483 | { 484 | "cell_type": "markdown", 485 | "metadata": {}, 486 | "source": [ 487 | "In our objective function we include our 2 costs: \n", 488 | "* Our variable costs is the product of the variable costs per unit and production\n", 489 | "* Our fixed costs is the factory status - 1 (on) or 0 (off) - multiplied by the fixed cost of production" 490 | ] 491 | }, 492 | { 493 | "cell_type": "code", 494 | "execution_count": 8, 495 | "metadata": { 496 | "collapsed": false 497 | }, 498 | "outputs": [], 499 | "source": [ 500 | "model += pulp.lpSum(\n", 501 | " [production[month, factory] * factories.loc[(month, factory), 'Variable_Costs'] for month, factory in factories.index]\n", 502 | " + [factory_status[month, factory] * factories.loc[(month, factory), 'Fixed_Costs'] for month, factory in factories.index]\n", 503 | ")" 504 | ] 505 | }, 506 | { 507 | "cell_type": "markdown", 508 | "metadata": {}, 509 | "source": [ 510 | "We build up our constraints" 511 | ] 512 | }, 513 | { 514 | "cell_type": "code", 515 | "execution_count": 9, 516 | "metadata": { 517 | "collapsed": false 518 | }, 519 | "outputs": [], 520 | "source": [ 521 | "# Production in any month must be equal to demand\n", 522 | "months = demand.index\n", 523 | "for month in months:\n", 524 | " model += production[(month, 'A')] + production[(month, 'B')] == demand.loc[month, 'Demand']" 525 | ] 526 | }, 527 | { 528 | "cell_type": "markdown", 529 | "metadata": {}, 530 | "source": [ 531 | "An issue we run into here is that in linear programming we can't use conditional constraints. \n", 532 | "\n", 533 | "For example we can't add to our model that if the factory is off factory status must be 0, and if it is on factory status must be 1. Before we've solved our model though, we don't know if the factory will be on or off in a given month.\n", 534 | "\n", 535 | "In this case construct constraints that have minimum and maximum capacities that are constant variables, which we multiply by the factory status.\n", 536 | "\n", 537 | "Now, either factory status is 0 and:\n", 538 | "* $ \\text{min_production} \\geq 0$\n", 539 | "* $ \\text{max_production} \\leq 0$\n", 540 | "\n", 541 | "Or factory status is 1 and:\n", 542 | "* $ \\text{min_production} \\leq \\text{min_capacity}$\n", 543 | "* $ \\text{max_production} \\leq \\text{max_capacity}$\n", 544 | "\n", 545 | "*(In some cases we can use linear constraints to model conditional statements, we'll explore this in part 6)*" 546 | ] 547 | }, 548 | { 549 | "cell_type": "code", 550 | "execution_count": 10, 551 | "metadata": { 552 | "collapsed": true 553 | }, 554 | "outputs": [], 555 | "source": [ 556 | "# Production in any month must be between minimum and maximum capacity, or zero.\n", 557 | "for month, factory in factories.index:\n", 558 | " min_production = factories.loc[(month, factory), 'Min_Capacity']\n", 559 | " max_production = factories.loc[(month, factory), 'Max_Capacity']\n", 560 | " model += production[(month, factory)] >= min_production * factory_status[month, factory]\n", 561 | " model += production[(month, factory)] <= max_production * factory_status[month, factory]" 562 | ] 563 | }, 564 | { 565 | "cell_type": "code", 566 | "execution_count": 11, 567 | "metadata": { 568 | "collapsed": true 569 | }, 570 | "outputs": [], 571 | "source": [ 572 | "# Factory B is off in May\n", 573 | "model += factory_status[5, 'B'] == 0\n", 574 | "model += production[5, 'B'] == 0" 575 | ] 576 | }, 577 | { 578 | "cell_type": "markdown", 579 | "metadata": {}, 580 | "source": [ 581 | "We then solve the model" 582 | ] 583 | }, 584 | { 585 | "cell_type": "code", 586 | "execution_count": 12, 587 | "metadata": { 588 | "collapsed": false 589 | }, 590 | "outputs": [ 591 | { 592 | "data": { 593 | "text/plain": [ 594 | "'Optimal'" 595 | ] 596 | }, 597 | "execution_count": 12, 598 | "metadata": {}, 599 | "output_type": "execute_result" 600 | } 601 | ], 602 | "source": [ 603 | "model.solve()\n", 604 | "pulp.LpStatus[model.status]" 605 | ] 606 | }, 607 | { 608 | "cell_type": "markdown", 609 | "metadata": {}, 610 | "source": [ 611 | "Let's take a look at the optimal production schedule output for each month from each factory. For ease of viewing we'll output the data to a pandas DataFrame." 612 | ] 613 | }, 614 | { 615 | "cell_type": "code", 616 | "execution_count": 13, 617 | "metadata": { 618 | "collapsed": false 619 | }, 620 | "outputs": [ 621 | { 622 | "data": { 623 | "text/html": [ 624 | "
\n", 625 | "\n", 626 | " \n", 627 | " \n", 628 | " \n", 629 | " \n", 630 | " \n", 631 | " \n", 632 | " \n", 633 | " \n", 634 | " \n", 635 | " \n", 636 | " \n", 637 | " \n", 638 | " \n", 639 | " \n", 640 | " \n", 641 | " \n", 642 | " \n", 643 | " \n", 644 | " \n", 645 | " \n", 646 | " \n", 647 | " \n", 648 | " \n", 649 | " \n", 650 | " \n", 651 | " \n", 652 | " \n", 653 | " \n", 654 | " \n", 655 | " \n", 656 | " \n", 657 | " \n", 658 | " \n", 659 | " \n", 660 | " \n", 661 | " \n", 662 | " \n", 663 | " \n", 664 | " \n", 665 | " \n", 666 | " \n", 667 | " \n", 668 | " \n", 669 | " \n", 670 | " \n", 671 | " \n", 672 | " \n", 673 | " \n", 674 | " \n", 675 | " \n", 676 | " \n", 677 | " \n", 678 | " \n", 679 | " \n", 680 | " \n", 681 | " \n", 682 | " \n", 683 | " \n", 684 | " \n", 685 | " \n", 686 | " \n", 687 | " \n", 688 | " \n", 689 | " \n", 690 | " \n", 691 | " \n", 692 | " \n", 693 | " \n", 694 | " \n", 695 | " \n", 696 | " \n", 697 | " \n", 698 | " \n", 699 | " \n", 700 | " \n", 701 | " \n", 702 | " \n", 703 | " \n", 704 | " \n", 705 | " \n", 706 | " \n", 707 | " \n", 708 | " \n", 709 | " \n", 710 | " \n", 711 | " \n", 712 | " \n", 713 | " \n", 714 | " \n", 715 | " \n", 716 | " \n", 717 | " \n", 718 | " \n", 719 | " \n", 720 | " \n", 721 | " \n", 722 | " \n", 723 | " \n", 724 | " \n", 725 | " \n", 726 | " \n", 727 | " \n", 728 | " \n", 729 | " \n", 730 | " \n", 731 | " \n", 732 | " \n", 733 | " \n", 734 | " \n", 735 | " \n", 736 | " \n", 737 | " \n", 738 | " \n", 739 | " \n", 740 | " \n", 741 | " \n", 742 | " \n", 743 | " \n", 744 | " \n", 745 | " \n", 746 | " \n", 747 | " \n", 748 | " \n", 749 | " \n", 750 | " \n", 751 | " \n", 752 | " \n", 753 | " \n", 754 | " \n", 755 | " \n", 756 | " \n", 757 | " \n", 758 | " \n", 759 | " \n", 760 | " \n", 761 | " \n", 762 | " \n", 763 | " \n", 764 | " \n", 765 | " \n", 766 | " \n", 767 | " \n", 768 | " \n", 769 | " \n", 770 | " \n", 771 | " \n", 772 | " \n", 773 | " \n", 774 | "
Factory StatusProduction
MonthFactory
1A170000
B150000
2A145000
B155000
3A170000
B160000
4A130000
B1100000
5A1140000
B00
6A160000
B170000
7A190000
B160000
8A170000
B1100000
9A1100000
B1100000
10A1190000
B00
11A180000
B160000
12A1100000
B00
\n", 775 | "
" 776 | ], 777 | "text/plain": [ 778 | " Factory Status Production\n", 779 | "Month Factory \n", 780 | "1 A 1 70000\n", 781 | " B 1 50000\n", 782 | "2 A 1 45000\n", 783 | " B 1 55000\n", 784 | "3 A 1 70000\n", 785 | " B 1 60000\n", 786 | "4 A 1 30000\n", 787 | " B 1 100000\n", 788 | "5 A 1 140000\n", 789 | " B 0 0\n", 790 | "6 A 1 60000\n", 791 | " B 1 70000\n", 792 | "7 A 1 90000\n", 793 | " B 1 60000\n", 794 | "8 A 1 70000\n", 795 | " B 1 100000\n", 796 | "9 A 1 100000\n", 797 | " B 1 100000\n", 798 | "10 A 1 190000\n", 799 | " B 0 0\n", 800 | "11 A 1 80000\n", 801 | " B 1 60000\n", 802 | "12 A 1 100000\n", 803 | " B 0 0" 804 | ] 805 | }, 806 | "execution_count": 13, 807 | "metadata": {}, 808 | "output_type": "execute_result" 809 | } 810 | ], 811 | "source": [ 812 | "output = []\n", 813 | "for month, factory in production:\n", 814 | " var_output = {\n", 815 | " 'Month': month,\n", 816 | " 'Factory': factory,\n", 817 | " 'Production': production[(month, factory)].varValue,\n", 818 | " 'Factory Status': factory_status[(month, factory)].varValue\n", 819 | " }\n", 820 | " output.append(var_output)\n", 821 | "output_df = pd.DataFrame.from_records(output).sort_values(['Month', 'Factory'])\n", 822 | "output_df.set_index(['Month', 'Factory'], inplace=True)\n", 823 | "output_df" 824 | ] 825 | }, 826 | { 827 | "cell_type": "markdown", 828 | "metadata": {}, 829 | "source": [ 830 | "Notice above that the factory status is 0 when not producing and 1 when it is producing" 831 | ] 832 | }, 833 | { 834 | "cell_type": "code", 835 | "execution_count": 14, 836 | "metadata": { 837 | "collapsed": false 838 | }, 839 | "outputs": [ 840 | { 841 | "name": "stdout", 842 | "output_type": "stream", 843 | "text": [ 844 | "12906400.0\n" 845 | ] 846 | } 847 | ], 848 | "source": [ 849 | "# Print our objective function value (Total Costs)\n", 850 | "print pulp.value(model.objective)" 851 | ] 852 | } 853 | ], 854 | "metadata": { 855 | "kernelspec": { 856 | "display_name": "Python 2", 857 | "language": "python", 858 | "name": "python2" 859 | }, 860 | "language_info": { 861 | "codemirror_mode": { 862 | "name": "ipython", 863 | "version": 2 864 | }, 865 | "file_extension": ".py", 866 | "mimetype": "text/x-python", 867 | "name": "python", 868 | "nbconvert_exporter": "python", 869 | "pygments_lexer": "ipython2", 870 | "version": "2.7.10" 871 | } 872 | }, 873 | "nbformat": 4, 874 | "nbformat_minor": 0 875 | } 876 | -------------------------------------------------------------------------------- /LaTeX_formatted_ipynb_files/part2.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
6 |
7 |

Introduction to Linear Programming with Python - Part 2

Introduction to PuLP

8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |

PuLP is an open source linear programming package for python. PuLP can be installed using pip, instructions here.

17 |

In this notebook, we'll explore how to construct and solve the linear programming problem described in Part 1 using PuLP.

18 |

A brief reminder of our linear programming problem:

19 |

We want to find the maximum solution to the objective function:

20 |

$Z = 4x + 3y$

21 |

Subject to the following constraints:

22 |

$ 23 | x \geq 0 \\ 24 | y \geq 2 \\ 25 | 2y \leq 25 - x \\ 26 | 4y \geq 2x - 8 \\ 27 | y \leq 2x -5 \\ 28 | $

29 | 30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |

We'll begin by importing PuLP

39 | 40 |
41 |
42 |
43 |
44 |
45 |
In [1]:
46 |
47 |
48 |
import pulp
 49 | 
50 | 51 |
52 |
53 |
54 | 55 |
56 |
57 |
58 |
59 |
60 |
61 |

Then instantiate a problem class, we'll name it "My LP problem" and we're looking for an optimal maximum so we use LpMaximize

62 | 63 |
64 |
65 |
66 |
67 |
68 |
In [2]:
69 |
70 |
71 |
my_lp_problem = pulp.LpProblem("My LP Problem", pulp.LpMaximize)
 72 | 
73 | 74 |
75 |
76 |
77 | 78 |
79 |
80 |
81 |
82 |
83 |
84 |

We then model our decision variables using the LpVariable class. In our example, x had a lower bound of 0 and y had a lower bound of 2.

85 |

Upper bounds can be assigned using the upBound parameter.

86 | 87 |
88 |
89 |
90 |
91 |
92 |
In [3]:
93 |
94 |
95 |
x = pulp.LpVariable('x', lowBound=0, cat='Continuous')
 96 | y = pulp.LpVariable('y', lowBound=2, cat='Continuous')
 97 | 
98 | 99 |
100 |
101 |
102 | 103 |
104 |
105 |
106 |
107 |
108 |
109 |

The objective function and constraints are added using the += operator to our model.

110 |

The objective function is added first, then the individual constraints.

111 | 112 |
113 |
114 |
115 |
116 |
117 |
In [4]:
118 |
119 |
120 |
# Objective function
121 | my_lp_problem += 4 * x + 3 * y, "Z"
122 | 
123 | # Constraints
124 | my_lp_problem += 2 * y <= 25 - x
125 | my_lp_problem += 4 * y >= 2 * x - 8
126 | my_lp_problem += y <= 2 * x - 5
127 | 
128 | 129 |
130 |
131 |
132 | 133 |
134 |
135 |
136 |
137 |
138 |
139 |

We have now constructed our problem and can have a look at it.

140 | 141 |
142 |
143 |
144 |
145 |
146 |
In [5]:
147 |
148 |
149 |
my_lp_problem
150 | 
151 | 152 |
153 |
154 |
155 | 156 |
157 |
158 | 159 | 160 |
Out[5]:
161 | 162 | 163 |
164 |
My LP Problem:
165 | MAXIMIZE
166 | 4*x + 3*y + 0
167 | SUBJECT TO
168 | _C1: x + 2 y <= 25
169 | 
170 | _C2: - 2 x + 4 y >= -8
171 | 
172 | _C3: - 2 x + y <= -5
173 | 
174 | VARIABLES
175 | x Continuous
176 | 2 <= y Continuous
177 |
178 | 179 |
180 | 181 |
182 |
183 | 184 |
185 |
186 |
187 |
188 |
189 |
190 |

PuLP supports open source linear programming solvers such as CBC and GLPK, as well as commercial solvers such as Gurobi and IBM's CPLEX.

191 |

The default solver is CBC, which comes packaged with PuLP upon installation.

192 |

For most applications, the open source CBC from COIN-OR will be enough for most simple linear programming optimisation algorithms.

193 | 194 |
195 |
196 |
197 |
198 |
199 |
In [6]:
200 |
201 |
202 |
my_lp_problem.solve()
203 | pulp.LpStatus[my_lp_problem.status]
204 | 
205 | 206 |
207 |
208 |
209 | 210 |
211 |
212 | 213 | 214 |
Out[6]:
215 | 216 | 217 |
218 |
'Optimal'
219 |
220 | 221 |
222 | 223 |
224 |
225 | 226 |
227 |
228 |
229 |
230 |
231 |
232 |

We have also checked the status of the solver, there are 5 status codes:

233 |
    234 |
  • Not Solved: Status prior to solving the problem.
  • 235 |
  • Optimal: An optimal solution has been found.
  • 236 |
  • Infeasible: There are no feasible solutions (e.g. if you set the constraints x <= 1 and x >=2).
  • 237 |
  • Unbounded: The constraints are not bounded, maximising the solution will tend towards infinity (e.g. if the only constraint was x >= 3).
  • 238 |
  • Undefined: The optimal solution may exist but may not have been found.
  • 239 |
240 | 241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |

We can now view our maximal variable values and the maximum value of Z.

250 |

We can use the varValue method to retrieve the values of our variables x and y, and the pulp.value function to view the maximum value of the objective function.

251 | 252 |
253 |
254 |
255 |
256 |
257 |
In [7]:
258 |
259 |
260 |
for variable in my_lp_problem.variables():
261 |     print "{} = {}".format(variable.name, variable.varValue)
262 | 
263 | 264 |
265 |
266 |
267 | 268 |
269 |
270 | 271 | 272 |
273 |
274 |
x = 14.5
275 | y = 5.25
276 | 
277 |
278 |
279 | 280 |
281 |
282 | 283 |
284 |
285 |
286 |
In [8]:
287 |
288 |
289 |
print pulp.value(my_lp_problem.objective)
290 | 
291 | 292 |
293 |
294 |
295 | 296 |
297 |
298 | 299 | 300 |
301 |
302 |
73.75
303 | 
304 |
305 |
306 | 307 |
308 |
309 | 310 |
311 |
312 |
313 |
314 |
315 |
316 |

Same values as our manual calculations in part 1.

317 |

In the next part we'll be looking at a more real world problem.

318 | 319 |
320 |
321 |
-------------------------------------------------------------------------------- /LaTeX_formatted_ipynb_files/part3.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
6 |
7 |

Introduction to Linear Programming with Python - Part 3

Real world examples - Resourcing Problem

8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |

We'll now look at 2 more real world examples.

17 |

The first is a resourcing problem and the second is a blending problem.

18 |

Resourcing Problem

19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |

We're consulting for a boutique car manufacturer, producing luxury cars.

28 |

They run on one month (30 days) cycles, we have one cycle to show we can provide value.

29 |

There is one robot, 2 engineers and one detailer in the factory. The detailer has some holiday off, so only has 21 days available.

30 |

The 2 cars need different time with each resource:

31 |

Robot time: Car A - 3 days; Car B - 4 days.

32 |

Engineer time: Car A - 5 days; Car B - 6 days.

33 |

Detailer time: Car A - 1.5 days; Car B - 3 days.

34 |

Car A provides €30,000 profit, whilst Car B offers €45,000 profit.

35 |

At the moment, they produce 4 of each cars per month, for €300,000 profit. Not bad at all, but we think we can do better for them.

36 | 37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |

This can be modelled as follows:

46 |

Maximise

47 |

$\text{Profit} = 30,000A + 45,000B$

48 |

Subject to:

49 |

$ 50 | A \geq 0 \\ 51 | B \geq 0 \\ 52 | 3A + 4B \leq 30 \\ 53 | 5A + 6B \leq 60 \\ 54 | 1.5A + 3B \leq 21 \\ 55 | $

56 | 57 |
58 |
59 |
60 |
61 |
62 |
In [1]:
63 |
64 |
65 |
import pulp
 66 | 
67 | 68 |
69 |
70 |
71 | 72 |
73 |
74 |
75 |
In [2]:
76 |
77 |
78 |
# Instantiate our problem class
 79 | model = pulp.LpProblem("Profit maximising problem", pulp.LpMaximize)
 80 | 
81 | 82 |
83 |
84 |
85 | 86 |
87 |
88 |
89 |
90 |
91 |
92 |

Unlike our previous problem, the decision variables in this case won't be continuous (We can't sell half a car!), so the category is integer.

93 | 94 |
95 |
96 |
97 |
98 |
99 |
In [3]:
100 |
101 |
102 |
A = pulp.LpVariable('A', lowBound=0, cat='Integer')
103 | B = pulp.LpVariable('B', lowBound=0, cat='Integer')
104 | 
105 | 106 |
107 |
108 |
109 | 110 |
111 |
112 |
113 |
In [4]:
114 |
115 |
116 |
# Objective function
117 | model += 30000 * A + 45000 * B, "Profit"
118 | 
119 | # Constraints
120 | model += 3 * A + 4 * B <= 30
121 | model += 5 * A + 6 * B <= 60
122 | model += 1.5 * A + 3 * B <= 21
123 | 
124 | 125 |
126 |
127 |
128 | 129 |
130 |
131 |
132 |
In [5]:
133 |
134 |
135 |
# Solve our problem
136 | model.solve()
137 | pulp.LpStatus[model.status]
138 | 
139 | 140 |
141 |
142 |
143 | 144 |
145 |
146 | 147 | 148 |
Out[5]:
149 | 150 | 151 |
152 |
'Optimal'
153 |
154 | 155 |
156 | 157 |
158 |
159 | 160 |
161 |
162 |
163 |
In [6]:
164 |
165 |
166 |
# Print our decision variable values
167 | print "Production of Car A = {}".format(A.varValue)
168 | print "Production of Car B = {}".format(B.varValue)
169 | 
170 | 171 |
172 |
173 |
174 | 175 |
176 |
177 | 178 | 179 |
180 |
181 |
Production of Car A = 2.0
182 | Production of Car B = 6.0
183 | 
184 |
185 |
186 | 187 |
188 |
189 | 190 |
191 |
192 |
193 |
In [7]:
194 |
195 |
196 |
# Print our objective function value
197 | print pulp.value(model.objective)
198 | 
199 | 200 |
201 |
202 |
203 | 204 |
205 |
206 | 207 | 208 |
209 |
210 |
330000.0
211 | 
212 |
213 |
214 | 215 |
216 |
217 | 218 |
219 |
220 |
221 |
222 |
223 |
224 |

So that's €330,000 monthly profit, compared to their original monthly profit of €300,000

225 |

By producing 2 cars of Car A and 4 cars of Car B, we bolster the profits at the factory by €30,000 per month.

226 |

We take our consultancy fee and leave the company with €360,000 extra profit for the factory every year.

227 |

In the next part, we'll be making some sausages!

228 | 229 |
230 |
231 |
-------------------------------------------------------------------------------- /LaTeX_formatted_ipynb_files/part4.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
6 |
7 |

Introduction to Linear Programming with Python - Part 4

Real world examples - Blending Problem

8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |

We're going to make some sausages!

17 |

We have the following ingredients available to us:

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
IngredientCost (€/kg)Availability (kg)
Pork4.3230
Wheat2.4620
Starch1.8617
43 |

We'll make 2 types of sausage:

44 |
    45 |
  • Economy (>40% Pork)
  • 46 |
  • Premium (>60% Pork)
  • 47 |
48 |

One sausage is 50 grams (0.05 kg)

49 |

According to government regulations, the most starch we can use in our sausages is 25%

50 |

We have a contract with a butcher, and have already purchased 23 kg pork, that must go in our sausages.

51 |

We have a demand for 350 economy sausages and 500 premium sausages.

52 |

We need to figure out how to most cost effectively blend our sausages.

53 | 54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |

Let's model our problem:

63 |

$ 64 | p_e = \text{Pork in the economy sausages (kg)} \\ 65 | w_e = \text{Wheat in the economy sausages (kg)} \\ 66 | s_e = \text{Starch in the economy sausages (kg)} \\ 67 | p_p = \text{Pork in the premium sausages (kg)} \\ 68 | w_p = \text{Wheat in the premium sausages (kg)} \\ 69 | s_p = \text{Starch in the premium sausages (kg)} \\ 70 | $

71 |

We want to minimise costs such that:

72 |

$\text{Cost} = 0.72(p_e + p_p) + 0.41(w_e + w_p) + 0.31(s_e + s_p)$

73 |

With the following constraints:

74 |

$ 75 | p_e + w_e + s_e = 350 \times 0.05 \\ 76 | p_p + w_p + s_p = 500 \times 0.05 \\ 77 | p_e \geq 0.4(p_e + w_e + s_e) \\ 78 | p_p \geq 0.6(p_p + w_p + s_p) \\ 79 | s_e \leq 0.25(p_e + w_e + s_e) \\ 80 | s_p \leq 0.25(p_p + w_p + s_p) \\ 81 | p_e + p_p \leq 30 \\ 82 | w_e + w_p \leq 20 \\ 83 | s_e + s_p \leq 17 \\ 84 | p_e + p_p \geq 23 \\ 85 | $

86 | 87 |
88 |
89 |
90 |
91 |
92 |
In [1]:
93 |
94 |
95 |
import pulp
 96 | 
97 | 98 |
99 |
100 |
101 | 102 |
103 |
104 |
105 |
In [2]:
106 |
107 |
108 |
# Instantiate our problem class
109 | model = pulp.LpProblem("Cost minimising blending problem", pulp.LpMinimize)
110 | 
111 | 112 |
113 |
114 |
115 | 116 |
117 |
118 |
119 |
120 |
121 |
122 |

Here we have 6 decision variables, we could name them individually but this wouldn't scale up if we had hundreds/thousands of variables (you don't want to be entering all of these by hand multiple times).

123 |

We'll create a couple of lists from which we can create tuple indices.

124 | 125 |
126 |
127 |
128 |
129 |
130 |
In [3]:
131 |
132 |
133 |
# Construct our decision variable lists
134 | sausage_types = ['economy', 'premium']
135 | ingredients = ['pork', 'wheat', 'starch']
136 | 
137 | 138 |
139 |
140 |
141 | 142 |
143 |
144 |
145 |
146 |
147 |
148 |

Each of these decision variables will have similar characteristics (lower bound of 0, continuous variables). Therefore we can use PuLP's LpVariable object's dict functionality, we can provide our tuple indices.

149 |

These tuples will be keys for the ing_weight dict of decision variables

150 | 151 |
152 |
153 |
154 |
155 |
156 |
In [4]:
157 |
158 |
159 |
ing_weight = pulp.LpVariable.dicts("weight kg",
160 |                                      ((i, j) for i in sausage_types for j in ingredients),
161 |                                      lowBound=0,
162 |                                      cat='Continuous')
163 | 
164 | 165 |
166 |
167 |
168 | 169 |
170 |
171 |
172 |
173 |
174 |
175 |

PuLP provides an lpSum vector calculation for the sum of a list of linear expressions.

176 |

Whilst we only have 6 decision variables, I will demonstrate how the problem would be constructed in a way that could be scaled up to many variables using list comprehensions.

177 | 178 |
179 |
180 |
181 |
182 |
183 |
In [5]:
184 |
185 |
186 |
# Objective Function
187 | model += (
188 |     pulp.lpSum([
189 |         4.32 * ing_weight[(i, 'pork')]
190 |         + 2.46 * ing_weight[(i, 'wheat')]
191 |         + 1.86 * ing_weight[(i, 'starch')]
192 |         for i in sausage_types])
193 | )
194 | 
195 | 196 |
197 |
198 |
199 | 200 |
201 |
202 |
203 |
204 |
205 |
206 |

Now we add our constraints, bear in mind again here how the use of list comprehensions allows for scaling up to many ingredients or sausage types

207 | 208 |
209 |
210 |
211 |
212 |
213 |
In [6]:
214 |
215 |
216 |
# Constraints
217 | # 350 economy and 500 premium sausages at 0.05 kg
218 | model += pulp.lpSum([ing_weight['economy', j] for j in ingredients]) == 350 * 0.05
219 | model += pulp.lpSum([ing_weight['premium', j] for j in ingredients]) == 500 * 0.05
220 | 
221 | # Economy has >= 40% pork, premium >= 60% pork
222 | model += ing_weight['economy', 'pork'] >= (
223 |     0.4 * pulp.lpSum([ing_weight['economy', j] for j in ingredients]))
224 | 
225 | model += ing_weight['premium', 'pork'] >= (
226 |     0.6 * pulp.lpSum([ing_weight['premium', j] for j in ingredients]))
227 | 
228 | # Sausages must be <= 25% starch
229 | model += ing_weight['economy', 'starch'] <= (
230 |     0.25 * pulp.lpSum([ing_weight['economy', j] for j in ingredients]))
231 | 
232 | model += ing_weight['premium', 'starch'] <= (
233 |     0.25 * pulp.lpSum([ing_weight['premium', j] for j in ingredients]))
234 | 
235 | # We have at most 30 kg of pork, 20 kg of wheat and 17 kg of starch available
236 | model += pulp.lpSum([ing_weight[i, 'pork'] for i in sausage_types]) <= 30
237 | model += pulp.lpSum([ing_weight[i, 'wheat'] for i in sausage_types]) <= 20
238 | model += pulp.lpSum([ing_weight[i, 'starch'] for i in sausage_types]) <= 17
239 | 
240 | # We have at least 23 kg of pork to use up
241 | model += pulp.lpSum([ing_weight[i, 'pork'] for i in sausage_types]) >= 23
242 | 
243 | 244 |
245 |
246 |
247 | 248 |
249 |
250 |
251 |
In [7]:
252 |
253 |
254 |
# Solve our problem
255 | model.solve()
256 | pulp.LpStatus[model.status]
257 | 
258 | 259 |
260 |
261 |
262 | 263 |
264 |
265 | 266 | 267 |
Out[7]:
268 | 269 | 270 |
271 |
'Optimal'
272 |
273 | 274 |
275 | 276 |
277 |
278 | 279 |
280 |
281 |
282 |
In [8]:
283 |
284 |
285 |
for var in ing_weight:
286 |     var_value = ing_weight[var].varValue
287 |     print "The weight of {0} in {1} sausages is {2} kg".format(var[1], var[0], var_value)
288 | 
289 | 290 |
291 |
292 |
293 | 294 |
295 |
296 | 297 | 298 |
299 |
300 |
The weight of starch in premium sausages is 6.25 kg
301 | The weight of starch in economy sausages is 4.375 kg
302 | The weight of wheat in economy sausages is 6.125 kg
303 | The weight of wheat in premium sausages is 2.75 kg
304 | The weight of pork in economy sausages is 7.0 kg
305 | The weight of pork in premium sausages is 16.0 kg
306 | 
307 |
308 |
309 | 310 |
311 |
312 | 313 |
314 |
315 |
316 |
In [9]:
317 |
318 |
319 |
total_cost = pulp.value(model.objective)
320 | 
321 | print "The total cost is €{} for 350 economy sausages and 500 premium sausages".format(round(total_cost, 2))
322 | 
323 | 324 |
325 |
326 |
327 | 328 |
329 |
330 | 331 | 332 |
333 |
334 |
The total cost is €140.96 for 350 economy sausages and 500 premium sausages
335 | 
336 |
337 |
338 | 339 |
340 |
341 | 342 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction to Linear Programming with Python 2 | 3 | Linear Programming, also sometimes called linear optimisation, involves maximising or minimising a linear objective function, subject to a set of linear inequality or equality constraints. 4 | 5 | It has great applications in the field of operations management but can be used to solve a range of problems. 6 | 7 | Leonard Kantrovich was awarded the 1975 Nobel Prize in Economics for the optimal allocation of resources using linear programming. 8 | 9 | Examples of problems that can be solved by linear programming include: 10 | * Scheduling - Rota or Factory scheduling to meet production/workload demands 11 | * Resourcing Problems - How best to allocate resources to maximise profits 12 | * Blending Problems - Cost effectively blending a mixture of components 13 | * [Sudoku](https://coin-or.github.io/pulp/CaseStudies/a_sudoku_problem.html) 14 | 15 | In this set of notebooks, we explore some linear programming examples, starting with some very basic Mathematical theory behind the technique and moving on to some real world examples. 16 | 17 | We will be using python and the [PuLP](https://coin-or.github.io/pulp/) linear programming package to solve these linear programming problems. PuLP largely uses python syntax and comes packaged with the CBC solver; it also integrates nicely with a range of open source and commercial LP solvers. 18 | 19 | #### [Part 1](https://github.com/benalexkeen/Introduction-to-linear-programming/blob/master/%20Introduction%20to%20Linear%20Programming%20with%20Python%20-%20Part%201.ipynb) - Introduction to Linear Programming 20 | #### [Part 2](https://github.com/benalexkeen/Introduction-to-linear-programming/blob/master/Introduction%20to%20Linear%20Programming%20with%20Python%20-%20Part%202.ipynb) - Introduction to PuLP 21 | #### [Part 3](https://github.com/benalexkeen/Introduction-to-linear-programming/blob/master/Introduction%20to%20Linear%20Programming%20with%20Python%20-%20Part%203.ipynb) - Real world examples - Resourcing Problem 22 | #### [Part 4](https://github.com/benalexkeen/Introduction-to-linear-programming/blob/master/Introduction%20to%20Linear%20Programming%20with%20Python%20-%20Part%204.ipynb) - Real world examples - Blending Problem 23 | #### [Part 5](https://github.com/benalexkeen/Introduction-to-linear-programming/blob/master/%20Introduction%20to%20Linear%20Programming%20with%20Python%20-%20Part%205.ipynb) - Using PuLP with pandas and binary constraints to solve a scheduling problem 24 | #### [Part 6](https://github.com/benalexkeen/Introduction-to-linear-programming/blob/master/%20Introduction%20to%20Linear%20Programming%20with%20Python%20-%20Part%206.ipynb) - Mocking conditional statements using binary constraints 25 | 26 | 27 | -------------------------------------------------------------------------------- /csv/factory_variables.csv: -------------------------------------------------------------------------------- 1 | Month,Factory,Max_Capacity,Min_Capacity,Variable_Costs,Fixed_Costs 2 | 1,A,100000,20000,10,500 3 | 1,B,50000,20000,5,600 4 | 2,A,110000,20000,11,500 5 | 2,B,55000,20000,4,600 6 | 3,A,120000,20000,12,500 7 | 3,B,60000,20000,3,600 8 | 4,A,145000,20000,9,500 9 | 4,B,100000,20000,5,600 10 | 5,A,160000,20000,8,500 11 | 5,B,0,0,0,0 12 | 6,A,140000,20000,8,500 13 | 6,B,70000,20000,6,600 14 | 7,A,155000,20000,5,500 15 | 7,B,60000,20000,4,600 16 | 8,A,200000,20000,7,500 17 | 8,B,100000,20000,6,600 18 | 9,A,210000,20000,9,500 19 | 9,B,100000,20000,8,600 20 | 10,A,197000,20000,10,500 21 | 10,B,100000,20000,11,600 22 | 11,A,80000,20000,8,500 23 | 11,B,120000,20000,10,600 24 | 12,A,150000,20000,8,500 25 | 12,B,150000,20000,12,600 -------------------------------------------------------------------------------- /csv/monthly_demand.csv: -------------------------------------------------------------------------------- 1 | Month,Demand 2 | 1,120000 3 | 2,100000 4 | 3,130000 5 | 4,130000 6 | 5,140000 7 | 6,130000 8 | 7,150000 9 | 8,170000 10 | 9,200000 11 | 10,190000 12 | 11,140000 13 | 12,100000 --------------------------------------------------------------------------------