Skip to content

Commit

Permalink
[#147] Add routes w different vehicle types when possible
Browse files Browse the repository at this point in the history
  • Loading branch information
torressa committed Apr 28, 2023
1 parent ff325cb commit 2aae5c9
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 23 deletions.
31 changes: 31 additions & 0 deletions tests/test_issue147.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from networkx import DiGraph
from vrpy import VehicleRoutingProblem
import networkx as nx

from networkx import DiGraph
from vrpy import VehicleRoutingProblem


class TestIssue147:
def setup(self):
G = DiGraph()
for v in [1, 2, 3, 4]:
G.add_edge("Source", v, cost=[10, 11, 10, 10])
G.add_edge(v, "Sink", cost=[10, 11, 10, 10])
G.nodes[v]["demand"] = 10
G.add_edge(1, 2, cost=[10, 11, 10, 10])
G.add_edge(2, 3, cost=[10, 11, 10, 10])
G.add_edge(3, 4, cost=[15, 16, 10, 10])

self.prob = VehicleRoutingProblem(
G, mixed_fleet=True, load_capacity=[10, 10, 10, 10], use_all_vehicles=True
)

def test(self):
self.prob.solve()
assert self.prob.best_routes_type == {1: 0, 2: 1, 3: 2, 4: 3}

def test_set_num_vehicles(self):
self.prob.num_vehicles = [1, 1, 0, 2]
self.prob.solve()
assert set(self.prob.best_routes_type.values()) == set([0, 1, 3])
4 changes: 2 additions & 2 deletions tests/test_issue99.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from networkx import from_numpy_matrix, set_node_attributes, relabel_nodes, DiGraph
from networkx import from_numpy_array, set_node_attributes, relabel_nodes, DiGraph
from numpy import array
from vrpy import VehicleRoutingProblem

Expand Down Expand Up @@ -236,7 +236,7 @@ def setup(self):

# Transform distance matrix to DiGraph
A_ = array(distance_, dtype=[("cost", int)])
G_ = from_numpy_matrix(A_, create_using=DiGraph())
G_ = from_numpy_array(A_, create_using=DiGraph())

# Set demand
set_node_attributes(G_, values=demands, name="demand")
Expand Down
30 changes: 16 additions & 14 deletions vrpy/subproblem_cspy.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import logging
from math import floor

from numpy import zeros
from networkx import DiGraph, add_path
import networkx as nx
from numpy import zeros, unique

from cspy import BiDirectional, REFCallback


from vrpy.subproblem import _SubProblemBase

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -216,12 +218,12 @@ def solve(self, time_limit):
my_callback = self.get_REF()
direction = (
"forward"
if (
self.time_windows
or self.pickup_delivery
or self.distribution_collection
)
else "both"
# if (
# self.time_windows
# or self.pickup_delivery
# or self.distribution_collection
# )
# else "both"
)
# Run only twice: Once with `elementary=False` check if route already
# exists.
Expand All @@ -241,7 +243,7 @@ def solve(self, time_limit):
else:
thr = None
logger.debug(
f"Solving subproblem using elementary={elementary}, threshold={thr}, direction={direction}"
f"Solving subproblem using elementary={elementary}, threshold={thr}, direction={direction}, max_res={self.max_res}, min_res={self.min_res}"
)
alg = BiDirectional(
self.sub_G,
Expand All @@ -252,6 +254,7 @@ def solve(self, time_limit):
time_limit=time_limit - 0.5 if time_limit else None,
elementary=elementary,
REF_callback=my_callback,
two_cycle_elimination=True
# pickup_delivery_pairs=self.pickup_delivery_pairs,
)

Expand All @@ -268,6 +271,7 @@ def solve(self, time_limit):
if alg.total_cost is not None and alg.total_cost < -(1e-3):
new_route = self.create_new_route(alg.path)
logger.debug(alg.path)

path_len = len(alg.path)
if not any(
list(new_route.edges()) == list(r.edges()) for r in self.routes
Expand Down Expand Up @@ -323,8 +327,8 @@ def create_new_route(self, path):
"""Create new route as DiGraph and add to pool of columns"""
e = "elem" if len(set(path)) == len(path) else "non-elem"
route_id = "{}_{}".format(len(self.routes) + 1, e)
new_route = DiGraph(name=route_id, path=path)
add_path(new_route, path)
new_route = nx.DiGraph(name=route_id, path=path)
nx.add_path(new_route, path)
total_cost = 0
for (i, j) in new_route.edges():
edge_cost = self.sub_G.edges[i, j]["cost"][self.vehicle_type]
Expand Down Expand Up @@ -376,6 +380,4 @@ def get_REF(self):
self.T,
self.resources,
)
else:
# Use default
return None
return None
50 changes: 43 additions & 7 deletions vrpy/vrp.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from time import time
from typing import List, Union

import numpy as np
from networkx import DiGraph, shortest_path # draw_networkx

from vrpy.greedy import _Greedy
Expand Down Expand Up @@ -950,8 +951,24 @@ def _convert_initial_routes_to_digraphs(self):
Converts list of initial routes to list of Digraphs.
By default, initial routes are computed with the first feasible vehicle type.
"""

def _check_candidate(k):
if (
sum(self.G.nodes[v]["demand"] for v in r) <= self.load_capacity[k]
) and types_used[k] + 1 <= vehicles_available[k]:
G.graph["vehicle_type"] = k
types_used[k] += 1
return True
return False

self._routes = []
self._routes_with_node = {}
types_used = np.zeros(self._vehicle_types)

if not self.num_vehicles:
vehicles_available = np.array([len(self.G.nodes) - 2] * self._vehicle_types)
else:
vehicles_available = np.array(self.num_vehicles)
for route_id, r in enumerate(self._initial_routes, start=1):
total_cost = 0
G = DiGraph(name=route_id)
Expand All @@ -962,13 +979,32 @@ def _convert_initial_routes_to_digraphs(self):
total_cost += edge_cost
G.graph["cost"] = total_cost
if self.load_capacity:
for k in range(len(self.load_capacity)):
if (
sum(self.G.nodes[v]["demand"] for v in r)
<= self.load_capacity[k]
):
G.graph["vehicle_type"] = k
break
if self.mixed_fleet:
# Check vehicle_types but first try the ones that are not
# already assigned to a route.
diff = vehicles_available - types_used
candidates = np.argwhere(diff == np.amax(diff)).flatten().tolist()
vehicle_type_found = False
for k in candidates:
vehicle_type_found = _check_candidate(k)
if vehicle_type_found:
break
# In case this is not enough, now try the rest
if not vehicle_type_found:
candidates = [
k for k in range(self._vehicle_types) if k not in candidates
]
for k in candidates:
vehicle_type_found = _check_candidate(k)
if vehicle_type_found:
break
else:
for k in range(self._vehicle_types):
if (
sum(self.G.nodes[v]["demand"] for v in r)
<= self.load_capacity[k]
):
G.graph["vehicle_type"] = k
else:
G.graph["vehicle_type"] = 0
if "vehicle_type" not in G.graph:
Expand Down

0 comments on commit 2aae5c9

Please sign in to comment.