Diskrete Modellierung Wintersemester 2016/17 Martin Mundhenk Uni Jena, Institut für Informatik 6. November 2016 3.x Exkurs: Lindenmayer-Systeme The Algorithmic Beauty of Plants Mit Turtle-Graphik lassen sich mit einfachen Prinzipien komplizierte“ Bilder ” programmieren. Beispiel 1: Die Koch-Snowflake. Startregel: Male ein Dreieck aus drei geraden Strichen. Ersetzungsregel: Ersetze jeden Strich durch 4 Striche wie folgt: flocke.py malt die Koch-Snowflake mit Rekursionstiefe n import sys, stddraw from turtle import Turtle from color import Color def # t # # n flocke(t,n,strichlaenge): ist die Turtle, sie malt einen Strich der Länge strichlaenge ist die Anzahl der Ersetzungen if n==0: t.goForward(strichlaenge) return flocke(t,n-1,strichlaenge/3) t.turnLeft(60) flocke(t,n-1,strichlaenge/3) t.turnLeft(-120) flocke(t,n-1,strichlaenge/3) t.turnLeft(60) flocke(t,n-1,strichlaenge/3) def male(): n = int(sys.argv[1]) stddraw.setPenRadius(0.001) stddraw.setXscale(-0.3,1.3) stddraw.setYscale(-1,0.6) t = Turtle(0.0,0.0,0.0,Color(240,100,80),1,1) # Beginne mit dem Dreieck aus 3 Strichen. # n gibt an, wie oft der Strich mit der # Ersetzungsregel ersetzt werden soll. flocke(t,n,1.0) t.turnLeft(-120) flocke(t,n,1.0) t.turnLeft(-120) flocke(t,n,1.0) t.turnLeft(-120) stddraw.show() if __name__ == '__main__': male() 3.2.2 Die Spur der Funktionsaufrufe von flocke flocke(t,0,1) | t.goForward(1) |_return flocke(t,1,1) | flocke(t,0,1/3) | | t.goForward(1/3) | |_return | t.turnLeft(60) | flocke(t,0,1/3) | | t.goForward(1/3) | |_return | t.turnLeft(-120) | flocke(t,0,1/3) | | t.goForward(1/3) | |_return | t.turnLeft(-120) | flocke(t,0,1/3) | | t.goForward(1/3) | |_return |_return flocke(t,2,1) | flocke(t,1,1/3) | | flocke(t,0,1/9) | | | t.goForward(1/9) | | |_return | | t.turnLeft(60) | | flocke(t,0,1/9) | | | t.goForward(1/9) | | |_return | | t.turnLeft(-120) | | flocke(t,0,1/9) | | | t.goForward(1/9) | | |_return | | t.turnLeft(-120) | |_return | t.turnLeft(60) | flocke(t,1,1/3) | | flocke(t,0,1/9) | | | t.goForward(1/9) | | |_return | | t.turnLeft(60) | | flocke(t,0,1/9) | | | t.goForward(1/9) | | |_return | | t.turnLeft(-120) | | flocke(t,0,1/9) | | | t.goForward(1/9) | | |_return | | t.turnLeft(-120) | |_return | t.turnLeft(-120) | flocke(t,1,1/3) | | ... | |_return | t.turnLeft(60) |_return Die Spur . . . mit den Aktionen der Turtle flocke(t,0,1) | t.goForward(1) |_return flocke(t,1,1) | flocke(t,0,1/3) | | t.goForward(1/3) | |_return | t.turnLeft(60) | flocke(t,0,1/3) | | t.goForward(1/3) | |_return | t.turnLeft(-120) | flocke(t,0,1/3) | | t.goForward(1/3) | |_return | t.turnLeft(60) | flocke(t,0,1/3) | | t.goForward(1/3) | |_return |_return F F F ++ F F flocke(t,2,1) | flocke(t,1,1/3) | | flocke(t,0,1/9) | | | t.goForward(1/9) | | |_return | | t.turnLeft(60) | | flocke(t,0,1/9) | | | t.goForward(1/9) | | |_return | | t.turnLeft(-120) | | flocke(t,0,1/9) | | | t.goForward(1/9) | | |_return | | t.turnLeft(60) | | flocke(t,0,1/9) | | | t.goForward(1/9) | | |_return | |_return | t.turnLeft(60) | flocke(t,1,1/3) | | flocke(t,0,1/9) | | | t.goForward(1/9) | | |_return | | t.turnLeft(60) | | flocke(t,0,1/9) | | | t.goForward(1/9) | | |_return | | t.turnLeft(-120) | | flocke(t,0,1/9) | | | t.goForward(1/9) | | |_return | | t.turnLeft(60) | | flocke(t,0,1/9) | | | t.goForward(1/9) | | |_return | |_return | t.turnLeft(-120) | flocke(t,1,1/3) | | ... F F ++ F F F F ++ F F ++ ... Eine Programmiersprache für die Turtle Beim Malen der Schneeflocke führt die Turtle nur 3 verschiedene Aktionen“ aus: ” Aktion t.goForward(strichlaenge) t.turnLeft(-60) t.turnLeft(60) Beschreibung der Aktion F + - Mit dieser Programmiersprache beschreiben wir das Malen der n-Schneeflocken: n “ 0: F++F++F n “ 1: F-F++F-F ++ F-F++F-F ++ F-F++F-F n “ 2: F-F++F-F - F-F++F-F ++ F-F++F-F - F-F++F-F ++ ...++ ... Diese Programme bauen sich nach einfachen Regeln auf: § Startregel: starte mit F++F++F § Ersetzungsregel: ersetze jedes F durch F-F++F-F Das Programm zu Malen der n-Schneeflocke entsteht durch n-maliges Anwenden der Ersetzungsregel auf die Startregel. 3.2.5 Etwas abstrakter: Lindenmayer-Systeme Lindenmayer-Systeme (kurz: L-Systeme) sind Ersetzungssysteme für Strings. Beispiel: Startwort: Ersetzungsregel 1: Ersetzungsregel 2: a a Ñ ab bÑa Die Ersetzung beginnt mit einem Startstring. Ersetzungsschritt: ersetze (gleichzeitig) jede Zeichenfolge im String, die auf der linken Seite einer Regel vorkommt, durch die rechte Seite der Regel. Startwort: 1.Ersetzungsschritt 2.Ersetzungsschritt 3.Ersetzungsschritt 4.Ersetzungsschritt a ab aba abaab abaababa Programmierung von Turtles mit Lindenmayer-Systemen Wir wollen ein Programm schreiben, das ein Lindenmayer-System einliest und die Turtle-Programme, die daraus erzeugt werden, ausführt. Die Turtle-Programme bestehen aus folgenden Befehlen: F f + - gehe eine Schrittlänge vorwärts und male einen Strich dabei gehe eine Schrittlänge vorwärts und male keinen Strich dabei drehe vorgegebene Gradzahl nach rechts drehe vorgegebene Gradzahl nach links 3.2.7 Ein Lindenmayer-System geben wir als Datei an. In der ersten Zeile steht die Gradzahl für die Drehungen + und -. Dahinter stehen zeilenweise die Regeln: zuerst das Zeichen auf der linken Seite der Regel, dann ein Leerzeichen, und dann (ohne Leerzeichen) die Zeichen der rechten Seite der Regel. Bei der Startregel steht auf der linken Seite start. Beispiel: 90 start F+F+F+F F F-F+F+FF-F-F+F 3.2.8 LSystem.py Die Klasse LSystem dient zum Einlesen eines Lindenmayer-Systems und zur Berechnung der Strings, die es mit beliebig vielen Ersetzungsschritten erzeugt. Sie hat folgende API: Operation Beschreibung LSystem(eingabeStream) ein neues Lindenmayer-System wird erzeugt; die Regeln werden zeilenweise aus eingabeStream gelesen ls.erzeugeString(i) der String des Lindenmayer-Systems mit i Iterationen wird als Ergebnis zurückgegeben 3.2.9 Bemerkungen zu den Funktionen von LSystem.py § init : die Regeln des Lindenmayer-Systems werden im Dictionary regeln gespeichert. Eine Regel F Ñ F ` F ´ F , die in der Datei für das Lindenmayer-System als Zeile F F+F-F steht, kommt dann als Eintrag {’F’:’F+F-F’} in das Dictionary. Die rechte Seite dieser Regel erhält man dann mit regeln[’F’]. § ersetzungsSchritt: erzeugt aus einem String altS den String neuS, indem jedes Zeichen zeichen von altS durch regeln[zeichen] – also durch die rechte Seite der Regel mit zeichen auf der linken Seite – ersetzt wird (falls es eine passende Regel gibt). Das entspricht einem Ersetzungsschritt. § erzeugeString(i): beginnt mit dem Startstring und wendet darauf i Ersetzungschritte an. 3.2.10 # LSystem.py import sys from instream import InStream class LSystem: def __init__(self, eingabe): def _ersetzungsSchritt(self,altS): # Die Regeln werden zeilenweise # Aus altS entsteht neuS durch Anwendung einer # aus dem Eingabestream eingabe gelesen # Regel des L-Systems auf jedes Zeichen. # und in ein Dictionary eingetragen. neuS = "" self._regeln = dict() for zeichen in altS: while not eingabe.isEmpty(): if zeichen in self._regeln: name = eingabe.readString() neuS = neuS + self._regeln[zeichen] wert = eingabe.readString() else: neuS = neuS + zeichen self._regeln[name] = wert return neuerString def erzeugeString(self,iterationen): # Der Startstring wird genommen # und iterationen-mal wird auf jedes seiner Zeichen # die passende Regel angewendet. # Dadurch entsteht der ergebnisString. startString = self._regeln['start'] ergebnisString = "" if iterationen==0: return startString for i in range(iterationen): ergebnisString = self._ersetzungsSchritt(startString) startString = ergebnisString return ergebnisString runTurtle.py Das Modul runTurtle stellt die Funktion runTurtle zur Verfügung, mit der ein Turtle-Programm ausgeführt werden kann. D.h., das durch das Turtle-Programm und den Winkel für die Drehungen bestimmte Bild wird gemalt. runTurtle.py hat folgende API: Operation runTurtle(programm,winkel) Beschreibung gibt auf stddraw das vom Programm und dem Winkel beschrieben Bild aus 3.2.12 Bemerkungen zu runTurtle.py runTurtle(programm,winkel) benötigt die minimalen und maximalen x- und y -Koordinaten (extreme Koordinaten), die im Bild vorkommen. Dafür wurde die API der Klasse Turtle erweitert: Operation t.maxx() Beschreibung gibt die maximale x-Koordinate, die bisher bemalt wurde, zurück t.maxy() gibt die maximale y -Koordinate, die bisher bemalt wurde, zurück t.minx() gibt die minimale x-Koordinate, die bisher bemalt wurde, zurück t.miny() gibt die minimale y -Koordinate, die bisher bemalt wurde, zurück t.goForward(s,zeigen=True) Turtle t geht s Schritte vorwärts; falls zeigen==True, malt sie dabei einen Strich; falls zeigen==False, wird kein Strich gemalt runTurtle(programm,winkel) berechnet zuerst die extremen Koordinaten, indem das Bild gemalt, aber nicht auf die Leinwand gebracht wird. Dazu dient die Funktion durchlauf. Sie setzt die Befehle des Turtle-Programms in Operationen der Turtle um. Nach einem Durchlauf ohne Zeichnen liefert die Turtle die extremen Koordinaten. Aus ihnen wird dann die X - und Y -Skalierung der Ausgabegraphik bestimmt. Damit das Bild nicht verzerrt wird, muss der Bereich für beide Skalen gleichgroß sein. Sind z.B. die x-Werte im Bereich ´3 . . . 5 und die y -Werte im Bereich 1 . . . 4, so hat die quadratische Ausgabegraphik die Seitenlänge 8 und die x-Koordinaten ´3 . . . 5 und die y -Koordinaten ´1.5 . . . 6.5. Mit diesen Werten wird die X - und Y ´Skalierung der Ausgabegraphik eingestellt. Beim folgenden Aufruf von durchlauf wird das Bild gemalt. import stddraw from turtle import Turtle from color import Color def durchlauf(t,programm,winkel,zeichnen=True): for befehl in programm: if befehl=='f': t.goForward(1,zeichnen) elif befehl=='F': t.goForward(1,zeichnen) elif befehl=='+': t.turnLeft(winkel) elif befehl=='-': t.turnLeft(-winkel) def runTurtle(programm, winkel): # starte zuerst einen Trockendurchlauf ohne zu zeichnen, um die Größe des Bildes festzustellen t = Turtle(0.0,0.0,0.0) durchlauf(t,programm,winkel,False) # Nun kennt t die extremen Koordinaten im Bild. Wir nutzen sie, um eine # quadratische Leinwand zu konstruieren, auf die das ganze Bild passt. breite = t.maxx() - t.minx() xmitte = t.maxx() - breite/2.0 hoehe = t.maxy() - t.miny() ymitte = t.maxy() - hoehe/2.0 halbebildseite = max(breite,hoehe) / 2.0 stddraw.setXscale(xmitte-halbebildseite,xmitte+halbebildseite) stddraw.setYscale(ymitte-halbebildseite,ymitte+halbebildseite) # Jetzt kann das Zeichnen beginnen. t = Turtle(0.0,0.0,0.0,Color(240,100,80),1,1) durchlauf(t,programm,winkel,True) 3.2.15 Der Klient, der alles zusammenbringt Der Klient maleLSBild.py § liest aus einer Datei den Winkel für die Drehungen der Turtle und das Lindenmayer-System ein, und liest von der Konsole die Anzahl der Iterationen, für die das Turtle-Programm erzeugt werden soll, § erzeugt das Turtle-Programm mittels LSystem und § malt das Bild mittels runTurtle. import sys, stddraw from instream import InStream from LSystem import LSystem from runTurtle import runTurtle def main(): # Die Datei mit der Bildbeschreibung hat in der ersten Zeile # den Winkel (float) für die + und - Operationen, # und in den darauffolgenden Zeilen die Regeln (ohne Pfeile, also z.B. F F+F+F+F). dateiname = sys.argv[1] eingabe = InStream(dateiname) # Zuerst wird der Winkel eingelesen. winkel = eingabe.readFloat() # Aus dem Rest der Eingabedatei wird das Lindenmayer-System erzeugt. LS = LSystem(eingabe) # Es wird eingelesen, durch wieviele Iterationen des L-Systems # das Turtle-Programm erzeugt werden soll. iterationen = int(sys.argv[2]) # Das Turtle-Programm wird erzeugt. programm = LS.erzeugeString(iterationen) # Das Bild wird gemalt. stddraw.setPenRadius(0.001) runTurtle(programm,winkel) stddraw.show() if __name__ == '__main__': main() 3.2.17 Beispiele Datei sierpinski.ls: 60 start F F +G-F-G+ G -F+G+F(Bei F und G wird gemalt.) python maleLSBild.py sierpinski.ls 7 Datei dragon.ls: 90 start FX X X+YF+ Y -FX-Y (Bei F wird gemalt, bei X und Y nicht.) python maleLSBild.py dragon.ls 12 Es geht noch schöner . . . Man kann die Turtle-Programm um einen Verzweigungsoperator erweitern: ’[’ bedeutet: starte eine neue Turtle am aktuellen Punkt und mache mit der alten Turtle weiter, wenn das korrespondierende ’]’ erreicht wird (Dazu kann man die Datenstruktur Stack benutzen.) Datei fractal-plant.ls: 25 start +++X X F-[[X]+X]+F[+FX]-X F FF (Bei F wird gemalt, bei X nicht.) python maleLSBild.py fractal-plant.ls 9 Zusammenfassung Es gibt viele weitere Varianten von Lindenmayer-Systemen. Wir haben gesehen, wie man § mit Programmen für Bilder das Malen vereinfachen kann § Programme einer Programmiersprache“ zum Bildermalen laufen ” lässt . . . 3.2.20