|
| 1 | +/// \file |
| 2 | +/// \ingroup tutorial_ntuple |
| 3 | +/// \notebook |
| 4 | +/// Example of RNTuple I/O on collections with a SoA memory layout |
| 5 | +/// |
| 6 | +/// RNTuple on-disk collections can be used in struct-of-arrays (SoA) memory layout. |
| 7 | +/// An RNTuple SoA class consists of persistent members of type `RVec` corresponding to |
| 8 | +/// and underlying record type, as shown in this tutorial. |
| 9 | +/// |
| 10 | +/// NOTE: The RNTuple SoA I/O is still experimental at this point. |
| 11 | +/// Functionality and interface are subject to changes. |
| 12 | +/// |
| 13 | +/// \macro_code |
| 14 | +/// |
| 15 | +/// \date April 2026 |
| 16 | +/// \author The ROOT Team |
| 17 | + |
| 18 | +#include <ROOT/REntry.hxx> |
| 19 | +#include <ROOT/RNTupleModel.hxx> |
| 20 | +#include <ROOT/RNTupleReader.hxx> |
| 21 | +#include <ROOT/RNTupleWriter.hxx> |
| 22 | +#include <ROOT/RVec.hxx> |
| 23 | + |
| 24 | +#include <TCanvas.h> |
| 25 | +#include <TClass.h> |
| 26 | +#include <TDictAttributeMap.h> |
| 27 | +#include <TEllipse.h> |
| 28 | +#include <TGraph.h> |
| 29 | +#include <TRandom.h> |
| 30 | + |
| 31 | +#include <iostream> |
| 32 | +#include <memory> |
| 33 | + |
| 34 | +constexpr const char *kFileName = "ntpl020_soa.root"; |
| 35 | +constexpr const char *kNTupleName = "ntpl"; |
| 36 | + |
| 37 | +// The SoA class for this tutorial. Contains a number of 2D points. All vectors have to have the same length. |
| 38 | +// Note that RVecs can adopt memory. |
| 39 | +struct PointSoA { |
| 40 | + ROOT::RVec<float> fX; |
| 41 | + ROOT::RVec<float> fY; |
| 42 | +}; |
| 43 | + |
| 44 | +// The underlying record type for the SoA class. Members between the SoA class and the underlying record type |
| 45 | +// are matched by name. Every member of type `T` in the underlying record type has to be of type `ROOT::RVec<T>` in |
| 46 | +// the SoA class. |
| 47 | +struct PointRecord { |
| 48 | + float fX; |
| 49 | + float fY; |
| 50 | +}; |
| 51 | + |
| 52 | +void Write() |
| 53 | +{ |
| 54 | + // Create a model with a SoA field |
| 55 | + auto model = ROOT::RNTupleModel::CreateBare(); |
| 56 | + model->AddField(ROOT::RFieldBase::Create("points", "PointSoA").Unwrap()); |
| 57 | + |
| 58 | + auto writer = ROOT::RNTupleWriter::Recreate(std::move(model), kNTupleName, kFileName); |
| 59 | + auto entry = writer->GetModel().CreateBareEntry(); |
| 60 | + |
| 61 | + for (auto nPoints : {100, 500, 1000, 10000}) { |
| 62 | + // We will use our own memory are to store the points |
| 63 | + auto memory = std::make_unique<float[]>(nPoints * 2); |
| 64 | + |
| 65 | + // Create random points in the unit square |
| 66 | + gRandom->RndmArray(2 * nPoints, memory.get()); |
| 67 | + |
| 68 | + // Adopt the memory by a PointSoA object. First all x values, then all y values. |
| 69 | + PointSoA points{ROOT::RVec<float>(memory.get(), nPoints), ROOT::RVec<float>(memory.get() + nPoints, nPoints)}; |
| 70 | + |
| 71 | + entry->BindRawPtr("points", &points); |
| 72 | + writer->Fill(*entry); |
| 73 | + } |
| 74 | +} |
| 75 | + |
| 76 | +void Read() |
| 77 | +{ |
| 78 | + auto reader = ROOT::RNTupleReader::Open(kNTupleName, kFileName); |
| 79 | + |
| 80 | + // Show on-disk layout: collection of underlying record type (AoS) |
| 81 | + reader->PrintInfo(); |
| 82 | + |
| 83 | + // Used to draw the points |
| 84 | + auto canvas = new TCanvas("c", "", 1200, 1200); |
| 85 | + canvas->Divide(2, 2); |
| 86 | + |
| 87 | + // Read back the points in two steps: first get the size and then read into adopted RVecs. |
| 88 | + auto viewSize = reader->GetCollectionView("points"); |
| 89 | + PointSoA points; |
| 90 | + auto viewSoA = reader->GetView("points", &points, "PointSoA"); |
| 91 | + |
| 92 | + for (auto i : reader->GetEntryRange()) { |
| 93 | + const auto N = viewSize(i); // size of the SoA collection of the first (and only) entry |
| 94 | + auto memory = std::make_unique<float[]>(N * 2); |
| 95 | + |
| 96 | + points.fX = ROOT::RVec<float>(memory.get(), N); |
| 97 | + points.fY = ROOT::RVec<float>(memory.get() + N, N); |
| 98 | + viewSoA(i); |
| 99 | + |
| 100 | + // Use the raw memory area to draw the points |
| 101 | + canvas->cd(i + 1); |
| 102 | + auto *graph = new TGraph(N, &memory[0], &memory[N]); |
| 103 | + graph->SetTitle((std::to_string(N) + " Points").c_str()); |
| 104 | + graph->SetMarkerStyle(29); |
| 105 | + graph->SetMarkerSize(1); |
| 106 | + graph->SetMarkerColor(kRed); |
| 107 | + graph->Draw("AP"); |
| 108 | + auto *circle = new TEllipse(0.5, 0.5, 0.5, 0.5); |
| 109 | + circle->SetFillStyle(0); |
| 110 | + circle->SetLineColor(kBlue); |
| 111 | + circle->SetLineWidth(4); |
| 112 | + circle->Draw(); |
| 113 | + |
| 114 | + // Use adopted RVec's to approximate PI |
| 115 | + points.fX -= 0.5; |
| 116 | + points.fY -= 0.5; |
| 117 | + auto isInCircle = points.fX * points.fX + points.fY * points.fY < 0.25; |
| 118 | + auto hits = ROOT::VecOps::Sum(isInCircle); |
| 119 | + float approxPI = 4.0 * static_cast<float>(hits) / static_cast<float>(N); |
| 120 | + std::cout << "Approximated PI with " << N << " points to " << approxPI << std::endl; |
| 121 | + } |
| 122 | + canvas->Update(); |
| 123 | +} |
| 124 | + |
| 125 | +void ntpl020_soa() |
| 126 | +{ |
| 127 | + // Usually, the SoA class dictionary definition would mark it as a SoA class of the corresponding |
| 128 | + // underlying record type like this |
| 129 | + // #pragma link C++ options=rntupleSoARecord(PointRecord) class PointSoA+; |
| 130 | + // For the interpreted classes in this tutorial, we mark the SoA class at runtime: |
| 131 | + auto cl = TClass::GetClass("PointSoA"); |
| 132 | + cl->CreateAttributeMap(); |
| 133 | + cl->GetAttributeMap()->AddProperty("rntuple.SoARecord", "PointRecord"); |
| 134 | + |
| 135 | + Write(); |
| 136 | + Read(); |
| 137 | +} |
0 commit comments