Мы привыкли думать о вычислениях как о битах, регистрах и арифметике. А что, если базовой единицей вычисления сделать не бит, а локальную геометрическую конфигурацию тетраэдров? В этой статье я покажу дискретный тетраэдрический движок состояний, симметрийную канонизацию, аттракторы, иерархические jump-таблицы и реальные замеры на RTX 3090 — с измеренным exact-ускорением в 554.92 раза на одной и той же задаче.
1. Зачем вообще считать не битами, а тетраэдрами
Обычная вычислительная логика устроена очень просто: есть биты 0/1, есть операции над ними, есть длинные цепочки преобразований.
Но можно посмотреть на вычисление иначе: не как на манипуляцию отдельными битами, а как на эволюцию локальных геометрических конфигураций.
Вместо «ячейка памяти хранит 0 или 1» можно взять более богатую сущность:
положение объекта,
ориентацию,
фазу,
допустимые симметрии,
правила локального преобразования.
В моей модели такой сущностью стал локальный кластер тетраэдров.
Бит — это лампочка: либо включена, либо выключена.
Тетраэдрический мотив — это уже маленькая механическая конструкция, у которой есть:
как она стоит,
куда повернута,
в каком состо��нии находится,
как она может перестроиться дальше.
То есть одна «единица состояния» становится намного богаче обычного бита.
2. Почему именно тетраэдр
Тетраэдр — это простейшее трёхмерное тело с богатой симметрией:
4 вершины,
4 грани,
6 рёбер.
У него достаточно сложная геометрия, чтобы порождать интересные классы состояний, но при этом он ещё достаточно прост, чтобы всё это можно было эффективно кодировать.
В вычислительном смысле тетраэдр удобен по трём причинам:
у него компактная группа симметрий;
локальное состояние можно дискретизировать;
последовательности преобразований можно предвычислять.
Тетраэдр — это «минимальная интересная 3D-фигура». Куб слишком привычный. Треугольник — уже не 3D. А тетраэдр — это маленькая 3D-ячейка, на которой уже можно строить нетривиальную «механику состояний».
3. Как устроено состояние в движке
В моей модели локальный кластер устроен так:
есть 4 слота;
в них находятся 3 тетраэдра;
1 слот пустой.
Каждый занятый слот хранит локальное состояние тетраэдра:
ориентация: 0..11,
фаза: 0..2.
Итого одно локальное состояние тетраэдра:
[ 12 × 3 = 36 ] вариантов.
Пустой слот кодируется отдельно.
То есть полный raw-state кластера — это конфигурация вида:
4 позиции,
из них 3 заняты,
в каждой занятой позиции — один из 36 вариантов.
Представьте 4 ячейки, как будто 4 места вокруг маленькой геометрической конструкции. На трёх местах стоят тетраэдры, одно место пустое. Каждый тетраэдр может:
стоять в одной из 12 ориентаций,
быть в одной из 3 фаз.
Из этого собирается локальная «сцена».
4. Сколько вообще таких состояний
Если не учитывать симметрии, то количество raw-state равно:
выбор пустого слота: 4,
три заполненных слота, в каждом по 36 вариантов.
[ 4 · 36^3 = 186,624 ]
Именно это и показал код:
Raw states: 186 624
Но это ещё не настоящие рабочие состояния, потому что многие из них эквивалентны с точностью до вращения тетраэдрической схемы.
Если просто «в лоб» перечислить все варианты, их почти 187 тысяч. Но среди них много одинаковых по сути, просто повернутых в пространстве. Как если бы вы посмотрели на один и тот же предмет с другой стороны — сам предмет не изменился, изменился только ракурс.
5. Симметрийная канонизация: как 186 тысяч состояний превращаются в 15 тысяч
Это один из ключевых шагов.
Мы берём группу вращений тетраэдра. Для неё я использовал 12 вращений — это ориентационно-сохраняющая группа симметрий тетраэдра, изоморфная A₄.
Для каждого raw-state мы:
применяем все допустимые симметрии;
получаем множество эквивалентных конфигураций;
выбираем канонический представитель, например, лексикографически минимальный packed-state.
Так raw-state схлопываются в классы эквивалентности.
Результат:
Canonical motifs: 15 576 Symmetry count: 12
То есть:[ 186,624 → 15,576 ]
Это уже серьёзное сжатие пространства состояний.
Мы перестаём хранить «одну и ту же фигуру, повёрнутую по-разному» как разные состояния. Мы говорим: у этой кон��игурации есть «канонический вид», и всё приводим к нему. Это примерно как вместо «кошка лицом влево» и «кошка лицом вправо» хранить просто «кошка», если для задачи поворот не важен.
6. Что такое мотив
После канонизации базовой единицей вычисления становится motif — канонический локальный класс конфигурации.
То есть дальше движок работает уже не с произвольными «сырыми» состояниями, а с компактным идентификатором мотива:
[ \text{motifId} \in [0,15575] ]
Именно motif становится базовой «единицей вычисления».
После всей уборки и удаления дубликатов остаётся не 186 тысяч хаотичных вариантов, а 15 576 «типовых конструкций». Вот эти типовые конструкции и есть «слова» нашего геометрического языка.
7. Что такое генераторы преобразований
Дальше нужен механизм вычисления — то есть правила, как одна конфигурация переходит в другую.
Для этого вводятся 4 генератора:
[ G_0, G_1, G_2, G_3 ]
Каждый генератор делает три вещи:
переставляет слоты;
двигает один тетраэдр в пустую позицию;
изменяет его ориентацию и фазу;
слегка обновляет состояния остальных тетраэдров.
Фактически это локальная геометрическая операция переписывания.
Генератор — это как «одна маленькая команда»: что-то повернуть, что-то сдвинуть, пустое место поменять, обновить локальные состояния. То есть не арифметика, а маленькая геометрическая перестройка.
8. Что такое путь
Одна команда — это хорошо, но вычисление обычно состоит из последовательности команд.
Поэтому вводится путь — короткое слово над генераторами.
Например, базовый путь у меня был такой:
[ (2,2,1,2,2) ]
То есть:[ F = G_2 \circ G_2 \circ G_1 \circ G_2 \circ G_2 ]
Это уже не единичное движение, а маленькая программа.
Во второй и третьей версиях движка я использовал уже 8 разных путей:
pathId=0 [2,2,1,2,2] pathId=1 [0,1,2,3,0] pathId=2 [3,2,1,0,3] pathId=3 [1,1,1,1,1] pathId=4 [0,2,0,2,1] pathId=5 [3,3,0,1,2] pathId=6 [1,3,2,1,0] pathId=7 [2,0,3,1,2]
Если генератор — это одна команда, то путь — это короткий скрипт из 5 команд. То есть мы уже не просто «щёлкаем одной операцией», а задаём маленькую программу над геометрической системой.
9. Как происходит вычисление «в лоб»
Если делать всё напрямую, то для каждого мотива и каждого пути нужно последовательно применить все генераторы в слове.
Если путь повторяется много раз, например F⁶⁴, то приходится выполнять длинную це��очку переходов.
Именно так работает direct-режим.
Для случая F⁶⁴ и 4 млн элементов результаты такие:
Direct random time: 17,28 ms Direct grouped time: 10,10 ms
Это «честный тупой способ»: берём состояние, 5 раз применяем команды пути, потом ещё 64 раза повторяем весь путь, и так для миллионов элементов. Понятно, что это работает, но дорого.
10. Главная идея ускорения: предвычислить длинные композиции
Вот здесь начинается самое интересное.
Если путь фиксирован, то можно заранее вычислить, куда он переводит каждый мотив:
[ \text{motif} \to F(\text{motif}) ]
Это таблица J0 = F¹.
Но можно пойти дальше и заранее вычислить:
J1 = F⁶⁴
J2 = F⁴⁰⁹⁶
J3 = F²⁶²¹⁴⁴
J4 = F¹⁶⁷⁷⁷²¹⁶
То есть очень длинная программа превращается в один lookup. Именно это и есть jump-таблицы.
Вместо того чтобы каждый раз снова и снова выполнять длинную последовательность команд, мы делаем как в математике с таблицей степеней: заранее знаем, что получится после 64 шагов, после 4096 шагов, после 262 тысяч шагов, после 16.7 миллионов шагов. Тогда вычисление превращается в «сразу взять готовый ответ из таблицы».
11. Иерархия jump-таблиц
В base-64 варианте иерархия такая:
J0: F^1 J1: F^64 J2: F^4 096 J3: F^262 144 J4: F^16 777 216
Это означает, что:
J1 эквивалентен 64 применениям J0,
J2 эквивалентен 64 применениям J1,
и так далее.
Результат — очень глубокая композиция кодируется одной табличной операцией.
Это как матрёшка уровней ускорения: один уровень знает 1 шаг, следующий — 64 шага, следующий — уже 4096 шагов, и так дальше. Чем выше уровень, тем длиннее программа, спрятанная в одном lookup.
12. Почему понадобился ещё и binary jump hierarchy
Base-64 отлично работает для степеней 64, но плохо подходит для произвольного числа повторов.
Для этого была построена ещё и бинарная иерархия:
B0: F^1 B1: F^2 B2: F^4 B3: F^8 ... B24: F^16 777 216
Тогда любое число повторов N можно разложить по двоичным степеням и применить нужные jump-уровни. Это даёт лучший arbitrary-режим.
И код подтвердил это на практике:
Base64 arbitrary time: 3,86 ms Binary arbitrary time: 1,84 ms Binary arbitrary: 2,10x vs base64-arbitrary
Если нужно сделать не «ровно 64 в степени k», а какое-то произвольное число шагов, например 12 345 678, удобнее раскладывать не по 64, а по двоичным кускам: 1, 2, 4, 8, 16... Это стандартная идея «быстрого возведения», только применённая к геометрическим переходам.
13. Аттракторы: почему разные мотивы сходятся к одним и тем же циклам
Если взять отображение
[ \text{motif} \to F(\text{motif}) ]
то оно задаёт функциональный граф.
У такого графа каждая вершина имеет ровно одно исходящее ребро, а значит вся система распадается на:
хвосты,
циклы,
бассейны притяжения этих циклов.
Именно это и даёт аттракторную структуру.
Для разных путей картина оказалась очень разной. Например:
pathId=0 [2,2,1,2,2] | cycles=2, maxCycle=20, avgSteps=66,52, dominantBasin=15 486 (99,42%) pathId=5 [3,3,0,1,2] | cycles=3, maxCycle=145, avgSteps=26,21, dominantBasin=15 504 (99,54%) pathId=3 [1,1,1,1,1] | cycles=17, maxCycle=74, avgSteps=20,13, dominantBasin=7 913 (50,80%)
Если много раз применять один и тот же путь, система обычно не гуляет бесконечно куда попало. Она либо приходит в цикл, либо в маленькое семейство устойчивых режимов. Это как шарик, катящийся по поверхности: почти всегда он попадает в какую-то «ямку» или начинает крутиться по устойчивой траектории.
14. Что значат разные типы путей
Пути оказались не просто разными, а семантически разными.
Сильно сжимающие пути
Например, path 0 и path 5: почти все состояния затягиваются в один доминирующий basin. Это похоже на нормализацию или приведение к канонической форме.
Более выразительные пути
Например, path 3, path 2, path 6, path 7: больше циклов, больше разнообразия, меньше «схлопывание». Такие пути ближе к богатой логике состояний.
Какие-то пути работают как «пылесос» — почти всё засасывают в один режим. А какие-то пути больше похожи на «разветвитель» — сохраняют разнообразие и дают системе больше вариантов поведения. То есть пути начинают вести себя как разные инструкции процессора.
15. Packed metadata: как аттракторную информацию упаковали в один int
Для каждого (pathId, motifId) можно знать:
представителя цикла,
длину цикла,
число шагов до цикла.
Чтобы не читать несколько буферов, я упаковал это в один 32-битный integer:
representative: 14 бит,
cycleLength: 8 бит,
stepsToCycle: 10 бит.
Это уменьшает память и сокращает число чтений.
Вместо того чтобы хранить 3 разных числа в 3 разных местах, мы складываем их в одну компактную коробку из 32 бит. Меньше памяти — быстрее lookup.
16. Fused kernel: почему лучше сразу делать и jump, и attractor lookup
Отдельные проходы по памяти — дорого.
Поэтому был сделан fused-режим, который за один dispatch делает:
jump;
lookup packed attractor metadata.
То есть pipeline стал таким:
[ (\text{motifId},\text{pathId}) \to \text{newMotifId} + \text{attractorMeta} ]
Практически это оказалось очень удачным решением.
Вместо двух походов в память мы идём один раз: сразу прыгаем в итоговое состояние, сразу забираем всё, что нужно знать о его аттракторе. Это как сходить в магазин один раз и купить всё сразу, а не идти сначала за хлебом, потом отдельно за молоком.
17. Почему grouped pathId оказался быстрее random pathId
В direct-режиме путь для каждого элемента выбирается по pathId.
Если pathId случайный, потоки GPU идут по более «рваным» паттернам доступа. Если pathId сгруппирован, потоки становятся более согласованными.
Результат:
Direct random time: 17,28 ms Direct grouped time: 10,10 ms
То есть grouped scheduling даёт ускорение в 1.71x.
Для fused-режима эффект гораздо слабее:
Fused J1 random time: 1,85 ms Fused J1 grouped time: 1,74 ms
Это значит, что jump/fused-режим значительно устойчивее к path divergence.
Если соседние потоки делают похожую работу, GPU это любит. Если каждый поток живёт своей жизнью, начинаются потери. Поэтому «сортировать задачи по типу пути» — это хорошая идея, особенно для direct-режима.
18. Результаты по скорости
Вот ключевые результаты замеров.
Базовые тесты
Standard compute: 3,60 ms Direct F^64 random: 17,28 ms Direct F^64 grouped: 10,10 ms Jump J1 random: 1,62 ms Fused J1 random: 1,85 ms Fused J1 grouped: 1,74 ms Base64 arbitrary: 3,86 ms Binary arbitrary: 1,84 ms Base64 exact deep: 1,31 ms Binary exact deep: 1,66 ms
Простыми словами
Что это значит:
прямой пересчёт длинных путей — медленный;
jump-таблицы — очень быстрые;
fused-режим почти не уступает чистому jump;
binary лучше для произвольной глубины;
base64 лучше для точных степеней 64.
19. Что оказалось лучше для каких задач
Для F⁶⁴
Лучший режим — jump или fused jump.
Jump J1 random: 1,62 ms Fused J1 random: 1,85 ms
Против direct random:
17,28 ms
Для arbitrary repeat
Лучший режим — binary decomposition.
Binary arbitrary: 1,84 ms vs Base64 arbitrary: 3,86 ms
Для exact powers of 64
Лучший режим — base64 exact jump.
Base64 exact deep: 1,31 ms vs Binary exact deep: 1,66 ms
Если сильно упростить:
ровные большие степени → бери base64;
любое произвольное число шагов → бери binary;
нужна ещё информация про аттрактор → бери fused.
20. Самый важный тест: реальная точная задача на 30 секунд
Чтобы уйти от «оценок по линейной экстраполяции», я сделал реальный exact benchmark на 30 секунд.
Задача была одна и та же:
[ F^{4096} ]
Считалась двумя способами:
Direct exact: 64 последовательных запуска F⁶⁴.
Jump exact: 1 запуск J2 = F⁴⁰⁹⁶.
Обе реализации вычисляют одну и ту же точную функцию.
Результаты:
DIRECT EXACT F^4096 Time: 30,69 s Batches completed: 41 Exact depth per batch: F^4 096 Batches/sec: 1,3359 Path applications/sec: 21 887 824 042 Generator applications/sec: 109 439 120 208 JUMP EXACT F^4096 Time: 30,00 s Batches completed: 22 240 Exact depth per batch: F^4 096 Batches/sec: 741,3308 Path applications/sec: 12 145 964 037 056 Generator applications/sec: 60 729 820 185 278 Measured exact throughput speedup: 554,92x
Мы не сравниваем «примерно похожие штуки». Мы сравниваем одну и ту же задачу: либо выполнять её длинной прямой цепочкой, либо взять уже сжатый jump. И вот тут ускорение получилось 554.92x. То есть не «в теории», а реально, по секундомеру.
21. Почему 554.92x важнее, чем «миллионы раз по оценке»
Ранее по более глубоким jump-уровням можно было получить очень большие оценочные факторы, например миллионы раз, если линейно экстраполировать direct на F¹⁶·⁷⁷⁷·²¹⁶. Но это всё равно остаётся оценкой.
А вот ускорение в 554.92x на F⁴⁰⁹⁶ — это уже измеренный exact speedup.
22. За счёт чего вообще получается такая скорость
Теперь главный вопрос: почему это всё так быстро работает?
Причина №1. Мы считаем не арифметику, а переходы между классами состояний
Вместо длинной последовательности операций над числами мы работаем с компактными motif-id.
Простыми словами: Мы не считаем формулы в лоб, а ходим по карте уже известных состояний.
Причина №2. Симметрии сильно уменьшают пространство
Raw-state: 186,624 → Canonical motifs: 15,576.
Простыми словами: Мы избавились от огромного количества дубликатов.
Причина №3. Длинные программы предвычислены
F⁴⁰⁹⁶ превращается не в 4096 повторов, а в один lookup.
Простыми словами: Мы не повторяем длинную работу, а достаём уже готовый итог.
Причина №4. GPU хорошо любит табличные переходы малого размера
Все ключевые таблицы маленькие по современным меркам:
Generator table: ~0.24 MB
Base64 jump tables: ~2.38 MB
Packed attractor metadata: ~0.48 MB
Binary jump tables: ~11.88 MB
Простыми словами: Таблицы достаточно компактные, чтобы GPU быстро с ними работал.
Причина №5. Fused kernel уменьшает число проходов по памяти
Jump + attractor metadata делаются за один проход.
Простыми словами: Меньше бегаем туда-сюда по памяти — быстрее работаем.
Причина №6. Для arbitrary repeat используется правильная иерархия
Binary decomposition дала ускорение в 2.10x относительно base64 decomposition для произвольной глубины.
Простыми словами: Для разных типов задач нужен разный «способ прыжка». Мы это учли.
23. Что это НЕ означает
Чтобы остаться честными, нужно сказать и о границах метода.
Это не «ускорение любых вычислений»
Метод ускоряет длинные композиции в дискретной геометрической системе, а не произвольные задачи мира.
Простыми словами: Это не «новый суперкомпьютер для всего подряд», а очень сильный ускоритель для определённого класса вычислений.
Это не замена криптографии
Такой движок не заменяет SHA-256 и не делает валидный Bitcoin mining.
Простыми словами: Биткоин всё равно требует свой хеш, и сеть не примет «тетраэдрический ответ».
Это не магия
Основной выигрыш возникает потому, что вычисление имеет структуру, а структура позволяет сделать jump-таблицы.
Простыми словами: Если задача не имеет повторяемых переходов и симметрий, такого ускорения не будет.
24. Что это уже означает на практике
Зато вот что действительно означает:
У нас есть рабочий дискретный геометрический вычислитель:
богатая единица состояния;
симметрийная канонизация;
множество путей;
attractor-структура;
jump-компрессия.
У нас есть зачаток instruction set. Разные пути ведут себя по-разному: одни сильно сжимают пространство, другие дают богатую динамику, третьи создают длинные циклы.
У нас есть runtime-стратегия:
exact powers of 64 → base64 jump;
arbitrary repeats → binary jump;
metadata → fused;
path grouping → для лучшего scheduler behavior.
Итог
Если формулировать вывод максимально честно и точно, то он такой:
В этой работе я построил дискретный тетраэдрический вычислительный движок, в котором базовой единицей является не бит, а канонический геометрический мотив.
За счёт:
симметрийной канонизации,
предвычисления переходов,
иерархических jump-таблиц,
packed attractor metadata,
fused GPU-kernel,
и правильного выбора стратегии исполнения,
удалось получить измеренное exact-ускорение в 554.92x на одной и той же задаче F⁴⁰⁹⁶ на RTX 3090.
А также показать, что:
base64 jump лучше для точных степеней,
binary jump лучше для произвольной глубины,
разные пути формируют разные attractor-профили, а значит у такой системы уже появляется вычислительная семантика.
Главная мысль статьи очень простая: если считать не битами, а геометрическими состояниями, и заранее знать, как длинные цепочки преобразований схлопываются, то можно получить очень серьёзное ускорение на точных задачах.
Код программы:
Листинг кода
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using ComputeSharp; namespace TetraCombinationEngineV3 { internal static class Config { // ---------------------------------------------------- // Дискретная тетра-кодировка // ---------------------------------------------------- public const int SLOT_COUNT = 4; public const int OCCUPIED_COUNT = 3; public const int ORIENTATION_COUNT = 12; public const int PHASE_COUNT = 3; public const int LOCAL_STATE_COUNT = ORIENTATION_COUNT * PHASE_COUNT; // 36 public const int EMPTY = LOCAL_STATE_COUNT; // 36 public const int PACK_BASE = LOCAL_STATE_COUNT + 1; // 37 // ---------------------------------------------------- // Paths // ---------------------------------------------------- public const int PATH_LENGTH = 5; public static readonly int[][] PATHS = { new[] { 2,2,1,2,2 }, new[] { 0,1,2,3,0 }, new[] { 3,2,1,0,3 }, new[] { 1,1,1,1,1 }, new[] { 0,2,0,2,1 }, new[] { 3,3,0,1,2 }, new[] { 1,3,2,1,0 }, new[] { 2,0,3,1,2 } }; public static readonly string[] PATH_NAMES = { "2,2,1,2,2", "0,1,2,3,0", "3,2,1,0,3", "1,1,1,1,1", "0,2,0,2,1", "3,3,0,1,2", "1,3,2,1,0", "2,0,3,1,2" }; public static int PATH_COUNT => PATHS.Length; // ---------------------------------------------------- // Base-64 hierarchy // ---------------------------------------------------- // J0 = F^1 // J1 = F^64 // J2 = F^4096 // J3 = F^262144 // J4 = F^16777216 public const int BASE64_JUMP_BASE = 64; public const int BASE64_LEVELS = 5; // ---------------------------------------------------- // Binary hierarchy // ---------------------------------------------------- // B0 = F^1 // B1 = F^2 // ... // B24 = F^16777216 public const int BINARY_LEVELS = 25; // ---------------------------------------------------- // Packed attractor metadata // rep:14 bits [0..16383] // len:8 bits [0..255] // step:10 bits [0..1023] // total:32 bits // ---------------------------------------------------- public const int REP_BITS = 14; public const int LEN_BITS = 8; public const int STEP_BITS = 10; public const int REP_MASK = (1 << REP_BITS) - 1; public const int LEN_MASK = (1 << LEN_BITS) - 1; public const int STEP_MASK = (1 << STEP_BITS) - 1; public const int LEN_SHIFT = REP_BITS; public const int STEP_SHIFT = REP_BITS + LEN_BITS; // ---------------------------------------------------- // Benchmark // ---------------------------------------------------- public const int ELEMENTS = 4_000_000; public const int CORRECTNESS_TEST_SIZE = 100_000; public const int STANDARD_ITERATIONS = 80; public const int DIRECT_GPU_WORD_REPEATS = 64; public const int ARBITRARY_REPEAT_COUNT = 12_345_678; // exact deep target public const int EXACT_DEEP_REPEAT = 16_777_216; // ---------------------------------------------------- // Real 30-second task // exact semantic task:F^4096 // direct exact = 64 launches of F^64 // jump exact = one launch of J2 // ---------------------------------------------------- public const int REAL_TASK_REPEAT = 4_096; public const int REAL_TASK_BASE64_LEVEL = 2; // J2 = F^4096 public const int REAL_TASK_SECONDS = 30; } internal sealed class PathSummary { public required int PathId { get; init; } public required int CycleCount { get; init; } public required int MaxCycleLength { get; init; } public required float AverageSteps { get; init; } public required int DominantBasinSize { get; init; } public required int DominantCycleLength { get; init; } public required int DominantRepresentative { get; init; } } internal sealed class EngineBuildResult { public required int MotifCount { get; init; } public required int RawStateCount { get; init; } public required int SymmetryCount { get; init; } public required int[] CanonicalPackedByMotif { get; init; } public required int[] PathWords { get; init; } public required int[] GeneratorTransitions { get; init; } public required int[] Base64JumpTables { get; init; } // [((path * levels)+level)*motifCount + motif] public required long[] Base64LevelApplications { get; init; } public required int[] BinaryJumpTables { get; init; } // [((path * levels)+level)*motifCount + motif] public required long[] BinaryLevelApplications { get; init; } public required int[] PackedAttractorMetadata { get; init; } // [path * motifCount + motif] public required PathSummary[] PathSummaries { get; init; } } internal sealed class RealTaskResult { public required string Name { get; init; } public required int Elements { get; init; } public required int ExactRepeat { get; init; } public required double Seconds { get; init; } public required long BatchesCompleted { get; init; } public long TotalElementEvaluations => BatchesCompleted * (long)Elements; public long TotalPathApplications => BatchesCompleted * (long)Elements * ExactRepeat; public long TotalGeneratorApplications => TotalPathApplications * Config.PATH_LENGTH; public double BatchesPerSecond => BatchesCompleted / Seconds; public double PathApplicationsPerSecond => TotalPathApplications / Seconds; public double GeneratorApplicationsPerSecond => TotalGeneratorApplications / Seconds; } // ============================================================ // STANDARD GPU COMPUTE // ============================================================ [ThreadGroupSize(256, 1, 1)] [GeneratedComputeShaderDescriptor] public readonly partial struct StandardComputeKernel : IComputeShader { public readonly ReadWriteBuffer<float> Input; public readonly ReadWriteBuffer<float> Output; public readonly int Elements; public readonly int Iterations; public StandardComputeKernel( ReadWriteBuffer<float> input, ReadWriteBuffer<float> output, int elements, int iterations) { Input = input; Output = output; Elements = elements; Iterations = iterations; } public void Execute() { int i = ThreadIds.X; if (i >= Elements) return; float x = Input[i]; for (int iter = 0; iter < Iterations; iter++) { x = Hlsl.Sin(x * 1.13f) * Hlsl.Cos(x * 0.91f) + 0.31f * Hlsl.Exp(Hlsl.Abs(x) * 0.12f) + 0.27f * Hlsl.Log(1.0f + Hlsl.Abs(x) + 0.1f) + 0.19f * Hlsl.Sqrt(Hlsl.Abs(x) + 0.2f) - 0.08f * Hlsl.Tan(x * 0.15f); x = x * 0.87f + 0.013f; } Output[i] = x; } } // ============================================================ // DIRECT MULTI-PATH F^64 // ============================================================ [ThreadGroupSize(256, 1, 1)] [GeneratedComputeShaderDescriptor] public readonly partial struct DirectMultiPathKernel : IComputeShader { public readonly ReadWriteBuffer<int> InputMotifs; public readonly ReadWriteBuffer<int> InputPathIds; public readonly ReadWriteBuffer<int> OutputMotifs; public readonly ReadWriteBuffer<int> GeneratorTransitions; public readonly ReadWriteBuffer<int> PathWords; public readonly int Elements; public readonly int MotifCount; public readonly int PathLength; public readonly int WordRepeats; public DirectMultiPathKernel( ReadWriteBuffer<int> inputMotifs, ReadWriteBuffer<int> inputPathIds, ReadWriteBuffer<int> outputMotifs, ReadWriteBuffer<int> generatorTransitions, ReadWriteBuffer<int> pathWords, int elements, int motifCount, int pathLength, int wordRepeats) { InputMotifs = inputMotifs; InputPathIds = inputPathIds; OutputMotifs = outputMotifs; GeneratorTransitions = generatorTransitions; PathWords = pathWords; Elements = elements; MotifCount = motifCount; PathLength = pathLength; WordRepeats = wordRepeats; } public void Execute() { int i = ThreadIds.X; if (i >= Elements) return; int motif = InputMotifs[i]; int pathId = InputPathIds[i]; int pathOffset = pathId * PathLength; for (int r = 0; r < WordRepeats; r++) { for (int step = 0; step < PathLength; step++) { int g = PathWords[pathOffset + step]; motif = GeneratorTransitions[g * MotifCount + motif]; } } OutputMotifs[i] = motif; } } // ============================================================ // SINGLE-LEVEL JUMP LOOKUP // Works for both base64 and binary hierarchies // ============================================================ [ThreadGroupSize(256, 1, 1)] [GeneratedComputeShaderDescriptor] public readonly partial struct MultiPathJumpKernel : IComputeShader { public readonly ReadWriteBuffer<int> InputMotifs; public readonly ReadWriteBuffer<int> InputPathIds; public readonly ReadWriteBuffer<int> OutputMotifs; public readonly ReadWriteBuffer<int> JumpTables; public readonly int Elements; public readonly int MotifCount; public readonly int LevelCount; public readonly int Level; public MultiPathJumpKernel( ReadWriteBuffer<int> inputMotifs, ReadWriteBuffer<int> inputPathIds, ReadWriteBuffer<int> outputMotifs, ReadWriteBuffer<int> jumpTables, int elements, int motifCount, int levelCount, int level) { InputMotifs = inputMotifs; InputPathIds = inputPathIds; OutputMotifs = outputMotifs; JumpTables = jumpTables; Elements = elements; MotifCount = motifCount; LevelCount = levelCount; Level = level; } public void Execute() { int i = ThreadIds.X; if (i >= Elements) return; int motif = InputMotifs[i]; int pathId = InputPathIds[i]; int offset = ((pathId * LevelCount) + Level) * MotifCount; OutputMotifs[i] = JumpTables[offset + motif]; } } // ============================================================ // FUSED JUMP + PACKED ATTRACTOR LOOKUP // Works for both base64 and binary hierarchies // ============================================================ [ThreadGroupSize(256, 1, 1)] [GeneratedComputeShaderDescriptor] public readonly partial struct FusedJumpAttractorKernel : IComputeShader { public readonly ReadWriteBuffer<int> InputMotifs; public readonly ReadWriteBuffer<int> InputPathIds; public readonly ReadWriteBuffer<int> OutputMotifs; public readonly ReadWriteBuffer<int> OutputPackedMeta; public readonly ReadWriteBuffer<int> JumpTables; public readonly ReadWriteBuffer<int> PackedAttractorMetadata; public readonly int Elements; public readonly int MotifCount; public readonly int LevelCount; public readonly int Level; public FusedJumpAttractorKernel( ReadWriteBuffer<int> inputMotifs, ReadWriteBuffer<int> inputPathIds, ReadWriteBuffer<int> outputMotifs, ReadWriteBuffer<int> outputPackedMeta, ReadWriteBuffer<int> jumpTables, ReadWriteBuffer<int> packedAttractorMetadata, int elements, int motifCount, int levelCount, int level) { InputMotifs = inputMotifs; InputPathIds = inputPathIds; OutputMotifs = outputMotifs; OutputPackedMeta = outputPackedMeta; JumpTables = jumpTables; PackedAttractorMetadata = packedAttractorMetadata; Elements = elements; MotifCount = motifCount; LevelCount = levelCount; Level = level; } public void Execute() { int i = ThreadIds.X; if (i >= Elements) return; int motif = InputMotifs[i]; int pathId = InputPathIds[i]; int jumpOffset = ((pathId * LevelCount) + Level) * MotifCount; motif = JumpTables[jumpOffset + motif]; int metaOffset = pathId * MotifCount; int packed = PackedAttractorMetadata[metaOffset + motif]; OutputMotifs[i] = motif; OutputPackedMeta[i] = packed; } } // ============================================================ // BASE-64 ARBITRARY DECOMPOSITION // ============================================================ [ThreadGroupSize(256, 1, 1)] [GeneratedComputeShaderDescriptor] public readonly partial struct Base64DecomposedMultiPathKernel : IComputeShader { public readonly ReadWriteBuffer<int> InputMotifs; public readonly ReadWriteBuffer<int> InputPathIds; public readonly ReadWriteBuffer<int> OutputMotifs; public readonly ReadWriteBuffer<int> JumpTables; public readonly int Elements; public readonly int MotifCount; public readonly int LevelCount; public readonly int RepeatCount; public readonly int JumpBase; public Base64DecomposedMultiPathKernel( ReadWriteBuffer<int> inputMotifs, ReadWriteBuffer<int> inputPathIds, ReadWriteBuffer<int> outputMotifs, ReadWriteBuffer<int> jumpTables, int elements, int motifCount, int levelCount, int repeatCount, int jumpBase) { InputMotifs = inputMotifs; InputPathIds = inputPathIds; OutputMotifs = outputMotifs; JumpTables = jumpTables; Elements = elements; MotifCount = motifCount; LevelCount = levelCount; RepeatCount = repeatCount; JumpBase = jumpBase; } public void Execute() { int i = ThreadIds.X; if (i >= Elements) return; int motif = InputMotifs[i]; int pathId = InputPathIds[i]; int baseOffset = pathId * LevelCount * MotifCount; int remaining = RepeatCount; int d0 = remaining % JumpBase; remaining /= JumpBase; int d1 = remaining % JumpBase; remaining /= JumpBase; int d2 = remaining % JumpBase; remaining /= JumpBase; int d3 = remaining % JumpBase; remaining /= JumpBase; int d4 = remaining % JumpBase; int o0 = baseOffset + 0 * MotifCount; int o1 = baseOffset + 1 * MotifCount; int o2 = baseOffset + 2 * MotifCount; int o3 = baseOffset + 3 * MotifCount; int o4 = baseOffset + 4 * MotifCount; for (int k = 0; k < d0; k++) motif = JumpTables[o0 + motif]; for (int k = 0; k < d1; k++) motif = JumpTables[o1 + motif]; for (int k = 0; k < d2; k++) motif = JumpTables[o2 + motif]; for (int k = 0; k < d3; k++) motif = JumpTables[o3 + motif]; for (int k = 0; k < d4; k++) motif = JumpTables[o4 + motif]; OutputMotifs[i] = motif; } } // ============================================================ // BINARY ARBITRARY DECOMPOSITION // ============================================================ [ThreadGroupSize(256, 1, 1)] [GeneratedComputeShaderDescriptor] public readonly partial struct BinaryDecomposedMultiPathKernel : IComputeShader { public readonly ReadWriteBuffer<int> InputMotifs; public readonly ReadWriteBuffer<int> InputPathIds; public readonly ReadWriteBuffer<int> OutputMotifs; public readonly ReadWriteBuffer<int> JumpTables; public readonly int Elements; public readonly int MotifCount; public readonly int LevelCount; public readonly uint RepeatCount; public BinaryDecomposedMultiPathKernel( ReadWriteBuffer<int> inputMotifs, ReadWriteBuffer<int> inputPathIds, ReadWriteBuffer<int> outputMotifs, ReadWriteBuffer<int> jumpTables, int elements, int motifCount, int levelCount, uint repeatCount) { InputMotifs = inputMotifs; InputPathIds = inputPathIds; OutputMotifs = outputMotifs; JumpTables = jumpTables; Elements = elements; MotifCount = motifCount; LevelCount = levelCount; RepeatCount = repeatCount; } public void Execute() { int i = ThreadIds.X; if (i >= Elements) return; int motif = InputMotifs[i]; int pathId = InputPathIds[i]; int baseOffset = pathId * LevelCount * MotifCount; uint mask = RepeatCount; for (int level = 0; level < LevelCount; level++) { if ((mask & (1u << level)) != 0u) { motif = JumpTables[baseOffset + level * MotifCount + motif]; } } OutputMotifs[i] = motif; } } // ============================================================ // FENCE // ============================================================ [ThreadGroupSize(1, 1, 1)] [GeneratedComputeShaderDescriptor] public readonly partial struct FenceKernel : IComputeShader { public readonly ReadWriteBuffer<int> Fence; public FenceKernel(ReadWriteBuffer<int> fence) => Fence = fence; public void Execute() => Fence[0] = Fence[0] + 1; } internal static class Program { static void Main() { Console.OutputEncoding = Encoding.UTF8; Console.WriteLine("================================================"); Console.WriteLine("TETRA COMBINATION ENGINE - V3 BINARY + REAL 30s"); Console.WriteLine("================================================"); Console.WriteLine(); var device = GraphicsDevice.GetDefault(); Console.WriteLine($"GPU:{device.Name}"); Console.WriteLine($"Elements:{Config.ELEMENTS:N0}"); Console.WriteLine($"Path count:{Config.PATH_COUNT}"); Console.WriteLine($"Path length:{Config.PATH_LENGTH}"); Console.WriteLine($"Base64 levels:{Config.BASE64_LEVELS}"); Console.WriteLine($"Binary levels:{Config.BINARY_LEVELS}"); Console.WriteLine(); Console.WriteLine("Building engine on CPU..."); var swBuild = Stopwatch.StartNew(); EngineBuildResult engine = BuildEngine(); swBuild.Stop(); Console.WriteLine($"Build time:{swBuild.Elapsed.TotalMilliseconds:F2} ms"); Console.WriteLine($"Raw states:{engine.RawStateCount:N0}"); Console.WriteLine($"Canonical motifs:{engine.MotifCount:N0}"); Console.WriteLine($"Symmetry count:{engine.SymmetryCount}"); Console.WriteLine($"Generator table:{engine.GeneratorTransitions.Length:N0} ints (~{engine.GeneratorTransitions.Length * sizeof(int) / 1024.0 / 1024.0:F2} MB)"); Console.WriteLine($"Path word table:{engine.PathWords.Length:N0} ints"); Console.WriteLine($"Base64 jump tables:{engine.Base64JumpTables.Length:N0} ints (~{engine.Base64JumpTables.Length * sizeof(int) / 1024.0 / 1024.0:F2} MB)"); Console.WriteLine($"Binary jump tables:{engine.BinaryJumpTables.Length:N0} ints (~{engine.BinaryJumpTables.Length * sizeof(int) / 1024.0 / 1024.0:F2} MB)"); Console.WriteLine($"Packed attractor metadata:{engine.PackedAttractorMetadata.Length:N0} ints (~{engine.PackedAttractorMetadata.Length * sizeof(int) / 1024.0 / 1024.0:F2} MB)"); Console.WriteLine(); PrintBase64LevelStats(engine); Console.WriteLine(); PrintBinaryLevelStats(engine); Console.WriteLine(); PrintPathSummaries(engine); Console.WriteLine("\nTesting standard GPU compute..."); var standardTime = TestStandardCompute(device); Console.WriteLine($"Standard time:{standardTime.TotalMilliseconds:F2} ms"); Console.WriteLine($"\nTesting direct multi-path F^{Config.DIRECT_GPU_WORD_REPEATS:N0} (random pathId)..."); var directRandomTime = TestDirectMultiPath(device, engine, groupedPaths: false); Console.WriteLine($"Direct random time:{directRandomTime.TotalMilliseconds:F2} ms"); Console.WriteLine($"\nTesting direct multi-path F^{Config.DIRECT_GPU_WORD_REPEATS:N0} (grouped pathId)..."); var directGroupedTime = TestDirectMultiPath(device, engine, groupedPaths: true); Console.WriteLine($"Direct grouped time:{directGroupedTime.TotalMilliseconds:F2} ms"); Console.WriteLine("\nTesting base64 jump J1 = F^64 (random pathId)..."); var jump64RandomTime = TestSingleJump(device, engine, engine.Base64JumpTables, Config.BASE64_LEVELS, level: 1, groupedPaths: false); Console.WriteLine($"Jump J1 random time:{jump64RandomTime.TotalMilliseconds:F2} ms"); Console.WriteLine("\nTesting base64 fused J1 = F^64 + attractor (random pathId)..."); var fused64RandomTime = TestFusedSingleJump(device, engine, engine.Base64JumpTables, Config.BASE64_LEVELS, level: 1, groupedPaths: false); Console.WriteLine($"Fused J1 random time:{fused64RandomTime.TotalMilliseconds:F2} ms"); Console.WriteLine("\nTesting base64 fused J1 = F^64 + attractor (grouped pathId)..."); var fused64GroupedTime = TestFusedSingleJump(device, engine, engine.Base64JumpTables, Config.BASE64_LEVELS, level: 1, groupedPaths: true); Console.WriteLine($"Fused J1 grouped time:{fused64GroupedTime.TotalMilliseconds:F2} ms"); Console.WriteLine($"\nTesting base64 arbitrary F^{Config.ARBITRARY_REPEAT_COUNT:N0}..."); var base64ArbitraryTime = TestBase64Arbitrary(device, engine, Config.ARBITRARY_REPEAT_COUNT, groupedPaths: false); Console.WriteLine($"Base64 arbitrary time:{base64ArbitraryTime.TotalMilliseconds:F2} ms"); Console.WriteLine($"\nTesting binary arbitrary F^{Config.ARBITRARY_REPEAT_COUNT:N0}..."); var binaryArbitraryTime = TestBinaryArbitrary(device, engine, Config.ARBITRARY_REPEAT_COUNT, groupedPaths: false); Console.WriteLine($"Binary arbitrary time:{binaryArbitraryTime.TotalMilliseconds:F2} ms"); int exactBase64Level = TryGetExactLevel(Config.EXACT_DEEP_REPEAT, engine.Base64LevelApplications); int exactBinaryLevel = TryGetExactLevel(Config.EXACT_DEEP_REPEAT, engine.BinaryLevelApplications); Console.WriteLine($"\nTesting exact deep base64 jump F^{Config.EXACT_DEEP_REPEAT:N0}..."); var base64ExactTime = TestFusedSingleJump(device, engine, engine.Base64JumpTables, Config.BASE64_LEVELS, exactBase64Level, groupedPaths: false); Console.WriteLine($"Base64 exact deep time:{base64ExactTime.TotalMilliseconds:F2} ms"); Console.WriteLine($"\nTesting exact deep binary jump F^{Config.EXACT_DEEP_REPEAT:N0}..."); var binaryExactTime = TestFusedSingleJump(device, engine, engine.BinaryJumpTables, Config.BINARY_LEVELS, exactBinaryLevel, groupedPaths: false); Console.WriteLine($"Binary exact deep time:{binaryExactTime.TotalMilliseconds:F2} ms"); Console.WriteLine("\n================================================"); Console.WriteLine("PERFORMANCE COMPARISON"); Console.WriteLine("================================================"); double standardMs = standardTime.TotalMilliseconds; double directRandomMs = directRandomTime.TotalMilliseconds; double directGroupedMs = directGroupedTime.TotalMilliseconds; double jump64RandomMs = jump64RandomTime.TotalMilliseconds; double fused64RandomMs = fused64RandomTime.TotalMilliseconds; double fused64GroupedMs = fused64GroupedTime.TotalMilliseconds; double base64ArbMs = base64ArbitraryTime.TotalMilliseconds; double binaryArbMs = binaryArbitraryTime.TotalMilliseconds; double base64ExactMs = base64ExactTime.TotalMilliseconds; double binaryExactMs = binaryExactTime.TotalMilliseconds; Console.WriteLine($"Standard compute:{standardMs:F2} ms (baseline)"); Console.WriteLine($"Direct F^64 random:{directRandomMs:F2} ms ({standardMs / directRandomMs:F2}x vs baseline)"); Console.WriteLine($"Direct F^64 grouped:{directGroupedMs:F2} ms ({standardMs / directGroupedMs:F2}x vs baseline,{directRandomMs / directGroupedMs:F2}x vs direct-random)"); Console.WriteLine($"Jump J1 random:{jump64RandomMs:F2} ms ({standardMs / jump64RandomMs:F2}x vs baseline,{directRandomMs / jump64RandomMs:F2}x vs direct-random)"); Console.WriteLine($"Fused J1 random:{fused64RandomMs:F2} ms ({standardMs / fused64RandomMs:F2}x vs baseline,{directRandomMs / fused64RandomMs:F2}x vs direct-random)"); Console.WriteLine($"Fused J1 grouped:{fused64GroupedMs:F2} ms ({standardMs / fused64GroupedMs:F2}x vs baseline,{fused64RandomMs / fused64GroupedMs:F2}x vs fused-random)"); Console.WriteLine($"Base64 arbitrary:{base64ArbMs:F2} ms"); Console.WriteLine($"Binary arbitrary:{binaryArbMs:F2} ms ({base64ArbMs / binaryArbMs:F2}x vs base64-arbitrary)"); Console.WriteLine($"Base64 exact deep:{base64ExactMs:F2} ms"); Console.WriteLine($"Binary exact deep:{binaryExactMs:F2} ms"); Console.WriteLine("\n================================================"); Console.WriteLine("LOGICAL COMPRESSION"); Console.WriteLine("================================================"); double equivalentVsDirect64 = (double)Config.EXACT_DEEP_REPEAT / Config.DIRECT_GPU_WORD_REPEATS; double estimatedDirectDeepMs = directRandomMs * equivalentVsDirect64; Console.WriteLine($"Exact deep target:F^{Config.EXACT_DEEP_REPEAT:N0}"); Console.WriteLine($"Equivalent path applications vs direct F^64:{equivalentVsDirect64:N0}x"); Console.WriteLine($"Equivalent generator applications per element:{(long)Config.EXACT_DEEP_REPEAT * Config.PATH_LENGTH:N0}"); Console.WriteLine($"Estimated direct time for same depth (linear extrapolation):{estimatedDirectDeepMs / 1000.0:F2} s"); Console.WriteLine($"Estimated speedup of binary exact deep vs extrapolated direct:{estimatedDirectDeepMs / binaryExactMs:F2}x"); Console.WriteLine("\n================================================"); Console.WriteLine("REAL 30-SECOND EXACT TASK"); Console.WriteLine("================================================"); Console.WriteLine($"Exact semantic task:F^{Config.REAL_TASK_REPEAT:N0}"); Console.WriteLine("Direct exact implementation:64 sequential launches of F^64"); Console.WriteLine("Jump exact implementation:one J2 = F^4096 lookup"); Console.WriteLine($"Duration per mode:{Config.REAL_TASK_SECONDS} seconds"); Console.WriteLine(); var realDirect = RunRealThirtySecondDirectExactTask(device, engine, groupedPaths: false); var realJump = RunRealThirtySecondJumpExactTask(device, engine, groupedPaths: false); PrintRealTaskResult(realDirect); PrintRealTaskResult(realJump); Console.WriteLine(); Console.WriteLine($"Measured exact throughput speedup (path applications/sec):{realJump.PathApplicationsPerSecond / realDirect.PathApplicationsPerSecond:F2}x"); Console.WriteLine($"Measured exact throughput speedup (generator applications/sec):{realJump.GeneratorApplicationsPerSecond / realDirect.GeneratorApplicationsPerSecond:F2}x"); Console.WriteLine($"Measured exact batch speedup:{realJump.BatchesPerSecond / realDirect.BatchesPerSecond:F2}x"); Console.WriteLine("\n================================================"); Console.WriteLine("CORRECTNESS CHECK"); Console.WriteLine("================================================"); CheckCorrectness(device, engine); Console.WriteLine("\nDone. Press any key..."); Console.ReadKey(); } // ============================================================ // BUILD ENGINE // ============================================================ static EngineBuildResult BuildEngine() { var symmetryGroup = BuildA4SymmetryGroup(); var canonicalToId = new Dictionary<int, int>(capacity: 32768); var motifPackedList = new List<int>(capacity: 32768); int rawCount = 0; for (int missing = 0; missing < 4; missing++) { for (int a = 0; a < Config.LOCAL_STATE_COUNT; a++) { for (int b = 0; b < Config.LOCAL_STATE_COUNT; b++) { for (int c = 0; c < Config.LOCAL_STATE_COUNT; c++) { rawCount++; int[] slots = new int[4]; for (int i = 0; i < 4; i++) slots[i] = Config.EMPTY; int idx = 0; for (int s = 0; s < 4; s++) { if (s == missing) continue; slots[s] = idx switch { 0 => a, 1 => b, _ => c }; idx++; } int packed = PackSlots(slots[0], slots[1], slots[2], slots[3]); int canonical = Canonicalize(packed, symmetryGroup); if (!canonicalToId.ContainsKey(canonical)) { canonicalToId[canonical] = motifPackedList.Count; motifPackedList.Add(canonical); } } } } } int motifCount = motifPackedList.Count; int[] pathWords = new int[Config.PATH_COUNT * Config.PATH_LENGTH]; for (int p = 0; p < Config.PATH_COUNT; p++) { for (int k = 0; k < Config.PATH_LENGTH; k++) { pathWords[p * Config.PATH_LENGTH + k] = Config.PATHS[p][k]; } } int[] generatorTransitions = new int[4 * motifCount]; for (int motif = 0; motif < motifCount; motif++) { int packed = motifPackedList[motif]; for (int g = 0; g < 4; g++) { int nextRaw = ApplyGenerator(packed, g); int nextCanonical = Canonicalize(nextRaw, symmetryGroup); int nextMotif = canonicalToId[nextCanonical]; generatorTransitions[g * motifCount + motif] = nextMotif; } } // ------------------------- // Base64 jump tables // ------------------------- int[] base64JumpTables = new int[Config.PATH_COUNT * Config.BASE64_LEVELS * motifCount]; long[] base64LevelApps = new long[Config.BASE64_LEVELS]; base64LevelApps[0] = 1; for (int level = 1; level < Config.BASE64_LEVELS; level++) base64LevelApps[level] = base64LevelApps[level - 1] * Config.BASE64_JUMP_BASE; for (int pathId = 0; pathId < Config.PATH_COUNT; pathId++) { int offset0 = GetTableOffset(pathId, 0, motifCount, Config.BASE64_LEVELS); for (int motif = 0; motif < motifCount; motif++) { int current = motif; for (int step = 0; step < Config.PATH_LENGTH; step++) { int g = pathWords[pathId * Config.PATH_LENGTH + step]; current = generatorTransitions[g * motifCount + current]; } base64JumpTables[offset0 + motif] = current; } } for (int pathId = 0; pathId < Config.PATH_COUNT; pathId++) { for (int level = 1; level < Config.BASE64_LEVELS; level++) { int prevOffset = GetTableOffset(pathId, level - 1, motifCount, Config.BASE64_LEVELS); int curOffset = GetTableOffset(pathId, level, motifCount, Config.BASE64_LEVELS); for (int motif = 0; motif < motifCount; motif++) { int current = motif; for (int r = 0; r < Config.BASE64_JUMP_BASE; r++) { current = base64JumpTables[prevOffset + current]; } base64JumpTables[curOffset + motif] = current; } } } // ------------------------- // Binary jump tables // ------------------------- int[] binaryJumpTables = new int[Config.PATH_COUNT * Config.BINARY_LEVELS * motifCount]; long[] binaryLevelApps = new long[Config.BINARY_LEVELS]; binaryLevelApps[0] = 1; for (int level = 1; level < Config.BINARY_LEVELS; level++) binaryLevelApps[level] = binaryLevelApps[level - 1] * 2; for (int pathId = 0; pathId < Config.PATH_COUNT; pathId++) { int base64Offset0 = GetTableOffset(pathId, 0, motifCount, Config.BASE64_LEVELS); int binaryOffset0 = GetTableOffset(pathId, 0, motifCount, Config.BINARY_LEVELS); Array.Copy(base64JumpTables, base64Offset0, binaryJumpTables, binaryOffset0, motifCount); } for (int pathId = 0; pathId < Config.PATH_COUNT; pathId++) { for (int level = 1; level < Config.BINARY_LEVELS; level++) { int prevOffset = GetTableOffset(pathId, level - 1, motifCount, Config.BINARY_LEVELS); int curOffset = GetTableOffset(pathId, level, motifCount, Config.BINARY_LEVELS); for (int motif = 0; motif < motifCount; motif++) { int current = binaryJumpTables[prevOffset + motif]; current = binaryJumpTables[prevOffset + current]; binaryJumpTables[curOffset + motif] = current; } } } // ------------------------- // Attractor metadata from F^1 // ------------------------- int[] packedAttractorMetadata = new int[Config.PATH_COUNT * motifCount]; PathSummary[] pathSummaries = new PathSummary[Config.PATH_COUNT]; for (int pathId = 0; pathId < Config.PATH_COUNT; pathId++) { int offset0 = GetTableOffset(pathId, 0, motifCount, Config.BASE64_LEVELS); int[] next = new int[motifCount]; Array.Copy(base64JumpTables, offset0, next, 0, motifCount); AnalyzeFunctionalGraph( next, out int[] cycleId, out int[] cycleRepresentative, out int[] cycleLength, out int[] stepsToCycle); int maxRep = cycleRepresentative.Max(); int maxLen = cycleLength.Max(); int maxSteps = stepsToCycle.Max(); if (maxRep > Config.REP_MASK) throw new InvalidOperationException($"Representative {maxRep} exceeds packed REP range."); if (maxLen > Config.LEN_MASK) throw new InvalidOperationException($"Cycle length {maxLen} exceeds packed LEN range."); if (maxSteps > Config.STEP_MASK) throw new InvalidOperationException($"Steps-to-cycle {maxSteps} exceeds packed STEP range."); int metaOffset = pathId * motifCount; for (int motif = 0; motif < motifCount; motif++) { packedAttractorMetadata[metaOffset + motif] = PackMeta(cycleRepresentative[motif], cycleLength[motif], stepsToCycle[motif]); } int cycleCount = cycleId.Distinct().Count(); int maxCycleLength = cycleLength.Max(); float averageSteps = (float)stepsToCycle.Average(); var dominant = cycleId .GroupBy(x => x) .Select(g => { int firstIndex = Array.IndexOf(cycleId, g.Key); return new { BasinSize = g.Count(), CycleLength = cycleLength[firstIndex], Representative = cycleRepresentative[firstIndex] }; }) .OrderByDescending(x => x.BasinSize) .First(); pathSummaries[pathId] = new PathSummary { PathId = pathId, CycleCount = cycleCount, MaxCycleLength = maxCycleLength, AverageSteps = averageSteps, DominantBasinSize = dominant.BasinSize, DominantCycleLength = dominant.CycleLength, DominantRepresentative = dominant.Representative }; } return new EngineBuildResult { MotifCount = motifCount, RawStateCount = rawCount, SymmetryCount = symmetryGroup.Count, CanonicalPackedByMotif = motifPackedList.ToArray(), PathWords = pathWords, GeneratorTransitions = generatorTransitions, Base64JumpTables = base64JumpTables, Base64LevelApplications = base64LevelApps, BinaryJumpTables = binaryJumpTables, BinaryLevelApplications = binaryLevelApps, PackedAttractorMetadata = packedAttractorMetadata, PathSummaries = pathSummaries }; } // ============================================================ // BENCHMARKS // ============================================================ static TimeSpan TestStandardCompute(GraphicsDevice device) { int size = Config.ELEMENTS; using var input = device.AllocateReadWriteBuffer<float>(size); using var output = device.AllocateReadWriteBuffer<float>(size); FillRandomFloatData(device, input, size, 12345); device.For(size, new StandardComputeKernel(input, output, size, 8)); Synchronize(device); var sw = Stopwatch.StartNew(); device.For(size, new StandardComputeKernel(input, output, size, Config.STANDARD_ITERATIONS)); Synchronize(device); sw.Stop(); return sw.Elapsed; } static TimeSpan TestDirectMultiPath(GraphicsDevice device, EngineBuildResult engine, bool groupedPaths) { int size = Config.ELEMENTS; using var inputMotifs = device.AllocateReadWriteBuffer<int>(size); using var inputPathIds = device.AllocateReadWriteBuffer<int>(size); using var outputMotifs = device.AllocateReadWriteBuffer<int>(size); using var transitions = device.AllocateReadWriteBuffer<int>(engine.GeneratorTransitions.Length); using var pathWords = device.AllocateReadWriteBuffer<int>(engine.PathWords.Length); transitions.CopyFrom(engine.GeneratorTransitions); pathWords.CopyFrom(engine.PathWords); FillRandomMotifIds(device, inputMotifs, size, engine.MotifCount, 12345); if (groupedPaths) FillGroupedPathIds(device, inputPathIds, size, Config.PATH_COUNT); else FillRandomPathIds(device, inputPathIds, size, Config.PATH_COUNT, 54321); device.For(size, new DirectMultiPathKernel( inputMotifs, inputPathIds, outputMotifs, transitions, pathWords, size, engine.MotifCount, Config.PATH_LENGTH, Config.DIRECT_GPU_WORD_REPEATS)); Synchronize(device); var sw = Stopwatch.StartNew(); device.For(size, new DirectMultiPathKernel( inputMotifs, inputPathIds, outputMotifs, transitions, pathWords, size, engine.MotifCount, Config.PATH_LENGTH, Config.DIRECT_GPU_WORD_REPEATS)); Synchronize(device); sw.Stop(); return sw.Elapsed; } static TimeSpan TestSingleJump( GraphicsDevice device, EngineBuildResult engine, int[] jumpTables, int levelCount, int level, bool groupedPaths) { int size = Config.ELEMENTS; using var inputMotifs = device.AllocateReadWriteBuffer<int>(size); using var inputPathIds = device.AllocateReadWriteBuffer<int>(size); using var outputMotifs = device.AllocateReadWriteBuffer<int>(size); using var jumps = device.AllocateReadWriteBuffer<int>(jumpTables.Length); jumps.CopyFrom(jumpTables); FillRandomMotifIds(device, inputMotifs, size, engine.MotifCount, 12345); if (groupedPaths) FillGroupedPathIds(device, inputPathIds, size, Config.PATH_COUNT); else FillRandomPathIds(device, inputPathIds, size, Config.PATH_COUNT, 54321); device.For(size, new MultiPathJumpKernel( inputMotifs, inputPathIds, outputMotifs, jumps, size, engine.MotifCount, levelCount, level)); Synchronize(device); var sw = Stopwatch.StartNew(); device.For(size, new MultiPathJumpKernel( inputMotifs, inputPathIds, outputMotifs, jumps, size, engine.MotifCount, levelCount, level)); Synchronize(device); sw.Stop(); return sw.Elapsed; } static TimeSpan TestFusedSingleJump( GraphicsDevice device, EngineBuildResult engine, int[] jumpTables, int levelCount, int level, bool groupedPaths) { int size = Config.ELEMENTS; using var inputMotifs = device.AllocateReadWriteBuffer<int>(size); using var inputPathIds = device.AllocateReadWriteBuffer<int>(size); using var outputMotifs = device.AllocateReadWriteBuffer<int>(size); using var outputMeta = device.AllocateReadWriteBuffer<int>(size); using var jumps = device.AllocateReadWriteBuffer<int>(jumpTables.Length); using var meta = device.AllocateReadWriteBuffer<int>(engine.PackedAttractorMetadata.Length); jumps.CopyFrom(jumpTables); meta.CopyFrom(engine.PackedAttractorMetadata); FillRandomMotifIds(device, inputMotifs, size, engine.MotifCount, 12345); if (groupedPaths) FillGroupedPathIds(device, inputPathIds, size, Config.PATH_COUNT); else FillRandomPathIds(device, inputPathIds, size, Config.PATH_COUNT, 54321); device.For(size, new FusedJumpAttractorKernel( inputMotifs, inputPathIds, outputMotifs, outputMeta, jumps, meta, size, engine.MotifCount, levelCount, level)); Synchronize(device); var sw = Stopwatch.StartNew(); device.For(size, new FusedJumpAttractorKernel( inputMotifs, inputPathIds, outputMotifs, outputMeta, jumps, meta, size, engine.MotifCount, levelCount, level)); Synchronize(device); sw.Stop(); return sw.Elapsed; } static TimeSpan TestBase64Arbitrary(GraphicsDevice device, EngineBuildResult engine, int repeatCount, bool groupedPaths) { int size = Config.ELEMENTS; using var inputMotifs = device.AllocateReadWriteBuffer<int>(size); using var inputPathIds = device.AllocateReadWriteBuffer<int>(size); using var outputMotifs = device.AllocateReadWriteBuffer<int>(size); using var jumps = device.AllocateReadWriteBuffer<int>(engine.Base64JumpTables.Length); jumps.CopyFrom(engine.Base64JumpTables); FillRandomMotifIds(device, inputMotifs, size, engine.MotifCount, 12345); if (groupedPaths) FillGroupedPathIds(device, inputPathIds, size, Config.PATH_COUNT); else FillRandomPathIds(device, inputPathIds, size, Config.PATH_COUNT, 54321); device.For(size, new Base64DecomposedMultiPathKernel( inputMotifs, inputPathIds, outputMotifs, jumps, size, engine.MotifCount, Config.BASE64_LEVELS, repeatCount, Config.BASE64_JUMP_BASE)); Synchronize(device); var sw = Stopwatch.StartNew(); device.For(size, new Base64DecomposedMultiPathKernel( inputMotifs, inputPathIds, outputMotifs, jumps, size, engine.MotifCount, Config.BASE64_LEVELS, repeatCount, Config.BASE64_JUMP_BASE)); Synchronize(device); sw.Stop(); return sw.Elapsed; } static TimeSpan TestBinaryArbitrary(GraphicsDevice device, EngineBuildResult engine, int repeatCount, bool groupedPaths) { int size = Config.ELEMENTS; using var inputMotifs = device.AllocateReadWriteBuffer<int>(size); using var inputPathIds = device.AllocateReadWriteBuffer<int>(size); using var outputMotifs = device.AllocateReadWriteBuffer<int>(size); using var jumps = device.AllocateReadWriteBuffer<int>(engine.BinaryJumpTables.Length); jumps.CopyFrom(engine.BinaryJumpTables); FillRandomMotifIds(device, inputMotifs, size, engine.MotifCount, 12345); if (groupedPaths) FillGroupedPathIds(device, inputPathIds, size, Config.PATH_COUNT); else FillRandomPathIds(device, inputPathIds, size, Config.PATH_COUNT, 54321); device.For(size, new BinaryDecomposedMultiPathKernel( inputMotifs, inputPathIds, outputMotifs, jumps, size, engine.MotifCount, Config.BINARY_LEVELS, (uint)repeatCount)); Synchronize(device); var sw = Stopwatch.StartNew(); device.For(size, new BinaryDecomposedMultiPathKernel( inputMotifs, inputPathIds, outputMotifs, jumps, size, engine.MotifCount, Config.BINARY_LEVELS, (uint)repeatCount)); Synchronize(device); sw.Stop(); return sw.Elapsed; } // ============================================================ // REAL 30-SECOND EXACT TASK // ============================================================ static RealTaskResult RunRealThirtySecondDirectExactTask(GraphicsDevice device, EngineBuildResult engine, bool groupedPaths) { int size = Config.ELEMENTS; using var motifsA = device.AllocateReadWriteBuffer<int>(size); using var motifsB = device.AllocateReadWriteBuffer<int>(size); using var pathIds = device.AllocateReadWriteBuffer<int>(size); using var transitions = device.AllocateReadWriteBuffer<int>(engine.GeneratorTransitions.Length); using var pathWords = device.AllocateReadWriteBuffer<int>(engine.PathWords.Length); transitions.CopyFrom(engine.GeneratorTransitions); pathWords.CopyFrom(engine.PathWords); FillRandomMotifIds(device, motifsA, size, engine.MotifCount, 10101); if (groupedPaths) FillGroupedPathIds(device, pathIds, size, Config.PATH_COUNT); else FillRandomPathIds(device, pathIds, size, Config.PATH_COUNT, 20202); // warmup device.For(size, new DirectMultiPathKernel( motifsA, pathIds, motifsB, transitions, pathWords, size, engine.MotifCount, Config.PATH_LENGTH, Config.DIRECT_GPU_WORD_REPEATS)); Synchronize(device); bool aToB = true; long batches = 0; var sw = Stopwatch.StartNew(); while (sw.Elapsed.TotalSeconds < Config.REAL_TASK_SECONDS) { // exact F^4096 = 64 dispatches of F^64 for (int i = 0; i < 64; i++) { if (aToB) { device.For(size, new DirectMultiPathKernel( motifsA, pathIds, motifsB, transitions, pathWords, size, engine.MotifCount, Config.PATH_LENGTH, Config.DIRECT_GPU_WORD_REPEATS)); } else { device.For(size, new DirectMultiPathKernel( motifsB, pathIds, motifsA, transitions, pathWords, size, engine.MotifCount, Config.PATH_LENGTH, Config.DIRECT_GPU_WORD_REPEATS)); } aToB = !aToB; } Synchronize(device); batches++; } sw.Stop(); return new RealTaskResult { Name = "DIRECT EXACT F^4096", Elements = size, ExactRepeat = Config.REAL_TASK_REPEAT, Seconds = sw.Elapsed.TotalSeconds, BatchesCompleted = batches }; } static RealTaskResult RunRealThirtySecondJumpExactTask(GraphicsDevice device, EngineBuildResult engine, bool groupedPaths) { int size = Config.ELEMENTS; using var motifsA = device.AllocateReadWriteBuffer<int>(size); using var motifsB = device.AllocateReadWriteBuffer<int>(size); using var pathIds = device.AllocateReadWriteBuffer<int>(size); using var metaOut = device.AllocateReadWriteBuffer<int>(size); using var jumps = device.AllocateReadWriteBuffer<int>(engine.Base64JumpTables.Length); using var meta = device.AllocateReadWriteBuffer<int>(engine.PackedAttractorMetadata.Length); jumps.CopyFrom(engine.Base64JumpTables); meta.CopyFrom(engine.PackedAttractorMetadata); FillRandomMotifIds(device, motifsA, size, engine.MotifCount, 10101); if (groupedPaths) FillGroupedPathIds(device, pathIds, size, Config.PATH_COUNT); else FillRandomPathIds(device, pathIds, size, Config.PATH_COUNT, 20202); // warmup device.For(size, new FusedJumpAttractorKernel( motifsA, pathIds, motifsB, metaOut, jumps, meta, size, engine.MotifCount, Config.BASE64_LEVELS, Config.REAL_TASK_BASE64_LEVEL)); Synchronize(device); bool aToB = true; long batches = 0; var sw = Stopwatch.StartNew(); while (sw.Elapsed.TotalSeconds < Config.REAL_TASK_SECONDS) { if (aToB) { device.For(size, new FusedJumpAttractorKernel( motifsA, pathIds, motifsB, metaOut, jumps, meta, size, engine.MotifCount, Config.BASE64_LEVELS, Config.REAL_TASK_BASE64_LEVEL)); } else { device.For(size, new FusedJumpAttractorKernel( motifsB, pathIds, motifsA, metaOut, jumps, meta, size, engine.MotifCount, Config.BASE64_LEVELS, Config.REAL_TASK_BASE64_LEVEL)); } aToB = !aToB; Synchronize(device); batches++; } sw.Stop(); return new RealTaskResult { Name = "JUMP EXACT F^4096", Elements = size, ExactRepeat = Config.REAL_TASK_REPEAT, Seconds = sw.Elapsed.TotalSeconds, BatchesCompleted = batches }; } // ============================================================ // CORRECTNESS // ============================================================ static void CheckCorrectness(GraphicsDevice device, EngineBuildResult engine) { int size = Config.CORRECTNESS_TEST_SIZE; using var inputMotifs = device.AllocateReadWriteBuffer<int>(size); using var inputPathIds = device.AllocateReadWriteBuffer<int>(size); using var directOut = device.AllocateReadWriteBuffer<int>(size); using var jump64Out = device.AllocateReadWriteBuffer<int>(size); using var fused64Out = device.AllocateReadWriteBuffer<int>(size); using var fused64Meta = device.AllocateReadWriteBuffer<int>(size); using var base64ArbOut = device.AllocateReadWriteBuffer<int>(size); using var binaryArbOut = device.AllocateReadWriteBuffer<int>(size); using var transitions = device.AllocateReadWriteBuffer<int>(engine.GeneratorTransitions.Length); using var pathWords = device.AllocateReadWriteBuffer<int>(engine.PathWords.Length); using var base64Jumps = device.AllocateReadWriteBuffer<int>(engine.Base64JumpTables.Length); using var binaryJumps = device.AllocateReadWriteBuffer<int>(engine.BinaryJumpTables.Length); using var meta = device.AllocateReadWriteBuffer<int>(engine.PackedAttractorMetadata.Length); transitions.CopyFrom(engine.GeneratorTransitions); pathWords.CopyFrom(engine.PathWords); base64Jumps.CopyFrom(engine.Base64JumpTables); binaryJumps.CopyFrom(engine.BinaryJumpTables); meta.CopyFrom(engine.PackedAttractorMetadata); FillRandomMotifIds(device, inputMotifs, size, engine.MotifCount, 777); FillRandomPathIds(device, inputPathIds, size, Config.PATH_COUNT, 888); device.For(size, new DirectMultiPathKernel( inputMotifs, inputPathIds, directOut, transitions, pathWords, size, engine.MotifCount, Config.PATH_LENGTH, Config.DIRECT_GPU_WORD_REPEATS)); device.For(size, new MultiPathJumpKernel( inputMotifs, inputPathIds, jump64Out, base64Jumps, size, engine.MotifCount, Config.BASE64_LEVELS, 1)); device.For(size, new FusedJumpAttractorKernel( inputMotifs, inputPathIds, fused64Out, fused64Meta, base64Jumps, meta, size, engine.MotifCount, Config.BASE64_LEVELS, 1)); device.For(size, new Base64DecomposedMultiPathKernel( inputMotifs, inputPathIds, base64ArbOut, base64Jumps, size, engine.MotifCount, Config.BASE64_LEVELS, Config.ARBITRARY_REPEAT_COUNT, Config.BASE64_JUMP_BASE)); device.For(size, new BinaryDecomposedMultiPathKernel( inputMotifs, inputPathIds, binaryArbOut, binaryJumps, size, engine.MotifCount, Config.BINARY_LEVELS, Config.ARBITRARY_REPEAT_COUNT)); Synchronize(device); int[] direct = new int[size]; int[] jump64 = new int[size]; int[] fused = new int[size]; int[] metaOut = new int[size]; int[] base64Arb = new int[size]; int[] binaryArb = new int[size]; int[] pathIds = new int[size]; directOut.CopyTo(direct, 0, 0, size); jump64Out.CopyTo(jump64, 0, 0, size); fused64Out.CopyTo(fused, 0, 0, size); fused64Meta.CopyTo(metaOut, 0, 0, size); base64ArbOut.CopyTo(base64Arb, 0, 0, size); binaryArbOut.CopyTo(binaryArb, 0, 0, size); inputPathIds.CopyTo(pathIds, 0, 0, size); int directVsJump = 0; int jumpVsFused = 0; int metaMatches = 0; int base64VsBinaryArb = 0; for (int i = 0; i < size; i++) { if (direct[i] == jump64[i]) directVsJump++; if (jump64[i] == fused[i]) jumpVsFused++; int expectedMeta = engine.PackedAttractorMetadata[pathIds[i] * engine.MotifCount + fused[i]]; if (expectedMeta == metaOut[i]) metaMatches++; if (base64Arb[i] == binaryArb[i]) base64VsBinaryArb++; } Console.WriteLine($"Direct F^64 vs jump J1 match rate:{directVsJump * 100.0 / size:F2}%"); Console.WriteLine($"Jump J1 vs fused J1 motif match rate:{jumpVsFused * 100.0 / size:F2}%"); Console.WriteLine($"Fused packed metadata match rate:{metaMatches * 100.0 / size:F2}%"); Console.WriteLine($"Base64 arbitrary vs binary arbitrary match rate:{base64VsBinaryArb * 100.0 / size:F2}%"); // Exact deep equality:base64 J4 vs binary B24 { int sanity = 2000; int ok = 0; var rand = new Random(999); for (int i = 0; i < sanity; i++) { int pathId = rand.Next(Config.PATH_COUNT); int motif = rand.Next(engine.MotifCount); int j4 = engine.Base64JumpTables[GetTableOffset(pathId, 4, engine.MotifCount, Config.BASE64_LEVELS) + motif]; int b24 = engine.BinaryJumpTables[GetTableOffset(pathId, 24, engine.MotifCount, Config.BINARY_LEVELS) + motif]; if (j4 == b24) ok++; } Console.WriteLine($"Base64 J4 vs binary B24 sanity:{ok * 100.0 / sanity:F2}%"); } // Exact F^4096 equality: // direct exact by 64x J1 vs one J2 { int sanity = 2000; int ok = 0; var rand = new Random(1234); for (int i = 0; i < sanity; i++) { int pathId = rand.Next(Config.PATH_COUNT); int motif = rand.Next(engine.MotifCount); int current = motif; int j1Offset = GetTableOffset(pathId, 1, engine.MotifCount, Config.BASE64_LEVELS); for (int k = 0; k < 64; k++) current = engine.Base64JumpTables[j1Offset + current]; int j2 = engine.Base64JumpTables[GetTableOffset(pathId, 2, engine.MotifCount, Config.BASE64_LEVELS) + motif]; if (current == j2) ok++; } Console.WriteLine($"Exact F^4096 sanity (64x J1 == J2):{ok * 100.0 / sanity:F2}%"); } Console.WriteLine("Sample decoded fused metadata:"); for (int i = 0; i < 5; i++) { int rep = UnpackRep(metaOut[i]); int len = UnpackLen(metaOut[i]); int steps = UnpackSteps(metaOut[i]); Console.WriteLine( $" sample {i}:pathId={pathIds[i]},finalMotif={fused[i]},rep={rep},cycleLen={len},stepsToCycle={steps}"); } } // ============================================================ // REPORTING // ============================================================ static void PrintBase64LevelStats(EngineBuildResult engine) { Console.WriteLine("BASE64 JUMP HIERARCHY"); Console.WriteLine("------------------------------------------------"); for (int level = 0; level < engine.Base64LevelApplications.Length; level++) Console.WriteLine($" J{level}:F^{engine.Base64LevelApplications[level]:N0}"); } static void PrintBinaryLevelStats(EngineBuildResult engine) { Console.WriteLine("BINARY JUMP HIERARCHY"); Console.WriteLine("------------------------------------------------"); for (int level = 0; level < engine.BinaryLevelApplications.Length; level += 4) { var parts = new List<string>(); for (int k = 0; k < 4 && level + k < engine.BinaryLevelApplications.Length; k++) { int l = level + k; parts.Add($"B{l}:F^{engine.BinaryLevelApplications[l]:N0}"); } Console.WriteLine(" " + string.Join(" | ", parts)); } } static void PrintPathSummaries(EngineBuildResult engine) { Console.WriteLine("PATH ATTRACTOR SUMMARIES"); Console.WriteLine("------------------------------------------------"); for (int p = 0; p < engine.PathSummaries.Length; p++) { var s = engine.PathSummaries[p]; Console.WriteLine( $" pathId={p} [{Config.PATH_NAMES[p]}] | cycles={s.CycleCount},maxCycle={s.MaxCycleLength},avgSteps={s.AverageSteps:F2},dominantBasin={s.DominantBasinSize:N0} ({s.DominantBasinSize * 100.0 / engine.MotifCount:F2}%),dominantLen={s.DominantCycleLength},rep={s.DominantRepresentative}"); } } static void PrintRealTaskResult(RealTaskResult result) { Console.WriteLine(result.Name); Console.WriteLine($" Time:{result.Seconds:F2} s"); Console.WriteLine($" Batches completed:{result.BatchesCompleted:N0}"); Console.WriteLine($" Exact depth per batch:F^{result.ExactRepeat:N0}"); Console.WriteLine($" Batches/sec:{result.BatchesPerSecond:N4}"); Console.WriteLine($" Path applications/sec:{result.PathApplicationsPerSecond:N0}"); Console.WriteLine($" Generator applications/sec:{result.GeneratorApplicationsPerSecond:N0}"); } // ============================================================ // AUTO / LEVEL HELPERS // ============================================================ static int TryGetExactLevel(int repeatCount, long[] levelApplications) { for (int level = 0; level < levelApplications.Length; level++) { if (levelApplications[level] == repeatCount) return level; } return -1; } // ============================================================ // PACKED META // ============================================================ static int PackMeta(int representative, int cycleLength, int stepsToCycle) { return (representative & Config.REP_MASK) | ((cycleLength & Config.LEN_MASK) << Config.LEN_SHIFT) | ((stepsToCycle & Config.STEP_MASK) << Config.STEP_SHIFT); } static int UnpackRep(int packed) => packed & Config.REP_MASK; static int UnpackLen(int packed) => (packed >> Config.LEN_SHIFT) & Config.LEN_MASK; static int UnpackSteps(int packed) => (packed >> Config.STEP_SHIFT) & Config.STEP_MASK; // ============================================================ // GEOMETRIC / COMBINATORIAL CORE // ============================================================ static int ApplyGenerator(int packed, int generator) { int[] slots = UnpackSlots(packed); int[] perm = GetGeneratorPermutation(generator); int[] permuted = ApplyPermutation(slots, perm); int missing = FindEmpty(permuted); int movedTarget; if (missing != generator) { permuted[missing] = TransformMove(permuted[generator], generator, generator, missing); permuted[generator] = Config.EMPTY; movedTarget = missing; } else { int donor = FindFirstOccupied(permuted, generator); permuted[generator] = TransformMove(permuted[donor], generator, donor, generator); permuted[donor] = Config.EMPTY; movedTarget = generator; } for (int s = 0; s < 4; s++) { if (permuted[s] == Config.EMPTY) continue; if (s == movedTarget) continue; permuted[s] = TransformStay(permuted[s], generator, s); } return PackSlots(permuted[0], permuted[1], permuted[2], permuted[3]); } static int TransformMove(int localState, int generator, int sourceSlot, int targetSlot) { if (localState == Config.EMPTY) return Config.EMPTY; int orientation = localState % Config.ORIENTATION_COUNT; int phase = localState / Config.ORIENTATION_COUNT; orientation = (orientation + 1 + 3 * generator + 2 * sourceSlot + targetSlot) % Config.ORIENTATION_COUNT; phase = (phase + 1 + generator + sourceSlot + 2 * targetSlot) % Config.PHASE_COUNT; return phase * Config.ORIENTATION_COUNT + orientation; } static int TransformStay(int localState, int generator, int slot) { if (localState == Config.EMPTY) return Config.EMPTY; int orientation = localState % Config.ORIENTATION_COUNT; int phase = localState / Config.ORIENTATION_COUNT; orientation = (orientation + GeneratorOrientationDelta(generator, slot)) % Config.ORIENTATION_COUNT; phase = (phase + GeneratorPhaseDelta(generator, slot) + ((orientation & 3) == 0 ? 1 : 0)) % Config.PHASE_COUNT; return phase * Config.ORIENTATION_COUNT + orientation; } static int GeneratorOrientationDelta(int generator, int slot) { return generator switch { 0 => slot + 1, 1 => 2 * slot + 3, 2 => 3 * slot + 2, _ => 5 + slot }; } static int GeneratorPhaseDelta(int generator, int slot) { return (generator + 2 * slot + 1) % Config.PHASE_COUNT; } static int[] GetGeneratorPermutation(int generator) { return generator switch { 0 => new[] { 0, 2, 3, 1 }, 1 => new[] { 3, 1, 0, 2 }, 2 => new[] { 1, 3, 2, 0 }, _ => new[] { 2, 0, 1, 3 } }; } static int Canonicalize(int packed, List<int[]> symmetryGroup) { int best = int.MaxValue; foreach (var perm in symmetryGroup) { int transformed = ApplyPackedPermutation(packed, perm); if (transformed < best) best = transformed; } return best; } static List<int[]> BuildA4SymmetryGroup() { int[] a = { 0, 2, 3, 1 }; int[] b = { 1, 0, 3, 2 }; var result = new List<int[]>(); var seen = new HashSet<int>(); var queue = new Queue<int[]>(); int[] identity = { 0, 1, 2, 3 }; queue.Enqueue(identity); seen.Add(PermutationCode(identity)); while (queue.Count > 0) { int[] p = queue.Dequeue(); result.Add((int[])p.Clone()); int[] pa = ComposePermutations(p, a); int[] pb = ComposePermutations(p, b); int codeA = PermutationCode(pa); int codeB = PermutationCode(pb); if (seen.Add(codeA)) queue.Enqueue(pa); if (seen.Add(codeB)) queue.Enqueue(pb); } result.Sort((x, y) => PermutationCode(x).CompareTo(PermutationCode(y))); return result; } static int[] ComposePermutations(int[] first, int[] second) { int[] result = new int[4]; for (int old = 0; old < 4; old++) result[old] = second[first[old]]; return result; } static int PermutationCode(int[] perm) { return perm[0] + 4 * perm[1] + 16 * perm[2] + 64 * perm[3]; } static int[] ApplyPermutation(int[] slots, int[] oldToNew) { int[] result = new int[4]; for (int old = 0; old < 4; old++) result[oldToNew[old]] = slots[old]; return result; } static int ApplyPackedPermutation(int packed, int[] oldToNew) { int[] slots = UnpackSlots(packed); int[] transformed = ApplyPermutation(slots, oldToNew); return PackSlots(transformed[0], transformed[1], transformed[2], transformed[3]); } static int FindEmpty(int[] slots) { for (int i = 0; i < 4; i++) if (slots[i] == Config.EMPTY) return i; throw new InvalidOperationException("No empty slot found."); } static int FindFirstOccupied(int[] slots, int startSlot) { for (int k = 1; k <= 3; k++) { int s = (startSlot + k) & 3; if (slots[s] != Config.EMPTY) return s; } throw new InvalidOperationException("No occupied slot found."); } static int PackSlots(int s0, int s1, int s2, int s3) { int p = s3; p = p * Config.PACK_BASE + s2; p = p * Config.PACK_BASE + s1; p = p * Config.PACK_BASE + s0; return p; } static int[] UnpackSlots(int packed) { int[] slots = new int[4]; for (int i = 0; i < 4; i++) { slots[i] = packed % Config.PACK_BASE; packed /= Config.PACK_BASE; } return slots; } static int GetTableOffset(int pathId, int level, int motifCount, int levelCount) { return ((pathId * levelCount) + level) * motifCount; } // ============================================================ // FUNCTIONAL GRAPH / ATTRACTOR ANALYSIS // ============================================================ static void AnalyzeFunctionalGraph( int[] next, out int[] cycleId, out int[] cycleRepresentative, out int[] cycleLength, out int[] stepsToCycle) { int n = next.Length; cycleId = new int[n]; cycleRepresentative = new int[n]; cycleLength = new int[n]; stepsToCycle = new int[n]; Array.Fill(cycleId, -1); bool[] done = new bool[n]; int cycleCounter = 0; for (int start = 0; start < n; start++) { if (done[start]) continue; var path = new List<int>(64); var pos = new Dictionary<int, int>(64); int v = start; while (true) { if (done[v]) { int cid = cycleId[v]; int rep = cycleRepresentative[v]; int clen = cycleLength[v]; int steps = stepsToCycle[v]; for (int i = path.Count - 1; i >= 0; i--) { int node = path[i]; done[node] = true; cycleId[node] = cid; cycleRepresentative[node] = rep; cycleLength[node] = clen; stepsToCycle[node] = steps + 1; steps++; } break; } if (pos.TryGetValue(v, out int cycleStart)) { int cid = cycleCounter++; int rep = path[cycleStart]; int clen = path.Count - cycleStart; for (int i = cycleStart; i < path.Count; i++) if (path[i] < rep) rep = path[i]; for (int i = cycleStart; i < path.Count; i++) { int node = path[i]; done[node] = true; cycleId[node] = cid; cycleRepresentative[node] = rep; cycleLength[node] = clen; stepsToCycle[node] = 0; } int steps = 1; for (int i = cycleStart - 1; i >= 0; i--) { int node = path[i]; done[node] = true; cycleId[node] = cid; cycleRepresentative[node] = rep; cycleLength[node] = clen; stepsToCycle[node] = steps; steps++; } break; } pos[v] = path.Count; path.Add(v); v = next[v]; } } } // ============================================================ // DATA INIT / SYNC // ============================================================ static void FillRandomFloatData(GraphicsDevice device, ReadWriteBuffer<float> buffer, int size, int seed) { const int CHUNK = 1_000_000; var rand = new Random(seed); int offset = 0; while (offset < size) { int count = Math.Min(CHUNK, size - offset); float[] tmp = new float[count]; for (int i = 0; i < count; i++) tmp[i] = (float)(rand.NextDouble() * 2.0 - 1.0); buffer.CopyFrom(tmp, 0, offset, count); offset += count; } } static void FillRandomMotifIds(GraphicsDevice device, ReadWriteBuffer<int> buffer, int size, int motifCount, int seed) { const int CHUNK = 1_000_000; var rand = new Random(seed); int offset = 0; while (offset < size) { int count = Math.Min(CHUNK, size - offset); int[] tmp = new int[count]; for (int i = 0; i < count; i++) tmp[i] = rand.Next(motifCount); buffer.CopyFrom(tmp, 0, offset, count); offset += count; } } static void FillRandomPathIds(GraphicsDevice device, ReadWriteBuffer<int> buffer, int size, int pathCount, int seed) { const int CHUNK = 1_000_000; var rand = new Random(seed); int offset = 0; while (offset < size) { int count = Math.Min(CHUNK, size - offset); int[] tmp = new int[count]; for (int i = 0; i < count; i++) tmp[i] = rand.Next(pathCount); buffer.CopyFrom(tmp, 0, offset, count); offset += count; } } static void FillGroupedPathIds(GraphicsDevice device, ReadWriteBuffer<int> buffer, int size, int pathCount) { const int CHUNK = 1_000_000; int offset = 0; while (offset < size) { int count = Math.Min(CHUNK, size - offset); int[] tmp = new int[count]; for (int i = 0; i < count; i++) { long globalIndex = offset + i; tmp[i] = (int)((globalIndex * pathCount) / size); if (tmp[i] >= pathCount) tmp[i] = pathCount - 1; } buffer.CopyFrom(tmp, 0, offset, count); offset += count; } } static void Synchronize(GraphicsDevice device) { using var fence = device.AllocateReadWriteBuffer<int>(1); device.For(1, new FenceKernel(fence)); int[] tmp = new int[1]; fence.CopyTo(tmp); } } }