Il metodo della spirale aurea
Hai detto che non riuscivi a far funzionare il metodo della spirale aurea ed è un peccato perché è davvero, davvero buono. Vorrei darti una comprensione completa in modo che forse tu possa capire come evitare che questo sia "ammucchiato".
Quindi ecco un modo veloce e non casuale per creare un reticolo approssimativamente corretto; come discusso sopra, nessun reticolo sarà perfetto, ma questo potrebbe essere abbastanza buono. Viene confrontato con altri metodi, ad esempio in BendWavy.org, ma ha solo un aspetto gradevole e carino, oltre a una garanzia di spaziatura uniforme nel limite.
Primer: spirali di girasole sul disco dell'unità
Per comprendere questo algoritmo, ti invito prima a guardare l'algoritmo della spirale di girasole 2D. Ciò si basa sul fatto che il numero più irrazionale è la sezione aurea (1 + sqrt(5))/2
e se si emettono punti con l'approccio "stare al centro, girare una sezione aurea di intere curve, quindi emettere un altro punto in quella direzione", si costruisce naturalmente un spirale che, man mano che si arriva a numeri sempre più alti di punti, rifiuta tuttavia di avere 'barre' ben definite su cui i punti si allineano. (Nota 1.)
L'algoritmo per la spaziatura uniforme su un disco è,
from numpy import pi, cos, sin, sqrt, arange
import matplotlib.pyplot as pp
num_pts = 100
indices = arange(0, num_pts, dtype=float) + 0.5
r = sqrt(indices/num_pts)
theta = pi * (1 + 5**0.5) * indices
pp.scatter(r*cos(theta), r*sin(theta))
pp.show()
e produce risultati che assomigliano a (n = 100 en = 1000):
Distanziare i punti radialmente
La cosa strana chiave è la formula r = sqrt(indices / num_pts)
; come sono arrivato a quello? (Nota 2.)
Bene, sto usando la radice quadrata qui perché voglio che abbiano una spaziatura uniforme attorno al disco. Ciò equivale a dire che nel limite di N grande voglio che una piccola regione R ∈ ( r , r + d r ), Θ ∈ ( θ , θ + d θ ) contenga un numero di punti proporzionale alla sua area, che è r d r d θ . Ora, se pretendiamo di parlare di una variabile casuale qui, questa ha un'interpretazione semplice come dire che la densità di probabilità congiunta per ( R , Θ ) è semplicemente crper qualche costante c . La normalizzazione sull'unità disco forzerebbe quindi c = 1 / π.
Ora lascia che ti presenti un trucco. Deriva dalla teoria della probabilità in cui è noto come campionamento della CDF inversa : supponiamo di voler generare una variabile casuale con una densità di probabilità f ( z ) e di avere una variabile casuale U ~ Uniforme (0, 1), proprio come esce da random()
nella maggior parte dei linguaggi di programmazione. Come fai a fare questo?
- Innanzitutto, trasforma la tua densità in una funzione di distribuzione cumulativa o CDF, che chiameremo F ( z ). Un CDF, ricorda, aumenta monotonicamente da 0 a 1 con la derivata f ( z ).
- Quindi calcola la funzione inversa del CDF F -1 ( z ).
- Scoprirai che Z = F -1 ( U ) è distribuito in base alla densità target. (Nota 3).
Ora il trucco della spirale della sezione aurea distanzia i punti in uno schema piacevolmente uniforme per θ quindi integriamolo; per l'unità disco ci rimane con F ( r ) = r 2 . Quindi la funzione inversa è F -1 ( u ) = u 1/2 , e quindi genereremmo punti casuali sul disco in coordinate polari conr = sqrt(random()); theta = 2 * pi * random()
.
Ora invece di campionare casualmente questa funzione inversa, la stiamo campionando uniformemente , e la cosa bella del campionamento uniforme è che i nostri risultati su come i punti sono distribuiti nel limite del grande N si comporteranno come se lo avessimo campionato casualmente. Questa combinazione è il trucco. Invece di random()
usiamo (arange(0, num_pts, dtype=float) + 0.5)/num_pts
, in modo che, diciamo, se vogliamo campionare 10 punti, sono r = 0.05, 0.15, 0.25, ... 0.95
. Campioniamo uniformemente r per ottenere una spaziatura di area uguale e utilizziamo l'incremento del girasole per evitare terribili "barre" di punti nell'output.
Ora facendo il girasole su una sfera
I cambiamenti che dobbiamo fare per punteggiare la sfera di punti implicano semplicemente la sostituzione delle coordinate polari con coordinate sferiche. La coordinata radiale ovviamente non entra in questo perché siamo su una sfera unitaria. Per mantenere le cose un po 'più coerenti qui, anche se sono stato addestrato come fisico, userò le coordinate dei matematici in cui 0 ≤ φ ≤ π è la latitudine che scende dal polo e 0 ≤ θ ≤ 2π è la longitudine. Quindi la differenza da sopra è che stiamo sostanzialmente sostituendo la variabile r con φ .
Il nostro elemento area, che era r d r d θ , ora diventa il non molto più complicato sin ( φ ) d φ d θ . Quindi la nostra densità congiunta per una spaziatura uniforme è sin ( φ ) / 4π. Integrando θ , troviamo f ( φ ) = sin ( φ ) / 2, quindi F ( φ ) = (1 - cos ( φ )) / 2. Invertendo questo possiamo vedere che una variabile casuale uniforme apparirebbe come acos (1 - 2 u ), ma campioniamo in modo uniforme invece che in modo casuale, quindi usiamo invece φ k = acos (1 - 2 ( k+ 0,5) / N ). E il resto dell'algoritmo lo proietta solo sulle coordinate x, yez:
from numpy import pi, cos, sin, arccos, arange
import mpl_toolkits.mplot3d
import matplotlib.pyplot as pp
num_pts = 1000
indices = arange(0, num_pts, dtype=float) + 0.5
phi = arccos(1 - 2*indices/num_pts)
theta = pi * (1 + 5**0.5) * indices
x, y, z = cos(theta) * sin(phi), sin(theta) * sin(phi), cos(phi);
pp.figure().add_subplot(111, projection='3d').scatter(x, y, z);
pp.show()
Anche in questo caso per n = 100 en = 1000 i risultati sono:
Ulteriore ricerca
Volevo ringraziare il blog di Martin Roberts. Nota che sopra ho creato un offset dei miei indici aggiungendo 0,5 a ciascun indice. Questo è stato solo visivamente attraente per me, ma si scopre che la scelta dell'offset è molto importante e non è costante nell'intervallo e può significare ottenere fino all'8% di precisione nell'imballaggio se scelto correttamente. Dovrebbe esserci anche un modo per fare in modo che la sua sequenza R 2 ricopra una sfera e sarebbe interessante vedere se anche questo ha prodotto una bella copertura uniforme, forse così com'è ma forse deve essere, diciamo, presa solo da metà l'unità quadrata tagliata in diagonale o giù di lì e si estende per ottenere un cerchio.
Appunti
Quelle "barre" sono formate da approssimazioni razionali a un numero, e le migliori approssimazioni razionali a un numero derivano dalla sua espressione di frazione continua, z + 1/(n_1 + 1/(n_2 + 1/(n_3 + ...)))
dove z
è un numero intero ed n_1, n_2, n_3, ...
è una sequenza finita o infinita di numeri interi positivi:
def continued_fraction(r):
while r != 0:
n = floor(r)
yield n
r = 1/(r - n)
Poiché la parte della frazione 1/(...)
è sempre compresa tra zero e uno, un intero grande nella frazione continua consente un'approssimazione razionale particolarmente buona: "uno diviso per qualcosa tra 100 e 101" è meglio di "uno diviso per qualcosa tra 1 e 2." Il numero più irrazionale è quindi quello che è 1 + 1/(1 + 1/(1 + ...))
e non ha approssimazioni razionali particolarmente buone; si può risolvere φ = 1 + 1 / φ moltiplicando per φ per ottenere la formula del rapporto aureo.
Per le persone che non hanno molta familiarità con NumPy, tutte le funzioni sono "vettorializzate", quindi sqrt(array)
è lo stesso di quello che potrebbero scrivere gli altri linguaggi map(sqrt, array)
. Quindi questa è un'applicazione componente per componente sqrt
. Lo stesso vale per la divisione per uno scalare o per l'addizione con gli scalari: quelli si applicano a tutti i componenti in parallelo.
La dimostrazione è semplice una volta che sai che questo è il risultato. Se chiedi qual è la probabilità che z < Z < z + d z , equivale a chiedere qual è la probabilità che z < F -1 ( U ) < z + d z , applica F a tutte e tre le espressioni notando che è una funzione monotonicamente crescente, quindi F ( z ) < U < F ( z + d z ), espandere il lato destro verso l'esterno per trovare F ( z ) + f (z ) d z , e poiché U è uniforme questa probabilità è solo f ( z ) d z come promesso.