Skip to content

Commit 6f9e5ee

Browse files
committed
Simple event pools merger, with example
1 parent e02ff4d commit 6f9e5ee

6 files changed

Lines changed: 326 additions & 1 deletion

File tree

Generators/CMakeLists.txt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@ if(doBuildSimulation)
146146
# PUBLIC_LINK_LIBRARIES O2::Generators)
147147
endif()
148148

149-
150149
o2_add_test_root_macro(share/external/tgenerator.C
151150
PUBLIC_LINK_LIBRARIES O2::Generators
152151
LABELS generators)
@@ -165,6 +164,17 @@ o2_add_test_root_macro(share/egconfig/pythia8_userhooks_charm.C
165164
LABELS generators)
166165
endif()
167166

167+
o2_add_executable(merge-evtpool
168+
COMPONENT_NAME generators
169+
SOURCES src/MergeEventPool.cxx
170+
PUBLIC_LINK_LIBRARIES O2::CommonUtils
171+
O2::SimulationDataFormat
172+
ROOT::Core
173+
ROOT::RIO
174+
ROOT::Tree
175+
ROOT::Net
176+
Boost::program_options)
177+
168178
o2_data_file(COPY share/external DESTINATION Generators)
169179
o2_data_file(COPY share/egconfig DESTINATION Generators)
170180
o2_data_file(COPY share/TPCLoopers DESTINATION Generators)

Generators/src/MergeEventPool.cxx

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
// Copyright 2019-2026 CERN and copyright holders of ALICE O2.
2+
// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3+
// All rights not expressly granted are reserved.
4+
//
5+
// This software is distributed under the terms of the GNU General Public
6+
// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7+
//
8+
// In applying this license CERN does not waive the privileges and immunities
9+
// granted to it by virtue of its status as an Intergovernmental Organization
10+
// or submit itself to any jurisdiction.
11+
12+
/// \brief Merges multiple event-pool files (e.g. evtpool.root / genevents_Kine.root,
13+
/// produced by o2-sim --noGeant) into a single "o2sim" tree.
14+
///
15+
/// This tool merges event pools with TFileMerger (the engine behind hadd).
16+
///
17+
/// Input handling is added in addition to hadd: files can be given directly, or collected
18+
/// from local text files listing further paths (one per line, '#' comments allowed,
19+
/// resolved recursively). The pools themselves can live on AliEn (alien:// URLs) and are
20+
/// read straight from the storage elements
21+
///
22+
/// Every input is validated (tree and required branches present) before anything is
23+
/// written, so a bad file is reported and nothing is produced, and the merged pool is
24+
/// checked once more at the end.
25+
///
26+
/// Usage:
27+
///
28+
/// # a few pools given directly
29+
/// o2-generators-merge-evtpool -i poolA.root,poolB.root -o merged.root
30+
///
31+
/// # a local text file listing pools, which may be local and/or alien://
32+
/// o2-generators-merge-evtpool -i pools.txt -o merged.root
33+
///
34+
/// Options: --input/-i (required), --output/-o (evtpool.root), --treename/-t (o2sim),
35+
/// --help/-h. Shell variables are expanded in every path, both in --input and inside
36+
/// list files.
37+
///
38+
/// @author Marco Giacalone, mgiacalo@cern.ch, 08/2026
39+
40+
#include "CommonUtils/FileSystemUtils.h"
41+
#include "CommonUtils/StringUtils.h"
42+
#include <fairlogger/Logger.h>
43+
#include <TFile.h>
44+
#include <TFileMerger.h>
45+
#include <TGrid.h>
46+
#include <TTree.h>
47+
#include <boost/program_options.hpp>
48+
#include <algorithm>
49+
#include <filesystem>
50+
#include <fstream>
51+
#include <memory>
52+
#include <optional>
53+
#include <string>
54+
#include <vector>
55+
56+
namespace bpo = boost::program_options;
57+
namespace fs = std::filesystem;
58+
59+
namespace
60+
{
61+
const char* kTrackBranch = "MCTrack";
62+
const char* kHeaderBranch = "MCEventHeader.";
63+
const char* kTrackRefBranch = "TrackRefs";
64+
const char* kProtocol = "alien://";
65+
66+
bool isAlienPath(std::string const& path)
67+
{
68+
return o2::utils::Str::beginsWith(path, kProtocol);
69+
}
70+
71+
// Connects to AliEn if that has not happened yet
72+
bool GridOn()
73+
{
74+
if (gGrid) {
75+
return true;
76+
}
77+
LOG(info) << "Connecting to AliEn ...";
78+
if (!TGrid::Connect("alien:") || !gGrid) {
79+
LOG(error) << "Could not connect to AliEn; check your alien token";
80+
return false;
81+
}
82+
return true;
83+
}
84+
85+
// Reads the lines of a local text file. nullopt if it could not be opened.
86+
std::optional<std::vector<std::string>> readLocalListFileLines(std::string const& path)
87+
{
88+
std::ifstream in(path);
89+
if (!in.is_open()) {
90+
return std::nullopt;
91+
}
92+
std::vector<std::string> lines;
93+
std::string line;
94+
while (std::getline(in, line)) {
95+
lines.push_back(line);
96+
}
97+
return lines;
98+
}
99+
100+
// Reads a text file listing input paths, one per line ('#' comments and blank lines
101+
// ignored). Each listed path is either a .root file (local or alien://) or itself
102+
// another list file. The lists themselves are always read locally.
103+
void expandInputEntry(std::string const& rawEntry, std::vector<std::string>& out, std::vector<std::string>& stack)
104+
{
105+
// done here so that the expansion works also when the variables appear in a list file
106+
const auto entry = o2::utils::expandShellVarsInFileName(rawEntry);
107+
if (o2::utils::Str::endsWith(entry, ".root")) {
108+
out.push_back(entry);
109+
return;
110+
}
111+
if (std::find(stack.begin(), stack.end(), entry) != stack.end()) {
112+
LOG(error) << "Reference to an existing list " << entry << "; ignoring";
113+
return;
114+
}
115+
auto lines = readLocalListFileLines(entry);
116+
if (!lines) {
117+
LOG(error) << "Cannot open " << entry << " (neither a .root file nor a readable local list)";
118+
return;
119+
}
120+
stack.push_back(entry);
121+
for (auto line : *lines) {
122+
o2::utils::Str::trim(line);
123+
if (line.empty() || line[0] == '#') {
124+
continue;
125+
}
126+
expandInputEntry(line, out, stack);
127+
}
128+
stack.pop_back();
129+
}
130+
131+
// Expands a list of raw --input entries (each either a .root file or a list) into
132+
// the flat list of .root files to merge.
133+
std::vector<std::string> expandInputs(std::vector<std::string> const& rawEntries)
134+
{
135+
std::vector<std::string> result;
136+
std::vector<std::string> stack;
137+
for (auto const& e : rawEntries) {
138+
expandInputEntry(e, result, stack);
139+
}
140+
return result;
141+
}
142+
143+
// Checks that a file is readable and holds a tree with the branches expected from a
144+
// standard o2-sim event pool, reporting its event count and compression settings.
145+
// Returns an empty string when the file is usable, the reason otherwise.
146+
std::string inspectFile(std::string const& path, std::string const& treename,
147+
Long64_t& entries, int& compression)
148+
{
149+
if (!isAlienPath(path) && !fs::exists(path)) {
150+
return "file does not exist";
151+
}
152+
std::unique_ptr<TFile> file(TFile::Open(path.c_str(), "READ"));
153+
if (!file || file->IsZombie()) {
154+
return "file cannot be opened";
155+
}
156+
auto tree = (TTree*)file->Get(treename.c_str());
157+
if (!tree) {
158+
return "no tree named '" + treename + "' in the file";
159+
}
160+
if (tree->GetBranch(kTrackBranch) == nullptr || tree->GetBranch(kHeaderBranch) == nullptr ||
161+
tree->GetBranch(kTrackRefBranch) == nullptr) {
162+
return std::string("missing the required '") + kTrackBranch + "', '" + kHeaderBranch + "' and/or '" +
163+
kTrackRefBranch + "' branch";
164+
}
165+
entries = tree->GetEntries();
166+
compression = file->GetCompressionSettings();
167+
return {};
168+
}
169+
170+
// Checks every input before anything is written. Reports the total number of events and
171+
// the compression settings of the first input
172+
bool checkFiles(std::vector<std::string> const& files, std::string const& treename,
173+
Long64_t& totalEvents, int& compression)
174+
{
175+
bool ok = true;
176+
totalEvents = 0;
177+
compression = -1;
178+
for (auto const& f : files) {
179+
Long64_t entries = 0;
180+
int fileCompression = -1;
181+
const auto issue = inspectFile(f, treename, entries, fileCompression);
182+
if (!issue.empty()) {
183+
LOG(error) << "Input file " << f << ": " << issue;
184+
ok = false;
185+
continue;
186+
}
187+
if (compression < 0) {
188+
compression = fileCompression;
189+
}
190+
totalEvents += entries;
191+
LOG(info) << " OK " << f << " (" << entries << " events)";
192+
}
193+
return ok;
194+
}
195+
196+
// Re-opens the merged output and checks that it holds the expected tree, branches and
197+
// number of events, so that a truncated or half-written pool does not pass unnoticed.
198+
bool validateOutput(std::string const& outfile, std::string const& treename, Long64_t expected)
199+
{
200+
Long64_t entries = 0;
201+
int compression = -1;
202+
const auto issue = inspectFile(outfile, treename, entries, compression);
203+
if (!issue.empty()) {
204+
LOG(error) << "Merged file " << outfile << " is not usable: " << issue;
205+
return false;
206+
}
207+
if (entries != expected) {
208+
LOG(error) << "Merged file " << outfile << " has " << entries << " events, but " << expected
209+
<< " were merged into it";
210+
return false;
211+
}
212+
return true;
213+
}
214+
} // namespace
215+
216+
int main(int argc, char* argv[])
217+
{
218+
bpo::options_description options("o2-generators-merge-evtpool options");
219+
auto add = options.add_options();
220+
add("input,i", bpo::value<std::string>()->required(),
221+
"comma-separated list of inputs: event-pool ROOT files (local or alien://), and/or "
222+
"local text files listing more paths (one per line, '#' comments allowed)");
223+
add("output,o", bpo::value<std::string>()->default_value("evtpool.root"),
224+
"output ROOT file with the merged event pool");
225+
add("treename,t", bpo::value<std::string>()->default_value("o2sim"), "name of the tree to merge");
226+
add("help,h", "produce help message");
227+
bpo::variables_map vm;
228+
try {
229+
bpo::store(bpo::parse_command_line(argc, argv, options), vm);
230+
if (vm.count("help")) {
231+
LOG(info) << options;
232+
return 0;
233+
}
234+
bpo::notify(vm);
235+
} catch (const bpo::error& e) {
236+
LOG(fatal) << "Error parsing command-line arguments: " << e.what() << "\n\n"
237+
<< options;
238+
return 1;
239+
}
240+
const auto rawEntries = o2::utils::Str::tokenize(vm["input"].as<std::string>(), ',');
241+
if (rawEntries.empty()) {
242+
LOG(fatal) << "No input files given";
243+
return 1;
244+
}
245+
const auto infiles = expandInputs(rawEntries);
246+
if (infiles.empty()) {
247+
LOG(fatal) << "No input files resolved from the given --input entries";
248+
return 1;
249+
}
250+
// Check Grid connection if any input is on AliEn
251+
if (std::any_of(infiles.begin(), infiles.end(), isAlienPath) && !GridOn()) {
252+
LOG(fatal) << "Some inputs live on AliEn but the grid is not available";
253+
return 1;
254+
}
255+
const std::string outfile = vm["output"].as<std::string>();
256+
const std::string treename = vm["treename"].as<std::string>();
257+
LOG(info) << "Validating " << infiles.size() << " input file(s) ...";
258+
Long64_t totalEvents = 0;
259+
int compression = -1;
260+
if (!checkFiles(infiles, treename, totalEvents, compression)) {
261+
LOG(fatal) << "Validation failed; not writing any output";
262+
return 1;
263+
}
264+
LOG(info) << "Merging " << totalEvents << " events into " << outfile << " ...";
265+
TFileMerger merger(/*isLocal*/ false, /*histoOneGo*/ false);
266+
merger.SetPrintLevel(0);
267+
if (!merger.OutputFile(outfile.c_str(), "RECREATE", compression)) {
268+
LOG(fatal) << "Cannot create output file " << outfile;
269+
return 1;
270+
}
271+
for (auto const& f : infiles) {
272+
if (!merger.AddFile(f.c_str())) {
273+
LOG(fatal) << "Cannot add " << f << " to the merge";
274+
return 1;
275+
}
276+
}
277+
if (!merger.Merge()) {
278+
LOG(fatal) << "Merging failed; output " << outfile << " is incomplete";
279+
return 1;
280+
}
281+
if (!validateOutput(outfile, treename, totalEvents)) {
282+
LOG(fatal) << "The merged pool did not pass the final check";
283+
return 1;
284+
}
285+
LOG(info) << "Done: wrote " << totalEvents << " events to " << outfile;
286+
return 0;
287+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<!-- doxy
2+
\page refrunSimExamplesMergeEventPools Example MergeEventPools
3+
/doxy -->
4+
5+
This example demonstrates how to merge event pools using the dedicated `o2-generators-merge-evtpool`.
6+
The pools to merge are listed in `pools.txt`. They can be local files or `alien://` paths, and a line can also point to
7+
another local file list. AliEn pools are read directly from the storage elements, so a valid alien token is needed.
8+
The merged pool is an ordinary evtpool.root file (by default), so it can be used with the `evtpool` generator.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# One evtpool.root file per line; '#' comments and blank lines are ignored.
2+
# Paths can be local or on AliEn, and a line can also point to another (local) list file.
3+
alien:///alice/sim/2026/EP26a2/19/001/evtpool.root
4+
alien:///alice/sim/2026/EP26a2/19/002/evtpool.root
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/env bash
2+
3+
# A simple example showing how to merge several event pools.
4+
# The pools are listed in pools.txt. In the example they are read from AliEn, so an alien token
5+
# is needed.
6+
# Additionally this could be run as
7+
# o2-generators-merge-evtpool -i poolA.root,poolB.root -o merged.root
8+
# to give the pools directly, or
9+
# o2-generators-merge-evtpool -i poolA.root,pools.txt -o merged.root
10+
# to mix single pools with a list. The list files themselves must be local.
11+
12+
set -x
13+
14+
# merge the pools listed in pools.txt
15+
o2-generators-merge-evtpool -i pools.txt -o evtpool.root

run/SimExamples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
* \subpage refrunSimExamplesPythia
2525
* \subpage refrunSimExamplesForceDecay_Lambda_Neutron_Dalitz
2626
* \subpage refrunSimExamplesJustPrimaryKinematics
27+
* \subpage refrunSimExamplesMergeEventPools
2728
* \subpage refrunSimExamplesSelective_Transport
2829
* \subpage refrunSimExamplesSelective_Transport_pi0
2930
* \subpage refrunSimExamplesStepMonitoringSimple1

0 commit comments

Comments
 (0)