Метод золотой спирали
Вы сказали, что не можете заставить работать метод золотой спирали, и это позор, потому что он действительно хорош. Я хотел бы дать вам полное представление об этом, чтобы, возможно, вы могли понять, как уберечь это от «скучивания».
Итак, вот быстрый и неслучайный способ создать приблизительно правильную решетку; как обсуждалось выше, ни одна решетка не будет идеальной, но этого может быть достаточно. Его сравнивают с другими методами, например, на BendWavy.org, но у него просто красивый и красивый вид, а также гарантия равного интервала в пределах.
Праймер: спирали подсолнечника на агрегатном диске
Чтобы понять этот алгоритм, я сначала предлагаю вам взглянуть на алгоритм 2D спирали подсолнечника. Это основано на том факте, что наиболее иррациональным числом является золотое сечение, (1 + sqrt(5))/2
и если кто-то излучает точки по принципу «встаньте в центре, поверните золотое сечение целых витков, а затем испустите еще одну точку в этом направлении», то естественно построить спираль, которая по мере того, как вы набираете все большее и большее количество точек, тем не менее отказывается иметь четко определенные «полосы», по которым выстраиваются точки. (Примечание 1.)
Алгоритм равномерного размещения на диске:
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()
и он дает результаты, которые выглядят (n = 100 и n = 1000):
Радиальное расположение точек
Ключевая странность - это формула r = sqrt(indices / num_pts)
; как я пришел к этому? (Заметка 2.)
Что ж, я использую здесь квадратный корень, потому что я хочу, чтобы они имели равномерный интервал вокруг диска. Это то же самое, что сказать, что в пределе большого N я хочу, чтобы небольшая область R ∈ ( r , r + d r ), Θ ∈ ( θ , θ + d θ ) содержала количество точек, пропорциональное ее площади, что есть r d r d θ . Теперь, если мы сделаем вид, что мы говорим здесь о случайной величине, это имеет прямую интерпретацию как утверждение, что совместная плотность вероятности для ( R , Θ ) равна crдля некоторой постоянной c . Нормализация на единичном круге тогда вынудила бы c = 1 / π.
Теперь позвольте мне представить трюк. Он исходит из теории вероятностей, где он известен как выборка обратного CDF : предположим, вы хотите сгенерировать случайную величину с плотностью вероятности f ( z ), и у вас есть случайная величина U ~ Uniform (0, 1), точно так же, как получается из random()
на большинстве языков программирования. Как ты делаешь это?
- Сначала превратите вашу плотность в кумулятивную функцию распределения или CDF, которую мы назовем F ( z ). Напомним, что CDF монотонно увеличивается от 0 до 1 с производной f ( z ).
- Затем вычислите обратную функцию CDF F -1 ( z ).
- Вы обнаружите, что Z = F -1 ( U ) распределяется в соответствии с целевой плотностью. (Заметка 3).
Теперь уловка со спиралью золотого сечения распределяет точки по хорошо равномерному шаблону для θ, так что давайте интегрировать это; для единичного круга остается F ( r ) = r 2 . Таким образом, обратная функция F -1 ( u ) = u 1/2 , и поэтому мы будем генерировать случайные точки на диске в полярных координатах с r = sqrt(random()); theta = 2 * pi * random()
.
Теперь вместо случайной выборки этой обратной функции мы ее выбираем равномерно , и хорошая вещь в равномерной выборке заключается в том, что наши результаты о том, как точки распределены в пределах большого N, будут вести себя так, как если бы мы выбрали ее случайным образом. Эта комбинация и есть уловка. Вместо того, random()
чтобы использовать (arange(0, num_pts, dtype=float) + 0.5)/num_pts
, так что, скажем, если мы хотим выбрать 10 точек, они есть r = 0.05, 0.15, 0.25, ... 0.95
. Мы единообразно выбираем r, чтобы получить равные интервалы площадей, и используем приращение подсолнечника, чтобы избежать ужасных «полос» точек на выходе.
Теперь делаем подсолнух на сфере
Изменения, которые нам нужно внести, чтобы расставить точки на сфере, просто включают в себя отключение полярных координат для сферических координат. Радиальная координата, конечно, сюда не входит, потому что мы находимся на единичной сфере. Для большей согласованности здесь, хотя я был физиком по образованию, я буду использовать координаты математиков, где 0 ≤ φ ≤ π - это широта, идущая от полюса, а 0 ≤ θ ≤ 2π - долгота. Таким образом, отличие от приведенного выше в том, что мы в основном заменяем переменную r на φ .
Наш элемент площади, который был r d r d θ , теперь становится не намного более сложным sin ( φ ) d φ d θ . Таким образом, наша плотность стыков для равномерного расстояния равна sin ( φ ) / 4π. Интегрируя θ , находим f ( φ ) = sin ( φ ) / 2, поэтому F ( φ ) = (1 - cos ( φ )) / 2. Обращая это, мы видим, что однородная случайная величина будет выглядеть как acos (1 - 2 u ), но мы делаем выборку равномерно, а не случайным образом, поэтому вместо этого мы используем φ k = acos (1 - 2 ( k+ 0,5) / N ). А остальная часть алгоритма просто проецирует это на координаты x, y и 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()
Снова для n = 100 и n = 1000 результаты выглядят так:
Дальнейшие исследования
Я хотел отдать должное блогу Мартина Робертса. Обратите внимание, что выше я создал смещение своих индексов, добавив 0,5 к каждому индексу. Это было просто визуально привлекательно для меня, но оказалось, что выбор смещения имеет большое значение и не является постоянным в течение интервала и может означать повышение точности упаковки на 8% при правильном выборе. Также должен быть способ заставить его последовательность R 2 покрывать сферу, и было бы интересно посмотреть, дает ли это также хорошее равномерное покрытие, возможно, как есть, но, возможно, нужно, чтобы быть, например, взято только из половины единичный квадрат разрезают по диагонали или около того и растягиваются, чтобы получить круг.
Ноты
Эти «полосы» образованы рациональными приближениями к числу, а наилучшие рациональные приближения к числу получаются из его выражения непрерывной дроби, z + 1/(n_1 + 1/(n_2 + 1/(n_3 + ...)))
где z
- целое число и n_1, n_2, n_3, ...
представляет собой конечную или бесконечную последовательность положительных целых чисел:
def continued_fraction(r):
while r != 0:
n = floor(r)
yield n
r = 1/(r - n)
Поскольку дробная часть 1/(...)
всегда находится между нулем и единицей, большое целое число в непрерывной дроби позволяет получить особенно хорошее рациональное приближение: «один, разделенный на что-то от 100 до 101» лучше, чем «один, разделенный на что-то между 1 и 2». Поэтому наиболее иррациональным числом является то, которое 1 + 1/(1 + 1/(1 + ...))
не имеет особенно хороших рациональных приближений; можно решить φ = 1 + 1 / φ , умножив на φ, чтобы получить формулу золотого сечения.
Для людей, которые не так хорошо знакомы с NumPy - все функции «векторизованы», так что sqrt(array)
это то же самое, что и другие языки map(sqrt, array)
. Итак, это приложение для каждого компонента sqrt
. То же самое справедливо и для деления на скаляр или сложения со скалярами - они применяются ко всем компонентам параллельно.
Доказательство становится простым, если вы знаете, что это результат. Если вы спросите, какова вероятность того, что z < Z < z + d z , это то же самое, что спросить, какова вероятность того, что z < F -1 ( U ) < z + d z , примените F ко всем трем выражениям, отметив, что это монотонно возрастающая функция, следовательно, F ( z ) < U < F ( z + d z ), разверните правую часть, чтобы найти F ( z ) + f( z ) d z , и поскольку U равномерно, эта вероятность равна f ( z ) d z, как и было обещано.