Визуализация Пи, Тау и простых чисел

—

от автора


источник изображения

Возможно, вы видели предыдущий пост, где были предоставлены визуализации первых 1000 цифр $\pi, \tau$ и $\sqrt{2}$. Он возник в результате небольшого спора о том, лучше ли $\tau$, чем $\pi$. По этому поводу идут бесконечные дебаты, и я подумал, что могу пошутить по этому поводу. В этом посте я хочу показать, как создать визуализации, и надеюсь, что вы захотите попробовать удивительный пакет Luxor.jl после прочтения. Вчера я начал читать туториал, и это потрясающе! В прошлый раз визуализация делалась на Javascript, и я подумал, что этот аккуратный маленький проект сойдет, чтобы начать изучать Луксор. Как уже упоминалось в let me be your mentor: я думаю, что очень важно иметь такие маленькие проекты, чтобы освоить новый инструмент.

Основная идея

Я хотел воссоздать визуализацию, которую видел в Numberphile от Мартина Крживинского.

Там был круг (который, вполне ассоциируется и с $\pi$ и с $\tau$) разделенный на 10 сегментов, по одному для каждой цифры. Цифры нашего иррационального числа представляются кривыми внутри этого круга, так что 3.1415 (я начинаю с 14) — это кривая от сегмента 1 до сегмента 4, а затем обратно к 1, потом до 5 и так далее. Каждый раз мы перемещаемся немного по часовой стрелке в сегменте так, что 1→4 создает различные кривые (в зависимости от текущего положения, в котором мы находимся).

Потом надобавляем всякие фичи. Мы должны начать чувствовать себя комфортно с Луксором. Важно: не надо искать математическую интерпретацию — это просто небольшой проект визуализации 😉

Я знаю, вам интересно, как должен выглядеть конечный результат:

Начинаем

using Luxor  function vis()     @png begin         sethue("black")         circle(O, 50, :stroke)         setdash("dot")         circle(O, 70, :stroke)         sethue("darkblue")         circle(O, 10, :fill)      end 500 200 "./start.png" end

вызываем vis() и создаем файл start.png который будет выглядеть как-то так:

Давайте быстренько пройдемся по командам:

@png begin end width height "filename.png"

просто хороший макрос. 🙂

sethue задает цвет и принимает либо строку, как показано выше или цвет пакета из Colors. Он устанавливает цвет для следующих команд рисования до тех пор, пока вы не выберете другой. То же самое верно и при установке ширины линии с помощью setline, или при установке размера шрифта, или при других общих настройках.

Команды рисования, такие как circle, обычно принимают некоторые параметры и заканчиваются параметром действия, таким как :stroke или :fill.

О — это буква "О", а не число "0". 🙂 Она представляет собой начало координат и является краткой формой для Point(0, 0). В Луксоре начало находится в центре полотна. В качестве второго параметра должен быть задан радиус.

Давайте сначала нарисуем внешний круг и добавим цифры:

radius = 100 @png begin     background("black")      sethue("white")     circle(O, radius, :stroke)      for i in 0:9         θ = 2π*0.1*i+0.1*π         mid = Point(             radius*sin(θ),             -radius*cos(θ),         )         label(string(i), :N, mid)     end  end 700 300 "./first_step.png"

Первая часть должна быть достаточно простой.

θ = 2π*0.1*i+0.1*π

возможно, это не идеально написано (кроме того, я мог бы использовать $\tau$ :D). 2π*0.1*i начинает с северного положения, а затем для следующего i происходит перемещение на $36^\circ$. Я добавляю "0.1 π", потому что хочу переходить к середине каждого сегмента. Может быть, следует написать 0.5/10*2π. Затем мы просто поворачиваем наш холст и двигаясь чуть выше радиуса, рисуем метки. На самом деле такое можно проделать в Luxor, используя rotate и translate. Но я решил сделать вручную, так как мне все равно это пригодится позже. В общем формула такова:

$ \begin{aligned} x' &= x \cos (\theta) - y \sin(\theta) \\ y' &= x \sin (\theta) + y \cos(\theta) \\ \end{aligned} $

Такое преобразование поворачивает плоскость на $\theta$ и производит трансляцию на x,y. Поскольку я перевожу только на y, мне не нужно первое тождество. Помните, что y увеличивается, когда идет вниз.

В настоящее время есть две проблемы:

  • на самом деле нам не нужен круг, нам нужны дуги (сегменты) для каждой цифры
  • подписи не читаются

Команда label принимает три значения: текст, вращение и положение, где вращение может быть записано как :N,: E,: S,: W для севера, востока, юга, запада или как угол (в радианах). :N есть $-\frac{\pi}{2}$. Поэтому мы хотим начать с $ - \frac{\pi}{2}$, а потом добавлять текущий угол поворота. Кроме того, смещение было бы здорово, если бы оно не доставало непосредственно до окружности или не подходило слишком близко к ней. Здесь мы могли бы увеличить радиус или использовать ;offset в команде label.

Для первой задачи нам нужна функция arc2r, которая принимает три аргумента
c1, p1, p2 + действие: c1 — это центр окружности, а p1 и p2 — точки на окружности, между которыми должен быть показан сегмент. По умолчанию выбрано направление по часовой стрелке.

Мы определяем следующую функцию, чтобы получить $\theta$ и соответствующую точку более простым способом:

function get_coord(val, radius)     θ = 2π*0.1*val     return Point(         radius*sin(θ),         -radius*cos(θ),     ) end

а потом:

background("black")  for i in 0:9     from = get_coord(i, radius)     to = get_coord(i+1, radius)     randomhue()      θ = 2π*0.1*i+0.1*π     mid = Point(         radius*sin(θ),         -radius*cos(θ),     )     label(string(i), -π/2+θ, mid; offset=15)     move(from)     arc2r(O, from, to, :stroke) end

Я использовал randomhue, чтобы получить случайный цвет. Мы исправим это в следующий раз 🙂
Также я переставлял порядок Label и arc2r и поставил move, так как в противном случае линии рисуются от метки дуги. Это происходит потому, что arc продолжает текущий путь.

Выглядит намного лучше! Давайте возьмем несколько хороших цветов из Colorschemes.jl.

Я использовал схему rainbow, начиная с 7-го цвета :D. Вы, возможно, захотите испытать другие цветовые схемы, так как здесь цвета не так легко различить, но мне все равно почему-то нравится именно она.

using ColorSchemes colors = ColorSchemes.rainbow[7:end]

и затем

sethue(colors[i+1])

помните, что индексация массивов в Julia начинается с единицы.

Каковы следующие шаги?

  • Добавление строк
  • Рефакторинг кода
  • Оживление процесса
  • Добавление точек
  • Добавление гистограммы сверху

Я думаю, что визуально привлекательно иметь круг посередине, где мы можем добавить символ $\pi$ (или $\tau$) позже.
Поэтому мы не можем провести прямые линии от одного сегмента к другому. Для этого я использую квадратичные кривые Безье.

Давайте сначала получим цифры числа Пи:

max_digits = 10 digits = setprecision(BigFloat, Int(ceil(log2(10) * max_digits+10))) do     return parse.(Int, collect(string(BigFloat(pi))[3:max_digits+2])) end

это дает нам первые 10 цифр после десятичной точки числа Пи. Для этого мне нужно установить точность BigFloat. Довольно интересно, что пи не является жестко закодированной константой в Джулии. Оно вычислено таким образом, что я в принципе могу получить любую точность, какую захочу. Точность должна быть задана в количестве битов, так что необходимо выполнить небольшое вычисление. Я добавил +10 в конце, чтобы быть уверенным 😀

Чтобы нарисовать квадратичную кривую Безье, нам нужны три точки. Начало, конец и контрольная точка. В качестве контрольной точки я выбираю точку на внутреннем круге, который просто также разделен на десять сегментов, и выбираю сегмент, который находится посередине между текущей цифрой from_val и следующей цифрой to_val.

Я должен уточнить, что я имею в виду под серединой: средняя точка между 0 и 4 должна быть 2, но между 8 и 0 она должна быть 9. Она определяется кратчайшим путем от одного сегмента к другому, а потом берется середина.

Кроме того, у меня на самом деле нет 10 дискретных сегментов, это просто для понимания. Я могу использовать среднюю точку 1,23 или что-то в этом роде. Это используется, потому что мы меняем нашу начальную и конечную позиции на основе текущей позиции, которую мы находимся в нашем массиве цифр.

Я надеюсь, что все станет яснее, ели взглянуть на код:

small_radius = 70 for i in 1:max_digits-1     from_val = digits[i]     to_val = digits[i+1]     sethue(colors[from_val+1])      f = from_val+(i-1)/max_digits     t = to_val+i/max_digits      from = get_coord(f, radius)     to = get_coord(t, radius)      # get the correct mid point for example for 0-9 it should be 9.5 and not 4.5     mid_val = (f+t)/2     mid_control = get_coord(mid_val, small_radius)     if abs(f-t) >= 5         mid_control = get_coord(mid_val+5, small_radius)     end      pts = Point[from, mid_control, mid_control, to]     bezpath = BezierPathSegment(pts...)     drawbezierpath(bezpath, :stroke, close=false) end

Думаю, уже выглядит достаточно хорошо. Цвета линий подгоняются под цвета из под цифр. Итак, в какой-то момент мы переходим от 9 к 2. Вместо этого я хотел бы посмотреть, куда мы идем и откуда идем. Это можно сделать с помощью blend и setblend. Это линейная смена цвета "от" и "до", так что на самом деле не по кривой, но я думаю, что она достаточно хороша.

setblend(blend(from, to, colors[to_val+1], colors[from_val+1]))

Это похоже на sethue поэтому нам нужно задать его в какой-то момент, прежде чем мы вызовем drawbezierpath.

Давайте добавим еще несколько цифр и немного уменьшим ширину линии: setline(0.1)

Ладно я думаю что внутренний радиус немного велик:

small_radius = 40

Затем мы можем добавить $\pi$ в середине, прежде чем немного очистить код, чтобы создать нашу первую анимацию.

Luxor.jl не поддерживает латексные стринги LaTeXStrings.jl — это облом, но мы можем использовать UnicodeFun.jl.

using UnicodeFun center_text = to_latex("\\pi")

и промеж циклов ставим:

sethue("white") fontsize(60) text(center_text, Point(-2, 0), valign=:middle, halign=:center)

Мне кажется Point(-2, 0) более центральная, чем Point(0, 0) или O.

Анимация

Я хотел бы получить gif из конвейера визуализации таким образом, чтобы в каждом кадре добавлялась новая линия.

В Луксоре это можно сделать с помощью функции animate, которая берет несколько сцен и их номера кадров. Это также обеспечит немного большую структуру кода.

У нас может быть сцена для устойчивого фона и одна для линий.

Прежде чем мы напишем функцию, давайте определим очень короткую анимацию, чтобы увидеть, как это делается.

function draw_background(scene, framenumber)     background("black") end  function circ(scene, framenumber)     setdash("dot")     sethue("white")     translate(-200, 0)     @layer begin          translate(framenumber*2, 0)         circle(O, 50, :fill)     end end  function anim()     anim = Movie(600, 200, "test")      animate(anim, [         Scene(anim, draw_background, 0:200),         Scene(anim, circ, 0:200),     ],     creategif = true,     pathname = "./test.gif"     ) end

Сначала мы создаем Movie с width, height и name.
Затем мы вызываем animate с помощью созданного Movie и списка scenes, а затем функции и диапазон кадров, начинающихся с 0.

Происходит вызов draw_background(сцена, 0) и circ(scene, 0) для первого кадра. Сцена может содержать некоторые аргументы, которые мы будем использовать для нашей анимации. Остальное в основном так же, как и раньше, просто мы можем, конечно, использовать переменную framenumber.

Теперь я разделю все это дело на функции и определю переменные, такие как цифры, которые мы хотим визуализировать, чтобы нам было легче визуализировать $\tau$ или другие вещи.

Полный код

using Luxor, ColorSchemes using UnicodeFun  function get_coord(val, radius)     θ = 2π*0.1*val     return Point(         radius*sin(θ),         -radius*cos(θ),     ) end  function draw_background(scene, framenumber)     background("black")      radius = scene.opts[:radius]     colors = scene.opts[:colors]     center_text = scene.opts[:center_text]      for i in 0:9         from = get_coord(i, radius)         to = get_coord(i+1, radius)         sethue(colors[i+1])          θ = 2π*0.1*i+0.1*π         mid = Point(             radius*sin(θ),             -radius*cos(θ),         )         label(string(i), -π/2+θ, mid; offset=15)         move(from)         arc2r(O, from, to, :stroke)     end      sethue("white")     fontsize(60)     text(center_text, Point(-2, 0), valign=:middle, halign=:center) end  function dig_line(scene, framenumber)     radius = scene.opts[:radius]     colors = scene.opts[:colors]     center_text = scene.opts[:center_text]     bezier_radius = scene.opts[:bezier_radius]     max_digits = scene.opts[:max_digits]     digits = scene.opts[:digits]      setline(0.1)      for i in 1:min(framenumber, max_digits-1)         from_val = digits[i]         to_val = digits[i+1]          f = from_val+(i-1)/max_digits         t = to_val+i/max_digits          from = get_coord(f, radius)         to = get_coord(t, radius)          # get the correct mid point for example for 0-9 it should be 9.5 and not 4.5         mid_val = (f+t)/2         mid_control = get_coord(mid_val, bezier_radius)         if abs(f-t) >= 5             mid_control = get_coord(mid_val+5, bezier_radius)         end          pts = Point[from, mid_control, mid_control, to]         bezpath = BezierPathSegment(pts...)         # reverse the color to see where it is going         setblend(blend(from, to, colors[to_val+1], colors[from_val+1]))         drawbezierpath(bezpath, :stroke, close=false)     end end  function anim()     anim = Movie(700, 300, "test")      radius = 100     bezier_radius = 40     colors = ColorSchemes.rainbow[7:end]     max_digits = 1000     center_text = to_latex("\\pi")      digits_arr = setprecision(BigFloat, Int(ceil(log2(10) * max_digits+10))) do         return parse.(Int, collect(string(BigFloat(pi))[3:max_digits+2]))     end      args = Dict(:radius => radius,         :bezier_radius => bezier_radius,         :colors => colors, :max_digits => max_digits,         :digits => digits_arr, :center_text => center_text     )      animate(anim, [         Scene(anim, draw_background, 0:max_digits+50, optarg=args),         Scene(anim, dig_line, 0:max_digits+50, optarg=args),     ],     creategif = true,     pathname = "./pi_first.gif"     ) end

Единственное, что я еще не объяснил, — это optarg в функции Scene и получение его с помощью radius = scene.opts[:radius].

Мы как бы потеряли возможность создавать простые образы. Поэтому я создал структуру

struct PNGScene     opts::Dict{Symbol, Any} end

и использую некоторые аргументы в функции anim, которую я переименую в viz 😀

Тогда я могу использовать что-то вроде:

scene = PNGScene(args) @png begin     draw_background(scene, max_digits)     dig_line(scene, max_digits) end 700 300 "./$fname.png"

Не волнуйтесь, в конце есть репка, где вы можете увидеть весь код целиком. Просто немного сложно описать здесь изменения.

Может, мне стоило снять видео? 😀

Добавление точки Фейнмана

Мы визуализировали соединение цифр с цифрами с помощью кривых, но если бы у нас встретилось что-то вроде 555 в цифрах, мы бы видели только линию, идущую в направлении центра и обратно (или, может быть, мы видим две в зависимости от наших максимальных цифр, разрешения и т. д.)

Вместо этого мы можем показать дополнительную точку всякий раз, когда это происходит. Это можно получить благодаря аргументу функции show_dots, что вы можете найти в моем коде 😉

Я только что проверил длину последовательности, и когда она больше 1, я рисую круг, где это происходит, и цвет — это цифра после этой последовательности. Большой круг в сегменте 9 — это так называемая точка Фейнмана, где цифра 9 появляется 6 раз в позиции 762.

Добавление гистограмм

Последняя вещь в моем списке — получить гистограмму на каждом сегменте, чтобы показать, случаются ли некоторые комбинации пар чаще, чем другие.

Для этого я использую функцию poly с четырьмя точками. В идеале, она должна быть ограничена двумя дугами, а не двумя линиями, но я оставляю это читателю 🙂

Тау

Да, можно было бы в принципе сгенерировать случайное число с 1000 цифрами и получить аналогичный результат…

Простое число

В двух словах: использование нашей функции для визуализации большилства элементов не так разумно, но так или иначе может получится что-то интересное.

При этом в качестве числовой последовательности используются последние цифры простых чисел. Я визуализировал простые числа меньше 100 000. Честно говоря, соединительные линии немного бесполезны, так как большую часть времени (если мы игнорируем первые несколько простых чисел: все время) возможны только четыре цифры. Это создает своего рода беспорядок в середине.

Тем не менее, гистограммы становятся все интереснее, я думаю:

Это ясно показывает, что не все пары одинаково вероятны. Особенно, если у нас есть простое число $p_n$ с последней цифрой x, то всегда менее вероятно, что последняя цифра $p_{n+1}$ также заканчивается на x по сравнению с одним из трех других вариантов.

Давайте сосредоточимся на гистограммах и визуализируем простые числа под 10 000 000:

Узор сохраняется.

Код

Окай, тут у нас репка

Я хотел бы создать что-то вроде штучек, из 3b1b.

По крайней мере, небольшие простые версии с некоторыми удобными функциями визуализации 🙂

Спасибо за чтение и особая благодарность моим 10 покровителям!

Я буду держать вас в курсе событий на Twitter OpenSourcES и на более личном:
Twitter Wikunia_de

ссылка на оригинал статьи https://habr.com/ru/post/531050/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *