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,8 +26,7 @@ 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
@ -41,7 +40,7 @@ namespace axis {
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,8 +29,7 @@ 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.
@ -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,11 +165,18 @@ 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.
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.
@ -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);