Ich hatte – angeregt durch die Lektüre von Peter Farrells wunderbarem Buch »Math Adventures with Python« mal wieder Lust, mit Processing.py etwas völlig Sinnbefreites zu programmieren, das im Endeffekt dann aber doch nicht so sinnbefreit ist, sondern sogar eine wichtige und vielleicht auch überraschende Erkenntnis hervorbringt. Dafür schien mir das im Kapitel 9 vorgestellte »Crazy Sheep Programm« ein durchaus geeigneter Kandidat zu sein, denn – auch wenn Peter Farrell das nicht anspricht – so grazy ist das Programm gar nicht.
Doch fangen wir klein an. Zuerst einmal geht es um nicht mehr, als ein Schaf (ich habe es aus naheliegenden Gründen Shaun genannt) über eine grüne Wiese zu schicken, die es abweiden soll. Natürlich geht es auch um objektorientierte Programmierung und daher bekommt das Schaf erst einmal eine eigene Klasse Sheep
spendiert:
# coding=utf-8 from random import randint class Sheep(): def __init__(self, x, y): self.x = x self.y = y self.sz = 10 self.move = 10 def update(self): self.x += randint(-self.move, self.move) self.y += randint(-self.move, self.move) if self.x > width: self.x %= width if self.y > height: self.y %= height if self.x < 0: self.x += width if self.y < 0: self.y += height circle(self.x, self.y, self.sz)
Diese habe ich in der Datei shaun.py
abgesichert und da ich dazu neige, in den Kommentaren Umlaute zu verwenden und Processing.py (da Jython, also Python 2.7) darauf allergisch reagiert, mußte in der ersten Zeile die Direktive # coding=utf-8
untergebracht werden.
Die update()
-Methode ist erst einmal sehr einfach gehalten: Das Schaf (eigentlich ein Kreis) bewegt sich bei jedem Zeitschritt zufällig zwischen -10 und 10 Pixeln in x- und y-Richtung weiter und wenn es auf den Rand des Bildschirms trifft, taucht es auf der anderen Seite wieder auf (Torus-Welt).
Da ich schon dabei war, habe ich in der gleichen Datei auch noch eine Klasse Settings
untergebracht, die ein paar nützliche Konstanten enthält:
class Settings(): def __init__(self): # Einige nützliche Konstanten: self.WIDTH = 640 self.HEIGHT = 480 self.FPS = 5 # Ein paar Farbdefinitionen self.WHITE = color(255, 255, 255) self.BLACK = color(0, 0, 0) self.GREEN = color(0, 100, 0)
Die Simulationsgeschwindigkeit wird durch die Framerate gesteuert und da ich es nicht so hektisch mag, habe ich sie mit self.FPS = 5
auf fünf Zeitschritte je Sekunde heruntergesetzt.
Das Hauptprogramm macht ja noch nicht viel und ist daher sehr kurz geraten:
from shaun import Sheep, Settings shaun = Sheep(300, 200) s = Settings() def setup(): size(s.WIDTH, s.HEIGHT) this.surface.setTitle("Shaun das Schaf (1)") frameRate(s.FPS) def draw(): background(s.GREEN) shaun.update()
Wenn Ihr es laufen laßt, werden Ihr sehen, was ich mit »es macht nicht viel« meinte: Ein weißer Kreis irrt ziellos über ein grüne Ebene. Das ist alles.
Das ändert sich jedoch massiv mit der zweiten Version des Programms. Zum einen habe ich ein Refaktoring vorgenommen und jeder Klasse eine eigene Datei (einen eigenen Tab) spendiert. Das heißt, die Settings
besitzen nun eine eigene Datei (config.py
) und haben auch noch die Konstante PATCHSIZE
(dazu später mehr) und die Farbe Braun spendiert bekommen:
# coding=utf-8 class Settings(): def __init__(self): # Einige nützliche Konstanten: self.WIDTH = 640 self.HEIGHT = 480 self.FPS = 5 self.PATCHSIZE = 10 # Ein paar Farbdefinitionen self.WHITE = color(255, 255, 255) self.BLACK = color(0, 0, 0) self.GREEN = color(0, 100, 0) self.BROWN = color(100, 50, 0)
Neu ist auch die Klasse Grass
, denn die Schafe sollen es abweiden können und hinterlassen dann eine abgeweidete, also braune Schafweide. Jedes mal, wenn ein Schaf einen »Patch« Gras abfrißt, bekommt es fünf Enegiepunkte spendiert. Daher sieht die Datei grass.py
so aus:
# coding=utf-8 from config import Settings s = Settings() class Grass(): def __init__(self, x, y, sz): self.x = x self.y = y self.energy = 5 self.eaten = False self.sz = sz def update(self): if self.eaten: fill(s.BROWN) else: fill(s.GREEN) rect(self.x, self.y, self.sz, self.sz)
Die meisten Änderungen gibt es in der Klasse Sheep
, die ja nun eine ganze Schafherde hervorbringen soll:
# coding=utf-8 from random import randint from config import Settings s = Settings() class Sheep(): def __init__(self, x, y): self.x = x self.y = y self.sz = s.PATCHSIZE # Shapesize self.move = 10 self.energy = 20 self.rows = height/self.sz def update(self, lawn): self.energy -= 1 self.x += randint(-self.move, self.move) self.y += randint(-self.move, self.move) if self.x >= width - self.sz: self.x = width - self.sz if self.y >= height - self.sz: self.y = height - self.sz if self.x <= 0: self.x = 0 if self.y <= 0: self.y = 0 xscl = int(self.x/self.sz) yscl = int(self.y/self.sz) grass = lawn[xscl*self.rows + yscl] if not grass.eaten: self.energy += grass.energy grass.eaten = True fill(s.WHITE) circle(self.x, self.y, self.sz)
Zum einen bekommt jedes Schaf mit self.energy = 20
eine Startenergie von 20 Punkten zugewiesen. Zum anderen habe ich die Randbedingungen geändert. Da es eher unwahrscheinlich ist, daß eine Schafherde eine ganze (Torus-) Welt bevölkert, habe ich sie auf die rechtecke Weide der Fenstergröße eingekesselt. Sobald sie auf den Rand des Fensters stoßen, müssen sie dort verharren, bis der Zufallszahlengenerator ihnen eine neue Bewegung in eine andere Richtung zuweist, die sie auf ein anderes Feld innerhalb der Weide (des Fensters) führt.
Außerdem bekommt jedes Schaf bei jedem Zeitschritt einen Energiepunkt abgezogen. Energie auftanken kann es nur, wenn es Gras frißt, denn Gras fressen soll sich schließlich wieder lohnen.
Aber es gilt noch ein anderes Problem zu berücksichtigen: Der Zufallszahlengenerator führt die Schafe nicht eindeutig auf ein Patchfeld mit Gras, da mußte ich dann ein wenig runden: Zum einen ist die Weide in einem eindimensionalen Array abgespeichert, daher habe ich im Konstruktor mit
self.rows = height/self.sz
erst einmal die Reihen des Feldes ermittelt. (Ihr erinnert Euch? In Python 2.7 erkennt Pythons Duck Typing eine Integer-Division, wenn Zähler und Nenner eines Bruches Integer-Werte sind. Da dies sowohl bei der Fensterhöhe wie auch bei PATCHSIZE
immer der Fall ist, kommt hier immer ein Ganzzahl-Ergebnis heraus (das heißt, die Nachkommastellen werden gnadenlos abgeschnitten), das man natürlich im Zweifelsfall auch mit
self.rows = int(height/self.sz)
erzwingen kann.) Und dann habe ich mit
xscl = int(self.x/self.sz) yscl = int(self.y/self.sz) grass = lawn[xscl*self.rows + yscl]
dafür gesorgt, daß jedem Schaf immer der am nächsten gelegene Graspatch zum Abweiden zugewiesen wird. (Erfahrenen Processing-Programmierern wird dies aus der Bildverarbeitung bekannt vorkommen, da Processing auch Bilder immer als eindimensionale Arrays abspeichert und man so mit der gleichen Methode die x- und y-Koordinaten eines Bildes berechnen muß.)
Ja, was noch? Wenn der Rasenpatch abgeweidet ist, bekommt das Schaf fünf Energiepunkte spendiert und der Weidepatch eine braune Farbe zugewiesen.
(Patches als eigene Objekte (und Agenten) hat meines Wissens als erster Mitchel Resnick 1994 in StarLogo implementiert und in seinem Buch »Turtles, Termites, and Traffic Jams« vorgestellt.)
Das Hauptprogramm ist immer noch kurz geraten:
from config import Settings from shaun import Sheep from grass import Grass from random import randint s = Settings() sz = s.PATCHSIZE sheeps = [] lawn = [] def setup(): size(s.WIDTH, s.HEIGHT) this.surface.setTitle("Shaun das Schaf (2): Vom Schaf zur Schafherde") frameRate(s.FPS) for _ in range(20): sheeps.append(Sheep(randint(50, width - 50), random(50, height - 50))) for x in range(0, width, sz): for y in range(0, height, sz): lawn.append(Grass(x, y, sz)) def draw(): background(s.WHITE) for grass in lawn: grass.update() for sheep in sheeps: sheep.update(lawn) if sheep.energy <= 0: sheeps.remove(sheep)
Neu sind hier eigentlich nur die beiden Listen für die Schafe und den Rasen (lawn
), die in der setup()
-Funktion in zwei Schleifen jeweils gefüllt werden und die Tatsache, daß ein Schaf mit der Anweisung
if sheep.energy <= 0: sheeps.remove(sheep)
»stirbt«, sobald es keine Energie mehr besitzt. Wenn Ihr das Programm so laufen laßt, werdet Ihr es merken. Da kein Rasen nachwächst, wird irgendwann Eure stolze Herde verhungert sein. Darum möchte ich in der nächsten Version dieses Programmes nicht nur das Nachwachsen des Grases simulieren, sondern energiegeladene Schafe sollen sich auch reproduzieren, also vermehren können.
Diesen Abschnitt kann ich erst einmal mit einer guten Nachricht einleiten: Die Klassen Settings
, und Sheep
bleiben unverändert, es gibt Änderungen im Hauptprogramm
from config import Settings from shaun import Sheep from grass import Grass from random import randint s = Settings() sz = s.PATCHSIZE sheeps = [] lawn = [] def setup(): size(s.WIDTH, s.HEIGHT) this.surface.setTitle("Shaun das Schaf (3): Geburt und Tod") frameRate(s.FPS) for _ in range(20): sheeps.append(Sheep(randint(50, width - 50), random(50, height - 50))) for x in range(0, width, sz): for y in range(0, height, sz): lawn.append(Grass(x, y, sz)) def draw(): background(s.WHITE) for grass in lawn: grass.update() for sheep in sheeps: sheep.update(lawn) if sheep.energy <= 0: sheeps.remove(sheep) if sheep.energy >= 50: sheep.energy -= 30 sheeps.append(Sheep(sheep.x, sheep.y)) print(len(sheeps))
und da auch eigentlich nur in der draw()
-Funktion. Dort wird mit den Zeilen
if sheep.energy >= 50: sheep.energy -= 30 sheeps.append(Sheep(sheep.x, sheep.y))
festgelegt, daß ein Schaf, das einen Energielevel von 50 Punkten besitzt, sich vermehren soll. Dieser Vorgang kostet dem Tier zwar 30 Energiepunkte, aber dafür hat es sich quasi verdoppelt. Das neue Schaf wird auf der gleichen Position geboren, auf der sein Elternschaf sitzt, aber der Zufallszahlengenerator sorgt schnell dafür, daß beide Tiere bald getrennte Wege gehen.
Die zweite Änderung betrifft die update()
-Methode der Klasse Grass
:
def update(self): if self.eaten: if random(1000) < 5: self.eaten = False else: fill(s.BROWN) else: fill(s.GREEN) rect(self.x, self.y, self.sz, self.sz)
Hier wird mit einer Wahrscheinlichkeit von 5 Promille dem Gras die Chance gegeben, wieder zu wachsen, das heißt einen Patch wieder grün und abweidbar werden zu lassen.
Diese geringe Wahrscheinlichkeit reicht aus. Wenn Ihr die Simulation über einen längeren Zeitraum laufen laßt, werdet Ihr feststellen, daß die Schafspopulation stabil bleibt. Sie kann zwar mal – vornehmlich zu Beginn, wenn alles noch grün ist – sehr groß werden oder im weiteren Verlauf auch sehr klein (unter zehn Schafe), aber sie stirbt nicht mehr aus, sondern die Population pendelt immer um einen Mittelwert herum.
Dieses Verhalten ist aus ähnlichen Räuber- und Beute-Simulationen bekannt (ich ernenne die Schafe jetzt einfach mal zu gefährlichen Raubtieren ehrenhalber und das Gras zu ihrer Beute) und wird nach ihren Entdeckern Lotka-Volterra-Regeln, beziehungsweise mathematisch präzise Lotka-Volterra-Gleichungen genannt.
Als abschließende Änderung habe ich die Schafe in vier unterschiedlich farbige Varianten aufgeteilt, während das sonstige Verhalten unverändert geblieben ist. Daher sind in der Klasse Settings
auch nur noch ein paar weitere Farbdefinitionen hinzugekommen:
# Ein paar Farbdefinitionen self.WHITE = color(255, 255, 255) self.YELLOW = color(255, 200, 0) self.RED = color(255, 0, 0) self.BLUE = color(0, 255, 255) self.ORANGE = color(255, 140, 0) self.BLACK = color(0, 0, 0) self.GREEN = color(0, 100, 0) self.BROWN = color(100, 50, 0)
und die Klasse Sheep
bekommt im Konstruktor eine Farbe mit übergeben
def __init__(self, x, y, c): # snip self.col = c
und in der Methode update()
als vorletzte Anweisung den Befehl:
fill(self.col)
bevor sie das Schaf als Kreis zeichnen soll.
Die Klasse Grass
bleibt bis auf eine Kleinigkeit unverändert. Mit der Zeile
noStroke()
als erste Anweisung der update()
-Methode habe ich das Gitterraster abgeschaltet.
Nur das Hauptprogramm mußte ein paar wesentlichere Änderungen bekommen:
from config import Settings from shaun import Sheep from grass import Grass from random import randint, choice s = Settings() sz = s.PATCHSIZE sheeps = [] lawn = [] colors = [s.WHITE, s.RED, s.BLUE, s.YELLOW] def setup(): size(s.WIDTH, s.HEIGHT) this.surface.setTitle("Shaun das Schaf (4): Es kann nur einen geben!") frameRate(s.FPS) for _ in range(20): c = choice(colors) sheeps.append(Sheep(randint(50, width - 50), random(50, height - 50), c)) for x in range(0, width, sz): for y in range(0, height, sz): lawn.append(Grass(x, y, sz))
Hier wird also zusätzlich von random
noch die Methode choice()
importiert, die dazu dient, aus der Liste colors
zufällig, aber gleichverteilt die Farben herauszusuchen und an die einzelnen Schafe zu verteilen. Die draw()
-Funktion habe ich nicht noch einmal abgeschrieben, da sich in ihr nichts geändert hat.
Was glaubt Ihr, was nun passiert? Zu Beginn der Simulation tummeln sich fröhlich vier Gruppen verschieden farbiger Schafe auf der Weide. Im weiteren Verlauf stirbt jedoch eine Farbe nach der anderen aus, bis nur noch eine Farbe übrigbleibt. Welche Farbe jedoch übrigbleibt, ist nicht vorhersehbar und rein zufällig. (Um nicht so lange auf das Aussterben der Populationen warten zu müssen – das kann sich manchmal lange hinziehen –, habe ich die FrameRate
auf 60 FPS erhöht.)
Auch dieses Verhalten ist bekannt und wurde 1975 unter anderem von Manfred Eigen und Ruthild Winkler in ihrem Buch »Das Spiel – Naturgesetze steuern den Zufall« im Kapitel über Selektion beschrieben:
Es kann mit Sicherheit bei jedem Spiel die Tatsache der Selektion vorausgesagt werden, nicht dagegen das Detailergebnis, nämlich welche Kugelfarbe selektiert wird.
Denn mit Ausnahme der Farbe verhalten sich alle Kreise identisch, das heißt, sie sind mit dem exakt gleichen genetischen Material ausgestattet. Man kann, um die Sache nicht zu kompliziert zu machen, auch einfach annehemen, daß die Farben Markierungen sind, die vom Beobachter auf die ansonsten genetisch identischen Schafe angebracht wurden.
Zu einem ähnlichen Ergebnis, kam ja schon das Demokratie-Spiel, das Alexander K. Dewdney in der Scientific American beschrieben hat: Wenn man lange genug Demokratie spielt, dann gewinnt zum Schluß eine Partei alle Sitze. Nur weiß man im Voraus nicht, welche.
Und in einem Ökosystem, in dem zwei oder mehr exakt gleiche Räuberpopulationen um die gleiche Beute konkurrieren, stirbt über kurz oder lang eine der beiden Populationen aus. Jede Population kann nur getrennt überleben, wenn sie sich ihre eigene, ökologische Nische sucht.
Wenn man jedoch, und auch das haben Eigen und Winkler beschrieben, einer Population auch nur einen winzigen Vorteil verschafft, dann überlebt diese Population. Wenn Ihr nämlich in der Klasse Sheep
in der update()
-Methode diese Anweisung einfügt,
if self.col == s.RED and randint(0, 1000) < 5: self.move = 25
dann überlebt immer (und zwar ziemlich schnell) die rote Population (im Quellcode habe ich diese Zeilen auskommentiert, Ihr könnt sie ja für Eure eigenen Experimente wieder einsetzen.
Der Vollständigkeit halber hier also jetzt der komplette Quellcode in der endgültigen Fassung. Zuerst die Datei config.py
:
# coding=utf-8 class Settings(): def __init__(self): # Einige nützliche Konstanten: self.WIDTH = 640 self.HEIGHT = 480 self.FPS = 5 self.PATCHSIZE = 10 # Ein paar Farbdefinitionen self.WHITE = color(255, 255, 255) self.YELLOW = color(255, 200, 0) self.RED = color(255, 0, 0) self.BLUE = color(0, 255, 255) self.ORANGE = color(255, 140, 0) self.BLACK = color(0, 0, 0) self.GREEN = color(0, 100, 0) self.BROWN = color(100, 50, 0)
Dann der Rasen (grass.py
):
# coding=utf-8 from config import Settings s = Settings() class Grass(): def __init__(self, x, y, sz): self.x = x self.y = y self.energy = 5 self.eaten = False self.sz = sz def update(self): noStroke() if self.eaten: if random(1000) < 5: self.eaten = False else: fill(s.BROWN) else: fill(s.GREEN) rect(self.x, self.y, self.sz, self.sz)
Dann das Schaf (shaun.py
):
# coding=utf-8 from random import randint from config import Settings s = Settings() class Sheep(): def __init__(self, x, y, c): self.x = x self.y = y self.sz = 10 # Shapesize self.move = 10 self.energy = 20 self.rows = height/self.sz self.col = c def update(self, lawn): self.energy -= 1 # if self.col == s.RED and randint(0, 1000) < 5: # self.move = 25 self.x += randint(-self.move, self.move) self.y += randint(-self.move, self.move) if self.x >= width - self.sz: self.x = width - self.sz if self.y >= height - self.sz: self.y = height - self.sz if self.x <= 0: self.x = 0 if self.y <= 0: self.y = 0 xscl = int(self.x/self.sz) yscl = int(self.y/self.sz) grass = lawn[xscl*self.rows + yscl] if not grass.eaten: self.energy += grass.energy grass.eaten = True fill(self.col) circle(self.x, self.y, self.sz)
Und zu guter Letzt das Hauptprogramm, das ich shaunsim
genannt habe:
from config import Settings from shaun import Sheep from grass import Grass from random import randint, choice s = Settings() sz = s.PATCHSIZE sheeps = [] lawn = [] colors = [s.WHITE, s.RED, s.BLUE, s.YELLOW] def setup(): size(s.WIDTH, s.HEIGHT) this.surface.setTitle("Shaun das Schaf (4): Es kann nur einen geben!") frameRate(s.FPS) for _ in range(20): c = choice(colors) sheeps.append(Sheep(randint(50, width - 50), random(50, height - 50), c)) for x in range(0, width, sz): for y in range(0, height, sz): lawn.append(Grass(x, y, sz)) def draw(): background(s.WHITE) for grass in lawn: grass.update() for sheep in sheeps: sheep.update(lawn) if sheep.energy <= 0: sheeps.remove(sheep) if sheep.energy >= 50: sheep.energy -= 30 sheeps.append(Sheep(sheep.x, sheep.y, sheep.col)) # print(len(sheeps))
Ihr könnt natürlich noch weiter an den Parametern schrauben (genau darum habe ich sie ja auch in die Datei config.py
ausgelagert), um vielleicht noch weitere Einsichten aus dieser einfachen Simulation zu gewinnen. Eine Möglichkeit wäre, die Lebenszeit der Schafe zu begrenzen. Sie könnten mit 255 Lebenspunkten anfangen und bei jedem Durchlauf wird ihnen davon etwas abgezogen. Sie »sterben«, wenn Ihre Lebenspunkte auf Null gefallen sind.
Interessant wäre dann, herauszufinden ob und wie sich unterschiedliche Lebensspannen auf die Überlebenschancen der Populationen auswirken. Ihr seht, selbst so eine einfache und kleine Simulation kann einen lange beschäftigen.
Über …
Der Schockwellenreiter ist seit dem 24. April 2000 das Weblog digitale Kritzelheft von Jörg Kantel (Neuköllner, EDV-Leiter Rentner, Autor, Netzaktivist und Hundesportler — Reihenfolge rein zufällig). Hier steht, was mir gefällt. Wem es nicht gefällt, der braucht ja nicht mitzulesen. Wer aber mitliest, ist herzlich willkommen und eingeladen, mitzudiskutieren!
Alle eigenen Inhalte des Schockwellenreiters stehen unter einer Creative-Commons-Lizenz, jedoch können fremde Inhalte (speziell Videos, Photos und sonstige Bilder) unter einer anderen Lizenz stehen.
Der Besuch dieser Webseite wird aktuell von der Piwik Webanalyse erfaßt. Hier können Sie der Erfassung widersprechen.
Diese Seite verwendet keine Cookies. Warum auch? Was allerdings die iframes
von Amazon, YouTube und Co. machen, entzieht sich meiner Kenntnis.
Werbung