{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "*This notebook contains course material from [CBE40455](https://jckantor.github.io/CBE40455) by\n", "Jeffrey Kantor (jeff at nd.edu); the content is available [on Github](https://github.com/jckantor/CBE40455.git).\n", "The text is released under the [CC-BY-NC-ND-4.0 license](https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode),\n", "and code is released under the [MIT license](https://opensource.org/licenses/MIT).*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "< [Critical Path Method](http://nbviewer.jupyter.org/github/jckantor/CBE40455/blob/master/notebooks/04.01-Critical-Path-Method.ipynb) | [Contents](toc.ipynb) | [Job Shop Scheduling](http://nbviewer.jupyter.org/github/jckantor/CBE40455/blob/master/notebooks/04.03-Job-Shop-Scheduling.ipynb) >

\"Open

\"Download\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Machine Bottleneck\n", "\n", "This notebook demonstrates the formulation and solution of the a machine bottleneck problem using Pyomo/GLPK. The task is to schedule a set of jobs on a single machine given the release time, duration, and due time for each job. Date for the example problem is from Christelle Gueret, Christian Prins, Marc Sevaux, \"Applications of Optimization with Xpress-MP,\" Chapter 5, Dash Optimization, 2000." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Example\n", "\n", "The problem is to schedule a sequence of jobs for a single machine. The data consists of a list of jobs. For each job, the data provides the time at which the job is released to the for machine processing, the expected duration of the job, and the due date. The problem is to sequence the jobs on the machine to meet the due dates, or show that no such sequence is possible.\n", "\n", "The following data was presented in the Machine Bottleneck Example from Christelle Gueret, Christian Prins, Marc Sevaux, \"Applications of Optimization with Xpress-MP,\" Chapter 5, Dash Optimization, 2000. " ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "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", " \n", "
ABCDEFG
due1021151051522
duration5684232
release2540089
\n", "
" ], "text/plain": [ " A B C D E F G\n", "due 10 21 15 10 5 15 22\n", "duration 5 6 8 4 2 3 2\n", "release 2 5 4 0 0 8 9" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "JOBS = {\n", " 'A': {'release': 2, 'duration': 5, 'due': 10},\n", " 'B': {'release': 5, 'duration': 6, 'due': 21},\n", " 'C': {'release': 4, 'duration': 8, 'due': 15},\n", " 'D': {'release': 0, 'duration': 4, 'due': 10},\n", " 'E': {'release': 0, 'duration': 2, 'due': 5},\n", " 'F': {'release': 8, 'duration': 3, 'due': 15},\n", " 'G': {'release': 9, 'duration': 2, 'due': 22},\n", "}\n", "\n", "import pandas as pd\n", "from IPython.display import display\n", "display(pd.DataFrame(JOBS))" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "A 2.0\n", "B 5.0\n", "C 4.0\n", "D 0.0\n", "E 0.0\n", "F 8.0\n", "G 9.0\n" ] } ], "source": [ "from pyomo.environ import *\n", "from IPython.display import display\n", "import pandas as pd\n", "\n", "def schedule(JOBS):\n", " \n", " # create model\n", " m = ConcreteModel()\n", " \n", " # index set to simplify notation\n", " J = list(JOBS.keys())\n", "\n", " # decision variables\n", " m.start = Var(J, domain=NonNegativeReals)\n", " m.pastdue = Var(J, domain=NonNegativeReals)\n", " m.early = Var(J, domain=NonNegativeReals)\n", " m.y = Var(J, J, domain=Binary)\n", "\n", " \n", " m.obj = Objective(expr = sum(m.pastdue[j] for j in J))\n", " \n", " m.cons = ConstraintList()\n", " for j in J:\n", " m.cons.add(m.start[j] >= JOBS[j]['release'])\n", " \n", " solver = SolverFactory('glpk')\n", " solver.solve(m)\n", " \n", " for j in J:\n", " print(j, m.start[j]())\n", " \n", "schedule(JOBS)\n", " " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Modeling" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Data \n", "\n", "The data for this problem consists of a list of jobs. Each job is tagged with a unique ID along with numerical data giving the time at which the job will be released for machine processing, the expected duration, and the time at which it is due.\n", "\n", "| Symbol | Description \n", "| ------ | :---------- \n", "| $\\text{ID}_{j}$ | Unique ID for task $j$ \n", "| $\\text{due}_{j}$ | Due time for task $j$ \n", "| $\\text{duration}_{j}$ | Duration of task $j$ \n", "| $\\text{release}_{j}$ | Time task $j$ becomes available for processing " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Decision Variables\n", "\n", "For a single machine, the essential decision variable is the start time at which the job begins processing.\n", "\n", "| Symbol | Description |\n", "| ------ | :---------- |\n", "| $\\text{start}_{j}$ | Start of task $j$\n", "| $\\text{makespan}$ | Time to complete *all* jobs.\n", "| $\\text{pastdue}_{j}$ | Time by which task $j$ is past due\n", "| $\\text{early}_{j}$ | Time by which task $j$ is finished early\n", "\n", "A job cannot start until it is released for processing\n", "\\begin{align*}\n", "\\text{start}_{j} & \\geq \\text{release}_{j}\\\\\n", "\\end{align*}\n", "\n", "Once released for processing, we assume the processing continues until the job is finished. The finish time is compared to the due time, and the result stored in either the early or pastdue decision variables. These decision variables are needed to handle cases where it might not be possible to complete all jobs by the time they are due.\n", "\n", "\\begin{align*}\n", "\\text{start}_{j} + \\text{duration}_{j} + \\text{early}_{j} & = \\text{due}_{j} + \\text{pastdue}_{j}\\\\\n", "\\text{early}_{j} & \\geq 0 \\\\\n", "\\text{pastdue}_{j} & \\geq 0\n", "\\end{align*}\n", "\n", "Finally, we include a single decision variable measuring the overall makespan for all jobs.\n", "\\begin{align*}\n", "\\text{start}_{j} +\\text{duration}_{j} \\leq \\text{makespan}\n", "\\end{align*}\n", "\n", "The final set of constraints requires that, for any given pair of jobs $j$ and $k$, that either $j$ starts before $k$ finishes, or $k$ finishes before $j$ starts. The boolean variable $y_{jk} = 1$ indicates $j$ finishes before $k$ starts, and is 0 for the opposing case. Note that we only need to consider cases $j > k$\n", "\n", "\\begin{align*}\n", "\\text{start}_{i}+\\text{duration}_{i} & \\leq \\text{start}_{j}+My_{i,j}\\\\\n", "\\text{start}_{j}+\\text{duration}_{j} & \\leq \\text{start}_{i}+M(1-y_{i,j})\n", "\\end{align*}\n", "\n", "where $M$ is a sufficiently large enough to assure the relaxed constraint is satisfied for all plausible values of the decision variables." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Pyomo Model" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "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", " \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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
ABCDEFGTotal
due10.021.015.010.05.015.022.0
duration5.06.08.04.02.03.02.030
early0.01.00.04.03.01.00.09
finish11.020.030.06.02.014.022.0
ispastdue1.00.01.00.00.00.00.02
pastdue1.00.015.00.00.00.00.016
release2.05.04.00.00.08.09.0
start6.014.022.02.00.011.020.0
\n", "
" ], "text/plain": [ " A B C D E F G Total\n", "due 10.0 21.0 15.0 10.0 5.0 15.0 22.0 \n", "duration 5.0 6.0 8.0 4.0 2.0 3.0 2.0 30\n", "early 0.0 1.0 0.0 4.0 3.0 1.0 0.0 9\n", "finish 11.0 20.0 30.0 6.0 2.0 14.0 22.0 \n", "ispastdue 1.0 0.0 1.0 0.0 0.0 0.0 0.0 2\n", "pastdue 1.0 0.0 15.0 0.0 0.0 0.0 0.0 16\n", "release 2.0 5.0 4.0 0.0 0.0 8.0 9.0 \n", "start 6.0 14.0 22.0 2.0 0.0 11.0 20.0 " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from pyomo.environ import *\n", "from IPython.display import display\n", "import pandas as pd\n", "\n", "def schedule(JOBS):\n", " \n", " # create model\n", " m = ConcreteModel()\n", " \n", " # index set to simplify notation\n", " J = list(JOBS.keys())\n", "\n", " # decision variables\n", " m.start = Var(J, domain=NonNegativeReals)\n", " m.makespan = Var(domain=NonNegativeReals)\n", " m.pastdue = Var(J, domain=NonNegativeReals)\n", " m.early = Var(J, domain=NonNegativeReals)\n", " \n", " # additional decision variables for use in the objecive\n", " m.ispastdue = Var(J, domain=Binary)\n", " m.maxpastdue = Var(domain=NonNegativeReals)\n", "\n", " # for modeling disjunctive constraints\n", " m.y = Var(J, J, domain=Binary)\n", " BigM = 1000 # max([JOBS[j]['release'] for j in J]) + sum([JOBS[j]['duration'] for j in J])\n", "\n", " m.OBJ = Objective(\n", " expr = sum([m.pastdue[j] for j in J]),\n", " sense = minimize\n", " )\n", "\n", " m.cons = ConstraintList()\n", " for j in J:\n", " m.cons.add(m.start[j] >= JOBS[j]['release'])\n", " m.cons.add(m.start[j] + JOBS[j]['duration'] + m.early[j] == JOBS[j]['due'] + m.pastdue[j])\n", " m.cons.add(m.pastdue[j] <= m.maxpastdue)\n", " m.cons.add(m.start[j] + JOBS[j]['duration'] <= m.makespan)\n", " m.cons.add(m.pastdue[j] <= BigM*m.ispastdue[j])\n", " for k in J:\n", " if j < k:\n", " m.cons.add(m.start[j] + JOBS[j]['duration'] <= m.start[k] + BigM*(1-m.y[j,k]))\n", " m.cons.add(m.start[k] + JOBS[k]['duration'] <= m.start[j] + BigM*(m.y[j,k]))\n", " \n", " SolverFactory('glpk').solve(m)\n", "\n", " for j in J:\n", " JOBS[j]['start'] = m.start[j]()\n", " JOBS[j]['finish'] = m.start[j]() + JOBS[j]['duration']\n", " JOBS[j]['pastdue'] = m.pastdue[j]()\n", " JOBS[j]['early'] = m.early[j]()\n", " JOBS[j]['ispastdue'] = m.ispastdue[j]()\n", " \n", " # display table of results\n", " df = pd.DataFrame(JOBS)\n", " df['Total'] = df.sum(axis=1)\n", " df.loc[['due','finish','release','start'],'Total'] = ''\n", " display(df)\n", " \n", " return JOBS\n", "\n", "JOBS = schedule(JOBS)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Visualization" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import matplotlib.pyplot as plt\n", "%matplotlib inline\n", "\n", "def visualize(JOBS):\n", " plt.figure(figsize=(12,6))\n", " idx = 0\n", " for j in sorted(JOBS.keys()):\n", " idx += 1\n", " plt.plot([JOBS[j]['start'],JOBS[j]['finish']],[idx,idx],\n", " color='red',alpha=1.0,lw=25,solid_capstyle=\"butt\")\n", " plt.plot([JOBS[j]['release'],JOBS[j]['due']],[idx,idx],\n", " color='cyan',alpha=0.6,lw=25,solid_capstyle=\"butt\")\n", " plt.text((JOBS[j]['start'] + JOBS[j]['finish'])/2.0,idx,\n", " 'Job ' + j, color='white', weight='bold',\n", " horizontalalignment='center', verticalalignment='center')\n", "\n", " plt.ylim(plt.ylim()[0]-0.5,plt.ylim()[1]+0.5)\n", " plt.title('Machine Schedule')\n", " plt.xlabel('Time')\n", " plt.ylabel('Job Index')\n", " plt.grid()\n", "\n", "visualize(JOBS)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Multiple Machines\n", "\n", "The case of multiple machines requires a modest extension of model described above. Given a set $M$ of machines, we introduce an additional decision binary variable $z_{j,m}$ indicating if job $j$ has been assigned to machine $m$. The additional constraints\n", "\n", "\\begin{align*}\n", "\\sum_{m\\in M}z_{j,m} = 1\n", "\\end{align*}\n", "\n", "require each job to be assigned to exactly one machine for processing. \n", "\n", "If both jobs $j$ and $k$ have been assigned to machine $m$, then the disjunctive ordering constraints must apply. This logic is equivalent to the following constraints for $j < k$.\n", "\n", "\\begin{align*}\n", "\\text{start}_{i}+\\text{duration}_{i} & \\leq \\text{start}_{j}+M(1 -y_{i,j}) + M(1-z_{j,m}) + M(1-z_{k,m})\\\\\n", "\\text{start}_{j}+\\text{duration}_{j} & \\leq \\text{start}_{i}+M y_{i,j} + M(1-z_{j,m}) + M(1-z_{k,m}))\n", "\\end{align*}" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": true }, "outputs": [], "source": [ "MACHINES = ['A','B']" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " A B C D E F G Total\n", "due 10 21 15 10 5 15 22 \n", "duration 5 6 8 4 2 3 2 30\n", "early 3 8 3 6 3 0 7 30\n", "finish 7 13 12 4 2 15 15 \n", "ispastdue 0 0 0 0 0 0 0 0\n", "machine B B A A B A B \n", "pastdue 0 0 0 0 0 0 0 0\n", "release 2 5 4 0 0 8 9 \n", "start 2 7 4 0 0 12 13 \n" ] } ], "source": [ "from pyomo.environ import *\n", "from IPython.display import display\n", "import pandas as pd\n", "\n", "def schedule_machines(JOBS,MACHINES):\n", " \n", " # create model\n", " m = ConcreteModel()\n", " \n", " # index set to simplify notation\n", " J = list(JOBS.keys())\n", " M = list(MACHINES)\n", "\n", " # decision variables\n", " m.start = Var(J, domain=NonNegativeReals)\n", " m.makespan = Var(domain=NonNegativeReals)\n", " m.pastdue = Var(J, domain=NonNegativeReals)\n", " m.early = Var(J, domain=NonNegativeReals)\n", " \n", " # additional decision variables for use in the objecive\n", " m.ispastdue = Var(J, domain=Binary)\n", " m.maxpastdue = Var(domain=NonNegativeReals)\n", " \n", " # for binary assignment of jobs to machines\n", " m.z = Var(J, M, domain=Binary)\n", "\n", " # for modeling disjunctive constraints\n", " m.y = Var(J, J, domain=Binary)\n", " BigM = max([JOBS[j]['release'] for j in J]) + sum([JOBS[j]['duration'] for j in J])\n", "\n", " m.OBJ = Objective(\n", " expr = \n", " + 1*sum(m.pastdue[j] for j in J)\n", " + 1*m.makespan\n", " - 1*sum(m.early[j] for j in J),\n", " sense = minimize\n", " )\n", "\n", " m.cons = ConstraintList()\n", " for j in J:\n", " m.cons.add(m.start[j] >= JOBS[j]['release'])\n", " m.cons.add(m.start[j] + JOBS[j]['duration'] + m.early[j] == JOBS[j]['due'] + m.pastdue[j])\n", " m.cons.add(m.pastdue[j] <= m.maxpastdue)\n", " m.cons.add(m.start[j] + JOBS[j]['duration'] <= m.makespan)\n", " m.cons.add(m.pastdue[j] <= BigM*m.ispastdue[j])\n", " m.cons.add(sum([m.z[j,mach] for mach in M]) == 1)\n", " for mach in M:\n", " for k in J:\n", " if j < k:\n", " m.cons.add(m.start[j] + JOBS[j]['duration'] \n", " <= m.start[k] + BigM*((1-m.y[j,k]) + (1-m.z[j,mach]) + (1-m.z[k,mach])))\n", " m.cons.add(m.start[k] + JOBS[k]['duration'] \n", " <= m.start[j] + BigM*((m.y[j,k]) + (1-m.z[j,mach]) + (1-m.z[k,mach])))\n", " \n", " SolverFactory('glpk').solve(m)\n", "\n", " for j in J:\n", " JOBS[j]['start'] = m.start[j]()\n", " JOBS[j]['finish'] = m.start[j]() + JOBS[j]['duration']\n", " JOBS[j]['pastdue'] = m.pastdue[j]()\n", " JOBS[j]['early'] = m.early[j]()\n", " JOBS[j]['ispastdue'] = m.ispastdue[j]()\n", " JOBS[j]['machine'] = [mach for mach in MACHINES if m.z[j,mach]][0]\n", " \n", " # display table of results\n", " df = pd.DataFrame(JOBS)\n", " df['Total'] = df.sum(axis=1)\n", " df.loc[['due','finish','release','start','machine'],'Total'] = ''\n", " print(df)\n", " \n", " return JOBS\n", "\n", "JOBS = schedule_machines(JOBS,MACHINES)" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " A B C D E F G Total\n", "due 10 21 15 10 5 15 22 \n", "duration 5 6 8 4 2 3 2 30\n", "early 3 10 3 6 3 4 9 38\n", "finish 7 11 12 4 2 11 13 \n", "ispastdue 0 0 0 0 0 0 0 0\n", "machine 3 2 1 1 2 3 2 \n", "pastdue 0 0 0 0 0 0 0 0\n", "release 2 5 4 0 0 8 9 \n", "start 2 5 4 0 0 8 11 \n" ] }, { "data": { "image/png": "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\nkgwAzGwcsAH4jnPubI/d5UC2c67JzO4D/gvIH0giZrYaWA2Qnp6O3+8fyOGDoqmpKSZxZXjROJFw\naaxIODROJBwaJ7EVVsFsZqMJFstPOeee77m/awHt/k979x8cV3keevz74J8Y2RhbWAbbQk4wbQgN\nLghIiofILSEmgUBCJsG0ack04zsZkikzvbeTm3aazL2Tf3LvzWXahFD3hlBmGjxtAwkhTg1JEIYQ\nCLZRsIFAHLw2kn8oxsggsCIL3vvHu8YrS1qt8a5Wsr+fmZ3dc867532PzrPnPHr3PWdTWhsRt0ZE\nI9AFLCopurA4b4iU0mpgNUBra2tqa2urdBuqpr29nXrUq4nFOFGljBVVwjhRJYyT+qrkLhkBfAt4\nNqX0tRHKzC+WIyIuLq73JeAJYElELI6IqcD1wL3VarwkSZJUa5X0MF8KfArYHBEdxXlfBJoBUkq3\nAR8HPhsRA8AB4PqUUgIGIuJzwDpgEnB7SunpKm+DJEmSVDOjJswppUeAGKXM14Gvj7BsLbD2bbVO\nkiRJqjN/6U+SJEkqw4RZkiRJKsOEWZIkSSrDhFmSJEkqw4RZkiRJKsOEWZIkSSrDhFmSJEkqw4RZ\nkiRJKsOEWZIkSSrDhFmSJEkqw4RZkiRJKsOEWZIkSSrDhFmSJEkqw4RZkiRJKmPUhDkiFkXEgxHx\nTEQ8HRF/NUyZP42IpyJic0Q8GhHnlywrFOd3RMSGam+AJEmSVEuTKygzAPx1SmlTRMwENkbEAyml\nZ0rKbAPen1J6OSKuBFYDl5QsX55S2lu9ZkuSJEljY9SEOaW0C9hVfP1qRDwLLACeKSnzaMlbHgMW\nVrmdkiRJUl0c1RjmiGgB/hB4vEyxvwR+VDKdgB9HxMaIWHW0DZQkSZLqKVJKlRWMaAAeAr6SUrp7\nhDLLgVuBZSmll4rzFqSUuiJiHvAA8PmU0vph3rsKWAXQ1NR04Zo1a97O9hyT3t5eGhoaxrxeTSzG\niSplrKgSxokqYZzUxvLlyzemlFpHK1dRwhwRU4D7gHUppa+NUOY9wD3AlSml50co82WgN6X0v8vV\n19ramjZsGPvrA9vb22lraxvzejWxGCeqlLGiShgnqoRxUhsRUVHCXMldMgL4FvBsmWS5Gbgb+FRp\nshwRpxQvFCQiTgGuALZUtgmSJElS/VVyl4xLgU8BmyOiozjvi0AzQErpNuDvgbnArTm/ZqCYrTcB\n9xTnTQa+k1L6z6pugSRJklRDldwl4xEgRinzGeAzw8x/ATh/6DskSZKkicFf+pMkSZLKMGGWJEmS\nyjb6M+wAABl4SURBVDBhliRJksowYZYkSZLKMGGWJEmSyjBhliRJksowYZYkSZLKMGGWJEmSyjBh\nliRJksowYZYkSZLKGPWnsU8kHR2zaW+vdytqo60tP3Tsjuc4UXUVCi1jFiuzZ0NPz9jUVQ8ewyTV\nkz3MkiRJUhn2MB/hy1+G7m649daRy1x7LSxdCqtXw86d5dd3882556fUbbfB7t3H3NQhFi6ED34Q\nTj89T7/wAtx3H7z+evXr0vGtlp+Dvr4c/z/8Ifz2t1Vr8iAXXADvfS+cdlqu76mn4IEHalPXeFLt\n/QbQ2goXXQRz58LBg3mfPfggbNtWtWYP8qEPwTnnQEMD7N8P7e2weXNt6pKkSo3awxwRiyLiwYh4\nJiKejoi/GqZMRMQ/RMTWiHgqIi4oWbYiIp4rLvtCtTdgIhgYgP/4j8OPWn1tOnduTo4feAB+/Ws4\n91z4wAdqU5d0tA59Dn7xC2hpqW1snnkmbN8OP/oRvPIKXHopnH9+7eo7XrW1wVVXwdSp8JOf5ES5\ntzf/c14rZ54JHR2wbh1Mnw4f/Wj+x0eS6qmSHuYB4K9TSpsiYiawMSIeSCk9U1LmSmBJ8XEJ8E3g\nkoiYBHwD+ADQCTwREfce8d5xadIkuPxyOO88mDIFCgVYuzaffA857zy44Ybc63LvvSP3uLz5Zu7t\nPaSvb2iZWbNyz8pZZ+XE4umnc+L7xhu5d27GDNi0KZ/09+2Du+7KJ65SW7bAL3+ZX2/eDH/wBzBv\n3jH9GXSCq8XnoLcXLrsMpk0bvlxzM1xxRY7d3l742c9g48bcQ33zzfDii/kz1NwMzz8P3/3u0HX8\n6Ef5swN5HTfccGJ9Fqqx36ZMyf9oDAzAHXfk3l7I//BMHuHMccEF+T0zZ+ae6HXrYMeO3KN97bX5\nuHT66XDqqbnn+PHHh67j298+vO9OOw3+6I+gsfEY/yCSdIxG7WFOKe1KKW0qvn4VeBZYcESxa4A7\nU/YYMDsizgAuBramlF5IKfUDa4plx73LLoP3vQ9+8xt45JH8FeF11w0us2ABPPRQTmY/9rF8khrO\n1KnwN39z+DGc667LdfzsZ7B1a/46+bLLBq9j2rScICxYkE9MRzp0kgE4++z8vH175dssHakWn4Mb\nb8zJ809/OrTMySfDypU5Ob7/fnjtNbj6ali8eHB927fD3r35n8Lm5qHrOdE/C9XYb/Pm5aR5796c\nLEfksjNmDL+PFy+Gj3wkf8u1bl1OileuzPu0tMzGjfn15ZcPv55D++6kk3L5/v7Kho5IUi0d1UV/\nEdEC/CFwZL/AAuDFkunO4ryR5o97S5bkk/oPfpBPOJ2d+cQ8derhMu3t8MQT8Ktf5R6VkXpBDh6E\nO+88/DjS1Km5Z7mzM9d133257kMnesjTP/xhrg+GjosutWgRXHNNPsl4Nwcdi1p8Du65JydFy5cP\nLbNoUU6wNm2CDRvyEAAY/Fno6sptefbZPF3us3DJJXDxxXldzz9/VJs+oVVjv6U0+Lmp6fA//StX\nDl8n5H22cWPehyefPHj4xpNP5jq7unIyfsopw7f/pJNyEj9/ft6G1157W38GSaqaii/6i4gG4LvA\nzSmlV0Yrf7QiYhWwCqCpqYn2OmR6Bw7k74j7+/s5eDABU9m+fTsDA9DXN5+I6Wzfvp3e3rlAA3v2\n7KZQ6OO11xqBBrq6uujsPDhonQMDC3njjZP46U93jFjvtGkBnEVfXx+Fwu7i151n8bvf9VMo7GJg\nYCH9/SexdesOBgamAmfy+uu9FAp7h6zrnHOmccMNTezefZCvfnUPr732JgAdHT3AcXzPqTF04MA0\nCoVCvZtRYy01/Ry85z1n8M53TmPnzu3096e3yp166slAEz09PRQKPcyYMR2Yzyuv7Kez81VgIS+9\n9DqFQje///uzgDns27eXQuGI8UnAihWzuPLKOTzySC/f+tbetxK/sdTf3z9msdLQMACcXbX9tnNn\ncOONi2hsDHp6Otm1603+8R9P5vOfn/fWsarU/v2nAaeye3deb0/PbGA23d17mDlzEtBIV9c+CoVX\n6O2dB8xg585O9u4dGLSeSZPgs589nXPPncG3v/0S69fnfXs8H8N6e3vrcs7TxGKc1FdFCXNETCEn\ny/+aUrp7mCJdwKKS6YXFeVNGmD9ESmk1sBqgtbU1tdXhhpsdHR0ATJ06la1b89eBN93Uwt69uYer\nUIAzzjiLhoZc/pOfnM+WLXDhhfDqqzBt2gJaWgavc/Lk/LjqqsMLtm/P5UsVCnD22dP5sz9rYc6c\n3MOyY8c0WlpamDw5T7e0tHDmmbl8Q0MDLS0Ng9Zxxhnw6U/nr063bJnG8uXN9PfnnrWlS72HabV0\ndHTQcuSOPg7V6nMwc2a+6K+3F84886xB5fr74cABWL58NhGz37pQb8+eU1m48FQAZsyYQUtL/pwA\nNDY20tIyuHu0tTVfrLZvH+zZ08CHP9zAyy/nns2xVCgUxixWDvW0V3O/PfpoPm783d8t4he/yMch\ngOnTpw/Zru7u/Hz99fP55S/z+w4cgP7+prd6r+fMmUNLyxxmzMjTCxcufKs9h1x3XR5q8/zzMGtW\nI1dd1Uhn5/F9DGtvb6ce5zxNLMZJfY2aMEdEAN8Cnk0pfW2EYvcCn4uINeSL/vanlHZFxG+BJRGx\nmJwoXw/cUJ2mV9+0aXlAXV8fPPxwvkL73e+Gd70rH7zXrh1cvqsLli3LJ4Xvf3/wuMlSkyfDxz9+\neHrNmvw1aKm7784X/S1blr+6fuyx3Iaj0dR0+CvXD384P/f0nFhfRevYHRpzWqvPwcGDObm6//6h\nZQ4cyBe0XnFFvkVib2/+Sr5QKD/04kiHhgHMmXN47G5Hx9gnzGPp0EWU1dxv7e15TPJFF+V98rvf\n5YsDN2wYWnbbtnzx4KWX5n136KK/AweObjsWFbtYzjknPwC+972jW4ckVVukUb6njIhlwMPAZuDN\n4uwvAs0AKaXbikn114EVwOvAp1NKG4rv/xBwCzAJuD2l9JXRGtXa2po2DHdErrHOzl4WLmzgkUfg\nxz8e8+pryl/Jqp5bbumgp2dpvZtRE4sX5zg56yyOy8/BWBurHubFi+FP/iT/o3C87rfj+Rhmz6Eq\nYZzURkRsTCm1jlZu1B7mlNIjQIxSJgE3jbBsLbB2uGXjzZw503jySVi/vt4tkeqjuTlf/OXnYGJp\nbs696e43SaoNf+mvxOrVTx+3PYdSJR56KD80sTz0UL4He61+FEmSTnRHdVs5SZIk6URjwixJkiSV\n4ZCMEkuX9hy3F5WoeowTVaq9vUBbW0u9myFJOkb2MEuSJEllmDBLkiRJZZgwS5IkSWWYMEuSJEll\nmDBLkiRJZZgwS5IkSWWYMEuSJEllmDBLkiRJZZgwS5IkSWWYMEuSJElljPrT2BFxO3AV0J1SOm+Y\n5f8N+NOS9b0LOD2ltC8iCsCrwBvAQEqptVoNlyRJksZCJT3MdwArRlqYUvpfKaWlKaWlwH8HHkop\n7Sspsry43GRZkiRJE86oCXNKaT2wb7RyRSuBu46pRZIkSdI4Eiml0QtFtAD3DTcko6TMDKATOPtQ\nD3NEbAP2k4dk/FNKaXWZ968CVgE0NTVduGbNmsq3okp6e3tpaGgY83o1sRgnqpSxokoYJ6qEcVIb\ny5cv31jJKIhRxzAfhauBnx0xHGNZSqkrIuYBD0TEr4o91kMUk+nVAK2tramtra2KTatMe3s79ahX\nE4txokoZK6qEcaJKGCf1Vc27ZFzPEcMxUkpdxedu4B7g4irWJ0mSJNVcVRLmiDgVeD/w/ZJ5p0TE\nzEOvgSuALdWoT5IkSRorldxW7i6gDWiMiE7gS8AUgJTSbcViHwXuTym9VvLWJuCeiDhUz3dSSv9Z\nvaZLkiRJtTdqwpxSWllBmTvIt58rnfcCcP7bbZgkSZI0HvhLf5IkSVIZJsySJElSGSbMkiRJUhkm\nzJIkSVIZJsySJElSGSbMkiRJUhkmzJIkSVIZJsySJElSGSbMkiRJUhkmzJIkSVIZJsySJElSGSbM\nkiRJUhkmzJIkSVIZoybMEXF7RHRHxJYRlrdFxP6I6Cg+/r5k2YqIeC4itkbEF6rZcEmSJGksVNLD\nfAewYpQyD6eUlhYf/wMgIiYB3wCuBM4FVkbEucfSWEmSJGmsjZowp5TWA/vexrovBramlF5IKfUD\na4Br3sZ6JEmSpLqp1hjmP4qIpyLiRxHx7uK8BcCLJWU6i/MkSZKkCWNyFdaxCWhOKfVGxIeA7wFL\njnYlEbEKWAXQ1NREe3t7FZp2dHp7e+tSryYW40SVMlZUCeNElTBO6uuYE+aU0islr9dGxK0R0Qh0\nAYtKii4szhtpPauB1QCtra2pra3tWJt21Nrb26lHvZpYjBNVylhRJYwTVcI4qa9jHpIREfMjIoqv\nLy6u8yXgCWBJRCyOiKnA9cC9x1qfJEmSNJZG7WGOiLuANqAxIjqBLwFTAFJKtwEfBz4bEQPAAeD6\nlFICBiLic8A6YBJwe0rp6ZpshSRJklQjoybMKaWVoyz/OvD1EZatBda+vaZJkiRJ9ecv/UmSJEll\nmDBLkiRJZZgwS5IkSWWYMEuSJEllmDBLkiRJZZgwS5IkSWWYMEuSJEllmDBLkiRJZZgwS5IkSWWY\nMEuSJEllmDBLkiRJZZgwS5IkSWWYMEuSJEllmDBLkiRJZYyaMEfE7RHRHRFbRlj+pxHxVERsjohH\nI+L8kmWF4vyOiNhQzYZLkiRJY6GSHuY7gBVllm8D3p9S+gPgfwKrj1i+PKW0NKXU+vaaKEmSJNXP\n5NEKpJTWR0RLmeWPlkw+Biw89mZJkiRJ40OklEYvlBPm+1JK541S7r8Cv59S+kxxehuwH3gD+KeU\n0pG9z6XvXQWsAmhqarpwzZo1FW5C9fT29tLQ0DDm9WpiMU5UKWNFlTBOVAnjpDaWL1++sZJREKP2\nMFcqIpYDfwksK5m9LKXUFRHzgAci4lcppfXDvb+YTK8GaG1tTW1tbdVqWsXa29upR72aWIwTVcpY\nUSWME1XCOKmvqtwlIyLeA/w/4JqU0kuH5qeUuorP3cA9wMXVqE+SJEkaK8ecMEdEM3A38KmU0vMl\n80+JiJmHXgNXAMPeaUOSJEkar0YdkhERdwFtQGNEdAJfAqYApJRuA/4emAvcGhEAA8WxIE3APcV5\nk4HvpJT+swbbIEmSJNVMJXfJWDnK8s8Anxlm/gvA+UPfIUmSJE0c/tKfJEmSVIYJsyRJklSGCbMk\nSZJUhgmzJEmSVIYJsyRJklSGCbMkSZJUhgmzJEmSVIYJsyRJklSGCbMkSZJUhgmzJEmSVMaoP419\nIunomE17e71bobdj9mzo6RmbugqFFuNEFTmeY6WtLT8k6URgD7MkSZJUhj3MdfTlL0N3N9x668hl\nrr0Wli6F1ath587y67v55tzT+sYb0NcHu3fDww9DoVDNVg929tnwgQ/AvHkQAV/9Krz+eu3q09io\ndmwCXHQRXHwxnHYaHDgAzz0H991XtSYPcsYZcPXVMH8+nHRS5W2UJGk4o/YwR8TtEdEdEVtGWB4R\n8Q8RsTUinoqIC0qWrYiI54rLvlDNhmt4AwPwve/Bpk2waBH8+Z/D4sW1q2/KFNi+Hfbtq10dmvja\n2uDDH87J6/33w89/Dk1NtatvypT8D+OuXbWrQ5J04qikh/kO4OvAnSMsvxJYUnxcAnwTuCQiJgHf\nAD4AdAJPRMS9KaVnjrXRx5tJk+Dyy+G88/KJvlCAtWvhlVcOlznvPLjhBjh4EO69F7ZtG35db74J\nmzfn193dcN11OVkZrvwFF8Cll8LMmfDb38K6dbBjR+41vPbavJ7TT4dTT4X2dnj88aHrePbZ/Ljx\nRpg799j+Dhp/qhGbU6bkOBsYgDvvhP378/yf/3z4On/v9+CP/zj3RPf0wE9+knujW1pynD3/PEyf\nnr/V2LgRHnhg6Dp27MiPa6+FBQuq8IeQJJ3QRu1hTimtB8r1H14D3Jmyx4DZEXEGcDGwNaX0Qkqp\nH1hTLKsjXHYZvO998JvfwCOPwDnn5ES31IIF8NBDMGMGfOxjOZEZza9/nZ/PPHPossWL4SMfycMn\n1q3LSfHKlXDyyYPLbNyYX19+eWV16vhSjdicNy8nzXv3Hk6WAVIaWt/cufCJT+R1rFuXe6Q/8YnB\n/4y1tMDTT+dhHZdemmNXkqRaqsZFfwuAF0umO4vzRpqvIyxZknuGf/CDnJR0dkJzM0yderhMezs8\n8QT86le5R7ixcfT1RuTn4RKTJUvy84MP5qR406acLC9ceLjMk0/mOru6csJzyilvexM1QVUjNoeL\nv5G88505WX700RyXjz6ap9/xjsNlnnsuf9vxm9/kaRNmSVKtjZuL/iJiFbAKoKmpifY63IvpwIFp\nFGp5hdwQLfT393PwYAKmsn37dgYGoK9vPhHT2b59O729c4EG9uzZTaHQx2uvNQINdHV10dl5cNDa\nBgYW8uabJ1Eo7ADgkktOAU5n27Y+CoXdg8ru338acCq7d+f19vTMBmbT3b2HmTMnAY10de2jUHiF\n3t55wAx27uxk796BYbekr28+MJ0dO3bQ2/tmVf9KlWhoGKC3d2zCub+/f4zjpB6qF5s7dwY33riI\nuXOD/fs7efnlN4D8D92RyfQ73zkTmMvevXspFHppbm4AGtm37yV27z4IzGfPnlcoFPbR0zMHmEV3\nd65/OL29uU07d+6kUOiv+l9pNMdzrHR09ABjdC/H41xvb29dznmaWIyT+qpGhtEFLCqZXlicN2WE\n+cNKKa0GVgO0tramtjrc4LOjo4OWlpYxrXPq1Kls3ZqHP9x0Uwt79+Y7TxQKcMYZZ9HQkMt98pPz\n2bIFLrwQXn0Vpk1bwJFNnTw5P66+uoXTT4f3vjf3Dj722PQh29XdnZ+vv34+v/xlHud84AD09ze9\n1UM4Z84cWlrmMGNGnl64cOFb7Tlkzpz8Ffmh91x1VTMvvXR4OMhYGdv7MBfGPE7qoZqx+eijOcb+\n9m8X8fjjOU7f9S64/fbB5Xp68l1err66kcbGRt73vjzd0zOX+fNzmVmzZtHSMotZs/L0/Pnz6Tsi\nX25oyMNHmpvz9PLlZ9LdnYdyjKXjOVaWLvU+zNXS3t5OPc55mliMk/qqRsJ8L/C5iFhDvuhvf0pp\nV0T8FlgSEYvJifL1wA1VqO+4cGiscF9fvvXb9Onw7nfnJOL55/OFVaW6umDZspzUfv/7OYkYzuTJ\n+UKnvj548UVYvz7fxeJI27blC7QuvRQ++MHDF/0dOHB029HcnMdCH7JiRU6oxjphVvXUIjbb2/N4\n+YsuyjFy6LZyR3rpJfi3f8sX/V15Jbz8Mvz7v+f5M2dWvg2NjYPj8v3vz8n4WCfMkqTjQ6RRBhhG\nxF1AG9AI7AG+RO49JqV0W0QE+S4aK4DXgU+nlDYU3/sh4BZgEnB7SukrlTSqtbU1bdiw4e1szzG5\n5ZYOenqW1ryexYtzz8xZZ+VxoT/+cc2rPO7Zw1wdxmZ1Hc+x4i/9VY89h6qEcVIbEbExpdQ6WrlR\ne5hTSitHWZ6Am0ZYthZYO9yyE1lzc+4Be/LJ3AMsjRfGpiRJQ42bi/5OJA89lB/SeGNsSpI0VDVu\nKydJkiQdt0yYJUmSpDIcklFi6dIeL2LRqNrbC7S1tdS7GZoAjBVJOj6MepeMeijekm6Ym6HVXCOw\ntw71amIxTlQpY0WVME5UCeOkNs5KKZ0+WqFxmTDXS0RsqOTWIjqxGSeqlLGiShgnqoRxUl+OYZYk\nSZLKMGGWJEmSyjBhHmx1vRugCcE4UaWMFVXCOFEljJM6cgyzJEmSVIY9zJIkSVIZJsxARKyIiOci\nYmtEfKHe7dH4FRGFiNgcER0RsaHe7dH4EBG3R0R3RGwpmTcnIh6IiF8Xn0+rZxs1PowQK1+OiK7i\ncaUjIj5Uzzaq/iJiUUQ8GBHPRMTTEfFXxfkeV+rkhE+YI2IS8A3gSuBcYGVEnFvfVmmcW55SWurt\nfVTiDmDFEfO+APwkpbQE+ElxWrqDobEC8H+Lx5WlKaW1Y9wmjT8DwF+nlM4F3gvcVMxNPK7UyQmf\nMAMXA1tTSi+klPqBNcA1dW6TpAkkpbQe2HfE7GuAfym+/hfg2jFtlMalEWJFGiSltCultKn4+lXg\nWWABHlfqxoQ5B+CLJdOdxXnScBLw44jYGBGr6t0YjWtNKaVdxde7gaZ6Nkbj3ucj4qnikA2/Ztdb\nIqIF+EPgcTyu1I0Js3R0lqWUlpKH8NwUEZfVu0Ea/1K+HZG3JNJIvgm8A1gK7AL+T32bo/EiIhqA\n7wI3p5ReKV3mcWVsmTBDF7CoZHphcZ40REqpq/jcDdxDHtIjDWdPRJwBUHzurnN7NE6llPaklN5I\nKb0J/DMeVwRExBRysvyvKaW7i7M9rtSJCTM8ASyJiMURMRW4Hri3zm3SOBQRp0TEzEOvgSuALeXf\npRPYvcBfFF//BfD9OrZF49ihBKjoo3hcOeFFRADfAp5NKX2tZJHHlTrxh0uA4i18bgEmAbenlL5S\n5yZpHIqId5B7lQEmA98xVgQQEXcBbUAjsAf4EvA94N+AZmA78ImUkhd7neBGiJU28nCMBBSA/1Iy\nTlUnoIhYBjwMbAbeLM7+Inkcs8eVOjBhliRJkspwSIYkSZJUhgmzJEmSVIYJsyRJklSGCbMkSZJU\nhgmzJEmSVIYJsyRJklSGCbMkSZJUhgmzJEmSVMb/By0zwnmcIFn0AAAAAElFTkSuQmCC\n", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import matplotlib.pyplot as plt\n", "%matplotlib inline\n", "\n", "def visualize_machines(JOBS,MACHINES):\n", " plt.figure(figsize=(12,8))\n", " plt.subplot(2,1,1)\n", " idx = 0\n", " for j in sorted(JOBS.keys()):\n", " idx += 1\n", " plt.plot([JOBS[j]['start'],JOBS[j]['finish']],[idx,idx],\n", " color='red',alpha=1.0,lw=15,solid_capstyle=\"butt\")\n", " plt.plot([JOBS[j]['release'],JOBS[j]['due']],[idx,idx],\n", " color='cyan',alpha=0.6,lw=15,solid_capstyle=\"butt\")\n", " txt = 'Job '+j+' on '+JOBS[j]['machine']\n", " plt.text((JOBS[j]['start'] + JOBS[j]['finish'])/2.0,idx,\n", " txt, color='white', weight='bold',\n", " horizontalalignment='center', verticalalignment='center')\n", "\n", " plt.ylim(plt.ylim()[0]-0.5,plt.ylim()[1]+0.5)\n", " xmin,xmax = plt.xlim()\n", " plt.title('Machine Schedule')\n", " plt.xlabel('Time')\n", " plt.ylabel('Job Index')\n", " plt.grid()\n", " \n", " plt.subplot(2,1,2)\n", " idx = 0\n", " for m in sorted(MACHINES):\n", " idx += 1\n", " color = 'blue'\n", " for j in sorted(JOBS.keys()):\n", " if m == JOBS[j]['machine']:\n", " plt.plot([JOBS[j]['start'],JOBS[j]['finish']],[idx,idx],\n", " color=color,alpha=0.5,lw=15,solid_capstyle=\"butt\")\n", " txt = 'Job '+j+' on '+JOBS[j]['machine']\n", " plt.text((JOBS[j]['start'] + JOBS[j]['finish'])/2.0,idx,\n", " txt, color='white', weight='bold',\n", " horizontalalignment='center', verticalalignment='center')\n", " plt.xlim(xmin,xmax)\n", " plt.grid() \n", "\n", "MACHINES = ['1','2','3']\n", "JOBS = schedule_machines(JOBS,MACHINES)\n", "visualize_machines(JOBS,MACHINES)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "< [Critical Path Method](http://nbviewer.jupyter.org/github/jckantor/CBE40455/blob/master/notebooks/04.01-Critical-Path-Method.ipynb) | [Contents](toc.ipynb) | [Job Shop Scheduling](http://nbviewer.jupyter.org/github/jckantor/CBE40455/blob/master/notebooks/04.03-Job-Shop-Scheduling.ipynb) >

\"Open

\"Download\"" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.3" } }, "nbformat": 4, "nbformat_minor": 2 }