15.5 Mandelbrot- und Julia-Mengen
Mandelbrot- und Julia-Mengen sind Zahlenmengen von komplexen Zahlen. Zur Veranschaulichung werden sie in der komplexen Zahlenebene grafisch dargestellt. Während von der Mandelbrot-Menge nur eine Visualisierung möglich ist, das sogenannte Apfelmännchen, gibt es theoretisch unendlich viele Julia-Mengen. Erzeugt werden diese Mengen durch rekursive Zahlenfolgen, deren komplexe Funktionswerte in einem Kreis mit dem Radius 2 liegen müssen. Der in den 1920er-Jahren weltberühmte französische Mathematiker Gaston Julia (1893–1978) beschrieb bereits 1918 die nach ihm benannten Mengen. Sein Werk geriet aber weitgehend in Vergessenheit, bis Benoît Mandelbrot um 1977 seine Ideen wieder aufgriff und so dazu beitrug, dass die Julia-Mengen wieder Gegenstand der mathematischen Forschung wurden. Erst mit der Einführung des Computers wurde es möglich, diese Mengen zu visualisieren, und durch ihre außergewöhnliche Ästhetik gewannen sie in einer breiten Öffentlichkeit an Popularität.
15.5.1 Mandelbrot-Menge
Die nach Benoît Mandelbrot benannte Menge wird aus einer rekursiven Folge von komplexen Zahlen

ermittelt. Wenn diese Zahlen zn+1 innerhalb eines bestimmten Radius, des sogenannten Fluchtradius, mit dem Wert 2 liegen, dann gehören diese Zahlen zur Mandelbrot-Menge. Die Folge startet mit z0 = 0. Für c werden komplexe Zahlen eingesetzt. Berechnet wird also: c, dann c2 + c, dann (c2 + c)2 + c, dann [(c2 + c)2 + c]2 + c und so weiter.
Komplexe Zahlen
Komplexe Zahlen z setzen sich aus einem Real- und einem Imaginärteil zusammen: z = a + bi. Das Symbol i steht für imaginäre Einheit. Es gilt allgemein i2 = –1.
Mit komplexen Zahlen können alle mathematischen Grundoperationen durchgeführt werden.
Abbildung 15.8 zeigt eine komplexe Zahl c = 1 + i. Sie liegt ebenso wie der Punkt C im Fluchtkreis.
Abbildung 15.8 Komplexe Zahlenebene mit Fluchtkreis r = 2
Der Fluchtkreis hat einen Radius von 2. Man könnte annehmen, dass die Folge für den Startwert von c = 1 + i konvergieren würde, weil c ja noch im Fluchtkreis liegt. Das ist aber nicht der Fall. Denn die komplexe Zahl c muss ja noch quadriert werden, und zu dem Quadrat muss wiederum c addiert werden usw. Nicht in allen Fällen kann mathematisch exakt vorausgesagt werden, ob eine komplexe Zahl c zur Mandelbrot-Menge gehört. Mit Listing 15.8 können Sie dieses Problem genauer untersuchen.
Mandelbrot-Menge
Bei der Mandelbrot-Rekursion wird ein Punkt C in der komplexen Zahlenebene gewählt. Dieser Punkt wird durch eine komplexe Zahl c repräsentiert. Dann wird eine Mandelbrot-Folge zn mit diesem c gestartet. Wenn die Folge nie den Fluchtkreis verlässt, gehört der Punkt C zur Mandelbrot-Menge . Wenn die Folge einmal den Fluchtkreis verlässt, kehrt sie nie zurück, sondern läuft ins Unendliche. Der Punkt C gehört dann nicht zur Mandelbrot-Menge [Haftendorn: 123].
Mit Listing 15.8 können Sie testen, ob mit den Startwerten c Folgenglieder erzeugt werden, die noch zur Mandelbrot-Menge gehören. In Zeile 03 sind verschiedene Startwerte mit reellen und komplexen Zahlen in eine Liste eingetragen. Der Imaginärteil einer komplexen Zahl wird definiert, indem direkt hinter einer reellen Zahl ein j angehängt wird. Sie können diese Werte ändern, um zu testen, ob die Folge z=z**2+c (Zeile 08) für diesen Wert noch konvergiert.
01 #08_mandelbrot1.py
02 import numpy as np
03 C=[-0.5,-1,1,-1+0.1j]
04 for c in C:
05 z=c
06 print("\nc =",c,end=':\n')
07 for i in range(1,6):
08 z=z**2+c
09 print(np.round(z,4),end=', ')
Listing 15.8 Wertetabelle für die Mandelbrot-Menge
Ausgabe
c = -0.5:
-0.25, -0.4375, -0.3086, -0.4048, -0.3362,
c = -1:
0, -1, 0, -1, 0,
c = 1:
2, 5, 26, 677, 458330,
c = (-1+0.1j):
(-0.01-0.1j), (-1.0099+0.102j), (0.0095-0.106j),
(-1.0112+0.098j), (0.0128-0.0982j),
Analyse
Für c = 1 divergiert die Folge. In den anderen Fällen scheint sie zu konvergieren. Dieses experimentelle Verfahren mit wenigen ausgewählten Werten für c verschafft nur einen ersten Zugang zur Problemantik. Bis heute konnte mathematisch noch nicht exakt geklärt werden, ob ein c zur Mandelbrot-Menge gehört. Das j hinter einer Zahl kennzeichnet den Imaginärteil einer komplexen Zahl.
Grafische Darstellung der Mandelbrot-Menge
Die Zahlen der Folgenglieder sind nicht sehr aussagekräftig. Besser wäre es, die komplexen Zahlen in der komplexen Zahlenebene zu visualisieren. In Abbildung 15.9 wird die Mandelbrot-Menge in der komplexen Zahlenebene grafisch dargestellt. Jedes Pixel steht für einen Real- und Imaginärteil von z in der komplexen Zahlenebene.
Abbildung 15.9 Mandelbrot-Menge (»Apfelmännchen«) in der komplexen Zahlenebene
Alle dunkel eingefärbten Pixel gehören zur Mandelbrot-Menge. Bei den Rändern handelt es sich um Fraktale, die aber in dieser Abbildung wegen der schlechten Auflösung nicht sichtbar sind. Im Internet finden Sie Programme, mit denen Sie die Ränder durch Zoomen sichtbar machen können (z. B.: http://weitz.de/mandelbrot/). Wenn Sie keine Programme auf Ihrem Rechner installieren wollen, dann können Sie unter https://math.hws.edu/eck/js/mandelbrot/MB.html die Mandelbrot-Fraktale sogar online simulieren.
Abbildung 15.10 zeigt einen gezoomten Bildausschnitt aus dem Randbereich einer Mandelbrot-Menge.
Abbildung 15.10 Ein gezoomtes Fraktal aus dem Randbereich der Mandelbrot-Menge (Baby-Mandelbrot-Menge)
Für die grafische Darstellung der Mandelbrot-Menge werden die NumPy-Funktion ogrid und die Matplotlib-Methode imshow benötigt.
Mit der Funktion ogrid können Sie eindimensionale Arrays
>>> from numpy import ogrid
>>> ogrid[-2:1:5j]
array([-2., -1.25,-0.5 ,0.25, 1.])
oder auch zweidimensionale Arrays
>>> ogrid[-2:1:5j,-1:1:5j]
[array([[-2. ],
[-1.25],
[-0.5 ],
[ 0.25],
[ 1. ]]), array([[-1. , -0.5, 0. , 0.5, 1. ]])]
mit komplexen Zahlen erzeugen.
Die Matplotlib-Methode imshow(bild, cmap='...', ...) erzeugt Bilder, die als Pixel auf dem Bildschirm angezeigt werden. Bei dem Objekt bild handelt es sich um ein zweidimensionales NumPy-Array, in dem alle Bilddaten abgespeichert sind. Mit der Eigenschaft cmap können Sie die Darstellungsart variieren.
Mehr über imshow erfahren Sie unter: https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.imshow.html
Nachdem Sie Listing 15.9 gestartet haben, wird die Mandelbrot-Menge als farbig eingefärbte Pixel auf dem Bildschirm dargestellt (siehe Abbildung 15.9).
01 #09_mandelbrot2.py
02 import numpy as np
03 import matplotlib.pyplot as plt
04 import warnings
05 N=50
06 x1,x2=-2,1
07 y1,y2=-1.5,1.5
08 x,y = np.ogrid[x1:x2:800j,y1:y2:800j]
09 c = x + 1j*y
10 z = c
11 warnings.filterwarnings('ignore')
12 for _ in range(N):
13 z = z**2 + c
14 mandelbrot = (np.abs(z) < 2).transpose()
15 #Grafikbereich
16 fig, ax = plt.subplots()
17 ax.imshow(mandelbrot,cmap='Blues',extent=[x1, x2, y1, y2])
18 ax.set_xlabel("Re(z)")
19 ax.set_ylabel("Im(z)",rotation=90)
20 fig.tight_layout()
21 ax.set_aspect('equal')
22 plt.show()
Listing 15.9 Darstellung der Mandelbrot-Menge in der komplexen Zahlenebene
Analyse
In Zeile 08 erzeugt die NumPy-Funktion ogrid ein quadratisches Array aus 800 × 800 komplexen Zahlen. Die reelle x-Achse hat einen Wertebereich von –2 bis +1. Die imaginäre y-Achse hat einen Wertebereich von –1,5 bis +1,5.
In Zeile 09 wird ein zweidimensionales Array-Objekt c aus komplexen Zahlen erzeugt, das alle Koordinatendaten der komplexen Zahlenebene enthält. In Zeile 10 wird das Objekt z mit c initialisiert.
Innerhalb der for-Schleife (Zeile 12) wird nach der Rekursionsvorschrift für die Ermittlung der Mandelbrot-Menge z=z**2+c eine Folge von komplexen Zahlen generiert.
Die Anweisung in Zeile 11 unterdrückt die Warnungen bei einem Speicherüberlauf.
In Zeile 14 wird ein Objekt mandelbrot erzeugt, das die in Zeile 13 berechneten Daten der Mandelbrot-Menge enthält. Es werden nur diejenigen komplexen Zahlen gespeichert, die innerhalb des Fluchtradius 2 liegen. Wenn Sie in Zeile 05 die Anzahl N der Iterationen vergrößern (z. B. auf 1000 bis 5000), dann werden Sie keine Veränderung in der Darstellung der Mandelbrot-Menge mehr erkennen können.
Das heißt, streng genommen nähert man sich bei der Vergrößerung von N der Mandelbrot-Menge nur an. Die NumPy-Funktion transpose() transponiert die Matrix der Bilddaten. Alternativ können Sie die Matrix auch mit der Anweisung (np.abs(z) < 2).T transponieren.
In Zeile 17 sorgt die Matplotlib-Methode imshow() dafür, dass die Mandelbrot-Menge auf dem Bildschirm dargestellt wird.
Übung
Ändern Sie in Zeile 05 die Anzahl der Schleifendurchläufe von N in dem Bereich von 1 bis 50, und beobachten Sie, was nach einem Programmstart passiert.
Kommentieren Sie die Funktion transpose() in Zeile 14 aus , und starten Sie das Programm neu. Was passiert?
Ändern Sie den Fluchtradius in Zeile 14. Was passiert nach einem Programmstart?
Kommentieren Sie Zeile 11 aus, und starten Sie das Programm neu. Interpretieren Sie die Fehlermeldung.
15.5.2 Julia-Menge
Die Rekursionsvorschriften für die Berechnung der Julia-Menge und der Mandelbrot-Menge stimmen formal überein:

Der Unterschied besteht lediglich darin, dass sich die komplexe Zahl c bei der Julia-Menge für alle Iterationen nicht ändert.
Durch Variation der komplexen Konstanten c können sehr unterschiedliche Julia-Mengen erzeugt werden. Abbildung 15.11 zeigt einige Möglichkeiten.
Abbildung 15.11 Unterschiedliche Möglichkeiten, um Julia-Mengen zu erzeugen (Quelle: http://www.3d-meier.de/tut20/Julia2/Seite1.html)
Für die Wahl der komplexen Konstanten c gilt:
-
Wenn ein Wert für c aus der Mandelbrot-Menge gewählt wird, dann entstehen zusammenhängende Figuren.
-
Wenn ein Wert für c außerhalb der Mandelbrot-Menge gewählt wird, dann entstehen mehrere nicht miteinander verbundene Figuren [Haftendorn: 127].
Abbildung 15.12 zeigt eine visualisierte Julia-Menge, die mit der komplexen Konstanten c = –0,1 + 0,65i erzeugt wurde.
Abbildung 15.12 Julia-Menge für c = –0,1 + 0,65i
Mit Listing 15.10 können Sie verschiedene Julia-Mengen visualisieren. Dazu brauchen Sie nur die entsprechenden Kommentare in den Zeilen 08 bis 16 zu entfernen.
01 #10_julia.py
02 import numpy as np
03 import matplotlib.pyplot as plt
04 import warnings
05 N=500
06 cc='Blues'#hot,Blues,plasma,gray,twilight,magma
07 c = -0.1+0.65j
08 #c = -1.2 #-1.4 bis +0.2
09 #c=-0.74543+0.1130j
10 #c= 0.909 - 0.27j
11 #c=0 + 0.8j
12 #c=0.37+0.1j
13 #c=0.355+0.355j
14 #c=-0.54+0.54j
15 #c=-0.4-0.59j
16 #c=-0.1+0.9j
17 x1,x2=-2,2
18 y1,y2=-1.5,1.5
19 x,y = np.ogrid[x1:x2:800j,y1:y2:800j]
20 z=x + 1j*y
21 warnings.filterwarnings('ignore')
22 for _ in range(N):
23 z = z**2 + c
24 julia = (np.abs(z) < 2).T
25 #Grafikbereich
26 fig, ax=plt.subplots()
27 ax.imshow(julia,cmap=cc,extent=[x1, x2, y1, y2])
28 ax.set_xlabel("Re(z)")
29 ax.set_ylabel("Im(z)",rotation=90)
30 fig.tight_layout()
31 plt.show()
Listing 15.10 Julia-Menge für c = –0,1+0,65i
Analyse
Das Programm stimmt weitgehend mit Listing 15.9 überein. Der einzige Unterschied besteht darin, dass in Zeile 07 für c ein konstanter Wert vorgegeben wird. Und in Zeile 20 wird ein quadratisches Array mit komplexen Zahlen z = x + 1j*y erzeugt. In der Vorgängerversion stand dort c = x + 1j*y.
Übung
Ändern Sie die Realteile in Zeile 08 von -1.3 bis +0.2, und beobachten Sie die Formen der Julia-Mengen.
Testen Sie das Programm mit den auskommentierten komplexen Konstanten in den Zeilen 09 bis 16. In einigen Fällen muss die Anzahl N der Schleifendurchläufe in Zeile 05 verringert oder vergrößert werden.
Testen Sie das Programm mit verschiedenen Einfärbungsvarianten cc (Zeile 06).
15.5.3 Farbige Darstellung von Julia-Mengen
In der Literatur oder im Internet finden sich häufig farbige Darstellungen von Julia-Mengen. Sie sind wegen ihrer außergewöhnlichen Farbverläufe ästhetisch ansprechender und wecken deshalb leichter das Interesse der Betrachter. Dabei sollten Sie beachten, dass diese Einfärbungen mathematisch nicht relevant sind, denn Farbe ist keine Eigenschaft der Julia-Mengen; sie codiert lediglich die Fluchtgeschwindigkeit der komplexen Folgenglieder. In Abbildung 15.13 sehen Sie eine farbige Darstellung der Julia-Menge für c = –0,1 + 0,65i, die mit Listing 15.11 erzeugt wurde.
Abbildung 15.13 Julia-Menge für c = –0,1 + 0,65i (farbig)
Listing 15.11 verwendet das Modul PIL (Python Imaging Library), das Sie mit pip install Pillow gegebenenfalls installieren müssen. Bei diesem Modul handelt es sich um eine kostenlose Open-Source-Zusatzbibliothek, die die Bearbeitung und das Speichern vieler verschiedener Bilddateiformate unterstützt.
Die Methode Image.new("RGB",(X,Y),"white") erzeugt ein Bildobjekt. Es verwendet RGB-Farben, es hat eine Breite von X Pixeln und eine Höhe von Y Pixeln, der Hintergrund ist weiß.
Die PIL-Methode putpixel(x,y,value) zeichnet das Fraktal. Sie ändert ein Pixel an der angegebenen Position. Die Farbe wird als Tupel für RGB-Bilder angegeben.
Das Programm verwendet zwei ineinander verschachtelte for-Schleifen, um die x,y-Koordinaten der komplexen Zahl z = x + yi in der komplexen Zahlenebene zu erfassen. Durch Entfernen der Kommentare ab Zeile 04 können Sie verschiedene Julia-Mengen erzeugen.
01 #11_julia_farbe.py
02 from PIL import Image
03 c = -0.1+0.65j
04 #c = -1.2 #-1.4 bis +0.2
05 #c=-0.74543+0.1130j
06 #c= 0.909 - 0.27j
07 #c=0 + 0.8j
08 #c=0.37+0.1j
09 #c=0.355+0.355j
10 #c=-0.54+0.54j
11 #c=-0.4-0.59j
12 #c=-0.1+0.9j
13 X,Y = 800,800
14 x1,x2 = -2,2
15 y1,y2 = -1.5,1.5
16 sx=(x2-x1)/X
17 sy=(y2-y1)/Y
18 julia = Image.new("RGB",(X,Y),"white")
19 for x in range(X):
20 zx = sx*x + x1
21 for y in range(Y):
22 zy = sy*y + y1
23 z = zx + 1j*zy
24 i = 255
25 while abs(z)<2 and i>1:
26 z=z**2+c
27 i = i - 1
28 #julia.putpixel((x,y),(i%4*64,i%8*32,i%16*16))
29 #julia.putpixel((x,y),(i%16*16,i%8*32,i%4*64))
30 julia.putpixel((x,y),((i<<21)+(i<<10)+i*8))
31 #julia.save("julia_menge.png")
32 julia.show()
Listing 15.11 Julia-Menge in Farbe
Analyse
In Zeile 02 wird die Klasse Image aus dem Modul PIL importiert. In Zeile 03 wird die komplexe Konstante c für die Berechnung einer Julia-Menge vorgegeben. Die folgenden auskommentierten Zeilen sind für weitere Testläufe vorgesehen.
Zeile 13 legt die Breite X und die Höhe Y des Bildes mit 800 × 800 Pixeln fest.
Die Zeilen 14 und 15 bestimmen den Wertebereich der x- und y-Achse in der komplexen Zahlenebene.
In den Zeilen 16 und 17 werden die Skalierungsfaktoren sx und sy berechnet.
In Zeile 18 erzeugt der Konstruktor Image.new("RGB",(X,Y),"white") dieser Klasse das Objekt julia. Das Bild wird mit RGB-Farben erstellt, es hat eine Breite von X=800 Pixeln und eine Höhe von Y=800 Pixeln. Die Hintergrundfarbe ist Weiß.
Die Julia-Menge wird innerhalb der drei ineinander verschachtelten Schleifen erzeugt (Zeilen 19 bis 30).
In Zeile 20 werden die aktuellen Werte für die reelle Achse zx berechnet, und in Zeile 22 werden aktuellen Werte zy für die imaginäre Achse berechnet. Zeile 23 erzeugt daraus die komplexe Zahl z=zx+1j*zy.
In Zeile 24 wird eine Zählvariable i mit dem Wert 255 initialisiert. Der Fluchtradius 2 legt die Abbruchbedingung der while-Schleife fest. In Zeile 26 wird die Zahlenfolge der Julia-Menge berechnet.
Bei jedem Schleifendurchlauf in der while-Schleife wird die Zählvariable i um den Wert 1 verringert. Das Dekrementieren i=i-1 beeinflusst den Farbverlauf. Wenn stattdessen inkrementiert i=i+1 würde, dann ergäbe sich ein anderer Farbverlauf.
In Zeile 30 erzeugt die Methode putpixel((x,y),...) das Bildobjekt julia. Der zweite Parameter legt den Verlauf der RGB-Farben fest. Hierzu können Sie entweder den Modulo-Operator % oder den Shift-Operator << benutzen. Der Operator << bewirkt das Verschieben nach links um den jeweils dahinter angegebenen Wert.
Die Anweisung julia.show() in Zeile 32 bewirkt, dass die Julia-Menge standardmäßig als Bild im png-Format in dem Standardbildbetrachter des Betriebssystems auf dem Monitor angezeigt wird.
Wenn Sie den Kommentar in Zeile 31 entfernen, können Sie das Bild auch auf der Festplatte speichern. Neben dem png-Bildformat werden auch noch die Formate jpg, gif und eps unterstützt.
Übung
Testen Sie das Programm mit verschiedenen komplexen Konstanten c (Zeilen 04 bis 12).
Testen Sie das Programm mit verschiedenen Farbverläufen (Zeilen 28 und 29).
Testen Sie das Programm mit einem Inkrement i=i+1 (Zeile 27). Dazu müssen Sie die Laufvariable i in Zeile 24 mit 0 initialisieren und die Abbruchbedingung in der while-Schleife ändern.