In this project we’ll be solving a variant of John Koza‘s Lawnmower Problem. The previous projects in this series successively drove out the functionality of a genetic solver capable of handling this kind of problem. This project raises our understanding of genetic algorithms and their application to problem solving to a whole new level.

The Lawnmower Problem asks us to provide instructions to a lawnmower to make it mow a field of grass. The field wraps in all directions – if the mower goes off the top it ends up at the bottom and vice versa and the same side-to-side. Let’s say the mower begins in the middle of an 8×8 field facing south.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . M . . . . . . . . . . . . . . . . . . . . . . . . . . .

Next let’s say the available instructions are: mow and turn. mow moves the mower forward one grid square in whatever direction it is facing and cuts the grass in that square. turn causes the mower to turn left 90 degrees.

Simple enough right? Using our previous experience in this series we know we can define a gene for each instruction and use hill climbing to find an optimal solution. I leave this as an exercise to the reader – check out the previous post if you need a refresher.

You should end up with a solution that looks something like the this:

mow mow mow mow mow mow mow mow turn mow mow mow mow mow mow mow turn mow mow mow mow mow mow mow turn mow mow mow mow mow mow turn mow mow mow mow mow mow turn mow mow mow mow mow turn mow mow mow mow mow turn mow mow mow mow turn mow mow mow mow turn mow mow mow turn mow mow mow turn mow mow turn mow mow turn mow turn mow

We can visualize it by numbering each grid square in the order it is mowed:

64 57 42 19 4 31 50 61 63 56 41 18 5 32 51 62 54 55 40 17 6 33 52 53 37 38 39 16 7 34 35 36 12 13 14 15 8* 9 10 11 25 24 23 22 1 28 27 26 46 45 44 21 2 29 48 47 59 58 43 20 3 30 49 60 * - starting location

Or by showing the route traveled graphically:

Cool, a spiral.

Now let’s expand the available instruction set because all we ever get is spiral shaped mowing patterns. The new instruction is: jump. jump has 2 non-negative arguments, forward and right. The mower will jump forward and to the right the specified number of squares and cut the grass where it lands.

For example, if the mower is facing south at the start location (4,4)

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . M . . . . . . . . . . . . . . . . . . . . . . . . . . .

and is told to jump (2,3) it will end up at (1,6), still facing south.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . # . . . . . . . . . . . . M . . . . . . . . . . . . . .

To implement this simply add jump to the set of genes that require special handling and treat the two genes that follow it as the arguments. If less than 2 genes follow it because it is at or near the end of the gene sequence then fill the missing arguments with zeros. Again, I leave the implementation as an exercise. Note: also make your implementation of getFitness() prefer shorter sequences if and only if the gene sequence mows the entire field.

You may end up with a result similar to the following:

jump (2,5) mow mow mow mow mow mow mow jump (4,3) mow mow mow mow mow mow mow jump (0,3) mow mow mow mow mow mow mow jump (0,3) mow mow mow mow mow mow mow jump (3,3) mow mow mow mow mow mow mow jump (5,3) mow mow mow mow mow jump (5,3) mow mow mow mow mow mow mow jump (1,3) mow mow mow mow mow mow mow jump (5,2) mow

Note that the genetic algorithm has completely abandoned the turn instruction in favor of the more powerful jump instruction because this results in a shorter program.

Here’s a visualization of the mowing route:

44 17 56 40 16 48 26 3 45 18 57 33 9 49 27 4 46 19 58 34 10 50 28 5 63 20 59 35 11 51 29 6 64 21 60 36 12* 52 30 7 41 22 61 37 13 53 31 8 42 23 62 38 14 54 32 1 43 24 55 39 15 47 25 2 * - starting location

Now back the Koza’s purpose for this problem. As interesting as this solution is, the sequence of instructions generated by the solver are completely different from the solution a human would use. Now think about how you’d tell another person to mow a toroidal field. You wouldn’t give them detailed instructions for every step right? You’d break it down into a set of repeatable sequences. In a non-toroid field you might say something like: start at the corner of the field and mow a strip along the edge of the field all the way to the other side.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . # # # # # # # M>

Then turn the mower back this way and mow the strip right next to the one you just completed until you get back to where you started.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . <M # # # # # # # # # # # # # # #

Turn around again and repeat the process until you’ve mowed the whole field.

You automatically combine squares into strips and trips across-and-back into a repeatable pattern. How do we do that with the mower?

The best result we’ve generated so far requires 64 jump and mow instructions, one for each grid square, to tell the mower how to cut the grass in the field. How can we make it look more like the instructions you’d give a human? We have to introduce the ability to repeat a sequence of instructions by reference and make this an instruction too.

This is where things get interesting. We’re going from using the genes of the genetic sequence more-or-less one-to-one to solve a problem, to genetic programming.

Implementation-wise this means we need two more special genes: begin-numbered-reference and call-numbered-reference. begin-numbered-reference will increment an id and start a new instruction sequence, or block, if and only if the current block is non-empty. call-numbered-sequence, or more simply call, will take a parameter for the id of the sequence to execute. Once that sequence has completed, execution will return to the sequence that made the call – exactly like calling a subroutine.

Here’s a Go implementation of a gene decoder that builds a program for the mower as described above.

func parseProgram(genes string, f *field, m *mower) *program { p := NewProgram() instructionCodes := make(chan int) builders := make(chan builder) go streamInstructionCodes(genes, instructionCodes) go func() { offset := 0 for instructionCode := range instructionCodes { switch instructionCode % numberOfInstructions { case 0: builders <- createMowBuilder() case 1: builders <- createTurnBuilder() case 2: builders <- createJumpBuilder(instructionCodes) case 3: builders <- createBlockBuilder() case 4: builders <- createCallBuilder(instructionCodes) default: panic(fmt.Sprint("No builder defined for instructionCode '", instructionCode, "' from gene '", genes[offset:offset+1], "'")) } offset++ } builders <- builder{stop: true} }() currentBlockName := "main" blockId := -1 instructions := make([]Instruction, 0, len(genes)) for builder := range builders { if builder.stop { break } if builder.startNewBlock { if len(instructions) > 0 { p.addBlock(currentBlockName, instructions) blockId++ currentBlockName = createBlockName(blockId) instructions = make([]Instruction, 0, len(genes)) } continue } instructions = append(instructions, builder.create(f, m)) } if len(instructions) > 0 { p.addBlock(currentBlockName, instructions) } return p }

Note: Koza’s implementation prevents recursion. This implementation allows recursion but it isn’t difficult to modify it to work Koza’s way. I just find the recursive results more interesting.

Now when we move to from one-to-one gene evaluation to running a program, determining fitness becomes a problem in its own right. On the face it is the same: perform the instructions, determine how many field positions were mowed, switch evaluation strategies to program length if all field squares have been mowed. The problem is we need to handle the flow control involved in fetching subroutines, running them, and returning to the previous location upon completion. We also need the ability to track the number of instructions executed and optionally exit if we run beyond a pre-determined maximum – this prevents infinite loops from blocking evolution in the solver.

I implemented this functionality as a domain independent interpreter (GitHub) while coding my implementation of the Lawnmower Problem.

With the gene-to-program converter and interpreter in place we are free to continue our exploration of the Lawnmower Problem. Here’s a sample optimal solution found by my implementation:

Final: 10119 16.6669533s main: block1 block0: mow mow mow jump (0,1) block1: block0 block0 mow block1 52 15 44 5 35 60 26 25 17 16 45 6 36 61 27 53 18 46 8 7 37 62 28 54 19 47 9 39 38 63 29 55 20 48 10 40 64* 31 30 56 21 49 11 41 1 32 57 22 50 13 12 42 2 33 58 23 51 14 43 4 3 34 59 24 * - starting location

Note that the program is recursive in that block1 calls itself.

If we change the logic to prevent recursion we get a slightly longer optimal solution like this:

Final: 10115 15.4188819s main: block0 block0 block0 block0 block0: jump (6,6) block1 block1 block1 block1 block1: jump (0,1) mow mow mow 34 37 24 10 9 60 48 50 39 38 25 11 62 61 49 51 40 27 26 12 63 2 1 52 41 28 14 13 64 3 54 53 42 29 15 19 18* 4 55 43 31 30 16 20 6 5 56 44 32 35 17 21 7 58 57 45 33 36 23 22 8 59 47 46 * - starting location

Welcome to the world of genetic programming!

You can get the code for my Lawnmower Problem solver from my GitHub repository.