├── README.md ├── puzzle_mild.txt └── sudoku.py /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This project is capable of solving a Sudoku puzzle using a genetic algorithm. Puzzle configurations are read in from a plain text file containing a string of 9 x 9 digits separated by spaces, with an example provided in the file `puzzle_mild.txt`. Zero is used to represent an unknown digit, whereas numbers in [1, 9] are assumed to be known/given. 4 | 5 | Run the code by executing `python sudoku.py` at the command line. Example output looks like this: 6 | 7 | ``` 8 | ~/sudoku-genetic-algorithm $ python sudoku.py 9 | Seeding complete. 10 | Generation 0 11 | Best fitness: 0.286694 12 | Generation 1 13 | Best fitness: 0.303155 14 | Generation 2 15 | Best fitness: 0.303155 16 | Generation 3 17 | Best fitness: 0.303155 18 | Generation 4 19 | Best fitness: 0.303155 20 | Generation 5 21 | Best fitness: 0.390947 22 | Generation 6 23 | Best fitness: 0.473251 24 | Generation 7 25 | Best fitness: 0.473251 26 | Generation 8 27 | Best fitness: 0.473251 28 | Generation 9 29 | Best fitness: 0.473251 30 | Generation 10 31 | Best fitness: 0.473251 32 | Generation 11 33 | Best fitness: 0.489712 34 | Generation 12 35 | Best fitness: 0.489712 36 | Generation 13 37 | Best fitness: 0.489712 38 | Generation 14 39 | Best fitness: 0.489712 40 | Generation 15 41 | Best fitness: 0.489712 42 | Generation 16 43 | Best fitness: 0.536351 44 | Generation 17 45 | Best fitness: 0.536351 46 | Generation 18 47 | Best fitness: 0.536351 48 | Generation 19 49 | Best fitness: 0.536351 50 | Generation 20 51 | Best fitness: 0.536351 52 | Generation 21 53 | Best fitness: 0.536351 54 | Generation 22 55 | Best fitness: 0.536351 56 | Generation 23 57 | Best fitness: 0.547325 58 | Generation 24 59 | Best fitness: 0.547325 60 | Generation 25 61 | Best fitness: 0.547325 62 | Generation 26 63 | Best fitness: 0.547325 64 | Generation 27 65 | Best fitness: 0.604938 66 | Generation 28 67 | Best fitness: 0.604938 68 | Generation 29 69 | Best fitness: 0.604938 70 | Generation 30 71 | Best fitness: 0.604938 72 | Generation 31 73 | Best fitness: 0.604938 74 | Generation 32 75 | Best fitness: 0.851852 76 | Generation 33 77 | Best fitness: 0.851852 78 | Generation 34 79 | Best fitness: 0.851852 80 | Generation 35 81 | Best fitness: 0.851852 82 | Generation 36 83 | Best fitness: 0.851852 84 | Generation 37 85 | Solution found at generation 37! 86 | [[ 8. 3. 9. 2. 7. 4. 6. 5. 1.] 87 | [ 5. 2. 4. 1. 3. 6. 7. 8. 9.] 88 | [ 7. 6. 1. 5. 8. 9. 4. 2. 3.] 89 | [ 1. 9. 7. 8. 5. 3. 2. 6. 4.] 90 | [ 6. 8. 3. 4. 9. 2. 5. 1. 7.] 91 | [ 2. 4. 5. 6. 1. 7. 9. 3. 8.] 92 | [ 3. 5. 2. 7. 4. 1. 8. 9. 6.] 93 | [ 9. 7. 8. 3. 6. 5. 1. 4. 2.] 94 | [ 4. 1. 6. 9. 2. 8. 3. 7. 5.]] 95 | ``` 96 | -------------------------------------------------------------------------------- /puzzle_mild.txt: -------------------------------------------------------------------------------- 1 | 0 3 0 0 7 0 0 5 0 5 0 0 1 0 6 0 0 9 0 0 1 0 0 0 4 0 0 0 9 0 0 5 0 0 6 0 6 0 0 4 0 2 0 0 7 0 4 0 0 1 0 0 3 0 0 0 2 0 0 0 8 0 0 9 0 0 3 0 5 0 0 2 0 1 0 0 2 0 0 7 0 2 | -------------------------------------------------------------------------------- /sudoku.py: -------------------------------------------------------------------------------- 1 | """ Solves a Sudoku puzzle using a genetic algorithm. This is based on a piece of coursework produced by Christian Thomas Jacobs as part of the CS3M6 Evolutionary Computation module at the University of Reading. 2 | 3 | Copyright (c) 2009, 2017 Christian Thomas Jacobs 4 | """ 5 | 6 | import numpy 7 | import random 8 | random.seed() 9 | 10 | Nd = 9 # Number of digits (in the case of standard Sudoku puzzles, this is 9). 11 | 12 | 13 | class Population(object): 14 | """ A set of candidate solutions to the Sudoku puzzle. These candidates are also known as the chromosomes in the population. """ 15 | 16 | def __init__(self): 17 | self.candidates = [] 18 | return 19 | 20 | def seed(self, Nc, given): 21 | self.candidates = [] 22 | 23 | # Determine the legal values that each square can take. 24 | helper = Candidate() 25 | helper.values = [[[] for j in range(0, Nd)] for i in range(0, Nd)] 26 | for row in range(0, Nd): 27 | for column in range(0, Nd): 28 | for value in range(1, 10): 29 | if((given.values[row][column] == 0) and not (given.is_column_duplicate(column, value) or given.is_block_duplicate(row, column, value) or given.is_row_duplicate(row, value))): 30 | # Value is available. 31 | helper.values[row][column].append(value) 32 | elif(given.values[row][column] != 0): 33 | # Given/known value from file. 34 | helper.values[row][column].append(given.values[row][column]) 35 | break 36 | 37 | # Seed a new population. 38 | for p in range(0, Nc): 39 | g = Candidate() 40 | for i in range(0, Nd): # New row in candidate. 41 | row = numpy.zeros(Nd) 42 | 43 | # Fill in the givens. 44 | for j in range(0, Nd): # New column j value in row i. 45 | 46 | # If value is already given, don't change it. 47 | if(given.values[i][j] != 0): 48 | row[j] = given.values[i][j] 49 | # Fill in the gaps using the helper board. 50 | elif(given.values[i][j] == 0): 51 | row[j] = helper.values[i][j][random.randint(0, len(helper.values[i][j])-1)] 52 | 53 | # If we don't have a valid board, then try again. There must be no duplicates in the row. 54 | while(len(list(set(row))) != Nd): 55 | for j in range(0, Nd): 56 | if(given.values[i][j] == 0): 57 | row[j] = helper.values[i][j][random.randint(0, len(helper.values[i][j])-1)] 58 | 59 | g.values[i] = row 60 | 61 | self.candidates.append(g) 62 | 63 | # Compute the fitness of all candidates in the population. 64 | self.update_fitness() 65 | 66 | print("Seeding complete.") 67 | 68 | return 69 | 70 | def update_fitness(self): 71 | """ Update fitness of every candidate/chromosome. """ 72 | for candidate in self.candidates: 73 | candidate.update_fitness() 74 | return 75 | 76 | def sort(self): 77 | """ Sort the population based on fitness. """ 78 | self.candidates.sort(self.sort_fitness) 79 | return 80 | 81 | def sort_fitness(self, x, y): 82 | """ The sorting function. """ 83 | if(x.fitness < y.fitness): 84 | return 1 85 | elif(x.fitness == y.fitness): 86 | return 0 87 | else: 88 | return -1 89 | 90 | 91 | class Candidate(object): 92 | """ A candidate solutions to the Sudoku puzzle. """ 93 | def __init__(self): 94 | self.values = numpy.zeros((Nd, Nd), dtype=int) 95 | self.fitness = None 96 | return 97 | 98 | def update_fitness(self): 99 | """ The fitness of a candidate solution is determined by how close it is to being the actual solution to the puzzle. The actual solution (i.e. the 'fittest') is defined as a 9x9 grid of numbers in the range [1, 9] where each row, column and 3x3 block contains the numbers [1, 9] without any duplicates (see e.g. http://www.sudoku.com/); if there are any duplicates then the fitness will be lower. """ 100 | 101 | row_count = numpy.zeros(Nd) 102 | column_count = numpy.zeros(Nd) 103 | block_count = numpy.zeros(Nd) 104 | row_sum = 0 105 | column_sum = 0 106 | block_sum = 0 107 | 108 | for i in range(0, Nd): # For each row... 109 | for j in range(0, Nd): # For each number within it... 110 | row_count[self.values[i][j]-1] += 1 # ...Update list with occurrence of a particular number. 111 | 112 | row_sum += (1.0/len(set(row_count)))/Nd 113 | row_count = numpy.zeros(Nd) 114 | 115 | for i in range(0, Nd): # For each column... 116 | for j in range(0, Nd): # For each number within it... 117 | column_count[self.values[j][i]-1] += 1 # ...Update list with occurrence of a particular number. 118 | 119 | column_sum += (1.0 / len(set(column_count)))/Nd 120 | column_count = numpy.zeros(Nd) 121 | 122 | 123 | # For each block... 124 | for i in range(0, Nd, 3): 125 | for j in range(0, Nd, 3): 126 | block_count[self.values[i][j]-1] += 1 127 | block_count[self.values[i][j+1]-1] += 1 128 | block_count[self.values[i][j+2]-1] += 1 129 | 130 | block_count[self.values[i+1][j]-1] += 1 131 | block_count[self.values[i+1][j+1]-1] += 1 132 | block_count[self.values[i+1][j+2]-1] += 1 133 | 134 | block_count[self.values[i+2][j]-1] += 1 135 | block_count[self.values[i+2][j+1]-1] += 1 136 | block_count[self.values[i+2][j+2]-1] += 1 137 | 138 | block_sum += (1.0/len(set(block_count)))/Nd 139 | block_count = numpy.zeros(Nd) 140 | 141 | # Calculate overall fitness. 142 | if (int(row_sum) == 1 and int(column_sum) == 1 and int(block_sum) == 1): 143 | fitness = 1.0 144 | else: 145 | fitness = column_sum * block_sum 146 | 147 | self.fitness = fitness 148 | return 149 | 150 | def mutate(self, mutation_rate, given): 151 | """ Mutate a candidate by picking a row, and then picking two values within that row to swap. """ 152 | 153 | r = random.uniform(0, 1.1) 154 | while(r > 1): # Outside [0, 1] boundary - choose another 155 | r = random.uniform(0, 1.1) 156 | 157 | success = False 158 | if (r < mutation_rate): # Mutate. 159 | while(not success): 160 | row1 = random.randint(0, 8) 161 | row2 = random.randint(0, 8) 162 | row2 = row1 163 | 164 | from_column = random.randint(0, 8) 165 | to_column = random.randint(0, 8) 166 | while(from_column == to_column): 167 | from_column = random.randint(0, 8) 168 | to_column = random.randint(0, 8) 169 | 170 | # Check if the two places are free... 171 | if(given.values[row1][from_column] == 0 and given.values[row1][to_column] == 0): 172 | # ...and that we are not causing a duplicate in the rows' columns. 173 | if(not given.is_column_duplicate(to_column, self.values[row1][from_column]) 174 | and not given.is_column_duplicate(from_column, self.values[row2][to_column]) 175 | and not given.is_block_duplicate(row2, to_column, self.values[row1][from_column]) 176 | and not given.is_block_duplicate(row1, from_column, self.values[row2][to_column])): 177 | 178 | # Swap values. 179 | temp = self.values[row2][to_column] 180 | self.values[row2][to_column] = self.values[row1][from_column] 181 | self.values[row1][from_column] = temp 182 | success = True 183 | 184 | return success 185 | 186 | 187 | class Given(Candidate): 188 | """ The grid containing the given/known values. """ 189 | 190 | def __init__(self, values): 191 | self.values = values 192 | return 193 | 194 | def is_row_duplicate(self, row, value): 195 | """ Check whether there is a duplicate of a fixed/given value in a row. """ 196 | for column in range(0, Nd): 197 | if(self.values[row][column] == value): 198 | return True 199 | return False 200 | 201 | def is_column_duplicate(self, column, value): 202 | """ Check whether there is a duplicate of a fixed/given value in a column. """ 203 | for row in range(0, Nd): 204 | if(self.values[row][column] == value): 205 | return True 206 | return False 207 | 208 | def is_block_duplicate(self, row, column, value): 209 | """ Check whether there is a duplicate of a fixed/given value in a 3 x 3 block. """ 210 | i = 3*(int(row/3)) 211 | j = 3*(int(column/3)) 212 | 213 | if((self.values[i][j] == value) 214 | or (self.values[i][j+1] == value) 215 | or (self.values[i][j+2] == value) 216 | or (self.values[i+1][j] == value) 217 | or (self.values[i+1][j+1] == value) 218 | or (self.values[i+1][j+2] == value) 219 | or (self.values[i+2][j] == value) 220 | or (self.values[i+2][j+1] == value) 221 | or (self.values[i+2][j+2] == value)): 222 | return True 223 | else: 224 | return False 225 | 226 | 227 | class Tournament(object): 228 | """ The crossover function requires two parents to be selected from the population pool. The Tournament class is used to do this. 229 | 230 | Two individuals are selected from the population pool and a random number in [0, 1] is chosen. If this number is less than the 'selection rate' (e.g. 0.85), then the fitter individual is selected; otherwise, the weaker one is selected. 231 | """ 232 | 233 | def __init__(self): 234 | return 235 | 236 | def compete(self, candidates): 237 | """ Pick 2 random candidates from the population and get them to compete against each other. """ 238 | c1 = candidates[random.randint(0, len(candidates)-1)] 239 | c2 = candidates[random.randint(0, len(candidates)-1)] 240 | f1 = c1.fitness 241 | f2 = c2.fitness 242 | 243 | # Find the fittest and the weakest. 244 | if(f1 > f2): 245 | fittest = c1 246 | weakest = c2 247 | else: 248 | fittest = c2 249 | weakest = c1 250 | 251 | selection_rate = 0.85 252 | r = random.uniform(0, 1.1) 253 | while(r > 1): # Outside [0, 1] boundary. Choose another. 254 | r = random.uniform(0, 1.1) 255 | if(r < selection_rate): 256 | return fittest 257 | else: 258 | return weakest 259 | 260 | class CycleCrossover(object): 261 | """ Crossover relates to the analogy of genes within each parent candidate mixing together in the hopes of creating a fitter child candidate. Cycle crossover is used here (see e.g. A. E. Eiben, J. E. Smith. Introduction to Evolutionary Computing. Springer, 2007). """ 262 | 263 | def __init__(self): 264 | return 265 | 266 | def crossover(self, parent1, parent2, crossover_rate): 267 | """ Create two new child candidates by crossing over parent genes. """ 268 | child1 = Candidate() 269 | child2 = Candidate() 270 | 271 | # Make a copy of the parent genes. 272 | child1.values = numpy.copy(parent1.values) 273 | child2.values = numpy.copy(parent2.values) 274 | 275 | r = random.uniform(0, 1.1) 276 | while(r > 1): # Outside [0, 1] boundary. Choose another. 277 | r = random.uniform(0, 1.1) 278 | 279 | # Perform crossover. 280 | if (r < crossover_rate): 281 | # Pick a crossover point. Crossover must have at least 1 row (and at most Nd-1) rows. 282 | crossover_point1 = random.randint(0, 8) 283 | crossover_point2 = random.randint(1, 9) 284 | while(crossover_point1 == crossover_point2): 285 | crossover_point1 = random.randint(0, 8) 286 | crossover_point2 = random.randint(1, 9) 287 | 288 | if(crossover_point1 > crossover_point2): 289 | temp = crossover_point1 290 | crossover_point1 = crossover_point2 291 | crossover_point2 = temp 292 | 293 | for i in range(crossover_point1, crossover_point2): 294 | child1.values[i], child2.values[i] = self.crossover_rows(child1.values[i], child2.values[i]) 295 | 296 | return child1, child2 297 | 298 | def crossover_rows(self, row1, row2): 299 | child_row1 = numpy.zeros(Nd) 300 | child_row2 = numpy.zeros(Nd) 301 | 302 | remaining = range(1, Nd+1) 303 | cycle = 0 304 | 305 | while((0 in child_row1) and (0 in child_row2)): # While child rows not complete... 306 | if(cycle % 2 == 0): # Even cycles. 307 | # Assign next unused value. 308 | index = self.find_unused(row1, remaining) 309 | start = row1[index] 310 | remaining.remove(row1[index]) 311 | child_row1[index] = row1[index] 312 | child_row2[index] = row2[index] 313 | next = row2[index] 314 | 315 | while(next != start): # While cycle not done... 316 | index = self.find_value(row1, next) 317 | child_row1[index] = row1[index] 318 | remaining.remove(row1[index]) 319 | child_row2[index] = row2[index] 320 | next = row2[index] 321 | 322 | cycle += 1 323 | 324 | else: # Odd cycle - flip values. 325 | index = self.find_unused(row1, remaining) 326 | start = row1[index] 327 | remaining.remove(row1[index]) 328 | child_row1[index] = row2[index] 329 | child_row2[index] = row1[index] 330 | next = row2[index] 331 | 332 | while(next != start): # While cycle not done... 333 | index = self.find_value(row1, next) 334 | child_row1[index] = row2[index] 335 | remaining.remove(row1[index]) 336 | child_row2[index] = row1[index] 337 | next = row2[index] 338 | 339 | cycle += 1 340 | 341 | return child_row1, child_row2 342 | 343 | def find_unused(self, parent_row, remaining): 344 | for i in range(0, len(parent_row)): 345 | if(parent_row[i] in remaining): 346 | return i 347 | 348 | def find_value(self, parent_row, value): 349 | for i in range(0, len(parent_row)): 350 | if(parent_row[i] == value): 351 | return i 352 | 353 | 354 | class Sudoku(object): 355 | """ Solves a given Sudoku puzzle using a genetic algorithm. """ 356 | 357 | def __init__(self): 358 | self.given = None 359 | return 360 | 361 | def load(self, path): 362 | # Load a configuration to solve. 363 | with open(path, "r") as f: 364 | values = numpy.loadtxt(f).reshape((Nd, Nd)).astype(int) 365 | self.given = Given(values) 366 | return 367 | 368 | def save(self, path, solution): 369 | # Save a configuration to a file. 370 | with open(path, "w") as f: 371 | numpy.savetxt(f, solution.values.reshape(Nd*Nd), fmt='%d') 372 | return 373 | 374 | def solve(self): 375 | Nc = 1000 # Number of candidates (i.e. population size). 376 | Ne = int(0.05*Nc) # Number of elites. 377 | Ng = 1000 # Number of generations. 378 | Nm = 0 # Number of mutations. 379 | 380 | # Mutation parameters. 381 | phi = 0 382 | sigma = 1 383 | mutation_rate = 0.06 384 | 385 | # Create an initial population. 386 | self.population = Population() 387 | self.population.seed(Nc, self.given) 388 | 389 | # For up to 10000 generations... 390 | stale = 0 391 | for generation in range(0, Ng): 392 | 393 | print("Generation %d" % generation) 394 | 395 | # Check for a solution. 396 | best_fitness = 0.0 397 | for c in range(0, Nc): 398 | fitness = self.population.candidates[c].fitness 399 | if(fitness == 1): 400 | print("Solution found at generation %d!" % generation) 401 | print(self.population.candidates[c].values) 402 | return self.population.candidates[c] 403 | 404 | # Find the best fitness. 405 | if(fitness > best_fitness): 406 | best_fitness = fitness 407 | 408 | print("Best fitness: %f" % best_fitness) 409 | 410 | # Create the next population. 411 | next_population = [] 412 | 413 | # Select elites (the fittest candidates) and preserve them for the next generation. 414 | self.population.sort() 415 | elites = [] 416 | for e in range(0, Ne): 417 | elite = Candidate() 418 | elite.values = numpy.copy(self.population.candidates[e].values) 419 | elites.append(elite) 420 | 421 | # Create the rest of the candidates. 422 | for count in range(Ne, Nc, 2): 423 | # Select parents from population via a tournament. 424 | t = Tournament() 425 | parent1 = t.compete(self.population.candidates) 426 | parent2 = t.compete(self.population.candidates) 427 | 428 | ## Cross-over. 429 | cc = CycleCrossover() 430 | child1, child2 = cc.crossover(parent1, parent2, crossover_rate=1.0) 431 | 432 | # Mutate child1. 433 | old_fitness = child1.fitness 434 | success = child1.mutate(mutation_rate, self.given) 435 | child1.update_fitness() 436 | if(success): 437 | Nm += 1 438 | if(child1.fitness > old_fitness): # Used to calculate the relative success rate of mutations. 439 | phi = phi + 1 440 | 441 | # Mutate child2. 442 | old_fitness = child2.fitness 443 | success = child2.mutate(mutation_rate, self.given) 444 | child2.update_fitness() 445 | if(success): 446 | Nm += 1 447 | if(child2.fitness > old_fitness): # Used to calculate the relative success rate of mutations. 448 | phi = phi + 1 449 | 450 | # Add children to new population. 451 | next_population.append(child1) 452 | next_population.append(child2) 453 | 454 | # Append elites onto the end of the population. These will not have been affected by crossover or mutation. 455 | for e in range(0, Ne): 456 | next_population.append(elites[e]) 457 | 458 | # Select next generation. 459 | self.population.candidates = next_population 460 | self.population.update_fitness() 461 | 462 | # Calculate new adaptive mutation rate (based on Rechenberg's 1/5 success rule). This is to stop too much mutation as the fitness progresses towards unity. 463 | if(Nm == 0): 464 | phi = 0 # Avoid divide by zero. 465 | else: 466 | phi = phi / Nm 467 | 468 | if(phi > 0.2): 469 | sigma = sigma/0.998 470 | elif(phi < 0.2): 471 | sigma = sigma*0.998 472 | 473 | mutation_rate = abs(numpy.random.normal(loc=0.0, scale=sigma, size=None)) 474 | Nm = 0 475 | phi = 0 476 | 477 | # Check for stale population. 478 | self.population.sort() 479 | if(self.population.candidates[0].fitness != self.population.candidates[1].fitness): 480 | stale = 0 481 | else: 482 | stale += 1 483 | 484 | # Re-seed the population if 100 generations have passed with the fittest two candidates always having the same fitness. 485 | if(stale >= 100): 486 | print("The population has gone stale. Re-seeding...") 487 | self.population.seed(Nc, self.given) 488 | stale = 0 489 | sigma = 1 490 | phi = 0 491 | Nm = 0 492 | mutation_rate = 0.06 493 | 494 | print("No solution found.") 495 | return None 496 | 497 | s = Sudoku() 498 | s.load("puzzle_mild.txt") 499 | solution = s.solve() 500 | if(solution): 501 | s.save("solution.txt", solution) 502 | --------------------------------------------------------------------------------