libtcspc C++ API
Streaming TCSPC and time tag data processing
Loading...
Searching...
No Matches
fit_sequence.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 "common.hpp"
11#include "errors.hpp"
12#include "int_types.hpp"
13#include "introspect.hpp"
14#include "numeric_traits.hpp"
15#include "processor.hpp"
16#include "timing_misc.hpp"
17
18#include <algorithm>
19#include <cassert>
20#include <cstddef>
21#include <functional>
22#include <numeric>
23#include <stdexcept>
24#include <type_traits>
25#include <utility>
26#include <vector>
27
28namespace tcspc {
29
30// Design note: It is important that we use double, not float, for these
31// computations. The abstime units might be picoseconds, and the event interval
32// being fit might be several microseconds (typical pixel clock) with a
33// sequence length of up to ~1000. Under these conditions (9-10 orders of
34// magnitude between unit and total), even with the use of relative time values
35// (as we do), float may lose precision before the end of a single sequence is
36// reached.
37
38namespace internal {
39
40struct periodic_fit_result {
41 double intercept;
42 double slope;
43 double mse; // mean squared error
44};
45
46class periodic_fitter {
47 // Linear fit: yfit = a + b * x; y from data sequence, x from fixed
48 // indices:
49 //
50 // [ y_0] [1 0] [ 0]
51 // [ y_1] [1 1] [ 1]
52 // [ y_1] [1 2] [ 2]
53 // y = [ .], X = [. .], x = [ .]
54 // [ .] [. .] [ .]
55 // [ .] [. .] [ .]
56 // [y_n-1] [1 n-1] [n-1]
57
58 double n;
59 double sigma_x; // Sum of 0, 1, ..., n - 1
60 double sigma_xx; // Sum of 0^2, 1^2, ..., (n - 1)^2
61 double det_XtX; // Determinant of Xt X
62 std::vector<double> x; // 0, ..., n - 1
63
64 public:
65 // n should be at least 3. If it is 2, mse will be NaN. If it is 0 or 1,
66 // intercept and slope will be NaN.
67 explicit periodic_fitter(u64 length)
68 : n(static_cast<double>(length)), sigma_x((n - 1.0) * n * 0.5),
69 sigma_xx((n - 1.0) * n * (2.0 * n - 1.0) / 6.0),
70 det_XtX(n * sigma_xx - sigma_x * sigma_x), x(length) {
71 std::iota(x.begin(), x.end(), 0.0);
72 }
73
74 [[nodiscard]] auto fit(std::vector<double> const &y) const
75 -> periodic_fit_result {
76 assert(static_cast<double>(y.size()) == n);
77
78 // Sum of y_0, y_1, ..., y_n-1
79 double const sigma_y = std::reduce(y.cbegin(), y.cend());
80
81 // Sum of x_0 * y_0, x_1 * y_1, ..., x_n-1 * y_n-1
82 double const sigma_xy =
83 std::transform_reduce(x.cbegin(), x.cend(), y.cbegin(), 0.0);
84
85 // Solve ordinary linear least squares:
86 // [a b]t = (Xt X)^-1 Xt y
87 double const a = (sigma_xx * sigma_y - sigma_x * sigma_xy) / det_XtX;
88 double const b = (n * sigma_xy - sigma_x * sigma_y) / det_XtX;
89
90 // Sum of squared residuals
91 double const ssr =
92 std::transform_reduce(x.cbegin(), x.cend(), y.cbegin(), 0.0,
93 std::plus<>(), [&](double x, double y) {
94 auto const yfit = a + b * x;
95 auto const r = y - yfit;
96 return r * r;
97 });
98 double const mse = ssr / (n - 2.0);
99
100 return {a, b, mse};
101 }
102};
103
104template <typename Event, typename NumericTraits, typename Downstream>
105 requires processor<Downstream,
106 periodic_sequence_model_event<NumericTraits>>
107class fit_periodic_sequences {
108 using abstime_type = NumericTraits::abstime_type;
109
110 std::size_t len; // At least 3
111
112 // Record times relative to first event of the series, to prevent overflow
113 // or loss of precision on large abstime values.
114 abstime_type first_tick_time{};
115 abstime_type tick_offset; // Offset relative tick times so as not to be
116 // near zero (avoid subnormal intercept)
117 std::vector<double> relative_ticks; // First element will be tick_offset.
118
119 // Colder data (only used when fitting).
120 periodic_fitter fitter;
121 double min_interval_cutoff;
122 double max_interval_cutoff;
123 double mse_cutoff;
124
125 Downstream downstream;
126
127 LIBTCSPC_NOINLINE void fit_and_emit(abstime_type last_tick_time) {
128 auto const result = fitter.fit(relative_ticks);
129 if (result.mse > mse_cutoff)
130 throw model_fit_error(
131 "fit periodic sequences: mean squared error exceeded cutoff");
132 if (result.slope < min_interval_cutoff ||
133 result.slope > max_interval_cutoff)
134 throw model_fit_error(
135 "fit periodic sequences: estimated time interval was not in expected range");
136
137 // Convert intercept (relative to first_tick_time + tick_offset) to
138 // delay (relative to last_tick_time).
139 auto const delay =
140 result.intercept -
141 static_cast<double>(last_tick_time - first_tick_time) -
142 static_cast<double>(tick_offset);
143
144 downstream.handle(periodic_sequence_model_event<NumericTraits>{
145 last_tick_time, delay, result.slope});
146 }
147
148 public:
149 explicit fit_periodic_sequences(arg::length<std::size_t> length,
150 arg::min_interval<double> min_interval,
151 arg::max_interval<double> max_interval,
152 arg::max_mse<double> max_mse,
153 Downstream downstream)
154 : len(length.value),
155 tick_offset(static_cast<abstime_type>(max_interval.value) + 10),
156 fitter(length.value), min_interval_cutoff(min_interval.value),
157 max_interval_cutoff(max_interval.value), mse_cutoff(max_mse.value),
158 downstream(std::move(downstream)) {
159 if (len < 3)
160 throw std::invalid_argument(
161 "fit_periodic_sequences length must be at least 3");
162 if (min_interval_cutoff > max_interval_cutoff)
163 throw std::invalid_argument(
164 "fit_periodic_sequences min interval cutoff must be less than or equal to max interval cutoff");
165 if (max_interval_cutoff <= 0)
166 throw std::invalid_argument(
167 "fit_periodic_sequences max interval cutoff must be positive");
168 relative_ticks.reserve(len);
169 }
170
171 [[nodiscard]] auto introspect_node() const -> processor_info {
172 return processor_info(this, "fit_periodic_sequences");
173 }
174
175 [[nodiscard]] auto introspect_graph() const -> processor_graph {
176 return downstream.introspect_graph().push_entry_point(this);
177 }
178
179 void handle(Event const &event) {
180 static_assert(std::is_same_v<decltype(event.abstime), abstime_type>);
181
182 if (relative_ticks.empty())
183 first_tick_time = event.abstime;
184
185 relative_ticks.push_back(static_cast<double>(
186 event.abstime - first_tick_time + tick_offset));
187
188 if (relative_ticks.size() == len) {
189 fit_and_emit(event.abstime);
190 relative_ticks.clear();
191 }
192 }
193
194 // NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved)
195 void handle(Event &&event) { handle(static_cast<Event const &>(event)); }
196
197 template <typename OtherEvent>
198 requires handler_for<Downstream, std::remove_cvref_t<OtherEvent>>
199 void handle(OtherEvent &&event) {
200 downstream.handle(std::forward<OtherEvent>(event));
201 }
202
203 void flush() { downstream.flush(); }
204};
205
206} // namespace internal
207
266template <typename Event, typename NumericTraits = default_numeric_traits,
267 typename Downstream>
269 arg::min_interval<double> min_interval,
270 arg::max_interval<double> max_interval,
271 arg::max_mse<double> max_mse,
272 Downstream downstream) {
273 return internal::fit_periodic_sequences<Event, NumericTraits, Downstream>(
274 length, min_interval, max_interval, max_mse, std::move(downstream));
275}
276
277} // namespace tcspc
auto delay(arg::delta< typename NumericTraits::abstime_type > delta, Downstream downstream)
Create a processor that applies an abstime offset to all events.
Definition delay.hpp:123
auto fit_periodic_sequences(arg::length< std::size_t > length, arg::min_interval< double > min_interval, arg::max_interval< double > max_interval, arg::max_mse< double > max_mse, Downstream downstream)
Create a processor that fits fixed-length periodic sequences of events and estimates the start time a...
Definition fit_sequence.hpp:268
std::uint64_t u64
Short name for uint64_t.
Definition int_types.hpp:33
libtcspc namespace.
Definition acquire.hpp:30
Function argument wrapper for length parameter.
Definition arg_wrappers.hpp:197
Function argument wrapper for maximum interval parameter.
Definition arg_wrappers.hpp:257
Function argument wrapper for maximum MSE parameter.
Definition arg_wrappers.hpp:277
Function argument wrapper for minimum interval parameter.
Definition arg_wrappers.hpp:317
The default numeric traits.
Definition numeric_traits.hpp:27