diff --git a/hist/histv7/inc/ROOT/RAxes.hxx b/hist/histv7/inc/ROOT/RAxes.hxx index 5d2de75752d0e..ab2043fd4fb6b 100644 --- a/hist/histv7/inc/ROOT/RAxes.hxx +++ b/hist/histv7/inc/ROOT/RAxes.hxx @@ -55,6 +55,7 @@ public: const std::vector &Get() const { return fAxes; } friend bool operator==(const RAxes &lhs, const RAxes &rhs) { return lhs.fAxes == rhs.fAxes; } + friend bool operator!=(const RAxes &lhs, const RAxes &rhs) { return !(lhs == rhs); } /// Compute the total number of bins for all axes. /// diff --git a/hist/histv7/inc/ROOT/RHistEngine.hxx b/hist/histv7/inc/ROOT/RHistEngine.hxx index 3de48ef08aec9..3ad23fde793c3 100644 --- a/hist/histv7/inc/ROOT/RHistEngine.hxx +++ b/hist/histv7/inc/ROOT/RHistEngine.hxx @@ -82,11 +82,26 @@ public: { } - // Copy constructor and assignment operator are deleted to avoid surprises. + /// The copy constructor is deleted. + /// + /// Copying all bin contents can be an expensive operation, depending on the number of bins. If required, users can + /// explicitly call Clone(). RHistEngine(const RHistEngine &) = delete; + /// Efficiently move construct a histogram engine. + /// + /// After this operation, the moved-from object is invalid. RHistEngine(RHistEngine &&) = default; + + /// The copy assignment operator is deleted. + /// + /// Copying all bin contents can be an expensive operation, depending on the number of bins. If required, users can + /// explicitly call Clone(). RHistEngine &operator=(const RHistEngine &) = delete; + /// Efficiently move a histogram engine. + /// + /// After this operation, the moved-from object is invalid. RHistEngine &operator=(RHistEngine &&) = default; + ~RHistEngine() = default; const std::vector &GetAxes() const { return fAxes.Get(); } @@ -153,6 +168,43 @@ public: return GetBinContent(indices); } + /// Add all bin contents of another histogram. + /// + /// Throws an exception if the axes configurations are not identical. + /// + /// \param[in] other another histogram + void Add(const RHistEngine &other) + { + if (fAxes != other.fAxes) { + throw std::invalid_argument("axes configurations not identical in Add"); + } + for (std::size_t i = 0; i < fBinContents.size(); i++) { + fBinContents[i] += other.fBinContents[i]; + } + } + + /// Clear all bin contents. + void Clear() + { + for (std::size_t i = 0; i < fBinContents.size(); i++) { + fBinContents[i] = {}; + } + } + + /// Clone this histogram engine. + /// + /// Copying all bin contents can be an expensive operation, depending on the number of bins. + /// + /// \return the cloned object + RHistEngine Clone() const + { + RHistEngine h(fAxes.Get()); + for (std::size_t i = 0; i < fBinContents.size(); i++) { + h.fBinContents[i] = fBinContents[i]; + } + return h; + } + /// Whether this histogram engine type supported weighted filling. static constexpr bool SupportsWeightedFilling = std::is_floating_point_v; diff --git a/hist/histv7/test/hist_engine.cxx b/hist/histv7/test/hist_engine.cxx index 76cc27e93cf41..ab0c04d02f3f2 100644 --- a/hist/histv7/test/hist_engine.cxx +++ b/hist/histv7/test/hist_engine.cxx @@ -63,6 +63,104 @@ TEST(RHistEngine, GetBinContentNotFound) EXPECT_THROW(engine.GetBinContent(Bins), std::invalid_argument); } +TEST(RHistEngine, Add) +{ + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, 0, Bins); + RHistEngine engineA({axis}); + RHistEngine engineB({axis}); + RHistEngine engineC({axis}); + + engineA.Fill(-100); + for (std::size_t i = 0; i < Bins; i++) { + engineA.Fill(i); + engineA.Fill(i); + engineB.Fill(i); + } + engineB.Fill(100); + + engineC.Add(engineA); + engineC.Add(engineB); + + engineA.Add(engineB); + + EXPECT_EQ(engineA.GetBinContent(RBinIndex::Underflow()), 1); + EXPECT_EQ(engineB.GetBinContent(RBinIndex::Underflow()), 0); + EXPECT_EQ(engineC.GetBinContent(RBinIndex::Underflow()), 1); + for (auto index : axis.GetNormalRange()) { + EXPECT_EQ(engineA.GetBinContent(index), 3); + EXPECT_EQ(engineB.GetBinContent(index), 1); + EXPECT_EQ(engineC.GetBinContent(index), 3); + } + EXPECT_EQ(engineA.GetBinContent(RBinIndex::Overflow()), 1); + EXPECT_EQ(engineB.GetBinContent(RBinIndex::Overflow()), 1); + EXPECT_EQ(engineC.GetBinContent(RBinIndex::Overflow()), 1); +} + +TEST(RHistEngine, AddDifferent) +{ + // The equality operators of RAxes and the axis objects are already unit-tested separately, so here we only check one + // case with different the number of bins. + RHistEngine engineA(10, 0, 1); + RHistEngine engineB(20, 0, 1); + + EXPECT_THROW(engineA.Add(engineB), std::invalid_argument); +} + +TEST(RHistEngine, Clear) +{ + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, 0, Bins); + RHistEngine engine({axis}); + + engine.Fill(-100); + for (std::size_t i = 0; i < Bins; i++) { + engine.Fill(i); + } + engine.Fill(100); + + engine.Clear(); + + EXPECT_EQ(engine.GetBinContent(RBinIndex::Underflow()), 0); + for (auto index : axis.GetNormalRange()) { + EXPECT_EQ(engine.GetBinContent(index), 0); + } + EXPECT_EQ(engine.GetBinContent(RBinIndex::Overflow()), 0); +} + +TEST(RHistEngine, Clone) +{ + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, 0, Bins); + RHistEngine engineA({axis}); + + engineA.Fill(-100); + for (std::size_t i = 0; i < Bins; i++) { + engineA.Fill(i); + } + engineA.Fill(100); + + RHistEngine engineB = engineA.Clone(); + ASSERT_EQ(engineB.GetNDimensions(), 1); + ASSERT_EQ(engineB.GetTotalNBins(), Bins + 2); + + EXPECT_EQ(engineB.GetBinContent(RBinIndex::Underflow()), 1); + for (auto index : axis.GetNormalRange()) { + EXPECT_EQ(engineB.GetBinContent(index), 1); + } + EXPECT_EQ(engineB.GetBinContent(RBinIndex::Overflow()), 1); + + // Check that we can continue filling the clone. + for (std::size_t i = 0; i < Bins; i++) { + engineB.Fill(i); + } + + for (auto index : axis.GetNormalRange()) { + EXPECT_EQ(engineA.GetBinContent(index), 1); + EXPECT_EQ(engineB.GetBinContent(index), 2); + } +} + TEST(RHistEngine, Fill) { static constexpr std::size_t Bins = 20;