diff --git a/include/boost/histogram/algorithm/reduce.hpp b/include/boost/histogram/algorithm/reduce.hpp index d7fbc1a6..383532e4 100644 --- a/include/boost/histogram/algorithm/reduce.hpp +++ b/include/boost/histogram/algorithm/reduce.hpp @@ -27,6 +27,11 @@ namespace detail { struct reduce_command { static constexpr unsigned unset = static_cast(-1); unsigned iaxis; + enum class range_t : char { + none, + indices, + values, + } range = range_t::none; union { axis::index_type index; double value; @@ -36,36 +41,169 @@ struct reduce_command { double value; } end; unsigned merge = 0; // default value indicates unset option - enum class state_t : char { - rebin, - slice, - shrink, - } state; + bool crop = false; // for internal use by the reduce algorithm - bool is_ordered; - bool use_underflow_bin; - bool use_overflow_bin; + bool is_ordered = true; + bool use_underflow_bin = true; + bool use_overflow_bin = true; }; } // namespace detail namespace algorithm { -/** Base type for all @ref reduce commands. +/** Base type for all reduce commands. - Use this type to store commands in a container. The internals of this type are an - implementation detail. Casting a derived command to this base is safe and never causes - slicing. - */ + Use this type to store commands in a container. The internals of this type are an + implementation detail. Casting a derived command to this base is safe and never causes + object slicing. +*/ using reduce_command = detail::reduce_command; using reduce_option [[deprecated("use reduce_command instead")]] = reduce_command; ///< deprecated +/** Shrink command to be used in reduce(). + + Shrinking is based on an inclusive value interval. The bin which contains the first + value starts the range of bins to keep. The bin which contains the second value is the + last included in that range. When the second value is exactly equal to a lower bin edge, + then the previous bin is the last in the range. + + The counts in removed bins are added to the corresponding underflow and overflow bins, + if they are present. If they are not present, the counts are discarded. Also see @ref + crop, which always discards the counts. +*/ +struct shrink : reduce_command { + + /** Command is applied to axis with given index. + + @param iaxis which axis to operate on. + @param lower bin which contains lower is first to be kept. + @param upper bin which contains upper is last to be kept, except if upper is equal to + the lower edge. + */ + shrink(unsigned iaxis, double lower, double upper) { + if (lower == upper) + BOOST_THROW_EXCEPTION(std::invalid_argument("lower != upper required")); + reduce_command::iaxis = iaxis; + reduce_command::range = reduce_command::range_t::values; + reduce_command::begin.value = lower; + reduce_command::end.value = upper; + reduce_command::merge = 1; + reduce_command::crop = false; + } + + /** Command is applied to corresponding axis in order of reduce arguments. + + @param lower bin which contains lower is first to be kept. + @param upper bin which contains upper is last to be kept, except if upper is equal to + the lower edge. + */ + shrink(double lower, double upper) : shrink{reduce_command::unset, lower, upper} {} +}; + +/** Crop command to be used in reduce(). + + Works like @ref shrink (see shrink documentation for details), but counts in removed + bins are always discarded, whether underflow and overflow bins are present or not. +*/ +struct crop : reduce_command { + + /** Command is applied to axis with given index. + + @param iaxis which axis to operate on. + @param lower bin which contains lower is first to be kept. + @param upper bin which contains upper is last to be kept, except if upper is equal to + the lower edge. + */ + crop(unsigned iaxis, double lower, double upper) + : reduce_command{shrink{iaxis, lower, upper}} { + reduce_command::crop = true; + } + + /** Command is applied to corresponding axis in order of reduce arguments. + + @param lower bin which contains lower is first to be kept. + @param upper bin which contains upper is last to be kept, except if upper is equal to + the lower edge. + */ + crop(double lower, double upper) : crop{reduce_command::unset, lower, upper} {} +}; + +/** Slice command to be used in reduce(). + + Slicing works like @ref shrink or @ref crop, but uses bin indices instead of values. +*/ +struct slice : reduce_command { + + /// Whether to behave like @ref shrink or @ref crop regarding removed bins. + enum class mode { shrink, crop }; + + /** Command is applied to axis with given index. + + @param iaxis which axis to operate on. + @param begin first index that should be kept. + @param end one past the last index that should be kept. + @param mode whether to behave like @ref shrink or @ref crop regarding removed bins. + */ + slice(unsigned iaxis, axis::index_type begin, axis::index_type end, + slice::mode mode = slice::mode::shrink) { + + if (!(begin < end)) + BOOST_THROW_EXCEPTION(std::invalid_argument("begin < end required")); + + reduce_command::iaxis = iaxis; + reduce_command::range = reduce_command::range_t::indices; + reduce_command::begin.index = begin; + reduce_command::end.index = end; + reduce_command::merge = 1; + reduce_command::crop = mode == slice::mode::crop; + } + + /** Command is applied to corresponding axis in order of reduce arguments. + + @param begin first index that should be kept. + @param end one past the last index that should be kept. + @param mode whether to behave like @ref shrink or @ref crop regarding removed bins. + */ + slice(axis::index_type begin, axis::index_type end, + slice::mode mode = slice::mode::shrink) + : slice{reduce_command::unset, begin, end, mode} {} +}; + +/** Rebin command to be used in reduce(). + + The command merges N adjacent bins into one. This makes the axis coarser and the bins + wider. The original number of bins is divided by N. If there is a rest to this devision, + the axis is implicitly shrunk at the upper end by that rest. +*/ +struct rebin : reduce_command { + + /** Command is applied to axis with given index. + + @param iaxis which axis to operate on. + @param merge how many adjacent bins to merge into one. + */ + rebin(unsigned iaxis, unsigned merge) { + if (merge == 0) BOOST_THROW_EXCEPTION(std::invalid_argument("merge > 0 required")); + reduce_command::iaxis = iaxis; + reduce_command::merge = merge; + reduce_command::range = reduce_command::range_t::none; + reduce_command::crop = false; + } + + /** Command is applied to corresponding axis in order of reduce arguments. + + @param merge how many adjacent bins to merge into one. + */ + rebin(unsigned merge) : rebin{reduce_command::unset, merge} {} +}; + /** Shrink and rebin command to be used in reduce(). To @ref shrink and @ref rebin in one command (see the respective commands for more - details). Equivalent to passing both commands for the same axis to @ref reduce. - */ + details). Equivalent to passing both commands for the same axis to reduce(). +*/ struct shrink_and_rebin : reduce_command { /** Command is applied to axis with given index. @@ -76,15 +214,9 @@ struct shrink_and_rebin : reduce_command { whole interval is removed. @param merge how many adjacent bins to merge into one. */ - shrink_and_rebin(unsigned iaxis, double lower, double upper, unsigned merge) { - if (lower == upper) - BOOST_THROW_EXCEPTION(std::invalid_argument("lower != upper required")); - if (merge == 0) BOOST_THROW_EXCEPTION(std::invalid_argument("merge > 0 required")); - reduce_command::iaxis = iaxis; - reduce_command::begin.value = lower; - reduce_command::end.value = upper; - reduce_command::merge = merge; - reduce_command::state = reduce_command::state_t::shrink; + shrink_and_rebin(unsigned iaxis, double lower, double upper, unsigned merge) + : reduce_command{shrink{iaxis, lower, upper}} { + reduce_command::merge = rebin{0, merge}.merge; } /** Command is applied to corresponding axis in order of reduce arguments. @@ -98,11 +230,42 @@ struct shrink_and_rebin : reduce_command { : shrink_and_rebin(reduce_command::unset, lower, upper, merge) {} }; +/** Crop and rebin command to be used in reduce(). + + To @ref crop and @ref rebin in one command (see the respective commands for more + details). Equivalent to passing both commands for the same axis to reduce(). +*/ +struct crop_and_rebin : reduce_command { + + /** Command is applied to axis with given index. + + @param iaxis which axis to operate on. + @param lower lowest bound that should be kept. + @param upper highest bound that should be kept. If upper is inside bin interval, + the whole interval is removed. + @param merge how many adjacent bins to merge into one. + */ + crop_and_rebin(unsigned iaxis, double lower, double upper, unsigned merge) + : reduce_command{algorithm::crop{iaxis, lower, upper}} { + reduce_command::merge = rebin{0, merge}.merge; + } + + /** Command is applied to corresponding axis in order of reduce arguments. + + @param lower lowest bound that should be kept. + @param upper highest bound that should be kept. If upper is inside bin interval, + the whole interval is removed. + @param merge how many adjacent bins to merge into one. + */ + crop_and_rebin(double lower, double upper, unsigned merge) + : crop_and_rebin(reduce_command::unset, lower, upper, merge) {} +}; + /** Slice and rebin command to be used in reduce(). To @ref slice and @ref rebin in one command (see the respective commands for more - details). Equivalent to passing both commands for the same axis to @ref reduce. - */ + details). Equivalent to passing both commands for the same axis to reduce(). +*/ struct slice_and_rebin : reduce_command { /** Command is applied to axis with given index. @@ -111,18 +274,12 @@ struct slice_and_rebin : reduce_command { @param begin first index that should be kept. @param end one past the last index that should be kept. @param merge how many adjacent bins to merge into one. + @param mode slice mode, see slice::mode. */ slice_and_rebin(unsigned iaxis, axis::index_type begin, axis::index_type end, - unsigned merge) { - if (!(begin < end)) - BOOST_THROW_EXCEPTION(std::invalid_argument("begin < end required")); - if (merge == 0) BOOST_THROW_EXCEPTION(std::invalid_argument("merge > 0 required")); - - reduce_command::iaxis = iaxis; - reduce_command::begin.index = begin; - reduce_command::end.index = end; - reduce_command::merge = merge; - reduce_command::state = reduce_command::state_t::slice; + unsigned merge, slice::mode mode = slice::mode::shrink) + : reduce_command{slice{iaxis, begin, end, mode}} { + reduce_command::merge = rebin{0, merge}.merge; } /** Command is applied to corresponding axis in order of reduce arguments. @@ -130,89 +287,14 @@ struct slice_and_rebin : reduce_command { @param begin first index that should be kept. @param end one past the last index that should be kept. @param merge how many adjacent bins to merge into one. + @param mode slice mode, see slice::mode. */ - slice_and_rebin(axis::index_type begin, axis::index_type end, unsigned merge) - : slice_and_rebin(reduce_command::unset, begin, end, merge) {} + slice_and_rebin(axis::index_type begin, axis::index_type end, unsigned merge, + slice::mode mode = slice::mode::shrink) + : slice_and_rebin(reduce_command::unset, begin, end, merge, mode) {} }; -/** Shrink command to be used in reduce(). - - The shrink is inclusive. The bin which contains the first value starts the range of bins - to keep. The bin which contains the second value is the last included in that range. - When the second value is exactly equal to a lower bin edge, then the previous bin is - the last in the range. - */ -struct shrink : shrink_and_rebin { - - /** Command is applied to axis with given index. - - @param iaxis which axis to operate on. - @param lower bin which contains lower is first to be kept. - @param upper bin which contains upper is last to be kept, except if upper is equal to - the lower edge. - */ - shrink(unsigned iaxis, double lower, double upper) - : shrink_and_rebin{iaxis, lower, upper, 1u} {} - - /** Command is applied to corresponding axis in order of reduce arguments. - - @param lower bin which contains lower is first to be kept. - @param upper bin which contains upper is last to be kept, except if upper is equal to - the lower edge. - */ - shrink(double lower, double upper) : shrink{reduce_command::unset, lower, upper} {} -}; - -/** Slice command to be used in reduce(). - - Slicing works like shrinking, but uses bin indices instead of values. - */ -struct slice : slice_and_rebin { - /** Command is applied to axis with given index. - - @param iaxis which axis to operate on. - @param begin first index that should be kept. - @param end one past the last index that should be kept. - */ - slice(unsigned iaxis, axis::index_type begin, axis::index_type end) - : slice_and_rebin{iaxis, begin, end, 1u} {} - - /** Command is applied to corresponding axis in order of reduce arguments. - - @param begin first index that should be kept. - @param end one past the last index that should be kept. - */ - slice(axis::index_type begin, axis::index_type end) - : slice{reduce_command::unset, begin, end} {} -}; - -/** Rebin command to be used in reduce(). - - The command merges N adjacent bins into one. This makes the axis coarser and the bins - wider. The original number of bins is divided by N. If there is a rest to this devision, - the axis is implicitly shrunk at the upper end by that rest. - */ -struct rebin : reduce_command { - /** Command is applied to axis with given index. - - @param iaxis which axis to operate on. - @param merge how many adjacent bins to merge into one. - */ - rebin(unsigned iaxis, unsigned merge) { - if (merge == 0) BOOST_THROW_EXCEPTION(std::invalid_argument("merge > 0 required")); - reduce_command::iaxis = iaxis; - reduce_command::merge = merge; - reduce_command::state = reduce_command::state_t::rebin; - } - - /** Command is applied to corresponding axis in order of reduce arguments. - - @param merge how many adjacent bins to merge into one. - */ - rebin(unsigned merge) : rebin{reduce_command::unset, merge} {} -}; - -/** Shrink, slice, and/or rebin axes of a histogram. +/** Shrink, crop, slice, and/or rebin axes of a histogram. Returns a new reduced histogram and leaves the original histogram untouched. @@ -227,7 +309,7 @@ struct rebin : reduce_command { @param options iterable sequence of reduce commands: shrink_and_rebin, slice_and_rebin, @ref shrink, @ref slice, or @ref rebin. The element type of the iterable should be reduce_command. - */ +*/ template > Histogram reduce(const Histogram& hist, const Iterable& options) { using axis::index_type; @@ -235,6 +317,8 @@ Histogram reduce(const Histogram& hist, const Iterable& options) { const auto& old_axes = unsafe_access::axes(hist); auto opts = detail::make_stack_buffer(old_axes); + + // check for invalid commands, merge commands, and set iaxis for positional commands unsigned iaxis = 0; for (const reduce_command& o_in : options) { BOOST_ASSERT(o_in.merge > 0); @@ -244,23 +328,22 @@ Histogram reduce(const Histogram& hist, const Iterable& options) { if (o_out.merge == 0) { o_out = o_in; } else { - // Some option was already set for this axis, see if we can combine requests. - // We can combine a rebin and non-rebin request. - if (!((o_in.state == reduce_command::state_t::rebin) ^ - (o_out.state == reduce_command::state_t::rebin)) || + // Some command was already set for this axis, see if we can combine commands. + // We can combine a rebin and non-rebin 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)) BOOST_THROW_EXCEPTION(std::invalid_argument( - "multiple non-fuseable reduce requests for axis " + + "multiple conflicting reduce commands for axis " + std::to_string(o_in.iaxis == reduce_command::unset ? iaxis : o_in.iaxis))); - if (o_in.state != reduce_command::state_t::rebin) { - o_out.state = o_in.state; + if (o_in.range != reduce_command::range_t::none) { + o_out.range = o_in.range; o_out.begin = o_in.begin; o_out.end = o_in.end; } else { o_out.merge = o_in.merge; } } - o_out.iaxis = reduce_command::unset; // value not used below ++iaxis; } @@ -284,14 +367,16 @@ Histogram reduce(const Histogram& hist, const Iterable& options) { auto& o = opts[iaxis]; o.is_ordered = axis::traits::ordered(a_in); if (o.merge > 0) { // option is set? + o.use_underflow_bin = !o.crop && AO::test(axis::option::underflow); + o.use_overflow_bin = !o.crop && AO::test(axis::option::overflow); detail::static_if_c::value>( [&o](auto&& a_out, const auto& a_in) { using A = std::decay_t; - if (o.state == reduce_command::state_t::rebin) { + if (o.range == reduce_command::range_t::none) { o.begin.index = 0; o.end.index = a_in.size(); } else { - if (o.state == reduce_command::state_t::shrink) { + if (o.range == reduce_command::range_t::values) { const auto end_value = o.end.value; o.begin.index = axis::traits::index(a_in, o.begin.value); o.end.index = axis::traits::index(a_in, o.end.value); @@ -314,17 +399,14 @@ Histogram reduce(const Histogram& hist, const Iterable& options) { " is not reducible")); }, axis::get(detail::axis_get(axes, iaxis)), a_in); - // will be configurable with crop() + } else { + // command was not set for this axis; fill noop values and copy original axis o.use_underflow_bin = AO::test(axis::option::underflow); o.use_overflow_bin = AO::test(axis::option::overflow); - } else { - // option was not set for this axis; fill noop values and copy original axis o.merge = 1; o.begin.index = 0; o.end.index = a_in.size(); axis::get(detail::axis_get(axes, iaxis)) = a_in; - o.use_underflow_bin = AO::test(axis::option::underflow); - o.use_overflow_bin = AO::test(axis::option::overflow); } ++iaxis; }); @@ -380,7 +462,7 @@ Histogram reduce(const Histogram& hist, const Iterable& options) { @param opt first reduce command; one of @ref shrink, @ref slice, @ref rebin, shrink_and_rebin, or slice_or_rebin. @param opts more reduce commands. - */ +*/ template Histogram reduce(const Histogram& hist, const reduce_command& opt, const Ts&... opts) { // this must be in one line, because any of the ts could be a temporary diff --git a/test/algorithm_reduce_test.cpp b/test/algorithm_reduce_test.cpp index d00a2305..5d29211f 100644 --- a/test/algorithm_reduce_test.cpp +++ b/test/algorithm_reduce_test.cpp @@ -21,6 +21,15 @@ using namespace boost::histogram; using namespace boost::histogram::algorithm; +struct unreducible { + axis::index_type index(int) const { return 0; } + axis::index_type size() const { return 1; } + friend std::ostream& operator<<(std::ostream& os, const unreducible&) { + os << "unreducible"; + return os; + } +}; + template void run_tests() { // limitations: shrink does not work with arguments not convertible to double @@ -36,28 +45,33 @@ void run_tests() { // not allowed: invalid axis index BOOST_TEST_THROWS((void)reduce(h, slice(10, 2, 3)), std::invalid_argument); - // not allowed: repeated indices + // two slice requests for same axis not allowed BOOST_TEST_THROWS((void)reduce(h, slice(1, 0, 2), slice(1, 1, 3)), std::invalid_argument); - // two rebin requests for same axis cannot be fused + // two rebin requests for same axis not allowed BOOST_TEST_THROWS((void)reduce(h, rebin(0, 2), rebin(0, 2)), std::invalid_argument); // rebin and slice_and_rebin with merge > 1 requests for same axis cannot be fused BOOST_TEST_THROWS((void)reduce(h, slice_and_rebin(0, 1, 3, 2), rebin(0, 2)), std::invalid_argument); - BOOST_TEST_THROWS((void)reduce(h, shrink(1, 0, 2), shrink(1, 0, 2)), + BOOST_TEST_THROWS((void)reduce(h, shrink(1, 0, 2), crop(1, 0, 2)), std::invalid_argument); // not allowed: slice with begin >= end BOOST_TEST_THROWS((void)reduce(h, slice(0, 1, 1)), std::invalid_argument); BOOST_TEST_THROWS((void)reduce(h, slice(0, 2, 1)), std::invalid_argument); // not allowed: shrink with lower == upper BOOST_TEST_THROWS((void)reduce(h, shrink(0, 0, 0)), std::invalid_argument); + // not allowed: crop with lower == upper + BOOST_TEST_THROWS((void)reduce(h, crop(0, 0, 0)), std::invalid_argument); // not allowed: shrink axis to zero size BOOST_TEST_THROWS((void)reduce(h, shrink(0, 10, 11)), std::invalid_argument); // not allowed: rebin with zero merge BOOST_TEST_THROWS((void)reduce(h, rebin(0, 0)), std::invalid_argument); + // not allowed: reducing unreducible axis + BOOST_TEST_THROWS((void)reduce(make(Tag(), unreducible{}), slice(0, 1)), + std::invalid_argument); } - // shrink behavior when value on edge and not on edge is inclusive: + // shrink and crop behavior when value on edge and not on edge is inclusive: // - lower edge of shrink: pick bin which contains edge, lower <= x < upper // - upper edge of shrink: pick bin which contains edge + 1, lower < x <= upper { @@ -78,6 +92,17 @@ void run_tests() { BOOST_TEST_EQ(reduce(h, shrink(0, 2.001)).axis(), ID(0, 3)); BOOST_TEST_EQ(reduce(h, shrink(0, 2)).axis(), ID(0, 2)); BOOST_TEST_EQ(reduce(h, shrink(0, 1.999)).axis(), ID(0, 2)); + + BOOST_TEST_EQ(reduce(h, crop(-1, 5)).axis(), ID(0, 3)); + BOOST_TEST_EQ(reduce(h, crop(0, 3)).axis(), ID(0, 3)); + BOOST_TEST_EQ(reduce(h, crop(1, 3)).axis(), ID(1, 3)); + BOOST_TEST_EQ(reduce(h, crop(1.001, 3)).axis(), ID(1, 3)); + BOOST_TEST_EQ(reduce(h, crop(1.999, 3)).axis(), ID(1, 3)); + BOOST_TEST_EQ(reduce(h, crop(2, 3)).axis(), ID(2, 3)); + BOOST_TEST_EQ(reduce(h, crop(0, 2.999)).axis(), ID(0, 3)); + BOOST_TEST_EQ(reduce(h, crop(0, 2.001)).axis(), ID(0, 3)); + BOOST_TEST_EQ(reduce(h, crop(0, 2)).axis(), ID(0, 2)); + BOOST_TEST_EQ(reduce(h, crop(0, 1.999)).axis(), ID(0, 2)); } { @@ -131,21 +156,17 @@ void run_tests() { /* matrix layout: - x -> + x y 1 0 1 0 - | 1 1 0 0 - v 0 2 1 3 + 1 1 0 0 + 0 2 1 3 */ hr = reduce(h, shrink_and_rebin(0, 2, 5, 2), rebin(1, 3)); BOOST_TEST_EQ(hr.rank(), 2); BOOST_TEST_EQ(sum(hr), 10); - BOOST_TEST_EQ(hr.axis(0).size(), 1); - BOOST_TEST_EQ(hr.axis(1).size(), 1); - BOOST_TEST_EQ(hr.axis(0).bin(0).lower(), 2); - BOOST_TEST_EQ(hr.axis(0).bin(0).upper(), 4); - BOOST_TEST_EQ(hr.axis(1).bin(0).lower(), -1); - BOOST_TEST_EQ(hr.axis(1).bin(0).upper(), 2); + BOOST_TEST_EQ(hr.axis(0), R(1, 2, 4)); + BOOST_TEST_EQ(hr.axis(1), R(1, -1, 2)); BOOST_TEST_EQ(hr.at(-1, 0), 2); // underflow BOOST_TEST_EQ(hr.at(0, 0), 5); BOOST_TEST_EQ(hr.at(1, 0), 3); // overflow @@ -163,16 +184,66 @@ void run_tests() { BOOST_TEST_EQ(hr4, hr); } + // crop + { + auto h = make_s(Tag(), std::vector(), R(4, 1, 5), R(3, 1, 4)); + + /* + matrix layout: + x + y 1 0 1 0 + 1 1 0 0 + 0 2 1 3 + */ + h.at(0, 0) = 1; + h.at(0, 1) = 1; + h.at(1, 1) = 1; + h.at(1, 2) = 2; + h.at(2, 0) = 1; + h.at(2, 2) = 1; + h.at(3, 2) = 3; + + /* + crop first and last column in x and y + matrix layout after: + x + y 3 1 + */ + + auto hr = reduce(h, crop(2, 4), crop_and_rebin(2, 4, 2)); + BOOST_TEST_EQ(hr.rank(), 2); + BOOST_TEST_EQ(sum(hr), 4); + BOOST_TEST_EQ(hr.axis(0), R(2, 2, 4)); + BOOST_TEST_EQ(hr.axis(1), R(1, 2, 4)); + BOOST_TEST_EQ(hr.at(0, 0), 3); + BOOST_TEST_EQ(hr.at(1, 0), 1); + + // slice with crop mode + auto hr2 = reduce(h, slice(1, 3, slice::mode::crop), + slice_and_rebin(1, 3, 2, slice::mode::crop)); + BOOST_TEST_EQ(hr, hr2); + + // explicit axis indices + auto hr3 = reduce(h, crop_and_rebin(1, 2, 4, 2), crop(0, 2, 4)); + BOOST_TEST_EQ(hr, hr3); + auto hr4 = reduce(h, slice_and_rebin(1, 1, 3, 2, slice::mode::crop), + slice(0, 1, 3, slice::mode::crop)); + BOOST_TEST_EQ(hr, hr4); + } + // mixed axis types { R r(5, 0.0, 5.0); V v{{1., 2., 3.}}; CI c{{1, 2, 3}}; - auto h = make(Tag(), r, v, c); + unreducible u; + + auto h = make(Tag(), r, v, c, u); auto hr = algorithm::reduce(h, shrink(0, 2, 4), slice(2, 1, 3)); BOOST_TEST_EQ(hr.axis(0), (R{2, 2, 4})); BOOST_TEST_EQ(hr.axis(1), (V{{1., 2., 3.}})); BOOST_TEST_EQ(hr.axis(2), (CI{{2, 3}})); + BOOST_TEST_EQ(hr.axis(3), u); BOOST_TEST_THROWS((void)algorithm::reduce(h, rebin(2, 2)), std::invalid_argument); }