Перейти к основному содержимому

Генератор pattern

Когда пригодится: у сигнала есть форма, которую не описать формулой — спрос по часам, демографическая волна, скриншот биржевого графика, любая «извилистая» линия. Вы рисуете картинку, а её форма растягивается на все строки: горизонтальная ось — номер строки (от первой до последней), вертикальная — значение. Рисунок может быть крошечным, строк — миллионы.

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

Примеры вывода ниже — иллюстративные, по версиям ядра числа могут чуть отличаться; гарантируется форма.

Зачем рисовать форму, а не брать распределение из списка

У готовых распределений (normal, poisson, …) форма фиксированная — колокол, затухающий хвост. Настоящему сигналу часто нужна форма, которой в этом списке нет: два горба, где правый выше, плато со всплеском в конце, спрос, который растёт к полудню и падает ночью. «Два горба разной высоты» из normal не получить. pattern снимает это ограничение: силуэт вы рисуете сами, и он ложится на строки как есть.

Как включить

Каждый <gen> с type="pattern" берёт ровно один источник формы. Обычный источник — файл: рисуете в любом редакторе или делаете скриншот графика и отдаёте его генератору:

<!-- ваш рисунок: SVG или PNG -->
<gen type="pattern" src="chart.svg" y_range="0..40"/>

Если форма достаточно простая, чтобы набрать её руками, тот же генератор принимает точки прямо в конфиге — одна линия через points или полоса через upper / lower:

<gen type="pattern" points="0,5 20,40 40,8 60,45 80,15 100,5" y_range="0..40"/>
<gen type="pattern" upper="0,20 50,40 100,20" lower="0,5 50,10 100,5" y_range="0..40"/>
АтрибутЧто задаёт
srcФайл SVG или PNG — обычный способ задать форму
pointsПары x,y прямо в конфиге вместо файла: x — по ширине, y — высота
upper / lowerДве границы прямо в конфиге — коридор
modesignal (по умолчанию) — траектория; densityраспределение
y_rangemin..max — диапазон значений, которые вы получите; рисунок растягивается в него
interplinear (по умолчанию) / smooth / step — поведение линии между точками
spreadРазброс: каждая строка ±N в единицах y_range (по умолчанию 0 — точная линия)
ink_threshold0..1 — насколько тёмным должен быть пиксель PNG, чтобы считаться чернилами (по умолчанию 0.5)
decimalsЗнаков после запятой (по умолчанию 0)

src — картинка и есть конфиг

Читаются два формата, и оба внутри превращаются в одно и то же — векторный контур:

  • SVG — читается по настоящей геометрии: <path>, <polyline>, <line>, <rect>, <circle>, <ellipse>. Кривые Безье и дуги проходятся так, как нарисованы, а transform на фигуре или на любой объемлющей <g> применяется. Рисуйте в Illustrator, Figma, Inkscape или руками — экспортируйте и работайте.
  • PNG с прозрачным фоном — нарисованные пиксели и есть форма, прозрачность — пустота. Именно так выглядит обрезанный по графику скриншот или мазок кистью из любого растрового редактора. Если у PNG сплошной фон, включается запасное правило: чернила — тёмные пиксели, светлые — фон, а порог двигает ink_threshold.

Ось y в SVG растёт вниз — так её хранит любая программа для рисования. TDC переворачивает её, поэтому «выше на экране» всегда означает «больше значение».

Целый пример, от начала до конца

Вот настоящий файл — 240×140 пикселей, штрих на прозрачном фоне и больше ничего. Ни осей, ни подписей, ни рамки: всё непрозрачное в файле читается как часть рисунка.

line-input.png — одна волнистая линия на прозрачном фоне
line-input.png — весь файл целиком. Белая подложка здесь только для того, чтобы линию было видно: у самого файла фона нет.
<gen type="pattern" src="line-input.png" y_range="0..100"/>

Прогоняем на 40 строк и накладываем результат на рисунок. Зелёные точки — это сгенерированные значения, оранжевая линия — картинка:

Сгенерированные значения ложатся точно на нарисованную кривую
Каждая строка попадает ровно на штрих — у одиночной линии нет никакой случайности.
  • линия, нарисованная в файле
  • сгенерированные значения (40 строк)

В картинке 240 пикселей ширины, а в прогоне 40 строк, и эти числа никак не связаны: просите 40 строк или 4 000 000 — тот же рисунок растянется на них.

То же самое из векторного файла

SVG не аппроксимируется. Вот файл с одной кубической кривой Безье внутри <g>, который переворачивает ось y, — две вещи, которые редакторы выдают постоянно:

curve-input.svg — одна кривая Безье внутри переворачивающего transform
curve-input.svg — один path, один transform.
Сгенерированные значения идут точно по кривой Безье с учётом transform
Кривая проходится как нарисована: Безье обходится по-настоящему, transform применяется, ось y возвращается на место.
  • кривая Безье из файла, с применённым transform
  • сгенерированные значения (40 строк)

Рисунок читается по столбцам

TDC никогда не гадает, что у вас на картинке — «одна линия» или «полоса». Он измеряет, и измерение само отвечает на этот вопрос.

Для каждого столбца рисунка берутся два замера: сверху вниз до первой встреченной отметки и снизу вверх до первой встреченной отметки.

Два замера на столбец: сошлись в одном пикселе — точное значение, разошлись — полоса
Те же два замера на одиночной линии и на полосе. Больше ничего не анализируется.
  • Aзамеры сошлись в одном пикселе — значение точное
  • Bзамеры разошлись — значение случайное между ними
  • сами замеры: сверху вниз и снизу вверх
  • Оба замера пришли в одну и ту же точку → этот столбец даёт точное значение на графике. Все строки, попавшие сюда, получат его без всякой случайности.
  • Замеры разошлись → этот столбец — полоса. Строка получит случайное значение между двумя краями, зафиксированное seed.

Поэтому картинка из двух штрихов — это коридор, и каждая строка берёт случайное значение между ними. Вот входной файл и 300 строк, сгенерированных по нему:

tunnel-input.png — два штриха, образующие полосу
tunnel-input.png — два штриха, и больше ничего.
300 сгенерированных значений внутри нарисованной полосы
Все значения лежат внутри полосы. Где полоса широкая — разброс широкий, где сужается — сужается и он.
  • полоса, нарисованная в файле
  • сгенерированные значения (300 строк)
  • Aодин столбец: значение берётся между его краями

Два следствия, которые стоит знать:

  • Одна картинка может быть и тем и другим. Линия, которая сначала идёт одна, а потом раздваивается, до развилки даёт точные значения, а после — случайные. Ни атрибута, ни выбора режима не нужно.
  • Всё замкнутое — это коридор. Нарисуйте машину, лист, кляксу: у неё есть верхний край и нижний, значит она читается как полоса между ними. Это не фигура речи — вот машина:
car-input.png — залитый силуэт машины
car-input.png — силуэт, залитый сплошняком.
Сгенерированные значения заполняют силуэт машины
Значения заполняют кузов между верхним и нижним краем — а там, где рисунок вырождается в одну линию (земля до и после машины), они схлопываются в одно точное значение.
  • нарисованная машина
  • сгенерированные значения (300 строк)
  • Aколесо опускает нижний край кузова
  • Bземля до и после машины — одна линия, значения точные

Вот линия, которая идёт ровно, а потом раздваивается — одна ветка вверх, другая вниз:

split-input.png — линия, которая на середине раздваивается
split-input.png — один штрих, который становится двумя.
До развилки значения точные, после неё — случайные внутри полосы
До развилки каждая строка получает одно и то же точное значение, после — случайное внутри расходящейся полосы.
  • линии из файла
  • сгенерированные значения (60 строк)
  • Aздесь линия раздваивается
  • Bдо развилки — одно и то же точное значение
  • Cпосле — случайное внутри расходящейся полосы

Тот же прогон на двух разных сидах показывает это в лоб — первая половина одинаковая, вторая нет:

./run split.tdc (11 строк, два сида)
seed A:  50  50  50  50  50  50  44  47  66  18  45
seed B:  50  50  50  50  50  51  57  61  60  81  35

y_range — задаём масштаб по высоте

Рисунок — это псевдографик без собственного масштаба: y_range="min..max" говорит, что означают его низ и верх. Самая нижняя точка становится min, самая верхняя — max, всё между растягивается линейно. Нужен всегда, когда нужные числа не совпадают с координатами картинки, — включая отрицательные и дробные.

<gen type="pattern" src="chart.svg" y_range="0..40"/>
<gen type="pattern" src="chart.svg" y_range="-2..2"/>

Без y_range берутся сырые y как есть. Сочетайте с decimals, когда нужны непрерывные значения, а не округлённые целые.

Один и тот же треугольник при трёх разных y_range
Один рисунок, три масштаба. Форма одинаковая, меняются только числа на оси.
  • один и тот же нарисованный треугольник
  • сгенерированные значения (25 строк)

y_range всегда означает диапазон значений, которые вы получите. В режиме mode="density" ось значений — это ширина рисунка, а не высота, поэтому там диапазон растягивается по картинке слева направо: смысл атрибута тот же, меняется только ось.

decimals — знаки после запятой

decimals задаёт, сколько дробных знаков остаётся у значения (по умолчанию 0, то есть целые). Пригодится, когда кривая кормит цену, температуру или любую измеряемую величину, где округление до целых сплющит форму.

<gen type="pattern" points="0,0 50,100 100,0" y_range="-1..1" decimals="2"/>
./run decimals.tdc (11 строк)
-1.00   -0.60   -0.20   0.20   0.60   1.00   0.60   0.20   -0.20   -0.60   -1.00

Растягивание — точек на рисунке почти никогда не столько, сколько строк

Строка i из count читает рисунок в точке t = i / (count − 1). Ширина рисунка раскладывается на весь прогон, поэтому две стороны несовпадения обрабатываются по-разному.

Строк меньше, чем деталей на рисунке → усреднение. Строка берёт не одну точку, а кусок рисунка шириной в свою долю, и выдаёт среднее по этому куску. Пила из 200 зубцов от 0 до 100, ужатая в 5 строк, даёт среднее по зубцам, а не тот зубец, который случайно оказался под точкой замера:

./run saw.tdc (200 зубцов в 5 строк)
50.2   50.1   50.0   49.9   49.8

Расширьте тот же прогон до 400 строк — и зубцы вернутся, потому что теперь строка достаточно узкая, чтобы их различать: 100 50 13 50 87 51 13 49 87 …

Одна и та же пила на 300 строках и на 6 строках
Одна и та же пила. Когда строк с запасом — зубцы проходят целиком; когда их шесть — каждая выдаёт среднее по всему, что накрывает, а не тот зубец, который случайно оказался под точкой замера.
  • нарисованная пила
  • сгенерированные значения
  • A300 строк — зубцы проходят целиком
  • B6 строк — каждая выдаёт среднее по своему куску

Строк больше, чем точек → интерполяция, и вот тут в дело вступает interp.

interp — как ведёт себя линия между двумя точками

Между двумя далеко отстоящими точками может уместиться несколько тысяч строк. Сам по себе прямой отрезок растёт ровно на одну и ту же величину каждую строку — математически верно и заметно искусственно. interp выбирает поведение:

ЗначениеЧто делает
linear (по умолчанию)Прямые отрезки — постоянная скорость между двумя точками
smoothКривая через точки с плавным входом и выходом, без вылетов
stepДержит значение точки до следующей — лесенка

smooth использует монотонную кубическую кривую: она скругляет углы и меняет скорость, но никогда не уводит линию выше или ниже реально нарисованных значений — из изгиба не появится фантомный пик. Тот же рисунок из трёх точек на 11 строк:

./run kink.tdc (11 строк, три режима)
linear:  0   8  16  24  32  41  52  64  76  88  100
smooth:  0   8  15  23  31  40  50  62  75  88  100
step:    0   0   0   0   0  30  40  40  40  40   40

На рисунке с настоящими изломами разница видна сразу — пунктир это то, что нарисовано, точки это то, что вышло:

linear идёт по ломаной, smooth скругляет углы, step держит значение
linear едет ровно по ломаной; smooth скругляет каждый угол, ни разу не выходя за нарисованное; step держит значение точки до следующей.
  • что нарисовано: ломаная через пять точек
  • сгенерированные значения (41 строка)

Посмотрите на шаги: у linear повторяется 8, 8, 8, 8 — идеально предсказуемо. У smooth идёт 8, 7, 8, 8, 9, 10, 12, 13 — линия мягко отходит от точки, разгоняется и мягко приходит.

spread — превратить одну линию в туннель

spread="N" разбрасывает каждую строку на ±N в единицах вашего y_range. По умолчанию 0: линия точная и на 100% предсказуемая. Задайте значение — и нарисованная линия становится центром полосы, так что рисовать оба края коридора ради небольшого дрожания уже не нужно.

Разброс следует объявленному масштабу: при y_range="0..100" spread="1" — это один пункт шума, а при y_range="0..1" тот же относительный эффект даст spread="0.001".

<gen type="pattern" src="ramp.svg" y_range="0..100" spread="5"/>
./run ramp.tdc (11 строк)
spread 0, seed A:   0  10  20  30  40  50  60  70  80  90  100
spread 5, seed A:   5  13  16  29  37  52  57  69  83  86   99
spread 5, seed B:   5  11  21  26  45  53  64  73  82  94   99

Тот же одиночный штрих из line-input.png, но уже со spread="6":

Нарисованная линия с полосой ±6 вокруг и значениями внутри неё
Одна нарисованная линия — и туннель, который вы не рисовали. Форма не тронута, каждая строка просто садится куда-то в пределах ±6 от неё.
  • нарисованная линия — центр
  • коридор ±6, который вы не рисовали
  • сгенерированные значения (60 строк)

Тренд не тронут, просто каждая строка садится куда-то в пределах ±5 от него. Как и у коридора, разброс детерминирован: тот же сид воспроизводит его точь-в-точь. spread работает и поверх рисунка, который уже является полосой, — расширяет её на N в обе стороны.

ink_threshold — порог «тёмный/светлый» для PNG

Когда у PNG сплошной фон, чернила определяются темнотой. ink_threshold задаёт, насколько тёмным должен быть пиксель, по шкале 0..1 (по умолчанию 0.5). Опустите к 0, чтобы поймать бледные сглаженные края; поднимите к 1, чтобы игнорировать серое и оставить только почти чёрные штрихи. На PNG, нарисованном на прозрачности, порог не влияет ни на что: там альфа-канал уже говорит, что нарисовано.

<gen type="pattern" src="faint-curve.png" ink_threshold="0.3" y_range="0..100"/>

В этом файле два штриха на белом холсте — чёрный и светло-серый, — и порог решает, существует ли серый:

threshold-input.png — чёрный и серый штрихи на белом
threshold-input.png — два штриха на непрозрачном холсте.
При 0.5 читается только чёрный штрих, при 0.8 — оба, и получается коридор
На пороге по умолчанию серый штрих — это фон, и читается одна точная линия. Поднимите порог — серый тоже становится чернилами, краёв стало два, и значения пошли случайные между ними.
  • чёрный штрих
  • светло-серый штрих
  • сгенерированные значения (60 строк)

Сигнал — это траектория, а не гистограмма

Значение строки — это высота кривой в её позиции по порядку. То есть числа выходят вдоль кривой, как след пера: это траектория, а не «куча значений». Если взять те же числа и, забыв про порядок, построить гистограмму (как часто встречается каждое значение), форма кривой в ней не проявится. Вот настоящая гистограмма рисунка из двух горбов на 300 строках:

./run humps.tdc --count 300 | histogram
0  | ################################## 29
3  | ######################################## 34
7  | ######################################## 34
10 | #################################### 31
13 | ######################### 21
17 | ########################## 22
20 | ################################# 28
23 | ########################## 22
27 | ######################### 21
30 | ################################## 29
33 | ################### 16
37 | ############### 13

Никаких двух горбов — гистограмма почти ровная. Это не баг: сигнал проходит через каждую высоту примерно одинаково часто, поэтому куча его значений не похожа на рисунок. Если нужно, чтобы рисунок задавал частоту, — это второе прочтение, mode="density", о нём ниже.

mode — два вопроса, которые можно задать рисунку

Одна и та же картинка отвечает на два разных вопроса, mode выбирает какой:

РежимВопросЧто выходит
signal (по умолчанию)«какое значение получит эта строка0, 20, 40, 70, 90, 100, 90, 70, 40, 20, 0 — идём по линии, по порядку
density«как часто встречается это значение?»куча чисел, сгущённая там, где нарисован горб, в случайном порядке

В density оси меняются смыслом: по горизонтали — значение, а высота кривой — как часто оно встречается. Нарисовал горб посередине — большинство чисел упадёт в середину; нарисовал два горба разной высоты — тот, что выше, заберёт пропорционально больше. По сути «нарисуй свою вероятность» вместо выбора normal / poisson из списка.

<!-- треугольник, стоящий между x=25 и x=75 -->
<gen type="pattern" points="0,0 25,0 50,100 75,0 100,0" y_range="0..100" mode="density"/>
./run density.tdc --count 6000 | histogram
  0- 9 |
10-19 |
20-29 | ## 85
30-39 | ########################### 901
40-49 | ########################################################## 1911
50-59 | ############################################################ 1955
60-69 | ############################## 978
70-79 | ##### 170
80-89 |
90-99 |

Сами значения выходят вперемешку — 60 57 36 49 48 40 67 53 …, — потому что у распределения нет порядка. И за пределами 25..75 не появляется ничего: там рисунок плоский, а плоско — значит «никогда».

То же самое, но картинкой вместо набранных руками точек. Вот горб, нарисованный заливкой:

hump-input.png — залитый горб колоколом
hump-input.png — горб, намазанный кистью и сохранённый на прозрачном фоне.
<gen type="pattern" src="hump-input.png" y_range="0..100" mode="density"/>

Генерируем по нему 6000 строк и считаем, как часто выпало каждое значение:

Сверху нарисованный горб, снизу гистограмма сгенерированных значений на той же оси
Сверху — что нарисовано. Снизу — как часто на самом деле выпадало каждое значение. Горизонтальная ось одна и та же, и это значение, а не номер строки.
  • Aчто нарисовано в файле
  • Bгистограмма 6000 сгенерированных значений
  • Cвысота рисунка здесь = как часто выпадает это значение

В этом вся идея: силуэт, который вы намазали кистью, становится гистограммой данных. Никакой формулы нигде не выбиралось.

Причём высота работает пропорционально, а не просто «тут есть, тут нет»: нарисуйте один горб вдвое выше другого — он заберёт примерно вдвое больше значений:

Два горба разной высоты и соответствующая гистограмма
Два горба, правый вдвое выше. Гистограмма снизу повторяет оба — в пропорции.
  • Aчто нарисовано: два горба разной высоты
  • Bгистограмма 6000 сгенерированных значений
  • Cлевый горб
  • Dправый, вдвое выше — и значений вдвое больше

Что стоит знать про density:

  • Ноль — это пол картинки. У PNG это нижний край изображения, у SVG и точек в конфиге — самая низкая точка рисунка. Где кривая ниже всего, то значение не выпадает никогда, — поэтому доводите кривую до основания по краям нужного диапазона.
  • От полосы берётся верхний край. Если нарисован коридор, форму распределения задаёт его контур.
  • interp работает и здесьsmooth даёт скруглённое распределение вместо угловатого.
  • spread здесь запрещён. Рисунок и так задаёт разброс, так что сочетание этих двух вещей — ошибка, а не возможность.
  • Плоский рисунок означает «мне всё равно» и даёт равномерный разброс по диапазону, а не ошибку.
  • Детерминированно и потоково, как и всё остальное: тот же сид воспроизводит ту же кучу на любом количестве строк.

Детали

  • Детерминированность: там, где рисунок — одиночная линия, значение строки просто равно высоте в этой точке (никакой случайности вообще); там, где полоса, — или там, где задан spread, — разброс зафиксирован seed. Тот же сид и конфиг → тот же результат.
  • Любой объём, любой движок: каждая строка считается из своего номера, память не растёт. Миллион строк из маленького рисунка спокойно едет на потоковых движках.
  • Файл читается один раз, в начале прогона, и превращается в геометрию — стоимость не зависит от количества строк.

Проверить руками

Несколько однострочников для интуиции:

  • points="0,0 50,100 100,0" — треугольник: значения растут к середине и падают.
  • points="0,10 90,10 100,100" — ровная линия со всплеском только в последних 10% строк.
  • upper="0,0 50,40 100,0" — полоса: случайное от 0 до центрального пика.
  • src="chart.png" y_range="0..100" — нарисованная форма становится данными вдоль контура.
  • добавьте к любому из них spread="2" — та же форма, но с дрожанием.
  • а с mode="density" та же форма начинает решать, как часто выпадает каждое значение.

Случай со всплеском лучше всего показывает идею «позиция = номер строки»:

./run spike.tdc (20 строк)
строки 1–18: 10   (ровный участок)
строка 19: 55      (подъём)
строка 20: 100     (всплеск)
Позже

Фронтенд-рисовалка, которая экспортирует ровно такие SVG/PNG, — чтобы набрасывать кривую мышью, а не открывать редактор.

См. также