Skip to content
Merged
8 changes: 8 additions & 0 deletions doc/concepts/Axis.qbk
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ An [*Axis] maps input values to indices. It holds state specific to that axis, l
* `a` and `b` are values of type `A`
* `i` and `j` are indices of type [headerref boost/histogram/fwd.hpp `boost::histogram::axis::index_type`]
* `n` is a value of type `unsigned`
* `b` and `e` are values of type `const boost::histogram::axis::index_type*` which form the index range `[b, e)`
* `M` is a metadata type that is [@https://en.cppreference.com/w/cpp/named_req/DefaultConstructible DefaultConstructible], [@https://en.cppreference.com/w/cpp/named_req/CopyConstructible CopyConstructible] and [@https://en.cppreference.com/w/cpp/named_req/CopyAssignable CopyAssignable]. It it supports moves, it must be *nothrow* [@https://en.cppreference.com/w/cpp/named_req/MoveAssignable MoveAssignable].
* `ar` is a value of an archive with Boost.Serialization semantics

Expand All @@ -72,6 +73,13 @@ An [*Axis] maps input values to indices. It holds state specific to that axis, l
Special constructor used by the reduce algorithm. `a` is the original axis instance, `i` and `j` are the index range to keep in the reduced axis. If `n` is larger than 1, `n` adjacent bins are merged into one larger cell. If this constructor is not implemented, [funcref boost::histogram::algorithm::reduce] throws an exception on an attempt to reduce this axis.
]
]
[
[`A(a, b, e)`]
[]
[
Special constructor used by the reduce algorithm to handle the pick command. `a` is the original axis instance, `b` and `e` delimit a range with the indices of the bins to keep, in the order in which they should appear in the new axis. Must only be implemented for axes which are not ordered, like the category axis. If this constructor is not implemented, [funcref boost::histogram::algorithm::reduce] throws an exception on an attempt to pick bins from this axis.
]
]
[
[`a.options()`]
[`unsigned`]
Expand Down
4 changes: 2 additions & 2 deletions doc/guide.qbk
Original file line number Diff line number Diff line change
Expand Up @@ -293,9 +293,9 @@ The library provides the [funcref boost::histogram::algorithm::project] function

[section Reduction]

A projection removes an axis completely. A less drastic way to obtain a smaller histogram is the [funcref boost::histogram::algorithm::reduce reduce] function, which allows one to /slice/, /shrink/ or /rebin/ individual axes.
A projection removes an axis completely. A less drastic way to obtain a smaller histogram is the [funcref boost::histogram::algorithm::reduce reduce] function, which allows one to /slice/, /shrink/, /pick/ or /rebin/ individual axes.

Shrinking means that the value range of an axis is reduced and the number of bins along that axis. Slicing does the same, but is based on axis indices while shrinking is based on the axis values. To /rebin/ means that adjacent bins are merged into larger bins, the histogram is made coarser. For N adjacent bins, a new bin is formed which covers the common interval of the merged bins and has their added content. These two operations can be combined and applied to several axes at once. Doing it in one step is much more efficient than doing it in several steps.
Shrinking means that the value range of an axis is reduced and the number of bins along that axis. Slicing does the same, but is based on axis indices while shrinking is based on the axis values. To /rebin/ means that adjacent bins are merged into larger bins, the histogram is made coarser. For N adjacent bins, a new bin is formed which covers the common interval of the merged bins and has their added content. Picking selects an arbitrary subset of bins by index from an axis which is not ordered, like the category axis. Unlike a slice, the picked bins do not have to be adjacent. Shrinking or slicing can be combined with rebinning and applied to several axes at once; picking cannot be combined with the other operations on the same axis. Doing it in one step is much more efficient than doing it in several steps.

The [funcref boost::histogram::algorithm::reduce reduce] function does not change the total count if all modified axes in the histogram have underflow and overflow bins. Counts in removed bins are added to the corresponding under- and overflow bins. As in case of the [funcref boost::histogram::algorithm::project project] function, such a histogram is guaranteed to be identical to one obtained from filling the original data.

Expand Down
18 changes: 18 additions & 0 deletions examples/guide_histogram_reduction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@

#include <boost/histogram.hpp>
#include <cassert>
#include <string>

int main() {
using namespace boost::histogram;
// import reduce commands into local namespace to save typing
using algorithm::pick;
using algorithm::rebin;
using algorithm::shrink;
using algorithm::slice;
Expand Down Expand Up @@ -43,6 +45,22 @@ int main() {

assert(h3.axis(0) == h.axis(0)); // unchanged
assert(h3.axis(1) == axis::regular<>(2, 0.0, 2.0));

// pick selects an arbitrary subset of bins from an axis which is not ordered, like
// the category axis; unlike a slice, the picked bins do not have to be adjacent
auto h4 = make_histogram(axis::category<std::string>({"red", "green", "blue"}));

h4("red");
h4("green");
h4("blue");

// pick the bins for "blue" and "red", in that order
auto h5 = algorithm::reduce(h4, pick({2, 0}));

assert(h5.axis(0) == axis::category<std::string>({"blue", "red"}));
assert(h5.at(0) == 1 && h5.at(1) == 1);
// the count for "green" was moved to the overflow bin of the category axis
assert(h5.at(2) == 1);
}

//]
115 changes: 99 additions & 16 deletions include/boost/histogram/algorithm/reduce.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#ifndef BOOST_HISTOGRAM_ALGORITHM_REDUCE_HPP
#define BOOST_HISTOGRAM_ALGORITHM_REDUCE_HPP

#include <algorithm>
#include <boost/histogram/axis/traits.hpp>
#include <boost/histogram/detail/axes.hpp>
#include <boost/histogram/detail/make_default.hpp>
Expand All @@ -21,6 +22,8 @@
#include <initializer_list>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>

namespace boost {
namespace histogram {
Expand Down Expand Up @@ -316,23 +319,74 @@ inline reduce_command slice_and_rebin(axis::index_type begin, axis::index_type e
return slice_and_rebin(reduce_command::unset, begin, end, merge, mode);
}

/** Shrink, crop, slice, and/or rebin axes of a histogram.
/** Pick command to be used in `reduce`.
Command is applied to axis with given index.
Picking selects an arbitrary subset of bins by index. The new axis consists of the
picked bins in the order in which the indices are given, which may differ from their
order in the original axis. In contrast to `slice`, the picked bins do not have to be
adjacent. Each index must be valid and may only appear once.
Picking only works on axes that are not ordered, like the category axis, since
removing an arbitrary subset of bins from an ordered axis would create gaps in the
axis range. The counts in bins that were not picked are added to the overflow bin,
if it is present. If it is not present, the counts are discarded. In crop mode, the
counts in unpicked bins and in the original overflow bin are always discarded.
@param iaxis which axis to operate on.
@param indices indices of the bins to keep, must be unique.
@param mode whether to behave like `shrink` or `crop` regarding removed bins.
*/
inline reduce_command pick(unsigned iaxis, std::vector<axis::index_type> indices,
slice_mode mode = slice_mode::shrink) {
if (indices.empty())
BOOST_THROW_EXCEPTION(std::invalid_argument("at least one index required"));
for (auto it = indices.begin(); it != indices.end(); ++it)
if (std::find(indices.begin(), it, *it) != it)
BOOST_THROW_EXCEPTION(std::invalid_argument("indices must be unique"));
reduce_command r;
r.iaxis = iaxis;
r.range = reduce_command::range_t::indices_list;
r.indices = std::move(indices);
r.merge = 1;
r.crop = mode == slice_mode::crop;
return r;
}

/** Pick command to be used in `reduce`.
Command is applied to corresponding axis in order of reduce arguments.
Picking selects an arbitrary subset of bins by index, see
pick(unsigned, std::vector<axis::index_type>, slice_mode) for details.
@param indices indices of the bins to keep, must be unique.
@param mode whether to behave like `shrink` or `crop` regarding removed bins.
*/
inline reduce_command pick(std::vector<axis::index_type> indices,
slice_mode mode = slice_mode::shrink) {
return pick(reduce_command::unset, std::move(indices), mode);
}

/** Shrink, crop, slice, pick, and/or rebin axes of a histogram.
Returns a new reduced histogram and leaves the original histogram untouched.
The commands `rebin` and `shrink` or `slice` for the same axis are
automatically combined, this is not an error. Passing a `shrink` and a `slice`
command for the same axis or two `rebin` commands triggers an `invalid_argument`
exception. Trying to reducing a non-reducible axis triggers an `invalid_argument`
exception. The `pick` command cannot be combined with any other command for the
same axis. Trying to reducing a non-reducible axis triggers an `invalid_argument`
exception. Histograms with non-reducible axes can still be reduced along the
other axes that are reducible.
An overload allows one to pass reduce_command as positional arguments.
@param hist original histogram.
@param options iterable sequence of reduce commands: `shrink`, `slice`, `rebin`,
`shrink_and_rebin`, or `slice_and_rebin`. The element type of the iterable should be
`reduce_command`.
`pick`, `shrink_and_rebin`, or `slice_and_rebin`. The element type of the iterable
should be `reduce_command`.
*/
template <class Histogram, class Iterable, class = detail::requires_iterable<Iterable>>
Histogram reduce(const Histogram& hist, const Iterable& options) {
Expand All @@ -351,6 +405,32 @@ Histogram reduce(const Histogram& hist, const Iterable& options) {
if (o.merge > 0) { // option is set?
o.use_underflow_bin = AO::test(axis::option::underflow);
o.use_overflow_bin = AO::test(axis::option::overflow);
if (o.range == reduce_command::range_t::indices_list) {
for (const auto idx : o.indices)
if (idx < 0 || idx >= a_in.size())
BOOST_THROW_EXCEPTION(std::invalid_argument("index out of range"));
// crop discards counts of unpicked bins instead of moving them to overflow
if (o.crop) o.use_overflow_bin = false;
return detail::static_if_c<axis::traits::has_pick_constructor<A>::value>(
[&o](const auto& a_in) {
static_assert(!axis::traits::is_ordered<A>::value,
"pick requires an unordered axis");
// lut maps old index to new index; unpicked bins and the
// overflow bin map to the overflow bin of the new axis
o.reduced_end = static_cast<index_type>(o.indices.size());
o.lut.assign(static_cast<std::size_t>(a_in.size()) + 1,
o.reduced_end);
for (index_type k = 0; k < o.reduced_end; ++k)
o.lut[o.indices[k]] = k;
return std::decay_t<decltype(a_in)>(
a_in, o.indices.data(), o.indices.data() + o.indices.size());
},
[iaxis](const auto& a_in) -> std::decay_t<decltype(a_in)> {
BOOST_THROW_EXCEPTION(std::invalid_argument(
"axis " + std::to_string(iaxis) + " is not pickable"));
},
a_in);
}
return detail::static_if_c<axis::traits::is_reducible<A>::value>(
[&o](const auto& a_in) {
if (o.range == reduce_command::range_t::none) {
Expand Down Expand Up @@ -415,19 +495,21 @@ Histogram reduce(const Histogram& hist, const Iterable& options) {
bool skip = false;

for (auto j : x.indices()) {
*i = (j - o->begin.index);
if (o->is_ordered && *i <= -1) {
*i = -1;
if (!o->use_underflow_bin) skip = true;
if (o->range == reduce_command::range_t::indices_list) {
*i = o->lut[j];
} else {
if (*i >= 0)
*i = (j - o->begin.index);
if (o->is_ordered && *i <= -1) {
*i = -1;
if (!o->use_underflow_bin) skip = true;
} else if (*i >= 0)
*i /= static_cast<index_type>(o->merge);
else
*i = o->reduced_end;
if (*i >= o->reduced_end) {
*i = o->reduced_end;
if (!o->use_overflow_bin) skip = true;
}
}
if (*i >= o->reduced_end) {
*i = o->reduced_end;
if (!o->use_overflow_bin) skip = true;
}

++i;
Expand All @@ -440,21 +522,22 @@ Histogram reduce(const Histogram& hist, const Iterable& options) {
return result;
}

/** Shrink, slice, and/or rebin axes of a histogram.
/** Shrink, crop, slice, pick, and/or rebin axes of a histogram.
Returns a new reduced histogram and leaves the original histogram untouched.
The commands `rebin` and `shrink` or `slice` for the same axis are
automatically combined, this is not an error. Passing a `shrink` and a `slice`
command for the same axis or two `rebin` commands triggers an invalid_argument
exception. It is safe to reduce histograms with some axis that are not reducible along
exception. The `pick` command cannot be combined with any other command for the
same axis. It is safe to reduce histograms with some axis that are not reducible along
the other axes. Trying to reducing a non-reducible axis triggers an invalid_argument
exception.
An overload allows one to pass an iterable of reduce_command.
@param hist original histogram.
@param opt first reduce command; one of `shrink`, `slice`, `rebin`,
@param opt first reduce command; one of `shrink`, `slice`, `rebin`, `pick`,
`shrink_and_rebin`, or `slice_or_rebin`.
@param opts more reduce commands.
*/
Expand Down
7 changes: 7 additions & 0 deletions include/boost/histogram/axis/category.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ class category : public iterator_mixin<category<Value, MetaData, Options, Alloca
BOOST_THROW_EXCEPTION(std::invalid_argument("cannot merge bins for category axis"));
}

/// Constructor used by algorithm::reduce to pick arbitrary bins (not for users).
category(const category& src, const index_type* begin, const index_type* end)
: metadata_base(src), vec_(src.get_allocator()) {
vec_.reserve(static_cast<std::size_t>(end - begin));
for (; begin != end; ++begin) vec_.emplace_back(src.vec_[*begin]);
}

/// Return index for value argument.
index_type index(const value_type& x) const noexcept {
const auto beg = vec_.begin();
Expand Down
22 changes: 22 additions & 0 deletions include/boost/histogram/axis/traits.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,28 @@ using is_reducible = std::is_constructible<Axis, const Axis&, axis::index_type,
struct is_reducible;
#endif

/** Meta-function to detect whether an axis has the pick constructor.
Doxygen does not render this well. This is a meta-function (template alias), it accepts
an axis type and represents compile-time boolean which is true or false, depending on
whether the axis has the special constructor required by the pick command in
boost::histogram::algorithm::reduce(). See Axis concept for details.
This constructor must only be implemented for axes which are not ordered, like the
category axis, since picking an arbitrary subset of bins from an ordered axis
would create gaps in the axis range.
@tparam Axis axis type.
*/
template <class Axis>
#ifndef BOOST_HISTOGRAM_DOXYGEN_INVOKED
using has_pick_constructor = std::is_constructible<Axis, const Axis&,
const axis::index_type*,
const axis::index_type*>;
#else
struct has_pick_constructor;
#endif

/** Get axis options for axis type.
Doxygen does not render this well. This is a meta-function (template alias), it accepts
Expand Down
13 changes: 10 additions & 3 deletions include/boost/histogram/detail/reduce_command.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <cassert>
#include <stdexcept>
#include <string>
#include <vector>

namespace boost {
namespace histogram {
Expand All @@ -25,15 +26,18 @@ struct reduce_command {
none,
indices,
values,
indices_list,
} range = range_t::none;
union {
axis::index_type index;
double value;
} begin{0}, end{0};
unsigned merge = 0; // default value indicates unset option
std::vector<axis::index_type> indices; // set by the pick command
unsigned merge = 0; // default value indicates unset option
bool crop = false;
// for internal use by the reduce algorithm
axis::index_type reduced_end = 0; // (end - begin) / merge
axis::index_type reduced_end = 0; // overflow index of the new axis
std::vector<axis::index_type> lut; // pick: maps old bin index to new bin index
bool is_ordered = true;
bool use_underflow_bin = true;
bool use_overflow_bin = true;
Expand All @@ -55,9 +59,12 @@ inline void normalize_reduce_commands(span<reduce_command> out,
o_out = o_in;
} else {
// Some command was already set for this axis, try to fuse commands.
// A pick command cannot be fused with any other command.
if (!((o_in.range == reduce_command::range_t::none) ^
(o_out.range == reduce_command::range_t::none)) ||
(o_out.merge > 1 && o_in.merge > 1))
(o_out.merge > 1 && o_in.merge > 1) ||
o_in.range == reduce_command::range_t::indices_list ||
o_out.range == reduce_command::range_t::indices_list)
BOOST_THROW_EXCEPTION(std::invalid_argument(
"multiple conflicting reduce commands for axis " +
std::to_string(o_in.iaxis == reduce_command::unset ? iaxis : o_in.iaxis)));
Expand Down
4 changes: 3 additions & 1 deletion test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ boost_test(TYPE compile-fail SOURCES histogram_fail1.cpp)
boost_test(TYPE compile-fail SOURCES histogram_fail2.cpp)
boost_test(TYPE compile-fail SOURCES histogram_fail3.cpp)
boost_test(TYPE compile-fail SOURCES histogram_fail4.cpp)
boost_test(TYPE compile-fail SOURCES algorithm_reduce_pick_fail.cpp)

set(BOOST_TEST_LINK_LIBRARIES Boost::histogram Boost::core)

Expand All @@ -56,7 +57,8 @@ boost_test(TYPE run SOURCES accumulators_weighted_mean_test.cpp)
boost_test(TYPE run SOURCES accumulators_weighted_sum_test.cpp)
boost_test(TYPE run SOURCES accumulators_collector_test.cpp)
boost_test(TYPE run SOURCES algorithm_project_test.cpp)
boost_test(TYPE run SOURCES algorithm_reduce_test.cpp)
boost_test(TYPE run SOURCES algorithm_reduce_test.cpp
COMPILE_OPTIONS $<$<CXX_COMPILER_ID:MSVC>:/bigobj>)
boost_test(TYPE run SOURCES algorithm_sum_test.cpp)
boost_test(TYPE run SOURCES algorithm_empty_test.cpp)
boost_test(TYPE run SOURCES axis_boolean_test.cpp)
Expand Down
1 change: 1 addition & 0 deletions test/Jamfile
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ alias failure :
[ compile-fail histogram_fail2.cpp ]
[ compile-fail histogram_fail3.cpp ]
[ compile-fail histogram_fail4.cpp ]
[ compile-fail algorithm_reduce_pick_fail.cpp ]
;

alias threading :
Expand Down
26 changes: 26 additions & 0 deletions test/algorithm_reduce_pick_fail.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2026 Hans Dembinski
//
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE_1_0.txt
// or copy at http://www.boost.org/LICENSE_1_0.txt)

#include <boost/histogram/algorithm/reduce.hpp>
#include <boost/histogram/axis/integer.hpp>
#include <boost/histogram/histogram.hpp>
#include <boost/histogram/make_histogram.hpp>
#include <vector>

int main() {
using namespace boost::histogram;
using int_axis = axis::integer<>;
struct ordered_with_pick_ctor : int_axis {
using int_axis::int_axis;
// ordered axis is not pickable, even if it has the pick constructor
ordered_with_pick_ctor(const ordered_with_pick_ctor&, const axis::index_type*,
const axis::index_type*) {}
};

auto h = make_histogram(ordered_with_pick_ctor(0, 5));
h(2);
(void)algorithm::reduce(h, algorithm::pick({0, 2}));
}
Loading
Loading