add crop command for reduce

This commit is contained in:
Hans Dembinski 2020-01-31 21:41:32 +01:00 committed by GitHub
parent a872c6e1c3
commit 3e882bce26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 300 additions and 147 deletions

View File

@ -27,6 +27,11 @@ namespace detail {
struct reduce_command {
static constexpr unsigned unset = static_cast<unsigned>(-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
<a href="./boost/histogram/algorithm/reduce_command.html">reduce_command</a>.
*/
*/
template <class Histogram, class Iterable, class = detail::requires_iterable<Iterable>>
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<reduce_command>(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<axis::traits::is_reducible<A>::value>(
[&o](auto&& a_out, const auto& a_in) {
using A = std::decay_t<decltype(a_in)>;
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<A>(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<A>(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 <class Histogram, class... Ts>
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

View File

@ -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 <typename Tag>
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<int>(), 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);
}