Skip to content

Commit 26edb25

Browse files
authored
Add files via upload
1 parent 31afb8c commit 26edb25

12 files changed

+356
-0
lines changed

gls/__init__.py

Whitespace-only changes.
191 Bytes
Binary file not shown.
13 KB
Binary file not shown.
758 Bytes
Binary file not shown.
3.46 KB
Binary file not shown.
1.75 KB
Binary file not shown.
541 Bytes
Binary file not shown.

gls/base_solver.py

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
from ortools.constraint_solver import pywrapcp, routing_enums_pb2
2+
3+
from gls.data_model import ProblemInstance
4+
from gls.solver_model import SolverSetting
5+
6+
7+
class Solver:
8+
"""
9+
Solver object that takes a problem instance as input, creates and solves a capacitated vehicle routing problem with time
10+
windows. Objective of the optimization are hierarchical: 1) Minimize number of vehicles 2) Minimize total distance.
11+
Distance is Euclidean, and the value of travel time is equal to the value of distance between two nodes.
12+
13+
Parameters
14+
----------
15+
data : ProblemInstance
16+
Problem data according to ProblemInstance model.
17+
time_precision_scaler : int
18+
Variable defining the precision of travel and service times, e.g. 100 means precision of two decimals.
19+
"""
20+
21+
def __init__(self, data: ProblemInstance, time_precision_scaler: int):
22+
self.data = data
23+
self.time_precision_scaler = time_precision_scaler
24+
self.manager = None
25+
self.routing = None
26+
self.solution = None
27+
28+
def create_model(self):
29+
"""
30+
Create vehicle routing model for Solomon instance.
31+
"""
32+
# Create the routing index manager, i.e. number of nodes, vehicles and depot
33+
self.manager = pywrapcp.RoutingIndexManager(
34+
len(self.data["time_matrix"]), self.data["num_vehicles"], self.data["depot"]
35+
)
36+
37+
# Create routing model
38+
self.routing = pywrapcp.RoutingModel(self.manager)
39+
40+
# Create and register a transit callback
41+
def time_callback(from_index, to_index):
42+
"""Returns the travel time between the two nodes."""
43+
# Convert from solver internal routing variable Index to time matrix NodeIndex.
44+
from_node = self.manager.IndexToNode(from_index)
45+
to_node = self.manager.IndexToNode(to_index)
46+
return self.data["time_matrix"][from_node][to_node]
47+
48+
transit_callback_index = self.routing.RegisterTransitCallback(time_callback)
49+
50+
# Define cost of each arc and fixed vehicle cost
51+
self.routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
52+
# Make sure to first minimize number of vehicles
53+
self.routing.SetFixedCostOfAllVehicles(100000)
54+
55+
# Create and register demand callback
56+
def demand_callback(from_index):
57+
"""Returns the demand of the node."""
58+
# Convert from routing variable Index to demands NodeIndex.
59+
from_node = self.manager.IndexToNode(from_index)
60+
return self.data["demands"][from_node]
61+
62+
demand_callback_index = self.routing.RegisterUnaryTransitCallback(
63+
demand_callback
64+
)
65+
66+
# Register vehicle capacitites
67+
self.routing.AddDimensionWithVehicleCapacity(
68+
demand_callback_index,
69+
0, # null capacity slack
70+
self.data["vehicle_capacities"], # vehicle maximum capacities
71+
True, # start cumul to zero
72+
"Capacity",
73+
)
74+
75+
# Add Time Windows constraint.
76+
self.routing.AddDimension(
77+
transit_callback_index,
78+
10 ** 10, # allow waiting time at nodes
79+
10 ** 10, # maximum time per vehicle route
80+
False, # Don't force start cumul to zero, i.e. vehicles can start after time 0 from depot
81+
"Time",
82+
)
83+
84+
# Allow to drop nodes.
85+
# penalty = 1000
86+
# for node in range(1, len(self.data["distance_matrix"])):
87+
# self.routing.AddDisjunction([self.manager.NodeToIndex(node)], penalty)
88+
# end penalty
89+
90+
time_dimension = self.routing.GetDimensionOrDie("Time")
91+
92+
# Add time window constraints for each location except depot.
93+
for location_idx, time_window in enumerate(self.data["time_windows"]):
94+
if location_idx == self.data["depot"]:
95+
continue
96+
index = self.manager.NodeToIndex(location_idx)
97+
time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1])
98+
99+
# Add time window constraints for each vehicle start node.
100+
depot_idx = self.data["depot"]
101+
for vehicle_id in range(self.data["num_vehicles"]):
102+
index = self.routing.Start(vehicle_id)
103+
time_dimension.CumulVar(index).SetRange(
104+
self.data["time_windows"][depot_idx][0],
105+
self.data["time_windows"][depot_idx][1],
106+
)
107+
# The solution finalizer is called each time a solution is found during search
108+
# and tries to optimize (min/max) variables values
109+
for i in range(self.data["num_vehicles"]):
110+
self.routing.AddVariableMinimizedByFinalizer(
111+
time_dimension.CumulVar(self.routing.Start(i))
112+
)
113+
self.routing.AddVariableMinimizedByFinalizer(
114+
time_dimension.CumulVar(self.routing.End(i))
115+
)
116+
117+
def solve_model(self, settings: SolverSetting):
118+
"""
119+
Solver model with solver settings.
120+
121+
Parameters
122+
----------
123+
settings : SolverSetting
124+
Solver settings according to SolverSetting model.
125+
"""
126+
127+
# Setting first solution heuristic.
128+
search_parameters = pywrapcp.DefaultRoutingSearchParameters()
129+
search_parameters.first_solution_strategy = (
130+
routing_enums_pb2.FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION
131+
)
132+
search_parameters.local_search_metaheuristic = (
133+
routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH
134+
)
135+
search_parameters.time_limit.seconds = settings["time_limit"]
136+
137+
# Solve the problem.
138+
self.solution = self.routing.SolveWithParameters(search_parameters)
139+
140+
def print_solution(self):
141+
"""
142+
Print solution to console.
143+
"""
144+
print(f"Solution status: {self.routing.status()}\n")
145+
if self.routing.status() == 1:
146+
print(
147+
f"Objective: {self.solution.ObjectiveValue()/self.time_precision_scaler}\n"
148+
)
149+
time_dimension = self.routing.GetDimensionOrDie("Time")
150+
cap_dimension = self.routing.GetDimensionOrDie("Capacity")
151+
total_time = 0
152+
total_vehicles = 0
153+
for vehicle_id in range(self.data["num_vehicles"]):
154+
index = self.routing.Start(vehicle_id)
155+
plan_output = f"Route for vehicle {vehicle_id}:\n"
156+
while not self.routing.IsEnd(index):
157+
time_var = time_dimension.CumulVar(index)
158+
cap_var = cap_dimension.CumulVar(index)
159+
plan_output += f"{self.manager.IndexToNode(index)} -> "
160+
index = self.solution.Value(self.routing.NextVar(index))
161+
time_var = time_dimension.CumulVar(index)
162+
plan_output += f"{self.manager.IndexToNode(index)}\n"
163+
plan_output += f"Time of the route: {self.solution.Min(time_var)/self.time_precision_scaler}min\n"
164+
plan_output += f"Load of vehicle: {self.solution.Min(cap_var)}\n"
165+
print(plan_output)
166+
total_time += self.solution.Min(time_var) / self.time_precision_scaler
167+
if self.solution.Min(time_var) > 0:
168+
total_vehicles += 1
169+
total_travel_time = (
170+
total_time
171+
- sum(self.data["service_times"]) / self.time_precision_scaler
172+
)
173+
print(f"Total time of all routes: {total_time}min")
174+
print(f"Total travel time of all routes: {total_travel_time}min")
175+
print(f"Total vehicles used: {total_vehicles}")
176+
177+
def get_solution(self):
178+
"""
179+
Get solution as list of lists of nodes.
180+
Skip empty routes.
181+
"""
182+
routes = []
183+
if self.routing.status() == 1:
184+
time_dimension = self.routing.GetDimensionOrDie("Time")
185+
for vehicle_id in range(self.data["num_vehicles"]):
186+
index = self.routing.Start(vehicle_id)
187+
route = []
188+
while not self.routing.IsEnd(index):
189+
index = self.solution.Value(self.routing.NextVar(index))
190+
node = self.manager.IndexToNode(index)
191+
if node != self.data["depot"]:
192+
route.append(node)
193+
time_var = time_dimension.CumulVar(index)
194+
if self.solution.Min(time_var) > 0:
195+
routes.append(route)
196+
return routes
197+
198+
def get_solution_time(self):
199+
"""
200+
Get solution time value.
201+
"""
202+
if self.routing.status() == 1:
203+
time_dimension = self.routing.GetDimensionOrDie("Time")
204+
total_time = 0
205+
for vehicle_id in range(self.data["num_vehicles"]):
206+
index = self.routing.Start(vehicle_id)
207+
while not self.routing.IsEnd(index):
208+
time_var = time_dimension.CumulVar(index)
209+
index = self.solution.Value(self.routing.NextVar(index))
210+
time_var = time_dimension.CumulVar(index)
211+
total_time += self.solution.Min(time_var) / self.time_precision_scaler
212+
return total_time
213+
else:
214+
return None
215+
216+
# get total travel time
217+
def get_solution_travel_time(self):
218+
"""
219+
Get solution travel time value.
220+
"""
221+
if self.routing.status() == 1:
222+
time_dimension = self.routing.GetDimensionOrDie("Time")
223+
total_travel_time = 0
224+
for vehicle_id in range(self.data["num_vehicles"]):
225+
index = self.routing.Start(vehicle_id)
226+
while not self.routing.IsEnd(index):
227+
time_var = time_dimension.CumulVar(index)
228+
index = self.solution.Value(self.routing.NextVar(index))
229+
time_var = time_dimension.CumulVar(index)
230+
total_travel_time += self.solution.Min(time_var) / self.time_precision_scaler
231+
total_travel_time = total_travel_time - sum(self.data["service_times"]) / self.time_precision_scaler
232+
return total_travel_time
233+
else:
234+
return None

gls/data_model.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from pydantic import BaseModel
2+
3+
4+
class ProblemInstance(BaseModel):
5+
time_matrix: list
6+
time_windows: list
7+
demands: list
8+
depot: int
9+
num_vehicles: int
10+
vehicle_capacities: list
11+
service_times: list

gls/instance_loader.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import math
2+
import re
3+
4+
import pandas as pd
5+
6+
from gls.data_model import ProblemInstance
7+
8+
9+
def load_instance(problem_path: str, time_precision_scaler: int) -> ProblemInstance:
10+
"""
11+
Load instance of Solomon benchmark with defined precision scaler.
12+
13+
Parameters
14+
----------
15+
time_precision_scaler : int
16+
Variable defining the precision of travel and service times, e.g. 100 means precision of two decimals.
17+
"""
18+
19+
data = {}
20+
data["depot"] = 0
21+
df = pd.read_csv(
22+
problem_path,
23+
sep="\s+",
24+
skiprows=8,
25+
names=[
26+
"customer",
27+
"xcord",
28+
"ycord",
29+
"demand",
30+
"ready_time",
31+
"due_date",
32+
"service_time",
33+
],
34+
)
35+
df["service_time"] = df["service_time"] * time_precision_scaler
36+
df["ready_time"] = df["ready_time"] * time_precision_scaler
37+
df["due_date"] = df["due_date"] * time_precision_scaler
38+
39+
data["service_times"] = list(df.service_time)
40+
41+
travel_times = df[["xcord", "ycord", "service_time"]].to_dict()
42+
time_matrix = []
43+
for i in df.customer:
44+
time_vector = []
45+
for j in df.customer:
46+
if i == j:
47+
time_vector.append(0)
48+
else:
49+
time = int(
50+
time_precision_scaler
51+
* math.hypot(
52+
(travel_times["xcord"][i] - travel_times["xcord"][j]),
53+
(travel_times["ycord"][i] - travel_times["ycord"][j]),
54+
)
55+
)
56+
time += travel_times["service_time"][j]
57+
time_vector.append(time)
58+
time_matrix.append(time_vector)
59+
data["time_matrix"] = time_matrix
60+
61+
with open(problem_path) as f:
62+
lines = f.readlines()
63+
data["num_vehicles"] = int(re.findall("[0-9]+", lines[4])[0])
64+
data["vehicle_capacities"] = [int(re.findall("[0-9]+", lines[4])[1])] * data[
65+
"num_vehicles"
66+
]
67+
data["demands"] = list(df.demand)
68+
69+
windows = df[["ready_time", "due_date", "service_time"]].to_dict()
70+
time_windows = []
71+
for i in df.customer:
72+
time_windows.append(
73+
(
74+
windows["ready_time"][i] + windows["service_time"][i],
75+
windows["due_date"][i] + windows["service_time"][i],
76+
)
77+
)
78+
data["time_windows"] = time_windows
79+
80+
data["xcord"] = list(df.xcord)
81+
data["ycord"] = list(df.ycord)
82+
83+
return data

0 commit comments

Comments
 (0)