From c9d81d2aa3080fe8ad5754917ab2c2bd74f67565 Mon Sep 17 00:00:00 2001 From: Jonas Hahnfeld Date: Wed, 21 Oct 2026 17:13:09 +0200 Subject: [PATCH 1/3] [df] Remove GetResultPtr() from ActionHelpers.hxx They are only needed for Book(). --- tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx b/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx index 99ae2942e7e56..f646c4e7541c6 100644 --- a/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx +++ b/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx @@ -550,8 +550,6 @@ public: RHistFillHelper &operator=(RHistFillHelper &&) = default; ~RHistFillHelper() = default; - std::shared_ptr GetResultPtr() const { return fFiller.GetHist(); } - void Initialize() {} void InitTask(TTreeReader *, unsigned int) {} @@ -611,8 +609,6 @@ public: RHistEngineFillHelper &operator=(RHistEngineFillHelper &&) = default; ~RHistEngineFillHelper() = default; - std::shared_ptr GetResultPtr() const { return fHist; } - void Initialize() {} void InitTask(TTreeReader *, unsigned int) {} @@ -1522,8 +1518,6 @@ public: std::string GetActionName() { return "Median"; } - std::shared_ptr GetResultPtr() const { return fResult; } - MedianHelper MakeNew(void *newResult, std::string_view /*variation*/ = "nominal") { auto &result = *static_cast *>(newResult); From bee002985cd20c4c69dae20fc583acbdeb1ddceb Mon Sep 17 00:00:00 2001 From: Jonas Hahnfeld Date: Sat, 24 Oct 2026 13:31:32 +0200 Subject: [PATCH 2/3] [df] Use double in RDFHist.Variations* tests --- tree/dataframe/test/dataframe_hist.cxx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tree/dataframe/test/dataframe_hist.cxx b/tree/dataframe/test/dataframe_hist.cxx index 9de45fb4acff6..642e8380e63cc 100644 --- a/tree/dataframe/test/dataframe_hist.cxx +++ b/tree/dataframe/test/dataframe_hist.cxx @@ -721,11 +721,11 @@ TEST_P(RDFHist, EngineWeightContainer) TEST_P(RDFHist, Variations) { RDataFrame df(10); - auto dfX = df.Define("x", [](ULong64_t e) -> Float_t { return e + 5.5f; }, {"rdfentry_"}) - .Vary("x", [](Float_t x) { return ROOT::RVecF{x - 1.0f, x + 1.0f}; }, {"x"}, 2); + auto dfX = df.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}) + .Vary("x", [](double x) { return ROOT::RVecD{x - 1.0, x + 1.0}; }, {"x"}, 2); const RRegularAxis axis(10, {5.0, 15.0}); - auto hist = dfX.Hist({axis}, {"x"}); + auto hist = dfX.Hist({axis}, {"x"}); auto vars = ROOT::RDF::Experimental::VariationsFor(hist); // Check nominal @@ -756,12 +756,12 @@ TEST_P(RDFHist, Variations) TEST_P(RDFHist, VariationsEngine) { RDataFrame df(10); - auto dfX = df.Define("x", [](ULong64_t e) -> Float_t { return e + 5.5f; }, {"rdfentry_"}) - .Vary("x", [](Float_t x) { return ROOT::RVecF{x - 1.0f, x + 1.0f}; }, {"x"}, 2); + auto dfX = df.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}) + .Vary("x", [](double x) { return ROOT::RVecD{x - 1.0, x + 1.0}; }, {"x"}, 2); const RRegularAxis axis(10, {5.0, 15.0}); auto hist = std::make_shared>(axis); - auto resPtr = dfX.Hist(hist, {"x"}); + auto resPtr = dfX.Hist(hist, {"x"}); auto vars = ROOT::RDF::Experimental::VariationsFor(resPtr); // Trigger the run From 47ad179cbb0277dfb09e7bcf37d7414f1795cbd3 Mon Sep 17 00:00:00 2001 From: Jonas Hahnfeld Date: Thu, 22 Oct 2026 11:08:46 +0200 Subject: [PATCH 3/3] [df] Integrate RProfile filling Similar to RHist filling, but extracting the value argument that is passed as separate argument to the Fill method. --- tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx | 81 +++ .../dataframe/inc/ROOT/RDF/InterfaceUtils.hxx | 26 + tree/dataframe/inc/ROOT/RDF/RInterface.hxx | 229 +++++++ tree/dataframe/inc/ROOT/RDF/Utils.hxx | 2 + tree/dataframe/inc/ROOT/RDFHelpers.hxx | 8 + tree/dataframe/src/RDFUtils.cxx | 12 + tree/dataframe/test/CMakeLists.txt | 1 + tree/dataframe/test/dataframe_profile.cxx | 593 ++++++++++++++++++ 8 files changed, 952 insertions(+) create mode 100644 tree/dataframe/test/dataframe_profile.cxx diff --git a/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx b/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx index f646c4e7541c6..1ea27810e9850 100644 --- a/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx +++ b/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx @@ -39,6 +39,10 @@ #include #include #include +#include +#include +#include +#include #include #endif @@ -643,6 +647,83 @@ public: std::string GetActionName() { return "Hist"; } }; + +template +class R__CLING_PTRCHECK(off) RProfileFillHelper : public RActionImpl>, + public ExecLoopTrait> { +public: + using Result_t = ROOT::Experimental::RProfile; + +private: + std::unique_ptr fFiller; + std::vector> fContexts; + +public: + RProfileFillHelper(std::shared_ptr p, unsigned int nSlots) + : fFiller(new ROOT::Experimental::RProfileConcurrentFiller(p)), fContexts(nSlots) + { + for (unsigned int i = 0; i < nSlots; i++) { + fContexts[i] = fFiller->CreateFillContext(); + } + } + RProfileFillHelper(const RProfileFillHelper &) = delete; + RProfileFillHelper(RProfileFillHelper &&) = default; + RProfileFillHelper &operator=(const RProfileFillHelper &) = delete; + RProfileFillHelper &operator=(RProfileFillHelper &&) = default; + ~RProfileFillHelper() = default; + + void Initialize() {} + void InitTask(TTreeReader *, unsigned int) {} + + template + void ExecWithValueAndWeight(unsigned int slot, const std::tuple &columnValues, + std::index_sequence) + { + // Build a tuple of const references with the actual arguments, stripping the value and weight and avoiding + // copies. + std::tuple> &...> args(std::get(columnValues)...); + const auto &value = std::get(columnValues); + ROOT::Experimental::RWeight weight(std::get(columnValues)); + fContexts[slot]->Fill(args, value, weight); + } + + template + void + ExecWithValue(unsigned int slot, const std::tuple &columnValues, std::index_sequence) + { + // Build a tuple of const references with the actual arguments, stripping the value and avoiding copies. + std::tuple> &...> args(std::get(columnValues)...); + const auto &value = std::get(columnValues); + fContexts[slot]->Fill(args, value); + } + + template + void ExecSingle(unsigned int slot, const ColumnTypes &...columnValues) + { + auto t = std::forward_as_tuple(columnValues...); + if constexpr (WithWeight) { + ExecWithValueAndWeight(slot, t, std::make_index_sequence()); + } else { + ExecWithValue(slot, t, std::make_index_sequence()); + } + } + + void Finalize() + { + for (auto &&context : fContexts) { + context->Flush(); + } + } + + RProfileFillHelper MakeNew(void *newResult, std::string_view /*variation*/ = "nominal") + { + auto &result = *static_cast *>(newResult); + result->Clear(); + return RProfileFillHelper(result, fContexts.size()); + } + + std::string GetActionName() { return "Profile"; } +}; #endif class R__CLING_PTRCHECK(off) FillTGraphHelper : public ROOT::Detail::RDF::RActionImpl { diff --git a/tree/dataframe/inc/ROOT/RDF/InterfaceUtils.hxx b/tree/dataframe/inc/ROOT/RDF/InterfaceUtils.hxx index bc72c1cacccd4..6eaccf5b3377e 100644 --- a/tree/dataframe/inc/ROOT/RDF/InterfaceUtils.hxx +++ b/tree/dataframe/inc/ROOT/RDF/InterfaceUtils.hxx @@ -99,6 +99,8 @@ struct Graph{}; struct GraphAsymmErrors{}; struct Profile1D{}; struct Profile2D{}; +struct Profile{}; +struct ProfileWithWeight{}; struct Min{}; struct Max{}; struct Sum{}; @@ -225,6 +227,30 @@ BuildAction(const ColumnNames_t &columnList, const std::shared_ptr>; return std::make_unique(Helper_t(h), columnList, std::move(prevNode), colRegister); } + +// Action for RProfile using RProfileConcurrentFiller without weights +template +std::unique_ptr +BuildAction(const ColumnNames_t &columnList, const std::shared_ptr &p, + const unsigned int nSlots, std::shared_ptr prevNode, ActionTags::Profile, + const RColumnRegister &colRegister) +{ + using Helper_t = RProfileFillHelper<>; + using Action_t = RAction>; + return std::make_unique(Helper_t(p, nSlots), columnList, std::move(prevNode), colRegister); +} + +// Action for RProfile using RProfileConcurrentFiller with weights +template +std::unique_ptr +BuildAction(const ColumnNames_t &columnList, const std::shared_ptr &p, + const unsigned int nSlots, std::shared_ptr prevNode, ActionTags::ProfileWithWeight, + const RColumnRegister &colRegister) +{ + using Helper_t = RProfileFillHelper; + using Action_t = RAction>; + return std::make_unique(Helper_t(p, nSlots), columnList, std::move(prevNode), colRegister); +} #endif template diff --git a/tree/dataframe/inc/ROOT/RDF/RInterface.hxx b/tree/dataframe/inc/ROOT/RDF/RInterface.hxx index c6345c10aed3e..b53d11a545c89 100644 --- a/tree/dataframe/inc/ROOT/RDF/RInterface.hxx +++ b/tree/dataframe/inc/ROOT/RDF/RInterface.hxx @@ -50,6 +50,7 @@ #include #include #include +#include #endif #include @@ -2357,6 +2358,234 @@ public: return CreateAction( columnListWithWeights, h, h, fProxiedPtr, columnListWithWeights.size()); } + + //////////////////////////////////////////////////////////////////////////// + /// \brief Fill and return a one-dimensional RProfile (*lazy action*). + /// \param[in] nNormalBins The returned profile histogram will be constructed using this number of normal bins. + /// \param[in] interval The axis interval of the constructed profile histogram (lower end inclusive, upper end + /// exclusive). + /// \param[in] xName The name of the column that will fill the profile histogram. + /// \param[in] vName The name of the column that will provide the values for the profile histogram. + /// \return the profile histogram wrapped in a RResultPtr. + /// + /// Columns can be of a container type (e.g. `std::vector` or `RVec`), in which case the profile 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. + /// + /// ### Example usage: + /// ~~~{.cpp} + /// auto myProfile = myDf.Profile(10, {5, 15}, "col0", "colV"); + /// ~~~ + template + RResultPtr Profile(std::uint64_t nNormalBins, std::pair interval, + std::string_view xName, std::string_view vName) + { + std::shared_ptr p = std::make_shared(nNormalBins, interval); + + const ColumnNames_t columnList = {std::string(xName)}; + + return Profile(p, columnList, vName); + } + + //////////////////////////////////////////////////////////////////////////// + /// \brief Fill and return an RProfile (*lazy action*). + /// \param[in] axes The returned profile histogram will be constructed using these axes. + /// \param[in] columnList A list containing the names of the columns that will be passed when calling `Fill` + /// \param[in] vName The name of the column that will provide the values for the profile histogram. + /// \return the profile histogram wrapped in a RResultPtr. + /// + /// Columns can be of a container type (e.g. `std::vector` or `RVec`), in which case the profile 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. + /// + /// ### Example usage: + /// ~~~{.cpp} + /// ROOT::Experimental::RRegularAxis axis(10, {5.0, 15.0}); + /// auto myProfile = myDf.Profile({axis}, {"col0"}, "colV"); + /// ~~~ + template + RResultPtr + Profile(std::vector axes, const ColumnNames_t &columnList, std::string_view vName) + { + if (axes.size() != columnList.size()) { + std::string msg = "Wrong number of columns for the specified number of histogram axes: "; + msg += "expected " + std::to_string(axes.size()) + ", got " + std::to_string(columnList.size()); + throw std::invalid_argument(msg); + } + + std::shared_ptr p = std::make_shared(std::move(axes)); + + return Profile(p, columnList, vName); + } + + //////////////////////////////////////////////////////////////////////////// + /// \brief Fill the provided RProfile (*lazy action*). + /// \param[in] p The profile histogram that should be filled. + /// \param[in] columnList A list containing the names of the columns that will be passed when calling `Fill` + /// \param[in] vName The name of the column that will provide the values for the profile histogram. + /// \return the profile histogram wrapped in a RResultPtr. + /// + /// Columns can be of a container type (e.g. `std::vector` or `RVec`), in which case the profile 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. + /// + /// During execution of the computation graph, the passed profile histogram must only be accessed with methods that + /// are allowed during concurrent filling. + /// + /// ### Example usage: + /// ~~~{.cpp} + /// auto p = std::make_shared(10, {5.0, 15.0}); + /// auto myProfile = myDf.Profile(h, {"col0"}, "colV"); + /// ~~~ + template + RResultPtr + Profile(std::shared_ptr p, const ColumnNames_t &columnList, std::string_view vName) + { + RDFInternal::WarnProfile(); + + if (p->GetNDimensions() != columnList.size()) { + std::string msg = "Wrong number of columns for the passed profile histogram: "; + msg += "expected " + std::to_string(p->GetNDimensions()) + ", got " + std::to_string(columnList.size()); + throw std::invalid_argument(msg); + } + + // Add the value column to the list of argument columns to pass it through the infrastructure. + ColumnNames_t columnListWithValues(columnList); + columnListWithValues.push_back(std::string(vName)); + + return CreateAction( + columnListWithValues, p, p, fProxiedPtr, columnListWithValues.size()); + } + + //////////////////////////////////////////////////////////////////////////// + /// \brief Fill and return a one-dimensional RProfile with weights (*lazy action*). + /// \param[in] nNormalBins The returned profile histogram will be constructed using this number of normal bins. + /// \param[in] interval The axis interval of the constructed profile histogram (lower end inclusive, upper end + /// exclusive). + /// \param[in] xName The name of the column that will fill the profile histogram. + /// \param[in] vName The name of the column that will provide the values for the profile histogram. + /// \param[in] wName The name of the column that will provide the weights. + /// \return the profile histogram wrapped in a RResultPtr. + /// + /// Columns can be of a container type (e.g. `std::vector` or `RVec`), in which case the profile 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. + /// + /// ### Example usage: + /// ~~~{.cpp} + /// auto myProfile = myDf.Profile(10, {5, 15}, "col0", "colV", "colW"); + /// ~~~ + template + RResultPtr + Profile(std::uint64_t nNormalBins, std::pair interval, std::string_view xName, + std::string_view vName, std::string_view wName) + { + std::shared_ptr p = std::make_shared(nNormalBins, interval); + + const ColumnNames_t columnList = {std::string(xName)}; + + return Profile(p, columnList, vName, wName); + } + + //////////////////////////////////////////////////////////////////////////// + /// \brief Fill and return an RProfile with weights (*lazy action*). + /// \param[in] axes The returned profile histogram will be constructed using these axes. + /// \param[in] columnList A list containing the names of the columns that will be passed when calling `Fill` + /// \param[in] vName The name of the column that will provide the values for the profile histogram. + /// \param[in] wName The name of the column that will provide the weights. + /// \return the profile histogram wrapped in a RResultPtr. + /// + /// Columns can be of a container type (e.g. `std::vector` or `RVec`), in which case the profile 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. + /// + /// ### Example usage: + /// ~~~{.cpp} + /// ROOT::Experimental::RRegularAxis axis(10, {5.0, 15.0}); + /// auto myProfile = myDf.Profile({axis}, {"col0"}, "colV", "colW"); + /// ~~~ + template + RResultPtr + Profile(std::vector axes, const ColumnNames_t &columnList, std::string_view vName, + std::string_view wName) + { + if (axes.size() != columnList.size()) { + std::string msg = "Wrong number of columns for the specified number of histogram axes: "; + msg += "expected " + std::to_string(axes.size()) + ", got " + std::to_string(columnList.size()); + throw std::invalid_argument(msg); + } + + std::shared_ptr p = std::make_shared(std::move(axes)); + + return Profile(p, columnList, vName, wName); + } + + //////////////////////////////////////////////////////////////////////////// + /// \brief Fill the provided RProfile with weights (*lazy action*). + /// \param[in] p The profile histogram that should be filled. + /// \param[in] columnList A list containing the names of the columns that will be passed when calling `Fill` + /// \param[in] vName The name of the column that will provide the values for the profile histogram. + /// \param[in] wName The name of the column that will provide the weights. + /// \return the profile histogram wrapped in a RResultPtr. + /// + /// Columns can be of a container type (e.g. `std::vector` or `RVec`), in which case the profile 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. + /// + /// During execution of the computation graph, the passed profile histogram must only be accessed with methods that + /// are allowed during concurrent filling. + /// + /// ### Example usage: + /// ~~~{.cpp} + /// auto h = std::make_shared(10, {5.0, 15.0}); + /// auto myProfile = myDf.Profile(h, {"col0"}, "colV", "colW"); + /// ~~~ + template + RResultPtr + Profile(std::shared_ptr p, const ColumnNames_t &columnList, std::string_view vName, + std::string_view wName) + { + RDFInternal::WarnProfile(); + + if (p->GetNDimensions() != columnList.size()) { + std::string msg = "Wrong number of columns for the passed profile histogram: "; + msg += "expected " + std::to_string(p->GetNDimensions()) + ", got " + std::to_string(columnList.size()); + throw std::invalid_argument(msg); + } + + // Add the value and weight columns to the list of argument columns to pass it through the infrastructure. + ColumnNames_t columnListWithValuesAndWeights(columnList); + columnListWithValuesAndWeights.push_back(std::string(vName)); + columnListWithValuesAndWeights.push_back(std::string(wName)); + + return CreateAction( + columnListWithValuesAndWeights, p, p, fProxiedPtr, columnListWithValuesAndWeights.size()); + } #endif //////////////////////////////////////////////////////////////////////////// diff --git a/tree/dataframe/inc/ROOT/RDF/Utils.hxx b/tree/dataframe/inc/ROOT/RDF/Utils.hxx index ba580999fb68d..73573e990e165 100644 --- a/tree/dataframe/inc/ROOT/RDF/Utils.hxx +++ b/tree/dataframe/inc/ROOT/RDF/Utils.hxx @@ -67,6 +67,8 @@ namespace RDF { /// Warn once about experimental filling of RHist. void WarnHist(); +/// Warn once about experimental filling of RProfile. +void WarnProfile(); using namespace ROOT::TypeTraits; using namespace ROOT::Detail::RDF; diff --git a/tree/dataframe/inc/ROOT/RDFHelpers.hxx b/tree/dataframe/inc/ROOT/RDFHelpers.hxx index ad78d554ab9d0..c721ca9a7f3de 100644 --- a/tree/dataframe/inc/ROOT/RDFHelpers.hxx +++ b/tree/dataframe/inc/ROOT/RDFHelpers.hxx @@ -23,6 +23,7 @@ #ifdef R__HAS_ROOT7 #include #include +#include #endif #include @@ -102,6 +103,13 @@ std::shared_ptr> CopyForVariations(const ROOT { return std::make_shared>(obj.Clone()); } + +/// \brief Specialization of CopyForVariations for ROOT::Experimental::RProfile objects, which are not copyable but +/// clonable. +inline std::shared_ptr CopyForVariations(const ROOT::Experimental::RProfile &obj) +{ + return std::make_shared(obj.Clone()); +} #endif } // namespace RDF diff --git a/tree/dataframe/src/RDFUtils.cxx b/tree/dataframe/src/RDFUtils.cxx index 5374fa5cb45a3..c0df7e57b879f 100644 --- a/tree/dataframe/src/RDFUtils.cxx +++ b/tree/dataframe/src/RDFUtils.cxx @@ -58,6 +58,18 @@ void ROOT::Internal::RDF::WarnHist() std::call_once(once, ::WarnHist); } +// A static function, not in an anonymous namespace, because the function name is included in the user-visible message. +static void WarnProfile() +{ + R__LOG_WARNING(RDFLogChannel()) << "Filling RProfile is experimental and still under development."; +} + +void ROOT::Internal::RDF::WarnProfile() +{ + static std::once_flag once; + std::call_once(once, ::WarnProfile); +} + namespace { using TypeInfoRef = std::reference_wrapper; struct TypeInfoRefHash { diff --git a/tree/dataframe/test/CMakeLists.txt b/tree/dataframe/test/CMakeLists.txt index fb2e05122eab0..a3fc5fefd948a 100644 --- a/tree/dataframe/test/CMakeLists.txt +++ b/tree/dataframe/test/CMakeLists.txt @@ -144,6 +144,7 @@ endif() if (root7) ROOT_ADD_GTEST(dataframe_hist dataframe_hist.cxx LIBRARIES ROOTDataFrame ROOTHist) + ROOT_ADD_GTEST(dataframe_profile dataframe_profile.cxx LIBRARIES ROOTDataFrame ROOTHist) endif() if(ARROW_FOUND) diff --git a/tree/dataframe/test/dataframe_profile.cxx b/tree/dataframe/test/dataframe_profile.cxx new file mode 100644 index 0000000000000..5a9d9a452682b --- /dev/null +++ b/tree/dataframe/test/dataframe_profile.cxx @@ -0,0 +1,593 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using ROOT::RDataFrame; +using ROOT::Experimental::RProfile; +using ROOT::Experimental::RRegularAxis; +using ROOT::Experimental::RVariableBinAxis; +using ROOT::RDF::RunGraphs; + +// Fixture for all tests in this file to optionally run with multi-threading. +class RDFProfile : public ::testing::TestWithParam { + ROOT::TestSupport::CheckDiagsRAII fDiag; + +public: + RDFProfile() + { + fDiag.optionalDiag(kWarning, "", "Filling RProfile is experimental", /*matchFullMessage=*/false); + fDiag.optionalDiag(kWarning, "cling", + "expected alignment (8 bytes) exceeds the actual alignment (4 bytes) [-Watomic-alignment]", + false); + if (GetParam()) + ROOT::EnableImplicitMT(4); + } + ~RDFProfile() override + { + if (GetParam()) + ROOT::DisableImplicitMT(); + } +}; + +TEST_P(RDFProfile, Regular) +{ + RDataFrame df(10); + auto dfXY = df.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}) + .Define("y", [](ULong64_t e) { return 2 * e + 0.5; }, {"rdfentry_"}); + + const RRegularAxis axis(10, {5.0, 15.0}); + auto profile = dfXY.Profile({axis}, {"x"}, "y"); + EXPECT_EQ(profile->GetNEntries(), 10); + for (auto index : axis.GetNormalRange()) { + EXPECT_EQ(profile->GetBinContent(index).fSum, 1.0); + EXPECT_EQ(profile->GetBinContent(index).fSumValues, 2 * index.GetIndex() + 0.5); + } + + // The one-dimensional specialization returns the same type. + profile = dfXY.Profile(10, {5.0, 15.0}, "x", "y"); + EXPECT_EQ(profile->GetNEntries(), 10); +} + +TEST_P(RDFProfile, RegularJit) +{ + RDataFrame df(10); + auto dfXY = df.Define("x", "rdfentry_ + 5.5").Define("y", "2 * rdfentry_ + 0.5"); + + const RRegularAxis axis(10, {5.0, 15.0}); + auto profile = dfXY.Profile({axis}, {"x"}, "y"); + EXPECT_EQ(profile->GetNEntries(), 10); + for (auto index : axis.GetNormalRange()) { + EXPECT_EQ(profile->GetBinContent(index).fSum, 1.0); + EXPECT_EQ(profile->GetBinContent(index).fSumValues, 2 * index.GetIndex() + 0.5); + } + + // The one-dimensional specialization returns the same type. + profile = dfXY.Profile(10, {5.0, 15.0}, "x", "y"); + EXPECT_EQ(profile->GetNEntries(), 10); +} + +TEST_P(RDFProfile, MultiDim) +{ + RDataFrame df(10); + auto dfXYZ = df.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}) + .Define("y", [](ULong64_t e) { return 2 * e + 0.5; }, {"rdfentry_"}) + .Define("z", [](ULong64_t e) { return 3 * e + 10.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 profile = dfXYZ.Profile({regularAxis, variableBinAxis}, {"x", "y"}, "z"); + EXPECT_EQ(profile->GetNEntries(), 10); + for (auto x : regularAxis.GetNormalRange()) { + for (auto y : variableBinAxis.GetNormalRange()) { + if (2 * x.GetIndex() == y.GetIndex()) { + EXPECT_EQ(profile->GetBinContent(x, y).fSum, 1.0); + EXPECT_EQ(profile->GetBinContent(x, y).fSumValues, 3 * x.GetIndex() + 10.5); + } else { + EXPECT_EQ(profile->GetBinContent(x, y).fSum, 0.0); + } + } + } +} + +TEST_P(RDFProfile, MultiDimJit) +{ + RDataFrame df(10); + auto dfXYZ = + df.Define("x", "rdfentry_ + 5.5").Define("y", "2 * rdfentry_ + 0.5").Define("z", "3 * rdfentry_ + 10.5"); + + 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 profile = dfXYZ.Profile({regularAxis, variableBinAxis}, {"x", "y"}, "z"); + EXPECT_EQ(profile->GetNEntries(), 10); + for (auto x : regularAxis.GetNormalRange()) { + for (auto y : variableBinAxis.GetNormalRange()) { + if (2 * x.GetIndex() == y.GetIndex()) { + EXPECT_EQ(profile->GetBinContent(x, y).fSum, 1.0); + EXPECT_EQ(profile->GetBinContent(x, y).fSumValues, 3 * x.GetIndex() + 10.5); + } else { + EXPECT_EQ(profile->GetBinContent(x, y).fSum, 0.0); + } + } + } +} + +TEST_P(RDFProfile, Ptr) +{ + RDataFrame df(10); + auto dfXY = df.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}) + .Define("y", [](ULong64_t e) { return 2 * e + 0.5; }, {"rdfentry_"}); + + auto profile = std::make_shared(10, std::make_pair(5.0, 15.0)); + auto resPtr = dfXY.Profile(profile, {"x"}, "y"); + EXPECT_EQ(profile, resPtr.GetSharedPtr()); + EXPECT_EQ(profile->GetNEntries(), 10); +} + +TEST_P(RDFProfile, PtrJit) +{ + RDataFrame df(10); + auto dfXY = df.Define("x", "rdfentry_ + 5.5").Define("y", "2 * rdfentry_ + 0.5"); + + auto profile = std::make_shared(10, std::make_pair(5.0, 15.0)); + auto resPtr = dfXY.Profile(profile, {"x"}, "y"); + EXPECT_EQ(profile, resPtr.GetSharedPtr()); + EXPECT_EQ(profile->GetNEntries(), 10); +} + +TEST_P(RDFProfile, InvalidNumberOfArguments) +{ + RDataFrame df(10); + auto dfX = df.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}); + + const RRegularAxis axis(10, {5.0, 15.0}); + try { + // Cannot use EXPECT_THROW because of template arguments... + dfX.Profile({axis}, {"x", "x"}, "x"); + FAIL() << "expected std::invalid_argument"; + } catch (const std::invalid_argument &) { + // expected + } + + auto profile = std::make_shared(axis); + try { + // Cannot use EXPECT_THROW because of template arguments... + dfX.Profile(profile, {"x", "x"}, "x"); + FAIL() << "expected std::invalid_argument"; + } catch (const std::invalid_argument &) { + // expected + } +} + +TEST_P(RDFProfile, InvalidNumberOfArgumentsJit) +{ + RDataFrame df(10); + auto dfX = df.Define("x", "rdfentry_ + 5.5"); + + const RRegularAxis axis(10, {5.0, 15.0}); + EXPECT_THROW(dfX.Profile({axis}, {"x", "x"}, "x"), std::invalid_argument); + + auto profile = std::make_shared(axis); + EXPECT_THROW(dfX.Profile(profile, {"x", "x"}, "x"), std::invalid_argument); +} + +TEST_P(RDFProfile, Weight) +{ + RDataFrame df(10); + auto dfXYW = df.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}) + .Define("y", [](ULong64_t e) { return 2 * e + 0.5; }, {"rdfentry_"}) + .Define("w", [](ULong64_t e) { return 0.1 + e * 0.03; }, {"rdfentry_"}); + + const RRegularAxis axis(10, {5.0, 15.0}); + auto profile = dfXYW.Profile({axis}, {"x"}, "y", "w"); + EXPECT_EQ(profile->GetNEntries(), 10); + for (auto index : axis.GetNormalRange()) { + auto &bin = profile->GetBinContent(index); + double value = 2 * index.GetIndex() + 0.5; + double weight = 0.1 + index.GetIndex() * 0.03; + EXPECT_FLOAT_EQ(bin.fSum, weight); + EXPECT_FLOAT_EQ(bin.fSum2, weight * weight); + EXPECT_FLOAT_EQ(bin.fSumValues, weight * value); + EXPECT_FLOAT_EQ(bin.fSumValues2, weight * value * value); + } + + // The one-dimensional specialization returns the same type. + profile = dfXYW.Profile(10, {5.0, 15.0}, "x", "y", "w"); + EXPECT_EQ(profile->GetNEntries(), 10); +} + +TEST_P(RDFProfile, WeightJit) +{ + RDataFrame df(10); + auto dfXYW = + df.Define("x", "rdfentry_ + 5.5").Define("y", "2 * rdfentry_ + 0.5").Define("w", "0.1 + rdfentry_ * 0.03"); + + const RRegularAxis axis(10, {5.0, 15.0}); + auto profile = dfXYW.Profile({axis}, {"x"}, "y", "w"); + EXPECT_EQ(profile->GetNEntries(), 10); + for (auto index : axis.GetNormalRange()) { + auto &bin = profile->GetBinContent(index); + double value = 2 * index.GetIndex() + 0.5; + double weight = 0.1 + index.GetIndex() * 0.03; + EXPECT_FLOAT_EQ(bin.fSum, weight); + EXPECT_FLOAT_EQ(bin.fSum2, weight * weight); + EXPECT_FLOAT_EQ(bin.fSumValues, weight * value); + EXPECT_FLOAT_EQ(bin.fSumValues2, weight * value * value); + } + + // The one-dimensional specialization returns the same type. + profile = dfXYW.Profile(10, {5.0, 15.0}, "x", "y", "w"); + EXPECT_EQ(profile->GetNEntries(), 10); +} + +TEST_P(RDFProfile, PtrWeight) +{ + RDataFrame df(10); + auto dfXYW = df.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}) + .Define("y", [](ULong64_t e) { return 2 * e + 0.5; }, {"rdfentry_"}) + .Define("w", [](ULong64_t e) { return 0.1 + e * 0.03; }, {"rdfentry_"}); + + auto profile = std::make_shared(10, std::make_pair(5.0, 15.0)); + auto resPtr = dfXYW.Profile(profile, {"x"}, "y", "w"); + EXPECT_EQ(profile, resPtr.GetSharedPtr()); + EXPECT_EQ(profile->GetNEntries(), 10); +} + +TEST_P(RDFProfile, PtrWeightJit) +{ + RDataFrame df(10); + auto dfXYW = + df.Define("x", "rdfentry_ + 5.5").Define("y", "2 * rdfentry_ + 0.5").Define("w", "0.1 + rdfentry_ * 0.03"); + + auto profile = std::make_shared(10, std::make_pair(5.0, 15.0)); + auto resPtr = dfXYW.Profile(profile, {"x"}, "y", "w"); + EXPECT_EQ(profile, resPtr.GetSharedPtr()); + EXPECT_EQ(profile->GetNEntries(), 10); +} + +TEST_P(RDFProfile, WeightInvalidNumberOfArguments) +{ + RDataFrame df(10); + auto dfXW = df.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}) + .Define("w", [](ULong64_t e) { return 0.1 + e * 0.03; }, {"rdfentry_"}); + + const RRegularAxis axis(10, {5.0, 15.0}); + try { + // Cannot use EXPECT_THROW because of template arguments... + dfXW.Profile({axis}, {"x", "x"}, "x", "w"); + FAIL() << "expected std::invalid_argument"; + } catch (const std::invalid_argument &) { + // expected + } + + auto profile = std::make_shared(axis); + try { + // Cannot use EXPECT_THROW because of template arguments... + dfXW.Profile(profile, {"x", "x"}, "x", "w"); + FAIL() << "expected std::invalid_argument"; + } catch (const std::invalid_argument &) { + // expected + } +} + +TEST_P(RDFProfile, WeightInvalidNumberOfArgumentsJit) +{ + RDataFrame df(10); + auto dfXW = df.Define("x", "rdfentry_ + 5.5").Define("w", "0.1 + rdfentry_ * 0.03"); + + const RRegularAxis axis(10, {5.0, 15.0}); + EXPECT_THROW(dfXW.Profile({axis}, {"x", "x"}, "x", "w"), std::invalid_argument); + + auto profile = std::make_shared(axis); + EXPECT_THROW(dfXW.Profile(profile, {"x", "x"}, "x", "w"), std::invalid_argument); +} + +TEST_P(RDFProfile, 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 profile = dfX.Profile({axis}, {"x"}, "x"); + EXPECT_EQ(profile->GetNEntries(), 20); + for (auto index : axis.GetNormalRange()) { + EXPECT_EQ(profile->GetBinContent(index).fSum, 1.0); + } +} + +TEST_P(RDFProfile, 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 profile = dfX.Profile, ROOT::RVec>({axis}, {"x"}, "x"); + EXPECT_EQ(profile->GetNEntries(), 20); + for (auto index : axis.GetNormalRange()) { + EXPECT_EQ(profile->GetBinContent(index).fSum, 1.0); + } +} + +TEST_P(RDFProfile, ContainerJit) +{ + RDataFrame df(10); + auto dfX = df.Define("x", "ROOT::RVecD{rdfentry_ + 5.5}"); + + const RRegularAxis axis(10, {5.0, 15.0}); + auto profile = dfX.Profile({axis}, {"x"}, "x"); + EXPECT_EQ(profile->GetNEntries(), 10); + for (auto index : axis.GetNormalRange()) { + EXPECT_EQ(profile->GetBinContent(index).fSum, 1.0); + } +} + +TEST_P(RDFProfile, ContainerMultiDim) +{ + RDataFrame df(10); + auto dfXYZ = 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_"}) + .Define("z", [](ULong64_t e) { return ROOT::RVecD{3 * e + 10.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 profile = dfXYZ.Profile({regularAxis, variableBinAxis}, {"x", "y"}, "z"); + EXPECT_EQ(profile->GetNEntries(), 10); + for (auto x : regularAxis.GetNormalRange()) { + for (auto y : variableBinAxis.GetNormalRange()) { + if (2 * x.GetIndex() == y.GetIndex()) { + EXPECT_EQ(profile->GetBinContent(x, y).fSum, 1.0); + EXPECT_EQ(profile->GetBinContent(x, y).fSumValues, 3 * x.GetIndex() + 10.5); + } else { + EXPECT_EQ(profile->GetBinContent(x, y).fSum, 0.0); + } + } + } +} + +TEST_P(RDFProfile, ContainerMultiDimBroadcast) +{ + RDataFrame df(10); + auto dfXYZ = df.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}) + .Define("y", [](ULong64_t e) { return ROOT::RVecD{2 * e + 0.5}; }, {"rdfentry_"}) + .Define("z", [](ULong64_t e) { return ROOT::RVecD{3 * e + 10.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 profile = dfXYZ.Profile({regularAxis, variableBinAxis}, {"x", "y"}, "z"); + EXPECT_EQ(profile->GetNEntries(), 10); + for (auto x : regularAxis.GetNormalRange()) { + for (auto y : variableBinAxis.GetNormalRange()) { + if (2 * x.GetIndex() == y.GetIndex()) { + EXPECT_EQ(profile->GetBinContent(x, y).fSum, 1.0); + EXPECT_EQ(profile->GetBinContent(x, y).fSumValues, 3 * x.GetIndex() + 10.5); + } else { + EXPECT_EQ(profile->GetBinContent(x, y).fSum, 0.0); + } + } + } +} + +TEST_P(RDFProfile, ContainerMultiDimNestedBroadcast) +{ + RDataFrame df(10); + auto dfXYZ = 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_"}) + .Define("z", [](ULong64_t e) { return ROOT::RVecD{3 * e + 10.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 profile = + dfXYZ.Profile, ROOT::RVecD>({regularAxis, variableBinAxis}, {"x", "y"}, "z"); + EXPECT_EQ(profile->GetNEntries(), 10); + for (auto x : regularAxis.GetNormalRange()) { + for (auto y : variableBinAxis.GetNormalRange()) { + if (2 * x.GetIndex() == y.GetIndex()) { + EXPECT_EQ(profile->GetBinContent(x, y).fSum, 1.0); + EXPECT_EQ(profile->GetBinContent(x, y).fSumValues, 3 * x.GetIndex() + 10.5); + } else { + EXPECT_EQ(profile->GetBinContent(x, y).fSum, 0.0); + } + } + } + + profile = dfXYZ.Profile, ROOT::RVecD>({regularAxis, variableBinAxis}, + {"xV", "y"}, "z"); + EXPECT_EQ(profile->GetNEntries(), 10); +} + +TEST_P(RDFProfile, 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.Profile({regularAxis, variableBinAxis}, {"x0", "y1"}, "y1"); + FAIL() << "expected std::runtime_error"; + } catch (const std::runtime_error &) { + // expected + } + + try { + // Cannot use EXPECT_THROW because of template arguments... + *dfXY.Profile({regularAxis, variableBinAxis}, {"x2", "y1"}, "x2"); + FAIL() << "expected std::runtime_error"; + } catch (const std::runtime_error &e) { + // expected + } +} + +TEST_P(RDFProfile, WeightContainer) +{ + RDataFrame df(10); + auto dfXW = + df.Define("xV", [](ULong64_t e) { return ROOT::RVecD{e + 5.5, e + 15.5}; }, {"rdfentry_"}) + .Define("yV", [](ULong64_t e) { return ROOT::RVecD{2 * e + 0.5, 2 * e + 2.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 profile = dfXW.Profile({axis}, {"xV"}, "yV", "wV"); + EXPECT_EQ(profile->GetNEntries(), 20); + for (auto index : axis.GetNormalRange()) { + auto &bin = profile->GetBinContent(index); + auto i = index.GetIndex(); + double value = i >= 10 ? (2 * (i - 10) + 2.5) : (2 * i + 0.5); + 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); + EXPECT_FLOAT_EQ(bin.fSumValues, weight * value); + EXPECT_FLOAT_EQ(bin.fSumValues2, weight * value * value); + } +} + +TEST_P(RDFProfile, WeightContainerJit) +{ + RDataFrame df(10); + auto dfXYW = df.Define("xV", "ROOT::RVecD{rdfentry_ + 5.5, rdfentry_ + 15.5}") + .Define("yV", "ROOT::RVecD{2 * rdfentry_ + 0.5, 2 * rdfentry_ + 2.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 profile = dfXYW.Profile({axis}, {"xV"}, "yV", "wV"); + EXPECT_EQ(profile->GetNEntries(), 20); + for (auto index : axis.GetNormalRange()) { + auto &bin = profile->GetBinContent(index); + auto i = index.GetIndex(); + double value = i >= 10 ? (2 * (i - 10) + 2.5) : (2 * i + 0.5); + 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); + EXPECT_FLOAT_EQ(bin.fSumValues, weight * value); + EXPECT_FLOAT_EQ(bin.fSumValues2, weight * value * value); + } +} + +TEST_P(RDFProfile, WeightContainerBroadcast) +{ + RDataFrame df(10); + auto dfXYW = + 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("yV", [](ULong64_t e) { return ROOT::RVecD{2 * e + 0.5, 2 * e + 2.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 profile = dfXYW.Profile({axis}, {"xV"}, "yV", "w"); + EXPECT_EQ(profile->GetNEntries(), 20); + for (auto index : axis.GetNormalRange()) { + auto &bin = profile->GetBinContent(index); + auto i = index.GetIndex(); + double value = i >= 10 ? (2 * (i - 10) + 2.5) : (2 * i + 0.5); + double weight = 0.1 + (i >= 10 ? i - 10 : i) * 0.03; + EXPECT_FLOAT_EQ(bin.fSum, weight); + EXPECT_FLOAT_EQ(bin.fSum2, weight * weight); + EXPECT_FLOAT_EQ(bin.fSumValues, weight * value); + EXPECT_FLOAT_EQ(bin.fSumValues2, weight * value * value); + } + + profile = dfXYW.Profile({axis}, {"x"}, "yV", "wV"); + EXPECT_EQ(profile->GetNEntries(), 20); + EXPECT_EQ(profile->GetBinContent(2).fSum, 0.1 + 2 * 0.03 + 0.2 + 2 * 2 * 0.06); +} + +TEST_P(RDFProfile, Variations) +{ + RDataFrame df(10); + auto dfXY = df.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}) + .Vary( + "x", [](double x) { return ROOT::RVecD{x - 1.0, x + 1.0}; }, {"x"}, 2) + .Define("y", [](ULong64_t e) { return 2 * e + 0.5; }, {"rdfentry_"}); + + const RRegularAxis axis(10, {5.0, 15.0}); + auto profile = dfXY.Profile({axis}, {"x"}, "y"); + auto vars = ROOT::RDF::Experimental::VariationsFor(profile); + + // Check nominal + EXPECT_EQ(profile->GetNEntries(), 10); + for (auto index : axis.GetNormalRange()) { + EXPECT_EQ(profile->GetBinContent(index).fSum, 1.0); + } + + // Check variations + EXPECT_EQ(vars.GetKeys().size(), 3); // nominal + 2 variations + for (const auto &key : vars.GetKeys()) { + auto &varHist = vars[key]; + EXPECT_EQ(varHist.GetNEntries(), 10); + for (auto index : axis.GetNormalRange()) { + if (key == "nominal") { + EXPECT_EQ(varHist.GetBinContent(index).fSum, 1.0); + } else if (key == "x:0") { + EXPECT_EQ(varHist.GetBinContent(index).fSum, (index.GetIndex() == 9 ? 0.0 : 1.0)); + } else if (key == "x:1") { + EXPECT_EQ(varHist.GetBinContent(index).fSum, (index.GetIndex() == 0 ? 0.0 : 1.0)); + } else { + FAIL() << "Unexpected variation key: " << key; + } + } + } +} + +INSTANTIATE_TEST_SUITE_P(Seq, RDFProfile, ::testing::Values(false)); + +#ifdef R__USE_IMT +INSTANTIATE_TEST_SUITE_P(MT, RDFProfile, ::testing::Values(true)); +#endif