diff --git a/TODOS.txt b/TODOS.txt new file mode 100644 index 0000000..6dfd1ec --- /dev/null +++ b/TODOS.txt @@ -0,0 +1,2 @@ +distance matrix does not have 0 on diagonals; this is problematic for us. + diff --git a/model.ipynb b/model.ipynb new file mode 100644 index 0000000..147b9c1 --- /dev/null +++ b/model.ipynb @@ -0,0 +1,698 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Lost Luggage Distribution Problem\n", + "\n", + "## Objective and Prerequisites\n", + "\n", + "In this example, you’ll learn how to use mathematical optimization to solve a vehicle routing problem with time windows, which involves helping a company figure out the minimum number of trucks required to deliver pieces of lost or delayed baggage to their rightful owners and determining the optimal assignment of trucks to customers.\n", + "\n", + "This model is example 27 from the fifth edition of Model Building in Mathematical Programming by H. Paul Williams on pages 287-289 and 343-344.\n", + "\n", + "This modeling example is at the advanced level, where we assume that you know Python and the Gurobi Python API and that you have advanced knowledge of building mathematical optimization models. Typically, the objective function and/or constraints of these examples are complex or require advanced features of the Gurobi Python API.\n", + "\n", + "**Download the Repository**
\n", + "You can download the repository containing this and other examples by clicking [here](https://github.com/Gurobi/modeling-examples/archive/master.zip). \n", + "\n", + "**Gurobi License**
\n", + "In order to run this Jupyter Notebook properly, you must have a Gurobi license. If you do not have one, you can request an [evaluation license](https://www.gurobi.com/downloads/request-an-evaluation-license/?utm_source=3PW&utm_medium=OT&utm_campaign=WW-MU-MUI-OR-O_LEA-PR_NO-Q3_FY20_WW_JPME_Lost_Luggage_Distribution_COM_EVAL_GitHub&utm_term=Lost%20Luggage%20Distribution&utm_content=C_JPM) as a *commercial user*, or download a [free license](https://www.gurobi.com/academia/academic-program-and-licenses/?utm_source=3PW&utm_medium=OT&utm_campaign=WW-MU-EDU-OR-O_LEA-PR_NO-Q3_FY20_WW_JPME_Lost_Luggage_Distribution_COM_EVAL_GitHub&utm_term=Lost%20Luggage%20Distribution&utm_content=C_JPM) as an *academic user*." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Problem Description\n", + "\n", + "A small company with six trucks has a contract with a number of airlines to pick up lost or delayed baggage, belonging to customers in the London area, from Heathrow airport at 6 p.m. each evening. The contract stipulates that each customer must have their baggage delivered by 8 p.m. The company requires a model to advise them what is the minimum number of trucks they need to use and to which customers each van should deliver and in what order. There is no practical capacity limitation on each van. Each van can hold all baggage that needs to be delivered in a two-hour period. To solve this problem, we can formulate an optimization model that minimizes the number of trucks that need to be used.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Model Formulation\n", + "\n", + "\n", + "### Sets and Indices\n", + "\n", + "$i,j \\in \\text{Customers} \\equiv L=\\{0,1..(n-1)\\}$: Set of locations where $0$ is the depot, and $n$ is the number of customers.\n", + "\n", + "$k \\in \\text{Trucks} \\equiv V=\\{0..K-1\\}$: Index and set of trucks, where $K$ is the number of trucks.\n", + "\n", + "### Parameters\n", + "\n", + "$d_{i,j} \\in \\mathbb{R}^+$: Distance (total service time) from customer $i$ to customer $j$.\n", + "\n", + "### Decision Variables\n", + "\n", + "$x_{i,j,k} \\in \\{0,1 \\}$: This binary variable is equal 1, if truck $k$ visits and goes directly from location $i$ to location $j$, and zero otherwise.\n", + "\n", + "$y_{i,k} \\in \\{0,1 \\}$: This binary variable is equal 1, if truck $k$ visits location $i$, and zero otherwise.\n", + "\n", + "$z_{k} \\in \\{0,1 \\}$: This binary variable is equal 1, if truck $k \\in \\{1,2..K\\}$ is used, and zero otherwise.\n", + "\n", + "### Objective Function\n", + "\n", + "**Number of trucks**: Minimize number of trucks used.\n", + "\n", + "\\begin{equation}\n", + "\\text{Minimize} \\quad \\sum_{k = 1}^{K} z_k\n", + "\\end{equation}\n", + "\n", + "### Constraints\n", + "\n", + "**truck utilization**: For all locations different from the depot, i.e. $i > 0$, if the location is visited by truck $k$, then it is used.\n", + "\n", + "\\begin{equation}\n", + "y_{i,k} \\leq z_{k} \\quad \\forall i \\in L \\setminus \\{0\\}, \\; k \\in V\n", + "\\end{equation}\n", + "\n", + "\n", + "**Visit all customers**: Each customer location is visited by exactly one truck.\n", + "\n", + "\\begin{equation}\n", + "\\sum_{k \\in V} y_{i,k} = 1 \\quad \\forall i \\in L \\setminus \\{0\\}\n", + "\\end{equation}\n", + "\n", + "**Depot**: Depot is visited by every truck used. \n", + "\n", + "\n", + "\n", + "$$\\sum{y_{0k}}=K$$\n", + "\n", + "**Arriving at a location**: If location $j$ is visited by truck $k$, then the truck is coming from another location $i$.\n", + "\n", + "\\begin{equation}\n", + "\\sum_{i \\in L} x_{i,j,k} = y_{j,k} \\quad \\forall j \\in L, \\; k \\in V\n", + "\\end{equation}\n", + "\n", + "**Leaving a location**: If truck $k$ leaves location $j$, then the truck is going to another location $i$.\n", + "\n", + "\\begin{equation}\n", + "\\sum_{i \\in L} x_{j,i,k} = y_{j,k} \\quad \\forall j \\in L, \\; k \\in V\n", + "\\end{equation}\n", + "\n", + "**Breaking symmetry**: **I GUESS** this is to minimize number of trucks used. we want to utilize previous trucks as much as possible before adding new ones\n", + "\n", + "\\begin{equation}\n", + "\\sum_{i \\in L} y_{i,k} \\geq \\sum_{i \\in L} y_{i,k+1} \\quad \\forall k \\in \\{0..K-1\\}\n", + "\\end{equation}\n", + "\n", + "**Time windows**: Keep track of opening and closing times for each location.\n", + "$$O_i \\leq s_i$$\n", + "\n", + "$$s_i \\leq C_i$$\n", + "\n", + "**Timeline**: keep track of time to avoid subtours via big $M$ and either-or\n", + "$$s_i+d_{ij}-s_j \\leq M(1-x_{ijk})$$\n", + "\n", + "**max time contraint**:\n", + "$$C_N = T_{max}$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Python Implementation\n", + "\n", + "We import the Gurobi Python Module and other Python libraries." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "import math\n", + "import random\n", + "from itertools import permutations\n", + "import gurobipy as gp\n", + "from gurobipy import GRB\n", + "\n", + "# tested with Python 3.7.0 & Gurobi 9.1.0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Input data \n", + "We define all the input data for the model. The user defines the number of locations, including the depot, and the number of trucks. We randomly determine the coordinates of each location and then calculate the Euclidean distance between each pair of locations. We assume a speed of 60 km/hr, which is 1 km/min. Hence travel time is equal to the distance." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# number of locations, including the depot. The index of the depot is 0\n", + "n = 17\n", + "locations = [*range(n)]\n", + "\n", + "# number of trucks\n", + "K = 6\n", + "trucks = [*range(K)]\n", + "\n", + "# Create n random points\n", + "# Depot is located at (0,0) coordinates\n", + "random.seed(1)\n", + "points = [(0, 0)]\n", + "points += [(random.randint(0, 50), random.randint(0, 50)) for i in range(n-1)]\n", + "\n", + "# Dictionary of Euclidean distance between each pair of points\n", + "# Assume a speed of 60 km/hr, which is 1 km/min. Hence travel time = distance\n", + "time = {(i, j):\n", + " math.sqrt(sum((points[i][k]-points[j][k])**2 for k in range(2)))\n", + " for i in locations for j in locations if i != j}\n", + "\n", + "# big M\n", + "M = 1000 #TODO: to be changes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Model Deployment\n", + "\n", + "We create a model and the variables. The decision variables determines the order in which each van visits a subset of custormers, which customer is visited by each van, and if a van is used or not." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Set parameter Username\n", + "Academic license - for non-commercial use only - expires 2022-06-17\n" + ] + } + ], + "source": [ + "m = gp.Model('lost_luggage_distribution.lp')\n", + "\n", + "# Create variables: \n", + "\n", + "# x =1, if van k visits and goes directly from location i to location j \n", + "x = m.addVars(time.keys(), trucks, vtype=GRB.BINARY, name='FromToBy')\n", + "\n", + "# y = 1, if customer i is visited by van k\n", + "y = m.addVars(locations, trucks, vtype=GRB.BINARY, name='visitBy')\n", + "\n", + "# Number of trucks used is a decision variable\n", + "z = m.addVars(trucks, vtype=GRB.BINARY, name='used')\n", + "\n", + "# Travel time per van\n", + "t = m.addVars(trucks, ub=120, name='travelTime')\n", + "\n", + "# Maximum travel time\n", + "T = m.addVar(name='maxTravelTime')\n", + "\n", + "# Opening times o_i\n", + "o = m.addVars(locations, name='openingTime')\n", + "\n", + "# Closing times\n", + "c = m.addVars(locations, name='closingTime')\n", + "\n", + "# Service times s_i\n", + "serviceTime = m.addVars(locations, name='serviceTime')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Constraints\n", + "\n", + "For all locations different from depot, i.e. $i > 0$, if the location is visited by van $k$, then it is used." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# Truck utilization constraint\n", + "\n", + "visitCustomer = m.addConstrs((y[i,k] <= z[k] for k in trucks for i in locations if i > 0), name='visitCustomer' )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "No van travels for more than 120 min. We make a small change from the original H.P. Williams version to introduce a slack variable for the travel time for each van, t[k]." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# Travel time constraint\n", + "# Exclude the time to return to the depot\n", + "\n", + "travelTime = m.addConstrs((gp.quicksum(time[i,j]*x[i,j,k] for i,j in time.keys() if j > 0) == t[k] for k in trucks), \n", + " name='travelTimeConstr' )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Each customer location is visited by exactly one van" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# Visit all customers\n", + "visitAll = m.addConstrs((y.sum(i,'*') == 1 for i in locations if i > 0), name='visitAll' )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Heathrow (depot) is visited by every van used." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# Depot constraint\n", + "depotConstr = m.addConstr(y.sum(0,'*') >= z.sum(), name='depotConstr' )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If location j is visited by van k , then the van is coming from another location i." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "# Arriving at a customer location constraint\n", + "ArriveConstr = m.addConstrs((x.sum('*',j,k) == y[j,k] for j,k in y.keys()), name='ArriveConstr' )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " If van k leaves location j , then the van is going to another location i." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# Leaving a customer location constraint\n", + "LeaveConstr = m.addConstrs((x.sum(j,'*',k) == y[j,k] for j,k in y.keys()), name='LeaveConstr' )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Breaking symmetry constraints." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "breakSymm = m.addConstrs((y.sum('*',k-1) >= y.sum('*',k) for k in trucks if k>0), name='breakSymm' )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Relate the maximum travel time to the travel times of each van" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "maxTravelTime = m.addConstrs((t[k] <= T for k in trucks), name='maxTravelTimeConstr')\n", + "\n", + "# Alternately, as a general constraint:\n", + "# maxTravelTime = m.addConstr(s == gp.max_(t), name='maxTravelTimeConstr')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Objective Function\n", + "We use two hierarchical objectives:\n", + "- First, minimize the number of trucks used\n", + "- Then, minimize the maximum of the time limit constraints" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "m.ModelSense = GRB.MINIMIZE\n", + "m.setObjectiveN(z.sum(), 0, priority=1, name=\"Number of trucks\")\n", + "m.setObjectiveN(T, 1, priority=0, name=\"Travel time\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Callback Definition\n", + "Subtour constraints prevent a van from visiting a set of destinations without starting or ending at the Heathrow depot. Because there are an exponential number of these constraints, we don't want to add them all to the model. Instead, we use a callback function to find violated subtour constraints and add them to the model as lazy constraints." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "# Callback - use lazy constraints to eliminate sub-tours\n", + "def subtourelim(model, where):\n", + " if where == GRB.Callback.MIPSOL:\n", + " # make a list of edges selected in the solution\n", + " vals = model.cbGetSolution(model._x)\n", + " selected = gp.tuplelist((i,j) for i, j, k in model._x.keys()\n", + " if vals[i, j, k] > 0.5)\n", + " # find the shortest cycle in the selected edge list\n", + " tour = subtour(selected)\n", + " if len(tour) < n: \n", + " for k in trucks:\n", + " model.cbLazy(gp.quicksum(model._x[i, j, k]\n", + " for i, j in permutations(tour, 2))\n", + " <= len(tour)-1)\n", + "\n", + "\n", + "# Given a tuplelist of edges, find the shortest subtour not containing depot (0)\n", + "def subtour(edges):\n", + " unvisited = list(range(1, n))\n", + " cycle = range(n+1) # initial length has 1 more city\n", + " while unvisited:\n", + " thiscycle = []\n", + " neighbors = unvisited\n", + " while neighbors:\n", + " current = neighbors[0]\n", + " thiscycle.append(current)\n", + " if current != 0:\n", + " unvisited.remove(current)\n", + " neighbors = [j for i, j in edges.select(current, '*')\n", + " if j == 0 or j in unvisited]\n", + " if 0 not in thiscycle and len(cycle) > len(thiscycle):\n", + " cycle = thiscycle\n", + " return cycle" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Solve the model" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Set parameter LazyConstraints to value 1\n", + "Gurobi Optimizer version 9.5.1 build v9.5.1rc2 (win64)\n", + "Thread count: 4 physical cores, 8 logical processors, using up to 8 threads\n", + "Optimize a model with 334 rows, 1798 columns and 5492 nonzeros\n", + "Model fingerprint: 0xaa4ca881\n", + "Variable types: 58 continuous, 1740 integer (1740 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 7e+01]\n", + " Objective range [1e+00, 1e+00]\n", + " Bounds range [1e+00, 1e+02]\n", + " RHS range [1e+00, 1e+00]\n", + "\n", + "---------------------------------------------------------------------------\n", + "Multi-objectives: starting optimization with 2 objectives ... \n", + "---------------------------------------------------------------------------\n", + "\n", + "Multi-objectives: applying initial presolve ...\n", + "---------------------------------------------------------------------------\n", + "\n", + "Presolve time: 0.01s\n", + "Presolved: 334 rows and 1798 columns\n", + "---------------------------------------------------------------------------\n", + "\n", + "Multi-objectives: optimize objective 1 (Number of trucks) ...\n", + "---------------------------------------------------------------------------\n", + "\n", + "Presolve time: 0.01s\n", + "Presolved: 334 rows, 1798 columns, 5492 nonzeros\n", + "Variable types: 58 continuous, 1740 integer (1740 binary)\n", + "\n", + "Root relaxation: objective 1.000000e+00, 706 iterations, 0.02 seconds (0.01 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 1.27085 0 113 - 1.27085 - - 0s\n", + " 0 0 2.00000 0 151 - 2.00000 - - 0s\n", + " 0 0 2.00000 0 120 - 2.00000 - - 0s\n", + " 0 0 2.00000 0 140 - 2.00000 - - 0s\n", + " 0 0 2.00000 0 14 - 2.00000 - - 0s\n", + " 0 0 2.00000 0 4 - 2.00000 - - 0s\n", + " 0 0 2.00000 0 23 - 2.00000 - - 0s\n", + " 0 0 2.00000 0 18 - 2.00000 - - 0s\n", + " 0 0 2.00000 0 17 - 2.00000 - - 0s\n", + " 0 0 2.00000 0 17 - 2.00000 - - 0s\n", + " 0 2 2.00000 0 14 - 2.00000 - - 1s\n", + "* 699 455 71 4.0000000 2.00000 50.0% 30.6 1s\n", + "H 1049 310 3.0000000 2.00000 33.3% 32.9 1s\n", + " 1177 296 2.00000 14 47 3.00000 2.00000 33.3% 58.2 5s\n", + "H 1922 152 2.0000000 2.00000 0.00% 100 8s\n", + "\n", + "Cutting planes:\n", + " Gomory: 1\n", + " Implied bound: 7\n", + " Clique: 7\n", + " MIR: 1\n", + " Zero half: 1\n", + " RLT: 1\n", + " Lazy constraints: 120\n", + "\n", + "Explored 1943 nodes (213182 simplex iterations) in 8.25 seconds (5.98 work units)\n", + "Thread count was 8 (of 8 available processors)\n", + "\n", + "Solution count 3: 2 3 4 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 2.000000000000e+00, best bound 2.000000000000e+00, gap 0.0000%\n", + "---------------------------------------------------------------------------\n", + "\n", + "Multi-objectives: optimize objective 2 (Travel time) ...\n", + "---------------------------------------------------------------------------\n", + "\n", + "\n", + "Loaded user MIP start with objective 117\n", + "\n", + "Presolve time: 0.02s\n", + "Presolved: 335 rows, 1798 columns, 5498 nonzeros\n", + "Variable types: 58 continuous, 1740 integer (1740 binary)\n", + "\n", + "Root simplex log...\n", + "\n", + "Iteration Objective Primal Inf. Dual Inf. Time\n", + " 0 0.0000000e+00 1.600000e+01 0.000000e+00 8s\n", + " 653 2.9010676e+01 0.000000e+00 0.000000e+00 8s\n", + "\n", + "Root relaxation: objective 2.901068e+01, 653 iterations, 0.04 seconds (0.02 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 89.68911 0 69 117.00000 89.68911 23.3% - 8s\n", + " 0 0 93.64408 0 67 117.00000 93.64408 20.0% - 8s\n", + " 0 0 94.43074 0 17 117.00000 94.43074 19.3% - 8s\n", + " 0 0 95.46321 0 59 117.00000 95.46321 18.4% - 8s\n", + " 0 0 95.50664 0 60 117.00000 95.50664 18.4% - 8s\n", + "H 0 0 116.4889483 95.50664 18.0% - 8s\n", + " 0 0 95.66792 0 62 116.48895 95.66792 17.9% - 8s\n", + " 0 0 95.68927 0 64 116.48895 95.68927 17.9% - 8s\n", + " 0 0 95.70252 0 67 116.48895 95.70252 17.8% - 8s\n", + " 0 0 95.72314 0 58 116.48895 95.72314 17.8% - 8s\n", + " 0 0 95.73173 0 59 116.48895 95.73173 17.8% - 8s\n", + " 0 0 95.75299 0 58 116.48895 95.75299 17.8% - 8s\n", + " 0 0 95.80745 0 62 116.48895 95.80745 17.8% - 8s\n", + " 0 0 95.81155 0 61 116.48895 95.81155 17.8% - 8s\n", + " 0 0 95.82275 0 65 116.48895 95.82275 17.7% - 8s\n", + " 0 0 95.82353 0 65 116.48895 95.82353 17.7% - 8s\n", + " 0 0 95.82353 0 65 116.48895 95.82353 17.7% - 8s\n", + " 0 2 96.72441 0 65 116.48895 96.72441 17.0% - 8s\n", + "* 217 126 21 114.8599746 98.13859 14.6% 10.1 8s\n", + "* 255 136 23 113.7234014 100.13030 12.0% 10.1 8s\n", + "* 610 227 10 112.4116095 103.12886 8.26% 10.0 8s\n", + "* 837 192 15 105.4165478 104.05065 1.30% 10.6 9s\n", + "\n", + "Cutting planes:\n", + " Gomory: 2\n", + " Clique: 1\n", + " Zero half: 9\n", + " Mod-K: 2\n", + " Lazy constraints: 62\n", + "\n", + "Explored 1155 nodes (12200 simplex iterations) in 9.03 seconds (6.32 work units)\n", + "Thread count was 8 (of 8 available processors)\n", + "\n", + "Solution count 5: 105.417 112.412 113.723 ... 116.489\n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 1.054165478154e+02, best bound 1.054165478154e+02, gap 0.0000%\n", + "\n", + "---------------------------------------------------------------------------\n", + "Multi-objectives: solved in 9.04 seconds (6.32 work units), solution count 8\n", + "\n", + "\n", + "User-callback calls 13351, time in user-callback 0.29 sec\n" + ] + } + ], + "source": [ + "# Verify model formulation\n", + "\n", + "m.write('lost_luggage_distribution.lp')\n", + "\n", + "# Run optimization engine\n", + "m._x = x\n", + "m.Params.LazyConstraints = 1\n", + "m.optimize(subtourelim)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Analysis\n", + "\n", + "The optimal route of each van used and the total lost luggage delivery time report follows." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Route for van 0: 0 -> 3 -> 16 -> 15 -> 2 -> 7 -> 14 -> 6 -> 5 -> 13 -> 0. Travel time: 105.42 min\n", + "Route for van 1: 0 -> 9 -> 8 -> 1 -> 12 -> 10 -> 4 -> 11 -> 0. Travel time: 104.78 min\n" + ] + }, + { + "ename": "NameError", + "evalue": "name 's' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mNameError\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m~\\AppData\\Local\\Temp/ipykernel_13220/3456469386.py\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 12\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34mf\". Travel time: {round(t[k].X,2)} min\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 13\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 14\u001b[1;33m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34mf\"Max travel time: {round(s.X,2)}\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[1;31mNameError\u001b[0m: name 's' is not defined" + ] + } + ], + "source": [ + "# Print optimal routes\n", + "for k in trucks:\n", + " route = gp.tuplelist((i,j) for i,j in time.keys() if x[i,j,k].X > 0.5)\n", + " if route:\n", + " i = 0\n", + " print(f\"Route for van {k}: {i}\", end='')\n", + " while True:\n", + " i = route.select(i, '*')[0][1]\n", + " print(f\" -> {i}\", end='')\n", + " if i == 0:\n", + " break\n", + " print(f\". Travel time: {round(t[k].X,2)} min\")\n", + "\n", + "print(f\"Max travel time: {round(s.X,2)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "H. Paul Williams, Model Building in Mathematical Programming, fifth edition.\n", + "\n", + "Copyright © 2020 Gurobi Optimization, LLC" + ] + } + ], + "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.9.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}