Beispiel 2: Virusausbreitung#

In diesem Beispiel programmieren wir eine Simulation in Anlehnung an diesen Artikel aus der Washinton Post. V.a. geht es darum, sich in einem zweidimensionalen Raum frei bewegende Agenten, deren Interaktion mit der Wand (Abprallen) und deren Interaktion untereinander (Ansteckung) zu modellieren. Zudem wird am Ende dieses Beispiels der Ansteckungsverlauf in der Simulation systematisch für verschiedene Parameter-Werte (Anzahl mobiler Agenten) analysiert.

Beginnen wir mit der Definition der Agenten-Klasse. Eine Instanz dieser Klasse befindet sich zu jedem Zeitpunkt an einer bestimmten Position, welche durch X- und Y-Koordinaten festgelegt ist. Die Position eines Agenten wird durch die beiden Attribute x_position und y_position ausgedrückt. Die Agenten bewegen sich in dem Raum, verändern also in jedem Zeitschritt ihre Position. Die Veränderung ihrer Position auf der X-Dimension und Y-Dimension entspricht den beiden Attributen x_velocity und y_velocity. Diese beiden Attribute geben über ihren Wert nicht nur die Geschwindigkeit, sondern auch die Richtung der Veränderung an. Negative Werte auf der X-Achse lassen den Agenten nach links und positive Werte lassen den Agenten nach rechts fahren. Negative Werte auf der Y-Achse lassen den Agenten nach oben (bzw. je nach Darstellungsart auch unten) und positive Werte lassen den Agenten nach unten (bzw. nach oben) fahren. Alle Attribute werden bei der Erstellung der Instanz als Input übergeben.

Damit die Agenten sich auch bewegen können, gebe ich der Klasse die Methode move(). Die Methode move() verändert die Position eines Agenten, indem sie die Attribute x_velocity und y_velocity auf die Atttribute x_position und y_position aufaddiert. Die Position verschiebt sich dann je nach Wert von x_velocity und y_velocity entsprechend.

class Agent:
    
    def __init__(self, x_position, y_position, x_velocity, y_velocity):
        
        # Position
        self.x_position = x_position
        self.y_position = y_position
        
        # Geschwindigkeit
        self.x_velocity = x_velocity
        self.y_velocity = y_velocity

    
    def move(self):
        # Position des Agenten um den Wert von x_velocity & y_velocity verändern
        self.x_position += self.x_velocity
        self.y_position += self.y_velocity

Unten wird eine Instanz der Klasse Agent erstellt, wobei ich für das Attribut x_position den Wert 50, für das Attribut y_position den Wert 50, für das Attribut x_velocity den Wert 1 und für das Attribut y_velocity den Wert 0 eingebe. Der Agent startet als auf der Position (50, 50) und bewegt sich mit einer Geschwindigkeit von 1 pro Aufruf der Methode move() nach rechts. Auf der Y-Achse verändert er mit einem Wert von 0 seine Position jedoch nicht.

agent = Agent(50, 50, 1, 0)

Prüfen wir, wo sich der Agent aktuell befindet, indem wir uns die beiden Positionsattribute anschauen:

agent.x_position, agent.y_position
(50, 50)

Im Folgenden lasse ich den Agenten für 100 Zeitschritte die Methode move() ausführen:

# für jeden Zeitschritt
for tick in range(100):
    
    # Handlungen der Agenten
    agent.move()

Der Agent hat seine Position tatsächlich verändert, wie wir sehen, wenn wir uns nun die Positionsattribute des Agenten anschauen:

agent.x_position, agent.y_position
(150, 50)

Wie zu erwarten war, befindet sich der Agent nun auf einer Position 100 Schritte weiter rechts von seiner Startposition, da dieser auf dem Attribut x_velocity den Wert 1 und auf dem Attribut y_velocity den Wert 0 aufweist.

Im Folgenden mache ich die Bewegung des Agenten graphisch sichtbar, indem ich in jedem Zeitschritt die Position des Agenten mit einem Punkt auf einem Streudiagramm (ein Streudiagramm kann man mit der Matplotlib-Funktion/Methode scatter() erstellen) plotte, ein Foto davon mit Celluloid schieße und dann die Diagramme zu einer Animation zusammensetze.

from matplotlib import pyplot as plt
import celluloid as cld

# Matplotlib-Diagramm initialisieren
fig, ax = plt.subplots()
fig.set_size_inches(8,8)

# Celluloid-Kamera erstellen
camera = cld.Camera(fig)   

# für jeden Zeitschritt
for tick in range(100):
    
    # Agenten bewegen
    agent.move()
    
    # Position des Agenten mit Streudiagramm plotten
    ax.scatter(agent.x_position, agent.y_position, color = "black")
    
    # Foto von Diagramm schießen
    camera.snap()

# Endzustand von Diagramm anzeigen
plt.show()

# Animation erstellen
animation = camera.animate()

# Animation exportieren
animation.save("moving_dots.mp4", fps = 50)
../_images/amp09a_03_bsp2_11_0.png
from IPython.display import Video

Video("moving_dots.mp4", embed = True)

Das sieht ja schon mal nicht schlecht aus. Was man aber beim genaueren Hinschauen auf die Achsen bzw. Skalen sieht, ist dass ein ganz bestimmter Ausschnitt des “Raumes” im Diagramm dargestellt wird: Der Ausschnitt zeigt genau den Teil des Raumes, der vom Agent benötigt wird. In der Höhe braucht der Agent quasi keinen Raum, weshalb auch die Skala bzw. die Spannweite der anzeigten Y-Achse extrem klein ist. Auf der X-Achse hingegen weist die Skala eine höhere Spannweite auf, weil der Agent sich eben von links nach rechts bewegt und alle Positionen angezeigt werden. Der angezeigte Raum richtet sich also nach den Positionen des Agenten. Das kann sinnvoll sein, wenn man einen unbegrenzten Raum modellieren möchte, wir wollen jedoch klare Grenzen des Raumes festlegen.

Im nächsten Schritt lege ich daher mit den Variablen X_MIN, X_MAX, Y_MIN und Y_MAX die Größe des Raumes bzw. die Grenzen des Raumes fest. X_MIN ist die Position der unteren (bzw. linken) Raumgrenze auf der X-Achse, X_MAX ist die Position der oberen (bzw. rechte) Raumgrenze auf der X-Achse und so weiter. Diese Grenzen verwende ich unten um mit ax.set(xlim=(X_MIN, X_MAX), ylim = (Y_MIN, Y_MAX)) die im Diagramm anzeigten Achsenabschnitte festzulegen:

from matplotlib import pyplot as plt
import celluloid as cld

# Diagrammgrenzen bestimmen
X_MIN = 0
X_MAX = 100
Y_MIN = 0
Y_MAX = 100


# Matplotlib-Diagramm initialisieren
fig, ax = plt.subplots()
fig.set_size_inches(8,8)
ax.set(xlim=(X_MIN, X_MAX), ylim = (Y_MIN, Y_MAX))

# Celluloid-Kamera erstellen
camera = cld.Camera(fig)   

# Agent erstellen
agent = Agent(50, 50, 1, 0)

# für jeden Zeitschritt
for tick in range(100):
    
    # Agent bewegen
    agent.move()
    
    # Position des Agenten plotten
    ax.scatter(agent.x_position, agent.y_position, color = "black")
    
    # Foto von Diagramm schießen
    camera.snap()

# Endzustand von Diagramm anzeigen
plt.show()

# Animation erstellen
animation = camera.animate()

# Animation exportieren
animation.save("moving_dots.mp4", fps = 50)
../_images/amp09a_03_bsp2_14_0.png
Video("moving_dots.mp4", embed = True)

Upps! Es wird zwar jetzt der gewünschte Raum angezeigt, das interessiert den Agenten aber herzlich wenig. Er kennt keine Grenzen und folgt weiter seiner Bestimmung: von der Position (50, 50) aus 100 Schritte nach rechts laufen. Ob er dabei im sichtbaren Bereich des Diagramms ist, ist ihm egal. Er weiß davon ja auch gar nichts.

Das geht so nicht weiter. Wir sollten den Agenten in seine Schranken weisen. Ich erweitere daher die Klasse Agent um die Methode border_bounce(). Diese Methode soll bewirken, dass ein Agent, sobald er die vorgegebenen Grenzen eines Raumes erreicht an dieser “abprallt”, indem er die entsprechende Bewegungsrichtung wechselt.

    
    def border_bounce(self, x_min, x_max, y_min, y_max):
        
        # wenn Untergrenze oder Obergrenze auf X-Achse überschritten wird
        if self.x_position <= x_min or self.x_position >= x_max:
            # Bewegungsrichtung auf X-Achse umkehren
            self.x_velocity *= -1
       
        # wenn Untergrenze oder Obergrenze auf Y-Achse überschritten wird
        if self.y_position <= y_min or self.y_position >= y_max:
            # Bewegungsrichtung auf Y-Achse umkehren
            self.y_velocity *= -1

Wie funktioniert die Methode border_bounce()? Über die Parameter werden die festgelegten Grenzen des Raumes von außen eingegeben. Innerhalb der Methode guckt der Agent nach, ob er sich gerade auf oder außerhalb einer Grenze befindet, ob also z.B. die x_position kleinergleich der X-Untergrenze x_min ist oder die x_position größergleich der X-Obergrenze x_min. Falls das so ist, dann kehrt er seine Bewegungsrichtung auf der entsprechenden Dimension um, indem die entsprechende Bewegung mit -1 multipliziert wird.

Unten definiere ich die neue Klasse Agent mit der zusätzlichen Methode border_bounce().

import random
from matplotlib import pyplot as plt
import celluloid as cld


class Agent:
    
    def __init__(self, x_position, y_position, x_velocity, y_velocity):
        
        # Position
        self.x_position = x_position
        self.y_position = y_position
        
        # Geschwindigkeit
        self.x_velocity = x_velocity
        self.y_velocity = y_velocity

    
    def move(self):
        # Position des Agenten um den Wert von x_velocity & y_velocity verändern
        self.x_position += self.x_velocity
        self.y_position += self.y_velocity
    
    
    def border_bounce(self, x_min, x_max, y_min, y_max):
        
        # wenn Untergrenze oder Obergrenze auf X-Achse überschritten wird
        if self.x_position <= x_min or self.x_position >= x_max:
            self.x_velocity *= -1
       
        # wenn Untergrenze oder Obergrenze auf Y-Achse überschritten wird
        if self.y_position <= y_min or self.y_position >= y_max:
            self.y_velocity *= -1
        
        

Nun führe ich die gesamte Simulation nocheinmal aus, wobei der Agent nun zusätzlich in jedem Zeitschritt die Methode border_bounce() ausführt und somit im Falle einer Grenzberührung seine Bewegungsrichtung entsprechend umkehrt.

# Diagrammgrenzen bestimmen
X_MIN = 0
X_MAX = 100
Y_MIN = 0
Y_MAX = 100


# Matplotlib-Diagramm initialisieren
fig, ax = plt.subplots()
fig.set_size_inches(8,8)
ax.set(xlim=[X_MIN, X_MAX], ylim = [Y_MIN, Y_MAX])

# Celluloid-Kamera erstellen
camera = cld.Camera(fig)   

# Agent erstellen
agent = Agent(50, 50, 1, 0)

# für jeden Zeitschritt
for tick in range(200):
    
    # Agent bewegen
    agent.move()
    agent.border_bounce(X_MIN, X_MAX, Y_MIN, Y_MAX)
    
    # Position des Agenten plotten
    ax.scatter(agent.x_position, agent.y_position, color = "black")
    
    # Foto von Diagramm schießen
    camera.snap()

# Endzustand von Diagramm anzeigen
plt.show()

# Animation erstellen
animation = camera.animate()

# Animation exportieren
animation.save("moving_dots.mp4", fps = 50)
../_images/amp09a_03_bsp2_19_0.png
Video("moving_dots.mp4", embed = True)

Sieht so aus als würde es funktionieren. Ich teste die Methode nocheinmal, indem ich den Agenten mit einer anderen Bewegungsrichtung initialisiere: Agent(50, 50, -1, 0.3). Der so erstellte Agent läuft nun zu Beginn in jedem Zeitschritt mit einem Wert von -1 eine 1 Einheit nach links und 0.3 Einheiten nach oben.

from matplotlib import pyplot as plt
import celluloid as cld

# Diagrammgrenzen bestimmen
X_MIN = 0
X_MAX = 100
Y_MIN = 0
Y_MAX = 100


# Matplotlib-Diagramm initialisieren
fig, ax = plt.subplots()
fig.set_size_inches(8,8)
ax.set(xlim=[X_MIN, X_MAX], ylim = [Y_MIN, Y_MAX])

# Celluloid-Kamera erstellen
camera = cld.Camera(fig)   

# Agent erstellen
agent = Agent(50, 50, -1, 0.3)

# für jeden Zeitschritt
for tick in range(1000):
    
    # Agent bewegen
    agent.move()
    agent.border_bounce(X_MIN, X_MAX, Y_MIN, Y_MAX)
    
    # Position des Agenten plotten
    ax.scatter(agent.x_position, agent.y_position, color = "black")
    
    # Foto von Diagramm schießen
    camera.snap()

# Endzustand von Diagramm anzeigen
plt.show()

# Animation erstellen
animation = camera.animate()

# Animation exportieren
animation.save("moving_dots.mp4", fps = 50)
../_images/amp09a_03_bsp2_22_0.png
Video("moving_dots.mp4", embed = True)

Der Vorteil an Klassen ist ja, dass man unkompliziert viele Instanzen dieser Klasse erstellen kann und dann alle Instanzen handeln lassen kann. Im Folgenden führe ich dieselbe Simulation daher nochmal, diesmal aber mit 100 Instanzen, aus. Dabei gebe ich bei der Erstellung der Agenten jeweils zufällige Startpositionen im Bereich zwischen X_MIN und X_MAX sowie zwischen Y_MIN und Y_MAX ein. Ebenfalls gebe ich zufällige Bewegungengeschwindigkeiten jeweils zwischen -1 und 1 ein. Desweiteren muss nun in jedem Zeitschritt die gesamte Population durchgegangen werden, sodass jeder Agent der Population handelt. Außerdem ändert sich das Plotten ein bisschen. Theoretisch hätten wir weiterhin die Position eines Agenten direkt mit ax.scatter(agent.x_position, agent.y_position, color = "black") plotten können, so wie es oben gemacht wurde. Bei vielen Agenten erhöht das einzelne Einzeichnen eines einzelnen Agenten die Rechenzeit jedoch extrem. Daher werden in jeder Runde in den Listen x_data und y_data die aktuellen Positionen der Agenten gesammelt, um diese dann am Ende jeder Runde mit ax.scatter(x_data, y_data, color = "black") gemeinsam auf einmal einzuzeichnen.

# Diagrammgrenzen bestimmen
X_MIN = 0
X_MAX = 100
Y_MIN = 0
Y_MAX = 100

# Matplotlib-Diagramm initialisieren
fig, ax = plt.subplots()
fig.set_size_inches(8,8)
ax.set(xlim=[X_MIN, X_MAX], ylim = [Y_MIN, Y_MAX])

# Celluloid-Kamera erstellen
camera = cld.Camera(fig)      

# Population erstellen
population = []
for i in range(100):
    x_position = random.uniform(X_MIN, X_MAX)
    y_position = random.uniform(Y_MIN, Y_MAX)
    x_velocity = random.uniform(-1, 1)
    y_velocity = random.uniform(-1, 1)
    
    agent = Agent(x_position, y_position, x_velocity, y_velocity)
    
    population.append(agent)

    
# für jeden Zeitschritt
for tick in range(1000):
    
    # leere Datencontainer erstellen, welche die X- und Y-Position der Kugeln sammeln
    x_data = []
    y_data = []
    
    # für jeden Agenten in Population
    for agent in population:
        
        # Handlungen der Agenten
        agent.move()
        agent.border_bounce(X_MIN, X_MAX, Y_MIN, Y_MAX)
        
        # X- und Y-Position in Datenliste ablegen
        x_data.append(agent.x_position)
        y_data.append(agent.y_position)
    
    # Datenlisten d.h. Agenten an ihrer Position mit Streudiagramm plotten
    ax.scatter(x_data, y_data, color = "black")
    
    # Foto von Diagramm schießen
    camera.snap()

# Endzustand von Diagramm anzeigen
plt.show()

# Animation erstellen
animation = camera.animate()

# Animation exportieren
animation.save("moving_dots.mp4", fps = 50)
../_images/amp09a_03_bsp2_25_0.png
Video("moving_dots.mp4", embed = True, width = 500)