diff --git a/hist/histv7/CMakeLists.txt b/hist/histv7/CMakeLists.txt index e69de29bb2d1d..4e5918d833f87 100644 --- a/hist/histv7/CMakeLists.txt +++ b/hist/histv7/CMakeLists.txt @@ -0,0 +1,11 @@ +ROOT_STANDARD_LIBRARY_PACKAGE(ROOTHist + HEADERS + ROOT/RLinearizedIndex.hxx + ROOT/RRegularAxis.hxx + ROOT/RVariableBinAxis.hxx + NO_SOURCES + DEPENDENCIES + Core +) + +ROOT_ADD_TEST_SUBDIRECTORY(test) diff --git a/hist/histv7/doc/DesignImplementation.md b/hist/histv7/doc/DesignImplementation.md index 4c6acec92e668..7ccbffb7aaa06 100644 --- a/hist/histv7/doc/DesignImplementation.md +++ b/hist/histv7/doc/DesignImplementation.md @@ -71,3 +71,6 @@ The implementation uses standard [C++17](https://en.cppreference.com/w/cpp/17.ht * No ROOT types, to make sure the histogram package can be compiled standalone. Small objects are passed by value instead of by reference (`RBinIndex`, `RWeight`). + +Complex objects, such as `std::vector`, that have to be copied (for example in a constructor) are also accepted by value. +This allows a single overload that can efficiently take expiring ("moved") objects. diff --git a/hist/histv7/inc/LinkDef.h b/hist/histv7/inc/LinkDef.h new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hist/histv7/inc/ROOT/RLinearizedIndex.hxx b/hist/histv7/inc/ROOT/RLinearizedIndex.hxx new file mode 100644 index 0000000000000..fe10fc1d39cd6 --- /dev/null +++ b/hist/histv7/inc/ROOT/RLinearizedIndex.hxx @@ -0,0 +1,28 @@ +/// \warning This is part of the ROOT 7 prototype! It will change without notice. It might trigger earthquakes. Feedback +/// is welcome! + +#ifndef ROOT_RLinearizedIndex +#define ROOT_RLinearizedIndex + +#include + +namespace ROOT { +namespace Experimental { + +/** +A linearized index that can be invalid. + +For example, when an argument is outside the axis and underflow / overflow bins are disabled. + +\warning This is part of the ROOT 7 prototype! It will change without notice. It might trigger earthquakes. Feedback is +welcome! +*/ +struct RLinearizedIndex final { + std::size_t fIndex; + bool fValid; +}; + +} // namespace Experimental +} // namespace ROOT + +#endif diff --git a/hist/histv7/inc/ROOT/RRegularAxis.hxx b/hist/histv7/inc/ROOT/RRegularAxis.hxx new file mode 100644 index 0000000000000..128b0649469c6 --- /dev/null +++ b/hist/histv7/inc/ROOT/RRegularAxis.hxx @@ -0,0 +1,93 @@ +/// \warning This is part of the ROOT 7 prototype! It will change without notice. It might trigger earthquakes. Feedback +/// is welcome! + +#ifndef ROOT_RRegularAxis +#define ROOT_RRegularAxis + +#include "RLinearizedIndex.hxx" + +#include + +namespace ROOT { +namespace Experimental { + +/** +A regular axis with equidistant bins in the interval \f$[fLow, fHigh)\f$. + +For example, the following creates a regular axis with 10 normal bins between 5 and 15: +~~~ {.cxx} +ROOT::Experimental::RRegularAxis axis(10, 5, 15); +~~~ + +It is possible to disable underflow and overflow bins by passing `enableFlowBins = false`. In that case, arguments +outside the axis will be silently discarded. + +\warning This is part of the ROOT 7 prototype! It will change without notice. It might trigger earthquakes. Feedback is +welcome! +*/ +class RRegularAxis final { + /// The number of normal bins + std::size_t fNumNormalBins; + /// The lower end of the axis interval + double fLow; + /// The upper end of the axis interval + double fHigh; + /// The cached inverse of the bin width to speed up ComputeLinearizedIndex + double fInvBinWidth; //! + /// Whether underflow and overflow bins are enabled + bool fEnableFlowBins; + +public: + /// Construct a regular axis object. + /// + /// \param[in] numNormalBins the number of normal bins + /// \param[in] low the lower end of the axis interval (inclusive) + /// \param[in] high the upper end of the axis interval (exclusive) + /// \param[in] enableFlowBins whether to enable underflow and overflow bins + RRegularAxis(std::size_t numNormalBins, double low, double high, bool enableFlowBins = true) + : fNumNormalBins(numNormalBins), fLow(low), fHigh(high), fEnableFlowBins(enableFlowBins) + { + // FIXME: should validate numNormalBins > 0 and low < high + fInvBinWidth = numNormalBins / (high - low); + } + + std::size_t GetNumNormalBins() const { return fNumNormalBins; } + std::size_t GetTotalNumBins() const { return fEnableFlowBins ? fNumNormalBins + 2 : fNumNormalBins; } + double GetLow() const { return fLow; } + double GetHigh() const { return fHigh; } + bool HasFlowBins() const { return fEnableFlowBins; } + + friend bool operator==(const RRegularAxis &lhs, const RRegularAxis &rhs) + { + return lhs.fNumNormalBins == rhs.fNumNormalBins && lhs.fLow == rhs.fLow && lhs.fHigh == rhs.fHigh && + lhs.fEnableFlowBins == rhs.fEnableFlowBins; + } + + /// Compute the linarized index for a single argument. + /// + /// The normal bins have indices \f$0\f$ to \f$fNumNormalBins - 1\f$, the underflow bin has index + /// \f$fNumNormalBins\f$, and the overflow bin has index \f$fNumNormalBins + 1\f$. If the argument is outside the + /// interval \f$[fLow, fHigh)\f$ and the flow bins are disabled, the return value is invalid. + /// + /// \param[in] x the argument + /// \return the linearized index that may be invalid + RLinearizedIndex ComputeLinearizedIndex(double x) const + { + bool underflow = x < fLow; + // Put NaNs into overflow bin. + bool overflow = !(x < fHigh); + if (underflow) { + return {fNumNormalBins, fEnableFlowBins}; + } else if (overflow) { + return {fNumNormalBins + 1, fEnableFlowBins}; + } + + std::size_t bin = (x - fLow) * fInvBinWidth; + return {bin, true}; + } +}; + +} // namespace Experimental +} // namespace ROOT + +#endif diff --git a/hist/histv7/inc/ROOT/RVariableBinAxis.hxx b/hist/histv7/inc/ROOT/RVariableBinAxis.hxx new file mode 100644 index 0000000000000..0858f9f7dca82 --- /dev/null +++ b/hist/histv7/inc/ROOT/RVariableBinAxis.hxx @@ -0,0 +1,91 @@ +/// \warning This is part of the ROOT 7 prototype! It will change without notice. It might trigger earthquakes. Feedback +/// is welcome! + +#ifndef ROOT_RVariableBinAxis +#define ROOT_RVariableBinAxis + +#include "RLinearizedIndex.hxx" + +#include +#include +#include + +namespace ROOT { +namespace Experimental { + +/** +An axis with variable bins defined by their edges. + +For example, the following creates an axis with 3 log-spaced bins: +~~~ {.cxx} +std::vector binEdges = {1, 10, 100, 1000}; +ROOT::Experimental::RVariableBinAxis axis(binEdges); +~~~ + +It is possible to disable underflow and overflow bins by passing `enableFlowBins = false`. In that case, arguments +outside the axis will be silently discarded. + +\warning This is part of the ROOT 7 prototype! It will change without notice. It might trigger earthquakes. Feedback is +welcome! +*/ +class RVariableBinAxis final { + /// The (ordered) edges of the normal bins + std::vector fBinEdges; + /// Whether underflow and overflow bins are enabled + bool fEnableFlowBins; + +public: + /// Construct an axis object with variable bins. + /// + /// \param[in] binEdges the (ordered) edges of the normal bins + /// \param[in] enableFlowBins whether to enable underflow and overflow bins + RVariableBinAxis(std::vector binEdges, bool enableFlowBins = true) + : fBinEdges(std::move(binEdges)), fEnableFlowBins(enableFlowBins) + { + // FIXME: should validate that fBinEdges is sorted + } + + std::size_t GetNumNormalBins() const { return fBinEdges.size() - 1; } + std::size_t GetTotalNumBins() const { return fEnableFlowBins ? fBinEdges.size() + 1 : fBinEdges.size() - 1; } + const std::vector &GetBinEdges() const { return fBinEdges; } + bool HasFlowBins() const { return fEnableFlowBins; } + + friend bool operator==(const RVariableBinAxis &lhs, const RVariableBinAxis &rhs) + { + return lhs.fBinEdges == rhs.fBinEdges && lhs.fEnableFlowBins == rhs.fEnableFlowBins; + } + + /// Compute the linarized index for a single argument. + /// + /// The normal bins have indices \f$0\f$ to \f$fBinEdges.size() - 2\f$, the underflow bin has index + /// \f$fBinEdges.size() - 1\f$, and the overflow bin has index \f$fBinEdges.size()\f$. If the argument is outside all + /// bin edges and the flow bins are disabled, the return value is invalid. + /// + /// \param[in] x the argument + /// \return the linearized index that may be invalid + RLinearizedIndex ComputeLinearizedIndex(double x) const + { + bool underflow = x < fBinEdges.front(); + // Put NaNs into overflow bin. + bool overflow = !(x < fBinEdges.back()); + if (underflow) { + return {fBinEdges.size() - 1, fEnableFlowBins}; + } else if (overflow) { + return {fBinEdges.size(), fEnableFlowBins}; + } + + // TODO (for later): The following can be optimized with binary search... + for (std::size_t bin = 0; bin < fBinEdges.size() - 2; bin++) { + if (x < fBinEdges[bin + 1]) { + return {bin, true}; + } + } + std::size_t bin = fBinEdges.size() - 2; + return {bin, true}; + } +}; + +} // namespace Experimental +} // namespace ROOT + +#endif diff --git a/hist/histv7/test/CMakeLists.txt b/hist/histv7/test/CMakeLists.txt new file mode 100644 index 0000000000000..b89bb983c5127 --- /dev/null +++ b/hist/histv7/test/CMakeLists.txt @@ -0,0 +1,2 @@ +ROOT_ADD_GTEST(hist_regular hist_regular.cxx LIBRARIES ROOTHist) +ROOT_ADD_GTEST(hist_variable hist_variable.cxx LIBRARIES ROOTHist) diff --git a/hist/histv7/test/hist_regular.cxx b/hist/histv7/test/hist_regular.cxx new file mode 100644 index 0000000000000..7c28c1816653f --- /dev/null +++ b/hist/histv7/test/hist_regular.cxx @@ -0,0 +1,89 @@ +#include "hist_test.hxx" + +#include + +TEST(RRegularAxis, Constructor) +{ + static constexpr std::size_t Bins = 20; + RRegularAxis axis(Bins, 0, Bins); + EXPECT_EQ(axis.GetNumNormalBins(), Bins); + EXPECT_EQ(axis.GetTotalNumBins(), Bins + 2); + EXPECT_EQ(axis.GetLow(), 0); + EXPECT_EQ(axis.GetHigh(), Bins); + EXPECT_TRUE(axis.HasFlowBins()); + + axis = RRegularAxis(Bins, 0, Bins, /*enableFlowBins=*/false); + EXPECT_EQ(axis.GetNumNormalBins(), Bins); + EXPECT_EQ(axis.GetTotalNumBins(), Bins); + EXPECT_FALSE(axis.HasFlowBins()); +} + +TEST(RRegularAxis, Equality) +{ + static constexpr std::size_t Bins = 20; + const RRegularAxis axisA(Bins, 0, Bins); + const RRegularAxis axisANoFlowBins(Bins, 0, Bins, /*enableFlowBins=*/false); + const RRegularAxis axisA2(Bins, 0, Bins); + const RRegularAxis axisB(Bins / 2, 0, Bins); + const RRegularAxis axisC(Bins, 0, Bins / 2); + const RRegularAxis axisD(Bins, Bins / 2, Bins); + + EXPECT_TRUE(axisA == axisA); + EXPECT_TRUE(axisA == axisA2); + EXPECT_TRUE(axisA2 == axisA); + + EXPECT_FALSE(axisA == axisANoFlowBins); + + EXPECT_FALSE(axisA == axisB); + EXPECT_FALSE(axisA == axisC); + EXPECT_FALSE(axisA == axisD); + + EXPECT_FALSE(axisB == axisC); + EXPECT_FALSE(axisB == axisD); + + EXPECT_FALSE(axisC == axisD); + EXPECT_FALSE(axisD == axisC); +} + +TEST(RRegularAxis, ComputeLinearizedIndex) +{ + static constexpr std::size_t Bins = 20; + const RRegularAxis axis(Bins, 0, Bins); + const RRegularAxis axisNoFlowBins(Bins, 0, Bins, /*enableFlowBins=*/false); + + // Underflow + static constexpr double NegativeInfinity = -std::numeric_limits::infinity(); + static constexpr double UnderflowLarge = -static_cast(Bins); + static constexpr double UnderflowSmall = -0.1; + for (double underflow : {NegativeInfinity, UnderflowLarge, UnderflowSmall}) { + auto linIndex = axis.ComputeLinearizedIndex(underflow); + EXPECT_EQ(linIndex.fIndex, Bins); + EXPECT_TRUE(linIndex.fValid); + linIndex = axisNoFlowBins.ComputeLinearizedIndex(underflow); + EXPECT_EQ(linIndex.fIndex, Bins); + EXPECT_FALSE(linIndex.fValid); + } + + for (std::size_t i = 0; i < Bins; i++) { + auto linIndex = axis.ComputeLinearizedIndex(i + 0.5); + EXPECT_EQ(linIndex.fIndex, i); + EXPECT_TRUE(linIndex.fValid); + linIndex = axisNoFlowBins.ComputeLinearizedIndex(i + 0.5); + EXPECT_EQ(linIndex.fIndex, i); + EXPECT_TRUE(linIndex.fValid); + } + + // Overflow + static constexpr double PositiveInfinity = std::numeric_limits::infinity(); + static constexpr double NaN = std::numeric_limits::quiet_NaN(); + static constexpr double OverflowLarge = static_cast(Bins * 2); + static constexpr double OverflowSmall = Bins + 0.1; + for (double overflow : {PositiveInfinity, NaN, OverflowLarge, OverflowSmall}) { + auto linIndex = axis.ComputeLinearizedIndex(overflow); + EXPECT_EQ(linIndex.fIndex, Bins + 1); + EXPECT_TRUE(linIndex.fValid); + linIndex = axisNoFlowBins.ComputeLinearizedIndex(overflow); + EXPECT_EQ(linIndex.fIndex, Bins + 1); + EXPECT_FALSE(linIndex.fValid); + } +} diff --git a/hist/histv7/test/hist_test.hxx b/hist/histv7/test/hist_test.hxx new file mode 100644 index 0000000000000..c36ba8a722432 --- /dev/null +++ b/hist/histv7/test/hist_test.hxx @@ -0,0 +1,12 @@ +#ifndef hist_test +#define hist_test + +#include +#include + +#include "gtest/gtest.h" + +using ROOT::Experimental::RRegularAxis; +using ROOT::Experimental::RVariableBinAxis; + +#endif diff --git a/hist/histv7/test/hist_variable.cxx b/hist/histv7/test/hist_variable.cxx new file mode 100644 index 0000000000000..1b4782c2249f8 --- /dev/null +++ b/hist/histv7/test/hist_variable.cxx @@ -0,0 +1,111 @@ +#include "hist_test.hxx" + +#include +#include + +TEST(RVariableBinAxis, Constructor) +{ + static constexpr std::size_t Bins = 20; + std::vector bins; + for (std::size_t i = 0; i < Bins; i++) { + bins.push_back(i); + } + bins.push_back(Bins); + + RVariableBinAxis axis(bins); + EXPECT_EQ(axis.GetNumNormalBins(), Bins); + EXPECT_EQ(axis.GetTotalNumBins(), Bins + 2); + EXPECT_TRUE(axis.HasFlowBins()); + + axis = RVariableBinAxis(bins, /*enableFlowBins=*/false); + EXPECT_EQ(axis.GetNumNormalBins(), Bins); + EXPECT_EQ(axis.GetTotalNumBins(), Bins); + EXPECT_FALSE(axis.HasFlowBins()); +} + +TEST(RVariableBinAxis, Equality) +{ + static constexpr std::size_t Bins = 20; + std::vector binsA; + for (std::size_t i = 0; i < Bins; i++) { + binsA.push_back(i); + } + binsA.push_back(Bins); + + std::vector binsB; + for (std::size_t i = 0; i < Bins / 2; i++) { + binsB.push_back(i); + } + binsB.push_back(Bins / 2); + + std::vector binsC; + for (std::size_t i = Bins / 2; i < Bins; i++) { + binsC.push_back(i); + } + binsC.push_back(Bins); + + const RVariableBinAxis axisA(binsA); + const RVariableBinAxis axisANoFlowBins(binsA, /*enableFlowBins=*/false); + const RVariableBinAxis axisA2(binsA); + const RVariableBinAxis axisB(binsB); + const RVariableBinAxis axisC(binsC); + + EXPECT_TRUE(axisA == axisA); + EXPECT_TRUE(axisA == axisA2); + EXPECT_TRUE(axisA2 == axisA); + + EXPECT_FALSE(axisA == axisANoFlowBins); + + EXPECT_FALSE(axisA == axisB); + EXPECT_FALSE(axisA == axisC); + EXPECT_FALSE(axisB == axisC); +} + +TEST(RVariableBinAxis, ComputeLinearizedIndex) +{ + static constexpr std::size_t Bins = 20; + std::vector bins; + for (std::size_t i = 0; i < Bins; i++) { + bins.push_back(i); + } + bins.push_back(Bins); + + const RVariableBinAxis axis(bins); + const RVariableBinAxis axisNoFlowBins(bins, /*enableFlowBins=*/false); + + // Underflow + static constexpr double NegativeInfinity = -std::numeric_limits::infinity(); + static constexpr double UnderflowLarge = -static_cast(Bins); + static constexpr double UnderflowSmall = -0.1; + for (double underflow : {NegativeInfinity, UnderflowLarge, UnderflowSmall}) { + auto linIndex = axis.ComputeLinearizedIndex(underflow); + EXPECT_EQ(linIndex.fIndex, Bins); + EXPECT_TRUE(linIndex.fValid); + linIndex = axisNoFlowBins.ComputeLinearizedIndex(underflow); + EXPECT_EQ(linIndex.fIndex, Bins); + EXPECT_FALSE(linIndex.fValid); + } + + for (std::size_t i = 0; i < Bins; i++) { + auto linIndex = axis.ComputeLinearizedIndex(i + 0.5); + EXPECT_EQ(linIndex.fIndex, i); + EXPECT_TRUE(linIndex.fValid); + linIndex = axisNoFlowBins.ComputeLinearizedIndex(i + 0.5); + EXPECT_EQ(linIndex.fIndex, i); + EXPECT_TRUE(linIndex.fValid); + } + + // Overflow + static constexpr double PositiveInfinity = std::numeric_limits::infinity(); + static constexpr double NaN = std::numeric_limits::quiet_NaN(); + static constexpr double OverflowLarge = static_cast(Bins * 2); + static constexpr double OverflowSmall = Bins + 0.1; + for (double overflow : {PositiveInfinity, NaN, OverflowLarge, OverflowSmall}) { + auto linIndex = axis.ComputeLinearizedIndex(overflow); + EXPECT_EQ(linIndex.fIndex, Bins + 1); + EXPECT_TRUE(linIndex.fValid); + linIndex = axisNoFlowBins.ComputeLinearizedIndex(overflow); + EXPECT_EQ(linIndex.fIndex, Bins + 1); + EXPECT_FALSE(linIndex.fValid); + } +}