Skip to content

Instantly share code, notes, and snippets.

@270ajay
Last active August 26, 2018 14:30
Show Gist options
  • Save 270ajay/4ffcbd07477a9da57316490bfe433b47 to your computer and use it in GitHub Desktop.
Save 270ajay/4ffcbd07477a9da57316490bfe433b47 to your computer and use it in GitHub Desktop.
Optimizing Vehicle Routing Problem with Time Windows using Constraint Programming & Metaheuristics. Uses Google OR tools. -- Finds very good solution for large problem in seconds. Check https://developers.google.com/optimization/reference/constraint_solver/routing/RoutingModel/, https://developers.google.com/optimization/reference/constraint_sol…
"""Capacitated Vehicle Routing Problem with Time Windows (CVRPTW).
"""
import csv
import math
from ortools.constraint_solver import pywrapcp
from ortools.constraint_solver import routing_enums_pb2
"""Check https://developers.google.com/optimization/reference/constraint_solver/routing/RoutingModel/,
https://developers.google.com/optimization/reference/constraint_solver/routing/RoutingDimension/,
and https://developers.google.com/optimization/reference/constraint_solver/constraint_solver/Solver/
for more features. Check their examples in c++ (more number of examples) and python at
https://github.com/google/or-tools/tree/master/examples."""
###^##################### PARAMETERS, COSTS, AND SWITCHES ###############################################
####^#################### ###############################################
SOLVERTIMER = 30 # in seconds
NumberOfLocations = 100 # excluding depot 0. Need to change this /!\
NumberOfVehicles = 70
# --CAPACITY OF VEHICLE PARAMETERS
CapacityOfVehicles = 75 # ignore if SameCapacityORDifferentCapacities = 1
CapacitiesOfVehicles = [75, 50, 60, 30, 50] # ignore if SameCapacityORDifferentCapacities = 0
# --DEPOT PARAMETERS
depot = 0 # ignore if SingleDepotORMultipleStartEndLocations = 1
startLocations = [1, 2, 1, 2, 1] # ignore if SingleDepotORMultipleStartEndLocations = 0
endLocations = [1, 2, 1, 2, 1] # ignore if SingleDepotORMultipleStartEndLocations = 0
# --RESOURCE AT DEPOT PARAMETERS
VehicleLoadTime = 1 # ignore if ResourceConstraintAtDepot = 0
VehicleUnloadTime = 1 # ignore if ResourceConstraintAtDepot = 0
DepotCapacity = 2 # space available in depot, ignore if ResourceConstraintAtDepot = 0
DepotUsage = [1, 1, 1, 1, 1] # space each vehicle takes when loading & unloading, ignore if ResourceConstraintAtDepot = 0
MinimizeMaxOfDistanceTraveledCOST = 50 # ignore if MinimizeMaxOfDistanceTraveled = 0
MaxDistanceVehicleTravel = 12 # ignore if MinimizeMaxOfDistanceTraveled = 0
MinimizeNumberOfVehiclesCOST = 10 # ignore if MinimizeNumberOfVehicles = 0, and SameVehicleCostORDifferentCosts = 1
MinimizeNumberOfVehiclesDiffCOSTS = [10, 10, 15, 20, 10] # ignore if MinimizeNumberOfVehicles = 0, and SameVehicleCostORDifferentCosts = 0
CustomersCanBeSkippedWithPenaltyCOST = 100 # ignore if CustomersCanBeSkippedWithPenalty = 0
VehicleReturnToDepotTime = 44 # ignore if TimeWindowConstraint = 0
LimitEachVehicleDistance = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] # ignore if LimitEachVehicleTravel = 0
RestBreakTime = [24, 28, 2] # ignore if AllowVehicleToTakeBreak = 0 [startTimeMin, startTimeMax, duration]
InitialRoutes = [[1, 2, 5], [8, 3], [9, 6], [10, 4, 7], []] # ignore if ResumeOptFromSolutionEntered is 0, length of it == NumberOfVehicles
#------------------------------------------------------------------------------------------------------
# APPLICATION SWITCHES (0 or 1)
#------------------------------------------------------------------------------------------------------
SingleDepotORMultipleStartEndLocations = 0 # 0 -> use single depot, 1 -> use multiple start-end locations
SameCapacityORDifferentCapacities = 0 # 0 -> use same capacity for all vehicles, 1 -> use different capacities
SameVehicleCostORDifferentCosts = 0 # 0 -> use same cost for all vehicles, 1 -> use different costs
MinimizeTransitTimes = 1 # 0 -> solver will not minimize travel times/transit, 1 -> solver will minimize transit
MinimizeMaxOfDistanceTraveled = 0 # 0 -> solver will not try to minimize the max/highest distance traveled among vehicles
LimitEachVehicleTravel = 0 # 0 -> there is no limit on how much dist. vehicle can travel, 1 -> limit dist. for each vehicle
MinimizeNumberOfVehicles = 1 # 0 -> solver will not try to minimize the number of vehicles used, 1 -> minimize vehicles
CapacityConstraint = 1 # 0 -> constraint is off, 1 -> constraint in on
TimeWindowConstraint = 1 # 0 -> constraint is off, 1 -> constraint in on
ResourceConstraintAtDepot = 0 # 0 -> constraint is off, 1 -> constraint in on
CustomersCanBeSkippedWithPenalty = 0 # 0 -> All customers must be served, 1 -> customer(s) can be skipped with penalty cost
AllowVehicleToTakeBreak = 0 # 0 -> vehicles will not take lunch/rest break, 1 -> vehicles will take break
StoreSolutionToResumeOptLater = 0 # 0 -> Does not store/print solution, 1 -> stores/print solution which can be used to resume optimization later
ResumeOptFromSolutionEntered = 0 # 0 -> Starts optimization normally, 1 -> Starts optimization from the solution you have entered
##################################################################################################
##################################################################################################
#------------------------------------------------------------------------------------------------------
# GETTING & PREPARING DATA
#------------------------------------------------------------------------------------------------------
locations = list(range(NumberOfLocations + 1))
num_locations = len(locations)
if SingleDepotORMultipleStartEndLocations == 1:
assert NumberOfVehicles == len(startLocations) == len(endLocations) # Checks whether start and end locations are equal to number of vehicles
if SameCapacityORDifferentCapacities == 1:
assert NumberOfVehicles == len(CapacitiesOfVehicles) # check if capacity of vehicle information is available to every vehicle
if SameVehicleCostORDifferentCosts == 1:
assert NumberOfVehicles == len(MinimizeNumberOfVehiclesDiffCOSTS)
if ResourceConstraintAtDepot == 1:
assert NumberOfVehicles*2 == len(DepotUsage)
if ResumeOptFromSolutionEntered == 1:
assert NumberOfVehicles == len(InitialRoutes)
if LimitEachVehicleTravel == 1:
assert NumberOfVehicles == len(LimitEachVehicleDistance)
# -----------------------READING FROM TRAVEL TIMES DATA FILE AND STORING---------------------------
with open('travel_times.csv') as csvfile:
data = list(csv.reader(csvfile))
travelTimes = {}
for i in range(1, NumberOfLocations + 1):
for j in range(1, NumberOfLocations + 1):
if data[j][i].isdigit() and i != j:
travelTimes[(i, j)] = float(int(data[j][i]) / 10)
elif data[j][i].isdigit() and i == j:
travelTimes[(i, j)] = 0
# Addition of 0 as it is depot in this model.
for i in range(1, NumberOfLocations + 1):
travelTimes[(0, i)] = 0
travelTimes[(i, 0)] = 0
travelTimes[(0, 0)] = 0
# Addition of symmetrical travel times
for i in range(2, NumberOfLocations + 1):
for j in range(1, i):
travelTimes[(i, j)] = travelTimes[(j, i)]
# -----------------------READING FROM LOCATION DATA FILE AND STORING---------------------------
with open('location_data_CP.csv') as csvfile1:
data1 = list(csv.reader(csvfile1))
time_windows = []
for i in range(1, NumberOfLocations + 2):
time_windows.append((int(data1[i][6]), int(data1[i][7])))
serviceTimes = []
for i in range(1, NumberOfLocations + 2):
serviceTimes.append(int(data1[i][9]))
demands = []
for i in range(1, NumberOfLocations + 2):
demands.append(int(data1[i][1]))
#------------------------------------------------------------------------------------------------------
# FOR PRINTING THE RESULTS
#------------------------------------------------------------------------------------------------------
class ConsolePrinter():
"""Print solution to console"""
def __init__(self, routing, assignment):
"""Initializes the printer"""
self.routing = routing
self.assignment = assignment
def print(self):
"""Prints assignment on console"""
# Inspect solution.
capacity_dimension = self.routing.GetDimensionOrDie('Capacity')
time_dimension = self.routing.GetDimensionOrDie('Time')
total_dist = 0
total_time = 0
for vehicle_id in range(NumberOfVehicles):
if self.routing.IsVehicleUsed(self.assignment, vehicle_id):
index = self.routing.Start(vehicle_id)
plan_output = 'Route for vehicle {0}:\n'.format(vehicle_id)
route_dist = 0
while not self.routing.IsEnd(index):
node_index = self.routing.IndexToNode(index)
next_node_index = self.routing.IndexToNode(
self.assignment.Value(self.routing.NextVar(index)))
route_dist += travelTimes[node_index, next_node_index]
load_var = capacity_dimension.CumulVar(index)
route_load = self.assignment.Value(load_var)
time_var = time_dimension.CumulVar(index)
time_min = self.assignment.Min(time_var)
time_max = self.assignment.Max(time_var)
slack_var = time_dimension.SlackVar(index)
slack_min = self.assignment.Min(slack_var)
slack_max = self.assignment.Max(slack_var)
plan_output += ' {0} Load({1}) Time({2},{3}) Slack({4},{5}) ->'.format(
node_index,
route_load,
time_min, time_max,
slack_min, slack_max)
index = self.assignment.Value(self.routing.NextVar(index))
node_index = self.routing.IndexToNode(index)
load_var = capacity_dimension.CumulVar(index)
route_load = self.assignment.Value(load_var)
time_var = time_dimension.CumulVar(index)
route_time = self.assignment.Value(time_var)
time_min = self.assignment.Min(time_var)
time_max = self.assignment.Max(time_var)
total_dist += route_dist
total_time += route_time
plan_output += ' {0} Load({1}) Time({2},{3})\n'.format(node_index, route_load, time_min, time_max)
plan_output += 'Distance of the route: {0}m\n'.format(route_dist)
plan_output += 'Load of the route: {0}\n'.format(route_load)
plan_output += 'Time of the route: {0}min\n'.format(route_time)
print(plan_output)
print('Total Distance of all routes: {0}m'.format(total_dist))
print('Total Time of all routes: {0}min'.format(total_time))
def get_routes_array(self):
# Get the routes for an assignment and return as a list of lists.
routes = []
for vehicle_id in range(NumberOfVehicles):
if self.routing.IsVehicleUsed(self.assignment, vehicle_id):
node = self.routing.Start(vehicle_id)
route=[]
while not self.routing.IsEnd(node):
index = self.routing.NodeToIndex(node)
if not (node == self.routing.Start(vehicle_id)):
route.append(index)
node = self.assignment.Value(self.routing.NextVar(node))
else:
node = self.assignment.Value(self.routing.NextVar(node))
routes.append(route)
else:
continue
return routes
# ------------------------------------------------------------------------------------------------------
# FUNCTIONS USED IN THE CONSTRAINTS/OBJECTIVE FUNCTIONS
# ------------------------------------------------------------------------------------------------------
def distance_evaluator(from_node, to_node): # Need to change this for different data
return travelTimes[from_node, to_node] * 10
def demand_evaluator(from_node, to_node):
del to_node
return demands[from_node]
def time_evaluator(from_node, to_node):
return int(math.ceil(serviceTimes[from_node] + travelTimes[from_node, to_node]))
#------------------------------------------------------------------------------------------------------
# MAIN - THE PROGRAM STARTS FROM HERE
#------------------------------------------------------------------------------------------------------
def main():
# Creating Routing Model
if SingleDepotORMultipleStartEndLocations == 0:
routing = pywrapcp.RoutingModel(num_locations, NumberOfVehicles, depot)
elif SingleDepotORMultipleStartEndLocations == 1:
routing = pywrapcp.RoutingModel(num_locations, NumberOfVehicles, startLocations, endLocations)
# Set cost of each edge/arc in the objective function (for minimizing transit/travel times)
if MinimizeTransitTimes == 1:
routing.SetArcCostEvaluatorOfAllVehicles(distance_evaluator)
# Set cost for minimizing the max distance among vehicles in the objective function
# /!\ It doesn't mean the standard deviation is minimized
if MinimizeMaxOfDistanceTraveled == 1 or LimitEachVehicleTravel == 1:
distance = "Distance"
routing.AddDimension(distance_evaluator, 0, MaxDistanceVehicleTravel, True, distance)
distance_dimension = routing.GetDimensionOrDie(distance)
if MinimizeMaxOfDistanceTraveled == 1:
distance_dimension.SetGlobalSpanCostCoefficient(MinimizeMaxOfDistanceTraveledCOST)
if LimitEachVehicleTravel == 1:
for vehicle in range(NumberOfVehicles):
distance_dimension.SetSpanUpperBoundForVehicle(LimitEachVehicleDistance[vehicle], vehicle)
# Set cost of each vehicle used in the objective function. This is used for minimizing the number of vehicle used.
if MinimizeNumberOfVehicles == 1:
if SameVehicleCostORDifferentCosts == 0:
routing.SetFixedCostOfAllVehicles(MinimizeNumberOfVehiclesCOST)
elif SameVehicleCostORDifferentCosts == 1:
for vehicle in range(NumberOfVehicles):
routing.SetVehicleFixedCost(vehicle, MinimizeNumberOfVehiclesDiffCOSTS[vehicle])
# Constraint to ensure that vehicle capacity is satisfied
if CapacityConstraint == 1:
capacity = "Capacity"
if SameCapacityORDifferentCapacities == 0:
routing.AddDimension(demand_evaluator, 0, CapacityOfVehicles, True, capacity)
elif SameCapacityORDifferentCapacities == 1:
routing.AddDimensionWithVehicleCapacity(demand_evaluator, 0, CapacitiesOfVehicles, True, capacity)
# Constraint to ensure that time windows are satisfied
if TimeWindowConstraint == 1:
time = "Time"
routing.AddDimension(time_evaluator, VehicleReturnToDepotTime, VehicleReturnToDepotTime, False, time)
time_dimension = routing.GetDimensionOrDie(time)
for location_idx, time_window in enumerate(time_windows):
if location_idx == 0:
continue
index = routing.NodeToIndex(location_idx)
time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1])
routing.AddToAssignment(time_dimension.SlackVar(index))
for vehicle_id in range(NumberOfVehicles):
index = routing.Start(vehicle_id)
time_dimension.CumulVar(index).SetRange(time_windows[0][0], time_windows[0][1])
routing.AddToAssignment(time_dimension.SlackVar(index))
# Adding resource constraints at the depot (start and end location of routes).
# Adding constraints that allow vehicle to take lunch/rest breaks.
if ResourceConstraintAtDepot == 1 or AllowVehicleToTakeBreak == 1:
solver = routing.solver()
if ResourceConstraintAtDepot == 1:
intervals = []
for i in range(NumberOfVehicles):
# Add time windows at start of routes
intervals.append(solver.FixedDurationIntervalVar(routing.CumulVar(routing.Start(i), time), VehicleLoadTime, "depot_interval"))
# Add time windows at end of routes.
intervals.append(solver.FixedDurationIntervalVar(routing.CumulVar(routing.End(i), time), VehicleUnloadTime, "depot_interval"))
# Constrain the number of maximum simultaneous intervals at depot.
solver.AddConstraint(solver.Cumulative(intervals, DepotUsage, DepotCapacity, "depot"))
# Instantiate route start and end times to produce feasible times.
for i in range(NumberOfVehicles):
routing.AddVariableMinimizedByFinalizer(routing.CumulVar(routing.End(i), time))
routing.AddVariableMinimizedByFinalizer(routing.CumulVar(routing.Start(i), time))
if AllowVehicleToTakeBreak == 1:
for j in range(NumberOfVehicles):
breakVar = [solver.FixedDurationIntervalVar(RestBreakTime[0], RestBreakTime[1], RestBreakTime[2], False, "break_interval")]
time_dimension.SetBreakIntervalsOfVehicle(breakVar, j)
# Can be used to skip locations. If a location is skipped, a penalty cost(100) is added to the objective function.
if CustomersCanBeSkippedWithPenalty == 1:
skipOrder = []
skipOrder.append(0)
for i in range(1,routing.nodes()):
skipOrder[0] = i
routing.AddDisjunction(skipOrder, CustomersCanBeSkippedWithPenaltyCOST)
# ------------------------------------------------------------------------------------------------------
# SOLVER PARAMETERS
# ------------------------------------------------------------------------------------------------------
search_parameters = pywrapcp.RoutingModel.DefaultSearchParameters()
search_parameters.local_search_metaheuristic = routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH
search_parameters.time_limit_ms = SOLVERTIMER * 1000
# search_parameters.optimization_step(1)
if ResumeOptFromSolutionEntered == 1:
initialAssignment = routing.ReadAssignmentFromRoutes(InitialRoutes, True)
print("******************Initial Assignment*****************: \n")
initialPrinter = ConsolePrinter(routing, initialAssignment)
initialPrinter.print()
print("#################################################################################")
print("\n**********************Final Assignment*****************: ")
assignment = routing.SolveFromAssignmentWithParameters(initialAssignment, search_parameters)
elif ResumeOptFromSolutionEntered == 0:
# Setting first solution heuristic (cheapest addition).
search_parameters.first_solution_strategy = routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC
# Solving the problem
assignment = routing.SolveWithParameters(search_parameters)
# ------------------------------------------------------------------------------------------------------
# OTHERS
# ------------------------------------------------------------------------------------------------------
print("Status of Optimization "
"(0-Not Solved, 1-Solution Found,2-No Solution Found,"
"3-No Solution found within time limit,"
"4-Model Invalid): ", routing.status())
print()
printer = ConsolePrinter(routing, assignment)
printer.print()
print()
count = 0
for i in range(NumberOfVehicles):
if routing.IsVehicleUsed(assignment,i):
print("Vehicle:",i," is used")
count += 1
print()
print("Total number of vehicles used: ", count)
print()
print("Objective Value :", assignment.ObjectiveValue())
print()
print("Number of locations unvisited: ", routing.GetNumberOfDisjunctions())
print()
# To print routes array which can be read later to find solutions. Need to store this to excel, etc /!\
if StoreSolutionToResumeOptLater == 1:
print("Routes which can be used to resume Optimization later:")
print(printer.get_routes_array())
if __name__ == '__main__':
main()
Location Demand TimeIn TimeOut ServiceTime
Point0 0 0 0 0
Point1 23 20 28 2
Point2 24 20 28 1
Point3 17 20 28 2
Point4 25 20 28 2
Point5 13 20 28 1
Point6 29 20 28 1
Point7 24 20 28 1
Point8 25 20 28 1
Point9 26 20 28 1
Point10 22 20 28 1
Point11 11 20 28 1
Point12 16 20 28 1
Point13 25 20 28 1
Point14 23 20 28 1
Point15 13 20 28 1
Point16 18 20 28 2
Point17 23 20 28 2
Point18 21 20 28 1
Point19 15 20 28 1
Point20 20 20 28 1
Point21 13 28 36 1
Point22 20 28 36 2
Point23 15 28 36 1
Point24 12 28 36 2
Point25 18 28 36 2
Point26 12 28 36 2
Point27 27 28 36 2
Point28 10 28 36 1
Point29 14 28 36 1
Point30 17 28 36 2
Point31 26 28 36 1
Point32 16 28 36 2
Point33 10 28 36 1
Point34 30 28 36 1
Point35 23 28 36 2
Point36 16 28 36 1
Point37 30 28 36 1
Point38 28 28 36 1
Point39 14 28 36 1
Point40 22 28 36 2
Point41 30 36 40 1
Point42 16 36 40 2
Point43 29 36 40 1
Point44 17 36 40 1
Point45 13 36 40 1
Point46 22 36 40 2
Point47 21 36 40 2
Point48 14 36 40 1
Point49 18 36 40 2
Point50 13 36 40 2
Point51 16 20 28 2
Point52 10 20 28 1
Point53 17 20 28 2
Point54 23 20 28 2
Point55 11 20 28 1
Point56 27 20 28 1
Point57 22 20 28 1
Point58 26 20 28 1
Point59 21 20 28 1
Point60 11 20 28 1
Point61 18 20 28 1
Point62 28 20 28 1
Point63 13 20 28 1
Point64 25 20 28 1
Point65 10 20 28 1
Point66 21 20 28 2
Point67 23 20 28 2
Point68 15 20 28 1
Point69 19 20 28 1
Point70 18 20 28 1
Point71 14 28 36 1
Point72 30 28 36 2
Point73 29 28 36 1
Point74 22 28 36 2
Point75 17 28 36 2
Point76 23 28 36 2
Point77 17 28 36 2
Point78 26 28 36 1
Point79 17 28 36 1
Point80 29 28 36 2
Point81 28 28 36 1
Point82 21 28 36 2
Point83 21 28 36 1
Point84 18 28 36 1
Point85 18 28 36 2
Point86 20 28 36 1
Point87 17 28 36 1
Point88 16 28 36 1
Point89 13 28 36 1
Point90 12 28 36 2
Point91 23 36 40 1
Point92 28 36 40 2
Point93 11 36 40 1
Point94 24 36 40 1
Point95 25 36 40 1
Point96 14 36 40 2
Point97 17 36 40 2
Point98 21 36 40 1
Point99 29 36 40 2
Point100 24 36 40 2
drive_time_sec Point1 Point2 Point3 Point4 Point5 Point6 Point7 Point8 Point9 Point10 Point11 Point12 Point13 Point14 Point15 Point16 Point17 Point18 Point19 Point20 Point21 Point22 Point23 Point24 Point25 Point26 Point27 Point28 Point29 Point30 Point31 Point32 Point33 Point34 Point35 Point36 Point37 Point38 Point39 Point40 Point41 Point42 Point43 Point44 Point45 Point46 Point47 Point48 Point49 Point50 Point51 Point52 Point53 Point54 Point55 Point56 Point57 Point58 Point59 Point60 Point61 Point62 Point63 Point64 Point65 Point66 Point67 Point68 Point69 Point70 Point71 Point72 Point73 Point74 Point75 Point76 Point77 Point78 Point79 Point80 Point81 Point82 Point83 Point84 Point85 Point86 Point87 Point88 Point89 Point90 Point91 Point92 Point93 Point94 Point95 Point96 Point97 Point98 Point99 Point100
Point1 0
Point2 7 0
Point3 8 12 0
Point4 13 16 9 0
Point5 5 6 7 9 0
Point6 8 12 15 19 17 0
Point7 20 20 7 15 13 8 0
Point8 8 9 20 17 10 12 16 0
Point9 18 5 17 13 17 13 13 12 0
Point10 9 19 9 6 6 18 11 6 8 0
Point11 8 8 15 13 15 14 14 9 17 10 0
Point12 5 6 6 17 18 5 10 7 17 15 14 0
Point13 10 13 8 11 11 20 10 13 13 9 12 19 0
Point14 10 18 14 15 6 18 11 8 6 7 14 10 8 0
Point15 19 9 7 14 8 14 13 7 19 8 6 9 10 14 0
Point16 6 14 13 16 18 16 12 13 13 15 9 6 12 13 10 0
Point17 11 11 8 18 7 8 15 9 10 5 12 18 16 14 7 19 0
Point18 5 9 6 13 8 14 6 8 17 11 11 13 8 12 14 9 18 0
Point19 14 9 7 19 17 19 14 9 13 9 9 10 16 11 6 16 20 13 0
Point20 15 16 9 13 7 15 7 19 9 19 8 13 18 18 10 20 13 17 7 0
Point21 17 10 8 8 20 14 7 19 12 17 6 10 12 11 9 10 9 5 8 9 0
Point22 7 11 18 7 16 8 18 13 7 11 19 9 8 13 5 12 16 17 6 6 6 0
Point23 14 5 13 10 14 11 19 11 20 19 16 13 9 6 17 15 16 5 13 13 9 19 0
Point24 19 15 13 18 19 17 14 19 11 11 14 11 11 15 14 8 15 14 18 5 17 19 19 0
Point25 9 7 5 11 7 18 9 5 9 10 15 6 6 6 15 16 5 11 6 5 12 9 17 16 0
Point26 10 20 16 13 10 16 14 13 18 12 14 11 16 13 9 5 8 6 6 11 6 11 14 20 14 0
Point27 12 8 16 16 15 10 8 18 14 10 16 16 20 13 14 15 10 12 6 14 13 18 13 13 11 10 0
Point28 20 8 11 19 15 19 10 14 9 19 12 11 10 15 15 12 15 9 8 17 9 8 8 20 7 7 16 0
Point29 5 14 9 17 15 10 17 6 10 15 19 13 15 20 15 6 9 6 16 12 15 19 7 16 15 7 10 5 0
Point30 13 13 18 11 8 20 18 19 12 9 15 8 19 16 20 17 20 16 17 17 20 13 8 5 19 6 9 12 15 0
Point31 20 9 8 10 10 7 15 13 14 8 5 12 17 20 19 14 9 14 7 13 20 10 18 17 14 19 8 6 8 6 0
Point32 6 7 6 6 19 13 18 17 12 13 7 16 18 13 17 11 13 13 9 18 18 17 16 16 15 12 15 11 13 18 14 0
Point33 15 6 5 7 19 19 6 11 10 18 19 18 19 15 20 17 10 7 13 8 18 16 16 8 8 8 10 13 5 11 7 16 0
Point34 8 5 18 10 10 7 10 6 6 18 17 19 19 11 6 19 13 16 19 12 15 17 20 19 7 7 19 14 18 10 14 20 11 0
Point35 5 16 10 15 9 6 9 14 8 10 14 6 16 6 7 9 8 15 20 8 19 7 12 9 16 15 13 10 10 7 16 14 7 15 0
Point36 19 14 19 18 14 7 8 16 6 12 19 8 11 16 17 10 12 8 8 20 8 10 18 5 17 6 9 16 8 20 5 10 18 9 19 0
Point37 11 8 14 16 13 8 8 18 8 18 15 9 7 18 19 8 11 15 5 17 9 18 17 9 5 13 7 5 8 18 15 18 5 17 13 11 0
Point38 5 17 15 16 12 12 14 6 20 15 10 8 15 17 9 8 8 10 20 9 18 13 6 20 6 12 19 8 5 8 8 10 18 7 11 11 13 0
Point39 12 18 9 19 17 14 13 10 15 17 7 7 14 19 5 13 7 5 6 13 12 11 6 10 20 15 17 11 15 15 13 11 6 15 15 6 8 11 0
Point40 19 14 9 14 20 7 19 12 19 6 18 8 18 18 20 12 12 17 14 12 19 13 9 11 6 17 16 9 11 14 7 11 11 15 12 19 10 17 5 0
Point41 6 15 8 6 17 7 7 17 11 7 19 6 13 20 12 17 6 12 15 10 8 18 20 10 9 13 14 8 20 16 5 17 19 6 16 18 5 19 14 14 0
Point42 10 15 11 9 6 19 19 15 13 6 18 19 8 15 12 19 18 11 18 20 17 15 9 16 12 11 14 19 7 12 10 14 13 17 6 5 10 18 17 7 20 0
Point43 13 6 7 16 16 14 11 18 18 15 10 14 11 17 5 14 9 6 15 20 13 19 10 10 15 10 8 11 5 12 18 6 20 13 5 15 20 8 14 5 9 8 0
Point44 5 12 5 15 17 16 6 10 5 6 5 8 9 11 19 20 18 6 8 20 12 16 20 6 17 7 7 8 16 10 14 13 20 13 20 14 19 14 11 10 12 7 15 0
Point45 18 11 6 17 6 9 19 8 12 9 9 10 17 13 13 17 18 17 6 12 18 10 16 18 18 5 10 20 14 10 9 10 18 16 12 17 9 13 10 7 5 7 17 16 0
Point46 13 9 11 9 14 17 18 10 10 8 20 9 7 20 8 20 20 19 7 7 8 12 19 14 15 16 17 7 18 7 16 15 10 19 9 9 7 16 17 7 12 14 18 9 6 0
Point47 6 11 14 17 14 9 10 18 6 9 19 18 8 8 15 20 16 12 8 11 11 6 14 20 14 19 11 6 8 20 8 19 11 9 16 10 13 10 19 20 12 9 15 16 5 18 0
Point48 14 18 18 12 10 9 6 14 14 10 10 18 14 18 10 14 12 13 20 19 14 7 8 8 12 7 18 7 14 16 12 12 8 14 20 9 15 11 17 12 13 16 19 15 18 13 8 0
Point49 9 18 20 13 13 13 12 17 14 16 19 12 11 18 12 20 19 6 12 19 17 16 12 6 7 13 7 15 5 20 5 17 7 13 10 15 14 13 14 18 15 11 18 10 11 17 17 18 0
Point50 19 14 11 17 12 10 5 15 18 10 17 19 9 19 7 15 11 15 7 18 15 18 13 20 8 18 20 7 10 5 15 8 8 12 18 11 6 18 15 14 5 20 16 12 12 6 5 11 12 0
Point51 18 10 6 8 18 5 14 7 16 7 9 15 13 9 7 20 18 12 5 17 6 5 17 5 11 7 8 6 12 20 15 12 18 8 9 16 19 19 13 18 8 9 16 10 13 11 8 18 16 18 0
Point52 10 13 14 7 10 11 5 12 7 19 5 6 6 20 19 16 20 7 9 20 16 16 19 15 13 20 15 15 18 19 12 7 19 18 12 17 7 12 14 7 15 16 7 10 6 6 10 13 14 16 5 0
Point53 8 17 7 13 6 20 9 12 9 6 15 9 17 9 11 14 16 11 12 8 7 15 18 15 17 18 11 11 18 9 14 8 17 20 17 20 14 6 17 8 7 17 13 15 17 16 16 8 12 7 8 8 0
Point54 16 9 9 18 20 14 8 7 7 16 17 5 12 20 16 7 19 11 18 14 7 10 9 5 18 5 11 19 19 20 5 6 12 9 13 8 19 12 11 20 17 19 9 6 16 11 7 14 13 15 5 9 5 0
Point55 12 16 6 20 8 17 19 9 18 9 8 5 19 15 18 17 15 16 12 11 7 19 20 11 13 8 13 16 18 20 10 17 11 7 10 15 15 13 12 17 13 14 13 20 15 7 11 15 5 7 7 7 17 11 0
Point56 11 8 7 13 5 11 6 14 9 14 13 18 5 15 15 15 19 11 15 19 5 20 20 20 14 7 14 5 13 13 17 12 9 7 9 10 12 19 18 20 14 14 6 16 7 10 17 11 19 10 6 11 5 11 19 0
Point57 9 15 6 8 8 8 6 20 19 15 9 8 13 17 18 19 10 12 6 18 6 5 5 17 5 18 17 10 7 10 10 17 14 12 7 9 10 18 10 5 17 19 12 12 11 16 19 15 11 7 8 7 7 6 6 8 0
Point58 16 14 18 10 7 8 20 14 20 20 18 5 7 10 12 11 5 11 11 10 7 9 15 16 15 8 12 8 7 12 10 16 6 17 8 20 13 6 11 12 18 16 10 6 6 13 10 10 7 16 5 8 19 15 7 13 10 0
Point59 17 16 19 14 11 9 13 10 10 15 15 20 15 8 14 15 13 9 15 10 16 14 19 13 11 12 7 7 9 11 6 11 18 13 11 14 16 11 12 8 12 11 18 19 18 9 6 13 13 8 13 16 7 7 6 14 9 15 0
Point60 16 5 6 6 10 18 5 6 15 6 11 13 18 16 16 18 6 8 17 15 19 5 9 9 9 6 9 15 6 9 11 11 16 8 9 9 18 8 8 9 19 7 15 5 7 10 18 5 13 13 11 20 6 18 5 11 14 6 6 0
Point61 19 7 20 7 11 7 10 17 18 9 18 17 10 9 18 6 14 17 18 10 8 5 11 7 16 5 15 13 12 19 19 6 9 20 6 13 15 5 9 19 6 10 16 6 14 15 10 8 13 10 20 13 11 12 15 14 11 8 5 8 0
Point62 17 15 13 14 5 6 12 12 9 20 11 20 10 20 20 18 6 18 14 11 15 18 7 20 20 17 9 11 12 10 20 17 15 11 7 9 14 8 7 12 19 15 13 15 6 8 14 16 8 8 17 8 6 7 10 19 16 20 16 10 19 0
Point63 19 5 13 10 15 8 9 17 8 9 13 6 11 12 16 5 6 7 7 18 6 13 6 10 6 15 19 11 14 19 5 8 10 8 8 5 13 18 11 16 18 9 18 5 15 19 6 17 18 10 5 7 11 12 19 16 15 14 10 10 19 12 0
Point64 13 5 15 7 14 12 17 11 8 15 9 18 16 18 17 11 10 18 18 5 7 18 16 8 14 16 7 11 10 17 6 19 7 20 9 15 14 16 10 15 8 14 8 16 18 11 6 20 7 11 6 9 7 7 8 17 16 7 13 17 8 20 7 0
Point65 16 9 11 11 16 16 10 20 15 17 14 18 6 18 9 15 18 9 17 17 18 13 16 20 11 8 5 9 7 8 14 10 12 8 20 15 12 10 18 11 13 7 9 14 20 17 9 10 10 7 19 19 13 16 20 17 18 6 17 14 15 20 12 19 0
Point66 12 5 20 16 9 6 12 19 14 14 13 11 9 19 11 17 6 5 7 16 9 17 15 20 15 13 16 9 5 10 9 12 8 15 6 19 10 19 11 9 6 10 10 11 10 11 17 17 17 17 12 15 17 6 10 16 9 19 16 10 16 7 9 15 20 0
Point67 11 7 14 14 9 10 19 11 6 18 13 6 15 20 19 11 17 19 15 17 15 8 19 13 17 15 14 12 12 20 10 8 7 9 20 20 9 19 10 12 11 13 18 11 7 8 9 14 18 18 7 17 10 14 18 14 9 8 13 5 5 18 20 14 18 20 0
Point68 8 20 15 17 15 18 20 13 19 7 12 15 17 9 13 17 14 20 12 5 18 12 7 10 10 11 7 16 8 18 9 8 9 20 5 12 13 20 15 5 10 14 19 11 6 12 5 17 8 15 5 11 14 12 19 15 15 6 12 14 18 7 16 13 9 12 7 0
Point69 15 14 10 20 9 8 5 7 6 8 9 7 10 6 18 5 10 6 14 16 19 8 9 7 17 6 11 5 13 8 15 18 11 7 6 7 16 7 19 19 6 7 14 19 20 16 13 5 18 6 18 14 5 6 20 12 20 10 11 7 6 13 9 7 15 12 20 7 0
Point70 6 20 10 16 10 19 8 19 15 15 8 5 14 12 15 5 11 16 9 7 12 12 6 10 18 18 9 14 16 6 19 5 8 8 12 8 17 6 12 7 6 17 18 9 18 6 12 5 12 7 9 5 8 8 5 8 11 18 18 7 14 9 19 12 12 10 16 11 9 0
Point71 7 8 18 19 17 13 9 16 13 10 9 12 13 19 20 5 11 16 6 8 13 13 11 19 19 11 13 9 15 14 7 5 20 10 17 12 15 16 16 15 7 11 14 14 11 20 19 5 19 20 15 19 15 6 15 13 10 5 7 14 12 9 18 9 10 17 15 11 14 8 0
Point72 9 6 15 6 10 13 11 8 9 9 10 17 5 5 11 12 17 16 12 14 10 6 17 5 7 16 19 8 7 14 8 20 19 19 15 6 10 7 16 5 13 8 16 5 13 6 10 20 14 6 15 18 16 11 8 19 11 12 20 17 13 20 10 17 10 15 9 17 18 7 20 0
Point73 18 15 5 16 15 20 19 20 17 5 5 10 7 16 20 19 8 5 7 15 12 9 17 11 7 20 9 16 5 20 20 11 17 10 20 7 8 5 13 14 20 11 14 13 10 16 14 17 10 6 7 6 16 6 15 14 8 12 20 13 12 19 9 14 12 8 8 9 8 17 17 14 0
Point74 17 5 6 17 7 18 7 5 9 11 12 17 14 9 12 12 7 19 10 20 6 20 16 15 6 18 10 12 10 16 18 10 9 8 7 5 12 12 14 18 19 5 11 7 7 12 20 13 14 8 10 19 5 19 8 14 15 13 13 11 12 20 11 12 8 14 17 13 7 10 13 18 8 0
Point75 5 15 5 12 12 8 16 20 19 13 10 9 8 18 12 6 12 9 19 13 13 13 5 12 14 20 10 6 17 11 8 13 6 16 17 11 17 15 7 14 5 5 7 16 7 19 14 10 18 14 7 9 16 9 14 20 15 12 7 14 9 7 17 16 12 14 12 6 10 11 12 20 13 12 0
Point76 16 20 18 5 13 13 13 6 13 18 6 19 5 6 12 5 6 6 18 12 7 20 16 5 19 15 15 11 10 9 11 15 19 9 17 5 17 18 12 10 10 10 12 17 13 9 16 7 13 20 5 20 18 5 17 16 8 19 20 16 13 11 9 20 11 5 17 10 8 16 18 11 16 9 14 0
Point77 13 6 14 13 16 18 16 6 8 19 8 13 6 8 17 19 12 6 7 11 15 19 19 13 11 17 15 20 6 18 17 15 6 16 13 8 19 12 5 11 15 6 14 15 10 17 15 8 15 18 20 13 17 18 15 13 6 6 9 16 14 8 8 5 18 13 9 16 16 10 14 17 13 6 9 20 0
Point78 17 14 15 16 9 9 17 7 18 5 20 10 18 14 15 14 6 16 10 18 9 18 14 5 19 19 10 12 8 19 9 18 16 19 9 7 14 7 10 17 13 13 8 10 14 15 20 9 10 10 19 9 10 9 18 15 10 12 20 12 8 18 12 12 17 18 8 16 18 6 16 15 20 9 10 18 17 0
Point79 18 5 11 6 5 19 5 5 9 14 5 9 14 17 18 16 16 11 19 12 10 6 7 17 8 7 9 12 7 9 5 10 17 7 5 12 18 17 19 9 16 11 7 5 15 13 6 19 13 9 15 19 5 10 10 19 15 19 13 19 13 14 12 17 6 18 10 8 18 19 11 5 7 11 20 9 10 8 0
Point80 11 20 19 11 18 14 5 16 19 16 5 7 15 16 18 6 9 16 10 20 9 20 8 13 10 16 15 13 15 5 18 9 12 12 9 10 18 10 12 5 14 13 10 11 8 10 17 16 13 18 14 16 13 16 19 10 19 16 5 16 18 14 16 17 15 9 8 19 17 13 19 15 14 16 19 13 18 12 16 0
Point81 13 9 6 15 6 16 6 13 6 16 6 6 12 14 20 14 20 8 9 20 17 11 5 17 20 14 15 11 5 11 6 11 17 9 5 16 12 8 12 12 17 5 10 9 15 20 19 15 20 6 9 14 12 14 6 15 6 7 19 6 11 8 8 8 15 11 12 9 9 10 14 18 8 7 7 14 14 12 15 17 0
Point82 7 13 7 5 18 18 6 11 11 17 19 14 19 17 14 17 11 12 16 18 18 16 14 10 8 20 12 15 6 19 10 12 7 7 9 6 12 19 14 13 17 6 9 11 14 20 10 12 10 16 16 19 10 11 10 9 6 9 18 9 20 12 14 7 17 20 9 20 8 8 19 16 8 11 15 5 5 19 16 6 10 0
Point83 5 6 5 13 5 12 20 5 17 5 11 17 11 8 11 7 6 12 17 16 18 11 9 8 18 16 19 14 6 12 5 15 10 8 9 17 9 20 14 18 13 5 6 14 10 12 11 15 8 11 11 18 18 17 15 19 6 5 19 7 17 17 10 9 5 13 5 15 10 20 7 11 5 11 14 14 12 16 15 15 6 16 0
Point84 5 13 9 16 10 9 20 5 14 7 7 12 20 7 14 20 17 13 13 14 18 5 17 7 16 8 13 11 14 5 8 19 7 14 17 10 6 9 10 14 19 20 16 11 5 17 16 5 16 16 13 18 5 8 6 16 20 20 15 17 6 16 15 11 5 17 17 11 10 7 8 15 20 12 17 11 20 7 17 15 5 11 9 0
Point85 7 5 12 11 20 10 7 20 16 7 5 18 8 20 5 12 19 12 12 6 6 12 5 13 8 11 17 12 8 15 19 10 19 17 10 14 7 15 19 11 5 12 13 8 8 19 13 5 13 10 5 13 11 15 8 15 17 13 18 9 18 17 12 18 8 10 10 9 16 14 18 15 16 19 12 14 13 5 19 8 10 19 16 12 0
Point86 7 19 10 11 7 10 6 11 9 10 7 5 10 16 18 17 15 11 10 5 16 18 7 14 6 6 15 9 16 12 10 18 13 19 19 16 9 15 18 7 7 9 10 20 20 18 8 15 7 15 19 6 11 10 5 8 17 9 15 16 14 10 14 16 14 12 5 18 5 14 7 7 15 16 18 9 15 13 13 14 11 16 7 11 18 0
Point87 15 7 17 16 20 11 9 10 19 13 10 14 13 7 18 10 12 15 8 7 5 18 9 18 18 10 10 18 7 20 7 7 8 7 19 17 13 7 12 16 14 16 15 6 18 17 20 5 14 16 9 18 18 14 14 6 20 20 7 7 10 8 20 16 20 19 19 14 6 15 16 7 19 13 6 19 7 19 16 15 20 8 19 8 13 14 0
Point88 19 5 18 10 12 10 20 5 16 8 10 19 7 13 6 15 5 10 7 19 5 10 11 12 17 9 16 5 20 12 11 9 13 18 12 10 8 8 5 8 18 15 5 13 14 18 9 12 9 17 16 18 6 16 6 15 10 13 9 10 15 7 10 5 8 14 15 16 14 19 17 10 11 14 18 16 15 10 12 19 18 6 16 7 9 12 7 0
Point89 14 20 13 8 5 8 16 16 14 6 6 6 20 11 14 9 7 10 11 19 12 10 17 8 11 6 14 12 20 17 11 9 12 15 20 13 15 18 20 17 5 13 16 15 10 20 6 15 7 20 14 16 16 16 9 11 20 13 6 11 20 10 17 20 10 6 8 10 18 5 12 5 20 14 18 16 12 12 12 11 10 18 9 19 17 13 17 20 0
Point90 20 10 16 12 19 13 18 7 6 10 11 16 13 18 6 7 18 14 7 13 19 10 19 20 20 14 8 5 18 17 17 14 7 6 9 14 15 19 5 14 13 19 7 8 8 20 12 6 7 19 10 7 7 5 15 7 13 13 14 14 13 14 11 13 13 10 20 6 12 14 14 6 9 14 5 18 10 12 10 17 10 16 13 9 15 11 16 11 15 0
Point91 7 8 10 5 13 10 15 11 8 6 18 5 16 6 14 14 13 18 13 15 9 19 16 10 6 14 20 6 6 15 15 15 14 15 9 16 20 10 5 6 15 17 12 10 10 6 16 5 7 13 11 15 12 19 14 8 10 7 11 8 6 20 14 8 15 16 8 16 18 8 17 17 15 6 7 5 10 14 14 7 13 13 14 7 8 6 15 7 15 13 0
Point92 20 7 10 17 10 20 11 18 16 10 8 12 20 16 10 17 19 8 17 7 5 15 16 13 8 15 6 7 20 17 12 6 8 10 7 11 17 13 10 13 5 7 6 15 10 13 16 6 9 18 9 17 17 8 5 12 7 11 11 8 19 5 12 19 6 10 20 14 8 9 7 14 10 19 11 12 12 11 6 19 10 15 12 13 14 7 11 6 5 7 20 0
Point93 11 13 16 16 14 6 14 16 6 18 13 20 20 5 12 15 9 11 6 15 9 19 9 15 8 10 9 15 15 8 19 9 9 20 5 19 5 12 15 7 20 16 20 5 19 17 19 13 8 15 18 16 11 18 18 20 17 10 16 7 16 8 13 8 10 16 11 20 14 19 11 13 17 5 9 11 18 20 9 20 17 13 5 10 12 8 6 17 17 16 7 13 0
Point94 7 11 9 19 16 18 13 11 15 5 8 7 15 9 11 13 6 18 6 14 20 13 6 12 14 10 14 5 20 14 16 10 13 20 8 10 17 6 5 20 8 18 5 17 7 17 7 15 18 14 12 6 15 10 11 17 5 19 12 5 14 6 11 15 19 17 18 11 19 17 18 10 6 13 16 18 17 19 5 18 7 7 6 6 5 20 8 7 5 6 15 20 18 0
Point95 7 15 11 16 20 5 15 6 6 14 12 5 10 5 7 19 15 17 10 19 7 14 13 19 17 20 16 14 9 8 19 11 17 15 17 6 11 9 18 9 5 15 5 5 14 10 18 17 12 6 15 8 15 12 8 19 15 9 6 20 15 9 15 14 20 20 19 14 18 10 18 8 20 9 14 13 5 5 7 20 19 8 18 5 8 9 6 19 14 5 17 19 20 8 0
Point96 17 10 6 18 7 13 10 7 10 9 20 7 16 15 15 8 11 12 7 13 9 5 19 19 7 19 11 19 5 12 8 20 12 11 17 10 20 19 11 17 6 5 19 16 17 10 19 17 17 14 18 16 17 16 14 9 11 16 16 15 16 9 9 19 15 16 8 16 20 20 16 18 10 15 20 16 19 6 19 5 18 8 13 10 13 8 6 10 6 14 15 19 12 13 16 0
Point97 14 15 6 5 16 11 7 7 20 19 11 12 8 20 18 15 12 16 13 14 19 15 10 14 11 14 20 10 5 9 6 15 13 8 17 5 15 11 8 17 16 8 15 17 13 14 18 13 13 20 6 11 16 12 20 9 5 18 7 6 16 15 12 16 18 17 17 14 11 6 13 5 15 20 18 15 17 15 20 6 5 7 9 5 7 13 19 12 9 17 19 7 14 17 18 6 0
Point98 19 7 15 6 17 10 19 14 13 19 20 14 13 20 18 16 14 10 18 6 10 14 9 14 15 9 9 15 15 14 8 18 9 8 16 17 10 8 14 6 11 20 18 15 10 14 7 17 17 15 14 19 14 7 15 12 14 15 9 12 7 12 10 18 5 6 12 5 16 12 18 12 19 19 9 13 16 13 19 19 16 5 8 16 7 6 15 17 15 11 20 10 13 15 9 12 5 0
Point99 14 12 13 7 11 14 8 9 8 17 7 14 19 15 20 6 15 20 6 17 15 15 18 9 16 7 8 6 12 14 10 15 5 12 7 20 11 5 5 17 7 17 19 8 7 9 8 13 16 11 14 13 7 11 9 10 5 9 8 20 11 16 16 19 14 16 17 10 5 12 8 8 15 13 8 19 12 18 17 15 17 13 6 17 14 13 19 16 15 13 20 9 7 9 14 5 17 17 0
Point100 12 13 5 13 6 6 19 6 20 7 6 5 13 10 5 19 13 6 10 9 6 8 9 19 20 20 18 5 14 15 9 9 20 16 19 20 8 14 14 5 13 10 9 19 20 7 11 16 19 13 20 15 11 7 17 12 17 8 13 14 18 7 14 10 16 10 14 6 7 15 9 14 7 9 18 14 7 6 7 6 8 20 20 20 6 19 16 14 7 10 9 19 7 12 12 12 8 11 13 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment