Dies ist eine alte Version des Dokuments!
Als Hauptcode ist die Datei „creatureV2.py“ gemeint. Sie beinhaltet (neben ein paar Funktionen, einem Koordinatenumrechner und einem Rating-Register) die Klasse „Creature“, welche eine Kreatur nach Bauplan erstellt. Praktisch das wichtigste, was wir für unser Programm brauchen!
Mit einer älteren Version1) haben wir begonnen zu programmieren, sie jedoch komplett neu geschrieben als wir fast fertig waren, wobei die Funktionalität dieselbe blieb.
Über den Lauf des Projektes haben sich unsere Programmierfähigkeiten enorm verbessert und wir bekamen genauere Vorstellungen von unserem Projekt und wie es aussehen soll. Der alte Code zeigte zu deutlich die Anfänge und hatte teilweise Inhaltsfehler, die auch durch erste (manchmal verworfene) Ideen verschuldet sind.
Der neue Hauptcode ist also nicht nur mit viel besserer Programmierung gelöst, sondern viel strukturierter und enorm benutzerfreundlich geworden!! Wer den alten Code vergleichen möchte, dem habe ich hier alte Codes verlinkt.
Bevor die Kreaturen Klasse erklärt wird ein paar Worte zu den anderen ebenfalls wichtigen Codestücken in der Datei:
class DEF78():
Der Code beginnt mit einigen typischen Importen und wird direkt danach von dem Erstellen eines „Logger“ gefolgt. Kurzgesagt hilft uns der Logger beim Verstehen und Debuggen vom Programm, wer jedoch genaueres dazu erfahren möchte empfehle ich das Tutorial dazu von Stefan Born.
Als nächstes im Code stehen verschiedene Variablen die unter dem Kommentar „settings“ stehen.
#settings start = {"facing" : 0.0, "spread" : 90} #in degree goal = {"coord" : (800,200) , "size" : (20,20)} length_zoom = 1 #float ( <0 will invert coords) step_cap = 10 #in steps LR_ratio = 0.5 #in [0;1] outlier_rate = 0.005 #in [0;1] rating_number = 1 #must be a valid number
Alles hier verändert gewisse Funktionalitäten im Programm und ist deshalb mit Bedeutung oder Definitionsbereichen beschrieben.
start
beeinflusst die Guckrichtung (mit einer Streuung), mit der die ersten Kreaturen losgehen2)
goal
beeinflusst die Lage und größe vom Ziel, dass die Kreaturen versuchen zu erreichen
length_zoom
verändert nur den Zoom vom Koordinatensystem ohne die Bewegung der Kreaturen zu skalieren
step_cap
gibt an wie lange sich die Kreaturen maximal bewegen dürfen
LR_ratio
beeinflusst mit welcher Wahrscheinlichkeit nach Links abgebogen wird
outlier_rate
bedeutet, mit welcher Wahrscheinlichkeit eine Tochterkreatur trotz Eltern-Input an dieser ihren eigenen Schritt geht
rating_number
gibt die Stelle im Rating-Register an, welches Rating benutzt werden soll
Die Coord Klasse als erstes großes Codeschnipsel kann das Simulations-Koordinatensystem auf andere Systeme anpassen. Anstatt also jedesmal Koordinaten einen Wert hinzuzurechnen, damit sie in der Visualisierung an gewollten Stellen platziert sind, muss nur noch mittels der passenden Umrechnungsfunktion gearbeitet werden. Wenn es keine Passende gibt, muss sie noch geschrieben werden.
Bevor über die wichtigsten Teile vom Programm geredet wird, ist ein kurzer Blick auf das Ende des Codes gewidmet, wo sich ein paar Funktionen befinden. Sie sollen Rechnungen verkürzen oder sind wichtig für die Simulation. So z.B procreation
, welches aus einer übergebenen Kreatur eine Tochterkreatur zurückgibt. Letzten Endes sorgen sie für eine Übersichtlichkeit im Code.
Mit dem Rating-Register beginnt der essenzielle Part vom Programm. Unsere Kreaturen brauchen eine Aufgabe, um sich zu Messen und die Selektion ein Kriterium, nach dem aussortiert werden kann. Die Aufgabe ist einfach: „Erreicht das Ziel“. Nur wie bewertet man? Es gibt verschiedene Ansätze zu bewerten und jeder hat seine Berechtigung, da eben jeder sein eigenes Umfeld, mit anderen Faktoren, für die Kreaturen zum Überleben erstellt. Aus diesem Grund gibt es das Rating-Register
mit einer Sammlung an möglichen Ratings. Es kann jederzeit erweitert werden. In den settings
kann man sich die Nummer des Ratings aussuchen und das Register gibt die entsprechende innere Funktion aus.
def ratingRegister(R): def rating1(): #dostuff def rating2(): #dostuff2 if R == 0: return rating1 elif R == 1: return rating2
Jede Kreatur erstellt also eine Funktion rating()
und bekommt nach folgender Zeile:
rating() = ratingRegister(R=0)
die gewollte Ratingfunktion zugewiesen, welche sich mit rating()
abrufen lässt.
Alles drumherum ist erklärt und es kann sich dem Herzstück der Kreaturen gewidmet werden: Der Creature
Klasse.
Eine Klasse bedeutet, dass jede Kreatur nach dieser Klasse funktioniert und wir somit beliebig viele Kreaturen nach dem selben Schema erstellen können.
Angefangen mit der __init__
, sind alle Attribute definiert und Eigenschaften gesetzt.
def __init__(self, species=0, ancestor_way=[]): ''' Kreatur wird erstellt ''' #Spezienzuweisung if species == 0: self.traits = {"specie" : 0 , "tendency" : 70 , "arc" : (22.5,17.5) , "path_length" : 10 , "mutation_rate" : 0} else: self.traits = {"specie" : 1 , "tendency" : 66 , "arc" : (10,0) , "path_length" : 18 , "mutation_rate" : 5} #erste Generation startet innerhalb eines parametisierten Winkels if ancestor_way == [] and start["spread"] != 0: ancestor_way = [rng.randint(start["spread"])-start["spread"]/2] #Attributzuweisung self.briefing = [] self.ancestor_way = ancestor_way self.rating = ratingRegister(self,rating_number) self.stats = {"pos" : (0.0,0.0) , "viewpoint" : start["facing"] , "step_counter" : 0 , "dead" : False , "success" : False} self.career = {"best" : 0.0 , "time_stamp" : 0 , "score" : 0}
Dem Konstruktor der Kreatur kann man eine Spezies und eine Wegbeschreibung übergeben. Beides ist für die Vererbung wichtig und bestimmt Eigenschaften und erste Wegentscheidungen.
Eigenschaften, Werte und Bewertungsdaten sind als ein dictionary in traits
, stats
und career
sortiert und eingeordnet. Dazu eine kurze Erklärung einzelner Variablen:
traits
specie
benennt einfach nur die Spezietendency
gibt die prozentuale Wahrscheinlichkeit an abzubiegen bzw. als Gegenwahrscheinlichkeit geradeaus zu bewegenarc
ist der Winkel in dem abgebogen wird und der Spanne drumherumpath_length
ist die Schrittlängemutation_rate
gibt die Stärke der Mutation bei Vererbung an.stats
pos
sind die Koordinaten der Kreaturviewpoint
ist die Blickrichtung als Winkel zur x-Achsestep_counter
zählt die Anzahl der zurückgelegten Schrittedead
sagt ob die Kreatur Tod ist oder eben nichtsuccess
ist auf True, wenn die Kreatur das Ziel erreicht hatcareer
best
speichert irgendwelche Bestwerte fürs Ratingtime_stamp
speichert einen Zeitpunkt bis zu dem das Erbgut weitervererbt werden sollscore
ist das Rating als Zahl zusammengefasst
Dazu kommen noch ein paar weitere Variablen wie:
briefing
ist die Wegbeschreibung
ancestor_way
ist ein Teil der Wegbeschreibung der Eltern der unterschiedlich grob kopiert wird
rating
beinhaltet die Ratingfunktion nach der Bewertet wird
Mit allen diesen Variablen lässt sich die Kreatur komplett beschreiben, nur muss sie damit auch etwas anfangen können. Jede Kreatur muss folgendes können:
procreation
arbeiten kann
Die Klasse besitzt mehrere Funktionen, damit jede Kreatur die Aufgaben bewältigen kann. Im Code steht zu jeder Funktion was sie tut, sodass es hier nicht extra erläutert ist. Stattdessen wird die Funktionsweise näher erklärt:
#Bewegung self.appendWay() #erste Wegerweiterung #eigentliche Bewegung for e in self.briefing: self.nextStep(e) if self.stats["step_counter"] < step_cap and not self.stats["dead"]: self.appendWay()
Jede Kreatur, nachdem sie konstruiert wurde, besitzt eine leere Wegbeschreibung. Bevor die Bewegung initiiert wird, muss die Wegbeschreibung, also briefing
, einmal erweitert werden. Dann kann der erste Schritt getan werden und briefing
wieder erweitert werden. Wieder ein Schritt und wieder erweitert und das solange, bis die Kreatur stirbt oder die step_cap
erreicht. So kommt eine flüssige, kontinuierliche Bewegung zustande. Zwischen jeden Schritten wird geprüft ob die Kreatur noch lebt und das Rating aufgerufen und aktualisiert.
#Intensitäts-Rating if not self.stats["dead"]: ''' wird nach jeder Bewegung ausgeführt ''' self.career["score"] += 100 - (distance(self.stats["pos"], goal["coord"])/distance(goal["coord"])*100) elif self.stats["dead"]: ''' wird nach dem Tod erstellt ''' self.career["time_stamp"] = self.stats["step_counter"] return self.career["score"]
In jeder Rating-Funktion wird unterschieden ob die Kreatur noch lebt oder nicht. Falls sie Tod ist werden die letzten Dinge für die Vererbung vorbereitet und gibt den score
zurück mit dem eine spätere Sortierfunktion arbeiten wird. Falls die Kreatur noch lebt wird die letzte Bewegung analysiert und entsprechend der score
aktualisiert. Im Intensitäts-Rating bedeutet das, dass nach jedem Schritt die Kreatur „riecht“ und dabei erkennt wie intensiv es „riecht“ bzw. wie nah sie dem Ziel ist. Je näher die Einheit dem Ziel kommt, desto höher ist der Wert, der dem score
hinzugefügt wird4)
Was bedeutet einen Schritt gehen?
def nextStep(self, r): ''' bewegt die Kreatur einen Schritt ''' if self.stats["dead"]: ''' Kreatur bereits gestorben -> Bewegung gestoppt ''' return self.stats["viewpoint"] += r #auf dem kreis bleiben: if self.stats["viewpoint"] > 359: self.stats["viewpoint"] -= 360 elif self.stats["viewpoint"] < 0: self.stats["viewpoint"] += 360 #eigentliche Bewegung im Koordinatensystem self.stats["pos"] = (self.stats["pos"][0] + length_zoom*(self.traits["path_length"]) * np.cos(self.stats["viewpoint"]/180*np.pi), self.stats["pos"][1] + length_zoom*(self.traits["path_length"]) * np.sin(self.stats["viewpoint"]/180*np.pi)) self.stats["step_counter"] += 1
Ein Schritt heißt, dass die Kreatur, mit der nextStep
Funktion und einem übergeben Input aus der Wegbeschreibung, die aktuellen Positionskoordinaten ändert. Dazu wird die Blickrichtung (viewpoint
) entsprechend des Inputs geändert und dann, zusammen mit der Schrittlänge (path_length
) und der Trigonometrie, die neuen Koordinaten errechnet.
Zu guter Letzt ist nur noch ungeklärt, wie die Wegbeschreibung erweitert wird. Ganz wichtig ist dabei zu Unterscheiden, ob es für die zu erweiternde Stelle eine von „Eltern“ vorgegebene Richtung gibt oder eben nicht.
Bei der ersten Generation gibt es diese nicht und die Funktion bezieht sich auf die Eigenschaften der Spezies:
#ohne Input nach Eigenschaften gehen else: if rng.random()*100 > self.traits["tendency"]: self.briefing.append(0) elif rng.random() < LR_ratio: self.briefing.append(rng.randint(self.traits["arc"][0]-self.traits["arc"][1], self.traits["arc"][0]+self.traits["arc"][1]+1)) else: self.briefing.append(-(rng.randint(self.traits["arc"][0]-self.traits["arc"][1], self.traits["arc"][0]+self.traits["arc"][1]+1)))
Ein zufälliger Wert bestimmt, ob sich die Kreatur geradeaus bewegt oder abbiegt. Ähnlich wird geguckt ob die Bewegung nach links oder recht gehen soll. Dementsprechend wird briefing
ein Wert hinzugehangen, der entweder 0 für geradeaus oder positiv/negativ um den durch arc
definierten Winkel für links/rechts Drehungen steht.
Falls ancestor_way
von den Eltern eine Richtung vorbestimmt, widmit sich die Funktion diesem Input5).
#Eltern Input existiert r = self.ancestor_way[self.stats["step_counter"]] # MUTATION VON R #r begrenzen if r < (self.traits["arc"][0]+self.traits["arc"][1])*-2: r += rng.random()*self.traits["mutation_rate"] elif r > (self.traits["arc"][0]+self.traits["arc"][1])*2: r -= rng.random()*self.traits["mutation_rate"] #ansonsten Mutation freien Lauf lassen elif rng.random() < LR_ratio: r += rng.random()*self.traits["mutation_rate"] else: r -= rng.random()*self.traits["mutation_rate"] self.briefing.append(r)
Es wird zunächst der Wert eins zu eins übernommen, allerdings im Anschluss nochmal leicht verändert. Diese Mutation ist enorm wichtig für die gesamte Evolution! Damit die Mutation nicht unkontrolliert in alle Richtungen ausschert ist r
begrenzt und lässt an den Grenzen nur Mutation zu, die davon wegführt. Ansonsten kann sich r
frei mutieren; je höher die Rate in den Eigenschaften, desto stärker.
Mit all den Funktionen zusammen bekommen wir voll funktionsfähige Kreaturen für unser Programm und es liegt nun an einer geschickten Visualisierung, aus dem Programm eine gelungene Simulation zu erstellen.