Skip to content

Commit

Permalink
greedy initial solution #31 #33
Browse files Browse the repository at this point in the history
  • Loading branch information
Romain MONTAGNÉ committed Jun 16, 2020
1 parent 9506ad2 commit db16caf
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 3 deletions.
118 changes: 118 additions & 0 deletions vrpy/greedy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from networkx import DiGraph, add_path, shortest_path
from random import choice
import logging

logger = logging.getLogger(__name__)


class Greedy:
"""
Greedy algorithm. Iteratively adds closest feasible node to current path.
Args:
G (DiGraph): Graph on which algorithm is run.
load_capacity (int, optional) : Maximum load per route. Defaults to None.
num_stops (int, optional) : Maximum stops per route. Defaults to None.
"""

def __init__(self, G, load_capacity=None, num_stops=None):
self.G = G.copy()
self._format_cost()
self._best_routes = []
self._unprocessed_nodes = [
v for v in self.G.nodes() if v not in ["Source", "Sink"]
]

if isinstance(load_capacity, list):
# capacity of vehicle type 1 is used!
self.load_capacity = load_capacity[0]
else:
self.load_capacity = load_capacity
self.num_stops = num_stops

self._best_value = 0

def run(self):
"""The forward search is run."""
while self._unprocessed_nodes != []:
self._load = 0
self._stops = 0
self._run_forward()
self._update_routes()

def _run_forward(self):
"""
A path starting from Source is greedily extended
until Sink is reached.
The procedure aborts if path becomes infeasible.
"""
self._current_path = ["Source"]
while True:
self._get_next_node()
self._update()
if self._new_node == "Sink":
break

def _get_next_node(self):
self._last_node = self._current_path[-1]
out_going_costs = {}
# Store the successors cost that meet constraints
for v in self.G.successors(self._last_node):
if self._constraints_met(v):
if v in self._unprocessed_nodes:
out_going_costs[v] = self.G.edges[self._last_node, v]["cost"]
if out_going_costs == {}:
logger.debug("path cannot be extended")
self._new_node = "Sink"
self._best_value += self.G.edges[self._last_node, "Sink"]["cost"]
else:
# Select best successor
self._new_node = sorted(out_going_costs, key=out_going_costs.get)[0]
self._best_value += out_going_costs[self._new_node]

def _constraints_met(self, v):
"""Checks if constraints are respected."""
if v in self._current_path or self._check_source_sink(v):
return False
elif self.load_capacity and not self._check_capacity(v):
return False
else:
return True

def _update(self):
"""Updates path, path load, unprocessed nodes."""
self._load += self.G.nodes[self._new_node]["demand"]
self._current_path.append(self._new_node)
if self._new_node not in ["Source", "Sink"]:
self._unprocessed_nodes.remove(self._new_node)
self._stops += 1
if self._stops == self.num_stops:
# End path
self._new_node = "Sink"
self._current_path.append("Sink")

def _update_routes(self):
"""Stores best routes as list of nodes."""
self._best_routes.append(self._current_path)

def _check_source_sink(self, v):
"""Checks if edge Source Sink."""
return self._last_node == "Source" and v == "Sink"

def _check_capacity(self, v):
"""Checks capacity constraint."""
return self._load + self.G.nodes[v]["demand"] <= self.load_capacity

def _format_cost(self):
"""If list of costs is given, first item of list is considered."""
for (i, j) in self.G.edges():
if isinstance(self.G.edges[i, j]["cost"], list):
self.G.edges[i, j]["cost"] = self.G.edges[i, j]["cost"][0]

@property
def best_value(self):
return self._best_value

@property
def best_routes(self):
return self._best_routes
23 changes: 20 additions & 3 deletions vrpy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
from time import time

from vrpy.greedy import Greedy
from vrpy.master_solve_pulp import MasterSolvePulp
from vrpy.subproblem_lp import SubProblemLP
from vrpy.subproblem_cspy import SubProblemCSPY
Expand Down Expand Up @@ -344,7 +345,7 @@ def _find_columns(self):
# self._get_time_remaining(),
self._time_limit,
# 30,
# exact=False,
exact=False,
)
if self._more_routes:
break
Expand Down Expand Up @@ -484,8 +485,21 @@ def _get_initial_solution(self):
self.G, self.load_capacity, self.duration, self.num_stops
)
alg.run()
logger.info("Initial solution found with value %s" % alg.best_value)
self._initial_routes = alg.best_routes
logger.info(
"Clarke & Wright solution found with value %s and %s vehicles"
% (alg.best_value, len(alg.best_routes))
)

# Run greedy algorithm if possible
if not self.duration:
alg = Greedy(self.G, self.load_capacity)
alg.run()
logger.info(
"Greedy solution found with value %s and %s vehicles"
% (alg.best_value, len(alg.best_routes))
)
self._initial_routes += alg.best_routes

# If pickup and delivery, initial routes are Source-pickup-delivery-Sink
elif self.pickup_delivery:
Expand Down Expand Up @@ -520,7 +534,10 @@ def _convert_initial_routes_to_digraphs(self):
G.graph["vehicle_type"] = 0
self._routes.append(G)
for v in r:
self._routes_with_node[v] = [G]
if v in self._routes_with_node:
self._routes_with_node[v].append(G)
else:
self._routes_with_node[v] = [G]

def _get_num_stops_upper_bound(self, max_capacity):
"""
Expand Down

0 comments on commit db16caf

Please sign in to comment.