diff --git a/benchmark/histogram_filling.cpp b/benchmark/histogram_filling.cpp index dbe8bf1b..470aafcf 100644 --- a/benchmark/histogram_filling.cpp +++ b/benchmark/histogram_filling.cpp @@ -31,7 +31,33 @@ using DStore = boost::histogram::adaptive_storage<>; #endif using namespace boost::histogram; +namespace op = boost::histogram::axis::option; using reg = axis::regular<>; +using reg_closed = + axis::regular; + +class reg_closed_unsafe { +public: + reg_closed_unsafe(axis::index_type n, double start, double stop) + : min_{start}, delta_{stop - start}, size_{n} {} + + axis::index_type index(double x) const noexcept { + // Runs in hot loop, please measure impact of changes + auto z = (x - min_) / delta_; + // assume that z < 0 and z > 1 never happens, promised by inclusive() + if (z == 1) return size() - 1; + return static_cast(z * size()); + } + + axis::index_type size() const noexcept { return size_; } + + static constexpr bool inclusive() { return true; } + +private: + double min_; + double delta_; + axis::index_type size_; +}; template static void fill_1d(benchmark::State& state) { @@ -41,6 +67,22 @@ static void fill_1d(benchmark::State& state) { state.SetItemsProcessed(state.iterations()); } +template +static void fill_1d_closed(benchmark::State& state) { + auto h = make_s(Tag(), Storage(), reg_closed(100, 0, 1)); + auto gen = generator(); + for (auto _ : state) benchmark::DoNotOptimize(h(gen())); + state.SetItemsProcessed(state.iterations()); +} + +template +static void fill_1d_closed_unsafe(benchmark::State& state) { + auto h = make_s(Tag(), Storage(), reg_closed_unsafe(100, 0, 1)); + auto gen = generator(); + for (auto _ : state) benchmark::DoNotOptimize(h(gen())); + state.SetItemsProcessed(state.iterations()); +} + template static void fill_n_1d(benchmark::State& state) { auto h = make_s(Tag(), Storage(), reg(100, 0, 1)); @@ -49,6 +91,22 @@ static void fill_n_1d(benchmark::State& state) { state.SetItemsProcessed(state.iterations() * gen.size()); } +template +static void fill_n_1d_closed(benchmark::State& state) { + auto h = make_s(Tag(), Storage(), reg_closed(100, 0, 1)); + auto gen = generator(); + for (auto _ : state) h.fill(gen); + state.SetItemsProcessed(state.iterations() * gen.size()); +} + +template +static void fill_n_1d_closed_unsafe(benchmark::State& state) { + auto h = make_s(Tag(), Storage(), reg_closed_unsafe(100, 0, 1)); + auto gen = generator(); + for (auto _ : state) h.fill(gen); + state.SetItemsProcessed(state.iterations() * gen.size()); +} + template static void fill_2d(benchmark::State& state) { auto h = make_s(Tag(), Storage(), reg(100, 0, 1), reg(100, 0, 1)); @@ -111,6 +169,8 @@ BENCHMARK_TEMPLATE(fill_1d, uniform, dynamic_tag); // BENCHMARK_TEMPLATE(fill_1d, uniform, dynamic_tag, DStore); BENCHMARK_TEMPLATE(fill_1d, normal, dynamic_tag); // BENCHMARK_TEMPLATE(fill_1d, normal, dynamic_tag, DStore); +BENCHMARK_TEMPLATE(fill_1d_closed, uniform, static_tag); +BENCHMARK_TEMPLATE(fill_1d_closed_unsafe, uniform, static_tag); BENCHMARK_TEMPLATE(fill_n_1d, uniform, static_tag); // BENCHMARK_TEMPLATE(fill_n_1d, uniform, static_tag, DStore); @@ -120,6 +180,8 @@ BENCHMARK_TEMPLATE(fill_n_1d, uniform, dynamic_tag); // BENCHMARK_TEMPLATE(fill_n_1d, uniform, dynamic_tag, DStore); BENCHMARK_TEMPLATE(fill_n_1d, normal, dynamic_tag); // BENCHMARK_TEMPLATE(fill_n_1d, normal, dynamic_tag, DStore); +BENCHMARK_TEMPLATE(fill_n_1d_closed, uniform, static_tag); +BENCHMARK_TEMPLATE(fill_n_1d_closed_unsafe, uniform, static_tag); BENCHMARK_TEMPLATE(fill_2d, uniform, static_tag); // BENCHMARK_TEMPLATE(fill_2d, uniform, static_tag, DStore); diff --git a/include/boost/histogram/axis/category.hpp b/include/boost/histogram/axis/category.hpp index 9871e9b7..bcaececc 100644 --- a/include/boost/histogram/axis/category.hpp +++ b/include/boost/histogram/axis/category.hpp @@ -26,22 +26,21 @@ namespace boost { namespace histogram { namespace axis { -/** - Maps at a set of unique values to bin indices. +/** Maps at a set of unique values to bin indices. - The axis maps a set of values to bins, following the order of arguments in the - constructor. The optional overflow bin for this axis counts input values that - are not part of the set. Binning has O(N) complexity, but with a very small - factor. For small N (the typical use case) it beats other kinds of lookup. + The axis maps a set of values to bins, following the order of arguments in the + constructor. The optional overflow bin for this axis counts input values that + are not part of the set. Binning has O(N) complexity, but with a very small + factor. For small N (the typical use case) it beats other kinds of lookup. - @tparam Value input value type, must be equal-comparable. - @tparam MetaData type to store meta data. - @tparam Options see boost::histogram::axis::option. - @tparam Allocator allocator to use for dynamic memory management. + @tparam Value input value type, must be equal-comparable. + @tparam MetaData type to store meta data. + @tparam Options see boost::histogram::axis::option. + @tparam Allocator allocator to use for dynamic memory management. - The options `underflow` and `circular` are not allowed. The options `growth` - and `overflow` are mutually exclusive. -*/ + The options `underflow` and `circular` are not allowed. The options `growth` + and `overflow` are mutually exclusive. + */ template class category : public iterator_mixin>, public metadata_base_t { @@ -66,12 +65,12 @@ public: explicit category(allocator_type alloc) : vec_(alloc) {} /** Construct from iterator range of unique values. - * - * @param begin begin of category range of unique values. - * @param end end of category range of unique values. - * @param meta description of the axis (optional). - * @param options see boost::histogram::axis::option (optional). - * @param alloc allocator instance to use (optional). + + @param begin begin of category range of unique values. + @param end end of category range of unique values. + @param meta description of the axis (optional). + @param options see boost::histogram::axis::option (optional). + @param alloc allocator instance to use (optional). */ template > category(It begin, It end, metadata_type meta = {}, options_type options = {}, @@ -91,11 +90,11 @@ public: : category(begin, end, std::move(meta), {}, std::move(alloc)) {} /** Construct axis from iterable sequence of unique values. - * - * @param iterable sequence of unique values. - * @param meta description of the axis. - * @param options see boost::histogram::axis::option (optional). - * @param alloc allocator instance to use. + + @param iterable sequence of unique values. + @param meta description of the axis. + @param options see boost::histogram::axis::option (optional). + @param alloc allocator instance to use. */ template > category(const C& iterable, metadata_type meta = {}, options_type options = {}, @@ -110,11 +109,11 @@ public: std::move(alloc)) {} /** Construct axis from an initializer list of unique values. - * - * @param list `std::initializer_list` of unique values. - * @param meta description of the axis. - * @param options see boost::histogram::axis::option (optional). - * @param alloc allocator instance to use. + + @param list `std::initializer_list` of unique values. + @param meta description of the axis. + @param options see boost::histogram::axis::option (optional). + @param alloc allocator instance to use. */ template category(std::initializer_list list, metadata_type meta = {}, diff --git a/include/boost/histogram/axis/integer.hpp b/include/boost/histogram/axis/integer.hpp index 4110875c..d61dfdd8 100644 --- a/include/boost/histogram/axis/integer.hpp +++ b/include/boost/histogram/axis/integer.hpp @@ -29,14 +29,13 @@ namespace boost { namespace histogram { namespace axis { -/** - Axis for an interval of integer values with unit steps. +/** Axis for an interval of integer values with unit steps. - Binning is a O(1) operation. This axis bins faster than a regular axis. + Binning is a O(1) operation. This axis bins faster than a regular axis. - @tparam Value input value type. Must be integer or floating point. - @tparam MetaData type to store meta data. - @tparam Options see boost::histogram::axis::option. + @tparam Value input value type. Must be integer or floating point. + @tparam MetaData type to store meta data. + @tparam Options see boost::histogram::axis::option. */ template class integer : public iterator_mixin>, @@ -72,11 +71,11 @@ public: constexpr integer() = default; /** Construct over semi-open integer interval [start, stop). - * - * @param start first integer of covered range. - * @param stop one past last integer of covered range. - * @param meta description of the axis (optional). - * @param options see boost::histogram::axis::option (optional). + + @param start first integer of covered range. + @param stop one past last integer of covered range. + @param meta description of the axis (optional). + @param options see boost::histogram::axis::option (optional). */ integer(value_type start, value_type stop, metadata_type meta = {}, options_type options = {}) diff --git a/include/boost/histogram/axis/regular.hpp b/include/boost/histogram/axis/regular.hpp index 3f7df9e0..403931f8 100644 --- a/include/boost/histogram/axis/regular.hpp +++ b/include/boost/histogram/axis/regular.hpp @@ -165,15 +165,22 @@ step_type step(T t) { return step_type{t}; } -/** - Axis for equidistant intervals on the real line. +/** Axis for equidistant intervals on the real line. - The most common binning strategy. Very fast. Binning is a O(1) operation. + The most common binning strategy. Very fast. Binning is a O(1) operation. - @tparam Value input value type, must be floating point. - @tparam Transform builtin or user-defined transform type. - @tparam MetaData type to store meta data. - @tparam Options see boost::histogram::axis::option. + If the axis has an overflow bin (the default), a value on the upper edge of the last + bin is put in the overflow bin. The axis range represents a semi-open interval. + + If the overflow bin is deactivated, then a value on the upper edge of the last bin is + still counted towards the last bin. The axis range represents a closed interval. This + is the desired behavior for random numbers drawn from a bounded interval, which is + usually closed. + + @tparam Value input value type, must be floating point. + @tparam Transform builtin or user-defined transform type. + @tparam MetaData type to store meta data. + @tparam Options see boost::histogram::axis::option. */ template class regular : public iterator_mixin>, @@ -207,13 +214,13 @@ public: constexpr regular() = default; /** Construct n bins over real transformed range [start, stop). - * - * @param trans transform instance to use. - * @param n number of bins. - * @param start low edge of first bin. - * @param stop high edge of last bin. - * @param meta description of the axis (optional). - * @param options see boost::histogram::axis::option (optional). + + @param trans transform instance to use. + @param n number of bins. + @param start low edge of first bin. + @param stop high edge of last bin. + @param meta description of the axis (optional). + @param options see boost::histogram::axis::option (optional). */ regular(transform_type trans, unsigned n, value_type start, value_type stop, metadata_type meta = {}, options_type options = {}) @@ -232,30 +239,30 @@ public: } /** Construct n bins over real range [start, stop). - * - * @param n number of bins. - * @param start low edge of first bin. - * @param stop high edge of last bin. - * @param meta description of the axis (optional). - * @param options see boost::histogram::axis::option (optional). + + @param n number of bins. + @param start low edge of first bin. + @param stop high edge of last bin. + @param meta description of the axis (optional). + @param options see boost::histogram::axis::option (optional). */ regular(unsigned n, value_type start, value_type stop, metadata_type meta = {}, options_type options = {}) : regular({}, n, start, stop, std::move(meta), options) {} /** Construct bins with the given step size over real transformed range - * [start, stop). - * - * @param trans transform instance to use. - * @param step width of a single bin. - * @param start low edge of first bin. - * @param stop upper limit of high edge of last bin (see below). - * @param meta description of the axis (optional). - * @param options see boost::histogram::axis::option (optional). - * - * The axis computes the number of bins as n = abs(stop - start) / step, - * rounded down. This means that stop is an upper limit to the actual value - * (start + n * step). + [start, stop). + + @param trans transform instance to use. + @param step width of a single bin. + @param start low edge of first bin. + @param stop upper limit of high edge of last bin (see below). + @param meta description of the axis (optional). + @param options see boost::histogram::axis::option (optional). + + The axis computes the number of bins as n = abs(stop - start) / step, + rounded down. This means that stop is an upper limit to the actual value + (start + n * step). */ template regular(transform_type trans, step_type step, value_type start, value_type stop, @@ -267,16 +274,16 @@ public: std::move(meta), options) {} /** Construct bins with the given step size over real range [start, stop). - * - * @param step width of a single bin. - * @param start low edge of first bin. - * @param stop upper limit of high edge of last bin (see below). - * @param meta description of the axis (optional). - * @param options see boost::histogram::axis::option (optional). - * - * The axis computes the number of bins as n = abs(stop - start) / step, - * rounded down. This means that stop is an upper limit to the actual value - * (start + n * step). + + @param step width of a single bin. + @param start low edge of first bin. + @param stop upper limit of high edge of last bin (see below). + @param meta description of the axis (optional). + @param options see boost::histogram::axis::option (optional). + + The axis computes the number of bins as n = abs(stop - start) / step, + rounded down. This means that stop is an upper limit to the actual value + (start + n * step). */ template regular(step_type step, value_type start, value_type stop, metadata_type meta = {}, @@ -311,6 +318,8 @@ public: else return -1; } + // upper edge of last bin is inclusive if overflow bin is not present + if (!options_type::test(option::overflow) && z == 1) return size() - 1; } return size(); // also returned if x is NaN } diff --git a/include/boost/histogram/axis/variable.hpp b/include/boost/histogram/axis/variable.hpp index f80e54c1..d34df5ed 100644 --- a/include/boost/histogram/axis/variable.hpp +++ b/include/boost/histogram/axis/variable.hpp @@ -34,17 +34,24 @@ namespace boost { namespace histogram { namespace axis { -/** - Axis for non-equidistant bins on the real line. +/** Axis for non-equidistant bins on the real line. Binning is a O(log(N)) operation. If speed matters and the problem domain allows it, prefer a regular axis, possibly with a transform. + If the axis has an overflow bin (the default), a value on the upper edge of the last + bin is put in the overflow bin. The axis range represents a semi-open interval. + + If the overflow bin is deactivated, then a value on the upper edge of the last bin is + still counted towards the last bin. The axis range represents a closed interval. This + is the desired behavior for random numbers drawn from a bounded interval, which is + usually closed. + @tparam Value input value type, must be floating point. @tparam MetaData type to store meta data. @tparam Options see boost::histogram::axis::option. @tparam Allocator allocator to use for dynamic memory management. - */ +*/ template class variable : public iterator_mixin>, public metadata_base_t { @@ -72,12 +79,12 @@ public: explicit variable(allocator_type alloc) : vec_(alloc) {} /** Construct from iterator range of bin edges. - * - * @param begin begin of edge sequence. - * @param end end of edge sequence. - * @param meta description of the axis (optional). - * @param options see boost::histogram::axis::option (optional). - * @param alloc allocator instance to use (optional). + + @param begin begin of edge sequence. + @param end end of edge sequence. + @param meta description of the axis (optional). + @param options see boost::histogram::axis::option (optional). + @param alloc allocator instance to use (optional). */ template > variable(It begin, It end, metadata_type meta = {}, options_type options = {}, @@ -106,11 +113,11 @@ public: : variable(begin, end, std::move(meta), {}, std::move(alloc)) {} /** Construct variable axis from iterable range of bin edges. - * - * @param iterable iterable range of bin edges. - * @param meta description of the axis (optional). - * @param options see boost::histogram::axis::option (optional). - * @param alloc allocator instance to use (optional). + + @param iterable iterable range of bin edges. + @param meta description of the axis (optional). + @param options see boost::histogram::axis::option (optional). + @param alloc allocator instance to use (optional). */ template > variable(const U& iterable, metadata_type meta = {}, options_type options = {}, @@ -125,11 +132,11 @@ public: std::move(alloc)) {} /** Construct variable axis from initializer list of bin edges. - * - * @param list `std::initializer_list` of bin edges. - * @param meta description of the axis (optional). - * @param options see boost::histogram::axis::option (optional). - * @param alloc allocator instance to use (optional). + + @param list `std::initializer_list` of bin edges. + @param meta description of the axis (optional). + @param options see boost::histogram::axis::option (optional). + @param alloc allocator instance to use (optional). */ template variable(std::initializer_list list, metadata_type meta = {}, @@ -159,6 +166,8 @@ public: const auto b = vec_[size()]; x -= std::floor((x - a) / (b - a)) * (b - a); } + // upper edge of last bin is inclusive if overflow bin is not present + if (!options_type::test(option::overflow) && x == vec_.back()) return size() - 1; return static_cast(std::upper_bound(vec_.begin(), vec_.end(), x) - vec_.begin() - 1); } diff --git a/test/axis_regular_test.cpp b/test/axis_regular_test.cpp index 2fece7f2..3c3102e2 100644 --- a/test/axis_regular_test.cpp +++ b/test/axis_regular_test.cpp @@ -20,6 +20,7 @@ int main() { using namespace boost::histogram; using def = use_default; namespace tr = axis::transform; + namespace op = axis::option; BOOST_TEST(std::is_nothrow_move_assignable>::value); BOOST_TEST(std::is_nothrow_move_constructible>::value); @@ -202,7 +203,7 @@ int main() { // with growth { using pii_t = std::pair; - axis::regular a{1, 0, 1}; + axis::regular a{1, 0, 1}; BOOST_TEST_EQ(a.size(), 1); BOOST_TEST_EQ(a.update(0), pii_t(0, 0)); BOOST_TEST_EQ(a.size(), 1); @@ -223,11 +224,32 @@ int main() { BOOST_TEST_EQ(a.update(-std::numeric_limits::infinity()), pii_t(-1, 0)); } + // axis with overflow bin represents open interval + { + axis::regular a{2, 0, 1}; + BOOST_TEST_EQ(a.index(0), 0); + BOOST_TEST_EQ(a.index(0.49), 0); + BOOST_TEST_EQ(a.index(0.50), 1); + BOOST_TEST_EQ(a.index(0.99), 1); + BOOST_TEST_EQ(a.index(1), 2); // overflow bin + BOOST_TEST_EQ(a.index(1.1), 2); // overflow bin + } + + // axis without overflow bin represents a closed interval + { + axis::regular a{2, 0, 1}; + BOOST_TEST_EQ(a.index(0), 0); + BOOST_TEST_EQ(a.index(0.49), 0); + BOOST_TEST_EQ(a.index(0.50), 1); + BOOST_TEST_EQ(a.index(0.99), 1); + BOOST_TEST_EQ(a.index(1), 1); // last ordinary bin + BOOST_TEST_EQ(a.index(1.1), 2); // out of range + } + // iterators { test_axis_iterator(axis::regular<>(5, 0, 1), 0, 5); - test_axis_iterator(axis::regular(5, 0, 1), 0, - 5); + test_axis_iterator(axis::regular(5, 0, 1), 0, 5); test_axis_iterator(axis::circular<>(5, 0, 1), 0, 5); } diff --git a/test/axis_variable_test.cpp b/test/axis_variable_test.cpp index b4c51ba1..5b205c71 100644 --- a/test/axis_variable_test.cpp +++ b/test/axis_variable_test.cpp @@ -18,6 +18,7 @@ #include "utility_str.hpp" using namespace boost::histogram; +namespace op = boost::histogram::axis::option; int main() { constexpr auto inf = std::numeric_limits::infinity(); @@ -104,7 +105,7 @@ int main() { // axis::variable circular { - axis::variable a{-1, 1, 2}; + axis::variable a{-1, 1, 2}; BOOST_TEST_EQ(a.value(-2), -4); BOOST_TEST_EQ(a.value(-1), -2); BOOST_TEST_EQ(a.value(0), -1); @@ -125,7 +126,7 @@ int main() { // axis::regular with growth { using pii_t = std::pair; - axis::variable a{0, 1}; + axis::variable a{0, 1}; BOOST_TEST_EQ(a.size(), 1); BOOST_TEST_EQ(a.update(0), pii_t(0, 0)); BOOST_TEST_EQ(a.size(), 1); @@ -149,11 +150,33 @@ int main() { BOOST_TEST_EQ(a.update(nan), pii_t(a.size(), 0)); } + // axis with overflow bin represents open interval + { + axis::variable a{0.0, 0.5, 1.0}; + BOOST_TEST_EQ(a.index(0), 0); + BOOST_TEST_EQ(a.index(0.49), 0); + BOOST_TEST_EQ(a.index(0.50), 1); + BOOST_TEST_EQ(a.index(0.99), 1); + BOOST_TEST_EQ(a.index(1), 2); // overflow bin + BOOST_TEST_EQ(a.index(1.1), 2); // overflow bin + } + + // axis without overflow bin represents a closed interval + { + axis::variable a{0.0, 0.5, 1.0}; + BOOST_TEST_EQ(a.index(0), 0); + BOOST_TEST_EQ(a.index(0.49), 0); + BOOST_TEST_EQ(a.index(0.50), 1); + BOOST_TEST_EQ(a.index(0.99), 1); + BOOST_TEST_EQ(a.index(1), 1); // last ordinary bin + BOOST_TEST_EQ(a.index(1.1), 2); // out of range + } + // iterators { test_axis_iterator(axis::variable<>{1, 2, 3}, 0, 2); - test_axis_iterator( - axis::variable{1, 2, 3}, 0, 2); + test_axis_iterator(axis::variable{1, 2, 3}, + 0, 2); } // shrink and rebin @@ -176,7 +199,7 @@ int main() { // shrink and rebin with circular option { - using A = axis::variable; + using A = axis::variable; auto a = A({1, 2, 3, 4, 5}); BOOST_TEST_THROWS(A(a, 1, 4, 1), std::invalid_argument); BOOST_TEST_THROWS(A(a, 0, 3, 1), std::invalid_argument);