From a24d2e69071dc05bf7ea0a6ed5097ee674fa3764 Mon Sep 17 00:00:00 2001 From: Giovanni Date: Wed, 29 Jul 2026 12:27:59 +0200 Subject: [PATCH] [df] Support VariationsFor for RHist --- tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx | 14 ++++ tree/dataframe/inc/ROOT/RDFHelpers.hxx | 37 +++++++++- tree/dataframe/test/dataframe_hist.cxx | 72 +++++++++++++++++++ 3 files changed, 120 insertions(+), 3 deletions(-) diff --git a/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx b/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx index 536e8b0292b75..45df52d56ed20 100644 --- a/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx +++ b/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx @@ -541,6 +541,13 @@ public: } } + RHistFillHelper MakeNew(void *newResult, std::string_view /*variation*/ = "nominal") + { + auto &result = *static_cast *>(newResult); + result->Clear(); + return RHistFillHelper(result, fContexts.size()); + } + std::string GetActionName() { return "Hist"; } }; @@ -588,6 +595,13 @@ public: void Finalize() {} + RHistEngineFillHelper MakeNew(void *newResult, std::string_view /*variation*/ = "nominal") + { + auto &result = *static_cast *>(newResult); + result->Clear(); + return RHistEngineFillHelper(result); + } + std::string GetActionName() { return "Hist"; } }; #endif diff --git a/tree/dataframe/inc/ROOT/RDFHelpers.hxx b/tree/dataframe/inc/ROOT/RDFHelpers.hxx index 2b3115a39289e..f4d753a7f45c3 100644 --- a/tree/dataframe/inc/ROOT/RDFHelpers.hxx +++ b/tree/dataframe/inc/ROOT/RDFHelpers.hxx @@ -65,6 +65,37 @@ auto PassAsVec(F &&f) -> PassAsVecHelper, T, F> return PassAsVecHelper, T, F>(std::forward(f)); } +/** + * \brief Helper function to add a copy of an object to a vector of shared_ptrs, used in the implementation of + * VariationsFor. + * \tparam T An object that is used as result of a RDataFrame action, e.g. a histogram + * \param obj The object to be copied and wrapped by a new std::shared_ptr. + * + * The default implementation of this function template uses copy constructor, which should work for most objects types + * since they are copied for each slot. + */ +template +std::shared_ptr CopyForVariations(const T &obj) +{ + return std::make_shared(obj); +} + +/// \brief Specialization of CopyForVariations for ROOT::Experimental::RHist objects, which are not copyable but +/// clonable. +template +std::shared_ptr> CopyForVariations(const ROOT::Experimental::RHist &obj) +{ + return std::make_shared>(obj.Clone()); +} + +/// \brief Specialization of CopyForVariations for ROOT::Experimental::RHistEngine objects, which are not copyable but +/// clonable. +template +std::shared_ptr> CopyForVariations(const ROOT::Experimental::RHistEngine &obj) +{ + return std::make_shared>(obj.Clone()); +} + } // namespace RDF } // namespace Internal @@ -243,9 +274,9 @@ RResultMap VariationsFor(RResultPtr resPtr) // clone the result once for each variation variedResults.reserve(nVariations); for (auto i = 0u; i < nVariations; ++i){ - // implicitly assuming that T is copiable: this should be the case - // for all result types in use, as they are copied for each slot - variedResults.emplace_back(new T{*resPtr.fObjPtr}); + + // Make a copy of the result object for this variation + variedResults.push_back(ROOT::Internal::RDF::CopyForVariations(*resPtr.fObjPtr)); // Check if the result's type T inherits from TNamed if constexpr (std::is_base_of::value) { diff --git a/tree/dataframe/test/dataframe_hist.cxx b/tree/dataframe/test/dataframe_hist.cxx index f5fe396b50de3..4b5f2f7c80c65 100644 --- a/tree/dataframe/test/dataframe_hist.cxx +++ b/tree/dataframe/test/dataframe_hist.cxx @@ -455,6 +455,78 @@ TEST_P(RDFHist, WeightInvalidNumberOfArgumentsJit) EXPECT_THROW(dfXW.Hist(engine, {"x", "x"}, "w"), std::invalid_argument); } +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); + + const RRegularAxis axis(10, {5.0, 15.0}); + auto hist = dfX.Hist({axis}, {"x"}); + auto vars = ROOT::RDF::Experimental::VariationsFor(hist); + + // Check nominal + EXPECT_EQ(hist->GetNEntries(), 10); + for (auto index : axis.GetNormalRange()) { + EXPECT_EQ(hist->GetBinContent(index), 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), 1.0); + } else if (key == "x:0") { + EXPECT_EQ(varHist.GetBinContent(index), (index.GetIndex() == 9 ? 0.0 : 1.0)); + } else if (key == "x:1") { + EXPECT_EQ(varHist.GetBinContent(index), (index.GetIndex() == 0 ? 0.0 : 1.0)); + } else { + FAIL() << "Unexpected variation key: " << key; + } + } + } +} + +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); + + const RRegularAxis axis(10, {5.0, 15.0}); + auto hist = std::make_shared>(axis); + auto resPtr = dfX.Hist(hist, {"x"}); + auto vars = ROOT::RDF::Experimental::VariationsFor(resPtr); + + // Trigger the run + resPtr.GetValue(); + + // Check nominal + for (auto index : axis.GetNormalRange()) { + EXPECT_EQ(hist->GetBinContent(index), 1.0); + } + + // Check variations + EXPECT_EQ(vars.GetKeys().size(), 3); // nominal + 2 variations + for (const auto &key : vars.GetKeys()) { + auto &varHist = vars[key]; + for (auto index : axis.GetNormalRange()) { + if (key == "nominal") { + EXPECT_EQ(varHist.GetBinContent(index), 1.0); + } else if (key == "x:0") { + EXPECT_EQ(varHist.GetBinContent(index), (index.GetIndex() == 9 ? 0.0 : 1.0)); + } else if (key == "x:1") { + EXPECT_EQ(varHist.GetBinContent(index), (index.GetIndex() == 0 ? 0.0 : 1.0)); + } else { + FAIL() << "Unexpected variation key: " << key; + } + } + } +} + INSTANTIATE_TEST_SUITE_P(Seq, RDFHist, ::testing::Values(false)); #ifdef R__USE_IMT