From 6b2ab81ecf8731595a0c205ee17018070dc9c41f Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 22 Apr 2026 11:50:42 -0400 Subject: [PATCH 1/2] feat: support passing a coverage parameter to project This would be useful for boost-histogram, as we could support `flow=False` there directly with this. This uses the existing coverage tag, allowing it to be passed in. Assisted-by: Copilot:Kimi-K2.6 Signed-off-by: Henry Schreiner --- include/boost/histogram/algorithm/project.hpp | 38 +++++++++++-- test/algorithm_project_test.cpp | 54 +++++++++++++++++++ 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/include/boost/histogram/algorithm/project.hpp b/include/boost/histogram/algorithm/project.hpp index 90ad8cd9f..a8e873312 100644 --- a/include/boost/histogram/algorithm/project.hpp +++ b/include/boost/histogram/algorithm/project.hpp @@ -33,9 +33,15 @@ namespace algorithm { Arguments are the source histogram and compile-time numbers, the remaining indices of the axes. Returns a new histogram which only contains the subset of axes. The source histogram is summed over the removed axes. + + @param h source histogram. + @param cov whether to include underflow/overflow bins in the sum. + @param n remaining axis index. + @param ns remaining axis indices. */ template -auto project(const histogram& h, std::integral_constant, Ns...) { +auto project(const histogram& h, coverage cov, std::integral_constant n, + Ns... ns) { using LN = mp11::mp_list, Ns...>; static_assert(mp11::mp_is_set::value, "indices must be unique"); @@ -53,7 +59,7 @@ auto project(const histogram& h, std::integral_constant, Ns.. using A2 = decltype(axes); auto result = histogram(std::move(axes), detail::make_default(old_storage)); auto idx = detail::make_stack_buffer(unsafe_access::axes(result)); - for (auto&& x : indexed(h, coverage::all)) { + for (auto&& x : indexed(h, cov)) { auto i = idx.begin(); mp11::mp_for_each([&i, &x](auto J) { *i++ = x.index(J); }); result.at(idx) += *x; @@ -61,14 +67,28 @@ auto project(const histogram& h, std::integral_constant, Ns.. return result; } +/** + Returns a lower-dimensional histogram, summing over removed axes. + + This overload uses coverage::all by default. +*/ +template +auto project(const histogram& h, std::integral_constant n, Ns... ns) { + return project(h, coverage::all, n, ns...); +} + /** Returns a lower-dimensional histogram, summing over removed axes. This version accepts a source histogram and an iterable range containing the remaining indices. + + @param h source histogram. + @param cov whether to include underflow/overflow bins in the sum. + @param c iterable of remaining axis indices. */ template > -auto project(const histogram& h, const Iterable& c) { +auto project(const histogram& h, coverage cov, const Iterable& c) { using namespace boost::mp11; const auto& old_axes = unsafe_access::axes(h); @@ -88,7 +108,7 @@ auto project(const histogram& h, const Iterable& c) { auto result = histogram(std::move(axes), detail::make_default(old_storage)); auto idx = detail::make_stack_buffer(unsafe_access::axes(result)); - for (auto&& x : indexed(h, coverage::all)) { + for (auto&& x : indexed(h, cov)) { auto i = idx.begin(); for (auto d : c) *i++ = x.index(d); result.at(idx) += *x; @@ -97,6 +117,16 @@ auto project(const histogram& h, const Iterable& c) { return result; } +/** + Returns a lower-dimensional histogram, summing over removed axes. + + This overload uses coverage::all by default. +*/ +template > +auto project(const histogram& h, const Iterable& c) { + return project(h, coverage::all, c); +} + } // namespace algorithm } // namespace histogram } // namespace boost diff --git a/test/algorithm_project_test.cpp b/test/algorithm_project_test.cpp index e9fd3753c..88b997f72 100644 --- a/test/algorithm_project_test.cpp +++ b/test/algorithm_project_test.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -65,6 +66,17 @@ void run_tests() { BOOST_TEST_EQ(hyx.at(0, 1), 1); BOOST_TEST_EQ(hyx.at(1, 1), 1); BOOST_TEST_EQ(hyx.at(2, 1), 2); + + // explicit coverage::all should match default behavior + auto hx_all = project(h, coverage::all, 0_c); + BOOST_TEST_EQ(sum(hx_all), sum(hx)); + + // coverage::inner excludes underflow/overflow; for integer axes with default + // options there are no underflow/overflow bins, so inner gives same result + auto hx_inner = project(h, coverage::inner, 0_c); + BOOST_TEST_EQ(sum(hx_inner), 6); + BOOST_TEST_EQ(hx_inner.at(0), 2); + BOOST_TEST_EQ(hx_inner.at(1), 4); } { @@ -134,6 +146,35 @@ void run_tests() { BOOST_TEST_EQ(h_210.at(2, 0, 0), 1); BOOST_TEST_EQ(h_210.at(2, 0, 1), 1); } + + { + // regular axes have underflow/overflow by default + auto h = make(Tag(), axis::regular<>(2, 0.0, 2.0), axis::regular<>(3, 0.0, 3.0)); + h(-1, -1); // underflow,underflow + h(0.5, 0.5); + h(0.5, 1.5); + h(1.5, 0.5); + h(1.5, 1.5); + h(1.5, 2.5); // overflow on second axis + h(2.5, 0.5); // overflow on first axis + + auto hx_all = project(h, coverage::all, 0_c); + BOOST_TEST_EQ(hx_all.rank(), 1); + // all bins summed: 7 entries total + BOOST_TEST_EQ(sum(hx_all), 7); + // hx has underflow + 2 bins + overflow + BOOST_TEST_EQ(hx_all.at(-1), 1); + BOOST_TEST_EQ(hx_all.at(0), 2); + BOOST_TEST_EQ(hx_all.at(1), 3); + BOOST_TEST_EQ(hx_all.at(2), 1); + + auto hx_inner = project(h, coverage::inner, 0_c); + BOOST_TEST_EQ(hx_inner.rank(), 1); + // only inner bins of original histogram are summed + BOOST_TEST_EQ(sum(hx_inner), 5); + BOOST_TEST_EQ(hx_inner.at(0), 2); + BOOST_TEST_EQ(hx_inner.at(1), 3); + } } // split out dynamic tests as workaround for compiler bug in @@ -179,6 +220,19 @@ void run_dynamic_tests() { BOOST_TEST_EQ(hyx.at(1, 1), 1); BOOST_TEST_EQ(hyx.at(2, 1), 2); + // explicit coverage::all should match default behavior + x = {0}; + auto hx_all = project(h, coverage::all, x); + BOOST_TEST_EQ(sum(hx_all), sum(hx)); + + // coverage::inner excludes underflow/overflow; for integer axes with default + // options there are no underflow/overflow bins, so inner gives same result + x = {0}; + auto hx_inner = project(h, coverage::inner, x); + BOOST_TEST_EQ(sum(hx_inner), 6); + BOOST_TEST_EQ(hx_inner.at(0), 2); + BOOST_TEST_EQ(hx_inner.at(1), 4); + // indices must be unique x = {0, 0}; BOOST_TEST_THROWS((void)project(h, x), std::invalid_argument); From 98d9b038c6479380ee5a439f887f5b5043f61f1d Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 22 Apr 2026 14:16:49 -0400 Subject: [PATCH 2/2] fix: reduce diff with develop a bit Signed-off-by: Henry Schreiner --- include/boost/histogram/algorithm/project.hpp | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/include/boost/histogram/algorithm/project.hpp b/include/boost/histogram/algorithm/project.hpp index a8e873312..bff48f9d0 100644 --- a/include/boost/histogram/algorithm/project.hpp +++ b/include/boost/histogram/algorithm/project.hpp @@ -33,15 +33,10 @@ namespace algorithm { Arguments are the source histogram and compile-time numbers, the remaining indices of the axes. Returns a new histogram which only contains the subset of axes. The source histogram is summed over the removed axes. - - @param h source histogram. - @param cov whether to include underflow/overflow bins in the sum. - @param n remaining axis index. - @param ns remaining axis indices. */ template -auto project(const histogram& h, coverage cov, std::integral_constant n, - Ns... ns) { +auto project(const histogram& h, coverage cov, std::integral_constant, + Ns...) { using LN = mp11::mp_list, Ns...>; static_assert(mp11::mp_is_set::value, "indices must be unique"); @@ -82,10 +77,6 @@ auto project(const histogram& h, std::integral_constant n, Ns This version accepts a source histogram and an iterable range containing the remaining indices. - - @param h source histogram. - @param cov whether to include underflow/overflow bins in the sum. - @param c iterable of remaining axis indices. */ template > auto project(const histogram& h, coverage cov, const Iterable& c) {