Тераграф Cloud. Архитектура программного и аппаратного обеспечения
Московский государственный технический университет им. Н.Э.Баумана, 28 октября - 23 декабря 2024
- Аннотация
- 1. Графы знаний
- 2. Структура микропроцессора Леонард Эйлер и вычислительного комплекса Тераграф
- 2.1. Набор команд дискретной математики
- 2.2. Системная архитектура вычислительного комплекса Тераграф
- 2.3. Микроархитектура гетерогенного ядра обработки графов
- 2.4. Принципы взаимодействия микропроцессора Леонард Эйлер и хост-подсистемы
- 2.5. Библиотека lnh64 L0
- 2.6. Взаимодействие CPE(riscv64im) и SPE(lnh64)
- 2.7. Программная модель микропроцессора Леонард Эйлер
- 3. Практакум №1. Разработка и отладка программ в вычислительном комплексе Тераграф
- 4. Практикум №2. Обработка и визуализация графов в вычислительном комплексе Тераграф
- 4.1. Конвейер визуализации графов
- 4.2. Представление информационных моделей алгоритма в виде структур данных
- 4.3. Использование библиотеки шаблонов для обработки графов
- 4.4. Примеры реализации алгоритмов на графах
- 4.5. Библиотека gpc64io
- 4.6. Примеры создания и применения графов знаний
- 4.7. Сборка и запуск примеров проектов
- 4.7.1. Пример 4. Использования языка python и библиотеки gpc64io для приемо-передачи данных между хост-подсистемой и sw_kernel
- 4.8. Индивидуальные задания
- 5. Командный практикум. Обработка и визуализация графов в вычислительном комплексе Тераграф
Организаторы
Алексей Попов, МГТУ им. Н.Э.Баумана |
Станислав Ибрагимов МГТУ им. Н.Э.Баумана |
Егор Дубровин МГТУ им. Н.Э.Баумана |
Ли Цзяцзянь МГТУ им. Н.Э.Баумана |
Максим Калитвенцев, МГТУ им. Н.Э.Баумана |
Михаил Гейне МГТУ им. Н.Э.Баумана |
Гор Парамазян МГТУ им. Н.Э.Баумана |
Тимофей Курохтин МГТУ им. Н.Э.Баумана |
Аннотация
Всем участникам предоставляется доступ к вычислительному комплексу Тераграф для реализации проекта на основе графов знаний.
В ходе практикума студенты знакомяться с архитектурой и принципами работы вычислительного комплекса Тераграф, выполняют практические задания по программированию гетерогенных ядер обработки графов, знакомятся с библиотеками для обработки и визуализации графов. Доступ к вычислительному комплексу осуществляется с использование облачной платформы Тераграф Cloud, обеспечивающей одновременный доступ многих пользователей к гетерогенным ядрам обработки, входящим в состав микропроцессора Леонард Эйлер. В разделах 2 и 3 приводится структура вычислительного комплекса Тераграф, микропроцессора Леонард Эйлер и структура гетерогенного ядра обработки графов, а также особенности их программирования. Приводятся примеры инициализации графов в памяти ядра обработки графов, алгоритмов обработки и создания структур для визуализации. Практикум завершается командным проектом генерации музыкальных композиций, построенных на основе обхода графа знаний (графа ДеБрюйна).
На основе изложенных сведений необходимо разработать распределенное приложение обработки и визуализации графов, функционирующее в системе Тераграф.
Практикум состоит из трех этапов:
-
Исследование принципов функционирования вычислительного комплекса Тераграф
-
Практикум по программированию гетерогенного вычислительного узла
-
Командный практикум по генерации музыкальных композиций на основе графов знаний
1. Графы знаний
1.1. Актуальность создания эффективных программных и аппаратных средств обработки графов
Граф G(V,E) – множество вершин V, на элементах которого определены двуместные отношения смежности (ребра) – (vi, vj) ∈ E, где vi, vj ∈ V (обратите внимание на наличие скобок в первом выражении и их отсутствие во втором). Тогда пара вершин, находящихся в отношении смежности, рассматривается как ребро ek = (vi, vj), ek ∈ E. Вершина vi смежна вершине vj тогда и только тогда, когда существует ребро ek, инцидентное vi, такое, что vj инцидентно ему. Аналогично ребру ek смежно ребро el тогда и только тогда, когда существует вершина vi, инцидентная ребру ek, такая, что el инцидентно этой вершине.
Существует несколько видов графов, отличающихся свойствами предикатов инцидентности – неориентированные, ориентированные, гипер- и ультраграфы, метаграфы.
Графы знаний являются способом представления модели знаний в виде графовой структуры. Технологии представления и обработки знаний в виде графов приобрели большое значение во многих областях, в которых другие методы показали низкую эффективность. Благодаря способности сохранять информацию о различных объектах и явлениях и учитывать связи между ними, графы знаний могут использоваться при анализе больших данных в биоинформатике [1], в персонифицированной медицине, системах безопасности городов [2][3][4][5][6], в компьютерных сетях, финансовом секторе, при контроле сложного промышленного производства, для анализа информации социальных сетей и во многих других областях.
Существенное влияние на эффективность применения аппаратных средств в задачах обработки графов оказывает адекватность их применения в рамках парадигмы рассматриваемых вычислений. Так, ряд задач обработки графов основан на статических графах, изменение которых либо не предусматривается вообще, либо происходит за пределами графового вычислителя. Для такого класса задач обработки графов характерным является этап передачи графа из исходного места хранения в оперативное хранилище графового вычислителя или же потоковая обработка. Подобная обработка позволяет применять классические варианты построения вычислительных систем, в которых передача данных происходит большими или непрерывными пакетами, а останов ритмичной обработки не предусматривается спецификой алгоритмов. Для решения данного класса задач хорошо зарекомендовали себя графические ускорители GPU [7] и матрично-конвейерные структуры на ПЛИС [8]. Второй вариант постановки задач обработки графов отличается тем, что информация графа должна меняться как под воздействием внутренней обработки (например, результата поиска кратчайшего пути или центральных вершин), так и под воздействием внешних факторов (запросов на изменение информации графов). В этом случае граф должен находится непосредственно в оперативной памяти (памяти процессора общего назначения или специального устройства обработки графов). Такой вариант предполагает непрерывность процессов обработки и изменения, что приводит к необходимости применения иных архитектурных принципов. Вычислительные средства, эффективно воплощающие подобную функциональность, опираются на оптимизацию алгоритмов доступа к структурам данных и графам в памяти, на повышение эффективности подсистемы памяти, на увеличение степени параллельности при обработке каждой нити вычислений.
Приведенные выше различия статических и динамических задач обработки графов приводят к тому, что несмотря на большое количество и разнообразие средств вычислительной техники, потребность в высокопроизводительных ЭВМ для решения задач обработки графов знаний, чрезвычайно высока. При решении подобных задач дальнейшее увеличение скорости обработки на основе универсальных микропроцессорных систем трудно достижимо. Даже благодаря высокому уровню параллелизма, глубокой конвейеризации и большим тактовым частотам современные микропроцессоры и графические ускорители не способны эффективно решать проблемы обработки больших графов. Сказываются такие фундаментальные проблемы, как: зависимости по данным [9][10]; необходимость распределения вычислительной нагрузки при обработке нерегулярных графов; наличия конфликтов при доступе к памяти большого количества обрабатывающих ядер [11].
В МГТУ им. Н.Э.Баумана в настоящее время создается вычислительный комплекс, предназначенный для обработки графов и обладающий передовыми техническими характеристиками: аппаратная реализация набора команд дискретной математики, гетерогенная архитектура, хранение и обработка до 1 триллиона вершин графа. В ходе практикума все участникам будет предоставлен доступ к одной карте комплекса Тераграф.
1.2. Применения графов в задачах аналитики данных и искусственном интеллекте
Безусловным достижением последнего десятилетия является внедрение систем анализа данных на основе алгоритмов и методов машинного обучения. Эти технологии позволяют решить одну из важнейших задач: выявление фактов из огромного потока данных. Следующим звеном в цепи интеллектуального анализа данных должна быть система, способная хранить и обрабатывать найденные факты и связи между ними в виде графов знаний (Рисунок 2).
Уже сейчас оказывается недостаточным просто хранить огромные массивы фактов и извлекать их по запросу. Необходимо иметь систему, способную анализировать причинно-следственные связи между событиями, оценивать достоверность и полноту сведений, выявлять и хранить контекстную информацию. Именно графы позволяют получать ответы на те вопросы, которые интересуют пользователя такой системы. Например, насколько вероятно развитие дорожной обстановки по неблагоприятному сценарию и как его избежать, имеют ли место незаконные финансовые операции, кто в них задействован и какие схожие сценарии возможны? При этом подход к формированию ответов на такие вопросы должен принципиально отличаться от простого поиска ситуаций похожих на те, что система видела раньше (как это делается сейчас в нейронных сетях). Аналитическая система будущего должна не просто искать сходства и различия, но уметь логически рассуждать на основе графов знаний. В этом смысле, все известные системе факты и правила будут использоваться для принятия решения. Использование логического вывода на основе графов способно избавить аналитическую систему от ошибок, связанных с игнорированием контекста и здравого смысла, и, главное, делает результат объяснимым.
Важность повышения эффективности алгоритмов на графах и совершенствования вычислительных средств для их реализации привели к появлению такого направления как Graph Data Science. Это подразумевает выделение в отельную научную область интересов всего, что связано с аналитикой данных с использованием графов. В набор средств анализа входят такие алгоритмы, как: обнаружение сообществ в графах, центральность, поиск подобия и изоморфизм, поиска кратчайших путей и максимальных потоков, и ряд других. Приведем некоторые примеры применения графовых алгоритмов для решения важных практических задач.
Обнаружение незаконных финансовых операций
Мошенники делят грязные деньги на множество малых частей и смешивают их с законными средствами, и затем превращают их в легальные активы. Для этого используется круговое движение денежных средств, которое скрывает первоначальный источник за длинной цепочкой транзакций. Графы позволяют построить модель движения денег для таких мошеннических схем и своевременно препятствовать им.
Обнаружение финансового мошенничества в реальном времени
Все банки стремятся ускорить доступ клиентов к услугам и денежным переводам, что представляет собой потенциальную опасность. Необходимо анализировать множество факторов, сопровождающих транзакцию: местоположение клиента и ip адрес устройства; расстояние до предыдущего места нахождения клиента и время, прошедшее с этого момента; номер карты и счета клиента; история расходов клиента по карте и многое другое. Эти данные формируют графовую структуру, позволяя банку оценить отношения событий и имеющихся в его распоряжении данных.
Контроль мошеннической деятельности в налоговой сфере
Системы налогообложения и выявление неуплаты налогов должны постоянно совершенствоваться, чтобы учитывать новые способы ухода от уплаты налогов и мошеннические схемы. С повышением доступности предпринимательства и автоматизацией бизнес-деятельности у преступников появились дополнительные возможности создания подставных юридических лиц, через которых передаются незаконно полученные денежные средства. Графы позволяют анализировать сложные схемы использования подставных юридических лиц и обнаружить мошенническую схему по структуре и взаимосвязи субъектов, участвующих в бизнес-деятельности. Подозрительные паттерны быстро обнаруживаются и выявляются структурные закономерности, которые позволяют установить единый центр мошеннической деятельности.
Промышленное производство и контроль жизненного цикла оборудования
Современное промышленное производство основано на длинных цепочках поставок. При этом сроки поставок и жизненный срок изделий различных поставщиков может отличаться. Если представить масштабы работы таких технически сложных объектов, как энергосеть региона или даже страны, становится понятным сложность планирования и модернизации их работы. Необходимо учесть взаимное влияние возможных отказов оборудования, сложность и стоимость их замены, гарантийный срок службы и т.д. Графы позволяют создать модель таких сложных систем и осуществлять управление ими.
Персонифицированная медицина
Графы хорошо подходят для хранения и визуализации медицинской информации. Данные о состоянии организма пациента являются взаимосвязанными, и могут быть соотнесены с аналогичными данными других пациентов. Компания AstraZeneca провела успешные исследования, в которых граф знаний об организме одного члена “сообщества” использовался при выборе терапии для больного по аналогии с другими подобными случаями.
Биомедицинские исследования
Цепочки химических реакций также представимы в виде графов, в связи с чем в биологии и биомедицине стоит проблема моделирования подобных структур. Обмен веществ в организме человека - это также сеть химических реакций, катализируемых ферментами. В настоящее время изучены более 10 тысяч различных химических реакций, которые происходят в организме для построения клеточных структур. С помощью графов можно описать метаболизм как круговорот атомов, представив в них все реакции с химической структурой небольших соединений (метаболитов). Это, в свою очередь, открывает перед исследователями возможности синтеза новых лекарственных препаратов.
2. Структура микропроцессора Леонард Эйлер и вычислительного комплекса Тераграф
Анализ графов существенно отличается от привычной арифметико-логической обработки. Самыми существенными особенностями алгоритмов обработки графов являются:
-
зависимости по данным между последовательными итерациями поиска и анализа информации
-
большее количество операций доступа к памяти по сравнению с количеством арифметико-логических операций.
Поэтому в МГТУ им. Н.Э.Баумана была разработана гетерогенная архитектура вычислительного комплекса Тераграф
, учитывающая особенности обработки графов. Отличительными чертами комплекса являются:
-
Доступ к графам и их обработку осуществляет специализированный микропроцессор
lnh64
с набором команд дискретной математики (Discrete mathematics instruction set computer
,DISC
). -
Оперативное хранилище графов (так называемая
Локальная память структур
,Local Structure Memory
,LSM
) имеет большой размер (2.5 ГБ на один микропроцессорlnh64
) и организована как ассоциативная память. -
DISC
микропроцессорlnh64
подключен непосредственно к шине памяти малого арифметического процессораriscv64im
. Пара процессоровlnh64
иriscv64im
и составляет гетерогенное ядро обработки графов (Graph Processing Core
,GPC
). -
Множество гетерогенных ядер обработки графов
GPC
составляют многоядерный микропроцессорЛеонард Эйлер
(также обозначается какStructure Processing Unit
,SPU
).
Рассмотрим структуру комплекса Тераграф более подробно.
2.1. Набор команд дискретной математики
Ключевым вопросом при проектировании любого программно-управляемого устройства является выбор набора команд. Так как целями создания микропроцессорного ядра lnh64 являются аппаратная поддержка дискретной математики, набор инструкций составлен на основе таких понятий, как кванторы, отношения и операции над множествами.
Таблица 1 – Соответствие инструкций DISC функциям, кванторам и операциям дискретной математики
Функции, кванторы и операции дискретной математики | Инструкции набора команд DISC |
---|---|
Функция хранения кортежа | INS |
Функция отношения элементов множества | NEXT,PREV,NSM,NGR,MIN,MAX |
Мощность множества | CNT |
Функция принадлежности элемента множеству | SRCH |
Добавление элемента в множество | INS |
Исключение элемента из множества | DEL,DELS |
Исключение подмножества из кортежа | DELS |
Включение подмножества в кортеж | INS,LS,GR,LSEQ,GREQ,GRLS |
Отношение эквивалентности множеств | INS,LS,GR,LSEQ,GREQ,GRLS |
Объединение множеств | OR |
Пересечение множеств | AND |
Разность множеств | NOT |
Последняя версия набора команд DISC состоит из 21 высокоуровневой инструкции, перечисленных в таблице 1:
-
Search (SRCH) выполняет поиск значения, связанного с ключом.
-
Insert (INS) вставляет пару ключ-значение в структуру. SPU обновляет значение, если указанный ключ уже находится в структуре.
-
Операция Delete (DEL) выполняет поиск указанного ключа и удаляет его из структуры данных.
-
Последняя версия набора команд была расширена двумя новыми инструкциями (NSM и NGR) для обеспечения требований некоторых алгоритмов. Команды NSM/NGR выполняют поиск соседнего ключа, который меньше (или больше) заданного и возвращает его значение. Операции могут быть использованы для эвристических вычислений, где интерполяция данных используется вместо точных вычислений (например, кластеризация или агрегация).
-
Maximum /minimum (MAX, MIN) ищут первый или последний ключи в структуре данных.
-
Операция Cardinality (CNT) определяет количество ключей, хранящихся в структуре.
-
Команды AND, OR, NOT выполняют объединения, пересечения и дополнения в двух структурах данных.
-
Срезы (LS, GR, LSEQ, GREQ, GRLS) извлекают подмножество одной структуры данных в другую.
-
Переход к следующему или предыдущему (NEXT, PREV) находят соседний (следующий или предыдущий) ключ в структуре данных относительно переданного ключа. В связи с тем, что исходный ключ должен обязательно присутствовать в структуре данных, операции NEXT/PREV отличаются от NSM/NGR.
-
Удаление структуры (DELS) очищает все ресурсы, используемые заданной структурой.
-
Команда Squeeze (SQ) дефрагментирует блоки локальной памяти, используемые структурой.
-
Команда Jump (JT) указывает код ветвления, который должен быть синхронизирован с хост CPE (команда доступна только в режиме МКОД при синхронной обработке данных CPU и SPU в составе вычислительного комплекса).
Вызов команд lnh64 осуществляется передачей из микропроцессора riscv64im операндов и кода операции. Результаты выполнения команд сохраняются в регистрах результата (ключ и значение) и регистре статуса. Дополнительно предусмотрена очередь результатов, содержащая аналогичные данные, расположенные последовательно в порядке завершения инструкций. Другим способом передачи результата являются так называемые регистры mailbox.
Механизм ожидания результатов mailbox предполагает наличие регистров, чтение данных из которых возможно только при поступлении в них действительных значений результатов. В случае, если регистр не содержит результатов (статус регистра установлен аппаратно в состояние “регистр пуст”), чтение из него вызывает ошибку доступа и приостанавливает транзакцию на шине. При записи данных со стороны аппаратного обеспечения стату регистра устанавливается в состояние “регистр содержит данные”. После прочтения данных со стороны вычислительного элемента CPE результат регистра mailbox аннулируется (статус регистра снова сбрасывается в состояние “регистр пуст”).
Примеры вызова команд и ожидания результатов будут рассмотрены в практической части работы.
2.2. Системная архитектура вычислительного комплекса Тераграф
Комплекс Тераграф предназначен для хранения и обработки графов сверхбольшой размерности и будет применяться для моделирования биологических систем, анализа финансовых потоков в режиме реального времени, для хранения знаний в системах искусственного интеллекта, создания интеллектуальных автопилотов с функциями анализа дорожной обстановки, и в других прикладных задачах. Он способен обрабатывать графы сверхбольшой размерности до 1012 (одного триллиона) вершин и 2·1012 ребер.
Конструктив комплекса состоит из следующих элементов:
- Три однотипных узла обработки графов (смотри рисунок 4 и 5).
- Консоль управления комплексом.
- Сеть связи гетерогенных ядер, построенная на основе высокоскоростных сетевых соединений 100Gb Ethernet, показана оранжевым цветом (смотри рисунок 4 и 5).
- Сеть связи узлов обработки графов, показана зеленым цветом.
- Два источника бесперебойного питания.
- Подсистема обработки графов состоит из трех однотипных карт микропроцессора Леонард Эйлер. Каждый микропроцессор содержит 24 гетерогенных ядра обработки графов.
- Подсистема хранения графов, реализованная на основе твердотельных накопителей.
- Хост-подсистема, основанная на микропроцессорах общего назначения, оперативной памяти и накопителей на жестких магнитных дисках.
- Подсистема сетевого взаимодействия узлов обработки графов.
В зависимости от решаемой задачи, имеющиеся аппаратные ресурсы комплекса, а также хранилища данных могут быть программно перераспределены между задачами обработкой множеств и графов, вычислительными задачами общего назначения, задачами машинного обучения, а также обработкой на специализированных ускорителях вычислений на базе ПЛИС FPGA. Комплекс сопровождается программными библиотеками системного и прикладного уровней. Для доступа к аппаратным ресурсам используется контейнеризация прикладных задач. Благодаря этому обеспечивается возможность разделения ресурсов между пользователями и их запущенными задачами, упрощается создание прикладных программ и достигается высокий уровень быстродействия комплекса.
Структурная схема одного узла представлена на рисунке 7.
Для объединения подсистем обработки графов в единый комплекс предусмотрены сетевые интерфейсы: на уровне узла применяется подсистема сетевого взаимодействия узлов; на уровне ядер GPC применяется кольцевая сеть связи ядер.
Далее рассмотрим подробнее подробно структуру и состав подсистем гетерогенного узла обработки графов.
2.2.1. Хост-подсистема
Основная вычислительная системы (так называемая хост-подсистема) берет на себя функции управления запуском вычислительных задач, поддержкой сетевых подключений, обработкой и балансировкой нагрузки. В хост-подсистему входят два многоядерных ЦПУ по 26 ядер каждый, оперативная память на 1 Тбайт и дополнительная энергонезависимая память на 8 Тбайт, где хранятся атрибуты вершин и ребер графа, буферизируются поступающие запросы на обработку и визуализацию графов, хранятся временные данные об изменениях в графах. В хост-подсистеме используется процессор с архитектурой x86 для обеспечения сетевого взаимодействия и связи системы с внешним миром. В функции хост-подсистемы входят:
-
на стадии инициализации комплекса: настройка сетевой подсистемы, подсистем хранения и обработки графов;
-
на стадии создания/изменения графов в локальной памяти подсистемы обработки графов: реализация очередей запросов на вставку/изменения, балансировка запросов к DISC системам, выделение и освобождение структур, контроль выполнения операций изменения;
-
на стадии запуска алгоритмов оптимизации: буферизация запросов оптимизации, инициализация процедур обработки, их запуск и контроль исполнения;
-
на стадии визуализации графов: буферизация запросов на визуализацию, настройка процедур формирования представлений графов для пользовательских процессов, запуск формирования представлений и контроль результатов, буферизация и передача представлений или изменений в представлениях пользовательским процессам.
Указанные функции реализованы в Программном ядре хост-подсистемы (host software kernel) – программном обеспечении, взаимодействующим с подсистемой обработки графов через шину PCIe.
2.2.2. Подсистема хранения графов
В подсистему хранения графов входят основная память 30Тбайт, состоящая из четырех NVMe SSD дисков по 7,7 Тбайт каждый. Технология NVMe (Non-Volatile Memory Express) обеспечивает интерфейс связи с увеличенной полосой пропускания, что повышает производительность и эффективность обработки графов. Доступ к подсистеме хранения осуществляется как из хост-подсистемы, так и из Подсистемы обработки графов через механизм прямого доступа к памяти.
2.2.3. Подсистема связи узлов
Данная подсистема представляет собой карту PCIe c двумя сетевыми интерфейсами 100Gb Ethernet, размещенную в каждом узле обработки графов. Подсистема позволяет организовать соединение гетерогенного узла с каждым другим узлом комплекса. Шина PCIe обеспечивает высокопроизводительное взаимодействие хост-подсистемы с процессорами Леонард Эйлер, а также последних с подсистемой хранения графов.
2.2.4. Подсистема связи ядер
Подсистема связи ядер представляет собой кольцевую сеть передачи данных, объединяющую все микропроцессоры Леонард Эйлер комплекса. Благодаря этому может быть достигнут высокий уровень производительности системы в случае параллельной обработки графов. Так как гетерогенные ядра обработки графов имеют непосредственный доступ к подсистеме связи, что исключает участие в этом хост-подсистемы, отсутствует централизованное управление передачей, и тем самым устраняется бутылочное горло централизованных подходов к коммутации.
Протокол реализует физический, канальный и сетевой уровни взаимодействия, и обеспечивает следующие возможности:
- Организацию нескольких ядер в логическую группу, взаимодействие в рамках которой аппаратно изолировано от других групп. Информация, передаваемая между устройствами группы, не может быть прочтена или изменена другими устройствами, подключенными к сети.
- Очереди сообщений TX и RX для каждого ядра обработки графов. Поддерживаются очереди сообщений для передачи и приема для каждого ядра. Информация о загрузке очередей устройств передается в виде служебных пакетов канального уровня.
- Обмен данными внутри логической группы в режиме Unicast и Multicast поддерживается аппаратным обеспечением сетевого уровня.
2.2.5. Подсистема обработки графов
Подсистема обработки графов каждого узла комплекса состоит из 3-х или 4-х карт (в зависимости от версии комплекса) многоядерных микропроцессоров Леонард Эйлер, каждый из которых в свою очередь включает 3 или 4 группы гетерогенных ядер (так называемых Core Groups, CG). В каждую такую группу входят от 2-х до 6-ти ядер DISC GPC, обладающих следующими характеристиками: объем доступной локальной памяти для хранения графов - до 2.5 Гбайт; разрядность ключей и значений - 64 бита; количество хранимых ключей и значений - до 117 миллионов; количество одновременно хранимых структур в локальной памяти структур - до 7; объем ОЗУ CPE - 64 КБайт. Взаимодействие гетерогенных DISC ядер и хост-подсистемы осуществляется как через блоки H2C и С2H с использованием механизма прямого доступа к памяти.
Таким образом, комплекс «Тераграф» может содержать до 288 гетерогенных ядра DISC GPC, и хранить в оперативном доступе (в локальной памяти подсистемы обработки графов) до 11 миллиардов вершин. Группа ядер Core Group содержит контроллеры памяти, которые обеспечивает взаимодействие между GPC и Локальной памятью структур типа DDR4, а также с Глобальной памятью. Один и тот же блок Глобальной памяти используется всеми гетерогенными ядрами группы для передачи данных внутри группы и обмена данными с хост-подсистемой.
Структурная схема микропроцессора Леонард Эйлер версии 4 представлена на рисунке 8. В качестве единицы передаваемых данных принят блок размером 384Б, который передается между хост-подсистемой и группой ядер CG с помощью механизмов прямого доступа к памяти.
2.3. Микроархитектура гетерогенного ядра обработки графов
Как было отмечено ранее, обработка графов в системе Тераграф выполняется в гетерогенных ядрах, состоящих из микропроцессоров двух типов: CPE и SPE (см. рисунок 9). При этом CPE является универсальным RISC ядром с арифметическим набором команд, в то время как SPE реализует набором команд дискретной математики. Каждый вычислительный элемент CPE состоит из очереди команд, блока выборки, блока декодирования команд, модуля предсказания переходов, арифметико-логического устройства, устройства доступа в память, интерфейса AXI4MM, блока ветвлений и интерфейса шины ускорителя AXL. Также вычислительный элемент связан шиной памяти с ПЗУ, в которой записан загрузчик, обеспечивающий передачу программных ядер. Для размещения программ и данных, каждый CPE имеет оперативную память размером 64КБ.
Процессор обработки структур SPE представляет собой управляемое специальным набором команд, и предназначено для хранения и обработки больших множеств дискретной информации. Он состоит из модуля очередей, блока выборки и контроля, памяти структур, операционного буфера, блока обработки трассы в деревьях, двух-ассоциативного блока хранения трассы и модуля интерфейса шины AXI.
Под управлением поступающих из хост-системы команд SPE выполняет хранение ключей и значений в многоуровневой подсистеме памяти, выполняет поиск, изменение и выдачу информации другим устройствам системы. Для ускорения поиска и обработки всего набора команд микропроцессор использует внутренне представление множеств в виде B+ дерева, для которого возможна параллельная обработка нескольких вершин дерева как на промежуточных уровнях, используемых для поиска, так и на нижнем уровне, хранящем непосредственно ключи и значения. В связи с этим любая операция над структурой начинается с поиска информации в B+ дереве, а заканчивается обработкой вершин нижнего уровня.
Последовательность вершин B+ дерева, составляющая путь для аппаратного поиска ключа от корня до листа, называется трассой. Трасса сохраняется во внутренней памяти процессора обработки структур для возможности оперативного доступа. После формирования полной трассы в каталоге становится известным информация вершины нижнего уровня, где хранятся ключи и значения. Далее следует загрузить вершину нижнего уровня из внутренней памяти структур в операционный буфер. Для этого требуется транслировать номер вершин в двоичный код, выборки который служит для обращения во внутреннюю память структур. В качестве кода может использоваться как простой линейный адрес ОЗУ, так и код выборки строки из специального массива. Если вершина отсутствует в памяти структур, это приводит к вычислению ее адреса, загрузке вершины из внешней памяти – локальной памяти структур.
Вершина нижнего уровня B+ дерева, состоящая из некоторого количества ключей и значений, хранится в специальной памяти внутри памяти структур и называется линейкой. Информация одной линейки может обрабатываться в параллельно.
Операционный буфер принимает блок данных, составляющий вершину нижнего уровня, и сохраняет ее во внутренних процессорных элементах, обеспечивающих выполнение однотипных команд над каждым ключом. Требуется выполнять вставку новых ключей в соответствии с их порядком, т.е. поддерживая упорядоченной при хранении. В следствии такой вставки может произойти переполнение линейки, что требует добавления вытесненного значения в следующую линейку или изменение структуры дерева таким образом, чтобы за переполненной линейкой была добавлена новая, после чего в нее будет перенесена часть ключей.
Другой важной функцией операционного буфера является выполнение операций над несколькими линейками для реализации И-ИЛИ-НЕ операций и команд срезов. Таким образом, операционный буфер выполняет изменение линеек и передает их обратно в память структур, а также передает информацию о результатах обработки устройству управления. Модифицированная трасса, в случае создания новых линеек, при необходимости полной или частичной загрузки новой трассы, должна быть сохранена во внешней памяти структур.
Таким образом, основная работа с графами проводится в специально разработанном блоке вычисления структур, что повышает эффективность работы.
2.4. Принципы взаимодействия микропроцессора Леонард Эйлер и хост-подсистемы
Основу взаимодействия подсистем при обработке графов составляет передача сообщений между GPC и хост-подсистемой. Для передачи сообщений для каждого GPC реализованы два аппаратных блока, реализующие механизмы прямого доступа к памяти хоста. Блоки содержат FIFO буферы на 512 64-х битных записей: H2C для передачи от хост-подсистемы к ядру, и C2H для передачи в обратную сторону.
Обработка начинается с того, что скомпилированный программный код CPE, называемое далее “программное ядро” (software kernel) загружается в локальное ОЗУ одного или нескольких CPE (микропроцессора riscv64im). Для этого используется программно доступная конфигурационная Глобальная память размером 64КБ, расположенная в микропроцессоре Леонар Эйлер (смотри рисунок 10). Далее в ядро, подлежащее инициализации, передается сигнал инициализации. В свою очередь, инициализируемый GPC (один или несколько) вместе с сигналом инициализации получают информацию о размере образа sowftware kernel. После этого управление принимает специальный загрузчик, хранимый в ПЗУ CPE. Загрузчик выполняет копирование программного ядра из Глобальной памяти в ОЗУ CPE и передает управление на начальный адрес программы обработки. Предусмотрен режим работы GPC, при котором во время обработки происходит обмен данными и сообщениями. Эти два варианта работы реализуется через буферы и очереди соответственно. На рисунке 10 представлена диаграмма последовательностей первого сценария работы – вызов обработчика с передачей параметров и возвратом значения через очередь сообщений.
Если код программного ядра уже загружен в ОЗУ CPE, хост-подсистема может вызвать любой из содержащихся в нем обработчиков. Для этого в GPC передает оговоренный номер обработчика (handler), после чего автоматически передается сигнал запуска указанного обработчика (сигнал START). В ответ CPE устанавливает состояние BUSY и начинает саму обработку. В ходе обработки ядро может обмениваться сообщениями с хост-подсистемой через очереди (команды записи в очередь и чтения из очереди). По завершении обработки устанавливается состояние IDLE, что приводит к выработке прерывания DONE, которое перехватывается хост-подсистемой и информирует пользовательский код об изменении состояния программного ядра. Далее, пользовательское приложение хост-подсистемы может прочитать результат (если таковой был передан через передачу сообщений), и повторить всю указанную процедуру.
Основным назначением вычислительного ядра CPE является управление ходом работы ядра обработки структур SPE. Принимаемые от Хост-подсистемы данные преобразуются в запросы к ядру SPE lnh64. На рисунке 11 показан пример, когда полученные от хост-подсистемы данные транслируются в запрос вставки ключа и значения в одну из структур по команде lnh_ins(str,key,val). Второй блок данных, полученных с использованием механизма передачи данных через очереди, приводит в выдаче команды поиска по ключу lnh_search(str,key). Результат поиска передается в блок C2H и и использованием механизма прямого доступа к памяти оказывается в памяти хоста. Более подробно, механизм прямого доступа к памяти будет рассмотрен ниже.
2.5. Библиотека lnh64 L0
Библиотека lnh64 L0 (библиотека уровня “ноль”) представляет собой API системного уровня, реализующего базовые функции взаимодействия с драйвером ускорительной карты микропроцессора Леонард Эйлер. Библиотека позволяет:
- Создание файлового дескриптора для эксклюзивного доступа к символьному устройству GPC
/dev/gpc*
. - Инициализация вычислительного элемента CPE.
- Запуск обработчика вычислительного элемента CPE.
- Прием и передачи сообщений для взаимодействия Host и CPE.
- Взаимодействие CPE и SPE (микропроцессором lnh64 DISC).
Библиотека разделена на две части, представленные в таблице 2.
Таблица 2 - Описание частей библиотеки lnh64 L0
Раздел библиотеки | Описание | Язык программирования | Архитектура, Компилятор | Способ отладки |
---|---|---|---|---|
Host Lib | Управления хост-подсистемой и взаимодействие с ядрами GPC | C/C++, объектная модель | x86, g++ | Jupyter,VSCode,gdb |
SW Kernel Lib | Взаимодействие с микропроцессором lnh64 | C/C++, процедурная модель | riscv64, g++ | Вывод сообщений |
Функциональные возможности Host Lib:
- Получение информации от дайвера о количестве и состоянии гетерогенных ядер gpc
- Открытие указанного пользователем или любого свободного gpc в эксклюзивнымй доступ пользователя.
- Инициализация буферов пользователького пространства для взаимодействия хост-подсистемы и подсистемы обработки графов (ядер обработки графов GPC).
- Передача и прием данных и сообщений к/от GPC через очереди
mq
(методmq_send()
иmq_receive()
).
Функциональные возможности SW Kernel Lib:
- Обмен сообщениями с хост-подсистемой через очереди
mq
(методmq_send()
иmq_receive()
). - Установка состояния sw_kernel (IDLE - свободен ,BUSY - занят).
- Вызов пользовательского прерывания sw_interrupt с передачей 64 бит данных (так называемый payload)
- Сервиcные функции для преобразования типов float и double в беззнаковый образ (необходимо для хранения данных вещественных типов в качестве ключей lnh64)
- Реализация вызовов команд DISC в SPE DISC lnh64 (полное название: Процессорный элемент обработки структур данных с набором команд дискретной математики и микроархитектурой lnh64).
- Передача операндов и кодов операций набора команд дискретной математики DISC в микропроцессор lnh64.
- Контроль результатов исполнения DISC команд.
2.5.1. Структура базового приложения “hello world”
Для использования функций библиотек необходимо также реализовать основные приложения для host и sw_kernel частей. Рассмотрим примеры таких приложений, выполняющих базовые операции. Ниже приведен пример кода программы хост-подсистемы, выполняющей инициализацию и измерение тактовой частоты GPC (hello world). Код доступен по следующему адресу: Леонард Эйлер, пример 1
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <time.h>
#include "host_main.h"
#define BUF_SIZE 117440512*sizeof(unsigned long long)
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
int main(int argc, char** argv)
{
unsigned int err = 0;
unsigned long long data;
double LNH_CLOCKS_PER_SEC;
clock_t start, stop;
gpc *gpc64_inst;
if (argc<2) {
printf("Usage: host_main <rawbinary file>\n");
return -1;
}
//Захват ядра gpc и запись sw_kernel
gpc64_inst = new gpc();
printf("Open gpc on %s\n",gpc64_inst->gpc_dev_path);
if (gpc64_inst->load_swk(argv[1])==0) {
printf("Rawbinary loaded from %s\n",argv[1]);
}
else {
printf("Rawbinary %s file error\n",argv[1]);
return -1;
}
//Обработкчик для чтения версии sw_kernel
gpc64_inst->start(__event__(get_version));
printf("sw_kernel version: 0x%0llx\n", gpc64_inst->mq_receive());
//Запуск обработчика для измерения тактовой частоты gpc
gpc64_inst->start(__event__(frequency_measurement));
gpc64_inst->sync();
sleep(1);
gpc64_inst->sync();
LNH_CLOCKS_PER_SEC = (double)gpc64_inst->mq_receive();
printf("Leonhard clock frequency (LNH_CF) %f MHz\n", LNH_CLOCKS_PER_SEC/1000000.0);
//Обработкчик для посылки эхо-пакетов
gpc64_inst->start(__event__(echo_mq));
//Создание исходного массив
unsigned long long *buf_out=(unsigned long long *)malloc(BUF_SIZE);
unsigned long long *buf_in=(unsigned long long *)malloc(BUF_SIZE);
for (int i=0;i<(BUF_SIZE>>3);i++) {
buf_out[i]=(rand()<<32)|rand();
}
//Запуск потоков приема-передачи
auto send_thread = gpc64_inst->mq_send(BUF_SIZE,(char*)buf_out);
auto receive_thread = gpc64_inst->mq_receive(BUF_SIZE,(char*)buf_in);
send_thread->join();
receive_thread->join();
for (int i=0;i<(BUF_SIZE>>3);i++) {
if (buf_out[i]!=buf_in[i]) {
printf("Error: buf_out[%d]=0x%016llx - buf_in[%d]=0x%016llx\n",i,buf_out[i],i,buf_in[i]);
err++;
}
}
if (!err) {printf("Test done\n");}
// gpc64_inst->finish(); //newer finished
//Освобождение ресурсов
free(gpc64_inst);
return 0;
}
Для представленного листинга должен быть также создан и скомпилирован ответный код для микропроцессора riscv64im, который будет работать в составе гетерогенного ядра обработки графов. В коде должна быть реализована логика установки состояния ядра: одно из двух состояний READY или BUSY. Также разработчиком должы быть реализованы обработчики вызываемых из хост-подсистемы функций get_version(), get_lnh_status_high(), get_lnh_status_low(), frequency_measurement().
Номер обработчика может быть задан явным образом в Xост и sw_kernel частях, однако удобнее использовать механизм автоматической нумерации обработчиков на основе макросов С. Для этого мы будем использовать файл gpc_handkers.h, который должен быть включен как в проект хоста, так и в проект sw_kernel:
/*
* gpc_handlers.h
*
* host and sw_kernel library
*
* Macro instantiation for handlers
*
*/
#ifndef DEF_HANDLERS_H_
#define DEF_HANDLERS_H_
#define DECLARE_EVENT_HANDLER(handler) \
const unsigned int event_ ## handler =__LINE__; \
void handler ();
#define __event__(handler) event_ ## handler
// Event handlers declarations by declaration line number
DECLARE_EVENT_HANDLER(frequency_measurement);
DECLARE_EVENT_HANDLER(get_lnh_status);
DECLARE_EVENT_HANDLER(get_version);
DECLARE_EVENT_HANDLER(echo_mq);
#endif
Таким образом, условное имя обработчика ставится в однозначное соответствие номеру строки, в которой он объявлен в файле include/gpc_handlers.h.
В результате получим следующий код основного модуля sw_kernel
#include <stdlib.h>
#include "lnh64.h"
#include "gpc_io_swk.h"
#include "gpc_handlers.h"
#define SW_VERSION 0x20232109
#define __fast_recall__
extern lnh lnh_core;
volatile unsigned int event_source;
int main(void) {
/////////////////////////////////////////////////////////
// Main Event Loop
/////////////////////////////////////////////////////////
//Leonhard driver structure should be initialised
lnh_init();
for (;;) {
//Wait for event
event_source = wait_event();
switch(event_source) {
/////////////////////////////////////////////
// Measure GPN operation frequency
/////////////////////////////////////////////
case __event__(frequency_measurement) : frequency_measurement(); break;
case __event__(get_lnh_status) : get_lnh_status(); break;
case __event__(get_version): get_version(); break;
case __event__(echo_mq): echo_mq(); break;
}
set_gpc_state(READY);
}
}
//-------------------------------------------------------------
// Глобальные переменные
//-------------------------------------------------------------
unsigned int LNH_key;
unsigned int LNH_value;
unsigned int LNH_status;
uint64_t TSC_start,TSC_stop;
int i,j;
unsigned int err=0;
//-------------------------------------------------------------
// Измерение тактовой частоты GPN
//-------------------------------------------------------------
void frequency_measurement() {
sync(); //синхронизация с host
TSC_start = TSC; //сохранение счетчика тактов
sync(); //синхронизация с host
mq_send_flush(TSC-TSC_start); //вычисление количества тактов и перезача в host
}
//-------------------------------------------------------------
// Получить версию микрокода
//-------------------------------------------------------------
void get_version() {
mq_send_flush(SW_VERSION); //передача в очередь c2h номера версии программы
}
//-------------------------------------------------------------
// Получить регистр статуса LOW Leonhard
//-------------------------------------------------------------
void get_lnh_status() {
mq_send_flush(LNH_STATE); //передача в очередь c2h номера аппаратной версии SPE lnh64
}
//-------------------------------------------------------------
// Передача эхо пакетов через очереди сообщений
//-------------------------------------------------------------
void echo_mq() {
while(1) {
unsigned long long data = mq_receive(); //получить сообщение
mq_send_flush(data); //передать сообщение
}
}
2.5.2. Обмен данными между GPC и хост-подсистемой через аппаратные очереди
Взаимодействие обработчика sw_kernel и хост-подсистемы осуществляется через апааратные очереди сообщений c2h и h2c с помощью команд передачи и приема (mq_send()
и mq_receive()
). Передача сообщений на аппаратном уровне выполняется с использованием механизма прямого доступа к памяти, что существенно ускоряет обмен больших блоков данных. Отметим, что данные могут ритмично передаваться только в том случае, если принимающая сторона выполняет их чтение. В противном случае внутренние буферы будут переполнены, и передача временно прекратится.
В хост подсистеме реализованы многопоточные асинхронные методы передачи и приема сообщений, блокирующие доступ других потоков к приему и передаче. Это позволяет запускать множество потоков одновременно, и при этом не нарушать последовательность их запуска. Рассмотрим методы класса gpc, описанного в библиотеке lnh64 L0
:
Таблица 3 - Методы класса gpc для передачи и приема сообщений хост-подсистемой
Метод класса | Назначение |
---|---|
void mq_send(unsigned long long data) | Синхронная передача 8 байт (базовый размер операнда lnh64) в gpc. |
std::thread* mq_send(unsigned int bufsize,char *buf) | Асинхронная передача блока данных из буфера buf, bufsize байт. Возвращает указатель на объект потока thread, в котором выполняется запись. Несколько запущенных потоков выполняются последовательно в порядке запуска. |
unsigned long long mq_receive() | Синхронное получение 8 байт из gpc. Возвращает данные типа unsigned long long |
std::thread* mq_receive(unsigned int bufsize,char *buf) | Асинхронный прием блока данных из gpc в буфер buf, bufsize байт. Возвращает указатель на объект потока thread, в котором выполняется чтение. Несколько запущенных потоков выполняются последовательно в порядке запуска. |
Потоки mq_send() и mq_receive() могут и должны выполняться параллельно, что приводит к необходимости синхронизации вычислений в хост-подсистеме и gpc. В связи с этимна обоих сторонах реализован метод sync()
, осуществляющий процедуру рукопожатия двух сторон.
На стороне хост-подсистемы:
//====================================================
// Синхронизация с gpc (рукопожатие)
//====================================================
void gpc::sync()
{
mq_send(0xdeadbeafdeadbeaf);
while (mq_receive()!=0xbeafdeadbeafdead);
}
Пример ответного кода sw_kernel:
//====================================================
// Синхронизация с хостом (рукопожатие)
//====================================================
void sync()
{
while (mq_receive()!=0xdeadbeafdeadbeaf);
mq_send_flush(0xbeafdeadbeafdead);
}
Со стороны sw_kernel, работающего в gpc, также реализованы функции приема и передачи сообщений. Отличие от хост подсистемы состоит в том, что в sw_kernel принята процедурная модель разработки, существенно сокращающая объем кода. Ядро CPE riscv64im не реализует многозадачность, многонитевость или многопоточность, в связи с чем программист дожен представить программу в последовательном виде. В случае, если требуется реализовать прием и передачу сообщений одновременно, необходимо попеременное чтение данных, используя чтение в буфер (0 и более байт), и в зависимости от потребностей алгоритма выполнить отправку необходимой информации.
Для реализации всех необходимых разработчику действий в библиотеке sw_kernel-lib реализованы следующие функции:
Таблица 4 - Функции для передачи и приема сообщений программным ядром sw_kernel
Функция | Назначение |
---|---|
void mq_send(unsigned long long data); | Передача 8 байт (базовый размер операнда lnh64) в хост-подсистему |
void mq_send_flush(unsigned long long data); | Ожидание завершения потока передачи (любого из ранее описанных методов mq_send) |
void mq_send(unsigned int bufsize,char *buf); | Передача блока данных из буфера buf, bufsize байт в хост-подсистему |
unsigned long long mq_receive(); | Получение 8 байт из хост-подсистемы |
unsigned int mq_receive(unsigned int bufsize,char *buf); | Получение блока данных из хост-подсистемы в буфер buf, не более bufsize байт (от 0 до bufsize) |
На аппаратном уровне gpc передача реализована в двух вариантах:
- Пакетная передача. Данный способ передачи обладает наибольшей эффективностью, так как за цикл прямой передачи в память хост-подсистемы осуществляется передача большого пакета данных и поле данных в пакете pcie используется в максимальной степени.
Такой вариант передачи осуществляется послеовательной записью данных по адресу FIFO буфера очереди c2h:
for (offs=0;offs<bufsize;offs+=8) { C2H = *((volatile unsigned long long*)(buf+offs)); }
Однако, если подготовленный пакет не заполнен полностью (384 байт), данные ожидают в буфере и не передаются. Это может оказаться неудобным в том случае, когда программист хочет незамедлительно уведомить хост-подсистему о наличии новых данных. В связи с этим предусмотрен второй вариант обмена.
- Незамедлительная отправка сообщений. Способ предусматривает передачу всех накопленных в выходной FIFO очереди данных, сколько бы их там не находилось. Минимальный размер составляет 8 байт, максимальный размер ограничен размером FIFO очереди (для текущей версии составляет 4KБайт-16 байт). В этом случает программист, помимо записи данных в C2H (адрес FIFO буфера), в следующей команде посылает флаг в регистр C2H_FLUSH:
void mq_send(unsigned int bufsize,char *buf) {
unsigned int offs;
for (offs=0;offs<bufsize;offs+=8) {
C2H = *((volatile unsigned long long*)(buf+offs));
}
C2H_FLUSH = 1;
}
2.6. Взаимодействие CPE(riscv64im) и SPE(lnh64)
Микропроцессор lnh64 с набором команд дискретной математики (Discrete Mathematics Instruction Set Computer) является ассоциативным процессором, т.е. устройством, выполняющим операции обработки над данными, хранящимися в ассоциативной памяти (так называемой Локальной памяти структур). В качестве таковой выступает адресная память DDR4, причем для каждого ядра lnh64 доступны 2.5 ГБ адресного пространства в ней. Для организации ассоциативного способа доступа к адресному устройству микропроцессор lnh64 организует на аппаратном уровне структуру B+дерева. Причем 512МБ занимает древовидая структура от верхнего и до предпоследнего уровня, 2048МБ занимает последний уровень дерева, на котором и хранятся 64х разрядные ключи и значения. Каждый микропроцессор lnh64 может хранить и обрабатывать до 117 миллионов ключей и значений.
Исходя из этого, обработка множеств или графов представляется в DISC наборе команд, как работа со структурами ключей и значений (key-value). Однако, как было показано ранее при описании набора команд DISC, в отличие от общепринятых key-value хранилищ, доступны такие операции как ближайший больший (NGR), ближайший меньший (NSM), команды объединения множеств (OR) и ряд других. Это и позволяет использовать lnh64 в качестве устройства, хранящего большие множества (для графов это множества вершин и ребер).
2.6.1. Описание регистров микропроцессорного ядра SPE с набором команд дискретной математики DISC
Доступ к микропроцессору lnh64 (Structure Processing Element) осуществляется чтением и записью в пространство памяти микропроцессора riscv64im (Computing Processing Element) в диапазоне 0x300000 - 0x301000. Карта памяти представлена в файле gpc_swk.h:
Микропроцессор lnh64 получает на вход команды 6 различных форматов. Так, для команды вставки INS задействуются регистр кода операции, регистр ключа операнда и регистр значения операнда. Результатом выполнения команды является статус ее исполнения, который записывается в регистр статуса. Для команды поиска SRCH задействуются регистры ключа операнда и регистр кода операции, а результаты записываются в регистры ключа результата, значения результата и регистр статуса.
Перечень программно-доступных регистров и их смещения в адресном пространстве относительно базового адреса (0x300000) указан в таблице 5:
Таблица 5 - Программно доступные регистры lnh64
Регистр | Смещение | Режим | Начальное значение | Назначение |
---|---|---|---|---|
KEY2LNH | 0x0000 | W | 0x00000000 | Регистр содержит ключ операнда команд DISC |
LNH2KEY | 0x0000 | R | 0x00000000 | Регистр содержит ключ результата команды DISC |
VAL2LNH | 0x0008 | W | 0x00000000 | Регистр содержит значение операнда команд DISC |
LNH2VAL | 0x0008 | R | 0x00000000 | Регистр содержит значение результата команд DISC |
CMD2LNH | 0x0010 | W | 0x00000000 | Регистр содержит код операции DISC |
LNH_STATE | 0x0010 | R | 0x09110611 | Регистр содержит статус микропроцессора lnh64 |
CARDINALITY | 0x0018 | R | 0x00000000 | Количество ключей в структуре , указанной в поле R регистра CMD2LNH |
LNH_CNTL | 0x0020 | W | 0x00000000 | Регистр управления |
LNH2KEYQ | 0x0028 | R | 0x00000000 | Очередь ключей результатов команд DISC |
LNH2VALQ | 0x0030 | R | 0x00000000 | Очередь значений результатов команд DISC |
LNH_STATEQ | 0x0038 | R | 0x00000000 | Очередь статуса результатов команд DISC |
TSC | 0x0040 | R | 0x00000000 | Регистр счетчика тактов |
СSC | 0x0048 | R | 0x00000000 | Регистр счетчика тактов исполнения команд |
DBG_A | 0x0050 | R | 0x00000000 | Регистр отладки A |
DBG_B | 0x0058 | R | 0x00000000 | Регистр отладки B |
DBG_C | 0x0060 | R | 0x00000000 | Регистр отладки C |
DBG_D | 0x0068 | R | 0x00000000 | Регистр отладки D |
MR0 | 0x0080 | R | 0x00000000 | Регистр 0 mailbox с ожиданием результата |
MR1 | 0x0088 | R | 0x00000000 | Регистр 1 mailbox с ожиданием результата |
MR2 | 0x0090 | R | 0x00000000 | Регистр 2 mailbox с ожиданием результата |
MR3 | 0x0098 | R | 0x00000000 | Регистр 3 mailbox с ожиданием результата |
MR4 | 0x00A0 | R | 0x00000000 | Регистр 4 mailbox с ожиданием результата |
MR5 | 0x00A8 | R | 0x00000000 | Регистр 5 mailbox с ожиданием результата |
MR6 | 0x00B0 | R | 0x00000000 | Регистр 6 mailbox с ожиданием результата |
MR7 | 0x00B8 | R | 0x00000000 | Регистр 7 mailbox с ожиданием результата |
Регистр управления содержит биты для управления ресурсами lnh64. Биты 0..31 устанавливаются в необходимое состояние записью соответствующего значения в регистр LNH_CNTL. Биты 32..63 при записи логической 1 выдают одиночный импульс сброса ресурса, после чего автоматически устанавливаются в значение 0. Назначение бит для регистра управления представлено в таблице 6.
Таблица 6 -Назначение разрядов регистра управления
Название | Бит | Назначение |
---|---|---|
ALLOW_LNH_FLAG | 0 | Разрешение работы lnh64 |
SUSPEND_Q_FLAG | 1 | Останов выдачи транзакций из очереди запросов на запись в lnh64 |
LSM_DMA_FLAG | 2 | Разрешение прямого доступа к LSM |
LCM_DMA_FLAG | 3 | Не используется |
ENABLE_TSC_FLAG | 4 | Разрешение работы счетчика тактов |
ENABLE_READY_INT | 5 | Не используется |
RESET_MAILBOX[0] | 32 | Запуск импульса сброса регистра mailbox[0] |
RESET_MAILBOX[1] | 33 | Запуск импульса сброса регистра mailbox[1] |
RESET_MAILBOX[2] | 34 | Запуск импульса сброса регистра mailbox[2] |
RESET_MAILBOX[3] | 35 | Запуск импульса сброса регистра mailbox[3] |
RESET_MAILBOX[4] | 36 | Запуск импульса сброса регистра mailbox[4] |
RESET_MAILBOX[5] | 37 | Запуск импульса сброса регистра mailbox[5] |
RESET_MAILBOX[6] | 38 | Запуск импульса сброса регистра mailbox[6] |
RESET_MAILBOX[7] | 39 | Запуск импульса сброса регистра mailbox[7] |
RESET_SPU | 48 | Сброс lnh64 в начальное состояние (мягкий сброс) |
RESET_ALL_QUEUES | 49 | Сброс состояния всех очередей |
RESET_LNH2AXI_QUEUE | 50 | Сброс очереди запросов на чтение lnh64 |
RESET_AXI2LNH_QUEUE | 51 | Сброс очереди запросов на запись lnh64 |
RESET_TSC | 52 | Сброс счетчика тактов |
RESET_RISCV | 53 | Аппаратный сброс lnh64 в начальное состояние |
Регистр статуса позволяет отслеживать готовность результатов выполнения операций (готовность, наличие ошибки), состояние очередей, версию аппаратного обеспечения и ряд других параметров. Назначение бит для регистра статуса представлено в таблице 7.
Таблица 7 - Назначение разрядов регистра статуса
Название | Бит | Назначение |
---|---|---|
SPU_READY_FLAG | 0 | Флаг завершения команды/готовности к приему команды |
SPU_ERROR_FLAG | 1 | Флаг ошибки выполнения команды |
SPU_ERROR_Q_FLAG | 2 | Флаг ошибки выполнения команды в очереди статуса результатов |
DDR_Q_OVF_FLAG | 3 | Флаг переполнения очереди к DDR LSM памяти |
DDR_TEST_SUCC_FLAG | 4 | Результат верификации контролера памяти DDR4 (не использован) = 0 |
NU | 5-8 | Не использованы |
SPU_ALL_DONE | 9 | Очередь команд пуста и последняя команда исполнена |
AXI2LNH_Q_EMP_FLAG | 16 | Очередь запросов на запись пуста |
AXI2LNH_Q_FULL_FLAG | 17 | Очередь запросов на запись переполнена |
AXI2LNH_Q_AEMP_FLAG | 18 | Очередь запросов на запись наполовину пуста (содержит <256 значений) |
AXI2LNH_Q_AFULL_FLAG | 19 | Очередь запросов на запись наполовину заполнена (содержит >256 значений) |
LNH2AXI_Q_EMP_FLAG | 20 | Очередь запросов на чтение пуста |
LNH2AXI_Q_FULL_FLAG | 21 | Очередь запросов на чтение переполнена |
LNH2AXI_Q_AEMP_FLAG | 22 | Очередь запросов на чтение наполовину пуста (содержит <256 значений) |
LNH2AXI_Q_AFULL_FLAG | 23 | Очередь запросов на чтение наполовину заполнена (содержит >256 значений) |
MBOX_VFLAG | 32-40 | Биты готовности операндов в регистрах mailbox[0..7], 1 - готовность |
LNH_DATA_PARTITION | 48-50 | Номер партиции DDR данных нижнего уровня B+дерева (0..7) |
LNH_INDEX_PARTITION | 51-53 | Номер партиции DDR индексной части B+дерева (0..7) |
LNH_INDEX_REGION | 54-55 | Номер региона DDR индексной части B+дерева в LNH_INDEX_PARTITION (0..3) |
2.6.2. Вызовы и передача операндов команд дискретной математики
Операции чтения и записи регистров lnh64 в DISC в библиотеке SW Kernel Lib выполняются с помощью макросов как при помощи непосредственных параметров (по значению), так и с помощью адреса параметра (по ссылке).
Таблица 8 - Макросы доступа к регистрам lnh64
Макрос | Назначение |
---|---|
lnh_wr_reg64_byref(adr, value) | Запись регистра 64 бит (Адрес, Данные) по ссылке |
lnh_wr_reg64_byval(adr, value) | Запись регистра 64 бит (Адрес, Данные) по значению |
lnh_rd_reg64_byref(adr, value) | Чтение регистра 64 бит (Адрес, Данные) по ссылке |
lnh_rd_reg64_byval(adr) | Чтение регистра 64 бит (Адрес) => Данные по значению |
lnh_wr_reg32_l2l_byref(adr, value) | Запись регистра 32 бит (Адрес, Данные) по ссылке |
lnh_wr_reg32_byval(adr, value) | Запись регистра 32 бит (Адрес, Данные) по значению |
lnh_rd_reg32_byref(adr, value) | Чтение регистра 32 бит (Адрес, Данные) по ссылке |
lnh_rd_reg32_byval(adr) | Чтение регистра 32 бит (Адрес) => Данные по значению |
Для ускорения запуска команд, когда по сравнению с предыдущей командой меняется только часть операндов, может применяться функция __fast_recall()
, передающая только измененные относительно предыдущей команды операнды и код операции.
Типичным примером использования макросов для запуска команды на выполнение и ожидание результатов является команда Вставки (INS) ключа и значения. Для этого, необходимо проверить возможность записи в очередь запросов на запись. При этом, обращение к очереди требуется выполнять только в том случае, если исчерпан лимит на количество последовательных операций записи (256 записей). Только в случае превышения лимита проверяется флаг AXI2LNH_Q_AFULL_FLAG. При освобождении половины от имеющего ся места в очереди посылка транзакций возобновлется.
Микроархитектура lnh64 допускает обращение к одной из семи независимых структур (1..7). Структура с инексом 0 не используется для хранения и зарезервирована.
Далее происходит запись ключей и значений в регистры KEY2LNH и VAL2LNH, а также посылка кода операции в регистр CMD2LNH. При этом указывается параметр str, определяющий номер структуры, в которую должна произойти вставка нового ключа. После записи старшей части регистра CMD2LNH (CMD2LNH_HIGH) происходит запуск команды на исполнение.
Далее выполняется ожидание готовности (проверяется бит SPU_READY_FLAG регистра статуса), после чего выполняется чтение регистра состояния и анализ результата. Статус выполнения команды, а для других команд ключ и значение результата записываются в структуру lnh_core.result.
//====================================================
// Добавление (Структура, Ключ, Значение)
//====================================================
bool lnh_ins_sync(uint64_t str, uint64_t key, uint64_t value)
{
//проверка готовности устройства
lnh_axi2lnh_queue_credits_check;
//запись исходных данных
lnh_wr_reg64_byref(KEY2LNH, &key);
lnh_wr_reg64_byref(VAL2LNH, &value);
lnh_wr_reg64_byval(CMD2LNH, (INS<<lnh_cmd)|str);
//ожидание готовности очереди команд
lnh_sync();
//чтение результата
lnh_rd_reg64_byref(LNH_STATE,&lnh_core.result.status);
//results
if ((lnh_core.result.status & (1<<SPU_ERROR_FLAG)) != 0) {
return false;
} else {
return true;
}
}
Функции для вызова команд DISC организованы в виде шести групп:
Таблица 9 - Функции библиотеки lnh64 L0 для вызова команд DISC lnh64
Группа функций / функция | Пояснение |
---|---|
Функции для работы с Leonhard API | В группу входят функции чтения и записи регистров Lnh64 |
lnh_hw_reset() | Аппаратный сброс GPC, удаление всех структур Lnh64, сброс riscv микропроцессора, сброс очередей mq |
lnh_sw_reset() | Программное удаление всех структур Lnh64 |
lnh_init() | Инициализация lnh64, установка указателей на буферы Глобальной памяти |
lnh_rd_reg64(adr) | Чтение 64 разрядного регистра lnh64 микропроцессора |
lnh_rd_reg32(adr) | Чтение 32 разрядной части регистра lnh64 микропроцессора |
lnh_fast_recall(key) lnh_fast_recall(key,value) |
Быстрый перезапуск предыдущей команды. Ускорение достигается благодаря передаче только части операндов (только ключ, только младшая часть ключа и т.д.) |
Сервисные функции Leonhard API | В группу входят функции преобразования типов и ожидания готовности lnh64 |
float2uint(value) | Представление значения типа float в целочисленном виде (инферсия знака мантиссы) для сохранения в ввиде поля ключа. Команда позволяет исопльзовать целочиссленное безщнаковое сравнение для чисел float |
uint2float(value) | Функция, обратная к float2uint, позволяет преобразовать часть ключа, сохраненного в виде unsigned int в тип float |
double2ull(value) | Представление значения типа double в целочисленном виде (инферсия знака мантиссы) для сохранения в ввиде поля ключа. Команда позволяет исопльзовать целочиссленное безщнаковое сравнение для чисел double |
ull2double(value) | Функция, обратная к double2ull, позволяет преобразовать часть ключа, сохраненного в виде unsigned int в тип double |
lnh_sync() | Ожидание готовности результатов выполнения команды в Lnh64. Функция ожидает завершения всех команд очереди команд Lnh64 |
lnh_syncm(mbr) | Ожидание готовности регистра Mailbox. При запуске команд DISC с записью результатов в регистры mbr сбрасывется флаг достоверности указанных регистров. При помещении результатов в mbrx регистр, флаг устанавливается и разрешается его чтение. |
Синхронные функции Leonhard API | Функции для вызова команд DISC с ожиданием их завершения и чтением результатов в структуру lnh_core.results |
lnh_ins_sync(str,key,value) | Вставка ключа key и значения value в структуру str. |
lnh_del_sync(str,key) | Удаление записи с ключом key из структуры str. |
lnh_get_num(str) | Получить количество записей в структуре str. |
lnh_del_str_sync(str) | Удаление структуры str. |
lnh_sq_sync(str) | Сжатие структуры в памяти lnh64 (сокращение занимаемого объема памяти). |
lnh_or_sync(A,B,R) | Объединение множеств ключей структуры A и B. Помещение результирующей структуры в R. |
lnh_and_sync(A,B,R) | Пересечение множеств ключей структуры A и B. Помещение результирующей структуры в R. |
lnh_not_sync(A,B,R) | Дополнение множеств ключей структуры A ключами структуры B. Помещение результирующей структуры в R. |
lnh_lseq_sync(key,A,R) | Срез структуры A по условию “меньше или равно” ключа key. Помещение результирующей структуры в R (все ключи, не соответствующие условию lseq в структуре R отсуствуют). |
lnh_ls_sync(key,A,R) | Срез структуры A по условию “меньше” ключа key. Помещение результирующей структуры в R. (все ключи, не соответствующие условию ls в структуре R отсуствуют). |
lnh_greq_sync(key,A,R) | Срез структуры A по условию “больше или равно” ключа key. Помещение результирующей структуры в R.(все ключи, не соответствующие условию greq в структуре R отсуствуют). |
lnh_gr_sync(key,A,R) | Срез структуры A по условию “больше” ключа key. Помещение результирующей структуры в R. (все ключи, не соответствующие условию gr в структуре R отсуствуют). |
lnh_grls_sync(key_ls,key_gr,A,R) | Срез структуры A по условию “больше” ключа key_ls и “меньше” ключа key_gr. Помещение результирующей структуры в R. (все ключи, не соответствующие условию gr и ls в структуре R отсуствуют). |
lnh_search(str,key) | Поиск ключа key в структуре str и выдача найденного ключа и значения value. |
lnh_next(str,key) | Поиск следующего ключа, следующего за ключом key в структуре str. Поиск выполняется в целочисленном беззнаковом порядке на ключах. |
lnh_prev(str,key) | Поиск предыдущего ключа, следующего перед ключом key в структуре str. Поиск выполняется в целочисленном беззнаковом порядке на ключах. |
lnh_ngr(str,key) | Поиск следующего ключа, большего значению key в структуре str (ключ key может отсутствовать в структуре). Поиск выполняется в целочисленном беззнаковом порядке на ключах. |
lnh_nsm(str,key) | Поиск следующего ключа, меньшего значению key в структуре str (ключ key может отсутствовать в структуре). Поиск выполняется в целочисленном беззнаковом порядке на ключах. |
lnh_ngr_signed(str,key) | Поиск следующего ключа, большего значению key в структуре str (ключ key может отсутствовать в структуре). Поиск выполняется в целочисленном знаковом порядке на ключах. |
lnh_nsm_signed(str,key) | Поиск следующего ключа, меньшего значению key в структуре str (ключ key может отсутствовать в структуре). Поиск выполняется в целочисленном знаковом порядке на ключах. |
lnh_get_first(str) | Поиск наименьшего ключа структуры str. Поиск выполняется в целочисленном беззнаковом порядке на ключах. |
lnh_get_last(str) | Поиск наибольшего ключа структуры str. Поиск выполняется в целочисленном беззнаковом порядке на ключах. |
lnh_get_first_signed(str) | Поиск наименьшего ключа структуры str. Поиск выполняется в целочисленном знаковом порядке на ключах. |
lnh_get_last_signed(str) | Поиск наибольшего ключа структуры str. Поиск выполняется в целочисленном знаковом порядке на ключах. |
Синхронные функции Leonhard API с записью в очередь результатов | Функции для вызова команд DISC с ожиданием их завершения и автоматической записью результатов в очередь (регистры LNH2KEYQ, LNH2VALQ, LNH_STATEQ) |
lnh_ins_syncq(str,key,value) | Вставка ключа key и значения value в структуру str. Запись результата в очередь. |
lnh_del_syncq(str,key) | Удаление записи с ключом key из структуры str. Запись результата в очередь. |
lnh_get_numq(str) | Получить количество записей в структуре str.Запись результата в очередь. |
lnh_del_str_syncq(str) | Удаление структуры str. Запись результата в очередь. |
lnh_sq_syncq(str) | Сжатие структуры в памяти lnh64 (сокращение занимаемого объема памяти). Запись результата в очередь. |
lnh_or_syncq(A,B,R) | Объединение множеств ключей структуры A и B. Помещение результирующей структуры в R. Запись результата в очередь. |
lnh_and_syncq(A,B,R) | Пересечение множеств ключей структуры A и B. Помещение результирующей структуры в R. Запись результата в очередь. |
lnh_not_syncq(A,B,R) | Дополнение множеств ключей структуры A ключами структуры B. Помещение результирующей структуры в R. Запись результата в очередь. |
lnh_lseq_syncq(key,A,R) | Срез структуры A по условию “меньше или равно” ключа key. Помещение результирующей структуры в R (все ключи, не соответствующие условию lseq в структуре R отсуствуют). Запись результата в очередь. |
lnh_ls_syncq(key,A,R) | Срез структуры A по условию “меньше” ключа key. Помещение результирующей структуры в R. (все ключи, не соответствующие условию ls в структуре R отсуствуют). Запись результата в очередь. |
lnh_greq_syncq(key,A,R) | Срез структуры A по условию “больше или равно” ключа key. Помещение результирующей структуры в R.(все ключи, не соответствующие условию greq в структуре R отсуствуют). Запись результата в очередь. |
lnh_gr_syncq(key,A,R) | Срез структуры A по условию “больше” ключа key. Помещение результирующей структуры в R. (все ключи, не соответствующие условию gr в структуре R отсуствуют). Запись результата в очередь. |
lnh_grls_syncq(key_ls,key_gr,A,R) | Срез структуры A по условию “больше” ключа key_ls и “меньше” ключа key_gr. Помещение результирующей структуры в R. (все ключи, не соответствующие условию gr и ls в структуре R отсуствуют). Запись результата в очередь. |
lnh_searchq(str,key) | Поиск ключа key в структуре str и выдача найденного ключа и значения value. Запись результата в очередь. |
lnh_nextq(str,key) | Поиск следующего ключа, следующего за ключом key в структуре str. Поиск выполняется в целочисленном беззнаковом порядке на ключах. Запись результата в очередь. |
lnh_prevq(str,key) | Поиск предыдущего ключа, следующего перед ключом key в структуре str. Поиск выполняется в целочисленном беззнаковом порядке на ключах. Запись результата в очередь. |
lnh_ngrq(str,key) | Поиск следующего ключа, большего значению key в структуре str (ключ key может отсутствовать в структуре). Поиск выполняется в целочисленном беззнаковом порядке на ключах. Запись результата в очередь. |
lnh_nsmq(str,key) | Поиск следующего ключа, меньшего значению key в структуре str (ключ key может отсутствовать в структуре). Поиск выполняется в целочисленном беззнаковом порядке на ключах. Запись результата в очередь. |
lnh_get_firstq(str) | Поиск наименьшего ключа структуры str. Поиск выполняется в целочисленном беззнаковом порядке на ключах. Запись результата в очередь. |
lnh_get_lastq(str) | Поиск наибольшего ключа структуры str. Поиск выполняется в целочисленном беззнаковом порядке на ключах. Запись результата в очередь. |
lnh_get_q() | Чтение результата из очереди |
Асинхронные функции Leonhard API с записью в асинхронный Mailbox | Вызов команд DISC без ожидания их завершения и с записью результатов в регистры mbr. На стороне sw_kernel возможно чтение с ожиданием готовности mbr регистров функцией lnh_syncm(int mbr) |
lnh_ins_syncm(st_mreg,str,key,value) | Вставка ключа key и значения value в структуру str. Запись результата в регистр st_mreg. |
lnh_del_syncm(st_mreg,str,key) | Удаление записи с ключом key из структуры str. Запись результата в регистр st_mreg. |
lnh_get_numm(str) | Получить количество записей в структуре str. Запись результата в регистр mrf. |
lnh_del_str_syncm(st_mreg,str) | Удаление структуры str. Запись результата в регистр st_mreg. |
lnh_sq_syncm(st_mreg,str) | Сжатие структуры в памяти lnh64 (сокращение занимаемого объема памяти). Запись результата в регистр st_mreg. |
lnh_or_syncm(st_mreg,A,B,R) | Объединение множеств ключей структуры A и B. Помещение результирующей структуры в R. Запись результата в регистр st_mreg. |
lnh_and_syncm(st_mreg,A,B,R) | Пересечение множеств ключей структуры A и B. Помещение результирующей структуры в R. Запись результата в регистр st_mreg. |
lnh_not_syncm(st_mreg,A,B,R) | Дополнение множеств ключей структуры A ключами структуры B. Помещение результирующей структуры в R. Запись результата в регистр st_mreg. |
lnh_lseq_syncm(st_mreg,key,A,R) | Срез структуры A по условию “меньше или равно” ключа key. Помещение результирующей структуры в R (все ключи, не соответствующие условию lseq в структуре R отсуствуют). Запись результата в регистр st_mreg. |
lnh_ls_syncm(st_mreg,key,A,R) | Срез структуры A по условию “меньше” ключа key. Помещение результирующей структуры в R. (все ключи, не соответствующие условию ls в структуре R отсуствуют). Запись результата в регистр st_mreg. |
lnh_greq_syncm(st_mreg,key,A,R) | Срез структуры A по условию “больше или равно” ключа key. Помещение результирующей структуры в R.(все ключи, не соответствующие условию greq в структуре R отсуствуют). Запись результата в регистр st_mreg. |
lnh_gr_syncm(st_mreg,key,A,R) | Срез структуры A по условию “больше” ключа key. Помещение результирующей структуры в R. (все ключи, не соответствующие условию gr в структуре R отсуствуют). Запись результата в регистр st_mreg. |
lnh_grls_syncm(st_mreg,key_ls,key_gr,A,R) | Срез структуры A по условию “меньше” ключfа key_ls и “больше” ключа key_gr. Помещение результирующей структуры в R. (все ключи, не соответствующие условию gr и ls в структуре R отсуствуют). Запись результата в регистр st_mreg. |
lnh_searchm(key_mreg,val_mreg,st_mreg,str,key) | Поиск ключа key в структуре str и выдача найденного ключа и значения value. Запись результата в регистр st_mreg. |
lnh_nextm(key_mreg,val_mreg,st_mregstr,key) | Поиск следующего ключа, следующего за ключом key в структуре str. Поиск выполняется в целочисленном беззнаковом порядке на ключах. Запись результата в регистр st_mreg. |
lnh_prevm(key_mreg,val_mreg,st_mregstr,key) | Поиск предыдущего ключа, следующего перед ключом key в структуре str. Поиск выполняется в целочисленном беззнаковом порядке на ключах. Запись результата в регистр st_mreg. |
lnh_ngrm(key_mreg,val_mreg,st_mregstr,key) | Поиск следующего ключа, большего значению key в структуре str (ключ key может отсутствовать в структуре). Поиск выполняется в целочисленном беззнаковом порядке на ключах. Запись результата в регистр st_mreg. |
lnh_nsmm(key_mreg,val_mreg,st_mregstr,key) | Поиск следующего ключа, меньшего значению key в структуре str (ключ key может отсутствовать в структуре). Поиск выполняется в целочисленном беззнаковом порядке на ключах. Запись результата в регистр st_mreg. |
lnh_get_firstm(key_mreg,val_mreg,st_mregstr) | Поиск следующего ключа, большего значению key в структуре str (ключ key может отсутствовать в структуре). Поиск выполняется в целочисленном знаковом порядке на ключах. Запись результата в регистр st_mreg. |
lnh_get_lastm(key_mreg,val_mreg,st_mregstr) | Поиск следующего ключа, меньшего значению key в структуре str (ключ key может отсутствовать в структуре). Поиск выполняется в целочисленном знаковом порядке на ключах. Запись результата в регистр st_mreg. |
lnh_get_m(mreg) | Чтение регистра Mailbox Запись результата в регистр st_mreg. |
Асинхронные функции Leonhard API без записи результатов | Функции для вызова команд DISC без ожидания их завершения. Записью результатов выполнения команд не производится |
lnh_ins_async(str,key,value) | Вставка ключа key и значения value в структуру str. |
lnh_del_async(str,key) | Удаление записи с ключом key из структуры str. |
lnh_del_str_async(str) | Удаление структуры str. |
lnh_sq_async(str) | Сжатие структуры в памяти lnh64 (сокращение занимаемого объема памяти). |
lnh_or_async(A,B,R) | Объединение множеств ключей структуры A и B. Помещение результирующей структуры в R. |
lnh_and_async(A,B,R) | Пересечение множеств ключей структуры A и B. Помещение результирующей структуры в R. |
lnh_not_async(A,B,R) | Дополнение множеств ключей структуры A ключами структуры B. Помещение результирующей структуры в R. |
lnh_lseq_async(key,A,R) | Срез структуры A по условию “меньше или равно” ключа key. Помещение результирующей структуры в R (все ключи, не соответствующие условию lseq в структуре R отсуствуют). |
lnh_ls_async(key,A,R) | Срез структуры A по условию “меньше” ключа key. Помещение результирующей структуры в R. (все ключи, не соответствующие условию ls в структуре R отсуствуют). |
lnh_greq_async(key,A,R) | Срез структуры A по условию “больше или равно” ключа key. Помещение результирующей структуры в R.(все ключи, не соответствующие условию greq в структуре R отсуствуют). |
lnh_gr_async(key,A,R) | Срез структуры A по условию “больше” ключа key. Помещение результирующей структуры в R. (все ключи, не соответствующие условию gr в структуре R отсуствуют). |
lnh_grls_async(key_ls,key_gr,A,R) | Срез структуры A по условию “больше” ключа key_ls и “меньше” ключа key_gr. Помещение результирующей структуры в R. (все ключи, не соответствующие условию gr и ls в структуре R отсуствуют). |
Полный перечень функций вызовов команд DISC вы можете посмотреть в файле lnh64.h.
//==================================
// Функции для работы с Leonhard API
//==================================
void lnh_hw_reset();
void lnh_sw_reset();
void lnh_init();
uint64_t lnh_rd_reg64(int adr);
uint32_t lnh_rd_reg32(int adr);
void lnh_fast_recall(uint32_t key);
void lnh_fast_recall(uint32_t key, uint32_t value);
void lnh_fast_recall(uint64_t key);
void lnh_fast_recall(uint64_t key, uint64_t value);
//================================
// Сервисные функции Leonhard API
//================================
uint32_t float2uint(float value);
float uint2float(uint32_t value);
uint64_t double2ull(double value);
double ull2double(uint64_t value);
void lnh_sync();
void lnh_syncm(int mbr);
//================================
// Синхронные функции Leonhard API
//================================
bool lnh_ins_sync(uint64_t str, uint64_t key, uint64_t value);
bool lnh_del_sync(uint64_t str, uint64_t key);
uint32_t lnh_get_num(uint64_t str);
bool lnh_del_str_sync(uint64_t str);
bool lnh_sq_sync(uint64_t str);
bool lnh_or_sync(uint64_t A, uint64_t B, uint64_t R);
bool lnh_and_sync(uint64_t A, uint64_t B, uint64_t R);
bool lnh_not_sync(uint64_t A, uint64_t B, uint64_t R);
bool lnh_lseq_sync(uint64_t key, uint64_t A, uint64_t R);
bool lnh_ls_sync(uint64_t key, uint64_t A, uint64_t R);
bool lnh_greq_sync(uint64_t key, uint64_t A, uint64_t R);
bool lnh_gr_sync(uint64_t key, uint64_t A, uint64_t R);
bool lnh_grls_sync(uint64_t key_ls, uint64_t key_gr, uint64_t A, uint64_t R);
bool lnh_search(uint64_t str, uint64_t key);
bool lnh_next(uint64_t str, uint64_t key);
bool lnh_prev(uint64_t str, uint64_t key);
bool lnh_nsm(uint64_t str, uint64_t key);
bool lnh_ngr(uint64_t str, uint64_t key);
bool lnh_nsm_signed(uint64_t str, long long int key);
bool lnh_ngr_signed(uint64_t str, long long int key);
bool lnh_get_first(uint64_t str);
bool lnh_get_last(uint64_t str);
bool lnh_get_first_signed(uint64_t str);
bool lnh_get_last_signed(uint64_t str);
//================================================================
// Синхронные функции Leonhard API с записью в очередь результатов
//================================================================
bool lnh_ins_syncq(uint64_t str, uint64_t key, uint64_t value);
bool lnh_del_syncq(uint64_t str, uint64_t key);
uint32_t lnh_get_numq(uint64_t str);
bool lnh_del_str_syncq(uint64_t str);
bool lnh_sq_syncq(uint64_t str);
bool lnh_or_syncq(uint64_t A, uint64_t B, uint64_t R);
bool lnh_and_syncq(uint64_t A, uint64_t B, uint64_t R);
bool lnh_not_syncq(uint64_t A, uint64_t B, uint64_t R);
bool lnh_lseq_syncq(uint64_t key, uint64_t A, uint64_t R);
bool lnh_ls_syncq(uint64_t key, uint64_t A, uint64_t R);
bool lnh_greq_syncq(uint64_t key, uint64_t A, uint64_t R);
bool lnh_gr_syncq(uint64_t key, uint64_t A, uint64_t R);
bool lnh_grls_syncq(uint64_t key_ls, uint64_t key_gr, uint64_t A, uint64_t R);
bool lnh_searchq(uint64_t str, uint64_t key);
bool lnh_nextq(uint64_t str, uint64_t key);
bool lnh_prevq(uint64_t str, uint64_t key);
bool lnh_nsmq(uint64_t str, uint64_t key);
bool lnh_ngrq(uint64_t str, uint64_t key);
bool lnh_get_firstq(uint64_t str);
bool lnh_get_lastq(uint64_t str);
bool lnh_get_q();
//=================================================================
// Асинхронные функции Leonhard API с записью в асинхронный Mailbox
//=================================================================
bool lnh_ins_syncm(int st_mreg, uint64_t str, uint64_t key, uint64_t value);
bool lnh_del_syncm(int st_mreg, uint64_t str, uint64_t key);
uint32_t lnh_get_numm(uint64_t str);
bool lnh_del_str_syncm(int st_mreg, uint64_t str);
bool lnh_sq_syncm(int st_mreg, uint64_t str);
bool lnh_or_syncm(int st_mreg, uint64_t A, uint64_t B, uint64_t R);
bool lnh_and_syncm(int st_mreg, uint64_t A, uint64_t B, uint64_t R);
bool lnh_not_syncm(int st_mreg, uint64_t A, uint64_t B, uint64_t R);
bool lnh_lseq_syncm(int st_mreg, uint64_t key, uint64_t A, uint64_t R);
bool lnh_ls_syncm(int st_mreg, uint64_t key, uint64_t A, uint64_t R);
bool lnh_greq_syncm(int st_mreg, uint64_t key, uint64_t A, uint64_t R);
bool lnh_gr_syncm(int st_mreg, uint64_t key, uint64_t A, uint64_t R);
bool lnh_grls_syncm(int st_mreg, uint64_t key_ls, uint64_t key_gr, uint64_t A, uint64_t R);
bool lnh_searchm(int key_mreg, int val_mreg, int st_mreg, uint64_t str, uint64_t key);
bool lnh_nextm(int key_mreg, int val_mreg, int st_mreg, uint64_t str, uint64_t key);
bool lnh_prevm(int key_mreg, int val_mreg, int st_mreg, uint64_t str, uint64_t key);
bool lnh_nsmm(int key_mreg, int val_mreg, int st_mreg, uint64_t str, uint64_t key);
bool lnh_ngrm(int key_mreg, int val_mreg, int st_mreg, uint64_t str, uint64_t key);
bool lnh_get_firstm(int key_mreg, int val_mreg, int st_mreg, uint64_t str);
bool lnh_get_lastm(int key_mreg, int val_mreg, int st_mreg, uint64_t str);
uint64_t lnh_get_m(int mreg);
//========================================================
// Асинхронные функции Leonhard API без записи результатов
//========================================================
bool lnh_ins_async(uint64_t str, uint64_t key, uint64_t value);
bool lnh_del_async(uint64_t str, uint64_t key);
bool lnh_del_str_async(uint64_t str);
bool lnh_sq_async(uint64_t str);
bool lnh_or_async(uint64_t A, uint64_t B, uint64_t R);
bool lnh_and_async(uint64_t A, uint64_t B, uint64_t R);
bool lnh_not_async(uint64_t A, uint64_t B, uint64_t R);
bool lnh_lseq_async(uint64_t key, uint64_t A, uint64_t R);
bool lnh_ls_async(uint64_t key, uint64_t A, uint64_t R);
bool lnh_greq_async(uint64_t key, uint64_t A, uint64_t R);
bool lnh_gr_async(uint64_t key, uint64_t A, uint64_t R);
bool lnh_grls_async(uint64_t key_ls, uint64_t key_gr, uint64_t A, uint64_t R);
2.6.3. Представление структур данных в виде ключей и значений
SPE lnh64 использует беззнаковое сравнение 64 битных ключей для формирования упорядоченной структуры B+дерева. Это позволяет выполнять большинство операций набора команд DISC за O(log8n) операций доступа к памяти.
Таким образом, чтобы реализовать хранение информации в SPE неободимо представить информацию в виде беззнаковых целых чисел.
Частым и востребованным случаем является формирование композитных ключей и значений, состоящих из нескольких полей. При этом старшие разряды определяют порядок сортировки.
В библиотеке lnh64 l0 реализованы шаблоны для работы со структурами составных ключей. Обязательным требованием к ним является общий размер, который должен быть равен 64 разрядам как для ключа, так и для значения.
Ниже представлен пример объявления структур композитного ключа и композитного значения:
//Структура данных
#define A 1 //Структура A
//Структура A - ключ
/*
* key[63..32] - Поле 0 - Идентификатор (id)
* key[31..0] - Поле 1 - Порядковый номер (index)
*/
STRUCT(A_key)
{
uint32_t index:32; //Поле 0:
uint32_t id :32; //Поле 1:
};
//Структура A - значение
/*
* value[63..32] - Поле 0 - Атрибут 0
* value[31..24] - Поле 1 - Атрибут 1
* value[23..8] - Поле 2 - Атрибут 2
* value[7..0] - Поле 3 - Атрибут 3
*/
STRUCT(A_value)
{
// Поля объявляются в обратной последовательности (старший байт расположен по старшему адресу)
uint8_t atr3 :8; //Поле 3: Атрибут 3
uint16_t atr2 :16; //Поле 2: Атрибут 2
uint8_t atr1 :8; //Поле 1: Атрибут 1
uint32_t atr0 :32; //Поле 0: Атрибут 0
};
2.7. Программная модель микропроцессора Леонард Эйлер
Программная модель микропроцессора Леонард Эйлер представляет собой совокупность программно-доступных ресурсов в следующих адресных пространствах:
- Адресное пространство хост-подсистемы: 64 битное пространство, в которое отображены ресурсы подсистемы обработки графов (за исключением ресурсов SPE), ресурсы хост-подсистемы (ОЗУ, дисковые устройства), хост-подсистемы, подсистема хранения графов, подсистемы сетевого взаимодействия узлов обработки графов.
- Адресное пространство ядер обработки графов: 64 битное пространство доступа к ресурсам SPE.
Таким образом, распределение программно-доступных ресурсов по двум адресным пространствам соответствует их назначению: программа, функционирущая в хост-подсистеме (включая программный драйвер) выполняет управление системными процессами в узле; программы в гетерогенных ядрах обработки графов выполняет управление процессорным элементом обработки структур SPE.
2.7.1. Ресурсы микропроцессора Леонард Эйлер в адресном пространстве хост-подсистемы
Микропроцессор Леонард Эйлер реализован в виде карты расширения, подключаемой к узлу через шин PCIe v3 x16. Все программно-доступные ресурсы отобразаются в адресное пространство через регистры базового адреса. В текущей версии используются регистры BAR0 (64 битный доступ), BAR2 (64 битный доступ) и BAR4 (32 битный доступ) В таблицах 10-14 перечислены программно доступные ресурсы.
Таблица 10 - Программно доступные регистры Леонард Эйлер для хост-подсистемы (SC = Self Clear, COR = Clear on Read, TOW = Toggle on Write, COH = Clear on Handshake)
Название | Смещение | Режим | Бит | Описание |
---|---|---|---|---|
1. BAR0 | В пространство отображены регистры гетерогенных ядер GPC | |||
Регион GPC# | 0x#00..3F | Регион регистров GPC номер # (всего 24 региона) (64 байта) | ||
CONTROL | 0x#00 | Регистр управления GPC | ||
STATUS | 0x#08 | Регистр статуса GPC# | ||
RV_CONFIG | 0x#10 | W | Регистр номера обработчика. При записи вырабатывается сигнал ap_start, читаемый со сторны GPC# | |
HANDLER_PAYLOAD | 0x#18 | R | Регистр данных пользовательского прерывания. Устанавливается программно программным кодом GPC# | |
C2H_PIDX_ADDR | 0x#20 | W | Регистр адреса поля Card2Host PIDX (продьюсер индекс) для уведомлении хоста об индексе последнего записанного слова | |
C2H_PIDX | 0x#28 | W | Регистр Host2Card CIDX (консьюмер индекс) для уведомлении карты об индексе последнего записанного слова | |
LNH_NFO_ADR | 0x#30 | W | Регистр адреса слова состояния lnh64 | |
LNH_NFO_DATA | 0x#30 | R | Регистр слова состояния lnh64 | |
SYS_CONTROL | 0x600 | |||
SYS_STATUS | 0x608 | |||
2.BAR2 | В пространство отображены блоки программно-доступной памяти | |||
GPC#_MSG_REGION | 0x#000.. FFF | R/W | Память для передачи блоков данных между Хост-подсистемой и GPC# (4КБайт) | |
BIN_MEMORY | 0x20000.. 30000 | R/W | Память конфигурирования GPC#. Память используется для передачи в GPC# программных ядер sw_kernel | |
3.BAR4 | В пространство отображена таблица векторов MSI-X | |||
MSIX_TABLE | 0x000.. FFF | R/W | Таблица векторов MSI-X: 49 векторов |
Таблица 11 - Назначение бит регистра управления гетерогенного ядра (SC = Self Clear, COR = Clear on Read)
Название | Режим | Бит | Описание |
---|---|---|---|
ap_start | R/W/COR | 0 | Бит запуска хэндлера. Устанавливается программно или автоматически по записи в регистр GPC_RV_CONFIG |
auto_restart | R/W | 1 | Бит автоматического повторного перезапуска хэндлера. При auto_restart=1 после завершения работы хэндлера происходит его повторный запуск |
mq_enable | R/W | 2 | Бит разрешения работы очередей MQ для GPC# |
done_ie | R/W | 3 | Бит разрешения прерывания по переходу в состояние IDLE |
user _ie | R/W | 4 | Бит разрешения пользовательского прерывания от GPC# |
gpc_reset | W/SC | 5 | Бит сброса GPC#. При этом также происходит сброс ядра lnh64 |
mq_h2c_reset | W/SC | 6 | Бит сброса очереди Host2Card ядра GPC# |
mq_c2h_reset | W/SC | 7 | Бит сброса очереди Card2Host ядра GPC# |
data_partition | R/W | 10..8 | Номер партиции данных для ядра lnh64 |
index_partition | R/W | 13..11 | Номер индексной партиции для ядра lnh64 |
index_region | R/W | 15..14 | Номер индексного региона для ядра lnh64 |
Таблица 12 - Назначение бит регистра статуса гетерогенного ядра (COR = Clear on Read)
Название | Режим | Бит | Описание |
---|---|---|---|
ap_idle | R | 0 | Бит состояния GPC#. 0 - BUSY/Занят; 1 - IDLE/Свободен |
ap_done | R/COR | 1 | Бит выдачи сигнала DONE, устанавливается при переходе из состояние BUSY в IDLE |
Таблица 13 - Назначение бит регистра управления микропроцессора (SC = Self Clear)
Название | Режим | Бит | Описание |
---|---|---|---|
Global reset | W/SC | 0 | Бит сброса микропроцессора (подлежат сбросу все GPC, включая ядра risv64im и lnh64) |
H2CQ_reset | W/SC | 1 | Бит сброса очереди Host2Card |
C2HQ_reset | W/SC | 2 | Бит сброса очереди Card2Host |
MSIX_ie | W | 3 | Бит глобального разрешения прерываний MSI-X |
Таблица 14 - Назначение бит регистра статуса микропроцессора
Название | Режим | Бит | Описание |
---|---|---|---|
System Ready | R | 0 | Микропроцессор инициализирован и готов к работа |
H2C Queue Empty | R | 1 | Очередь Host2Card пуста |
H2C Queue AEmpty | R | 2 | Очередь Host2Card почти пуста |
H2C Queue Full | R | 3 | Очередь Host2Card заполнена |
H2C Queue AFull | R | 4 | Очередь Host2Card почти заполнена |
C2H Queue Empty | R | 5 | Очередь Card2Host пуста |
C2H Queue AEmpty | R | 6 | Очередь Card2Host почти пуста |
C2H Queue Full | R | 7 | Очередь Card2Host заполнена |
C2H Queue AFull | R | 8 | Очередь Card2Host почти заполнена |
System revision | R | 63..32 | Версия микропроцессора Леонард Эйлер |
2.7.2. Ресурсы микропроцессора Леонард Эйлер в адресном пространстве CPE
Микропроцессорное ядро CPE riscv64im обладает 64х разрядным адресным пространством, в котором отображены следующие ресурсы (смотри таблицу 15):
Таблица 15 - Программно доступные регистры Леонард Эйлер для CPE (SC = Self Clear, COR = Clear on Read, TOW = Toggle on Write, COH = Clear on Handshake)
Название | Смещение | Режим | Бит | Описание |
---|---|---|---|---|
BOOTLOADER | 0x00000000 | R | ПЗУ, в котором записан программный загрузчик (256 байт) | |
RAM | 0x100000 | R/W | Оперативная память 64 КБайт | |
AXI4 | 0x200000 | R/W | Пространство локальной шины AXI4 | |
GPC_START | AXI4+0x00 | Регистр управления GPC | ||
ap_start | R/W/COR | 0 | Бит запуска хэнлера. Устанавливается программно или автоматически по записи в регистр GPC_RV_CONFIG | |
GPC_STATUS | AXI4+0x08 | Регистр статуса GPC | ||
ap_idle | W | 0 | Бит состояния GPC. 0 - BUSY/Занят; 1 - IDLE/Свободен | |
RV_CONFIG | AXI4+0x10 | R | Регистр номера обработчика. Данные валидны только при GPC_START.ap_start=1 | |
HANDLER_PAYLOAD | AXI4+0x18 | W | Регистр данных пользовательского прерывания. Прерывание вырабатывается автоматически при каждой записи регистра. | |
MSG_REGION | AXI4+0x1000.. 1FFF | R/W | Память для передачи блоков данных между Хост-подсистемой и GPC# (4КБайт) | |
BIN_MEMORY | AXI4+0x10000.. 20000 | R/W | Память конфигурирования GPC. Память используется для передачи в GPC программных ядер sw_kernel | |
LNH64 | 0x300000 | Пространство локальной шины AXL для подключения ядра lnh64 | ||
AXIS | 0x400000 | R/W | Пространство локальной шины AXI STREAM для связи с очередью C2H и DMA с хост-подсистемой | |
C2H | AXIS+0x00 | W | Регистр данных очереди Card2Host. Запись данных в очередь C2H происходит при каждой транзакции записи. Немедленная отправка не гарантируется. | |
C2H_FLUSH | AXIS+0x08 | W | Регистр запроса на ускоренную отправку данных в хост систему. |
3. Практакум №1. Разработка и отладка программ в вычислительном комплексе Тераграф
Практикум посвящен освоению принципов работы вычислительного комплекса Тераграф и получению практических навыков решения задач обработки множеств на основе гетерогенной вычислительной структуры. В ходе практикума необходимо ознакомиться с типовой структурой двух взаимодействующих программ: хост-подсистемы и программного ядра sw_kernel. Для выполнения практикума предоставляется доступ к облачной платформе devlab.bmstu.ru с установленными ускорительными картами микропроцессора Леонард Эйлер и настроенными средствами сборки проектов.
3.1. Пример взаимодествия устройств: система определения ролей.
Рассмотрим следующие примеры кода подсистемы и программного ядра, которые мы будем использовать в практикуме. Система определения ролей пользователя сохраняет в памяти микропроцессора DISC тестовый набор ролей пользователей (1К пользователей с 1K ролями каждого). Далее система отвечает на запросы вида: какие роли пользователя были использованы начиная с момента времени time
. Рассматриваемый пример выполняет следующие действия:
- Хост подсистема инициализирует gpc программным ядром
sw_kernel.rawbinary
.
gpc64_inst = new gpc();
log<<"Открывается доступ к "<<gpc64_inst->gpc_dev_path<<endl;
if (gpc64_inst->load_swk(argv[1])==0) {
log<<"Программное ядро загружено из файла "<<argv[1]<<endl;
}
else {
log<<"Ошибка загрузки sw_kernel файла << argv[1]"<<endl;
return -1;
}
- Если программное ядро успешно загружено, хост подсистема запускает в gpc обработчик
update
, выполняющий прием и запись ключей и значений в SPE (ins_async
). Код обработчика, функцуионирующего вsw_kernel
представлен ниже:
//-------------------------------------------------------------
// Вставка ключа и значения в структуру
//-------------------------------------------------------------
void update() {
while(1){
users::key key=users::key::from_int(mq_receive());
if (key==-1ull) break;
users::val val=users::val::from_int(mq_receive());
// Поля структуры могут записываться явно следующим образом
// auto new_key = users::key{.rec_idx=1,.user=2};
// auto new_val = users::val{.role=3,.lst_time=0}
// Копирование полей в переменные можно выполнить следующим образом:
// auto user = key.user;
// auto [lst_time,role] = val;
USERS.ins_async(key,val); //Вставка в таблицу с типизацией uint64_t
}
}
- Далее хост-подсистема инициализирует поток сообщений к программному ядру. Для этого могут быть использованы два способа:
- Последовательная пересылка ключей и значений unsigned long long короткими сообщениями.
for (uint32_t user=0;user<TEST_USER_COUNT;user++) { for (uint32_t idx=0;idx<TEST_ROLE_COUNT;idx++,offs+=2) { gpc64_inst->mq_send(users::key{.idx=idx,.user=user}); //запись о роли #idx gpc64_inst->mq_send(users::val{.role=idx,.time=time_t(0)}); //роль и время доступа } }
- Заполнение буфера данных и передача его драйверу (блочная передача). Данный способ обеспечивает большую пропускную способность передачи, так как реализуется через механизм прямого доступа к памяти. Передача данных из буфера выполняется в асинхронном режиме (процесс запускается по команде
mq_send
). Для ожидания момента завершения передачи методmq_send
возвращает указатель на поток передачи. Далее, если требуется ожидание завершения процесса передачи, необходимо использовать синхронизирующую командуjoin
(send_buf_th->join()). Пример кода блочной передачи приведен ниже:unsigned long long *buf = (unsigned long long*)malloc(sizeof(unsigned long long)*TEST_USER_COUNT*TEST_ROLE_COUNT*2); for (uint32_t user=0,offs=0;user<TEST_USER_COUNT;user++) { for (uint32_t idx=0;idx<TEST_ROLE_COUNT;idx++,offs+=2) { buf[offs]=users::key{.idx=idx,.user=user}; buf[offs+1]=users::val{.role=idx,.time=time_t(idx*3600)}; } } auto send_buf_th = gpc64_inst->mq_send(sizeof(unsigned long long)*TEST_USER_COUNT*TEST_ROLE_COUNT*2,(char*)buf); send_buf_th->join(); free(buf);
- По завершению передачи посылается терминальный символ (
0xffffffffffffffff
):
//Терминальный символ
gpc64_inst->mq_send(-1ull);
- В ответ на терминальный символ
sw_kernel
завершает обработчикupdate
, и код хост-подсистемы запускает обработчик запросов поискаselect
. - Система готова к приему запросов пользователя. Формат таблицы, представленной в SPE микропроцессоре следующий common.sh
//Запись для формирования ключей (* - наиболее значимые биты поля)
STRUCT(key)
{
uint32_t idx :idx_bits; //Поле 0:
uint32_t user :32; //Поле 1*
};
//Запись для формирования значений
STRUCT(val)
{
uint32_t role :32; //Поле 0:
time_t time :32; //Поле 1*
};
Поле ключа состоит из полей: user
(поле идентификатора пользователя, старшая часть ключа) и idx
(поле индекса записи о роли пользователя, младшая часть ключа).
Поле значения состоит из полей: role
(поле идентификатора роли) и time
(поле времени последнего доступа).
- Запрос состоит в выборе тех ролей пользователя из таблицы
users
, которые были использованы позднее момента времениtime
, заданного в запросе. Например:
select role from users where user=5 and time>100000;
-
Программный код хост-системы использует регулярные выражения (
regex
) для выделения полей в запросеselect
. -
Поля запроса
user
иtime
передаются вsw_kernel
:
gpc64_inst->mq_send(stoi(match_query1[4])); //пользователь
gpc64_inst->mq_send(stoi(match_query1[6])); //время доступа
-
Микропроцессор DISC выбирает из ассоциативной памяти все роли пользователя
user
и определяет те из них, которые соответствуют условию запроса (например,time>100000
). Найденные роли передаются в хост-подсистему. -
В итоге, хост подсистема выдает сообщение о результатах поиска в поток cout.
Полный код приложения для хост-подсистемы показан ниже:
#define TEST_USER_COUNT 1000
#define TEST_ROLE_COUNT 1000
int main(int argc, char** argv)
{
ofstream log("lab2.log"); //поток вывода сообщений
unsigned long long offs=0ull;
gpc *gpc64_inst; //указатель на класс gpc
regex select_regex_query("select +(.*?) +from +(.*?) +where +(.*?)=(.*?) +and +(.*?)>(.*);", //запрос
std::regex_constants::ECMAScript | std::regex_constants::icase);
//Инициализация gpc
if (argc<2) {
log<<"Использование: host_main <путь к файлу rawbinary>"<<endl;
return -1;
}
//Захват ядра gpc и запись sw_kernel
gpc64_inst = new gpc();
log<<"Открывается доступ к "<<gpc64_inst->gpc_dev_path<<endl;
if (gpc64_inst->load_swk(argv[1])==0) {
log<<"Программное ядро загружено из файла "<<argv[1]<<endl;
}
else {
log<<"Ошибка загрузки sw_kernel файла << argv[1]"<<endl;
return -1;
}
//Инициализация таблицы для вложенного запроса
gpc64_inst->start(__event__(update)); //обработчик вставки
//1-й вариант: пересылка коротких сообщений
for (uint32_t user=0;user<TEST_USER_COUNT;user++) {
for (uint32_t idx=0;idx<TEST_ROLE_COUNT;idx++,offs+=2) {
gpc64_inst->mq_send(users::key{.idx=idx,.user=user}); //запись о роли #idx
gpc64_inst->mq_send(users::val{.role=idx,.time=time_t(0)}); //роль и время доступа
}
}
//2-й вариант: блочная передача
unsigned long long *buf = (unsigned long long*)malloc(sizeof(unsigned long long)*TEST_USER_COUNT*TEST_ROLE_COUNT*2);
for (uint32_t user=0,offs=0;user<TEST_USER_COUNT;user++) {
for (uint32_t idx=0;idx<TEST_ROLE_COUNT;idx++,offs+=2) {
buf[offs]=users::key{.idx=idx,.user=user};
buf[offs+1]=users::val{.role=idx,.time=time_t(idx*3600)};
}
}
auto send_buf_th = gpc64_inst->mq_send(sizeof(unsigned long long)*TEST_USER_COUNT*TEST_ROLE_COUNT*2,(char*)buf);
send_buf_th->join();
free(buf);
//Терминальный символ
gpc64_inst->mq_send(-1ull);
gpc64_inst->start(__event__(select)); //обработчик запроса поиска
while(1) {
string query1;
//разбор полей запроса
smatch match_query1;
getline(cin, query1);
log<<"Введен запрос: "<<query1<<endl;
if (!query1.compare("exit")) {
gpc64_inst->mq_send(-1ull);
break;
}
if (regex_match (query1, match_query1, select_regex_query) &&
match_query1[3]=="user" &&
match_query1[5] == "time") {
//match_query1[1] - возвращаемое поле запроса
//match_query1[2] - номер структуры запроса
//match_query1[3] - поле поиска 1
//match_query1[4] - значение поля поиска 1
//match_query1[5] - поле поиска 2
//match_query1[6] - значение поля поиска 2
log << "Запрос принят в обработку." << endl;
log << "Поиск ролей пользователя " << match_query1[4] << "и time > " << time_t(stoi(match_query1[6])) << endl;
gpc64_inst->mq_send(stoi(match_query1[4])); //пользователь
gpc64_inst->mq_send(stoi(match_query1[6])); //время доступа
while (1) {
uint64_t result = gpc64_inst->mq_receive();
if (result!=-1ull) {
cout << "Роль: " << users::val::from_int(result).role << " - ";
cout << "Время доступа: " << users::val::from_int(result).time << endl;
} else {
break;
}
}
} else {
log << "Ошибка в запросе!" << endl;
}
}
log << "Выход!" << endl;
return 0;
}
Код соответствующего кода sw_kernel представлен ниже.
extern lnh lnh_core;
volatile unsigned int event_source;
int main(void) {
/////////////////////////////////////////////////////////
// Main Event Loop
/////////////////////////////////////////////////////////
//Leonhard driver structure should be initialised
lnh_init();
for (;;) {
//Wait for event
event_source = wait_event();
set_gpc_state(BUSY);
switch(event_source) {
/////////////////////////////////////////////
// Measure GPN operation frequency
/////////////////////////////////////////////
case __event__(update) : update(); break;
case __event__(select) : select(); break;
}
set_gpc_state(IDLE);
}
}
//-------------------------------------------------------------
// Вставка ключа и значения в структуру
//-------------------------------------------------------------
void update() {
while(1){
users::key key=users::key::from_int(mq_receive());
if (key==-1ull) break;
users::val val=users::val::from_int(mq_receive());
// Поля структуры могут записываться явно следующим образом
// auto new_key = users::key{.rec_idx=1,.user=2};
// auto new_val = users::val{.role=3,.lst_time=0}
// Копирование полей в переменные можно выполнить следующим образом:
// auto user = key.user;
// auto [lst_time,role] = val;
USERS.ins_async(key,val); //Вставка в таблицу с типизацией uint64_t
}
}
//-------------------------------------------------------------
// Передать все роли пользователя и время доступа
//-------------------------------------------------------------
void select() {
while(1){
uint32_t quser = mq_receive();
if (quser==-1) break;
uint32_t qtime = mq_receive();
//Найдем все роли пользователя и последнее время доступа:
// Результаты поиска могут быть доступны следующим образом:
// auto user = USERS.search(users::key{.idx=1,.user=2}).key().user;
// auto role = USERS.search(users::key{.idx=3,.user=4}).value().role;
//Вариант 1 - обход записей пользователя явным образом
auto crole = USERS.nsm(users::key{.idx=users::idx_min,.user=quser});
while (crole && crole.key().user==quser) {
if (crole.value().time>qtime) mq_send(crole.value());
crole = USERS.nsm(crole.key());
}
//Вариант 2 - использование итератора
for (users::val val : role_range(USERS,quser)) {
if (val.time>qtime) mq_send(val);
}
mq_send_flush(-1ull);
}
}
3.2. Подключение к облачной платформе Тераграф Cloud
Для подключение к облачной платформе необходимо получить у организаторов практикума логин и начальный пароль.
Далее необходимо выполнить подключение по протоколу ssh и сменить разовый пароль. Выполнить это можно с помощью терминальных программ, поддерживающих протокол SSH:
-
В ОС Linux и MacOS - ssh клиент доступен в терминальном режиме в консоли.
Подключение в консоли выполняется при помощи следующей команды:
ssh username@195.19.32.95
где: username - имя пользователя, выдается организаторами практикума каждому участнику.
Обратите внимание, что при троекратном введении неверного пароля аккаунт пользователя будет заблокирован на 2 часа. Рекомендуется прописать ключ пользователя для доступа к серверу с помощью команды
ssh-copy-id username@195.19.32.95
, после чего вход на сервер будет осуществляться без ввода пароля.
На сервере установлены все необходимые библиотеки, средства для сборки и отладки проекта:
-
набор средств сборки riscv toolchain и экспорт исполняемых файлов в
PATH
-
набор библиотек picolib и экспорт в
C_INCLUDE_PATH
-
библиотека gpc64io на языке python для для работы с микропроцессором Леонард Эйлер.
Облачная платформа доступна по ссылке:
Облачная платформа построена на основе открытого программного решения JupyterHub
и VSCode
, и предоставляет следующие функциональные возможности:
- Для работы с приложениями, написанными на языке Python (для 2-й части практикума), или для работы в консоли ssh может быть использована среда
JupyterHub
.
- Для разработки и отладки программ, написанных на языке C/C++, а также других языках (Python, Go и пр.) может быть испольхована среда
VSCode
.
3.3. Утилита для мониторинга состояния гетерогенных ядер обработки графов
Для мониторинга состояния ядер модет быть использована утлита lnh_nfo
, установленная в системе devlab. Вызов системы выполняется следующим образом:
lnh_nfo -t/j [-l0,6,..]
Например, при вызове без параметров будет выдана информация обо всех ядрах gpc в виде таблицы:
user@dl580:~$ lnh_nfo
Утилита для мониторинга состояния гетерогенных ядер обработки графов.
Использование: lnh_nfo -t/j [-l0,6,..]
-t - вывод в виде таблицы (по умолчанию); -j - вывод в виде json; -l - список ядер gpc (номера через запятую)
Условные обозначения: <ключи> - количество ключей в структурах 1..7; <атрибуты> - состояние b+поддеревьев 0..7;
<O> - флаг переполнения поддерева; <E> - флаг пустого поддерева; <S> - номер структуры, занимающей поддерево;
<busy> - ядро выполняет обработчик; <rdy> - ядро ожидает запуска обработчика; * - символическое устройство открыто
╔══════╤══════════╤═════════════╤═════════════╤═════════════╤═════════════╤═════════════╤═════════════╤═════════════╤═════════════╗
║ ядро │ параметр │ #0 │ #1 │ #2 │ #3 │ #4 │ #5 │ #6 │ #7 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ * 0 │ ключи │ - │ 1000000 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ busy │ атрибуты │ O=0 E=1 S=1 │ O=0 E=0 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ 1 │ ключи │ - │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ rdy │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ 2 │ ключи │ - │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ busy │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ 3 │ ключи │ - │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ busy │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ 6 │ ключи │ - │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ busy │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ 7 │ ключи │ - │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ busy │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ 12 │ ключи │ - │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ busy │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ 13 │ ключи │ - │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ busy │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ 18 │ ключи │ - │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ init │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ 19 │ ключи │ - │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ init │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ 20 │ ключи │ - │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ init │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ 21 │ ключи │ - │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ init │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╚══════╧══════════╧═════════════╧═════════════╧═════════════╧═════════════╧═════════════╧═════════════╧═════════════╧═════════════╝
- Параметр
-t/j
позволяет выбрать формат вывода информации: -t - табибличный вариант; -j - json формат. - Необязательный параметр -l позволяет выбрать номера ядер, включаемые в вывод. Например
-l0,1,2,3
сформирует вывод только для gpc0..gpc3 - Для каждого ядра gpc выводится состояние физического и логического представления структур SPE (микропроцессора с набором команд дискретной математики).
- В строке <ключи> выдается информация о логических структурах SPE (как было сказано ранее, доступно 7 структур с номерами 1..7). При решении прикладных задач, программист должен самостоятельно определить в sw_kernel номера структур и использовать их. Например, с помощью команды `lnh_ins_async(1,key,val)` будет выполнена вставка в логическую структуру 1), после чего в таблице можно наблюдать изменение поля <ключи> в столбце #1.ключи>ключи>
- В строке <атрибуты> представлено текущее состояние физических структур (B+ поддеревьев в локальной памяти SPE). Всего доступно 8 поддеревьев (0..7), на которые могут быть распределены логические структуры. Номер логической структуры, занимающей B+ поддерево указано в атрибуте S (при этом S=0 означает, что поддерево не занято). Атрибут O указывает на факт полного заполнения поддерева (O=1 - дерево заполнено). Флаг E указывает на то, что поддерево не содержит уключей (E=1 - поддерево пусто).атрибуты>
- В столбце <ядро> указывается состояние гетерогенного ядра обработки графов. Сивол `*` означает, что символическое устройство ядра открыто. Ниже указывается текущее состояние, в котором находится CPE:
- начальное состояние CPE после включения питания или ресета; ядро>- CPE выполняет обработчик; - CPE ожидает запуска обработчика;
3.4. Запуск демо проекта 1
Пример демонстрирует основные механизмы инициализации гетерогенных ядер gpc и взаимодействие хост-подсистем с Graph Processor Core, используются аппаратные очереди.
Для установки требуется рекурсивно клонировать репозиторий:
git clone --recursive https://latex.bmstu.ru/gitlab/hackathon2023/lab1/lab1.git
cd lab1
Для стандартного пользователя ВМ студенческой команды хакатона все необходимые переменные окружения установлены по-умолчанию.
Для сборки проекта следует выполнить команду:
make
Результатом выполнения команды станет файлы host_main, sw_kernel_main.rawbinary в директориях ./host/ и ./sw_kernel/.
Для запуска проекта
Параметры запуска проекта:
host_main <путь к="" файлу="" sw_kernel="">путь>
Например:
host/host_main sw-kernel/sw_kernel.rawbinary
Результат работы теста:
Open gpc on /dev/gpc1
Rawbinary loaded from ../sw-kernel/sw_kernel.rawbinary
sw_kernel version: 0x20232109
Leonhard clock frequency (LNH_CF) 190.091429 MHz
Test done
Очистка проекта выполняется следующим образом:
make clean
3.5. Запуск демо проекта 2
Программа демонстрирует принципы взаимодействия устройств в системе и примеры хранения и обработки множеств в микропроцессоре Lnh64.
Для установки требуется рекурсивно клонировать репозиторий:
git clone --recursive https://latex.bmstu.ru/gitlab/hackathon2023/lab2/lab2.git
cd lab2
Для сборки проекта следует выполнить команду:
make
Результатом выполнения команды станет файлы host_main, sw_kernel_main.rawbinary в директориях ./host/ и ./sw_kernel/.
Для запуска проекта
Параметры запуска проекта:
host_main <путь к="" файлу="" sw_kernel="">путь>
Например:
host/host_main sw-kernel/sw_kernel.rawbinary
Далее необходимо ввести запрос (например: select role from users where user=5 and time>7200;
) или exit
для выхода.
Результат работы теста:
...
Роль: 10 - Время доступа: 36000
Роль: 9 - Время доступа: 32400
Роль: 8 - Время доступа: 28800
Роль: 7 - Время доступа: 25200
Роль: 6 - Время доступа: 21600
Роль: 5 - Время доступа: 18000
Роль: 4 - Время доступа: 14400
Роль: 3 - Время доступа: 10800
Одновременно с выводом результата в поток stdout, приложение выводит лог работы в файл lab2.log. Просмотр лога возможен по команде:
tail -f lab2.log
Очистка проекта выполняется следующим образом:
make clean
3.6. Запуск демо проекта 3
Пример демонстрирует основные механизмы взаимодействия микропроцессоров CPE (resv64) и SPE (lnh64), и выполняет тестирование корректности команд DISC.
Для установки требуется рекурсивно клонировать репозиторий:
git clone --recursive https://latex.bmstu.ru/gitlab/hackathon2023/lab3.git
cd lab3
Для стандартного пользователя ВМ студенческой команды хакатона все необходимые переменные окружения установлены по-умолчанию.
Для сборки проекта следует выполнить команду:
make
Результатом выполнения команды станет файлы host_main, sw_kernel_main.rawbinary в директориях ./host/ и ./sw_kernel/.
Для запуска проекта
Параметры запуска проекта:
host_main <путь к="" файлу="" sw_kernel="">путь>
Например:
host/host_main sw-kernel/sw_kernel.rawbinary
На ядрах gpc начнется процесс тестирования:
Утилита для мониторинга состояния гетерогенных ядер обработки графов.
Использование: lnh_nfo -t/j [-l0,6,..]
-t - вывод в виде таблицы (по умолчанию); -j - вывод в виде json; -l - список ядер gpc (номера через запятую)
Условные обозначения: <ключи> - количество ключей в структурах 1..7; <атрибуты> - состояние b+поддеревьев 0..7;
<O> - флаг переполнения поддерева; <E> - флаг пустого поддерева; <S> - номер структуры, занимающей поддерево;
<busy> - ядро выполняет обработчик; <rdy> - ядро ожидает запуска обработчика; * - символическое устройство открыто
╔═══════╤══════════╤═════════════╤═════════════╤═════════════╤═════════════╤═════════════╤═════════════╤═════════════╤═════════════╗
║ ядро# │ параметр │ #0 │ #1 │ #2 │ #3 │ #4 │ #5 │ #6 │ #7 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ * 0 │ ключи │ - │ 48064052 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ * 1 │ ключи │ - │ 47589696 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ * 2 │ ключи │ - │ 47224312 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ * 3 │ ключи │ - │ 46900114 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ * 6 │ ключи │ - │ 46758051 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ * 7 │ ключи │ - │ 46407924 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ * 12 │ ключи │ - │ 45959794 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ * 13 │ ключи │ - │ 45607688 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ * 18 │ ключи │ - │ 45081866 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ * 19 │ ключи │ - │ 44720079 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ * 20 │ ключи │ - │ 44367391 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ * 21 │ ключи │ - │ 44017839 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 ║
║ busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=0 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╚═══════╧══════════╧═════════════╧═════════════╧═════════════╧═════════════╧═════════════╧═════════════╧═════════════╧═════════════╝
Результат работы тестов будет выведен на консоль:
Ядро /dev/gpc0. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc1. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc2. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc3. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc6. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc7. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc12. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc13. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc18. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc19. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc20. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc21. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc0. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc1. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc2. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc3. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc6. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc7. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc12. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc13. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc0. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc18. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc19. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc1. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc20. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc2. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc3. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc21. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc6. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc7. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc12. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc13. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc18. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc19. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc20. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc21. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc6. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc7. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc6. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc6. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc7. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc6. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc7. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc12. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc7. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc6. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc12. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc13. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc12. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc7. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc6. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc13. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc12. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc13. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc7. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc6. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc13. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc12. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc7. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc13. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc12. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc13. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc12. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc13. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc0. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc0. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc0. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc1. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc0. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc1. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc2. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc1. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc0. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc3. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc2. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc1. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc2. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc3. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc0. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc3. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc2. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc1. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc3. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc0. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc2. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc1. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc3. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc2. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc1. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc3. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc18. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc2. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc3. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc18. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc18. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc19. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc18. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc19. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc20. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc19. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc21. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc18. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc20. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc19. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc20. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc21. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc18. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc21. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc20. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc19. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc21. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc18. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc20. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc19. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc21. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc20. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc19. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc21. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc20. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc21. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Очистка проекта выполняется следующим образом:
make clean
3.7. Индивидуальные задания
Разработать программу для хост-подсистемы и обработчики программного ядра, выполняющие следующие действия:
Вариант 1:
Сформировать в хост-подсистеме и передать в SPE две коллекции.
Описание коллекций:
- students: student_id, name, age.
- courses: course_id, title, student_ids (список идентификаторов студентов, записанных на курс).
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран имена всех студентов, записанных на курс с заданным идентификатором CS101 (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
LET course = DOCUMENT(“courses”, “CS101”)
FOR studentId IN course.student_ids
FOR student IN students
FILTER student.student_id == studentId
RETURN student.name
Объяснение:
- LET course: Получаем документ курса с идентификатором CS101 из коллекции courses.
- FOR studentId IN course.student_ids: Итерируем по списку идентификаторов студентов, записанных на этот курс.
- FOR student IN students: Ищем студентов в коллекции students.
- FILTER student.student_id == studentId: Фильтруем студентов по совпадению идентификаторов.
- RETURN student.name: Возвращаем имена студентов.
Вариант 2:
Сформировать в хост-подсистеме и передать в SPE две коллекции.
Описание коллекций:
- professors: professor_id, name, department_id.
- departments: department_id, department_name.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран имена всех профессоров из кафедры с названием Computer Science (передается в запросе из хост системы)?
Эквивалентный запрос на языке AQL:
FOR dept IN departments
FILTER dept.department_name == “Computer Science”
FOR prof IN professors
FILTER prof.department_id == dept.department_id
RETURN prof.name
Объяснение:
- FOR dept IN departments: Итерируем по коллекции кафедр.
- FILTER dept.department_name == “Computer Science”: Находим нужную кафедру.
- FOR prof IN professors: Итерируем по коллекции профессоров.
- FILTER prof.department_id == dept.department_id: Находим профессоров из этой кафедры.
- RETURN prof.name: Возвращаем имена профессоров.
Вариант 3:
Сформировать в хост-подсистеме и передать в SPE две коллекции.
Описание коллекций:
- students: student_id, name, age.
- enrollments: enrollment_id, student_id, course_id, grade.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран имена студентов, которые получили оценку A по курсу с идентификатором CS101 (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
FOR enr IN enrollments
FILTER enr.course_id == “CS101” AND enr.grade == “A”
FOR student IN students
FILTER student.student_id == enr.student_id
RETURN student.name
Объяснение:
- FOR enr IN enrollments: Итерируем по записям о зачислении.
- FILTER enr.course_id == “CS101” AND enr.grade == “A”: Фильтруем записи по курсу и оценке.
- FOR student IN students: Итерируем по студентам.
- FILTER student.student_id == enr.student_id: Связываем записи о зачислении со студентами.
- RETURN student.name: Возвращаем имена студентов.
Вариант 4:
Сформировать в хост-подсистеме и передать в SPE две коллекции.
Описание коллекций:
- books: book_id, title, author, borrower_id.
- borrowers: borrower_id, name, student_id.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран названия книг, которые взял студент с именем Алексей Иванов (передается в запросе из хост системы)?
Эквивалентный запрос на языке AQL:
FOR borrower IN borrowers
FILTER borrower.name == “Алексей Иванов”
FOR book IN books
FILTER book.borrower_id == borrower.borrower_id
RETURN book.title
Объяснение:
- FOR borrower IN borrowers: Итерируем по заемщикам.
- FILTER borrower.name == “Алексей Иванов”: Находим нужного заемщика.
- FOR book IN books: Итерируем по книгам.
- FILTER book.borrower_id == borrower.borrower_id: Находим книги, взятые этим заемщиком.
- RETURN book.title: Возвращаем названия книг.
Вариант 5:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, club_ids (список идентификаторов клубов).
- clubs: club_id, club_name.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран названия клубов, в которых состоит студент с именем Елена Смирнова (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Елена Смирнова”
FOR clubId IN student.club_ids
FOR club IN clubs
FILTER club.club_id == clubId
RETURN DISTINCT club.club_name
Объяснение:
- FOR student IN students: Итерируем по студентам.
- FILTER student.name == “Елена Смирнова”: Находим нужного студента.
- FOR clubId IN student.club_ids: Итерируем по идентификаторам клубов студента.
- FOR club IN clubs: Итерируем по клубам.
- FILTER club.club_id == clubId: Находим соответствующие клубы.
- RETURN DISTINCT club.club_name: Возвращаем уникальные названия клубов.
Вариант 6:
Сформировать в хост-подсистеме и передать в SPE две коллекции.
Описание коллекций:
- courses: course_id, title, department_id.
- departments: department_id, department_name.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран названия всех курсов, которые принадлежат к кафедре Mathematics (передается в запросе из хост системы)?
Эквивалентный запрос на языке AQL:
FOR dept IN departments
FILTER dept.department_name == “Mathematics”
FOR course IN courses
FILTER course.department_id == dept.department_id
RETURN course.title
Объяснение:
- FOR dept IN departments: Итерируем по кафедрам.
- FILTER dept.department_name == “Mathematics”: Находим кафедру математики.
- FOR course IN courses: Итерируем по курсам.
- FILTER course.department_id == dept.department_id: Фильтруем курсы по кафедре.
- RETURN course.title: Возвращаем названия курсов.
Вариант 7:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, gpa.
- scholarships: scholarship_id, name, min_gpa.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран имена стипендий, на которые может претендовать студент с именем Иван Кузнецов (передается в запросе из хост-подсистемы), исходя из его среднего балла (gpa)?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Иван Кузнецов”
FOR scholarship IN scholarships
FILTER student.gpa >= scholarship.min_gpa
RETURN scholarship.name
Объяснение:
- FOR student IN students: Итерируем по студентам.
- FILTER student.name == “Иван Кузнецов”: Находим нужного студента.
- FOR scholarship IN scholarships: Итерируем по стипендиям.
- FILTER student.gpa >= scholarship.min_gpa: Проверяем соответствие среднего балла требованиям.
- RETURN scholarship.name: Возвращаем имена стипендий.
Вариант 8:
Сформировать в хост-подсистеме и передать в SPE две коллекции.
Описание коллекций:
- students: student_id, name.
- attendance: attendance_id, student_id, date, status (например, Present или Absent).
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран даты, когда студент с именем Наталья Соколова (передается в запросе из хост-подсистемы) был(а) отсутствующим(ей)?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Наталья Соколова”
FOR att IN attendance
FILTER att.student_id == student.student_id AND att.status ==
“Absent”
RETURN att.date
Объяснение:
- FOR student IN students: Итерируем по студентам.
- FILTER student.name == “Наталья Соколова”: Находим нужного студента.
- FOR att IN attendance: Итерируем по записям посещаемости.
- FILTER att.student_id == student.student_id AND att.status == “Absent”: Фильтруем по студенту и статусу отсутствия.
- RETURN att.date: Возвращаем даты отсутствия.
Вариант 9:
Сформировать в хост-подсистеме и передать в SPE две коллекции.
Описание коллекций:
- students: student_id, name, major.
- internships: internship_id, company, required_major.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран компании, предлагающие стажировки для специальности студента с именем Дмитрий Орлов (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Дмитрий Орлов”
FOR internship IN internships
FILTER internship.required_major == student.major
RETURN internship.company
Объяснение:
- FOR student IN students: Итерируем по студентам.
- FILTER student.name == “Дмитрий Орлов”: Находим нужного студента.
- FOR internship IN internships: Итерируем по стажировкам.
- FILTER internship.required_major == student.major: Фильтруем стажировки по специальности.
- RETURN internship.company: Возвращаем названия компаний.
Вариант 10:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name.
- payments: payment_id, student_id, amount, date.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
вычислить общую сумму платежей, сделанных студентом с именем Светлана Николаева (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
LET student = FIRST(
FOR s IN students
FILTER s.name == “Светлана Николаева”
RETURN s
)
LET totalAmount = SUM(
FOR payment IN payments
FILTER payment.student_id == student.student_id
RETURN payment.amount
)
RETURN { “name”: student.name, “totalPayments”: totalAmount }
Объяснение:
- LET student: Находим студента и сохраняем его документ.
- SUM: Вычисляем сумму платежей данного студента.
- FOR payment IN payments: Итерируем по платежам.
- FILTER payment.student_id == student.student_id: Фильтруем платежи по студенту.
- RETURN payment.amount: Собираем суммы платежей.
- RETURN: Возвращаем имя студента и общую сумму платежей.
Вариант 11:
Сформировать в хост-подсистеме и передать в SPE две коллекции.
Описание коллекций:
- students: student_id, name, advisor_id.
- advisors: advisor_id, name, department.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран имена научных руководителей (advisors) студентов, обучающихся на кафедре Physics (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
FOR advisor IN advisors
FILTER advisor.department == “Physics”
FOR student IN students
FILTER student.advisor_id == advisor.advisor_id
RETURN DISTINCT advisor.name
Объяснение:
- FOR advisor IN advisors: Итерируем по коллекции advisors (научные руководители).
- FILTER advisor.department == “Physics”: Фильтруем руководителей по кафедре физики.
- FOR student IN students: Итерируем по коллекции students (студенты).
- FILTER student.advisor_id == advisor.advisor_id: Находим студентов, у которых этот руководитель.
- RETURN DISTINCT advisor.name: Возвращаем уникальные имена руководителей.
Вариант 12:
Сформировать в хост-подсистеме и передать в SPE две коллекции.
Описание коллекций:
- students: student_id, name, graduation_year.
- awards: award_id, name, year, student_id.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран имена студентов, которые получили награды в год своего выпуска?
Эквивалентный запрос на языке AQL:
FOR student IN students
FOR award IN awards
FILTER award.student_id == student.student_id AND award.year ==
student.graduation_year
RETURN student.name
Объяснение:
- FOR student IN students: Итерируем по студентам.
- FOR award IN awards: Итерируем по наградам.
- FILTER award.student_id == student.student_id AND award.year == student.graduation_year: Фильтруем награды, полученные студентом в год выпуска.
- RETURN student.name: Возвращаем имена таких студентов.
Вариант 13:
Сформировать в хост-подсистеме и передать в SPE две коллекции.
Описание коллекций:
- students: student_id, name.
- alumni: alumni_id, student_id, current_company, position.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран текущие компании, в которых работают выпускники с именем Ольга Лебедева (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Ольга Лебедева”
FOR alum IN alumni
FILTER alum.student_id == student.student_id
RETURN alum.current_company
Объяснение:
- FOR student IN students: Находим студента по имени.
- FILTER student.name == “Ольга Лебедева”: Фильтруем по имени студента.
- FOR alum IN alumni: Итерируем по выпускникам.
- FILTER alum.student_id == student.student_id: Находим выпускника по идентификатору студента.
- RETURN alum.current_company: Возвращаем название текущей компании выпускника.
Вариант 14:
Сформировать в хост-подсистеме и передать в SPE две коллекции.
Описание коллекций:
- students: student_id, name.
- projects: project_id, title, student_ids (список идентификаторов студентов).
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран названия проектов, в которых участвует студент с именем Павел Морозов (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Павел Морозов”
FOR project IN projects
FILTER student.student_id IN project.student_ids
RETURN project.title
Объяснение:
- FOR student IN students: Находим нужного студента.
- FILTER student.name == “Павел Морозов”: Фильтруем по имени.
- FOR project IN projects: Итерируем по проектам.
- FILTER student.student_id IN project.student_ids: Проверяем участие студента в проекте.
- RETURN project.title: Возвращаем названия проектов.
Вариант 15:
Сформировать в хост-подсистеме и передать в SPE две коллекции.
Описание коллекций:
- students: student_id, name, country.
- exchange_programs: program_id, country, university, duration_months.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран названия университетов, предлагающих программы обмена в стране, откуда родом студент с именем Карина Васильева (передается в запросе из хост системы)?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Карина Васильева”
FOR program IN exchange_programs
FILTER program.country == student.country
RETURN program.university\
Объяснение:
- FOR student IN students: Находим студента по имени.
- FILTER student.name == “Карина Васильева”: Фильтруем по имени.
- FOR program IN exchange_programs: Итерируем по программам обмена.
- FILTER program.country == student.country: Фильтруем программы по стране студента.
- RETURN program.university: Возвращаем названия университетов.
Вариант 16:
Сформировать в хост-подсистеме и передать в SPE две коллекции.
Описание коллекций:
- students: student_id, name, major.
- tutors: tutor_id, name, majors (список специальностей).
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран имена репетиторов, которые могут помочь студенту с именем Виктория Сидорова (передается в запросе из хост-подсистемы) по её специальности?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Виктория Сидорова”
FOR tutor IN tutors
FILTER student.major IN tutor.majors
RETURN tutor.name
Объяснение:
- FOR student IN students: Находим нужного студента.
- FOR tutor IN tutors: Итерируем по репетиторам.
- FILTER student.major IN tutor.majors: Проверяем, преподаёт ли репетитор специальность студента.
- RETURN tutor.name: Возвращаем имена подходящих репетиторов.
Вариант 17:
Сформировать в хост-подсистеме и передать в SPE две коллекции.
Описание коллекций:
- students: student_id, name.
- disciplinary_actions: action_id, student_id, action, date.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран имена студентов, у которых не было дисциплинарных взысканий?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER NOT (
FOR action IN disciplinary_actions
FILTER action.student_id == student.student_id
LIMIT 1
RETURN 1
)
RETURN student.name
Объяснение:
- FOR student IN students: Итерируем по всем студентам.
- FILTER NOT (…): Проверяем отсутствие записей о взысканиях.
- Вложенный FOR: Ищем дисциплинарные действия для студента.
- LIMIT 1: Достаточно найти хотя бы одну запись.
- RETURN student.name: Возвращаем имена студентов без взысканий.
Вариант 18:
Сформировать в хост-подсистеме и передать в SPE две коллекции.
Описание коллекций:
- students: student_id, name.
- health_records: record_id, student_id, vaccination_status, notes.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран список студентов, у которых нет отметки о вакцинации?
Эквивалентный запрос на языке AQL:
FOR student IN students
FOR record IN health_records
FILTER record.student_id == student.student_id AND
record.vaccination_status == “Not Vaccinated”
RETURN student.name
Объяснение:
- FOR student IN students: Итерируем по студентам.
- FOR record IN health_records: Итерируем по медицинским записям.
- FILTER: Фильтруем записи без вакцинации.
- RETURN student.name: Возвращаем имена таких студентов.
Вариант 19:
Сформировать в хост-подсистеме и передать в SPE две коллекции.
Описание коллекций:
- students: student_id, name, year_of_study.
- mentorships: mentorship_id, mentor_id, mentee_id.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран имена студентов-третьекуурсников (передается в запросе из хост-подсистемы), у которых нет нучного руководителя?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.year_of_study == 1
FILTER NOT (
FOR mentorship IN mentorships
FILTER mentorship.mentee_id == student.student_id
LIMIT 1
RETURN 1
)
RETURN student.name
Объяснение:
- Отбираем первокурсников: FILTER student.year_of_study == 1.
- Проверяем отсутствие наставника: Используем вложенный запрос с NOT.
- Возвращаем имена студентов: RETURN student.name.
Вариант 20:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name.
- attendance: attendance_id, student_id, course_id, attendance_percentage.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран имена студентов, чья посещаемость по курсу Math101 менее 75% (передается в запросе из хост системы)?
Эквивалентный запрос на языке AQL:
FOR att IN attendance
FILTER att.course_id == “Math101” AND att.attendance_percentage <
75
FOR student IN students
FILTER student.student_id == att.student_id
RETURN student.name
Объяснение:
- FOR att IN attendance: Итерируем по посещаемости.
- Фильтруем по курсу и проценту: FILTER att.course_id == “Math101” AND att.attendance_percentage < 75.
- Связываем с студентами: Ищем студента по student_id.
- Возвращаем имена: RETURN student.name.
Вариант 21:
Сформировать в хост-подсистеме и передать в SPE две коллекции.
Описание коллекций:
- students: student_id, name.
- completed_courses: student_id, course_id.
- online_courses: course_id, title, required_courses (список курсов-пререквизитов).
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран названия онлайн-курсов, которые может пройти студент с именем Игорь Новиков (передается в запросе из хост-подсистемы), если он уже прошёл все необходимые курсы?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Игорь Новиков”
LET completedCourses = (
FOR cc IN completed_courses
FILTER cc.student_id == student.student_id
RETURN cc.course_id
)
FOR course IN online_courses
FILTER LENGTH(course.required_courses) == 0 OR
EVERY req IN course.required_courses
SATISFIES req IN completedCourses
RETURN course.title
Объяснение:
- Находим студента: FILTER student.name == “Игорь Новиков”.
- Получаем список пройденных курсов: Используем LET completedCourses.
- Итерируем по онлайн-курсам: FOR course IN online_courses.
- Проверяем пререквизиты: Используем EVERY для проверки, что все требуемые курсы пройдены.
- Возвращаем названия курсов: RETURN course.title.
Вариант 22:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, skills (список навыков).
- job_openings: job_id, title, required_skills (список требуемых навыков).
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран названия вакансий, для которых студент с именем Антон Белый (передается в запросе из хост системы) обладает всеми необходимыми навыками?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Антон Белый”
LET studentSkills = student.skills
FOR job IN job_openings
FILTER EVERY skill IN job.required_skills
SATISFIES skill IN studentSkills
RETURN job.title
Объяснение:
- Находим студента: FILTER student.name == “Антон Белый” — выбираем студента с указанным именем.
- Получаем список навыков студента: LET studentSkills = student.skills.
- Итерируем по вакансиям: FOR job IN job_openings.
- Проверяем, обладает ли студент всеми требуемыми навыками:
- Используем функцию EVERY, которая проверяет, что каждый элемент в job.required_skills удовлетворяет условию.
- SATISFIES skill IN studentSkills — проверяем, есть ли каждый требуемый навык в списке навыков студента.
- Возвращаем названия подходящих вакансий: RETURN job.title.
Вариант 23:
Сформировать в хост-подсистеме и передать в SPE две коллекции.
Описание коллекций:
- students: student_id, name.
- library_loans: loan_id, student_id, book_title, due_date.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран список книг, которые студент с именем Мария Ковалева (передается в запросе из хост-подсистемы) должен вернуть в библиотеку после 2024-01-01 (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Мария Ковалева”
FOR loan IN library_loans
FILTER loan.student_id == student.student_id AND loan.due_date
> “2024-01-01”
RETURN loan.book_title
Объяснение:
- Находим студента: FILTER student.name == “Мария Ковалева”.
- Итерируем по записям о выдаче книг: FOR loan IN library_loans.
- Фильтруем записи по студенту и дате возврата:
- loan.student_id == student.student_id — книга взята этим студентом.
- loan.due_date > “2024-01-01” — дата возврата после указанной.
- Возвращаем названия книг: RETURN loan.book_title.
Вариант 24:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, city.
- companies: company_id, name, city.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран названия компаний, расположенных в том же городе, где живёт студент Сергей Крылов (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Сергей Крылов”
FOR company IN companies
FILTER company.city == student.city
RETURN company.name
Объяснение:
- Находим студента: FILTER student.name == “Сергей Крылов”.
- Итерируем по компаниям: FOR company IN companies.
- Фильтруем компании по городу проживания студента: company.city == student.city.
- Возвращаем названия компаний: RETURN company.name.
Вариант 25:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name.
- exam_results: exam_id, student_id, subject, score.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран средний балл студента Алина Орлова (передается в запросе из хост-подсистемы) по всем экзаменам?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Алина Орлова”
LET scores = (
FOR result IN exam_results
FILTER result.student_id == student.student_id
RETURN result.score
)
RETURN {
“name”: student.name,
“average_score”: AVERAGE(scores)
}
Объяснение:
- Находим студента: FILTER student.name == “Алина Орлова”.
- Собираем оценки по экзаменам:
-
- Используем подзапрос LET scores = (…) для сбора всех оценок студента.
- FILTER result.student_id == student.student_id — выбираем результаты этого студента.
- RETURN result.score — возвращаем оценки.
- Вычисляем средний балл: Используем функцию AVERAGE(scores).
- Возвращаем результат: RETURN { “name”: student.name, “average_score”: … }.
Вариант 26:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name.
- housing_applications: application_id, student_id, dormitory, status.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран названия общежитий, в которые подал заявки студент Денис Фролов (передается в запросе из хост системы), и статус каждой заявки?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Денис Фролов”
FOR application IN housing_applications
FILTER application.student_id == student.student_id
RETURN {
“dormitory”: application.dormitory,
“status”: application.status
}
Объяснение:
- Находим студента: FILTER student.name == “Денис Фролов”.
- Итерируем по заявкам на проживание: FOR application IN housing_applications.
- Фильтруем по идентификатору студента: application.student_id == student.student_id.
- Возвращаем названия общежитий и статус заявок:
-
- RETURN { “dormitory”: application.dormitory, “status”: application.status }.
Вариант 27:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name.
- meal_plans: plan_id, student_id, plan_type, start_date, end_date.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
определить, есть ли у студента Олег Никитин (передается в запросе из хост-подсистемы) активный план питания на текущую дату (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
LET currentDate = DATE_NOW()
FOR student IN students
FILTER student.name == “Олег Никитин”
FOR plan IN meal_plans
FILTER plan.student_id == student.student_id
AND plan.start_date <= currentDate
AND plan.end_date >= currentDate
RETURN {
“name”: student.name,
“active_plan”: plan.plan_type
}
Объяснение:
- Получаем текущую дату: LET currentDate = DATE_NOW().
- Находим студента: FILTER student.name == “Олег Никитин”.
- Ищем активный план питания:
-
- plan.start_date <= currentDate и plan.end_date >= currentDate — план активен.
- Возвращаем результат:
-
- RETURN { “name”: student.name, “active_plan”: plan.plan_type }.
Вариант 28:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name.
- extracurricular_activities: activity_id, name, participants (список student_id).
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран названия внеклассных мероприятий, в которых не участвует студент Ирина Зайцева (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Ирина Зайцева”
FOR activity IN extracurricular_activities
FILTER NOT (student.student_id IN activity.participants)
RETURN activity.name
Объяснение:
- Находим студента: FILTER student.name == “Ирина Зайцева”.
- Итерируем по мероприятиям: FOR activity IN extracurricular_activities.
- Фильтруем мероприятия, где студент не участвует:
-
- NOT (student.student_id IN activity.participants).
- Возвращаем названия мероприятий: RETURN activity.name.
Вариант 29:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, email.
- email_subscriptions: subscription_id, email, subscription_type.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Проверить, подписан ли студент Андрей Лебедев (передается в запросе из хост-подсистемы) на рассылку типа Newsletter?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Андрей Лебедев”
LET subscription = FIRST(
FOR sub IN email_subscriptions
FILTER sub.email == student.email AND sub.subscription_type ==
“Newsletter”
RETURN sub
)
RETURN {
“name”: student.name,
“is_subscribed”: subscription != null
}
Объяснение:
- Находим студента: FILTER student.name == “Андрей Лебедев”.
- Ищем подписку:
-
- Используем LET subscription = FIRST(…) для получения первой подходящей подписки.
- FILTER sub.email == student.email AND sub.subscription_type == “Newsletter”.
- Проверяем наличие подписки:
-
- Если subscription != null, то студент подписан.
- Возвращаем результат:
-
- RETURN { “name”: student.name, “is_subscribed”: subscription != null }.
Вариант 30:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, preferred_language.
- language_courses: course_id, language, level.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран список языковых курсов, соответствующих уровню Beginner, на которые может записаться студент Екатерина Полякова (передается в запросе из хост-подсистемы), учитывая её предпочтительный язык?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Екатерина Полякова”
FOR course IN language_courses
FILTER course.language == student.preferred_language AND
course.level == “Beginner”
RETURN course
Объяснение:
- Находим студента: FILTER student.name == “Екатерина Полякова”.
- Итерируем по языковым курсам: FOR course IN language_courses.
- Фильтруем курсы по предпочтительному языку и уровню:
course.language == student.preferred_language.
course.level == “Beginner”.
- Возвращаем подходящие курсы: RETURN course.
Вариант 31:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, birthdate.
- events: event_id, name, date, type.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран названия мероприятий типа Birthday Party, которые совпадают с днём рождения студента Владимир Соловьёв (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Владимир Соловьёв”
LET birthDate = DATE_FORMAT(student.birthdate, “%m-%d”)
FOR event IN events
FILTER event.type == “Birthday Party”
AND DATE_FORMAT(event.date, “%m-%d”) == birthDate
RETURN event.name
Объяснение:
- Находим студента: FILTER student.name == “Владимир Соловьёв”.
- Получаем день и месяц рождения: LET birthDate = DATE_FORMAT(student.birthdate, “%m-%d”).
- Итерируем по мероприятиям: FOR event IN events.
- Фильтруем по типу и дате:
event.type == “Birthday Party”.
DATE_FORMAT(event.date, “%m-%d”) == birthDate — совпадает день и месяц.
- Возвращаем названия мероприятий: RETURN event.name.
Вариант 32:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, enrollment_year.
- yearly_fees: year, amount.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
вычислить общую сумму оплаты за обучение для студента Светлана Иванова (передается в запросе из хост-подсистемы) с момента поступления до указанной даты (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Светлана Иванова”
LET years = RANGE(student.enrollment_year, YEAR(DATE_NOW()))
LET totalFees = SUM(
FOR year IN years
FOR fee IN yearly_fees
FILTER fee.year == year
RETURN fee.amount
)
RETURN {
“name”: student.name,
“total_fees”: totalFees
}
Объяснение:
- Находим студента: FILTER student.name == “Светлана Иванова”.
- Определяем годы обучения: LET years = RANGE(student.enrollment_year, YEAR(DATE_NOW())).
- Вычисляем общую сумму оплаты:
-
- Итерируем по годам и сбораем соответствующие суммы из yearly_fees.
- Используем SUM для суммирования.
- Возвращаем результат: RETURN { “name”: student.name, “total_fees”: totalFees }
Вариант 33:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, major.
- study_abroad_programs: program_id, country, majors (список специальностей), duration_months.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран список программ обучения за рубежом, подходящих для специальности студента Максим Петров (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Максим Петров”
FOR program IN study_abroad_programs
FILTER student.major IN program.majors
RETURN {
“program_id”: program.program_id,
“country”: program.country,
“duration_months”: program.duration_months
}
Объяснение:
- Находим студента: FILTER student.name == “Максим Петров” — выбираем студента с указанным именем.
- Итерируем по программам обучения за рубежом: FOR program IN study_abroad_programs.
- Проверяем, подходит ли программа по специальности:
-
- student.major IN program.majors — проверяем, есть ли специальность студента в списке специальностей программы.
- Возвращаем информацию о подходящих программах:
-
- RETURN { “program_id”: …, “country”: …, “duration_months”: … }.
Вариант 34:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, year_of_study.
- course_materials: material_id, course_id, year_available, title.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран названия учебных материалов, доступных для студентов второго курса, таких как Елена Гордеева (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Елена Гордеева” AND student.year_of_study ==
2
FOR material IN course_materials
FILTER material.year_available <= student.year_of_study
RETURN material.title
Объяснение:
- Находим студента второго курса: FILTER student.name == “Елена Гордеева” AND student.year_of_study == 2.
- Итерируем по учебным материалам: FOR material IN course_materials.
- Проверяем доступность материала для года обучения студента:
-
- material.year_available <= student.year_of_study.
- Возвращаем названия материалов: RETURN material.title.
Вариант 35:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, email.
- notifications: notification_id, recipient_email, message, sent_date.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран последние 5 уведомлений, отправленных студенту Александра Белова (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Александра Белова”
FOR notification IN notifications
FILTER notification.recipient_email == student.email
SORT notification.sent_date DESC
LIMIT 5
RETURN notification.message
Объяснение:
- Находим студента: FILTER student.name == “Александра Белова”.
- Итерируем по уведомлениям: FOR notification IN notifications.
- Фильтруем уведомления по email студента: notification.recipient_email == student.email.
- Сортируем уведомления по дате отправки в порядке убывания: SORT notification.sent_date DESC.
- Ограничиваем результаты до 5 записей: LIMIT 5.
- Возвращаем сообщения уведомлений: RETURN notification.message.
Вариант 36:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name.
- volunteer_hours: record_id, student_id, hours, date.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Подсчитать общее количество волонтёрских часов, набранных студентом Дмитрий Егоров (передается в запросе из хост-подсистемы) за 2023 год (передается в запросе из хост-подсистемы)? Результат передать в хост систему и выдать на экран.
Эквивалентный запрос на языке AQL:
LET targetYear = 2023
FOR student IN students
FILTER student.name == “Дмитрий Егоров”
LET totalHours = SUM(
FOR record IN volunteer_hours
FILTER record.student_id == student.student_id
AND DATE_YEAR(record.date) == targetYear
RETURN record.hours
)
RETURN {
“name”: student.name,
“total_volunteer_hours”: totalHours
}
Объяснение:
- Устанавливаем целевой год: LET targetYear = 2023.
- Находим студента: FILTER student.name == “Дмитрий Егоров”.
- Суммируем волонтёрские часы за указанный год:
-
- Итерируем по записям volunteer_hours.
- Фильтруем по student_id и году (DATE_YEAR(record.date) == targetYear).
- Используем SUM для вычисления общей суммы часов.
- Возвращаем результат: RETURN { “name”: …, “total_volunteer_hours”: … }.
Вариант 37:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, preferences (список предпочитаемых кухонь).
- cafeterias: cafeteria_id, name, cuisines (список предлагаемых кухонь).
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Найти столовые, предлагающие кухни, предпочитаемые студентом Игорь Власов (передается в запросе из хост-подсистемы)? Результат передать в хост систему и выдать на экран.
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Игорь Власов”
FOR cafeteria IN cafeterias
FILTER LENGTH(INTERSECTION(student.preferences, cafeteria.cuisines))
> 0
RETURN cafeteria.name
Объяснение:
- Находим студента: FILTER student.name == “Игорь Власов”.
- Итерируем по столовым: FOR cafeteria IN cafeterias.
- Ищем пересечение предпочтений и предлагаемых кухонь:
-
- INTERSECTION(student.preferences, cafeteria.cuisines) возвращает общие элементы.
- Проверяем, что длина пересечения больше 0: LENGTH(…) > 0.
- Возвращаем названия подходящих столовых: RETURN cafeteria.name.
Вариант 38:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name.
- student_accounts: account_id, student_id, balance.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Найти студентов с отрицательным балансом на их счетах? Результат передать в хост систему и выдать на экран.
Эквивалентный запрос на языке AQL:
FOR account IN student_accounts
FILTER account.balance < 0
FOR student IN students
FILTER student.student_id == account.student_id
RETURN {
“name”: student.name,
“balance”: account.balance
}
Объяснение:
- Итерируем по счетам студентов: FOR account IN student_accounts.
- Фильтруем счета с отрицательным балансом: account.balance < 0.
- Находим соответствующих студентов: student.student_id == account.student_id.
- Возвращаем имена студентов и их баланс: RETURN { “name”: …, “balance”: … }.
Вариант 39:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, schedule (список идентификаторов занятий).
- classes: class_id, title, time.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран расписание занятий студента Татьяна Фомина (передается в запросе из хост-подсистемы), включая названия и время занятий?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Татьяна Фомина”
FOR classId IN student.schedule
FOR class IN classes
FILTER class.class_id == classId
RETURN {
“title”: class.title,
“time”: class.time
}
Объяснение:
- Находим студента: FILTER student.name == “Татьяна Фомина”.
- Итерируем по идентификаторам занятий из расписания студента: FOR classId IN student.schedule.
- Находим соответствующие занятия: FILTER class.class_id == classId.
- Возвращаем названия и время занятий: RETURN { “title”: …, “time”: … }.
Вариант 40:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name.
- internships: internship_id, company, students_applied (список student_id).
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Определить, подал ли студент Владислав Михайлов (передается в запросе из хост-подсистемы) заявку на стажировку в компанию TechCorp (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
LET studentId = FIRST(
FOR student IN students
FILTER student.name == “Владислав Михайлов”
RETURN student.student_id
)
LET internship = FIRST(
FOR intern IN internships
FILTER intern.company == “TechCorp” AND studentId IN
intern.students_applied
RETURN intern
)
RETURN {
“name”: “Владислав Михайлов”,
“applied_to_TechCorp”: internship != null
}
Объяснение:
- Получаем studentId студента: Используем LET и FIRST для извлечения student_id.
- Проверяем, подал ли заявку на стажировку в TechCorp:
-
- Ищем стажировку в компании TechCorp.
- Проверяем, есть ли studentId в списке students_applied.
- Возвращаем результат:
-
- Если internship != null, значит студент подал заявку.
- RETURN { “name”: …, “applied_to_TechCorp”: … }.
Вариант 41:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, languages (список языков, которыми владеет студент).
- language_exchange: exchange_id, language, partner_required_language.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
найти возможности языкового обмена для студента София Климова (передается в запросе из хост-подсистемы), учитывая языки, которыми она владеет, и которые хочет выучить?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “София Климова”
FOR exchange IN language_exchange
FILTER exchange.partner_required_language IN student.languages
RETURN {
“exchange_id”: exchange.exchange_id,
“language_to_learn”: exchange.language
}
Объяснение:
- Находим студента: FILTER student.name == “София Климова”.
- Итерируем по возможностям языкового обмена: FOR exchange IN language_exchange.
- Проверяем, владеет ли студент языком, который требуется партнёру:
-
- exchange.partner_required_language IN student.languages.
- Возвращаем информацию о возможностях обмена: RETURN { “exchange_id”: …, “language_to_learn”: … }.
Вариант 42:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, enrollment_status.
- financial_aid: aid_id, student_id, amount, year.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
определить, получал ли студент Артур Назаров (передается в запросе из хост-подсистемы) финансовую помощь в год его зачисления?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Артур Назаров”
LET enrollmentYear = DATE_YEAR(student.enrollment_status)
LET aid = FIRST(
FOR fa IN financial_aid
FILTER fa.student_id == student.student_id AND fa.year ==
enrollmentYear
RETURN fa
)
RETURN {
“name”: student.name,
“received_aid_in_enrollment_year”: aid != null
}
Объяснение:
- Находим студента: FILTER student.name == “Артур Назаров”.
- Получаем год зачисления: LET enrollmentYear = DATE_YEAR(student.enrollment_status).
- Ищем запись о финансовой помощи в год зачисления:
-
- FILTER fa.student_id == student.student_id AND fa.year == enrollmentYear.
- Используем FIRST для получения первой записи.
- Возвращаем результат:
-
- Если aid != null, значит студент получал помощь.
- RETURN { “name”: …, “received_aid_in_enrollment_year”: … }.
Вариант 43:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, graduation_year.
- theses: thesis_id, student_id, title, supervisor.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран названия дипломных работ студентов, которые выпускаются в текущем году (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
LET currentYear = DATE_YEAR(DATE_NOW())
FOR student IN students
FILTER student.graduation_year == currentYear
FOR thesis IN theses
FILTER thesis.student_id == student.student_id
RETURN {
“student_name”: student.name,
“thesis_title”: thesis.title
}
Объяснение:
- Получаем текущий год: LET currentYear = DATE_YEAR(DATE_NOW()).
- Итерируем по студентам: FOR student IN students.
- Фильтруем студентов, которые выпускаются в текущем году: student.graduation_year == currentYear.
- Итерируем по дипломным работам: FOR thesis IN theses.
- Находим дипломные работы студентов: thesis.student_id == student.student_id.
- Возвращаем имя студента и название дипломной работы:
-
- RETURN { “student_name”: student.name, “thesis_title”: thesis.title }.
Вариант 44:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, major.
- scholarships: scholarship_id, name, eligible_majors (список специальностей), amount.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран список стипендий, на которые может претендовать студентка Марина Алексеева (передается в запросе из хост-подсистемы), исходя из её специальности?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Марина Алексеева”
FOR scholarship IN scholarships
FILTER student.major IN scholarship.eligible_majors
RETURN {
“scholarship_name”: scholarship.name,
“amount”: scholarship.amount
}
Объяснение:
- Находим студентку: FILTER student.name == “Марина Алексеева”.
- Итерируем по стипендиям: FOR scholarship IN scholarships.
- Проверяем, подходит ли специальность студентки: student.major IN scholarship.eligible_majors.
- Возвращаем название стипендии и её сумму:
-
- RETURN { “scholarship_name”: scholarship.name, “amount”: scholarship.amount }.
Вариант 45:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, completed_courses (список course_id).
- advanced_courses: course_id, title, prerequisites (список course_id).
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран список продвинутых курсов, для которых студент Никита Крылов (передается в запросе из хост системы) выполнил все пререквизиты?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Никита Крылов”
LET completed = student.completed_courses
FOR course IN advanced_courses
FILTER EVERY prereq IN course.prerequisites
SATISFIES prereq IN completed
RETURN course.title
Объяснение:
- Находим студента: FILTER student.name == “Никита Крылов”.
- Получаем список пройденных курсов: LET completed = student.completed_courses.
- Итерируем по продвинутым курсам: FOR course IN advanced_courses.
- Проверяем, выполнены ли все пререквизиты:
-
- Используем EVERY prereq IN course.prerequisites SATISFIES prereq IN completed.
- Возвращаем названия курсов: RETURN course.title.
Вариант 46:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, nationality.
- cultural_events: event_id, name, country, date.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран список культурных мероприятий, связанных со страной происхождения студентки Анна Романова (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Анна Романова”
FOR event IN cultural_events
FILTER event.country == student.nationality
RETURN event.name
Объяснение:
- Находим студентку: FILTER student.name == “Анна Романова”.
- Итерируем по культурным мероприятиям: FOR event IN cultural_events.
- Проверяем, связаны ли мероприятия со страной студентки: event.country == student.nationality.
- Возвращаем названия мероприятий: RETURN event.name.
Вариант 47:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, gpa.
- honor_stependias: stependia_id, name, minimum_gpa.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
определить, может ли студент Алексей Морозов (передается в запросе из хост-подсистемы) получать степендию от спонсоров Phi Beta Kappa, исходя из его среднего балла?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Алексей Морозов”
FOR stependia IN honor_ stependias
FILTER stependia.name == “Phi Beta Kappa”
LET eligible = student.gpa >= stependia.minimum_gpa
RETURN {
“student_name”: student.name,
“eligible”: eligible
}
Объяснение:
- Находим студента: FILTER student.name == “Алексей Морозов”.
- Ищем конкретную степендию: FILTER stependia.name == “Phi Beta Kappa”.
- Проверяем соответствие среднего балла: LET eligible = student.gpa >= stependia.minimum_gpa.
- Возвращаем результат: RETURN { “student_name”: student.name, “eligible”: eligible }.
Вариант 48:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, current_courses (список course_id).
- exams: exam_id, course_id, date, location.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран расписание экзаменов для студентки Екатерина Смирнова (передается в запросе из хост-подсистемы), включая дату и место проведения?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Екатерина Смирнова”
FOR courseId IN student.current_courses
FOR exam IN exams
FILTER exam.course_id == courseId
RETURN {
“course_id”: exam.course_id,
“date”: exam.date,
“location”: exam.location
}
Объяснение:
- Находим студентку: FILTER student.name == “Екатерина Смирнова”.
- Итерируем по текущим курсам студентки: FOR courseId IN student.current_courses.
- Итерируем по экзаменам: FOR exam IN exams.
- Находим экзамены по курсам студентки: exam.course_id == courseId.
- Возвращаем информацию об экзаменах:
-
- RETURN { “course_id”: exam.course_id, “date”: exam.date, “location”: exam.location }.
Вариант 49:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, skills (список навыков).
- project_teams: team_id, project_name, required_skills (список навыков).
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
найти проектные команды, для которых студент Илья Соколов (передается в запросе из хост-подсистемы) обладает всеми необходимыми навыками?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Илья Соколов”
LET studentSkills = student.skills
FOR team IN project_teams
FILTER EVERY skill IN team.required_skills
SATISFIES skill IN studentSkills
RETURN team.project_name
Объяснение:
- Находим студента: FILTER student.name == “Илья Соколов”.
- Получаем навыки студента: LET studentSkills = student.skills.
- Итерируем по проектным командам: FOR team IN project_teams.
- Проверяем, обладает ли студент всеми необходимыми навыками:
-
- EVERY skill IN team.required_skills SATISFIES skill IN studentSkills.
- Возвращаем названия проектов: RETURN team.project_name.
Вариант 50:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, advisor_id.
- faculty: faculty_id, name, department.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран имя научного руководителя студентки Валерии Кузнецовой (передается в запросе из хост системы) и его (руководителя) кафедру?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Валерия Кузнецова”
FOR advisor IN faculty
FILTER advisor.faculty_id == student.advisor_id
RETURN {
“student_name”: student.name,
“advisor_name”: advisor.name,
“department”: advisor.department
}
Объяснение:
- Находим студентку: FILTER student.name == “Валерия Кузнецова”.
- Итерируем по преподавателям: FOR advisor IN faculty.
- Находим научного руководителя студентки: advisor.faculty_id == student.advisor_id.
- Возвращаем информацию о научном руководителе:
-
- RETURN { “student_name”: student.name, “advisor_name”: advisor.name, “department”: advisor.department }.
Вариант 51:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, interests (список интересов).
- workshops: workshop_id, topic, date.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран список семинаров, темы которых совпадают с интересами студента Дмитрий Волков (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Дмитрий Волков”
FOR workshop IN workshops
FILTER workshop.topic IN student.interests
RETURN {
“workshop_topic”: workshop.topic,
“date”: workshop.date
}
Объяснение:
- Находим студента: FILTER student.name == “Дмитрий Волков”.
- Итерируем по семинарам: FOR workshop IN workshops.
- Проверяем, совпадает ли тема семинара с интересами студента: workshop.topic IN student.interests.
- Возвращаем тему семинара и дату:
-
- RETURN { “workshop_topic”: workshop.topic, “date”: workshop.date }.
Вариант 52:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, participation_points.
- rewards: reward_id, name, required_points.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
определить, какие награды может получить студентка Ольга Федорова (передается в запросе из хост-подсистемы), исходя из её количества баллов за участие?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Ольга Федорова”
FOR reward IN rewards
FILTER student.participation_points >= reward.required_points
RETURN reward.name
Объяснение:
- Находим студентку: FILTER student.name == “Ольга Федорова”.
- Итерируем по наградам: FOR reward IN rewards.
- Проверяем, достаточно ли у студентки баллов для награды: student.participation_points >= reward.required_points.
- Возвращаем названия доступных наград: RETURN reward.name.
Вариант 53:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, year_of_study.
- mentors: mentor_id, name, assigned_students (список student_id).
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
найти наставника, который курирует студента Сергей Лебедев(передается в запросе из хост-подсистемы), и получить его имя и количество студентов под его руководством?
Эквивалентный запрос на языке AQL:
FOR mentor IN mentors
FILTER “Сергей Лебедев” IN (
FOR studentId IN mentor.assigned_students
LET student = DOCUMENT(“students”, studentId)
RETURN student.name
)
LET studentCount = LENGTH(mentor.assigned_students)
RETURN {
“mentor_name”: mentor.name,
“student_count”: studentCount
}
Объяснение:
- Итерируем по наставникам: FOR mentor IN mentors.
- Проверяем, курирует ли наставник студента Сергей Лебедев:
-
- Создаём вложенный цикл по mentor.assigned_students.
- Получаем имя каждого студента и проверяем, есть ли среди них Сергей Лебедев.
- Подсчитываем количество студентов под руководством наставника: LET studentCount = LENGTH(mentor.assigned_students).
- Возвращаем имя наставника и количество студентов:
-
- RETURN { “mentor_name”: mentor.name, “student_count”: studentCount }.
Вариант 54:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, email_preferences (список типов рассылок).
- email_campaigns: campaign_id, type, subject, send_date.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
Получить в хост-подсистеме и выдать на экран список тем электронных писем, которые будут отправлены студенту Кирилл Зайцев (передается в запросе из хост-подсистемы), учитывая его предпочтения?
Эквивалентный запрос на языке AQL:
FOR student IN students
FILTER student.name == “Кирилл Зайцев”
FOR campaign IN email_campaigns
FILTER campaign.type IN student.email_preferences
RETURN campaign.subject
Объяснение:
- Находим студента: FILTER student.name == “Кирилл Зайцев”.
- Итерируем по электронным кампаниям: FOR campaign IN email_campaigns.
- Проверяем, соответствует ли тип кампании предпочтениям студента: campaign.type IN student.email_preferences.
- Возвращаем темы писем: RETURN campaign.subject.
Вариант 55:
Сформировать в хост-подсистеме и передать в SPE две коллекции. Описание коллекций:
- students: student_id, name, transportation_mode.
- parking_permits: permit_id, student_id, valid_until.
Все текстовые поля коллекций предварительно индексируются и сохраняются в std::map в хост-подсистеме (например, путем автоинкремента индекса). В SPE передаются только индексы.
Задание:
определить, нужен ли студенту Александр Гришин (передается в запросе из хост-подсистемы) новый парковочный пропуск, если его текущий истекает в этом месяце (передается в запросе из хост-подсистемы)?
Эквивалентный запрос на языке AQL:
LET currentMonth = DATE_FORMAT(DATE_NOW(), “%Y-%m”)
FOR student IN students
FILTER student.name == “Александр Гришин” AND
student.transportation_mode == “Car”
FOR permit IN parking_permits
FILTER permit.student_id == student.student_id
AND DATE_FORMAT(permit.valid_until, “%Y-%m”) == currentMonth
RETURN {
“student_name”: student.name,
“needs_new_permit”: true
}
Объяснение:
- Получаем текущий месяц: LET currentMonth = DATE_FORMAT(DATE_NOW(), “%Y-%m”).
- Находим студента, который ездит на машине: FILTER student.name == “Александр Гришин” AND student.transportation_mode == “Car”.
- Итерируем по парковочным пропускам: FOR permit IN parking_permits.
- Проверяем, истекает ли пропуск в этом месяце:
-
- permit.student_id == student.student_id.
- DATE_FORMAT(permit.valid_until, “%Y-%m”) == currentMonth.
- Возвращаем результат:
-
- RETURN { “student_name”: student.name, “needs_new_permit”: true }.
Если запись не найдена, значит пропуск ещё действителен, и вывод можно не возвращать.
4. Практикум №2. Обработка и визуализация графов в вычислительном комплексе Тераграф
Практикум посвящен освоению принципов представления графов и их обработке с помощью вычислительного комплекса Тераграф. В ходе практикума необходимо ознакомиться с вариантами представления графов в виде объединения структур языка C/C++, изучить и применить на практике примеры решения некоторых задач на графах. По индивидуальному варианту необходимо разработать программу хост-подсистемы и программного ядра sw_kernel, выполняющего обработку и визуализацию графов.
4.1. Конвейер визуализации графов
Визуализация графа — это графическое представление вершин и ребер графа. Визуализация строится на основе исходного графа, но направлена на получение дополнительных атрибутов вершин и ребер: размера, цвета, координат вершин, толщины и геометрии ребер. Помимо этого, в задачи визуализации входит определение масштаба представления визуализации. Для различных по своей природе графов, могут быть более применимы различные варианты визуализации. Таким образом задачи, входящие в последовательность подготовки графа к визуализации, формулируются исходя из эстетических и эвристических критериев. Графы можно визуализировать, используя:
-
2D графическую сцену - наиболее часто применяемый случай, обладающий приемлемой вычислительной сложностью (порядка O(n2 log n));
-
3D графическую сцену - такой вариант позволяет выполнять перемещение камеры наблюдения, что увеличивает возможное количество визуализируемых вершин;
-
Иерархическое представление - граф представляется в виде иерархически вложенных подграфов (уровней), что позволяет более наглядно представить тесно связанные компоненты первоначального графа.
Для визуализации графов было определено несколько показателей качества, позволяющие получить количественные оценки эстетики и удобства графического представления. Алгоритмы раскладки, в большинстве случаев, нацелены на оптимизацию следующих показателей:
-
Меньшее количество пересечений ребер: выравнивание вершин и ребер для получения наименьшего количества пересечений ребер делает визуализацию более понятной и менее запутанной.
-
Минимум наложений вершин и рёбер.
-
Распределение вершин и/или рёбер равномерно.
-
Более тесное расположение смежных вершин.
-
Формирование сообществ вершин из сильно связанных групп вершин.
Для больших графов метод выделения сообществ является предпочтительным, так как позволяет выявить скрытые кластеры вершин, показать центральную вершину сообщества, минимизировать количество внешних связей между сообществами.
Таким образом визуализация графов представляет собой многостадийный алгоритмический процесс. Кратко стадии процесса визуализации представлены на следующем рисунке.
Кратко поясним представленный процесс:
-
Исходный граф может быть представлен различными способами, повышающими эффективность алгоритмов их обработки. Такой граф служит исходными данными для задачи визуализации.
-
На первом этапе формируется граф визуализации, содержащий для каждой вершины и ребра дополнительные атрибуты, значение которых и требуется определить в ходе этого процесса. Могут быть использованы дополнительные атрибуты, позволяющие выявить свойства вершин и более наглядно визуализировать структуру графа. Частым случаем является определение свойства центральности для вершин и ребер. В конечном итоге, для каждой вершины требуется хранить еще и ее координаты (x,y), цвет (color), радиус окружности для представления на сцене визуализации (size).
-
Далее выполняется ряд алгоритмов для определения свойств вершин и ребер. В практикуме используется алгоритм Дейкстры для поиска кратчайших путей, на основе которого рассчитывается центральность вершин.
-
На следующем этапе происходит выделение групп вершин, входящих в так называемые сообществ: связность вершин внутри сообщества превосходит связность за его пределами. Примеры алгоритмов поиска сообществ представлены в примерах Практикума: Пример 5 и Пример 6.
-
Для каждого сообщества определяется область экрана для его размещения (алгоритмы глобального размещения, global placement).
-
Далее выполняется размещение вершин сообщества внутри каждой выделенной области. На данном этапе применяются алгоритмы, позволяющие представить связи небольшой группы вершин. Частым случаем является применение алгоритмов, основанных на имитации сил притяжения и отталкивания. сформированных на основе информации о вершинах и ребрах.
-
На заключительном этапе происходит передача готового графа визуализации в библиотеку. осуществляющую отрисовку сцены визуализации. В практикуме используется библиотека bokeh.
4.2. Представление информационных моделей алгоритма в виде структур данных
Представление алгоритма обработки множеств и графов в виде операций дискретной математики DISC требует принятия решений о количестве применяемых структур, а также о соответствии информации, используемой в алгоритме, ключам и данным структур. Указанный переход представляет процесс, аналогичный подготовке информации к хранению в базе данных типа “ключ-значение”. В частности, определяются составные ключевые поля, собранные в виде конкатенации данных. В качестве примеров приведем варианты представления графа.
4.2.1. Представление графа G(V,E) списком смежных вершин
Пусть в алгоритме требуется вести обход графа, например, методом поиска в глубину. Тогда основной операцией будет поиск вершин v ∈ Adj[u], инцидентных указанной, и последующий переход к обработке всех связанных вершин. Но, поскольку степень вершин различна, требуется также хранить количество исходящих ребер count. Поле G.KEY хранит номера вершин u и порядковый номер ребра. Поле данных G.VALUE хранит номер инцидентной вершины v и вес ребра w, как показано в Таблице:
Таблица - Пример представления графа G(V,E) списком смежных вершин (G.KEY[u,i], G.VALUE[v,w])
G.KEY | G.VALUE |
---|---|
u,0 | count |
u,1 | v1,w |
… | … |
u,count | vcount,w |
Заметим, что индексная запись (u,0) - (count) может быть перенесена в любое место диапазона индексов, например в последнее (максимальное) значение индекса: (u,-1) - (count).
Представим указанную структуру композитных ключей в виде структур языка С/C++, используя макросы объединения, описанные в разделе 2.6.3. Представление структур данных в виде ключей и значений.
//Структуры данных
#define G 1 //Граф
//Объявление индексов
#define ADJ_C_BITS 32 //количество бит для хранения индекса смежной вершины графа
const unsigned int IDX_MAX=(1ull<<ADJ_C_BITS)-1; //максимальная смежность
#define PTH_IDX IDX_MAX //номер индексной записи о вершине графа
////////////////////////////////////////////////////
// Структура графа тип 1: (G.KEY[u,i], G.VALUE[v,w])
////////////////////////////////////////////////////
//регистр ключа для вершины
/* Struktura 1 - G - описание графа
* key[63..32] - номер вершины
* key[31..0] - индекс записи о вершине (0,1..adj_u-1)
*/
STRUCT(Key){ //Data structure for graph operations
unsigned int index:32; //Поле 1: индекс записи
unsigned int u:32; //Поле 0: номер вершины
} ;
//регистр значения индексной записи для вершины (с индексом PTH_IDX)
/* key[31..0] = PTH_IDX
* key[63..32] = номер вершины u
*/
STRUCT(Attributes){ //Data structure for graph operations
unsigned int count:32; //Поле 1: количество записей
unsigned int any_atrs:32;//Поле 0: дополнительные атрибуты
} ;
//регистр значения для записей о смежных вершинах
/*
* key[31..0] = 0..PTH_IDX-1
* key[63..32] = номер вершины u
* data[31..0] - w[u,v] вес ребра
* atr[63..48] - номер смежной вершины v
*/
STRUCT(Edge) { //Data structure for graph operations
unsigned int v:32; //Поле 1: номер смежной вершины
unsigned int w:32; //Поле 0: вес ребра (u,v)
} ;
Для упрощения синтаксиса описания структур и обращения к их полям мы используем шаблоны, описанные в файле compose_keys.hxx. Макрос объявления структуры выглядит следующим образом:
#define STRUCT(name) struct name: _base_uint64_t<name>
. Шаблон структуры _base_uint64_tпозволяет описать 64 битное значение как беззнаковое целое стандартного типа uint64_t и разместить его в регистрах процессора, а не в оперативной памяти. Таким образом достигается большее быстродействие.
4.2.2. Представление графа G(V,E) списком инцидентных ребер
Если в алгоритме необходимо выполнить поиск ребер, соединяющих вершины (u,v), граф может быть представлен другим образом. Поле G.KEY в этом случае составляется из номеров вершин u и v, а поле данных G.VALUE хранит вес ребра w.
Таблица - Пример представления графа G(V,E) списком инцидентных ребер (G.KEY[u,v], G.VALUE[w])
G.KEY | G.VALUE |
---|---|
u,v | w |
Соответствующее описание структуры графа на языке С показано ниже:
//Структуры данных
#define G 1 //Граф
//////////////////////////////////////////////////
// Структура графа тип 2: (G.KEY[u,v], G.VALUE[w])
//////////////////////////////////////////////////
//регистр ключа для вершины
/* Struktura 1 - G - описание графа
* key[63..32] - номер вершины u
* key[31..0] - номер вершины v
*/
STRUCT(Key) { //Data structure for graph operations
unsigned int v:32; //Поле 1: номер вершины v
unsigned int u:32; //Поле 0: номер вершины u
};
//регистр значения записи для ребра (u,v)
/* key[31..0] = дополнительные атрибуты (не использованы)
* key[63..32] = номер вершины u
*/
STRUCT(Attributes) { //Data structure for graph operations
unsigned int w:32; //Поле 1: вес ребра (u,v)
unsigned int any_attrs:32;//Поле 0: дополнительные атрибуты
};
4.2.3. Представление графа G(V,E) упорядоченным списком инцидентных ребер
Часто требуется хранить граф таким образом, чтобы множество ребер было упорядочено по их весу: минимальный ключ должен принадлежать ребру с наименьшим весом. Так как связность в общем случае не является уникальной и в графе могут присутствовать несколько ребер с одинаковым весом, следует использовать более сложный составной ключ. В старшей части ключа должен храниться вес ребра w, а в младшей будут храниться номера вершин (u,v). Т.е. поле G.KEY=(w,u,v). Поле G.VALUE может оставаться пустым, так как необходимая информация об инцидентности вершин и ребер имеется в составном ключе. Однако, в алгоритме может возникнуть необходимость хранить дополнительные атрибуты ребра (флаги, переменные и пр.).
Таблица - Пример представления графа G(V,E) упорядоченным списком инцидентных ребер (G.KEY[w,u,v], G.VALUE[])
G.KEY | G.VALUE |
---|---|
w,u,v | дополнительные атрибуты |
Соответствующее описание структуры графа на языке С показано ниже:
//Структуры данных
#define G 1 //Граф
///////////////////////////////////////////////////
// Структура графа тип 3: (G.KEY[w,u,v], G.VALUE[])
///////////////////////////////////////////////////
//регистр ключа для вершины
/* Struktura 1 - G - описание графа
* key[63..48] - вес ребра (u,v)
* key[47..24] - номер вершины u
* key[23..0] - номер вершины v
*/
STRUCT(Key) { //Data structure for graph operations
unsigned int v:24; //Поле 2: номер вершины v
unsigned int u:24; //Поле 1: номер вершины u
unsigned int w:16; //Поле 0: вес ребра (u,v)
};
//регистр значения записи для ребра (u,v)
/* key[63..0] = дополнительные атрибуты
*/
STRUCT(Attributes) { //Data structure for graph operations
unsigned int any_atr1:32;//Поле 1: дополнительные атрибуты
unsigned int any_atr0:32;//Поле 0: дополнительные атрибуты
};
В зависимости от выполняемых в алгоритме действий возможно использование как одного варианта представления, так и одновременно несколько вариантов.
4.3. Использование библиотеки шаблонов для обработки графов
Для реализация алгоритмов обработки графов необходимо представить операции над множествами (в том числе, множествами вершин и ребер графа) в виде набора команд дискретной математики DISC. Все команды обработки структур данных изменяют регистр статуса, по которому можно определить, было ли выполнение команды успешным (Регистр LNH_STATE, бит SPU_ERROR_FLAG). Результаты, влияющие на работу программы, должны быть учтены в общем алгоритме. После завершения основания команд, основанных на поиске (SEARCH, DELETE, MAX, MIN, NEXT, PREV, NSM, NGR) в очередь данных попадают ключ и значение найденных записей (KEY, VALUE), которые могут быть использованы в алгоритме программного ядра CPE riscv32. Для команд И-ИЛИ-НЕ (пересечение,объединение,дополнение) передаются операнды номеров структур (R,A,B). Операнд R указывает на номер структуры, в которой будет сохранен результат. Структуры A и B используются в И-ИЛИ-НЕ операциях и срезах в качестве исходных.
Таблица - Выполнение базовых операций над структурами данных.
Действие | Псевдокод вызова команды DISC |
---|---|
Поиск по ключу | (KEY,VALUE) = SEARCH(G, Key) |
Поиск минимального ключа | (KEY,VALUE) = MIN(G, Key) |
Поиск максимального ключа | (KEY,VALUE) = MAX(G, Key) |
Поиск ближайшего меньшего | (KEY,VALUE) = NSM(G, Key) |
Поиск ближайшего большего | (KEY,VALUE) = NGR(G, Key) |
Поиск следующего | (KEY,VALUE) = NEXT(G, Key) |
Поиск предыдущего | (KEY,VALUE) = PREV(G, Key) |
Добавление | INSERT(G, Key, Data) |
Удаление | DELETE(G, Key) |
Объединение структур | OR(Result, Source_A, Source_B) |
Дополнение структур | NOT(Result, Source_A, Source_B) |
Пересечение структур | AND(Result, Source_A, Source_B) |
Срез структуры выше ключа | GR(Result, Source_A, Key) |
Срез структуры не ниже ключа | GREQ(Result, Source_A, Key) |
Двойной срез структуры | GRLS(Result, Source_A, Key_A, Key_B) |
После выполнения команд в структуре lnh_core формируется результат в виде ключа,значения и статуса, которые доступны в коде sw_kernel по именам полей:
- lnh_core.result.key - поле ключа (64 бит);
- lnh_core.result.value - поле значения (64 бит);
- lnh_core.result.status - статус выполнения команды (64 бит).
Для того, чтобы извлечь композитные поля структуры, может быть использован следующий вариант приведения полей lnh_core к шаблону структур:
-
Чтение поля из композитного ключа выполняется с помощью следующего шаблона:
get_result_key<ИМЯ_СТРУКТУРЫ>().ИМЯ_ПОЛЯ;
. Например:weight = get_result_key<Graph::Key>().u;
-
Чтение поля из композитного значения выполняется аналогично с помощью следующего шаблона:
get_result_value<ИМЯ_СТРУКТУРЫ>().ИМЯ_ПОЛЯ;
. Например:var = get_result_value<Graph::Edge>().w;
-
Запись композитных полей в структуру осуществляется с помощью стандартного шаблона инициализации структуры:
ИМЯ_СТРУКТУРЫ{.ИМЯ_ПОЛЯ1=ЗНАЧЕНИЕ, .ИМЯ_ПОЛЯ2=ЗНАЧЕНИЕ}
, например:Graph::Key{.index=BASE_IDX, .u=u}
Для упрощения разработки алгоритмов на графах, а также контроля корректности синтаксических конструкций работы с ядром lnh64 была разработана специализированное программное ядро, расширяющее функциональность библиотеки Lnh64 L0. По данной ссылке доступны дополнительные заголовочные и cpp файлы, в которых собраны шаблоны описаний типовых структур графа и различных сервисных структур (очередей, деревьев и т.д.), необходимых для обработки графов и их визуалиации. Описание структур приведено в файле graph_iterators.hxx
Описание каждой из перечисленных ниже структур состоит из следующих секций:
-
Описание константного значения номера структуры struct_number при хранении в ядре lnh64. Данный параметр передается при инициализации структуры и остается неизменным в дальнейшем, вплоть до удаления информации из памяти lnh64/ Допускаются указывать номера структур, не занятые другой информацией, в диапазоне 1..7.
-
Описание индексов и констант, используемых при описании ключей. Целесообразно использовать индексы в конце диапазона рабочих значений.
-
Описание множества шаблонов ключей и значений.
-
Связывание типов ключей и значений с помощью макросов DEFINE_DEFAULT_KEYVAL(<КЛЮЧ>,<ЗНАЧЕНИЕ>) и DEFINE_KEYVAL(<КЛЮЧ>,<ЗНАЧЕНИЕ>). Макросы служат для создания синтаксических ограничений, которые запрещают программисту использовать иные сочетания. Например, если указан макрос DEFINE_DEFAULT_KEYVAL(Key_type,Value_type), то результатов выполнения команды выборки минимального ключа множеств будет структура типа Key_type.ЗНАЧЕНИЕ>КЛЮЧ>ЗНАЧЕНИЕ>КЛЮЧ>
-
Область объявления итераторов над структурой.
//Объявление номера структуры для хранения графа в lnh64
#define G 1 //Граф
...
//Инициализация структуры и запись номера структуры lnh64
constexpr Graph G1(G);
...
// Получить ключ записи с минимальным значением ключа
auto key = G1.get_first().key();
// key имеет тип, указанный в качестве DEFINE_DEFAULT_KEYVAL для ключа
Ниже приведены описания структур и шаблонов ключей и значений, указанных в них. Также приведем перечень итераторов для каждой из структур.
Таблица - Шаблон структуры для представления графа.
Структура/-Шаблон/* Поле | Назначение |
---|---|
Graph | Представление графа G(V,E) списком смежных вершин |
Key | Поле ключа для записи о ребре (используется по умолчанию) |
index | Индекс записи (0..index_max-2) |
u | Номер вершины в графе |
Path_key | Поле ключа для записи о кратчайшем пути и центральности |
index | Индекс записи = index_max |
u | Номер вершины в графе |
Base_key | Поле ключа для записи атрибутов вершины |
index | Индекс записи = index_max-1 |
u | Номер вершины в графе |
Viz_key | Поле ключа для записи об атрибутах визуализации |
index | Индекс записи = index_max-2 |
u | Номер вершины в графе |
Edge | Поле значения для записи о ребре (используется по умолчанию) |
v | Номер смежной вершины |
w | Вес ребра |
attr | Дополнительные атрибуты ребра |
Shortest_path | Поле значения для записи о кратчайшем пути до вершины |
du | Кратчайший путь от данной вершины до стартовой вершины |
btwc | Центральность вершины |
Attributes | Поле значения для записи атрибутов |
pu | Номер предшествующей вершины в кратчайшем пути |
eQ | Флаг хранения вершины в очереди (используется в алгоритме Дейкстры) |
adj_c | Количество ребер вершины |
vAttributes | Поле значения для записи атрибутов визуализации |
x | Координата x для визуализации |
y | Координата y для визуализации |
size | Размер окружности для визуализации вершины |
color | Цвет окружности для визуализации вершины |
Для структуры графа определены следующие синтаксические ограничения:
//Обязательная типизация
DEFINE_DEFAULT_KEYVAL(Key, Edge)
//Дополнительная типизация
DEFINE_KEYVAL(Base_key, Attributes)
DEFINE_KEYVAL(Path_key, Shortest_path)
DEFINE_KEYVAL(Viz_key, vAttributes)
Для структуры графа определены итераторы:
-
Итератор вершин графа.
-
Итератор ребер для вершины графа.
Пример использования итератора вершин графа:
//Для каждой вершины графа
for (unsigned int v : virtex_range{G1}) {
...
}
Пример использования итератора ребер для вершины графа:
for (auto [adj, wu, attr] : edge_range(G1, v)) {
...
}
Например, можно использовать следующий пример для определения количества ребер графа и вычисления среднего веса ребра:
uint32_t weight_sum=0; //сумма весов ребер
uint32_t edge_count=0; //количество ребер
//Обход всех вершин графа и вычисление среднего веса ребра
for (auto com_u : virtex_range{G1}) {
//Для каждого ребра определить его вес
for (auto [com_v, wu, attr] : edge_range(G1, com_u)) {
weight_sum += wu;
edge_count++;
}
}
uint32_t weight_average = weight_sum/edge_count;
В ряде алгоритмов требуется использовать очередь вершин. Для этого в библиотеке создана следующая структура:
Таблица - Шаблон структуры для представления очереди вершин графа.
Структура / Шаблон / Поле | Назначение |
---|---|
Queue | Структура очереди |
Record | Поле ключа для записи очереди |
id | Номер вершины графа |
du | Кратчайший путь (используется в алгоритме Дейкстры) |
Attributes | Поле значения для атрибутов записи |
attr | Атрибуты записи |
Синтаксические ограничения на типы для очереди следующие:
//Обязательная типизация
DEFINE_DEFAULT_KEYVAL(Record, Attributes)
Для очереди реализован итераторы, позволяющие выполнить обход в прямом (в сторону увеличения значений ключа), так и о обратном порядке. Оба итератора выдают значения ключа для записи, а также удаляют найденную запись из очереди (значение читается только один раз).
! Обход очереди может осуществляться как в прямом порядке (увеличение ключей), так и в обратном (уменьшение ключей). При этом возможны два варианта перехода к следующей записи очереди: поиск следующего ключа по порядку, или удаление найденного элемента из очереди.
Приведем все четыре варианта обхода очереди.
В некоторых случаях требуется модифицировать очередь и выбирать записи из хвоста или головы очереди. Для этого можно использовать методы структуры очереди begin
или rbegin
(головная, первая запись очереди), rend
(хвост, последняя вершина очереди), а также функцию удаления элемента из очереди erase
. Пример использования итератора обхода очереди в прямом порядке с удалением записи из очереди:
//обход всех вершины графа
while(auto q_it = Q.begin()) { //Выбирается первая запись очереди
Q.erase(q_it);
auto [u, du] = *q_it;
//Получение суммарной длины ребер
du_sum += du;
...
}
Пример использования итератора обхода очереди в обратном порядке с удалением (итератор rbegin):
//обход всех вершины графа
while(auto q_it = Q.rbegin()) { //Выбирается поседняя запись очереди
Q.erase(q_it);
auto [u, du] = *q_it;
//Получение суммарной длины ребер
du_sum += du;
...
}
Пример использования итератора обхода очереди в прямом порядке без изменения очереди:
//обход всех вершины графа
for(auto [u, du]:Q) { //Выбирается следующая запись очереди
//Получение суммарной длины ребер
du_sum += du;
}
Пример использования итератора обхода очереди в обратном порядке без изменения очереди:
//обход всех вершины графа
for(auto [u, du]:reverse{Q}) { //Выбирается следующая запись очереди
//Получение суммарной длины ребер
du_sum += du;
...
}
Для визуализации графов часто требуется объединить вершины в сообщества по свойству модулярности (смотри алгоритм в разделе “4.4.3.1. Выделение сообществ на основе алгоритма MultiLevel”). Следующие структуры используются для построения сообществ. а также для размещения сообществ вершин в прямоугольных областях экрана.
Таблица - Шаблон структуры для представления сообществ.
Структура / Шаблон / Поле | Назначение |
---|---|
Community | Структура сообществ по модулярности (для визуализации) |
Key | Поле ключа для сообщества |
adj | Количество ребер сообщества |
id | Номер сообщества |
Value | Поле зачения для атрибутов сообщества |
first_virtex | Номер начальной вершины графа, принадлежащей сообществу |
last_virtex | Номер конечной вершины графа, принадлежащей сообществу |
id | Номер сообщества |
Синтаксические ограничения на типы для очереди следующие:
//Обязательная типизация
DEFINE_DEFAULT_KEYVAL(Key, Value)
Для структуры сообществ определены итераторы:
-
Итератор обхода сообществ
-
Итератор обхода вершин графа, входящий в сообщество.
Пример кода для обхода всех сообществ, хранимых в структуре cG1:
for (auto cmty : community_range{cG1}) {
...
}
Для обхода всех вершин графа одного сообщества может быть использован следующий код:
for (auto u : community_member_range(cG1, cmty)) {
...
}
Следующая структура используется для раскладки сообществ в прямоугольной области. ДЛя этого строится дерево сообществ, после чего экран делится для левого и правого поддеревьев пропорционально количеству вершин в них. Таким образом получается иерархически и итерационно разместить тесно связанные сообщества ближе друг к другу.
Таблица - Шаблон структуры для представления дерева сообществ.
Структура / Шаблон / Поле | Назначение |
---|---|
cTree | Структура дерева сообществ по модулярности (для визуализации) |
Key | Поле ключа для сообщества |
adj | Количество ребер сообщества |
com_id | Номер сообщества |
Vcount_key | Поле ключа для записи атрибутов сообщества |
index | Индекс записи об атрибутах сообщества, index = index_max |
com_id | Номер сообщества |
XY_key | Поле ключа для записи границ визуализации сообщества |
index | Индекс записи о границах поля визуализации, index = index_max-1 |
com_id | Номер сообщества |
Value | Поле зачения для атрибутов сообщества |
left_leaf | Номер сообщества левого листа в дереве |
right_leaf | Номер сообщества правого листа в дереве |
Vcount_value | Поле значения для записи атрибутов сообщества |
v_count | Количество вершин графа, входящих в сообщество |
is_leaf | Флаг, указывающий на запись дерева типа “лист” |
XY_value | Поле значения для записи атрибутов визуализации сообщества |
x0 | Координата x левого нижнего угла прямоугольной области визуализации |
y0 | Координата y левого нижнего угла прямоугольной области визуализации |
x1 | Координата x правого верхнего угла прямоугольной области визуализации |
y1 | Координата y правого верхнего угла прямоугольной области визуализации |
Для дерева сообществ определена следующая синтаксическая типизация:
//Обязательная типизация
DEFINE_DEFAULT_KEYVAL(Key, Value)
//Дополнительная типизация
DEFINE_KEYVAL(Vcount_key, Vcount_value)
DEFINE_KEYVAL(XY_key, XY_value)
Итератор для структуры дерева сообществ позволяет выполнить обход всех сообществ:
for (auto community : ctree_range(cT1)) {
...
}
Следующие две структур представляют собой прямую и обратные очереди, в которых записи упорядочены по значению изменения модулярности (демодулярности), возникающей при объединении двух сообществ. Таким образом на каждом шаге алгоритма можно выполнить поиск двух сообществ, в наибольше степени связанных между собой в сравнении со средним значением связности сообществ аналогичной размерности.
Структур mQueue упорядочивает записи по значению модулярности, в то время как в атрибутах записей хранит номера сообществ. Структура iQueue, наоборот, представляет записи по ключам сообществ, а в поле значения хранит модулярность (обратный формат записи).
Таблица - Шаблон структуры для представления очереди демодулярности (изменения модулярности при объединении сообществ u и v).
Структура / Шаблон / Поле | Назначение |
---|---|
mQueue | Структура очереди демодулярности (для визуализации) |
Modularity | Поле ключа для хранения записи о номере сообщества |
index | Номер записи (всегда = 1) |
id | Индекс записи с значением дельтамодулярности delta_mod |
delta_mod | Значение демодулярности |
Modularity_ext | Поле ключа дополнительной записи атрибутов |
index | Номер записи (всегда = 0) |
id | Индекс записи с значением дельтамодулярности delta_mod |
delta_mod | Значение демодулярности |
Communities | Поле зачения для атрибутов сообщества |
com_u | Номер сообщества u |
com_v | Номер сообщества v |
Attributes | Поле ключа для записи границ визуализации сообщества |
w_u_v | Количество ребер, связывающих сообщества u и v |
Для очереди модулярности заданы следующие соответствия типов:
//Дополнительная типизация
DEFINE_DEFAULT_KEYVAL(Modularity, Communities)
//Обязательная типизация
DEFINE_KEYVAL(Modularity_ext, Attributes)
Таблица - Шаблон структуры для представления обратной очереди демодулярности.
Структура / Шаблон / Поле | Назначение |
---|---|
iQueue | Структура обратной очереди демодулярности (для визуализации) |
Communities | Поле ключа для атрибутов сообщества (обратная запись) |
com_u | Номер сообщества u |
com_v | Номер сообщества v |
Modularity | Поле значения для хранения записи о номере сообщества |
index | Номер записи (всегда = 1) |
id | Индекс записи с значением дельтамодулярности delta_mod |
delta_mod | Значение демодулярности |
Для структуры обратной очереди задано ограничение по умолчанию:
//Обязательная типизация
DEFINE_DEFAULT_KEYVAL(Communities, Modularity)
Для очередей модулярности и демоделярности задан единый итератор, позволяющий обойти все элементы очереди модулярности в убывающем порядке демодулярности. Удаление записи о сообществе из одной очереди автоматически приводит к удалению соответствующей записи и во второй очереди. При этом итаратором рассматривается только те записи, для которых демодулярность положительная или нулевая. Итератор принимает в качестве параметров номера структур прямой и обратной очереди демодулярности
// Инициализация итератора
mqueue_range mqr{mQ, iQ};
//Основной цикл
while (auto mq_it = mqr.rbegin()) {
//u,v - номера объединяемых сообществ
//w_u_v - атрибут связности u и v
auto [com_v, com_u, com_u_index_val, com_u_delta_mod, w_u_v] = *mq_it;
//Удалить запись о модулярности связи сообществ u<->v
mqr.erase(mq_it);
...
}
Для обратной обратной очереди демодулярности реализован дополнительный итератор, позволяющий выполнить обход всех сообществ, связанных с указанным сообществом v:
for (auto mod_record : iqueue_range(iQ1,v)) {
...
}
Далее все примеры будут использовать указанные структуры и их итераторы.
4.4. Примеры реализации алгоритмов на графах
4.4.1. Алгоритм Дейкстры поиска кратчайшего пути
Рассмотрим реализацию алгоритма Дейкстры для поиска кратчайших путей на графе. Этот алгоритм используется, в частности, для сетевой маршрутизации и поиска кратчайших маршрутов в навигационных системах.
Задача формулируется следующим образом. Для взвешенного ориентированного графа G(V,E) без петель и дуг отрицательного веса найти кратчайшие пути от некоторой вершины до всех остальных. Пусть: w[u][v] - вес ребра, соединяющего вершину u и v; Adj[u] — множество вершин, смежных с u; s — исходная вершина; Q — множество вершин, которые осталось рассмотреть для поиска кратчайших путей; d[u] — расстояние от вершины s до вершины u; p[u] — вершина, предшествующая вершине u в кратчайшем пути от s до u.
В начальный момент времени множество Q состоит из всех вершин графа: Q=V. Алгоритм предполагает на каждом шаге поиск в множестве Q вершины u с наименьшим значением d[u], ее удаление из Q, а также вычислению значений d[v] для всех связанных с u вершин, входящих в Q. Если полученная длина пути короче ранее найденной, то она модифицируется, и сохраняется новый маршрут p[v] через вершину u. В итоге, когда будут просмотрены все вершины и Q останется пуст, в p[u] окажется кратчайший маршрут, а в d[u] — его длина. Псевдокод алгоритма представлен ниже:
Дейкстра(s)
Q = V
d[s] = 0
p[s] = 0
ЦИКЛ ПОКА
Для всех u ∈ V, u /= s
d[u] = ∞;
ВСЕ ЦИКЛ ПОКА
ЦИКЛ ПОКА Q /= ∅
ПОИСК (u ∈ Q с минимальным значением d[u])
Q=Q \ u;
ЦИКЛ ПОКА
Для всех вершин v ∈ Adj[u]
ЕСЛИ ( (v ∈ Q) и (d[v] > d[u] + w[u][v]) ) ТО
d[v] = d[u] + w[u][v];
p[v] = u;
ВСЕ ЕСЛИ
ВСЕ ЦИКЛ ПОКА
ВСЕ ЦИКЛ ПОКА
В алгоритме необходимо реализовать структуры данных для хранения графа G и очередь из вершин Q по порядку длины путей d[u] до стартовой вершины. Как говорилось ранее, для вычислительной системы с набором команд дискретной математики требуется представить G и Q в виде структур с 64x битными ключами и значениями.
Примем следующие обозначения: S.KEY(k1,..,kn) является составным ключом поиска в некоторой структуре S, состоящим из полей k1,..,kn; S.VALUE(d1,…,dn) являются данными для структуры S, состоящими из полей d1,…,dn. Доступ к конкретным полям данных может быть показан в псевдокоде алгоритма с помощью модификатора, т.е. как S.VALUE.d1. Например, изменение поля x в составном поле данных указывается как S.VALUE.x=10.
Множество Q используется для поиска вершины с наименьшим показателем d[u]. Т.к. значение d[u] для разных вершин в структуре Q может быть одинаковым, то d[u] не может являться ключом. В качестве ключа может быть выбрана пара значений Q.KEY=(d[u],u). Это обеспечивает не только поиск вершины с минимальным d[u], но и выбор вершины с наименьшим номером из нескольких возможных вариантов. Поле данных структуры Q не используется: Q.VALUE=(0).
Исходный граф G применяется в алгоритме для определения списка смежных вершин и другой информации, соответствующей каждой вершине. Ключом поиска в структуре G будет являться уникальный номер вершины в графе G: G.KEY=(u). Данными для структуры G является множество смежных вершин Adj[u], массив длин путей для них w[u], найденное кратчайшее расстояние d[u], маршрут p[u], а также бит принадлежности вершины к множеству Q: т.е. G.VALUE=(Adj[u],w[u],d[u],p[u],u∈Q). Пояснить использование этих структур можно на примере поиска кратчайших путей для графа, представленного на рисунке:
На первом шаге алгоритма из структуры Q выбирается минимальный ключ Q.KEY=(0,1) и по нему определяется код u, соответствующий вершине s=1. Для этой вершины в структуре G выбирается строка и определяются поля Adj[u],w[u],d[u],p[u],u∈Q.
Найденная строка исключается из структуры Q по известному ключу Q.KEY=(d[u],u). Далее, для каждой вершины v из множества Adj[u] по структуре G определяется принадлежность к множеству Q (параметр u∈Q). Если он равен 1, то проверяется условие d[v]=d[u]+w[u][v], где w[u][v] — один из элементов множества w[u], соответствующий ребру между u и v.
Цикл повторяется до полного опустошения структуры Q. В результате будут определены кратчайшие пути и их длины для всех вершин. Состояние структур после выполнения всех итераций показано на следующем рисунке.
Указанные на рисунке структуры могут быть объявлены следующим образом:
struct Graph {
using virtex_t = uint32_t;
int struct_number;
constexpr Graph(int struct_number) : struct_number(struct_number) {}
static const uint32_t adj_c_bits = 32;
static const uint32_t idx_max = (1ull << adj_c_bits) - 1;
static const uint32_t pth_idx = idx_max;
static const uint32_t base_idx = idx_max - 1;
static const uint32_t viz_idx = idx_max - 2;
//регистр ключа для вершины:
/* STRUCT(u_key) - G - описание графа
* key[63..32] - номер вершины
* key[31..0] - индекс записи о вершине (0,1..adj_u)
*/
STRUCT(Key) {
unsigned int index: 32;
virtex_t u: 32;
};
STRUCT(Path_key) {
unsigned int index: 32 = pth_idx;
virtex_t u: 32;
};
STRUCT(Base_key) {
unsigned int index: 32 = base_idx;
virtex_t u: 32;
};
//граф визуализации
STRUCT(Viz_key) {
unsigned int index: 32 = viz_idx;
virtex_t u: 32;
};
//регистр значения для записей о смежных вершинах:
STRUCT(Edge) {
virtex_t v: 32;
short int w: 16;
short int attr: 16;
};
//регистр значения индексной записи для вершины (с индексом PTH_IDX):
STRUCT(Shortest_path) {
virtex_t du: 32;
unsigned int btwc: 32;
};
//регистр значения атрибутов для вершины с индексом BASE_IDX: STRUCT(u_attributes)
STRUCT(Attributes) {
unsigned int pu: 32;
bool eQ: 8;
int non: 8 = 0;
short int adj_c: 16;
};
//регистр значения для записи атрибутов визуализации вершинах
STRUCT(vAttributes) { //Data structure for graph operations
unsigned short int x: 16; //Поле 1: координата x [0..64K]
unsigned short int y: 16; //Поле 2: координата y [0..64K]
unsigned short int size: 8; //Поле 3: размер [0..255]
unsigned int color: 24; //Поле 4: цвет [0x000000..0xFFFFFF]
};
//Обязательная типизация
DEFINE_DEFAULT_KEYVAL(Key, Edge)
//Дополнительная типизация
DEFINE_KEYVAL(Base_key, Attributes)
DEFINE_KEYVAL(Path_key, Shortest_path)
DEFINE_KEYVAL(Viz_key, vAttributes)
};
Далее рассмотрим код, реализующий алгоритм Дейкстры в программном ядре CPE.
void dijkstra() {
//получить начальную вершину графа из MQ
uint32_t start_virtex = mq_receive();
//получить конечную вершину графа из MQ
uint32_t stop_virtex = mq_receive();
//Очистить очередь Q
lnh_del_str_async(Q);
// Вставляем начальную вершину в Q с нулевым кратчайшим путем
lnh_ins_async(Q,INLINE(q_record,{.u=start_virtex,.du=0}),0);
//Получите btwc (центральность), чтобы сохранить его снова
lnh_search(G,INLINE(u_key,{.index=PTH_IDX,.u=start_virtex}));
btwc = (*(u_index*)&lnh_core.result.value).__struct.btwc;
// Сохраняем du для запуска virtex
lnh_ins_async(G,INLINE(u_key,{.index=PTH_IDX,.u=start_virtex}),
INLINE(u_index,{.du=0,.btwc=btwc}));
// Перебрать все вершины в очереди Q
while (lnh_get_first(Q)) {
u = (*(q_record*)&lnh_core.result.key).__struct.u;
du = (*(q_record*)&lnh_core.result.key).__struct.du;
//Удалит вершину из Q
lnh_del_async(Q,lnh_core.result.key);
lnh_search(G,INLINE(u_key,{.index=BASE_IDX, .u=u}));
pu = (*(u_attributes*)&lnh_core.result.value).__struct.pu;
eQ = (*(u_attributes*)&lnh_core.result.value).__struct.eQ;
adj_c = (*(u_attributes*)&lnh_core.result.value).__struct.adj_c;
// Очистить флаг eQ
lnh_ins_async(G,lnh_core.result.key,
INLINE(u_attributes,{.pu=pu, .eQ=false, .non=0, .adj_c=adj_c}));
//Для каждой вершины Adj
for (i=0;i<adj_c;i++) {
//Получить Adj[i]
lnh_search(G,INLINE(u_key,{.index=i,.u=u}));
wu = (*(edge*)&lnh_core.result.value).__struct.w;
adj = (*(edge*)&lnh_core.result.value).__struct.v;
//Получить информацию о смежных вершинах
lnh_search(G,INLINE(u_key,{.index=BASE_IDX,.u=adj}));
eQc=(*(u_attributes*)&lnh_core.result.value).__struct.eQ;
count=(*(u_attributes*)&lnh_core.result.value).__struct.adj_c;
lnh_search(G,INLINE(u_key,{.index=PTH_IDX,.u=adj}));
dv=(*(u_index*)&lnh_core.result.value).__struct.du;
btwc=(*(u_index*)&lnh_core.result.value).__struct.btwc;
//Если изменился кратчайший путь
if (dv>(du+wu)) {
if(eQc) {
if (dv!=INF) //если не петля, отправить вершину в Q
lnh_del_async(Q,INLINE(q_record,{.u=adj, .du=dv}));
lnh_ins_async(Q,INLINE(q_record,{.u=adj, .du=du+wu}),0);
}
// Обновляем кратчайший путь
lnh_ins_async(G,INLINE(u_key,{.index=PTH_IDX,.u=adj}),
INLINE(u_index,{.du=du+wu,.btwc=btwc})); //изменить du
lnh_ins_async(G,INLINE(u_key,{.index=BASE_IDX,.u=adj}),
INLINE(u_attributes,{.pu=u, .eQ=eQc, .non=0, .adj_c=count}));
}
}
}
// Сохранить кратчайший путь
lnh_search(G,INLINE(u_key,{.index=PTH_IDX, .u=stop_virtex}));
mq_send((*(u_index*)&lnh_core.result.value).__struct.du);
}
void dijkstra() {
//получить начальную вершину графа из MQ
uint32_t start_virtex = mq_receive();
//получить конечную вершину графа из MQ
uint32_t stop_virtex = mq_receive();
//Очистка очереди
lnh_del_str_async(Q);
//добавление стартовой вершины с du=0
lnh_ins_async(Q,q_record{.u=start_virtex,.index=0},0);
//Получите btwc (центральность), чтобы сохранить его снова
lnh_search(G,u_key{.index=PTH_IDX,.u=start_virtex});
btwc = get_result_value<u_index>().btwc;
// Сохраняем du для запуска virtex
lnh_ins_async(G,u_key{.index=PTH_IDX,.u=start_virtex},u_index{.du=0,.btwc=btwc});
// Перебрать все вершины в очереди Qа
while (lnh_get_first(Q)) {
u = get_result_key<q_record>().u;
du = get_result_key<q_record>().index;
//Удалит вершину из Q
lnh_del_async(Q,lnh_core.result.key);
//Получить значения pu, |Adj|, eQ
lnh_search(G,u_key{.index=BASE_IDX, .u=u});
pu = get_result_value<u_attributes>().pu;
eQ = get_result_value<u_attributes>().eQ;
adj_c = get_result_value<u_attributes>().adj_c;
// Очистить флаг eQ
lnh_ins_async(G,lnh_core.result.key,u_attributes{.pu=pu, .eQ=false, .non=0, .adj_c=adj_c});
//Для каждой вершины Adj
for (i=0;i<adj_c;i++) {
//Получить Adj[i]
lnh_search(G,u_key{.index=i,.u=u});
wu = get_result_value<edge>().w;
adj = get_result_value<edge>().v;
//Получить информацию о смежных вершинах
lnh_search(G,u_key{.index=BASE_IDX,.u=adj});
eQc=get_result_value<u_attributes>().eQ;
count=get_result_value<u_attributes>().adj_c;
lnh_search(G,u_key{.index=PTH_IDX,.u=adj});
dv=get_result_value<u_index>().du;
btwc=get_result_value<u_index>().btwc;
//Если изменился кратчайший путь
if (dv>(du+wu)) {
if (eQc) {
//Если не петля, отправить вершину в Q
if (dv!=INF) {
lnh_del_async(Q,q_record{.u=adj, .index=dv});
}
lnh_ins_async(Q,q_record{.u=adj, .index=du+wu},0);
}
// Обновляем кратчайший путь
lnh_ins_async(G,u_key{.index=PTH_IDX,.u=adj},u_index{.du=du+wu,.btwc=btwc});
lnh_ins_async(G,u_key{.index=BASE_IDX,.u=adj},u_attributes{.pu=u, .eQ=eQc, .non=0, .adj_c=count});
}
}
}
// Сохранить кратчайший путь
lnh_search(G,INLINE(u_key,{.index=PTH_IDX, .u=stop_virtex}));
mq_send(get_result_value<u_index>().du);
}
–>
void dijkstra_core(unsigned int start_virtex) {
//Очистка очереди
Q.del_str_async();
//добавление стартовой вершины с du=0
Q.ins_async(Queue::Record{.id = start_virtex, .du = 0}, Queue::Attributes{});
//Get btwc to store it again
auto [du, btwc] = G.search(Graph::Path_key{.u = start_virtex}).value();
//Save du for start virtex
G.ins_async(Graph::Path_key{.u = start_virtex}, Graph::Shortest_path{.du = 0, .btwc = btwc});
//обход всех вершины графа
while(auto q_it = Q.begin()) {
Q.erase(q_it);
auto [u, du] = *q_it;
//Get pu, |Adj|, eQ
auto result = G.search(Graph::Base_key{.u = u});
auto [pu, eQ, non, adj_c] = result.value();
// Clear eQ
G.ins_async(result.key(), Graph::Attributes{.pu = pu, .eQ = false, .non = 0, .adj_c = adj_c});
//For each Adj
for (auto [adj, wu, attr] : edge_range(G, u)) {
//Get information about Adj[i]
auto [adj_pu, eQc, non, count] = G.search(Graph::Base_key{.u = adj}).value();
auto [dv, btwc] = G.search(Graph::Path_key{.u = adj}).value();
//Change distance
if (dv > (du + wu)) {
if (eQc) {
//if not loopback, push to Q
if (dv != Graph::inf) {
Q.del_async(Queue::Record{.id = adj, .du = dv});
}
Q.ins_async(Queue::Record{.id = adj, .du = du + wu}, Queue::Attributes{});
}
//change du
G.ins_async(Graph::Path_key{.u = adj}, Graph::Shortest_path{.du = du + wu, .btwc = btwc});
//change pu
G.ins_async(Graph::Base_key{.u = adj}, Graph::Attributes{.pu = u, .eQ = eQc, .non = 0, .adj_c = count});
}
}
}
}
Обратите внимание, что в структуре графа для каждой вершины выделены дополнительные индексы pth_idx и viz_idx и поле btwc, которые будут использованы позднее в алгоритме вычисления центральности и алгоритме визуализации. В показанном примере поле центральности копируется из ранее найденных кратчайших путей, т.е. накапливается.
auto [du, btwc] = G1.search(Graph::Path_key{.u = start_virtex}).value();
G1.ins_async(Graph::Path_key{.u = start_virtex}, Graph::Shortest_path{.du = 0, .btwc = btwc});
4.4.2. Алгоритм поиска центральности
Для многих сетевых структур необходимо определить относительную важность входящих в нее узлов. Например, загруженность узла связи в компьютерной сети определяется как суммарное число кратчайших путей между всеми остальными узлами, которые проходят через узел i:
где:
σs,t(i) – число кратчайших путей из узла s в узел t через узел i;
σs,t – общее число кратчайших путей между всеми парами s и t.
Эту величину можно считать индикатором влиятельности людей в социальной сети, или же степень участия белка в различные реакции обмена веществ. Эта величина также важна в изучении транспортных потоков и обычно называется нагрузкой (загруженностью) узла (или связи), поскольку характеризует долю проходящих через узел кратчайших путей. Узлы с высоким значением центральности являются наиболее загруженными логистическими центрами. В отличие от степени узла (количества ребер, инцидентных вершине), понятие центральности узла отражает топологию всей сети.
Вы можете ознакомиться с дополнительными материалами по данной тематике в работе И.А. Евина: [“Введение в теорию сложных сетей”].
Таким образом, для вычисления центральности необходимо выполнить подсчет количества кратчайших путей, проходящих через каждую вершину. Это может быть выполнено с помощью перебора всех стартовых вершин для алгоритма Дейкстры, представленного в предыдущем разделе.
Центральность()
ЦИКЛ ПОКА
Для всех u ∈ V
btwc[u] = 0;
ВСЕ ЦИКЛ ПОКА
ЦИКЛ ПОКА
Для всех u ∈ V
Дейкстра(u);
ЦИКЛ ПОКА
Для всех v ∈ V , v /= u
ЕСЛИ (кратчайший путь (u,v) проходит через вершину k) TO
btwc[k] = btwc[k] + 1;
ВСЕ ЕСЛИ
ВСЕ ЦИКЛ
ВСЕ ЦИКЛ ПОКА
Указанный алгоритм определения центральности представлен в файле dijkstra.cpp.
//-------------------------------------------------------------
// Центральность
//-------------------------------------------------------------
void btwc () {
//Iterate u
for (Graph::virtex_t u : virtex_range{G1}) {
//Start Dijksra shortest path search
dijkstra_core(u);
//Iterate v
for (Graph::virtex_t v : virtex_range{G1}) {
//For undirected graphs needs to route 1/2 shortest paths (u<v)
if (u != v) {
auto du = G1.search(Graph::Path_key{.u = v}).value().du;
//If there is a route to u
if (du != INF) {
//Get pu
auto pu = G1.search(Graph::Base_key{.u = v}).value().pu;
while (pu != u) {
//Get btwc
auto [du, btwc] = G1.search(Graph::Path_key{.u = pu}).value();
//Write btwc, set du by the way
G1.ins_async(Graph::Path_key{.u = pu}, Graph::Shortest_path{.du = du, .btwc = btwc + 1});
//Route shortest path
pu = G1.search(Graph::Base_key{.u = pu}).value().pu;
}
}
}
}
//Init graph again
for (Graph::virtex_t v : virtex_range{G1}) {
//Init graph again
auto adj_c = G1.search(Graph::Base_key{.u = v}).value().adj_c;
G1.ins_sync(Graph::Base_key{.u = v}, Graph::Attributes{.pu = v, .eQ = true, .non = 0, .adj_c = adj_c});
auto btwc = G1.search(Graph::Path_key{.u = v}).value().btwc;
G1.ins_sync(Graph::Path_key{.u = v}, Graph::Shortest_path{.du = INF, .btwc = btwc});
}
}
}
Пример работы алгоритма для графа-решетки показан на следующем рисунке:
Центральность продемонстрирована с помощью размера и цвета вершины.
4.4.3. Алгоритмы поиска сообществ
Наличие сообществ являются свойством многих сетей, для которых находятся подмножества тесно связанных вершин. В общем случае вершины могут находиться одновременно в нескольких сообществах.
Для оценки целесообразности объединения вершин в сообщества используется числовая характеристика, которая описывает выраженность структуры сообществ в данном графе, и называемая модулярностью:
где δ(Ci, Cj) — дельта-функция, равная единице, если Ci = Cj и нулю иначе.
Физический смысл модулярности состоит в следующем. Возьмём две произвольные вершины i и j. Вероятность появления ребра между ними при генерации случайного графа с таким же количеством вершин и рёбер, как у исходного графа, равна didj/2m. Реальное количество рёбер в сообществе C будет равняться Σi,j ∈ C Ai,j.
Таким образом, модулярность равна разности между долей рёбер внутри сообщества при данном разбиении и долей рёбер в том случае, если бы ребра соединяли вершины случайным образом. Поэтому, метрика модулярности показывает выраженность сообществ (случайный граф структуры сообществ не имеет). Также стоит отметить, что модулярность равна 1 для полного графа, в котором все вершины помещены в одно сообщество, и равна нулю для разбиения на сообщества, при котором каждой вершине сопоставлено по отдельному сообществу. Для особо неудачных разбиений модулярность может быть отрицательной.
4.4.3.1. Выделение сообществ на основе алгоритма MultiLevel
Алгоритм основан на оптимизации модулярности. В начале работы алгоритма каждой вершине сначала ставится в соответствие по сообществу. Далее чередуются следующие этапы:
-
Первый этап
-
Для каждой вершины перебираем её соседей
-
Перемещаем в сообщество соседа, при котором модулярность увеличивается максимально
-
Если перемещение в любое другое сообщество может только уменьшить модулярность, то вершина остаётся в своём сообществе
-
Последовательно повторяем, пока какое-либо улучшение возможно
-
-
Второй этап
-
Создать метаграф из сообществ-вершин. При этом рёбра будут иметь веса, равные сумме весов всех рёбер из одного сообщества в другое или внутри сообщества (т.е. будет взвешенная петля)
-
Перейти на первый этап для нового графа
-
Работа алгоритма Multilevel: два прохода, для первого показаны оба этапа
Алгоритм прекращает работу, когда на обоих этапах модулярность не поддаётся улучшению. Все исходные вершины, которые входят в финальную метавершину, принадлежат одному сообществу.
4.4.3.2. Центральность ребер (Edge Betweenness) – метод Girvan – Newman
Для каждой пары вершин связного графа можно вычислить кратчайший путь, их соединяющий. Будем считать, что каждый такой путь имеет вес, равный 1/N, где N — число возможных кратчайших путей между выбранной парой вершин. Если такие веса посчитать для всех пар вершин, то каждому ребру можно поставить в соответствие значение Edge betweenness — сумму весов путей, прошедших через это ребро.
Для ясности приведём следующую иллюстрацию:
В данном графе можно выделить два сообщества: с вершинами 1-5 и 6-10. Граница же будет проходить через ребро, имеющее максимальный вес (25). На этой идее и основывается алгоритм: поэтапно удаляются ребра с наибольшим весом, а оставшиеся компоненты связности объявляем сообществами. Алгоритм состроит из 6 этапов:
-
Инициализировать веса
-
Удалить ребро с наибольшим весом
-
Пересчитать веса для рёбер
-
Сообществами считаются все компоненты связности
-
Подсчитать модулярность
-
Повторять шаги 2-6, пока есть рёбра
4.4.4. Алгоритмы раскладки графов
Раскладка неориентированных графов используется при проектировании топологии СБИС, целью которого является оптимизация схемы для получения наименьшего количества пересечений линий. Eades (1984) ввел модель Spring-Embedder, в которой вершины в графе заменяются стальными кольцами, а каждое ребро заменяется пружиной. Пружинная система запускается со случайным начальным состоянием, и вершины соответственно перемещаются под действием пружинных сил. Оптимальная компоновка достигается за счет того, что энергия системы сводится к минимуму.
Эта интуитивная идея была развита Камада и Каваи (1989), Фрухтерман и Рейнгольд (1991) в соответствующих алгоритмах.
4.4.4.1. The Spring Model
Модель spring-embedder была первоначально предложена Eades (1984) и в настоящее время является одним из самых популярных алгоритмов для рисования неориентированных графов с прямолинейными ребрами, широко используемого в системах визуализации информации.
Алгоритм Идеса следует двум эстетическим критериям: равномерная длина ребер и максимально возможная симметрия. В модели Spring-Embedder вершины графа обозначаются набором колец, и каждая пара колец соединена пружиной. Пружина связана с двумя видами сил: силами притяжения и силами отталкивания, в зависимости от расстояния и свойств соединительного пространства.
Раскладка графа приближается к оптимальной по мере уменьшения энергии пружинной системы. К узлам, соединенным пружиной, приложена сила притяжения (fa
), а к разъединенным узлам приложена сила отталкивания (fr
). Эти силы определяются следующим образом:
ka
и kr
— константы, а d
— текущее расстояние между узлами. Для соединенных узлов это расстояние d
является длиной пружины. Начальная компоновка графа настраивается случайным образом. В каждой итерации силы рассчитываются для каждого узла, и узлы соответственно перемещаются, чтобы уменьшить напряжение. Однако модель Spring-Embedder может не работать на очень больших графах.
4.4.4.2. Local Minimum
Модель spring-embedder привела к созданию ряда модифицированных и расширенных алгоритмов раскладки неориентированных графов. Например, силы отталкивания обычно вычисляются между всеми парами вершин, а силы притяжения могут быть рассчитаны только между соседними вершинами. Упрощенная модель уменьшает временную сложность: вычисление сил притяжения между соседями выполняется за O(|E|)
, хотя вычисление силы отталкивания выполняется за O(|V|²)
, что в является недостатком алгоритмов с n телами. Камада и Каваи (1989) представили алгоритм, основанный на модели пружинного внедрения Идса, который пытается достичь следующих двух критериев или эвристик рисования графа:
-
Количество пересечений ребер должно быть минимальным.
-
Вершины и ребра распределены равномерно.
Цель алгоритма состоит в том, чтобы найти локальный минимум энергии в соответствии с вектором градиента σ = 0, что является необходимым, но не достаточным условием глобального минимума энергии. С точки зрения вычислительной сложности, такой поиск требует большого количества операций, поэтому в реализацию часто включаются дополнительные элементы управления, чтобы гарантировать, что пружинная система не окажется в локальном минимуме.
В отличие от алгоритма Идеса, который явно не включает закон Гука, алгоритм Камады и Каваи перемещает вершины в новые положения по одной, так что общая энергия пружинной системы уменьшается с новой конфигурацией. Он также вводит понятие желаемого расстояния между вершинами на визуализации: расстояние между двумя вершинами пропорционально длине кратчайшего пути между ними.
Для динамической системы из n частиц, соединенных между собой пружинами, пусть p1, p2 … pn будут частицами в области поля визуализации, соответствующими вершинам v1, v2 … vn V графа соответственно. Сбалансированное расположение вершин может быть достигнуто с помощью динамически сбалансированной пружинной системы. Камада и Каваи сформулировали степень дисбаланса как общую энергию пружин:
Данная модель подразумевает, что наилучшее расположение графа — это состояние с минимальным значением E. Расстояние dij между двумя вершинами vi и vj в графе определяется как длина кратчайшего пути между vi и vj. Алгоритм направлен на согласование длины пружины lij между частицами pi и pj с кратчайшим расстоянием пути, чтобы достичь оптимальной длины между ними на чертеже. Длина lij определяется следующим образом:
где L — желаемая длина одного ребра в области рисования. L можно определить на основе наибольшего расстояния между вершинами в графе. Если L0 — длина стороны квадрата области рисования, L можно получить следующим образом:
Сила пружины, соединяющей pi и pj, обозначается параметром kij:
Затем алгоритм ищет визуальное положение для каждого узла v в топологии сети и пытается уменьшить функцию энергии во всей сети. То есть алгоритм вычисляет частные производные для всех узлов топологии сети с точки зрения каждого xv и yv, которые равны нулю (т.е. ∂E / ∂xv = ∂E / ∂yv = 0; 1 < v < n).
Однако эти нелинейные уравнения зависимы, поэтому для решения задачи можно использовать итерационный подход, основанный на методе Ньютона-Рафсона. На каждой итерации алгоритм выбирает узел m с наибольшим максимальным изменением (Δm). Другими словами, узел m перемещается в новое положение, где он может достичь более низкого уровня Δm, чем раньше. Между тем, другие узлы остаются фиксированными. Максимальное изменение (Δm) рассчитывается следующим образом:
4.4.4.3. Force-Directed Placement
Алгоритм Фрухтермана-Рейнгольда основан на модели пружинного встраивания Идса. Он равномерно распределяет узлы, минимизируя пересечения ребер, а также поддерживает одинаковую длину ребер. В отличие от алгоритма Камада-Каваи, алгоритм Фрухтермана-Рейнгольда использует две силы (силы притяжения и силы отталкивания) для обновления узлов, а не использует функцию энергии с теоретическим графическим расстоянием.
Сила притяжения (fa
) и сила отталкивания (fr
) определяются следующим образом:
где d
— расстояние между двумя узлами, а k
— константа идеального попарного расстояния. Константа идеального расстояния k = √(area / n)
. Здесь area
— область рамки чертежа, n
— общее количество узлов в топологии сети.
Алгоритм Фрухтермана-Рейнгольда выполняется итеративно, и все узлы перемещаются одновременно после расчета сил для каждой итерации. Алгоритм добавляет атрибут «смещения» для контроля смещения положения узлов. В начале итерации алгоритм Фрухтермана-Рейнгольда вычисляет начальное значение смещения для всех узлов с использованием силы отталкивания (fr
). Алгоритм также использует силу притяжения (fa
) для многократного обновления визуального положения узлов на каждом ребре. Наконец, он обновляет смещение положения узлов, используя значение смещения.
4.5. Библиотека gpc64io
Библиотека gpc64io
для языка Python3 позволяет разрабатывать приложения, использующие аппаратные ресурсы микпропроцессора Леонард Эйлер. Библиотека состоит из двух частей:
- Описание и реализация основного объекта
GPC
на языке Python3, обеспечивающего высокоуровневый интерфейс связи прогрммного кода Python3 с аппаратным ядром обработки графов через библиотеку lnh64.so. - Высокопроизволительная библиотека lnh64.so, написанная на языке C++, которая обеспечивает низкоуровневое взаимодействие в драйвером символьного устройства /dev/gpc*.
Для использование ресурсов библиотеки программист должен создать экземплярр класса GPC одним из способов:
# 1. Создание экземпляра класса и получение доступа к свободному устройству
gpc = GPC()
# 2. Создание экземпляра класса и получение доступа к указанному устройству
gpc = GPC(device_path=<путь к символьному устройству /dev/gpc*>)
# 3. Создание экземпляра класса, получение доступа к указанному устройству и загрузка sw_kernel
gpc = GPC(device_path=<путь к символьному устройству /dev/gpc*>,swk_path=<путь к sw_kernel файлу rawbinary>)
Далее программа может обратиться к созданному классу для вызова методов, указанных в таблице:
Метод класса | Назначение | Параметры | Возвращаемое значение | |
---|---|---|---|---|
load_swk(swk_path: str) | Загрузка программного ядра sw_kernel | swk_path - полный путь к файлу rawbinary | ‘0’ - загрузка выполнена успешно; ‘-1’ - загрузка завершилась с ошибкой | |
def_handlers(handler_file_path: str) | Формирование списка обработчиков | handler_file_path - полный путь к файлу gpc_handlers.h с объявлением заголовков обработчиков | - | |
mq_send_uint64(message: int) | Пересылка короткого сообщения в gpc | message - посылаемое сообщение размером до 8 байт (unsigned long long) | - | |
mq_send_buf(ba: bytearray) | Передача блока сообщений в gpc | ba - массив значений типа bytearray | Указатель на объект потока приема (thread) | |
close_dev() | Освобождение символьного устроства gpc | - | ‘0’ - устройство обвобождено; другое значение - код ошибки | |
finish() | Ожидание готовности gpc к запуску обработчика (состояние ready) | - | - | |
sync_with_gpc() | Синхронизация с ядром (операция рукопожатия) | - | - | |
join(thread) | Ожидание завершения потока | thread - объект потока, возвращаемый методами mq_receive_buf или mq_send_buf | - | |
mq_receive_uint64() | Прием короткого сообщения из gpc | - | полученное сообщение размером до 8 байт (unsigned long long) | |
mq_receive_buf(buf_size: int, ba: bytearray) | Прием блока сообщений из gpc | buf_size - размер блока передаваемых сообщений (в байтах); ba - массив значений типа bytearray | Указатель на объект потока приема (thread) |
В следующем примере производится инициализация ядра gpc, после чего выполняется тестовая передача и прием массива. Результат сравнивается с ожидаемым:
#Импорт библиотек
import array
import pathlib as pth
import ctypes
from gpc64io.base import GPC
#Количество передаваемых значений
TEST_PACK_SIZE=1000000
#Получить доступ к свободному gpc
gpc = GPC()
print(gpc.dev_path)
#Загрузить sw_kernel
swk_path=str(pth.Path().absolute()/"sw-kernel/sw_kernel.rawbinary") #Путь к проекту sw_kenrel
if gpc.load_swk(swk_path)!=0:
print("Error when loading sw_kernel file "+swk_path)
#Загрузить id и имен обработчиков из файла gpc_handlers.h
handlers_path=str(pth.Path().absolute()/"include/gpc_handlers.h")
gpc.def_handlers(handlers_path)
print(gpc.handlers)
#Запуск обработчика эхо-пакетов
gpc.start_handler("echo_mq")
#Тест приемо-передачи коротких сообщений
test = 0x123456789
gpc.mq_send_uint64(test)
if test == gpc.mq_receive_uint64():
print("Echo uint64_t test result: True")
else:
print("Echo uint64_t test result: False")
#Тест блочной приемо-передачи
arr=array.array('Q',(i for i in range(0,TEST_PACK_SIZE)))
buf_out=bytearray(arr)
buf_in=bytearray(arr)
write_thread = gpc.mq_send_buf(buf_out)
read_thread = gpc.mq_receive_buf(len(buf_in),buf_in)
#Ожидание завершения потоков передачи и приема
gpc.join(write_thread)
gpc.join(read_thread)
#Проверка корректности данных
if buf_out == buf_in:
print("Echo test result: True")
else:
print("Echo test result: False")
#Освобождение ресурсов
del(gpc)
С кодом примера можно ознакомиться тут.
4.6. Примеры создания и применения графов знаний
4.6.1. Пример выделения сообществ
В данном примере создается случайный граф, в котором задаются сообщества сильно-связанных компонент. Далее применяется алгоритм поиска центральности для получения оценок параметра virtex betweeness (центральность вершин). Этот параметр позволяет получить раскладку вершин внутри сообществ, имеющую топологический смысл: вершины с большей центральностью располагаются на в пространстве ближе к центру сообщества. Такой способ визуализации сообществ демонстрирует не только наличие сообществ, но и их структуру.
Ниже представлен код формирования случайного графа:
#Генерируем случайный связный граф с сообществами
gpc.start_handler("delete_graph")
#Создадим несколько собществ
def make_community(virtex,width):
for u in range(virtex,virtex+width):
for v in range(u+1,virtex+width):
insert_edge(gpc,u,v,randrange(1,MAX_WEIGHT))
for community in range(10): make_community(community*VERTEX_COUNT//10, VERTEX_COUNT//30)
#Случайные слабые ребра
u = randrange(VERTEX_COUNT)
for edge in range(EDGE_COUNT):
while True:
v = randrange(VERTEX_COUNT)
if u!=v: break
insert_edge(gpc,u,v,1)
u=v
Функция insert_edge() выполняет передачу в gpc двух ребер: uv и vu:
#Функция вставки ребра в граф
def insert_edge(gpci, u, v, w):
#ребро UV
gpci.start_handler("insert_edges")
gpci.mq_send_uint64(u) #вершина u
gpci.mq_send_uint64(v) #вершина v
gpci.mq_send_uint64(w) #вес ребра
#ребро VU
gpci.start_handler("insert_edges")
gpci.mq_send_uint64(v) #вершина u
gpci.mq_send_uint64(u) #вершина v
gpci.mq_send_uint64(w) #вес ребра
Далее выполняется запуск алгоритма поиска центральности:
#Запустить расчет центральности
gpc.start_handler("btwc")
Следующее действие запускает одну из раскладок, выбранных пользователем:
match VISUALIZATION:
case 1: #Cоздать inbox визуализацию на основе модулярности
gpc.start_handler("create_communities_forest_vizualization")
case 2: #Cоздать визуализацию на основе силового алгоритма Фрухтерамана-Рейнгольда
gpc.start_handler("create_communities_forced_vizualization")
case 3: #Cоздать спиральную визуализацию на основе центральности
gpc.start_handler("create_centrality_spiral_visualization")
case 4: #Cоздать визуализацию на основе центральности
gpc.start_handler("create_centrality_visualization")
case 5: #Cоздать матричную визуализацию на основе центральности
gpc.start_handler("create_visualization")
gpc.mq_send_uint64(int(math.sqrt(VERTEX_COUNT))) #сторона x
gpc.mq_send_uint64(int(math.sqrt(VERTEX_COUNT))) #сторона y
После завершения процедуры рендера его результат передается на сервер bokeh
и выдается на экран.
С кодом примера можно ознакомиться тут.
4.6.2. Пример визуализации графа деБрюйна музыкального произведения
Данный пример использует в качестве исходных данных музыкальные произведения, записанные в midi формате.
Формат midi файлов (смотри описание midi) представляет собой двоично кодированный файл, состоящий из последовательностей событий. Стандарт предусматривает 16 независимых каналов, состояние каждого из которых задается событиями. Канал 10 используется для записи партий ударных инструментов, а нотами в нем одируются ударные инструменты. Остальные каналы могут использоваться для представления событий музыкальных инструментов. Ниже показан пример событий одного канала:
Track 0: Acoustic Guitar
<meta message track_name name='Acoustic Guitar' time=0>
<note_on channel=0 note=58 velocity=72 time=0>
<note_off channel=0 note=58 velocity=64 time=50>
<note_on channel=0 note=60 velocity=72 time=0>
<note_off channel=0 note=60 velocity=64 time=30>
<note_on channel=0 note=61 velocity=72 time=0>
<note_off channel=0 note=61 velocity=64 time=30>
<note_on channel=0 note=60 velocity=72 time=0>
<note_off channel=0 note=60 velocity=0 time=30>
<note_on channel=0 note=70 velocity=72 time=0>
<note_off channel=0 note=70 velocity=64 time=100>
<note_on channel=0 note=66 velocity=72 time=0>
<note_off channel=0 note=66 velocity=64 time=33>
<note_on channel=0 note=72 velocity=72 time=0>
<note_off channel=0 note=72 velocity=64 time=38>
<note_on channel=0 note=66 velocity=72 time=0>
<note_off channel=0 note=66 velocity=64 time=21>
<note_on channel=0 note=70 velocity=72 time=0>
<note_off channel=0 note=70 velocity=64 time=41>
<note_on channel=0 note=66 velocity=72 time=0>
<note_off channel=0 note=66 velocity=64 time=33>
<note_on channel=0 note=70 velocity=72 time=0>
...
Событие note_on
означает нажатую ноту (высота звучания задается полем note
), а сообщение note_off
- отпущенную ноту (отметим, что звучание к моменту отпускания ноты может завершиться, или продолжаться еще и после возникновения события note_off
в зависимости от инструмента). Поле channel
определяет канал, к которому относится сообщение. Поле velocity
означает динамическую арактеристику атаки ноты (например, скорость/силу удара в ударных инструмента для клавишно-ударного инструмента фортепиано). Поле time
означает время в настроенных временных единицах (долях ноты и количестве нот в секунду), прошедшее с момента предыдущего сообщения. Таким образом события в midi следую относительно друг друга. Если два события должны произойти одновременно, то для второго события должно быть задан time=0
.
В рассмариваемом примере используется следующий конвейер обработки:
- Объединение треков и каналов — инструменты исходного midi сводятся в один голос.
- Определение тональности — используется алгоритм на основе Байесовского классификатора (TemperleyKostkaPayne алгоритм)
- Восстановление цепочек аккордов — последовательность событий преобразуется в состояния
- Формирование кода вершины — каждое состояние кодируется в виде последовательности нот. Для состояния из Словаря уникальных кодов вершин получается уникальный ключ вершины.
- Запись вершины и ребра в граф - ключ вершины добавляется в граф деБрюйна и соединяется ребром с предыдущей вершиной.
- Граф ДеБрюйна передается в gpc и выполняется его анализ алгоритмом Ньюмана (выделение сообществ).
- Рендер графа передается в хост-подсистему для визуализации.
С кодом примера можно ознакомиться тут.
4.7. Сборка и запуск примеров проектов
4.7.1. Пример 4. Использования языка python и библиотеки gpc64io для приемо-передачи данных между хост-подсистемой и sw_kernel
Пример демонстрирует основные механизмы инициализации гетерогенных ядер gpc и взаимодействие хост-подсистемы с Graph Processor Core, используются аппаратные очереди. Для хост подсистемы используется бибилиотека gpc64io
Установка
Для установки требуется рекурсивно клонировать репозиторий:
git clone --recursive https://latex.bmstu.ru/gitlab/hackathon2023/lab4.git
cd lab4
Сборка проекта
Следует выполнить команду:
make
Результатом выполнения команды станет файлы sw_kernel_main.rawbinary в директории sw_kernel.
Запуск проекта
Заупуск проекта осуществляется в ноутбуке lab4.ipynb.
Очистка проекта
Следует выполнить команду:
make clean
4.7.2. Пример 5. Демонстрация примения Jupyter ноутбуков и языка python для визуализации графов
Пример демонстрирует варианты анализа графов знаний и их визуализацию. Реализованы пять алгоритмов визуализации:
- Визуализация inbox на основе модулярности Ньюмана.
- Визуализация на основе силового алгоритма Фрухтерамана-Рейнгольда.
- Спиральная визуализация на основе центральности.
- Спиральная матричная визуализация на основе центральности.
- Визуализацию графа-решетки на основе центральности.
Для выбора варианта визуализации используется параметр VIZUALIZATION
Установка
Для установки требуется рекурсивно клонировать репозиторий:
git clone --recursive https://latex.bmstu.ru/gitlab/hackathon2023/lab5.git
cd lab5
Далее в облаке devlab.bmstu.ru необходимо открыть файл lab5.ipynb
Сборка sw-kernel части проекта
Следует выполнить команду:
make
Результатом выполнения команды станет файлы sw_kernel_main.rawbinary в директории sw_kernel.
Запуск проекта
Заупуск проекта осуществляется в ноутбуке lab5.ipynb.
Очистка проекта
Следует выполнить команду:
make clean
4.7.3. Пример 6. Демонстрация использования микропроцессора Леонард Эйлер для анализа графов знаний
Пример демонстрирует визуализацию графа гармоний музыкального произведения. Для формирования графа знаний используется запись музыкального произведения в формате midi
. По последовательности аккордов строится граф ДеБрюйна с размером окна L, задаваемого параметрически в программе.
Установка
Для установки требуется рекурсивно клонировать репозиторий:
git clone --recursive https://latex.bmstu.ru/gitlab/hackathon2023/lab6.git
cd lab6
Далее в облаке devlab.bmstu.ru необходимо открыть файл lab6.ipynb
Сборка sw-kernel части проекта
Следует выполнить команду:
make
Результатом выполнения команды станет файлы sw_kernel_main.rawbinary в директории sw_kernel.
Запуск проекта
Заупуск проекта осуществляется в ноутбуке lab6.ipynb. Исходные midi файлы должны быть помещены в папку data/midi_sources/
Очистка проекта
Следует выполнить команду:
make clean
4.8. Индивидуальные задания
Ознакомиться с проектами Примера 4, Примера 5, Примера 6.
Выбрать пять музыкальных произведений различных композиторов и жанров. Произведение должно быть доступно в формате midi. Используя код Проекта 6 получить по две визуализации для каждого музыкального произведения.
5. Командный практикум. Обработка и визуализация графов в вычислительном комплексе Тераграф
Данная часть практикума выполняется командами от 5 до 10 человек. Задачей третьей части является создание музыкального произведения. Рассматриваются два подхода к созданию алгоритмической музыки на основе графов знаний:
- Генерация музыки на основе графов ДеБрюйна.
- Генерация музыки технологией структурного синтеза музыкальных произведений.
Результатом командной разработки является музыкальная композиция в формате mp3 длительностью от 30 до 60 секунд.
5.1. Подготовка данных для работы конвейера генерации музыки
В данном разделе рассматривается способ генерации музыкального произведения на основе графов ДеБрюйна, и его последующая стилизация. Получение графов ДеБрюйна музыкальных произведений было подробно рассмотрено в Примере 6.
На основе многих музыкальных произведений получаются многочисленные графы ДюБрюйна, которые далее объединяются в единый граф знаний. Участникам практикума предоставляется два варианта заранее подготовленных графов знаний:
- Библиотека цепочек аккордов и музыкальных фраз (168 тысяч отрывков в формате Midi). Графы ДеБрюйна с параметром L=5 собраны на облачной платформе devlab в
/data/hackathon2023/PianoChords_dst_l5_concatenated
. - Библиотека музыкальных произведений (116 тысяч композиций всех жанров в формате Midi). Графы ДеБрюйна с параметром L=5 собраны на облачной платформе devlab в
/data/hackathon2023/WorldMusic_dst_l5_concatenated
.
Помимо этого можно формировать собственные графы деБрюйна с помощью кода Примера 6.
В процессе генерации музыки применяется нейросетевой перенос стиля (проект Петра Шумнова, ИУ7). Таким образом удается добиться естественного звучания фортепианного варианта исполнения. Однако, фортепиано является струнным клавишным инструментом, что определяет характер звучания. Для других вариантов звукоизвлечения такая стилизация может быть не пригодна (например, для духовых инструментов), поэтому целесообразно также формировать исходную (не стилизованную) композицию, удобную для последующей обработки.
Для стилизации исполнения может быть выбран произвольный образец произведения в формате Midi. Требование к такому образцу: количество событий 128 и более, наличие только одного голоса (звучание двух нот одновременно не допускается).
В приведенном варианте генерации используется обход графа ДеБрюйна случайным образом, т.е. в каждой вершине выбирается случайное ребро, после чего совершается переход по нему к новой вершине (новому аккорду). Возможно, однако, усовершенствовать такой вариант, используя предпочтительное движение мелодии (вверх, вниз), предпочтительную длину фразы (завершение фразы устойчивым интервалом), предпочтитльную длительность звучания аккорда и т.д.
После получания музыкального произведения он формируется в формате Midi, по которому должен быть сгенерирован звук. Для этого используется консольный генератор Timidity. Данная утилита способна генерировать раздельное звучание для каждого канала и трека Midi файла, поэтому целесообразно разделять однотрековый Midi на голоса.
Для разделения резульирующего трека на голоса используется интервальный метод: каждому инструменту ставится в соответствие интервал высот тона. Если звучащая нота попадает в интервал, она добавляется в отдельный канал инструмента. Далее в настроечном файле Timidity (указывается в параметрах запуска) прописыется так называемый звуковой шрифт (soundfont), который используется для генерации звука инструмента. Соответствие канала звуковому шрифту также задается в конфигурационном файле.
Выбор музыкальных инструментов и звуковых шрифтов позволяет существенно улучшить звучание музыкального произведения. Звуковые шрифты могут быть скачаны из сети Интернет в формате *.sf2
.
В итоге получается сформировать не только исходные Midi файлы всего произведения и отдельных инструментов, но и синтезировать файлы mp3
.
Далее на полученное произведение может быть наложен ритм, изменен тем и прочее. Это выполняется в специальных редакторах (DAW).
5.2. Конвейер генерации музыки на основе графов ДеБрюйна
5.2.1. Стадия 1. Формирование графов де Брюйна
Описание стадии 1 подробно представлено в разделе: Примера 6.
С помощью кода примера 6 можно создавать графы по тому набору композиций, которые предполагается исопльзвать впоследствии рпи генерации звука. Например, можно собрать этническую музыку, или же музыку опеределенного стиля. Итоговое произведение будет, таким образом, построено исключительно на аккордовых цепочках выбранных произведений.
5.2.2. Стадия 2. Обход графов де Брюйна
Для используется следующий конвейер обработки:
- Объединение графов ДеБрюйна — поиск в директории всех графов с заданной тональностью. Граф записывается в ядро обработки графов вычислительного комплекса Тераграф
- Обход графа — проход по графу ДеБрюйна по случайному маршруту с заданным количеством шагов
- Восстановление аккорда — выборка полной записи о вершине из словаря аккордов, хранимого в GPC суперЭВМ Тераграф
- Запись потока событий — аккорд преобразуется в последовательность событий midi.
В пункте 1 можно указать любую из существующих тональностей (24 шт.). Будут выбраны только те цепочки аккордов, которые были встречены в произведении, написанном в указанной тональности. Последующая обработка выполняется автоматически и настроек не требует.
5.2.3. Стадия 3. Стилизация, многоголосие и синтез звука
- Разделение на одноголосные треки — выделяются ноты в каждом аккорде: нижняя, верхняя, 2-я, 3-я, и т. д. для стилевой обработки одноголосных партий.
- Нейросетевой перенос стиля — используется стилевая композиция для определения вектора коэффициентов. Далее применяется итерационное изменение параметров Velocity обрабатываемого midi для минимизации ошибки
- Объединение midi — стилизованные голоса сводятся вместе
- Разделение на голоса — по настройкам пользователя выделяются верхний, нижний голаса, или голоса по заданному диапазону нот. Каждый голос записывается в отдельный канал.
- Синтезатор — для каждого голоса задается soundfont (.sf2) и номер инструмента. Звук генерируется с использованием timidity, далее wav кодируется в mp3 с использованием ffmpeg.
На данном этапе погут применять настойки генерации звукового файла, доступные для утилиты timidity. Например, могут быть выдраны звуковые шрифты, и параметры генерации каждого голоса. В приведенном ниже примере конфигурационного файла задается файл со звуковым шрифтом, номер банка и номер инструмента в нем (по терминологии звуковых шрифтов - номера программы). Например, усиление amp задается для каждого голоса в отдельности и позволяет сделать голос громче или тише. Параметр pan определяет смещение голоса относительно центра влево (-100) или вправо (+100) в стерео звучании. Допускаются также параметры left,right,center.
#Гитара
3 %font '/data/hackathon2023/soundfonts/STEEL_STRING_GUITAR.sf2' 0 0 pan=-80 amp=400
#Хор Ах
2 %font '/data/hackathon2023/soundfonts/KBH_Real_and_Swell_Choir.sf2' 0 2 pan=-20 amp=40
#Две виолончели
1 %font '/data/hackathon2023/soundfonts/Essential Keys-sforzando-v9.6.sf2' 0 29 pan=80 amp=120
Для подбора голосов используется утилита sf2_nfo, доступная на сервере devlab:
sf2_nfo '/data/hackathon2023/soundfonts/Essential Keys-sforzando-v9.6.sf2'
fluidsynth: warning: Failed to pin the sample data to RAM; swapping is possible.
fluidsynth: warning: No preset found on channel 9 [bank=128 prog=0]
bank: 0 prog: 0 name: Yamaha C5 Grand
bank: 0 prog: 1 name: Large Concert Grand
bank: 0 prog: 2 name: Mellow C5 Grand
bank: 0 prog: 3 name: Bright C5 Grand
bank: 0 prog: 4 name: Upright Piano
bank: 0 prog: 5 name: Chateau Grand
bank: 0 prog: 6 name: Mellow Chateau Grand
bank: 0 prog: 7 name: Dark Chateau Grand
bank: 0 prog: 8 name: Rhodes EP
bank: 0 prog: 9 name: DX7 EP
bank: 0 prog: 10 name: Rhodes Bell EP
bank: 0 prog: 11 name: Rotary Organ
bank: 0 prog: 12 name: Small Pipe Organ
bank: 0 prog: 13 name: Pipe Organ Full
bank: 0 prog: 14 name: Small Plein-Jeu
bank: 0 prog: 15 name: Flute Sml Plein-Jeu
bank: 0 prog: 16 name: FlutePad Sml Plein-J
bank: 0 prog: 17 name: Plein-jeu Organ Lge
bank: 0 prog: 18 name: Pad Plein-Jeu Large
bank: 0 prog: 19 name: Warm Pad
bank: 0 prog: 20 name: Synth Strings
bank: 0 prog: 21 name: Voyager-8
bank: 0 prog: 22 name: Full Strings Vel
bank: 0 prog: 23 name: Full Orchestra
bank: 0 prog: 24 name: Chamber Strings 1
bank: 0 prog: 25 name: Chamber Str 2 (SSO)
bank: 0 prog: 26 name: Violin (all around)
bank: 0 prog: 27 name: Two Violins
bank: 0 prog: 28 name: Cello 1
bank: 0 prog: 29 name: Cello 2 (SSO)
bank: 0 prog: 30 name: Trumpet
bank: 0 prog: 31 name: Trumpet+8 Vel
bank: 0 prog: 32 name: Tuba
bank: 0 prog: 33 name: Oboe
bank: 0 prog: 34 name: Tenor Sax
bank: 0 prog: 35 name: Alto Sax
bank: 0 prog: 36 name: Flute Expr+8 (SSO)
bank: 0 prog: 37 name: Flute 2
bank: 0 prog: 38 name: Timpani
bank: 0 prog: 39 name: Banjo 5 String
bank: 0 prog: 40 name: Steel Guitar
bank: 0 prog: 41 name: Nylon Guitar
bank: 0 prog: 42 name: Spanish Guitar
bank: 0 prog: 43 name: Spanish V Slide
bank: 0 prog: 44 name: Clean Guitar
bank: 0 prog: 45 name: LP Twin Elec Gtr
bank: 0 prog: 46 name: LP Twin Dynamic
bank: 0 prog: 47 name: Muted LP Twin
bank: 0 prog: 48 name: Jazz Guitar
bank: 0 prog: 49 name: Chorus Guitar
bank: 0 prog: 50 name: YamC5 + Pad
bank: 0 prog: 51 name: YamC5+LowStrings
bank: 0 prog: 52 name: YamC5+ChamberStr
bank: 0 prog: 53 name: YamC5+Strings
bank: 0 prog: 54 name: Chateau Grand+Pad
bank: 0 prog: 55 name: Ch Grand+LowStrings
bank: 0 prog: 56 name: Ch Grand+ChamberStr
bank: 0 prog: 57 name: Ch Grand+Strings
bank: 0 prog: 58 name: DX7+Pad
bank: 0 prog: 59 name: DX7+LowStrings
done
Названия программ позволяют выбрать их для генерации звука. Если требуется изменить результат, то в проекте предусмотрен скрипт midi2mp3.sh
, который можно вызвать в консоли с параметрами:
./midi2mp3.sh <Темп> <Максимальная длительность в секундах> <Исходный Midi> <Имя файла результата.mp3> <Конфигурационный файл>
Дополнительные опции можно найти тут: Timidity man
В итоге формируется композиция, подобная следующей:
Загадочная мелодия в Фа-миноре:
5.3. Пример 7. Использование микропроцессора Леонард Эйлер для генерации музыкальныых произведений
5.3.1. Общее описание
Пример демонстрирует генерацию последовательности аккордов по графу ДеБрюйна, созданному в примере 6. Для формирования графа знаний используется база графов ДеБрюйна, полученных на основе больших сборников midi произведений в различных тональностях (120 тысяч произведений и 168 тысяч аккоровых последовательностей). Граф ДеБрюйна обходится случайным образом и формируется композиция. Далее она разделяется на голоса и стилизуется с помощью нейронной сети. Таким образом получается midi файл с естественно выставленными значениями поля velocity (динамика звукоизвлечения). Далее производитя генерация звука с помощью консольного синтезатора timidity. Итогом работы является произвдеение в трех вариантах:
- Фортепианное исполнение произведения.
- Стилизованное исполнение с разделением на голоса и инструменты.
- Оригинальное не стилизованное произведение с разделением на голоса и инструменты.
5.3.2. Установка
Для установки требуется рекурсивно клонировать репозиторий:
git clone --recursive https://latex.bmstu.ru/gitlab/hackathon2023/lab7.git
cd lab7
Далее в облаке devlab.bmstu.ru необходимо открыть файл lab7.ipynb
5.3.3. Зависимости
Зависимости для сборки проекта:
-
набор средст сборки riscv toolchain и экспорт исполняемых файлов в
PATH
-
набор библиотек picolib и экспорт в
C_INCLUDE_PATH
Для стандартного пользователя ВМ студенческой команды хакатона все необходимые переменные окружения установлены по-умолчанию.
5.3.4. Сборка sw-kernel части проекта
Следует выполнить команду:
make
Результатом выполнения команды станет файлы sw_kernel_main.rawbinary в директории sw_kernel.
5.3.5. Запуск проекта
Заупуск проекта осуществлдяется в ноутбуке lab7.ipynb.
Для очистки следует выполнить команду:
make clean
Для удаление генерированных произведений следует выполнить команду:
make clear
5.4 Генерация музыки технологией структурного синтеза музыкальных произведений
5.4.1 Общее описание
Суть данного подхода в использовании для генерации одновременно двух различных графов - графа ритма и графа мелодии. В отличие от предыдущего метода, набор следующих друг за другом звуков получается посредством коротких проходов по данным графам с учетом большого количества параметров, таких как тональность, размер, предпочтительный темп и др. Таким образом генерируются короткие (длиной 1-2 музыкальных такта) ритмическо-мелодические последовательности, которые записываются в объекты класса “Мотив”. Далее управление процессом генерации все больше переходит от непосредственно графов к иерархической структуре, построенной по принципам музыкальной формы. Последовательность из двух стоящих друг за другом “Мотивов” включается в объект класса “Фраза”, представляющего собой, по сути, их обертку со своими методами управления музыкальной последовательностью. Точно так же “Фразы” включаются в “Предложения”, “Предложения” в “Периоды”, а “Периоды” в единую “Форму”, повышая уровень абстракции с каждым иерархическим переходом.
Подробно иерархический принцип работы описан в разделе 5.4.3. Инструкции и примеры работы приведены в блокнотах GPC_music.ipynb и Composing.ipynb в репозитории.
5.4.2 Установка
Клонируйте репозиторий:
git clone --recursive https://latex.bmstu.ru/gitlab/hackathon2023/lab8.git
cd lab8
Выполните команду:
make install
5.4.3 Принцип работы
Используемые графы
В работе используются графы двух типов - граф ритма (rhythm_graph
) и граф мелодии (melody_graph
). В графе ритма вершинами являются длительности звуков (1/2, 1/4 и т.д.), представленные в виде десятичной дроби. По графу ритма генерируется последовательность длительностей для мелодии. Граф мелодии содержит отдельные ноты в качестве вершин. В каждой вершине записано название ноты, а также ее целочисленное значение высоты звука для интерпретации в MIDI формате.
В обоих графах вершины соединены направленными ребрами, имеющими веса. Точных границ значений весов нет, считается, что 100 - вес достаточно большой, 10 - достаточно малый. Упрощенно, чем больше вес, тем больше вероятность пройти по этому ребру.
Алгоритм прохода по графу
В общем случае, проход по графу начинается со случайной вершины. Каждый раз, попадая в новую вершину, алгоритм формирует массив из весов ребер, выходящих из данной вершины. Каждый вес домножается на случайный кожффициент в диапазоне от 0,05 до 1,0. То ребро, чей вес оказался больше остальных после домножения на коэффициент, считается победителем, осуществляется переход в вершину, в которую ведет данное ребро. Коэффициенты генерируются в зависимости от задаваемого внешне сида (random_state
).
Проход по графу ритма
Каждая вершина в графе ритма представляет собой длительность, зарезервированную для ноты либо для паузы, это указано в специальном ее свойстве duration_type, принимающем значения “note
” либо “pause
”.
Еще одно свойство вершин графа ритма - кратность (rate
). По умолчанию кратность равна 1, но может принимать любое целое значение. Длительность, указанная в вершине, делится на равные части согласно кратности. Т.е., например, длительность 1/4 с кратностью 4 будет представлять из себя 4 длительности 1/16. Также возможно нечетное деление (триоли, пентоли и т.д.).
Проход по графу ритма завершается, когда собранные по пути длительности в сумме дают длительность одного либо двух тактов (в зависимости от наличия залигованной длительности).
Проход по графу мелодии
Для графа мелодии используется более сложный алгоритм, учитывающий тональность, в которой находится генерируемая последовательность.
Мелодия в тональности может генерироваться с альтерациями (разрешаются звуки, не входящие в тональность) либо без. Первый случай предусмотрен, но основным считается второй. В основном случае при попадании в любую вершину и дальнейшем осмотре ее ближайших соседей отсекаются те из них, которые не лежат в заданной тональности.
Каждая тональность состоит из ступеней, где каждая ступень - звук, входящий в данную тональность. Всего ступеней, как правило, 5 либо 7. 1, 3, 5 ступени считаются устойчивыми, остальные - неустойчивыми. Считается, что у неустойчивых ступеней существует тяготение к устойчивым. Помимо этого, отдельно существует тяготение 5 ступени к 1. Поэтому, когда алгоритм попадает в вершину графа, происходит определение текущей ступени. Если ступень имеет тяготение к другим ступеням, таковые ищутся среди соседей данной вершины. Если таковые находятся, веса ребер, ведущих к ним, увеличиваются на 100 в рамках данной итерации, создавая таким образом оное тяготение.
Дополнительно в алгоритме может быть задано восходящее либо нисходящее движение мелодии. Тогда ребра, ведущие к звукам, более высоким либо более низким, нежели текущий, также получают дополнительный вес.
Генерация мотивов
В алгоритме генерации, используемом в данной работе, мотив является элементарной ритмо-мелодической единицей. Состоит из одного либо двух тактов. Представлен классом Motiff
.
Параметры, используемые в конструкторе объекта, следующие:
gpci
- объект, содержащий ссылку на используемое ядро GPCrv_num
- количество вершин в загруженном в GPC графе ритмаmv_num
- количество вершин в загруженном в GPC графе мелодииsignature
- размер, по умолчанию 4/4signature_big_endian
- предназначен для разметки сложных неквадратных размеров, типа 5/4. Если True, то размечает их в таком отношении, что больший размер оказывается впереди (5/4 = 3/4 + 2/4)key_stages
- структура, содержащая информацию о тональности и ступенях в ней. Опционален, но без него генерация будет происходить не в тональностиallow_alterations
- Разрешение/запрет альтерацийcadence
- завершенность,full
- оканчивается на сильную долю и 1 ступень,incomplete
- на слабую долю и 4, 5 ступень,half
- на любую долю, на неустойчивую ступень,None
- регуляция отсутствует;random_state
- сид для генерации случайных коэффициентовrhythm
- уже готовая последовательность длительностейmelody
- уже готовая последовательность нотmelody_vertices
- уже готовый список идентификаторов посещенных при генерации вершин;movement
- тип мелодического движения:ascend
- восходящее,descend
- нисходящее,wave
- волнообразное,None
- регуляция отсутствует;movement_start_move
- для типа движения wave, тип движения, с которого начинается волна (ascend
- восходящее,descend
- нисходящее);phrase_role
- роля мотива во фразе,opening
- открывающий, должен начинаться с устойчивой ступени,closing
- закрывающий, завершается согласно параметруcadence
;motiff
- уже готовый мотив, дает возможность создания из копии.
Основной метод генерации - create()
. Принимает следующие параметры:
start_duration
- длительность, с которой следует начать, опционален;start_note
- имя ноты, с которой следует начать, оционален;include_start_duration
- включать ли стартовую длительность в итоговый результат, по умолчаниюTrue
;include_start_note
- включать ли стартовую ноту в итоговый результат, по умолчаниюTrue
;rhythm_type
- тип ритмики, может принимать следующие значения:slow
,medium
,fast
,None
. В зависимости от значения, при генерации алгоритм будет отдавать предпочтения меньшим или большим длительностям:slow
- от 1/2 и больше,medium
- от 1/8 до 1/4,fast
- от 1/8 и меньше.
В ходе работы метода сначала генерируется последовательность длительностей, если она не была задана извне, затем над ней выполняются вариации, затем в зависимости от параметра cadence
последняя длительность в мотиве изменяется таким образом, чтобы попасть на сильную либо слабую долю. Затем генерируется последовательность нот, если она не была задана извне. В конце мелодия также подстраивается в зависимости от параметра cadence
, последняя нота “подтягивается” к нужной ступени.
Готовый мотив содержит последовательность длительностей и нот.
Еще один метод - develop()
. Служит для развития сгенерированной музыкальной темы и осуществления вариаций.
Параметры метода:
devtype
- тип развития:melody_variance
- мелодическая вариация,rhythm_variance
- вариация ритма,full_variance
- полная перегенерация,sequence
- секвенция, повторение мелодии на другой высоте,rhythm_stretch
- растяжение/сжатие длительностей,melodic_reversal
- обращение, движение мелодических интервалов в обратную сторону,inversion
- инверсия, движение мелодии в обратную сторону;rhythm_vartype
- тип ритмической вариации,unite
- объединение малых длительностей в большие,divide
- расщепление больших длительностей на малые;melody_vartype
- тип мелодической вариации,weak
- изменить ноты на слабых долях,strong
- на сильных долях;melody_varproba
- вероятность, с которой будет изменена каждая конкретная нота;seq_semitones
- для типа развитияsequence
, кол-вополутонов
, на которое необходимо поднять/опустить мелодию;rhythm_stretch_coef
- для типа развитияrhythm_stretch
, коэффициент сжатия/растяжения длительностей.
Метод может быть применен несколько раз подряд с разными типами развития.
Генерация фраз
Фраза - последовательность, состоящая из двух мотивов. Представлена классом Phrase
. Параметры конструктора во многом повторяют параметры класса Motiff
. Появилась дополнительная опция для параметра movement
: dwave
. Теперь значение данного параметра wave
будет означать, что в открывающем мотиве мелодическое движение будет идти в одну сторону, в закрывающем - в другую. Если же нужно, чтобы внутри мотивов сохранялось волнообразное движение, необходимо использовать значение dwave.
В рамках конструктора можно сразу задать оба мотива, пользуясь параметрами motiff_1
и motiff_2
. Также их можно задать отдельным методом set_motiff()
. Для класса Phrase
также доступно создание из копии, посредством параметра конструктора phrase
.
В конструкторе создаются два объекта класса Motiff
, в которые сразу передаются касающиеся их параметры.
Основной метод генерации - create()
, с уже описанными параметрами. Поочередно запускает генерацию каждого из мотивов.
Присутствует метод develop()
, обладающий теми же параметрами, что и в классе Motiff
. Развитие применяется ко всей фразе.
Генерация предложений
Предложение - единая музыкальная мысль, состоит из 2, реже 3, еще реже 4, фраз. Последняя фраза называется каденцией - завершением предложения. Представлена классом Sentence
.
Параметры конструктора частично повторяют параметры предыдущих классов, собственные параметры:
key_note_name
- имя ноты, от которой строится тональность;key_tone_name
- имя тональности (“major
”, “natural_minor
” и т.д.). Последние два параметра нужны для определения тональности иkey_stages
внутри самого предложения, без того, чтобы задавать их извне;phrase_num
- количество фраз;movement
- параметр, регулирующий мелодическое движение. Тип данных -tuple
, первое значение в котором - общая тенденция мелодического движения в предложении:ascend
,descend
либо None, а второе - полнота выполнения этой тенденции:full
- тип движения сохраняется во всех фразах предложения,incomplete
- меняется от фразы к фразе,half
- движение становится волнообразным в рамках каждой фразы,None
- движение становится волнообразным в рамках каждого мотива, фактически, регуляция теряется;repetitiveness
- повторяемость. Он обязателен, в зависимости от данного параметра выстраивается структура предложения:- “
full
” - генерируется первая фраза, вторая копируется из первой, к ней применяется мелодическая динамика (о ней ниже). Последующие фразы, если они есть, являются точными копиями первой и второй последовательно. - “
strong
” - более мягкий вариант предыдущей опции. Все фразы, следующие за первой, получаются ее копированием и применением к ней любых видов вариаций; - “
opening
” - открывающий мотив у всех фраз одинаковый, закрывающий же подвергается вариациям; - “
closing
” - аналогично предыдущему, только наоборот: закрывающий мотив остается неизменным, открывающий варьируется; - “
weak
” - каждая фраза генерируется отдельно, без копирования материала других. Вариации и динамики применимы к уже сгенерированным последовательностям.
- “
melody_vartype
- тип вариации мелодии,weak
,strong
либоNone
. Не работает для полной повторяемости;melody_varproba
- вероятность, с которой будет изменена каждая конкретная нота;rhythm_dynamics
- динамика ритма, такжеtuple
. Первое значение - основная ритмическая тенденция, то же, что и параметрrhythm_type
. Второе значение - динамическое изменение ритма в процессе развития темы:accel
- ускорение, расщепление больших длительностей,deccel
- замедление, слияние мелких длительностей в большие,None
- нет динамики;melody_dynamics
- динамика мелодии, типdict
. Представляет собой словарь, в который заносятся типы вариаций мелодической динамики вместе с вероятностью их применения. Пример:{'inversion': 0.5, 'sequence': 0.2, 'melodic_reversal': 1.0}
. Всего может быть три типа вариаций, все предствалены в примере. Применяются они в следующем порядке: сначала инверсия, затем мелодическое обращение, затем секвенция;seq_semitones
- уже известный параметр, количество полутонов секвенции;sentence
- параметр для создания из копии.
Основной метод генерации предложения - create()
, инициирующий создание объектов нижестоящих классов, генерацию и развитие темы в соответствии с параметрами, заданными в конструкторе.
Метод change_key()
позволяет поменять тональность уже сгенерированной мелодии. В метод могут быть переданы как наименование тональности (key_note_name
, key_tone_name
), так и сразу структура key_stages
. Необязательный параметр direction
(ascend
, descend
) определяет, в сторону повышения либо понижения должна отклониться мелодия.
Генерация периодов
Период - единица, состоящая из нескольких предложений. Может представлять как самостоятельное произведение, так и часть более сложной структуры. В популярной музыке куплет, как правило, является периодом.
В рамках реализации представлен классом Period
.
- Инкапсулирует объекты класса
Sentence
; - Тональность задается входным параметром
key
, представляющим собой кортеж, в котором обозначены нота и сама тональность (("C", "major")
,("D", "natural_minor")
и т.д.); - Параметры
repetitiveness
,melody_dynamics
работают аналогичным предложению образом; - Параметр
structure
задает тип структуры периода:rsquare
- квадратный повторного строения (2 предложения, А + А1),nrsquare
- квадратный неповторного строения (2 предложения, А + В),nsquare
- неквадратный (2 предложения, 2 фразы + 3 фразы),third
- троичный (3 предложения),double
- сложный, двойной (4 предложения, А + В1 + А + В2),uniform
- единый (1 предложение, от 4 до 16 фраз); - Период имеет роль - основную либо вспомогательную, задаваемую параметром
role
.main
- основная,auxiliary
- вспомогательная. Отличие одних от других состоит в том, что вспомогательные периоды короткие, не могут иметь другой структуры кромеrsquare
,nrsquare
иuniform
с малым количеством предложений. Вспомогательные периоды нужны в качестве промежуточных звеньев между основными; - Ввиду сложности в рамках данной реализации регулировать квадратность периода в привычном музыкальном понимании, под квадратностью здесь понимается не количество тактов, соответствующее степени двойки, а количество фраз, соответствующее степени двойки;
- В рамках периода происходит вывод музыкальных характеристик на качественно новый уровень, т.к. на структуре такого размера мы уже можем говорить о настроении музыки, ее характере и сложности. Нововведенные параметры периода перечислены ниже:
character
- характер музыки, может быть активнымactive
либо пассивнымpassive
. Активный характер подразумевает постоянное развитие динамики, высокую вероятность мелодических вариаций, но при этом склонность придерживаться уже высказанных утверждений (звуки на сильных долях остаются на своих местах). Пассивный же характер, напротив, не склонен к динамичности, стремится остаться в той конфигурации, в который уже был, но при этом подчиняется насильным изменениям извне (вариации на сильные доли);complexity
- сложность. Отвечает за склонность алгоритма к формированию сложных ритмическо-мелодических конструкций. Имеет диапазон в 3 значения:low
,medium
,high
. При низкой сложности предпочтение отдается медленным либо средним длительностям, мелодические и ритмические вариации сведены к минимуму, снижены вероятности мелодических динамик. Средняя сложность использует средние и быстрые длительности, вероятность вариаций и мелодических динамик остается в районе заданной. Высокая сложность представляет собой засилие быстрых длительностей, имеет большую вероятность вариаций и динамик.mood
- настрой, может быть позитивнымpositive
, негативнымnegative
и нейтральнымneutral
. При позитивном настрое напряжение либо все время понижается, либо падает к концу периода, при негатвном - возрастает, при нейтральном мелодическое движение волнообразно и не дает четкого ощущения роста/спада напряжения.
Генерация форм
Форма - крупная музыкальная единица, в некоторых случаях представляющая собой целое произведение. Инкапсулирует периоды, представлена классом Form
.
- Параметры
key
,repetitiveness
,melody_dynamics
,character
,complexity
,mood
остаются без изменений по сравнению с периодом; - Основной параметр формы один -
partition_type
, отвечает за структуру всей формы в целом. Принимает следующие значения:2pnrep
- простая двухчастная безрепризная форма. Состоит из двух периодов, оба из которых квадратные и генерируются по отдельности;2prep
- простая двухчастная репризная форма. От безрепризной отличается тем, что второе предложение первого периода повторяется в качестве второго предложения второго периода, возможно с вариациями, зависит от сложности (complexity
);3p
- простая трехчастная форма, состоит из двух вспомогательных периодов (1 и 3) и одного основного (второй). Третий период повторяет первый, возможно, с вариациями. Если сложность низкая, то второй период представляет собой повторение первого в подчиненной тональности. По мере увеличения сложности усложняется структура второго периода - квадратный повторного строения, квадратный неповторного строения, двойной;35p
- трехпятичастная форма. То же, что трехчастная, но второй и третий периоды повторяются;ornamental
- форма темы с вариациями (орнаментальная). Конструкция вида A + A1 + A2 + A3 + … Основная тема формируется в первом периоде, все последующие повторяют ее, внося изменения в любое предложение;rondo
- форма рондо. Крупная конструкция вида A + B + A + C + A + … Тема из первого периода (рефрен) повторяется раз за разом, между повторениями вставлены сторонние темы (эпизоды). В зависимости от сложности, эпизоды генерируются в основной либо подчиненных тональностях, от сложности также зависит размер всей конструкции.
Выгрузить получившиеся последовательности в MIDI можно методом make_midi()
, присутствующим у каждого из классов Motiff
, Phrase
, Sentence
. В метод передаются:
filename
- имя выгружаемого файла;track_name
- название MIDI-трека.
Ручная инкапсуляция структур
Генерация музыкальных последовательностей “сверху вниз”, когда мы создаем объект (к, примеру, предложение) и сразу вызываем метод create()
, это один из способов работы с данным пакетом. Никто не запрещает производить генерацию “снизу вверх”, когда отдельно создаются мотивы, фразы и т.д., которые потом можно инкапсулировать в объект, стоящий на более высокой иерархической ступени, и после работать уже с ним. Для этого у каждого класса есть ряд однотипных методов:
- Для фразы:
set_motiff(motiff, motiff_num
) - объект класса “Мотив”, переданный в полеmotiff
, вставляется на позицию под номеромmotiff_num
(не больше 2 для фразы). Обратите внимание, здесь, как и во всех прочих подобных методах, нумерация идет с 1; - Для предложения:
set_phrase(phrase, phrase_num)
иset_phrases(phrases)
, на случай, если необходимо задать не одну фразу, а сразу весь список; - Для периода:
set_sentence(sentence, sentence)num)
,set_sentences(sentences)
; - Для формы:
set_period(period, period_num)
,set_periods(periods)
.
Также, нет никаких ограничений на обращение непосредственно к объектам, находящимся внутри более крупных структур. Например:
sentence_1.phrases[1].develop()
Здесь мы вызвали метод develop()
для второй фразы, содержащейся в пердложении sentence_1
.