numpy vectorization of cellular automata

Question:

Trying to optimize my current implementation of a program that generates cellular automata using Wolfram Numbering. I am having trouble applying the rule to the board after calculating the neighbors for each cell. The current example uses 2 states and is the same as Conway’s Game of Life, but my program can do any number of states. The decimal 224 corresponds to the ruleset for CGOL in the following way:
[0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0]
Basically, there are 18 positions, or nine possible neighborhood sums for each state (0-8).
If the current cell is 1, you index into the rule in the following way:

>>> [0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0][1::2]
[0, 0, 1, 1, 0, 0, 0, 0, 0]

1 being the value of the cell, and 2 being the number of states. As you can see, if the state is 1, then if there are 2 or 3 neighbors the cell survives, else dies. From there you index w/ the neighborhood sum for that cell to get the actual update value of the cell. So to update a cell in each generation you do: Rule[state_value::total_states][sum of neighbors].
E.g.,

>>> [0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0][1::2][2]
1

Currently then, I have the grid of all cells, called world, of an arbitrary shape, another equally shaped numpy array that has the sum of all the neighbors for each of those cells calculated using convolve from scipy – call it nbrs -, and the previously mentioned list for the rule – is it possible to update the value of each cell in world while avoiding a for loop?

For instance:
world = rule[cell_value::total_states][sum_of_neighbors_for_given_cell_in_nbrs]

Asked By: user18615293

||

Answers:

You haven’t given us a lot of code to work with, so here is a minimal idea of how it could work.

First, create an array that you can index with the current state to get the rule:

rule = [0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0]
rule_map = np.stack((rule[0::2], rule[1::2]), axis=0)

Now you can use your current states to get the rule for each cell:

world = np.random.randint(2, size=(5, 6))
cur_cell_rules = rule_map[world]  # shape: (5, 6, 9)

To get the new world state, we can use index interators. Here, I use an array containing all world indices to first get the (flattened) current cell neighborhood sums, and then use those to get the (flattened) new states. In the assignment, I unflatten it again to the world shape. (There probably is an easier way to do this…)

cur_cell_neighborhood_sum = ...  # shape: (5, 6)
world_ind = np.asarray([*np.ndindex(world.shape)])

# update world
world[world_ind[:, 0], world_ind[:, 1]] = cur_cell_rules[world_ind[:, 0], world_ind[:, 1], cur_cell_neighborhood_sum[world_ind[:, 0], world_ind[:, 1]]]

Edit:

To avoid the large cur_cell_rules array, you can go the other way, too:

world_potential = rule_map[:, cur_cell_neighborhood_sum]  # shape: (2, 5, 6)

# update world, this time in smaller steps
world_flat = world[world_ind[:, 0], world_ind[:, 1]]
world_new_flat = world_potential[world_flat, world_ind[:, 0], world_ind[:, 1]]
world[world_ind[:, 0], world_ind[:, 1]] = world_new_flat
Answered By: cheersmate