This document defines the coding standards and architectural patterns for the RAD (Reaction Analysis & Design) framework. Adherence to this guide ensures thread safety, high performance with ROOT RDataFrame, and maintainability.
- All code is contained within
.hfiles. - Strict Separation of Interface and Implementation:
- Class Body: Must contain only member variable and method declarations.
- Implementations: Must be placed outside the class body, at the bottom of the file.
- Inline Keyword: All implementations must be marked
inlineto prevent One Definition Rule (ODR) violations when included in multiple translation units.
Example:
namespace rad {
class MyClass {
public:
// Declaration only
void DoSomething(int value);
private:
int _internalValue;
};
// Implementation at bottom
inline void MyClass::DoSomething(int value) {
_internalValue = value;
}
}
- All code must reside within the
radnamespace (or sub-namespaces likerad::io,rad::physics). - Use
#pragma oncefor header guards. - Minimize Includes: Do not include headers inside class headers unless types are explicitly required in declarations. Forward declare if possible.
- Never utilize
using namespacein a header file (global scope pollution).
-
Prefer Standard Aliases: Use the type aliases defined in
CommonDefines.hrather than raw ROOT/STL types. This improves readability and facilitates global precision changes (e.g., swappingDouble_tforFloat_t). -
Indices:
- Use
Indices_tinstead ofROOT::RVecIorstd::vector<int>. - Use
RVecIndices(orRVecIndexMap) for nested structures (RVec<RVecI>).
- Use
-
Kinematics Data:
- Use
RVecResultTypeinstead ofROOT::RVec<double>orRVecDfor kinematic columns. - Use
ResultType_tinstead ofdoublefor scalar calculations.
- Use
-
Strings:
- Use
ParticleNames_tinstead ofstd::vector<std::string>.
- Use
Example:
// Good
void Calculate(const RVecIndices& map, const RVecResultType& px);
// Avoid
void Calculate(const ROOT::RVec<ROOT::RVecI>& map, const ROOT::RVec<double>& px);
- Namespaces:
lower_case(e.g.,rad,rad::io). - Classes:
PascalCase(e.g.,KinematicsProcessor,SnapshotCombi). - Methods:
PascalCase(e.g.,InitMap,GetReactionIndex,FinalizeTask). - Member Variables:
_camelCasewith leading underscore (e.g.,_fileName,_totalCount,_merger). - Local Variables:
snake_caseorcamelCase(be consistent within scope). - Template Parameters:
TorPascalCase(e.g.,Tp,RecordType). - Types/Aliases:
PascalCaseusually ending with_t(e.g.,IndexMap_t,ParticleNames_t). - Constants/Enums:
PascalCaseorkPascalCase(e.g.,ColType::Double,OrderX()).
All public methods and classes must have full Doxygen documentation using the Javadoc style /** ... */ block before the declaration.
@brief: A single-line summary.@details: (Recommended) Expanded explanation of logic, especially for complex RDataFrame operations or threading models.@param: Description of every argument.@return: Description of the return value (if not void).@throw: (Optional) Document specific exceptions (e.g.,std::runtime_error) if the method performs critical checks.
Example:
/** * @brief Forces an input particle to be registered in the map.
* @details Essential for Master processors when a Linked Clone needs a particle
* that the Master itself does not explicitly use.
* @param name The name of the input particle to require.
* @return True if registration was successful.
*/
bool RequireParticle(const std::string& name);
-
Prefer
ROOT::VecOps::RVec<T>(or the aliases inCommonDefines.h) overstd::vector<T>for all data that interacts with RDataFrame columns. -
This ensures seamless interoperability with JIT-compiled actions and vector arithmetic.
// Good void Process(const RVecResultType& px);
// Avoid (unless internal logic only) void Process(const std::vector& px);
-
Assume Multi-threading: All Actions and Processors must be thread-safe by design to support
ROOT::EnableImplicitMT(). -
Lock-Free Design: Avoid
std::mutexinsideExec()oroperator(). Use Thread-Local Storage or Pre-allocated Vectors indexed byslot(obtained viaInitTask). -
Pre-allocation Pattern:
// In Initialize(): const auto nSlots = ROOT::IsImplicitMTEnabled() ? ROOT::GetThreadPoolSize() : 1; _threadLocalData.resize(nSlots); // Pre-allocate to avoid race conditions
-
TDirectory Context: When opening ROOT files in worker threads (e.g.,
InitTask), always guard the global state to prevent crashing the Input Reader.void InitTask(TTreeReader*, unsigned int slot) { TDirectory::TContext c; // Scoped guard to prevent gDirectory pollution _files[slot] = _merger->GetFile(); }
-
Memory Stability: Use
std::unique_ptrfor buffers linked toTTree::Branch. This prevents memory address invalidation if the parent container resizes.std::vector<std::unique_ptr<Buffer_t>> _buffers;
Custom RDataFrame Actions (RActionImpl) must strictly follow the ROOT lifecycle to ensure correct initialization and data flushing:
- Constructor: Configuration only. No resource allocation.
Initialize(): Open files, allocate mergers, pre-size vectors (Main Thread).InitTask(slot): Connect thread to resources / Lazy initialization (Worker Thread).Exec(slot, ...): High-performance event loop (Worker Thread).FinalizeTask(slot): Flush thread-local buffers to merger (Worker Thread).Finalize(): Merge results, close files, and cleanup (Main Thread).
- Do not create arrays of objects (e.g.,
vector<TLorentzVector>). - Use Structure of Arrays (SoA): separate
RVecResultTypefor Px, Py, Pz, Mass. - This enables CPU auto-vectorization and better cache locality.
-
Avoid Redundant Math: Do not compute Energy (
$E = \sqrt{P^2 + M^2}$ ) inside combinatorial loops. Store and copy Mass instead. Compute Energy only when plotting/saving if strictly necessary. -
Functors: Implement processors as stateless functors (
operator()) that consumeRVecIndicesrather than objects.
- Never use raw
map::at()or[]operators on critical maps (like particle indices) without a wrapper check. - Use a "Safe Getter" pattern that throws a
std::runtime_errorwith a descriptive message including context (Prefix, Suffix, missing key).
- Output actions must handle the case where 0 events pass selections.
- Always ensure the output ROOT file contains the expected
TTreeheader (even if empty) to prevent downstream crashes.
- Because RDataFrame is lazy, complex chains (especially Snapshots) should be explicitly triggered via
rad_df.TriggerSnapshots()before the macro exits to guarantee data is flushed.
- Master: Configures the primary topology and definitions.
- CloneForType: Copies configuration to a different data stream (e.g., Rec -> Truth) by swapping prefixes.
- CloneLinked: Creates a new hypothesis on the same data stream by adding a unique suffix.
- When cloning across types (Rec -> Truth), use
PortGroupsFromto inspect the parent's groups and attempt to register equivalent groups for the new type automatically.
- Use
Redefine()when passing a C++ lambda or function pointer. - Use
RedefineExpr()when passing a string containing C++ code (JIT compilation).
- Prefer explicit argument types in lambdas over
autowhen dealing withRVecto ensure ROOT can infer types correctly during compilation. - Example:
[](const Indices_t& a) { ... }instead of[](auto a) { ... }.