-
Notifications
You must be signed in to change notification settings - Fork 1.5k
[ntuple] add tutorial for low-precision float fields #21318
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
silverweed
merged 4 commits into
root-project:master
from
silverweed:ntuple_tutorial_lowprecfloat
Feb 24, 2026
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
c07f25f
[ntuple] add tutorial for low-precision float fields
silverweed 7920022
[ntuple] ntpl018 tutorial: fix entry numbering
silverweed 8cbd23d
[ntuple] Add struct subfield to ntpl018 tutorial
silverweed 1a9151a
[NFC][ntuple] add missing RNTuple tutorials to index.md
silverweed File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| /// \file | ||
| /// \ingroup tutorial_ntuple | ||
| /// \notebook | ||
| /// Example creating low-precision floating point fields in RNTuple | ||
| /// | ||
| /// 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. | ||
| /// | ||
| /// 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<float> or RField<double> and sets its desired column | ||
| /// representation by calling, respectively: | ||
| /// - RField<float>::SetHalfPrecision() (for Real16) | ||
| /// - RField<float>::SetTruncated() (for Real32Trunc) | ||
| /// - RField<float>::SetQuantized() (for Real32Quant) | ||
| /// | ||
| /// Other than these, one can also setup the field to use the ROOT `Double32_t` type, either via | ||
| /// RField<double>::SetDouble32() or by directly creating one such field via RFieldBase::Create("f", "Double32_t"). | ||
| /// | ||
| /// \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; | ||
|
|
||
| struct Event { | ||
| std::vector<float> fPt; | ||
| std::vector<double> fE; | ||
| }; | ||
|
|
||
| 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<ROOT::RField<float>>("myReal16"); | ||
| fieldReal16->SetHalfPrecision(); // this is now a Real16-backed float field | ||
| model->AddField(std::move(fieldReal16)); | ||
| } | ||
| { | ||
| auto fieldReal32Trunc = std::make_unique<ROOT::RField<float>>("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<ROOT::RField<float>>("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)); | ||
| } | ||
|
|
||
| // We can also change the column type of a struct/class subfield: | ||
| { | ||
| auto fieldEvents = std::make_unique<ROOT::RField<Event>>("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<Event> | ||
| // fPt: RField<vector<float>> | ||
| // _0: RField<float> <-- we need to change this | ||
| // fE: RField<vector<double> | ||
| // _0: RField<double> <-- we need to change this | ||
| // | ||
| for (auto &field : *fieldEvents) { | ||
| if (auto *fldDouble = dynamic_cast<ROOT::RField<double> *>(&field)) { | ||
| std::cout << "Setting field " << field.GetQualifiedFieldName() << " to truncated.\n"; | ||
| fldDouble->SetTruncated(16); | ||
| } else if (auto *fldFloat = dynamic_cast<ROOT::RField<float> *>(&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<float>("myReal16"); | ||
| auto myReal32Trunc = entry.GetPtr<float>("myReal32Trunc"); | ||
| auto myReal32Quant = entry.GetPtr<float>("myReal32Quant"); | ||
| auto myEvents = entry.GetPtr<Event>("myEvents"); | ||
|
|
||
| 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(); | ||
| myEvents->fPt.push_back(i); | ||
| myEvents->fE.push_back(i); | ||
| 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<float>("myReal16"); | ||
| auto myReal32Trunc = entry.GetPtr<float>("myReal32Trunc"); | ||
| auto myReal32Quant = entry.GetPtr<float>("myReal32Quant"); | ||
| auto myEvents = entry.GetPtr<Event>("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 << ", Events avg pt: " << eventsAvgPt << ", E: " << eventsAvgE | ||
| << "\n"; | ||
| } | ||
| } | ||
|
|
||
| void ntpl018_low_precision_floats() | ||
| { | ||
| Write(); | ||
| Read(); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.