INSTINCT Code Coverage Report


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