Skip to content

Commit 59594e2

Browse files
committed
[hist] Implement initial RHistAutoAxisFiller
To fill a regular one-dimensional histogram without specifying an axis interval during construction.
1 parent aa9ced9 commit 59594e2

File tree

6 files changed

+289
-0
lines changed

6 files changed

+289
-0
lines changed

hist/histv7/doc/CodeArchitecture.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,9 @@ Objects of this type are passed by value; most notably to `GetBinContent` and `S
7474

7575
A range of `RBinIndex` from `begin` (inclusive) to `end` (exclusive).
7676
The class exposes an iterator interface that can be used in range-based loops.
77+
78+
### `RHistAutoAxisFiller`
79+
80+
A specialized class to automatically determine the axis interval during filling.
81+
It constructs a regular axis based on the minimum and maximum values of the initial entries.
82+
The implementation is currently restricted to one dimension and sequential filling.

hist/histv7/headers.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ set(histv7_headers
55
ROOT/RBinWithError.hxx
66
ROOT/RCategoricalAxis.hxx
77
ROOT/RHist.hxx
8+
ROOT/RHistAutoAxisFiller.hxx
89
ROOT/RHistEngine.hxx
910
ROOT/RHistStats.hxx
1011
ROOT/RHistUtils.hxx
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/// \file
2+
/// \warning This is part of the %ROOT 7 prototype! It will change without notice. It might trigger earthquakes.
3+
/// Feedback is welcome!
4+
5+
#ifndef ROOT_RHistAutoAxisFiller
6+
#define ROOT_RHistAutoAxisFiller
7+
8+
#include "RHist.hxx"
9+
10+
#include <algorithm>
11+
#include <cmath>
12+
#include <cstddef>
13+
#include <limits>
14+
#include <optional>
15+
#include <stdexcept>
16+
#include <utility>
17+
#include <vector>
18+
19+
namespace ROOT {
20+
namespace Experimental {
21+
22+
/**
23+
A histogram filler that automatically determines the axis interval.
24+
25+
This class allows filling a regular one-dimensional histogram without specifying an axis interval during construction.
26+
After a configurable number of buffered entries, or upon request, a RRegularAxis is constructed using the minimum and
27+
maximum values until that point. This ensures all initial entries are filled into normal bins. Note that this cannot be
28+
guaranteed for further calls to Fill.
29+
30+
\code
31+
ROOT::Experimental::RHistAutoAxisFiller<int> filler(20);
32+
filler.Fill(1.0);
33+
filler.Fill(1.5);
34+
filler.Fill(2.0);
35+
36+
// The following will implicitly trigger the histogram creation
37+
auto &hist = filler.GetHist();
38+
// hist.GetNEntries() will return 3
39+
\endcode
40+
41+
\warning This is part of the %ROOT 7 prototype! It will change without notice. It might trigger earthquakes.
42+
Feedback is welcome!
43+
*/
44+
template <typename BinContentType>
45+
class RHistAutoAxisFiller final {
46+
/// The filled histogram, after it has been constructed
47+
std::optional<RHist<BinContentType>> fHist;
48+
49+
/// The number of normal bins
50+
std::size_t fNNormalBins;
51+
/// The maximum buffer size until Flush() is automatically called
52+
std::size_t fMaxBufferSize;
53+
54+
/// The buffer of filled entries
55+
// FIXME: need to store weights!
56+
std::vector<double> fBuffer;
57+
/// The minimum of the filled entries
58+
double fMinimum = std::numeric_limits<double>::infinity();
59+
/// The maximum of the filled entries
60+
double fMaximum = -std::numeric_limits<double>::infinity();
61+
62+
public:
63+
/// Create a filler object.
64+
///
65+
/// \param[in] nNormalBins the number of normal bins, must be > 0
66+
/// \param[in] maxBufferSize the maximum buffer size, must be > 0
67+
explicit RHistAutoAxisFiller(std::size_t nNormalBins, std::size_t maxBufferSize = 1024)
68+
: fNNormalBins(nNormalBins), fMaxBufferSize(maxBufferSize)
69+
{
70+
if (nNormalBins == 0) {
71+
throw std::invalid_argument("nNormalBins must be > 0");
72+
}
73+
if (maxBufferSize == 0) {
74+
throw std::invalid_argument("maxBufferSize must be > 0");
75+
}
76+
}
77+
78+
std::size_t GetNNormalBins() const { return fNNormalBins; }
79+
std::size_t GetMaxBufferSize() const { return fMaxBufferSize; }
80+
81+
/// Fill an entry into the histogram.
82+
///
83+
/// \param[in] x the argument
84+
void Fill(double x)
85+
{
86+
// If the histogram exists, forward the Fill call.
87+
if (fHist) {
88+
fHist->Fill(x);
89+
return;
90+
}
91+
92+
fBuffer.push_back(x);
93+
fMinimum = std::min(fMinimum, x);
94+
fMaximum = std::max(fMaximum, x);
95+
96+
if (fBuffer.size() >= fMaxBufferSize) {
97+
Flush();
98+
}
99+
}
100+
101+
/// Flush the buffer of entries and construct the histogram.
102+
///
103+
/// Throws an exception if the buffer is empty, the axis interval cannot be determined, or if it would be empty
104+
/// because the minimum equals the maximum.
105+
void Flush()
106+
{
107+
if (fHist) {
108+
assert(fBuffer.empty() && "buffer should have been emptied");
109+
return;
110+
}
111+
112+
if (fBuffer.empty()) {
113+
throw std::runtime_error("buffer is empty, cannot create histogram");
114+
}
115+
if (!std::isfinite(fMinimum) || !std::isfinite(fMaximum)) {
116+
throw std::runtime_error("could not determine axis interval");
117+
}
118+
if (fMinimum == fMaximum) {
119+
throw std::runtime_error("axis interval is empty");
120+
}
121+
122+
// Slightly increase the upper limit to make sure the maximum is included in the last bin.
123+
double high = std::nextafter(fMaximum, std::numeric_limits<double>::infinity());
124+
assert(high > fMaximum);
125+
fHist.emplace(fNNormalBins, std::make_pair(fMinimum, high));
126+
127+
for (double x : fBuffer) {
128+
fHist->Fill(x);
129+
}
130+
fBuffer.clear();
131+
}
132+
133+
/// Return the constructed histogram.
134+
///
135+
/// \see Flush()
136+
RHist<BinContentType> &GetHist()
137+
{
138+
Flush();
139+
assert(fHist.has_value());
140+
return *fHist;
141+
}
142+
};
143+
144+
} // namespace Experimental
145+
} // namespace ROOT
146+
147+
#endif

hist/histv7/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
HIST_ADD_GTEST(hist_auto hist_auto.cxx)
12
HIST_ADD_GTEST(hist_axes hist_axes.cxx)
23
HIST_ADD_GTEST(hist_categorical hist_categorical.cxx)
34
HIST_ADD_GTEST(hist_engine hist_engine.cxx)

hist/histv7/test/hist_auto.cxx

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#include "hist_test.hxx"
2+
3+
#include <limits>
4+
#include <stdexcept>
5+
#include <utility>
6+
7+
TEST(RHistAutoAxisFiller, Constructor)
8+
{
9+
static constexpr std::size_t Bins = 20;
10+
RHistAutoAxisFiller<int> filler(Bins);
11+
EXPECT_EQ(filler.GetNNormalBins(), Bins);
12+
EXPECT_EQ(filler.GetMaxBufferSize(), 1024);
13+
14+
EXPECT_THROW(RHistAutoAxisFiller<int>(0), std::invalid_argument);
15+
EXPECT_THROW(RHistAutoAxisFiller<int>(1, 0), std::invalid_argument);
16+
}
17+
18+
TEST(RHistAutoAxisFiller, Fill)
19+
{
20+
static constexpr std::size_t Bins = 20;
21+
RHistAutoAxisFiller<int> filler(Bins);
22+
23+
// Fill some entries
24+
for (std::size_t i = 0; i < Bins; i++) {
25+
filler.Fill(i);
26+
}
27+
28+
// NaN should be ignored for the axis interval
29+
filler.Fill(std::numeric_limits<double>::quiet_NaN());
30+
31+
// Get the histogram, which first flushes the buffer
32+
auto &hist = filler.GetHist();
33+
auto &axis = std::get<RRegularAxis>(hist.GetAxes()[0]);
34+
EXPECT_EQ(axis.GetNNormalBins(), Bins);
35+
EXPECT_TRUE(axis.HasFlowBins());
36+
EXPECT_DOUBLE_EQ(axis.GetLow(), 0);
37+
EXPECT_DOUBLE_EQ(axis.GetHigh(), Bins - 1);
38+
39+
EXPECT_EQ(hist.GetNEntries(), Bins + 1);
40+
EXPECT_EQ(hist.GetBinContent(RBinIndex::Underflow()), 0);
41+
for (auto index : axis.GetNormalRange()) {
42+
EXPECT_EQ(hist.GetBinContent(index), 1);
43+
}
44+
// The NaN entry
45+
EXPECT_EQ(hist.GetBinContent(RBinIndex::Overflow()), 1);
46+
47+
// Fill some more entries that are now directly forwarded to the histogram
48+
for (std::size_t i = 0; i < Bins; i++) {
49+
filler.Fill(i);
50+
}
51+
for (auto index : axis.GetNormalRange()) {
52+
EXPECT_EQ(hist.GetBinContent(index), 2);
53+
}
54+
}
55+
56+
TEST(RHistAutoAxisFiller, FillAutoFlush)
57+
{
58+
static constexpr std::size_t Bins = 20;
59+
RHistAutoAxisFiller<int> filler(Bins);
60+
61+
// Fill entries so that it triggers auto-flushing
62+
for (std::size_t i = 0; i < 1024; i++) {
63+
filler.Fill(i);
64+
}
65+
66+
// Further entries may land in the flow bins
67+
filler.Fill(-1);
68+
filler.Fill(2000);
69+
70+
auto &hist = filler.GetHist();
71+
EXPECT_EQ(hist.GetBinContent(RBinIndex::Underflow()), 1);
72+
EXPECT_EQ(hist.GetBinContent(RBinIndex::Overflow()), 1);
73+
}
74+
75+
TEST(RHistAutoAxisFiller, FillMax0)
76+
{
77+
static constexpr std::size_t Bins = 20;
78+
RHistAutoAxisFiller<int> filler(Bins);
79+
80+
filler.Fill(-1);
81+
filler.Fill(0);
82+
83+
auto &hist = filler.GetHist();
84+
EXPECT_EQ(hist.GetBinContent(RBinIndex::Underflow()), 0);
85+
EXPECT_EQ(hist.GetBinContent(RBinIndex::Overflow()), 0);
86+
}
87+
88+
TEST(RHistAutoAxisFiller, FlushError)
89+
{
90+
static constexpr std::size_t Bins = 20;
91+
92+
{
93+
RHistAutoAxisFiller<int> filler(Bins);
94+
// Flush without entries
95+
EXPECT_THROW(filler.Flush(), std::runtime_error);
96+
}
97+
98+
{
99+
RHistAutoAxisFiller<int> filler(Bins);
100+
// NaN should be ignored for the axis interval
101+
filler.Fill(std::numeric_limits<double>::quiet_NaN());
102+
EXPECT_THROW(filler.Flush(), std::runtime_error);
103+
}
104+
105+
{
106+
RHistAutoAxisFiller<int> filler(Bins);
107+
// Fill with infinities
108+
filler.Fill(std::numeric_limits<double>::infinity());
109+
filler.Fill(-std::numeric_limits<double>::infinity());
110+
EXPECT_THROW(filler.Flush(), std::runtime_error);
111+
}
112+
113+
{
114+
RHistAutoAxisFiller<int> filler(Bins);
115+
// Fill with identical values
116+
filler.Fill(1);
117+
filler.Fill(1);
118+
EXPECT_THROW(filler.Flush(), std::runtime_error);
119+
}
120+
}
121+
122+
TEST(RHistAutoAxisFiller, GetHist)
123+
{
124+
static constexpr std::size_t Bins = 20;
125+
RHistAutoAxisFiller<int> filler(Bins);
126+
127+
filler.Fill(0);
128+
filler.Fill(1);
129+
130+
// The histogram can be moved out of the filler that constructed it.
131+
RHist<int> hist(std::move(filler.GetHist()));
132+
}

hist/histv7/test/hist_test.hxx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <ROOT/RBinWithError.hxx>
88
#include <ROOT/RCategoricalAxis.hxx>
99
#include <ROOT/RHist.hxx>
10+
#include <ROOT/RHistAutoAxisFiller.hxx>
1011
#include <ROOT/RHistEngine.hxx>
1112
#include <ROOT/RHistStats.hxx>
1213
#include <ROOT/RRegularAxis.hxx>
@@ -21,6 +22,7 @@ using ROOT::Experimental::RBinIndexRange;
2122
using ROOT::Experimental::RBinWithError;
2223
using ROOT::Experimental::RCategoricalAxis;
2324
using ROOT::Experimental::RHist;
25+
using ROOT::Experimental::RHistAutoAxisFiller;
2426
using ROOT::Experimental::RHistEngine;
2527
using ROOT::Experimental::RHistStats;
2628
using ROOT::Experimental::RRegularAxis;

0 commit comments

Comments
 (0)