Line | Branch | Exec | Source |
---|---|---|---|
1 | // This file is part of INSTINCT, the INS Toolkit for Integrated | ||
2 | // Navigation Concepts and Training by the Institute of Navigation of | ||
3 | // the University of Stuttgart, Germany. | ||
4 | // | ||
5 | // This Source Code Form is subject to the terms of the Mozilla Public | ||
6 | // License, v. 2.0. If a copy of the MPL was not distributed with this | ||
7 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
8 | |||
9 | /// @file PolynomialCycleSlipDetector.hpp | ||
10 | /// @brief Polynomial Cycle-slip detection algorithm | ||
11 | /// @author T. Topp (topp@ins.uni-stuttgart.de) | ||
12 | /// @date 2023-10-30 | ||
13 | |||
14 | #pragma once | ||
15 | |||
16 | #include <vector> | ||
17 | #include <utility> | ||
18 | #include <optional> | ||
19 | |||
20 | #include "Navigation/GNSS/Core/SatelliteIdentifier.hpp" | ||
21 | #include "Navigation/Math/PolynomialRegressor.hpp" | ||
22 | #include "Navigation/Time/InsTime.hpp" | ||
23 | #include "util/Container/Unordered_map.hpp" | ||
24 | #include "util/Container/Pair.hpp" | ||
25 | |||
26 | #include "internal/gui/widgets/imgui_ex.hpp" | ||
27 | #include "internal/gui/widgets/EnumCombo.hpp" | ||
28 | |||
29 | namespace NAV | ||
30 | { | ||
31 | |||
32 | /// GnssAnalyzer forward declaration | ||
33 | class GnssAnalyzer; | ||
34 | /// CycleSlipDetector forward declaration | ||
35 | class CycleSlipDetector; | ||
36 | |||
37 | /// Cycle-slip detection result type | ||
38 | enum class PolynomialCycleSlipDetectorResult : uint8_t | ||
39 | { | ||
40 | Disabled, ///< The cycle-slip detector is disabled | ||
41 | LessDataThanWindowSize, ///< Less data than the specified window size (cannot predict cycle-slip yet) | ||
42 | NoCycleSlip, ///< No cycle-slip found | ||
43 | CycleSlip, ///< Cycle-slip found | ||
44 | }; | ||
45 | |||
46 | /// @brief Cycle-slip detection | ||
47 | template<typename Key> | ||
48 | class PolynomialCycleSlipDetector | ||
49 | { | ||
50 | public: | ||
51 | /// @brief Constructor | ||
52 | /// @param[in] windowSize Amount of points to use for the fit (sliding window) | ||
53 | /// @param[in] polyDegree Polynomial degree to fit | ||
54 | /// @param[in] enabled Whether the detector is enabled | ||
55 | 113 | explicit PolynomialCycleSlipDetector(size_t windowSize, size_t polyDegree, bool enabled = true) | |
56 | 113 | : _enabled(enabled), _windowSize(windowSize), _polyDegree(polyDegree) {} | |
57 | |||
58 | /// @brief Checks for a cycle slip | ||
59 | /// @param[in] key Key of the detector | ||
60 | /// @param[in] insTime Time of the measurement | ||
61 | /// @param[in] measurementDifference Measurement difference | ||
62 | /// @param[in] threshold Threshold to categorize a measurement as cycle slip | ||
63 | /// @return Cycle-slip result | ||
64 | 7 | [[nodiscard]] PolynomialCycleSlipDetectorResult checkForCycleSlip(const Key& key, InsTime insTime, double measurementDifference, double threshold) | |
65 | { | ||
66 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 7 times.
|
7 | if (!_enabled) { return PolynomialCycleSlipDetectorResult::Disabled; } |
67 |
3/4✓ Branch 1 taken 7 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 6 times.
|
7 | if (!_detectors.contains(key)) |
68 | { | ||
69 |
1/2✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
|
1 | addMeasurement(key, insTime, measurementDifference); |
70 | 1 | return PolynomialCycleSlipDetectorResult::LessDataThanWindowSize; | |
71 | } | ||
72 | |||
73 |
1/2✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
|
6 | const auto& detector = _detectors.at(key); |
74 |
3/4✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 2 times.
|
6 | if (!detector.polyReg.windowSizeReached()) |
75 | { | ||
76 |
1/2✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
|
4 | addMeasurement(key, insTime, measurementDifference); |
77 | 4 | return PolynomialCycleSlipDetectorResult::LessDataThanWindowSize; | |
78 | } | ||
79 | |||
80 |
1/2✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
|
2 | auto polynomial = detector.polyReg.calcPolynomial(); |
81 |
2/4✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
|
2 | auto predictedValue = polynomial.f(calcRelativeTime(insTime, detector)); |
82 | |||
83 |
2/2✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
|
2 | if (std::abs(measurementDifference - predictedValue) > threshold) |
84 | { | ||
85 |
1/2✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
|
1 | reset(key); |
86 |
1/2✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
|
1 | addMeasurement(key, insTime, measurementDifference); |
87 | 1 | return PolynomialCycleSlipDetectorResult::CycleSlip; | |
88 | } | ||
89 |
1/2✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
|
1 | addMeasurement(key, insTime, measurementDifference); |
90 | 1 | return PolynomialCycleSlipDetectorResult::NoCycleSlip; | |
91 | 2 | } | |
92 | |||
93 | /// Empties the collected polynomials | ||
94 | ✗ | void clear() | |
95 | { | ||
96 | ✗ | _detectors.clear(); | |
97 | ✗ | } | |
98 | |||
99 | /// @brief Reset the polynomial for the given combination | ||
100 | /// @param[in] key Key of the detector | ||
101 | 1 | void reset(const Key& key) | |
102 | { | ||
103 |
1/2✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
|
1 | if (_detectors.contains(key)) |
104 | { | ||
105 | 1 | _detectors.erase(key); | |
106 | } | ||
107 | 1 | } | |
108 | |||
109 | /// @brief Is the cycle-slip detector enabled? | ||
110 | ✗ | [[nodiscard]] bool isEnabled() const | |
111 | { | ||
112 | ✗ | return _enabled; | |
113 | } | ||
114 | /// @brief Sets the enabled state | ||
115 | /// @param[in] enabled Whether to enabled or not | ||
116 | ✗ | void setEnabled(bool enabled) | |
117 | { | ||
118 | ✗ | _enabled = enabled; | |
119 | ✗ | } | |
120 | |||
121 | /// @brief Get the window size for the polynomial fit | ||
122 | ✗ | [[nodiscard]] size_t getWindowSize() const { return _windowSize; } | |
123 | /// @brief Sets the amount of points used for the fit (sliding window) | ||
124 | /// @param[in] windowSize Amount of points to use for the fit | ||
125 | ✗ | void setWindowSize(size_t windowSize) | |
126 | { | ||
127 | ✗ | _windowSize = windowSize; | |
128 | ✗ | for (auto& detector : _detectors) | |
129 | { | ||
130 | ✗ | detector.second.polyReg.setWindowSize(windowSize); | |
131 | } | ||
132 | ✗ | } | |
133 | |||
134 | /// @brief Get the degree of the polynomial which is used for fitting | ||
135 | ✗ | [[nodiscard]] size_t getPolynomialDegree() const { return _polyDegree; } | |
136 | /// @brief Sets the degree of the polynomial which is used for fitting | ||
137 | /// @param[in] polyDegree Polynomial degree to fit | ||
138 | ✗ | void setPolynomialDegree(size_t polyDegree) | |
139 | { | ||
140 | ✗ | _polyDegree = polyDegree; | |
141 | ✗ | for (auto& detector : _detectors) | |
142 | { | ||
143 | ✗ | detector.second.polyReg.setPolynomialDegree(polyDegree); | |
144 | } | ||
145 | ✗ | } | |
146 | |||
147 | /// Strategies for fitting | ||
148 | using Strategy = PolynomialRegressor<>::Strategy; | ||
149 | |||
150 | /// @brief Get the strategy used for fitting | ||
151 | ✗ | [[nodiscard]] Strategy getFitStrategy() const { return _strategy; } | |
152 | /// @brief Sets the strategy used for fitting | ||
153 | /// @param[in] strategy Strategy for fitting | ||
154 | ✗ | void setFitStrategy(Strategy strategy) | |
155 | { | ||
156 | ✗ | _strategy = strategy; | |
157 | ✗ | for (auto& detector : _detectors) | |
158 | { | ||
159 | ✗ | detector.second.polyReg.setStrategy(strategy); | |
160 | } | ||
161 | ✗ | } | |
162 | |||
163 | private: | ||
164 | /// @brief Signal Detector struct | ||
165 | struct SignalDetector | ||
166 | { | ||
167 | /// @brief Constructor | ||
168 | /// @param[in] startTime Time when the first message for this detector was received | ||
169 | /// @param[in] windowSize Window size for the sliding window | ||
170 | /// @param[in] polyDegree Polynomial degree to fit | ||
171 | /// @param[in] strategy Strategy for fitting | ||
172 | 7 | SignalDetector(InsTime startTime, size_t windowSize, size_t polyDegree, Strategy strategy) | |
173 | 7 | : startTime(startTime), polyReg(polyDegree, windowSize, strategy) {} | |
174 | |||
175 | InsTime startTime; ///< Time when the first message for this detector was received | ||
176 | PolynomialRegressor<double> polyReg; ///< Polynomial Regressor | ||
177 | }; | ||
178 | |||
179 | bool _enabled = true; ///< Whether the cycle-slip detector is enabled | ||
180 | size_t _windowSize; ///< Window size for the sliding window | ||
181 | size_t _polyDegree = 2; ///< Polynomial degree to fit | ||
182 | Strategy _strategy = Strategy::HouseholderQR; ///< Strategy used for fitting | ||
183 | unordered_map<Key, SignalDetector> _detectors; ///< Detectors, one for each key | ||
184 | |||
185 | /// @brief Calculate the relative time to the start time of the detector | ||
186 | /// @param[in] insTime Time of the measurement | ||
187 | /// @param[in] detector Detector to use | ||
188 | 9 | [[nodiscard]] static double calcRelativeTime(const InsTime& insTime, const SignalDetector& detector) | |
189 | { | ||
190 |
1/2✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
|
9 | return static_cast<double>((insTime - detector.startTime).count()); |
191 | } | ||
192 | /// @brief Calculate the relative time to the start time of the detector | ||
193 | /// @param[in] key Key of the detector | ||
194 | /// @param[in] insTime Time of the measurement | ||
195 | ✗ | [[nodiscard]] std::optional<double> calcRelativeTime(const Key& key, const InsTime& insTime) const | |
196 | { | ||
197 | ✗ | if (!_detectors.contains(key)) { return {}; } | |
198 | |||
199 | ✗ | return calcRelativeTime(insTime, _detectors.at(key)); | |
200 | } | ||
201 | |||
202 | /// @brief Predicts a value from the collected data and polynomial fit | ||
203 | /// @param[in] key Key of the detector | ||
204 | /// @param[in] insTime Time of the measurement | ||
205 | ✗ | [[nodiscard]] std::optional<double> predictValue(const Key& key, const InsTime& insTime) const | |
206 | { | ||
207 | ✗ | if (!_detectors.contains(key)) { return {}; } | |
208 | |||
209 | ✗ | const auto& detector = _detectors.at(key); | |
210 | |||
211 | ✗ | auto polynomial = detector.polyReg.calcPolynomial(); | |
212 | ✗ | return polynomial.f(calcRelativeTime(insTime, detector)); | |
213 | ✗ | } | |
214 | |||
215 | /// @brief Calculates the polynomial from the collected data | ||
216 | /// @param[in] key Key of the detector | ||
217 | ✗ | [[nodiscard]] std::optional<Polynomial<double>> calcPolynomial(const Key& key) const | |
218 | { | ||
219 | ✗ | if (!_detectors.contains(key)) { return {}; } | |
220 | |||
221 | ✗ | return _detectors.at(key).polyReg.calcPolynomial(); | |
222 | } | ||
223 | |||
224 | /// @brief Add a measurement to the polynomial fit | ||
225 | /// @param[in] key Key of the detector | ||
226 | /// @param[in] insTime Time of the measurement | ||
227 | /// @param[in] measurementDifference Measurement difference | ||
228 | 7 | void addMeasurement(const Key& key, InsTime insTime, double measurementDifference) | |
229 | { | ||
230 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 7 times.
|
7 | if (!_enabled) { return; } |
231 |
2/7✓ Branch 1 taken 7 times.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 7 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
|
7 | auto& detector = _detectors.insert({ key, SignalDetector(insTime, _windowSize, _polyDegree, _strategy) }).first->second; |
232 | |||
233 |
2/4✓ Branch 1 taken 7 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 7 times.
✗ Branch 5 not taken.
|
7 | detector.polyReg.push_back(calcRelativeTime(insTime, detector), measurementDifference); |
234 | } | ||
235 | |||
236 | friend class GnssAnalyzer; | ||
237 | friend class CycleSlipDetector; | ||
238 | }; | ||
239 | |||
240 | /// @brief Shows a GUI for advanced configuration of the PolynomialCycleSlipDetector | ||
241 | /// @param[in] label Label to show beside the combo box. This has to be a unique id for ImGui. | ||
242 | /// @param[in] polynomialCycleSlipDetector Reference to the cycle-slip detector to configure | ||
243 | /// @param[in] width Width of the widget | ||
244 | template<typename Key> | ||
245 | ✗ | bool PolynomialCycleSlipDetectorGui(const char* label, PolynomialCycleSlipDetector<Key>& polynomialCycleSlipDetector, float width = 0.0F) | |
246 | { | ||
247 | ✗ | bool changed = false; | |
248 | |||
249 | ✗ | bool enabled = polynomialCycleSlipDetector.isEnabled(); | |
250 | ✗ | if (ImGui::Checkbox(fmt::format("Enabled##{}", label).c_str(), &enabled)) | |
251 | { | ||
252 | ✗ | changed = true; | |
253 | ✗ | polynomialCycleSlipDetector.setEnabled(enabled); | |
254 | } | ||
255 | |||
256 | ✗ | if (!enabled) { ImGui::BeginDisabled(); } | |
257 | |||
258 | ✗ | ImGui::SetNextItemWidth(width); | |
259 | ✗ | if (int windowSize = static_cast<int>(polynomialCycleSlipDetector.getWindowSize()); | |
260 | ✗ | ImGui::InputIntL(fmt::format("Window size##{}", label).c_str(), &windowSize, | |
261 | ✗ | std::max(1, static_cast<int>(polynomialCycleSlipDetector.getPolynomialDegree()) + 1))) | |
262 | { | ||
263 | ✗ | changed = true; | |
264 | ✗ | polynomialCycleSlipDetector.setWindowSize(static_cast<size_t>(windowSize)); | |
265 | } | ||
266 | |||
267 | ✗ | ImGui::SetNextItemWidth(width); | |
268 | ✗ | if (int polyDegree = static_cast<int>(polynomialCycleSlipDetector.getPolynomialDegree()); | |
269 | ✗ | ImGui::InputIntL(fmt::format("Polynomial Degree##{}", label).c_str(), &polyDegree, | |
270 | ✗ | 0, std::min(static_cast<int>(polynomialCycleSlipDetector.getWindowSize()) - 1, std::numeric_limits<int>::max()))) | |
271 | { | ||
272 | ✗ | changed = true; | |
273 | ✗ | polynomialCycleSlipDetector.setPolynomialDegree(static_cast<size_t>(polyDegree)); | |
274 | } | ||
275 | |||
276 | ✗ | ImGui::SetNextItemWidth(width); | |
277 | ✗ | if (auto strategy = polynomialCycleSlipDetector.getFitStrategy(); | |
278 | ✗ | gui::widgets::EnumCombo(fmt::format("Strategy##{}", label).c_str(), strategy)) | |
279 | { | ||
280 | ✗ | changed = true; | |
281 | ✗ | polynomialCycleSlipDetector.setFitStrategy(strategy); | |
282 | } | ||
283 | |||
284 | ✗ | if (!enabled) { ImGui::EndDisabled(); } | |
285 | |||
286 | ✗ | return changed; | |
287 | } | ||
288 | |||
289 | /// @brief Write info to a json object | ||
290 | /// @param[out] j Json output | ||
291 | /// @param[in] data Object to read info from | ||
292 | template<typename Key> | ||
293 | ✗ | void to_json(json& j, const PolynomialCycleSlipDetector<Key>& data) | |
294 | { | ||
295 | ✗ | j = json{ | |
296 | ✗ | { "enabled", data.isEnabled() }, | |
297 | ✗ | { "windowSize", data.getWindowSize() }, | |
298 | ✗ | { "polynomialDegree", data.getPolynomialDegree() }, | |
299 | ✗ | { "strategy", data.getFitStrategy() }, | |
300 | }; | ||
301 | ✗ | } | |
302 | /// @brief Read info from a json object | ||
303 | /// @param[in] j Json variable to read info from | ||
304 | /// @param[out] data Output object | ||
305 | template<typename Key> | ||
306 | ✗ | void from_json(const json& j, PolynomialCycleSlipDetector<Key>& data) | |
307 | { | ||
308 | ✗ | if (j.contains("enabled")) | |
309 | { | ||
310 | ✗ | auto enabled = j.at("enabled").get<bool>(); | |
311 | ✗ | data.setEnabled(enabled); | |
312 | } | ||
313 | ✗ | if (j.contains("windowSize")) | |
314 | { | ||
315 | ✗ | auto windowSize = j.at("windowSize").get<size_t>(); | |
316 | ✗ | data.setWindowSize(windowSize); | |
317 | } | ||
318 | ✗ | if (j.contains("polynomialDegree")) | |
319 | { | ||
320 | ✗ | auto polynomialDegree = j.at("polynomialDegree").get<size_t>(); | |
321 | ✗ | data.setPolynomialDegree(polynomialDegree); | |
322 | } | ||
323 | ✗ | if (j.contains("strategy")) | |
324 | { | ||
325 | ✗ | auto strategy = j.at("strategy").get<size_t>(); | |
326 | ✗ | data.setFitStrategy(static_cast<typename PolynomialCycleSlipDetector<Key>::Strategy>(strategy)); | |
327 | } | ||
328 | ✗ | } | |
329 | |||
330 | } // namespace NAV | ||
331 |