{ "metadata": { "name": "" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "Solving Linear Programming Problems with PuLP" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This [IPython notebook](http://ipython.org/notebook.html) demonstrates the use of the [Python package PuLP](https://pypi.python.org/pypi/PuLP/1.5.4) for the modeling and solution of linear programming problems.\n", "\n", "J.C. Kantor (Kantor.1@nd.edu)\n", " \n", "The latest version of this IPython notebook is available at [http://github.com/jckantor/CBE20255](http://github.com/jckantor/CBE20255) for noncommercial use under terms of the [Creative Commons Attribution Noncommericial ShareAlike License](http://creativecommons.org/licenses/by-nc-sa/4.0/)." ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Installing PuLP" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "PuLP is an external package that may need to be installed into your python environment. The following cell issues a system command to install PuLP if needed. PuLP includes the [COIN-OR CBC](https://projects.coin-or.org/Cbc) solver." ] }, { "cell_type": "code", "collapsed": false, "input": [ "!pip install PuLP" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Requirement already satisfied (use --upgrade to upgrade): PuLP in /Applications/anaconda/lib/python2.7/site-packages\r\n", "Requirement already satisfied (use --upgrade to upgrade): pyparsing<=1.9.9 in /Applications/anaconda/lib/python2.7/site-packages (from PuLP)\r\n", "Cleaning up...\r\n" ] } ], "prompt_number": 1 }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Example: Giapetto's Workshop" ] }, { "cell_type": "heading", "level": 3, "metadata": {}, "source": [ "Model Formulation" ] }, { "cell_type": "code", "collapsed": false, "input": [ "import pulp\n", "\n", "# Create problem instance\n", "giapetto = pulp.LpProblem(\"Giapetto's Workshop\", pulp.LpMaximize)\n", "\n", "# Define Variables with lower bounds of zero\n", "x1 = pulp.LpVariable(\"Soldiers\",0)\n", "x2 = pulp.LpVariable(\"Trains\",0)\n", "\n", "# Add Objective\n", "giapetto += 3*x1 + 2*x2, \"Profit\"\n", "\n", "# Add Constraints\n", "giapetto += 2*x1 + x2 <= 100,\"Finishing Labor\"\n", "giapetto += x1 + x2 <= 80, \"Carpentry Labor\"\n", "giapetto += x1 <= 40, \"Soldier Demand\"\n", "giapetto += x1 + x2 == 20, \"Minimum Production\"\n", "\n", "# Display Problem\n", "print giapetto" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Giapetto's Workshop:\n", "MAXIMIZE\n", "3*Soldiers + 2*Trains + 0\n", "SUBJECT TO\n", "Finishing_Labor: 2 Soldiers + Trains <= 100\n", "\n", "Carpentry_Labor: Soldiers + Trains <= 80\n", "\n", "Soldier_Demand: Soldiers <= 40\n", "\n", "Minimum_Production: Soldiers + Trains = 20\n", "\n", "VARIABLES\n", "Soldiers Continuous\n", "Trains Continuous\n", "\n" ] } ], "prompt_number": 2 }, { "cell_type": "heading", "level": 3, "metadata": {}, "source": [ "Model Solution" ] }, { "cell_type": "code", "collapsed": false, "input": [ "giapetto.solve()\n", "\n", "print pulp.LpStatus[giapetto.status]\n", "print pulp.LpSenses[giapetto.sense], giapetto.objective.name, \"=\", pulp.value(giapetto.objective)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Optimal\n", "Maximize OBJ = 60.0\n" ] } ], "prompt_number": 3 }, { "cell_type": "heading", "level": 3, "metadata": {}, "source": [ "Displaying the Decision Variables" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Values for the decision variables can be accessed with the `pulp.value()` method." ] }, { "cell_type": "code", "collapsed": false, "input": [ "print x1.name, \"=\" , pulp.value(x1)\n", "print x2.name, \"=\" , pulp.value(x2)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Soldiers = 20.0\n", "Trains = 0.0\n" ] } ], "prompt_number": 4 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Alternatively, the decision variables can be accessed by interating through the list created by the `variables()` method." ] }, { "cell_type": "code", "collapsed": false, "input": [ "for x in giapetto.variables():\n", " print \"Name: \", x.name\n", " print \"Value: \", x.varValue\n", " print \"Category: \", x.cat\n", " print \"Lower Bound:\", x.lowBound\n", " print \"Upper Bound:\", x.upBound\n", " print " ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Name: Soldiers\n", "Value: 20.0\n", "Category: Continuous\n", "Lower Bound: 0\n", "Upper Bound: None\n", "\n", "Name: Trains\n", "Value: 0.0\n", "Category: Continuous\n", "Lower Bound: 0\n", "Upper Bound: None\n", "\n" ] } ], "prompt_number": 5 }, { "cell_type": "markdown", "metadata": {}, "source": [ "[Pandas](http://pandas.pydata.org/) is a python package for managing and analysing data. The following cell shows how to use a list comprehension and panda DataFrame to create and display a table from the solution data." ] }, { "cell_type": "heading", "level": 3, "metadata": {}, "source": [ "Displaying the Constraints" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The constraints are stored as an ordered dictionary in `constraints` attribute of the problem. " ] }, { "cell_type": "code", "collapsed": false, "input": [ "for (name,constraint) in giapetto.constraints.items():\n", " print \"Name: \", name\n", " print \"Constraint: \", constraint\n", " print \"Lower Bound: \", constraint.getLb()\n", " print \"Upper Bound: \", constraint.getUb()\n", " print \"Value: \", pulp.value(constraint)\n", " print \"Sense: \", pulp.LpConstraintSenses[constraint.sense]\n", " print \"Constant: \", constraint.constant\n", " print \"Slack: \", constraint.slack\n", " print \"Slack (Feas):\", constraint.slack if constraint.sense < 0 else -constraint.slack\n", " print" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Name: Finishing_Labor\n", "Constraint: 2*Soldiers + Trains <= 100\n", "Lower Bound: None\n", "Upper Bound: 100\n", "Value: -60.0\n", "Sense: <=\n", "Constant: -100\n", "Slack: 60.0\n", "Slack (Feas): 60.0\n", "\n", "Name: Carpentry_Labor\n", "Constraint: Soldiers + Trains <= 80\n", "Lower Bound: None\n", "Upper Bound: 80\n", "Value: -60.0\n", "Sense: <=\n", "Constant: -80\n", "Slack: 60.0\n", "Slack (Feas): 60.0\n", "\n", "Name: Soldier_Demand\n", "Constraint: Soldiers <= 40\n", "Lower Bound: None\n", "Upper Bound: 40\n", "Value: -20.0\n", "Sense: <=\n", "Constant: -40\n", "Slack: 20.0\n", "Slack (Feas): 20.0\n", "\n", "Name: Minimum_Production\n", "Constraint: Soldiers + Trains\n", "Lower Bound: 20\n", "Upper Bound: 20\n", "Value: 0.0\n", "Sense: =\n", "Constant: -20\n", "Slack: -0.0\n", "Slack (Feas): 0.0\n", "\n" ] } ], "prompt_number": 6 }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "A simple function to solve and display the solution to a linear program" ] }, { "cell_type": "code", "collapsed": false, "input": [ "import pandas\n", "\n", "def solvelp(lp):\n", " lp.solve();\n", "\n", " data = [[lp.objective.name, pulp.LpSenses[lp.sense], pulp.LpStatus[lp.status], pulp.value(lp.objective)]]\n", " objDF = pandas.DataFrame(data,columns=[\"Objective\", \"Sense\", \"Status\", \"Value\"])\n", "\n", " data = [[x.name, x.cat, x.lowBound, x.upBound, x.varValue] \n", " for x in lp.variables()]\n", " varDF = pandas.DataFrame(data,columns=[\"Name\",\"Category\",\"Lower Bound\",\"Upper Bound\",\"Value\"]) \n", "\n", " data = [[c.name, c.getLb(), c.getUb(), c.slack, pulp.value(c) - c.constant] \n", " for (k,c) in lp.constraints.items()]\n", " conDF = pandas.DataFrame(data,columns=[\"Name\", \"Lower Bound\", \"Upper Bound\", \"Slack\", \"Value\"])\n", "\n", " return (objDF,varDF,conDF)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 7 }, { "cell_type": "code", "collapsed": false, "input": [ "(odf,vdf,cdf) = solvelp(giapetto);\n", "\n", "from IPython.display import display,HTML\n", "\n", "display(HTML('

Model

'))\n", "display(giapetto)\n", "display(HTML('

Objective

'))\n", "display(odf)\n", "display(HTML('

Variables

'))\n", "display(vdf)\n", "display(HTML('

Constraints

'))\n", "display(cdf)" ], "language": "python", "metadata": {}, "outputs": [ { "html": [ "

Model

" ], "metadata": {}, "output_type": "display_data", "text": [ "" ] }, { "metadata": {}, "output_type": "display_data", "text": [ "Giapetto's Workshop:\n", "MAXIMIZE\n", "3*Soldiers + 2*Trains + 0\n", "SUBJECT TO\n", "Finishing_Labor: 2 Soldiers + Trains <= 100\n", "\n", "Carpentry_Labor: Soldiers + Trains <= 80\n", "\n", "Soldier_Demand: Soldiers <= 40\n", "\n", "Minimum_Production: Soldiers + Trains = 20\n", "\n", "VARIABLES\n", "Soldiers Continuous\n", "Trains Continuous\n" ] }, { "html": [ "

Objective

" ], "metadata": {}, "output_type": "display_data", "text": [ "" ] }, { "html": [ "
\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
ObjectiveSenseStatusValue
0 OBJ Maximize Optimal 60
\n", "
" ], "metadata": {}, "output_type": "display_data", "text": [ " Objective Sense Status Value\n", "0 OBJ Maximize Optimal 60" ] }, { "html": [ "

Variables

" ], "metadata": {}, "output_type": "display_data", "text": [ "" ] }, { "html": [ "
\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
NameCategoryLower BoundUpper BoundValue
0 Soldiers Continuous 0 None 20
1 Trains Continuous 0 None 0
\n", "
" ], "metadata": {}, "output_type": "display_data", "text": [ " Name Category Lower Bound Upper Bound Value\n", "0 Soldiers Continuous 0 None 20\n", "1 Trains Continuous 0 None 0" ] }, { "html": [ "

Constraints

" ], "metadata": {}, "output_type": "display_data", "text": [ "" ] }, { "html": [ "
\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
NameLower BoundUpper BoundSlackValue
0 Finishing_LaborNaN 100 60 40
1 Carpentry_LaborNaN 80 60 20
2 Soldier_DemandNaN 40 20 20
3 Minimum_Production 20 20 -0 20
\n", "
" ], "metadata": {}, "output_type": "display_data", "text": [ " Name Lower Bound Upper Bound Slack Value\n", "0 Finishing_Labor NaN 100 60 40\n", "1 Carpentry_Labor NaN 80 60 20\n", "2 Soldier_Demand NaN 40 20 20\n", "3 Minimum_Production 20 20 -0 20" ] } ], "prompt_number": 8 }, { "cell_type": "code", "collapsed": false, "input": [ "cdf.plot(title=\"Value\",kind='bar')" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 9, "text": [ "" ] }, { "metadata": {}, "output_type": "display_data", "png": "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\ngg==\n", "text": [ "" ] } ], "prompt_number": 9 } ], "metadata": {} } ] }