Skip to content

Commit f38920b

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 f38920b

File tree

6 files changed

+292
-0
lines changed

6 files changed

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