Skip to content

Commit 6c4093a

Browse files
committed
[hist] Implement basic operations on RProfile
Add, AddAtomic, Clear, Clone, Scale; implemented similarly to RHist, forwarding to RHistEngine and RHistStats.
1 parent 88fba14 commit 6c4093a

2 files changed

Lines changed: 346 additions & 0 deletions

File tree

hist/histv7/inc/ROOT/RProfile.hxx

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include "RAxisVariant.hxx"
99
#include "RBinIndex.hxx"
10+
#include "RBinIndexMultiDimRange.hxx"
1011
#include "RHistEngine.hxx"
1112
#include "RHistStats.hxx"
1213
#include "RHistUtils.hxx"
@@ -93,6 +94,35 @@ public:
9394
fSum2 += rhs.fWeight * rhs.fWeight;
9495
return *this;
9596
}
97+
98+
RProfileBin &operator+=(const RProfileBin &rhs)
99+
{
100+
fSumValues += rhs.fSumValues;
101+
fSumValues2 += rhs.fSumValues2;
102+
fSum += rhs.fSum;
103+
fSum2 += rhs.fSum2;
104+
return *this;
105+
}
106+
107+
RProfileBin &operator*=(double factor)
108+
{
109+
fSumValues *= factor;
110+
fSumValues2 *= factor;
111+
fSum *= factor;
112+
fSum2 *= factor * factor;
113+
return *this;
114+
}
115+
116+
/// Add another bin content using atomic instructions.
117+
///
118+
/// \param[in] rhs another bin content that must not be modified during the operation
119+
void AtomicAdd(const RProfileBin &rhs)
120+
{
121+
Internal::AtomicAdd(&fSumValues, rhs.fSumValues);
122+
Internal::AtomicAdd(&fSumValues2, rhs.fSumValues2);
123+
Internal::AtomicAdd(&fSum, rhs.fSum);
124+
Internal::AtomicAdd(&fSum2, rhs.fSum2);
125+
}
96126
};
97127

98128
private:
@@ -271,6 +301,66 @@ public:
271301
return fEngine.GetBinContent(args...);
272302
}
273303

304+
/// Get the multidimensional range of all bins.
305+
///
306+
/// \return the multidimensional range
307+
RBinIndexMultiDimRange GetFullMultiDimRange() const { return fEngine.GetFullMultiDimRange(); }
308+
309+
/// Set the content of a single bin.
310+
///
311+
/// \code
312+
/// ROOT::Experimental::RProfile profile({/* two dimensions */});
313+
/// std::array<ROOT::Experimental::RBinIndex, 2> indices = {3, 5};
314+
/// ROOT::Experimental::RProfile::RProfileBin value = /* ... */;
315+
/// profile.SetBinContent(indices, value);
316+
/// \endcode
317+
///
318+
/// \note Compared to TH1 conventions, the first normal bin has index 0 and underflow and overflow bins are special
319+
/// values. See also the class documentation of RBinIndex.
320+
///
321+
/// Throws an exception if the number of indices does not match the axis configuration or the bin is not found.
322+
///
323+
/// \warning Setting the bin content will taint the global histogram statistics. Attempting to access its values, for
324+
/// example calling GetNEntries(), will throw exceptions.
325+
///
326+
/// \param[in] indices the array of indices for each axis
327+
/// \param[in] value the new value of the bin content
328+
/// \par See also
329+
/// the \ref SetBinContent(const A &... args) "variadic function template overload" accepting arguments directly
330+
template <std::size_t N, typename V>
331+
void SetBinContent(const std::array<RBinIndex, N> &indices, const V &value)
332+
{
333+
fEngine.SetBinContent(indices, value);
334+
fStats.Taint();
335+
}
336+
337+
/// Set the content of a single bin.
338+
///
339+
/// \code
340+
/// ROOT::Experimental::RProfile profile({/* two dimensions */});
341+
/// ROOT::Experimental::RProfile::RProfileBin value = /* ... */;
342+
/// profile.SetBinContent(3, 5, value);
343+
/// \endcode
344+
///
345+
/// \note Compared to TH1 conventions, the first normal bin has index 0 and underflow and overflow bins are special
346+
/// values. See also the class documentation of RBinIndex.
347+
///
348+
/// Throws an exception if the number of arguments does not match the axis configuration or the bin is not found.
349+
///
350+
/// \warning Setting the bin content will taint the global histogram statistics. Attempting to access its values, for
351+
/// example calling GetNEntries(), will throw exceptions.
352+
///
353+
/// \param[in] args the arguments for each axis and the new value of the bin content
354+
/// \par See also
355+
/// the \ref SetBinContent(const std::array<RBinIndex, N> &indices, const V &value) "function overload" accepting
356+
/// `std::array`
357+
template <typename... A>
358+
void SetBinContent(const A &...args)
359+
{
360+
fEngine.SetBinContent(args...);
361+
fStats.Taint();
362+
}
363+
274364
/// \}
275365
/// \name Filling
276366
/// \{
@@ -379,6 +469,60 @@ public:
379469
}
380470
}
381471

472+
/// \}
473+
/// \name Operations
474+
/// \{
475+
476+
/// Add all bin contents and statistics of another profile histogram.
477+
///
478+
/// Throws an exception if the axes configurations are not identical.
479+
///
480+
/// \param[in] other another profile histogram
481+
void Add(const RProfile &other)
482+
{
483+
fEngine.Add(other.fEngine);
484+
fStats.Add(other.fStats);
485+
}
486+
487+
/// Add all bin contents and statistics of another profile histogram using atomic instructions.
488+
///
489+
/// Throws an exception if the axes configurations are not identical.
490+
///
491+
/// \param[in] other another profile histogram that must not be modified during the operation
492+
void AddAtomic(const RProfile &other)
493+
{
494+
fEngine.AddAtomic(other.fEngine);
495+
fStats.AddAtomic(other.fStats);
496+
}
497+
498+
/// Clear all bin contents and statistics.
499+
void Clear()
500+
{
501+
fEngine.Clear();
502+
fStats.Clear();
503+
}
504+
505+
/// Clone this profile histogram.
506+
///
507+
/// Copying all bin contents can be an expensive operation, depending on the number of bins.
508+
///
509+
/// \return the cloned object
510+
RProfile Clone() const
511+
{
512+
RProfile profile(fEngine.Clone());
513+
profile.fStats = fStats;
514+
return profile;
515+
}
516+
517+
/// Scale all bin contents and statistics.
518+
///
519+
/// \param[in] factor the scale factor
520+
void Scale(double factor)
521+
{
522+
fEngine.Scale(factor);
523+
fStats.Scale(factor);
524+
}
525+
382526
/// \}
383527

384528
/// %ROOT Streamer function to throw when trying to store an object of this class.

hist/histv7/test/hist_profile.cxx

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,185 @@ TEST(RProfile, Constructor)
5656
EXPECT_EQ(profile.GetNDimensions(), 3);
5757
}
5858

59+
TEST(RProfile, GetFullMultiDimRange)
60+
{
61+
static constexpr std::size_t Bins = 20;
62+
RProfile profile(Bins, {0, Bins});
63+
64+
std::mt19937 gen;
65+
std::uniform_real_distribution<double> distX(0, Bins);
66+
std::uniform_real_distribution<double> distV(Bins, 2 * Bins);
67+
std::uniform_real_distribution<double> distW(0.8, 1.2);
68+
static constexpr std::uint64_t Entries = 1000;
69+
for (std::uint64_t i = 0; i < Entries; i++) {
70+
profile.Fill(distX(gen), distV(gen), RWeight(distW(gen)));
71+
}
72+
ASSERT_EQ(profile.GetNEntries(), Entries);
73+
74+
auto range = profile.GetFullMultiDimRange();
75+
EXPECT_EQ(std::distance(range.begin(), range.end()), Bins + 2);
76+
77+
double sumW = 0;
78+
double sumValues = 0;
79+
for (auto &&indices : range) {
80+
auto &bin = profile.GetBinContent(indices);
81+
sumW += bin.fSum;
82+
sumValues += bin.fSumValues;
83+
}
84+
// Numerical differences with EXPECT_DOUBLE_EQ
85+
EXPECT_FLOAT_EQ(profile.GetStats().GetSumW(), sumW);
86+
EXPECT_FLOAT_EQ(profile.GetStats().GetDimensionStats(1).fSumWX, sumValues);
87+
}
88+
89+
TEST(RProfile, SetBinContent)
90+
{
91+
static constexpr std::size_t Bins = 20;
92+
const RRegularAxis axis(Bins, {0, Bins});
93+
94+
RProfile::RProfileBin bin;
95+
bin.fSumValues = 42;
96+
97+
{
98+
RProfile profile(axis);
99+
ASSERT_FALSE(profile.GetStats().IsTainted());
100+
profile.SetBinContent(RBinIndex(1), bin);
101+
EXPECT_EQ(profile.GetBinContent(RBinIndex(1)).fSumValues, 42);
102+
EXPECT_TRUE(profile.GetStats().IsTainted());
103+
EXPECT_THROW(profile.GetNEntries(), std::logic_error);
104+
}
105+
106+
{
107+
RProfile profile(axis);
108+
ASSERT_FALSE(profile.GetStats().IsTainted());
109+
const std::array<RBinIndex, 1> indices = {2};
110+
profile.SetBinContent(indices, bin);
111+
EXPECT_EQ(profile.GetBinContent(indices).fSumValues, 42);
112+
EXPECT_TRUE(profile.GetStats().IsTainted());
113+
EXPECT_THROW(profile.GetNEntries(), std::logic_error);
114+
}
115+
}
116+
117+
TEST(RProfile, Add)
118+
{
119+
static constexpr std::size_t Bins = 20;
120+
const RRegularAxis axis(Bins, {0, Bins});
121+
RProfile profileA(axis);
122+
RProfile profileB(axis);
123+
124+
profileA.Fill(8.5, 23.0);
125+
profileB.Fill(9.5, 25.0);
126+
127+
profileA.Add(profileB);
128+
129+
EXPECT_EQ(profileA.GetNEntries(), 2);
130+
EXPECT_EQ(profileA.GetBinContent(RBinIndex(8)).fSumValues, 23.0);
131+
EXPECT_EQ(profileA.GetBinContent(RBinIndex(9)).fSumValues, 25.0);
132+
}
133+
134+
TEST(RProfile, AddAtomic)
135+
{
136+
static constexpr std::size_t Bins = 20;
137+
const RRegularAxis axis(Bins, {0, Bins});
138+
RProfile profileA(axis);
139+
RProfile profileB(axis);
140+
141+
profileA.Fill(8.5, 23.0);
142+
profileB.Fill(9.5, 25.0);
143+
144+
profileA.AddAtomic(profileB);
145+
146+
EXPECT_EQ(profileA.GetNEntries(), 2);
147+
EXPECT_EQ(profileA.GetBinContent(RBinIndex(8)).fSumValues, 23.0);
148+
EXPECT_EQ(profileA.GetBinContent(RBinIndex(9)).fSumValues, 25.0);
149+
}
150+
151+
TEST(RProfile, StressAddAtomic)
152+
{
153+
static constexpr std::size_t NThreads = 4;
154+
static constexpr std::size_t NAddsPerThread = 10000;
155+
static constexpr std::size_t NAdds = NThreads * NAddsPerThread;
156+
157+
// Fill a single bin, to maximize contention.
158+
const RRegularAxis axis(1, {0, 1});
159+
RProfile profileA(axis);
160+
RProfile profileB(axis);
161+
profileB.Fill(0.5, 23.0);
162+
163+
StressInParallel(NThreads, [&] {
164+
for (std::size_t i = 0; i < NAddsPerThread; i++) {
165+
profileA.AddAtomic(profileB);
166+
}
167+
});
168+
169+
EXPECT_EQ(profileA.GetNEntries(), NAdds);
170+
EXPECT_EQ(profileA.GetBinContent(0).fSumValues, 23.0 * NAdds);
171+
}
172+
173+
TEST(RProfile, AddExceptionSafety)
174+
{
175+
static constexpr std::size_t Bins = 20;
176+
const RRegularAxis regularAxis(Bins, {0, Bins});
177+
const std::vector<std::string> categories = {"a", "b", "c"};
178+
const RCategoricalAxis categoricalAxis(categories);
179+
180+
RProfile profileA({regularAxis, regularAxis});
181+
RProfile profileB({regularAxis, categoricalAxis});
182+
183+
profileA.Fill(1.5, 2.5, 23.0);
184+
ASSERT_EQ(profileA.GetNEntries(), 1);
185+
ASSERT_EQ(profileA.GetBinContent(RBinIndex(1), RBinIndex(2)).fSumValues, 23.0);
186+
profileB.Fill(1.5, "b", 25.0);
187+
188+
EXPECT_THROW(profileA.Add(profileB), std::invalid_argument);
189+
EXPECT_THROW(profileA.AddAtomic(profileB), std::invalid_argument);
190+
191+
// Verify exception safety. Only the original entry should be there.
192+
EXPECT_EQ(profileA.GetNEntries(), 1);
193+
EXPECT_EQ(profileA.GetBinContent(RBinIndex(1), RBinIndex(2)).fSumValues, 23.0);
194+
EXPECT_EQ(profileA.GetStats().GetSumW(), 1);
195+
EXPECT_EQ(profileA.GetStats().GetSumW2(), 1);
196+
EXPECT_EQ(profileA.GetStats().GetDimensionStats(0).fSumWX, 1.5);
197+
EXPECT_EQ(profileA.GetStats().GetDimensionStats(1).fSumWX, 2.5);
198+
}
199+
200+
TEST(RProfile, Clear)
201+
{
202+
static constexpr std::size_t Bins = 20;
203+
RProfile profile(Bins, {0, Bins});
204+
205+
profile.Fill(8.5, 23.0);
206+
profile.Fill(9.5, 25.0);
207+
208+
profile.Clear();
209+
210+
EXPECT_EQ(profile.GetNEntries(), 0);
211+
EXPECT_EQ(profile.GetBinContent(RBinIndex(8)).fSumValues, 0);
212+
EXPECT_EQ(profile.GetBinContent(RBinIndex(9)).fSumValues, 0);
213+
}
214+
215+
TEST(RProfile, Clone)
216+
{
217+
static constexpr std::size_t Bins = 20;
218+
RProfile profileA(Bins, {0, Bins});
219+
220+
profileA.Fill(8.5, 23.0);
221+
222+
RProfile profileB = profileA.Clone();
223+
ASSERT_EQ(profileB.GetNDimensions(), 1);
224+
ASSERT_EQ(profileB.GetTotalNBins(), Bins + 2);
225+
226+
EXPECT_EQ(profileB.GetNEntries(), 1);
227+
EXPECT_EQ(profileB.GetBinContent(8).fSumValues, 23.0);
228+
229+
// Check that we can continue filling the clone.
230+
profileB.Fill(9.5, 25.0);
231+
232+
EXPECT_EQ(profileA.GetNEntries(), 1);
233+
EXPECT_EQ(profileB.GetNEntries(), 2);
234+
EXPECT_EQ(profileA.GetBinContent(9).fSumValues, 0);
235+
EXPECT_EQ(profileB.GetBinContent(9).fSumValues, 25.0);
236+
}
237+
59238
TEST(RProfile, Fill)
60239
{
61240
static constexpr std::size_t Bins = 20;
@@ -225,3 +404,26 @@ TEST(RProfile, FillExceptionSafety)
225404
EXPECT_EQ(profile.GetStats().GetDimensionStats(1).fSumWX, 2.5);
226405
EXPECT_EQ(profile.GetStats().GetDimensionStats(2).fSumWX, 3.5);
227406
}
407+
408+
TEST(RProfile, Scale)
409+
{
410+
static constexpr std::size_t Bins = 20;
411+
RProfile profile(Bins, {0, Bins});
412+
413+
profile.Fill(8.5, 23.0, RWeight(0.8));
414+
profile.Fill(9.5, 25.0, RWeight(0.9));
415+
416+
static constexpr double Factor = 0.8;
417+
profile.Scale(Factor);
418+
419+
EXPECT_FLOAT_EQ(profile.GetBinContent(8).fSumValues, Factor * 0.8 * 23.0);
420+
EXPECT_FLOAT_EQ(profile.GetBinContent(9).fSumValues, Factor * 0.9 * 25.0);
421+
422+
EXPECT_EQ(profile.GetNEntries(), 2);
423+
EXPECT_FLOAT_EQ(profile.GetStats().GetSumW(), Factor * 1.7);
424+
EXPECT_FLOAT_EQ(profile.GetStats().GetSumW2(), Factor * Factor * 1.45);
425+
// Cross-checked with TH1 - unchanged compared to FillWeight because the factor cancels out.
426+
EXPECT_FLOAT_EQ(profile.ComputeNEffectiveEntries(), 1.9931034);
427+
EXPECT_FLOAT_EQ(profile.ComputeMean(), 9.0294118);
428+
EXPECT_FLOAT_EQ(profile.ComputeStdDev(), 0.49913420);
429+
}

0 commit comments

Comments
 (0)