INSTINCT Code Coverage Report


Directory: src/
File: Nodes/DataProcessor/GNSS/GnssAnalyzer.cpp
Date: 2025-11-25 23:34:18
Exec Total Coverage
Lines: 13 264 4.9%
Functions: 4 16 25.0%
Branches: 15 478 3.1%

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 "GnssAnalyzer.hpp"
10
11 #include <imgui_internal.h>
12
13 #include "util/Logger.hpp"
14
15 #include "internal/FlowManager.hpp"
16
17 #include "internal/gui/widgets/HelpMarker.hpp"
18
19 #include "Navigation/Constants.hpp"
20 #include "Navigation/GNSS/Core/Code.hpp"
21 #include "Navigation/GNSS/Core/SatelliteIdentifier.hpp"
22
23 #include "NodeData/GNSS/GnssObs.hpp"
24 #include "NodeData/GNSS/GnssCombination.hpp"
25
26 namespace NAV
27 {
28
29 /// @brief Write info to a json object
30 /// @param[out] j Json output
31 /// @param[in] data Object to read info from
32 void to_json(json& j, const GnssAnalyzer::Combination::Term& data)
33 {
34 j = json{
35 { "obsType", data.obsType },
36 { "satSigId", data.satSigId },
37 { "sign", data.sign },
38 };
39 }
40 /// @brief Read info from a json object
41 /// @param[in] j Json variable to read info from
42 /// @param[out] data Output object
43 void from_json(const json& j, GnssAnalyzer::Combination::Term& data)
44 {
45 if (j.contains("obsType")) { j.at("obsType").get_to(data.obsType); }
46 if (j.contains("satSigId")) { j.at("satSigId").get_to(data.satSigId); }
47 if (j.contains("sign")) { j.at("sign").get_to(data.sign); }
48 }
49
50 /// @brief Write info to a json object
51 /// @param[out] j Json output
52 /// @param[in] data Object to read info from
53 void to_json(json& j, const GnssAnalyzer::Combination& data)
54 {
55 j = json{
56 { "description", data.description() },
57 { "terms", data.terms },
58 { "unit", data.unit },
59 { "polynomialCycleSlipDetector", data.polynomialCycleSlipDetector },
60 { "polynomialCycleSlipDetector.thresholdPercentage", data.polynomialCycleSlipDetectorThresholdPercentage },
61 { "polynomialCycleSlipDetector.outputWhenWindowSizeNotReached", data.polynomialCycleSlipDetectorOutputWhenWindowSizeNotReached },
62 { "polynomialCycleSlipDetector.outputPolynomials", data.polynomialCycleSlipDetectorOutputPolynomials },
63 };
64 }
65 /// @brief Read info from a json object
66 /// @param[in] j Json variable to read info from
67 /// @param[out] data Output object
68 void from_json(const json& j, GnssAnalyzer::Combination& data)
69 {
70 if (j.contains("terms"))
71 {
72 j.at("terms").get_to(data.terms);
73 }
74 if (j.contains("unit"))
75 {
76 j.at("unit").get_to(data.unit);
77 }
78 if (j.contains("polynomialCycleSlipDetector"))
79 {
80 j.at("polynomialCycleSlipDetector").get_to(data.polynomialCycleSlipDetector);
81 }
82 if (j.contains("polynomialCycleSlipDetector.thresholdPercentage"))
83 {
84 j.at("polynomialCycleSlipDetector.thresholdPercentage").get_to(data.polynomialCycleSlipDetectorThresholdPercentage);
85 }
86 if (j.contains("polynomialCycleSlipDetector.outputWhenWindowSizeNotReached"))
87 {
88 j.at("polynomialCycleSlipDetector.outputWhenWindowSizeNotReached").get_to(data.polynomialCycleSlipDetectorOutputWhenWindowSizeNotReached);
89 }
90 if (j.contains("polynomialCycleSlipDetector.outputPolynomials"))
91 {
92 j.at("polynomialCycleSlipDetector.outputPolynomials").get_to(data.polynomialCycleSlipDetectorOutputPolynomials);
93 }
94 }
95
96 } // namespace NAV
97
98 114 NAV::GnssAnalyzer::GnssAnalyzer()
99
5/10
✓ 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.
✓ Branch 10 taken 114 times.
✓ Branch 11 taken 114 times.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
456 : Node(typeStatic())
100 {
101 LOG_TRACE("{}: called", name);
102
103 114 _hasConfig = true;
104 114 _guiConfigDefaultWindowSize = { 630, 410 };
105
106
4/8
✓ Branch 2 taken 114 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 114 times.
✗ Branch 7 not taken.
✓ Branch 9 taken 114 times.
✓ Branch 10 taken 114 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
456 CreateOutputPin("GnssComb", Pin::Type::Flow, { NAV::GnssCombination::type() });
107
108
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("GnssObs", Pin::Type::Flow, { NAV::GnssObs::type() }, &GnssAnalyzer::receiveGnssObs);
109 456 }
110
111 228 NAV::GnssAnalyzer::~GnssAnalyzer()
112 {
113 LOG_TRACE("{}: called", nameId());
114 228 }
115
116 228 std::string NAV::GnssAnalyzer::typeStatic()
117 {
118
1/2
✓ Branch 1 taken 228 times.
✗ Branch 2 not taken.
456 return "Gnss Analyzer";
119 }
120
121 std::string NAV::GnssAnalyzer::type() const
122 {
123 return typeStatic();
124 }
125
126 114 std::string NAV::GnssAnalyzer::category()
127 {
128
1/2
✓ Branch 1 taken 114 times.
✗ Branch 2 not taken.
228 return "Data Processor";
129 }
130
131 void NAV::GnssAnalyzer::guiConfig()
132 {
133 ImGui::TextUnformatted("Create combinations of GNSS measurements by adding or subtracting signals.");
134
135 std::vector<size_t> combToDelete;
136 for (size_t c = 0; c < _combinations.size(); c++)
137 {
138 auto& comb = _combinations.at(c);
139
140 bool keepCombination = true;
141 if (ImGui::CollapsingHeader(fmt::format("Combination {}##id{}", c, size_t(id)).c_str(),
142 _combinations.size() > 1 ? &keepCombination : nullptr, ImGuiTreeNodeFlags_DefaultOpen))
143 {
144 if (ImGui::Button(fmt::format("Add Term##id{} c{}", size_t(id), c).c_str()))
145 {
146 flow::ApplyChanges();
147 comb.terms.emplace_back();
148 if (comb.terms.size() != 2) { comb.polynomialCycleSlipDetector.setEnabled(false); }
149 }
150
151 ImGui::SameLine();
152 int selected = comb.unit == Combination::Unit::Meters ? 0 : 1;
153 ImGui::SetNextItemWidth(100.0F);
154 if (ImGui::Combo(fmt::format("Output unit##id{} c{}", size_t(id), c).c_str(), &selected, "Meters\0Cycles\0\0"))
155 {
156 flow::ApplyChanges();
157 comb.unit = selected == 0 ? Combination::Unit::Meters : Combination::Unit::Cycles;
158 }
159
160 ImGui::SameLine();
161 if (ImGui::Button(fmt::format("Cycle-slip detector##id{} c{}", size_t(id), c).c_str()))
162 {
163 ImGui::OpenPopup(fmt::format("Cycle-slip detector##Popup - id{} c{}", size_t(id), c).c_str());
164 }
165 if (ImGui::BeginPopup(fmt::format("Cycle-slip detector##Popup - id{} c{}", size_t(id), c).c_str()))
166 {
167 constexpr float WIDTH = 145.0F;
168 if (PolynomialCycleSlipDetectorGui(fmt::format("Cycle-slip detector id{} c{}", size_t(id), c).c_str(),
169 comb.polynomialCycleSlipDetector, WIDTH))
170 {
171 flow::ApplyChanges();
172 }
173
174 ImGui::SetNextItemWidth(WIDTH);
175 if (double val = comb.polynomialCycleSlipDetectorThresholdPercentage * 100.0;
176 ImGui::DragDouble(fmt::format("Threshold##id{} c{}", size_t(id), c).c_str(), &val, 1.0F,
177 1.0, std::numeric_limits<double>::max(), "%.2f %%"))
178 {
179 comb.polynomialCycleSlipDetectorThresholdPercentage = val / 100.0;
180 flow::ApplyChanges();
181 }
182 ImGui::SameLine();
183 std::string description = "As percentage of the smallest wavelength of the combination terms.";
184 if (auto maxF = std::ranges::max_element(comb.terms, [](const Combination::Term& a, const Combination::Term& b) {
185 return a.satSigId.freq().getFrequency(a.freqNum) < b.satSigId.freq().getFrequency(b.freqNum);
186 });
187 maxF != comb.terms.end())
188 {
189 double lambda = InsConst::C / maxF->satSigId.freq().getFrequency(maxF->freqNum);
190 double threshold = comb.polynomialCycleSlipDetectorThresholdPercentage * lambda;
191 description += fmt::format("\nFor [{} {}] the wavelength is λ = {:.3f} [m].\nThe threshold is then {:.3f} [m].",
192 maxF->satSigId.toSatId().satSys, maxF->satSigId.freq(), lambda, threshold);
193 }
194 gui::widgets::HelpMarker(description.c_str());
195
196 if (!comb.polynomialCycleSlipDetector.isEnabled()) { ImGui::BeginDisabled(); }
197 if (ImGui::Checkbox(fmt::format("Output when insufficient points##id{} c{}", size_t(id), c).c_str(), &comb.polynomialCycleSlipDetectorOutputWhenWindowSizeNotReached))
198 {
199 flow::ApplyChanges();
200 }
201 if (ImGui::Checkbox(fmt::format("Output polynomials##id{} c{}", size_t(id), c).c_str(), &comb.polynomialCycleSlipDetectorOutputPolynomials))
202 {
203 flow::ApplyChanges();
204 }
205 if (!comb.polynomialCycleSlipDetector.isEnabled()) { ImGui::EndDisabled(); }
206
207 ImGui::EndPopup();
208 }
209
210 std::vector<size_t> termToDelete;
211 if (ImGui::BeginTable(fmt::format("##Table id{} c{}", size_t(id), c).c_str(), 3 * static_cast<int>(comb.terms.size()) + 1,
212 ImGuiTableFlags_NoHostExtendX | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollX,
213 ImVec2(0, 70.0F)))
214 {
215 ImGui::TableNextRow();
216
217 for (size_t t = 0; t < comb.terms.size(); t++)
218 {
219 auto& term = comb.terms.at(t);
220
221 ImGui::TableSetColumnIndex(static_cast<int>(t) * 3);
222 int selected = term.sign == 1 ? 0 : 1;
223 ImGui::SetNextItemWidth(50.0F);
224 if (ImGui::Combo(fmt::format("##Sign id{} c{} t{}", size_t(id), c, t).c_str(), &selected, "+1\0-1\0\0"))
225 {
226 flow::ApplyChanges();
227 term.sign = selected == 0 ? +1 : -1;
228 }
229
230 ImGui::TableSetColumnIndex(static_cast<int>(t) * 3 + 1);
231 selected = term.obsType == Combination::Term::ObservationType::Pseudorange ? 0 : 1;
232 ImGui::SetNextItemWidth(62.0F);
233 if (ImGui::Combo(fmt::format("##ObsType id{} c{} t{}", size_t(id), c, t).c_str(), &selected, comb.unit == Combination::Unit::Cycles ? "P\0Φ\0\0" : "p\0φ\0\0"))
234 {
235 flow::ApplyChanges();
236 term.obsType = selected == 0 ? Combination::Term::ObservationType::Pseudorange : Combination::Term::ObservationType::Carrier;
237 }
238
239 ImGui::TableSetColumnIndex(static_cast<int>(t) * 3 + 2);
240 ImGui::SetNextItemWidth(62.0F);
241 if (ShowCodeSelector(fmt::format("##Code id{} c{} t{}", size_t(id), c, t).c_str(), term.satSigId.code, Freq_All, true))
242 {
243 flow::ApplyChanges();
244 }
245 ImGui::SameLine();
246 ImGui::Dummy(ImVec2(10.0F, 0.0F));
247 }
248 ImGui::TableNextColumn();
249 ImGui::TextUnformatted("= Combined Frequency");
250
251 ImGui::TableNextRow();
252 for (size_t t = 0; t < comb.terms.size(); t++)
253 {
254 auto& term = comb.terms.at(t);
255
256 ImGui::TableSetColumnIndex(static_cast<int>(t) * 3);
257 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 8.0F);
258 float radius = 8.0F;
259 ImGui::GetWindowDrawList()->AddCircleFilled(ImGui::GetCursorScreenPos() + ImVec2(radius, ImGui::GetTextLineHeight() / 2.0F + 2.0F), radius,
260 term.receivedDuringRun ? IM_COL32(0, 255, 0, 255) : IM_COL32(255, 0, 0, 255));
261 ImGui::Dummy(ImVec2(radius * 2.0F, ImGui::GetTextLineHeight()));
262 if (ImGui::IsItemHovered()) { ImGui::SetTooltip(term.receivedDuringRun ? "Signal was received" : "Signal was not received"); }
263
264 if (comb.terms.size() > 1)
265 {
266 ImGui::SameLine();
267 if (ImGui::Button(fmt::format("X##id{} c{} t{}", size_t(id), c, t).c_str()))
268 {
269 flow::ApplyChanges();
270 termToDelete.push_back(t);
271 }
272 if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Remove Term?"); }
273 }
274
275 ImGui::TableSetColumnIndex(static_cast<int>(t) * 3 + 1);
276 double f = term.satSigId.freq().getFrequency(term.freqNum);
277 ImGui::TextUnformatted(fmt::format("{:.2f}", f * 1e-6).c_str());
278 if (ImGui::IsItemHovered()) { ImGui::SetTooltip("%s", fmt::format("Frequency in [Mhz]\nλ = {:.3f}m", InsConst::C / f).c_str()); }
279
280 ImGui::TableSetColumnIndex(static_cast<int>(t) * 3 + 2);
281 ImGui::SetNextItemWidth(62.0F);
282 SatId satId = SatId(term.satSigId.toSatId().satSys, term.satSigId.satNum);
283 if (ShowSatelliteSelector(fmt::format("##SatNum id{} c{} t{}", size_t(id), c, t).c_str(), satId, satId.satSys, true))
284 {
285 term.satSigId.satNum = satId.satNum;
286 flow::ApplyChanges();
287 }
288 }
289 ImGui::TableNextColumn();
290 double f = comb.calcCombinationFrequency();
291 ImGui::TextUnformatted(fmt::format(" {:.2f} MHz", f * 1e-6).c_str());
292 if (ImGui::IsItemHovered()) { ImGui::SetTooltip("%s", fmt::format("λ = {:.3f}m", InsConst::C / f).c_str()); }
293
294 ImGui::EndTable();
295 }
296
297 for (const auto& t : termToDelete) { comb.terms.erase(std::next(comb.terms.begin(), static_cast<std::ptrdiff_t>(t))); }
298 }
299
300 if (!keepCombination) { combToDelete.push_back(c); }
301 }
302 for (const auto& c : combToDelete) { _combinations.erase(std::next(_combinations.begin(), static_cast<std::ptrdiff_t>(c))); }
303
304 ImGui::Separator();
305 if (ImGui::Button(fmt::format("Add Combination##id{}", size_t(id)).c_str()))
306 {
307 flow::ApplyChanges();
308 _combinations.emplace_back();
309 _combinations.back().polynomialCycleSlipDetector.setResetAfterCycleSlip(false);
310 }
311 }
312
313 [[nodiscard]] json NAV::GnssAnalyzer::save() const
314 {
315 LOG_TRACE("{}: called", nameId());
316
317 json j;
318
319 j["combinations"] = _combinations;
320
321 return j;
322 }
323
324 void NAV::GnssAnalyzer::restore(json const& j)
325 {
326 LOG_TRACE("{}: called", nameId());
327
328 if (j.contains("combinations"))
329 {
330 j.at("combinations").get_to(_combinations);
331 }
332 }
333
334 bool NAV::GnssAnalyzer::initialize()
335 {
336 LOG_TRACE("{}: called", nameId());
337
338 for (auto& comb : _combinations)
339 {
340 comb.polynomialCycleSlipDetector.clear();
341 comb.polynomials.clear();
342 for (auto& term : comb.terms)
343 {
344 term.receivedDuringRun = false;
345 }
346 }
347
348 return true;
349 }
350
351 void NAV::GnssAnalyzer::deinitialize()
352 {
353 LOG_TRACE("{}: called", nameId());
354
355 for (auto& comb : _combinations)
356 {
357 for (auto& term : comb.terms)
358 {
359 term.receivedDuringRun = false;
360 }
361 }
362 }
363
364 void NAV::GnssAnalyzer::receiveGnssObs(NAV::InputPin::NodeDataQueue& queue, size_t /* pinIdx */)
365 {
366 auto gnssObs = std::static_pointer_cast<const GnssObs>(queue.extract_front());
367 LOG_DATA("{}: Received GnssObs for [{}]", nameId(), gnssObs->insTime);
368
369 auto gnssComb = std::make_shared<GnssCombination>();
370 gnssComb->insTime = gnssObs->insTime;
371
372 for (size_t c = 0; c < _combinations.size(); c++)
373 {
374 auto& comb = _combinations.at(c);
375 GnssCombination::Combination combination;
376 combination.description = comb.description();
377 for (size_t i = 0; i < _combinations.size(); i++)
378 {
379 if (i == c) { continue; }
380 if (combination.description == _combinations.at(i).description())
381 {
382 combination.description += fmt::format(" - {}", c);
383 break;
384 }
385 }
386 LOG_DATA("{}: {}", nameId(), combination.description);
387
388 double result = 0.0;
389 double lambdaMin = 100.0;
390 size_t termsFound = 0;
391 for (auto& term : comb.terms)
392 {
393 GnssCombination::Combination::Term oTerm;
394 oTerm.sign = term.sign;
395 oTerm.satSigId = term.satSigId;
396 oTerm.obsType = term.obsType == Combination::Term::ObservationType::Pseudorange
397 ? GnssObs::ObservationType::Pseudorange
398 : GnssObs::ObservationType::Carrier;
399 LOG_DATA("{}: {}[{}][{}]", nameId(), oTerm.sign == 1 ? "+" : "-", oTerm.satSigId, oTerm.obsType);
400
401 double freq = term.satSigId.freq().getFrequency(term.freqNum);
402 double lambda = InsConst::C / freq;
403 lambdaMin = std::min(lambdaMin, lambda);
404
405 if (auto obs = (*gnssObs)(term.satSigId))
406 {
407 if (term.obsType == Combination::Term::ObservationType::Pseudorange)
408 {
409 if (auto psr = obs->get().pseudorange)
410 {
411 term.receivedDuringRun = true;
412
413 double value = psr->value;
414 result += static_cast<double>(term.sign) * value;
415
416 if (comb.unit == Combination::Unit::Cycles)
417 {
418 value /= lambda;
419 }
420 oTerm.value = value;
421
422 termsFound++;
423 }
424 else
425 {
426 LOG_DATA("{}: Resetting cycle-slip detector as no pseudorange in measurement", nameId());
427 comb.polynomialCycleSlipDetector.reset(comb.description());
428 }
429 }
430 else // if (term.obsType == Combination::Term::ObservationType::Carrier)
431 {
432 if (auto carrier = obs->get().carrierPhase)
433 {
434 term.receivedDuringRun = true;
435
436 double value = carrier->value * lambda;
437 result += static_cast<double>(term.sign) * value;
438
439 if (comb.unit == Combination::Unit::Cycles)
440 {
441 value /= lambda;
442 }
443 oTerm.value = value;
444
445 termsFound++;
446 }
447 else
448 {
449 LOG_DATA("{}: Resetting cycle-slip detector as no carrier-phase in measurement", nameId());
450 comb.polynomialCycleSlipDetector.reset(comb.description());
451 }
452 }
453 }
454
455 combination.terms.push_back(oTerm);
456 }
457 LOG_DATA("{}: Found {}/{}", nameId(), termsFound, comb.terms.size());
458 if (termsFound == comb.terms.size())
459 {
460 auto lambda = InsConst::C / comb.calcCombinationFrequency();
461 double resultCycles = result / lambda;
462 combination.result = comb.unit == Combination::Unit::Cycles ? resultCycles : result;
463
464 if (comb.polynomialCycleSlipDetector.isEnabled())
465 {
466 auto key = comb.description();
467 LOG_DATA("{}: Polynomial {}/{} data points ({} needed for calculation)", nameId(),
468 comb.polynomialCycleSlipDetector.getDataSize(key), comb.polynomialCycleSlipDetector.getWindowSize(),
469 comb.polynomialCycleSlipDetector.getPolynomialDegree() + 1);
470 combination.cycleSlipPrediction = comb.polynomialCycleSlipDetector.predictValue(key, gnssComb->insTime);
471 if (combination.cycleSlipPrediction.has_value())
472 {
473 combination.cycleSlipMeasMinPred = *combination.result - *combination.cycleSlipPrediction;
474 LOG_DATA("{}: Predicting: meas - pred = {}", nameId(), *combination.cycleSlipMeasMinPred);
475 }
476
477 if (comb.polynomialCycleSlipDetectorOutputPolynomials)
478 {
479 if (auto polynomial = comb.polynomialCycleSlipDetector.calcPolynomial(key))
480 {
481 comb.polynomials.emplace_back(gnssComb->insTime, *polynomial);
482 }
483 if (auto relTime = comb.polynomialCycleSlipDetector.calcRelativeTime(key, gnssComb->insTime))
484 {
485 for (const auto& poly : comb.polynomials)
486 {
487 double value = poly.second.f(*relTime);
488 LOG_DATA("f({:.2f}) = {:.2f} ({})", *relTime, value, poly.second.toString());
489 combination.cycleSlipPolynomials.emplace_back(poly.first, poly.second, value);
490 }
491 }
492 }
493 double threshold = comb.polynomialCycleSlipDetectorThresholdPercentage * lambdaMin;
494 if (comb.unit == Combination::Unit::Cycles) { threshold /= lambda; }
495
496 // This adds the measurement to the polynomial
497 combination.cycleSlipResult = comb.polynomialCycleSlipDetector.checkForCycleSlip(key, gnssComb->insTime, *combination.result, threshold);
498
499 if (!comb.polynomialCycleSlipDetectorOutputWhenWindowSizeNotReached
500 && *combination.cycleSlipResult == PolynomialCycleSlipDetectorResult::LessDataThanWindowSize)
501 {
502 combination.cycleSlipPrediction.reset();
503 combination.cycleSlipMeasMinPred.reset();
504 }
505 }
506 }
507
508 gnssComb->combinations.push_back(combination);
509 }
510
511 invokeCallbacks(OUTPUT_PORT_INDEX_GNSS_COMBINATION, gnssComb);
512 }
513