1. Architecture Overview

RAD is a header-only framework designed around a Structure of Arrays (SoA) philosophy to maximize cache locality and allow efficient vectorization within the RDataFrame event loop.

AnalysisManager

The Orchestrator.
Manages Data Streams (Rec/Truth), Lambda Recipes, and Lifecycle Execution.

ConfigReaction / Interface

Wraps ROOT::RDataFrame.
Handles Combinatorics and Podio / HIPO Data Extraction.

KinematicsProcessor

The Engine.
Operates on raw SoA columns (Px, Py, Pz) using SIMD instructions.

Injector

Data Normalization

Creator

Topology & Grouping

Modifier

Calibrations & Smearing

2. Data Layout: Zero-Copy Combinatorics

To avoid deep copies of heavy particle objects (like TLorentzVector), RAD separates Topology (Indices) from Data (Momentum Arrays).

  • Input: Flat RVecs (rec_px, rec_py...) containing all tracks in the event.
  • Map: ReactionMap. A matrix of indices pointing to the flat arrays. The physics engine performs integer lookups directly.
  • Output: Variables are calculated column-wise in a single pass using SIMD vectorization.
// Visualization of ReactionMap // Scenario: 2 candidates for e- (indices 0, 2), 1 for e+ (index 5) IndexMap = { // e- e+ { 0, 5 }, // Combination 1 { 2, 5 } // Combination 2 }

3. Parallel I/O & Masking

Lock-Free TTree Writing

Writing TTrees from multiple threads typically causes segmentation faults. RAD handles this safely via SnapshotCombi, which is invoked by AnalysisManager::Snapshot().

  • Thread-Local Buffers: Pre-allocates buffers and TTree instances based on the thread pool size.
  • Context Safety: Guards gDirectory to prevent file operation crashes during the event loop.
  • Lazy Flattening: Flattens nested combinatorial outputs (Event → Combination) into a clean N-Tuple. Event-level scalars are automatically broadcasted.

Lazy Masking vs. Filtering

Standard Filter() drops events entirely, ruining sideband analyses. RAD employs a Masking strategy:

  • Selections generate a boolean mask column.
  • Trees and histograms only process entries where the mask is true, preserving underlying array alignment for complex topologies.

4. Extending the Framework

How to write and inject custom physics kernels into the recipe manager.

Writing Custom Physics Kernels

To add new physics variables (like Helicity Angles or specific Mandelstam variables), developers write a C++ function and register it inside the Topology Recipe. Because RAD uses lazy evaluation, you don't call this function directly to get a return value; instead, you register it as a named column in the RDataFrame graph.

1. The Implementation (Header File / Separate Cell)

Define your calculation in a separate header or macro cell before your main analysis script.

// MyPhysicsKernels.h namespace rad { namespace physics { // The function must accept the topology map and the SoA momentum arrays inline RVecResultType CalcMyAngle(const RVecIndexMap& map, const RVecResultType& px, const RVecResultType& py) { // A. Integer lookups using standard group names auto idx_ele = map[rad::consts::OrderScatEle()]; auto idx_pro = map[rad::consts::OrderBaryon()]; // B. Perform SIMD math across the vectors auto result = px[idx_ele] * py[idx_pro]; // example return result; } } }

2. The User Script (Analysis Macro)

Include your custom kernel and inject it via the Lambda recipes.

// MainAnalysis.C #include "MyPhysicsKernels.h" // 1. Inject it via the Topology Recipe Lambda auto topology_recipe = [](Processor& p) { // Registers the calculation as a new column named "MyAngleVal" p.RegisterCalc("MyAngleVal", rad::physics::CalcMyAngle); }; // 2. Call the registered calculation by its string name in downstream recipes auto histogram_recipe = [](rad::histo::Histogrammer& h) { // Use the "MyAngleVal" column to populate a 1D histogram h.Create("hMyAngle", "Custom Angle; #theta [rad]", 100, 0, 3.14, "MyAngleVal"); }; auto selection_recipe = [](rad::PhysicsSelection& s) { // Or use it to generate a lazy mask s.AddCutMin("AngleCut", "MyAngleVal", 0.5); };