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 "data_types.hpp"
12#include "errors.hpp"
13#include "int_types.hpp"
14#include "introspect.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 DataTypes, typename Downstream>
105class fit_periodic_sequences {
106 static_assert(
107 processor<Downstream, periodic_sequence_model_event<DataTypes>>);
108
109 using abstime_type = typename DataTypes::abstime_type;
110
111 std::size_t len; // At least 3
112
113 // Record times relative to first event of the series, to prevent overflow
114 // or loss of precision on large abstime values.
115 abstime_type first_tick_time{};
116 abstime_type tick_offset; // Offset relative tick times so as not to be
117 // near zero (avoid subnormal intercept)
118 std::vector<double> relative_ticks; // First element will be tick_offset.
119
120 // Colder data (only used when fitting).
121 periodic_fitter fitter;
122 double min_interval_cutoff;
123 double max_interval_cutoff;
124 double mse_cutoff;
125
126 Downstream downstream;
127
128 LIBTCSPC_NOINLINE void fit_and_emit(abstime_type last_tick_time) {
129 auto const result = fitter.fit(relative_ticks);
130 if (result.mse > mse_cutoff)
131 throw model_fit_error(
132 "fit periodic sequences: mean squared error exceeded cutoff");
133 if (result.slope < min_interval_cutoff ||
134 result.slope > max_interval_cutoff)
135 throw model_fit_error(
136 "fit periodic sequences: estimated time interval was not in expected range");
137
138 // Convert intercept (relative to first_tick_time + tick_offset) to
139 // delay (relative to last_tick_time).
140 auto const delay =
141 result.intercept -
142 static_cast<double>(last_tick_time - first_tick_time) -
143 static_cast<double>(tick_offset);
144
145 downstream.handle(periodic_sequence_model_event<DataTypes>{
146 last_tick_time, delay, result.slope});
147 }
148
149 public:
150 explicit fit_periodic_sequences(arg::length<std::size_t> length,
151 arg::min_interval<double> min_interval,
152 arg::max_interval<double> max_interval,
153 arg::max_mse<double> max_mse,
154 Downstream downstream)
155 : len(length.value),
156 tick_offset(static_cast<abstime_type>(max_interval.value) + 10),
157 fitter(length.value), min_interval_cutoff(min_interval.value),
158 max_interval_cutoff(max_interval.value), mse_cutoff(max_mse.value),
159 downstream(std::move(downstream)) {
160 if (len < 3)
161 throw std::invalid_argument(
162 "fit_periodic_sequences length must be at least 3");
163 if (min_interval_cutoff > max_interval_cutoff)
164 throw std::invalid_argument(
165 "fit_periodic_sequences min interval cutoff must be less than or equal to max interval cutoff");
166 if (max_interval_cutoff <= 0)
167 throw std::invalid_argument(
168 "fit_periodic_sequences max interval cutoff must be positive");
169 relative_ticks.reserve(len);
170 }
171
172 [[nodiscard]] auto introspect_node() const -> processor_info {
173 return processor_info(this, "fit_periodic_sequences");
174 }
175
176 [[nodiscard]] auto introspect_graph() const -> processor_graph {
177 return downstream.introspect_graph().push_entry_point(this);
178 }
179
180 void handle(Event const &event) {
181 static_assert(std::is_same_v<decltype(event.abstime), abstime_type>);
182
183 if (relative_ticks.empty())
184 first_tick_time = event.abstime;
185
186 relative_ticks.push_back(static_cast<double>(
187 event.abstime - first_tick_time + tick_offset));
188
189 if (relative_ticks.size() == len) {
190 fit_and_emit(event.abstime);
191 relative_ticks.clear();
192 }
193 }
194
195 // NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved)
196 void handle(Event &&event) { handle(static_cast<Event const &>(event)); }
197
198 template <typename OtherEvent>
199 requires handler_for<Downstream, std::remove_cvref_t<OtherEvent>>
200 void handle(OtherEvent &&event) {
201 downstream.handle(std::forward<OtherEvent>(event));
202 }
203
204 void flush() { downstream.flush(); }
205};
206
207} // namespace internal
208
267template <typename Event, typename DataTypes = default_data_types,
268 typename Downstream>
270 arg::min_interval<double> min_interval,
271 arg::max_interval<double> max_interval,
272 arg::max_mse<double> max_mse,
273 Downstream downstream) {
274 return internal::fit_periodic_sequences<Event, DataTypes, Downstream>(
275 length, min_interval, max_interval, max_mse, std::move(downstream));
276}
277
278} // namespace tcspc
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
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:269
std::uint64_t u64
Short name for uint64_t.
Definition int_types.hpp:33
libtcspc namespace.
Definition acquire.hpp:29
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 data type set.
Definition data_types.hpp:24