O método da espiral dourada
Você disse que não conseguiria fazer o método da espiral dourada funcionar e isso é uma pena, porque é muito, muito bom. Eu gostaria de dar a você um entendimento completo disso, para que talvez você possa entender como evitar que isso seja "amontoado".
Portanto, aqui está uma maneira rápida e não aleatória de criar uma rede que seja aproximadamente correta; como discutido acima, nenhuma rede será perfeita, mas isso pode ser bom o suficiente. É comparado a outros métodos, por exemplo, em BendWavy.org, mas tem uma aparência bonita e bonita, bem como uma garantia de espaçamento uniforme no limite.
Primer: espirais de girassol no disco da unidade
Para entender esse algoritmo, primeiro convido você a examinar o algoritmo 2D da espiral do girassol. Isso se baseia no fato de que o número mais irracional é a proporção áurea (1 + sqrt(5))/2
e se alguém emitir pontos pela abordagem "fique no centro, gire uma proporção áurea de voltas inteiras, em seguida, emita outro ponto nessa direção", naturalmente se constrói um espiral que, conforme você chega a um número cada vez maior de pontos, se recusa a ter 'barras' bem definidas sobre as quais os pontos se alinham. (Nota 1.)
O algoritmo para espaçamento uniforme em um 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 produz resultados semelhantes a (n = 100 e n = 1000):
Espaçando os pontos radialmente
O mais estranho é a fórmula r = sqrt(indices / num_pts)
; como eu cheguei a esse? (Nota 2.)
Bem, estou usando a raiz quadrada aqui porque quero que eles tenham um espaçamento de área uniforme ao redor do disco. Isso é o mesmo que dizer que no limite de N grande eu quero uma pequena região R ∈ ( r , r + d r ), Θ ∈ ( θ , θ + d θ ) para conter um número de pontos proporcionais à sua área, que é r d r d θ . Agora, se fingirmos que estamos falando sobre uma variável aleatória aqui, isso tem uma interpretação direta, dizendo que a densidade de probabilidade conjunta para ( R , Θ ) é apenas crpara alguma constante c . A normalização no disco unitário então forçaria c = 1 / π.
Agora, deixe-me apresentar um truque. Vem da teoria da probabilidade, onde é conhecido como amostragem do CDF inverso : suponha que você queira gerar uma variável aleatória com uma densidade de probabilidade f ( z ) e você tem uma variável aleatória U ~ Uniforme (0, 1), assim como sai de random()
na maioria das linguagens de programação. Como você faz isso?
- Primeiro, transforme sua densidade em uma função de distribuição cumulativa ou CDF, que chamaremos de F ( z ). Um CDF, lembre-se, aumenta monotonicamente de 0 a 1 com a derivada f ( z ).
- Em seguida, calcule a função inversa F -1 ( z ) do CDF .
- Você descobrirá que Z = F -1 ( U ) é distribuído de acordo com a densidade alvo. (Nota 3).
Agora, o truque da espiral de proporção áurea espaça os pontos em um padrão bem uniforme para θ, então vamos integrar isso; para o disco unitário, ficamos com F ( r ) = r 2 . Portanto, a função inversa é F -1 ( u ) = u 1/2 e, portanto, geraríamos pontos aleatórios no disco em coordenadas polares com r = sqrt(random()); theta = 2 * pi * random()
.
Agora, em vez de amostrar aleatoriamente essa função inversa, estamos amostrando-a uniformemente , e o bom da amostragem uniforme é que nossos resultados sobre como os pontos são espalhados no limite de N grande se comportarão como se tivéssemos amostrado aleatoriamente. Essa combinação é o truque. Em vez de random()
usarmos (arange(0, num_pts, dtype=float) + 0.5)/num_pts
, de modo que, digamos, se quisermos amostrar 10 pontos eles são r = 0.05, 0.15, 0.25, ... 0.95
. Amostramos uniformemente r para obter espaçamento de área igual e usamos o incremento do girassol para evitar horríveis “barras” de pontos na saída.
Agora fazendo o girassol em uma esfera
As mudanças que precisamos fazer para pontilhar a esfera com pontos envolvem apenas trocar as coordenadas polares por coordenadas esféricas. A coordenada radial é claro não entra nisto porque estamos em uma esfera unitária. Para manter as coisas um pouco mais consistentes aqui, embora eu tenha sido treinado como um físico, usarei coordenadas matemáticas onde 0 ≤ φ ≤ π é a latitude descendo do pólo e 0 ≤ θ ≤ 2π é a longitude. Portanto, a diferença acima é que basicamente substituímos a variável r por φ .
Nosso elemento de área, que era r d r d θ , agora se torna o não muito mais complicado sen ( φ ) d φ d θ . Portanto, nossa densidade de junta para espaçamento uniforme é sin ( φ ) / 4π. Integrando fora de θ , encontramos f ( φ ) = sin ( φ ) / 2, assim F ( φ ) = (1 - cos ( φ )) / 2. Invertendo isso, podemos ver que uma variável aleatória uniforme se pareceria com acos (1 - 2 u ), mas nós amostramos uniformemente em vez de aleatoriamente, então usamos φ k = acos (1 - 2 ( k+ 0,5) / N ). E o resto do algoritmo está apenas projetando isso nas coordenadas x, y e z:
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()
Novamente, para n = 100 en = 1000, os resultados se parecem com:
Mais pesquisa
Eu queria dar uma mensagem para o blog de Martin Roberts. Observe que acima eu criei um deslocamento dos meus índices adicionando 0,5 a cada índice. Isso era apenas visualmente atraente para mim, mas descobri que a escolha do deslocamento é muito importante e não é constante ao longo do intervalo e pode significar obter até 8% a mais de precisão na embalagem, se escolhido corretamente. Também deve haver uma maneira de fazer com que sua sequência R 2 cubra uma esfera e seria interessante ver se isso também produziu uma boa cobertura uniforme, talvez como está, mas talvez precise ser, digamos, tirada de apenas metade de o quadrado da unidade é cortado na diagonal ou assim e esticado para obter um círculo.
Notas
Essas "barras" são formadas por aproximações racionais de um número, e as melhores aproximações racionais de um número vêm de sua expressão de fração contínua, z + 1/(n_1 + 1/(n_2 + 1/(n_3 + ...)))
onde z
é um inteiro e n_1, n_2, n_3, ...
é uma sequência finita ou infinita de inteiros positivos:
def continued_fraction(r):
while r != 0:
n = floor(r)
yield n
r = 1/(r - n)
Como a parte da fração 1/(...)
está sempre entre zero e um, um grande número inteiro na fração contínua permite uma aproximação racional particularmente boa: "um dividido por algo entre 100 e 101" é melhor do que "um dividido por algo entre 1 e 2." O número mais irracional é, portanto, aquele que é 1 + 1/(1 + 1/(1 + ...))
e não tem aproximações racionais particularmente boas; pode-se resolver φ = 1 + 1 / φ multiplicando por φ para obter a fórmula da razão áurea.
Para pessoas que não estão tão familiarizadas com o NumPy - todas as funções são "vetorizadas", de modo que sqrt(array)
é o mesmo que outras linguagens podem escrever map(sqrt, array)
. Portanto, este é um sqrt
aplicativo componente por componente . O mesmo também se aplica à divisão por um escalar ou adição com escalares - esses se aplicam a todos os componentes em paralelo.
A prova é simples quando você sabe que esse é o resultado. Se você perguntar qual é a probabilidade de z < Z < z + d z , isso é o mesmo que perguntar qual é a probabilidade de z < F -1 ( U ) < z + d z , aplicar F a todas as três expressões observando que é uma função monotonicamente crescente, portanto, F ( z ) < U < F ( z + d z ), expanda o lado direito para encontrar F ( z ) + f ( z ) d z , e como U é uniforme, essa probabilidade é apenas f ( z ) d z, conforme prometido.