Skip to content

Commit c87697d

Browse files
committed
[ntuple] add SoA tutorial
1 parent f5a9d2a commit c87697d

1 file changed

Lines changed: 127 additions & 0 deletions

File tree

tutorials/io/ntuple/ntpl020_soa.C

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
constexpr unsigned int kNPoints = 10000;
37+
38+
// The SoA class for this tutorial. Contains a number of 2D points. All vectors have to have the same length.
39+
// Note that RVecs can adopt memory.
40+
struct PointSoA {
41+
ROOT::RVec<float> fX;
42+
ROOT::RVec<float> fY;
43+
};
44+
45+
// The underlying record type for the SoA class. Members between the SoA class and the underlying record type
46+
// are matched by name. Every member of type `T` in the underlying record type has to be of type `ROOT::RVec<T>` in
47+
// the SoA class.
48+
struct PointRecord {
49+
float fX;
50+
float fY;
51+
};
52+
53+
void Write()
54+
{
55+
// We will use our own memory are to store the points
56+
auto memory = std::make_unique<float[]>(kNPoints * 2);
57+
58+
// Create random points in the unit square
59+
gRandom->RndmArray(2 * kNPoints, memory.get());
60+
61+
// Adopt the memory by a PointSoA object. First all x values, then all y values.
62+
PointSoA points{ROOT::RVec<float>(memory.get(), kNPoints),
63+
ROOT::RVec<float>(memory.get() + kNPoints, kNPoints)};
64+
65+
// Create a model with a SoA field
66+
auto model = ROOT::RNTupleModel::CreateBare();
67+
model->AddField(ROOT::RFieldBase::Create("points", "PointSoA").Unwrap());
68+
69+
// Write a single entry with the points
70+
auto writer = ROOT::RNTupleWriter::Recreate(std::move(model), kNTupleName, kFileName);
71+
auto entry = writer->GetModel().CreateBareEntry();
72+
entry->BindRawPtr("points", &points);
73+
writer->Fill(*entry);
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+
// Read back the points in two steps: first get the size and then read into adopted RVecs.
84+
auto viewSize = reader->GetCollectionView("points");
85+
const auto N = viewSize(0); // size of the SoA collection of the first (and only) entry
86+
auto memory = std::make_unique<float[]>(N * 2);
87+
88+
PointSoA points{ROOT::RVec<float>(memory.get(), N), ROOT::RVec<float>(memory.get() + N, N)};
89+
auto viewSoA = reader->GetView("points", &points, "PointSoA");
90+
viewSoA(0);
91+
92+
// Use the raw memory area to draw the points
93+
auto c = new TCanvas("c", "", 1000, 1000);
94+
auto *graph = new TGraph(N, &memory[0], &memory[N]);
95+
graph->SetMarkerStyle(29);
96+
graph->SetMarkerSize(1);
97+
graph->SetMarkerColor(kRed);
98+
graph->Draw("AP");
99+
auto *circle = new TEllipse(0.5, 0.5, 0.5, 0.5);
100+
circle->SetFillStyle(0);
101+
circle->SetLineColor(kBlue);
102+
circle->SetLineWidth(4);
103+
circle->Draw();
104+
c->Update();
105+
106+
// Use adopted RVec's to approximate PI
107+
points.fX -= 0.5;
108+
points.fY -= 0.5;
109+
auto isInCircle = points.fX * points.fX + points.fY * points.fY < 0.25;
110+
auto hits = ROOT::VecOps::Sum(isInCircle);
111+
float approxPI = 4.0 * static_cast<float>(hits) / static_cast<float>(N);
112+
std::cout << "Approximated PI to " << approxPI << std::endl;
113+
}
114+
115+
void ntpl020_soa()
116+
{
117+
// Usually, the SoA class dictionary definition would mark it as a SoA class of the corresponding
118+
// underlying record type like this
119+
// #pragma link C++ options=rntupleSoARecord(PointRecord) class PointSoA+;
120+
// For the interpreted classes in this tutorial, we mark the SoA class at runtime:
121+
auto cl = TClass::GetClass("PointSoA");
122+
cl->CreateAttributeMap();
123+
cl->GetAttributeMap()->AddProperty("rntuple.SoARecord", "PointRecord");
124+
125+
Write();
126+
Read();
127+
}

0 commit comments

Comments
 (0)