Skip to the content.

Plugin Development Guide

This guide explains how to add new features to RDFAnalyzerCore by creating custom plugins.

Table of Contents

Overview

The plugin system allows you to extend RDFAnalyzerCore with new functionality without modifying the core framework. Plugins are:

When to Create a Plugin

Create a plugin when you need to:

Don’t create a plugin for:

Plugin Types

1. Named Object Manager

Manages a collection of named objects (models, corrections, etc.).

Base Class: NamedObjectManager<T>

Examples: BDTManager, OnnxManager, CorrectionManager

When to use: You need to load, store, and apply multiple configured objects.

2. Service Plugin

Performs a specific service during analysis (counting, validation, etc.).

Base Class: IPluggableManager + IContextAware

Examples: CounterService, TriggerManager

When to use: You need to perform systematic operations or collect information.

3. Output Plugin

Manages specialized output or metadata collection.

Base Class: IPluggableManager + IContextAware

Examples: NDHistogramManager

When to use: You need custom output formats or complex metadata.

Step-by-Step: Creating a Plugin

Let’s create a complete example plugin: WeightManager that applies event weights from external JSON files.

Step 1: Define the Interface

Create core/plugins/WeightManager/IWeightManager.h:

#pragma once

#include <api/IPluggableManager.h>
#include <string>
#include <vector>

/**
 * Interface for weight management
 */
class IWeightManager : public IPluggableManager {
public:
    virtual ~IWeightManager() = default;
    
    /**
     * Apply a specific weight to the DataFrame
     * @param weightName Name of the weight configuration
     */
    virtual void applyWeight(const std::string& weightName) = 0;
    
    /**
     * Apply all configured weights
     */
    virtual void applyAllWeights() = 0;
    
    /**
     * Get the list of all available weight names
     */
    virtual std::vector<std::string> getWeightNames() const = 0;
    
    /**
     * Check if a weight exists
     */
    virtual bool hasWeight(const std::string& name) const = 0;
};

Step 2: Implement the Manager

Create core/plugins/WeightManager/WeightManager.h:

#pragma once

#include "IWeightManager.h"
#include <api/IConfigurationProvider.h>
#include <api/IContextAware.h>
#include <api/ManagerContext.h>
#include <unordered_map>
#include <memory>

// External library for reading weights (hypothetical)
#include <nlohmann/json.hpp>

/**
 * Manager for applying event weights from JSON configurations
 */
class WeightManager : public IWeightManager, public IContextAware {
public:
    /**
     * Constructor
     * @param config Configuration provider
     */
    explicit WeightManager(IConfigurationProvider& config);
    
    // IPluggableManager interface
    void initialize() override;
    void finalize() override;
    std::string getName() const override { return "WeightManager"; }
    
    // IContextAware interface
    void setContext(const ManagerContext& ctx) override;
    
    // IWeightManager interface
    void applyWeight(const std::string& weightName) override;
    void applyAllWeights() override;
    std::vector<std::string> getWeightNames() const override;
    bool hasWeight(const std::string& name) const override;

private:
    struct WeightConfig {
        std::string name;           // Output column name
        std::string file;           // JSON file path
        std::string weightKey;      // Key in JSON
        std::vector<std::string> inputVariables;  // Input column names
    };
    
    // Load weight configurations from config file
    void loadWeights();
    
    // Parse a single weight configuration
    WeightConfig parseWeightConfig(const std::map<std::string, std::string>& cfg);
    
    // Load JSON weight file
    nlohmann::json loadWeightFile(const std::string& path);
    
    IConfigurationProvider* config_;
    ManagerContext* context_;
    
    std::unordered_map<std::string, WeightConfig> weights_;
    std::unordered_map<std::string, nlohmann::json> loadedFiles_;
};

Step 3: Implement the Manager

Create core/plugins/WeightManager/WeightManager.cc:

#include "WeightManager.h"
#include <fstream>
#include <stdexcept>
#include <sstream>

WeightManager::WeightManager(IConfigurationProvider& config)
    : config_(&config), context_(nullptr) {
    loadWeights();
}

void WeightManager::setContext(const ManagerContext& ctx) {
    context_ = const_cast<ManagerContext*>(&ctx);
}

void WeightManager::initialize() {
    if (!context_) {
        throw std::runtime_error("WeightManager: Context not set");
    }
    context_->logger.info("WeightManager initialized with " + 
                         std::to_string(weights_.size()) + " weights");
}

void WeightManager::finalize() {
    context_->logger.info("WeightManager finalized");
}

void WeightManager::loadWeights() {
    if (!config_->has("weightConfig")) {
        return;  // No weights configured
    }
    
    std::string weightConfigFile = config_->get("weightConfig");
    auto weightConfigs = config_->getMultiKeyConfigs(weightConfigFile);
    
    for (const auto& cfg : weightConfigs) {
        WeightConfig wc = parseWeightConfig(cfg);
        weights_[wc.name] = wc;
        
        // Load JSON file if not already loaded
        if (loadedFiles_.find(wc.file) == loadedFiles_.end()) {
            loadedFiles_[wc.file] = loadWeightFile(wc.file);
        }
    }
}

WeightManager::WeightConfig WeightManager::parseWeightConfig(
    const std::map<std::string, std::string>& cfg) {
    
    WeightConfig wc;
    wc.name = cfg.at("name");
    wc.file = cfg.at("file");
    wc.weightKey = cfg.at("weightKey");
    
    // Parse comma-separated input variables
    std::string inputVarsStr = cfg.at("inputVariables");
    std::stringstream ss(inputVarsStr);
    std::string var;
    while (std::getline(ss, var, ',')) {
        wc.inputVariables.push_back(var);
    }
    
    return wc;
}

nlohmann::json WeightManager::loadWeightFile(const std::string& path) {
    std::ifstream file(path);
    if (!file.is_open()) {
        throw std::runtime_error("Failed to open weight file: " + path);
    }
    
    nlohmann::json j;
    file >> j;
    return j;
}

void WeightManager::applyWeight(const std::string& weightName) {
    auto it = weights_.find(weightName);
    if (it == weights_.end()) {
        throw std::runtime_error("Weight not found: " + weightName);
    }
    
    const WeightConfig& wc = it->second;
    const nlohmann::json& weightData = loadedFiles_.at(wc.file);
    
    // Create lambda that captures weight data
    auto weightFunc = [weightData, wc](double pt, double eta) -> double {
        // Look up weight in JSON based on pt/eta
        // (Simplified example - real implementation would be more complex)
        std::string key = std::to_string(int(pt / 10)) + "_" + 
                         std::to_string(int(eta * 10));
        
        if (weightData[wc.weightKey].contains(key)) {
            return weightData[wc.weightKey][key].get<double>();
        }
        return 1.0;  // Default weight
    };
    
    // Apply weight to DataFrame
    context_->dataManager.Define(
        wc.name,
        weightFunc,
        wc.inputVariables,
        context_->systematicManager
    );
    
    context_->logger.info("Applied weight: " + weightName);
}

void WeightManager::applyAllWeights() {
    for (const auto& [name, _] : weights_) {
        applyWeight(name);
    }
}

std::vector<std::string> WeightManager::getWeightNames() const {
    std::vector<std::string> names;
    for (const auto& [name, _] : weights_) {
        names.push_back(name);
    }
    return names;
}

bool WeightManager::hasWeight(const std::string& name) const {
    return weights_.find(name) != weights_.end();
}

Step 4: Create CMakeLists.txt

Create core/plugins/WeightManager/CMakeLists.txt:

# WeightManager Plugin

add_library(WeightManager SHARED
    WeightManager.cc
)

target_link_libraries(WeightManager
    PRIVATE
    RDFCore
    # Add any external dependencies (e.g., nlohmann_json)
)

target_include_directories(WeightManager
    PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}
)

# Install the library
install(TARGETS WeightManager
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
    RUNTIME DESTINATION bin
)

# Install headers
install(FILES
    WeightManager.h
    IWeightManager.h
    DESTINATION include/plugins/WeightManager
)

Step 5: Register in Core Build

Add to core/plugins/CMakeLists.txt:

add_subdirectory(WeightManager)

# Link to core
target_link_libraries(RDFCore PUBLIC WeightManager)

Step 6: Create Configuration Format

Document the configuration format. Create docs/WEIGHT_MANAGER.md:

file=path/to/weights.json weightKey=scale_factors name=my_weight inputVariables=pt,eta

Add to main config:

weightConfig=cfg/weights.txt

JSON Weight File Format

{
  "scale_factors": {
    "pt_eta_bin": weight_value,
    "20_0": 1.05,
    "20_5": 1.03,
    ...
  }
}

Step 7: Use in Analysis

In your analysis code:

#include <analyzer.h>
#include <plugins/WeightManager/IWeightManager.h>

int main(int argc, char** argv) {
    // Create analyzer with plugin
    Analyzer analyzer(argv[1]);
    
    // Define input variables
    analyzer.Define("pt", ...);
    analyzer.Define("eta", ...);
    
    // Get and use plugin
    auto* weightMgr = analyzer.getPlugin<IWeightManager>("weight");
    if (weightMgr) {
        weightMgr->applyAllWeights();
        // Now "my_weight" column exists
    }
    
    // Use the weight
    analyzer.Define("total_weight",
        [](double gen_weight, double my_weight) {
            return gen_weight * my_weight;
        },
        {"genWeight", "my_weight"}
    );
    
    analyzer.save();
    return 0;
}

Advanced Plugin Patterns

Pattern 1: Deferred Application

Load configurations at construction, but defer DataFrame operations:

class MyManager {
public:
    MyManager(IConfigurationProvider& config) {
        // Load configs early
        loadConfigs();
    }
    
    void applyModel(const std::string& name) {
        // Apply to DataFrame later
        context_->dataManager.Define(...);
    }
};

Why: Allows user to control when operations are applied, useful for dependency management.

Pattern 2: Multi-Output Operations

Create multiple output columns from one operation:

void MyManager::applyMultiOutput(const std::string& name) {
    // Define multiple outputs
    context_->dataManager.Define(name + "_output0", ...);
    context_->dataManager.Define(name + "_output1", ...);
    context_->dataManager.Define(name + "_output2", ...);
    
    // Optional: Create aggregate column
    context_->dataManager.Define(name + "_all",
        [](float out0, float out1, float out2) {
            return std::vector<float>{out0, out1, out2};
        },
        {name + "_output0", name + "_output1", name + "_output2"}
    );
}

Pattern 3: Conditional Execution

Skip expensive operations based on a condition:

void MyManager::applyConditional(const std::string& name, 
                                 const std::string& runVar) {
    context_->dataManager.Define(name,
        [this, name](bool should_run, auto&&... inputs) {
            if (!should_run) return -1.0f;  // Skip
            return this->expensiveComputation(inputs...);
        },
        {runVar, /* input columns */}
    );
}

Pattern 4: Systematic Variations

Support systematic uncertainties:

void MyManager::applyWithSystematics(const std::string& name) {
    // Get systematic variations
    auto systematics = context_->systematicManager.getSystematicNames();
    
    for (const auto& sys : systematics) {
        std::string varName = name + "_" + sys;
        
        context_->dataManager.Define(varName,
            [sys](auto&&... inputs) {
                // Apply systematic variation
                if (sys == "up") return compute(inputs...) * 1.1;
                if (sys == "down") return compute(inputs...) * 0.9;
                return compute(inputs...);  // Nominal
            },
            {/* input columns */}
        );
    }
}

Pattern 5: Metadata Collection

Collect information during finalize:

class MetadataCollector : public IPluggableManager, public IContextAware {
public:
    void collectData() {
        // Store data for later
        metadata_["event_count"] = event_count_;
        metadata_["sum_weights"] = sum_weights_;
    }
    
    void finalize() override {
        // Write to output
        TTree* tree = new TTree("metadata", "Analysis Metadata");
        for (const auto& [key, value] : metadata_) {
            tree->Branch(key.c_str(), &value);
        }
        tree->Fill();
        
        context_->metaSink.writeObject(tree, "metadata_tree");
    }

private:
    std::map<std::string, double> metadata_;
    int event_count_ = 0;
    double sum_weights_ = 0.0;
};

Pattern 6: Thread-Safe Operations

Ensure thread safety for parallel processing:

class ThreadSafeManager {
public:
    void applyOperation(const std::string& name) {
        // Capture thread-safe objects
        auto sharedResource = std::make_shared<ThreadSafeResource>();
        
        context_->dataManager.Define(name,
            [sharedResource](auto&&... inputs) {
                // Each thread gets a copy of the shared_ptr
                // Resource is thread-safe internally
                return sharedResource->compute(inputs...);
            },
            {/* columns */}
        );
    }
};

Testing Your Plugin

Unit Tests

Create core/plugins/WeightManager/test_WeightManager.cc:

#include <gtest/gtest.h>
#include "WeightManager.h"
#include <ConfigurationManager.h>

TEST(WeightManagerTest, Construction) {
    ConfigurationManager config("test_config.txt");
    WeightManager mgr(config);
    EXPECT_EQ(mgr.getName(), "WeightManager");
}

TEST(WeightManagerTest, LoadWeights) {
    ConfigurationManager config("test_config.txt");
    WeightManager mgr(config);
    
    EXPECT_TRUE(mgr.hasWeight("test_weight"));
    EXPECT_FALSE(mgr.hasWeight("nonexistent"));
}

TEST(WeightManagerTest, GetWeightNames) {
    ConfigurationManager config("test_config.txt");
    WeightManager mgr(config);
    
    auto names = mgr.getWeightNames();
    EXPECT_EQ(names.size(), 1);
    EXPECT_EQ(names[0], "test_weight");
}

TEST(WeightManagerTest, ApplyWeight) {
    // Create test configuration
    ConfigurationManager config("test_config.txt");
    WeightManager mgr(config);
    
    // Create mock context
    // ... set up DataManager, etc.
    
    EXPECT_NO_THROW(mgr.applyWeight("test_weight"));
}

Integration Tests

Test with full analysis workflow:

TEST(WeightManagerIntegrationTest, EndToEnd) {
    // Create analyzer with plugin
    std::unordered_map<std::string, std::unique_ptr<IPluggableManager>> plugins;
    plugins["weight"] = std::make_unique<WeightManager>(config);
    
    Analyzer analyzer("test_config.txt", std::move(plugins));
    
    // Define variables
    analyzer.Define("pt", ...);
    analyzer.Define("eta", ...);
    
    // Apply weight
    auto* weightMgr = analyzer.getPlugin<IWeightManager>("weight");
    ASSERT_NE(weightMgr, nullptr);
    weightMgr->applyAllWeights();
    
    // Check output exists
    auto df = analyzer.getDataFrame();
    EXPECT_TRUE(df.HasColumn("my_weight"));
}

Test Configuration Files

Create core/test/cfg/test_weights.txt:

file=core/test/cfg/test_weight_data.json weightKey=factors name=test_weight inputVariables=pt,eta

Create core/test/cfg/test_weight_data.json:

{
  "factors": {
    "20_0": 1.05,
    "30_5": 1.03
  }
}

Running Tests

cd build
ctest -R WeightManager -V

Best Practices

1. Interface Design

2. Configuration Handling

3. Resource Management

4. Error Handling

5. Thread Safety

6. Performance

7. Documentation

Examples

Example 1: Simple Service Plugin

// Header
class ValidationService : public IPluggableManager, public IContextAware {
public:
    void initialize() override;
    void finalize() override;
    std::string getName() const override { return "ValidationService"; }
    void setContext(const ManagerContext& ctx) override { context_ = &ctx; }
    
    void validateColumn(const std::string& column, 
                       std::function<bool(double)> validator);

private:
    ManagerContext* context_;
    std::vector<std::string> failures_;
};

// Implementation
void ValidationService::validateColumn(
    const std::string& column,
    std::function<bool(double)> validator) {
    
    auto df = context_->dataManager.getDataFrame();
    
    auto validationResult = df.Define("__valid__",
        [validator](double value) { return validator(value); },
        {column}
    ).Filter([](bool valid) { return valid; }, {"__valid__"});
    
    auto passed = validationResult.Count();
    auto total = df.Count();
    
    if (*passed != *total) {
        std::string msg = "Validation failed for " + column + 
                         ": " + std::to_string(*passed) + "/" + 
                         std::to_string(*total) + " passed";
        context_->logger.warn(msg);
        failures_.push_back(msg);
    }
}

void ValidationService::finalize() {
    if (!failures_.empty()) {
        context_->logger.error("Validation failures occurred:");
        for (const auto& failure : failures_) {
            context_->logger.error("  " + failure);
        }
    }
}

Example 2: Named Object Manager

// Simple scale factor manager
class ScaleFactorManager : public NamedObjectManager<std::function<double(double, double)>> {
public:
    ScaleFactorManager(IConfigurationProvider& config);
    
    void loadObjects(const std::vector<std::map<std::string, std::string>>& configs) override;
    void applyScaleFactor(const std::string& name);
    void applyAllScaleFactors();

private:
    std::unordered_map<std::string, std::vector<std::string>> inputs_;
};

void ScaleFactorManager::loadObjects(
    const std::vector<std::map<std::string, std::string>>& configs) {
    
    for (const auto& cfg : configs) {
        std::string name = cfg.at("name");
        
        // Create scale factor function
        // (Simplified - real implementation would load from file)
        auto sfFunc = [](double pt, double eta) {
            return 0.95 + 0.001 * pt;  // Example
        };
        
        objects_[name] = sfFunc;
        
        // Store input variable names
        std::string inputStr = cfg.at("inputVariables");
        inputs_[name] = parseCommaSeparated(inputStr);
    }
}

void ScaleFactorManager::applyScaleFactor(const std::string& name) {
    auto sfFunc = getObject(name);
    const auto& inputs = inputs_.at(name);
    
    context_->dataManager.Define(name, sfFunc, inputs, 
                                context_->systematicManager);
}

Common Pitfalls

1. Not Calling setContext

Problem: Context is null when trying to use it.

Solution: Ensure Analyzer calls setContext before initialize.

2. Applying Before Inputs Exist

Problem: DataFrame column doesn’t exist when defining operation.

Solution: Document that input variables must be defined first, or defer operation.

3. Non-Thread-Safe Operations

Problem: Crashes or wrong results with ImplicitMT enabled.

Solution: Use thread-safe objects or synchronization.

4. Memory Leaks

Problem: Resources not cleaned up.

Solution: Use RAII, smart pointers, and implement finalize correctly.

5. Missing Error Handling

Problem: Silent failures or cryptic error messages.

Solution: Validate inputs and provide clear error messages.

Next Steps

Happy plugin development!