Выражения
Маленький язык с четырьмя местами обитания — и везде он читается одинаково:
| Где | На что отвечает |
|---|---|
if= | участвует ли <gen>, <line>, <case> или <data> в этой строке |
filter= | из каких участников пула эта строка может тянуть |
expr= | ЗНАЧЕНИЕ колонки formula |
| параметр распределения | значение mean, sd, lambda … на этой строке |
Первые два берут ответ как «да/нет» и выбрасывают значение; последние два его оставляют. Те же операторы, те же функции, те же имена для тех же колонок — именно это не даёт условию и вычисленной колонке начать понимать одни и те же слова по-разному.
<sequence name="Zone">
<gen if="Country in [US, CA, MX]" type="text" value="NAFTA"/>
<gen if="Country in [FR, DE]" type="text" value="EU"/>
<gen type="text" value="ROW"/>
</sequence>
<sequence name="Handling">
<gen if="Weight > 20" type="text" value="freight"/>
<gen if="_count % 2 == 0" type="text" value="courier-even"/>
<gen type="text" value="parcel"/>
</sequence>
US NAFTA 2kg parcel FR EU 14kg courier-even CA NAFTA 7kg parcel DE EU 30kg freight MX NAFTA 5kg parcel JP ROW 22kg freight
У последнего <gen> в каждой последовательности нет if=, поэтому он забирает всё, что не
подошло условиям выше, — та же форма, что и else.
Значения
| Вы пишете | Что это значит |
|---|---|
Country | значение, которое эта последовательность дала на этой строке |
Person.Email | поле составной последовательности |
Gender.Male | «сейчас Gender равен Male?» — читается как parent="Gender.Male" |
Male | голое слово: имя, которое не является последовательностью, — это текст |
42, 1.5 | число |
'текст' | строка в кавычках, когда в тексте пробелы или он похож на имя |
_count, _last | встроенное — номер строки, признак последней |
Голое слово — это то, что позволяет писать Gender == Male без кавычек. Оно же означает, что
опечатка сравнивается сама с собой и молча ни с чем не совпадает, — поэтому неизвестное
имя справа от точки даёт TDC193, а не проходит.
Целые числа
Double держит каждое целое до 2⁵³, а дальше начинает их пропускать — и выражение, построенное на одних double, отвечает так:
9007199254740993 == 9007199254740992 true 9007199254740993 - 9007199254740992 0
И то и другое неверно, и неверно молча — а для генератора данных это худший вид ошибки: прогон завершается, файл выглядит нормально. Поэтому операнд, который является целым, и несётся как целое, а double становится только когда его об этом попросят:
| литерал без точки и экспоненты | остаётся целым |
| колонка, значение которой читается как цифры | сравнивается как целое с другим целым |
+ - * % над двумя целыми | остаются целыми |
abs round floor ceil trunc над целым | остаются целыми: округление целого — это оно само, какого бы размера оно ни было |
min max sum, пока все аргументы целые | остаются целыми |
/ | всегда с плавающей точкой — деление не замкнуто на целых |
всё, что уходит в sqrt, log, sin… | становится double: у них точного ответа нет |
Домен — знаковые 64 бита, тот же, что у слоя compute: это самое широкое целое, которое все пять реализаций держат сами. РЕЗУЛЬТАТ арифметики за его пределом — отказ, теми же словами, что у compute, а не тихое сползание обратно в плавающую точку:
tdcv2: integer overflow: 9223372036854775808 is outside the signed 64-bit range
Отказ приходит от прогона, а не от check: оба слагаемых лежат внутри домена, за его
пределом только ответ, и доказывать валидатору нечего.
ЛИТЕРАЛ шире домена — другое дело: это double. 1 / 0 > 100000000000000000000 — это способ
написать «больше любого целого, которое мы держим», и он работает; плата в том, что два
литерала, округляющиеся к одному double, равны между собой, поэтому
10000000000000000000 == 10000000000000000001 — истина. Если имелся в виду не число, а
имя, пишите его текстом.
Один край стоит знать: −2⁶³ достижимо арифметикой, но не записывается литералом — в
-9223372036854775808 минус применяется к величине на единицу больше самого большого
положительного.
Операторы
| Группа | Операторы |
|---|---|
| сравнение | == != === !== < > <= >= |
| логика | && || ! |
| арифметика | + - * / % |
+ складывает, когда обе стороны читаются как числа, а колонка всегда текст —
поэтому Price + Delivery на двух дробных колонках это арифметика, а не склейка. Он
склеивает только тогда, когда сторона и правда не число: 'a' + 'b' даёт ab. Чтобы
соединить значения нарочно, поставьте их рядом там, где они печатаются:
${{First}} ${{Last}}.
| принадлежность | in |
| выбор | a ? b : c |
== спрашивает, одно ли это число; === — печатаются ли обе стороны одними и теми же
символами. Каждая колонка — текст, поэтому эти два вопроса действительно разные: см.
Сравнение и истинность, где заодно разобрано, что считается истиной для
голого if="Flag".
% евклидов, и это не то, что делает ваш язык-3 % 2 здесь 1. JavaScript, Java, C# и Rust отвечают −1; Python отвечает 1.
Отрицательный ДЕЛИТЕЛЬ — место, где расходится и Python: 7 % -3 здесь 1, а там −2.
Результат никогда не бывает со знаком — он всегда в 0 … |делитель| - 1.
Причина не во вкусе. В слое compute уже был <mod>, и он уже отвечал 1, — так
что %, взявший соглашение хозяина, заставил бы один движок давать два разных ответа на один
вопрос в зависимости от того, к какому слою вы потянулись.
in берёт справа список и ничего кроме — список в любом другом месте даёт
TDC259. Сравнение внутри такое же нестрогое, как у ==, поэтому текстовая
колонка против списка числовых слов всё равно совпадёт.
<gen if="Country in [US, CA, MX]" .../> <!-- вместо трёх == через || -->
Функции
| Функция | Берёт | Даёт |
|---|---|---|
abs(x) | 1 | модуль |
ceil(x) floor(x) | 1 | вверх / вниз до целого |
trunc(x) | 1 | к нулю — trunc(-7.5) это −7, а floor даёт −8 |
round(x) | 1 | ближайшее, половина от нуля |
min(…) max(…) | 1 и больше | наименьшее / наибольшее |
len(s) | 1 | сколько символов |
is_empty(s) | 1 | пуст ли текст |
starts_with(s, p) | 2 | проверка префикса |
ends_with(s, p) | 2 | проверка суффикса |
contains(s, p) | 2 | проверка вхождения |
lower(s) upper(s) | 1 | регистр |
split(s, sep) | 2 | текст, разрезанный в список — см. Списки внутри строки |
join(list, sep) | 2 | список обратно в текст |
count(list) | 1 | сколько элементов |
at(list, i) | 2 | i-й элемент, счёт с нуля |
sum(list) | 1 | сумма — остаётся целой, пока целы все элементы |
mean(list) median(list) | 1 | среднее и середина |
stddev(list) | 1 | популяционное стандартное отклонение, делится на n |
sqrt(x) | 1 | квадратный корень |
pow(x, y) | 2 | x в степени y |
exp(x) | 1 | e в степени x |
log(x) log10(x) | 1 | натуральный / десятичный логарифм |
sin(x) cos(x) tan(x) | 1 | тригонометрия, в радианах |
asin(x) acos(x) atan(x) | 1 | обратные к ним, дают радианы |
atan2(y, x) | 2 | угол точки (x, y), в диапазоне (−π, π] |
sinh(x) cosh(x) tanh(x) | 1 | гиперболические функции |
cbrt(x) | 1 | кубический корень — берётся и от отрицательных |
expm1(x) log1p(x) | 1 | eˣ−1 и log(1+x), точные около нуля |
log2(x) | 1 | двоичный логарифм — точен на степени двойки |
asinh(x) acosh(x) atanh(x) | 1 | обратные гиперболические |
hypot(x, y) | 2 | длина вектора, без переполнения по дороге |
hash(n, salt) | 2 | воспроизводимое значение в [0, 1) от пары чисел |
noise(t, scale, salt) | 3 | плавный дрейф: новое значение каждые scale строк, со сглаживанием |
gauss(x, c, w) | 3 | колокол с центром в c: exp(-((x - c) / w)²) |
clamp(x, lo, hi) | 3 | x, удержанный в [lo, hi]; при lo > hi побеждает потолок |
lerp(a, b, t) | 3 | доля t пути от a к b; точен на обоих концах, вне отрезка экстраполирует |
sign(x) | 1 | −1, 0 или 1 |
erf(x) erfc(x) | 1 | функция ошибок и её дополнение |
gamma(x) lgamma(x) | 1 | Γ(x) и log |Γ(x)| — на случай переполнения Γ |
beta(a, b) | 2 | Γ(a)Γ(b)/Γ(a+b) |
digamma(x) | 1 | ψ(x), производная от log Γ |
zeta(s) | 1 | дзета-функция Римана, для вещественного s |
degrees(x) radians(x) | 1 | между двумя способами записать угол |
prev(Column, initial) | 2 | эта колонка на строку назад — нужен mode="sequential", см. ниже |
Операторы — сравнения, логические связки и арифметика — точные: собраны из того, что IEEE-754 задаёт однозначно, поэтому пять реализаций не могут о них разойтись. А ФУНКЦИИ из таблицы выше TDC считает сам — потому у каждой и стоит граница точности, а не обещание точного ответа.
round отправляет половину от нуля. round(0.5) это 1, а round(-0.5) это −1.
JavaScript округляет половину к +∞, Python — к чётному, Java — вверх: три хозяина, три
ответа, ни одного симметричного. TDC называет своё правило, чтобы колонка отрицательных вела
себя как колонка положительных.
len считает кодовые точки. len("😀") это 1, а не 2, которые дал бы UTF-16, — но
семейный эмодзи, собранный из нескольких кодовых точек, посчитается как несколько. Графемы
были бы человеческим ответом, но им нужна таблица сегментации Unicode, которую не всякая
реализация может нести, — поэтому побеждает переносимая единица. len("10") это 2: строковая
функция читает аргумент как текст, а не как число.
Повторяемая случайность и формы, в которые её кладут
Пять функций из таблицы выше существуют ради одной задачи: построить величину, которая МЕНЯЕТСЯ так, как меняются настоящие данные, ничего при этом не разыгрывая. Каждая — это обычный расчёт над своими аргументами, поэтому одни и те же аргументы всегда дают один и тот же ответ, в любой из пяти реализаций.
hash(n, salt) — повторяемое значение из [0, 1) по паре чисел
То, за чем тянешься, когда каждой строке нужен свой коэффициент:
<tdc><env count="5" seed="w">
<sequence name="N"><gen type="increment" value="1"/></sequence>
<sequence name="Amp"><gen type="formula" decimals="4"
expr="1.4 * (0.94 + 0.12 * hash(N, 12.9))"/></sequence>
</env><block><line><data>${{N}},${{Amp}}</data></line></block></tdc>
1,1.3862
2,1.4128
3,1.4559
4,1.4518
5,1.4734
salt — то, чем различаются две колонки одной строки: hash(N, 1) и hash(N, 2) —
несвязанные последовательности, так что одна строка может нести несколько независимых
коэффициентов без второго зерна.
Функция заменяет шейдерный трюк — sin(n * 12.9898) * 43758.5453 минус целая часть, — к
которому обычно тянутся. У нас этот трюк безопасен: TDC считает свой sin сам, и пять
реализаций сходятся до последнего бита. Цена его — два трансцендентных вызова на строку,
считаемых программно, и строка, которую никто не прочтёт. hash — целочисленная
арифметика, и он говорит, что он такое.
noise(t, scale, salt) — плавный дрейф
Свежее значение каждые scale строк, между ними — сглаживание. Именно так и выглядит
блуждающая базовая линия, и три модулированных синуса так не умеют: на 4096 отсчётах
синусы кладут 74.6% мощности в три частотные корзины, где это кладёт 51.5%.
<sequence name="Drift"><gen type="formula" expr="0.3 * (noise(_count, 300, 7) - 0.5)"/></sequence>
Два свойства, на которые можно опираться, — оба точные, а не приблизительные:
- В узле решётки значение И ЕСТЬ
hashв этой точке.noise(k * scale, scale, salt)равенhash(k, salt)— теми же битами, а не «с точностью до ulp», — потому что сглаживание интерполирует какa * (1 - u) + b * u. Значит, граница ячейки непрерывна. - Середина ячейки — обычное среднее её концов.
scale, равный нулю, даёт деление на ноль и NaN — тот же ответ, что здесь даёт
sqrt(-1). В колонке это не печатается, а вызывает отказ по имени: файл, полный NaN, о
которых никого не предупредили, хуже прогона, который остановился.
gauss(x, c, w) — колокол с центром в c
exp(-((x - c) / w)²): единица в центре, спад в обе стороны, w задаёт ширину.
<tdc><env count="7" seed="w">
<sequence name="X"><gen type="increment" value="1"/></sequence>
<sequence name="G"><gen type="formula" decimals="4" expr="gauss(X, 4, 1.5)"/></sequence>
</env><block><line><data>${{X}},${{G}}</data></line></block></tdc>
1,0.0183
2,0.1690
3,0.6412
4,1.0000
5,0.6412
6,0.1690
7,0.0183
Это не плотность вероятности — она не нормирована к единичному интегралу. Это ФОРМА, на которую умножают что-то другое.
clamp(x, lo, hi) — удержать значение в границах
Отбойник для всего, что накапливается. Если lo больше hi, побеждает потолок:
clamp(x, 9, 2) равен 2 при любом x. Порядок важен, и он оговорён, а не отдан на волю
того, какое сравнение реализация выполнит первым: однажды эталон и четыре порта об этом
не сошлись, и поймала это общая фикстура.
lerp(a, b, t) — доля t пути от a к b
Точно попадает в оба конца: lerp(10, 20, 0) — ровно 10, lerp(10, 20, 1) — ровно
20. Ради этого функция и существует, вместо записи от руки: естественная запись
a + (b - a) * t промахивается мимо b при t = 1 в 41% из 200 000 случайных пар,
потому что вычитание теряет младшие биты, а сложение их не возвращает. Здесь считается
a * (1 - t) + b * t, который потерять не может.
За пределами [0, 1] экстраполирует, а не отказывает: lerp(10, 20, 2) — это 30. Значение,
выехавшее за границы, обычно арифметика вызывающего, а не ошибка, а clamp рядом на тот
случай, когда всё-таки ошибка.
Колонка, читающая собственное прошлое
Все функции выше отвечают из ОДНОЙ строки. prev(Колонка, начальное) — исключение: она
читает Колонку из предыдущей строки, а на первой отдаёт начальное. Это то, что нужно
случайному блужданию — каждая строка есть предыдущая плюс шаг, — и то, чего выражения
раньше сказать не могли.
<env count="3600" seed="p001" mode="sequential">
<sequence name="RR">
<gen type="formula" decimals="3"
expr="clamp(prev(RR, 700) + (hash(_count, 3) - 0.5) * 180, 350, 1400)"/>
</sequence>
</env>
prev берёт ИМЯ колонки, невычисленным. Написав RR в любом другом месте того же
выражения, вы получите значение ЭТОЙ строки — ровно то, мимо чего prev и смотрит.
Нужен mode="sequential" у <env>, и без него движок скажет об этом. Режим — это
обещание, что строка N считается после строки N−1. Без него движок вправе получить любую
строку, не трогая предыдущую (так он и выдаёт прогон больше памяти), а значит prev()
читал бы строку, которой ещё нет.
mode="sequential" и order="sequential" — разные вещиorder="sequential" у <gen> идёт по значениям генератора в том порядке, в каком они
написаны, вместо случайного выбора. Это про значения ОДНОЙ колонки.
mode="sequential" у <env> — про весь ПРОГОН: строки считаются одна за другой.
Слово совпадает, предмет — нет.
Чего режим стоит и в чём отказывает:
- Прогон держится в памяти (движок 1), поколоночные оптимизации выключены. Это честная цена колонки, читающей своё прошлое, и платит её только тот, кто попросил.
mode="sequential"вместе сengine="2"илиengine="3"— отказ, называющий оба атрибута. Те движки получают любую строку без предыдущей, это их устройство, и оба условия сразу выполнить нельзя.prev()внутриif=тоже отказ.if=— выбор, делаемый построчно, и движок, отвечающий на него, вправе брать строки в любом порядке.
Всё остальное рядом работает: percent=, uniq и distinct не затронуты — колонки
регистрируются в порядке объявления, а это тот же порядок, на котором стоит prev().
Списки внутри строки
Последовательность с repeat= кладёт в одно поле несколько значений, склеенных её
separator=. Выражение видит склеенный текст — именно он и лежит в поле, — поэтому
split и есть тот мост, после которого список становится списком.
<sequence name="Prices">
<gen type="number" value="10..200" repeat="3" separator=","/>
</sequence>
<sequence name="Basket">
<gen if="sum(split(Prices, ',')) > 300" type="text" value="large"/>
<gen type="text" value="ordinary"/>
</sequence>
min и max одинаково читают и список, и россыпь аргументов: работает и
max(split(Prices, ',')), и max(1, 9, 4). Пустой разделитель режет на отдельные символы —
ту же единицу считает len, — поэтому count(split(s, '')) и len(s) никогда не разойдутся.
at считает с нуля и отказывается от индекса, который индексом не являетсяat(list, 0) — первый элемент. За концом списка — пустой текст, и это намеренно:
repeat="1..4" специально делает строки разной длины, и вопрос про третий элемент строки из
двух — настоящий вопрос с пустым ответом. Чтобы спросить заранее, есть count(list).
Всё остальное — отказ, а не та же самая пустая строка: отрицательный индекс, дробный, не число и подлежащее, которое ни разу не резали. Последнее пишут первым делом все —
<gen if="at(Prices, 1) > 100" …/> <!-- отказ: TDC260 -->
<gen if="at(split(Prices, ','), 1) > 100" …/> <!-- что имелось в виду -->
Prices — это склеенный текст, поэтому первая строка просила второй элемент списка из
одного и раньше давала пустую колонку, а запуск при этом сообщал об успехе. Написанные
прямо ошибки ловит tdcv2 check ещё до первой строки; индекс, который считается на ходу —
at(list, _count - 1), — проверяется при сборке строки.
Почему TDC считает трансцендентные функции сам
IEEE-754 задаёт для + - * / и sqrt ровно один допустимый ответ, поэтому о них все языки
договариваются. О sin, cos, exp, log и pow он не говорит ничего — каждая libm
выбирает свой алгоритм, — и расхождение измеримо, а не умозрительно:
tan(1) | |
|---|---|
| Node | 3ff8eb245cbee3a6 |
| Python | 3ff8eb245cbee3a5 |
Из семидесяти семи взятых значений шестнадцать расходятся хотя бы у двух реализаций из пяти.
В timeseries этого не видно: каждое число округляется до десятичной строки, прежде чем стать
выводом, — последний бит умирает на выходе. У сравнения шага округления нет, поэтому этот бит
превращается в другую строку и другой файл — у инструмента, всё обещание которого в том, что
пять реализаций дают одни и те же байты.
Поэтому TDC считает их сам — так же, как он уже сам считает случайные числа, вместо того чтобы доверять каждому языку. Каждая ложится в пределах 4 ulp от истинного значения — там же, где живёт и libm, — и, что куда важнее, на один и тот же double во всех пяти. Совпасть с какой-то конкретной libm — не цель и не могло ею быть: libm'ы не совпадают между собой.
Эта четвёрка проверяется, а не заявляется, — на сетках, которые доходят до краёв области определения. Края и важны: ряд, оборванный на два члена раньше, в середине интервала незаметен, а на краю даёт тринадцать ulp. Ровно эту ошибку проверка и нашла.
У pow граница шире, и причину стоит знать:
| показатель | как считается | расхождение |
|---|---|---|
| целый или половинный | возведение в квадрат, для половины — sqrt | растёт с показателем: ~4 ulp при 3, ~22 при 20 |
| любой другой | exp(y · log x) | растёт с |y · log x|: ~2 ulp при 1, ~457 при 400 |
И то и другое — усиление, а не дефект: возведение в квадрат удваивает полученную ошибку, а exp
превращает абсолютную ошибку аргумента в относительную ошибку ответа. Двенадцать значащих цифр
выживают в обоих случаях.
<sequence name="Month"><gen type="increment" value="1"/></sequence>
<sequence name="Load">
<gen if="cos(Month / 2) > 0.5" type="text" value="peak"/>
<gen if="cos(Month / 2) < -0.5" type="text" value="trough"/>
<gen type="text" value="normal"/>
</sequence>
<sequence name="Tier">
<gen if="pow(2, Month) > 100" type="text" value="large"/>
<gen type="text" value="small"/>
</sequence>
1 peak small 2 peak small 3 normal small 4 normal small 5 trough small 6 trough small 7 trough large 8 trough large
Этот файл прогнали через все пять реализаций, и каждая выдала эти самые байты.
Из того, что функции свои, а не заимствованные, следуют две вещи. pow с целым показателем
идёт через возведение в квадрат, поэтому pow(10, 3) — ровно 1000, а не 999.9999999999998;
конфиг, сравнивающий с круглым числом, разницу бы заметил. И тригонометрия берёт радианы,
градусного варианта нет: одно соглашение, названное один раз.
Пара, которая существует потому, что вычитание теряет
expm1 и log1p — не сокращения для exp(x) - 1 и log(1 + x). Это те же выражения, но
посчитанные так, чтобы ответ уцелел:
expm1(1e-20) 1e-20 exp(1e-20) - 1 0 log1p(1e-20) 1e-20 log(1 + 1e-20) 0
Второй столбец — не погрешность округления, это весь ответ целиком. 1 + 1e-20 как double
РАВНО единице, поэтому логарифм аргумента вообще не видит; а exp(1e-20) равно 1.0000…, и
вычитание съедает все значащие цифры. asinh и atanh построены на log1p по той же причине и
наследуют его точность.
hypot спасает от зеркальной беды на другом конце диапазона: sqrt(x² + y²) переполняется в
бесконечность при x = 10²⁰⁰, хотя ответ прекрасно представим. А log2 отделяет экспоненту до
того, как берёт логарифм, поэтому log2(8) — это 3, а не 2.9999999999999996.
Где граница перестаёт измеряться в ulp
У этих четырёх граница не сводится к «в пределах 4 ulp», и это часть справки, а не сноска:
| Функция | Что гарантируется |
|---|---|
erf | в пределах 4 ulp |
erfc | в пределах 8 ulp — она проходит через e^(−x²), а эта экспонента несёт округление квадрата |
gamma | точно на целых до 23, в пределах 7 ulp на всех 171, что влезают в double; двенадцать значащих цифр в остальном |
lgamma | в пределах 32 ulp вдали от нулей; при x = 1 и x = 2 осмысленная граница абсолютная, меньше 10⁻¹³ |
Интересен здесь lgamma. Он равен нулю в 1 и в 2, и никакой метод, складывающий
слагаемые порядка единицы, не может быть относительно точен в том, что они
сократились в ноль — заявление там обязано быть абсолютным, и оно такое. Оба нуля
выходят ровно нулями.
gamma вне целых заканчивается экспонентой, поэтому расхождение растёт вместе с
log Γ(x) — то же усиление, что и у pow, по той же причине. Ради этого целое
число и идёт факториальным путём.
Чего нет намеренно
Математика, которой генератору данных нечего делать. besselj, bessely, airy,
elliptic_k, elliptic_e и polygamma отвергаются по имени, а не угадываются:
error[TDC257]: besselj() is not available yet in an if expression
Обратите внимание, чего сообщение НЕ говорит: «может, вы имели в виду beta?» Расстояние
редактирования предложило бы именно это, а exp — совсем не функция ошибок. Имя из
этого списка получает в ответ причину, а не догадку.
Каждая из них — проект, а не функция, и ни одна никогда всерьёз не относилась к предикату строки. Они остаются в списке, чтобы человек, потянувшийся за одной из них, получил ответ, а не «неизвестная функция».
Циклы и рекурсия. Движок выбирается по конфигу до генерации первой строки,
preflight() оценивает память до прогона, а --jobs режет
строки между воркерами. Всем троим нужно знать работу на строку, не выполняя её. Цикл ломает
все три сразу, а то, ради чего к циклу тянутся — «чётная ли это строка?», — это %.
Побитовые операторы. _count & 1 — это _count % 2, записанное для машины. Они
разбираются, чтобы сообщение могло назвать их по имени, и затем отвергаются.
Когда выражения не хватает
У последовательности <compute> есть целочисленное деление, остатки, работа со
строками, кодировки и контрольные цифры. Она даёт значение, как любая другая
последовательность, и if= затем сравнивает уже его:
<sequence name="Checksum">
<compute><result><mod><to_number><field name="Account"/></to_number><int v="97"/></mod></result></compute>
</sequence>
<sequence name="Flag">
<gen if="Checksum == 0" type="text" value="divisible"/>
<gen type="text" value="."/>
</sequence>
Конфиг похож на XML, но это не XML
TDC не разворачивает сущности, поэтому < — это четыре обычных символа, а не <. Пишите
символ напрямую:
<gen if="Weight > 20" .../> <!-- да -->
<gen if="Weight > 20" .../> <!-- нет: TDC103, сущность — это четыре символа -->