reduce.hpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. // Copyright 2018-2019 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. #ifndef BOOST_HISTOGRAM_ALGORITHM_REDUCE_HPP
  7. #define BOOST_HISTOGRAM_ALGORITHM_REDUCE_HPP
  8. #include <boost/histogram/axis/traits.hpp>
  9. #include <boost/histogram/detail/axes.hpp>
  10. #include <boost/histogram/detail/make_default.hpp>
  11. #include <boost/histogram/detail/reduce_command.hpp>
  12. #include <boost/histogram/detail/static_if.hpp>
  13. #include <boost/histogram/fwd.hpp>
  14. #include <boost/histogram/indexed.hpp>
  15. #include <boost/histogram/unsafe_access.hpp>
  16. #include <boost/throw_exception.hpp>
  17. #include <cassert>
  18. #include <cmath>
  19. #include <initializer_list>
  20. #include <stdexcept>
  21. #include <string>
  22. namespace boost {
  23. namespace histogram {
  24. namespace algorithm {
  25. /** Holder for a reduce command.
  26. Use this type to store reduce commands in a container. The internals of this type are an
  27. implementation detail.
  28. */
  29. using reduce_command = detail::reduce_command;
  30. /** Shrink command to be used in `reduce`.
  31. Command is applied to axis with given index.
  32. Shrinking is based on an inclusive value interval. The bin which contains the first
  33. value starts the range of bins to keep. The bin which contains the second value is the
  34. last included in that range. When the second value is exactly equal to a lower bin edge,
  35. then the previous bin is the last in the range.
  36. The counts in removed bins are added to the corresponding underflow and overflow bins,
  37. if they are present. If they are not present, the counts are discarded. Also see
  38. `crop`, which always discards the counts.
  39. @param iaxis which axis to operate on.
  40. @param lower bin which contains lower is first to be kept.
  41. @param upper bin which contains upper is last to be kept, except if upper is equal to
  42. the lower edge.
  43. */
  44. inline reduce_command shrink(unsigned iaxis, double lower, double upper) {
  45. if (lower == upper)
  46. BOOST_THROW_EXCEPTION(std::invalid_argument("lower != upper required"));
  47. reduce_command r;
  48. r.iaxis = iaxis;
  49. r.range = reduce_command::range_t::values;
  50. r.begin.value = lower;
  51. r.end.value = upper;
  52. r.merge = 1;
  53. r.crop = false;
  54. return r;
  55. }
  56. /** Shrink command to be used in `reduce`.
  57. Command is applied to corresponding axis in order of reduce arguments.
  58. Shrinking is based on an inclusive value interval. The bin which contains the first
  59. value starts the range of bins to keep. The bin which contains the second value is the
  60. last included in that range. When the second value is exactly equal to a lower bin edge,
  61. then the previous bin is the last in the range.
  62. The counts in removed bins are added to the corresponding underflow and overflow bins,
  63. if they are present. If they are not present, the counts are discarded. Also see
  64. `crop`, which always discards the counts.
  65. @param lower bin which contains lower is first to be kept.
  66. @param upper bin which contains upper is last to be kept, except if upper is equal to
  67. the lower edge.
  68. */
  69. inline reduce_command shrink(double lower, double upper) {
  70. return shrink(reduce_command::unset, lower, upper);
  71. }
  72. /** Crop command to be used in `reduce`.
  73. Command is applied to axis with given index.
  74. Works like `shrink` (see shrink documentation for details), but counts in removed
  75. bins are always discarded, whether underflow and overflow bins are present or not.
  76. @param iaxis which axis to operate on.
  77. @param lower bin which contains lower is first to be kept.
  78. @param upper bin which contains upper is last to be kept, except if upper is equal to
  79. the lower edge.
  80. */
  81. inline reduce_command crop(unsigned iaxis, double lower, double upper) {
  82. reduce_command r = shrink(iaxis, lower, upper);
  83. r.crop = true;
  84. return r;
  85. }
  86. /** Crop command to be used in `reduce`.
  87. Command is applied to corresponding axis in order of reduce arguments.
  88. Works like `shrink` (see shrink documentation for details), but counts in removed bins
  89. are discarded, whether underflow and overflow bins are present or not. If the cropped
  90. range goes beyond the axis range, then the content of the underflow
  91. or overflow bin which overlaps with the range is kept.
  92. If the counts in an existing underflow or overflow bin are discared by the crop, the
  93. corresponding memory cells are not physically removed. Only their contents are set to
  94. zero. This technical limitation may be lifted in the future, then crop may completely
  95. remove the cropped memory cells.
  96. @param lower bin which contains lower is first to be kept.
  97. @param upper bin which contains upper is last to be kept, except if upper is equal to
  98. the lower edge.
  99. */
  100. inline reduce_command crop(double lower, double upper) {
  101. return crop(reduce_command::unset, lower, upper);
  102. }
  103. /// Whether to behave like `shrink` or `crop` regarding removed bins.
  104. enum class slice_mode { shrink, crop };
  105. /** Slice command to be used in `reduce`.
  106. Command is applied to axis with given index.
  107. Slicing works like `shrink` or `crop`, but uses bin indices instead of values.
  108. @param iaxis which axis to operate on.
  109. @param begin first index that should be kept.
  110. @param end one past the last index that should be kept.
  111. @param mode whether to behave like `shrink` or `crop` regarding removed bins.
  112. */
  113. inline reduce_command slice(unsigned iaxis, axis::index_type begin, axis::index_type end,
  114. slice_mode mode = slice_mode::shrink) {
  115. if (!(begin < end))
  116. BOOST_THROW_EXCEPTION(std::invalid_argument("begin < end required"));
  117. reduce_command r;
  118. r.iaxis = iaxis;
  119. r.range = reduce_command::range_t::indices;
  120. r.begin.index = begin;
  121. r.end.index = end;
  122. r.merge = 1;
  123. r.crop = mode == slice_mode::crop;
  124. return r;
  125. }
  126. /** Slice command to be used in `reduce`.
  127. Command is applied to corresponding axis in order of reduce arguments.
  128. Slicing works like `shrink` or `crop`, but uses bin indices instead of values.
  129. @param begin first index that should be kept.
  130. @param end one past the last index that should be kept.
  131. @param mode whether to behave like `shrink` or `crop` regarding removed bins.
  132. */
  133. inline reduce_command slice(axis::index_type begin, axis::index_type end,
  134. slice_mode mode = slice_mode::shrink) {
  135. return slice(reduce_command::unset, begin, end, mode);
  136. }
  137. /** Rebin command to be used in `reduce`.
  138. Command is applied to axis with given index.
  139. The command merges N adjacent bins into one. This makes the axis coarser and the bins
  140. wider. The original number of bins is divided by N. If there is a rest to this devision,
  141. the axis is implicitly shrunk at the upper end by that rest.
  142. @param iaxis which axis to operate on.
  143. @param merge how many adjacent bins to merge into one.
  144. */
  145. inline reduce_command rebin(unsigned iaxis, unsigned merge) {
  146. if (merge == 0) BOOST_THROW_EXCEPTION(std::invalid_argument("merge > 0 required"));
  147. reduce_command r;
  148. r.iaxis = iaxis;
  149. r.merge = merge;
  150. r.range = reduce_command::range_t::none;
  151. r.crop = false;
  152. return r;
  153. }
  154. /** Rebin command to be used in `reduce`.
  155. Command is applied to corresponding axis in order of reduce arguments.
  156. The command merges N adjacent bins into one. This makes the axis coarser and the bins
  157. wider. The original number of bins is divided by N. If there is a rest to this devision,
  158. the axis is implicitly shrunk at the upper end by that rest.
  159. @param merge how many adjacent bins to merge into one.
  160. */
  161. inline reduce_command rebin(unsigned merge) {
  162. return rebin(reduce_command::unset, merge);
  163. }
  164. /** Shrink and rebin command to be used in `reduce`.
  165. Command is applied to corresponding axis in order of reduce arguments.
  166. To shrink(unsigned, double, double) and rebin(unsigned, unsigned) in one command (see
  167. the respective commands for more details). Equivalent to passing both commands for the
  168. same axis to `reduce`.
  169. @param iaxis which axis to operate on.
  170. @param lower lowest bound that should be kept.
  171. @param upper highest bound that should be kept. If upper is inside bin interval, the
  172. whole interval is removed.
  173. @param merge how many adjacent bins to merge into one.
  174. */
  175. inline reduce_command shrink_and_rebin(unsigned iaxis, double lower, double upper,
  176. unsigned merge) {
  177. reduce_command r = shrink(iaxis, lower, upper);
  178. r.merge = rebin(merge).merge;
  179. return r;
  180. }
  181. /** Shrink and rebin command to be used in `reduce`.
  182. Command is applied to corresponding axis in order of reduce arguments.
  183. To `shrink` and `rebin` in one command (see the respective commands for more
  184. details). Equivalent to passing both commands for the same axis to `reduce`.
  185. @param lower lowest bound that should be kept.
  186. @param upper highest bound that should be kept. If upper is inside bin interval, the
  187. whole interval is removed.
  188. @param merge how many adjacent bins to merge into one.
  189. */
  190. inline reduce_command shrink_and_rebin(double lower, double upper, unsigned merge) {
  191. return shrink_and_rebin(reduce_command::unset, lower, upper, merge);
  192. }
  193. /** Crop and rebin command to be used in `reduce`.
  194. Command is applied to axis with given index.
  195. To `crop` and `rebin` in one command (see the respective commands for more
  196. details). Equivalent to passing both commands for the same axis to `reduce`.
  197. @param iaxis which axis to operate on.
  198. @param lower lowest bound that should be kept.
  199. @param upper highest bound that should be kept. If upper is inside bin interval,
  200. the whole interval is removed.
  201. @param merge how many adjacent bins to merge into one.
  202. */
  203. inline reduce_command crop_and_rebin(unsigned iaxis, double lower, double upper,
  204. unsigned merge) {
  205. reduce_command r = crop(iaxis, lower, upper);
  206. r.merge = rebin(merge).merge;
  207. return r;
  208. }
  209. /** Crop and rebin command to be used in `reduce`.
  210. Command is applied to corresponding axis in order of reduce arguments.
  211. To `crop` and `rebin` in one command (see the respective commands for more
  212. details). Equivalent to passing both commands for the same axis to `reduce`.
  213. @param lower lowest bound that should be kept.
  214. @param upper highest bound that should be kept. If upper is inside bin interval,
  215. the whole interval is removed.
  216. @param merge how many adjacent bins to merge into one.
  217. */
  218. inline reduce_command crop_and_rebin(double lower, double upper, unsigned merge) {
  219. return crop_and_rebin(reduce_command::unset, lower, upper, merge);
  220. }
  221. /** Slice and rebin command to be used in `reduce`.
  222. Command is applied to axis with given index.
  223. To `slice` and `rebin` in one command (see the respective commands for more
  224. details). Equivalent to passing both commands for the same axis to `reduce`.
  225. @param iaxis which axis to operate on.
  226. @param begin first index that should be kept.
  227. @param end one past the last index that should be kept.
  228. @param merge how many adjacent bins to merge into one.
  229. @param mode slice mode, see slice_mode.
  230. */
  231. inline reduce_command slice_and_rebin(unsigned iaxis, axis::index_type begin,
  232. axis::index_type end, unsigned merge,
  233. slice_mode mode = slice_mode::shrink) {
  234. reduce_command r = slice(iaxis, begin, end, mode);
  235. r.merge = rebin(merge).merge;
  236. return r;
  237. }
  238. /** Slice and rebin command to be used in `reduce`.
  239. Command is applied to corresponding axis in order of reduce arguments.
  240. To `slice` and `rebin` in one command (see the respective commands for more
  241. details). Equivalent to passing both commands for the same axis to `reduce`.
  242. @param begin first index that should be kept.
  243. @param end one past the last index that should be kept.
  244. @param merge how many adjacent bins to merge into one.
  245. @param mode slice mode, see slice_mode.
  246. */
  247. inline reduce_command slice_and_rebin(axis::index_type begin, axis::index_type end,
  248. unsigned merge,
  249. slice_mode mode = slice_mode::shrink) {
  250. return slice_and_rebin(reduce_command::unset, begin, end, merge, mode);
  251. }
  252. /** Shrink, crop, slice, and/or rebin axes of a histogram.
  253. Returns a new reduced histogram and leaves the original histogram untouched.
  254. The commands `rebin` and `shrink` or `slice` for the same axis are
  255. automatically combined, this is not an error. Passing a `shrink` and a `slice`
  256. command for the same axis or two `rebin` commands triggers an `invalid_argument`
  257. exception. Trying to reducing a non-reducible axis triggers an `invalid_argument`
  258. exception. Histograms with non-reducible axes can still be reduced along the
  259. other axes that are reducible.
  260. An overload allows one to pass reduce_command as positional arguments.
  261. @param hist original histogram.
  262. @param options iterable sequence of reduce commands: `shrink`, `slice`, `rebin`,
  263. `shrink_and_rebin`, or `slice_and_rebin`. The element type of the iterable should be
  264. `reduce_command`.
  265. */
  266. template <class Histogram, class Iterable, class = detail::requires_iterable<Iterable>>
  267. Histogram reduce(const Histogram& hist, const Iterable& options) {
  268. using axis::index_type;
  269. const auto& old_axes = unsafe_access::axes(hist);
  270. auto opts = detail::make_stack_buffer(old_axes, reduce_command{});
  271. detail::normalize_reduce_commands(opts, options);
  272. auto axes =
  273. detail::axes_transform(old_axes, [&opts](std::size_t iaxis, const auto& a_in) {
  274. using A = std::decay_t<decltype(a_in)>;
  275. using AO = axis::traits::get_options<A>;
  276. auto& o = opts[iaxis];
  277. o.is_ordered = axis::traits::ordered(a_in);
  278. if (o.merge > 0) { // option is set?
  279. o.use_underflow_bin = AO::test(axis::option::underflow);
  280. o.use_overflow_bin = AO::test(axis::option::overflow);
  281. return detail::static_if_c<axis::traits::is_reducible<A>::value>(
  282. [&o](const auto& a_in) {
  283. if (o.range == reduce_command::range_t::none) {
  284. // no range restriction, pure rebin
  285. o.begin.index = 0;
  286. o.end.index = a_in.size();
  287. } else {
  288. // range striction, convert values to indices as needed
  289. if (o.range == reduce_command::range_t::values) {
  290. const auto end_value = o.end.value;
  291. o.begin.index = axis::traits::index(a_in, o.begin.value);
  292. o.end.index = axis::traits::index(a_in, o.end.value);
  293. // end = index + 1, unless end_value equal to upper bin edge
  294. if (axis::traits::value_as<double>(a_in, o.end.index) != end_value)
  295. ++o.end.index;
  296. }
  297. // crop flow bins if index range does not include them
  298. if (o.crop) {
  299. o.use_underflow_bin &= o.begin.index < 0;
  300. o.use_overflow_bin &= o.end.index > a_in.size();
  301. }
  302. // now limit [begin, end] to [0, size()]
  303. if (o.begin.index < 0) o.begin.index = 0;
  304. if (o.end.index > a_in.size()) o.end.index = a_in.size();
  305. }
  306. // shorten the index range to a multiple of o.merge;
  307. // example [1, 4] with merge = 2 is reduced to [1, 3]
  308. o.end.index -=
  309. (o.end.index - o.begin.index) % static_cast<index_type>(o.merge);
  310. using A = std::decay_t<decltype(a_in)>;
  311. return A(a_in, o.begin.index, o.end.index, o.merge);
  312. },
  313. [iaxis](const auto& a_in) {
  314. return BOOST_THROW_EXCEPTION(std::invalid_argument(
  315. "axis " + std::to_string(iaxis) + " is not reducible")),
  316. a_in;
  317. },
  318. a_in);
  319. } else {
  320. // command was not set for this axis; fill noop values and copy original axis
  321. o.use_underflow_bin = AO::test(axis::option::underflow);
  322. o.use_overflow_bin = AO::test(axis::option::overflow);
  323. o.merge = 1;
  324. o.begin.index = 0;
  325. o.end.index = a_in.size();
  326. return a_in;
  327. }
  328. });
  329. auto result =
  330. Histogram(std::move(axes), detail::make_default(unsafe_access::storage(hist)));
  331. auto idx = detail::make_stack_buffer<index_type>(unsafe_access::axes(result));
  332. for (auto&& x : indexed(hist, coverage::all)) {
  333. auto i = idx.begin();
  334. auto o = opts.begin();
  335. bool skip = false;
  336. for (auto j : x.indices()) {
  337. *i = (j - o->begin.index);
  338. if (o->is_ordered && *i <= -1) {
  339. *i = -1;
  340. if (!o->use_underflow_bin) skip = true;
  341. } else {
  342. if (*i >= 0)
  343. *i /= static_cast<index_type>(o->merge);
  344. else
  345. *i = o->end.index;
  346. const auto reduced_axis_end =
  347. (o->end.index - o->begin.index) / static_cast<index_type>(o->merge);
  348. if (*i >= reduced_axis_end) {
  349. *i = reduced_axis_end;
  350. if (!o->use_overflow_bin) skip = true;
  351. }
  352. }
  353. ++i;
  354. ++o;
  355. }
  356. if (!skip) result.at(idx) += *x;
  357. }
  358. return result;
  359. }
  360. /** Shrink, slice, and/or rebin axes of a histogram.
  361. Returns a new reduced histogram and leaves the original histogram untouched.
  362. The commands `rebin` and `shrink` or `slice` for the same axis are
  363. automatically combined, this is not an error. Passing a `shrink` and a `slice`
  364. command for the same axis or two `rebin` commands triggers an invalid_argument
  365. exception. It is safe to reduce histograms with some axis that are not reducible along
  366. the other axes. Trying to reducing a non-reducible axis triggers an invalid_argument
  367. exception.
  368. An overload allows one to pass an iterable of reduce_command.
  369. @param hist original histogram.
  370. @param opt first reduce command; one of `shrink`, `slice`, `rebin`,
  371. `shrink_and_rebin`, or `slice_or_rebin`.
  372. @param opts more reduce commands.
  373. */
  374. template <class Histogram, class... Ts>
  375. Histogram reduce(const Histogram& hist, const reduce_command& opt, const Ts&... opts) {
  376. // this must be in one line, because any of the ts could be a temporary
  377. return reduce(hist, std::initializer_list<reduce_command>{opt, opts...});
  378. }
  379. } // namespace algorithm
  380. } // namespace histogram
  381. } // namespace boost
  382. #endif