1 // Copyright 2018 Hans Dembinski
2 //
3 // Distributed under the Boost Software License, Version 1.0.
4 // (See accompanying file LICENSE_1_0.txt
5 // or copy at http://www.boost.org/LICENSE_1_0.txt)
6
7 #ifndef BOOST_HISTOGRAM_AXIS_TRAITS_HPP
8 #define BOOST_HISTOGRAM_AXIS_TRAITS_HPP
9
10 #include <boost/histogram/axis/option.hpp>
11 #include <boost/histogram/detail/args_type.hpp>
12 #include <boost/histogram/detail/detect.hpp>
13 #include <boost/histogram/detail/priority.hpp>
14 #include <boost/histogram/detail/static_if.hpp>
15 #include <boost/histogram/detail/try_cast.hpp>
16 #include <boost/histogram/detail/type_name.hpp>
17 #include <boost/histogram/fwd.hpp>
18 #include <boost/mp11/algorithm.hpp>
19 #include <boost/mp11/list.hpp>
20 #include <boost/mp11/utility.hpp>
21 #include <boost/throw_exception.hpp>
22 #include <boost/variant2/variant.hpp>
23 #include <stdexcept>
24 #include <string>
25 #include <utility>
26
27 namespace boost {
28 namespace histogram {
29 namespace detail {
30
31 static axis::null_type null_value;
32
33 template <class Axis>
34 struct value_type_deducer {
35 using type =
36 std::remove_cv_t<std::remove_reference_t<detail::arg_type<decltype(&Axis::index)>>>;
37 };
38
39 template <class Axis>
40 auto traits_options(priority<2>) -> axis::option::bitset<Axis::options()>;
41
42 template <class Axis>
43 auto traits_options(priority<1>) -> decltype(&Axis::update, axis::option::growth_t{});
44
45 template <class Axis>
46 auto traits_options(priority<0>) -> axis::option::none_t;
47
48 template <class Axis>
49 auto traits_is_inclusive(priority<1>) -> std::integral_constant<bool, Axis::inclusive()>;
50
51 template <class Axis>
52 auto traits_is_inclusive(priority<0>)
53 -> decltype(traits_options<Axis>(priority<2>{})
54 .test(axis::option::underflow | axis::option::overflow));
55
56 template <class Axis>
57 auto traits_is_ordered(priority<1>) -> std::integral_constant<bool, Axis::ordered()>;
58
59 template <class Axis, class ValueType = typename value_type_deducer<Axis>::type>
60 auto traits_is_ordered(priority<0>) -> typename std::is_arithmetic<ValueType>::type;
61
62 template <class I, class D, class A,
63 class J = std::decay_t<arg_type<decltype(&A::value)>>>
value_method_switch(I && i,D && d,const A & a,priority<1>)64 decltype(auto) value_method_switch(I&& i, D&& d, const A& a, priority<1>) {
65 return static_if<std::is_same<J, axis::index_type>>(std::forward<I>(i),
66 std::forward<D>(d), a);
67 }
68
69 template <class I, class D, class A>
value_method_switch(I &&,D &&,const A &,priority<0>)70 double value_method_switch(I&&, D&&, const A&, priority<0>) {
71 // comma trick to make all compilers happy; some would complain about
72 // unreachable code after the throw, others about a missing return
73 return BOOST_THROW_EXCEPTION(
74 std::runtime_error(type_name<A>() + " has no value method")),
75 double{};
76 }
77
78 struct variant_access {
79 template <class T, class Variant>
get_ifboost::histogram::detail::variant_access80 static auto get_if(Variant* v) noexcept {
81 using T0 = mp11::mp_first<std::decay_t<Variant>>;
82 return static_if<std::is_pointer<T0>>(
83 [](auto* vptr) {
84 using TP = mp11::mp_if<std::is_const<std::remove_pointer_t<T0>>, const T*, T*>;
85 auto ptp = variant2::get_if<TP>(vptr);
86 return ptp ? *ptp : nullptr;
87 },
88 [](auto* vptr) { return variant2::get_if<T>(vptr); }, &(v->impl));
89 }
90
91 template <class T0, class Visitor, class Variant>
visit_implboost::histogram::detail::variant_access92 static decltype(auto) visit_impl(mp11::mp_identity<T0>, Visitor&& vis, Variant&& v) {
93 return variant2::visit(std::forward<Visitor>(vis), v.impl);
94 }
95
96 template <class T0, class Visitor, class Variant>
visit_implboost::histogram::detail::variant_access97 static decltype(auto) visit_impl(mp11::mp_identity<T0*>, Visitor&& vis, Variant&& v) {
98 return variant2::visit(
99 [&vis](auto&& x) -> decltype(auto) { return std::forward<Visitor>(vis)(*x); },
100 v.impl);
101 }
102
103 template <class Visitor, class Variant>
visitboost::histogram::detail::variant_access104 static decltype(auto) visit(Visitor&& vis, Variant&& v) {
105 using T0 = mp11::mp_first<std::decay_t<Variant>>;
106 return visit_impl(mp11::mp_identity<T0>{}, std::forward<Visitor>(vis),
107 std::forward<Variant>(v));
108 }
109 };
110
111 template <class A>
112 decltype(auto) metadata_impl(A&& a, decltype(a.metadata(), 0)) {
113 return std::forward<A>(a).metadata();
114 }
115
116 template <class A>
metadata_impl(A &&,float)117 axis::null_type& metadata_impl(A&&, float) {
118 return detail::null_value;
119 }
120
121 } // namespace detail
122
123 namespace axis {
124 namespace traits {
125
126 /** Value type for axis type.
127
128 Doxygen does not render this well. This is a meta-function (template alias), it accepts
129 an axis type and returns the value type.
130
131 The value type is deduced from the argument of the `Axis::index` method. Const
132 references are decayed to the their value types, for example, the type deduced for
133 `Axis::index(const int&)` is `int`.
134
135 The deduction always succeeds if the axis type models the Axis concept correctly. Errors
136 come from violations of the concept, in particular, an index method that is templated or
137 overloaded is not allowed.
138
139 @tparam Axis axis type.
140 */
141 template <class Axis>
142 #ifndef BOOST_HISTOGRAM_DOXYGEN_INVOKED
143 using value_type = typename detail::value_type_deducer<Axis>::type;
144 #else
145 struct value_type;
146 #endif
147
148 /** Whether axis is continuous or discrete.
149
150 Doxygen does not render this well. This is a meta-function (template alias), it accepts
151 an axis type and returns a compile-time boolean.
152
153 If the boolean is true, the axis is continuous (covers a continuous range of values).
154 Otherwise it is discrete (covers discrete values).
155 */
156 template <class Axis>
157 #ifndef BOOST_HISTOGRAM_DOXYGEN_INVOKED
158 using is_continuous = typename std::is_floating_point<traits::value_type<Axis>>::type;
159 #else
160 struct is_continuous;
161 #endif
162
163 /** Meta-function to detect whether an axis is reducible.
164
165 Doxygen does not render this well. This is a meta-function (template alias), it accepts
166 an axis type and represents compile-time boolean which is true or false, depending on
167 whether the axis can be reduced with boost::histogram::algorithm::reduce().
168
169 An axis can be made reducible by adding a special constructor, see Axis concept for
170 details.
171
172 @tparam Axis axis type.
173 */
174 template <class Axis>
175 #ifndef BOOST_HISTOGRAM_DOXYGEN_INVOKED
176 using is_reducible = std::is_constructible<Axis, const Axis&, axis::index_type,
177 axis::index_type, unsigned>;
178 #else
179 struct is_reducible;
180 #endif
181
182 /** Get axis options for axis type.
183
184 Doxygen does not render this well. This is a meta-function (template alias), it accepts
185 an axis type and returns the boost::histogram::axis::option::bitset.
186
187 If Axis::options() is valid and constexpr, get_options is the corresponding
188 option type. Otherwise, it is boost::histogram::axis::option::growth_t, if the
189 axis has a method `update`, else boost::histogram::axis::option::none_t.
190
191 @tparam Axis axis type
192 */
193 template <class Axis>
194 #ifndef BOOST_HISTOGRAM_DOXYGEN_INVOKED
195 using get_options = decltype(detail::traits_options<Axis>(detail::priority<2>{}));
196
197 template <class Axis>
198 using static_options [[deprecated("use get_options instead")]] = get_options<Axis>;
199
200 #else
201 struct get_options;
202 #endif
203
204 /** Meta-function to detect whether an axis is inclusive.
205
206 Doxygen does not render this well. This is a meta-function (template alias), it accepts
207 an axis type and represents compile-time boolean which is true or false, depending on
208 whether the axis is inclusive or not.
209
210 An axis with underflow and overflow bins is always inclusive, but an axis may be
211 inclusive under other conditions. The meta-function checks for the method `constexpr
212 static bool inclusive()`, and uses the result. If this method is not present, it uses
213 get_options<Axis> and checks whether the underflow and overflow bits are present.
214
215 An inclusive axis has a bin for every possible input value. A histogram which consists
216 only of inclusive axes can be filled more efficiently, since input values always
217 end up in a valid cell and there is no need to keep track of input tuples that need to
218 be discarded.
219
220 @tparam Axis axis type
221 */
222 template <class Axis>
223 #ifndef BOOST_HISTOGRAM_DOXYGEN_INVOKED
224 using is_inclusive = decltype(detail::traits_is_inclusive<Axis>(detail::priority<1>{}));
225
226 template <class Axis>
227 using static_is_inclusive [[deprecated("use is_inclusive instead")]] = is_inclusive<Axis>;
228
229 #else
230 struct is_inclusive;
231 #endif
232
233 /** Meta-function to detect whether an axis is ordered.
234
235 Doxygen does not render this well. This is a meta-function (template alias), it accepts
236 an axis type and returns a compile-time boolean. If the boolean is true, the axis is
237 ordered.
238
239 The meta-function checks for the method `constexpr static bool ordered()`, and uses the
240 result. If this method is not present, it returns true if the value type of the Axis is
241 arithmetic and false otherwise.
242
243 An ordered axis has a value type that is ordered, which means that indices i <
244 j < k implies either value(i) < value(j) < value(k) or value(i) > value(j) > value(k)
245 for all i,j,k. For example, the integer axis is ordered, but the category axis is not.
246 Axis which are not ordered must not have underflow bins, because they only have an
247 "other" category, which is identified with the overflow bin if it is available.
248
249 @tparam Axis axis type
250 */
251 template <class Axis>
252 #ifndef BOOST_HISTOGRAM_DOXYGEN_INVOKED
253 using is_ordered = decltype(detail::traits_is_ordered<Axis>(detail::priority<1>{}));
254 #else
255 struct is_ordered;
256 #endif
257
258 /** Returns axis options as unsigned integer.
259
260 See get_options for details.
261
262 @param axis any axis instance
263 */
264 template <class Axis>
options(const Axis & axis)265 constexpr unsigned options(const Axis& axis) noexcept {
266 (void)axis;
267 return get_options<Axis>::value;
268 }
269
270 // specialization for variant
271 template <class... Ts>
options(const variant<Ts...> & axis)272 unsigned options(const variant<Ts...>& axis) noexcept {
273 return axis.options();
274 }
275
276 /** Returns true if axis is inclusive or false.
277
278 See is_inclusive for details.
279
280 @param axis any axis instance
281 */
282 template <class Axis>
inclusive(const Axis & axis)283 constexpr bool inclusive(const Axis& axis) noexcept {
284 (void)axis;
285 return is_inclusive<Axis>::value;
286 }
287
288 // specialization for variant
289 template <class... Ts>
inclusive(const variant<Ts...> & axis)290 bool inclusive(const variant<Ts...>& axis) noexcept {
291 return axis.inclusive();
292 }
293
294 /** Returns true if axis is ordered or false.
295
296 See is_ordered for details.
297
298 @param axis any axis instance
299 */
300 template <class Axis>
ordered(const Axis & axis)301 constexpr bool ordered(const Axis& axis) noexcept {
302 (void)axis;
303 return is_ordered<Axis>::value;
304 }
305
306 // specialization for variant
307 template <class... Ts>
ordered(const variant<Ts...> & axis)308 bool ordered(const variant<Ts...>& axis) noexcept {
309 return axis.ordered();
310 }
311
312 /** Returns true if axis is continuous or false.
313
314 See is_continuous for details.
315
316 @param axis any axis instance
317 */
318 template <class Axis>
continuous(const Axis & axis)319 constexpr bool continuous(const Axis& axis) noexcept {
320 (void)axis;
321 return is_continuous<Axis>::value;
322 }
323
324 // specialization for variant
325 template <class... Ts>
continuous(const variant<Ts...> & axis)326 bool continuous(const variant<Ts...>& axis) noexcept {
327 return axis.continuous();
328 }
329
330 /** Returns axis size plus any extra bins for under- and overflow.
331
332 @param axis any axis instance
333 */
334 template <class Axis>
extent(const Axis & axis)335 index_type extent(const Axis& axis) noexcept {
336 const auto opt = options(axis);
337 return axis.size() + (opt & option::underflow ? 1 : 0) +
338 (opt & option::overflow ? 1 : 0);
339 }
340
341 /** Returns reference to metadata of an axis.
342
343 If the expression x.metadata() for an axis instance `x` (maybe const) is valid, return
344 the result. Otherwise, return a reference to a static instance of
345 boost::histogram::axis::null_type.
346
347 @param axis any axis instance
348 */
349 template <class Axis>
metadata(Axis && axis)350 decltype(auto) metadata(Axis&& axis) noexcept {
351 return detail::metadata_impl(std::forward<Axis>(axis), 0);
352 }
353
354 /** Returns axis value for index.
355
356 If the axis has no `value` method, throw std::runtime_error. If the method exists and
357 accepts a floating point index, pass the index and return the result. If the method
358 exists but accepts only integer indices, cast the floating point index to int, pass this
359 index and return the result.
360
361 @param axis any axis instance
362 @param index floating point axis index
363 */
364 template <class Axis>
value(const Axis & axis,real_index_type index)365 decltype(auto) value(const Axis& axis, real_index_type index) {
366 return detail::value_method_switch(
367 [index](const auto& a) { return a.value(static_cast<index_type>(index)); },
368 [index](const auto& a) { return a.value(index); }, axis, detail::priority<1>{});
369 }
370
371 /** Returns axis value for index if it is convertible to target type or throws.
372
373 Like boost::histogram::axis::traits::value, but converts the result into the requested
374 return type. If the conversion is not possible, throws std::runtime_error.
375
376 @tparam Result requested return type
377 @tparam Axis axis type
378 @param axis any axis instance
379 @param index floating point axis index
380 */
381 template <class Result, class Axis>
value_as(const Axis & axis,real_index_type index)382 Result value_as(const Axis& axis, real_index_type index) {
383 return detail::try_cast<Result, std::runtime_error>(
384 value(axis, index)); // avoid conversion warning
385 }
386
387 /** Returns axis index for value.
388
389 Throws std::invalid_argument if the value argument is not implicitly convertible.
390
391 @param axis any axis instance
392 @param value argument to be passed to `index` method
393 */
394 template <class Axis, class U>
index(const Axis & axis,const U & value)395 axis::index_type index(const Axis& axis, const U& value) noexcept(
396 std::is_convertible<U, value_type<Axis>>::value) {
397 return axis.index(detail::try_cast<value_type<Axis>, std::invalid_argument>(value));
398 }
399
400 // specialization for variant
401 template <class... Ts, class U>
index(const variant<Ts...> & axis,const U & value)402 axis::index_type index(const variant<Ts...>& axis, const U& value) {
403 return axis.index(value);
404 }
405
406 /** Return axis rank (how many arguments it processes).
407
408 @param axis any axis instance
409 */
410 template <class Axis>
rank(const Axis & axis)411 constexpr unsigned rank(const Axis& axis) {
412 (void)axis;
413 using T = value_type<Axis>;
414 // cannot use mp_eval_or since T could be a fixed-sized sequence
415 return mp11::mp_eval_if_not<detail::is_tuple<T>, mp11::mp_size_t<1>, mp11::mp_size,
416 T>::value;
417 }
418
419 // specialization for variant
420 template <class... Ts>
rank(const axis::variant<Ts...> & axis)421 unsigned rank(const axis::variant<Ts...>& axis) {
422 return detail::variant_access::visit([](const auto& a) { return rank(a); }, axis);
423 }
424
425 /** Returns pair of axis index and shift for the value argument.
426
427 Throws `std::invalid_argument` if the value argument is not implicitly convertible to
428 the argument expected by the `index` method. If the result of
429 boost::histogram::axis::traits::get_options<decltype(axis)> has the growth flag set,
430 call `update` method with the argument and return the result. Otherwise, call `index`
431 and return the pair of the result and a zero shift.
432
433 @param axis any axis instance
434 @param value argument to be passed to `update` or `index` method
435 */
436 template <class Axis, class U>
update(Axis & axis,const U & value)437 std::pair<index_type, index_type> update(Axis& axis, const U& value) noexcept(
438 std::is_convertible<U, value_type<Axis>>::value) {
439 return detail::static_if_c<get_options<Axis>::test(option::growth)>(
440 [&value](auto& a) {
441 return a.update(detail::try_cast<value_type<Axis>, std::invalid_argument>(value));
442 },
443 [&value](auto& a) -> std::pair<index_type, index_type> {
444 return {index(a, value), 0};
445 },
446 axis);
447 }
448
449 // specialization for variant
450 template <class... Ts, class U>
update(variant<Ts...> & axis,const U & value)451 std::pair<index_type, index_type> update(variant<Ts...>& axis, const U& value) {
452 return visit([&value](auto& a) { return a.update(value); }, axis);
453 }
454
455 /** Returns bin width at axis index.
456
457 If the axis has no `value` method, throw std::runtime_error. If the method exists and
458 accepts a floating point index, return the result of `axis.value(index + 1) -
459 axis.value(index)`. If the method exists but accepts only integer indices, return 0.
460
461 @param axis any axis instance
462 @param index bin index
463 */
464 template <class Axis>
width(const Axis & axis,index_type index)465 decltype(auto) width(const Axis& axis, index_type index) {
466 return detail::value_method_switch(
467 [](const auto&) { return 0; },
468 [index](const auto& a) { return a.value(index + 1) - a.value(index); }, axis,
469 detail::priority<1>{});
470 }
471
472 /** Returns bin width at axis index.
473
474 Like boost::histogram::axis::traits::width, but converts the result into the requested
475 return type. If the conversion is not possible, throw std::runtime_error.
476
477 @param axis any axis instance
478 @param index bin index
479 */
480 template <class Result, class Axis>
width_as(const Axis & axis,index_type index)481 Result width_as(const Axis& axis, index_type index) {
482 return detail::value_method_switch(
483 [](const auto&) { return Result{}; },
484 [index](const auto& a) {
485 return detail::try_cast<Result, std::runtime_error>(a.value(index + 1) -
486 a.value(index));
487 },
488 axis, detail::priority<1>{});
489 }
490
491 } // namespace traits
492 } // namespace axis
493 } // namespace histogram
494 } // namespace boost
495
496 #endif
497