Skip to content

Commit 0897dc6

Browse files
committed
[ntuple] add SoA tutorial
1 parent fa7d58b commit 0897dc6

1 file changed

Lines changed: 137 additions & 0 deletions

File tree

tutorials/io/ntuple/ntpl020_soa.C

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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

Comments
 (0)