diff --git a/tree/dataframe/CMakeLists.txt b/tree/dataframe/CMakeLists.txt index 828da568d4cea..01ddbc3e3774e 100644 --- a/tree/dataframe/CMakeLists.txt +++ b/tree/dataframe/CMakeLists.txt @@ -26,6 +26,10 @@ if (imt) list(APPEND RDATAFRAME_EXTRA_DEPS Imt) endif(imt) +if (root7) + list(APPEND RDATAFRAME_EXTRA_DEPS ROOTHist) +endif() + set (EXTRA_DICT_OPTS) if (runtime_cxxmodules AND WIN32) set (EXTRA_DICT_OPTS NO_CXXMODULE) diff --git a/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx b/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx index 0c892b00b9297..2145d918f51d3 100644 --- a/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx +++ b/tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx @@ -32,6 +32,13 @@ #include "ROOT/RDF/RActionImpl.hxx" #include "ROOT/RDF/RMergeableValue.hxx" +#include "RConfigure.h" // for R__HAS_ROOT7 +#ifdef R__HAS_ROOT7 +#include +#include +#include +#endif + #include #include #include @@ -40,6 +47,7 @@ #include #include #include +#include #include #include // std::index_sequence #include @@ -469,6 +477,66 @@ public: } }; +#ifdef R__HAS_ROOT7 +template +class R__CLING_PTRCHECK(off) RHistFillHelper + : public ROOT::Detail::RDF::RActionImpl> { +public: + using Result_t = ROOT::Experimental::RHist; + +private: + std::unique_ptr> fFiller; + std::vector>> fContexts; + +public: + RHistFillHelper(std::shared_ptr> h, unsigned int nSlots) + : fFiller(new ROOT::Experimental::RHistConcurrentFiller(h)), fContexts(nSlots) + { + for (unsigned int i = 0; i < nSlots; i++) { + fContexts[i] = fFiller->CreateFillContext(); + } + } + RHistFillHelper(const RHistFillHelper &) = delete; + RHistFillHelper(RHistFillHelper &&) = default; + RHistFillHelper &operator=(const RHistFillHelper &) = delete; + RHistFillHelper &operator=(RHistFillHelper &&) = default; + ~RHistFillHelper() = default; + + std::shared_ptr GetResultPtr() const { return fFiller.GetHist(); } + + void Initialize() {} + void InitTask(TTreeReader *, unsigned int) {} + + template + void ExecWithWeight(unsigned int slot, const std::tuple &columnValues, std::index_sequence) + { + std::tuple args{std::get(columnValues)...}; + ROOT::Experimental::RWeight weight(std::get(columnValues)); + fContexts[slot]->Fill(args, weight); + } + + template + void Exec(unsigned int slot, const ColumnTypes &...columnValues) + { + if constexpr (WithWeight) { + auto t = std::forward_as_tuple(columnValues...); + ExecWithWeight(slot, t, std::make_index_sequence()); + } else { + fContexts[slot]->Fill(columnValues...); + } + } + + void Finalize() + { + for (auto &&context : fContexts) { + context->Flush(); + } + } + + std::string GetActionName() { return "Hist"; } +}; +#endif + class R__CLING_PTRCHECK(off) FillTGraphHelper : public ROOT::Detail::RDF::RActionImpl { public: using Result_t = ::TGraph; diff --git a/tree/dataframe/inc/ROOT/RDF/InterfaceUtils.hxx b/tree/dataframe/inc/ROOT/RDF/InterfaceUtils.hxx index acfe71b5d9f12..04a79ad7ce3a0 100644 --- a/tree/dataframe/inc/ROOT/RDF/InterfaceUtils.hxx +++ b/tree/dataframe/inc/ROOT/RDF/InterfaceUtils.hxx @@ -28,6 +28,7 @@ #include #include #include +#include // for R__HAS_ROOT7 #include // gErrorIgnoreLevel #include #include // IsImplicitMTEnabled @@ -90,6 +91,8 @@ struct Histo2D{}; struct Histo3D{}; struct HistoND{}; struct HistoNSparseD{}; +struct Hist{}; +struct HistWithWeight{}; struct Graph{}; struct GraphAsymmErrors{}; struct Profile1D{}; @@ -171,6 +174,32 @@ BuildAction(const ColumnNames_t &bl, const std::shared_ptr &h, } } +#ifdef R__HAS_ROOT7 +// Action for RHist using RHistConcurrentFiller +template +std::unique_ptr +BuildAction(const ColumnNames_t &columnList, const std::shared_ptr> &h, + const unsigned int nSlots, std::shared_ptr prevNode, ActionTags::Hist, + const RColumnRegister &colRegister) +{ + using Helper_t = RHistFillHelper; + using Action_t = RAction>; + return std::make_unique(Helper_t(h, nSlots), columnList, std::move(prevNode), colRegister); +} + +// Action for RHist using RHistConcurrentFiller +template +std::unique_ptr +BuildAction(const ColumnNames_t &columnList, const std::shared_ptr> &h, + const unsigned int nSlots, std::shared_ptr prevNode, ActionTags::HistWithWeight, + const RColumnRegister &colRegister) +{ + using Helper_t = RHistFillHelper; + using Action_t = RAction>; + return std::make_unique(Helper_t(h, nSlots), columnList, std::move(prevNode), colRegister); +} +#endif + template std::unique_ptr BuildAction(const ColumnNames_t &bl, const std::shared_ptr &g, const unsigned int nSlots, diff --git a/tree/dataframe/inc/ROOT/RDF/RInterface.hxx b/tree/dataframe/inc/ROOT/RDF/RInterface.hxx index 0025a2ff7df0a..e508c947749c7 100644 --- a/tree/dataframe/inc/ROOT/RDF/RInterface.hxx +++ b/tree/dataframe/inc/ROOT/RDF/RInterface.hxx @@ -45,6 +45,13 @@ #include "TProfile2D.h" #include "TStatistic.h" +#include "RConfigure.h" // for R__HAS_ROOT7 +#ifdef R__HAS_ROOT7 +#include +#include +#include +#endif + #include #include #include @@ -2357,6 +2364,142 @@ public: columnList, h, h, fProxiedPtr, columnList.size()); } +#ifdef R__HAS_ROOT7 + //////////////////////////////////////////////////////////////////////////// + /// \brief Fill and return an RHist (*lazy action*). + /// \tparam BinContentType The bin content type of the returned RHist. + /// \param[in] axes The returned histogram will be constructed using these axes. + /// \param[in] columnList A list containing the names of the columns that will be passed when calling `Fill` + /// \return the histogram wrapped in a RResultPtr. + /// + /// This action is *lazy*: upon invocation of this method the calculation is + /// booked but not executed. Also see RResultPtr. + /// + /// ### Example usage: + /// ~~~{.cpp} + /// ROOT::Experimental::RRegularAxis axis(10, {5.0, 15.0}); + /// auto myHist = myDf.Hist({axis}, {"col0"}); + /// ~~~ + template + RResultPtr> + Hist(std::vector axes, const ColumnNames_t &columnList) + { + std::shared_ptr h = std::make_shared>(std::move(axes)); + if (h->GetNDimensions() != columnList.size()) { + throw std::runtime_error("Wrong number of columns for the specified number of histogram axes."); + } + + return Hist(h, columnList); + } + + //////////////////////////////////////////////////////////////////////////// + /// \brief Fill the provided RHist (*lazy action*). + /// \param[in] h The histogram that should be filled. + /// \param[in] columnList A list containing the names of the columns that will be passed when calling `Fill` + /// \return the histogram wrapped in a RResultPtr. + /// + /// This action is *lazy*: upon invocation of this method the calculation is + /// booked but not executed. Also see RResultPtr. + /// + /// During execution of the computation graph, the passed histogram must only be accessed with methods that are + /// allowed during concurrent filling. + /// + /// ### Example usage: + /// ~~~{.cpp} + /// auto h = std::make_shared>(10, {5.0, 15.0}); + /// auto myHist = myDf.Hist(h, {"col0"}); + /// ~~~ + template + RResultPtr> + Hist(std::shared_ptr> h, const ColumnNames_t &columnList) + { + RDFInternal::WarnHist(); + + if (h->GetNDimensions() != columnList.size()) { + throw std::runtime_error("Wrong number of columns for the passed histogram."); + } + + return CreateAction(columnList, h, h, fProxiedPtr, + columnList.size()); + } + + //////////////////////////////////////////////////////////////////////////// + /// \brief Fill and return an RHist with weights (*lazy action*). + /// \tparam BinContentType The bin content type of the returned RHist. + /// \param[in] axes The returned histogram will be constructed using these axes. + /// \param[in] columnList A list containing the names of the columns that will be passed when calling `Fill` + /// \param[in] wName The name of the column that will provide the weights. + /// \return the histogram wrapped in a RResultPtr. + /// + /// This action is *lazy*: upon invocation of this method the calculation is + /// booked but not executed. Also see RResultPtr. + /// + /// This overload is not available for integral bin content types (see \ref RHistEngine::SupportsWeightedFilling). + /// + /// ### Example usage: + /// ~~~{.cpp} + /// ROOT::Experimental::RRegularAxis axis(10, {5.0, 15.0}); + /// auto myHist = myDf.Hist({axis}, {"col0"}, "colW"); + /// ~~~ + template + RResultPtr> + Hist(std::vector axes, const ColumnNames_t &columnList, std::string_view wName) + { + static_assert(ROOT::Experimental::RHistEngine::SupportsWeightedFilling, + "weighted filling is not supported for integral bin content types"); + + std::shared_ptr h = std::make_shared>(std::move(axes)); + if (h->GetNDimensions() != columnList.size()) { + throw std::runtime_error("Wrong number of columns for the specified number of histogram axes."); + } + + return Hist(h, columnList, wName); + } + + //////////////////////////////////////////////////////////////////////////// + /// \brief Fill the provided RHist with weights (*lazy action*). + /// \param[in] h The histogram that should be filled. + /// \param[in] columnList A list containing the names of the columns that will be passed when calling `Fill` + /// \param[in] wName The name of the column that will provide the weights. + /// \return the histogram wrapped in a RResultPtr. + /// + /// This action is *lazy*: upon invocation of this method the calculation is + /// booked but not executed. Also see RResultPtr. + /// + /// This overload is not available for integral bin content types (see \ref RHistEngine::SupportsWeightedFilling). + /// + /// During execution of the computation graph, the passed histogram must only be accessed with methods that are + /// allowed during concurrent filling. + /// + /// ### Example usage: + /// ~~~{.cpp} + /// auto h = std::make_shared>(10, {5.0, 15.0}); + /// auto myHist = myDf.Hist(h, {"col0"}, "colW"); + /// ~~~ + template + RResultPtr> + Hist(std::shared_ptr> h, const ColumnNames_t &columnList, + std::string_view wName) + { + static_assert(ROOT::Experimental::RHistEngine::SupportsWeightedFilling, + "weighted filling is not supported for integral bin content types"); + + RDFInternal::WarnHist(); + + if (h->GetNDimensions() != columnList.size()) { + throw std::runtime_error("Wrong number of columns for the passed histogram."); + } + + // Add the weight column to the list of argument columns to pass it through the infrastructure. + ColumnNames_t columnListWithWeights(columnList); + columnListWithWeights.push_back(std::string(wName)); + + return CreateAction( + columnListWithWeights, h, h, fProxiedPtr, columnListWithWeights.size()); + } +#endif + //////////////////////////////////////////////////////////////////////////// /// \brief Fill and return a TGraph object (*lazy action*). /// \tparam X The type of the column used to fill the x axis. diff --git a/tree/dataframe/inc/ROOT/RDF/Utils.hxx b/tree/dataframe/inc/ROOT/RDF/Utils.hxx index 795f4545b2e1d..62a8b76fb1cc5 100644 --- a/tree/dataframe/inc/ROOT/RDF/Utils.hxx +++ b/tree/dataframe/inc/ROOT/RDF/Utils.hxx @@ -65,6 +65,9 @@ struct RInferredType { namespace Internal { namespace RDF { +/// Warn once about experimental filling of RHist. +void WarnHist(); + using namespace ROOT::TypeTraits; using namespace ROOT::Detail::RDF; using namespace ROOT::RDF; diff --git a/tree/dataframe/src/RDFUtils.cxx b/tree/dataframe/src/RDFUtils.cxx index 2639e281f8ec0..4ead72d824c29 100644 --- a/tree/dataframe/src/RDFUtils.cxx +++ b/tree/dataframe/src/RDFUtils.cxx @@ -29,6 +29,7 @@ #include "TTree.h" #include +#include #include // nlohmann::json::parse #include #include @@ -45,6 +46,18 @@ ROOT::RLogChannel &ROOT::Detail::RDF::RDFLogChannel() return c; } +// A static function, not in an anonymous namespace, because the function name is included in the user-visible message. +static void WarnHist() +{ + R__LOG_WARNING(RDFLogChannel()) << "Filling RHist is experimental and still under development."; +} + +void ROOT::Internal::RDF::WarnHist() +{ + static std::once_flag once; + std::call_once(once, ::WarnHist); +} + namespace { using TypeInfoRef = std::reference_wrapper; struct TypeInfoRefHash { diff --git a/tree/dataframe/test/CMakeLists.txt b/tree/dataframe/test/CMakeLists.txt index aad10aab36b54..fc791d3249e56 100644 --- a/tree/dataframe/test/CMakeLists.txt +++ b/tree/dataframe/test/CMakeLists.txt @@ -139,6 +139,10 @@ if (imt) ROOT_ADD_GTEST(dataframe_concurrency dataframe_concurrency.cxx LIBRARIES ROOTDataFrame) endif() +if (root7) + ROOT_ADD_GTEST(dataframe_hist dataframe_hist.cxx LIBRARIES ROOTDataFrame ROOTHist) +endif() + if(ARROW_FOUND) ROOT_ADD_GTEST(datasource_arrow datasource_arrow.cxx LIBRARIES ROOTDataFrame ${ARROW_SHARED_LIB}) target_include_directories(datasource_arrow BEFORE PRIVATE ${ARROW_INCLUDE_DIR}) diff --git a/tree/dataframe/test/dataframe_hist.cxx b/tree/dataframe/test/dataframe_hist.cxx new file mode 100644 index 0000000000000..ff1a28e50f419 --- /dev/null +++ b/tree/dataframe/test/dataframe_hist.cxx @@ -0,0 +1,274 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using ROOT::RDataFrame; +using ROOT::Experimental::RBinWithError; +using ROOT::Experimental::RHist; +using ROOT::Experimental::RRegularAxis; +using ROOT::Experimental::RVariableBinAxis; +using ROOT::RDF::RunGraphs; + +// Fixture for all tests in this file to optionally run with multi-threading. +class RDFHist : public ::testing::TestWithParam { + ROOT::TestSupport::CheckDiagsRAII fDiag; + +public: + RDFHist() + { + fDiag.optionalDiag(kWarning, "", "Filling RHist is experimental", /*matchFullMessage=*/false); + if (GetParam()) + ROOT::EnableImplicitMT(4); + } + ~RDFHist() override + { + if (GetParam()) + ROOT::DisableImplicitMT(); + } +}; + +TEST_P(RDFHist, Regular) +{ + RDataFrame df(10); + const RRegularAxis axis(10, {5.0, 15.0}); + auto hist = df.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}) + .Hist({axis}, {"x"}); + EXPECT_EQ(hist->GetNEntries(), 10); + for (auto index : axis.GetNormalRange()) { + EXPECT_EQ(hist->GetBinContent(index), 1.0); + } +} + +TEST_P(RDFHist, RegularJit) +{ + RDataFrame df(10); + const RRegularAxis axis(10, {5.0, 15.0}); + auto hist = df.Define("x", "rdfentry_ + 5.5").Hist({axis}, {"x"}); + EXPECT_EQ(hist->GetNEntries(), 10); + for (auto index : axis.GetNormalRange()) { + EXPECT_EQ(hist->GetBinContent(index), 1.0); + } +} + +TEST_P(RDFHist, MultiDim) +{ + RDataFrame df(10); + const RRegularAxis regularAxis(10, {5.0, 15.0}); + static constexpr std::size_t BinsY = 20; + std::vector bins; + for (std::size_t i = 0; i < BinsY; i++) { + bins.push_back(i); + } + bins.push_back(BinsY); + const RVariableBinAxis variableBinAxis(bins); + + auto hist = df.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}) + .Define("y", [](ULong64_t e) { return 2 * e + 0.5; }, {"rdfentry_"}) + .Hist({regularAxis, variableBinAxis}, {"x", "y"}); + EXPECT_EQ(hist->GetNEntries(), 10); + for (auto x : regularAxis.GetNormalRange()) { + for (auto y : variableBinAxis.GetNormalRange()) { + if (2 * x.GetIndex() == y.GetIndex()) { + EXPECT_EQ(hist->GetBinContent(x, y), 1.0); + } else { + EXPECT_EQ(hist->GetBinContent(x, y), 0.0); + } + } + } +} + +TEST_P(RDFHist, MultiDimJit) +{ + RDataFrame df(10); + const RRegularAxis regularAxis(10, {5.0, 15.0}); + static constexpr std::size_t BinsY = 20; + std::vector bins; + for (std::size_t i = 0; i < BinsY; i++) { + bins.push_back(i); + } + bins.push_back(BinsY); + const RVariableBinAxis variableBinAxis(bins); + + auto hist = df.Define("x", "rdfentry_ + 5.5") + .Define("y", "2 * rdfentry_ + 0.5") + .Hist({regularAxis, variableBinAxis}, {"x", "y"}); + EXPECT_EQ(hist->GetNEntries(), 10); + for (auto x : regularAxis.GetNormalRange()) { + for (auto y : variableBinAxis.GetNormalRange()) { + if (2 * x.GetIndex() == y.GetIndex()) { + EXPECT_EQ(hist->GetBinContent(x, y), 1.0); + } else { + EXPECT_EQ(hist->GetBinContent(x, y), 0.0); + } + } + } +} + +TEST_P(RDFHist, Ptr) +{ + RDataFrame df(10); + auto hist = std::make_shared>(10, std::make_pair(5.0, 15.0)); + auto resPtr = df.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}).Hist(hist, {"x"}); + EXPECT_EQ(hist, resPtr.GetSharedPtr()); + EXPECT_EQ(hist->GetNEntries(), 10); +} + +TEST_P(RDFHist, PtrJit) +{ + RDataFrame df(10); + auto hist = std::make_shared>(10, std::make_pair(5.0, 15.0)); + auto resPtr = df.Define("x", "rdfentry_ + 5.5").Hist(hist, {"x"}); + EXPECT_EQ(hist, resPtr.GetSharedPtr()); + EXPECT_EQ(hist->GetNEntries(), 10); +} + +TEST_P(RDFHist, PtrRunGraphs) +{ + auto hist = std::make_shared>(10, std::make_pair(5.0, 15.0)); + + RDataFrame df1(10); + auto resPtr1 = df1.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}).Hist(hist, {"x"}); + + RDataFrame df2(7); + auto resPtr2 = df2.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}).Hist(hist, {"x"}); + + RunGraphs({resPtr1, resPtr2}); + EXPECT_EQ(hist->GetNEntries(), 17); +} + +TEST_P(RDFHist, InvalidNumberOfArguments) +{ + RDataFrame df(10); + const RRegularAxis axis(10, {5.0, 15.0}); + auto dfX = df.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}); + try { + // Cannot use EXPECT_THROW because of template arguments... + dfX.Hist({axis}, {"x", "x"}); + FAIL() << "expected std::runtime_error"; + } catch (const std::runtime_error &e) { + // expected + } + + auto hist = std::make_shared>(10, std::make_pair(5.0, 15.0)); + try { + // Cannot use EXPECT_THROW because of template arguments... + dfX.Hist(hist, {"x", "x"}); + FAIL() << "expected std::runtime_error"; + } catch (const std::runtime_error &e) { + // expected + } +} + +TEST_P(RDFHist, InvalidNumberOfArgumentsJit) +{ + RDataFrame df(10); + const RRegularAxis axis(10, {5.0, 15.0}); + auto dfX = df.Define("x", "rdfentry_ + 5.5"); + EXPECT_THROW(dfX.Hist({axis}, {"x", "x"}), std::runtime_error); + + auto hist = std::make_shared>(10, std::make_pair(5.0, 15.0)); + EXPECT_THROW(dfX.Hist(hist, {"x", "x"}), std::runtime_error); +} + +TEST_P(RDFHist, Weight) +{ + RDataFrame df(10); + const RRegularAxis axis(10, {5.0, 15.0}); + auto hist = df.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}) + .Define("w", [](ULong64_t e) { return 0.1 + e * 0.03; }, {"rdfentry_"}) + .Hist({axis}, {"x"}, "w"); + EXPECT_EQ(hist->GetNEntries(), 10); + for (auto index : axis.GetNormalRange()) { + auto &bin = hist->GetBinContent(index); + double weight = 0.1 + index.GetIndex() * 0.03; + EXPECT_FLOAT_EQ(bin.fSum, weight); + EXPECT_FLOAT_EQ(bin.fSum2, weight * weight); + } +} + +TEST_P(RDFHist, WeightJit) +{ + RDataFrame df(10); + const RRegularAxis axis(10, {5.0, 15.0}); + auto hist = df.Define("x", "rdfentry_ + 5.5").Define("w", "0.1 + rdfentry_ * 0.03").Hist({axis}, {"x"}, "w"); + EXPECT_EQ(hist->GetNEntries(), 10); + for (auto index : axis.GetNormalRange()) { + auto &bin = hist->GetBinContent(index); + double weight = 0.1 + index.GetIndex() * 0.03; + EXPECT_FLOAT_EQ(bin.fSum, weight); + EXPECT_FLOAT_EQ(bin.fSum2, weight * weight); + } +} + +TEST_P(RDFHist, PtrWeight) +{ + RDataFrame df(10); + auto hist = std::make_shared>(10, std::make_pair(5.0, 15.0)); + auto resPtr = df.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}) + .Define("w", [](ULong64_t e) { return 0.1 + e * 0.03; }, {"rdfentry_"}) + .Hist(hist, {"x"}, "w"); + EXPECT_EQ(hist, resPtr.GetSharedPtr()); + EXPECT_EQ(hist->GetNEntries(), 10); +} + +TEST_P(RDFHist, PtrWeightJit) +{ + RDataFrame df(10); + auto hist = std::make_shared>(10, std::make_pair(5.0, 15.0)); + auto resPtr = df.Define("x", "rdfentry_ + 5.5").Define("w", "0.1 + rdfentry_ * 0.03").Hist(hist, {"x"}, "w"); + EXPECT_EQ(hist, resPtr.GetSharedPtr()); + EXPECT_EQ(hist->GetNEntries(), 10); +} + +TEST_P(RDFHist, WeightInvalidNumberOfArguments) +{ + RDataFrame df(10); + const RRegularAxis axis(10, {5.0, 15.0}); + auto dfXW = df.Define("x", [](ULong64_t e) { return e + 5.5; }, {"rdfentry_"}) + .Define("w", [](ULong64_t e) { return 0.1 + e * 0.03; }, {"rdfentry_"}); + try { + // Cannot use EXPECT_THROW because of template arguments... + dfXW.Hist({axis}, {"x", "x"}, "w"); + FAIL() << "expected std::runtime_error"; + } catch (const std::runtime_error &e) { + // expected + } + + auto hist = std::make_shared>(10, std::make_pair(5.0, 15.0)); + try { + // Cannot use EXPECT_THROW because of template arguments... + dfXW.Hist(hist, {"x", "x"}, "w"); + FAIL() << "expected std::runtime_error"; + } catch (const std::runtime_error &e) { + // expected + } +} + +TEST_P(RDFHist, WeightInvalidNumberOfArgumentsJit) +{ + RDataFrame df(10); + const RRegularAxis axis(10, {5.0, 15.0}); + auto dfXW = df.Define("x", "rdfentry_ + 5.5").Define("w", "0.1 + rdfentry_ * 0.03"); + EXPECT_THROW(dfXW.Hist({axis}, {"x", "x"}, "w"), std::runtime_error); + + auto hist = std::make_shared>(10, std::make_pair(5.0, 15.0)); + EXPECT_THROW(dfXW.Hist(hist, {"x", "x"}, "w"), std::runtime_error); +} + +INSTANTIATE_TEST_SUITE_P(Seq, RDFHist, ::testing::Values(false)); + +#ifdef R__USE_IMT +INSTANTIATE_TEST_SUITE_P(MT, RDFHist, ::testing::Values(true)); +#endif