{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Deadheading in Ride-Hailing\n",
"\n",
"**Authors:** Juan Carrillo, Anas Mahmoud and Sheran Cardoza
\n",
"**Course:** ECE1724H: Bio-inspired Algorithms for Smart Mobility - Fall 2021
\n",
"**Instructor:** Dr. Alaa Khamis
\n",
"**Department:** Edward S. Rogers Sr. Department of Electrical & Computer Engineering, University of Toronto\n",
"\n",
"\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Introduction\n",
"\n",
"Ridesharing is one of several on-demand mobility services gaining momentum in recent years\n",
"and represents a flexible and convenient alternative for transportation. Despite its\n",
"attractiveness, ridesharing also causes undesired effects such as increased traffic and\n",
"emissions. In this project, we study specifically the problem of ridesharing vehicles roaming\n",
"without passengers, also known as deadheading."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import copy\n",
"import time\n",
"import random\n",
"import statistics\n",
"from math import cos\n",
"from configparser import ConfigParser\n",
"\n",
"# Data and plotting\n",
"import numpy as np\n",
"import pandas as pd\n",
"from pandas.io import parsers\n",
"import folium\n",
"import matplotlib.pyplot as plt\n",
"from tqdm.notebook import tqdm\n",
"\n",
"# Mobility\n",
"import osmnx\n",
"from smart_mobility_utilities.common import Node\n",
"from smart_mobility_utilities.common import cost\n",
"from smart_mobility_utilities.viz import draw_route\n",
"from smart_mobility_utilities.search import dijkstra, astar"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Data\n",
"\n",
"The data source for this example contains 175 delivery records for addresses in the Greater Toronto Area (GTA).\n",
"\n",
"These entries with latitude and longitude information are then converted into OSM Node IDs (closest node). One location is assigned per vehicle as a \"initial depot location\". Each rider is assigned a pickup node and a dropoff node."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Problem Classes\n",
"\n",
"### Rider"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"class Rider(object):\n",
" def __init__(\n",
" self,\n",
" id: int,\n",
" pickup_location: tuple,\n",
" dropoff_location: tuple,\n",
" pickup_map_node_id: int,\n",
" dropoff_map_node_id: int\n",
" ):\n",
" \"\"\" Instantiate a rider object\n",
" Parameters\n",
" ----------\n",
" id : A unique id for each rider\n",
" pickup_location: pickup lat and long\n",
" dropoff_location: dropoff lat and long\n",
" pickup_map_node_id: osmid of the nearest node to pickup_location\n",
" dropoff_map_node_id: osmid of the nearest node to dropoff_location\n",
" \"\"\"\n",
" self.id = id\n",
" self.pickup_location = pickup_location\n",
" self.dropoff_location = dropoff_location\n",
" self.pickup_map_node_id = pickup_map_node_id\n",
" self.dropoff_map_node_id = dropoff_map_node_id\n",
" def __str__(self)->str:\n",
" return \"ID of rider: {}\\n\".format(self.id) + \\\n",
" \"Pickup location of rider: {} {}\\n\".format(self.pickup_location[0], self.pickup_location[1]) + \\\n",
" \"Dropoff location of rider: {} {}\\n\".format(self.dropoff_location[0], self.dropoff_location[1]) + \\\n",
" \"Pickup node id of rider: {}\\n\".format(self.pickup_map_node_id) + \\\n",
" \"Dropoff node id of rider: {}\\n\".format(self.dropoff_map_node_id)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Driver"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"class Driver(object):\n",
" def __init__(\n",
" self,\n",
" id,\n",
" initial_location: tuple,\n",
" initial_map_node_id: int\n",
" ):\n",
" \"\"\"Instantiate a drive object\n",
" Parameters\n",
" ----------\n",
" id : A unique id for each driver\n",
" initial_location: initial lat and long\n",
" initial_map_node_id: osmid of the nearest node to initial driver location\n",
" \"\"\"\n",
" self.id = id\n",
" self.initial_location = initial_location\n",
" self.initial_map_node_id = initial_map_node_id\n",
" def __str__(self) -> str:\n",
" return \"ID of driver: {}\\n\".format(self.id) + \\\n",
" \"Initial location of driver: {} {}\\n\".format(self.initial_location[0], self.initial_location[1]) + \\\n",
" \"Initial node id of driver: {}\\n\".format(self.initial_map_node_id)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Coord (Node)"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"class Coord(object):\n",
" def __init__(self, lat, lng, osmid):\n",
" self.lat = lat\n",
" self.lng = lng\n",
" self.osmid = osmid\n",
" \n",
" def __repr__(self):\n",
" return \"[lat,lng,id]=[{},{},{}]\".format(self.lat, self.lng, self.osmid)\n",
" \n",
" def __eq__(self, other):\n",
" if isinstance(other, Coord):\n",
" return self.osmid == other.osmid\n",
" return False\n",
" \n",
" def __ne__(self, other):\n",
" return not self == other"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Data Sampler\n",
"\n",
"This class processes raw data and converts into a usable format."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"class Sampler(object):\n",
" def __init__(self):\n",
" pass\n",
"\n",
" def prepare_data(self, df):\n",
" \"\"\"Convert raw data to custom data format\n",
" Parameters\n",
" ----------\n",
" df : dataframe object of raw data\n",
" Returns\n",
" ----------\n",
" dataframe object of custom data\n",
" \"\"\"\n",
"\n",
" df = df.drop_duplicates()\n",
"\n",
" # Pick midpoint of all coordinates as the center of the graph\n",
" self.midpoint = (df['dropoff_lat'].mean(), df['dropoff_lng'].mean())\n",
"\n",
" # Calculate radius as distance of farthest coordinate from midpoint\n",
" dists = [osmnx.distance.great_circle_vec(self.midpoint[0], self.midpoint[1], row['dropoff_lat'], row['dropoff_lng']) for _, row in df.iterrows()]\n",
" self.radius = max(dists)\n",
" # self.radius = 2000\n",
"\n",
" # Generate graph (takes a long time)\n",
" graph = osmnx.graph.graph_from_point(\n",
" self.midpoint, dist=self.radius, clean_periphery=True, simplify=True)\n",
"\n",
" # Project graph\n",
" #graph = osmnx.project_graph(graph, to_crs={'init': 'epsg:32617'})\n",
"\n",
" # Extract coord info\n",
" lats = []\n",
" lngs = []\n",
" osmids = []\n",
" for index, row in df.iterrows():\n",
" lat = row['dropoff_lat']\n",
" lng = row['dropoff_lng']\n",
" osmid = osmnx.distance.nearest_nodes(graph, lng, lat)\n",
" lats.append(lat)\n",
" lngs.append(lat)\n",
" osmids.append(osmid)\n",
"\n",
" # Create dataframe\n",
" new_df = pd.DataFrame(list(zip(lats, lngs, osmids)),\n",
" columns=['lat','lng','osmid'])\n",
"\n",
" def init_data(self, df):\n",
" \"\"\"Initialize Sampler class attributes with prepared data\n",
" Parameters\n",
" ----------\n",
" df : dataframe object of prepared data\n",
" \"\"\"\n",
"\n",
" df = df.drop_duplicates()\n",
"\n",
" # Pick midpoint of all coordinates as the center of the graph\n",
" self.midpoint = (df['lat'].mean(), df['lng'].mean())\n",
"\n",
" # Calculate radius as distance of farthest coordinate from midpoint\n",
" dists = [osmnx.distance.great_circle_vec(self.midpoint[0], self.midpoint[1], row['lat'], row['lng']) for _, row in df.iterrows()]\n",
" self.radius = max(dists)\n",
"\n",
" # Extract coords\n",
" self.coords = [Coord(row['lat'], row['lng'], row['osmid']) for _,row in df.iterrows()]\n",
"\n",
" # Debug info\n",
" print(\"Num coords = {}\".format(len(self.coords)))\n",
" seen = set()\n",
" unique_ids = [seen.add(coord.osmid) or coord for coord in self.coords if coord.osmid not in seen]\n",
" print(\"Num coords with unique node id = {}\".format(len(unique_ids)))\n",
"\n",
" def get_samples(self, n_drivers, n_riders, radius=None, midpoint=None, return_graph=True):\n",
" \"\"\"Return sample data containing drivers, riders, and graph\n",
" Parameters\n",
" ----------\n",
" n_drivers : number of drivers\n",
" n_riders : number of riders\n",
" radius : radius in metres\n",
" midpoint : midpoint as a tuple of (lat, lng)\n",
" return_graph: generate a graph based on midpoint and radius\n",
" Returns\n",
" ---------\n",
" drivers : list of Driver objects\n",
" riders : list of Rider objects\n",
" graph : (optional) if return_graph=True, returns graph of nodes\n",
" \"\"\"\n",
" assert n_drivers < n_riders\n",
" midpoint = self.midpoint if midpoint is None else midpoint\n",
" radius = self.radius if radius is None else radius\n",
"\n",
" # Find valid coords within this radius\n",
" coords = []\n",
" for coord in self.coords:\n",
" dist = osmnx.distance.great_circle_vec(midpoint[0], midpoint[1], coord.lat, coord.lng)\n",
" if dist <= radius:\n",
" coords.append(coord)\n",
"\n",
" assert n_drivers + n_riders * \\\n",
" 2 <= len(coords), \"Error: n_drivers={} + n_riders*2={} > available_coords={}\".format(\n",
" n_drivers, n_riders*2, len(coords))\n",
"\n",
" # Shuffle the coords for some randomization\n",
" # commented for debugging purposes\n",
" random.shuffle(coords)\n",
"\n",
" # Assign drivers\n",
" drivers = []\n",
" for i, coord in enumerate(coords):\n",
" if i < n_drivers:\n",
" drivers.append(Driver(i, (coord.lat, coord.lng), coord.osmid))\n",
" else:\n",
" # Delete the drivers\n",
" coords = coords[i+1:]\n",
" break\n",
"\n",
" # Delete any excess coords we don't need\n",
" coords = coords[0:2*n_riders]\n",
"\n",
" # Assign riders\n",
" riders = []\n",
" it = iter(coords)\n",
" for i, coord in enumerate(it):\n",
" pickup = coord\n",
" dropoff = next(it)\n",
" riders.append(Rider(i, (pickup.lat, pickup.lng),\n",
" (dropoff.lat, dropoff.lng), pickup.osmid, dropoff.osmid))\n",
"\n",
" if return_graph:\n",
" # Generate graph for this custom radius\n",
" graph = osmnx.graph.graph_from_point(\n",
" midpoint, dist=radius, clean_periphery=True, simplify=True)\n",
" return drivers, riders, graph\n",
"\n",
" return drivers, riders"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Num coords = 169\n",
"Num coords with unique node id = 168\n",
"Data init time = 0.016378164291381836 seconds\n"
]
}
],
"source": [
"sampler = Sampler()\n",
"\n",
"# Data preparation takes a long time.\n",
"# Run just once and save to a CSV, then in subsequent runs\n",
"# just use the saved CSV.\n",
"\n",
"# Load in the data\n",
"start = time.time()\n",
"filename = 'PreparedData.csv'\n",
"df = pd.read_csv(filename)\n",
"sampler.init_data(df)\n",
"end = time.time()\n",
"print(\"Data init time = {} seconds\".format(end - start))"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Samples time = 25.41386079788208 seconds\n",
"Samples contains the following:\n",
"3 drivers\n",
"7 riders\n",
"48273 graph nodes\n"
]
}
],
"source": [
"start = time.time()\n",
"radius = 6000\n",
"num_drivers = 3\n",
"num_riders = 7\n",
"dt_midpoint = (43.653225, -79.383186)\n",
"drivers, riders, graph = sampler.get_samples(\n",
" num_drivers, num_riders, radius=radius, midpoint=dt_midpoint, return_graph=True)\n",
"end = time.time()\n",
"print(\"Samples time = {} seconds\".format(end - start))\n",
"\n",
"print(\"Samples contains the following:\")\n",
"print(f'{len(drivers)} drivers')\n",
"print(f'{len(riders)} riders')\n",
"print(f'{len(graph.nodes())} graph nodes')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Plot\n",
"Used to visualize coordinates on a map."
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
"class Plot():\n",
" def __init__(self, drivers, riders, graph) -> None:\n",
" \"\"\"Initializes a plot object\n",
" Args:\n",
" drivers (list): drivers available\n",
" riders (list): riders requesting service\n",
" graph (networkx.classes.multidigraph.MultiDiGraph): road network\n",
" \"\"\"\n",
" self.drivers = drivers\n",
" self.riders = riders\n",
" self.graph = graph\n",
" self.map = None\n",
"\n",
" def calc_centroid(self):\n",
" \"\"\" calculates centroid of points to plot\n",
" Returns:\n",
" list: latitude and longitude values of the centroid\n",
" \"\"\"\n",
" avg_lat = 0\n",
" avg_lon = 0\n",
" total_coords = len(self.drivers) + 2*len(self.riders)\n",
"\n",
" for driver in self.drivers:\n",
" driver_ini_lat, driver_ini_lon = driver.initial_location\n",
" avg_lat += driver_ini_lat\n",
" avg_lon += driver_ini_lon\n",
"\n",
" for rider in self.riders:\n",
" rider_pic_lat, rider_pic_lon = rider.pickup_location\n",
" rider_dro_lat, rider_dro_lon = rider.dropoff_location\n",
" avg_lat += rider_pic_lat + rider_dro_lat\n",
" avg_lon += rider_pic_lon + rider_dro_lon\n",
"\n",
" avg_lat /= total_coords\n",
" avg_lon /= total_coords\n",
"\n",
" return [avg_lat, avg_lon]\n",
"\n",
" def init_basemap(self):\n",
" \"\"\" initializes a folium basemap centered over drivers and riders\n",
" \"\"\"\n",
" self.map = folium.Map(location=self.calc_centroid(), zoom_start=10)\n",
"\n",
" def add_pins_to_basemap(self):\n",
" \"\"\" adds locations to basemap\n",
" \"\"\"\n",
" # checks that a basemap is initialized\n",
" assert (self.map is not None), \"must initialize basemap before adding locations\"\n",
" for driver in self.drivers:\n",
" folium.Marker(location=list(driver.initial_location),\n",
" popup=f'driver {driver.id}',\n",
" icon=folium.map.Icon(color='orange')\n",
" ).add_to(self.map)\n",
" for rider in self.riders:\n",
" folium.Marker(location=list(rider.pickup_location),\n",
" popup=f'rider {rider.id}: pickup',\n",
" icon=folium.map.Icon(color='blue')\n",
" ).add_to(self.map)\n",
" folium.Marker(location=list(rider.dropoff_location),\n",
" popup=f'rider {rider.id}: dropoff',\n",
" icon=folium.map.Icon(color='red')\n",
" ).add_to(self.map)\n",
"\n",
" def basemap_to_html(self):\n",
" \"\"\" exports the basemap with pins to html\n",
" \"\"\"\n",
" # checks that a basemap is initialized\n",
" assert (self.map is not None), \"must initialize basemap before saving\"\n",
" self.map.save('basemap_with_pins.html')\n",
"\n",
" def plot_graph_with_nodes(self):\n",
" \"\"\" plots the graph with nodes classified in colors\n",
" Args:\n",
" graph\n",
" \"\"\"\n",
"\n",
" colors_dict = self.get_colors_dict()\n",
" all_nodes_colors = self.get_nodes_colors(colors_dict)\n",
"\n",
" all_nodes_size = [50 if node_id in colors_dict.keys() else 2\n",
" for node_id in self.graph.nodes]\n",
"\n",
" fig, ax = osmnx.plot_graph(\n",
" self.graph, node_size=all_nodes_size,\n",
" node_color=all_nodes_colors, node_zorder=2,\n",
" bgcolor='#F2F3F5', edge_color='#B3B5B7',\n",
" save=True, filepath='graph_and_nodes.png')\n",
"\n",
" def get_colors_dict(self):\n",
" \"\"\" generates a dictionary mapping node ids to colors\n",
" Returns:\n",
" colors_dict\n",
" \"\"\"\n",
" c_orange = '#FF8A33' # drivers\n",
" c_blue = '#3AACE5' # rider pickup\n",
" c_red = '#FF3352' # rider drop off\n",
"\n",
" colors_dict = {}\n",
"\n",
" for driver in self.drivers:\n",
" colors_dict[driver.initial_map_node_id] = c_orange\n",
"\n",
" for rider in self.riders:\n",
" colors_dict[rider.pickup_map_node_id] = c_blue\n",
" colors_dict[rider.dropoff_map_node_id] = c_red\n",
"\n",
" return colors_dict\n",
"\n",
" def get_nodes_colors(self, colors_dict):\n",
" \"\"\" generates a list with color for each node id in graph\n",
" Args:\n",
" colors_dict\n",
" Returns:\n",
" all_nodes_colors\n",
" \"\"\"\n",
" c_grey = '#B3B5B7'\n",
" all_nodes_colors = []\n",
" for node_id in self.graph.nodes:\n",
" try:\n",
" all_nodes_colors.append(colors_dict[node_id])\n",
" except:\n",
" all_nodes_colors.append(c_grey)\n",
" return all_nodes_colors"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Problem Formulation\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Formulation\n",
"\n",
"The evaluation function is a multi-objective cost function and includes:\n",
"1. Minimizing deadheading by searching for solutions that reduce miles driven without passengers, \n",
"2. Maximizing fairness between independent drivers by encouraging solutions that minimize the standard deviation of the ratio between miles driven with a passenger over miles driven without a passenger\n",
"3. Maximizing fairness between wait-time of customers by minimizing the standard deviation of customer's wait time\n",
"4. Maximizing profit by serving higher priority customers before lower priority customers. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The following is the multi-objective function that we wish to minimize:\n",
"\n",
"$$\\begin{align*}\n",
" f =& \\underbrace{\\alpha_{dh}\\sum_{j=1}^{|D|} \\left(\\sum_{i=1}^{|d_{j}|-1} dis(c_{i, dr}, c_{i+1, cl})) \\right) + dis(d_{j,0}, c_{1, cl})}_{\\text{Deadheading cost}}+ \\\\\n",
" & \\underbrace{\\alpha_{d}\\sqrt{\\frac{\\sum_{j=1}^{|D|} \\left(\\frac{Pr_j}{Dh_j} - \\mu_{pd}\\right)^2}{|D|}}}_{\\text{Driver fairness}} + \\underbrace{\\alpha_c\\sqrt{\\frac{ \\sum_{i=1}^{|C|} \\left(ds_i - \\mu_{ds}\\right)^2}{|C|}}}_{\\text{Customer fairness}} - \\\\\n",
" &\\underbrace{\\alpha_p\\sum_{j=1}^{|D|} \\sum_{i=1}^{|d_{j}|} \\frac{dis(c_{i, cl}, c_{i, dr}) * p_i}{ds_i}}_{\\text{Priority}} \n",
"\\end{align*}$$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Subject to:\n",
"\n",
"$$\n",
" \\alpha_{dh} + \\alpha_d + \\alpha_c + \\alpha_p = 1\n",
"$$\n",
"$$\n",
" \\sum_{\\substack{j=1 \\\\ c_i \\in d_j}}^{|D|} 1 = 1 \\quad \\forall c_i\n",
"$$\n",
"$$\n",
" \\sum_{\\substack{j=1}}^{|D|} |d_j| = |C|\n",
"$$\n",
"$$\n",
" ts_i \\leq T_{max} \\quad \\forall c_i\n",
"$$\n",
"\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The first constraint shown in eq.2, ensures that the weights of each objective sums up to one. In eq.3 we ensure that every customer is assigned to only one driver and therefore also guarantees that all customers are matched, while in eq.4, we ensure that all customers are matched only once (i.e., no customer is matched to the same driver twice). Finally, the last constraint shown in eq.5 ensures that the wait time for any customer has to be less than or equal $T_{max}$ otherwise the solution would not be feasible.\n",
"\n",
"The design variable in this problem is the list of lists defining the order of assignments of customers to drivers and is denoted by $D$."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Variables:\n",
"\n",
"- $C$: List of customers to be served.\n",
"- $c_i$: A unique ID assigned each customer\n",
"- $c_{i, cl}$: 2D collection point of the $i^{th}$ customer.\n",
"- $c_{i, dr}$: 2D drop-off point of the $i^{th}$ customer.\n",
"- $t(i, j)$: Drive time of the shortest path between node $i$ and $j$.\n",
"- $dis(i, j)$: Distance of the shortest path between node $i$ and $j$.\n",
"- $d_{j, 0}$: Node representing the initial 2D location of $j^{th}$ driver.\n",
"- $D$: List of assignments for participating drivers. Each element of the list is a list of customers assigned to a driver.\n",
"- $d_j$: Ordered list of customers assigned to $j_{th}$ driver. Order defines the sequence of service.\n",
"- $Pr_j$: Total profitable miles driven by $j^{th}$ driver.\n",
"- $Dh_j$: Total deadheading miles driven by $j^{th}$ driver.\n",
"- $\\mu_{pd}$: Mean ratio between $Pr_j$ and $Dh_j$ of all drivers for a given solution.\n",
"- $ds_i$: Distance travelled before serving $i^{th}$ customer.\n",
"- $ts_i$: Time taken before serving $i^{th}$ customer.\n",
"- $\\mu_{{ds}}$: Mean of distance travelled before serving any customer for a given solution.\n",
"- $T_{max}$: Maximum wait time for any customer.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Solution Class"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [],
"source": [
"class DeadHeadingProblemEvaluation:\n",
" \"\"\"Evaluate feasiblity and Cost of a solution to the Deadheading Problem.\n",
" \"\"\"\n",
"\n",
" def __init__(\n",
" self,\n",
" graph,\n",
" drivers,\n",
" riders,\n",
" alpha_deadheading=0.8,\n",
" alpha_driver_fairness=0.1,\n",
" alpha_rider_fairness=0.1\n",
" ):\n",
" \"\"\"Initialize with problem parameters and do some precomputation.\n",
" Parameters\n",
" ----------\n",
" graph: map consists of nodes (used to compute shortest path)\n",
" drivers : A list of driver objects\n",
" riders : A list of rider objects\n",
" \"\"\"\n",
" self.graph = graph\n",
" self.drivers = list(drivers)\n",
" self.riders = list(riders)\n",
"\n",
" # weights for cost function\n",
" self.alpha_deadheading = alpha_deadheading\n",
" self.alpha_driver_fairness = alpha_driver_fairness\n",
" self.alpha_rider_fairness = alpha_rider_fairness\n",
" assert self.alpha_deadheading + self.alpha_driver_fairness + self.alpha_rider_fairness == 1\n",
"\n",
" # Generate dist_matrix that is indexed by (from_osmid, to_osmid) and returns distance\n",
"\n",
" self.dist_matrix = {}\n",
"\n",
" def add_dist(from_id, to_id):\n",
" from_node = Node(graph=graph, osmid=from_id)\n",
" to_node = Node(graph=graph, osmid=to_id)\n",
" shortest_route = astar(\n",
" G=graph, origin=from_node, destination=to_node)\n",
" cost_route = cost(graph, shortest_route)\n",
" self.dist_matrix[(from_id, to_id)] = cost_route\n",
"\n",
" # driver initial -> rider pickup\n",
" for driver in drivers:\n",
" for rider in riders:\n",
" add_dist(driver.initial_map_node_id, rider.pickup_map_node_id)\n",
" # rider dropoff -> rider pickup\n",
" for rider1 in riders:\n",
" for rider2 in riders:\n",
" if rider1 is not rider2:\n",
" add_dist(rider1.dropoff_map_node_id,\n",
" rider2.pickup_map_node_id)\n",
" # rider pickup -> dropoff\n",
" for rider in riders:\n",
" add_dist(rider.pickup_map_node_id, rider.dropoff_map_node_id)\n",
"\n",
" def set_solution(self, solution):\n",
" \"\"\"Add solution to evaluate, and do some precomputation.\n",
" Parameters\n",
" ----------\n",
" solution: A list of lists\n",
" \"\"\"\n",
" self.solution = solution\n",
" # constraints\n",
" assert len(self.solution) == len(self.drivers)\n",
" self.unique_assignment_constraint()\n",
" self.match_all_riders_constraint()\n",
" # compute distances once to speed up evaluation\n",
" self.deadheading_miles, self.profitable_mile, self.prearrival_miles = self.get_driver_deadhead_profit()\n",
"\n",
" def evaluate_cost_func(self):\n",
" return self.alpha_deadheading * self.evaluate_deadheading_cost() + \\\n",
" self.alpha_driver_fairness * self.evaluate_driver_fairness() + \\\n",
" self.alpha_rider_fairness * self.evaluate_rider_fairness()\n",
"\n",
" def evaluate_deadheading_cost(self):\n",
" return sum(self.deadheading_miles)\n",
"\n",
" def evaluate_driver_fairness(self):\n",
" delta_pr_dh = [abs(pr-dh) for dh,\n",
" pr in zip(self.deadheading_miles, self.profitable_mile)]\n",
" return statistics.stdev(delta_pr_dh)\n",
"\n",
" def evaluate_rider_fairness(self):\n",
" # Flatten prearrival times into a 1D list\n",
" all_prearrival_miles = sum(self.prearrival_miles, [])\n",
" return statistics.stdev(all_prearrival_miles)\n",
"\n",
" # return deadhing miles and profitable miles for each driver\n",
" def get_driver_deadhead_profit(self):\n",
" deadheading_miles = [] # deadheading miles for each driver\n",
" profitable_miles = [] # profitable miles for each driver\n",
" prearrival_miles = [] # same format as solution, represents driver miles travelled before reaching this rider\n",
" # loop over drivers\n",
" for driver_idx, riders in enumerate(self.solution):\n",
" driver = self.drivers[driver_idx]\n",
" dh_miles = 0\n",
" pr_miles = 0\n",
" pa_miles = []\n",
" # loop over riders of current driver\n",
" for rider_idx, rider in enumerate(riders):\n",
" # deadheading\n",
" if rider_idx == 0:\n",
" dh_miles += self.dist(osmid_a=driver.initial_map_node_id,\n",
" osmid_b=rider.pickup_map_node_id)\n",
" else:\n",
" prev_rider = riders[rider_idx-1]\n",
" dh_miles += self.dist(osmid_a=prev_rider.dropoff_map_node_id,\n",
" osmid_b=rider.pickup_map_node_id)\n",
" # prearrival\n",
" pa_miles.append(pr_miles + dh_miles)\n",
" # profit\n",
" pr_miles += self.dist(osmid_a=rider.pickup_map_node_id,\n",
" osmid_b=rider.dropoff_map_node_id)\n",
" # populate total deadhing and profitable miles for current driver\n",
" deadheading_miles.append(dh_miles)\n",
" profitable_miles.append(pr_miles) \n",
" prearrival_miles.append(pa_miles)\n",
" return deadheading_miles, profitable_miles, prearrival_miles\n",
"\n",
" def unique_assignment_constraint(self):\n",
" # loop over riders\n",
" for rider in self.riders:\n",
" # loop over assiment list of each driver\n",
" num_of_assignments = 0\n",
" for driver_solution in self.solution:\n",
" current_ids = [rider.id for rider in driver_solution]\n",
" if rider.id in current_ids:\n",
" num_of_assignments += 1\n",
" assert num_of_assignments == 1, \"Error: Number of assignments for Rider: {} is {}, Expected assignment \\\n",
" is 1\".format(rider.id, num_of_assignments)\n",
"\n",
" def match_all_riders_constraint(self):\n",
" number_of_assignments = sum(\n",
" [len(driver_assigment) for driver_assigment in self.solution])\n",
" assert number_of_assignments == len(\n",
" self.riders), \"Number of assigned riders greater than number of riders!\"\n",
"\n",
" # Compute shortest path between two nodes on a graph\n",
" def dist(self, osmid_a: int, osmid_b: int):\n",
" return self.dist_matrix[(osmid_a, osmid_b)]"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Eval init time = 12.58279013633728 seconds\n"
]
}
],
"source": [
"start = time.time()\n",
"eval_obj = DeadHeadingProblemEvaluation(graph, drivers, riders)\n",
"end = time.time()\n",
"print(\"Eval init time = {} seconds\".format(end - start))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Solution 1: Simulated Annealing"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [],
"source": [
"class RandomSolution():\n",
" def __init__(self, drivers, riders):\n",
" self.drivers = drivers\n",
" self.riders = riders\n",
"\n",
" # setting random seed from config file\n",
" config = ConfigParser()\n",
" config.read('config.ini')\n",
" #random.seed(config.get('main', 'seed'))\n",
"\n",
" def get_random_sol(self):\n",
" # init random solution\n",
" rs = []\n",
" # add empty list for each driver\n",
" for i in range(len(self.drivers)):\n",
" rs.append([])\n",
" # add each rider to one driver\n",
" for rider in self.riders:\n",
" # random index within drivers list\n",
" r_idx = random.sample(range(len(self.drivers)), 1)[0]\n",
" rs[r_idx].append(rider)\n",
" return rs\n",
"\n",
"\n",
"class NeighborSolutions():\n",
" def __init__(self) -> None:\n",
" self.s = None\n",
"\n",
" def set_solution(self, s):\n",
" self.s = s\n",
"\n",
" def get_neigbors(self):\n",
" assert (self.s is not None), 'Must set solution first'\n",
" # driver swap\n",
" ds_sol = self.driver_swap(copy.deepcopy(self.s))\n",
" # rider reassign\n",
" rr_sol = self.rider_reassign(copy.deepcopy(self.s))\n",
" # rider shuffle\n",
" rs_sol = self.rider_shuffle(copy.deepcopy(self.s))\n",
" return [ds_sol, rr_sol, rs_sol]\n",
"\n",
" def get_or_sol(self):\n",
" \"\"\" gets original variable \"\"\"\n",
" return self.or_sol\n",
"\n",
" def get_ds_sol(self):\n",
" \"\"\" gets driver swap \"\"\"\n",
" return self.ds_sol\n",
"\n",
" def get_rr_sol(self):\n",
" \"\"\" gets rider reassign \"\"\"\n",
" return self.rr_sol\n",
"\n",
" def get_rs_sol(self):\n",
" \"\"\" gets rider shuffle \"\"\"\n",
" return self.rs_sol\n",
"\n",
" def driver_swap(self, s):\n",
" \"\"\" swaps all riders between two drivers\n",
"\n",
" Args:\n",
" s (list): original solution\n",
"\n",
" Returns:\n",
" list: new solution\n",
" \"\"\"\n",
" num_drivers = len(s)\n",
" idx_swap = random.sample(range(num_drivers), 2)\n",
" temp_data = s[idx_swap[0]]\n",
" s[idx_swap[0]] = s[idx_swap[1]]\n",
" s[idx_swap[1]] = temp_data\n",
" return s\n",
"\n",
" def rider_reassign(self, s):\n",
" \"\"\" reassigns one rider to another driver]\n",
"\n",
" Args:\n",
" s (list): original solution\n",
"\n",
" Returns:\n",
" list: new solution\n",
" \"\"\"\n",
" num_drivers = len(s)\n",
" while True:\n",
" # a driver giving one rider\n",
" d_giving = random.sample(range(num_drivers), 1)[0]\n",
" # checks the giving driver has at least one rider\n",
" if len(s[d_giving]) > 0:\n",
" break\n",
" r_available = len(s[d_giving]) # available number of riders\n",
" # index of moving rider\n",
" idx_moving = random.sample(range(r_available), 1)[0]\n",
" r_moving = s[d_giving].pop(idx_moving) # rider moving\n",
" while True:\n",
" d_receiving = random.sample(range(num_drivers), 1)[0]\n",
" # checks receiving driver is not giving driver\n",
" if d_giving != d_receiving:\n",
" c_riders = len(s[d_receiving])\n",
" if c_riders > 0:\n",
" idx_moving = random.sample(range(c_riders), 1)[0]\n",
" s[d_receiving].insert(idx_moving, r_moving)\n",
" else:\n",
" s[d_receiving].append(r_moving)\n",
" break\n",
" return s\n",
"\n",
" def rider_shuffle(self, s):\n",
" \"\"\" shuffles riders of one driver\n",
"\n",
" Args:\n",
" s (list): original solution\n",
"\n",
" Returns:\n",
" list: new solution\n",
" \"\"\"\n",
" num_drivers = len(s)\n",
" while True:\n",
" # driver shuffling its riders\n",
" d_shuffling = random.sample(range(num_drivers), 1)[0]\n",
" if len(s[d_shuffling]) > 1:\n",
" random.shuffle(s[d_shuffling])\n",
" break\n",
" return s\n",
"\n",
"def get_SA_parameters(drivers, riders, eval_obj, iterations, adaptive):\n",
" \"\"\" function that determines proper parameters for the SA method\n",
"\n",
" Args:\n",
" drivers ([type]): [description]\n",
" riders ([type]): [description]\n",
" eval_obj ([type]): [description]\n",
" iterations ([type]): [description]\n",
"\n",
" Returns:\n",
" [type]: [description]\n",
" \"\"\"\n",
" # number of random samples to get statistics from\n",
" num_samples = 100\n",
" # store cost of sample solutions\n",
" sample_sol_c = []\n",
" for i in range(num_samples):\n",
" # random solution\n",
" s = copy.deepcopy(RandomSolution(\n",
" drivers, riders).get_random_sol())\n",
" eval_obj.set_solution(s)\n",
" # cost of random solution\n",
" c = eval_obj.evaluate_cost_func()\n",
" sample_sol_c.append(c)\n",
" # average and standard deviation of cost\n",
" avg_c = np.array(sample_sol_c).mean()\n",
" std_c = np.array(sample_sol_c).std()\n",
" # setting initial temperature as\n",
" # 3.5 or 1.0 of the std of the cost of 100 random solutions\n",
" if adaptive:\n",
" k = 3.5 * std_c # encourage more exploration in adaptive version\n",
" else:\n",
" k = 1.5 * std_c\n",
" # final temperature is set to 0.1 std of cost\n",
" t_final = 0.1 * std_c\n",
" # extracting lambda from temperature equation\n",
" lam = -1 * (np.log(t_final/k)) / iterations\n",
" return (k, lam)\n",
"\n",
"\n",
"def runSA(drivers, riders, graph, eval_obj, k, lam, iterations, adaptive):\n",
" hist_solutions = []\n",
" hist_costs = []\n",
" sa_solver = SAnnealingSolver(\n",
" drivers, riders, graph, eval_obj, k, lam, adaptive)\n",
" solution_i, cost_i = sa_solver.get_status()\n",
" hist_solutions.append(solution_i)\n",
" hist_costs.append(cost_i)\n",
" for i in tqdm(range(iterations)):\n",
" sa_solver.obtain_neighbors()\n",
" sa_solver.pick_solution()\n",
" sa_solver.determine_next_solution()\n",
" solution_i, cost_i = sa_solver.get_status()\n",
" hist_solutions.append(solution_i)\n",
" hist_costs.append(cost_i)\n",
" return hist_solutions, hist_costs\n",
"\n",
"\n",
"def print_SA_states(states):\n",
" for i, value in enumerate(states):\n",
" print('--------------------------------------------')\n",
" print(f'Solution {i} cost -> {value[1]}')\n",
"\n",
"\n",
"def print_best(hist_costs):\n",
" best = np.array(hist_costs).min()\n",
" print('--------------------------------------------')\n",
" print('--------------------------------------------')\n",
" print(f'Best solution cost -> {best}')\n",
" print('--------------------------------------------')\n",
" print('--------------------------------------------')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Solver Class"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [],
"source": [
"class SAnnealingSolver():\n",
" def __init__(self, drivers, riders, graph, eval_obj, k, lam, adaptive=False) -> None:\n",
" # problem setup\n",
" self.drivers = drivers\n",
" self.riders = riders\n",
" self.graph = graph\n",
" self.iteration = 0\n",
" # parameters of temperature schedule\n",
" self.schedule = self.exp_schedule(k, lam)\n",
" # simulated annealing variables\n",
" self.current_s = copy.deepcopy(RandomSolution(\n",
" drivers, riders).get_random_sol())\n",
" self.next_s = []\n",
" self.eval_obj = eval_obj\n",
" self.cost_current_s = self.get_cost(self.current_s)\n",
" self.neighbors = []\n",
" self.ns_generator = NeighborSolutions()\n",
" self.adaptive = adaptive\n",
" self.best_seen = copy.deepcopy(self.current_s)\n",
"\n",
" # from https://smartmobilityalgorithms.github.io/book/content/TrajectoryAlgorithms/SimulatedAnnealing.html\n",
" def exp_schedule(self, k, lam):\n",
" # i corresponds to the iteration\n",
" def function(i): return (k * np.exp(-lam*i))\n",
" return function\n",
"\n",
" def obtain_neighbors(self):\n",
" self.ns_generator.set_solution(copy.deepcopy(self.current_s))\n",
" self.neighbors = copy.deepcopy(self.ns_generator.get_neigbors())\n",
"\n",
" def pick_solution(self):\n",
" # ------- adaptive part -------\n",
" if self.adaptive:\n",
" if self.get_cost(self.current_s) < self.get_cost(self.best_seen):\n",
" self.best_seen = self.current_s\n",
" candidate_from_neighbors = copy.deepcopy(\n",
" random.choice(self.neighbors))\n",
" self.next_s = copy.deepcopy(random.choice(\n",
" [candidate_from_neighbors, self.best_seen]))\n",
" else:\n",
" self.next_s = copy.deepcopy(random.choice(self.neighbors))\n",
"\n",
" def get_cost(self, s):\n",
" self.eval_obj.set_solution(s)\n",
" return self.eval_obj.evaluate_cost_func()\n",
"\n",
" def get_T(self, iteration):\n",
" return self.schedule(iteration)\n",
"\n",
" # from https://github.com/SmartMobilityAlgorithms/smart_mobility_utilities/blob/master/smart_mobility_utilities/common.py\n",
" def probability(self, p):\n",
" return p > random.uniform(0.0, 1.0)\n",
"\n",
" # from https://smartmobilityalgorithms.github.io/book/content/TrajectoryAlgorithms/SimulatedAnnealing.html\n",
" def determine_next_solution(self):\n",
" self.cost_current_s = self.get_cost(self.current_s)\n",
" cost_next_s = self.get_cost(self.next_s)\n",
" delta_e = cost_next_s - self.cost_current_s\n",
" self.iteration += 1\n",
" T = self.get_T(self.iteration)\n",
" if delta_e < 0 or self.probability(np.exp(-1 * delta_e/T)):\n",
" #coin = random.choice([1, 1, 1, 0, 0, 0, 0, 0])\n",
" # if delta_e < 0 or coin:\n",
" self.current_s = copy.deepcopy(self.next_s)\n",
"\n",
" def get_status(self):\n",
" return self.current_s, self.cost_current_s"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Visualization"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"100%|██████████| 500/500 [00:00<00:00, 792.95it/s]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"SA time = 0.6326920986175537 seconds\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n"
]
}
],
"source": [
"# Simulated Annealing part for evaluation script\n",
"iterations = 500\n",
"k, lam = get_SA_parameters(drivers, riders, eval_obj, iterations, adaptive=False)\n",
"start = time.time()\n",
"hist_solutions, hist_costs = runSA(\n",
" drivers, riders, graph, eval_obj, k, lam, iterations, adaptive=False)\n",
"end = time.time()\n",
"print(\"\")\n",
"print(\"SA time = {} seconds\".format(end - start))"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Final cost:20512.625309653413\n"
]
},
{
"data": {
"image/png": "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",
"text/plain": [
"