Генератор 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 | Две границы прямо в конфиге — коридор |
mode | signal (по умолчанию) — траектория; density — распределение |
y_range | min..max — диапазон значений, которые вы получите; рисунок растягивается в него |
interp | linear (по умолчанию) / smooth / step — поведение линии между точками |
spread | Разброс: каждая строка ±N в единицах y_range (по умолчанию 0 — точная линия) |
ink_threshold | 0..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 пикселей, штрих на прозрачном фоне и больше ничего. Ни осей, ни подписей, ни рамки: всё непрозрачное в файле читается как часть рисунка.

<gen type="pattern" src="line-input.png" y_range="0..100"/>
Прогоняем на 40 строк и накладываем результат на рисунок. Зелёные точки — это сгенерированные значения, оранжевая линия — картинка:
- линия, нарисованная в файле
- сгенерированные значения (40 строк)
В картинке 240 пикселей ширины, а в прогоне 40 строк, и эти числа никак не связаны: просите 40 строк или 4 000 000 — тот же рисунок растянется на них.
То же самое из векторного файла
SVG не аппроксимируется. Вот файл с одной кубической кривой Безье внутри <g>,
который переворачивает ось y, — две вещи, которые редакторы выдают постоянно:
- кривая Безье из файла, с применённым transform
- сгенерированные значения (40 строк)
Рисунок читается по столбцам
TDC никогда не гадает, что у вас на картинке — «одна линия» или «полоса». Он измеряет, и измерение само отвечает на этот вопрос.
Для каждого столбца рисунка берутся два замера: сверху вниз до первой встреченной отметки и снизу вверх до первой встреченной отметки.
- Aзамеры сошлись в одном пикселе — значение точное
- Bзамеры разошлись — значение случайное между ними
- сами замеры: сверху вниз и снизу вверх
- Оба замера пришли в одну и ту же точку → этот столбец даёт точное значение на графике. Все строки, попавшие сюда, получат его без всякой случайности.
- Замеры разошлись → этот столбец — полоса. Строка получит случайное значение
между двумя краями, зафиксированное
seed.
Поэтому картинка из двух штрихов — это коридор, и каждая строка берёт случайное значение между ними. Вот входной файл и 300 строк, сгенерированных по нему:

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

- нарисованная машина
- сгенерированные значения (300 строк)
- Aколесо опускает нижний край кузова
- Bземля до и после машины — одна линия, значения точные
Вот линия, которая идёт ровно, а потом раздваивается — одна ветка вверх, другая вниз:

- линии из файла
- сгенерированные значения (60 строк)
- Aздесь линия раздваивается
- Bдо развилки — одно и то же точное значение
- Cпосле — случайное внутри расходящейся полосы
Тот же прогон на двух разных сидах показывает это в лоб — первая половина одинаковая, вторая нет:
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, когда нужны
непрерывные значения, а не округлённые целые.
- один и тот же нарисованный треугольник
- сгенерированные значения (25 строк)
y_range всегда означает диапазон значений, которые вы получите. В режиме
mode="density" ось значений — это ширина рисунка, а не высота, поэтому там
диапазон растягивается по картинке слева направо: смысл атрибута тот же, меняется
только ось.
decimals — знаки после запятой
decimals задаёт, сколько дробных знаков остаётся у значения (по умолчанию 0, то
есть целые). Пригодится, когда кривая кормит цену, температуру или любую измеряемую
величину, где округление до целых сплющит форму.
<gen type="pattern" points="0,0 50,100 100,0" y_range="-1..1" decimals="2"/>
-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 строк, даёт среднее по зубцам, а не тот зубец, который случайно оказался под точкой замера:
50.2 50.1 50.0 49.9 49.8
Расширьте тот же прогон до 400 строк — и зубцы вернутся, потому что теперь строка
достаточно узкая, чтобы их различать: 100 50 13 50 87 51 13 49 87 …
- нарисованная пила
- сгенерированные значения
- A300 строк — зубцы проходят целиком
- B6 строк — каждая выдаёт среднее по своему куску
Строк больше, чем точек → интерполяция, и вот тут в дело вступает interp.
interp — как ведёт себя линия между двумя точками
Между двумя далеко отстоящими точками может уместиться несколько тысяч строк. Сам по
себе прямой отрезок растёт ровно на одну и ту же величину каждую строку —
математически верно и заметно искусственно. interp выбирает поведение:
| Значение | Что делает |
|---|---|
linear (по умолчанию) | Прямые отрезки — постоянная скорость между двумя точками |
smooth | Кривая через точки с плавным входом и выходом, без вылетов |
step | Держит значение точки до следующей — лесенка |
smooth использует монотонную кубическую кривую: она скругляет углы и меняет
скорость, но никогда не уводит линию выше или ниже реально нарисованных значений — из
изгиба не появится фантомный пик. Тот же рисунок из трёх точек на 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
На рисунке с настоящими изломами разница видна сразу — пунктир это то, что нарисовано, точки это то, что вышло:
- что нарисовано: ломаная через пять точек
- сгенерированные значения (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"/>
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, который вы не рисовали
- сгенерированные значения (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"/>
В этом файле два штриха на белом холсте — чёрный и светло-серый, — и порог решает, существует ли серый:

- чёрный штрих
- светло-серый штрих
- сгенерированные значения (60 строк)
Сигнал — это траектория, а не гистограмма
Значение строки — это высота кривой в её позиции по порядку. То есть числа выходят вдоль кривой, как след пера: это траектория, а не «куча значений». Если взять те же числа и, забыв про порядок, построить гистограмму (как часто встречается каждое значение), форма кривой в ней не проявится. Вот настоящая гистограмма рисунка из двух горбов на 300 строках:
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"/>
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 не появляется ничего: там рисунок
плоский, а плоско — значит «никогда».
То же самое, но картинкой вместо набранных руками точек. Вот горб, нарисованный заливкой:

<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"та же форма начинает решать, как часто выпадает каждое значение.
Случай со всплеском лучше всего показывает идею «позиция = номер строки»:
строки 1–18: 10 (ровный участок) строка 19: 55 (подъём) строка 20: 100 (всплеск)
Фронтенд-рисовалка, которая экспортирует ровно такие SVG/PNG, — чтобы набрасывать кривую мышью, а не открывать редактор.
См. также
- Временные ряды — когда форма это тренд + сезон + шум.
- Number — для диапазонов и простых случайных целых.
- Детерминизм и пропорции — как
seedфиксирует разброс. - Большие объёмы и потоковая генерация — миллионы строк из одного маленького рисунка.