In a previous article I developed and presented a simple agent-based simulation model in Python. The model contained groups of agents on a battlefield grid. The model was coded in Python, using matplotlib for visualization. I went on to conduct a simple simulation run, showing one battle scenario and its outcome. In this article I remodel the problem. I thereby provide a more thorough introduction to agent-based modeling in Python.
Defining classes for agent-based modeling in Python
Below I remodel the problem from my previous article. I try to clean up the code and modularizing functionality into re-usable functions. The result is a modular framework and general appraoch. These can be reused for building even very advanced agent-based modeling in Python.
The agents are modelled as a class, as shown below:
# class, defining agents as abstract data types class agent: # init-method, the constructor method for agents def __init__(self,x,y,group): self.life = 100 # agent's life score self.x = x self.y = y self.group = group
The battlefield itself is modelled as a two-dimensional grid array, as shown below:
# creating empty 100 x 100 list using list comprehension in python battlefield = [[None for i in range(0,100)] for i in range(0,100)]
Defining helper functions for the simulation model
Agent groups can be populated onto the battlefield grid using the agentCreator-function.
# define a function for creating agents and assigning them to grid def agentCreator(size,group,groupList,field,n,m): # loop through entire group, i.e. in this case 1000 units for j in range(0,size): # select random available location while True: # random x coordinate x = random.choice(range(0,n)) # random y coordinate y = random.choice(range(0,m)) # check if spot is available; if not then re-iterate if field[x][y] == None: field[x][y] = agent(x=x,y=y,group=group) # append agent object reference to group list groupList.append(field[x][y]) # exit while loop; spot on field is taken break
For plotting agents with a positive life score as visual dots on the battlefield grid I create a plotting function, which can be called at any time throughout the simulation to get a snapshot of current battle status:
# import pyplot and colors from matplotlib from matplotlib import pyplot, colors # define function for plotting battlefield (all agents that are still alive) def plotBattlefield(populationArr, plotTitle): # using colors from matplotlib, define a color map colormap = colors.ListedColormap(["lightgrey","green","blue"]) # define figure size using pyplot pyplot.figure(figsize = (12,12)) # using pyplot add a title pyplot.title(plotTitle, fontsize = 24) # using pyplot add x and y labels pyplot.xlabel("x coordinates", fontsize = 20) pyplot.ylabel("y coordinates", fontsize = 20) # adjust x and y axis ticks, using pyplot pyplot.xticks(fontsize = 16) pyplot.yticks(fontsize = 16) # use .imshow() method from pyplot to visualize agent locations pyplot.imshow(X = populationArr, cmap = colormap)
Another function I will need is a function that can map a battlefield status to a numeric two-dimensional array containing values of 1.0 if an agent of type A is still located and alive within a given cell of the two-dimensional battlefield grid, or the value 2.0 if there is an agent of type B. I use this mapping process to feed my plotting function a numeric grid instead of a grid with objects of an abstract i.e. customized class:
# this function maps a battlefield grid to a numeric grid with 1 for agents of type A, 2 for type B and 0 for no agent def mapBattlefield(battlefieldArr): #.imshow() needs a matrix with float elements; populationArr = [[0.0 for i in range(0,100)] for i in range(0,100)] # if agent is of type A, put a 1.0, if of type B, pyt a 2.0 for i in range(1,100): for j in range(1,100): if battlefieldArr[i][j] == None: # empty pass # leave 0.0 in population cell elif battlefieldArr[i][j].group == "A": # group A agents populationArr[i][j] = 1.0 # 1.0 means "A" else: # group B agents populationArr[i][j] = 2.0 # 2.0 means "B" # return mapped values return(populationArr)
Using these model components I created an initial battlefield population and plotted agent locations using matplotlib. This is done in the code below and similar to my previous posts, only that in this case I use modularized functionality. The functionality of setting up an initial battlefield grid is transferred to a separate function below. That function is then executed.
# function for creating an initial battlefield grid def initBattlefield(populationSizeA,populationSizeB,battlefieldArr): # initializing new empty battlefield grid, using list comprehension in Python battlefieldArr = [[None for i in range(0,100)] for i in range(0,100)] # create empty list for containing agent references in future, type A & B agents_A = [] agents_B = [] # assigning random spots to agents of group A and B; import random agentCreator(size = populationSizeA, group = "A", groupList = agents_A, field = battlefieldArr, n = 100, m = 100) agentCreator(size = populationSizeB, group = "B", groupList = agents_B, field = battlefieldArr, n = 100, m = 100) # return populated battlefield grid return(battlefieldArr) # executing above function for a population size of 1000 for both groups battlefield = initBattlefield(populationSizeA=1000,populationSizeB=1000,battlefieldArr = battlefield) # plot battlefield status plotBattlefield(populationArr = mapBattlefield(battlefield), plotTitle = "battlefield before simulation run (green = A, blue = B)")
In the next section I will now define the rules for interaction, defining the basic strategies of the agent-based simulation.
Interaction rules for agent-based modeling in Python
In my previous post I then ran a simulation run based on the following rules:
Group A has the strategy of always hitting the same agent in each round
Group B has a random and independent strategy for attacking enemies. This means that each agent of type B will attack a randomly selected agent within that agents reach.
The simulation was conducted under the following conditions:
1) Each round is one iteration
2) In each round, each agent can attack one agent from within its reach
3) The reach of an agent is defined at the start of the simulation and defaults to 10
4) If an agent dies he will no longer be located on the battle field
5) An agent dies when his life score equals or goes below zero
6) Each agent has a randomly distributed attack damage, ranging from 10 to 60
7) In each round all agents get to launch an attack
Like in one of my previous posts I will now iterate through 50 rounds of battle. After each round, agents with a non-positive life score will be removed from the battlefield grid. As a deviation from my previous post I will however now modularize functionality.
Implementing rules and strategies into the model
First, I define a function for removing agents from the battlefield grid:
# function for removing agents from battlefield grid when life score is not strictly positive def removeDeadAgents(battlefieldArr): # identifying agents with life score of score or below - and removing them from the grid for i in range(0,len(battlefieldArr)): for j in range(0,len(battlefieldArr)): if battlefieldArr[i][j]: if battlefieldArr[i][j].life <= 0: # remove this agent since life score is not strictly positive battlefieldArr[i][j] = None
Next, I define a function implementing the fighting strategy for agents of type A:
# function implementing one round of fighting, for an agent of type A def oneRoundAgentA(i,j,attackRange): found_i = None found_j = None # look in neigbouring cells in same order for each iteration for k in range(i-attackRange,i+attackRange+1): for l in range(j-attackRange,j+attackRange+1): # check for negative index values; if so - break! if k < 0 or l < 0: break # check for index values above 99, if so break! if k > 99 or l > 99: break if battlefield[k][l]: if battlefield[k][l].group == "B": # then this is an enemy if found_i == None: found_i = k found_j = l # deal damage to identified enemies if found_i: battlefield[found_i][found_j].life = battlefield[found_i][found_j].life - random.randint(10,60)
Then I do the same for agents of type B:
# function implementing one round of fighting, for an agent of type B def oneRoundAgentB(i,j,attackRange): # first check if there even is an enemy in one of the surrounding cells enemy_found = False for k in range(i-attackRange,i+attackRange+1): for l in range(j-attackRange,j+attackRange+1): # check for negative index, if so break to next iteration if k < 0 or l < 0: break # check for index values above 99, if so break if k > 99 or l > 99: break if battlefield[k][l] != None: if battlefield[k][l].group == "A": enemy_found = True # select a random row and a random column found_i = None found_j = None while enemy_found and found_i == None: k = random.randint(i-attackRange,i+attackRange) l = random.randint(j-attackRange,j+attackRange) # check for negative index, if so continue to next iteration if k < 0 or l < 0: continue # check for index value > 99, if so continue if k > 99 or l > 99: continue if k != i: if battlefield[k][l]: if battlefield[k][l].group == "A": found_i = k found_j = l else: if l != j: if battlefield[k][l]: if battlefield[k][l].group == "A": found_i = k found_j = l # deal damage to identified enemy if found_i: battlefield[found_i][found_j].life = battlefield[found_i][found_j].life - random.randint(10,60)
I proceed to simulating the battle, using the functions already implemented:
for counter in range(0,50): # in this case I am conducting 50 iterations # iterating through all cells on the battlefield for x in range(0,len(battlefield)): for y in range(0,len(battlefield)): # print("top tier iteration, i: "+str(i)+", j: "+str(j)) # check if there is an agent within the respective cell if battlefield[x][y] != None: # depending on the type: execute respective attack strategy if battlefield[x][y].group == "A": # one round of battle for this agent of type A oneRoundAgentA(i = x, j = y,attackRange=10) else: # one round of battle for this agent of type B oneRoundAgentB(i = x, j = y,attackRange=10) # identifying agents with life score of score or below - and removing them from the grid removeDeadAgents(battlefieldArr = battlefield) # plot battlefield status plotBattlefield(populationArr = mapBattlefield(battlefield), plotTitle = "battlefield after 50 iterations (green = A, blue = B)")
So far, this has content-wise been identical to the simple simulation study presented in a previous post.
I now want to add a graph to analyze battle outcome and the progression of the battle itself. I thus define an additional plotting function which plots the number of agents by type, being still alive.
I also implement a function for updating the time series of agents still being alive.
# function for updating the time series of agents being alive def calcAgentsAliveA(resultsA,battlefieldArr): # these variables will be used for counting the number of agents alive countA = 0 countB = 0 # iterate through grid, find agents and update count if relevant for i in range(0,len(battlefieldArr)): for j in range(0,len(battlefieldArr)): if battlefieldArr[i][j]: if battlefieldArr[i][j].group == "A": countA = countA + 1 else: countB = countB + 1 # update results list and return it resultsA.append(countA) return(resultsA) # function for updating the time series of agents being alive def calcAgentsAliveB(resultsB,battlefieldArr): # these variables will be used for counting the number of agents alive countA = 0 countB = 0 # iterate through grid, find agents and update count if relevant for i in range(0,len(battlefieldArr)): for j in range(0,len(battlefieldArr)): if battlefieldArr[i][j]: if battlefieldArr[i][j].group == "A": countA = countA + 1 else: countB = countB + 1 # update results list and return it resultsB.append(countB) return(resultsB) # function for plotting the number of agents still alive def plotNumberOfAgentsAlive(plotTitle,iterations,resultsA,resultsB): from matplotlib import pyplot pyplot.figure(figsize = (12,12)) pyplot.title(plotTitle, fontsize = 24) pyplot.xlabel("iteration", fontsize = 20) pyplot.ylabel("agents still alive", fontsize = 20) pyplot.xticks(fontsize = 16) pyplot.yticks(fontsize = 16) ax = pyplot.subplot() ax.plot(iterations,resultsA, label = "type a agents") ax.plot(iterations,resultsB, label = "type b agents") ax.legend(fontsize=16)
Now, I can conduct another simulation run using, displaying the graph. Since I will be running various simulation scenarios I will write the simulation run to a function as well:
# defining function for conducting a simulation run def simulationRun(iterationLimit,attackRange,showPlot): iterations = [] resultsA = [] resultsB = [] for counter in range(0,iterationLimit): # in this case I am conducting 50 iterations # update iterations # update results iterations.append(counter+1) resultsA = calcAgentsAliveA(resultsA,battlefield) resultsB = calcAgentsAliveB(resultsB,battlefield) # iterating through all cells on the battlefield for x in range(0,len(battlefield)): for y in range(0,len(battlefield)): # print("top tier iteration, i: "+str(i)+", j: "+str(j)) # check if there is an agent within the respective cell if battlefield[x][y]: # depending on the type: execute respective attack strategy if battlefield[x][y].group == "A": # one round of battle for this agent of type A oneRoundAgentA(i = x, j = y, attackRange = attackRange) else: # one round of battle for this agent of type B oneRoundAgentB(i = x, j = y, attackRange = attackRange) # identifying agents with life score of score or below - and removing them from the grid removeDeadAgents(battlefieldArr = battlefield) # plot battle progression, but only if plot should be displayed if showPlot: plotNumberOfAgentsAlive("battle progression",iterations,resultsA,resultsB) # return results return([resultsA,resultsB])
Executing the simulation run and reporting statistics
I can now conduct a simulation run using one line of code only:
battlefield = initBattlefield(populationSizeA = 1000,populationSizeB = 1000,battlefieldArr = battlefield) results = simulationRun(iterationLimit = 50, attackRange = 10, showPlot = True)
It seems agents of type B have a somewhat effective attack strategy. But what if their number count is slightly lower when battle begins? Below I plot battle progression in a fight with 1000 initial A and 950 initial B agents.
battlefield = initBattlefield(populationSizeA = 1000,populationSizeB = 950,battlefieldArr = battlefield) results = simulationRun(iterationLimit = 50, attackRange = 10, showPlot = True)
What happens if attack range is reduced to 5?
battlefield = initBattlefield(populationSizeA = 1000,populationSizeB = 1000,battlefieldArr = battlefield) results = simulationRun(iterationLimit = 50, attackRange = 5, showPlot = True)
Initial battlefield location of agents is random. Final battle outcome might thus be randomly distributed as well. I want to investigate to what extent results are randomized. For this I implement a sensitivity test function that conduct repeats the simulation over and over again. This function will return results that can be visualized in a histogram, representing the total amount of agents alive at simulation end. In this case I repeat the simulation 50 times.
Below I implement the function conducting the sensitivity test:
# this function is used for conducting a sensitivity test with regards to battle outcome def sensitivityTest(iterationLimit,attackRange,runs): # indicate that battlefield is a global variable global battlefield # empty lists which will contain the final number of agents of respective type, at the end of battle outcomeA = [] outcomeB = [] # repeating the simulation with defined attack range and iteration limit, for "runs" number of times for i in range(0,runs): # before each simulation run battlefield must be initialized battlefield = initBattlefield(populationSizeA = 1000,populationSizeB = 950,battlefieldArr = battlefield) # conduct simulation run results = simulationRun(iterationLimit=iterationLimit,attackRange = attackRange,showPlot = False) # append result to relevant outcome list outcomeA.append(results[0][iterationLimit-1]) outcomeB.append(results[1][iterationLimit-1]) # returning the result in a list with two-sublist return([outcomeA,outcomeB])
Below I implement the histogram plotting function:
# function for plotting a histogram def plotHistogram(plotTitle,resultsA,resultsB): from matplotlib import pyplot pyplot.figure(figsize = (12,12)) pyplot.title(plotTitle, fontsize = 24) pyplot.xlabel("number of agents still alive", fontsize = 20) pyplot.ylabel("absolute frequency", fontsize = 20) pyplot.xticks(fontsize = 16) pyplot.yticks(fontsize = 16) ax = pyplot.subplot() ax.hist(resultsA, bins=20, histtype="bar",color="red",label="agent A",alpha=0.25) ax.hist(resultsB, bins=20, histtype="bar",color="blue",label="agent B",alpha=0.25) ax.legend(fontsize=16)
Finally, the code below executes the seensitivity test and plots the result.
# executing the sensitivityTest results = sensitivityTest(iterationLimit = 50,attackRange = 5,runs = 50) plotHistogram(plotTitle = "distribution of agents",resultsA = results[0], resultsB = results[1])
This result is for an attack range of 5.
Using this approach, a very advanced agent-based simulation model can be developed.
Other references for agent-based modeling in Python
I list some links to related content below:
- Link: A simple agent-based simulation run visualized using matplotlib in Python
- Link: Developing a simple agent-based model in Python
- Link: Visualizing 2D grids and arrays using matplotlib in Python
- Link: Towards a comprehensive agent-based simulation framework incorporating joint activity-scheduling and ride-sharing within households
Data scientist focusing on simulation, optimization and modeling in R, SQL, VBA and Python
Leave a Reply