Contenuti

Julia Set in Julia

Immagina il caos. Ma cosa è il caos? Per molti il caos è impredicibilità. Per me no… per me il caos è semplicemente armonia.

Immagina un sistema dinamico così complesso che anche una piccolissima perturbazione può cambiare drasticamente il seguito degli eventi. Immagina una farfalla che con un battito d’ali crea genera un tornado dall’altra parte del mondo. Ecco questo è il caos! E' semplicemente una flusso estremamente complesso e intrecciato di eventi che si influenzano a vicenda.

Gli eventi si influenzano ed amalgamano, ma sempre seguendo le leggi che governano il nostro universo. Nulla di impredicibile. Solamente difficile da comprendere.

A mio parare, una prova che conferma l’idea appena proposta, è che qualsiasi sistema caotico genera sempre un qualcosa di estremamente armonico. Due fluidi con densità diverse che si mescolano seguendo correnti totalmente casuali, generano sempre una serie di vortici che danzano in maniera così armoniosa. Una galassia che è composta da un ammasso di pietre, gass ed espolosioni catastrofiche, forma una spirale di Fibonacci. Il contenuto di una memoria RAM può apparire a livello logico come una serie di lucine che si alternano freneticamente, eppure il software che fanno funzionare ci consente di vedere un’immagine.

L’uomo per nautra tende sempre a ricercare uno schema in ciò che osserva e in ciò che fa. E il fatto che nel caos non ne riusciamo a trovarne uno apparentemente prevedibile, subito lo cataloghiamo come impossibile da comprendere… caotico! Ma il fatto che non vediamo uno schema, non è detto che non vi sia.

Lo schema nel caos c’è e come. Lo schema del caos è l’armonia. L’armonia nel senso più naturale del suo termine. Senza che segua alcuna legge o struttura che noi definiamo “armonica”. Semplicemente armonia nel senso di bello a vedersi. Quel bello che ti da un senso di quiete quando lo guardi, nonstante il caos sottostante. Come quando piove: milioni di gocce d’acqua che cadono in posti totalmente casuali, bagnando tutto, muovendo le foglie e causando come un rumore o interferenza di sottofondo. Eppure, quel rumore, qulla totale casualità di onde sonore, nel complesso è gradevole, e ci calma dentro.

Quello che voglio porporre in questo post, è un esempio calzante di armonia nel caos, e lo farò con il Julia Set. Farò vedere come in un sistema complesso, delle piccolissime variazioni influiscono considerevolmente sullo stato finale degli eventi, e che nonstante tutta questa impredicibilità il riusltato è compunque un qualcosa di armonico (secondo la definizione data in precedenza).

Julia Set in Julia

Fissiamo una costante $c \in \mathbb{C}$, e consideriamo la seguente funzione $f_c : \mathbb{C} \to \mathbb{C}$ definita come segue $$ f_c(z): z^2 + c $$

1
2
3
4
const c = -0.79 + 0.15im

f(z::Complex) = z^2 + c
f(z::Real) = f(z + 0im)

Adesso consideriamo solamente una porzione del piano complesso. Consideriamo un quadrato centrato nell’origine, con coordinate $(-R,-R), (R,R)$.

Tale quadrato, può essere visto come il prodotto cartesiano delle due porzioni di retta $[ -R, R ] \times [ -R, R ] \subset \mathbb{C}$. Per semplicità, d’ora in poi ci riferiremo ad esso come $Q_R$.

Scegliamo appositamente $R$ in modo tale che il punto $c$ sia contenuto nel quadrato, ovvero tale che $\vert c \vert \leq \sqrt{2}R$. Per esempio, scegliamo $R=2$.

1
const R = 2

Ora fissiamo un interno $n \in \mathbb{N}$, e riapplichiamo $f_c$ $n$ volte su qualisiasi punto $z \in Q_R$. $$f_c(z) \\ f_c(f_c(z)) \\ f_c(f_c(f_c(z))) \\ \vdots \\ f_c( … (f_c(f_c(z))) … )$$ Indichiamo l'$i$-esima di queste applicazioni con $f^{(i)}_c$.

Un modo più comodo per vedere $f^{(n)}_c$ è sottoforma di successione $z_n$, del tipo

$$\begin{cases} z_0 &= z \\ z_{i+1} &= z_i^2 + c \end{cases} \;\; \forall 0 \leq i \leq n$$

Il Julia Set rispetto alla funzione $f_c$ e al parametro $R$, è definito come il seguente insieme $$ J_R(f_c, R) = { z \in [ -R, R ] \times [ -R, R ] : \forall 0 \leq i \leq n, f^{(i)}_c \in Q_R } $$

Cerchiamo di visualizzare come sono fatti gli elementi di questo insieme. L’insieme $J_R(f_c)$ è l’insieme di tutti quei $z$ di $Q_R$ tali che ogni elemento della sequenza $z_i$ appartiene ancora a $Q_R$. Ovvero, applicando $n$ volte la funzione su $z$, ognuga di queste volte otteniamo un punto del quadrato $Q_R$.

Mi sembra abbastanza evidente che il sistema è caotico. Infatti alla più piccola variazione dei parametri $c,R,n$, otterremo un Julia set totalmente differente.

Eppure... si cela dell'armonia dietro questo caos, ed io cercherò di mostrarvela.

Prima implementazione

Bando alle ciance, e vediamo un po' di codice!

La prima cosa che vogliamo fare è individuare quali punti del quadrato $Q_R$ dobbiamo inserire nel Julia set $J_r(f_c)$.

Partiamo con l’osservazione che tutti i punti $z$ di $Q_R$ formano un insieme più che numerabile, in poche parole (molto più che) infinito.

Dato che invece noi abbiamo solamente un tempo finito su questa terra, ci piacerebbe davvero tanto vedere la fine dell’esecuzione del nostro codice prima o poi.

Perciò il primo step è quello di discretizzare i punti di $Q_R$. Ovvero dobbiamo considerare solamente un sottoinsieme finito di punti di $Q_R$, con una granularità dale da poter controllare il tempo di esecuzione del nostro codice.

Perciò definiamo un fattore di granularità $\varepsilon$. Ovvero consideriamo solamente i punti del piano complesso della forma

$$\hat{z} = \underbrace{(-R + k_1 \varepsilon)}_x + i \underbrace{(-R + k_2 \varepsilon)}_y$$

per ogni coppia di interi $k_1, k_2 \in \mathbb{N}$ tali che il punto $\hat{z}$ appartiene ancora al quadrato $Q_R$.

A questo punto, per scorrere tutti i punti, facciamo uso dell’arma più pontente del programmatore medio:

Il doppio ciclo for!
1
2
3
4
5
6
7
8
const ε = 0.016

for x=-R:ε:R
  for y=-R:ε:R
    z::Complex = complex(y,x)
    # ...
  end
end

A questo punto, dobbiamo applicare $n$ volte la funzione $f_c$ per ognuno dei punti nel ciclo, e vedere se tutte ed $n$ le volte $f_c$ ritorna un punto appartenente al quadrato $Q_R$.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
for x=-R:ε:R
  for y=-R:ε:R
    z::Complex = complex(y,x)
    
    i = 1
    n = 100

    while abs(z) < (2)*R && (i += 1) < n
      z = f(z)
    end
    # ...
  end
end

Usciremo dal ciclo while in due casi:

  1. se siamo risuciti ad applicare tutte ed $n$ le volte la funzione $f_c$ al punto z in questione. In tal caso possiamo dire che $z \in J_R(f_c)$.
  2. se a un certo punto la funzione $f_c$ ci restituisce un punto che non appartiene al quadrato $Q_R$. In tal caso possiamo dire che $z \not\in J_R(f_c)$.

Perciò, effettuando un semplice controllo sul contatore i, possiamo dire se z è o non è un punto del Julia set. In caso positivo stampiamo un carattere, altrimenti stampiamo uno spazio vuoto (ricordiamoci di andare a capo alla fine di ogni riga).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
for x=-R:ε:R
  for y=-R:ε:R
    z::Complex = complex(y,x)

    i = 1
    n = 100

    while abs(z)  (2)*R && (i += 1) < n
      z = f(z)
    end
    
    if i == n
      print("#")
    else
      print(" ")
    end

  end
  println()
end

Alla fine eseguendo il nostro porgramma (che finirà in un tempo finito) verrà stampato sul nostro terminale qualcosa del genere

Bello vero?

Scherzo, zoommate abbastanza indietro con crtl - e vedrete qualcosa di un po' meno brutto.

Cerchiamo ora di rendere visualizzare questo insieme in una maniera un po' più carina.

Implementazione più carina

Come possiamo osservare, l’output del nostro primito programma ricorda un po' l’immagine di preview di questo post. Però l’immagine di preview ha tanti colori e tante sfumature. Soffermiamoci sulle sfumature.

Così come lo abbiamo implementato noi, stampiamo a schermo il carattere # se un punto $z$ appartiene al nostro Julia set, altrimenti stampiamo uno spazio vuoto.

Invece di essere così drastici, cerchiamo di qunatificare il livello di appartenenza di un punto al nostro insieme.

Come fare?

Semplice, contiamo quante volte riusciamo ad applicare $f_c$ prima di fuori uscire dal quadrato $Q_R$ durante il ciclo while. In questa maniera tutti i punti hanno una opportunità di essere visualizzati a schermo.

Per ottenere diverse sfumature, sarebbe sensato visualizzare con maggiore intensità i punti che riescono a sopravvivere per maggior tempo nel ciclo while (ovvero quei punti che “appartengono di più” al Julia set) e con meno intensità quelli che invece sopravvivono meno (ovvero quei punti che “appartengono di meno” al Julia set).

A questo punto ti starai domandando:

Come fare a rendere questo effetto solamente coi ratarreti ASCII? È impossibile!

Se fosse stato impossibile mi sarei fermato alla sezione precedente, non credi?

L’idea è più semplice di quanto tu possa pensare: se un punto appartiene “tanto” al Julia set, allora stampiamo un carratere “più luminoso”, altrimenti ne stampiamo uno “meno luminoso”.

Furbo vero?

La domanda è ora

Si ma quando un carattere ASCII è luminoso?

Dato che io stampo dei caratteri bianchi su uno sfondo nero, semplicemente dico che un carattere ascii è luminoso se è composto da tanti pixel.

Perciò certamente uno spazio vuoto può rappresentare il colore nero, il carattere . (punto) rappresenta un colore quasi nero, il carattere + un grigio, e il carattere @ il bianco. Ovviamente la scelta dei caratteri è totalmente arbitraria, purché siano ordinati in maniera crescente (o decrescente se hai il tema chiaro) di luminosità.

La sequenza di caratteri che ho usato io è la seguente.

1
const symbols = " .:-=+*#%\$@"

A questo punto, usciti dal ciclo while basterà mappare l’indice i dall’intervallo $[ 1,n ]$, all’intervallo $[ 1,11 ]$. L’intervallo $[ 1,11 ]$ perché sono gli indici della stringa symbols (e perché Julia, il linguaggio che sto usando, inizia a contare da 1 e non da 0).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const symbols = " .:-=+*#%\$@"

for x=-R:ε:R
  for y=-R:ε:R
    z::Complex = complex(y,x)

    i = 1
    n = 100

    while abs(z)  (2)*R && (i += 1) < n
      z = f(z)
    end
    print(symbols[convert(Int64, ceil(i/11))])
  end
  println()
end

L’output sarà il seguente

c = -0.79 + i0.15

c = -0.54 + i0.54

c = 0.355 + i0.355


Codice

Puoi trovare il codice completo con alcuni esempi su questa repository, oppure se sei pigro beccati questo gist (ora che ho imparato cosa sono e come farli)


Osservazioni

Scrivere questo post mi ha fatto un po' riflettere. Ho realizzato che siamo circodani dal caos. Tutto è caos. La scrivania dalla quale sto scrivendo ora è il regno del caos. Ma d’ora in poi, a chiunque mi accuserà di avere una scrivania troppo disordinata risponderò

No, la mia scrivania non è caotica.
La mia scrivania è armonica.