Fraktle - wstęp

Za ojca fraktali powszechnie uznaje się francusko‑amerykańskiego matematyka Benoîta Mandelbrota, który w latach 70. XX wieku opisał zbiór nazwany później jego imieniem — zbiór Mandelbrota. Należy jednak podkreślić, że nie był to pierwszy znany przykład obiektów samopodobnych i nieskończenie złożonych, które dziś określamy mianem fraktali. Już wcześniej pojawiały się konstrukcje o podobnych własnościach (jak krzywa Kocha czy zbiór Cantora), jednak to właśnie zbiór Mandelbrota stał się ich najbardziej rozpoznawalnym i ikonograficznym przedstawieniem.

W niniejszym rozdziale nie będziemy jednak skupiać się na historii fraktali. Zamiast tego potraktujemy zbiór Mandelbrota jako ciekawy obiekt matematyczny, który — przy pozornie prostym wzorze iteracyjnym — ujawnia bardzo złożoną strukturę. Głównym celem będzie pokazanie, w jaki sposób logarytmy pojawiają się w tym temacie „naturalnie” (wynikając z dynamiki iteracji), a następnie jak mogą zostać użyte także w celach czysto wizualizacyjnych, aby wydobyć detale niewidoczne przy prostym, liniowym mapowaniu wartości na kolory.

Zbiór Mandelbrota

Zbiór Mandelbrota tworzą te punkty \({\displaystyle p\in \mathbb{C}}\) , dla których ciąg \(( z_i)^{\infty}_{i=0}\) zdefiniowany równaniem rekurencyjnym:

\[ {\displaystyle {\begin{cases}z_{0}=0,\\z_{n+1}=z_{n}^{2}+p.\end{cases}}} \]

nie dąży do nieskończoności, tzn.:

\[ {\displaystyle \lim _{n\to \infty }z_{n}\neq \infty .} \]

Można wykazać, że warunek ten jest równoważny znacznie prostszemu kryterium obliczeniowemu:

\[ \forall_{n \in \mathbb{N}} \quad |z_n| < R \]

gdzie \(R > 0\) jest ustalonym promieniem ucieczki (w praktyce niemal zawsze przyjmuje się \(R=2\)).

Z punktu widzenia teorii fraktali właściwym fraktalem nie jest cały zbiór Mandelbrota, lecz jego brzeg — a ściślej: obszar płaszczyzny zespolonej, w którym zachowanie ciągu \(z_n\) zmienia się w sposób skrajnie wrażliwy na położenie punktu \(p\).

W obliczeniach numerycznych wprowadza się pojęcie indeksu ucieczki \(n_{\mathrm{esc}}\), zdefiniowanego jako

\[ n_{\mathrm{esc}} = \min\{ n \in \mathbb{N} : |z_{n+1}| > R \}. \]

Jest to wielkość dyskretna, która informuje, po ilu iteracjach orbita punktu \(p\) przekroczyła promień ucieczki. Na jej podstawie można konstruować wizualizacje zbioru Mandelbrota, przypisując kolor każdemu punktowi w zależności od wartości \(n_{\mathrm{esc}}\).

Obszar obliczeń

W dalszych rozważaniach ograniczamy się do prostokątnego obszaru płaszczyzny zespolonej

\[ \mathcal{D} = \left\{ z \in \mathbb{C} \;\middle|\; \operatorname{Re}(z) \in [-2,\,1] \;\land\; \operatorname{Im}(z) \in [-1.2,\,1.2] \right\}. \]

Obszar ten jest dyskretyzowany do regularnej siatki punktów, a dla każdego z nich obliczana jest orbita ciągu \(z_n\) przy maksymalnej liczbie iteracji równej 400.

Obszar \(\mathcal{D}\) ostanie zdyskretyzowany do siatki 1000×800 punktów, a dla każdego z nich sprawdzimy zachowanie iteracji \(z_{n+1}=z_n^2+p\) przy maksymalnej liczbie iteracji równej 400. Jeżeli dla danego punktu nastąpi „ucieczka”, tzn. \(|z_n|>2\), to zapamiętamy indeks ucieczki \(n_{\mathrm{esc}} = n-1\).

Rozkład indeksu ucieczki

Na początek przyjrzyjmy się rozkładowi uzyskanych wartości \(n_{\mathrm{esc}}\).

df <- tibble(
  n_esc = as.vector(m$esc),
  nu = as.vector(m$nu))


df %>%
  ggplot(aes(n_esc)) +
  geom_density() +
  labs(
    title = "Rozkład indeksu ucieczki w obszarze obliczeń",
    x = expression(n[esc]),
    y = "Gęstość"
  )

W rozkładzie widać dwa charakterystyczne maksima. Pierwsze występuje w wąskim przedziale do około 10 iteracji — odpowiada ono punktom, które bardzo szybko „uciekają” poza promień 2. Drugie maksimum pojawia się na końcu skali, przy wartości 400, i reprezentuje punkty, które (przy przyjętym limicie iteracji) nie uciekły; praktycznie oznacza to punkty należące do zbioru Mandelbrota lub bardzo bliskie jego brzegu.

Dla lepszego zrozumienia pierwszego maksimum obejrzyjmy rozkład ograniczony do zakresu od 1 do 10 iteracji.

df %>%
  filter(n_esc < 11) %>%
  ggplot(aes(n_esc)) +
  geom_density() +
  scale_x_continuous(breaks = 0:10) +
  labs(
    title = expression(paste("Rozkład ", n[esc], " dla wczesnych ucieczek (1–10 iteracji)")),
    x = expression(n[esc]),
    y = "Gęstość"
  )

Jak widać, jest to rozkład dyskretny, z lokalnymi maksimami przypadającymi na wartości całkowite — czego należało oczekiwać, ponieważ \(n_{\mathrm{esc}}\) jest indeksem iteracji.

W kolejnych krokach zbudujemy paletę barw powiązaną z \(n_{\mathrm{esc}}\):

Tak przygotowaną paletę zmapujemy na uzyskany z obliczeń indeks ucieczki.

pal <- c(
  grDevices::colorRampPalette(c("white", "dodgerblue3"))(10*4096/200), 
  grDevices::colorRampPalette(c("dodgerblue3", "gold", "black"))(190*4096/200)
)


plot_mandel(m1, pal, main = "Manelbrot example - n_esc")

Efekt jest atrakcyjny, ale ma kilka istotnych ograniczeń.

Po pierwsze, \(n_{esc}\) jest wielkością dyskretną. Gdy mapujemy ją liniowo na barwy, otrzymujemy charakterystyczne, wyraźne pasy kolorów. Po drugie, w interesującym nas obszarze — tuż za granicą zbioru Mandelbrota — zachodzi bardzo duża zmienność, ale w mapowaniu liniowym często „ściska się” ona w wąskim fragmencie palety.

Promień ucieczki i interpolacja iteracji

Warto zauważyć, że samo stwierdzenie \(|z_n| > R\) mówi jedynie, że ucieczka nastąpiła „pomiędzy” iteracją \(n-1\) a \(n\). To sugeruje naturalną drogę do uciąglenia wyniku: zamiast traktować ucieczkę jako zdarzenie dyskretne, możemy spróbować oszacować „ułamek iteracji”, w którym przekroczenie progu rzeczywiście nastąpiło.

W literaturze stosuje się w tym celu tzw. indeks ucieczki \(\nu\) (czasem określany jako smooth coloring). Intuicja jest następująca:

\[ |z_{n+1}| \approx |z_n|^2 \]

Po zlogarytmowaniu:

\[ \log|z_{n+1}| \approx 2\log|z_n| \]

A po kolejnym logarytmowaniu:

\[ \log(\log|z_{n+1}|) \approx \log 2 + \log(\log|z_n|) \]

Widzimy, że w przestrzeni \(\log(\log|z|)\) przyrost na iterację jest w przybliżeniu stały (o wartość \(\log 2\)). To prowadzi do wzoru na ciągły indeks ucieczki:

\[ \nu = n + 1 - \frac{\log(\log|z_n|)}{\log 2} \]

gdzie \(n\) to iteracja, w której po raz pierwszy przekroczono \(R\), a \(z_n\) to wartość z tej iteracji.

Pora sprawdzić jak zmieni się nasz przykładowy zbiór Mandelbrota, kiedy przypiszemy kolory z naszej palety nie do \(n_{esc}\) ale do \(\nu\).

plot_mandel(m, pal, main = "Manelbrot example - ν")

Kolorowe pasy tak widoczne na pierwszym wykresie zbioru Mandelbrota już wyraźnie zaniknęły a przejścia pomiędzy kolorami stały się płynne. Niestety to co najbardziej zachwycające w tym fraktalu, czyli obszar tuż za granicą zbioru Mandelbrota, bardzo szybko przechodzi od czerni do jasno niebieskiego prawie całkowicie zatracając kolor złoty obecny przecież w naszej palecie kolorów. Pora więc jeszcze raz sięgnąć po logarytmy.

Logarytmy w wizualizacji

Jak możemy sobie przypomnieć z wcześniejszego rozdziału, rozkład uzyskanych wartości charakteryzował się dwoma maksimami, pomiędzy którymi — wydawać by się mogło — niewiele się działo. To jednak było bardzo mylące. W rzeczywistości przedział pomiędzy tymi maksimami jest najbardziej wizualnie ciekawym fragmentem płaszczyzny zespolonej. Niestety kolorując to paletą kolorów z liniowym przejściem pomiędzy barwami otrzymujemy efekt, który nie pozwala nam dostrzec tych subtelnych niuansów tuż za granicami. Co prawda nasza paleta nie była idealnie liniowa, a raczej składała się z dwóch liniowych obszarów (pierwsze 5% oraz pozostałymi 95%). Jednak był to celowy zabieg wprowadzony do lepszego zaprezentowania problemu skokowego przechodzenia wartości.

Aby więc wydobyć te wszystkie subtelne różnice, które zachodzą w tym obszarze, musimy zmienić sposób mapowania wartości na kolory. Na tym etapie logarytm nie wynika już z dynamiki iteracji (jak w wyprowadzeniu \(\nu\)), lecz pełni rolę czysto wizualizacyjną: wprowadza nieliniową kompresję zakresu wartości, dzięki czemu więcej „miejsca” w palecie kolorów przypada na obszar tuż za granicą zbioru.

W praktyce oznacza to, że z ciągłej wartości \(\nu\) tworzymy nową zmienną \(\log(\nu)\), która lepiej rozkłada się w interesującym nas zakresie. Warto podkreślić, że po tej transformacji \(\log(\nu)\) nie ma już interpretacji liczby iteracji — jest wyłącznie parametrem sterującym percepcją kontrastu i detalu.

Zobaczmy jednak na początku, jak taka transformacja wpłynie na rozkład wyników.

df %>%
  ggplot(aes(log(nu))) +
  geom_density() +
  labs(
    title = "Rozkład wartości log(ν) w obszarze obliczeńń",
    x = expression(log(nu)),
    y = "Gęstość"
  )

Jak widać, po zlogarytmowaniu \(\nu\), na wykresie gęstości bardzo wiele „się dzieje” w prawie całym zakresie wartości. To jest dokładnie efekt, na którym nam zależy: rozciągamy (w sensie wizualnym) obszary, które wcześniej były „ściśnięte” w wąskim zakresie i przez to praktycznie zlewały się do jednego koloru.

Zobaczmy więc na koniec, jak wpłynie to na wizualizację naszego fraktala.

m2 = m
m2$nu = log(m2$nu)
plot_mandel(m2, pal, main = "Manelbrot example - log(ν)")

Zlogarytmowanie wartości pozwoliło na wydobycie znacznie większej ilości szczegółów. Oczywiście to jest tylko efekt wizualny i mocno uzależniony od gustu odbiorcy, a o gustach się przecież nie dyskutuje. Jak można się jednak było przekonać, logarytmy mogą wpływać nawet na tak nieuchwytne i niematematyczne wrażenia zmysłowe ludzkiego oka.