Анализ физических событий в StromaV (I)

/ / sw-sci :: , , ,

Эта серия заметок содержит черновое описание API для анализа событий в экспериментальной физике. Планируется, что часть её я потом переведу на английский и размещу во внутренних Doxygen/(T)Wiki. API реализовано в библиотеке StromaV.

Библиотека StromaV предназначена для повторного использования в рамках различных экспериментов и содержит набор общих классов и процедур, основывающихся на достаточно генерализованных контрактах. Репозиторий StromaV, например, не содержит описания форматов данных, применяемых каким‐либо отдельным экспериментом.

Классы анализа данных в StromaV предлагают набор базовых контрактов чьи декларации исходят из следующих соображений:

  1. Пользовательский код работает с набором экземпляров относительно небольшой структуры данных — как минимум один экземпляр всегда умещается в памяти.
  2. Таких структур предусмотрено, в общем, две: структура данных симулированного события и структура данных экспериментального события.
  3. В наиболее общем случае пользовательскому коду требуется последовательно перебрать набор событий и произвести над каждым экземпляром одну и ту же последовательность операций.

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

Второе утверждение фиксируется с целью более связной интеграции с другими частями StromaV, предоставляющими обобщённую функциональность для работы с симулированными и реальными данными, — оба типа естественно присутствуют в концепции библиотеки и обозначить интроспекцию делается целесообразно.

Третье положение является одной из двух довольно расхожих практикой при анализе данных. Для экспериментальной статистики почти всегда нужно предусмотреть преобразования данных события, которые могут быть обобщены следующим образом:

Первый способ предусматривает фиксированные типы данных на входах и выходах некоторых слоёв абстракции разделённых по функциональному признаку (применение калибровок, вычитание пьедесталов, аппроксимацию сигнала, и т.д.). В такой концепции каждый последующий слой принимает и отдаёт различные структуры данных и таким образом C++ гарантирует совместимость таких модулей — если операция A принимает экземпляр «RawEvent» и возвращает экземпляр «EnergyDepostition_MeV», а операция B принимает «EnergyDepostition_MeV» и возвращает «EnergySum_MeV», то, очевидно, эти операции могут идти только в определённом порядке.

Второй способ предусматривает унифицированную структуру данных для каждой операции.

В StromaV мы применяем комбинированный подход, используя преимущества интроспекции экземпляров данных — событие представлено унифицированным типом позволяющим строго‐типизированные расширения посредством композиции. Главным, и, по существу, единственным недостатком такого подхода является необходимость изменения основной деклараций топологии события при добавлении новых данных в структуру события. Важно заметить, что, однако, существующие решения позволяют избежать значительного увеличения накладных расходов при обеспечении обратной совместимости.

Концепция положенная в основу инструментов анализа данных в StromaV подразумевает последовательное извлечение и обработку индивидуальных событий. В англоязычной номенклатуре, для такой организации классов можно встретить обозначение «pipeline».

Получение событий осуществляется экземпляром класса реализующего интерфейс последовательной вычитки iEventSequence. Экземпляр события затем передаётся списку обработчиков (хэндлеров), последовательно, в фиксированном порядке. Классы хэндлеров реализуют интерфейс обработчика событий iEventProcessor.

Размещение экземпляров‐обработчиков и ассоциирование управляется методами класса AnalysisPipeline.

Логика sV::AnalysisPipeline

Класс sV::AnalysisPipeline реализует расхожее решение подразумевающее наличие итерируемого источника данных (data source) и упорядоченной цепочки объектов‐обработчиков (handlers). В терминологии StromaV мы используем название «event sequence» для источника событий чтобы подчеркнуть единственное зафиксированное на данном уровне общности его свойство — однонаправленную итерируемость. Для обработчиков (handlers) применяется название «processors», поскольку «handler» часто подразумевает реактивность экземпляра‐обработчика.

UML диаграмма классов.

Экземпляр AnalysisPipeline реализует итерирование контрактора eventSequence типа iEventSequence и последовательно применение членов списка processorsChain типа iEventProcessor к извлечённому экземпляру события в методе process().

Унифицированное представление событий в StromaV

Типичный размер цифровой информации об отдельном событии зарегистрированном детектирующей установкой физического эксперимента колеблется от нескольких байт до нескольких десятков мегабайт. Эта информация, обычно, атомизирована: текущее событие не имеет данных зависящих от предыдущих событий, и формат хранения данных, таким образом, позволяет выбор между контейнерами AoS/SoA. С точки зрения ООП удобно представить такие данные в виде AoS, однако эффективный формат должен учитывать высокую разрежённость данных. Поскольку, как правило, далеко не все элементы установки и отдельных детекторов регистрируют значимую информацию, с целью экономии памяти формат обязан предусматривать её отсутствие.

Современное программное обеспечение предоставляет довольно широкий набор общих практик и частных инструментов для работы с данными такого вида. В частности ROOT предлагает как минимум два инструмента:

  • ROOT TTree — стандартный контейнер ROOT для хранения массива структур. Подразумевает декларацию топологии при помощи специальной лексики, имеет ряд особенностей использования связанных с физическим уровнем представления.
  • ROOT Reflex — позволяет сериализовать, в принципе, произвольный класс при помощи предварительно сгенерированных словарей интроспекции.

Хранение событий в формате TTree — довольно популярное решение, но оно накладывает довольно существенные ограничения:

  • Отсутствие обратной совместимости сериализуемых структур.
  • Базовая лексика не поддерживает описание наследования.
  • Сложный синтаксис описания топологии структур поддерживающий разрежённые данные.
  • Отсутствие поддержки других (кроме C++/CLANG) языковых сред1).

Учитывая характерные особенности данных о физических событиях, а так же необходимость работать с ними не только в C++, было принято решение использовать одно из популярных промышленных решений для сериализации данных. Обзор таких решений находится за рамками этой заметки (см. напр. Apache Thrift, ZeroMQ). Выбор был сделан в пользу google protocol buffers (protobufs) во многом благодаря развитой поддержке в различных языковых средах. Следует отметить, что сравнительную эффективность API protobufs среди похожих решений можно признать только удовлетворительной.

Класс sV::Event автоматически генерируется утилитой protoc на основе декларации содержащейся в файле event.proto:

message Event {
    oneof uevent {
        SimulatedEvent simulated = 1;
        ExperimentalEvent experimental = 5;
        bytes blob = 7;
    }
    Displayable displayableInfo = 8;
}

Эта декларация сформулирована на основе следующих предположений:

  • Отдельный экземпляр события может содержать информацию либо о смоделированном событии SimulatedEvent, либо об экспериментальном событий ExperimentalEvent, либо произвольные данные (байтовое поле blob).
  • Отдельный экземпляр события может иметь сопроводительную инфрмацию Displayable, предназначенную для оперативных решений, но не относящихся к физическим данным.

Случай, когда экспериментальное событие сопровождается модельным данными в StromaV не рассматривается, и должен быть предусмотрен пользовательским кодом вне базового API.

Интерфейс формата входных данных

StromaV не предполагает за экземплярами событий более детальной топологии, однако, поскольку всякий раз классы этих экземпляров представлены сообщениями protobuf, возникает общая нужда кэширования десериализованных данных при первом обращении с тем чтобы всякое последующее не вело к накладным расходам, а так же инвалидации этого кэша при вычитке следующего события в последовательности. Кроме того, при необходимоти сохранения обработанного события, общей для пользовательского кода надобностью является сериализации измененного кэша. Для этих целей предусмотрен шаблонный класс iEventPayloadProcessor, а так же контейнеры коллбэков для вспомогательных операций в AnalysisPipeline.

Примером класса‐контрактора iEventSequence может являться класс‐обёртка сетевого сокета осуществляющего последовательное чтение приходящих в реальном времени событий. Хотя в большинстве практических приложений плотность данных обычно слишком высока чтобы производить над ними какие‐то ресурсоёмкие операции, потребность считывания событий с потерями может возникнуть при организации онлайн‐монитора событий, когда актуальность считываемых данных важнее их целостности.

Класс‐наследник iEventSequence реализующий (де)кодирование событий получаемых из источника должен определить как минимум четыре метода:

Event * _V_initialize_reading();

Метод должен инициализировать внутреннее состояние экземпляра перед началом последовательной обработки. Предназначен для открытия файлов, выделения кэшей инициализация внутренних счётчиков, дескрипторов сокетов и т.д. Метод должен возвращать указатель на экземпляр события.

bool _V_is_good();

Метод должен возвращать true в случае, когда источник данных позволяет извлечь ещё как минимум одно событие (т.е., например, конец файла не был достигнут при последовательном чтении, и гарантируется наличие хотя бы одного события в нём, буфер сокета содержит ещё как минимум одно событие и т.д.).

void _V_next_event( Event *& );

Метод должен осуществлять вычитку следующего в очереде события. В общем случае, актуальный экземпляр содержащий информацию о событии может быть изменён, поэтому его передача осуществляется по ссылке на указатель.

void _V_finalize_reading();

Метод осуществляет освобождение ресурсов занятых экземпляром источника данных. Здесь должно быть произведено закрытие файлов, освобождение кэшей, дескрипторов сокетов и т.д.

Следует иметь в виду, что в течение одного запуска приложения использующего pipeline возможно неограниченное число циклов обработки данных включающих вызов пары методов _V_initialize_reading()/_V_finalize_reading().

Интерфейс обработчика событий

Класс-наследник iEventProcessor должен определить как минимум один метод:

bool _V_process_event( Event * );

Метод принимает указатель на экземпляр содержащий текущее событие и должен возвращать true если обработка события может быть продолжена последующими обработчиками в цепочке.

Вспомогательный класс обработчиков

StromaV не делает никаких предположений о содержимом полей simulated и experimental класса Event:

message SimulatedEvent {
    google.protobuf.Any payload = 1;
}

message ExperimentalEvent {
    google.protobuf.Any payload = 1;
}

В лексике Protocol Buffers версии 3 наследование структур было исключено. Вместо этого рекомендуется (см. замечание «if you are already familiar with») использовать поле произвольного типа google.protobuf.Any.

Распаковка конкретных структур в таких полях может приводит, как правило, к некоторым накладным расходам, если производится несколько раз в течение цикла обработки одного события. Чтобы исключить многократную распаковку, в StromaV предусмотрен вспомогательный шаблонный класс параметризуемый конкретным подклассом экспериментального (или симулированного) события PayloadT.

UML диаграмма классов.

Механизм кэширования включает в себя функции обратных вызовов для запаковки или распаковки пользовательского класса PayloadT и производит ленивое кэширование структур преобразованных к пользовательскому типу, связывая их время жизни с конкретным экземпляром AnalysisPipeline. Тем самым гарантируется валидность кэша от момента первого обращения, до окончания цикла обработки текущего события.

Расширения класса источников данных

StromaV предоставляет обобщённые инструменты для организации метаданных извлекаемых на основе экспериментальной статистики. Основная функция метаданных заключается в индексировании позиционной информации о физическом расположении информации о конкретном событии внутри различных артефактов (файлов или БД) с тем чтобы обеспечить возможность формирования запросов по различным критериям нарду с произвольным доступом.

UML диаграмма классов.

Базовый интерфейс источника данных поддерживающего произвольный доступ формируется параметризацией шаблонного класса iRandomAccessEventSource на основе конкретных типов идентификатора событий и типа индексируемых метаданных.

Нередко в практических приложениях возникает необходимость обеспечения произвольного доступа к определённому событию. Если физический источник данных (артефакт1)) принципиально позволяет организацию такого доступа (например являясь файлом на жёстком диске), StromaV предоставляет набор обобщений на некотором прикладном уровне организующих интеграцию метаданных и поисковой системой. Наиболее общий интерфейс такого источника данных описан в шаблонном интерфейсе iRandomAccessEventSource. Более частная специализация этого интерфейса должна подразумевать как минимум два общих случая: в зависимости от того нуждается ли артефакт источник данных в дополнительной информации о своей внутренней структуре.

В том случае, если программный интерфейс может быть инкапсулирован одним классом без значительных накладных расходов (напр. реализует запросы к БД) предполагается использование класса iBulkEventSource.

В противном случае, когда метаданные велики, фрагментированны или не могут быть приобретены за короткое время, на текущем архитектурном уровне может быть целесообразно предоставить пользовательскому коду доступ к идентификатором подресурса.

Например, в течение экспериментальной сессии статистика записывается в несколько файлов для которых, кроме пути размещения, задана определённая внутрення структура опредлённая режимом работы ускорителя подающего пучок в импульсном режиме. Внутри каждого импульса событие может быть однозначно идентифицировано порядковым номером. Таким образом, естественный идентификатор события состоял бы из номера сессии, номера файла, номера импульса и номера события.

Извлекать метаданные для накопленной статистики сразу и для всех сессий может быть нецелесообразным по причине того что накопление данных не закончено (а значит нужно предусмотреть возможность дополнения метаданных), определённые сеансы не представляют интереса, и, наконец, объём статистики настолько велик, что извлечение метаданных на одном узле неосуществимо. Для таких операций StromaV вводит набор шаблонных интерфейсов: iCachedMetadataType, реализующий логику запросов к интерфейсу iMetadataStore для идентификации фрагментированных данных, доступ к которым предоставляется абстрактным классом iSectionalEventSource.

Более детальное рассмотрение подсистемы организации метаданных будет дано в дальнейших заметках.

Резюме

  • StromaV предлагает общую модель организации кода для обработки событий в виде последовательного конвейера с одним источником и упорядоченным набором независимых классов‐обработчиков.
  • Не делается никаких предположений относительно источника данных, за исключением возможности последовательного перебора (однонаправленного выводного итерирования) событий.
  • StromaV подразумевает разделение типов событий на экспериментальные и модельные. Их идентификация опирается на механизм интроспекции protobuf и архитектурной обязанностью данного уровня абстракции является предоставление инструментального решения для дальнейшей идентификации в терминах StromaV, которое реализованно в шаблонном классе iEventPayloadProcessor параметризуемом типом загрузочных данных.

Хотя концепция «pipeline» предусматривает определённый произвол в определении полномочий отдельных классов‐обработчиков, ведущим принципом при построении таких классов является группировка на основе минимальной функциональной идентичности (известный принцип системного программирования UNIX «делать одно дело и делать его хорошо»). Наиболее общий контракт диктуемый классом AnalysisPipeline выступающим здесь в роли диспетчера событий закреплён абстрактным классом iEventProcessor и требует явной реализации метадов process_event() принимающего указатель на экземпляр события имея право модификации. В контракте так же определяются извещения об окончании обработки события (метод finalize_event()) и о завершении последовательности finalize(). Их введение обусловлено в большей степени инструментальным удобством — как правило классы‐процессоры ассоциируются со внешними ресурсами, которые следует оперативно освобождать для уменьшения накладных расходов (закрытие файла, заполнение гистограмм, сериализация, и т.д.).


Примечания

1. Хотя pyROOT и предоставляет автоматически сгенерированные обёртки на Python, десериализация осуществляется не в native structures.

2. Мы будем использовать термин «artefact» из UML 2.0 с тем чтобы избежать неоднозначности связанной с определением «физический источник данных». Здесь имеется в виду экземпляр на физическом носителе — файл, запись на стримере и пр.


Comments