Back to home page

EIC code displayed by LXR

 
 

    


Warning, /physics_benchmarks/RNTUPLE_MIGRATION_GUIDE.md is written in an unsupported language. File is not indexed.

0001 # RNTuple Migration Guide
0002 
0003 This guide provides instructions for migrating physics benchmarks from TTree to RNTuple format.
0004 
0005 ## Overview
0006 
0007 RNTuple is ROOT's next-generation columnar storage format, offering improved performance and better compression compared to TTree. This guide covers the necessary changes to migrate benchmark analysis code and Snakemake workflows.
0008 
0009 ## Table of Contents
0010 
0011 1. [Prerequisites](#prerequisites)
0012 2. [Simulation and Reconstruction Changes](#simulation-and-reconstruction-changes)
0013 3. [Analysis Code Migration](#analysis-code-migration)
0014 4. [Snakefile Updates](#snakefile-updates)
0015 5. [Common Migration Patterns](#common-migration-patterns)
0016 6. [Troubleshooting](#troubleshooting)
0017 
0018 ## Prerequisites
0019 
0020 - ROOT 6.28 or later (RNTuple support)
0021 - EICrecon with podio RNTuple backend support
0022 - Updated EDM4eic/EDM4hep with RNTuple support
0023 
0024 ## Simulation and Reconstruction Changes
0025 
0026 ### EICrecon Output Configuration
0027 
0028 To enable RNTuple output from eicrecon, use the podio backend configuration:
0029 
0030 ```bash
0031 # Old TTree format (default)
0032 eicrecon input.edm4hep.root -Ppodio:output_file=output.edm4eic.root
0033 
0034 # New RNTuple format
0035 eicrecon input.edm4hep.root -Ppodio:output_file=output.edm4eic.rnt.root -Ppodio:output_backend=rntuple
0036 ```
0037 
0038 **Note:** Use `.rnt.root` extension for RNTuple files to distinguish them from TTree `.root` files.
0039 
0040 ### File Naming Convention
0041 
0042 To distinguish RNTuple files from TTree files, use `.rnt.root` extension:
0043 
0044 ```
0045 # TTree format
0046 pythia8NCDIS_10x100_minQ2=1.edm4eic.root
0047 
0048 # RNTuple format
0049 pythia8NCDIS_10x100_minQ2=1.edm4eic.rnt.root
0050 ```
0051 
0052 ## Analysis Code Migration
0053 
0054 ### Overview of Changes
0055 
0056 The main differences between TTree and RNTuple APIs:
0057 
0058 | Feature | TTree API | RNTuple API |
0059 |---------|-----------|-------------|
0060 | Reader | `TTreeReader` | `ROOT::Experimental::RNTupleReader` |
0061 | Field access | `TTreeReaderArray<T>` | `RNTupleReader::GetView<T>()` |
0062 | Tree name | Required ("events") | Not used (single dataset per file) |
0063 | Iteration | `reader.Next()` | Range-based for loop or manual iteration |
0064 
0065 ### Header Files
0066 
0067 Replace TTree headers with RNTuple headers:
0068 
0069 ```cpp
0070 // Old TTree includes
0071 #include <TChain.h>
0072 #include <TTreeReader.h>
0073 #include <TTreeReaderArray.h>
0074 
0075 // New RNTuple includes
0076 #include <ROOT/RNTupleReader.hxx>
0077 ```
0078 
0079 ### Opening Files
0080 
0081 #### TTree Approach (Old)
0082 
0083 ```cpp
0084 TChain *mychain = new TChain("events");
0085 mychain->Add(rec_file.c_str());
0086 TTreeReader tree_reader(mychain);
0087 ```
0088 
0089 #### RNTuple Approach (New)
0090 
0091 ```cpp
0092 using ROOT::RNTupleReader;
0093 
0094 auto ntuple = RNTupleReader::Open("events", rec_file);
0095 if (!ntuple) {
0096   fmt::print(stderr, "ERROR: Failed to open RNTuple from file\n");
0097   return 1;
0098 }
0099 ```
0100 
0101 **Note:** In ROOT 6.40+, RNTuple is in the `ROOT::` namespace (not `ROOT::Experimental::`). Use `ROOT::RNTupleReader` for production code.
0102 
0103 **Note:** RNTuple does not support chaining multiple files like TChain. If you need to process multiple files, you must:
0104 1. Process them sequentially in a loop, or
0105 2. Use RDataFrame which can handle multiple RNTuple files
0106 
0107 ### Error Handling
0108 
0109 **Critical:** Unlike TTree which silently continues with missing branches, RNTuple throws exceptions for missing fields. Always wrap view creation in try-catch:
0110 
0111 ```cpp
0112 try {
0113   auto viewRecoNRG = ntuple->GetView<float>("ReconstructedChargedJets.energy");
0114   auto viewRecoMomX = ntuple->GetView<float>("ReconstructedChargedJets.momentum.x");
0115   // ... more views
0116 } catch (const std::exception& e) {
0117   fmt::print(stderr, "ERROR: Missing required field: {}\n", e.what());
0118   return 1;
0119 }
0120 ```
0121 
0122 This ensures your analysis fails fast if required collections are missing, rather than producing incorrect results silently.
0123 
0124 ### Reading Fields
0125 
0126 #### TTree Approach (Old)
0127 
0128 ```cpp
0129 TTreeReaderArray<float> recoNRG = {tree_reader, "ReconstructedChargedJets.energy"};
0130 TTreeReaderArray<float> recoMomX = {tree_reader, "ReconstructedChargedJets.momentum.x"};
0131 
0132 // In event loop
0133 while (tree_reader.Next()) {
0134     for (int i = 0; i < recoNRG.GetSize(); i++) {
0135         float energy = recoNRG[i];
0136         float px = recoMomX[i];
0137         // ... process data
0138     }
0139 }
0140 ```
0141 
0142 #### RNTuple Approach (New)
0143 
0144 ```cpp
0145 auto viewRecoNRG = ntuple->GetView<float>("ReconstructedChargedJets.energy");
0146 auto viewRecoMomX = ntuple->GetView<float>("ReconstructedChargedJets.momentum.x");
0147 
0148 for (auto entryId : *ntuple) {
0149     // For vector/array fields, you get a RVec-like object
0150     auto energy_vec = viewRecoNRG(entryId);
0151     auto momx_vec = viewRecoMomX(entryId);
0152 
0153     for (size_t i = 0; i < energy_vec.size(); i++) {
0154         float energy = energy_vec.at(i);  // Use .at() for ROOT compatibility
0155         float px = momx_vec.at(i);
0156         // ... process data
0157     }
0158 }
0159 ```
0160 
0161 **Alternative: Manual entry iteration**
0162 
0163 ```cpp
0164 for (auto i = 0; i < ntuple->GetNEntries(); ++i) {
0165     auto energy_vec = viewRecoNRG(i);
0166     // ... process
0167 }
0168 ```
0169 
0170 ### Field Type Mapping
0171 
0172 When getting views, use the appropriate type:
0173 
0174 ```cpp
0175 // Integer fields
0176 auto viewType = ntuple->GetView<int>("ReconstructedChargedJets.type");
0177 auto viewIndex = ntuple->GetView<int>("_ReconstructedChargedJets_constituents.index");
0178 
0179 // Float fields
0180 auto viewEnergy = ntuple->GetView<float>("ReconstructedChargedJets.energy");
0181 auto viewMomX = ntuple->GetView<float>("ReconstructedChargedJets.momentum.x");
0182 
0183 // Double fields (common in MCParticles)
0184 auto viewMCMomX = ntuple->GetView<double>("MCParticles.momentum.x");
0185 
0186 // Unsigned int fields
0187 auto viewBegin = ntuple->GetView<unsigned int>("ReconstructedChargedJets.constituents_begin");
0188 auto viewEnd = ntuple->GetView<unsigned int>("ReconstructedChargedJets.constituents_end");
0189 ```
0190 
0191 **Note on Collection Indexing:**  
0192 The exact syntax for indexing into collections returned by `view(entryId)` may vary by ROOT version. If you encounter errors like "subscripted value is not an array", try these alternatives:
0193 - Use `.at(i)` instead of `[i]`
0194 - Use range-based for loops: `for (const auto& val : view(entryId))`
0195 - Check your ROOT version's RNTuple documentation for the exact API
0196 
0197 ### Complete Migration Example
0198 
0199 Here's a complete before/after example:
0200 
0201 #### Before (TTree)
0202 
0203 ```cpp
0204 #include <TChain.h>
0205 #include <TTreeReader.h>
0206 #include <TTreeReaderArray.h>
0207 
0208 int analyze(const std::string& rec_file) {
0209     TChain *mychain = new TChain("events");
0210     mychain->Add(rec_file.c_str());
0211     TTreeReader tree_reader(mychain);
0212     
0213     TTreeReaderArray<float> recoNRG = {tree_reader, "ReconstructedChargedJets.energy"};
0214     TTreeReaderArray<float> recoMomZ = {tree_reader, "ReconstructedChargedJets.momentum.z"};
0215     
0216     while (tree_reader.Next()) {
0217         for (int i = 0; i < recoNRG.GetSize(); i++) {
0218             float energy = recoNRG[i];
0219             float pz = recoMomZ[i];
0220             // Process jet...
0221         }
0222     }
0223     return 0;
0224 }
0225 ```
0226 
0227 #### After (RNTuple)
0228 
0229 ```cpp
0230 #include <ROOT/RNTupleReader.hxx>
0231 
0232 int analyze(const std::string& rec_file) {
0233     using ROOT::RNTupleReader;  // ROOT 6.40+: use ROOT:: not ROOT::Experimental::
0234     
0235     auto ntuple = RNTupleReader::Open("events", rec_file);
0236     
0237     auto viewRecoNRG = ntuple->GetView<float>("ReconstructedChargedJets.energy");
0238     auto viewRecoMomZ = ntuple->GetView<float>("ReconstructedChargedJets.momentum.z");
0239     
0240     for (auto entryId : *ntuple) {
0241         auto energy_vec = viewRecoNRG(entryId);
0242         auto momz_vec = viewRecoMomZ(entryId);
0243 
0244         for (size_t i = 0; i < energy_vec.size(); i++) {
0245             float energy = energy_vec.at(i);  // Use .at() for ROOT compatibility
0246             float pz = momz_vec.at(i);
0247             // Process jet...
0248         }
0249     }
0250     return 0;
0251 }
0252 ```
0253 
0254 ## Snakefile Updates
0255 
0256 ### Update Reconstruction Rule
0257 
0258 Modify the reconstruction rule to use RNTuple output:
0259 
0260 ```python
0261 # Before
0262 rule my_reco_eicrecon:
0263     input:
0264         "sim_output/{DETECTOR_CONFIG}/input.edm4hep.root",
0265     output:
0266         "sim_output/{DETECTOR_CONFIG}/output.edm4eic.root",
0267     shell:
0268         """
0269         DETECTOR_CONFIG={wildcards.DETECTOR_CONFIG} eicrecon {input} -Ppodio:output_file={output}
0270         """
0271 
0272 # After
0273 rule my_reco_eicrecon:
0274     input:
0275         "sim_output/{DETECTOR_CONFIG}/input.edm4hep.root",
0276     output:
0277         "sim_output/{DETECTOR_CONFIG}/output.edm4eic.rnt.root",
0278     shell:
0279         """
0280         DETECTOR_CONFIG={wildcards.DETECTOR_CONFIG} eicrecon {input} \\
0281             -Ppodio:output_file={output} \\
0282             -Ppodio:output_backend=rntuple
0283         """
0284 ```
0285 
0286 ### Update File References
0287 
0288 Update all file references throughout the Snakefile:
0289 
0290 ```python
0291 # Before
0292 data="sim_output/{DETECTOR_CONFIG}/pythia8NCDIS_10x100.edm4eic.root",
0293 
0294 # After
0295 data="sim_output/{DETECTOR_CONFIG}/pythia8NCDIS_10x100.edm4eic.rnt.root",
0296 ```
0297 
0298 ## Common Migration Patterns
0299 
0300 ### Pattern 1: Simple Field Access
0301 
0302 ```cpp
0303 // Old
0304 TTreeReaderArray<float> field = {tree_reader, "Collection.field"};
0305 while (tree_reader.Next()) {
0306     float value = field[index];
0307 }
0308 
0309 // New
0310 auto viewField = ntuple->GetView<float>("Collection.field");
0311 for (auto entryId : *ntuple) {
0312     auto field_vec = viewField(entryId);
0313     float value = field_vec.at(index);  // Use .at() for safety and ROOT compatibility
0314 }
0315 ```
0316 
0317 ### Pattern 2: Checking Array Size
0318 
0319 ```cpp
0320 // Old
0321 int size = recoNRG.GetSize();
0322 
0323 // New
0324 auto energy_vec = viewRecoNRG(entryId);
0325 size_t size = energy_vec.size();
0326 ```
0327 
0328 ### Pattern 3: Conditional Field Access (Version-Dependent)
0329 
0330 ```cpp
0331 // Old
0332 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8,9,0)
0333   TTreeReaderArray<float> recoArea = {tree_reader, "ReconstructedChargedJets.area"};
0334 #endif
0335 
0336 // New
0337 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8,9,0)
0338   auto viewRecoArea = ntuple->GetView<float>("ReconstructedChargedJets.area");
0339 #endif
0340 ```
0341 
0342 ### Pattern 4: Association Tables
0343 
0344 ```cpp
0345 // Old
0346 TTreeReaderArray<unsigned int> recoCstsBegin = {tree_reader, "ReconstructedChargedJets.constituents_begin"};
0347 TTreeReaderArray<unsigned int> recoCstsEnd = {tree_reader, "ReconstructedChargedJets.constituents_end"};
0348 TTreeReaderArray<int> recoCstIndex = {tree_reader, "_ReconstructedChargedJets_constituents.index"};
0349 
0350 while (tree_reader.Next()) {
0351     for (int i = 0; i < recoType.GetSize(); i++) {
0352         unsigned int begin = recoCstsBegin[i];
0353         unsigned int end = recoCstsEnd[i];
0354         for (unsigned int j = begin; j < end; j++) {
0355             int idx = recoCstIndex[j];
0356             // Process constituent at idx
0357         }
0358     }
0359 }
0360 
0361 // New
0362 auto viewRecoCstsBegin = ntuple->GetView<unsigned int>("ReconstructedChargedJets.constituents_begin");
0363 auto viewRecoCstsEnd = ntuple->GetView<unsigned int>("ReconstructedChargedJets.constituents_end");
0364 auto viewRecoCstIndex = ntuple->GetView<int>("_ReconstructedChargedJets_constituents.index");
0365 
0366 for (auto entryId : *ntuple) {
0367     auto cstsBegin = viewRecoCstsBegin(entryId);
0368     auto cstsEnd = viewRecoCstsEnd(entryId);
0369     auto cstIndex = viewRecoCstIndex(entryId);
0370     
0371     for (size_t i = 0; i < cstsBegin.size(); i++) {
0372         unsigned int begin = cstsBegin[i];
0373         unsigned int end = cstsEnd[i];
0374         for (unsigned int j = begin; j < end; j++) {
0375             int idx = cstIndex[j];
0376             // Process constituent at idx
0377         }
0378     }
0379 }
0380 ```
0381 
0382 ## Troubleshooting
0383 
0384 ### "Cannot open RNTuple" Error
0385 
0386 **Cause:** Trying to open a TTree file with RNTupleReader or vice versa.
0387 
0388 **Solution:** Ensure the file was created with `-Ppodio:output_backend=rntuple` and verify the format:
0389 
0390 ```bash
0391 root -l -q 'file.root' -e 'gDirectory->ls()'
0392 ```
0393 
0394 Look for `RNTuple` instead of `TTree` in the output.
0395 
0396 ### Type Mismatch Errors
0397 
0398 **Cause:** Using wrong type in `GetView<T>()`.
0399 
0400 **Solution:** Check the field type in the RNTuple:
0401 
0402 ```cpp
0403 ntuple->GetDescriptor().PrintInfo();
0404 ```
0405 
0406 Common types:
0407 - `int` for PDG codes, type fields
0408 - `float` for most EDM4eic fields (energy, momentum)
0409 - `double` for MCParticles
0410 - `unsigned int` for indices and counts
0411 
0412 ### Performance Issues
0413 
0414 **Cause:** Creating views inside the event loop.
0415 
0416 **Solution:** Always create views once before the loop:
0417 
0418 ```cpp
0419 // BAD - creates view every iteration
0420 for (auto entryId : *ntuple) {
0421     auto view = ntuple->GetView<float>("field");  // DON'T DO THIS
0422 }
0423 
0424 // GOOD - creates view once
0425 auto view = ntuple->GetView<float>("field");
0426 for (auto entryId : *ntuple) {
0427     auto data = view(entryId);
0428 }
0429 ```
0430 
0431 ### Missing Fields
0432 
0433 **Cause:** Field name changed or doesn't exist in RNTuple.
0434 
0435 **Solution:** List all available fields:
0436 
0437 ```cpp
0438 ntuple->GetDescriptor().PrintInfo();
0439 // or
0440 for (const auto& field : ntuple->GetDescriptor().GetFieldRange()) {
0441     std::cout << field.GetFieldName() << std::endl;
0442 }
0443 ```
0444 
0445 ### podio DataFrame API (Recommended)
0446 
0447 **The recommended approach** for EDM4hep/EDM4eic analysis is using podio's DataFrame API, which provides format-agnostic access and handles podio collections natively:
0448 
0449 ```cpp
0450 #include <podio/DataSource.h>
0451 #include <ROOT/RDataFrame.hxx>
0452 
0453 int analyze(const std::string& rec_file) {
0454     // Automatically detects TTree vs RNTuple format
0455     auto df = podio::CreateDataFrame(rec_file);
0456     
0457     // Use standard RDataFrame operations
0458     // Collections are accessible with dot notation: "Collection.field"
0459     
0460     // Example: For complex per-event logic, use Foreach
0461     df.Foreach([&histogram1, &histogram2](
0462         ROOT::VecOps::RVec<float> jet_energy,
0463         ROOT::VecOps::RVec<float> jet_px,
0464         ROOT::VecOps::RVec<float> jet_py
0465     ) {
0466         // Your analysis logic with full control
0467         for (size_t i = 0; i < jet_energy.size(); i++) {
0468             float energy = jet_energy.at(i);
0469             float pt = sqrt(jet_px.at(i)*jet_px.at(i) + jet_py.at(i)*jet_py.at(i));
0470             histogram1->Fill(energy);
0471             histogram2->Fill(pt);
0472         }
0473     }, {
0474         "ReconstructedChargedJets.energy",
0475         "ReconstructedChargedJets.momentum.x",
0476         "ReconstructedChargedJets.momentum.y"
0477     });
0478     
0479     return 0;
0480 }
0481 ```
0482 
0483 **Advantages:**
0484 - ✅ Format-agnostic: Works with `.root` (TTree) and `.rnt.root` (RNTuple) transparently
0485 - ✅ Uses podio's official API for EDM4hep/EDM4eic data
0486 - ✅ No manual view creation or error handling needed
0487 - ✅ Cleaner initialization code
0488 - ✅ Can be parallelized with `ROOT::EnableImplicitMT()` (ensure side effects like manual `TH1::Fill` are made thread-safe, e.g. via `ForeachSlot` + per-slot histograms)
0489 - ✅ Perfect for complex analysis with nested constituent loops
0490 
0491 **Limitations with RNTuple:**
0492 - ⚠️ `.Define()` with string expressions may fail when reading RNTuple files due to missing type definitions at JIT compile time
0493 - ⚠️ Use `.Foreach()` or direct lambda expressions instead for reliable RNTuple support
0494 - ✅ Works reliably with TTree files
0495 
0496 **When to use `.Foreach()` vs pure declarative style:**
0497 - Use `.Foreach()` for complex per-event logic (constituent loops, jet matching, multi-histogram filling)
0498 - Use `.Define()` and `.Filter()` for simple transformations and cuts (works well with TTree, may require fallback for RNTuple)
0499 - See `benchmarks/Jets-HF/jets/analysis/jets.cxx` for a complete real-world example
0500 
0501 **Migration from RNTupleReader to podio::CreateDataFrame:**
0502 
0503 ```cpp
0504 // Before (RNTupleReader)
0505 #include <ROOT/RNTupleReader.hxx>
0506 
0507 auto ntuple = RNTupleReader::Open("events", rec_file);
0508 auto viewEnergy = ntuple->GetView<float>("ReconstructedChargedJets.energy");
0509 auto viewMomX = ntuple->GetView<float>("ReconstructedChargedJets.momentum.x");
0510 
0511 for (auto entryId : *ntuple) {
0512     auto energy = viewEnergy(entryId);
0513     auto momx = viewMomX(entryId);
0514     // ... process
0515 }
0516 
0517 // After (podio::CreateDataFrame)
0518 #include <podio/DataSource.h>
0519 #include <ROOT/RDataFrame.hxx>
0520 
0521 auto df = podio::CreateDataFrame(rec_file);
0522 
0523 df.Foreach([&histograms](
0524     ROOT::VecOps::RVec<float> energy,
0525     ROOT::VecOps::RVec<float> momx
0526 ) {
0527     // Same processing logic
0528 }, {
0529     "ReconstructedChargedJets.energy",
0530     "ReconstructedChargedJets.momentum.x"
0531 });
0532 ```
0533 
0534 ### Direct RDataFrame for RNTuple (Format-Agnostic Alternative)
0535 
0536 When working directly with RNTuple files and needing maximum compatibility, use ROOT::RDataFrame directly on the "events" tree without going through podio::CreateDataFrame:
0537 
0538 ```cpp
0539 #include <ROOT/RDataFrame.hxx>
0540 
0541 int analyze(const std::string& rec_file) {
0542     // Works with both TTree and RNTuple by using the "events" tree name
0543     ROOT::RDataFrame df("events", rec_file);
0544     
0545     // All column names are directly accessible
0546     auto result = df.Define("Q2_el", "InclusiveKinematicsElectron.Q2")
0547                      .Define("x_el", "InclusiveKinematicsElectron.x")
0548                      .Histo1D({"h_Q2", "; Q2 (GeV^2); counts", 100, 0, 100}, "Q2_el");
0549     
0550     return 0;
0551 }
0552 ```
0553 
0554 **Advantages:**
0555 - ✅ Format-agnostic: Works seamlessly with both `.root` (TTree) and `.rnt.root` (RNTuple)
0556 - ✅ String expressions in `.Define()` work reliably with both formats
0557 - ✅ Simpler than podio::CreateDataFrame when type definitions aren't needed
0558 - ✅ No special includes required (just `ROOT/RDataFrame.hxx`)
0559 
0560 **When to use:**
0561 - When you need simple column access and calculations
0562 - When you need guaranteed compatibility with both TTree and RNTuple formats
0563 - When JIT compilation reliability is important
0564 - See `benchmarks/Inclusive/dis/analysis/dis_electrons.cxx` for a complete real-world example (~800 lines using this pattern)
0565 
0566 **Real-World Example:**
0567 
0568 Migration of dis_electrons.cxx (DIS inclusive kinematics analysis):
0569 
0570 ```cpp
0571 // Before: Used podio::CreateDataFrame (had RNTuple issues)
0572 auto d = podio::CreateDataFrame(rec_file);
0573 d.Define("Q2_esigma", esigma_Q2_col_name)  // Failed with RNTuple
0574 
0575 // After: Direct ROOT::RDataFrame (works with both formats)
0576 ROOT::RDataFrame d("events", rec_file);
0577 d.Define("Q2_esigma", "InclusiveKinematicsESigma.Q2")  // Works!
0578 ```
0579 
0580 This pattern is especially useful for benchmarks that need to process both TTree (for backwards compatibility) and RNTuple (for new analyses) with identical code.
0581 
0582 ### Plain RDataFrame Alternative
0583 
0584 If podio DataFrame is not available, you can use plain RDataFrame which also works with both TTree and RNTuple:
0585 
0586 ```cpp
0587 // Works for both TTree and RNTuple (but less podio-aware)
0588 ROOT::RDataFrame df("events", rec_file);
0589 
0590 auto df_filtered = df.Filter("ReconstructedChargedJets.energy.size() > 0")
0591                      .Define("jet_pt", "sqrt(ReconstructedChargedJets.momentum.x[0]*ReconstructedChargedJets.momentum.x[0] + "
0592                                        "ReconstructedChargedJets.momentum.y[0]*ReconstructedChargedJets.momentum.y[0])");
0593 ```
0594 
0595 This approach requires minimal code changes but may have different performance characteristics.
0596 
0597 ## Migration Considerations and Limitations
0598 
0599 ### Multiple File Processing (TChain Replacement)
0600 
0601 **Limitation:** RNTuple does not have a direct equivalent to TChain for processing multiple files transparently.
0602 
0603 **Solutions:**
0604 
0605 1. **Sequential Processing** (simplest for benchmarks):
0606 ```cpp
0607 for (const auto& filename : input_files) {
0608     auto ntuple = RNTupleReader::Open("events", filename);
0609     // Process each file
0610 }
0611 ```
0612 
0613 2. **RDataFrame with Multiple Files** (recommended for analysis):
0614 ```cpp
0615 ROOT::RDataFrame df("events", {"file1.rnt.root", "file2.rnt.root", "file3.rnt.root"});
0616 // Declarative analysis works across all files
0617 ```
0618 
0619 3. **Format-Agnostic Wrapper** (for mixed TTree/RNTuple workflows):
0620 Create a wrapper class that detects file format and uses TChain for TTree or sequential RNTupleReader for RNTuple.
0621 
0622 ### Backwards Compatibility Strategy
0623 
0624 **Challenge:** Analyzing old TTree data alongside new RNTuple data requires either:
0625 - Two versions of analysis code, or
0626 - Format-agnostic code using RDataFrame
0627 
0628 **Recommendations:**
0629 
0630 1. **For Controlled Environments (Benchmarks)**:
0631    - Each campaign uses one format consistently
0632    - Cross-campaign comparisons can regenerate old data in RNTuple format if needed
0633    - This migration approach is suitable
0634 
0635 2. **For General Analysis (Ongoing Studies)**:
0636    - Use RDataFrame which handles both formats transparently
0637    - Or maintain a thin abstraction layer that dispatches to TTreeReader or RNTupleReader based on file format
0638 
0639 3. **Hybrid Approach**:
0640    - Store format detection logic in a utility function
0641    - Branch analysis code based on detected format
0642    - Example:
0643 ```cpp
0644 bool isRNTuple(const std::string& filename) {
0645     return filename.find(".rnt.root") != std::string::npos ||
0646            filename.find(".rntuple.root") != std::string::npos;
0647 }
0648 ```
0649 
0650 ### Python Support
0651 
0652 **Status:** Both uproot (5.x) and ROOT's Python bindings (PyROOT) support RNTuple starting with ROOT 6.28+.
0653 
0654 ## Python Script Migration Patterns
0655 
0656 Python analysis scripts can be migrated to work transparently with both TTree and RNTuple formats. The recommended approach is using **uproot 5.x**, which provides automatic format detection and a unified API for both formats.
0657 
0658 ### Overview of Python Migration Approaches
0659 
0660 | Approach | Pros | Cons | Best For |
0661 |----------|------|------|----------|
0662 | **uproot 5.x** (Recommended) | ✅ Automatic format detection<br>✅ Pure Python, no ROOT install needed<br>✅ Excellent performance<br>✅ Clean, Pythonic API | ⚠️ Requires uproot >= 4.0 | Most Python analysis scripts |
0663 | **PyROOT with podio** | ✅ Direct access to podio API<br>✅ Uses same pattern as C++ | ⚠️ Requires ROOT install<br>⚠️ Less Pythonic | Scripts that need C++ ROOT features |
0664 
0665 ### Format-Agnostic Python with uproot (Recommended)
0666 
0667 **Key Insight:** uproot 5.x automatically handles both TTree and RNTuple formats with the same syntax. Podio creates RNTuple files with the same `events` tree/RNTuple name and branch structure as TTree files, so existing uproot code often works without modification.
0668 
0669 #### Basic Pattern
0670 
0671 ```python
0672 import uproot
0673 
0674 # This works transparently with both:
0675 # - .edm4eic.root (TTree format)
0676 # - .edm4eic.rnt.root (RNTuple format)
0677 
0678 # Method 1 (Recommended for RNTuple): Direct array access
0679 file = uproot.open(rec_file)
0680 events = file["events"]
0681 data_Q2 = events['CollectionName.Q2'].array(library="ak")
0682 data_x = events['CollectionName.x'].array(library="ak")
0683 
0684 # Method 2 (Legacy, may have RNTuple issues): concatenate()
0685 # keys = uproot.concatenate(rec_file + ':events/' + 'CollectionName')
0686 # data_Q2 = keys['CollectionName.Q2']
0687 # data_x = keys['CollectionName.x']
0688 ```
0689 
0690 **Why it works:**
0691 - uproot 5.x detects whether `events` is a TTree or RNTuple automatically
0692 - Both formats use the same branch/field naming conventions from podio
0693 - Direct `.array()` access is more reliable with RNTuple (avoids context manager issues)
0694 - No code changes needed if branch names are consistent
0695 
0696 **Note:** `concatenate()` may have compatibility issues with RNTuple RField objects. Use direct array access via `.array()` for maximum compatibility.
0697 
0698 #### Adding Format-Agnostic Comments
0699 
0700 To document the format-agnostic capability and help future maintainers:
0701 
0702 ```python
0703 # Format-agnostic data loading: uproot 5.x automatically handles both TTree and RNTuple formats
0704 # Works with:
0705 #   - .edm4eic.root files (TTree format)
0706 #   - .edm4eic.rnt.root files (RNTuple format)
0707 # Both formats created by podio use the same 'events' tree/RNTuple name and branch structure
0708 
0709 file = uproot.open(rec_file)
0710 events = file["events"]
0711 
0712 # Use direct array access instead of concatenate() for better RNTuple compatibility
0713 Truth_Q2 = events['InclusiveKinematicsTruth.Q2'].array(library="ak")
0714 Truth_x = events['InclusiveKinematicsTruth.x'].array(library="ak")
0715 Truth = [Truth_Q2, Truth_x]
0716 ```
0717 
0718 #### Complete Real-World Example
0719 
0720 See `benchmarks/Inclusive/dis/analysis/kinematics_correlations.py` for a complete working example (~225 lines):
0721 
0722 ```python
0723 #!/usr/bin/env python
0724 import uproot as ur
0725 import awkward as ak
0726 import numpy as np
0727 
0728 # No format detection needed - uproot handles it automatically
0729 rec_file = "data.edm4eic.rnt.root"  # Works with both .edm4eic.root (TTree) and .edm4eic.rnt.root (RNTuple)
0730 
0731 file = ur.open(rec_file)
0732 events = file["events"]
0733 
0734 # Load data with direct array access - works reliably with both TTree and RNTuple
0735 Truth_Q2 = events['InclusiveKinematicsTruth.Q2'].array(library="ak")
0736 Truth_x = events['InclusiveKinematicsTruth.x'].array(library="ak")
0737 Truth = [Truth_Q2, Truth_x]
0738 
0739 Electron_Q2 = events['InclusiveKinematicsElectron.Q2'].array(library="ak")
0740 Electron_x = events['InclusiveKinematicsElectron.x'].array(library="ak")
0741 Electron = [Electron_Q2, Electron_x]
0742 
0743 # Process data with awkward arrays (same code for both formats)
0744 Q2values_T = Truth[0]
0745 Xvalues_T = Truth[1]
0746 T_Q2s = np.array(ak.flatten(Q2values_T))
0747 T_Xs = np.array(ak.flatten(Xvalues_T))
0748 
0749 # Create correlation plots
0750 import matplotlib.pyplot as plt
0751 plt.hist2d(T_Q2s, Q2values_E, bins=20)
0752 plt.savefig('Q2_correlation.png')
0753 ```
0754 
0755 **Key points from this working example:**
0756 - ✅ Opens file with `uproot.open()` then accesses the events tree
0757 - ✅ Uses `.array(library="ak")` for direct, reliable access to both formats
0758 - ✅ No try/except needed for format handling
0759 - ✅ Generates correlation plots successfully for both TTree and RNTuple
0760 - ✅ Tested with 100 events from RNTuple files
0761 
0762 ### Advanced: Explicit Format Detection (Optional)
0763 
0764 If you need to handle formats differently or provide informative logging:
0765 
0766 ```python
0767 import uproot
0768 
0769 def detect_format(filename):
0770     """Detect if file contains TTree or RNTuple."""
0771     with uproot.open(filename) as file:
0772         # Check what 'events' is
0773         events = file['events']
0774         if 'TTree' in events.classname:
0775             return 'TTree'
0776         elif 'RNTuple' in events.classname or hasattr(events, 'file'):
0777             return 'RNTuple'
0778     return 'Unknown'
0779 
0780 # Example usage
0781 rec_file = "data.edm4eic.root"
0782 format_type = detect_format(rec_file)
0783 print(f"Detected format: {format_type}")
0784 
0785 # Same code works regardless of format
0786 keys = uproot.concatenate(rec_file + ':events/' + 'Collection')
0787 ```
0788 
0789 ### Alternative: PyROOT with podio.CreateDataFrame
0790 
0791 For scripts that need ROOT features or want to mirror C++ patterns:
0792 
0793 ```python
0794 import ROOT
0795 from podio import CreateDataFrame
0796 
0797 # Load file (format-agnostic, just like C++)
0798 rec_file = "data.edm4eic.root"  # or .rnt.root
0799 df = CreateDataFrame(rec_file)
0800 
0801 # Use RDataFrame operations
0802 # Define columns
0803 df = df.Define("Q2_truth", "InclusiveKinematicsTruth.Q2")
0804 df = df.Define("x_truth", "InclusiveKinematicsTruth.x")
0805 
0806 # Convert to numpy for plotting
0807 Q2_array = df.AsNumpy(["Q2_truth"])["Q2_truth"]
0808 x_array = df.AsNumpy(["x_truth"])["x_truth"]
0809 ```
0810 
0811 **Pros:**
0812 - Mirrors C++ approach exactly
0813 - Access to full ROOT ecosystem
0814 - Type-safe column operations
0815 
0816 **Cons:**
0817 - Requires ROOT installation
0818 - Less Pythonic than uproot
0819 - Harder to use with awkward arrays
0820 
0821 ### Best Practices for Format-Agnostic Python
0822 
0823 1. **Use uproot 5.x or later**
0824    ```bash
0825    pip install 'uproot>=5.0'
0826    ```
0827 
0828 2. **Don't hardcode format assumptions**
0829    ```python
0830    # ❌ Bad - assumes TTree
0831    tree = file['events']
0832    assert isinstance(tree, uproot.TTree)
0833    
0834    # ✅ Good - works with both
0835    events = file['events']
0836    data = events.arrays(['branch1', 'branch2'])
0837    ```
0838 
0839 3. **Rely on consistent naming**
0840    - Podio ensures both TTree and RNTuple files have the same branch/field names
0841    - Use the same collection and field access patterns for both formats
0842 
0843 4. **Add format-agnostic comments**
0844    - Document that code works with both formats
0845    - List the file extensions supported
0846    - Note any assumptions about branch structure
0847 
0848 5. **Test with TTree first, RNTuple later**
0849    - Existing TTree files are the compatibility baseline
0850    - RNTuple files should work with the same code
0851    - If RNTuple doesn't work, that's a bug in the migration (not expected with uproot 5.x)
0852 
0853 6. **Handle legacy compatibility explicitly**
0854    ```python
0855    # Handle renamed collections gracefully
0856    try:
0857        keys = ur.concatenate(rec_file + ':events/' + 'NewCollectionName')
0858    except ur.KeyInFileError:
0859        # Fallback for older files
0860        keys = ur.concatenate(rec_file + ':events/' + 'OldCollectionName')
0861    ```
0862 
0863 ### Migration Checklist for Python Scripts
0864 
0865 - [ ] Verify uproot version is >= 5.0 (`import uproot; print(uproot.__version__)`)
0866 - [ ] Add format-agnostic comment at data loading section
0867 - [ ] Test script runs without errors (syntax, imports, basic logic)
0868 - [ ] Verify awkward array operations work (no format-specific assumptions)
0869 - [ ] Document any collection name fallbacks for legacy compatibility
0870 - [ ] Note in comments that script works with `.edm4eic.root` and `.edm4eic.rnt.root`
0871 
0872 ### Troubleshooting Python Migration
0873 
0874 #### "KeyInFileError: not found in file"
0875 
0876 **Cause:** Branch/field name doesn't exist or collection name changed.
0877 
0878 **Solution:**
0879 ```python
0880 # List available collections
0881 with uproot.open(rec_file) as file:
0882     print(file['events'].keys())  # Works for both TTree and RNTuple
0883 ```
0884 
0885 #### "Different array lengths" or shape mismatches
0886 
0887 **Cause:** Usually not format-related, but rather different event content.
0888 
0889 **Solution:** Verify the input file was created with correct simulation/reconstruction parameters.
0890 
0891 #### Performance differences between TTree and RNTuple
0892 
0893 **Expected:** RNTuple may be faster for columnar access, especially for large files.
0894 
0895 **Action:** No code changes needed. Document observed performance if significant.
0896 
0897 ### Summary
0898 
0899 **For most Python analysis scripts:**
0900 1. Ensure uproot >= 5.0 is installed
0901 2. Add a comment documenting format-agnostic capability
0902 3. Existing code using `uproot.concatenate(file + ':events/' + 'Collection')` should work unchanged
0903 4. Test with existing TTree files to verify no regressions
0904 
0905 **The key insight:** uproot 5.x + podio's consistent naming makes most Python migrations trivial - often just adding documentation rather than changing code.
0906 
0907 For a complete working example, see the migration of `benchmarks/Inclusive/dis/analysis/kinematics_correlations.py`.
0908 
0909 **Usage:**
0910 ```python
0911 import ROOT
0912 
0913 # Open RNTuple file
0914 ntuple = ROOT.RNTupleReader.Open("events", "output.rnt.root")
0915 
0916 # Access data
0917 for entry in ntuple:
0918     energy_view = ntuple.GetView[float]("ReconstructedChargedJets.energy")
0919     # Process data
0920 ```
0921 
0922 **Note:** Python analysis may have different ergonomics than C++. For production Python analysis, consider:
0923 - Using RDataFrame Python interface for declarative analysis
0924 - uproot library (check RNTuple support status in your uproot version)
0925 
0926 ## References
0927 
0928 - [ROOT RNTuple Documentation](https://root.cern/doc/master/md_tree_ntuple_v7_doc_README.html)
0929 - [RNTuple Tutorial](https://root.cern/doc/master/ntpl001__staff_8C.html)
0930 - [EDM4eic Documentation](https://github.com/eic/EDM4eic)
0931 - [Podio Documentation](https://github.com/AIDASoft/podio)
0932 
0933 ## Getting Help
0934 
0935 If you encounter issues during migration:
0936 1. Check this guide's troubleshooting section
0937 2. Verify ROOT version supports RNTuple (>= 6.28)
0938 3. Confirm podio backend is compiled with RNTuple support
0939 4. Ask in the EIC Software Group Slack channel
0940 5. Open an issue in the physics_benchmarks repository
0941 
0942 ## Migration Checklist
0943 
0944 When migrating a benchmark:
0945 
0946 - [ ] Update eicrecon command with `-Ppodio:output_backend=rntuple`
0947 - [ ] Update file extensions to `.edm4eic.rnt.root`
0948 - [ ] Replace `TChain`/`TTreeReader` includes with `RNTupleReader`
0949 - [ ] Convert file opening to `RNTupleReader::Open()` with null check
0950 - [ ] Add try-catch around all `GetView<T>()` calls for error handling
0951 - [ ] Replace all `TTreeReaderArray` with `GetView<T>()`
0952 - [ ] Update event loop from `while (reader.Next())` to `for (auto entryId : *ntuple)`
0953 - [ ] Update array access from `field[i]` to `view(entryId).at(i)`
0954 - [ ] Move view creation outside event loop for performance
0955 - [ ] Test compilation
0956 - [ ] Verify output produces expected results
0957 - [ ] Update documentation/comments mentioning TTree