libtcspc C++ API
Streaming TCSPC and time tag data processing
Loading...
Searching...
No Matches
histogram_impl.hpp
1/*
2 * This file is part of libtcspc
3 * Copyright 2019-2026 Board of Regents of the University of Wisconsin System
4 * SPDX-License-Identifier: MIT
5 */
6
7#pragma once
8
9#include "arg_wrappers.hpp"
10#include "bin_increment_cluster_encoding.hpp"
11#include "common.hpp"
12
13#include <algorithm>
14#include <cassert>
15#include <cstddef>
16#include <functional>
17#include <iterator>
18#include <limits>
19#include <ostream>
20#include <span>
21#include <type_traits>
22#include <vector>
23
24namespace tcspc::internal {
25
26struct saturate_on_internal_overflow {
27 explicit saturate_on_internal_overflow() = default;
28};
29
30struct stop_on_internal_overflow {
31 explicit stop_on_internal_overflow() = default;
32};
33
34// Helper for bin_increment_cluster_journal.
35template <typename BinIndex>
36class bin_increment_cluster_journal_encoding_adapter {
37 std::reference_wrapper<std::vector<BinIndex>> vec;
38
39 public:
40 explicit bin_increment_cluster_journal_encoding_adapter(
41 std::vector<BinIndex> &storage)
42 : vec(storage) {}
43
44 [[nodiscard]] auto available_capacity() const -> std::size_t {
45 // Effectively limitless for 64-bit (short-circuit the check).
46 if constexpr (sizeof(std::size_t) >= 8)
47 return std::numeric_limits<std::size_t>::max();
48 else
49 return vec.get().max_size() - vec.get().size();
50 }
51
52 [[nodiscard]] auto make_space(std::size_t size) -> std::span<BinIndex> {
53 auto const old_size = vec.get().size();
54 vec.get().resize(old_size + size);
55 return std::span(vec.get()).subspan(old_size);
56 }
57};
58
59template <typename BinIndex> class bin_increment_cluster_journal {
60 std::vector<BinIndex> encoded;
61
62 public:
63 using bin_index_type = BinIndex;
64
65 // Slow, not for use in production. For use by tests.
66 [[nodiscard]] auto size() const noexcept -> std::size_t {
67 auto const decoder = bin_increment_cluster_decoder(std::span(encoded));
68 return std::size_t(std::distance(decoder.begin(), decoder.end()));
69 }
70
71 [[nodiscard]] constexpr auto empty() const noexcept -> bool {
72 return encoded.empty();
73 }
74
75 void clear() noexcept { encoded.clear(); }
76
77 void append_cluster(std::span<bin_index_type const> cluster) {
78 [[maybe_unused]] bool const ok = encode_bin_increment_cluster(
79 bin_increment_cluster_journal_encoding_adapter(encoded), cluster);
80 assert(ok); // Otherwise bad_alloc would have been thrown.
81 }
82
83 using const_iterator =
84 typename bin_increment_cluster_decoder<bin_index_type>::const_iterator;
85
86 [[nodiscard]] auto begin() const noexcept -> const_iterator {
87 auto const decoder =
88 bin_increment_cluster_decoder<bin_index_type>(encoded);
89 return const_iterator(decoder.begin());
90 }
91
92 [[nodiscard]] auto end() const noexcept -> const_iterator {
93 auto const decoder = bin_increment_cluster_decoder(std::span(encoded));
94 return const_iterator(decoder.end());
95 }
96
97 auto operator==(bin_increment_cluster_journal const &) const noexcept
98 -> bool = default;
99
100 friend auto operator<<(std::ostream &s,
101 bin_increment_cluster_journal const &j)
102 -> std::ostream & {
103 s << "journal(";
104 for (auto const cluster : j) {
105 s << '{';
106 for (auto const i : cluster)
107 s << i << ", ";
108 s << "}, ";
109 }
110 return s << ')';
111 }
112};
113
114// Can be used to disable journaling.
115template <typename BinIndex> struct null_journal {
116 using bin_index_type = BinIndex;
117 void append_cluster(std::span<bin_index_type const> /* cluster */) {}
118 void clear() noexcept {}
119};
120
121// Adapter which can attach to a span and treat it as a histogram.
122template <typename BinIndex, typename Bin, typename OverflowPolicy>
123class single_histogram {
124 public:
125 using bin_index_type = BinIndex;
126 using bin_type = Bin;
127 static_assert(same_as_any_of<OverflowPolicy, saturate_on_internal_overflow,
128 stop_on_internal_overflow>);
129
130 private:
131 std::span<bin_type> hist;
132 bin_type bin_max = 0;
133 std::size_t n_bins{};
134
135 public:
136 // Attach to 'histogram' and allow bin values up to max_per_bin.
137 explicit single_histogram(std::span<bin_type> histogram,
138 arg::max_per_bin<bin_type> max_per_bin,
139 arg::num_bins<std::size_t> num_bins) noexcept
140 : hist(histogram), bin_max(max_per_bin.value), n_bins(num_bins.value) {
141 }
142
143 // Reconstruct with new span.
144 explicit single_histogram(std::span<bin_type> histogram,
145 single_histogram const &params) noexcept
146 : single_histogram(histogram, arg::max_per_bin{params.bin_max},
147 arg::num_bins{params.n_bins}) {}
148
149 [[nodiscard]] auto num_bins() const noexcept -> std::size_t {
150 return n_bins;
151 }
152
153 // Clear the histogram by setting all bins to zero.
154 void clear() noexcept { std::fill(hist.begin(), hist.end(), bin_type(0)); }
155
156 auto max_per_bin() const noexcept -> bin_type { return bin_max; }
157
158 // Increment each bin in 'increments'. Return n_applied, the actual number
159 // of increments applied without saturation. This value is between 0 and
160 // increments.size(), inclusive.
161 auto apply_increments(std::span<bin_index_type const> increments)
162 -> std::size_t {
163 assert(not hist.empty());
164 std::size_t n_applied = 0;
165 for (auto it = increments.begin(); it != increments.end(); ++it) {
166 assert(*it >= 0 && *it < hist.size());
167 bin_type &bin = hist[*it];
168 if (bin < bin_max) {
169 ++bin;
170 ++n_applied;
171 } else if constexpr (std::is_same_v<
172 OverflowPolicy,
173 saturate_on_internal_overflow>) {
174 continue;
175 } else if constexpr (std::is_same_v<OverflowPolicy,
176 stop_on_internal_overflow>) {
177 return n_applied;
178 } else {
179 static_assert(always_false_v<OverflowPolicy>);
180 }
181 }
182 return n_applied;
183 }
184
185 // Undo the given 'increments'. Behavior is undefined unless 'increments'
186 // equal the values passed to apply_increments() in an immediately prior
187 // call. Not available in saturate mode.
188 void undo_increments(std::span<bin_index_type const> increments) {
189 static_assert(
190 std::is_same_v<OverflowPolicy, stop_on_internal_overflow>);
191 assert(not hist.empty());
192 for (bin_index_type i : increments) {
193 assert(i >= 0 && i < hist.size());
194 --hist[i];
195 }
196 }
197};
198
199// One scan (frame, cycle, or repeat unit) of an array of histograms. Adapter
200// which can attach to a span.
201template <typename BinIndex, typename Bin, typename OverflowPolicy>
202class multi_histogram {
203 public:
204 using bin_index_type = BinIndex;
205 using bin_type = Bin;
206 static_assert(same_as_any_of<OverflowPolicy, saturate_on_internal_overflow,
207 stop_on_internal_overflow>);
208
209 private:
210 std::span<bin_type> hist_arr;
211 std::size_t element_index = 0;
212 bin_type bin_max = 0;
213 std::size_t n_bins = 0;
214 std::size_t n_elements = 0;
215 bool need_to_clear = false;
216
217 public:
218 explicit multi_histogram(std::span<bin_type> hist_array,
219 arg::max_per_bin<bin_type> max_per_bin,
220 arg::num_bins<std::size_t> num_bins,
221 arg::num_elements<std::size_t> num_elements,
222 bool clear) noexcept
223 : hist_arr(hist_array), bin_max(max_per_bin.value),
224 n_bins(num_bins.value), n_elements(num_elements.value),
225 need_to_clear(clear) {
226 assert(hist_array.empty() || hist_array.size() == n_bins * n_elements);
227 }
228
229 // Reconstruct with new span.
230 explicit multi_histogram(std::span<bin_type> hist_array,
231 multi_histogram const &params,
232 bool clear) noexcept
233 : multi_histogram(hist_array, arg::max_per_bin{params.bin_max},
234 arg::num_bins{params.n_bins},
235 arg::num_elements{params.n_elements}, clear) {}
236
237 [[nodiscard]] auto max_per_bin() const noexcept -> bin_type {
238 return bin_max;
239 }
240
241 [[nodiscard]] auto num_bins() const noexcept -> std::size_t {
242 return n_bins;
243 }
244
245 [[nodiscard]] auto num_elements() const noexcept -> std::size_t {
246 return n_elements;
247 }
248
249 // True if any increment clusters have been applied (and not rolled back).
250 [[nodiscard]] auto is_started() const noexcept -> bool {
251 return element_index > 0;
252 }
253
254 // True if scan is complete (no further increment clusters may be applied).
255 [[nodiscard]] auto is_complete() const noexcept -> bool {
256 return element_index >= n_elements;
257 }
258
259 // True if every bin of every element histogram has been initialized
260 // (cleared if requested; original value accepted otherwise). The
261 // hist_array data is not suitable for subsequent use unless this condition
262 // has been reached. When clearing is requested, the data is consistent
263 // when:
264 // - All elements have had increments applied,
265 // - apply_increment_cluster() returned false,
266 // - skip_remaining() was called at least once, or
267 // - roll_back() was called at least once.
268 // When clearing is not requested, the data is also consistent when no
269 // operations have been performed yet.
270 [[nodiscard]] auto is_consistent() const noexcept -> bool {
271 return (not is_started() && not need_to_clear) || is_complete();
272 }
273
274 [[nodiscard]] auto next_element_index() const noexcept -> std::size_t {
275 return element_index;
276 }
277
278 // Apply 'cluster' to the next element of the array of histograms. Return
279 // true if the entire cluster could be applied (without saturation); false
280 // if there was saturation or we stopped and rolled back.
281 template <typename Journal>
282 auto apply_increment_cluster(std::span<bin_index_type const> cluster,
283 Journal &journal) -> bool {
284 static_assert(
285 std::is_same_v<typename Journal::bin_index_type, bin_index_type>);
286 assert(not hist_arr.empty());
287 assert(not is_complete());
288 single_histogram<bin_index_type, bin_type, OverflowPolicy> single_hist(
289 hist_arr.subspan(n_bins * element_index, n_bins),
290 arg::max_per_bin{bin_max}, arg::num_bins{n_bins});
291 if (need_to_clear)
292 single_hist.clear();
293
294 auto n_applied = single_hist.apply_increments(cluster);
295
296 if constexpr (std::is_same_v<OverflowPolicy,
297 saturate_on_internal_overflow>) {
298 journal.append_cluster(cluster);
299 ++element_index;
300 return n_applied == cluster.size();
301 } else if constexpr (std::is_same_v<OverflowPolicy,
302 stop_on_internal_overflow>) {
303 if (n_applied == cluster.size()) {
304 journal.append_cluster(cluster);
305 ++element_index;
306 return true;
307 }
308 // Always handle increment clusters atomically.
309 single_hist.undo_increments(cluster.first(n_applied));
310 skip_remaining();
311 return false;
312 } else {
313 static_assert(always_false_v<OverflowPolicy>);
314 }
315 }
316
317 // Call to cancel processing and ensure that the remaining elements are
318 // cleared (if so requested). After the call, is_complete() and
319 // is_consistent() become true.
320 void skip_remaining() {
321 assert(not hist_arr.empty());
322 if (need_to_clear) {
323 auto remaining = hist_arr.subspan(n_bins * element_index);
324 std::fill(remaining.begin(), remaining.end(), bin_type(0));
325 need_to_clear = false;
326 }
327 element_index = n_elements;
328 }
329
330 // Roll back journaled increments and recover the array of histograms to
331 // its original state (if it was not cleared) or zero. Not available in
332 // saturate mode.
333 template <typename Journal> void roll_back(Journal const &journal) {
334 static_assert(
335 std::is_same_v<typename Journal::bin_index_type, bin_index_type>);
336 static_assert(
337 std::is_same_v<OverflowPolicy, stop_on_internal_overflow>);
338 assert(not hist_arr.empty());
339 std::size_t cluster_index = 0;
340 for (auto const cluster : journal) {
341 single_histogram<bin_index_type, bin_type, OverflowPolicy>
342 single_hist(hist_arr.subspan(n_bins * cluster_index, n_bins),
343 arg::max_per_bin{bin_max}, arg::num_bins{n_bins});
344 single_hist.undo_increments(cluster);
345 ++cluster_index;
346 }
347 // Ensure the previously untouched tail of the span gets cleared, if
348 // clearing was requested and has not happened yet.
349 skip_remaining();
350 element_index = 0;
351 }
352
353 // Replay journal. Must be in unstarted state. Previous reset (or
354 // constructor) must have requested clearing, or else the span must contain
355 // the same data as when the journal was constructed. Not available in
356 // saturate mode.
357 template <typename Journal> void replay(Journal const &journal) {
358 static_assert(
359 std::is_same_v<typename Journal::bin_index_type, bin_index_type>);
360 static_assert(
361 std::is_same_v<OverflowPolicy, stop_on_internal_overflow>);
362 assert(not hist_arr.empty());
363 assert(not is_started());
364 std::size_t cluster_index = 0;
365 for (auto const cluster : journal) {
366 single_histogram<bin_index_type, bin_type, OverflowPolicy>
367 single_hist(hist_arr.subspan(n_bins * cluster_index, n_bins),
368 arg::max_per_bin{bin_max}, arg::num_bins{n_bins});
369 if (need_to_clear)
370 single_hist.clear();
371 [[maybe_unused]] auto n_applied =
372 single_hist.apply_increments(cluster);
373 // Under correct usage, 'journal' only repeats previous success, so
374 // cannot overflow.
375 assert(n_applied == static_cast<std::size_t>(cluster.size()));
376 ++cluster_index;
377 }
378 element_index = cluster_index;
379 }
380
381 // Reset this instance for reuse on another scan through the array.
382 void reset(bool clear) noexcept {
383 element_index = 0;
384 need_to_clear = clear;
385 }
386};
387
388// An accumulation, over multiple scans, of an array of histograms. Adapter
389// which can attach to a span.
390template <typename BinIndex, typename Bin, typename OverflowPolicy>
391class multi_histogram_accumulation {
392 public:
393 using bin_index_type = BinIndex;
394 using bin_type = Bin;
395 static_assert(same_as_any_of<OverflowPolicy, saturate_on_internal_overflow,
396 stop_on_internal_overflow>);
397
398 private:
399 std::size_t scan_idx = 0;
400 multi_histogram<bin_index_type, bin_type, OverflowPolicy> cur_scan;
401
402 public:
403 explicit multi_histogram_accumulation(
404 std::span<bin_type> hist_array, arg::max_per_bin<bin_type> max_per_bin,
405 arg::num_bins<std::size_t> num_bins,
406 arg::num_elements<std::size_t> num_elements, bool clear_first) noexcept
407 : cur_scan(hist_array, max_per_bin, num_bins, num_elements,
408 clear_first) {}
409
410 // Reconstruct with new span.
411 explicit multi_histogram_accumulation(
412 std::span<bin_type> hist_array,
413 multi_histogram_accumulation const &params, bool clear_first) noexcept
414 : multi_histogram_accumulation(
415 hist_array, arg::max_per_bin{params.max_per_bin()},
416 arg::num_bins{params.num_bins()},
417 arg::num_elements{params.num_elements()}, clear_first) {}
418
419 [[nodiscard]] auto max_per_bin() const noexcept -> bin_type {
420 return cur_scan.max_per_bin();
421 }
422
423 [[nodiscard]] auto num_bins() const noexcept -> std::size_t {
424 return cur_scan.num_bins();
425 }
426
427 [[nodiscard]] auto num_elements() const noexcept -> std::size_t {
428 return cur_scan.num_elements();
429 }
430
431 [[nodiscard]] auto is_scan_started() const noexcept -> bool {
432 return cur_scan.is_started();
433 }
434
435 [[nodiscard]] auto is_scan_complete() const noexcept -> bool {
436 return cur_scan.is_complete();
437 }
438
439 [[nodiscard]] auto is_consistent() const noexcept -> bool {
440 return cur_scan.is_consistent();
441 }
442
443 [[nodiscard]] auto next_element_index() const noexcept -> std::size_t {
444 return cur_scan.next_element_index();
445 }
446
447 [[nodiscard]] auto scan_index() const noexcept -> std::size_t {
448 return scan_idx;
449 }
450
451 // Finish the current scan and start a new one. Must call once after each
452 // scan. Passing 'journal' (which is cleared) is required here to avoid
453 // forgetting to clear the journal for a new scan.
454 template <typename Journal>
455 void new_scan(Journal &journal, bool clear = false) {
456 assert(is_scan_complete());
457 ++scan_idx;
458 cur_scan.reset(clear);
459 journal.clear();
460 }
461
462 template <typename Journal>
463 auto apply_increment_cluster(std::span<bin_index_type const> cluster,
464 Journal &journal) -> bool {
465 assert(not is_scan_complete());
466 return cur_scan.apply_increment_cluster(cluster, journal);
467 }
468
469 void skip_remainder_of_current_scan() { cur_scan.skip_remaining(); }
470
471 // Restores histograms to state just after previous new_scan() call.
472 // Behavior undefined in saturate mode.
473 template <typename Journal>
474 void roll_back_current_scan(Journal const &journal) {
475 cur_scan.roll_back(journal);
476 }
477
478 void reset(bool clear_first = true) noexcept {
479 scan_idx = 0;
480 cur_scan.reset(clear_first);
481 }
482
483 template <typename Journal> void replay(Journal const &journal) {
484 cur_scan.replay(journal);
485 }
486};
487
488} // namespace tcspc::internal
auto histogram(arg::num_bins< std::size_t > num_bins, arg::max_per_bin< typename DataTypes::bin_type > max_per_bin, std::shared_ptr< bucket_source< typename DataTypes::bin_type > > buffer_provider, Downstream downstream)
Create a processor that collects a histogram.
Definition histogram.hpp:263