Skip to content

Commit 04a5ab3

Browse files
committed
[ntuple] add GetAttributes methods to RNTupleAttrSetReader
1 parent 10517c7 commit 04a5ab3

File tree

3 files changed

+280
-2
lines changed

3 files changed

+280
-2
lines changed

tree/ntuple/inc/ROOT/RNTupleAttrReading.hxx

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class RNTupleModel;
2525

2626
namespace Experimental {
2727

28+
class RNTupleAttrEntryIterable;
29+
2830
// clang-format off
2931
/**
3032
\class ROOT::Experimental::RNTupleAttrRange
@@ -111,6 +113,7 @@ for (auto idx : attrSet->GetAttributes(10)) {
111113
// clang-format on
112114
class RNTupleAttrSetReader final {
113115
friend class ROOT::RNTupleReader;
116+
friend class RNTupleAttrEntryIterable;
114117

115118
/// List containing pairs { entryRange, entryIndex }, used to quickly find out which entries in the Attribute
116119
/// RNTuple contain entries that overlap a given range. The list is sorted by range start, i.e.
@@ -148,6 +151,91 @@ public:
148151

149152
/// Returns the number of all attribute entries in this attribute set.
150153
std::size_t GetNEntries() const { return fEntryRanges.size(); }
154+
155+
/// Returns all the attributes in this Set. The returned attributes are sorted by entry range start.
156+
RNTupleAttrEntryIterable GetAttributes();
157+
/// Returns all the attributes whose range contains index `entryIndex`.
158+
RNTupleAttrEntryIterable GetAttributes(NTupleSize_t entryIndex);
159+
/// Returns all the attributes whose range fully contains `[startEntry, endEntry)`
160+
RNTupleAttrEntryIterable GetAttributesContainingRange(NTupleSize_t startEntry, NTupleSize_t endEntry);
161+
/// Returns all the attributes whose range is fully contained in `[startEntry, endEntry)`
162+
RNTupleAttrEntryIterable GetAttributesInRange(NTupleSize_t startEntry, NTupleSize_t endEntry);
163+
};
164+
165+
// clang-format off
166+
/**
167+
\class ROOT::Experimental::RNTupleAttrEntryIterable
168+
\ingroup NTuple
169+
\brief Iterable class used to loop over attribute entries.
170+
171+
This class allows to perform range-for iteration on some set of attributes, typically returned by the
172+
RNTupleAttrSetReader::GetAttributes family of methods.
173+
174+
See the documentation of RNTupleAttrSetReader for example usage.
175+
*/
176+
// clang-format on
177+
class RNTupleAttrEntryIterable final {
178+
public:
179+
struct RFilter {
180+
RNTupleAttrRange fRange;
181+
bool fIsContained;
182+
};
183+
184+
private:
185+
RNTupleAttrSetReader *fReader = nullptr;
186+
std::optional<RFilter> fFilter;
187+
188+
public:
189+
class RIterator final {
190+
private:
191+
using Iter_t = decltype(std::declval<RNTupleAttrSetReader>().fEntryRanges.begin());
192+
Iter_t fCur, fEnd;
193+
std::optional<RFilter> fFilter;
194+
195+
Iter_t SkipFiltered() const;
196+
bool FullyContained(RNTupleAttrRange range) const;
197+
198+
public:
199+
using iterator_category = std::forward_iterator_tag;
200+
using iterator = RIterator;
201+
using value_type = NTupleSize_t;
202+
using difference_type = std::ptrdiff_t;
203+
using pointer = const value_type *;
204+
using reference = const value_type &;
205+
206+
RIterator(Iter_t iter, Iter_t end, std::optional<RFilter> filter) : fCur(iter), fEnd(end), fFilter(filter)
207+
{
208+
if (fFilter) {
209+
if (fFilter->fRange.GetLength() == 0)
210+
fCur = end;
211+
else
212+
fCur = SkipFiltered();
213+
}
214+
}
215+
iterator operator++()
216+
{
217+
++fCur;
218+
fCur = SkipFiltered();
219+
return *this;
220+
}
221+
iterator operator++(int)
222+
{
223+
iterator it = *this;
224+
operator++();
225+
return it;
226+
}
227+
reference operator*() { return fCur->second; }
228+
bool operator!=(const iterator &rh) const { return !operator==(rh); }
229+
bool operator==(const iterator &rh) const { return fCur == rh.fCur; }
230+
};
231+
232+
explicit RNTupleAttrEntryIterable(RNTupleAttrSetReader &reader, std::optional<RFilter> filter = {})
233+
: fReader(&reader), fFilter(filter)
234+
{
235+
}
236+
237+
RIterator begin() { return RIterator{fReader->fEntryRanges.begin(), fReader->fEntryRanges.end(), fFilter}; }
238+
RIterator end() { return RIterator{fReader->fEntryRanges.end(), fReader->fEntryRanges.end(), fFilter}; }
151239
};
152240

153241
} // namespace Experimental

tree/ntuple/src/RNTupleAttrReading.cxx

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,93 @@ std::unique_ptr<ROOT::REntry> ROOT::Experimental::RNTupleAttrSetReader::CreateEn
112112
{
113113
return fUserModel->CreateEntry();
114114
}
115+
116+
ROOT::Experimental::RNTupleAttrEntryIterable
117+
ROOT::Experimental::RNTupleAttrSetReader::GetAttributesContainingRange(NTupleSize_t startEntry, NTupleSize_t endEntry)
118+
{
119+
RNTupleAttrRange range;
120+
if (endEntry <= startEntry) {
121+
R__LOG_WARNING(ROOT::Internal::NTupleLog())
122+
<< "empty range given when getting attributes from Attribute Set '" << GetDescriptor().GetName()
123+
<< "' (range given: [" << startEntry << ", " << endEntry << ")).";
124+
// Make sure we find 0 entries
125+
range = RNTupleAttrRange::FromStartLength(startEntry, 0);
126+
} else {
127+
range = RNTupleAttrRange::FromStartEnd(startEntry, endEntry);
128+
}
129+
RNTupleAttrEntryIterable::RFilter filter{range, /*fIsContained=*/false};
130+
return RNTupleAttrEntryIterable{*this, filter};
131+
}
132+
133+
ROOT::Experimental::RNTupleAttrEntryIterable
134+
ROOT::Experimental::RNTupleAttrSetReader::GetAttributesInRange(NTupleSize_t startEntry, NTupleSize_t endEntry)
135+
{
136+
RNTupleAttrRange range;
137+
if (endEntry <= startEntry) {
138+
R__LOG_WARNING(ROOT::Internal::NTupleLog())
139+
<< "empty range given when getting attributes from Attribute Set '" << GetDescriptor().GetName()
140+
<< "' (range given: [" << startEntry << ", " << endEntry << ")).";
141+
// Make sure we find 0 entries
142+
range = RNTupleAttrRange::FromStartLength(startEntry, 0);
143+
} else {
144+
range = RNTupleAttrRange::FromStartEnd(startEntry, endEntry);
145+
}
146+
RNTupleAttrEntryIterable::RFilter filter{range, /*fIsContained=*/true};
147+
return RNTupleAttrEntryIterable{*this, filter};
148+
}
149+
150+
ROOT::Experimental::RNTupleAttrEntryIterable
151+
ROOT::Experimental::RNTupleAttrSetReader::GetAttributes(NTupleSize_t entryIndex)
152+
{
153+
RNTupleAttrEntryIterable::RFilter filter{RNTupleAttrRange::FromStartEnd(entryIndex, entryIndex + 1),
154+
/*fIsContained=*/false};
155+
return RNTupleAttrEntryIterable{*this, filter};
156+
}
157+
158+
ROOT::Experimental::RNTupleAttrEntryIterable ROOT::Experimental::RNTupleAttrSetReader::GetAttributes()
159+
{
160+
return RNTupleAttrEntryIterable{*this};
161+
}
162+
163+
//
164+
// RNTupleAttrEntryIterable
165+
//
166+
bool ROOT::Experimental::RNTupleAttrEntryIterable::RIterator::FullyContained(RNTupleAttrRange range) const
167+
{
168+
assert(fFilter);
169+
if (fFilter->fIsContained) {
170+
return fFilter->fRange.GetStart() <= range.GetStart() && range.GetEnd() <= fFilter->fRange.GetEnd();
171+
} else {
172+
return range.GetStart() <= fFilter->fRange.GetStart() && fFilter->fRange.GetEnd() <= range.GetEnd();
173+
}
174+
}
175+
176+
ROOT::Experimental::RNTupleAttrEntryIterable::RIterator::Iter_t
177+
ROOT::Experimental::RNTupleAttrEntryIterable::RIterator::SkipFiltered() const
178+
{
179+
// If we have no filter, every entry is valid.
180+
if (!fFilter)
181+
return fCur;
182+
183+
// TODO: consider using binary search, since fEntryRanges is sorted
184+
// (maybe it should be done only if the size of the list is bigger than a threshold).
185+
for (auto it = fCur; it != fEnd; ++it) {
186+
const auto &[range, index] = *it;
187+
const auto &firstLast = range.GetFirstLast();
188+
// If this is nullopt it means this is a zero-length entry: we always skip those except
189+
// for the "catch-all" GetAttributes() (which is when fFilter is also nullopt).
190+
if (!firstLast)
191+
continue;
192+
193+
const auto &[first, last] = *firstLast;
194+
if (first >= fFilter->fRange.GetEnd()) {
195+
// Since fEntryRanges is sorted we know we are at the end of the iteration
196+
// TODO: tweak fEnd to directly pass the last entry?
197+
return fEnd;
198+
}
199+
200+
if (FullyContained(RNTupleAttrRange::FromStartEnd(first, last + 1)))
201+
return it;
202+
}
203+
return fEnd;
204+
}

tree/ntuple/test/ntuple_attributes.cxx

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,61 @@ TEST(RNTupleAttributes, BasicReadingWriting)
102102
EXPECT_EQ(ntuple, nullptr);
103103
}
104104

105+
/// Reading
105106
auto reader = RNTupleReader::Open("ntuple", fileGuard.GetPath());
106107
EXPECT_EQ(reader->GetDescriptor().GetNAttributeSets(), 1);
107108
for (const auto &attrSetIt : reader->GetDescriptor().GetAttrSetIterable()) {
108109
EXPECT_EQ(attrSetIt.GetName(), "AttrSet1");
109110
}
111+
112+
auto attrSetReader = reader->OpenAttributeSet("AttrSet1");
113+
EXPECT_EQ(attrSetReader->GetNEntries(), 1);
114+
auto pAttr = attrSetReader->GetModel().GetDefaultEntry().GetPtr<std::string>("attr");
115+
{
116+
int nAttrs = 0;
117+
// iterate all attributes
118+
for (auto idx : attrSetReader->GetAttributes()) {
119+
attrSetReader->LoadEntry(idx);
120+
EXPECT_EQ(*pAttr, "My Attribute");
121+
nAttrs += 1;
122+
}
123+
EXPECT_EQ(nAttrs, 1);
124+
}
125+
{
126+
int nAttrs = 0;
127+
// attributes containing entry 99
128+
for (auto idx : attrSetReader->GetAttributes(99)) {
129+
attrSetReader->LoadEntry(idx);
130+
EXPECT_EQ(*pAttr, "My Attribute");
131+
nAttrs += 1;
132+
}
133+
EXPECT_EQ(nAttrs, 1);
134+
}
135+
{
136+
// attributes containing entry 100 (no entry)
137+
auto iter = attrSetReader->GetAttributes(100);
138+
EXPECT_EQ(iter.begin(), iter.end());
139+
}
140+
{
141+
// attributes contained in entry range 50-200 (no entry)
142+
auto iter = attrSetReader->GetAttributesInRange(50, 200);
143+
EXPECT_EQ(iter.begin(), iter.end());
144+
}
145+
{
146+
int nAttrs = 0;
147+
// attributes contained in entry range 0-1000
148+
for (auto idx : attrSetReader->GetAttributesInRange(0, 1000)) {
149+
attrSetReader->LoadEntry(idx);
150+
EXPECT_EQ(*pAttr, "My Attribute");
151+
nAttrs += 1;
152+
}
153+
EXPECT_EQ(nAttrs, 1);
154+
}
155+
{
156+
// attributes containing entry range 200-300 (no entry)
157+
auto iter = attrSetReader->GetAttributesContainingRange(200, 300);
158+
EXPECT_EQ(iter.begin(), iter.end());
159+
}
110160
}
111161

112162
TEST(RNTupleAttributes, BasicWritingWithExplicitEntry)
@@ -201,13 +251,13 @@ TEST(RNTupleAttributes, MultipleSets)
201251

202252
auto attrModel1 = RNTupleModel::Create();
203253
auto pInt1 = attrModel1->MakeField<int>("int");
204-
auto attrSet1 = writer->CreateAttributeSet(attrModel1->Clone(), "MyAttrSet1");
254+
auto attrSet1 = writer->CreateAttributeSet(std::move(attrModel1), "MyAttrSet1");
205255

206256
auto attrModel2 = RNTupleModel::Create();
207257
auto pString2 = attrModel2->MakeField<std::string>("string");
208258
auto attrOpts2 = ROOT::RNTupleWriteOptions();
209259
attrOpts2.SetCompression(404);
210-
auto attrSet2 = writer->CreateAttributeSet(attrModel2->Clone(), "MyAttrSet2", &attrOpts2);
260+
auto attrSet2 = writer->CreateAttributeSet(std::move(attrModel2), "MyAttrSet2", &attrOpts2);
211261

212262
auto attrRange2 = attrSet2->BeginRange();
213263
for (int i = 0; i < 100; ++i) {
@@ -266,6 +316,56 @@ TEST(RNTupleAttributes, MultipleSets)
266316
if (++nHeader == 3)
267317
break;
268318
}
319+
320+
// check attribute ranges
321+
auto attrEntry1 = attrSetReader1->CreateEntry();
322+
auto pAttrInt = attrEntry1->GetPtr<int>("int");
323+
auto attrEntry2 = attrSetReader2->CreateEntry();
324+
auto pAttrString = attrEntry2->GetPtr<std::string>("string");
325+
{
326+
int nAttrs = 0;
327+
for (auto idx : attrSetReader1->GetAttributesInRange(0, 1000)) {
328+
auto range = attrSetReader1->LoadEntry(idx, *attrEntry1);
329+
EXPECT_EQ(*pAttrInt, idx);
330+
EXPECT_EQ(range.GetStart(), idx);
331+
EXPECT_EQ(range.GetLength(), 1);
332+
nAttrs += 1;
333+
}
334+
EXPECT_EQ(nAttrs, 100);
335+
}
336+
{
337+
int nAttrs = 0;
338+
for (auto idx : attrSetReader1->GetAttributes(42)) {
339+
auto range = attrSetReader1->LoadEntry(idx, *attrEntry1);
340+
EXPECT_EQ(*pAttrInt, 42);
341+
EXPECT_EQ(range.GetStart(), 42);
342+
EXPECT_EQ(range.GetLength(), 1);
343+
nAttrs += 1;
344+
}
345+
EXPECT_EQ(nAttrs, 1);
346+
}
347+
{
348+
int nAttrs = 0;
349+
for (auto idx : attrSetReader2->GetAttributes()) {
350+
auto range = attrSetReader2->LoadEntry(idx, *attrEntry2);
351+
EXPECT_EQ(*pAttrString, "Run 1");
352+
EXPECT_EQ(range.GetStart(), 0);
353+
EXPECT_EQ(range.GetLength(), 100);
354+
nAttrs += 1;
355+
}
356+
EXPECT_EQ(nAttrs, 1);
357+
}
358+
{
359+
for (auto idx : attrSetReader2->GetAttributes()) {
360+
// Reading into the wrong entry
361+
try {
362+
attrSetReader2->LoadEntry(idx, *attrEntry1);
363+
FAIL() << "reading into an unrelated entry should fail";
364+
} catch (const ROOT::RException &ex) {
365+
EXPECT_THAT(ex.what(), testing::HasSubstr("mismatch between entry and model"));
366+
}
367+
}
368+
}
269369
}
270370

271371
TEST(RNTupleAttributes, AttributeInvalidModel)

0 commit comments

Comments
 (0)