libtcspc C++ API
Streaming TCSPC and time tag data processing
Loading...
Searching...
No Matches
dither.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 "data_types.hpp"
11#include "errors.hpp"
12
13#include <cassert>
14#include <cmath>
15#include <cstddef>
16#include <cstdint>
17#include <cstring>
18#include <limits>
19#include <optional>
20#include <random>
21#include <stdexcept>
22#include <type_traits>
23
24namespace tcspc {
25
26namespace internal {
27
28// Our dithering is done by adding a triangularly-distributed noise (width 2.0)
29// before rounding to the nearest integer. This is the simplest way to keep the
30// noise distribution independent of the sample value. (For example, adding a
31// uniformly-distributed noise (width 1.0) would not have this property because
32// samples closer to integer values would receive noise with a narrower
33// distribution after quantization.)
34
35// Design note: We do not use std::uniform_real_distribution or
36// std::generate_canonical, because they may have issues (may return the upper
37// bound, depending on implementation). (Also, they do not produce the same
38// sequence across library implementations.) Instead, we use our own method to
39// produce random doubles in [0.0, 1.0).
40
41// We prefer std::minstd_rand() over std::mt19937[_64] because of its compact
42// state (the two have similar performance in a tight loop, but mt19937 has > 2
43// KiB of state, which can become a nontrivial fraction of the L1D if multiple
44// instances are in use). The "poor" quality of the MINSTD PRNG is likely not a
45// significant issue for dithering purposes.
46
47// The random integers from std::minstd_rand are in [1, 2147483646].
48static_assert(decltype(std::minstd_rand())::min() == 1);
49static_assert(decltype(std::minstd_rand())::max() == 2'147'483'646);
50// Do we care that 0 and 2^31-1 are not included in the range? Probably not for
51// dithering purposes.
52
53// Note that std::minstd_rand::result_type is either u32 or u64, depending on
54// standard library implementation. It is therefore used below explicitly, to
55// avoid unnecessary integer conversion.
56
57// Formality: Check our assumption that 'double' is IEEE 754 double precision.
58static_assert(std::numeric_limits<double>::is_iec559);
59static_assert(std::numeric_limits<double>::radix == 2);
60static_assert(std::numeric_limits<double>::digits == 53);
61
62// Make a uniformly-distributed random double value in [0.0, 1.0), given a
63// uniformly-distributed 32-bit random integer r from std::minstd_rand.
64[[nodiscard]] inline auto
65uniform_double_0_1_minstd(std::minstd_rand::result_type r) -> double {
66 assert(r < 2'147'483'648u); // Do allow 0 and 2147483647 in tests.
67
68 // Put the 31 random bits in the most significant part of the 52-bit
69 // fraction field; leave the sign positive and exponent to 0 (giving a
70 // value in [1.0, 2.0)).
71 auto bits = std::uint64_t(r) << (52u - 31u);
72 bits |= 1023uLL << 52; // Exponent = 0
73 double d{};
74 std::memcpy(&d, &bits, sizeof(d));
75 return d - 1.0; // Will not produce subnormal values.
76}
77
78// Make a triangularly-distributed random double value in (0.0, 2.0), centered
79// at 1.0, given two uniformly-distributed 32-bit random integers r0, r1 from
80// std::minstd_rand.
81[[nodiscard]] inline auto
82triangular_double_0_2_minstd(std::minstd_rand::result_type r0,
83 std::minstd_rand::result_type r1) -> double {
84 auto const d0 = uniform_double_0_1_minstd(r0);
85 auto const d1 = uniform_double_0_1_minstd(r1);
86 return d0 + (1.0 - d1);
87}
88
89// Given noise in [0, 2) (from triangular distribution), return dithered value.
90// The return value is in (v - 1.5, v + 1.5).
91template <typename T>
92[[nodiscard]] inline auto apply_dither(double value, double dither_noise_0_2)
93 -> T {
94 assert(dither_noise_0_2 >= 0.0);
95 assert(dither_noise_0_2 < 2.0);
96 return static_cast<T>(std::floor(value + dither_noise_0_2 - 0.5));
97}
98
99template <typename T> class dithering_quantizer {
100 std::minstd_rand prng; // Uses default seed (= 1) for reproducibility.
101
102 public:
103 [[nodiscard]] auto operator()(double value) -> T {
104 // Ensure r0, r1 are computed in order (for reproducibility).
105 auto const r0 = prng();
106 auto const r1 = prng();
107 return apply_dither<T>(value, triangular_double_0_2_minstd(r0, r1));
108 }
109};
110
111} // namespace internal
112
121template <typename DataTypes = default_data_types>
123 std::optional<typename DataTypes::abstime_type> next;
124 double dly;
125
126 internal::dithering_quantizer<typename DataTypes::abstime_type> dithq;
127
128 public:
137 : dly(delay.value) {
138 if (dly < 1.5)
139 throw std::invalid_argument(
140 "dithered timing generator delay must be at least 1.5");
141 }
142
144 template <typename TriggerEvent> void trigger(TriggerEvent const &event) {
145 static_assert(std::is_same_v<decltype(event.abstime),
146 typename DataTypes::abstime_type>);
147 next = event.abstime + dithq(dly);
148 }
149
151 [[nodiscard]] auto peek() const
152 -> std::optional<typename DataTypes::abstime_type> {
153 return next;
154 }
155
157 void pop() { next.reset(); }
158};
159
173template <typename DataTypes = default_data_types>
175 std::optional<typename DataTypes::abstime_type> next;
176
177 internal::dithering_quantizer<typename DataTypes::abstime_type> dithq;
178
179 public:
181 template <typename TriggerEvent> void trigger(TriggerEvent const &event) {
182 static_assert(std::is_same_v<decltype(event.abstime),
183 typename DataTypes::abstime_type>);
184 static_assert(std::is_same_v<decltype(event.delay), double>);
185 if (event.delay < 1.5)
187 "dithered timing generator delay must be at least 1.5");
188 next = event.abstime + dithq(event.delay);
189 }
190
192 [[nodiscard]] auto peek() const
193 -> std::optional<typename DataTypes::abstime_type> {
194 return next;
195 }
196
198 void pop() { next.reset(); }
199};
200
201namespace internal {
202
203template <typename Abstime> class dithered_linear_timing_generator_impl {
204 Abstime trigger_time{};
205 std::size_t remaining = 0;
206 Abstime next{};
207
208 double dly{};
209 double intv{3.0};
210 std::size_t ct{};
211
212 dithering_quantizer<Abstime> dithq;
213
214 void compute_next() {
215 if (remaining == 0)
216 return;
217 auto const index = ct - remaining;
218 next = trigger_time + dithq(dly + intv * static_cast<double>(index));
219 }
220
221 public:
222 dithered_linear_timing_generator_impl() = default;
223
224 explicit dithered_linear_timing_generator_impl(
225 arg::delay<double> delay, arg::interval<double> interval,
226 arg::count<std::size_t> count)
227 : dly(delay.value), intv(interval.value), ct(count.value) {
228 if (dly < 1.5)
229 throw std::invalid_argument(
230 "dithered timing generator delay must be at least 1.5");
231 if (intv < 3.0)
232 throw std::invalid_argument(
233 "dithered timing generator interval must be at least 3.0");
234 }
235
236 void trigger(arg::abstime<Abstime> abstime) {
237 trigger_time = abstime.value;
238 remaining = ct;
239 compute_next();
240 }
241
242 void trigger_and_configure(arg::abstime<Abstime> abstime,
243 arg::delay<double> delay,
244 arg::interval<double> interval,
245 arg::count<std::size_t> count) {
246 dly = delay.value;
247 intv = interval.value;
248 ct = count.value;
249 if (dly < 1.5)
250 throw data_validation_error(
251 "dithered timing generator delay must be at least 1.5");
252 if (intv < 3.0)
253 throw data_validation_error(
254 "dithered timing generator interval must be at least 3.0");
255 trigger(abstime);
256 }
257
258 [[nodiscard]] auto peek() const -> std::optional<Abstime> {
259 return remaining > 0 ? next : std::optional<Abstime>{};
260 }
261
262 void pop() {
263 --remaining;
264 compute_next();
265 }
266};
267
268} // namespace internal
269
278template <typename DataTypes = default_data_types>
280 internal::dithered_linear_timing_generator_impl<
281 typename DataTypes::abstime_type>
282 impl;
283
284 public:
296
298 template <typename TriggerEvent> void trigger(TriggerEvent const &event) {
299 static_assert(std::is_same_v<decltype(event.abstime),
300 typename DataTypes::abstime_type>);
301 impl.trigger(arg::abstime{event.abstime});
302 }
303
305 [[nodiscard]] auto peek() const
306 -> std::optional<typename DataTypes::abstime_type> {
307 return impl.peek();
308 }
309
311 void pop() { impl.pop(); }
312};
313
328template <typename DataTypes = default_data_types>
330 internal::dithered_linear_timing_generator_impl<
331 typename DataTypes::abstime_type>
332 impl;
333
334 public:
336 template <typename TriggerEvent> void trigger(TriggerEvent const &event) {
337 static_assert(std::is_same_v<decltype(event.abstime),
338 typename DataTypes::abstime_type>);
339 impl.trigger_and_configure(
340 arg::abstime{event.abstime}, arg::delay{event.delay},
341 arg::interval{event.interval}, arg::count{event.count});
342 }
343
345 [[nodiscard]] auto peek() const
346 -> std::optional<typename DataTypes::abstime_type> {
347 return impl.peek();
348 }
349
351 void pop() { impl.pop(); }
352};
353
354} // namespace tcspc
Error thrown when the data being processed does not meet expectations.
Definition errors.hpp:95
dithered_linear_timing_generator(arg::delay< double > delay, arg::interval< double > interval, arg::count< std::size_t > count)
Construct an instance that generates count timings at interval after delay relative to each trigger.
Definition dither.hpp:292
void pop()
Implements timing generator requirement.
Definition dither.hpp:311
auto peek() const -> std::optional< typename DataTypes::abstime_type >
Implements timing generator requirement.
Definition dither.hpp:305
void trigger(TriggerEvent const &event)
Implements timing generator requirement.
Definition dither.hpp:298
void pop()
Implements timing generator requirement.
Definition dither.hpp:157
dithered_one_shot_timing_generator(arg::delay< double > delay)
Construct an instance that generates a timing after delay (plus dither) relative to each trigger.
Definition dither.hpp:136
void trigger(TriggerEvent const &event)
Implements timing generator requirement.
Definition dither.hpp:144
auto peek() const -> std::optional< typename DataTypes::abstime_type >
Implements timing generator requirement.
Definition dither.hpp:151
Timing generator that generates a periodic series of timings, configured by the trigger event,...
Definition dither.hpp:329
void pop()
Implements timing generator requirement.
Definition dither.hpp:351
void trigger(TriggerEvent const &event)
Implements timing generator requirement.
Definition dither.hpp:336
auto peek() const -> std::optional< typename DataTypes::abstime_type >
Implements timing generator requirement.
Definition dither.hpp:345
Timing generator that generates a single, delayed timing, configured by the trigger event,...
Definition dither.hpp:174
void trigger(TriggerEvent const &event)
Implements timing generator requirement.
Definition dither.hpp:181
void pop()
Implements timing generator requirement.
Definition dither.hpp:198
auto peek() const -> std::optional< typename DataTypes::abstime_type >
Implements timing generator requirement.
Definition dither.hpp:192
auto count(access_tracker< count_access > &&tracker, Downstream downstream)
Create a processor that counts events of a given type.
Definition count.hpp:313
auto delay(arg::delta< typename DataTypes::abstime_type > delta, Downstream downstream)
Create a processor that applies an abstime offset to all events.
Definition delay.hpp:123
libtcspc namespace.
Definition acquire.hpp:29
Function argument wrapper for abstime parameter.
Definition arg_wrappers.hpp:37
Function argument wrapper for count parameter.
Definition arg_wrappers.hpp:97
Function argument wrapper for delay parameter.
Definition arg_wrappers.hpp:117
Function argument wrapper for interval parameter.
Definition arg_wrappers.hpp:177