Entwicklung eines agendbasierten Frameworks für Simulationsmodelle in Python

In einem früheren Beitrag habe ich ein einfaches agentenbasiertes Simulationsmodell erstellt, das Gruppen von Agenten enthält, die sich auf einem Schlachtfeldgitter befinden können. Das Modell wurde in Python unter Verwendung von matplotlib zur Visualisierung codiert. Ich führte einen einfachen Simulationslauf durch, der ein Kampfszenario und dessen Ergebnis zeigte.

Im Folgenden werde ich das Problem umgestalten, indem ich versuche, den Code zu bereinigen und die Funktionalität in wiederverwendbare Funktionen zu modularisieren. Das Ergebnis wird ein modulares Framework und eine allgemeine Beschreibung des Ansatzes sein. Beiden können zum Erstellen sehr fortschrittlicher agentenbasierter Simulationsmodelle verwendet werden.

Die Agenten werden wie folgt als Klasse modelliert:

# Klasse, die Agenten als abstrakte Datentypen definiert
class agent:
    # init-Methode, die Konstruktormethode für Agenten
    def __init__(self,x,y,group):
        self.life = 100 # agent's life score
        self.x = x
        self.y = y
        self.group = group

Das Schlachtfeld selbst wird wie unten gezeigt als zweidimensionales Gitterarray modelliert:

# Erstellen einer leeren 100 x 100-Liste mithilfe des Listenverständnisses in Python
battlefield = [[None for i in range(0,100)] for i in range(0,100)]

Agentengruppen können mit der agentCreator-Funktion in das Schlachtfeldraster eingefügt werden:

# Definieren Sie eine Funktion zum Erstellen und Zuweisen von Agenten zum Raster
def agentCreator(size,group,groupList,field,n,m):
    # Schleife durch die gesamte Gruppe, d. h. in diesem Fall 1000 Einheiten
    for j in range(0,size):
        # Wählen Sie einen zufällig verfügbaren Ort aus
        while True:
            # zufällige x-Koordinate
            x = random.choice(range(0,n))
            # zufällige y-Koordinate
            y = random.choice(range(0,m))
            # prüfen, ob Platz frei ist; Wenn nicht, wiederholen Sie den Vorgang
            if field[x][y] == None:
                field[x][y] = agent(x=x,y=y,group=group)
                # Agentenobjektreferenz an Gruppenliste anhängen
                groupList.append(field[x][y])
                # beende while-Schleife; Punkt auf dem Feld wird genommen
                break

Für das Plotten von Agenten mit einer positiven Lebensbewertung als visuelle Punkte auf dem Schlachtfeldgitter erstelle ich eine Plotfunktion, die jederzeit während der Simulation aufgerufen werden kann, um eine Momentaufnahme des aktuellen Kampfstatus zu erhalten:

# Pyplot und Farben aus Matplotlib importieren
from matplotlib import pyplot, colors
# Funktion zum Plotten des Schlachtfelds definieren (alle Agenten, die noch leben)
def plotBattlefield(populationArr, plotTitle):
    # Definiere mithilfe von Farben aus matplotlib eine Farbkarte
    colormap = colors.ListedColormap(["lightgrey","green","blue"])
    # Figurgröße mit Pyplot definieren
    pyplot.figure(figsize = (12,12))
    # Mit Pyplot einen Titel hinzufügen
    pyplot.title(plotTitle, fontsize = 24)
    # Füge mit Pyplot x- und y-Beschriftungen hinzu
    pyplot.xlabel("x coordinates", fontsize = 20)
    pyplot.ylabel("y coordinates", fontsize = 20)
    # Passe die Ticks der x- und y-Achse mithilfe des Pyplots an
    pyplot.xticks(fontsize = 16)
    pyplot.yticks(fontsize = 16)
    # Verwende .imshow()-Methode von pyplot, um die Agentenpositionen zu visualisieren
    pyplot.imshow(X = populationArr, cmap = colormap)

Eine weitere Funktion, die ich benötige, ist eine Funktion, die einen Schlachtfeldstatus einem numerischen zweidimensionalen Array mit Werten von zuordnen kann. 0,0 wenn kein Agent in der respektiven Zelle auf dem Schlachtfeld ist, 1,0 wenn sich ein Agent vom Typ A in einer bestimmten Zelle des zweidimensionalen Schlachtfeldgitters befindet und dort lebt, Wert 2,0 wenn es einen Agenten vom Typ B ist. Ich verwende diesen Zuordnungsprozess um meiner Plotfunktion ein numerisches Raster anstelle eines Rasters mit Objekten einer abstrakten, d.h. benutzerdefinierten, Python-Klasse zuzuweisen:

# Diese Funktion ordnet ein Schlachtfeldraster einem numerischen Raster zu, wobei 1 für Agenten vom Typ A, 2 für Typ B und 0 für keinen Agenten gilt
def mapBattlefield(battlefieldArr):
    # .imshow () benötigt eine Matrix mit float-Elementen;
    populationArr = [[0.0 for i in range(0,100)] for i in range(0,100)]
    # Wenn der Agent vom Typ A ist, geben Sie eine 1,0 ein. Wenn der Typ B ist, geben Sie eine 2,0 ein
    for i in range(1,100):
        for j in range(1,100):
            if battlefieldArr[i][j] == None: # leer
                pass # 0,0 in der Populationszelle belassen
            elif battlefieldArr[i][j].group == "A": # Agenten der Gruppe A.
                populationArr[i][j] = 1.0 # 1.0 bdeutet "A"
            else: # group B agents
                populationArr[i][j] = 2.0 # 2.0 bedeutet "B"
    # gebe codierten Werte zur¨ck
    return(populationArr)

Mit diesen Modellkomponenten erstellte ich eine erste Schlachtfeldpopulation und zeige mithilfe von matplotlib die Agentenpositionen auf dem Schlachtfeld auf. Dies geschieht im folgenden Code und ähnelt meinen vorherigen Beiträgen, nur dass ich in diesem Fall modularisierte Funktionen verwende. Die Funktionalität zum Einrichten eines anfänglichen Schlachtfeldgitters wird auf eine separate Funktion unten übertragen. Diese Funktion wird dann ausgeführt.

# Funktion zum Erstellen eines anfänglichen Schlachtfeldgitters
def initBattlefield(populationSizeA,populationSizeB,battlefieldArr):
    # Initialisierung eines neuen leeren Schlachtfeldgitters 
    battlefieldArr = [[None for i in range(0,100)] for i in range(0,100)]
    # Erstelle leere Liste für zukünftige Agentenreferenzen; für A und B Agenten
    agents_A = []
    agents_B = []
    # Ordne Agenten zufällig Positionen zu
    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)
    # gebe das befüllte Schlachtfeld zurück
    return(battlefieldArr)

# obige Funktion für beide Agentengruppen mit Bestandsgrösse 1000 durchführen
battlefield = initBattlefield(populationSizeA=1000,populationSizeB=1000,battlefieldArr = battlefield)

# aktuellen Stand auf dem Schlachtfeld plotten
plotBattlefield(populationArr = mapBattlefield(battlefield), 
                    plotTitle = "battlefield before simulation run (green = A, blue = B)")

In meinem vorherigen Beitrag habe ich dann einen Simulationslauf ausgeführt, der auf den folgenden Regeln basiert:

Gruppe A hat die Strategie, in jeder Runde immer den gleichen Agenten zu treffen

Gruppe B hat eine zufällige und unabhängige Strategie, um Feinde anzugreifen. Dies bedeutet, dass jeder Agent vom Typ B einen zufällig ausgewählten feindlichen Agenten innerhalb seiner Reichweite angreift

Die Simulation wurde unter folgenden Bedingungen durchgeführt:

1) Jede Runde entspricht einer Iteration
2) In jeder Runde greif ein Agent einen Feind innerhalb dessen Reichtweite an
3) Die Reichweite eines Agenten wird bei Simulationsbeginn definiert, mit Standardwert 10
4) Tote Agenten werden von dem Schlachtfeld entfernt
5) Ein Agent stirbt wenn dessen Lebenswert auf oder unter 0 fällt
6) Jeder Agent hat einen zufallsverteilten Angriffswert zwischen 10 und 60
7) In jeder Runde darf jeder Agent einen Angriff ausführen

Wie in einem meiner vorherigen Beiträge werde ich jetzt 50 Kampfrunden durchlaufen. Nach jeder Runde werden Agenten mit einer nicht positiven Lebensbewertung aus dem Schlachtfeldgitter entfernt. Abweichend von meinem vorherigen Beitrag werde ich jetzt jedoch die Funktionalität modularisieren.

Zuerst definiere ich eine Funktion zum Entfernen von Agenten aus dem Schlachtfeldgitter:

# Funktion zum Entfernen von Agenten aus dem Schlachtfeldgitter, wenn die Lebensbewertung nicht unbedingt positiv ist
def removeDeadAgents(battlefieldArr):
    # Identifizieren von Agenten mit einer Lebensbewertung von oder weniger - und Entfernen dieser aus dem Raster
    for i in range(0,len(battlefieldArr)):
        for j in range(0,len(battlefieldArr)):
            if battlefieldArr[i][j]:
                if battlefieldArr[i][j].life <= 0:
                    # Entfernen Sie diesen Agenten, da die Lebensbewertung nicht unbedingt positiv ist
                    battlefieldArr[i][j] = None

Als nächstes definiere ich eine Funktion, die die Kampfstrategie für Agenten vom Typ A implementiert:

# Funktion zur Implementierung einer Kampfrunde für einen Agenten vom Typ A.
def oneRoundAgentA(i,j,attackRange):
    found_i = None
    found_j = None
    # Suchen Sie in benachbarten Zellen für jede Iteration in derselben Reihenfolge
    for k in range(i-attackRange,i+attackRange+1):
        for l in range(j-attackRange,j+attackRange+1):
            # auf negative Indexwerte prüfen; wenn ja - Pause!
            if k < 0 or l < 0:
                break
                # auf Indexwerte über 99 prüfen, wenn ja brechen!
            if k > 99 or l > 99:
                break
            if battlefield[k][l]:
                if battlefield[k][l].group == "B": # dann ist das ein Feind
                    if found_i == None:
                        found_i = k
                        found_j = l
                    
    # identifizierten Gegnern Schaden zufügen
    if found_i:
        battlefield[found_i][found_j].life = battlefield[found_i][found_j].life - random.randint(10,60)

Dann mache ich dasselbe für Agenten vom Typ B:

# Funktion zur Implementierung einer Kampfrunde für einen Agenten vom Typ B.
def oneRoundAgentB(i,j,attackRange):
    # Überprüfen Sie zunächst, ob sich in einer der umliegenden Zellen überhaupt ein Feind befindet
    enemy_found = False
    for k in range(i-attackRange,i+attackRange+1):
        for l in range(j-attackRange,j+attackRange+1):
            # auf negativen Index prüfen, wenn ja, mit der nächsten Iteration abbrechen
            if k < 0 or l < 0:
                break
                # auf Indexwerte über 99 prüfen, falls dies nicht der Fall ist
            if k > 99 or l > 99:
                break
            if battlefield[k][l] != None:
                if battlefield[k][l].group == "A":
                    enemy_found = True
    # Wählen Sie eine zufällige Zeile und eine zufällige Spalte
    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)
        # auf negativen Index prüfen, wenn ja, mit der nächsten Iteration fortfahren
        if k < 0 or l < 0:
            continue
        # auf Indexwert> 99 prüfen, wenn ja, weiter
        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
    # Füge dem identifizierten Feind Schaden zu
    if found_i:
        battlefield[found_i][found_j].life = battlefield[found_i][found_j].life - random.randint(10,60)

Ich simuliere den Kampf mit den bereits implementierten Funktionen:

for counter in range(0,50): # in this case I am conducting 50 iterations 
    # Durchlaufen aller Zellen auf dem Schlachtfeld
    for x in range(0,len(battlefield)):
        for y in range(0,len(battlefield)):
            # print ("Iteration der obersten Ebene, i:" + str (i) + ", j:" + str (j))
            # Überprüfen Sie, ob sich in der jeweiligen Zelle ein Agent befindet
            if battlefield[x][y] != None:
                # je nach Typ: jeweilige Angriffsstrategie ausführen
                if battlefield[x][y].group == "A":
                    # eine Kampfrunde für diesen Agenten vom Typ A.
                    oneRoundAgentA(i = x, j = y,attackRange=10)
                else: 
                    # eine Kampfrunde für diesen Agenten vom Typ B.
                    oneRoundAgentB(i = x, j = y,attackRange=10)
    # Identifizieren von Agenten mit einer Lebensbewertung von oder weniger - und Entfernen dieser aus dem Raster
    removeDeadAgents(battlefieldArr = battlefield)
# Plot Schlachtfeldstatus
plotBattlefield(populationArr = mapBattlefield(battlefield), 
                plotTitle = "battlefield after 50 iterations (green = A, blue = B)")

Ich möchte jetzt ein Diagramm hinzufügen, um das Kampfergebnis und den Verlauf des Kampfes selbst zu analysieren. Ich definiere daher eine zusätzliche Plotfunktion, die die Anzahl der Agenten, die noch am Leben sind, nach Typ darstellt.

Ich implementiere auch eine Funktion zum Aktualisieren der Zeitreihen von Agenten, die noch am Leben sind.

# Funktion zum Aktualisieren der Zeitreihe der aktiven Agenten
def calcAgentsAliveA(resultsA,battlefieldArr):
    # Diese Variablen werden zum Zählen der Anzahl der aktiven Agenten verwendet
    countA = 0
    countB = 0
    # Durchlaufen Sie das Raster, suchen Sie nach Agenten und aktualisieren Sie die Anzahl, falls 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
    # Aktualisieren Sie die Ergebnisliste und geben Sie sie zurück
    resultsA.append(countA)
    return(resultsA)

# Funktion zum Aktualisieren der Zeitreihe der aktiven Agenten
def calcAgentsAliveB(resultsB,battlefieldArr):
    # Diese Variablen werden zum Zählen der Anzahl der aktiven Agenten verwendet
    countA = 0
    countB = 0
    # Durchlaufen Sie das Raster, suchen Sie nach Agenten und aktualisieren Sie gegebenenfalls die Anzahl
    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
    # Aktualisieren Sie die Ergebnisliste und geben Sie sie zurück
    resultsB.append(countB)
    return(resultsB)

# Funktion zum Zeichnen der Anzahl der noch lebenden Agenten
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)

Jetzt kann ich mit der Anzeige des Diagramms einen weiteren Simulationslauf durchführen. Da ich verschiedene Simulationsszenarien ausführen werde, schreibe ich den Simulationslauf auch in eine Funktion:

# Definierende Funktion zur Durchführung eines Simulationslaufs
def simulationRun(iterationLimit,attackRange,showPlot):
    iterations = []
    resultsA = []
    resultsB = []
    for counter in range(0,iterationLimit): # In diesem Fall führe ich 50 Iterationen durch
        # Iterationen aktualisieren
        # Ergebnisse aktualisieren
        iterations.append(counter+1)
        resultsA = calcAgentsAliveA(resultsA,battlefield)
        resultsB = calcAgentsAliveB(resultsB,battlefield)
        # Iteration durch alle Zellen auf dem Schlachtfeld
        for x in range(0,len(battlefield)):
            for y in range(0,len(battlefield)):
                # print ("Top-Tier-Iteration, i:" + str (i) + ", j:" + str (j))
                # Überprüfen Sie, ob sich in der jeweiligen Zelle ein Agent befindet
                if battlefield[x][y]:
                    # je nach Typ: jeweilige Angriffsstrategie ausführen
                    if battlefield[x][y].group == "A":
                        # eine Kampfrunde für diesen Agenten vom Typ A.
                        oneRoundAgentA(i = x, j = y, attackRange = attackRange)
                    else: 
                        # eine Kampfrunde um diesen Agenten vom Typ B.
                        oneRoundAgentB(i = x, j = y, attackRange = attackRange)
        # Identifizieren von Agenten mit einer Lebensbewertung von oder weniger - und Entfernen dieser aus dem Raster
        removeDeadAgents(battlefieldArr = battlefield)
    # Handlungsverlauf, aber nur, wenn die Handlung angezeigt werden soll
    if showPlot:
        plotNumberOfAgentsAlive("battle progression",iterations,resultsA,resultsB)
    # Ergebnisse zurückgeben
    return([resultsA,resultsB])

Ich kann jetzt einen Simulationslauf mit nur einer Codezeile durchführen:

battlefield = initBattlefield(populationSizeA = 1000,populationSizeB = 1000,battlefieldArr = battlefield)
results = simulationRun(iterationLimit = 50, attackRange = 10, showPlot = True)

Es scheint, dass Agenten vom Typ B eine ziemlich effektive Angriffsstrategie haben. Aber was ist, wenn ihre Anzahl zu Beginn des Kampfes etwas niedriger ist? Unten zeichne ich den Verlauf des Kampfes in einem Kampf mit 1000 anfänglichen A- und 950 anfänglichen B-Agenten.

battlefield = initBattlefield(populationSizeA = 1000,populationSizeB = 950,battlefieldArr = battlefield)
results = simulationRun(iterationLimit = 50, attackRange = 10, showPlot = True)

Was passiert, wenn die Angriffsreichweite auf 5 reduziert wird?

battlefield = initBattlefield(populationSizeA = 1000,populationSizeB = 1000,battlefieldArr = battlefield)
results = simulationRun(iterationLimit = 50, attackRange = 5, showPlot = True)

Die anfängliche Position der Agenten auf dem Schlachtfeld ist zufällig. Das endgültige Kampfergebnis kann daher auch zufällig verteilt werden. Ich möchte untersuchen, inwieweit die Ergebnisse randomisiert sind. Dazu implementiere ich eine Empfindlichkeitstestfunktion, die die Simulation immer wieder wiederholt. Diese Funktion gibt Ergebnisse zurück, die in einem Histogramm visualisiert werden können und die Gesamtmenge der am Ende der Simulation lebenden Agenten darstellen. In diesem Fall wiederhole ich die Simulation 50 Mal.

Im Folgenden implementiere ich die Funktion, die den Empfindlichkeitstest durchführt:

# Diese Funktion wird zur Durchführung eines Empfindlichkeitstests hinsichtlich des Kampfergebnisses verwendet
def sensitivityTest(iterationLimit,attackRange,runs):
    # gibt an, dass das Schlachtfeld eine globale Variable ist
    global battlefield
    # leere Listen, die am Ende des Kampfes die endgültige Anzahl der Agenten des jeweiligen Typs enthalten
    outcomeA = []
    outcomeB = []
    # Wiederholen der Simulation mit definiertem Angriffsbereich und Iterationslimit für "Läufe" mehrmals
    for i in range(0,runs):
        # Vor jedem Simulationslauf muss das Schlachtfeld initialisiert werden
        battlefield = initBattlefield(populationSizeA = 1000,populationSizeB = 950,battlefieldArr = battlefield)
        # Simulationslauf durchführen
        results = simulationRun(iterationLimit=iterationLimit,attackRange = attackRange,showPlot = False)
        # Ergebnis an relevante Ergebnisliste anfügen
        outcomeA.append(results[0][iterationLimit-1])
        outcomeB.append(results[1][iterationLimit-1])
    # Rückgabe des Ergebnisses in einer Liste mit zwei Unterlisten
    return([outcomeA,outcomeB])

Unten implementiere ich die Histogramm-Plot-Funktion:

# Funktion zum Zeichnen eines Histogramms
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)

Schließlich führt der folgende Code den Empfindlichkeitstest aus und zeichnet das Ergebnis auf.

# Ausführen des Empfindlichkeitstests
results = sensitivityTest(iterationLimit = 50,attackRange = 5,runs = 50)
plotHistogram(plotTitle = "distribution of agents",resultsA = results[0], resultsB = results[1])

Dieses Ergebnis gilt für eine Angriffsreichweite von 5.

Mit diesem Ansatz kann ein sehr fortschrittliches agentenbasiertes Simulationsmodell entwickelt werden.

You May Also Like

Leave a Reply

Leave a Reply

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.