pythonanimationmatplotlibconways-game-of-life

Issues with matplotlib Func.Animation


I'm trying to do some animation with matplotlib (Conway's game of life, to be specific) and have some problems with the .FuncAnimation

I figured out different cases which partly worked (but not the way I want) or result in different errors. I would like to understand the errors and work out a proper version of the code. Thanks for your help!

The function called through the .FuncAnimation is gameoflife which uses the variables w, h, grid to update the image.

For the whole commented code see below.

Case 1: Global Variables

If I use global variables everything works fine. I define w, h, grid global before I call gameoflife(self) through anim = animation.FuncAnimation(fig, gameoflife)

In gameoflife(self) I also define w, h, grid as global variables

w, h, grid = "something"

def gameoflife(self):
    global w
    global h
    global grid
    .
    .
    .
    img = ax.imshow(grid)
    return img

fig, ax = plt.subplots()
plt.axis('off')
img = ax.imshow(grid)
anim = animation.FuncAnimation(fig, gameoflife)
plt.show()

As said, this results in the animation as wanted. But I would like to get rid of the global variables, because of which I tried something else:

Case 2: Passing Objects

I don't defined w, h, grid as globals in gameoflife but passed them with anim = animation.FuncAniation(fig, gameoflife(w,h,grid)).

(I know that w, h, grid are still global in my example. I work on another version where they are not but since the errors are the same I think this simplified version should do it.)

This results in the following Error:

TypeError: 'AxesImage' object is not callable

I don't understand this error, since I don't call ax with the code changes.

w, h, grid = "something"

def gameoflife(w, h, grid):
    .
    .
    .
    img = ax.imshow(grid)
    return img

fig, ax = plt.subplots()
plt.axis('off')
img = ax.imshow(grid)
anim = animation.FuncAnimation(fig, gameoflife(w,h,grid))
plt.show()

Case 3: Passing Objects with fargs

In the third case I try to pass w, h, grid with the "frags" argument of .FuncAnimation resulting in just the first frame. (Or the first two, depending how you see it. The "first" frame is actually drawn through img = ax.imshow(grid))

w, h, grid = "something"

def gameoflife(self, w, h, grid):
    .
    .
    .
    img = ax.imshow(grid)
    return img

fig, ax = plt.subplots()
plt.axis('off')
img = ax.imshow(grid)
anim = animation.FuncAnimation(fig, gameoflife, fargs=(w,h,grid))
plt.show()

Complete Code

I hope its properly commented

There are two parts (beginning and end) where you can comment/uncomment parts to generate the respective case. By default its Case 1.

import random
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation

##defining grid size
w= 20
h = 20

##generating random grid
grid = np.array([[random.randint(0,1) for x in range(w)] for y in range(h)])

######
# Choose for different cases
######

##Case 1: Global Variables
def gameoflife(self):
    global w
    global h
    global grid

##Case 2: Passing Objects
#def gameoflife(w, h, grid):

##Case 3: Passing Objects with fargs
#def gameoflife(self, w, h, grid):

####### Choose part over
    
    # wt, ht as test values for position
    # x,y to set calculation position
    wt = w-1
    ht = h-1
    x,y = -1,0 #results in 0,0 for the first position
    
    # defining grid for calculation (calgrid)
    calgrid = np.array([[0 for x in range(w)] for y in range(h)])
    
    # testing for last position
    while y<ht or x<wt:    
        # moving position through the grid
        if x == wt:
            y +=1
            x = 0
        else:
            x += 1

        #sorrounding cells check value
        scv = 0
        
        #counting living cells around position x,y
        #if-else for exceptions at last column and row
        if y == ht:
            if x == wt:
                scv = grid[x-1][y-1] + grid[x][y-1] + grid[0][y-1] + grid[x-1][y] + grid[0][y] + grid[x-1][0] + grid[x][0] + grid[0][0]
            else:
                scv = grid[x-1][y-1] + grid[x][y-1] + grid[x+1][y-1] + grid[x-1][y] + grid[x+1][y] + grid[x-1][0] + grid[x][0] + grid[x+1][0]
        else:
            if x == wt:
                scv = grid[x-1][y-1] + grid[x][y-1] + grid[0][y-1] + grid[x-1][y] + grid[0][y] + grid[x-1][y+1] + grid[x][y+1] + grid[0][y+1]
            else:
                scv = grid[x-1][y-1] + grid[x][y-1] + grid[x+1][y-1] + grid[x-1][y] + grid[x+1][y] + grid[x-1][y+1] + grid[x][y+1] + grid[x+1][y+1]


        # test cell to conditions and write result in calgrid
        if grid[x][y] == 0:
            if scv == 3:
                calgrid [x][y] = 1
        else :
            if 1<scv<4:
                calgrid [x][y] = 1
    
    # updating grid, generating img and return it
    grid = calgrid
    img = ax.imshow(grid)
    return img


fig, ax = plt.subplots()
plt.axis('off')
img = ax.imshow(grid) #generates "first" Frame from seed
#####
# Choose vor Case
#####

## Case 1: Global Variables
anim = animation.FuncAnimation(fig, gameoflife)

## Case 2: Passing Variables
#anim = anim = animation.FuncAnimation(fig, gameoflife(w,h,grid))

## Case 3: Passing Variables with fargs
#anim = animation.FuncAnimation(fig, gameoflife, fargs=(w,h,grid))

####### Choose part over
plt.show()

Thanks for help and everything


Solution

  • Case 2: You call the function and pass the result into FuncAnimation.

    def gameoflife(w,h,grid):
        # ...
        return ax.imshow(grid)
    anim = animation.FuncAnimation(fig, gameoflife(w,h,grid))
    

    Is essentially the same as

    anim = animation.FuncAnimation(fig, ax.imshow(grid))
    

    which will not work because the second argument is expected to be a function, not the return of a function (in this case an image).

    To explain this better, consider a simple test case. g is a function and expects a function as input. It will return the function evaluated at 4. If you supply a function f, all works as expected, but if you supply the return of a function, it would fail if the return is not itself a function, which can be evaluated.

    def f(x):
        return 3*x
    
    def g(func):
        return func(4)
    
    g(f)     # works as expected
    g(f(2))  # throws TypeError: 'int' object is not callable
    

    Case 3: You calling the function repeatedly with the same arguments

    In the case of

    anim = animation.FuncAnimation(fig, gameoflife, fargs=(w,h,grid))
    

    you call the function gameoflife with the same initial arguments w,h,grid for each frame in the animation. Hence you get a static animation (the plot is animated, but each frame is the same, because the same arguments are used).

    Conclusion. Stay with Case 1

    Because case 1 is working fine, I don't know why not use it. A more elegant way would be to use a class and use class variables as e.g. in this question.