├── LICENSE ├── README.md ├── in-02.txt ├── in.txt ├── node-distance.png ├── vrp-sample-gen.py └── vrp.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Gustavo Dias 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A genetic algorithm for solving the Vehicle Routing Problem 2 | 3 | This is a command-line interface program written in Python language for solving the VPR, minimizing the costs of it's routes. 4 | 5 | ## Input data format 6 | # This is a comment line 7 | 8 | params: 9 | [param-name] [param-value] 10 | ... 11 | 12 | nodes: 13 | [node-label] [demand-value] [position-x] [position-y] 14 | ... 15 | 16 | Lines started with ```#``` are comments, then ignored by the program. 17 | 18 | All the strings of the input are case-sensitive, except the labels ```params:``` and ```nodes:``` that are case-insensitive. 19 | 20 | ### Params 21 | 22 | In ```params:``` block, it's defined the params for the VRP. 23 | 24 | The unique and required accepted param is ```capacity```, that is a decimal positive number representing the capacity of the vehicle. 25 | 26 | ### Nodes 27 | 28 | In ```nodes:``` block, it's defined the nodes of the VRP. 29 | 30 | 1. ```[node-label]``` is a label, terminated by a whitespace, to indentify that node; 31 | 2. ```[demand-value]``` is a decimal positive number that defines the demand value of the node; 32 | 3. ```[position-x]``` is the signed decimal value of the x-axis position in space of the node; 33 | 4. ```[position-y]``` is the signed decimal value of the y-axis position in space of the node. 34 | 35 | The node of the depot is implicitly pré-defined, with ```depot``` label, demand ```0``` and position xy ```(0, 0)```. 36 | 37 | #### Costs 38 | 39 | The cost of the path from a node to another is calculated by the euclidian distance between them. 40 | 41 | The formula below is the calculation of the distance between the nodes A and B: 42 | 43 | ![](node-distance.png) 44 | 45 | ## Output data format 46 | 47 | route: 48 | depot 49 | first-visited-node 50 | another-node 51 | ... 52 | depot 53 | cost: 54 | [total-cost-value-of-the-routes] 55 | 56 | In ```route:``` block, each line is the label of the visited node, in sequence. 57 | 58 | ## Running the program 59 | 60 | ```bash 61 | python vrp.py [population-size] [number-of-iterations] < input-file.txt 62 | ``` 63 | 64 | Required params: 65 | 66 | 1. ```[population-size]``` is an integer positive number that specifies the number of individuals of each generation in the genetic algorithm; 67 | 2. ```[number-of-iterations]``` is an integer positive number that specifies the number of iterations (population generations) of the genetic algorithm. 68 | 69 | Replace ```input-file.txt``` by the file from you want to read the input 70 | 71 | ### Example 72 | 73 | Command to run: 74 | ```bash 75 | python vrp.py 50 100 < in.txt 76 | ``` 77 | 78 | Input: 79 | 80 | # This is an example of data input 81 | 82 | params: 83 | capacity 5 84 | 85 | nodes: 86 | n1 2.3 -5 7 87 | n2 1.6 0 -10.1 88 | n3 0.98 -10 9 89 | n4 1.1 5.78 0 90 | n5 4.0 -5.78 0 91 | n6 2.2 5.78 1.1 92 | n7 1.1 5.78 -1.1 93 | n8 0.1 0.1 0 94 | n9 0.1 -0.1 0 95 | 96 | Output: 97 | 98 | route: 99 | depot 100 | n5 101 | n9 102 | depot 103 | n2 104 | n3 105 | n1 106 | depot 107 | n7 108 | n4 109 | n6 110 | n8 111 | depot 112 | cost: 113 | 71.176217 114 | 115 | ## Random input generator 116 | 117 | This project also has an input VRP random sample generator. 118 | 119 | ```bash 120 | python vrp-sample-gen.py [nodes-count] [capacity] [min-x] [max-x] [min-y] [max-y] > my-generated-vrp-input.txt 121 | ``` 122 | 123 | The program generates a formatted VRP input, with ```[nodes-count]``` nodes (not including the depot). With the capacity ```[capacity]```. Each node is choosen a random demand between ```0.0``` and ```[capacity]```; a random x-axis position between ```[min-x]``` and ```[max-x]```; and a random y-axis position between ```[min-y]``` and ```[max-y]```. 124 | 125 | ### Example 126 | 127 | Command to run: 128 | ```bash 129 | python vrp-sample-gen.py 10 45.89 -9.1 10 -7.63 8.05 130 | ``` 131 | 132 | Output: 133 | 134 | params: 135 | capacity 45.890000 136 | nodes: 137 | node01 17.511 9.194 1.066 138 | node02 12.335 1.134 -1.621 139 | node03 16.793 -6.862 -6.416 140 | node04 18.821 -6.452 -2.332 141 | node05 3.103 -5.402 -4.644 142 | node06 39.410 6.795 -4.982 143 | node07 33.389 6.707 0.483 144 | node08 45.697 -7.255 4.209 145 | node09 19.172 3.970 3.618 146 | node10 4.366 -2.550 2.892 147 | 148 | ### Piping the generated input to the VRP program 149 | 150 | You also can run the sample input generator then pass it directly to the VRP program to run. 151 | 152 | ```bash 153 | python vrp-sample-gen.py [params...] | python vrp.py [params...] 154 | ``` 155 | 156 | Example: 157 | ```bash 158 | python vrp-sample-gen.py 10 45.89 -9.1 10 -7.63 8.05 | python vrp.py 40 100 159 | ``` 160 | Output: 161 | 162 | route: 163 | depot 164 | node06 165 | node05 166 | depot 167 | node07 168 | depot 169 | node02 170 | depot 171 | node01 172 | depot 173 | node03 174 | depot 175 | node08 176 | depot 177 | node04 178 | depot 179 | node09 180 | node10 181 | depot 182 | cost: 183 | 112.169544 184 | 185 | ## Glossary 186 | 187 | **VRP** - Vehicle Routing Problem -------------------------------------------------------------------------------- /in-02.txt: -------------------------------------------------------------------------------- 1 | # This is another example of data input 2 | 3 | params: 4 | capacity 10.000000 5 | nodes: 6 | node1 0.331 -6.238 -9.760 7 | node2 4.239 2.141 -11.340 8 | node3 4.048 1.675 10.392 9 | node4 8.926 4.872 -1.665 10 | node5 1.346 0.407 4.963 11 | node6 2.588 7.111 -3.269 12 | node7 3.915 1.035 11.662 13 | node8 8.971 0.670 2.120 14 | node9 2.781 5.987 12.363 15 | node10 7.322 0.751 5.576 16 | node11 8.132 5.971 1.705 17 | node12 2.623 9.828 0.039 18 | node13 7.222 -4.603 10.678 19 | node14 1.474 -9.863 6.792 20 | node15 6.595 -1.747 -9.842 21 | node16 1.975 -5.225 -3.605 22 | node17 0.203 2.728 -9.208 23 | node18 8.868 8.022 -2.843 24 | node19 5.143 8.378 4.263 25 | node20 0.705 3.771 8.992 26 | -------------------------------------------------------------------------------- /in.txt: -------------------------------------------------------------------------------- 1 | # This is an example of data input 2 | 3 | params: 4 | capacity 5 5 | 6 | nodes: 7 | n1 2.3 -5 7 8 | n2 1.6 0 -10.1 9 | n3 0.98 -10 9 10 | n4 1.1 5.78 0 11 | n5 4.0 -5.78 0 12 | n6 2.2 5.78 1.1 13 | n7 1.1 5.78 -1.1 14 | n8 0.1 0.1 0 15 | n9 0.1 -0.1 0 -------------------------------------------------------------------------------- /node-distance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuDiasOliveira/vrp-genetic-algorithm-python/cf18d2a5d6eb46cd1d80fe8e7909cfdbc863f574/node-distance.png -------------------------------------------------------------------------------- /vrp-sample-gen.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import sys 3 | import random 4 | import math 5 | 6 | 7 | nodescount = int(sys.argv[1]) 8 | maxcap = float(sys.argv[2]) 9 | minX = float(sys.argv[3]) 10 | maxX = float(sys.argv[4]) 11 | minY = float(sys.argv[5]) 12 | maxY = float(sys.argv[6]) 13 | 14 | 15 | print 'params:' 16 | print ' capacity %f' % maxcap 17 | print 'nodes:' 18 | for i in range(nodescount): 19 | demand = random.uniform(0.0, maxcap) 20 | x = random.uniform(minX, maxX) 21 | y = random.uniform(minY, maxY) 22 | # On node label printing, the number of leading zeros is according to the amount of digits of the number of the nodes count, to adjust equal string length 23 | print (' node%0' + str(math.ceil(math.log(nodescount + 1) / math.log(10))) + 'd\t\t%.3f\t\t%.3f\t\t%.3f') % (i+1, demand, x, y) -------------------------------------------------------------------------------- /vrp.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import sys 3 | import random 4 | import math 5 | 6 | 7 | vrp = {} 8 | 9 | 10 | ## First reading the VRP from the input ## 11 | 12 | 13 | def readinput(): 14 | try: 15 | line = raw_input().strip() 16 | while line == '' or line.startswith('#'): 17 | line = raw_input().strip() 18 | return line 19 | except EOFError: 20 | return None 21 | 22 | 23 | line = readinput() 24 | if line == None: 25 | print >> sys.stderr, 'Empty input!' 26 | exit(1) 27 | 28 | if line.lower() != 'params:': 29 | print >> sys.stderr, 'Invalid input: it must be the VRP initial params at first!' 30 | exit(1) 31 | 32 | line = readinput() 33 | if line == None: 34 | print >> sys.stderr, 'Invalid input: missing VRP inital params and nodes!' 35 | exit(1) 36 | while line.lower() != 'nodes:': 37 | inputs = line.split() 38 | if len(inputs) < 2: 39 | print >> sys.stderr, 'Invalid input: too few arguments for a param!' 40 | exit(1) 41 | if inputs[0].lower() == 'capacity': 42 | vrp['capacity'] = float(inputs[1]) 43 | # Validating positive non-zero capacity 44 | if vrp['capacity'] <= 0: 45 | print >> sys.stderr, 'Invalid input: capacity must be neither negative nor zero!' 46 | exit(1) 47 | else: 48 | print >> sys.stderr, 'Invalid input: invalid VRP initial param!' 49 | exit(1) 50 | line = readinput() 51 | if line == None: 52 | print >> sys.stderr, 'Invalid input: missing nodes!' 53 | exit(1) 54 | 55 | if not set(vrp).issuperset({'capacity'}): 56 | print >> sys.stderr, 'Invalid input: missing some required VRP initial params!' 57 | exit(1) 58 | 59 | line = readinput() 60 | vrp['nodes'] = [{'label' : 'depot', 'demand' : 0, 'posX' : 0, 'posY' : 0}] 61 | while line != None: 62 | inputs = line.split() 63 | if len(inputs) < 4: 64 | print >> sys.stderr, 'Invalid input: too few arguments for a node!' 65 | exit(1) 66 | node = {'label' : inputs[0], 'demand' : float(inputs[1]), 'posX' : float(inputs[2]), 'posY' : float(inputs[3])} 67 | # Validating demand neither negative nor zero 68 | if node['demand'] <= 0: 69 | print >> sys.stderr, 'Invalid input: the demand if the node %s is negative or zero!' % node['label'] 70 | exit(1) 71 | # Validating demand not greater than capacity 72 | if node['demand'] > vrp['capacity']: 73 | print >> sys.stderr, 'Invalid input: the demand of the node %s is greater than the vehicle capacity!' % node['label'] 74 | exit(1) 75 | vrp['nodes'].append(node) 76 | line = readinput() 77 | 78 | # Validating no such nodes 79 | if len(vrp['nodes']) == 0: 80 | print >> sys.stderr, 'Invalid input: no such nodes!' 81 | exit(1) 82 | 83 | 84 | ## After inputting and validating it, now computing the algorithm ## 85 | 86 | 87 | def distance(n1, n2): 88 | dx = n2['posX'] - n1['posX'] 89 | dy = n2['posY'] - n1['posY'] 90 | return math.sqrt(dx * dx + dy * dy) 91 | 92 | def fitness(p): 93 | # The first distance is from depot to the first node of the first route 94 | s = distance(vrp['nodes'][0], vrp['nodes'][p[0]]) 95 | # Then calculating the distances between the nodes 96 | for i in range(len(p) - 1): 97 | prev = vrp['nodes'][p[i]] 98 | next = vrp['nodes'][p[i + 1]] 99 | s += distance(prev, next) 100 | # The last distance is from the last node of the last route to the depot 101 | s += distance(vrp['nodes'][p[len(p) - 1]], vrp['nodes'][0]) 102 | return s 103 | 104 | def adjust(p): 105 | # Adjust repeated 106 | repeated = True 107 | while repeated: 108 | repeated = False 109 | for i1 in range(len(p)): 110 | for i2 in range(i1): 111 | if p[i1] == p[i2]: 112 | haveAll = True 113 | for nodeId in range(len(vrp['nodes'])): 114 | if nodeId not in p: 115 | p[i1] = nodeId 116 | haveAll = False 117 | break 118 | if haveAll: 119 | del p[i1] 120 | repeated = True 121 | if repeated: break 122 | if repeated: break 123 | # Adjust capacity exceed 124 | i = 0 125 | s = 0.0 126 | cap = vrp['capacity'] 127 | while i < len(p): 128 | s += vrp['nodes'][p[i]]['demand'] 129 | if s > cap: 130 | p.insert(i, 0) 131 | s = 0.0 132 | i += 1 133 | i = len(p) - 2 134 | # Adjust two consective depots 135 | while i >= 0: 136 | if p[i] == 0 and p[i + 1] == 0: 137 | del p[i] 138 | i -= 1 139 | 140 | 141 | popsize = int(sys.argv[1]) 142 | iterations = int(sys.argv[2]) 143 | 144 | pop = [] 145 | 146 | # Generating random initial population 147 | for i in range(popsize): 148 | p = range(1, len(vrp['nodes'])) 149 | random.shuffle(p) 150 | pop.append(p) 151 | for p in pop: 152 | adjust(p) 153 | 154 | # Running the genetic algorithm 155 | for i in range(iterations): 156 | nextPop = [] 157 | # Each one of this iteration will generate two descendants individuals. Therefore, to guarantee same population size, this will iterate half population size times 158 | for j in range(int(len(pop) / 2)): 159 | # Selecting randomly 4 individuals to select 2 parents by a binary tournament 160 | parentIds = set() 161 | while len(parentIds) < 4: 162 | parentIds |= {random.randint(0, len(pop) - 1)} 163 | parentIds = list(parentIds) 164 | # Selecting 2 parents with the binary tournament 165 | parent1 = pop[parentIds[0]] if fitness(pop[parentIds[0]]) < fitness(pop[parentIds[1]]) else pop[parentIds[1]] 166 | parent2 = pop[parentIds[2]] if fitness(pop[parentIds[2]]) < fitness(pop[parentIds[3]]) else pop[parentIds[3]] 167 | # Selecting two random cutting points for crossover, with the same points (indexes) for both parents, based on the shortest parent 168 | cutIdx1, cutIdx2 = random.randint(1, min(len(parent1), len(parent2)) - 1), random.randint(1, min(len(parent1), len(parent2)) - 1) 169 | cutIdx1, cutIdx2 = min(cutIdx1, cutIdx2), max(cutIdx1, cutIdx2) 170 | # Doing crossover and generating two children 171 | child1 = parent1[:cutIdx1] + parent2[cutIdx1:cutIdx2] + parent1[cutIdx2:] 172 | child2 = parent2[:cutIdx1] + parent1[cutIdx1:cutIdx2] + parent2[cutIdx2:] 173 | nextPop += [child1, child2] 174 | # Doing mutation: swapping two positions in one of the individuals, with 1:15 probability 175 | if random.randint(1, 15) == 1: 176 | ptomutate = nextPop[random.randint(0, len(nextPop) - 1)] 177 | i1 = random.randint(0, len(ptomutate) - 1) 178 | i2 = random.randint(0, len(ptomutate) - 1) 179 | ptomutate[i1], ptomutate[i2] = ptomutate[i2], ptomutate[i1] 180 | # Adjusting individuals 181 | for p in nextPop: 182 | adjust(p) 183 | # Updating population generation 184 | pop = nextPop 185 | 186 | # Selecting the best individual, which is the final solution 187 | better = None 188 | bf = float('inf') 189 | for p in pop: 190 | f = fitness(p) 191 | if f < bf: 192 | bf = f 193 | better = p 194 | 195 | 196 | ## After processing the algorithm, now outputting it ## 197 | 198 | 199 | # Printing the solution 200 | print ' route:' 201 | print 'depot' 202 | for nodeIdx in better: 203 | print vrp['nodes'][nodeIdx]['label'] 204 | print 'depot' 205 | print ' cost:' 206 | print '%f' % bf --------------------------------------------------------------------------------