Skip to content

Commit ec29fea

Browse files
barnaliyknoepfelgithub-actions[bot]pcanal
authored
Add FORM output module for phlex product_store integration (#177)
* Add FORM output module for phlex product persistence Implements FormOutputModule that integrates phlex's product_store with FORM's persistence layer. The module extracts products from phlex stores and writes them using configurable FORM backend technologies. Key changes: - form_module.cpp: New output module with phlex product_store integration - Updates to form/CMakeLists.txt to build form_module library - Links against phlex::module and phlex::experimental interfaces Note: Requires phlex standalone build modifications (submitted separately) * Change to form::experimental::product_with_name * Add test_helpers.hpp with createTypeMap implementation for FORM tests * Remove mock_phlex directory * Add integration test for form_module plugin. This test exercises FormOutputModule by running a Phlex workflow that uses the form_module plugin, improving code coverage for form_module.cpp. The test follows the same pattern as test/plugins, using a jsonnet configuration file to instantiate and run the module through Phlex. * form: Properly store the TTree in its directory * Implementing Peter's suggestion: switch from sprintf to snprintf * Form: register more supported type. This is a temporary work-around until std::type_info is part of the Phlex API. Adding support for: long, float, double and std::vector of int, long, float and double. * Read product names from the jsonnet config 'products' field, making it generic for any configured products. --------- Co-authored-by: Kyle Knoepfel <knoepfel@fnal.gov> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Philippe Canal <pcanal@fnal.gov>
1 parent 57dcdf2 commit ec29fea

15 files changed

Lines changed: 396 additions & 259 deletions

form/CMakeLists.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
# ##############################################################################
99
# Copyright (C) 2025 ...
1010

11+
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
12+
1113
include_directories(${PROJECT_SOURCE_DIR}/form)
1214

1315
# ROOT Storage toggle
@@ -18,11 +20,16 @@ endif()
1820

1921
# Add sub directories
2022
add_subdirectory(form)
21-
add_subdirectory(mock_phlex)
2223
add_subdirectory(core)
2324
add_subdirectory(util)
2425
add_subdirectory(persistence)
2526
add_subdirectory(storage)
2627
if(FORM_USE_ROOT_STORAGE)
2728
add_subdirectory(root_storage)
2829
endif()
30+
31+
add_library(form_module MODULE form_module.cpp)
32+
33+
target_link_libraries(form_module PRIVATE phlex::module form)
34+
35+
target_include_directories(form_module PRIVATE ${PROJECT_SOURCE_DIR})

form/form/form.cpp

Lines changed: 37 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,88 +4,85 @@
44

55
namespace form::experimental {
66

7-
// Accept and store config
8-
form_interface::form_interface(std::shared_ptr<mock_phlex::product_type_names> tm,
9-
mock_phlex::config::parse_config const& config) :
7+
form_interface::form_interface(std::shared_ptr<product_type_names> tm,
8+
config::output_item_config const& output_config,
9+
config::tech_setting_config const& tech_config) :
1010
m_pers(nullptr), m_type_map(tm)
1111
{
12-
// Convert phlex config to form config
13-
form::experimental::config::output_item_config output_items;
14-
for (auto const& phlex_item : config.getItems()) {
15-
output_items.addItem(phlex_item.product_name, phlex_item.file_name, phlex_item.technology);
16-
m_product_to_config.emplace(
17-
phlex_item.product_name,
18-
form::experimental::config::PersistenceItem(
19-
phlex_item.product_name, phlex_item.file_name, phlex_item.technology));
12+
for (auto const& item : output_config.getItems()) {
13+
m_product_to_config.emplace(item.product_name,
14+
form::experimental::config::PersistenceItem(
15+
item.product_name, item.file_name, item.technology));
2016
}
2117

22-
config::tech_setting_config tech_config_settings;
23-
tech_config_settings.file_settings = config.getFileSettings();
24-
tech_config_settings.container_settings = config.getContainerSettings();
25-
2618
m_pers = form::detail::experimental::createPersistence();
27-
m_pers->configureOutputItems(output_items);
28-
m_pers->configureTechSettings(tech_config_settings);
19+
m_pers->configureOutputItems(output_config);
20+
m_pers->configureTechSettings(tech_config);
2921
}
3022

31-
void form_interface::write(std::string const& creator, mock_phlex::product_base const& pb)
23+
void form_interface::write(std::string const& creator,
24+
std::string const& segment_id,
25+
product_with_name const& pb)
3226
{
33-
// Look up creator from PersistenceItem.
27+
3428
auto it = m_product_to_config.find(pb.label);
3529
if (it == m_product_to_config.end()) {
3630
throw std::runtime_error("No configuration found for product: " + pb.label);
3731
}
3832

3933
std::string const type = m_type_map->names[pb.type];
40-
// FIXME: Really only needed on first call
34+
4135
std::map<std::string, std::string> products = {{pb.label, type}};
4236
m_pers->createContainers(creator, products);
37+
4338
m_pers->registerWrite(creator, pb.label, pb.data, type);
44-
m_pers->commitOutput(creator, pb.id);
39+
40+
m_pers->commitOutput(creator, segment_id);
4541
}
4642

47-
// Look up creator from config
4843
void form_interface::write(std::string const& creator,
49-
std::vector<mock_phlex::product_base> const& batch)
44+
std::string const& segment_id,
45+
std::vector<product_with_name> const& products)
5046
{
51-
if (batch.empty())
47+
48+
if (products.empty())
5249
return;
5350

54-
// Look up creator from config based on product name. O(1) lookup instead of loop
55-
auto it = m_product_to_config.find(batch[0].label);
51+
auto it = m_product_to_config.find(products[0].label);
5652
if (it == m_product_to_config.end()) {
57-
throw std::runtime_error("No configuration found for product: " + batch[0].label);
53+
throw std::runtime_error("No configuration found for product: " + products[0].label);
5854
}
5955

6056
// FIXME: Really only needed on first call
61-
std::map<std::string, std::string> products;
62-
for (auto const& pb : batch) {
57+
std::map<std::string, std::string> product_types;
58+
for (auto const& pb : products) {
6359
std::string const& type = m_type_map->names[pb.type];
64-
products.insert(std::make_pair(pb.label, type));
60+
product_types.insert(std::make_pair(pb.label, type));
6561
}
66-
m_pers->createContainers(creator, products);
67-
for (auto const& pb : batch) {
62+
63+
m_pers->createContainers(creator, product_types);
64+
65+
for (auto const& pb : products) {
6866
std::string const& type = m_type_map->names[pb.type];
6967
// FIXME: We could consider checking id to be identical for all product bases here
7068
m_pers->registerWrite(creator, pb.label, pb.data, type);
7169
}
72-
// Single commit per segment (product ID shared among products in the same segment)
73-
std::string const& id = batch[0].id;
74-
m_pers->commitOutput(creator, id);
70+
71+
m_pers->commitOutput(creator, segment_id);
7572
}
7673

77-
void form_interface::read(std::string const& creator, mock_phlex::product_base& pb)
74+
void form_interface::read(std::string const& creator,
75+
std::string const& segment_id,
76+
product_with_name& pb)
7877
{
79-
// Look up creator from config based on product name. O(1) lookup instead of loop
78+
8079
auto it = m_product_to_config.find(pb.label);
8180
if (it == m_product_to_config.end()) {
8281
throw std::runtime_error("No configuration found for product: " + pb.label);
8382
}
8483

85-
// Original type lookup
8684
std::string type = m_type_map->names[pb.type];
8785

88-
// Use full_label instead of pb.label
89-
m_pers->read(creator, pb.label, pb.id, &pb.data, type);
86+
m_pers->read(creator, pb.label, segment_id, &pb.data, type);
9087
}
9188
}

form/form/form.hpp

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,49 @@
44
#define __FORM_HPP__
55

66
#include "form/config.hpp"
7-
#include "mock_phlex/phlex_toy_config.hpp"
8-
#include "mock_phlex/phlex_toy_core.hpp" // FORM Interface may include core phlex modules
97
#include "persistence/ipersistence.hpp"
108

9+
#include <map>
1110
#include <memory>
1211
#include <string>
12+
#include <typeindex>
13+
#include <unordered_map>
14+
#include <vector>
1315

1416
namespace form::experimental {
17+
18+
struct product_type_names {
19+
std::unordered_map<std::type_index, std::string> names;
20+
};
21+
22+
struct product_with_name {
23+
std::string label;
24+
void const* data;
25+
std::type_index type;
26+
};
27+
1528
class form_interface {
1629
public:
17-
form_interface(std::shared_ptr<mock_phlex::product_type_names> tm,
18-
mock_phlex::config::parse_config const& config);
30+
form_interface(std::shared_ptr<product_type_names> tm,
31+
config::output_item_config const& output_config,
32+
config::tech_setting_config const& tech_config);
1933
~form_interface() = default;
2034

21-
void write(std::string const& creator, mock_phlex::product_base const& pb);
2235
void write(std::string const& creator,
23-
std::vector<mock_phlex::product_base> const& batch); // batch version
24-
void read(std::string const& creator, mock_phlex::product_base& pb);
36+
std::string const& segment_id,
37+
product_with_name const& product);
38+
39+
void write(std::string const& creator,
40+
std::string const& segment_id,
41+
std::vector<product_with_name> const& products);
42+
43+
void read(std::string const& creator,
44+
std::string const& segment_id,
45+
product_with_name& product);
2546

2647
private:
2748
std::unique_ptr<form::detail::experimental::IPersistence> m_pers;
28-
std::shared_ptr<mock_phlex::product_type_names> m_type_map;
29-
// Fast lookup maps built once in constructor
49+
std::shared_ptr<product_type_names> m_type_map;
3050
std::map<std::string, form::experimental::config::PersistenceItem> m_product_to_config;
3151
};
3252
}

form/form_module.cpp

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#include "phlex/model/product_store.hpp"
2+
#include "phlex/model/products.hpp"
3+
#include "phlex/module.hpp"
4+
5+
// FORM headers - these need to be available via CMake configuration
6+
// need to set up the build system to find these headers
7+
#include "form/config.hpp"
8+
#include "form/form.hpp"
9+
#include "form/technology.hpp"
10+
11+
#include <iostream>
12+
13+
namespace {
14+
15+
class FormOutputModule {
16+
public:
17+
FormOutputModule(std::shared_ptr<form::experimental::product_type_names> type_map,
18+
std::string output_file,
19+
int technology,
20+
std::vector<std::string> const& products_to_save) :
21+
m_type_map(type_map), m_output_file(std::move(output_file)), m_technology(technology)
22+
{
23+
std::cout << "FormOutputModule initialized\n";
24+
std::cout << " Output file: " << m_output_file << "\n";
25+
std::cout << " Technology: " << m_technology << "\n";
26+
27+
// Build FORM configuration
28+
form::experimental::config::output_item_config output_cfg;
29+
form::experimental::config::tech_setting_config tech_cfg;
30+
31+
// FIXME: Temporary solution to accommodate Phlex limitation.
32+
// Eventually, Phlex will communicate to FORM which products will be written
33+
// before executing any algorithms
34+
35+
// Temp. Sol for Phlex Prototype 0.1
36+
// Register products from config
37+
for (auto const& product : products_to_save) {
38+
output_cfg.addItem(product, m_output_file, m_technology);
39+
}
40+
41+
// Initialize FORM interface
42+
m_form_interface =
43+
std::make_unique<form::experimental::form_interface>(type_map, output_cfg, tech_cfg);
44+
}
45+
46+
// This method is called by Phlex - signature must be: void(product_store const&)
47+
void save_data_products(phlex::experimental::product_store const& store)
48+
{
49+
// Check if store is empty - smart way, check store not products vector
50+
if (store.empty()) {
51+
return;
52+
}
53+
54+
// STEP 1: Extract metadata from Phlex's product_store
55+
56+
// Extract creator (algorithm name)
57+
std::string creator = store.source();
58+
59+
// Extract segment ID (partition) - extract once for entire store
60+
std::string segment_id = store.id()->to_string();
61+
62+
std::cout << "\n=== FormOutputModule::save_data_products ===\n";
63+
std::cout << "Creator: " << creator << "\n";
64+
std::cout << "Segment ID: " << segment_id << "\n";
65+
std::cout << "Number of products: " << store.size() << "\n";
66+
67+
// STEP 2: Convert each Phlex product to FORM format
68+
69+
// Collect all products for writing
70+
std::vector<form::experimental::product_with_name> products;
71+
72+
// Reserve space for efficiency - avoid reallocations
73+
products.reserve(store.size());
74+
75+
// Iterate through all products in the store
76+
for (auto const& [product_name, product_ptr] : store) {
77+
// product_name: "tracks" (from the map key)
78+
// product_ptr: pointer to the actual product data
79+
80+
std::cout << " Product: " << product_name << "\n";
81+
82+
// Create FORM product with metadata
83+
products.emplace_back(product_name, // label, from map key
84+
product_ptr->address(), // data, from phlex product_base
85+
product_ptr->type() // type, from phlex product_base
86+
);
87+
}
88+
89+
// STEP 3: Send everything to FORM for persistence
90+
91+
// Write all products to FORM
92+
// Pass segment_id once for entire collection (not duplicated in each product)
93+
// No need to check if products is empty - already checked store.empty() above
94+
m_form_interface->write(creator, segment_id, products);
95+
std::cout << "Wrote " << products.size() << " products to FORM\n";
96+
}
97+
98+
private:
99+
std::shared_ptr<form::experimental::product_type_names> m_type_map;
100+
std::string m_output_file;
101+
int m_technology;
102+
std::unique_ptr<form::experimental::form_interface> m_form_interface;
103+
};
104+
105+
}
106+
107+
PHLEX_EXPERIMENTAL_REGISTER_ALGORITHMS(m, config)
108+
{
109+
std::cout << "Registering FORM output module...\n";
110+
111+
// Create type map
112+
auto type_map = std::make_shared<form::experimental::product_type_names>();
113+
114+
// Register some fundamental type for simple products
115+
type_map->names[std::type_index(typeid(int))] = "int";
116+
type_map->names[std::type_index(typeid(long))] = "long";
117+
type_map->names[std::type_index(typeid(float))] = "float";
118+
type_map->names[std::type_index(typeid(double))] = "double";
119+
type_map->names[std::type_index(typeid(std::vector<int>))] = "std::vector<int>";
120+
type_map->names[std::type_index(typeid(std::vector<long>))] = "std::vector<long>";
121+
type_map->names[std::type_index(typeid(std::vector<float>))] = "std::vector<float>";
122+
type_map->names[std::type_index(typeid(std::vector<double>))] = "std::vector<double>";
123+
124+
// Extract configuration from Phlex config
125+
std::string output_file = config.get<std::string>("output_file", "output.root");
126+
std::string tech_string = config.get<std::string>("technology", "ROOT_TTREE");
127+
128+
std::cout << "Configuration:\n";
129+
std::cout << " output_file: " << output_file << "\n";
130+
std::cout << " technology: " << tech_string << "\n";
131+
132+
// Map Phlex config string to FORM technology constant
133+
int technology = form::technology::ROOT_TTREE; // default
134+
135+
if (tech_string == "ROOT_TTREE") {
136+
technology = form::technology::ROOT_TTREE;
137+
} else if (tech_string == "ROOT_RNTUPLE") {
138+
technology = form::technology::ROOT_RNTUPLE;
139+
} else if (tech_string == "HDF5") {
140+
technology = form::technology::HDF5;
141+
} else {
142+
throw std::runtime_error("Unknown technology: " + tech_string);
143+
}
144+
145+
auto products_to_save = config.get<std::vector<std::string>>("products");
146+
147+
// Phlex needs an OBJECT
148+
// Create the FORM output module
149+
auto form_output = m.make<FormOutputModule>(type_map, output_file, technology, products_to_save);
150+
151+
// Phlex needs a MEMBER FUNCTION to call
152+
// Register the callback that Phlex will invoke
153+
form_output.output("save_data_products", &FormOutputModule::save_data_products);
154+
155+
std::cout << "FORM output module registered successfully\n";
156+
}

form/mock_phlex/CMakeLists.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)