# Written by Eric Martin for COMP9021


from tkinter import *
import tkinter.messagebox
import tkinter.simpledialog
from random import randint


NB_OF_STEPS = 300
BACKGROUND_COLOUR = '#F5F5F5'
HISTORY_COLOUR = 'green'


class ElementaryCellularAutomata(Tk):
    def __init__(self):
        Tk.__init__(self)
        self.title('Elementary Cellular Automata')
        menubar = Menu()
        help_menu = Menu(menubar)
        menubar.add_cascade(label = 'Elementary Cellular Automaton Help',
                            menu = help_menu)
        help_menu.add_command(label = 'Input', command = self.input_help)
        self.config(menu = menubar)
        
        self.input = Entry(self, width = 8)
        self.input.grid()
        self.previous_rule_number = None
        self.displaying_random_history = False
        self.shown_rule_number = StringVar()
        Label(self, width = 9,
                    height = 1,
                    textvariable = self.shown_rule_number,
                    fg = 'magenta'
              ).grid(row = 0, column = 1)
        self.shown_rule = Canvas(self, width = 250, height = 50)
        self.shown_rule.grid(row = 0, column = 1, columnspan = 2,
                             padx = (100, 0), pady = 10)
        Button(self, text = 'Display history from single 1',
                     command = self.work_from_single_1).grid(padx = (40, 0))
        self.shown_related_rule_numbers = StringVar()
        Label(self, width = 87,
                    height = 1,
                    textvariable = self.shown_related_rule_numbers
              ).grid(row = 1, column = 1)
        Button(self, text = 'Display history from random state',
                     command = self.work_from_random_state).grid(row = 1,
                                                                 column = 2,
                                                                 padx = (0, 40))
        self.history = Canvas(self, width = NB_OF_STEPS * 4 + 3,
                                    height = NB_OF_STEPS * 2 + 5,
                                    bg = BACKGROUND_COLOUR)
        self.history.grid(columnspan = 3, pady = 20)
    
    def input_help(self):
        tkinter.messagebox.showinfo('Input',
            'The input should be a nonnegative integer at most '
            'equal to 255, represented either in base 10 or in base 2 with '
            'exactly 8 bits.')
    
    def get_rule(self):
        input = self.input.get()
        self.input.delete(0, END)
        if input == '':
            if self.previous_rule_number == None:
                tkinter.messagebox.showerror('Rule error',
                                             'Please input a rule number')
                return False
            return None
        if not input.isdigit():
            self.warn_of_incorrect_input()
            return False
        if len(input) == 8:
            try:
                self.rule_number = int(input, 2)
            except ValueError:
                self.warn_of_incorrect_input()
                return False
            self.rule_bits = input
        else:
            if len(input) > 1 and input[0] == '0':
                self.warn_of_incorrect_input()
                return False
            self.rule_number = int(input)
            if self.rule_number >= 2 ** 8:
                self.warn_of_incorrect_input()
                return False
            self.rule_bits = '{:08b}'.format(self.rule_number)
        self.rule = {}
        # A rule R that in binary, is coded as
        #       b_0 b_1 b_2 b_2 b_2 b_2 b_6 b_7
        # maps
        # (0, 0, 0) to b_7
        # (0, 0, 1) to b_6
        # (0, 1, 0) to b_5
        # (0, 1, 1) to b_4
        # (1, 0, 0) to b_3
        # (1, 0, 1) to b_2
        # (1, 1, 0) to b_1
        # (1, 1, 1) to b_0
        for i in range(8):
            self.rule[i // 4, i // 2 % 2, i % 2] = int(self.rule_bits[7 - i])
        return True

    def display_rule_information(self):
        self.input.delete(0, END)
        self.shown_rule_number.set('Rule ' + str(self.rule_number))
        mirrored_rule_number = int(self.mirror(self.rule_bits), 2)
        # Complementation exchanges the roles of 0 and 1,
        # so the complementary rule of a rule R maps
        # (a, b, c) to not R(not a, not b, not c).
        # Hence if R is the rule that in binary, is coded as
        #       b_0 b_1 b_2 b_3 b_4 b_5 b_6 b_7
        # then the complementary of R is coded in binary as
        #       ~b_7 ~b_6 ~b_5 ~b_4 ~b_3 ~b_2 ~b_1 ~b_0        
        complementary_rule_bits =\
                self.rule_bits[: : -1].translate(str.maketrans('01', '10'))
        complementary_rule_number = int(complementary_rule_bits, 2)
        mirrored_complementary_rule_number =\
                int(self.mirror(complementary_rule_bits), 2)
        self.shown_related_rule_numbers.set(
                        'Mirrored rule: ' +
                        str(mirrored_rule_number) +
                        '            Complementary rule: ' +
                        str(complementary_rule_number) +
                        '            Mirrored complementary rule: ' +
                        str(mirrored_complementary_rule_number))
        self.shown_rule.delete(ALL)
        self.shown_rule.create_line(5, 5, 245, 5)
        self.shown_rule.create_line(5, 25, 245, 25)
        self.shown_rule.create_line(5, 45, 245, 45)
        for i in range(9):
            self.shown_rule.create_line(i * 30 + 5, 5, i * 30 + 5, 45)
        for i in range(8):
            self.shown_rule.create_text(i * 30 + 20, 15,
                                        text = '{:03b}'.format(7 - i))
            self.shown_rule.create_text(i * 30 + 20, 35,
                                        text = self.rule_bits[i])

    def warn_of_incorrect_input(self):
        tkinter.messagebox.showerror('Rule error', 'Incorrect rule number')
        self.input.delete(0, END)

    def mirror(self, rule_bits):
        # Reflection through a vertical axis,
        # so the mirrored rule of a rule R maps
        # (a, b, c) to R(c, b, a):
        # - (0, 0, 0) ---0--- to R(0, 0, 0) ---0---
        # - (0, 0, 1) ---1--- to R(1, 0, 0) ---4---
        # - (0, 1, 0) ---2--- to R(0, 1, 0) ---2---
        # - (0, 1, 1) ---3--- to R(1, 1, 0) ---6---
        # - (1, 0, 0) ---4--- to R(0, 0, 1) ---1---
        # - (1, 0, 1) ---5--- to R(1, 0, 1) ---5---
        # - (1, 1, 0) ---6--- to R(0, 1, 1) ---3---
        # - (1, 1, 1) ---7--- to R(1, 1, 1) ---7---
        return ''.join([self.rule_bits[0], self.rule_bits[4],
                        self.rule_bits[2], self.rule_bits[6],
                        self.rule_bits[1], self.rule_bits[5],
                        self.rule_bits[3], self.rule_bits[7]])

    def work_from_single_1(self):
        input = self.get_rule()
        if input == False:
            return
        if input == None or self.previous_rule_number == self.rule_number:
            if self.displaying_history_from_single_1:
                return
        else:
            self.display_rule_information()
        self.history.delete(ALL)
        line[0][NB_OF_STEPS] = 1
        self.history.create_rectangle(2 * NB_OF_STEPS + 3, 5,
                                      2 * NB_OF_STEPS + 5, 7,
                                      fill = HISTORY_COLOUR)
        # line[0] and line[1] will alternatively play the roles
        # of a line and the following line, the latter being determined
        # from the former.
        line = [0] * (2 * NB_OF_STEPS + 1), [0] * (2 * NB_OF_STEPS + 1)
        # Just a 1 in the middle
        a = False
        for i in range(1, NB_OF_STEPS - 1):
            line_a = line[a]
            line_not_a = line[not a]
            #             .x.
            #            .xxx.
            #           .xxxxx.
            #          .xxxxxxx.
            # Triangle increases by one cell
            # on each side for each new line.
            # All cells beyond the boundaries of the triangle
            # and on the same side have the same state,
            # which is also computed
            # (assigned to both "dots" around the x's) ...
            for j in range(NB_OF_STEPS - i - 1, NB_OF_STEPS + i + 2):
                self.compute_cell_state(line_a, line_not_a, j)
            # ... and propagated on the left side ...
            for j in range(NB_OF_STEPS - i - 1):
                line_not_a[j] = line_not_a[NB_OF_STEPS - i - 1]
            # ... and propagated on the right side.
            for j in range(NB_OF_STEPS + i + 2, 2 * NB_OF_STEPS + 1):
                line_not_a[j] = line_not_a[NB_OF_STEPS + i + 1]
            a = not a
            self.display_history_line(line, a, i, 0)
        line_a = line[a]
        line_not_a = line[not a]
        # For the last line, we do not compute the state
        # beyond the boundary of the triangle
        # (no assignment to both "dots" around the x's)
        for j in range(1, 2 * NB_OF_STEPS):
            self.compute_cell_state(line_a, line_not_a, j)
        self.display_history_line(line, not a, NB_OF_STEPS - 1, 0)
        self.previous_rule_number = self.rule_number
        self.displaying_history_from_single_1 = True

    def work_from_random_state(self):
        if self.get_rule() == False:
            return
        if self.previous_rule_number != self.rule_number:
            self.display_rule_information()
        self.history.delete(ALL)
        # .............
        # ....xxxxx....
        # ....xxxxx....
        # ....xxxxx....
        # ....xxxxx....
        # We need to generate about twice as many random numbers
        # as the number of lines being displayed, because
        # - the leftmost x on the last row depends on
        #   the last . before the first x on the last row,
        # - which depends on the second last .
        #   before the first x on the second last row,
        # - which depends on the third last .
        #   before the first x on the third last row
        # ...        
        line = ([randint(0, 1) for _ in range(4 * NB_OF_STEPS - 2)],
                [0] * (4 * NB_OF_STEPS - 2))
        self.display_history_line(line, 0, 0, NB_OF_STEPS - 2)
        a = False
        for i in range(1, NB_OF_STEPS):
            line_a = line[a]
            line_not_a = line[not a]
            for j in range(i, 4 * NB_OF_STEPS - 2 - i):
                self.compute_cell_state(line_a, line_not_a, j)
            a = not a
            self.display_history_line(line, a, i, NB_OF_STEPS - 2)
        self.previous_rule_number = self.rule_number
        self.displaying_history_from_single_1 = False

    def display_history_line(self, line, a, i, offset):
        line_a = line[a]
        for j in range(1, 2 * NB_OF_STEPS):
            if line_a[j + offset]:
                self.history.create_rectangle(2 * j + 3, 2 * i + 5,
                                              2 * j + 5, 2 * i + 7,
                                              fill = HISTORY_COLOUR)
                
    def compute_cell_state(self, previous_line, line, j):
        line[j] = self.rule[previous_line[j - 1],
                            previous_line[j],
                            previous_line[j + 1]]                    


if __name__ == '__main__':
    ElementaryCellularAutomata().mainloop()

Resource created Wednesday 05 August 2015, 11:08:27 AM, last modified Wednesday 05 August 2015, 11:08:45 AM.

file: elementary_cellular_automata.py


Back to top

COMP9021 15s2 (Principles of Programming) is powered by WebCMS3
CRICOS Provider No. 00098G