Desenvolvendo uma estrutura baseada em agente para modelos de simulação em Python

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.

Leave a Reply

Deixe um comentário

O seu endereço de e-mail não será publicado.

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.

Close

Meta