Возможно, вы видели предыдущий пост, где были предоставлены визуализации первых 1000 цифр и
. Он возник в результате небольшого спора о том, лучше ли
, чем
. По этому поводу идут бесконечные дебаты, и я подумал, что могу пошутить по этому поводу. В этом посте я хочу показать, как создать визуализации, и надеюсь, что вы захотите попробовать удивительный пакет Luxor.jl после прочтения. Вчера я начал читать туториал, и это потрясающе! В прошлый раз визуализация делалась на Javascript, и я подумал, что этот аккуратный маленький проект сойдет, чтобы начать изучать Луксор. Как уже упоминалось в let me be your mentor: я думаю, что очень важно иметь такие маленькие проекты, чтобы освоить новый инструмент.
Основная идея
Я хотел воссоздать визуализацию, которую видел в Numberphile от Мартина Крживинского.
Там был круг (который, вполне ассоциируется и с и с
) разделенный на 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*π
возможно, это не идеально написано (кроме того, я мог бы использовать :D).
2π*0.1*i начинает с северного положения, а затем для следующего i происходит перемещение на . Я добавляю "0.1 π", потому что хочу переходить к середине каждого сегмента. Может быть, следует написать
0.5/10*2π. Затем мы просто поворачиваем наш холст и двигаясь чуть выше радиуса, рисуем метки. На самом деле такое можно проделать в Luxor, используя rotate и translate. Но я решил сделать вручную, так как мне все равно это пригодится позже. В общем формула такова:
Такое преобразование поворачивает плоскость на и производит трансляцию на
x,y. Поскольку я перевожу только на y, мне не нужно первое тождество. Помните, что y увеличивается, когда идет вниз.
В настоящее время есть две проблемы:
- на самом деле нам не нужен круг, нам нужны дуги (сегменты) для каждой цифры
- подписи не читаются
Команда label принимает три значения: текст, вращение и положение, где вращение может быть записано как :N,: E,: S,: W для севера, востока, юга, запада или как угол (в радианах). :N есть . Поэтому мы хотим начать с
, а потом добавлять текущий угол поворота. Кроме того, смещение было бы здорово, если бы оно не доставало непосредственно до окружности или не подходило слишком близко к ней. Здесь мы могли бы увеличить радиус или использовать
;offset в команде label.
Для первой задачи нам нужна функция arc2r, которая принимает три аргумента
c1, p1, p2 + действие: c1 — это центр окружности, а p1 и p2 — точки на окружности, между которыми должен быть показан сегмент. По умолчанию выбрано направление по часовой стрелке.
Мы определяем следующую функцию, чтобы получить и соответствующую точку более простым способом:
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 начинается с единицы.

Каковы следующие шаги?
- Добавление строк
- Рефакторинг кода
- Оживление процесса
- Добавление точек
- Добавление гистограммы сверху
Я думаю, что визуально привлекательно иметь круг посередине, где мы можем добавить символ (или
) позже.
Поэтому мы не можем провести прямые линии от одного сегмента к другому. Для этого я использую квадратичные кривые Безье.
Давайте сначала получим цифры числа Пи:
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 в конце, чтобы быть уверенным :D
Чтобы нарисовать квадратичную кривую Безье, нам нужны три точки. Начало, конец и контрольная точка. В качестве контрольной точки я выбираю точку на внутреннем круге, который просто также разделен на десять сегментов, и выбираю сегмент, который находится посередине между текущей цифрой 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

Затем мы можем добавить в середине, прежде чем немного очистить код, чтобы создать нашу первую анимацию.
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.

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

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 :D
Тогда я могу использовать что-то вроде:
scene = PNGScene(args) @png begin draw_background(scene, max_digits) dig_line(scene, max_digits) end 700 300 "./$fname.png"
Не волнуйтесь, в конце есть репка, где вы можете увидеть весь код целиком. Просто немного сложно описать здесь изменения.
Может, мне стоило снять видео? :D
Добавление точки Фейнмана
Мы визуализировали соединение цифр с цифрами с помощью кривых, но если бы у нас встретилось что-то вроде 555 в цифрах, мы бы видели только линию, идущую в направлении центра и обратно (или, может быть, мы видим две в зависимости от наших максимальных цифр, разрешения и т. д.)
Вместо этого мы можем показать дополнительную точку всякий раз, когда это происходит. Это можно получить благодаря аргументу функции show_dots, что вы можете найти в моем коде ;)

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

Тау

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

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

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

Узор сохраняется.
Код
Окай, тут у нас репка
Я хотел бы создать что-то вроде штучек, из 3b1b.
По крайней мере, небольшие простые версии с некоторыми удобными функциями визуализации :)
Спасибо за чтение и особая благодарность моим 10 покровителям!
Я буду держать вас в курсе событий на Twitter OpenSourcES и на более личном:
Twitter Wikunia_de

