Was du hier lernst
- Komplexe Zahlen (1j)
- Verschachtelte Schleifen
- break
- Graustufen und Pixel
- Deterministisches Chaos
- Gleitkommazahlen und ihre Grenzen
Wie aus einer Rechenregel Chaos wird
Chaos klingt nach Zufall. Gemeint ist in der Mathematik aber etwas anderes: ein System, das vollständig festgelegten Regeln folgt und trotzdem nicht vorhersagbar ist, weil winzige Unterschiede am Anfang riesige Unterschiede am Ende bewirken. Das Programm zeigt beides – erst mit Zahlen, dann als Bild.
1. Chaos in zwölf Zeilen
Der erste Teil nimmt eine Zahl zwischen 0 und 1, verdoppelt sie und behält nur die Nachkommastellen. Das Ganze zwölfmal:
c = 0.8403
for x in range(12):
c *= 2
f, i = math.modf(c)
c = f
math.modf zerlegt eine Zahl in Nachkommateil und ganzzahligen Teil: math.modf(1.6806) ergibt (0.6806…, 1.0). Die Regel ist so einfach, dass man sie im Kopf rechnen kann. Trotzdem springt die Zahlenfolge scheinbar wahllos hin und her.
Warum das chaotisch ist
Startest du mit 0,8403 und mit 0,8404, trennen sich die Folgen schnell: Jede Verdopplung verdoppelt auch den Unterschied. Nach zehn Schritten ist aus einer Abweichung von 0,0001 schon eine von etwa 0,1 geworden. Wer das Ergebnis in 50 Schritten vorhersagen will, müsste den Startwert auf 15 Stellen genau kennen. Das ist deterministisches Chaos.
2. Komplexe Zahlen
Für das Fraktal braucht es komplexe Zahlen. In Python schreibt man den imaginären Teil mit einem j:
c = -0.767793511962608 + 0.0952946560332372j
z = re + im * 1j
Eine komplexe Zahl ist ein Punkt in einer Ebene: re ist die waagerechte, im die senkrechte Koordinate. Python rechnet mit ihnen wie mit normalen Zahlen: z * z, +, und abs(z) liefert den Abstand vom Nullpunkt.
3. Jeder Pixel ist ein Startwert
Die beiden äußeren Schleifen laufen über alle Pixel. Jede Pixelposition wird in eine Zahl zwischen −1 und 1 umgerechnet:
re = (x * 2.0 / SIZE) - 1.0
im = (y * 2.0 / SIZE) - 1.0
Links oben liegt also der Punkt −1 − 1j, rechts unten 1 + 1j. Aus beiden Werten wird die komplexe Startzahl z.
4. Die Iteration
Dann wendet das Programm immer wieder dieselbe Regel an: z = z * z + c. Bleibt z klein, gehört der Startpunkt zur Julia-Menge. Wird der Betrag größer als 2, ist klar, dass z davonfliegt – und break beendet die innere Schleife:
for i in range(128):
if abs(z) > 2.0:
break
z = z * z + c
Entscheidend ist, wie schnell ein Punkt flieht. Die Schrittzahl i liegt zwischen 0 und 127. Das Programm macht daraus einen Grauton: i * 2 ergibt Werte von 0 (schwarz) bis 254 (fast weiß). Punkte nahe der Menge brauchen viele Schritte und leuchten hell – so entstehen die feinen Spiralen.
5. Spalte für Spalte ins Bild
Weil die äußere Schleife über x läuft, entsteht das Bild spaltenweise. Jede Spalte wird als bytearray gesammelt – vier Bytes pro Punkt für Rot, Grün, Blau und Deckkraft – und mit screen.pixels gezeichnet. Nach je acht Spalten zeigt await screen.frame(None) den Zwischenstand an. So kannst du zusehen, wie das Fraktal von links nach rechts wächst.
Julia und Mandelbrot
Die Julia-Menge hält c fest und variiert den Startwert z. Die Mandelbrot-Menge macht es umgekehrt: z startet immer bei 0, und jeder Pixel ist ein anderes c. Die Mandelbrot-Menge ist sozusagen eine Landkarte aller Julia-Mengen – liegt c in ihr, ist die zugehörige Julia-Menge zusammenhängend.
Probier mal
Jede Variante ändert den Code oben. Ein Klick auf »Ausprobieren« übernimmt die Änderung in den Editor und startet das Programm. »Zurücksetzen« holt das Original zurück.
-
01In Farbe
Statt eines Grautons bekommt jede Schrittzahl eine Farbe aus dem Farbkreis. Punkte, die alle 128 Schritte überstehen, bleiben schwarz – sie gehören zur Julia-Menge.
- from c4f import Screen + from c4f import Screen, hsv - grey = i * 2 # wie d.point((x, y), i * 2) im Graustufenbild - column += bytes((grey, grey, grey, 255)) + r, g, b = hsv(0.55 + i / 90, 0.85, 1.0 if i < 127 else 0.0) + column += bytes((r, g, b, 255)) -
02Das ganze Fraktal
Das Listing zeigt nur den Ausschnitt von −1 bis 1. Mit einem größeren Fenster siehst du die ganze Julia-Menge – sie ist punktsymmetrisch zum Mittelpunkt.
- re = (x * 2.0 / SIZE) - 1.0 + re = (x * 3.4 / SIZE) - 1.7 - im = (y * 2.0 / SIZE) - 1.0 + im = (y * 3.4 / SIZE) - 1.7 -
03Eine andere Konstante
Jedes
cerzeugt eine eigene Julia-Menge.-0.8 + 0.156jergibt feine Spiralarme,0.285 + 0.01jeine blumenartige Figur,-0.4 + 0.6jeinen zerfransten »Hasen«. Probiere eigene Werte – interessant wird es nahe am Rand der Mandelbrot-Menge.- c = -0.767793511962608 + 0.0952946560332372j + c = -0.8 + 0.156j -
04Chaos zu Ende gedacht
Verdopple nicht 12-, sondern 60-mal. Nach gut 50 Schritten ist die Zahl plötzlich exakt 0 – und bleibt es. Das ist kein Fehler im Programm, sondern eine Grenze des Computers: Jede Verdopplung schiebt eine binäre Nachkommastelle heraus, und eine Gleitkommazahl hat nur 52 davon.
- for x in range(12): + for x in range(60): -
05Feiner auflösen
600 × 600 statt 400 × 400 Punkte: Das Bild wird schärfer, braucht aber mehr als doppelt so lange – die Rechenzeit wächst mit der Anzahl der Punkte, also quadratisch mit der Seitenlänge.
- SIZE = 400 # + SIZE = 600 #
Vom Buch in den Browser
Im Buch speichert das Julia-Programm ein Graustufenbild mit der Python Imaging Library (PIL) als PNG-Datei, 2048 × 2048 Punkte groß. Im Browser gibt es keine PIL, dafür die Leinwand von c4f – und die zeigt das Ergebnis direkt an, während es entsteht. Die Rechnung selbst ist Zeile für Zeile übernommen.
| Im Buch (PIL) | Hier (c4f) |
|---|---|
image = Image.new("L", (SIZE, SIZE)) |
screen = Screen(SIZE, SIZE) |
d.point((x, y), i * 2) |
Grauwert i * 2 als RGBA-Bytes in eine Spalte |
image.save("julia2.png", "PNG") |
screen.pixels(x, 0, 1, SIZE, column) pro Spalte |
Warum 400 statt 2048 Punkte? 2048 × 2048 sind über vier Millionen Punkte, jeder mit bis zu 128 Rechenschritten. Auf dem Desktop dauerte das damals Minuten, im Browser wäre es ähnlich. 400 × 400 sind 160 000 Punkte und in wenigen Sekunden fertig. Unter »Probier mal« kannst du die Größe erhöhen.
Python 3: Aus print c, "->", wird print(c, "->", end=" ") – das Komma am Ende einer print-Anweisung, das in Python 2 den Zeilenumbruch unterdrückte, heißt jetzt end=" ". Die Zahlen werden auf vier Stellen gerundet, damit die Folge übersichtlich bleibt. Außerdem heißt die Bibliothek heute Pillow und wird mit from PIL import Image, ImageDraw importiert statt mit import Image.
Original aus dem Buch ansehen julia.py · Python 2
'''
Created on 11.01.2010
@author: GWV7FD3
'''
import Image
import ImageDraw
SIZE = 2048
image = Image.new("L", (SIZE, SIZE))
d = ImageDraw.Draw(image)
c = -0.767793511962608 + 0.0952946560332372j
for x in range(SIZE):
for y in range(SIZE):
re = (x * 2.0 / SIZE) - 1.0
im = (y * 2.0 / SIZE) - 1.0
z=re+im*1j
for i in range(128):
if abs(z) > 2.0: break
z = z * z + c
d.point((x, y), i * 2)
image.save(r"c:\julia2.png", "PNG")
Original aus dem Buch ansehen chaos1.py · Python 2
'''
Created on 01.02.2010
@author: sourcer
'''
import math
c = 0.8403
print c, "->",
for x in range(12):
c *= 2
f, i = math.modf(c)
c = f
print c,