From 290a1d45114f37a5eb05cff1f2e7f9c2eeaac505 Mon Sep 17 00:00:00 2001 From: Jonas Hahnfeld Date: Tue, 28 Jul 2026 15:13:25 +0200 Subject: [PATCH 1/3] [df] Implement ExecLoopTrait It abstracts the loop when Exec'uting with data containers, in the following commit for RHist*FillHelper and in the future for the other FillHelpers. --- tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx | 39 +++++ tree/dataframe/test/dataframe_helpers.cxx | 145 +++++++++++++++++- 2 files changed, 183 insertions(+), 1 deletion(-) diff --git a/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx b/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx index 45df52d56ed20..23e7cf8bc36af 100644 --- a/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx +++ b/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx @@ -41,6 +41,7 @@ #include #include +#include #include #include #include @@ -325,6 +326,44 @@ std::size_t GetSize(const T &val) } } +// trait class to implement looping over data containers +template +class R__CLING_PTRCHECK(off) ExecLoopTrait { +private: + template + void ExecLoop(unsigned int slot, std::size_t elements, Iterators... its) + { + for (std::size_t i = 0; i < elements; i++) { + Exec(slot, *its...); + (std::advance(its, 1), ...); + } + } + +public: + template + void Exec(unsigned int slot, const ColumnTypes &...columnValues) + { + if constexpr (std::disjunction_v...>) { + constexpr std::array isContainer{IsDataContainer::value...}; + constexpr std::size_t firstContainerIdx = FindIdxTrue(isContainer); + std::array sizes = {{GetSize(columnValues)...}}; + std::size_t elements = 0; + for (std::size_t i = 0; i < isContainer.size(); i++) { + if (isContainer[i]) { + if (i == firstContainerIdx) { + elements = sizes[i]; + } else if (elements != sizes[i]) { + throw std::runtime_error("Cannot fill values in containers of different sizes."); + } + } + } + ExecLoop(slot, elements, MakeBegin(columnValues)...); + } else { + static_cast(this)->ExecSingle(slot, columnValues...); + } + } +}; + // Helpers for dealing with histograms and similar: template ().Reset())> void ResetIfPossible(H *h) diff --git a/tree/dataframe/test/dataframe_helpers.cxx b/tree/dataframe/test/dataframe_helpers.cxx index 97696841a27fc..89d7ec3ac61d4 100755 --- a/tree/dataframe/test/dataframe_helpers.cxx +++ b/tree/dataframe/test/dataframe_helpers.cxx @@ -10,8 +10,10 @@ #include #include #include -#include +#include #include +#include +#include #include "gtest/gtest.h" @@ -813,6 +815,147 @@ TEST(RDFHelpers, Cleanup_After_Exception) << "The Finalize method should have changed the value of testVal during the post-exception cleanup." << std::endl; } +struct SimpleExecLoopAction : public ROOT::Internal::RDF::ExecLoopTrait { + std::vector fValues; + + template + void ExecSingle(unsigned int, const ColumnTypes &...columnValues) + { + fValues.emplace_back(columnValues...); + } +}; + +TEST(RDFHelpers, ExecLoopTrait) +{ + SimpleExecLoopAction execLoop; + + execLoop.Exec(/*slot=*/0, 42.0); + ASSERT_EQ(execLoop.fValues.size(), 1); + EXPECT_EQ(execLoop.fValues.front(), 42.0); + execLoop.fValues.clear(); + + // One-dimensional containers are processed in order. + std::vector v = {43.0}; + execLoop.Exec(/*slot=*/0, v); + EXPECT_EQ(execLoop.fValues.size(), 1); + EXPECT_EQ(execLoop.fValues, v); + execLoop.fValues.clear(); + + v = {44.0, 45.0}; + execLoop.Exec(/*slot=*/0, v); + EXPECT_EQ(execLoop.fValues.size(), 2); + EXPECT_EQ(execLoop.fValues, v); + execLoop.fValues.clear(); + + v = {}; + execLoop.Exec(/*slot=*/0, v); + EXPECT_TRUE(execLoop.fValues.empty()); + execLoop.fValues.clear(); + + // Multi-dimensional containers are traversed recursively. + std::vector> v2 = {{1.0}, {}, {2.0, 3.0}, {4.0}}; + execLoop.Exec(/*slot=*/0, v2); + EXPECT_EQ(execLoop.fValues.size(), 4); + v = {1.0, 2.0, 3.0, 4.0}; + EXPECT_EQ(execLoop.fValues, v); + execLoop.fValues.clear(); + + std::vector>> v3 = {{{1.0}, {2.0}}, {}, {{3.0, 4.0}}, {{5.0}}}; + execLoop.Exec(/*slot=*/0, v3); + EXPECT_EQ(execLoop.fValues.size(), 5); + v = {1.0, 2.0, 3.0, 4.0, 5.0}; + EXPECT_EQ(execLoop.fValues, v); + execLoop.fValues.clear(); +} + +struct ExecLoopActionTwo : public ROOT::Internal::RDF::ExecLoopTrait { + std::vector> fValues; + + template + void ExecSingle(unsigned int, const ColumnTypes &...columnValues) + { + fValues.emplace_back(columnValues...); + } +}; + +TEST(RDFHelpers, ExecLoopTraitTwo) +{ + ExecLoopActionTwo execLoop; + + execLoop.Exec(/*slot=*/0, 42.0, 43.0); + ASSERT_EQ(execLoop.fValues.size(), 1); + EXPECT_EQ(execLoop.fValues.front(), std::make_pair(42.0, 43.0)); + execLoop.fValues.clear(); + + // Two containers are traversed in sync. + std::vector v1 = {1.0, 2.0}; + std::vector v2 = {3.0, 4.0}; + execLoop.Exec(/*slot=*/0, v1, v2); + ASSERT_EQ(execLoop.fValues.size(), 2); + EXPECT_EQ(execLoop.fValues.at(0), std::make_pair(1.0, 3.0)); + EXPECT_EQ(execLoop.fValues.at(1), std::make_pair(2.0, 4.0)); + execLoop.fValues.clear(); + + // A scalar is broadcasted to match the containers. + execLoop.Exec(/*slot=*/0, v1, 42.0); + ASSERT_EQ(execLoop.fValues.size(), 2); + EXPECT_EQ(execLoop.fValues.at(0), std::make_pair(1.0, 42.0)); + EXPECT_EQ(execLoop.fValues.at(1), std::make_pair(2.0, 42.0)); + execLoop.fValues.clear(); + + execLoop.Exec(/*slot=*/0, 42.0, v2); + ASSERT_EQ(execLoop.fValues.size(), 2); + EXPECT_EQ(execLoop.fValues.at(0), std::make_pair(42.0, 3.0)); + EXPECT_EQ(execLoop.fValues.at(1), std::make_pair(42.0, 4.0)); + execLoop.fValues.clear(); + + // Two nested containers are traversed in sync. + std::vector> v3 = {{1.0, 2.0}, {3.0}}; + std::vector> v4 = {{4.0, 5.0}, {6.0}}; + execLoop.Exec(/*slot=*/0, v3, v4); + ASSERT_EQ(execLoop.fValues.size(), 3); + EXPECT_EQ(execLoop.fValues.at(0), std::make_pair(1.0, 4.0)); + EXPECT_EQ(execLoop.fValues.at(1), std::make_pair(2.0, 5.0)); + EXPECT_EQ(execLoop.fValues.at(2), std::make_pair(3.0, 6.0)); + execLoop.fValues.clear(); + + // Broadcasting works recursively in multiple dimensions. + execLoop.Exec(/*slot=*/0, v3, 42.0); + ASSERT_EQ(execLoop.fValues.size(), 3); + EXPECT_EQ(execLoop.fValues.at(0), std::make_pair(1.0, 42.0)); + EXPECT_EQ(execLoop.fValues.at(1), std::make_pair(2.0, 42.0)); + EXPECT_EQ(execLoop.fValues.at(2), std::make_pair(3.0, 42.0)); + execLoop.fValues.clear(); + + execLoop.Exec(/*slot=*/0, v1, v3); + ASSERT_EQ(execLoop.fValues.size(), 3); + EXPECT_EQ(execLoop.fValues.at(0), std::make_pair(1.0, 1.0)); + EXPECT_EQ(execLoop.fValues.at(1), std::make_pair(1.0, 2.0)); + EXPECT_EQ(execLoop.fValues.at(2), std::make_pair(2.0, 3.0)); + execLoop.fValues.clear(); +} + +TEST(RDFHelpers, ExecLoopTraitInvalid) +{ + ExecLoopActionTwo execLoop; + + // The number of elements has to match for all columns. + std::vector v0; + std::vector v1 = {1.0}; + std::vector v2 = {2.0, 3.0}; + EXPECT_THROW(execLoop.Exec(/*slot=*/0, v0, v1), std::runtime_error); + EXPECT_THROW(execLoop.Exec(/*slot=*/0, v0, v2), std::runtime_error); + EXPECT_THROW(execLoop.Exec(/*slot=*/0, v2, v1), std::runtime_error); + + // For multiple dimensions, the number of elements has to match recursively. + std::vector> v4 = {{1.0}, {2.0}}; + std::vector> v5 = {{3.0, 4.0}}; + std::vector> v6 = {{3.0, 4.0}, {}}; + EXPECT_THROW(execLoop.Exec(/*slot=*/0, v4, v5), std::runtime_error); + EXPECT_THROW(execLoop.Exec(/*slot=*/0, v4, v6), std::runtime_error); + EXPECT_THROW(execLoop.Exec(/*slot=*/0, v6, v5), std::runtime_error); +} + TEST(RDFHelpers, ProgressBarRestorePrecision) { // Regression test for https://github.com/root-project/root/issues/21165 From 1f8d0abd55d5d5804601226ae9fdeeee5de73fbb Mon Sep 17 00:00:00 2001 From: Jonas Hahnfeld Date: Thu, 23 Jul 2026 10:26:14 +0200 Subject: [PATCH 2/3] [df] Handle containers in RHist* filling As for the existing Histo actions, the sizes of all containers must match while scalars are broadcasted. As an extension, this works recursively, so nested vectors are handled one level at a time. --- tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx | 11 +- tree/dataframe/inc/ROOT/RDF/RInterface.hxx | 38 +++ tree/dataframe/test/dataframe_hist.cxx | 260 ++++++++++++++++++ 3 files changed, 304 insertions(+), 5 deletions(-) diff --git a/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx b/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx index 23e7cf8bc36af..941576427c63e 100644 --- a/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx +++ b/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx @@ -524,8 +524,8 @@ public: #ifdef R__HAS_ROOT7 template -class R__CLING_PTRCHECK(off) RHistFillHelper - : public ROOT::Detail::RDF::RActionImpl> { +class R__CLING_PTRCHECK(off) RHistFillHelper : public RActionImpl>, + public ExecLoopTrait> { public: using Result_t = ROOT::Experimental::RHist; @@ -563,7 +563,7 @@ public: } template - void Exec(unsigned int slot, const ColumnTypes &...columnValues) + void ExecSingle(unsigned int slot, const ColumnTypes &...columnValues) { if constexpr (WithWeight) { auto t = std::forward_as_tuple(columnValues...); @@ -592,7 +592,8 @@ public: template class R__CLING_PTRCHECK(off) RHistEngineFillHelper - : public ROOT::Detail::RDF::RActionImpl> { + : public RActionImpl>, + public ExecLoopTrait> { public: using Result_t = ROOT::Experimental::RHistEngine; @@ -622,7 +623,7 @@ public: } template - void Exec(unsigned int, const ColumnTypes &...columnValues) + void ExecSingle(unsigned int, const ColumnTypes &...columnValues) { if constexpr (WithWeight) { auto t = std::forward_as_tuple(columnValues...); diff --git a/tree/dataframe/inc/ROOT/RDF/RInterface.hxx b/tree/dataframe/inc/ROOT/RDF/RInterface.hxx index 44bae42348e63..92348b0cac694 100644 --- a/tree/dataframe/inc/ROOT/RDF/RInterface.hxx +++ b/tree/dataframe/inc/ROOT/RDF/RInterface.hxx @@ -2019,6 +2019,9 @@ public: /// \param[in] vName The name of the column that will fill the histogram. /// \return the histogram wrapped in a RResultPtr. /// + /// The column can be of a container type (e.g. `std::vector` or `RVec`), in which case the histogram is filled with + /// each one of the elements of the container, and the container can also be nested. + /// /// This action is *lazy*: upon invocation of this method the calculation is /// booked but not executed. Also see RResultPtr. /// @@ -2044,6 +2047,11 @@ public: /// \param[in] columnList A list containing the names of the columns that will be passed when calling `Fill` /// \return the histogram wrapped in a RResultPtr. /// + /// Columns can be of a container type (e.g. `std::vector` or `RVec`), in which case the histogram is filled with + /// each one of the elements of the container. In case multiple columns of container type are provided, they must + /// have the same length for each event (but possibly different lengths between events). Containers can be nested, + /// in which case their sizes must match recursively. Scalars are broadcasted to match container columns. + /// /// This action is *lazy*: upon invocation of this method the calculation is /// booked but not executed. Also see RResultPtr. /// @@ -2073,6 +2081,11 @@ public: /// \param[in] columnList A list containing the names of the columns that will be passed when calling `Fill` /// \return the histogram wrapped in a RResultPtr. /// + /// Columns can be of a container type (e.g. `std::vector` or `RVec`), in which case the histogram is filled with + /// each one of the elements of the container. In case multiple columns of container type are provided, they must + /// have the same length for each event (but possibly different lengths between events). Containers can be nested, + /// in which case their sizes must match recursively. Scalars are broadcasted to match container columns. + /// /// This action is *lazy*: upon invocation of this method the calculation is /// booked but not executed. Also see RResultPtr. /// @@ -2109,6 +2122,11 @@ public: /// \param[in] wName The name of the column that will provide the weights. /// \return the histogram wrapped in a RResultPtr. /// + /// Columns can be of a container type (e.g. `std::vector` or `RVec`), in which case the histogram is filled with + /// each one of the elements of the container. In case multiple columns of container type are provided, they must + /// have the same length for each event (but possibly different lengths between events). Containers can be nested, + /// in which case their sizes must match recursively. Scalars are broadcasted to match container columns. + /// /// This action is *lazy*: upon invocation of this method the calculation is /// booked but not executed. Also see RResultPtr. /// @@ -2136,6 +2154,11 @@ public: /// \param[in] wName The name of the column that will provide the weights. /// \return the histogram wrapped in a RResultPtr. /// + /// Columns can be of a container type (e.g. `std::vector` or `RVec`), in which case the histogram is filled with + /// each one of the elements of the container. In case multiple columns of container type are provided, they must + /// have the same length for each event (but possibly different lengths between events). Containers can be nested, + /// in which case their sizes must match recursively. Scalars are broadcasted to match container columns. + /// /// This action is *lazy*: upon invocation of this method the calculation is /// booked but not executed. Also see RResultPtr. /// @@ -2172,6 +2195,11 @@ public: /// \param[in] wName The name of the column that will provide the weights. /// \return the histogram wrapped in a RResultPtr. /// + /// Columns can be of a container type (e.g. `std::vector` or `RVec`), in which case the histogram is filled with + /// each one of the elements of the container. In case multiple columns of container type are provided, they must + /// have the same length for each event (but possibly different lengths between events). Containers can be nested, + /// in which case their sizes must match recursively. Scalars are broadcasted to match container columns. + /// /// This action is *lazy*: upon invocation of this method the calculation is /// booked but not executed. Also see RResultPtr. /// @@ -2215,6 +2243,11 @@ public: /// \param[in] columnList A list containing the names of the columns that will be passed when calling `Fill` /// \return the histogram wrapped in a RResultPtr. /// + /// Columns can be of a container type (e.g. `std::vector` or `RVec`), in which case the histogram is filled with + /// each one of the elements of the container. In case multiple columns of container type are provided, they must + /// have the same length for each event (but possibly different lengths between events). Containers can be nested, + /// in which case their sizes must match recursively. Scalars are broadcasted to match container columns. + /// /// This action is *lazy*: upon invocation of this method the calculation is /// booked but not executed. Also see RResultPtr. /// @@ -2249,6 +2282,11 @@ public: /// \param[in] wName The name of the column that will provide the weights. /// \return the histogram wrapped in a RResultPtr. /// + /// Columns can be of a container type (e.g. `std::vector` or `RVec`), in which case the histogram is filled with + /// each one of the elements of the container. In case multiple columns of container type are provided, they must + /// have the same length for each event (but possibly different lengths between events). Containers can be nested, + /// in which case their sizes must match recursively. Scalars are broadcasted to match container columns. + /// /// This action is *lazy*: upon invocation of this method the calculation is /// booked but not executed. Also see RResultPtr. /// diff --git a/tree/dataframe/test/dataframe_hist.cxx b/tree/dataframe/test/dataframe_hist.cxx index 4b5f2f7c80c65..646b0e561cb67 100644 --- a/tree/dataframe/test/dataframe_hist.cxx +++ b/tree/dataframe/test/dataframe_hist.cxx @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -455,6 +456,265 @@ TEST_P(RDFHist, WeightInvalidNumberOfArgumentsJit) EXPECT_THROW(dfXW.Hist(engine, {"x", "x"}, "w"), std::invalid_argument); } +TEST_P(RDFHist, Container) +{ + RDataFrame df(10); + auto dfX = df.Define("x", [](ULong64_t e) { return ROOT::RVecD{e + 5.5, e + 15.5}; }, {"rdfentry_"}); + + const RRegularAxis axis(20, {5.0, 25.0}); + auto hist = dfX.Hist({axis}, {"x"}); + EXPECT_EQ(hist->GetNEntries(), 20); + for (auto index : axis.GetNormalRange()) { + EXPECT_EQ(hist->GetBinContent(index), 1.0); + } +} + +TEST_P(RDFHist, ContainerNested) +{ + RDataFrame df(10); + auto dfX = df.Define("x", [](ULong64_t e) { return ROOT::RVec{{e + 5.5}, {e + 15.5}}; }, {"rdfentry_"}); + + const RRegularAxis axis(20, {5.0, 25.0}); + auto hist = dfX.Hist>({axis}, {"x"}); + EXPECT_EQ(hist->GetNEntries(), 20); + for (auto index : axis.GetNormalRange()) { + EXPECT_EQ(hist->GetBinContent(index), 1.0); + } +} + +TEST_P(RDFHist, ContainerJit) +{ + RDataFrame df(10); + auto dfX = df.Define("x", "ROOT::RVecD{rdfentry_ + 5.5}"); + + const RRegularAxis axis(10, {5.0, 15.0}); + auto hist = dfX.Hist({axis}, {"x"}); + EXPECT_EQ(hist->GetNEntries(), 10); + for (auto index : axis.GetNormalRange()) { + EXPECT_EQ(hist->GetBinContent(index), 1.0); + } +} + +TEST_P(RDFHist, ContainerMultiDim) +{ + RDataFrame df(10); + auto dfXY = df.Define("x", [](ULong64_t e) { return ROOT::RVecD{e + 5.5}; }, {"rdfentry_"}) + .Define("y", [](ULong64_t e) { return ROOT::RVecD{2 * e + 0.5}; }, {"rdfentry_"}); + + const RRegularAxis regularAxis(10, {5.0, 15.0}); + static constexpr std::size_t BinsY = 20; + std::vector bins; + for (std::size_t i = 0; i < BinsY; i++) { + bins.push_back(i); + } + bins.push_back(BinsY); + const RVariableBinAxis variableBinAxis(bins); + + auto hist = + dfXY.Hist({regularAxis, variableBinAxis}, {"x", "y"}); + EXPECT_EQ(hist->GetNEntries(), 10); + for (auto x : regularAxis.GetNormalRange()) { + for (auto y : variableBinAxis.GetNormalRange()) { + if (2 * x.GetIndex() == y.GetIndex()) { + EXPECT_EQ(hist->GetBinContent(x, y), 1.0); + } else { + EXPECT_EQ(hist->GetBinContent(x, y), 0.0); + } + } + } +} + +TEST_P(RDFHist, ContainerMultiDimBroadcast) +{ + RDataFrame df(10); + auto dfXY = df.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}) + .Define("y", [](ULong64_t e) { return ROOT::RVecD{2 * e + 0.5}; }, {"rdfentry_"}); + + const RRegularAxis regularAxis(10, {5.0, 15.0}); + static constexpr std::size_t BinsY = 20; + std::vector bins; + for (std::size_t i = 0; i < BinsY; i++) { + bins.push_back(i); + } + bins.push_back(BinsY); + const RVariableBinAxis variableBinAxis(bins); + + auto hist = dfXY.Hist({regularAxis, variableBinAxis}, {"x", "y"}); + EXPECT_EQ(hist->GetNEntries(), 10); + for (auto x : regularAxis.GetNormalRange()) { + for (auto y : variableBinAxis.GetNormalRange()) { + if (2 * x.GetIndex() == y.GetIndex()) { + EXPECT_EQ(hist->GetBinContent(x, y), 1.0); + } else { + EXPECT_EQ(hist->GetBinContent(x, y), 0.0); + } + } + } +} + +TEST_P(RDFHist, ContainerMultiDimNestedBroadcast) +{ + RDataFrame df(10); + auto dfXY = df.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}) + .Define("xV", [](ULong64_t e) { return ROOT::RVecD{e + 5.5}; }, {"rdfentry_"}) + .Define("y", [](ULong64_t e) { return ROOT::RVec{{2 * e + 0.5}}; }, {"rdfentry_"}); + + const RRegularAxis regularAxis(10, {5.0, 15.0}); + static constexpr std::size_t BinsY = 20; + std::vector bins; + for (std::size_t i = 0; i < BinsY; i++) { + bins.push_back(i); + } + bins.push_back(BinsY); + const RVariableBinAxis variableBinAxis(bins); + + auto hist = + dfXY.Hist>({regularAxis, variableBinAxis}, {"x", "y"}); + EXPECT_EQ(hist->GetNEntries(), 10); + for (auto x : regularAxis.GetNormalRange()) { + for (auto y : variableBinAxis.GetNormalRange()) { + if (2 * x.GetIndex() == y.GetIndex()) { + EXPECT_EQ(hist->GetBinContent(x, y), 1.0); + } else { + EXPECT_EQ(hist->GetBinContent(x, y), 0.0); + } + } + } + + hist = dfXY.Hist>({regularAxis, variableBinAxis}, + {"xV", "y"}); + EXPECT_EQ(hist->GetNEntries(), 10); +} + +TEST_P(RDFHist, ContainerInvalidElements) +{ + RDataFrame df(10); + auto dfXY = df.Define("x0", [] { return ROOT::RVecD{}; }) + .Define("x2", [](ULong64_t e) { return ROOT::RVecD{e + 5.5, e + 15.5}; }, {"rdfentry_"}) + .Define("y1", [](ULong64_t e) { return ROOT::RVecD{2 * e + 0.5}; }, {"rdfentry_"}); + + const RRegularAxis regularAxis(20, {5.0, 25.0}); + static constexpr std::size_t BinsY = 20; + std::vector bins; + for (std::size_t i = 0; i < BinsY; i++) { + bins.push_back(i); + } + bins.push_back(BinsY); + const RVariableBinAxis variableBinAxis(bins); + + try { + // Cannot use EXPECT_THROW because of template arguments... + *dfXY.Hist({regularAxis, variableBinAxis}, {"x0", "y1"}); + FAIL() << "expected std::runtime_error"; + } catch (const std::runtime_error &) { + // expected + } + + try { + // Cannot use EXPECT_THROW because of template arguments... + *dfXY.Hist({regularAxis, variableBinAxis}, {"x2", "y1"}); + FAIL() << "expected std::runtime_error"; + } catch (const std::runtime_error &e) { + // expected + } +} + +TEST_P(RDFHist, EngineContainer) +{ + RDataFrame df(10); + auto dfX = df.Define("x", [](ULong64_t e) { return ROOT::RVecD{e + 5.5, e + 15.5}; }, {"rdfentry_"}); + + const RRegularAxis axis(20, {5.0, 25.0}); + auto hist = std::make_shared>(axis); + auto resPtr = dfX.Hist(hist, {"x"}); + EXPECT_EQ(hist, resPtr.GetSharedPtr()); + for (auto index : axis.GetNormalRange()) { + EXPECT_EQ(hist->GetBinContent(index), 1.0); + } +} + +TEST_P(RDFHist, WeightContainer) +{ + RDataFrame df(10); + auto dfXW = + df.Define("xV", [](ULong64_t e) { return ROOT::RVecD{e + 5.5, e + 15.5}; }, {"rdfentry_"}) + .Define("wV", [](ULong64_t e) { return ROOT::RVecD{0.1 + e * 0.03, 0.2 + e * e * 0.06}; }, {"rdfentry_"}); + + const RRegularAxis axis(20, {5.0, 25.0}); + auto hist = dfXW.Hist({axis}, {"xV"}, "wV"); + EXPECT_EQ(hist->GetNEntries(), 20); + for (auto index : axis.GetNormalRange()) { + auto &bin = hist->GetBinContent(index); + auto i = index.GetIndex(); + double weight = i >= 10 ? (0.2 + (i - 10) * (i - 10) * 0.06) : (0.1 + i * 0.03); + EXPECT_FLOAT_EQ(bin.fSum, weight); + EXPECT_FLOAT_EQ(bin.fSum2, weight * weight); + } +} + +TEST_P(RDFHist, WeightContainerJit) +{ + RDataFrame df(10); + auto dfXW = df.Define("xV", "ROOT::RVecD{rdfentry_ + 5.5, rdfentry_ + 15.5}") + .Define("wV", "ROOT::RVecD{0.1 + rdfentry_ * 0.03, 0.2 + rdfentry_ * rdfentry_ * 0.06}"); + + const RRegularAxis axis(20, {5.0, 25.0}); + auto hist = dfXW.Hist({axis}, {"xV"}, "wV"); + EXPECT_EQ(hist->GetNEntries(), 20); + for (auto index : axis.GetNormalRange()) { + auto &bin = hist->GetBinContent(index); + auto i = index.GetIndex(); + double weight = i >= 10 ? (0.2 + (i - 10) * (i - 10) * 0.06) : (0.1 + i * 0.03); + EXPECT_FLOAT_EQ(bin.fSum, weight); + EXPECT_FLOAT_EQ(bin.fSum2, weight * weight); + } +} + +TEST_P(RDFHist, WeightContainerBroadcast) +{ + RDataFrame df(10); + auto dfXW = + df.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}) + .Define("xV", [](ULong64_t e) { return ROOT::RVecD{e + 5.5, e + 15.5}; }, {"rdfentry_"}) + .Define("w", [](ULong64_t e) { return 0.1 + e * 0.03; }, {"rdfentry_"}) + .Define("wV", [](ULong64_t e) { return ROOT::RVecD{0.1 + e * 0.03, 0.2 + e * e * 0.06}; }, {"rdfentry_"}); + + const RRegularAxis axis(20, {5.0, 25.0}); + auto hist = dfXW.Hist({axis}, {"xV"}, "w"); + EXPECT_EQ(hist->GetNEntries(), 20); + for (auto index : axis.GetNormalRange()) { + auto &bin = hist->GetBinContent(index); + auto i = index.GetIndex(); + double weight = 0.1 + (i >= 10 ? i - 10 : i) * 0.03; + EXPECT_FLOAT_EQ(bin.fSum, weight); + EXPECT_FLOAT_EQ(bin.fSum2, weight * weight); + } + + hist = dfXW.Hist({axis}, {"x"}, "wV"); + EXPECT_EQ(hist->GetNEntries(), 20); + EXPECT_EQ(hist->GetBinContent(2).fSum, 0.1 + 2 * 0.03 + 0.2 + 2 * 2 * 0.06); +} + +TEST_P(RDFHist, EngineWeightContainer) +{ + RDataFrame df(10); + auto dfXW = + df.Define("xV", [](ULong64_t e) { return ROOT::RVecD{e + 5.5, e + 15.5}; }, {"rdfentry_"}) + .Define("wV", [](ULong64_t e) { return ROOT::RVecD{0.1 + e * 0.03, 0.2 + e * e * 0.06}; }, {"rdfentry_"}); + + const RRegularAxis axis(20, {5.0, 25.0}); + auto hist = std::make_shared>(axis); + auto resPtr = dfXW.Hist(hist, {"xV"}, "wV"); + EXPECT_EQ(hist, resPtr.GetSharedPtr()); + for (auto index : axis.GetNormalRange()) { + auto &bin = hist->GetBinContent(index); + auto i = index.GetIndex(); + double weight = i >= 10 ? (0.2 + (i - 10) * (i - 10) * 0.06) : (0.1 + i * 0.03); + EXPECT_FLOAT_EQ(bin.fSum, weight); + EXPECT_FLOAT_EQ(bin.fSum2, weight * weight); + } +} + TEST_P(RDFHist, Variations) { RDataFrame df(10); From cfe71024a8d96c686e06df87792cbdc12c14125e Mon Sep 17 00:00:00 2001 From: Jonas Hahnfeld Date: Tue, 28 Jul 2026 16:09:43 +0200 Subject: [PATCH 3/3] [df] Use axis object in tests if available This reduces repeated arguments that must be consistent. --- tree/dataframe/test/dataframe_hist.cxx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tree/dataframe/test/dataframe_hist.cxx b/tree/dataframe/test/dataframe_hist.cxx index 646b0e561cb67..94edf450c3c84 100644 --- a/tree/dataframe/test/dataframe_hist.cxx +++ b/tree/dataframe/test/dataframe_hist.cxx @@ -270,7 +270,7 @@ TEST_P(RDFHist, InvalidNumberOfArguments) // expected } - auto hist = std::make_shared>(10, std::make_pair(5.0, 15.0)); + auto hist = std::make_shared>(axis); try { // Cannot use EXPECT_THROW because of template arguments... dfX.Hist(hist, {"x", "x"}); @@ -279,7 +279,7 @@ TEST_P(RDFHist, InvalidNumberOfArguments) // expected } - auto engine = std::make_shared>(10, std::make_pair(5.0, 15.0)); + auto engine = std::make_shared>(axis); try { // Cannot use EXPECT_THROW because of template arguments... dfX.Hist(engine, {"x", "x"}); @@ -297,10 +297,10 @@ TEST_P(RDFHist, InvalidNumberOfArgumentsJit) const RRegularAxis axis(10, {5.0, 15.0}); EXPECT_THROW(dfX.Hist({axis}, {"x", "x"}), std::invalid_argument); - auto hist = std::make_shared>(10, std::make_pair(5.0, 15.0)); + auto hist = std::make_shared>(axis); EXPECT_THROW(dfX.Hist(hist, {"x", "x"}), std::invalid_argument); - auto engine = std::make_shared>(10, std::make_pair(5.0, 15.0)); + auto engine = std::make_shared>(axis); EXPECT_THROW(dfX.Hist(engine, {"x", "x"}), std::invalid_argument); } @@ -379,7 +379,7 @@ TEST_P(RDFHist, EngineWeight) .Define("w", [](ULong64_t e) { return 0.1 + e * 0.03; }, {"rdfentry_"}); const RRegularAxis axis(10, {5.0, 15.0}); - auto hist = std::make_shared>(10, std::make_pair(5.0, 15.0)); + auto hist = std::make_shared>(axis); auto resPtr = dfXW.Hist(hist, {"x"}, "w"); EXPECT_EQ(hist, resPtr.GetSharedPtr()); for (auto index : axis.GetNormalRange()) { @@ -396,7 +396,7 @@ TEST_P(RDFHist, EngineWeightJit) auto dfXW = df.Define("x", "rdfentry_ + 5.5").Define("w", "0.1 + rdfentry_ * 0.03"); const RRegularAxis axis(10, {5.0, 15.0}); - auto hist = std::make_shared>(10, std::make_pair(5.0, 15.0)); + auto hist = std::make_shared>(axis); auto resPtr = dfXW.Hist(hist, {"x"}, "w"); EXPECT_EQ(hist, resPtr.GetSharedPtr()); for (auto index : axis.GetNormalRange()) { @@ -422,7 +422,7 @@ TEST_P(RDFHist, WeightInvalidNumberOfArguments) // expected } - auto hist = std::make_shared>(10, std::make_pair(5.0, 15.0)); + auto hist = std::make_shared>(axis); try { // Cannot use EXPECT_THROW because of template arguments... dfXW.Hist(hist, {"x", "x"}, "w"); @@ -431,7 +431,7 @@ TEST_P(RDFHist, WeightInvalidNumberOfArguments) // expected } - auto engine = std::make_shared>(10, std::make_pair(5.0, 15.0)); + auto engine = std::make_shared>(axis); try { // Cannot use EXPECT_THROW because of template arguments... dfXW.Hist(engine, {"x", "x"}, "w"); @@ -449,10 +449,10 @@ TEST_P(RDFHist, WeightInvalidNumberOfArgumentsJit) const RRegularAxis axis(10, {5.0, 15.0}); EXPECT_THROW(dfXW.Hist({axis}, {"x", "x"}, "w"), std::invalid_argument); - auto hist = std::make_shared>(10, std::make_pair(5.0, 15.0)); + auto hist = std::make_shared>(axis); EXPECT_THROW(dfXW.Hist(hist, {"x", "x"}, "w"), std::invalid_argument); - auto engine = std::make_shared>(10, std::make_pair(5.0, 15.0)); + auto engine = std::make_shared>(axis); EXPECT_THROW(dfXW.Hist(engine, {"x", "x"}, "w"), std::invalid_argument); }