From 6d3b030ac99e23449e957b8ea200e43541c5e19d Mon Sep 17 00:00:00 2001 From: Jonas Hahnfeld Date: Fri, 14 Nov 2025 11:31:44 +0100 Subject: [PATCH] [hist] Support filling with user-defined weights Only in RHistEngine because RHist needs conversion to double for global statistics (RHistStats). --- hist/histv7/inc/ROOT/RHistEngine.hxx | 55 ++++++++++++++++++++++++++ hist/histv7/test/hist_user.cxx | 59 +++++++++++++++++++++++++++- 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/hist/histv7/inc/ROOT/RHistEngine.hxx b/hist/histv7/inc/ROOT/RHistEngine.hxx index 78f93f1e9e2b4..95b19ecda7a39 100644 --- a/hist/histv7/inc/ROOT/RHistEngine.hxx +++ b/hist/histv7/inc/ROOT/RHistEngine.hxx @@ -307,6 +307,36 @@ public: } } + /// Fill an entry into the histogram with a user-defined weight. + /// + /// This overload is only available for user-defined bin content types. + /// + /// If one of the arguments is outside the corresponding axis and flow bins are disabled, the entry will be silently + /// discarded. + /// + /// Throws an exception if the number of arguments does not match the axis configuration, or if an argument cannot be + /// converted for the axis type at run-time. + /// + /// \param[in] args the arguments for each axis + /// \param[in] weight the weight for this entry + template + void Fill(const std::tuple &args, const W &weight) + { + static_assert(std::is_class_v, + "user-defined weight types are only supported for user-defined bin content types"); + + // We could rely on RAxes::ComputeGlobalIndex to check the number of arguments, but its exception message might + // be confusing for users. + if (sizeof...(A) != GetNDimensions()) { + throw std::invalid_argument("invalid number of arguments to Fill"); + } + RLinearizedIndex index = fAxes.ComputeGlobalIndexImpl(args); + if (index.fValid) { + assert(index.fIndex < fBinContents.size()); + fBinContents[index.fIndex] += weight; + } + } + /// Fill an entry into the histogram. /// /// \code @@ -395,6 +425,31 @@ public: } } + /// Fill an entry into the histogram with a user-defined weight using atomic instructions. + /// + /// This overload is only available for user-defined bin content types. + /// + /// \param[in] args the arguments for each axis + /// \param[in] weight the weight for this entry + /// \see Fill(const std::tuple &args, const W &weight) + template + void FillAtomic(const std::tuple &args, const W &weight) + { + static_assert(std::is_class_v, + "user-defined weight types are only supported for user-defined bin content types"); + + // We could rely on RAxes::ComputeGlobalIndex to check the number of arguments, but its exception message might + // be confusing for users. + if (sizeof...(A) != GetNDimensions()) { + throw std::invalid_argument("invalid number of arguments to Fill"); + } + RLinearizedIndex index = fAxes.ComputeGlobalIndexImpl(args); + if (index.fValid) { + assert(index.fIndex < fBinContents.size()); + Internal::AtomicAdd(&fBinContents[index.fIndex], weight); + } + } + /// Fill an entry into the histogram using atomic instructions. /// /// \param[in] args the arguments for each axis diff --git a/hist/histv7/test/hist_user.cxx b/hist/histv7/test/hist_user.cxx index 32520846feb92..c7791fa770da3 100644 --- a/hist/histv7/test/hist_user.cxx +++ b/hist/histv7/test/hist_user.cxx @@ -2,6 +2,11 @@ #include +// User-defined weight type with a single double, but can be much more complicated +struct UserWeight { + double fWeight = 0; +}; + // User-defined bin content type consisting of a single double, but can be much more complicated. struct User { double fValue = 0; @@ -25,6 +30,12 @@ struct User { return *this; } + User &operator+=(const UserWeight &w) + { + fValue += w.fWeight; + return *this; + } + User &operator+=(const User &rhs) { fValue += rhs.fValue; @@ -41,6 +52,8 @@ struct User { void AtomicAdd(double w) { ROOT::Experimental::Internal::AtomicAdd(&fValue, w); } + void AtomicAdd(const UserWeight &w) { ROOT::Experimental::Internal::AtomicAdd(&fValue, w.fWeight); } + void AtomicAdd(const User &rhs) { ROOT::Experimental::Internal::AtomicAdd(&fValue, rhs.fValue); } }; @@ -149,6 +162,28 @@ TEST(RHistEngineUser, FillWeight) EXPECT_EQ(engine.GetBinContent(indices).fValue, 0.9); } +TEST(RHistEngineUser, FillUserWeight) +{ + // Weighted filling with user-defined weight uses operator+=(const UserWeight &) + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, {0, Bins}); + RHistEngine engine({axis}); + + // Must use overload accepting std::tuple + engine.Fill(std::make_tuple(9.5), UserWeight{0.9}); + + EXPECT_EQ(engine.GetBinContent(RBinIndex(9)).fValue, 0.9); +} + +TEST(RHistEngineUser, FillUserWeightInvalidNumberOfArguments) +{ + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, {0, Bins}); + RHistEngine engine({axis}); + + EXPECT_THROW(engine.Fill(std::make_tuple(8.5, 9.5), UserWeight{0.9}), std::invalid_argument); +} + TEST(RHistEngineUser, FillAtomic) { // Unweighted filling with atomic instructions uses AtomicInc @@ -166,7 +201,7 @@ TEST(RHistEngineUser, FillAtomic) TEST(RHistEngineUser, FillAtomicWeight) { - // Weighted filling with atomic instructions uses AtomicAdd + // Weighted filling with atomic instructions uses AtomicAdd(double) static constexpr std::size_t Bins = 20; const RRegularAxis axis(Bins, {0, Bins}); RHistEngine engine({axis}); @@ -179,6 +214,28 @@ TEST(RHistEngineUser, FillAtomicWeight) EXPECT_EQ(engine.GetBinContent(indices).fValue, 0.9); } +TEST(RHistEngineUser, FillAtomicUserWeight) +{ + // Weighted filling with user-defined weight and atomic instructions uses AtomicAdd(const UserWeight &) + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, {0, Bins}); + RHistEngine engine({axis}); + + // Must use overload accepting std::tuple + engine.FillAtomic(std::make_tuple(9.5), UserWeight{0.9}); + + EXPECT_EQ(engine.GetBinContent(RBinIndex(9)).fValue, 0.9); +} + +TEST(RHistEngineUser, FillAtomicUserWeightInvalidNumberOfArguments) +{ + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, {0, Bins}); + RHistEngine engine({axis}); + + EXPECT_THROW(engine.FillAtomic(std::make_tuple(8.5, 9.5), UserWeight{0.9}), std::invalid_argument); +} + TEST(RHistEngineUser, Scale) { // Scaling uses operator+=(double)