Skip to content

Commit

Permalink
[ntuple] Add RNTupleModel::RegisterSubfield
Browse files Browse the repository at this point in the history
This method allows users to register subfields to an RNTuple model for
direct access to the values of these fields in entries belonging to the
model. Registerging subfields in a collection is currently not
permitted, because this involves more complex restructuring of the
subfield itself (essentially having to turn the leaf field itself into a
collection). This will be added at a later point, borrowing from the
implementation of `RNTupleDS`.
  • Loading branch information
enirolf committed Oct 28, 2024
1 parent fe0f82d commit 63b8243
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 3 deletions.
6 changes: 3 additions & 3 deletions tree/ntuple/v7/inc/ROOT/REntry.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,15 @@ private:

void AddValue(RFieldBase::RValue &&value)
{
fFieldName2Token[value.GetField().GetFieldName()] = fValues.size();
fFieldName2Token[value.GetField().GetQualifiedFieldName()] = fValues.size();
fValues.emplace_back(std::move(value));
}

/// While building the entry, adds a new value to the list and return the value's shared pointer
template <typename T, typename... ArgsT>
std::shared_ptr<T> AddValue(RField<T> &field, ArgsT &&...args)
{
fFieldName2Token[field.GetFieldName()] = fValues.size();
fFieldName2Token[field.GetQualifiedFieldName()] = fValues.size();
auto ptr = std::make_shared<T>(std::forward<ArgsT>(args)...);
fValues.emplace_back(field.BindValue(ptr));
return ptr;
Expand Down Expand Up @@ -136,7 +136,7 @@ private:
if constexpr (!std::is_void_v<T>) {
const auto &v = fValues[token.fIndex];
if (v.GetField().GetTypeName() != RField<T>::TypeName()) {
throw RException(R__FAIL("type mismatch for field " + v.GetField().GetFieldName() + ": " +
throw RException(R__FAIL("type mismatch for field " + v.GetField().GetQualifiedFieldName() + ": " +
v.GetField().GetTypeName() + " vs. " + RField<T>::TypeName()));
}
}
Expand Down
9 changes: 9 additions & 0 deletions tree/ntuple/v7/inc/ROOT/RNTupleModel.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ private:
std::string fDescription;
/// The set of projected top-level fields
std::unique_ptr<Internal::RProjectedFields> fProjectedFields;
/// Keeps track of which subfields have been registered to be included in entries belonging to this model.
std::unordered_set<std::string> fRegisteredSubfields;
/// Every model has a unique ID to distinguish it from other models. Entries are linked to models via the ID.
/// Cloned models get a new model ID.
std::uint64_t fModelId = 0;
Expand All @@ -225,6 +227,8 @@ private:
/// The field name can be a top-level field or a nested field. Returns nullptr if the field is not in the model.
RFieldBase *FindField(std::string_view fieldName) const;

void AddSubfield(std::string_view fieldName, REntry &entry, bool initializeValue = true) const;

RNTupleModel(std::unique_ptr<RFieldZero> fieldZero);

public:
Expand Down Expand Up @@ -305,6 +309,11 @@ public:
/// Throws an exception if the field is null.
void AddField(std::unique_ptr<RFieldBase> field);

/// Registers the provided subfield to the model's default entry.
///
/// Throws an exception if the provided subfield could not be found in the model.
void RegisterSubfield(std::string_view qualifiedFieldName);

/// Adds a top-level field based on existing fields.
///
/// The mapping function takes one argument, which is a string containing the name of the projected field. The return
Expand Down
47 changes: 47 additions & 0 deletions tree/ntuple/v7/src/RNTupleModel.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -275,11 +275,15 @@ std::unique_ptr<ROOT::Experimental::RNTupleModel> ROOT::Experimental::RNTupleMod
cloneModel->fFieldNames = fFieldNames;
cloneModel->fDescription = fDescription;
cloneModel->fProjectedFields = fProjectedFields->Clone(*cloneModel);
cloneModel->fRegisteredSubfields = fRegisteredSubfields;
if (fDefaultEntry) {
cloneModel->fDefaultEntry = std::unique_ptr<REntry>(new REntry(cloneModel->fModelId, cloneModel->fSchemaId));
for (const auto &f : cloneModel->fFieldZero->GetSubFields()) {
cloneModel->fDefaultEntry->AddValue(f->CreateValue());
}
for (const auto &f : cloneModel->fRegisteredSubfields) {
cloneModel->AddSubfield(f, *cloneModel->fDefaultEntry);
}
}
return cloneModel;
}
Expand Down Expand Up @@ -318,6 +322,43 @@ void ROOT::Experimental::RNTupleModel::AddField(std::unique_ptr<RFieldBase> fiel
fFieldZero->Attach(std::move(field));
}

void ROOT::Experimental::RNTupleModel::AddSubfield(std::string_view qualifiedFieldName, REntry &entry,
bool initializeValue) const
{
auto field = FindField(qualifiedFieldName);
if (initializeValue)
entry.AddValue(field->CreateValue());
else
entry.AddValue(field->BindValue(nullptr));
}

void ROOT::Experimental::RNTupleModel::RegisterSubfield(std::string_view qualifiedFieldName)
{
if (qualifiedFieldName.empty())
throw RException(R__FAIL("no field name provided"));

if (fRegisteredSubfields.find(std::string(qualifiedFieldName)) != fRegisteredSubfields.end())
throw RException(R__FAIL("subfield already registered"));

EnsureNotFrozen();

auto *field = FindField(qualifiedFieldName);
if (!field) {
throw RException(R__FAIL("could not find subfield \"" + std::string(qualifiedFieldName) + "\" in model"));
}

auto parent = field->GetParent();
while (parent && !parent->GetFieldName().empty()) {
if (parent->GetStructure() == ENTupleStructure::kCollection) {
throw RException(R__FAIL("registering a subfield in a collection is not supported"));
}
parent = parent->GetParent();
}

fRegisteredSubfields.emplace(qualifiedFieldName);
AddSubfield(qualifiedFieldName, *fDefaultEntry);
}

ROOT::Experimental::RResult<void>
ROOT::Experimental::RNTupleModel::AddProjectedField(std::unique_ptr<RFieldBase> field, FieldMappingFunc_t mapping)
{
Expand Down Expand Up @@ -397,6 +438,9 @@ std::unique_ptr<ROOT::Experimental::REntry> ROOT::Experimental::RNTupleModel::Cr
for (const auto &f : fFieldZero->GetSubFields()) {
entry->AddValue(f->CreateValue());
}
for (const auto &f : fRegisteredSubfields) {
AddSubfield(f, *entry);
}
return entry;
}

Expand All @@ -409,6 +453,9 @@ std::unique_ptr<ROOT::Experimental::REntry> ROOT::Experimental::RNTupleModel::Cr
for (const auto &f : fFieldZero->GetSubFields()) {
entry->AddValue(f->BindValue(nullptr));
}
for (const auto &f : fRegisteredSubfields) {
AddSubfield(f, *entry, false /* initializeValue */);
}
return entry;
}

Expand Down
103 changes: 103 additions & 0 deletions tree/ntuple/v7/test/ntuple_basics.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,109 @@ TEST(REntry, Basics)
EXPECT_NE(&pt, e->GetPtr<void>("pt").get());
}

TEST(REntry, Subfields)
{
FileRaii fileGuard("test_rentry_subfields.root");
{
auto model = RNTupleModel::Create();
model->MakeField<float>("a", 3.14f);
model->MakeField<CustomStruct>("struct", CustomStruct{1.f, {2.f, 3.f}, {{4.f}, {5.f, 6.f, 7.f}}, "foo"});
model->MakeField<std::vector<CustomStruct>>(
"structVec", std::vector{CustomStruct{.1f, {.2f, .3f}, {{.4f}, {.5f, .6f, .7f}}, "bar"},
CustomStruct{-1.f, {-2.f, -3.f}, {{-4.f}, {-5.f, -6.f, -7.f}}, "baz"}});
model->MakeField<std::pair<CustomStruct, int>>(
"structPair", std::pair{CustomStruct{.1f, {.2f, .3f}, {{.4f}, {.5f, .6f, .7f}}, "bar"}, 42});

auto ntuple = RNTupleWriter::Recreate(std::move(model), "ntuple", fileGuard.GetPath());
ntuple->Fill();
}

auto model = RNTupleModel::Create();
auto fldA = RFieldBase::Create("a", "float").Unwrap();
auto fldStruct = RFieldBase::Create("struct", "CustomStruct").Unwrap();
auto fldStructVec = RFieldBase::Create("structVec", "std::vector<CustomStruct>").Unwrap();
auto fldStructPair = RFieldBase::Create("structPair", "std::pair<CustomStruct, int>").Unwrap();

model->AddField(std::move(fldA));
model->AddField(std::move(fldStruct));
model->AddField(std::move(fldStructVec));
model->AddField(std::move(fldStructPair));

model->RegisterSubfield("struct.a");
try {
model->RegisterSubfield("struct.doesnotexist");
FAIL() << "attempting to register a nonexistent subfield should throw";
} catch (const RException &err) {
EXPECT_THAT(err.what(), testing::HasSubstr("could not find subfield \"struct.doesnotexist\" in model"));
}

try {
model->RegisterSubfield("structVec._0.s");
FAIL() << "attempting to register a subfield in a collection should throw";
} catch (const RException &err) {
EXPECT_THAT(err.what(), testing::HasSubstr("registering a subfield in a collection is not supported"));
}

model->RegisterSubfield("structPair._0.s");

model->Freeze();
try {
model->RegisterSubfield("struct.v1");
FAIL() << "attempting to register a subfield in a frozen model should throw";
} catch (const RException &err) {
EXPECT_THAT(err.what(), testing::HasSubstr("invalid attempt to modify frozen model"));
}

const auto &defaultEntry = model->GetDefaultEntry();
const auto entry = model->CreateEntry();
const auto bareEntry = model->CreateBareEntry();
auto ntuple = RNTupleReader::Open(std::move(model), "ntuple", fileGuard.GetPath());

// default entry
ntuple->LoadEntry(0);
EXPECT_FLOAT_EQ(1.f, *defaultEntry.GetPtr<float>("struct.a"));
EXPECT_EQ("bar", *defaultEntry.GetPtr<std::string>("structPair._0.s"));
auto defaultTokenStructA = defaultEntry.GetToken("struct.a");
EXPECT_FLOAT_EQ(1.f, *defaultEntry.GetPtr<float>(defaultTokenStructA));

// explicitly created entry
ntuple->LoadEntry(0, *entry);
EXPECT_FLOAT_EQ(1.f, *entry->GetPtr<float>("struct.a"));
EXPECT_EQ("bar", *entry->GetPtr<std::string>("structPair._0.s"));
auto tokenStructA = entry->GetToken("struct.a");
EXPECT_FLOAT_EQ(1.f, *entry->GetPtr<float>(tokenStructA));

// bare entry
EXPECT_EQ(nullptr, bareEntry->GetPtr<float>("struct.a"));
EXPECT_EQ(nullptr, bareEntry->GetPtr<std::string>("structPair._0.s"));

auto cs = std::make_shared<CustomStruct>();
bareEntry->BindValue("struct", cs);
bareEntry->BindRawPtr("struct.a", &cs->a);
auto pcs = std::make_shared<std::pair<CustomStruct, int>>();
bareEntry->BindValue("structPair", pcs);
bareEntry->BindRawPtr("structPair._0.s", &pcs->first.s);
bareEntry->BindValue("a", std::make_shared<float>());
bareEntry->BindValue("structVec", std::make_shared<std::vector<CustomStruct>>());

ntuple->LoadEntry(0, *bareEntry);
EXPECT_FLOAT_EQ(1.f, *bareEntry->GetPtr<float>("struct.a"));
EXPECT_FLOAT_EQ(1.f, cs->a);
EXPECT_EQ("bar", *bareEntry->GetPtr<std::string>("structPair._0.s"));
EXPECT_EQ("bar", pcs->first.s);
auto bareTokenStructA = bareEntry->GetToken("struct.a");
EXPECT_FLOAT_EQ(1.f, *bareEntry->GetPtr<float>(bareTokenStructA));

// sanity check
try {
*defaultEntry.GetPtr<std::vector<float>>("struct.v1");
*entry->GetPtr<std::vector<float>>("struct.v1");
FAIL() << "subfields not explicitly registered shouldn't be present in the entry";
} catch (const RException &err) {
EXPECT_THAT(err.what(), testing::HasSubstr("invalid field name: struct.v1"));
}
}

TEST(RFieldBase, CreateObject)
{
auto ptrInt = RField<int>("name").CreateObject<int>();
Expand Down
11 changes: 11 additions & 0 deletions tree/ntuple/v7/test/ntuple_model.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,14 @@ TEST(RNTupleModel, Clone)
EXPECT_TRUE(clone->GetConstField("struct").GetTraits() & RFieldBase::kTraitTypeChecksum);
EXPECT_TRUE(clone->GetConstField("obj").GetTraits() & RFieldBase::kTraitTypeChecksum);
}

TEST(RNTupleModel, CloneRegisteredSubfield)
{
auto model = RNTupleModel::Create();
model->MakeField<CustomStruct>("struct");
model->RegisterSubfield("struct.a");
model->Freeze();

auto clone = model->Clone();
EXPECT_TRUE(clone->GetDefaultEntry().GetPtr<float>("struct.a"));
}

0 comments on commit 63b8243

Please sign in to comment.