La méthode de la spirale d'or
Vous avez dit que vous ne pouviez pas faire fonctionner la méthode de la spirale dorée et c'est dommage parce que c'est vraiment très bon. Je voudrais vous donner une compréhension complète de cela afin que vous puissiez peut-être comprendre comment éviter que cela ne soit «groupé».
Voici donc un moyen rapide et non aléatoire de créer un treillis qui est approximativement correct; comme indiqué ci-dessus, aucun réseau ne sera parfait, mais cela peut être suffisant. Il est comparé à d'autres méthodes, par exemple sur BendWavy.org, mais il a juste un joli et joli look ainsi qu'une garantie d'espacement uniforme dans la limite.
Primer: spirales de tournesol sur le disque de l'unité
Pour comprendre cet algorithme, je vous invite d'abord à vous pencher sur l'algorithme 2D de la spirale de tournesol. Ceci est basé sur le fait que le nombre le plus irrationnel est le nombre d'or (1 + sqrt(5))/2
et si l'on émet des points par l'approche «se tenir au centre, tourner un nombre d'or de tours entiers, puis émettre un autre point dans cette direction», on construit naturellement un spirale qui, à mesure que vous atteignez des nombres de points de plus en plus élevés, refuse néanmoins d'avoir des «barres» bien définies sur lesquelles les points s'alignent. (Note 1.)
L'algorithme pour l'espacement pair sur un disque est,
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()
et il produit des résultats qui ressemblent à (n = 100 et n = 1000):
Espacement radial des points
La principale chose étrange est la formule r = sqrt(indices / num_pts)
; comment suis-je arrivé à celui-là? (Note 2.)
Eh bien, j'utilise la racine carrée ici parce que je veux que celles-ci aient un espacement de zone uniforme autour du disque. Cela revient à dire que dans la limite du grand N je veux qu'une petite région R ∈ ( r , r + d r ), Θ ∈ ( θ , θ + d θ ) contienne un nombre de points proportionnel à son aire, qui est r d r d θ . Maintenant, si nous prétendons parler ici d'une variable aléatoire, cela a une interprétation simple comme disant que la densité de probabilité conjointe pour ( R , Θ ) est juste crpour une certaine constante c . La normalisation sur le disque unité forcerait alors c = 1 / π.
Maintenant, laissez-moi vous présenter une astuce. Cela vient de la théorie des probabilités où il est connu sous le nom d' échantillonnage du CDF inverse : supposons que vous vouliez générer une variable aléatoire avec une densité de probabilité f ( z ) et que vous ayez une variable aléatoire U ~ Uniform (0, 1), tout comme sort de random()
dans la plupart des langages de programmation. Comment est-ce que tu fais ça?
- Tout d'abord, transformez votre densité en une fonction de distribution cumulative ou CDF, que nous appellerons F ( z ). Un CDF, rappelez-vous, augmente de façon monotone de 0 à 1 avec le dérivé f ( z ).
- Calculez ensuite la fonction inverse F -1 ( z ) du CDF .
- Vous constaterez que Z = F -1 ( U ) est distribué en fonction de la densité cible. (Note 3).
Maintenant, l'astuce en spirale du nombre d'or espace les points dans un motif bien uniforme pour θ alors intégrons cela; pour le disque unitaire, on se retrouve avec F ( r ) = r 2 . Donc, la fonction inverse est F -1 ( u ) = u 1/2 , et donc nous générerions des points aléatoires sur le disque en coordonnées polaires avec r = sqrt(random()); theta = 2 * pi * random()
.
Maintenant, au lieu d' échantillonner au hasard cette fonction inverse, nous l' échantillonnons uniformément , et la bonne chose à propos de l'échantillonnage uniforme est que nos résultats sur la façon dont les points sont répartis dans la limite d'un grand N se comporteront comme si nous l'avions échantillonné au hasard. Cette combinaison est le truc. Au lieu de random()
nous utiliser (arange(0, num_pts, dtype=float) + 0.5)/num_pts
, de sorte que, disons, si nous voulons échantillonner 10 points, ils le sont r = 0.05, 0.15, 0.25, ... 0.95
. Nous échantillonnons uniformément r pour obtenir un espacement de surface égale, et nous utilisons l'incrément de tournesol pour éviter de terribles «barres» de points dans la sortie.
Maintenant en train de faire le tournesol sur une sphère
Les changements que nous devons apporter pour doter la sphère de points impliquent simplement de remplacer les coordonnées polaires par des coordonnées sphériques. La coordonnée radiale n'entre bien sûr pas dans cela car nous sommes sur une sphère unitaire. Pour garder les choses un peu plus cohérentes ici, même si j'ai été formé en tant que physicien, j'utiliserai les coordonnées des mathématiciens où 0 ≤ φ ≤ π est la latitude descendant du pôle et 0 ≤ θ ≤ 2π est la longitude. Donc, la différence par rapport à ci-dessus est que nous remplaçons fondamentalement la variable r par φ .
Notre élément d'aire, qui était r d r d θ , devient maintenant le sin ( φ ) d φ d θ pas beaucoup plus compliqué . Donc, notre densité conjointe pour un espacement uniforme est sin ( φ ) / 4π. En intégrant θ , nous trouvons f ( φ ) = sin ( φ ) / 2, donc F ( φ ) = (1 - cos ( φ )) / 2. En inversant cela, nous pouvons voir qu'une variable aléatoire uniforme ressemblerait à acos (1 - 2 u ), mais nous échantillonnons uniformément plutôt que de manière aléatoire, donc nous utilisons à la place φ k = acos (1 - 2 ( k+ 0,5) / N ). Et le reste de l'algorithme projette simplement ceci sur les coordonnées x, y et 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()
Encore une fois pour n = 100 et n = 1000, les résultats ressemblent à:
De plus amples recherches
Je voulais crier au blog de Martin Roberts. Notez que ci-dessus j'ai créé un offset de mes indices en ajoutant 0,5 à chaque index. C'était juste visuellement attrayant pour moi, mais il s'avère que le choix du décalage compte beaucoup et n'est pas constant sur l'intervalle et peut signifier obtenir jusqu'à 8% de meilleure précision dans l'emballage s'il est choisi correctement. Il devrait également y avoir un moyen de faire en sorte que sa séquence R 2 couvre une sphère et il serait intéressant de voir si cela produisait également une belle couverture uniforme, peut-être telle quelle mais peut-être devant être, par exemple, prise à partir de seulement la moitié de l'unité carrée coupée en diagonale environ et étirée pour former un cercle.
Remarques
Ces «barres» sont formées par des approximations rationnelles d'un nombre, et les meilleures approximations rationnelles d'un nombre proviennent de son expression de fraction continue, z + 1/(n_1 + 1/(n_2 + 1/(n_3 + ...)))
où z
est un entier et n_1, n_2, n_3, ...
est une séquence finie ou infinie d'entiers positifs:
def continued_fraction(r):
while r != 0:
n = floor(r)
yield n
r = 1/(r - n)
Comme la partie de fraction 1/(...)
est toujours comprise entre zéro et un, un grand entier dans la fraction continue permet une approximation rationnelle particulièrement bonne: "un divisé par quelque chose entre 100 et 101" est meilleur que "un divisé par quelque chose entre 1 et 2." Le nombre le plus irrationnel est donc celui qui n'est 1 + 1/(1 + 1/(1 + ...))
et n'a pas d'approximations rationnelles particulièrement bonnes; on peut résoudre φ = 1 + 1 / φ en multipliant par φ pour obtenir la formule du nombre d'or.
Pour les gens qui ne sont pas si familiers avec NumPy - toutes les fonctions sont «vectorisées», donc c'est sqrt(array)
la même chose que ce que d'autres langages pourraient écrire map(sqrt, array)
. Il s'agit donc d'une application composant par composant sqrt
. Il en va de même pour la division par un scalaire ou l'addition avec des scalaires - ceux-ci s'appliquent à tous les composants en parallèle.
La preuve est simple une fois que vous savez que c'est le résultat. Si vous demandez quelle est la probabilité que z < Z < z + d z , cela revient à demander quelle est la probabilité que z < F -1 ( U ) < z + d z , appliquez F aux trois expressions en notant que c'est une fonction monotone croissante, d'où F ( z ) < U < F ( z + d z ), étend le côté droit pour trouver F ( z ) + f( z ) d z , et comme U est uniforme, cette probabilité est juste f ( z ) d z comme promis.