From cd8e7524affd310e49814f6438c3c25150f478f4 Mon Sep 17 00:00:00 2001 From: Jonas Hahnfeld Date: Mon, 3 Nov 2025 11:02:04 +0100 Subject: [PATCH 1/2] [hist] Test forwarding methods of RHist --- hist/histv7/test/hist_hist.cxx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/hist/histv7/test/hist_hist.cxx b/hist/histv7/test/hist_hist.cxx index a163a3e1ae904..4f73b5a883a80 100644 --- a/hist/histv7/test/hist_hist.cxx +++ b/hist/histv7/test/hist_hist.cxx @@ -105,10 +105,10 @@ TEST(RHist, Fill) std::array indices = {9}; EXPECT_EQ(hist.GetBinContent(indices), 1); - EXPECT_EQ(hist.GetStats().GetNEntries(), 2); - EXPECT_FLOAT_EQ(hist.GetStats().ComputeNEffectiveEntries(), 2); - EXPECT_FLOAT_EQ(hist.GetStats().ComputeMean(), 9); - EXPECT_FLOAT_EQ(hist.GetStats().ComputeStdDev(), 0.5); + EXPECT_EQ(hist.GetNEntries(), 2); + EXPECT_FLOAT_EQ(hist.ComputeNEffectiveEntries(), 2); + EXPECT_FLOAT_EQ(hist.ComputeMean(), 9); + EXPECT_FLOAT_EQ(hist.ComputeStdDev(), 0.5); } TEST(RHist, FillWeight) @@ -124,9 +124,9 @@ TEST(RHist, FillWeight) std::array indices = {9}; EXPECT_FLOAT_EQ(hist.GetBinContent(indices), 0.9); - EXPECT_EQ(hist.GetStats().GetNEntries(), 2); + EXPECT_EQ(hist.GetNEntries(), 2); // Cross-checked with TH1 - EXPECT_FLOAT_EQ(hist.GetStats().ComputeNEffectiveEntries(), 1.9931034); - EXPECT_FLOAT_EQ(hist.GetStats().ComputeMean(), 9.0294118); - EXPECT_FLOAT_EQ(hist.GetStats().ComputeStdDev(), 0.49913420); + EXPECT_FLOAT_EQ(hist.ComputeNEffectiveEntries(), 1.9931034); + EXPECT_FLOAT_EQ(hist.ComputeMean(), 9.0294118); + EXPECT_FLOAT_EQ(hist.ComputeStdDev(), 0.49913420); } From 325a2130f1c87161a392e5a093b302990a60053c Mon Sep 17 00:00:00 2001 From: Jonas Hahnfeld Date: Mon, 3 Nov 2025 11:00:28 +0100 Subject: [PATCH 2/2] [hist] Implement Scale(double factor) --- hist/histv7/inc/ROOT/RBinWithError.hxx | 7 +++++ hist/histv7/inc/ROOT/RHist.hxx | 11 +++++++ hist/histv7/inc/ROOT/RHistEngine.hxx | 13 ++++++++ hist/histv7/inc/ROOT/RHistStats.hxx | 20 ++++++++++++ hist/histv7/test/hist_engine.cxx | 43 ++++++++++++++++++++++++++ hist/histv7/test/hist_hist.cxx | 26 ++++++++++++++++ hist/histv7/test/hist_stats.cxx | 41 ++++++++++++++++++++++++ hist/histv7/test/hist_user.cxx | 23 ++++++++++++++ 8 files changed, 184 insertions(+) diff --git a/hist/histv7/inc/ROOT/RBinWithError.hxx b/hist/histv7/inc/ROOT/RBinWithError.hxx index ce665497b0d69..34079daf679d7 100644 --- a/hist/histv7/inc/ROOT/RBinWithError.hxx +++ b/hist/histv7/inc/ROOT/RBinWithError.hxx @@ -48,6 +48,13 @@ struct RBinWithError final { return *this; } + RBinWithError &operator*=(double factor) + { + fSum *= factor; + fSum2 *= factor * factor; + return *this; + } + void AtomicInc() { Internal::AtomicInc(&fSum); diff --git a/hist/histv7/inc/ROOT/RHist.hxx b/hist/histv7/inc/ROOT/RHist.hxx index d7740d594f57b..e6f35e352324c 100644 --- a/hist/histv7/inc/ROOT/RHist.hxx +++ b/hist/histv7/inc/ROOT/RHist.hxx @@ -277,6 +277,17 @@ public: fStats.Fill(args...); } + /// Scale all histogram bin contents and statistics. + /// + /// This method is not available for integral bin content types. + /// + /// \param[in] factor the scale factor + void Scale(double factor) + { + fEngine.Scale(factor); + fStats.Scale(factor); + } + /// %ROOT Streamer function to throw when trying to store an object of this class. void Streamer(TBuffer &) { throw std::runtime_error("unable to store RHist"); } }; diff --git a/hist/histv7/inc/ROOT/RHistEngine.hxx b/hist/histv7/inc/ROOT/RHistEngine.hxx index e56ed6877862e..784cd73dc74f6 100644 --- a/hist/histv7/inc/ROOT/RHistEngine.hxx +++ b/hist/histv7/inc/ROOT/RHistEngine.hxx @@ -405,6 +405,19 @@ public: } } + /// Scale all histogram bin contents. + /// + /// This method is not available for integral bin content types. + /// + /// \param[in] factor the scale factor + void Scale(double factor) + { + static_assert(!std::is_integral_v, "scaling is not supported for integral bin content types"); + for (std::size_t i = 0; i < fBinContents.size(); i++) { + fBinContents[i] *= factor; + } + } + /// %ROOT Streamer function to throw when trying to store an object of this class. void Streamer(TBuffer &) { throw std::runtime_error("unable to store RHistEngine"); } }; diff --git a/hist/histv7/inc/ROOT/RHistStats.hxx b/hist/histv7/inc/ROOT/RHistStats.hxx index 4bde3b85fcaec..61a5e6e7fc4c5 100644 --- a/hist/histv7/inc/ROOT/RHistStats.hxx +++ b/hist/histv7/inc/ROOT/RHistStats.hxx @@ -75,6 +75,14 @@ public: fSumWX3 = 0.0; fSumWX4 = 0.0; } + + void Scale(double factor) + { + fSumWX *= factor; + fSumWX2 *= factor; + fSumWX3 *= factor; + fSumWX4 *= factor; + } }; private: @@ -393,6 +401,18 @@ public: } } + /// Scale the histogram statistics. + /// + /// \param[in] factor the scale factor + void Scale(double factor) + { + fSumW *= factor; + fSumW2 *= factor * factor; + for (std::size_t i = 0; i < fDimensionStats.size(); i++) { + fDimensionStats[i].Scale(factor); + } + } + /// %ROOT Streamer function to throw when trying to store an object of this class. void Streamer(TBuffer &) { throw std::runtime_error("unable to store RHistStats"); } }; diff --git a/hist/histv7/test/hist_engine.cxx b/hist/histv7/test/hist_engine.cxx index a16d503cb2f8c..3390e2d7ccb34 100644 --- a/hist/histv7/test/hist_engine.cxx +++ b/hist/histv7/test/hist_engine.cxx @@ -358,6 +358,28 @@ TEST(RHistEngine, FillTupleWeightInvalidNumberOfArguments) EXPECT_THROW(engine2.Fill(std::make_tuple(1, 2, 3), RWeight(1)), std::invalid_argument); } +TEST(RHistEngine, Scale) +{ + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, {0, Bins}); + RHistEngine engine({axis}); + + engine.Fill(-100, RWeight(0.25)); + for (std::size_t i = 0; i < Bins; i++) { + engine.Fill(i, RWeight(0.1 + i * 0.03)); + } + engine.Fill(100, RWeight(0.75)); + + static constexpr double Factor = 0.8; + engine.Scale(Factor); + + EXPECT_FLOAT_EQ(engine.GetBinContent(RBinIndex::Underflow()), Factor * 0.25); + for (auto index : axis.GetNormalRange()) { + EXPECT_FLOAT_EQ(engine.GetBinContent(index), Factor * (0.1 + index.GetIndex() * 0.03)); + } + EXPECT_FLOAT_EQ(engine.GetBinContent(RBinIndex::Overflow()), Factor * 0.75); +} + TEST(RHistEngine_RBinWithError, Add) { static constexpr std::size_t Bins = 20; @@ -415,3 +437,24 @@ TEST(RHistEngine_RBinWithError, FillWeight) EXPECT_FLOAT_EQ(bin.fSum2, weight * weight); } } + +TEST(RHistEngine_RBinWithError, Scale) +{ + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, {0, Bins}); + RHistEngine engine({axis}); + + for (std::size_t i = 0; i < Bins; i++) { + engine.Fill(i, RWeight(0.1 + i * 0.03)); + } + + static constexpr double Factor = 0.8; + engine.Scale(Factor); + + for (auto index : axis.GetNormalRange()) { + auto &bin = engine.GetBinContent(index); + double weight = Factor * (0.1 + index.GetIndex() * 0.03); + EXPECT_FLOAT_EQ(bin.fSum, weight); + EXPECT_FLOAT_EQ(bin.fSum2, weight * weight); + } +} diff --git a/hist/histv7/test/hist_hist.cxx b/hist/histv7/test/hist_hist.cxx index 4f73b5a883a80..e801185a4ae1c 100644 --- a/hist/histv7/test/hist_hist.cxx +++ b/hist/histv7/test/hist_hist.cxx @@ -125,8 +125,34 @@ TEST(RHist, FillWeight) EXPECT_FLOAT_EQ(hist.GetBinContent(indices), 0.9); EXPECT_EQ(hist.GetNEntries(), 2); + EXPECT_FLOAT_EQ(hist.GetStats().GetSumW(), 1.7); + EXPECT_FLOAT_EQ(hist.GetStats().GetSumW2(), 1.45); // Cross-checked with TH1 EXPECT_FLOAT_EQ(hist.ComputeNEffectiveEntries(), 1.9931034); EXPECT_FLOAT_EQ(hist.ComputeMean(), 9.0294118); EXPECT_FLOAT_EQ(hist.ComputeStdDev(), 0.49913420); } + +TEST(RHist, Scale) +{ + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, {0, Bins}); + RHist hist({axis}); + + hist.Fill(8.5, RWeight(0.8)); + hist.Fill(9.5, RWeight(0.9)); + + static constexpr double Factor = 0.8; + hist.Scale(Factor); + + EXPECT_FLOAT_EQ(hist.GetBinContent(8), Factor * 0.8); + EXPECT_FLOAT_EQ(hist.GetBinContent(9), Factor * 0.9); + + EXPECT_EQ(hist.GetNEntries(), 2); + EXPECT_FLOAT_EQ(hist.GetStats().GetSumW(), Factor * 1.7); + EXPECT_FLOAT_EQ(hist.GetStats().GetSumW2(), Factor * Factor * 1.45); + // Cross-checked with TH1 - unchanged compared to FillWeight because the factor cancels out. + EXPECT_FLOAT_EQ(hist.ComputeNEffectiveEntries(), 1.9931034); + EXPECT_FLOAT_EQ(hist.ComputeMean(), 9.0294118); + EXPECT_FLOAT_EQ(hist.ComputeStdDev(), 0.49913420); +} diff --git a/hist/histv7/test/hist_stats.cxx b/hist/histv7/test/hist_stats.cxx index fbcf8902e2e3f..2ac49e52a9ac3 100644 --- a/hist/histv7/test/hist_stats.cxx +++ b/hist/histv7/test/hist_stats.cxx @@ -455,3 +455,44 @@ TEST(RHistStats, FillTupleWeightInvalidNumberOfArguments) EXPECT_NO_THROW(stats2.Fill(std::make_tuple(1, 2), RWeight(1))); EXPECT_THROW(stats2.Fill(std::make_tuple(1, 2, 3), RWeight(1)), std::invalid_argument); } + +TEST(RHistStats, Scale) +{ + RHistStats stats(3); + ASSERT_EQ(stats.GetNEntries(), 0); + + static constexpr std::size_t Entries = 20; + for (std::size_t i = 0; i < Entries; i++) { + stats.Fill(i, 2 * i, i * i, RWeight(0.1 + 0.03 * i)); + } + + static constexpr double Factor = 0.8; + stats.Scale(Factor); + + // The number of entries should not have changed. + EXPECT_EQ(stats.GetNEntries(), Entries); + EXPECT_DOUBLE_EQ(stats.GetSumW(), Factor * 7.7); + EXPECT_DOUBLE_EQ(stats.GetSumW2(), Factor * Factor * 3.563); + + { + const auto &dimensionStats = stats.GetDimensionStats(/*=0*/); + EXPECT_DOUBLE_EQ(dimensionStats.fSumWX, Factor * 93.1); + EXPECT_DOUBLE_EQ(dimensionStats.fSumWX2, Factor * 1330.0); + EXPECT_DOUBLE_EQ(dimensionStats.fSumWX3, Factor * 20489.98); + EXPECT_DOUBLE_EQ(dimensionStats.fSumWX4, Factor * 330265.6); + } + { + const auto &dimensionStats = stats.GetDimensionStats(1); + EXPECT_DOUBLE_EQ(dimensionStats.fSumWX, Factor * 2 * 93.1); + EXPECT_DOUBLE_EQ(dimensionStats.fSumWX2, Factor * 4 * 1330.0); + EXPECT_DOUBLE_EQ(dimensionStats.fSumWX3, Factor * 8 * 20489.98); + EXPECT_DOUBLE_EQ(dimensionStats.fSumWX4, Factor * 16 * 330265.6); + } + { + const auto &dimensionStats = stats.GetDimensionStats(2); + EXPECT_DOUBLE_EQ(dimensionStats.fSumWX, Factor * 1330.0); + EXPECT_DOUBLE_EQ(dimensionStats.fSumWX2, Factor * 330265.6); + EXPECT_DOUBLE_EQ(dimensionStats.fSumWX3, Factor * 93164182.0); + EXPECT_DOUBLE_EQ(dimensionStats.fSumWX4, Factor * 28108731464.8); + } +} diff --git a/hist/histv7/test/hist_user.cxx b/hist/histv7/test/hist_user.cxx index 3d7a37119be07..850488aaaa593 100644 --- a/hist/histv7/test/hist_user.cxx +++ b/hist/histv7/test/hist_user.cxx @@ -31,6 +31,12 @@ struct User { return *this; } + User &operator*=(double factor) + { + fValue *= factor; + return *this; + } + void AtomicInc() { ROOT::Experimental::Internal::AtomicInc(&fValue); } void AtomicAdd(double w) { ROOT::Experimental::Internal::AtomicAdd(&fValue, w); } @@ -153,3 +159,20 @@ TEST(RHistEngineUser, FillAtomicWeight) std::array indices = {9}; EXPECT_EQ(engine.GetBinContent(indices).fValue, 0.9); } + +TEST(RHistEngineUser, Scale) +{ + // Scaling uses operator+=(double) + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, {0, Bins}); + RHistEngine engine({axis}); + + engine.Fill(8.5, RWeight(0.8)); + engine.Fill(9.5, RWeight(0.9)); + + static constexpr double Factor = 0.8; + engine.Scale(Factor); + + EXPECT_EQ(engine.GetBinContent(8).fValue, Factor * 0.8); + EXPECT_EQ(engine.GetBinContent(9).fValue, Factor * 0.9); +}