Skip to main content

· 4 min read
Ian Sotnek

Conway's Game of Life is a fascinating example of complex behavior of a system of cellular automata emerging from a set of simple rules. Programming this seemed to me like the 'Hello World!' of evolving systems, so here is my attempt - with some brief annotation - using basic matrix operations in Python.

GIF

Importing Packages

First, I'm importing the packages I need for this script. Here I'm using some matplotlib functions to visualize the results of the simulation this script creates & runs, followed by NumPy, which is used for most operations in the simulation, and SciPy, which is used for the convolution operation.

from matplotlib import animation
from matplotlib.animation import FFMpegWriter
import matplotlib.pyplot as plt
import numpy as np
import scipy as sp

Set Up the Simulation

Next, I set the initialization parameters of the simulation.

size = 64 # The length of either side of the game's square matrix
init_number = 750 # The number of cells to initialize as 'live'
generations = 1000 # The number of generations to run the game

if init_number > size * size:
raise Exception("Number of cells to initialize cannot exceed the total number of cells in the space")

Now that the simulation's parameters have been set, I can set up the simulation as such:

def setup_game(state, init_number):
random_x_idxs = np.random.randint(1, state.shape[0], init_number)
random_y_idxs = np.random.randint(1, state.shape[1], init_number)

state[random_x_idxs, random_y_idxs] = 1
return state

And now I have a sparse square matrix with some cells set to the value of 1 (here we use 0 to denote a dead cell and 1 to denote a live cell). This is the initial state of the simulation!

Define the System's Evolution

Now we need to evolve the state according to a set of rules. The rules we follow for this simulation are as follows:

  1. Any live cell with fewer than two live neighbors dies, as if by underpopulation.
  2. Any live cell with two or three live neighbors lives on to the next generation.
  3. Any live cell with more than three live neighbors dies, as if by overpopulation.
  4. Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction.

So, what we need to do is sum the number of live neighbors for each cell and then update the state of each cell. To avoid nested for-loops, here I use the SciPy 2-dimensional convolution function with a kernel which sums the number of live neighbors each cell has.

def update_game(state):
kernel = np.array([[1, 1, 1],
[1, 0, 1],
[1, 1, 1]])

live_cells = state == 1
dead_cells = state == 0

new_state = sp.ndimage.convolve(state,
kernel,
mode='constant',
cval=0)
born = (new_state == 3) & (state == 0)
survive = ((new_state == 2) | (new_state == 3)) & (state == 1)
state[:] = 0 # Reset state
state[born | survive] = 1 # Apply born and survive rule

return state

Now that we've defined the update rule for the system, we can set up an evolution function which orchestrates the game updates across n generations and saves the state at each timestep in the a 3-D tensor 'Generations'.

def evolution(generations):
state = np.zeros((size, size))
state = setup_game(state, init_number)
print(f'Life Initialized')

Generations = np.zeros((size, size, generations+1))
Generations[:, :, 0] = state

for i in range(generations+1):
state = update_game(np.copy(state))
Generations[:, :, i] = state
print(f'Sim Completed')
return Generations

Visualize the Simulation

Of course, we want to visualize the system's evolution. Here I use Matplotlib to create an MP4 video of our Generations tensor being plotted at each 'generation'.

def plot(Generations):
writer = FFMpegWriter(fps=20, bitrate=1800)
fig = plt.figure()
img = plt.imshow(Generations[:, :, 0], animated = True)

def update(i):
img.set_array(Generations[:, :, i])
return img,
ani = animation.FuncAnimation(fig, update, frames = Generations.shape[2], blit = True)
ani.save('Life.mp4', writer = writer)
print(f'Video Saved')

Run the Simulation

Now we need to call our evolution and plot functions...

def main():
Generations = evolution(generations)
plot(Generations)

...and finally enable the script to run:

if __name__ == '__main__':
main()

· 5 min read
Ian Sotnek

Note: this is part one of an as-yet unknown number of informal articles I’ll be posting here related to the dynamics of human-machine teams, including how these dynamics are informed by the degree of trust between teammates.


The wave of generative AI products and services that has swept the AI landscape and the popular consciousness in 2023 has done so for two reasons. The first is that this has been the most publicly engrossing, and some might say visceral, evidence that advancements in automation are beginning to encroach on domains like creativity, insight, and other characteristics that were previously characteristic of only natural intelligence (r.i.p. Turing test). The second is that generative AI offers clear and diverse paths to improving or enhancing human capabilities, enabling users to be somehow ‘more’ than they otherwise could be. Now a novice coder can generate a working application prototype with the help of code Llama or Github co-pilot in hours or days instead of weeks or months, just as tools like Midjourney and ChatGPT can allow teams to churn out dozens of ideas for marketing copy, website content, presentations, et cetera, allowing them to iterate quickly towards a desired end result. This promise of somehow improving the outcome, efficiency, safety, et cetera of human endeavors through the application of automation did not begin with generative AI, however: this is just the most recent and extremely visible example of the decades-old concept of human-machine teaming (HMT).

Untitled

Some machines are better teammates than others

Human-machine teaming is, simply put, the collaboration of one or more humans with one or more machines on some goal-directed task or tasks.

Paradigms in HMT can be broken out along several dimensions, including whether the team will be operating purely as an information system or a cyber-physical system (CPS), as well as the channel through which the HMT is able to interact with the world. In the table below, I present some examples of how different HMTs can be considered in this framework:

Untitled

Table 1: A framework to consider HMTs based on the domain in which they are intended to act and the member of the team which is ultimately responsible for making a decision.

An HMT is inherently a meta-system which is comprised of human and non-human agents with varying degrees of complexity underlying their decision-making processes, as well as the interrelationships that exist between members of the HMT. Crucially, it must be remembered that the state of an HMT might evolve an a function of time, as the decision-making processes or, if appropriate, the physical capabilities of any of its members is altered or if relationships between members of the team change (e.g. a human may rely more on its machine teammates temporarily if its cognitive and / or physiological properties are altered due to the time criticality of making an accurate decision).

Why HMTs?

A core part of being a human decision-maker over the previous few millennia has been accepting - to a certain extent - and seeking to address shortcomings in our ability to make the best decisions possible based on the most complete and accurate information possible. For the vast majority of this time, this approach involved extending the human umwelt with the help of other animals such as dogs and pigs for olfaction-based detection and classification, as well generally using other animals to aid our decision-making processes such as placing canaries in coal mines to inform humans of degenerating air quality. This use of assistive agents to augment human sensation naturally evolved to encompass more analytical tasks as advancements in computer science gave rise to machines which can, in some circumstances, perform analytical tasks. However, the analytical tasks which the present generation of machines excels at are still largely complementary to humans’ competencies.

Untitled

****Table 2: HMTs leverage the complementary strengths of their human and non-human components.****

What this table illustrates is that the promise of HMTs is to, in a domain or application-specific manner, pair teammates with complementary skillsets such that a shortcoming in the attributes of one team member does not imperil an operational objective. For example, cognitively speaking, humans excel at abstract reasoning under novel and uncertain conditions, whereas machines can much better cope with high-dimensional and / or high-throughput data analysis. Therefore, a machine teammate is well-suited to identifying threatening traffic patterns in network flow data, and the human is well-suited to taking that information and applying it by taking some corrective action or informing relevant stakeholders.

Dynamics at Play

As mentioned previously, the HMT is comprised of both the teammates themselves, as well as the relationships between the teammates, which may evolve over time. Ultimately, the performance of a HMT depends on all team members performing appropriately in their roles and being utilized the the extent that best improves the performance of the team. Therefore, you can consider the performance space to be the space of possible outcomes of the HMT as measured by situation-specific metrics, and the evolution of this performance space to be a function of numerous factors relating to the performance of the teammates as individuals and the degree to which the teammates perform cohesively with the other members of the team. Defining these dynamics will be the purpose of the next installments in this series, coming up next!