regular and variable axis are closed intervals if overflow bin is absent (#344)

This commit is contained in:
Hans Dembinski 2021-09-30 14:59:27 +02:00 committed by Hans Dembinski
parent b482598597
commit bf7712f0d7
7 changed files with 232 additions and 109 deletions

View File

@ -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<double, boost::use_default, boost::use_default, op::none_t>;
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<axis::index_type>(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 <class Distribution, class Tag, class Storage = SStore>
static void fill_1d(benchmark::State& state) {
@ -41,6 +67,22 @@ static void fill_1d(benchmark::State& state) {
state.SetItemsProcessed(state.iterations());
}
template <class Distribution, class Tag, class Storage = SStore>
static void fill_1d_closed(benchmark::State& state) {
auto h = make_s(Tag(), Storage(), reg_closed(100, 0, 1));
auto gen = generator<Distribution>();
for (auto _ : state) benchmark::DoNotOptimize(h(gen()));
state.SetItemsProcessed(state.iterations());
}
template <class Distribution, class Tag, class Storage = SStore>
static void fill_1d_closed_unsafe(benchmark::State& state) {
auto h = make_s(Tag(), Storage(), reg_closed_unsafe(100, 0, 1));
auto gen = generator<Distribution>();
for (auto _ : state) benchmark::DoNotOptimize(h(gen()));
state.SetItemsProcessed(state.iterations());
}
template <class Distribution, class Tag, class Storage = SStore>
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 <class Distribution, class Tag, class Storage = SStore>
static void fill_n_1d_closed(benchmark::State& state) {
auto h = make_s(Tag(), Storage(), reg_closed(100, 0, 1));
auto gen = generator<Distribution>();
for (auto _ : state) h.fill(gen);
state.SetItemsProcessed(state.iterations() * gen.size());
}
template <class Distribution, class Tag, class Storage = SStore>
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<Distribution>();
for (auto _ : state) h.fill(gen);
state.SetItemsProcessed(state.iterations() * gen.size());
}
template <class Distribution, class Tag, class Storage = SStore>
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);

View File

@ -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 Value, class MetaData, class Options, class Allocator>
class category : public iterator_mixin<category<Value, MetaData, Options, Allocator>>,
public metadata_base_t<MetaData> {
@ -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 <class It, class = detail::requires_iterator<It>>
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 <class C, class = detail::requires_iterable<C>>
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 <class U>
category(std::initializer_list<U> list, metadata_type meta = {},

View File

@ -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 Value, class MetaData, class Options>
class integer : public iterator_mixin<integer<Value, MetaData, Options>>,
@ -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 = {})

View File

@ -165,15 +165,22 @@ step_type<T> step(T t) {
return step_type<T>{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 Value, class Transform, class MetaData, class Options>
class regular : public iterator_mixin<regular<Value, Transform, MetaData, Options>>,
@ -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 <class T>
regular(transform_type trans, step_type<T> 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 <class T>
regular(step_type<T> 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
}

View File

@ -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 Value, class MetaData, class Options, class Allocator>
class variable : public iterator_mixin<variable<Value, MetaData, Options, Allocator>>,
public metadata_base_t<MetaData> {
@ -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 <class It, class = detail::requires_iterator<It>>
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 <class U, class = detail::requires_iterable<U>>
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 <class U>
variable(std::initializer_list<U> 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<index_type>(std::upper_bound(vec_.begin(), vec_.end(), x) -
vec_.begin() - 1);
}

View File

@ -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<axis::regular<>>::value);
BOOST_TEST(std::is_nothrow_move_constructible<axis::regular<>>::value);
@ -202,7 +203,7 @@ int main() {
// with growth
{
using pii_t = std::pair<axis::index_type, axis::index_type>;
axis::regular<double, def, def, axis::option::growth_t> a{1, 0, 1};
axis::regular<double, def, def, op::growth_t> 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<double>::infinity()), pii_t(-1, 0));
}
// axis with overflow bin represents open interval
{
axis::regular<double, def, def, op::overflow_t> 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<double, def, def, op::none_t> 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<double, def, def, axis::option::none_t>(5, 0, 1), 0,
5);
test_axis_iterator(axis::regular<double, def, def, op::none_t>(5, 0, 1), 0, 5);
test_axis_iterator(axis::circular<>(5, 0, 1), 0, 5);
}

View File

@ -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<double>::infinity();
@ -104,7 +105,7 @@ int main() {
// axis::variable circular
{
axis::variable<double, axis::null_type, axis::option::circular_t> a{-1, 1, 2};
axis::variable<double, axis::null_type, op::circular_t> 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::index_type, axis::index_type>;
axis::variable<double, axis::null_type, axis::option::growth_t> a{0, 1};
axis::variable<double, axis::null_type, op::growth_t> 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<double, boost::use_default, op::overflow_t> 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<double, boost::use_default, op::none_t> 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<double, axis::null_type, axis::option::circular_t>{1, 2, 3}, 0, 2);
test_axis_iterator(axis::variable<double, axis::null_type, op::circular_t>{1, 2, 3},
0, 2);
}
// shrink and rebin
@ -176,7 +199,7 @@ int main() {
// shrink and rebin with circular option
{
using A = axis::variable<double, axis::null_type, axis::option::circular_t>;
using A = axis::variable<double, axis::null_type, op::circular_t>;
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);