From c07f25fdd411c999078ac7050830c78026b7b89e Mon Sep 17 00:00:00 2001 From: silverweed Date: Thu, 19 Feb 2026 10:28:48 +0100 Subject: [PATCH 1/4] [ntuple] add tutorial for low-precision float fields --- .../io/ntuple/ntpl018_low_precision_floats.C | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 tutorials/io/ntuple/ntpl018_low_precision_floats.C diff --git a/tutorials/io/ntuple/ntpl018_low_precision_floats.C b/tutorials/io/ntuple/ntpl018_low_precision_floats.C new file mode 100644 index 0000000000000..8396014a245fd --- /dev/null +++ b/tutorials/io/ntuple/ntpl018_low_precision_floats.C @@ -0,0 +1,96 @@ +/// \file +/// \ingroup tutorial_ntuple +/// \notebook +/// Example creating low-precision floating point fields in RNTuple +/// +/// RNTuple supports 3 kinds of low-precision floating point column types: +/// Real16 (half-precision IEEE754 fp), +/// Real32Trunc (single-precision IEEE754 with truncated mantissa, using from 10 to 31 bits in total) and +/// Real32Quant (floating point within a specified range, represented as an integer with N bits of precision in a +/// linear quantized form). +/// +/// To use these column types in RNTuple, one creates a RField and sets its desired column representation by +/// calling, respectively: +/// - RField::SetHalfPrecision() (for Real16) +/// - RField::SetTruncated() (for Real32Trunc) +/// - RField::SetQuantized() (for Real32Quant) +/// +/// \macro_image +/// \macro_code +/// +/// \date February 2026 +/// \author The ROOT Team + +static constexpr char const *kNTupleName = "ntpl"; +static constexpr char const *kNTupleFileName = "ntpl018_low_precision_floats.root"; +static constexpr int kNEvents = 50; + +static void Write() +{ + auto model = ROOT::RNTupleModel::Create(); + + // Create 3 float fields: one backed by a Real16 column, one backed by a Real32Trunc column + // and one backed by a Real32Quant column. + // Since we need to call methods on the RField objects in order to make them into our specific column types, + // we don't use MakeField but rather we explicitly create the RFields and then use AddField on the model. + { + auto fieldReal16 = std::make_unique>("myReal16"); + fieldReal16->SetHalfPrecision(); // this is now a Real16-backed float field + model->AddField(std::move(fieldReal16)); + } + { + auto fieldReal32Trunc = std::make_unique>("myReal32Trunc"); + // Let's say we want 20 bits of precision. This means that this float's mantissa will be truncated to (20 - 9) = + // 11 bits. + fieldReal32Trunc->SetTruncated(20); + model->AddField(std::move(fieldReal32Trunc)); + } + { + auto fieldReal32Quant = std::make_unique>("myReal32Quant"); + // Declare that this field will never store values outside of the [-1, 1] range (this will be checked dynamically) + // and that we want to dedicate 24 bits to this number on disk. + fieldReal32Quant->SetQuantized(-1., 1., 24); + model->AddField(std::move(fieldReal32Quant)); + } + + // Get the pointers to the fields we just added: + const auto &entry = model->GetDefaultEntry(); + auto myReal16 = entry.GetPtr("myReal16"); + auto myReal32Trunc = entry.GetPtr("myReal32Trunc"); + auto myReal32Quant = entry.GetPtr("myReal32Quant"); + + auto writer = ROOT::RNTupleWriter::Recreate(std::move(model), kNTupleName, kNTupleFileName); + + // fill our entries + gRandom->SetSeed(); + for (int i = 0; i < kNEvents; i++) { + *myReal16 = gRandom->Rndm(); + *myReal32Trunc = gRandom->Rndm(); + *myReal32Quant = gRandom->Rndm(); + writer->Fill(); + } +} + +static void Read() +{ + auto reader = ROOT::RNTupleReader::Open(kNTupleName, kNTupleFileName); + + // We can read back our fields as regular floats. We can also read them as double if we impose our own model when + // creating the reader. + const auto &entry = reader->GetModel().GetDefaultEntry(); + auto myReal16 = entry.GetPtr("myReal16"); + auto myReal32Trunc = entry.GetPtr("myReal32Trunc"); + auto myReal32Quant = entry.GetPtr("myReal32Quant"); + + for (auto idx : reader->GetEntryRange()) { + reader->LoadEntry(idx); + std::cout << "[0] Real16: " << *myReal16 << ", Real32Trunc: " << *myReal32Trunc + << ", Real32Quant: " << *myReal32Quant << "\n"; + } +} + +void ntpl018_low_precision_floats() +{ + Write(); + Read(); +} From 792002205c4e5454dee8c6f3f11481599d36a06f Mon Sep 17 00:00:00 2001 From: Silverweed <7806878+silverweed@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:21:43 +0000 Subject: [PATCH 2/4] [ntuple] ntpl018 tutorial: fix entry numbering Co-authored-by: Florine de Geus --- tutorials/io/ntuple/ntpl018_low_precision_floats.C | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/io/ntuple/ntpl018_low_precision_floats.C b/tutorials/io/ntuple/ntpl018_low_precision_floats.C index 8396014a245fd..a3b787effae86 100644 --- a/tutorials/io/ntuple/ntpl018_low_precision_floats.C +++ b/tutorials/io/ntuple/ntpl018_low_precision_floats.C @@ -84,7 +84,7 @@ static void Read() for (auto idx : reader->GetEntryRange()) { reader->LoadEntry(idx); - std::cout << "[0] Real16: " << *myReal16 << ", Real32Trunc: " << *myReal32Trunc + std::cout << "[" << idx << "] Real16: " << *myReal16 << ", Real32Trunc: " << *myReal32Trunc << ", Real32Quant: " << *myReal32Quant << "\n"; } } From 8cbd23dbb8eb94e20975b009bec133ada63ce903 Mon Sep 17 00:00:00 2001 From: silverweed Date: Mon, 23 Feb 2026 09:09:01 +0100 Subject: [PATCH 3/4] [ntuple] Add struct subfield to ntpl018 tutorial --- .../io/ntuple/ntpl018_low_precision_floats.C | 69 ++++++++++++++++--- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/tutorials/io/ntuple/ntpl018_low_precision_floats.C b/tutorials/io/ntuple/ntpl018_low_precision_floats.C index a3b787effae86..5832f231c2d79 100644 --- a/tutorials/io/ntuple/ntpl018_low_precision_floats.C +++ b/tutorials/io/ntuple/ntpl018_low_precision_floats.C @@ -3,18 +3,25 @@ /// \notebook /// Example creating low-precision floating point fields in RNTuple /// -/// RNTuple supports 3 kinds of low-precision floating point column types: -/// Real16 (half-precision IEEE754 fp), -/// Real32Trunc (single-precision IEEE754 with truncated mantissa, using from 10 to 31 bits in total) and -/// Real32Quant (floating point within a specified range, represented as an integer with N bits of precision in a -/// linear quantized form). +/// RNTuple supports storing floating points on disk with less precision than their in-memory representation. +/// Under the right circumstances, this can in save storage space while not significantly altering the results +/// of an analysis. /// -/// To use these column types in RNTuple, one creates a RField and sets its desired column representation by -/// calling, respectively: +/// Storing low-precision floats is done by setting their column representation to one of the dedicated column types: +/// - Real16 (half-precision IEEE754 fp), +/// - Real32Trunc (single-precision IEEE754 with truncated mantissa, using from 10 to 31 bits in total) and +/// - Real32Quant (floating point within a specified range, represented as an integer with N bits of precision in a +/// linear quantized form). +/// +/// To use these column types in RNTuple, one creates a RField or RField and sets its desired column +/// representation by calling, respectively: /// - RField::SetHalfPrecision() (for Real16) /// - RField::SetTruncated() (for Real32Trunc) /// - RField::SetQuantized() (for Real32Quant) /// +/// Other than these, one can also setup the field to use the ROOT `Double32_t` type, either via +/// RField::SetDouble32() or by directly creating one such field via RFieldBase::Create("f", "Double32_t"). +/// /// \macro_image /// \macro_code /// @@ -25,6 +32,11 @@ static constexpr char const *kNTupleName = "ntpl"; static constexpr char const *kNTupleFileName = "ntpl018_low_precision_floats.root"; static constexpr int kNEvents = 50; +struct Event { + std::vector fPt; + std::vector fE; +}; + static void Write() { auto model = ROOT::RNTupleModel::Create(); @@ -53,11 +65,38 @@ static void Write() model->AddField(std::move(fieldReal32Quant)); } + // We can also change the column type of a struct/class subfield: + { + auto fieldEvents = std::make_unique>("myEvents"); + // Note that we iterate over `*fieldEvents`, not over fieldEvents->GetMutableSubfields(), as the latter won't + // recurse into fieldEvents's grandchildren. By iterating over the field itself we are sure to visit the entire + // field hierarchy, including the fields we need to change. + // The hierarchy of fieldEvents is like this: + // + // myEvents: RField + // fPt: RField> + // _0: RField <-- we need to change this + // fE: RField + // _0: RField <-- we need to change this + // + for (auto &field : *fieldEvents) { + if (auto *fldDouble = dynamic_cast *>(&field)) { + std::cout << "Setting field " << field.GetQualifiedFieldName() << " to truncated.\n"; + fldDouble->SetTruncated(16); + } else if (auto *fldFloat = dynamic_cast *>(&field)) { + std::cout << "Setting field " << field.GetQualifiedFieldName() << " to truncated.\n"; + fldFloat->SetTruncated(16); + } + } + model->AddField(std::move(fieldEvents)); + } + // Get the pointers to the fields we just added: const auto &entry = model->GetDefaultEntry(); auto myReal16 = entry.GetPtr("myReal16"); auto myReal32Trunc = entry.GetPtr("myReal32Trunc"); auto myReal32Quant = entry.GetPtr("myReal32Quant"); + auto myEvents = entry.GetPtr("myEvents"); auto writer = ROOT::RNTupleWriter::Recreate(std::move(model), kNTupleName, kNTupleFileName); @@ -67,6 +106,8 @@ static void Write() *myReal16 = gRandom->Rndm(); *myReal32Trunc = gRandom->Rndm(); *myReal32Quant = gRandom->Rndm(); + myEvents->fPt.push_back(i); + myEvents->fE.push_back(i); writer->Fill(); } } @@ -81,11 +122,23 @@ static void Read() auto myReal16 = entry.GetPtr("myReal16"); auto myReal32Trunc = entry.GetPtr("myReal32Trunc"); auto myReal32Quant = entry.GetPtr("myReal32Quant"); + auto myEvents = entry.GetPtr("myEvents"); for (auto idx : reader->GetEntryRange()) { reader->LoadEntry(idx); + + float eventsAvgPt = 0.f; + for (float pt : myEvents->fPt) + eventsAvgPt += pt; + eventsAvgPt /= myEvents->fPt.size(); + double eventsAvgE = 0.f; + for (double e : myEvents->fE) + eventsAvgE += e; + eventsAvgE /= myEvents->fE.size(); + std::cout << "[" << idx << "] Real16: " << *myReal16 << ", Real32Trunc: " << *myReal32Trunc - << ", Real32Quant: " << *myReal32Quant << "\n"; + << ", Real32Quant: " << *myReal32Quant << ", Events avg pt: " << eventsAvgPt << ", E: " << eventsAvgE + << "\n"; } } From 1a9151ad9aa03877144af8897a43fda691119368 Mon Sep 17 00:00:00 2001 From: silverweed Date: Tue, 24 Feb 2026 08:54:42 +0100 Subject: [PATCH 4/4] [NFC][ntuple] add missing RNTuple tutorials to index.md --- tutorials/io/ntuple/index.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tutorials/io/ntuple/index.md b/tutorials/io/ntuple/index.md index 07171423a0cf3..3269cfb2405f3 100644 --- a/tutorials/io/ntuple/index.md +++ b/tutorials/io/ntuple/index.md @@ -18,3 +18,6 @@ RNTuple is the evolution of TTree, offering better performance and new, more rob | ntpl013_staged.C | Apply staged cluster committing to multithreaded writing with the RNTupleParallelWriter | | ntpl014_framework.C | Use the various (more advanced) RNTuple interfaces to write data in the context of a framework | | ntpl015_processor_join.C | Join the entries from two RNTuples on a common field value and read it using the RNTupleProcessor | +| ntpl016_streaming_vector.C | A special purpose container that reads large vectors piece-wise | +| ntpl017_shared_reader.C | Efficient multi-threaded reading when multiple threads share a single reader | +| ntpl018_low_precision_floats.C | Creating low-precision floating point fields |