Beispiel 2: Virusausbreitung
Contents
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)
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)
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)
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)
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)
Video("moving_dots.mp4", embed = True, width = 500)