For quite some time, I’ve wanted to write some Python code to implement and elementary cellular automaton. In particular, I wanted to “investigate” (play around with) the famous Rule 110, which is known to be Turing complete.
Despite the name, this has nothing to do with cellphones. It’s a 1D variation on the well-known 2D Game of Life designed by British mathematician John Conway. [See the John Conway’s Life and Life With Class posts for Python versions]
Note: The code here turned out to be not quite right. Please see the follow-up post, Cellular Automaton Redux, for an improved version.
The end goal is generating images like this:
Which is the result of running Rule 110 for 500 iterations starting with the usual initial single row with one non-blank cell (the top row in the image above, a single pixel). Each iteration produces a new row below it.
A row is a list of bits, zeros (or blanks) and ones (non-blanks). The default starting row has a single non-blank cell with all others presumed blank. In the Python code, we’ll use a list populated with zeros and ones for easy handling.
Rule 110 is so named because 110 in binary is 01101110, and that string of eight bits specifies the rule. Each bit position has an index number, from 0 (right-most or least-significant bit) to 7 (left-most or most-significant bit). These index numbers, expressed in binary, range from 000 to 111.
Note that this eight-bit protocol means rules range from 0 to a max of 255.
Here’s a simple bit of code illustrating how a rule number converts to a set of eight index codes and resulting output:
002|
003| def display_rule (rule=110):
004| bit_string = f'{rule:08b}’
005|
006| for bx,b in enumerate(reversed(bit_string)):
007| print(f'{bx} [{bx:03b}]: {b}’)
008| print()
009|
010| return list(int(b) for b in bit_string)
011|
012| if __name__ == ‘__main__’:
013| rule = int(argv[1]) if 1 < len(argv) else 110
014|
015| bit_string = display_rule(rule)
016|
We use an f-string to convert the rule number into binary (line #4) [see Simple Python Tricks #8 and Simple Python Tricks #15 for an exploration into formatted strings].
The for loop (lines #6 to #8) prints each rule bit on a separate line starting with the bit index number in decimal and binary followed by the output bit.
When run (with the Rule 110 default), this prints:
0 [000]: 0 1 [001]: 1 2 [010]: 1 3 [011]: 1 4 [100]: 0 5 [101]: 1 6 [110]: 1 7 [111]: 0
To see how this works, let’s go step-by-step over how the automaton generates a new row given the default starting row consisting of just one non-blank cell (images run left-to-right, top-to-bottom):

For simplicity, we pad the ends of the row with three blank cells. This makes it easier for the algorithm to scan the row. If the row had only one cell (or only two), it would need handle partial triplets. Padding allows it to always see triplets.
The algorithm starts (upper left) with the left-most three cells, which are 0-0-0. Rule 110 specifies a 0 output in this case, and this is plugged into the center cell (relative to the triplet) in the new row below. The next triplet (upper center) holds a 0-0-1 pattern, and the output of that is a 1. Then we have a 0-1-0 pattern (upper right) for which the output is another 1.
Next is a 1-0-0 pattern — output 0 — and finally the trailing 0-0-0 pattern, which outputs a final 0. We fill in the leading and trailing cells with a 0. (Or just initialize new rows to all zeros.)
To pad rows, we use this simple expand_row function:
002| ”’\
002| Ensure that row has three blanks at the beginning and end.
002|
002| Arguments:
002| row -an automata row
002|
002| Returns:
002| the row (note that the passed row is altered)
002|
002| ”’
003| prefix = [0,0,0]
004|
005| # Ensure the row begins with three blanks…
006| while row[:3] != prefix:
007| row.insert(0, 0)
008|
009| # Ensure the row ends with three blanks…
010| while row[–3:] != prefix:
011| row.append(0)
012|
013| # Return the row…
014| return row
015|
016|
017| if __name__ == ‘__main__’:
018| print(expand_row([1]))
019| print(expand_row([0,1]))
020| print(expand_row([0,0,1]))
021| print(expand_row([0,0,0,1]))
022| print(expand_row([0,0,0,0,1]))
023| print(expand_row([1,0]))
024| print(expand_row([1,0,0]))
025| print(expand_row([1,0,0,0]))
026| print(expand_row([1,0,0,0,0]))
027| print(expand_row([0,1,0]))
028| print(expand_row([0,0,1,0,0]))
029| print(expand_row([0,0,0,1,0,0,0]))
030| print(expand_row([0,0,0,0,1,0,0,0,0]))
031|
Two while loops (lines #5 to #7 and #9 to #11) ensure three leading and trailing blanks. The function alters the passed list and for convenience also returns it.
Lines #17 to #30 test the function. When run, this prints:
[0, 0, 0, 1, 0, 0, 0] [0, 0, 0, 1, 0, 0, 0] [0, 0, 0, 1, 0, 0, 0] [0, 0, 0, 1, 0, 0, 0] [0, 0, 0, 0, 1, 0, 0, 0] [0, 0, 0, 1, 0, 0, 0] [0, 0, 0, 1, 0, 0, 0] [0, 0, 0, 1, 0, 0, 0] [0, 0, 0, 1, 0, 0, 0, 0] [0, 0, 0, 1, 0, 0, 0] [0, 0, 0, 1, 0, 0, 0] [0, 0, 0, 1, 0, 0, 0] [0, 0, 0, 0, 1, 0, 0, 0, 0]
Which is the expected output.
While it would be possible to hard-code all 256 rules, the protocol for converting an integer (from 0 to 255) into a rule allows us to generate rules on the fly with a generate_rule function:
002|
003| def generate_rule (rule=110):
004| ”’\
004| Generate a rule dictionary from a number.
004|
004| Arguments:
004| rule -rule number (from 0 to 255)
004|
004| Returns:
004| dictionary of the eight rule conditions
004|
004| ”’
005| assert 0 <= rule <= 255, ValueError(f’Invalid rule number: {rule}’)
006|
007| # Convert number to an eight-bit string…
008| sbits = f'{rule:08b}’
009|
010| # Convert bits string to a list of zeros and ones…
011| bits = [1 if b==‘1’ else 0 for b in sbits]
012|
013| # Generate and return the rule dictionary…
014| return {f'{bx:03b}’:b for bx,b in enumerate(reversed(bits))}
015|
016| if __name__ == ‘__main__’:
017| rule = int(argv[1]) if 1 < len(argv) else 110
018|
019| rule_dict = generate_rule(rule)
020| for bx,bit_patt in enumerate(rule_dict):
021| print(f'{bx} [{bit_patt}]: {rule_dict[bit_patt]}’)
022| print()
023|
Line #5 ensures a valid integer. Line #8 converts the number into an eight-bit string. Line #11 converts the string into a list of 0s and 1s.
Lastly, line #14 builds and returns a dictionary with bit indexes for keys and the corresponding output as the value (the return expression here is a dict comprehension).
Lines #16 to #22 exercise the function. The output is identical to that shown for the first code fragment above.
With these two helper functions, we’re ready to implement the automaton:
002| from os import path
003| from examples import expand_row, generate_rule
004|
005| def run_automata (rule, size, start=[1], fname=‘automata.out’, fpath=”):
006| ”’\
006| Elementary cellular automata (per almost every CS course ever).
006| See: https://en.wikipedia.org/wiki/Elementary_cellular_automaton
006|
006| Arguments:
006| rule -rule number, from 0 to 255
006| size -number of iterations
006| start -the initial row, typically just [1]
006| fname -filename for the text output (use None to use stdout)
006| fpath -filepath or None if fname is a full path/filename
006|
006| Returns:
006| list of size+1 generated rows (includes initial row)
006|
006| ”’
007| print(f’Run automata with rule {rule} ({size} rows)’)
008| print()
009|
010| # Ensure the row has leading blanks so the automata always
011| # reacts to a 0-0-0 input to start and end…
012| row = expand_row(start)
013| print(f’Initial row: {“”.join(str(x) for x in row)}’)
014| print()
015|
016| # Generated rows appended to this list…
017| rows = [row]
018|
019| # Get the rule for this run…
020| current_rule = generate_rule(rule)
021|
022| # Iterations of rule…
023| for rx in range(size):
024| # The new row is same length as current row but
025| # consists of just zeros (blanks)…
026| new_row = [0]*len(row)
027|
028| # For each triplet in the current row…
029| for ix in range(len(row)–2):
030|
031| # Convert the triplet to a key…
032| key = ”.join(str(x) for x in row[ix:ix+3])
033|
034| # Get the value per the rule…
035| val = current_rule[key]
036|
037| # And put it in the new row…
038| new_row[ix+1] = val
039|
040| # Ensure the new row has prefix and suffix blanks
041| # and make it the current row…
042| row = expand_row(new_row)
043|
044| # Append the (new) row to the output list…
045| rows.append(row)
046|
047| # We’ll default to the display…
048| sout = stdout
049| # But if a filename is supplied, open a file…
050| if fname:
051| # Include the rule ID in the filename (if requested)…
052| if ‘%s’ in fname:
053| fname = fname % f'{rule:03d}’
054|
055| filename = path.join(fpath,fname) if fpath else fname
056| sout = open(filename, mode=‘w’, encoding=‘utf8’)
057|
058| # For the display, convert 0s and 1s to something printable…
059| output = [”.join(‘#’ if c else ‘ ‘ for c in row) for row in rows]
060|
061| # Display (or write to file) the output…
062| try:
063| for line in output:
064| print(line, file=sout)
065|
066| except:
067| raise
068|
069| finally:
070| if fname:
071| sout.close()
072| print(f’wrote: {filename}’)
073| print()
074|
075| # Return the list of generated rows…
076| return rows
077|
078| if __name__ == ‘__main__’:
079| rule = int(argv[1]) if 1 < len(argv) else 110
080| size = int(argv[2]) if 2 < len(argv) else 20
081|
082| rows = run_automata(rule, size, fpath=r’C:\demo\hcc\python’)
083| for row in rows:
084| print(row)
085| print()
086|
The function is long but not complicated. Line #12 ensures the starting row has leading and trailing padding. Line #17 creates an output buffer for generated rows and initializes it with the starting row. The function eventually returns this list of lists after generating size new rows.
Line #20 generates the rule dictionary from the passed rule number.
The row-generation outer for loop (lines #22 to #45) starts by creating a new row of zeros (line #26). The inner for loop (lines #28 to #38) iterates over the current row (minus two because the last index we need is row[-2] — see the bottom center in the illustration above; the last index is 5 for a row length of 7).
Line #32 extracts the triplet from the row, converts the zeros and ones to string versions, and joins them to form the dictionary key (i.e. “000” to “111”). Line #35 uses the key to get the output value from the rule dictionary. Line #38 plugs the output value into the triplet center position in the new row.
Once the inner loop finishes, we pad the new row and make it the current row (line #42). Lastly, we append this new row to the list of rows (line #45).
Lines #47 to #56 determine if we’re writing rows to the screen or a text file. These lines also allow insertion of the rule number into the filename (lines #51 to #53).
Line #59 iterates through the rows to convert them to string versions. Lines #61 to #73 write the output. The last line of the function (line #76) returns the list of row lists.
Lines #78 to #84 test the run_automata function. Because fname defaults to a filename, the test writes the output to C:\demo\hcc\python\automata.out. The for loop (lines #82 to #84) displays the rows.
When run (with the defaults), this prints:
Run automata with rule 110 (20 rows) Initial row: 0001000 wrote: C:\demo\hcc\python\automata.out [0,0,0,1,0,0,0] [0,0,0,1,1,0,0,0] [0,0,0,1,1,1,0,0,0] [0,0,0,1,1,0,1,0,0,0] [0,0,0,1,1,1,1,1,0,0,0] [0,0,0,1,1,0,0,0,1,0,0,0] [0,0,0,1,1,1,0,0,1,1,0,0,0] [0,0,0,1,1,0,1,0,1,1,1,0,0,0] [0,0,0,1,1,1,1,1,1,1,0,1,0,0,0] [0,0,0,1,1,0,0,0,0,0,1,1,1,0,0,0] [0,0,0,1,1,1,0,0,0,0,1,1,0,1,0,0,0] [0,0,0,1,1,0,1,0,0,0,1,1,1,1,1,0,0,0] [0,0,0,1,1,1,1,1,0,0,1,1,0,0,0,1,0,0,0] [0,0,0,1,1,0,0,0,1,0,1,1,1,0,0,1,1,0,0,0] [0,0,0,1,1,1,0,0,1,1,1,1,0,1,0,1,1,1,0,0,0] [0,0,0,1,1,0,1,0,1,1,0,0,1,1,1,1,1,0,1,0,0,0] [0,0,0,1,1,1,1,1,1,1,1,0,1,1,0,0,0,1,1,1,0,0,0] [0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,0,0,1,1,0,1,0,0,0] [0,0,0,1,1,1,0,0,0,0,0,1,1,0,0,1,0,1,1,1,1,1,0,0,0] [0,0,0,1,1,0,1,0,0,0,0,1,1,1,0,1,1,1,1,0,0,0,1,0,0,0] [0,0,0,1,1,1,1,1,0,0,0,1,1,0,1,1,1,0,0,1,0,0,1,1,0,0,0]
(Spaces in the output removed to reduce width)
The output file looks like this:
# ## ### ## # ##### ## # ### ## ## # ### ####### # ## ### ### ## # ## # ##### ##### ## # ## # ### ## ### #### # ### ## # ## ##### # ######## ## ### ## #### ## # ### ## # ##### ## # ### #### # ##### ## ### # ##
Neither the displayed output nor the text file give us the kind of display we’d like to see. We need an image file like the one at the beginning of this post.
To do this, we’ll use the Pillow library — formerly the Python Imaging Library (PIL). [To install the library, see Basic Installation from the Pillow docs.]
Here’s our image generation function:
002| from os import path
003| from PIL import Image, ImageDraw
004| from examples import run_automata
005|
006| def make_automata_bitmap (rule, rows, fname=‘automata.png’, fpath=”):
007| ”’\
007| Generate a PNG file from the automata output.
007|
007| Arguments:
007| rows -output from the automata
007| rule -the rule number (for the filename)
007| fname -filename for the image file (required)
007| fpath -filepath or None if fname is a full path/filename
007|
007| Returns:
007| nothing (None)
007|
007| ”’
008| bg_color = (255,255,255) # white background
009| fg_color = (0,0,0) # black cells
010| margin = 10 # margin pixels all four sides
011|
012| # Image width is the longest row plus margins…
013| width = len(rows[–1]) + (2 * margin)
014| # Image height is the number of rows plus margins…
015| height = len(rows) + (2 * margin)
016|
017| # Create a new image…
018| img = Image.new(‘RGB’, size=(width,height), color=bg_color)
019| # And a drawing object…
020| draw = ImageDraw.Draw(img)
021|
022| # For each row of output…
023| for rx,row in enumerate(rows):
024|
025| # For each cell in the row…
026| for cx,cell in enumerate(row):
027| # Draw one black pixel if cell=1…
028| if cell:
029| px = cx + 10
030| py = rx + 10
031| draw.point((px, py), fill=fg_color)
032|
033| # Include the rule ID in the filename (if requested)…
034| if ‘%s’ in fname:
035| fname = fname % f'{rule:03d}’
036|
037| # We want a complete filename for the image…
038| filename = path.join(fpath,fname) if fpath else fname
039|
040| # Save the image…
041| img.save(filename)
042| print(f’wrote: {filename}’)
043| print()
044|
045| if __name__ == ‘__main__’:
046| rule = int(argv[1]) if 1 < len(argv) else 110
047| size = int(argv[2]) if 2 < len(argv) else 500
048|
049| # Run the automaton…
050| rows = run_automata(rule, size, fpath=r’C:\demo\hcc\python’)
051|
052| # Generate an image file…
053| make_automata_bitmap(rule, rows, fpath=r’C:\demo\hcc\python’)
054|
We start (lines #8 to #10) by defining some constants: the background color, the foreground color (the color of non-blank cells), and the image margins. Lines #12 to #15 calculate the image size based on the row data plus the margins.
Next, we create the new image (line #18) and use it to get a drawing object (line #20).
A nested pair of for loops (lines #22 to #31) iterate over the row data. If a cell is non-blank, it calculates the pixel coordinates (lines #29 and #30) and draws a pixel.
Lines #33 to #38 are similar to what we used in the run_automata function to generate a filename for the image file. Line #41 saves the new image.
Lines #45 to #53 test image generation. First, we call run_automata to generate row data. Then we call make_automata_bitmap to create the image.
Now we can generate an image for any rule, but a few more simple functions will make this a better app.
First, a function to run any given rule:
002|
003| def do_run_rule (**kwargs):
004| “””CMND: Run the automata with a given rule.”””
005| dpath = KWARG(‘dpath’, BasePath, str, kwargs)
006| dname = KWARG(‘dname’, ‘automata_%s.out’, str, kwargs)
007| ipath = KWARG(‘ipath’, BasePath, str, kwargs)
008| iname = KWARG(‘iname’, ‘automata_%s.png’, str, kwargs)
009| rule = KWARG(‘rule’ , 110, int, kwargs)
010| size = KWARG(‘size’ , 250, int, kwargs)
011| inits = KWARG(‘inits’, ‘1’, str, kwargs)
012| print(‘run-rule:: %s’ % str(list(kwargs.keys())))
013| print(f'{dpath=}’)
014| print(f'{dname=}’)
015| print(f'{ipath=}’)
016| print(f'{iname=}’)
017| print(f'{rule=}’)
018| print(f'{size=}’)
019| print(f'{inits=}’)
020| print()
021|
022| # Convert initialization string (“10101”) to a list of bits…
023| start = [1 if x else 0 for x in inits]
024|
025| # Run the automata…
026| rows = run_automata(rule, size, fname=dname, fpath=dpath)
027|
028| # Make a bitmap…
029| make_automata_bitmap(rule, rows, fname=iname, fpath=ipath)
030|
031| print()
032| return ‘Done!’
033|
034| if __name__ == ‘__main__’:
035| kwargs = {
036| ‘rule’: ‘110’,
037| ‘size’: ‘300’,
038| }
039| do_run_rule(**kwargs)
040|
Second, a function to run all 256 rules:
002| from examples import BasePath, KWARG, run_automata, make_automata_bitmap
003|
004| def do_run_all_rules (**kwargs):
005| “””CMND: Run the automata with all 250 rules.”””
006| dpath = KWARG(‘dpath’, BasePath, str, kwargs)
007| dname = KWARG(‘dname’, ‘automata_%s.out’, str, kwargs)
008| ipath = KWARG(‘ipath’, BasePath, str, kwargs)
009| iname = KWARG(‘iname’, ‘automata_%s.png’, str, kwargs)
010| size = KWARG(‘size’ , 250, int, kwargs)
011| print(‘run-rule:: %s’ % str(list(kwargs.keys())))
012| print(f'{dpath=}’)
013| print(f'{dname=}’)
014| print(f'{ipath=}’)
015| print(f'{iname=}’)
016| print(f'{size=}’)
017| print()
018|
019| # For all possible rules…
020| for rule in range(0,256):
021| print(f’Rule: {rule} ({rule:08b})’)
022|
023| # Run the automata…
024| rows = run_automata(rule, size, fname=dname, fpath=dpath)
025|
026| # Make a bitmap…
027| make_automata_bitmap(rule, rows, fname=iname, fpath=ipath)
028|
029| print()
030| return ‘Done!’
031|
032| if __name__ == ‘__main__’:
033| kwargs = {
034| ‘size’: ‘300’,
035| ‘dpath’: path.join(BasePath, ‘dat’),
036| ‘ipath’: path.join(BasePath, ‘img’),
037| }
038| do_run_all_rules(**kwargs)
039|
Both functions take a dictionary of parameters and use the KWARG function to extract the ones of interest (see examples.py in the ZIP file linked below). They also print these parameters The code that invokes run_automata and make_automata_bitmap are similar to what we’ve seen above. As usual, both fragments include a way to test the function.
Lastly, some code to stitch it all together and make it run:
002| from examples import do_run_rule, do_run_all_rules
003|
004| def dispatch (cmd, **kwargs):
005| “””Dispatch to a module function based on CMD.”””
006| print(f’CMD: {cmd}’)
007| for kw in kwargs:
008| print(f’argument[{kw}]: {kwargs[kw]}’)
009| print()
010|
011| if cmd == ‘one’: return do_run_rule(**kwargs)
012| if cmd == ‘all’: return do_run_all_rules(**kwargs)
013| return ‘Nothing to do!’
014|
015| if __name__ == ‘__main__’:
016| print(‘autorun: %s’ % argv[0])
017| print(argv[0])
018| print()
019| cmd = argv[1] if 1 < len(argv) else ”
020| kwargs = {}
021| for a in argv[2:]:
022| nv = a.split(‘=’, 2)
023| kwargs[nv[0]] = (nv[1] if 1<len(nv) else ”)
024| print(‘%s: “%s”‘ % (nv[0], kwargs[nv[0]]))
025|
026| print()
027| obj = dispatch(cmd, **kwargs)
028| print()
029| print(obj)
030| print()
031|
The dispatch function expects a command (cmd) and a dictionary of arguments. The command determines which of the two launch functions from above are called.
The dispatch function can be imported to another app and called directly, but the code from line #15 to #30 makes this a useable app by processing command line parameters into dictionary form — treating the first one as the command — and calling dispatch.
A command line might look something like this:
one rule=100 size=300 dpath=C:\py\automata\dat ipath=C:\py\automata\img
Note that, after the initial command, the arguments can appear in any order and are only required to override the defaults. You’ll find a complete app (automata.py) in the ZIP file.
Enjoy!
Link: Zip file containing all code fragments used in this post.
∅

ATTENTION: The WordPress Reader strips the style information from posts, which can destroy certain important formatting elements. If you’re reading this in the Reader, I highly recommend (and urge) you to [A] stop using the Reader and [B] always read blog posts on their website.
This post is: Elementary Cellular Automaton
Anyone spot the mistake in the
make_automata_bitmapfunction? Hint: It’s not a bug, per se, but would turn into one if you changed themargin = 10value on line #10.The problem lurks on lines #29 and #30 that use the hard-coded value 10 rather than the
marginvariable.Here’s what it should be:
026| for cx,cell in enumerate(row):
027| # Draw one black pixel if cell=1…
028| if cell:
029| px = cx + margin
030| py = rx + margin
031| draw.point((px, py), fill=fg_color)
032|
Note: I did not update the ZIP file.
Pingback: Cellular Automaton Redux | The Hard-Core Coder