2

I have made a class which initiates and updates the CA data, and I have made a function 'Simulate' which updates the cells based on the rule that fire spreads across trees, and leaves empty spaces. Empty spaces are replaced with trees based on a given probability.

There is a problem where it appears my function is applying the rule to the current time data holder, rather than the previous time data holder. I have set prevstate = self.state to act as a temporary data holder for the previous iteration, but running small tests I find that it gives the same results as if I didn't include this line at all. What am I doing wrong?

import numpy as np
import random
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap, colorConverter
from matplotlib.animation import FuncAnimation

#dimentions:
x = 10
y = 10

lighting = 0  #set to 0 for testing
grow = 0.3


#parameter values
empty = 0
tree = 1
fire = 2

random.seed(1)

#CA Rule definition
def update(mat, i, j, lighting, grow, prob):
    if mat[i, j] == empty:
        if prob < grow:
            return tree
        else:
            return empty
    elif mat[i, j] == tree:
        if max(mat[i-1, j], mat[i+1, j], mat[i, j-1], mat[i, j+1]) == fire:
            return fire
        elif prob < lighting:
            return fire
        else:
            return tree
    else:
        return empty


########## Data Holder
class Simulation:
    def __init__(self):
        self.frame = 0
        #self.state = np.random.randint(2, size=(x, y)) commented out for testing
        self.state = np.ones((x, y))
        self.state[5, 5] = 2  #initial fire started at this location for testing

    def updateS(self):
        prevstate = self.state    #line of code i think should be passing previous iteration through rule

        for i in range(1, y-1):
            for j in range(1, x-1):
                prob = random.random()
                self.state[i, j] = update(prevstate, i, j, lighting, grow, prob)

    def step(self):
        self.updateS()
        self.frame += 1


simulation = Simulation()
figure = plt.figure()

ca_plot = plt.imshow(simulation.state, cmap='seismic', interpolation='bilinear', vmin=empty, vmax=fire)
plt.colorbar(ca_plot)
transparent = colorConverter.to_rgba('black', alpha=0)
#wall_colormap = LinearSegmentedColormap.from_list('my_colormap', [transparent, 'green'], 2)


def animation_func(i):
    simulation.step()
    ca_plot.set_data(simulation.state)
    return ca_plot

animation = FuncAnimation(figure, animation_func, interval=1000)
mng = plt.get_current_fig_manager()
mng.window.showMaximized()
plt.show()

Any comments on better ways to implement a CA are most welcome!

ushham
  • 185
  • 1
  • 7

2 Answers2

4

Python assignments are pointers... So when you update self.state, then prevstate is also updated.

I expect if you set to:

prevstate = copy.copy(self.state)

That should fix your problem.

Copy docs

jabberwocky
  • 995
  • 10
  • 18
  • Thank you very much! Just to clarify, my issue was that as `prevstate` was pointing to `self.state`, it was updating and was always identical to `self.state` through the iteration? – ushham Apr 13 '20 at 11:14
  • No problem. Think of it more like. There's one array. The line: `prevstate = self.state` set both your variables to point at the same array. But still one array. Copy.copy() makes there be 2 arrays assigned to the 2 variables. – jabberwocky Apr 13 '20 at 13:32
2

As jabberwocky correctly notes, your problem is that the line prevstate = self.state makes prevstate a new reference to the same numpy array as self.state, so that modifying the contents of one also modifies the other.

Instead of copying the array on every iteration, however, a slightly more efficient solution would be to preallocate two arrays and swap them, something like this:

class Simulation:
    def __init__(self):
        self.frame = 0
        self.state = np.ones((x, y))
        self.state[5, 5] = 2
        self.prevstate = np.ones((x, y))  # <-- add this line

    def updateS(self):
        self.state, self.prevstate = self.prevstate, self.state  # <-- swap the buffers

        for i in range(1, y-1):
            for j in range(1, x-1):
                prob = random.random()
                self.state[i, j] = update(self.prevstate, i, j, lighting, grow, prob)

I say "slightly" because all you're really saving is a numpy array copy and some work for the garbage collector. However, if you optimize your inner state update loop enough — maybe e.g. implementing the CA rule using numba — the relative cost of an extra array copy will start to be significant. In any case, there are no real down sides to using this "double buffering" method, so it's a good habit to pick up.

Ilmari Karonen
  • 49,047
  • 9
  • 93
  • 153
  • Thank you, and great tip about numba. It is running much faster with your edits! Could you please explain the logic behind the syntax of the line which swaps the buffers? I do not understand 1. how the commas are used, or how setting `self.prevstate = self.prevstate` updates either array? – ushham Apr 15 '20 at 07:54
  • 1
    It's a [common idiom to swap variables in Python](https://stackoverflow.com/q/14836228) using [tuple unpacking](https://treyhunner.com/2018/03/tuple-unpacking-improves-python-code-readability/). The commas have higher precedence than the assignment operator, so it's parsed like `(state, prevstate) = (prevstate, state)`. (Note that [it doesn't work for numpy array slices](https://stackoverflow.com/q/14933577), though.) – Ilmari Karonen Apr 15 '20 at 07:54