INSTINCT Code Coverage Report


Directory: src/
File: Nodes/DataProcessor/ErrorModel/AllanDeviation.cpp
Date: 2025-11-25 23:34:18
Exec Total Coverage
Lines: 13 183 7.1%
Functions: 4 13 30.8%
Branches: 9 384 2.3%

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 #include "AllanDeviation.hpp"
10 #include <algorithm>
11 #include <cstddef>
12 #include <cstdint>
13 #include <imgui.h>
14 #include <mutex>
15
16 #include "util/Logger.hpp"
17
18 #include <Eigen/src/Core/util/Meta.h>
19 #include <fmt/core.h>
20 #include "internal/FlowManager.hpp"
21
22 #include "internal/gui/widgets/HelpMarker.hpp"
23 #include "internal/gui/widgets/imgui_ex.hpp"
24 #include "implot.h"
25
26 #include "NodeData/IMU/ImuObs.hpp"
27
28 #include <chrono>
29
30 #include <unsupported/Eigen/MatrixFunctions>
31
32 114 NAV::AllanDeviation::AllanDeviation()
33
3/6
✓ Branch 1 taken 114 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 114 times.
✗ Branch 5 not taken.
✓ Branch 8 taken 114 times.
✗ Branch 9 not taken.
114 : Node(typeStatic())
34 {
35 LOG_TRACE("{}: called", name);
36
37 114 _hasConfig = true;
38 114 _lockConfigDuringRun = false;
39 114 _guiConfigDefaultWindowSize = { 630, 530 };
40
41
4/8
✓ Branch 1 taken 114 times.
✗ Branch 2 not taken.
✓ Branch 5 taken 114 times.
✗ Branch 6 not taken.
✓ Branch 8 taken 114 times.
✓ Branch 9 taken 114 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
342 CreateInputPin("ImuObs", Pin::Type::Flow, { NAV::ImuObs::type() }, &AllanDeviation::receiveImuObs);
42 228 }
43
44 228 NAV::AllanDeviation::~AllanDeviation()
45 {
46 LOG_TRACE("{}: called", nameId());
47 228 }
48
49 228 std::string NAV::AllanDeviation::typeStatic()
50 {
51
1/2
✓ Branch 1 taken 228 times.
✗ Branch 2 not taken.
456 return "AllanDeviation";
52 }
53
54 std::string NAV::AllanDeviation::type() const
55 {
56 return typeStatic();
57 }
58
59 114 std::string NAV::AllanDeviation::category()
60 {
61
1/2
✓ Branch 1 taken 114 times.
✗ Branch 2 not taken.
228 return "Data Processor";
62 }
63
64 void NAV::AllanDeviation::guiConfig()
65 {
66 if (ImGui::Checkbox("Compute Allan Deviation last", &_updateLast)) { flow::ApplyChanges(); }
67 if (ImGui::Checkbox("Display Confidence Intervals", &_displayConfidence)) { flow::ApplyChanges(); }
68 if (_displayConfidence)
69 {
70 ImGui::SameLine();
71 ImGui::SetNextItemWidth(200.0F);
72 if (ImGui::SliderFloat("Confidence Alpha Channel", &_confidenceFillAlpha, 0.0F, 1.0F, "%.2f"))
73 {
74 flow::ApplyChanges();
75 }
76 }
77
78 const std::array<std::string, 3> legendEntries{ "x", "y", "z" };
79 constexpr std::array<double, 5> slopeTicks = { -2, -1, 0, 1, 2 };
80
81 if (ImGui::BeginTabBar("AllanDeviationTabBar", ImGuiTabBarFlags_None))
82 {
83 for (size_t sensorIter = 0; sensorIter < SensorType_COUNT; sensorIter++)
84 {
85 auto sensorType = static_cast<SensorType>(sensorIter);
86 const char* sensorName = to_string(sensorType);
87 auto& sensor = _sensors.at(sensorIter);
88 std::scoped_lock<std::mutex> lg(sensor.mutex);
89
90 if (ImGui::BeginTabItem(fmt::format("{}##TabItem {}", sensorName, size_t(id)).c_str()))
91 {
92 if (ImPlot::BeginPlot(fmt::format("Allan Deviation of {}##Plot {}", sensorName, size_t(id)).c_str()))
93 {
94 ImPlot::SetupLegend(ImPlotLocation_SouthWest, ImPlotLegendFlags_None);
95 ImPlot::SetupAxes("τ [s]", fmt::format("σ(τ) [{}]", unitString(sensorType)).c_str(),
96 ImPlotAxisFlags_AutoFit, ImPlotAxisFlags_AutoFit);
97 ImPlot::SetupAxisScale(ImAxis_X1, ImPlotScale_Log10);
98 ImPlot::SetupAxisScale(ImAxis_Y1, ImPlotScale_Log10);
99 ImPlot::PushStyleVar(ImPlotStyleVar_FillAlpha, _confidenceFillAlpha);
100 for (size_t d = 0; d < 3; d++)
101 {
102 if (_displayConfidence & !_averagingTimes.empty())
103 {
104 ImPlot::PlotShaded(legendEntries.at(d).data(),
105 _averagingTimes.data(),
106 sensor.allanDeviationConfidenceIntervals.at(d).at(0).data(),
107 sensor.allanDeviationConfidenceIntervals.at(d).at(1).data(),
108 static_cast<int>(_averagingTimes.size()));
109 }
110 ImPlot::PlotLine(legendEntries.at(d).data(),
111 _averagingTimes.data(),
112 sensor.allanDeviation.at(d).data(),
113 static_cast<int>(_averagingTimes.size()));
114 }
115 ImPlot::EndPlot();
116 }
117 if (ImGui::TreeNode("Slopes"))
118 {
119 if (ImPlot::BeginPlot("Slopes of Allan Variance"))
120 {
121 ImPlot::SetupLegend(ImPlotLocation_SouthWest, ImPlotLegendFlags_None);
122 ImPlot::SetupAxis(ImAxis_X1, "τ [s]", ImPlotAxisFlags_AutoFit);
123 ImPlot::SetupAxis(ImAxis_Y1, "µ [ ]", ImPlotAxisFlags_Lock);
124 ImPlot::SetupAxisScale(ImAxis_X1, ImPlotScale_Log10);
125 ImPlot::SetupAxisLimits(ImAxis_Y1, -2.5, 2.5);
126 ImPlot::SetupAxisTicks(ImAxis_Y1, slopeTicks.data(), 5, nullptr, false);
127 for (size_t d = 0; d < 3; d++)
128 {
129 ImPlot::PlotLine(legendEntries.at(d).data(),
130 _averagingTimes.data(),
131 sensor.slope.at(d).data(),
132 static_cast<int>(_averagingTimes.size()));
133 }
134 ImPlot::EndPlot();
135 }
136
137 ImGui::TreePop();
138 }
139 ImGui::EndTabItem();
140 }
141 }
142 ImGui::EndTabBar();
143 }
144 }
145
146 [[nodiscard]] json NAV::AllanDeviation::save() const
147 {
148 LOG_TRACE("{}: called", nameId());
149
150 return {
151 { "displayConfidence", _displayConfidence },
152 { "confidenceFillAlpha", _confidenceFillAlpha },
153 { "updateLast", _updateLast },
154 };
155 }
156
157 void NAV::AllanDeviation::restore(json const& j)
158 {
159 LOG_TRACE("{}: called", nameId());
160
161 if (j.contains("displayConfidence")) { j.at("displayConfidence").get_to(_displayConfidence); }
162 if (j.contains("confidenceFillAlpha")) { j.at("confidenceFillAlpha").get_to(_confidenceFillAlpha); }
163 if (j.contains("updateLast")) { j.at("updateLast").get_to(_updateLast); }
164 }
165
166 bool NAV::AllanDeviation::initialize()
167 {
168 LOG_TRACE("{}: called", nameId());
169
170 _startingInsTime.reset();
171
172 for (auto& sensor : _sensors)
173 {
174 std::scoped_lock<std::mutex> lg(sensor.mutex);
175 sensor.cumSum = { Eigen::Vector3d::Zero() };
176 sensor.allanSum = {};
177 sensor.allanVariance = {};
178 sensor.allanDeviation = {};
179 sensor.slope = {};
180 sensor.allanDeviationConfidenceIntervals = {};
181 }
182
183 _averagingFactors.clear();
184 _averagingTimes.clear();
185 _observationCount.clear();
186
187 _imuObsCount = 0;
188
189 _nextAveragingFactorExponent = 1;
190 _nextAveragingFactor = 1;
191 _samplingInterval = 0.0;
192
193 return true;
194 }
195
196 void NAV::AllanDeviation::deinitialize()
197 {
198 LOG_TRACE("{}: called", nameId());
199 }
200
201 void NAV::AllanDeviation::receiveImuObs(NAV::InputPin::NodeDataQueue& queue, size_t /* pinIdx */)
202 {
203 auto obs = std::static_pointer_cast<const ImuObs>(queue.extract_front());
204
205 bool lastMessage = false;
206 if (queue.empty()
207 && inputPins[INPUT_PORT_INDEX_IMU_OBS].isPinLinked()
208 && inputPins[INPUT_PORT_INDEX_IMU_OBS].link.getConnectedPin()->noMoreDataAvailable)
209 {
210 lastMessage = true;
211 }
212
213 // save InsTime of first imuObs for sampling interval computation
214 if (_startingInsTime.empty()) { _startingInsTime = obs->insTime; }
215
216 _imuObsCount++;
217 {
218 std::scoped_lock<std::mutex> lg(_sensors.at(Accel).mutex);
219 _sensors.at(Accel).cumSum.emplace_back(_sensors.at(Accel).cumSum.back() + obs->p_acceleration);
220 }
221 {
222 std::scoped_lock<std::mutex> lg(_sensors.at(Gyro).mutex);
223 _sensors.at(Gyro).cumSum.emplace_back(_sensors.at(Gyro).cumSum.back() + obs->p_angularRate);
224 }
225
226 // extending _averagingFactors if necessary
227 if (_imuObsCount == _nextAveragingFactor * 2)
228 {
229 _averagingFactors.push_back(_nextAveragingFactor);
230 _observationCount.push_back(0);
231
232 for (auto& sensor : _sensors)
233 {
234 for (size_t d = 0; d < 3; d++)
235 {
236 sensor.allanSum.at(d).push_back(0);
237 }
238 }
239
240 // computation of next averaging factor
241 while (static_cast<unsigned int>(round(pow(10., static_cast<double>(_nextAveragingFactorExponent) / _averagingFactorsPerDecade))) == _nextAveragingFactor)
242 {
243 _nextAveragingFactorExponent++;
244 }
245
246 _nextAveragingFactor = static_cast<unsigned int>(round(pow(10., static_cast<double>(_nextAveragingFactorExponent) / _averagingFactorsPerDecade)));
247 }
248
249 // computation Allan sum
250 for (size_t k = 0; k < _averagingFactors.size(); k++)
251 {
252 for (auto& sensor : _sensors)
253 {
254 std::scoped_lock<std::mutex> lg(sensor.mutex);
255 Eigen::Vector3d tempSum = sensor.cumSum.at(_imuObsCount)
256 - 2 * sensor.cumSum.at(_imuObsCount - static_cast<unsigned int>(_averagingFactors.at(k)))
257 + sensor.cumSum.at(_imuObsCount - 2 * static_cast<unsigned int>(_averagingFactors.at(k)));
258
259 for (size_t d = 0; d < 3; d++)
260 {
261 sensor.allanSum.at(d).at(k) += pow(tempSum(static_cast<Eigen::Index>(d)), 2);
262 }
263 }
264 _observationCount.at(k)++;
265 }
266
267 // computation of Allan Variance and Deviation
268 if (!_updateLast || lastMessage)
269 {
270 _samplingInterval = static_cast<double>((obs->insTime - _startingInsTime).count()) / _imuObsCount;
271 _averagingTimes.resize(_averagingFactors.size(), 0.);
272 _confidenceMultiplicationFactor.resize(_averagingFactors.size(), 0.);
273 for (size_t k = 0; k < _averagingFactors.size(); k++)
274 {
275 _averagingTimes.at(k) = _averagingFactors.at(k) * _samplingInterval;
276 _confidenceMultiplicationFactor.at(k) = sqrt(0.5 / (_imuObsCount / _averagingFactors.at(k) - 1));
277 }
278
279 for (auto& sensor : _sensors)
280 {
281 std::scoped_lock<std::mutex> lg(sensor.mutex);
282 for (size_t d = 0; d < 3; d++)
283 {
284 sensor.allanVariance.at(d).resize(_averagingFactors.size(), 0.);
285 sensor.allanDeviation.at(d).resize(_averagingFactors.size(), 0.);
286
287 for (size_t j = 0; j < 2; j++)
288 {
289 sensor.allanDeviationConfidenceIntervals.at(d).at(j).resize(_averagingFactors.size(), 0.);
290 }
291
292 for (size_t k = 0; k < _averagingFactors.size(); k++)
293 {
294 sensor.allanVariance.at(d).at(k) = sensor.allanSum.at(d).at(k) / (2 * pow(_averagingFactors.at(k), 2) * _observationCount.at(k));
295
296 sensor.allanDeviation.at(d).at(k) = sqrt(sensor.allanVariance.at(d).at(k));
297
298 sensor.allanDeviationConfidenceIntervals.at(d).at(0).at(k) = sensor.allanDeviation.at(d).at(k) * (1 - _confidenceMultiplicationFactor.at(k));
299 sensor.allanDeviationConfidenceIntervals.at(d).at(1).at(k) = sensor.allanDeviation.at(d).at(k) * (1 + _confidenceMultiplicationFactor.at(k));
300 }
301 }
302
303 // Compute Slopes
304 for (size_t d = 0; d < 3; d++) { sensor.slope.at(d).resize(_averagingFactors.size(), 0.); }
305
306 for (size_t k = 0; k < _averagingFactors.size(); k++)
307 {
308 size_t lo = (k == 0 ? k : k - 1);
309 size_t hi = (k == _averagingFactors.size() - 1 ? k : k + 1);
310
311 double divisor = log(_averagingFactors.at(hi) / _averagingFactors.at(lo));
312
313 for (size_t d = 0; d < 3; d++)
314 {
315 sensor.slope.at(d).at(k) = log(sensor.allanVariance.at(d).at(hi) / sensor.allanVariance.at(d).at(lo)) / divisor;
316 }
317 }
318 }
319 }
320
321 // empty _cumSum for performance's sake
322 if (lastMessage)
323 {
324 for (auto& sensor : _sensors) { sensor.cumSum.clear(); }
325 }
326 }
327
328 const char* NAV::AllanDeviation::to_string(SensorType sensorType)
329 {
330 switch (sensorType)
331 {
332 case SensorType::Accel:
333 return "Accelerometer";
334 case SensorType::Gyro:
335 return "Gyroscope";
336 case SensorType::SensorType_COUNT:
337 return "Error: Count";
338 }
339 return "";
340 }
341
342 const char* NAV::AllanDeviation::unitString(SensorType sensorType)
343 {
344 switch (sensorType)
345 {
346 case SensorType::Accel:
347 return "m/s²";
348 case SensorType::Gyro:
349 return "rad/s";
350 case SensorType::SensorType_COUNT:
351 return "Error: Count";
352 }
353 return "";
354 }
355