{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Al's Athletics Problem\n", "Make sure you've installed PuLP, Pandas and Plotly. If you haven't done this, see our other document on how to install these packages." ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [], "source": [ "# This is the block you should make changes\n", "# Only change things to the right of the '=' sign. \n", "# Note that the '#' symbol means a comment-- when the code runs, it ignores this.\n", "# Here you can change the scnario name, number of warehouses, the distance bands, and whether you draw the maps\n", "\n", "# ******** To Run, go to the menu item Cell and select 'Run All' *************\n", "\n", "# To see the results of the run: \n", "# Go to the directory where you put this file and you'll see two output files\n", "# One output file will be the summary and one will have details\n", "# And, tabs will open if you decide to map the input and output\n", "\n", "# You should change the name to the right of the '=' sign. This names each scenario. \n", "# If you don't change it, you'll overwrite the files when you run. Don't put spaces in the name\n", "# **** Change Scenario Name ****** And, keep the single quotes in the name. \n", "\n", "scenario_name = 'Scenario_name' #<<<----- you can change this name\n", "\n", "# **** Change the number of warehouses you would like to select.***** This number should be an integer\n", "\n", "number_of_whs = 3 #<<<------ you can change this number\n", "\n", "# ******** Change the distance bands **********, if you'd like. The band numbers represent the distance up to. These should be increasing.\n", "# You cannot add or subtract bands.\n", "\n", "distance_band_1 = 200 #<<<----- you can change these numbers\n", "distance_band_2 = 400\n", "distance_band_3 = 800\n", "distance_band_4 = 1600\n", "\n", "\n", "#************This is where you change the status of a warehouse************\n", "# meaning you can force it to be open or force it closed.\n", "# You want to changet the two numbers after the name. Don't delete or change the commas\n", "# it should be 0,1 to let the solver pick, 1,1 to force it to be used, or 0,0 to force it closed. 1,0 is an error- don't use that\n", "\n", "wh_status = {1:('Allentown',0,1),\n", " 2:('Atlanta',0,1),\n", " 3:('Baltimore',0,1),\n", " 4:('Boston',0,1),\n", " 5:('Chicago',0,1),\n", " 6:('Cincinnati',0,1),\n", " 7:('Columbus',0,1),\n", " 8:('Dallas',0,1),\n", " 9:('Denver',0,1),\n", " 10:('Indianapolis',0,1),\n", " 11:('Jacksonville',0,1),\n", " 12:('Kansas City',0,1),\n", " 13:('Las Vegas',0,1),\n", " 14:('Los Angeles',0,1),\n", " 15:('Memphis',0,1),\n", " 16:('Minneapolis',0,1),\n", " 17:('Nashville',0,1),\n", " 18:('New Orleans',0,1),\n", " 19:('Phoenix',0,1),\n", " 20:('Pittsburgh',0,1),\n", " 21:('Raleigh',0,1),\n", " 22:('Reno',0,1),\n", " 23:('San Francisco',0,1),\n", " 24:('Seattle',0,1),\n", " 25:('St. Louis',0,1),\n", " 26:('Lubbock - Current WH',0,1)}\n", "\n", "# **** If you don't want either the input or output map to draw, type False with a capital F instead of True\n", "\n", "input_map = True #<<<----- you can change these from True to False\n", "output_map = True\n", "\n", "# ********** To Run, go to the menu item Cell and select 'Run All'**************\n" ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [], "source": [ "#This is code to start running the model. You don't need to interact with this\n", "from pulp import *\n", "import time\n", "import pandas as pd\n", "import plotly as py\n", "\n", "\n", "start_time=time.time()\n", "\n", "def optimal_location(number_of_whs, warehouses, customers, customer_demands, distance, distance_band, scenario_name):\n", " # Create the 'prob' variable to contain the problem data\n", " als_problem = LpProblem(\"Als\", LpMinimize)\n", "\n", " # A dictionary called 'city_vars' is created to contain the decision variables Yi,j - the assignment of the city to a facility\n", " # lower bound is 0; upper bound is 1; integer variable - this makes it binary\n", " assign_vars = LpVariable.dicts(\"Asssignment\",[(w, c) for w in warehouses for c in customers],0,1,LpInteger)\n", " # Another dicionary called 'facility_vars' is created to contain the decision variables Xi - the decision of a facility at city i to be opened or not\n", " facility_vars = LpVariable.dicts(\"Open\", [w for w in warehouses],0,1,LpInteger)\n", "\n", " # The objective function is added to 'prob' first\n", " total_weighted_demand_objective = lpSum([customer_demands[c]*distance[w,c]*assign_vars[w,c] \n", " for w in warehouses for c in customers])\n", "\n", " # The five constraints are added to 'als_problem'\n", "\n", " # Every customer must be served\n", " \"\"\"\n", " \n", " A Pulp LP constraint: LpConstraint(e, sense, name, rhs)\n", " Parameters:\t\n", " e – an instance of LpExpression\n", " sense – one of LpConstraintEQ, LpConstraintGE, LpConstraintLE\n", " name – identifying string\n", " rhs – numerical value of constraint right hand side\n", " \"\"\"\n", " for customer in customers:\n", " als_problem += LpConstraint(e = lpSum([assign_vars[w, customer] for w in warehouses]), \n", " sense=LpConstraintEQ, \n", " name=str(customer)+\"_Served\", \n", " rhs=1)\n", "\n", " # Ensure that P and exactly P facilities are opened. We use the variable name 'number_of_whs' for P\n", " als_problem += LpConstraint(e = lpSum([facility_vars[w] for w in warehouses]), \n", " sense=LpConstraintEQ, \n", " name=\"FacilityCount\", \n", " rhs=number_of_whs)\n", " \n", " # Use the user's status to fix some warehouses in the solution\n", " \n", " for facility in warehouses:\n", " w = facility \n", " als_problem += LpConstraint(e = facility_vars[facility] , \n", " sense=LpConstraintGE, \n", " name=str(facility) + \"_\" + \"Lower Bound\",\n", " rhs=wh_status[w][1])\n", " for facility in warehouses:\n", " w = facility\n", " als_problem += LpConstraint(e = facility_vars[facility] , \n", " sense=LpConstraintLE, \n", " name=str(facility) + \"_\" + \"Upper Bound\",\n", " rhs=wh_status[w][2])\n", "\n", " # ***************** In this exercise, we'll toggle between these constraints ************** \n", " # An assignment can only exist between facility and customer if the facility is opened. We'll call this the \"route\"\n", "# for facility in warehouses:\n", "# for customer in customers:\n", "# als_problem += LpConstraint(e = assign_vars[facility, customer]-facility_vars[facility] , \n", "# sense=LpConstraintLE, \n", "# name=str(facility) + \"_\" + str(customer) + \"_Route\",\n", "# rhs=0)\n", " for facility in warehouses:\n", " bigM = 1000\n", " als_problem += LpConstraint(e = lpSum(assign_vars[facility, customer] for customer in customers)-bigM*facility_vars[facility] , \n", " sense=LpConstraintLE, \n", " name=str(facility) + \"_Route-BigM Version\",\n", " rhs=0) \n", " \n", "\n", " # Setting problem objective\n", " als_problem.setObjective(total_weighted_demand_objective)\n", " # The problem data is written to an .lp file\n", " #als_problem.writeLP(\"NineCity.lp\")\n", "\n", " # The problem is solved using PuLP's choice of Solver\n", " _solver = pulp.PULP_CBC_CMD(keepFiles=False,fracGap=0.00,maxSeconds=120, msg=True)\n", " als_problem.solve(solver=_solver)\n", "\n", " # The status of the solution is printed to the screen\n", " #print (\"Status:\", LpStatus[als_problem.status])\n", " file.write('\\nstatus:'+ LpStatus[als_problem.status])\n", " print(\"Optimization Status\",LpStatus[als_problem.status] ) #print in Jupyter Notebook\n", " if LpStatus[als_problem.status] == \"Infeasible\" :\n", " print(\"********* ERROR: Model not feasible, don't use results. One thing to check: see that the number of available warehouses is more than the fixed status.\")\n", " #print objective\n", " total_demand = sum(customer_demands.values())\n", " total_demand_to_warehouse = {w: sum(customer_demands[c]*assign_vars[w,c].varValue for c in customers)\n", " for w in warehouses\n", " if facility_vars[w].varValue>0}\n", " \n", " #print(total_demand_to_warehouse)\n", " #print(\"Total Demand\",total_demand)\n", " file.write(\"\\nTotal Demand:\"+ str(total_demand))\n", " #print(\"Objective: \", value(als_problem.objective))\n", " file.write(\"\\nObjective: \"+ str(value(als_problem.objective)))\n", " wgt_avg_dist = value(als_problem.objective)/total_demand\n", " #print(\"Weighted Average Distance: {:.1f} miles\" .format(wgt_avg_dist))\n", " file.write(\"\\nWeighted Average Distance: {:.1f} miles\" .format(wgt_avg_dist))\n", " print(\"Weighted Average Distance: {:.1f} miles\" .format(wgt_avg_dist))\n", " \n", " end_time = time.time()\n", " time_diff = end_time - start_time\n", " #print(\"Run Time in seconds {:.1f}\" .format(time_diff))\n", " file.write(\"\\nRun Time in seconds {:.1f}\" .format(time_diff))\n", " print(\"Run Time in seconds {:.1f}\" .format(time_diff))\n", " \n", " \n", " #preparing data to write in excel sheets\n", " opened_warehouses = []\n", " \n", " for w in facility_vars.keys():\n", " if(facility_vars[w].varValue > 0):\n", " #print(warehouses[w])\n", " wh = {\n", " 'Warehouse Key': w,\n", " 'Warehouse City':warehouses[w][1],\n", " 'State':warehouses[w][2],\n", " 'ZipCode':warehouses[w][3],\n", " 'Lat':warehouses[w][4],\n", " 'Lon':warehouses[w][5],\n", " 'Total Demand to Warehouse':total_demand_to_warehouse[w]\n", " }\n", " opened_warehouses.append(wh)\n", " \n", " \n", " #converting the list to dataframe \n", " df_wh = pd.DataFrame.from_records(opened_warehouses)\n", " \n", " df_wh = df_wh[['Warehouse Key', 'Warehouse City', 'State', 'ZipCode', 'Total Demand to Warehouse']]\n", " list_warehouses_open = list(df_wh['Warehouse Key'])\n", " \n", " # writing detailed files\n", " writer = pd.ExcelWriter(scenario_name+'_detailed.xlsx')\n", " df_wh.to_excel(writer,'Opened Warehouses',index=False)\n", " \n", "# print(\"Customers Assigned to warehouses\")\n", " customers_assignment = [] \n", " for (w,c) in assign_vars.keys():\n", " if assign_vars[(w,c)].varValue > 0:\n", " cust = {\n", " 'Warehouse':str(warehouses[w][1]+','+warehouses[w][2]),\n", " 'Customer':str(customers[c][1]+','+customers[c][2]),\n", " 'Customer Demand': customer_demands[c],\n", " 'Distance': distance[w,c],\n", " 'Warehouse Latitude' : warehouses[w][4],\n", " 'Warehouse Longitude' : warehouses[w][5],\n", " 'Customers Latitude' : customers[c][4],\n", " 'Customers Longitude': customers[c][5]\n", " }\n", " customers_assignment.append(cust)\n", " \n", " df_cu = pd.DataFrame.from_records(customers_assignment)\n", " df_cu_copy = df_cu.copy()\n", " df_cu = df_cu[['Warehouse', 'Customer', 'Distance', 'Customer Demand']]\n", " df_cu.to_excel(writer,'Customers Assignment',index=False)\n", " \n", " \n", " writer.close()\n", "\n", " #writing percent demand within each distance bands\n", " total_demand = sum(df_cu['Customer Demand'])\n", " percent_demand_distance_band_1 = sum(df_cu[df_cu['Distance']=0):\n", " pass\n", " else:\n", " file.write(f'\\nDistance between warehouse {w} and customer {c} is not available or invalid')\n", " \n", " wh_counter = 0\n", " for w in wh_status:\n", " if wh_status[w][1] == 1:\n", " if wh_status[w][2]==1:\n", " wh_counter = wh_counter+1\n", " else:\n", " print(\"In wh_status, if the first value is 1, the second value can't be 0. The problem is WH: \", w)\n", " file.write(f'\\nwh_status for warehouse {w} cannot have a 1 and 0 for status')\n", " if wh_counter <= number_of_whs:\n", " pass\n", " else: \n", " print(\"Your fixed number of warehouses in wh_status is larger than the number of warehouses. You need to fix and re-run\")\n", " file.write(f'\\nYour fixed number of warehouses in wh_status is larger than the number of warehouses. You need to fix and re-run') \n", " \n", "\n", "\n", " for c in customers.keys():\n", " if(customer_demands[c]>=0):\n", " pass\n", " else:\n", " file.write(f'\\nDemand for Customer {c} is not available')\n", " \n", " \n", " if ((distance_band[0] < distance_band[1]) and (distance_band[1] < distance_band[2]) and (distance_band[2] < distance_band[3]) ):\n", " pass\n", " else:\n", " file.write(f'\\nDistance bands are not in ascending order. You must fix and re-run *********')\n", " \n", " if isinstance(number_of_whs, int)==False:\n", " file.write(f'\\nnumber_of_whs = {number_of_whs} is not integer. You must fix and re-run***********')\n", " \n", " \n", " " ] }, { "cell_type": "code", "execution_count": 40, "metadata": {}, "outputs": [], "source": [ "# In this function, we are visualizaing the input data-- the data before the optimization has run\n", "def input_visual(warehouses, customers):\n", " warehouse_list = []\n", " for w in warehouses.keys():\n", " wh = {\n", " 'text':'Warehouse-'+warehouses[w][1],\n", " 'State':warehouses[w][2],\n", " 'ZipCode':warehouses[w][3],\n", " 'lat':warehouses[w][4],\n", " 'long':warehouses[w][5],\n", " 'cnt':10000000,\n", " 'size' : 30,\n", " 'color' : 'rgba(0, 100, 0)'\n", " }\n", " warehouse_list.append(wh) \n", "\n", " customer_list =[] \n", " for c in customers.keys():\n", " cust = {\n", " 'text':'Customer-'+customers[c][1],\n", " 'State':customers[c][2],\n", " 'ZipCode':customers[c][3],\n", " 'lat':customers[c][4],\n", " 'long':customers[c][5] ,\n", " 'cnt':customer_demands[c],\n", " 'size' : 3,\n", " 'color' : 'rgb(255, 0, 0)'\n", " }\n", " customer_list.append(cust) \n", "\n", " df = pd.DataFrame.from_records(warehouse_list)\n", " df['shape'] = \"triangle-down\"\n", " df_cust = pd.DataFrame.from_records(customer_list)\n", " df_cust['shape'] = \"circle\"\n", " df = df.append(df_cust, ignore_index = True) \n", "\n", " locations = [ dict(\n", " type = 'scattergeo',\n", " locationmode = 'USA-states',\n", " lon = df['long'],\n", " lat = df['lat'],\n", " hoverinfo = 'text',\n", " text = df['text'],\n", " mode = 'markers',\n", " marker = dict( \n", " size=df['size'], \n", " color=df['color'],\n", " symbol = df['shape'],\n", " line = dict(\n", " width=3,\n", " color='rgba(68, 68, 68, 0)'\n", " ),\n", "\n", " ))]\n", "\n", " layout = dict(\n", " title = 'Als Athletics - Input',\n", " showlegend = False, \n", " geo = dict(\n", " scope='usa',\n", " projection=dict( type='albers usa' ),\n", " showland = True,\n", " landcolor = 'rgb(243, 243, 243)',\n", " countrycolor = 'rgb(204, 204, 204)',\n", " ),\n", " )\n", " py.offline.plot({ \"data\":locations, \"layout\":layout}, filename = scenario_name+'_input.html') \n", "\n", " " ] }, { "cell_type": "code", "execution_count": 41, "metadata": {}, "outputs": [], "source": [ "# In this function, we are visualizaing the output data-- the data after the optimization has run\n", "def output_visual(warehouses, customers, df_cu, wh_loc, scenario_name ): \n", " \n", " warehouse_list = []\n", " for w in wh_loc:\n", " wh = {\n", " 'text':'Warehouse-'+warehouses[w][1],\n", " 'State':warehouses[w][2],\n", " 'ZipCode':warehouses[w][3],\n", " 'lat':warehouses[w][4],\n", " 'long':warehouses[w][5],\n", " 'cnt':10000000,\n", " 'size' : 30,\n", " 'color' : 'rgba(0, 100, 0)'\n", " }\n", " warehouse_list.append(wh) \n", " \n", " customer_list =[] \n", " for c in customers.keys():\n", " cust = {\n", " 'text':'Customer-'+customers[c][1],\n", " 'State':customers[c][2],\n", " 'ZipCode':customers[c][3],\n", " 'lat':customers[c][4],\n", " 'long':customers[c][5] ,\n", " 'cnt':customer_demands[c],\n", " 'size' : 3,\n", " 'color' : 'rgb(255, 0, 0)'\n", " }\n", " customer_list.append(cust)\n", " \n", " df = pd.DataFrame.from_records(warehouse_list)\n", " df['shape'] = \"triangle-down\"\n", " df_cust = pd.DataFrame.from_records(customer_list)\n", " df_cust['shape'] = \"circle\"\n", " df = df.append(df_cust, ignore_index = True) \n", "\n", " paths = [] \n", " for i in range( len( df_cu) ):\n", " paths.append(\n", " dict(\n", " type = 'scattergeo',\n", " locationmode = 'USA-states',\n", " lon = [ df_cu['Warehouse Longitude'][i], df_cu['Customers Longitude'][i] ],\n", " lat = [ df_cu['Warehouse Latitude'][i], df_cu['Customers Latitude'][i] ],\n", " mode = 'lines',\n", " line = dict(\n", " width = 1,\n", " color = 'red',\n", " ),\n", " opacity = 0.8,\n", " )\n", " ) \n", " \n", "\n", " locations = [ dict(\n", " type = 'scattergeo',\n", " locationmode = 'USA-states',\n", " lon = df['long'],\n", " lat = df['lat'],\n", " hoverinfo = 'text',\n", " text = df['text'],\n", " mode = 'markers',\n", " marker = dict( \n", " size=df['size'], \n", " color=df['color'],\n", " symbol = df['shape'],\n", " line = dict(\n", " width=3,\n", " color='rgba(68, 68, 68, 0)'\n", " ),\n", " ))]\n", "\n", " layout = dict(\n", " title = 'Als Athletics - Output',\n", " showlegend = False, \n", " geo = dict(\n", " scope='usa',\n", " projection=dict( type='albers usa' ),\n", " showland = True,\n", " landcolor = 'rgb(243, 243, 243)',\n", " countrycolor = 'rgb(204, 204, 204)',\n", " ),\n", " )\n", "\n", " py.offline.plot({\"data\":locations+paths, \"layout\":layout},filename=scenario_name+'_output.html') " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## This is the block of code that runs all the functions (including the optimization)" ] }, { "cell_type": "code", "execution_count": 42, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "loaded input data\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "C:\\Users\\Riccardo\\anaconda3\\lib\\site-packages\\pulp\\apis\\coin_api.py:89: UserWarning:\n", "\n", "Parameter fracGap is being depreciated for gapRel\n", "\n", "C:\\Users\\Riccardo\\anaconda3\\lib\\site-packages\\pulp\\apis\\coin_api.py:95: UserWarning:\n", "\n", "Parameter maxSeconds is being depreciated for timeLimit\n", "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Optimization Status Optimal\n", "Weighted Average Distance: 310.1 miles\n", "Run Time in seconds 4.9\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "C:\\Users\\Riccardo\\AppData\\Local\\Temp\\ipykernel_3696\\1129796365.py:35: FutureWarning:\n", "\n", "The frame.append method is deprecated and will be removed from pandas in a future version. Use pandas.concat instead.\n", "\n", "C:\\Users\\Riccardo\\AppData\\Local\\Temp\\ipykernel_3696\\2600587077.py:36: FutureWarning:\n", "\n", "The frame.append method is deprecated and will be removed from pandas in a future version. Use pandas.concat instead.\n", "\n" ] } ], "source": [ "#getting input data\n", "warehouses, customers, customer_demands, distance = get_data()\n", "\n", "#print(\"loaded input data\")\n", "distance_band = [distance_band_1, distance_band_2, distance_band_3, distance_band_4]\n", "\n", "#opening text file in write mode\n", "file = open(scenario_name+'summary' + '.txt',\"w\")\n", "file.write('loaded input data')\n", "print(\"loaded input data\") #Also print to Jupyter\n", "\n", "#testing input data\n", "file.write('\\nTesting Input Data')\n", "test_input(warehouses, customers, customer_demands, distance, distance_band,wh_status)\n", "\n", "#RunCode = () #We need some function in here that runs the whole thing.\n", "file.write('\\nBuilding the model')\n", "df_assign_vars, list_facility_vars = optimal_location(number_of_whs, warehouses, customers, customer_demands, distance, distance_band, scenario_name)\n", "\n", "#printing the maps\n", "if input_map == True:\n", " input_visual(warehouses, customers)\n", "\n", "if output_map == True:\n", " output_visual(warehouses, customers, df_assign_vars, list_facility_vars, scenario_name)\n", "\n", "#closing the file\n", "file.close()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.12" } }, "nbformat": 4, "nbformat_minor": 2 }