Em um post anterior eu construí um modelo simples de simulação baseado em agentes, contendo grupos de agentes que podem ser localizados em uma grade de campo de batalha. O modelo foi codificado em Python, usando matplotlib para visualização. Passei a conduzir uma simulação simples, mostrando um cenário de batalha e seu resultado.
Abaixo remodelo o problema, tentando limpar o código e modularizando a funcionalidade em funções reutilizáveis. O resultado será uma estrutura modular e descrição de abordagem geral que pode ser usada para construir modelos de simulação baseados em agentes muito avançados.
Os agentes são modelados como uma classe, conforme mostrado abaixo:
class agent: def __init__(self,x,y,group): self.life = 100 # pontuação de vida do agente self.x = x self.y = y self.group = group
O próprio campo de batalha é modelado como uma matriz de grade bidimensional, conforme mostrado abaixo:
battlefield = [[None for i in range(0,100)] for i in range(0,100)]
Os grupos de agentes podem ser preenchidos na grade do campo de batalha usando a função agentCreator:
def agentCreator(size,group,groupList,field,n,m): for j in range(0,size): while True: x = random.choice(range(0,n)) y = random.choice(range(0,m)) if field[x][y] == None: field[x][y] = agent(x=x,y=y,group=group) groupList.append(field[x][y]) break
Para plotar agentes com uma pontuação de vida positiva como pontos visuais na grade do campo de batalha, crio uma função de plotagem, que pode ser chamada a qualquer momento durante a simulação para obter um instantâneo do status atual da batalha:
from matplotlib import pyplot, colors def plotBattlefield(populationArr, plotTitle): colormap = colors.ListedColormap(["lightgrey","green","blue"]) pyplot.figure(figsize = (12,12)) pyplot.title(plotTitle, fontsize = 24) pyplot.xlabel("x coordinates", fontsize = 20) pyplot.ylabel("y coordinates", fontsize = 20) pyplot.xticks(fontsize = 16) pyplot.yticks(fontsize = 16) pyplot.imshow(X = populationArr, cmap = colormap)
Outra função que vou precisar é uma função que possa mapear um status do campo de batalha para uma matriz numérica bidimensional contendo valores de 1,0 se um agente do tipo A ainda estiver localizado e vivo dentro de uma determinada célula da grade bidimensional do campo de batalha, ou o valor 2.0 se houver um agente do tipo B. Eu uso esse processo de mapeamento para alimentar minha função de plotagem uma grade numérica em vez de uma grade com objetos de uma classe abstrata, ou seja, personalizada:
def mapBattlefield(battlefieldArr): populationArr = [[0.0 for i in range(0,100)] for i in range(0,100)] for i in range(1,100): for j in range(1,100): if battlefieldArr[i][j] == None: pass elif battlefieldArr[i][j].group == "A": populationArr[i][j] = 1.0 else: populationArr[i][j] = 2.0 return(populationArr)
Usando esses componentes do modelo, criei uma população inicial do campo de batalha e tracei as localizações dos agentes usando matplotlib. Isso é feito no código abaixo e semelhante aos meus posts anteriores, só que neste caso utilizo a funcionalidade modularizada. A funcionalidade de configurar uma grade inicial do campo de batalha é transferida para uma função separada abaixo. Essa função é então executada.
def initBattlefield(populationSizeA,populationSizeB,battlefieldArr): battlefieldArr = [[None for i in range(0,100)] for i in range(0,100)] agents_A = [] agents_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(battlefieldArr) battlefield = initBattlefield(populationSizeA=1000,populationSizeB=1000,battlefieldArr = battlefield) plotBattlefield(populationArr = mapBattlefield(battlefield), plotTitle = "battlefield before simulation run (green = A, blue = B)")
No meu post anterior, executei uma simulação com base nas seguintes regras:
O Grupo A tem a estratégia de sempre acertar o mesmo agente em cada rodada
O Grupo B tem uma estratégia aleatória e independente para atacar os inimigos. Isso significa que cada agente do tipo B atacará um agente selecionado aleatoriamente dentro do alcance desse agente.
A simulação foi realizada nas seguintes condições:
1) Cada rodada é uma iteração
2) Em cada rodada, cada agente pode atacar um agente ao seu alcance
3) O alcance de um agente é definido no início da simulação e o padrão é 10
4) Se um agente morrer, ele não estará mais localizado no campo de batalha
5) Um agente morre quando sua pontuação de vida é igual ou abaixo de zero
6) Cada agente tem um dano de ataque distribuído aleatoriamente, variando de 10 a 60
7) Em cada rodada todos os agentes podem lançar um ataque
Como em um dos meus posts anteriores, agora vou repetir 50 rodadas de batalha. Após cada rodada, os agentes com pontuação de vida não positiva serão removidos da grade do campo de batalha. Como um desvio do meu post anterior, agora vou modularizar a funcionalidade.
Primeiro, defino uma função para remover agentes da grade do campo de batalha:
def removeDeadAgents(battlefieldArr): for i in range(0,len(battlefieldArr)): for j in range(0,len(battlefieldArr)): if battlefieldArr[i][j]: if battlefieldArr[i][j].life <= 0: battlefieldArr[i][j] = None
Em seguida, defino uma função que implementa a estratégia de combate para agentes do tipo A:
def oneRoundAgentA(i,j,attackRange): found_i = None found_j = None for k in range(i-attackRange,i+attackRange+1): for l in range(j-attackRange,j+attackRange+1): if k < 0 or l < 0: break if k > 99 or l > 99: break if battlefield[k][l]: if battlefield[k][l].group == "B": # então isso é um inimigo if found_i == None: found_i = k found_j = l if found_i: battlefield[found_i][found_j].life = battlefield[found_i][found_j].life - random.randint(10,60)
Então faço o mesmo para agentes do tipo B:
def oneRoundAgentB(i,j,attackRange): enemy_found = False for k in range(i-attackRange,i+attackRange+1): for l in range(j-attackRange,j+attackRange+1): if k < 0 or l < 0: break if k > 99 or l > 99: break if battlefield[k][l] != None: if battlefield[k][l].group == "A": enemy_found = True 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) if k < 0 or l < 0: 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 # causa dano ao inimigo identificado if found_i: battlefield[found_i][found_j].life = battlefield[found_i][found_j].life - random.randint(10,60)
Prossigo simulando a batalha, utilizando as funções já implementadas:
for counter in range(0,50): # in this case I am conducting 50 iterations for x in range(0,len(battlefield)): for y in range(0,len(battlefield)): if battlefield[x][y] != None: if battlefield[x][y].group == "A": oneRoundAgentA(i = x, j = y,attackRange=10) else: oneRoundAgentB(i = x, j = y,attackRange=10) removeDeadAgents(battlefieldArr = battlefield) plotBattlefield(populationArr = mapBattlefield(battlefield), plotTitle = "battlefield after 50 iterations (green = A, blue = B)")
Até agora, em termos de conteúdo, isso foi idêntico ao estudo de simulação simples apresentado em um post anterior.
Agora quero adicionar um gráfico para analisar o resultado da batalha e a progressão da própria batalha. Assim, defino uma função de plotagem adicional que plota o número de agentes por tipo, estando ainda vivos.
Também implemento uma função para atualizar a série temporal de agentes ainda vivos.
def calcAgentsAliveA(resultsA,battlefieldArr): countA = 0 countB = 0 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 resultsA.append(countA) return(resultsA) def calcAgentsAliveB(resultsB,battlefieldArr): countA = 0 countB = 0 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 resultsB.append(countB) return(resultsB) 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)
Agora, posso realizar outra simulação usando, exibindo o gráfico. Como estarei executando vários cenários de simulação, também escreverei a simulação executada em uma função:
# definindo função para conduzir uma simulação def simulationRun(iterationLimit,attackRange,showPlot): iterations = [] resultsA = [] resultsB = [] for counter in range(0,iterationLimit): # neste caso estou realizando 50 iterações # iterações de atualização # atualiza resultados iterations.append(counter+1) resultsA = calcAgentsAliveA(resultsA,battlefield) resultsB = calcAgentsAliveB(resultsB,battlefield) # iterando por todas as células no campo de batalha for x in range(0,len(battlefield)): for y in range(0,len(battlefield)): # verifica se existe um agente dentro da respectiva célula if battlefield[x][y]: # dependendo do tipo: execute a respectiva estratégia de ataque if battlefield[x][y].group == "A": # uma rodada de batalha para este agente do tipo A oneRoundAgentA(i = x, j = y, attackRange = attackRange) else: # uma rodada de batalha para este agente do tipo B oneRoundAgentB(i = x, j = y, attackRange = attackRange) # identificando agentes com pontuação de vida igual ou inferior - e removendo-os da grade removeDeadAgents(battlefieldArr = battlefield) # enredo progressão da batalha, mas apenas se o enredo deve ser exibido if showPlot: plotNumberOfAgentsAlive("battle progression",iterations,resultsA,resultsB) # retorna resultados return([resultsA,resultsB])
Agora posso realizar uma simulação usando apenas uma linha de código:
battlefield = initBattlefield(populationSizeA = 1000,populationSizeB = 1000,battlefieldArr = battlefield) results = simulationRun(iterationLimit = 50, attackRange = 10, showPlot = True)
Parece que os agentes do tipo B têm uma estratégia de ataque um tanto eficaz. Mas e se a contagem de números deles for um pouco menor quando a batalha começar? Abaixo eu traço a progressão da batalha em uma luta com 1000 agentes iniciais A e 950 agentes iniciais B.
battlefield = initBattlefield(populationSizeA = 1000,populationSizeB = 950,battlefieldArr = battlefield) results = simulationRun(iterationLimit = 50, attackRange = 10, showPlot = True)
O que acontece se o alcance de ataque for reduzido para 5?
battlefield = initBattlefield(populationSizeA = 1000,populationSizeB = 1000,battlefieldArr = battlefield) results = simulationRun(iterationLimit = 50, attackRange = 5, showPlot = True)
A localização inicial dos agentes no campo de batalha é aleatória. O resultado final da batalha também pode ser distribuído aleatoriamente. Quero investigar até que ponto os resultados são aleatórios. Para isso, implemento uma função de teste de sensibilidade que conduz a simulação repetidas vezes. Esta função retornará resultados que podem ser visualizados em um histograma, representando a quantidade total de agentes vivos ao final da simulação. Neste caso, repito a simulação 50 vezes.
Abaixo implemento a função realizando o teste de sensibilidade:
# esta função é usada para realizar um teste de sensibilidade em relação ao resultado da batalha def sensitivityTest(iterationLimit,attackRange,runs): # indica que o campo de batalha é uma variável global global battlefield # listas vazias que conterão o número final de agentes do respectivo tipo, ao final da batalha outcomeA = [] outcomeB = [] # repetindo a simulação com alcance de ataque definido e limite de iteração, para "execuções" número de vezes for i in range(0,runs): # antes de cada simulação, o campo de batalha deve ser inicializado battlefield = initBattlefield(populationSizeA = 1000,populationSizeB = 950,battlefieldArr = battlefield) # realiza simulação de corrida results = simulationRun(iterationLimit=iterationLimit,attackRange = attackRange,showPlot = False) # anexar resultado à lista de resultados relevantes outcomeA.append(results[0][iterationLimit-1]) outcomeB.append(results[1][iterationLimit-1]) # retornando o resultado em uma lista com duas sublistas return([outcomeA,outcomeB])
Abaixo eu implemento a função de plotagem do histograma:
# função para plotar um histograma 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)
Por fim, o código abaixo executa o teste de visibilidade e plota o resultado.
# executando o teste de sensibilidade results = sensitivityTest(iterationLimit = 50,attackRange = 5,runs = 50) plotHistogram(plotTitle = "distribution of agents",resultsA = results[0], resultsB = results[1])
Este resultado é para um alcance de ataque de 5.
Usando esta abordagem, um modelo de simulação baseado em agente muito avançado pode ser desenvolvido.
Cientista de dados com foco em simulação, otimização e modelagem em R, SQL, VBA e Python
Leave a Reply