0.5.1
Loading...
Searching...
No Matches
RealTimeKinematic.cpp
Go to the documentation of this file.
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
10
11#include <array>
12#include <algorithm>
13#include <chrono>
14#include <cmath>
15#include <cstddef>
16#include <cstdint>
17#include <imgui.h>
18#include <iterator>
19#include <string>
20#include <unordered_map>
21#include <unordered_set>
22#include <tuple>
23#include <ranges>
24#include <regex>
25#include <utility>
26#include <variant>
27#include <vector>
28#include <spdlog/common.h>
29#include <spdlog/spdlog.h>
30
43#include "util/Json.hpp"
44#include "util/Logger.hpp"
47
49
55
56#include <fmt/core.h>
57#include <fmt/format.h>
59
61
67
68namespace States = NAV::RTK::States;
69namespace Meas = NAV::RTK::Meas;
70
71namespace NAV
72{
73
89
94
96{
97 return "RealTimeKinematic - RTK";
98}
99
100std::string RealTimeKinematic::type() const
101{
102 return typeStatic();
103}
104
106{
107 return "Data Processor";
108}
109
111{
112 auto nSatColumnContent = [&](size_t pinIndex) -> bool {
113 if (auto gnssNavInfo = getInputValue<GnssNavInfo>(pinIndex))
114 {
115 size_t usedSatNum = 0;
116 std::string usedSats;
117 std::string allSats;
118
119 std::string filler = ", ";
120 for (const auto& satellite : gnssNavInfo->v->satellites())
121 {
122 if (_obsFilter.isSatelliteAllowed(satellite.first))
123 {
124 usedSatNum++;
125 usedSats += (allSats.empty() ? "" : filler) + fmt::format("{}", satellite.first);
126 }
127 allSats += (allSats.empty() ? "" : filler) + fmt::format("{}", satellite.first);
128 }
129 ImGui::TextUnformatted(fmt::format("{} / {}", usedSatNum, gnssNavInfo->v->nSatellites()).c_str());
130 if (ImGui::IsItemHovered())
131 {
132 ImGui::SetTooltip("Used satellites: %s\n"
133 "Avail satellites: %s",
134 usedSats.c_str(), allSats.c_str());
135 }
136 }
137 return false;
138 };
139
140 if (_dynamicInputPins.ShowGuiWidgets(size_t(id), inputPins, this, { { .header = "# Sat", .content = nSatColumnContent } }))
141 {
143 }
144
145 ImGui::SameLine();
146 ImGui::BeginGroup();
147 ImGui::PushFont(Application::MonoFont());
148 auto totalSolutions = nFixSolutions + nFloatSolutions + nSingleSolutions;
149 ImGui::SetNextItemOpen(true, ImGuiCond_Always);
150 if (ImGui::TreeNode("Solution statistics:"))
151 {
152 ImGui::BulletText("Fix: %6.2f%% (%zu)", totalSolutions > 0 ? static_cast<double>(nFixSolutions) / static_cast<double>(totalSolutions) * 100.0 : 0.0, nFixSolutions);
153 ImGui::BulletText("Float: %6.2f%% (%zu)", totalSolutions > 0 ? static_cast<double>(nFloatSolutions) / static_cast<double>(totalSolutions) * 100.0 : 0.0, nFloatSolutions);
154 ImGui::BulletText("Single: %6.2f%% (%zu)", totalSolutions > 0 ? static_cast<double>(nSingleSolutions) / static_cast<double>(totalSolutions) * 100.0 : 0.0, nSingleSolutions);
155 ImGui::TreePop();
156 }
157 if (ImGui::TreeNode(fmt::format("Cycle-slips: {}", _nCycleSlipsLLI + _nCycleSlipsSingle + _nCycleSlipsDual).c_str()))
158 {
159 ImGui::BulletText("LLI: %zu", _nCycleSlipsLLI);
160 ImGui::BulletText("Single Freq: %zu", _nCycleSlipsSingle);
161 ImGui::SameLine();
162 gui::widgets::HelpMarker("Single Frequency detection is confirmed by\nthe dual frequency detector before accepted.\nIt then only counts towards the single frequency count though.");
163 ImGui::BulletText("Dual Freq: %zu", _nCycleSlipsDual);
164 ImGui::SameLine();
165 gui::widgets::HelpMarker("Only cycle-slips which were not recognized by\nthe single frequency detector or had the LLI flag set.");
166 ImGui::TreePop();
167 }
168 ImGui::BulletText("Pivot changes: %zu", _nPivotChange);
169 if (_kalmanFilter.isNISenabled()) { ImGui::BulletText("NIS triggered: %zu", _nMeasExcludedNIS); }
170 ImGui::PopFont();
171 ImGui::EndGroup();
172
173 const float unitWidth = 120.0F * gui::NodeEditorApplication::windowFontRatio();
174 const float itemWidth = 310.0F * gui::NodeEditorApplication::windowFontRatio();
175 const float cWidth = itemWidth - ImGui::GetStyle().IndentSpacing;
176
177 if (_obsFilter.ShowGuiWidgets<ReceiverType>(nameId().c_str(), itemWidth))
178 {
180 }
181
182 if (_obsEstimator.ShowGuiWidgets<ReceiverType>(nameId().c_str(), itemWidth))
183 {
185 }
186
187 ImGui::SetNextItemOpen(true, ImGuiCond_FirstUseEver);
188 if (ImGui::TreeNode(fmt::format("Kalman Filter##{}", nameId()).c_str()))
189 {
190 if (_kalmanFilter.showKalmanFilterGUI(nameId().c_str(), itemWidth))
191 {
193 }
194
195 if (_kalmanFilter.isNISenabled())
196 {
197 auto tmp = static_cast<int>(_maxRemoveOutlier);
198 ImGui::SetNextItemWidth(cWidth);
199 if (ImGui::InputIntL(fmt::format("Max. remove per epoch##{}", nameId()).c_str(), &tmp, 0))
200 {
201 _maxRemoveOutlier = static_cast<size_t>(tmp);
203 }
204 ImGui::SameLine();
205 gui::widgets::HelpMarker("Maximum amount of observations to remove per epoch if the NIS triggers");
206
207 tmp = static_cast<int>(_outlierRemoveEpochs);
208 ImGui::SetNextItemWidth(cWidth);
209 if (ImGui::InputIntL(fmt::format("Epochs to exclude outlier##{}", nameId()).c_str(), &tmp, 1))
210 {
211 _outlierRemoveEpochs = static_cast<size_t>(tmp);
213 }
214 ImGui::SameLine();
215 gui::widgets::HelpMarker("Amount of epochs to exclude an outlier for after being flagged");
216
217 tmp = static_cast<int>(_outlierMinSat);
218 ImGui::SetNextItemWidth(cWidth);
219 if (ImGui::InputIntL(fmt::format("Min. satellites for check##{}", nameId()).c_str(), &tmp, 1))
220 {
221 _outlierMinSat = static_cast<size_t>(tmp);
223 }
224 ImGui::SameLine();
225 gui::widgets::HelpMarker("Minumum amount of satellites for doing a NIS check");
226
227 tmp = static_cast<int>(_outlierMinPsrObsKeep);
228 ImGui::SetNextItemWidth(cWidth);
229 if (ImGui::InputIntL(fmt::format("# of pseudorange obs to keep##{}", nameId()).c_str(), &tmp, 1))
230 {
231 _outlierMinPsrObsKeep = static_cast<size_t>(tmp);
233 }
234 ImGui::SameLine();
235 gui::widgets::HelpMarker("Minumum amount of pseudorange observables to leave when doing a NIS check");
236
237 ImGui::SetNextItemWidth(cWidth);
238 if (ImGui::InputDoubleL(fmt::format("Max Position Var for NIS in startup##{}", size_t(id)).c_str(), &_outlierMaxPosVarStartup, 0, std::numeric_limits<double>::max(), 0.0, 0.0, "%.4f m"))
239 {
241 }
242 ImGui::SameLine();
243 gui::widgets::HelpMarker("Maximum position variance\nin order to attempt outlier removal during the startup phase.");
244 }
245 ImGui::SetNextItemOpen(false, ImGuiCond_FirstUseEver);
246 if (ImGui::TreeNode(fmt::format("Matrices##Kalman Filter {}", nameId()).c_str()))
247 {
248 _kalmanFilter.showKalmanFilterMatrixViews(nameId().c_str());
249 ImGui::TreePop();
250 }
251 ImGui::TreePop();
252 }
253
254 ImGui::SetNextItemOpen(true, ImGuiCond_FirstUseEver);
255 if (ImGui::TreeNode(fmt::format("System/Process noise (Standard Deviations)##{}", size_t(id)).c_str()))
256 {
257 if (gui::widgets::InputDouble2WithUnit(fmt::format("Acceleration due to user motion (Hor/Ver)##{}", size_t(id)).c_str(), cWidth, unitWidth, _gui_stdevAccel.data(), _gui_stdevAccelUnits, "m/√(s^3)\0\0", "%.2e", ImGuiInputTextFlags_CharsScientific))
258 {
259 LOG_DEBUG("{}: stdevAccel changed to horizontal {} and vertical {}", nameId(), _gui_stdevAccel.at(0), _gui_stdevAccel.at(1));
260 LOG_DEBUG("{}: stdevAccelNoiseUnits changed to {}", nameId(), fmt::underlying(_gui_stdevAccelUnits));
262 }
263 if (gui::widgets::InputDoubleWithUnit(fmt::format("Ambiguity noise (carrier-phase bias) (float)##{}", size_t(id)).c_str(),
264 cWidth, unitWidth, &_gui_ambiguityFloatProcessNoiseStDev,
265 _gui_stdevAmbiguityFloatUnits, "cycle\0\0",
266 0.0, 0.0, "%.2e", ImGuiInputTextFlags_CharsScientific))
267 {
268 LOG_DEBUG("{}: ambiguityFloatProcessNoiseStDev changed to {}", nameId(), _gui_ambiguityFloatProcessNoiseStDev);
269 LOG_DEBUG("{}: stdevAmbiguityFloatUnits changed to {}", nameId(), fmt::underlying(_gui_stdevAmbiguityFloatUnits));
270
271 switch (_gui_stdevAmbiguityFloatUnits)
272 {
273 case StdevAmbiguityUnits::Cycle:
274 _ambiguityFloatProcessNoiseVariance = std::pow(_gui_ambiguityFloatProcessNoiseStDev, 2);
275 break;
276 }
277
279 }
280 if (gui::widgets::InputDoubleWithUnit(fmt::format("Ambiguity noise (carrier-phase bias) (fix)##{}", size_t(id)).c_str(),
281 cWidth, unitWidth, &_gui_ambiguityFixProcessNoiseStDev,
282 _gui_stdevAmbiguityFixUnits, "cycle\0\0",
283 0.0, 0.0, "%.2e", ImGuiInputTextFlags_CharsScientific))
284 {
285 LOG_DEBUG("{}: ambiguityFixProcessNoiseStDev changed to {}", nameId(), _gui_ambiguityFixProcessNoiseStDev);
286 LOG_DEBUG("{}: stdevAmbiguityFixUnits changed to {}", nameId(), fmt::underlying(_gui_stdevAmbiguityFixUnits));
287
288 switch (_gui_stdevAmbiguityFixUnits)
289 {
290 case StdevAmbiguityUnits::Cycle:
291 _ambiguityFixProcessNoiseVariance = std::pow(_gui_ambiguityFixProcessNoiseStDev, 2);
292 break;
293 }
294
296 }
297
298 ImGui::TreePop();
299 }
300
301 ImGui::SetNextItemOpen(true, ImGuiCond_FirstUseEver);
302 if (ImGui::TreeNode(fmt::format("Ambiguity resolution##{}", size_t(id)).c_str()))
303 {
304 if (!_obsFilter.isObsTypeUsed(GnssObs::Carrier)) { ImGui::BeginDisabled(); }
305
306 ImGui::SetNextItemWidth(cWidth);
307 if (gui::widgets::EnumCombo(fmt::format("Strategy##{}", size_t(id)).c_str(), _ambiguityResolutionStrategy))
308 {
309 LOG_DEBUG("{}: Ambiguity resolution strategy changed to {}", nameId(), NAV::to_string(_ambiguityResolutionStrategy));
311 }
312
313 if (GuiAmbiguityResolution(fmt::format("Ambiguity resolution##{}", size_t(id)).c_str(), _ambiguityResolutionParameters, itemWidth))
314 {
315 LOG_DEBUG("{}: Ambiguity resolution parameters changed to [{}][{}]", nameId(),
316 NAV::to_string(_ambiguityResolutionParameters.decorrelationAlgorithm),
317 NAV::to_string(_ambiguityResolutionParameters.searchAlgorithm));
319 }
320
321 ImGui::SetNextItemWidth(cWidth);
322 if (ImGui::InputDoubleL(fmt::format("Max Position Var for AR##{}", size_t(id)).c_str(), &_maxPosVar, 0, std::numeric_limits<double>::max(), 0.0, 0.0, "%.4f m"))
323 {
325 }
326 ImGui::SameLine();
327 gui::widgets::HelpMarker("Maximum position variance\nin order to attempt ambiguity fixing.");
328
329 auto tmp = static_cast<int>(_nMinSatForAmbFix);
330 ImGui::SetNextItemWidth(cWidth);
331 if (ImGui::InputIntL(fmt::format("Min Sat for Fix##{}", size_t(id)).c_str(), &tmp, 0))
332 {
333 _nMinSatForAmbFix = static_cast<size_t>(tmp);
335 }
336 ImGui::SameLine();
337 gui::widgets::HelpMarker("Minimum amount of satellites with carrier observations\nin order to attempt ambiguity fixing.");
338 if (_ambiguityResolutionStrategy == AmbiguityResolutionStrategy::FixAndHold)
339 {
340 tmp = static_cast<int>(_nMinSatForAmbHold);
341 ImGui::SetNextItemWidth(cWidth);
342 if (ImGui::InputIntL(fmt::format("Min Sat for Hold##{}", size_t(id)).c_str(), &tmp, 0))
343 {
344 _nMinSatForAmbHold = static_cast<size_t>(tmp);
346 }
347 ImGui::SameLine();
348 gui::widgets::HelpMarker("Minimum amount of satellites with carrier observations\nin order to attempt ambiguity holding.");
349 }
350 if (ImGui::Checkbox(fmt::format("Apply fixed ambiguities with KF update##{}", size_t(id)).c_str(), &_applyFixedAmbiguitiesWithUpdate))
351 {
353 }
354 ImGui::SameLine();
355 gui::widgets::HelpMarker("Make a Kalman Filter update with the fixed ambiguities when checked.\n"
356 "Otherwise apply via\n"
357 "- a = a_fix\n"
358 "- b = b_float - Q_ba * Q_aa^-1 (a_fix - a_float)");
359
360 if (_applyFixedAmbiguitiesWithUpdate
361 && gui::widgets::InputDoubleWithUnit(fmt::format("Apply fixed ambiguities measurement noise##{}", size_t(id)).c_str(),
362 cWidth, unitWidth, &_gui_ambFixUpdateStdDev,
363 _gui_ambFixUpdateStdDevUnits, "cycle\0\0",
364 0.0, 0.0, "%.2e", ImGuiInputTextFlags_CharsScientific))
365 {
366 LOG_DEBUG("{}: ambFixUpdateStdDev changed to {}", nameId(), _gui_ambFixUpdateStdDev);
367 LOG_DEBUG("{}: ambFixUpdateStdDevUnits changed to {}", nameId(), fmt::underlying(_gui_ambFixUpdateStdDevUnits));
368
369 switch (_gui_ambFixUpdateStdDevUnits)
370 {
371 case StdevAmbiguityUnits::Cycle:
372 _ambFixUpdateVariance = std::pow(_gui_ambFixUpdateStdDev, 2);
373 break;
374 }
375
377 }
378
379 ImGui::SetNextItemOpen(true, ImGuiCond_FirstUseEver);
380 if (ImGui::TreeNode(fmt::format("Cycle-slip detection##Tree{}", size_t(id)).c_str()))
381 {
382 ImGui::SetNextItemOpen(true, ImGuiCond_FirstUseEver);
383 if (ImGui::TreeNode(fmt::format("Base##Tree{}", size_t(id)).c_str()))
384 {
385 if (CycleSlipDetectorGui(fmt::format("Cycle-slip detector Base##{}", size_t(id)).c_str(), _cycleSlipDetector[Base], 145.0F))
386 {
388 }
389 ImGui::TreePop();
390 }
391 ImGui::SetNextItemOpen(true, ImGuiCond_FirstUseEver);
392 if (ImGui::TreeNode(fmt::format("Rover##Tree{}", size_t(id)).c_str()))
393 {
394 if (CycleSlipDetectorGui(fmt::format("Cycle-slip detector Rover##{}", size_t(id)).c_str(), _cycleSlipDetector[Rover], 145.0F))
395 {
397 }
398 ImGui::TreePop();
399 }
400
401 ImGui::TreePop();
402 }
403
404 if (!_obsFilter.isObsTypeUsed(GnssObs::Carrier)) { ImGui::EndDisabled(); }
405 ImGui::TreePop();
406 }
407 if (ImGui::TreeNode(fmt::format("Misc##{}", size_t(id)).c_str()))
408 {
409 if (ImGui::Checkbox(fmt::format("Output a SPP solution if no base observation available##{}", size_t(id)).c_str(), &_calcSPPIfNoBase))
410 {
412 }
413 ImGui::SetNextItemWidth(cWidth);
414 if (ImGui::InputDoubleL(fmt::format("Max Time between rover & base obs##{}", size_t(id)).c_str(), &_maxTimeBetweenBaseRoverForRTK, 1e-3))
415 {
417 }
418
419 ImGui::TreePop();
420 }
421
422 ImGui::SetNextItemOpen(true, ImGuiCond_FirstUseEver);
423 if (ImGui::TreeNode(fmt::format("Events##{}", size_t(id)).c_str()))
424 {
425 if (ImGui::Checkbox(fmt::format("Output State Change Events##{}", size_t(id)).c_str(), &_outputStateEvents))
426 {
428 }
429 ImGui::SetNextItemWidth(cWidth);
430 if (ImGui::InputText(fmt::format("Events Filter Regex##{}", size_t(id)).c_str(), &_eventFilterRegex))
431 {
433 }
434 if (!_events.empty())
435 {
436 std::optional<std::regex> filter;
437 try
438 {
439 filter = std::regex(_eventFilterRegex, std::regex_constants::ECMAScript | std::regex_constants::icase);
440 }
441 catch (...) // NOLINT(bugprone-empty-catch)
442 {}
443
444 if (ImGui::BeginChild(fmt::format("Events list {}", size_t(id)).c_str(), ImVec2(0.0F, 600.0F), true))
445 {
446 ImGui::PushFont(Application::MonoFont());
447 for (size_t i = 0; i < _events.size(); i++)
448 {
449 if (!std::any_of(_events.at(i).second.begin(), _events.at(i).second.end(), [&](const std::string& text) {
450 return _eventFilterRegex.empty() || (filter && std::regex_search(text, *filter));
451 }))
452 {
453 continue;
454 }
455
456 if (i != 0) { ImGui::Separator(); }
457 ImGui::SetNextItemOpen(true, ImGuiCond_Always);
458 if (ImGui::TreeNode(fmt::format("{} GPST", _events.at(i).first.toYMDHMS(GPST)).c_str()))
459 {
460 for (const auto& text : _events.at(i).second)
461 {
462 if (_eventFilterRegex.empty() || (filter && std::regex_search(text, *filter)))
463 {
464 ImGui::BulletText("%s", text.c_str());
465 }
466 }
467 ImGui::TreePop();
468 }
469 }
470 ImGui::PopFont();
471 }
472 ImGui::EndChild();
473 }
474 ImGui::TreePop();
475 }
476}
477
478[[nodiscard]] json RealTimeKinematic::save() const
479{
480 LOG_TRACE("{}: called", nameId());
481
482 json j;
483
484 j["dynamicInputPins"] = _dynamicInputPins;
485
486 j["obsFilter"] = _obsFilter;
487 j["obsEstimator"] = _obsEstimator;
488 j["kalmanFilter"] = _kalmanFilter;
489 j["maxRemoveOutlier"] = _maxRemoveOutlier;
490 j["outlierRemoveEpochs"] = _outlierRemoveEpochs;
491 j["outlierMinSat"] = _outlierMinSat;
492 j["outlierMinPsrObsKeep"] = _outlierMinPsrObsKeep;
493 j["outlierMaxPosVarStartup"] = _outlierMaxPosVarStartup;
494 j["calcSPPIfNoBase"] = _calcSPPIfNoBase;
495 j["maxTimeBetweenBaseRoverForRTK"] = _maxTimeBetweenBaseRoverForRTK;
496
497 j["ambiguityResolutionParameters"] = _ambiguityResolutionParameters;
498 j["ambiguityResolutionStrategy"] = _ambiguityResolutionStrategy;
499 j["nMinSatForAmbFix"] = _nMinSatForAmbFix;
500 j["nMinSatForAmbHold"] = _nMinSatForAmbHold;
501 j["maxPosVar"] = _maxPosVar;
502 j["applyFixedAmbiguitiesWithUpdate"] = _applyFixedAmbiguitiesWithUpdate;
503 j["ambFixUpdateStdDev"] = _gui_ambFixUpdateStdDev;
504 j["ambFixUpdateStdDevUnits"] = _gui_ambFixUpdateStdDevUnits;
505
506 j["cycleSlipDetector"] = _cycleSlipDetector;
507 j["eventFilterRegex"] = _eventFilterRegex;
508 j["outputStateEvents"] = _outputStateEvents;
509
510 j["stdevAccelUnits"] = _gui_stdevAccelUnits;
511 j["stdevAccel"] = _gui_stdevAccel;
512 j["stdevAmbiguityFloatUnits"] = _gui_stdevAmbiguityFloatUnits;
513 j["ambiguityFloatProcessNoiseStDev"] = _gui_ambiguityFloatProcessNoiseStDev;
514 j["stdevAmbiguityFixUnits"] = _gui_stdevAmbiguityFixUnits;
515 j["ambiguityFixProcessNoiseStDev"] = _gui_ambiguityFixProcessNoiseStDev;
516
517 return j;
518}
519
521{
522 LOG_TRACE("{}: called", nameId());
523
524 if (j.contains("dynamicInputPins")) { NAV::gui::widgets::from_json(j.at("dynamicInputPins"), _dynamicInputPins, this); }
525
526 if (j.contains("obsFilter")) { j.at("obsFilter").get_to(_obsFilter); }
527 if (j.contains("obsEstimator")) { j.at("obsEstimator").get_to(_obsEstimator); }
528 if (j.contains("kalmanFilter")) { j.at("kalmanFilter").get_to(_kalmanFilter); }
529 if (j.contains("maxRemoveOutlier")) { j.at("maxRemoveOutlier").get_to(_maxRemoveOutlier); }
530 if (j.contains("outlierRemoveEpochs")) { j.at("outlierRemoveEpochs").get_to(_outlierRemoveEpochs); }
531 if (j.contains("outlierMinSat")) { j.at("outlierMinSat").get_to(_outlierMinSat); }
532 if (j.contains("outlierMinPsrObsKeep")) { j.at("outlierMinPsrObsKeep").get_to(_outlierMinPsrObsKeep); }
533 if (j.contains("outlierMaxPosVarStartup")) { j.at("outlierMaxPosVarStartup").get_to(_outlierMaxPosVarStartup); }
534 if (j.contains("calcSPPIfNoBase")) { j.at("calcSPPIfNoBase").get_to(_calcSPPIfNoBase); }
535 if (j.contains("maxTimeBetweenBaseRoverForRTK")) { j.at("maxTimeBetweenBaseRoverForRTK").get_to(_maxTimeBetweenBaseRoverForRTK); }
536
537 if (j.contains("ambiguityResolutionParameters")) { j.at("ambiguityResolutionParameters").get_to(_ambiguityResolutionParameters); }
538 if (j.contains("ambiguityResolutionStrategy")) { j.at("ambiguityResolutionStrategy").get_to(_ambiguityResolutionStrategy); }
539 if (j.contains("nMinSatForAmbFix")) { j.at("nMinSatForAmbFix").get_to(_nMinSatForAmbFix); }
540 if (j.contains("nMinSatForAmbHold")) { j.at("nMinSatForAmbHold").get_to(_nMinSatForAmbHold); }
541 if (j.contains("maxPosVar")) { j.at("maxPosVar").get_to(_maxPosVar); }
542 if (j.contains("applyFixedAmbiguitiesWithUpdate")) { j.at("applyFixedAmbiguitiesWithUpdate").get_to(_applyFixedAmbiguitiesWithUpdate); }
543 if (j.contains("ambFixUpdateStdDevUnits")) { j.at("ambFixUpdateStdDevUnits").get_to(_gui_ambFixUpdateStdDevUnits); }
544 if (j.contains("ambFixUpdateStdDev"))
545 {
546 j.at("ambFixUpdateStdDev").get_to(_gui_ambFixUpdateStdDev);
548 {
551 break;
552 }
553 }
554
555 if (j.contains("cycleSlipDetector")) { j.at("cycleSlipDetector").get_to(_cycleSlipDetector); }
556 if (j.contains("eventFilterRegex")) { j.at("eventFilterRegex").get_to(_eventFilterRegex); }
557 if (j.contains("outputStateEvents")) { j.at("outputStateEvents").get_to(_outputStateEvents); }
558
559 if (j.contains("stdevAccelUnits")) { _gui_stdevAccelUnits = j.at("stdevAccelUnits"); }
560 if (j.contains("stdevAccel")) { _gui_stdevAccel = j.at("stdevAccel"); }
561
562 if (j.contains("stdevAmbiguityFloatUnits")) { j.at("stdevAmbiguityFloatUnits").get_to(_gui_stdevAmbiguityFloatUnits); }
563 if (j.contains("ambiguityFloatProcessNoiseStDev"))
564 {
565 j.at("ambiguityFloatProcessNoiseStDev").get_to(_gui_ambiguityFloatProcessNoiseStDev);
567 {
570 break;
571 }
572 }
573 if (j.contains("stdevAmbiguityFixUnits")) { j.at("stdevAmbiguityFixUnits").get_to(_gui_stdevAmbiguityFixUnits); }
574
575 if (j.contains("ambiguityFixProcessNoiseStDev"))
576 {
577 j.at("ambiguityFixProcessNoiseStDev").get_to(_gui_ambiguityFixProcessNoiseStDev);
579 {
582 break;
583 }
584 }
585}
586
588{
589 LOG_TRACE("{}: called", nameId());
590
591 if (std::all_of(inputPins.begin() + INPUT_PORT_INDEX_GNSS_NAV_INFO, inputPins.end(), [](const InputPin& inputPin) { return !inputPin.isPinLinked(); }))
592 {
593 LOG_ERROR("{}: You need to connect a GNSS NavigationInfo provider", nameId());
594 return false;
595 }
596
597 _obsFilter.reset();
598 _events.clear();
599 _pivotSatellites.clear();
600 _lastUpdate.reset();
601 _dataInterval = 1;
602 _receiver = { { Receiver(static_cast<ReceiverType>(0), _obsFilter.getSystemFilter().toVector()),
603 Receiver(static_cast<ReceiverType>(1), _obsFilter.getSystemFilter().toVector()) } };
605 _ambiguitiesHold.clear();
606 nFixSolutions = 0;
607 nFloatSolutions = 0;
609 _nPivotChange = 0;
610 _nCycleSlipsLLI = 0;
614
616 _maxPosVarTriggered = false;
619
620 _sppAlgorithm.reset();
621 _sppAlgorithm._obsFilter = _obsFilter;
622 _sppAlgorithm._obsFilter.useObsType(GnssObs::Pseudorange);
623 _sppAlgorithm._obsFilter.useObsType(GnssObs::Doppler);
624 _sppAlgorithm._obsEstimator = _obsEstimator;
625 _sppAlgorithm._estimateInterFreqBiases = false;
626
627 json j;
632
633 _kalmanFilter.F.block<3>(States::Pos, States::Vel) = Eigen::Matrix3d::Identity();
634
635 for (auto& detector : _cycleSlipDetector) { detector.reset(); }
636
637 // 𝜎²_a Variance of the acceleration due to user motion in horizontal and vertical component in [m^2 / s^3]
638 std::array<double, 2> varAccel{};
639 switch (_gui_stdevAccelUnits)
640 {
641 case StdevAccelUnits::m_sqrts3: // [m / √(s^3)]
642 varAccel = { std::pow(_gui_stdevAccel.at(0), 2), std::pow(_gui_stdevAccel.at(1), 2) };
643 break;
644 }
645 LOG_DATA(" sigma2_accel = h: {}, v: {} [m^2 / s^3]", nameId(), varAccel.at(0), varAccel.at(1));
646 _kalmanFilter.W.block<3>(States::Vel, States::Vel).diagonal() << varAccel.at(0), varAccel.at(0), varAccel.at(1);
647
648 LOG_DEBUG("RealTimeKinematic initialized");
649
650 return true;
651}
652
654{
655 LOG_TRACE("{}: called", nameId());
656}
657
662
664{
665 node->DeleteInputPin(pinIdx);
666}
667
668void RealTimeKinematic::addEventToGui(const std::shared_ptr<RtkSolution>& rtkSol, const std::string& text)
669{
670 auto iter = std::ranges::find_if(_events, [&](const auto& event) { return event.first == rtkSol->insTime; });
671 if (iter == _events.end())
672 {
673 _events.emplace_back(rtkSol->insTime, std::vector{ text });
674 }
675 else
676 {
677 iter->second.emplace_back(text);
678 }
679 rtkSol->addEvent(text);
680}
681
682void RealTimeKinematic::printObservations([[maybe_unused]] const Observations& observations)
683{
684#if LOG_LEVEL <= LOG_LEVEL_DATA
687 for (const auto& obs : observations.signals)
688 {
689 satData[obs.first.toSatId()].first |= obs.first.freq();
690 satData[obs.first.toSatId()].second |= obs.first.code;
691 for (size_t obsType = 0; obsType < GnssObs::ObservationType_COUNT; obsType++)
692 {
693 if (std::ranges::all_of(obs.second.recvObs, [&obsType](const auto& recvObs) {
694 return recvObs.second->obs.contains(static_cast<GnssObs::ObservationType>(obsType));
695 }))
696 {
697 sigData[obs.first].insert(static_cast<GnssObs::ObservationType>(obsType));
698 }
699 }
700 }
701 for ([[maybe_unused]] const auto& [satId, freqCode] : satData)
702 {
703 LOG_DATA("{}: [{}] on frequencies [{}] with codes [{}]", nameId(), satId, freqCode.first, freqCode.second);
704 for (const auto& [satSigId, obs] : sigData)
705 {
706 if (satSigId.toSatId() != satId) { continue; }
707 std::string str;
708 for (const auto& o : obs)
709 {
710 if (!str.empty()) { str += ", "; }
711 str += fmt::format("{}", o);
712 }
713 LOG_DATA("{}: [{}] has obs: {}", nameId(), satSigId.code, str);
714 }
715 }
716#endif
717}
718
720{
721 auto gnssObs = std::static_pointer_cast<const Pos>(queue.extract_front());
722 LOG_DATA("{}: Received Base Position for [{}]", nameId(), gnssObs->insTime.toYMDHMS(GPST));
723
724 if (_receiver[Base].e_posMarker.isZero())
725 {
726 _receiver[Base].e_posMarker = gnssObs->e_position();
727 _receiver[Base].lla_posMarker = gnssObs->lla_position();
728 }
729 else
730 {
731 _receiver[Base].e_posMarker = (_receiver[Base].e_posMarker + gnssObs->e_position()) / 2;
732 _receiver[Base].lla_posMarker = trafo::ecef2lla_WGS84(_receiver[Base].e_posMarker);
733 }
734
735 if (_receiver[Base].gnssObs && _receiver[Rover].gnssObs && std::chrono::abs(_receiver[Base].gnssObs->insTime - _receiver[Rover].gnssObs->insTime) < std::chrono::milliseconds(10))
736 {
738 }
739}
740
742{
743 _receiver[Base].gnssObs = std::static_pointer_cast<const GnssObs>(queue.extract_front());
745 LOG_DATA("{}: Received Base GNSS Obs for [{}]", nameId(), _receiver[Base].gnssObs->insTime.toYMDHMS(GPST));
746
747 if (_receiver[Rover].gnssObs && (_receiver[Base].gnssObs->insTime - _receiver[Rover].gnssObs->insTime) > std::chrono::milliseconds(500))
748 {
749 LOG_TRACE("{}: Data gap between Base [{}] and Rover [{}] is {:.1f}s. Falling back to SPP for gaps larger than 0.5s", nameId(),
750 _receiver[Base].gnssObs->insTime.toYMDHMS(GPST), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST),
751 static_cast<double>((_receiver[Base].gnssObs->insTime - _receiver[Rover].gnssObs->insTime).count()));
752 if (auto rtkSol = calcFallbackSppSolution())
753 {
756 }
757 _receiver[Rover].gnssObs = nullptr;
758 }
759 else if (!_receiver[Base].e_posMarker.isZero() && _receiver[Base].gnssObs && _receiver[Rover].gnssObs
760 && std::chrono::abs(_receiver[Base].gnssObs->insTime - _receiver[Rover].gnssObs->insTime) < std::chrono::milliseconds(100))
761 {
763 }
764}
765
767{
768 _receiver[Rover].gnssObs = std::static_pointer_cast<const GnssObs>(queue.extract_front());
769 LOG_DATA("{}: Received Rover GNSS Obs for [{}]", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST));
770
771 if (_receiver[Base].gnssObs && (_receiver[Rover].gnssObs->insTime - _receiver[Base].gnssObs->insTime) > std::chrono::milliseconds(static_cast<int>(_maxTimeBetweenBaseRoverForRTK * 1e3)))
772 {
773 if (!_calcSPPIfNoBase) { return; }
774 LOG_TRACE("{}: Calculating SPP Fallback, as Rover obs [{}] is {:.3f} seconds away from last base obs [{}]. Maximum time to consider the base observation is {} seconds.",
775 nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST),
776 static_cast<double>((_receiver[Rover].gnssObs->insTime - _receiver[Base].gnssObs->insTime).count()),
777 _receiver[Base].gnssObs->insTime.toYMDHMS(GPST),
779 if (auto rtkSol = calcFallbackSppSolution())
780 {
783 }
784 _receiver[Rover].gnssObs = nullptr;
785 }
786 else if (!_receiver[Base].e_posMarker.isZero() && _receiver[Base].gnssObs && _receiver[Rover].gnssObs
787 && std::chrono::abs(_receiver[Base].gnssObs->insTime - _receiver[Rover].gnssObs->insTime) < std::chrono::milliseconds(100))
788 {
790 }
791}
792
793void RealTimeKinematic::assignSolutionToFilter(const std::shared_ptr<NAV::SppSolution>& sppSol)
794{
795 _lastUpdate = sppSol->insTime;
796 _receiver[Rover].e_posMarker = sppSol->e_position();
797 _receiver[Rover].lla_posMarker = trafo::ecef2lla_WGS84(_receiver[Rover].e_posMarker);
798 if (!sppSol->e_velocity().hasNaN())
799 {
800 _receiver[Rover].e_vel = sppSol->e_velocity();
801 }
802
803 _kalmanFilter.x.segment<3>(States::Pos) = _receiver[Rover].e_posMarker;
804 _kalmanFilter.x.segment<3>(States::Vel) = _receiver[Rover].e_vel;
805
806 if (sppSol->e_CovarianceMatrix().has_value())
807 {
808 if (sppSol->e_CovarianceMatrix()->get().hasRows(Keys::PosVel<Keys::MotionModelKey>))
809 {
811 }
812 else
813 {
815 _kalmanFilter.P(States::Vel, States::Vel) = Eigen::DiagonalMatrix<double, 3>(1, 1, 1);
816 }
817 }
818}
819
821{
822 auto dt = _lastUpdate.empty() ? 0.0 : static_cast<double>((_receiver[Rover].gnssObs->insTime - _lastUpdate).count());
823 LOG_DATA("{}: Calculate RTK Solution for rover [{}], base [{}] (dt = {:.2f})", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), _receiver[Base].gnssObs->insTime.toYMDHMS(GPST), dt);
824
825 // Collection of all connected navigation data providers
826 std::vector<InputPin::IncomingLink::ValueWrapper<GnssNavInfo>> gnssNavInfoWrappers;
827 std::vector<const GnssNavInfo*> gnssNavInfos;
828 for (size_t i = 0; i < _dynamicInputPins.getNumberOfDynamicPins(); i++)
829 {
831 {
832 gnssNavInfoWrappers.push_back(*gnssNavInfo);
833 gnssNavInfos.push_back(gnssNavInfo->v);
834 }
835 }
836 if (!gnssNavInfos.empty())
837 {
838 auto rtkSol = std::make_shared<RtkSolution>();
839 rtkSol->insTime = _receiver[Rover].gnssObs->insTime;
840 rtkSol->baseTime = _receiver[Base].gnssObs->insTime;
841
842 bool startupPhase = dt == 0.0 ? true : static_cast<double>(nFixSolutions + nFloatSolutions + nSingleSolutions) * dt < 60.0;
843
844 if (!startupPhase && !_receiver[Rover].e_posMarker.isZero())
845 {
846 if (dt > 10.0)
847 {
848 _ambiguitiesHold.clear();
849 _pivotSatellites.clear();
850 std::vector<States::StateKeyType> ambKeys;
851 for (size_t i = States::KFStates_COUNT; i < _kalmanFilter.x.rowKeys().size(); i++) // 0-2 Pos, 3-5 Vel
852 {
853 if (const auto* ambDD = std::get_if<States::AmbiguityDD>(&_kalmanFilter.x.rowKeys().at(i)))
854 {
855 ambKeys.emplace_back(*ambDD);
856 }
857 }
858 _kalmanFilter.removeStates(ambKeys);
859 _receiver[Rover].e_posMarker.setZero();
860 _receiver[Rover].e_vel.setZero();
861
862 std::string text = fmt::format("Outage of {:.1f} [s] (interval {:.1f} [s]). Reinitializing with SPP solution.", dt, _dataInterval);
863 LOG_WARN("{}: [{}] {}", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), text);
864 addEventToGui(rtkSol, text);
865 }
866 else if (_kalmanFilter.x.rows() > States::KFStates_COUNT && dt > 3.0 * _dataInterval)
867 {
868 _ambiguitiesHold.clear();
869 std::vector<States::StateKeyType> ambKeys;
870 for (size_t i = States::KFStates_COUNT; i < _kalmanFilter.x.rowKeys().size(); i++) // 0-2 Pos, 3-5 Vel
871 {
872 if (const auto* ambDD = std::get_if<States::AmbiguityDD>(&_kalmanFilter.x.rowKeys().at(i)))
873 {
874 ambKeys.emplace_back(*ambDD);
875 }
876 }
877 _kalmanFilter.removeStates(ambKeys);
878
879 std::string text = fmt::format("Outage of {:.1f} [s] (interval {:.1f} [s]). Resetting ambiguities.", dt, _dataInterval);
880 LOG_WARN("{}: [{}] {}", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), text);
881 addEventToGui(rtkSol, text);
882 }
883 }
884
885 // Calculate a Single point solution if the Rover Position is not known yet
886 if (_receiver[Rover].e_posMarker.isZero())
887 {
888 if (auto sol = _sppAlgorithm.calcSppSolution(_receiver[Rover].gnssObs, gnssNavInfos, nameId()))
889 {
891
892 LOG_DEBUG("{}: Initial base position: {}°, {}°, {}m (ECEF {} {} {} [m])", nameId(),
893 rad2deg(_receiver[Base].lla_posMarker(0)), rad2deg(_receiver[Base].lla_posMarker(1)), _receiver[Base].lla_posMarker(2),
894 _receiver[Base].e_posMarker.x(), _receiver[Base].e_posMarker.y(), _receiver[Base].e_posMarker.z());
895 LOG_DEBUG("{}: Initial rover position: {}°, {}°, {}m (ECEF {} {} {} [m])", nameId(),
896 rad2deg(_receiver[Rover].lla_posMarker(0)), rad2deg(_receiver[Rover].lla_posMarker(1)), _receiver[Rover].lla_posMarker(2),
897 _receiver[Rover].e_posMarker.x(), _receiver[Rover].e_posMarker.y(), _receiver[Rover].e_posMarker.z());
898 LOG_DEBUG("{}: Initial rover velocity: {} [m/s] (ECEF)", nameId(), _receiver[Rover].e_vel.transpose());
899 }
900 else
901 {
902 LOG_DATA("{}: Could not calculate initial position solution.", nameId());
903 return;
904 }
905 }
906
907 if (_outlierMaxPosVarStartupTriggered) { startupPhase = false; }
908 else if (startupPhase)
909 {
910 if (double posVar = _kalmanFilter.P(States::Pos, States::Pos).diagonal().sum() / 3.0;
912 {
914 addEventToGui(rtkSol, fmt::format("Doing NIS check from now on because\nposition variance is below {:.4f}m^2", _outlierMaxPosVarStartup));
915 }
916 }
917
918 // Collection of all connected Ionospheric Corrections
919 auto ionosphericCorrections = std::make_shared<const IonosphericCorrections>(gnssNavInfos);
920
922
923 {
924 auto logLevel = spdlog::get_level();
925 spdlog::set_level(spdlog::level::debug);
926 // Calculate all possible observations here, to put into solution for analysis
928 obsFilter.disableFilter();
929
930 Observations observations;
932 _receiver[Rover].e_posMarker,
933 _receiver[Rover].lla_posMarker,
934 _receiver[Rover].gnssObs,
935 gnssNavInfos,
936 observations, nullptr, nameId() + " no filter");
938 _receiver[Base].e_posMarker,
939 _receiver[Base].lla_posMarker,
940 _receiver[Base].gnssObs,
941 gnssNavInfos,
942 observations, nullptr, nameId() + " no filter");
943 removeSingleObservations(observations, nullptr, nullptr);
944
945 for (const auto& [satSigId, sigObs] : observations.signals)
946 {
947 auto satId = satSigId.toSatId();
948 if (std::ranges::find_if(rtkSol->satData, [&satId](const auto& sat) {
949 return satId == sat.first;
950 })
951 != rtkSol->satData.end())
952 {
953 continue;
954 }
955 rtkSol->satData.emplace_back(satSigId.toSatId(),
956 RtkSolution::SatData{ .satElevation = sigObs.recvObs.at(Rover)->satElevation(_receiver[Rover].e_posMarker, _receiver[Rover].lla_posMarker),
957 .satAzimuth = sigObs.recvObs.at(Rover)->satElevation(_receiver[Rover].e_posMarker, _receiver[Rover].lla_posMarker) });
958 }
959 for (const auto& [satSigId, sigObs] : observations.signals)
960 {
961 for (const auto& obs : sigObs.recvObs.at(Rover)->obs)
962 {
963 rtkSol->observableReceived.emplace(satSigId, obs.first);
964 }
965 }
966 spdlog::set_level(logLevel);
967 }
968
969 Observations observations;
971 _obsFilter.selectObservationsForCalculation(Rover,
972 _receiver[Rover].e_posMarker,
973 _receiver[Rover].lla_posMarker,
974 _receiver[Rover].gnssObs,
975 gnssNavInfos,
976 observations, &filtered, nameId());
977 _obsFilter.selectObservationsForCalculation(Base,
978 _receiver[Base].e_posMarker,
979 _receiver[Base].lla_posMarker,
980 _receiver[Base].gnssObs,
981 gnssNavInfos,
982 observations, &filtered, nameId());
983 removeSingleObservations(observations, &filtered, rtkSol);
984 rtkSol->filtered = filtered;
985
986 for (const auto& [satSigId, sigObs] : observations.signals)
987 {
988 for (const auto& obs : sigObs.recvObs.at(Rover)->obs)
989 {
990 rtkSol->observableFiltered.emplace(satSigId, obs.first);
991 }
992 }
993 rtkSol->solType = RtkSolution::SolutionType::Predicted;
994
995 if (!observations.signals.empty() && observations.satellites.size() > 1)
996 {
997 _obsEstimator.calcObservationEstimates(observations, _receiver[Rover], ionosphericCorrections, nameId(), ObservationEstimator::DoubleDifference);
998 _obsEstimator.calcObservationEstimates(observations, _receiver[Base], ionosphericCorrections, nameId(), ObservationEstimator::DoubleDifference);
999
1000 checkForCycleSlip(observations, rtkSol);
1001 updatePivotSatellites(observations, rtkSol);
1002 addOrRemoveKalmanFilterAmbiguities(observations, rtkSol);
1003
1004 printObservations(observations);
1005
1006 bool nisEnabled = !startupPhase && _kalmanFilter.isNISenabled();
1007 size_t maxIter = nisEnabled ? (_maxRemoveOutlier == 0 ? 1000 : _maxRemoveOutlier) : 1;
1008
1009 bool pivotChanged = true;
1010 Differences singleDifferences;
1011 Differences doubleDifferences;
1013
1014 for (size_t i = 0; i < maxIter; i++)
1015 {
1016 LOG_DATA("{}: Iteration: {}/{}", nameId(), i, maxIter);
1017
1018 if (pivotChanged)
1019 {
1020 singleDifferences = calcSingleDifferences(observations);
1021 doubleDifferences = calcDoubleDifferences(observations, singleDifferences);
1022
1023 Rtilde = calcSingleObsMeasurementNoiseMatrices(observations);
1024 calcKalmanUpdateMatrices(observations, doubleDifferences, Rtilde);
1025 pivotChanged = false;
1026 }
1027
1028 if (nisEnabled && !startupPhase)
1029 {
1030 LOG_DATA("{}: Doing NIS check", nameId());
1031 auto nisResult = _kalmanFilter.checkForOutliersNIS(nameId());
1032 if (!rtkSol->nisResultInitial) { rtkSol->nisResultInitial = nisResult; }
1033 rtkSol->nisResultFinal = nisResult;
1034 if (nisResult.triggered && observations.satellites.size() <= _outlierMinSat)
1035 {
1036 nisResult.triggered = false;
1037 addEventToGui(rtkSol, "Stopped doing NIS check, as minimum amount of satellites reached.");
1038 }
1039 if (nisResult.triggered)
1040 {
1041 LOG_DATA("{}: NIS triggered", nameId());
1042
1043 if (auto outliers = removeOutlier(observations, rtkSol);
1044 !outliers.empty())
1045 {
1046 removeSingleObservations(observations, &rtkSol->filtered, rtkSol);
1047 for (const auto& outlier : outliers)
1048 {
1049 pivotChanged |= outlier.pivot.has_value();
1050 }
1051 }
1052 else
1053 {
1054 LOG_DATA("{}: NIS check finished as nothing removed", nameId());
1055 break;
1056 }
1057 }
1058 else
1059 {
1060 LOG_DATA("{}: NIS check successful", nameId());
1061 break;
1062 }
1063 }
1064 }
1065 if (nisEnabled)
1066 {
1067 LOG_DATA("{}: Final NIS = {} < {} = r2. {} removed", nameId(), rtkSol->nisResultFinal->NIS, rtkSol->nisResultFinal->r2, rtkSol->nisRemovedCnt);
1068 observations.recalcObservableCounts(nameId());
1069 }
1070
1071 if (kalmanFilterUpdate(rtkSol).valid)
1072 {
1074 {
1075 rtkSol->solType = RtkSolution::SolutionType::RTK_Fixed;
1076 nFixSolutions++;
1077 }
1078 else
1079 {
1080 rtkSol->solType = RtkSolution::SolutionType::RTK_Float;
1082 }
1083 }
1084
1085 rtkSol->measInnovation = _kalmanFilter.z;
1086 }
1087 else
1088 {
1089 LOG_TRACE("{}: No observations found. Check that your base and rover file match and reconfigure the filter.", nameId());
1090 }
1091
1092 _lastSolutionStatus = rtkSol->solType;
1093
1094 // Write out results
1095 for (size_t obsType = 0; obsType < GnssObs::ObservationType_COUNT; obsType++)
1096 {
1097 rtkSol->nObservations.emplace(static_cast<GnssObs::ObservationType>(obsType), observations.nObservables.at(obsType));
1098 rtkSol->nObservationsUniqueSatellite.emplace(static_cast<GnssObs::ObservationType>(obsType), observations.nObservablesUniqueSatellite.at(obsType));
1099 }
1100 rtkSol->nSatellites = observations.satellites.size();
1101 rtkSol->distanceBaseRover = (_receiver[Base].e_posMarker - _receiver[Rover].e_posMarker).norm();
1102
1103 rtkSol->setPosVelAndCov_e(_receiver[Rover].e_posMarker, _receiver[Rover].e_vel,
1105 if (rtkSol->solType == RtkSolution::SolutionType::RTK_Fixed || rtkSol->solType == RtkSolution::SolutionType::RTK_Float)
1106 {
1107 rtkSol->ambiguityDD_br.reserve(_kalmanFilter.x.rowKeys().size() - States::KFStates_COUNT);
1108 for (size_t i = States::KFStates_COUNT; i < _kalmanFilter.x.rowKeys().size(); i++) // 0-2 Pos, 3-5 Vel
1109 {
1110 if (const auto* ambDD = std::get_if<States::AmbiguityDD>(&_kalmanFilter.x.rowKeys().at(i)))
1111 {
1112 const auto& key = States::AmbiguityDD(ambDD->satSigId);
1113 LOG_DATA("{}: Key: {}", nameId(), key);
1114 auto ambDDSol = RtkSolution::AmbiguityDD{
1115 .pivotSatSigId = _pivotSatellites.at({ ambDD->satSigId.code, GnssObs::Carrier }).satSigId,
1116 .satSigId = ambDD->satSigId,
1117 .value = UncertainValue<double>{ .value = _kalmanFilter.x(key),
1118 .stdDev = std::sqrt(_kalmanFilter.P(key, key)) }
1119 };
1120 rtkSol->ambiguityDD_br.push_back(ambDDSol);
1121 }
1122 }
1123 for (const auto& pivot : _pivotSatellites)
1124 {
1125 rtkSol->pivots.emplace(pivot.second.satSigId, pivot.first.second);
1126 }
1127 for (const auto& [satSigId, sigObs] : observations.signals)
1128 {
1129 for (const auto& obs : sigObs.recvObs.at(Rover)->obs)
1130 {
1131 rtkSol->observableUsed.emplace(satSigId, obs.first);
1132 }
1133 }
1134 }
1135
1136 if (dt > 1e-3) { _dataInterval = dt; }
1137 _lastUpdate = rtkSol->insTime;
1139 }
1140
1141 _receiver[Rover].gnssObs = nullptr;
1142 _baseObsReceivedThisEpoch = false;
1143}
1144
1146{
1147 LOG_DATA("{}: Calculate SPP Solution for [{}] (fallback)", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST));
1148
1149 // Collection of all connected navigation data providers
1150 std::vector<InputPin::IncomingLink::ValueWrapper<GnssNavInfo>> gnssNavInfoWrappers;
1151 std::vector<const GnssNavInfo*> gnssNavInfos;
1152 for (size_t i = 0; i < _dynamicInputPins.getNumberOfDynamicPins(); i++)
1153 {
1155 {
1156 gnssNavInfoWrappers.push_back(*gnssNavInfo);
1157 gnssNavInfos.push_back(gnssNavInfo->v);
1158 }
1159 }
1160
1161 if (auto sppSol = _sppAlgorithm.calcSppSolution(_receiver[Rover].gnssObs, gnssNavInfos, nameId()))
1162 {
1163 auto rtkSol = std::make_shared<RtkSolution>();
1164 rtkSol->insTime = sppSol->insTime;
1165 rtkSol->solType = RtkSolution::SolutionType::SPP;
1166 rtkSol->nSatellites = sppSol->nSatellites;
1167 if (!_receiver[Base].e_posMarker.isZero())
1168 {
1169 rtkSol->distanceBaseRover = (_receiver[Base].e_posMarker - _receiver[Rover].e_posMarker).norm();
1170 }
1171 rtkSol->nObservations[GnssObs::Pseudorange] = sppSol->nMeasPsr;
1172 rtkSol->nObservations[GnssObs::Doppler] = sppSol->nMeasDopp;
1173
1174 if (sppSol->e_CovarianceMatrix().has_value())
1175 {
1176 if (sppSol->e_CovarianceMatrix()->get().hasAnyRows(Keys::Vel<Keys::MotionModelKey>))
1177 {
1178 rtkSol->setPosVelAndCov_e(sppSol->e_position(), sppSol->e_velocity(),
1180 }
1181 else
1182 {
1183 rtkSol->setPositionAndCov_e(sppSol->e_position(),
1184 (*sppSol->e_CovarianceMatrix())(Keys::Pos<Keys::MotionModelKey>, Keys::Pos<Keys::MotionModelKey>));
1185 }
1186 }
1187 else
1188 {
1189 rtkSol->setPosition_e(sppSol->e_position());
1190 rtkSol->setVelocity_e(sppSol->e_velocity());
1191 }
1192
1193 assignSolutionToFilter(sppSol);
1194
1195 return rtkSol;
1196 }
1197
1198 return nullptr;
1199}
1200
1202{
1203 // Update the State transition matrix (𝚽) and the Process noise covariance matrix (𝐐)
1204
1205 auto dt = static_cast<double>((_receiver[Rover].gnssObs->insTime - _lastUpdate).count());
1206 LOG_DATA("{}: dt = {}s", nameId(), dt);
1207
1208 _kalmanFilter.G.block<3>(States::Vel, States::Vel) = trafo::e_Quat_n(_receiver[Rover].lla_posMarker(0), _receiver[Rover].lla_posMarker(1)).toRotationMatrix();
1209
1210 LOG_DATA("{}: F =\n{}", nameId(), _kalmanFilter.F);
1211 LOG_DATA("{}: G =\n{}", nameId(), _kalmanFilter.G);
1212 LOG_DATA("{}: W =\n{}", nameId(), _kalmanFilter.W);
1213 LOG_DATA("{}: GWG^T =\n{}", nameId(),
1215 * _kalmanFilter.W(all, all)
1216 * _kalmanFilter.G(all, all).transpose(),
1217 _kalmanFilter.G.rowKeys()));
1218
1222 dt);
1223 _kalmanFilter.Phi.block<6>(States::PosVel, States::PosVel) = Phi;
1225
1227 for (size_t i = States::KFStates_COUNT; i < _kalmanFilter.x.rowKeys().size(); i++) // 0-2 Pos, 3-5 Vel
1228 {
1229 if (const auto* ambDD = std::get_if<States::AmbiguityDD>(&_kalmanFilter.x.rowKeys().at(i)))
1230 {
1231 const auto& key = States::AmbiguityDD(ambDD->satSigId);
1232
1233 _kalmanFilter.W(key, key) = ambNoiseVar;
1234 _kalmanFilter.Q(key, key) = ambNoiseVar * dt;
1235 }
1236 }
1237
1238 LOG_DATA("{}: Phi =\n{}", nameId(), _kalmanFilter.Phi);
1239 LOG_DATA("{}: Q =\n{}", nameId(), _kalmanFilter.Q);
1240
1241 LOG_DATA("{}: P (a posteriori, t-1 = {}) =\n{}", nameId(), _lastUpdate, _kalmanFilter.P);
1242 LOG_DATA("{}: x (a posteriori, t-1 = {}) =\n{}", nameId(), _lastUpdate, _kalmanFilter.x.transposed());
1243 _kalmanFilter.predict();
1244 LOG_DATA("{}: Doing KF prediction", nameId());
1245 LOG_DATA("{}: x (a priori , t = {}) =\n{}", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), _kalmanFilter.x.transposed());
1246 LOG_DATA("{}: P (a priori , t = {}) =\n{}", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), _kalmanFilter.P);
1247 _receiver[Rover].e_posMarker = _kalmanFilter.x.segment<3>(States::Pos);
1248 _receiver[Rover].e_vel = _kalmanFilter.x.segment<3>(States::Vel);
1249 _receiver[Rover].lla_posMarker = trafo::ecef2lla_WGS84(_receiver[Rover].e_posMarker);
1250}
1251
1252void RealTimeKinematic::checkForCycleSlip(Observations& observations, const std::shared_ptr<RtkSolution>& rtkSol)
1253{
1254 if (std::ranges::none_of(_cycleSlipDetector, [](const CycleSlipDetector& detector) {
1258 }))
1259 {
1260 return;
1261 }
1262 LOG_DATA("{}: Checking for cycle-slips", nameId());
1263
1264 std::array<std::vector<CycleSlipDetector::SatelliteObservation>, ReceiverType_COUNT> cycleSlipObservations;
1265
1266 for (const auto& satId : observations.satellites)
1267 {
1268 bool added = false;
1269 for (const auto& [satSigId, signalObs] : observations.signals) // Signals
1270 {
1271 if (satSigId.toSatId() != satId) { continue; }
1272
1273 for (size_t r = 0; r < ReceiverType_COUNT; r++) // ReceiverType
1274 {
1275 auto recvType = static_cast<ReceiverType>(r);
1276
1277 if (recvType == Base && !_baseObsReceivedThisEpoch) { continue; } // Process base observation only in epoch where received
1278
1279 if (auto detector = _cycleSlipDetector.at(recvType);
1283 {
1284 continue;
1285 }
1286
1287 for (const auto& receiverObs : _receiver.at(recvType).gnssObs->data) // ObservationType
1288 {
1289 if (receiverObs.satSigId.toSatId() == satId && receiverObs.carrierPhase)
1290 {
1291 [[maybe_unused]] auto lambda = InsConst::C / receiverObs.satSigId.freq().getFrequency(signalObs.freqNum());
1292 LOG_DATA("[{}] [{}] Added to cycle-slip detection: {:.1f} [m]", receiverObs.satSigId, recvType, receiverObs.carrierPhase->value * lambda);
1293
1294 GnssObs::ObservationData::CarrierPhase carrier{ .value = receiverObs.carrierPhase->value, .LLI = receiverObs.carrierPhase->LLI };
1295 auto signal = CycleSlipDetector::SatelliteObservation::Signal{ .code = receiverObs.satSigId.code, .measurement = carrier };
1296
1297 auto sat = std::ranges::find_if(cycleSlipObservations.at(recvType),
1298 [&satId](const auto& satObs) { return satObs.satId == satId; });
1299
1300 if (sat == cycleSlipObservations.at(recvType).end())
1301 {
1302 CycleSlipDetector::SatelliteObservation satObs = { .satId = satId,
1303 .signals = { signal },
1304 .freqNum = signalObs.freqNum() };
1305 cycleSlipObservations.at(recvType).push_back(satObs);
1306 }
1307 else
1308 {
1309 sat->signals.push_back(signal);
1310 }
1311 added = true;
1312 }
1313 }
1314 }
1315 if (added) { break; }
1316 }
1317 }
1318
1319 std::vector<NAV::Code> pivotsToChange;
1320 std::vector<States::AmbiguityDD> removedStates;
1321 auto removeSatSig = [&](const SatSigId& satSigId, [[maybe_unused]] const std::string& reason) -> bool {
1322 auto key = States::AmbiguityDD(satSigId);
1323 if (_kalmanFilter.x.hasRow(key))
1324 {
1325 LOG_TRACE("{}: [{}] Cycle-slip detected in [{}], due to {}. Removing state: {}", nameId(),
1326 _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), satSigId, reason, key);
1327 _ambiguitiesHold.erase(satSigId);
1328 _kalmanFilter.removeState(key);
1329 if (_outputStateEvents) { removedStates.push_back(key); }
1330 for (auto& detector : _cycleSlipDetector) { detector.resetSignal(key.satSigId); }
1331 }
1332 else if (_pivotSatellites.contains({ satSigId.code, GnssObs::Carrier }) && _pivotSatellites.at({ satSigId.code, GnssObs::Carrier }).satSigId == satSigId)
1333 {
1334 LOG_TRACE("{}: [{}] Cycle-slip detected in pivot satellite [{}], due to {}", nameId(),
1335 _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), satSigId, reason);
1336 pivotsToChange.push_back(satSigId.code);
1337 }
1338 else
1339 {
1340 LOG_DATA("{}: [{}] Cycle-slip detected in [{}], due to {}. Not yet estimated (no action taken).", nameId(),
1341 _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), satSigId, reason);
1342 return false;
1343 }
1344 return true;
1345 };
1346
1347 for (size_t i = 0; i < cycleSlipObservations.size(); ++i)
1348 {
1349 std::string receiverName = fmt::format("{}", static_cast<ReceiverType>(i));
1350 LOG_DATA("{}: [{}][{}] Checking for cycle-slips", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), receiverName);
1351
1352 auto cycleSlips = _cycleSlipDetector.at(i).checkForCycleSlip(_receiver.at(i).gnssObs->insTime, cycleSlipObservations.at(i), nameId());
1353 for (const auto& cycleSlip : cycleSlips)
1354 {
1355 if (const auto* s = std::get_if<CycleSlipDetector::CycleSlipLossOfLockIndicator>(&cycleSlip))
1356 {
1357 if (s->signal.code & _obsFilter.getCodeFilter()
1358 && removeSatSig(s->signal, "LLI set in " + receiverName))
1359 {
1360 rtkSol->cycleSlipDetectorResult.emplace_back(cycleSlip, receiverName);
1361 }
1362 }
1363 else if (const auto* s = std::get_if<CycleSlipDetector::CycleSlipSingleFrequency>(&cycleSlip))
1364 {
1365 if (s->signal.code & _obsFilter.getCodeFilter()
1366 && removeSatSig(s->signal, "single frequency check in " + receiverName))
1367 {
1368 rtkSol->cycleSlipDetectorResult.emplace_back(cycleSlip, receiverName);
1369 }
1370 }
1371 else if (const auto* s = std::get_if<CycleSlipDetector::CycleSlipDualFrequency>(&cycleSlip))
1372 {
1373 bool first = true;
1374 for (const auto& signal : s->signals)
1375 {
1376 if (signal.code & _obsFilter.getCodeFilter()
1377 && removeSatSig(signal, "dual frequency check in " + receiverName)
1378 && first)
1379 {
1380 rtkSol->cycleSlipDetectorResult.emplace_back(cycleSlip, receiverName);
1381 first = false;
1382 }
1383 }
1384 }
1385 }
1386 }
1387
1388 for (const auto& code : pivotsToChange)
1389 {
1390 LOG_TRACE("{}: [{}] Changing pivot because of cycle-slip", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), code);
1392 }
1393
1394 for (const auto& cycleSlip : rtkSol->cycleSlipDetectorResult)
1395 {
1396 switch (cycleSlip.first.index())
1397 {
1398 case 0: // CycleSlipLossOfLockIndicator
1400 break;
1401 case 1: // CycleSlipDualFrequency
1403 break;
1404 case 2: // CycleSlipSingleFrequency
1406 break;
1407 default:
1408 break;
1409 }
1410 auto text = fmt::format("{}: {}", cycleSlip.second, cycleSlip.first);
1411 LOG_DATA("{}: {}", nameId(), text);
1412 addEventToGui(rtkSol, text);
1413 }
1414 for (const auto& state : removedStates)
1415 {
1416 auto text = fmt::format("State [{}] removed, because cycle-slip detected.", state);
1417 LOG_DATA("{}: {}", nameId(), text);
1418 addEventToGui(rtkSol, text);
1419 }
1420}
1421
1422void RealTimeKinematic::removeSingleObservations(Observations& observations, ObservationFilter::Filtered* filtered, const std::shared_ptr<RtkSolution>& rtkSol)
1423{
1424 LOG_DATA("{}: Checking for observations to remove when only one observable per code for double difference", nameId());
1425
1426 std::unordered_map<std::pair<Code, GnssObs::ObservationType>, uint16_t> signalCount;
1427 for (const auto& [satSigId, sigObs] : observations.signals)
1428 {
1429 const auto& recvObs = sigObs.recvObs.at(Base);
1430 for (const auto& obs : recvObs->obs)
1431 {
1432 signalCount[std::make_pair(satSigId.code, obs.first)]++;
1433 }
1434 }
1435
1436 std::vector<SatSigId> sigToRemove;
1437 for (auto& [satSigId, sigObs] : observations.signals)
1438 {
1439 for (auto& recvObs : sigObs.recvObs)
1440 {
1441 std::vector<GnssObs::ObservationType> obsToRemove;
1442 for (const auto& obs : recvObs.second->obs)
1443 {
1444 if (signalCount[std::make_pair(satSigId.code, obs.first)] < 2)
1445 {
1446 obsToRemove.push_back(obs.first);
1447 }
1448 }
1449 for (const GnssObs::ObservationType& obs : obsToRemove)
1450 {
1451 LOG_DATA("{}: Removing [{}] {}", nameId(), satSigId, obs);
1452 recvObs.second->obs.erase(obs);
1453 if (filtered) { filtered->singleObservation.push_back(satSigId); }
1454 }
1455 if (recvObs.second->obs.empty())
1456 {
1457 sigToRemove.push_back(satSigId);
1458 break;
1459 }
1460 }
1461 }
1462
1463 for (const auto& satSigId : sigToRemove)
1464 {
1465 LOG_DATA("{}: Removing [{}], because no more observations left for this signal", nameId(), satSigId);
1466
1467 if (filtered) // Only update pivot if called with filtered (means we modifying the actual observation data)
1468 {
1469 for (const auto& obsType : _obsFilter.getUsedObservationTypes())
1470 {
1471 auto key = std::make_pair(satSigId.code, obsType);
1472 if (_pivotSatellites.contains(key) && signalCount[key] < 2)
1473 {
1474 LOG_DATA("{}: Pivot [{}][{}] does not have enough signals anymore", nameId(), satSigId.code, obsType);
1475 updatePivotSatellite(satSigId.code, obsType, observations, rtkSol, RtkSolution::PivotChange::Reason::PivotNotObservedInEpoch);
1476 }
1477 }
1478 }
1479
1480 observations.removeSignal(satSigId, nameId());
1481 }
1482}
1483
1484std::optional<RtkSolution::PivotChange> RealTimeKinematic::updatePivotSatellite(Code code, GnssObs::ObservationType obsType,
1485 Observations& observations, const std::shared_ptr<RtkSolution>& rtkSol,
1487{
1488 using Reason = RtkSolution::PivotChange::Reason;
1489 std::optional<RtkSolution::PivotChange> pivotChange;
1490 if (reason != Reason::None)
1491 {
1492 pivotChange = RtkSolution::PivotChange{};
1493 pivotChange->reason = reason;
1494 pivotChange->obsType = obsType;
1495
1496 if (_pivotSatellites.contains({ code, obsType }))
1497 {
1498 const auto& pivot = _pivotSatellites.at({ code, obsType });
1499 if (observations.signals.contains(pivot.satSigId))
1500 {
1501 auto oldPivotObs = observations.signals.at(pivot.satSigId).recvObs.begin();
1502 pivotChange->oldPivotSat = pivot.satSigId;
1503 pivotChange->oldPivotElevation = oldPivotObs->second->satElevation(_receiver[Rover].e_posMarker, _receiver[Rover].lla_posMarker);
1504 }
1505 _pivotSatellites.erase({ code, obsType });
1506 }
1507 }
1508
1509 auto sumRecvElevation = [&](double el, const auto& recvData) {
1510 return el + recvData.second->satElevation(_receiver[Rover].e_posMarker, _receiver[Rover].lla_posMarker);
1511 };
1512
1513 for (const auto& [satSigId, observation] : observations.signals)
1514 {
1515 if (satSigId.code != code) { continue; }
1516
1517 bool anySignalOnThisCodeWasEstimatedYet = std::ranges::any_of(observations.signals,
1518 [&](const auto& obs) { return obs.first.code == code
1519 && _kalmanFilter.x.hasRow(States::AmbiguityDD(obs.first)); });
1520 bool anySignalOnThisCodeWithFix = std::ranges::any_of(_ambiguitiesHold,
1521 [&code](const auto& amb) { return amb.first.code == code; });
1522
1523 LOG_DATA("{}: [{}] Checking [{}] for new pivot and obsType [{}]",
1524 nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), satSigId, obsType);
1525
1526 size_t nFound = 0;
1527 for (const auto& recvObs : observation.recvObs)
1528 {
1529 if (recvObs.second->obs.contains(obsType)) { nFound++; }
1530 }
1531 if (nFound != observation.recvObs.size()) { continue; }
1532
1533#if LOG_LEVEL <= LOG_LEVEL_DATA
1534 if (obsType == GnssObs::Carrier)
1535 {
1536 std::string ambHold;
1537 ambHold.reserve(_ambiguitiesHold.size() * 17);
1538 for (const auto& amb : _ambiguitiesHold)
1539 {
1540 ambHold += fmt::format("[{} = {}]", amb.first, amb.second);
1541 }
1542 LOG_DATA("{}: [{}] anySignalOnThisCodeWasEstimatedYet = {}, anySignalOnThisCodeWithFix = {}, ambHold = {}",
1543 nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), anySignalOnThisCodeWasEstimatedYet, anySignalOnThisCodeWithFix, ambHold);
1544 }
1545#endif
1546
1547 // Don't allow same satellite to be pivot again
1548 if (pivotChange->oldPivotSat == satSigId) { continue; }
1549
1550 if (_kalmanFilter.x.rows() != States::KFStates_COUNT // Ignore first epoch, as we need to select a pivot
1551 && obsType == GnssObs::Carrier // Ambiguity do not matter for pseudorange or doppler
1552 && (anySignalOnThisCodeWasEstimatedYet || _pivotSatellites.contains({ code, GnssObs::Carrier })) // We have at least one satellite with an estimated ambiguity on this code
1553 && !_kalmanFilter.x.hasRow(States::AmbiguityDD(satSigId)) // Only consider signals which were estimated once (float solution)
1554 && _ambiguityResolutionParameters.searchAlgorithm != AmbiguityResolutionParameters::SearchAlgorithm::None // If we estimating ambiguities
1555 && (anySignalOnThisCodeWithFix || _pivotSatellites.contains({ code, GnssObs::Carrier })) // We have at least one satellite with a fix on this code
1556 && !_ambiguitiesHold.contains(satSigId)) // Only consider signals, which have a fix
1557 {
1558 continue;
1559 }
1560
1561 double satElevation = std::accumulate(observation.recvObs.begin(), observation.recvObs.end(), 0.0, sumRecvElevation) // NOLINT(boost-use-ranges,modernize-use-ranges) // There is no ranges::accumulate
1562 / static_cast<double>(observation.recvObs.size());
1563
1564 if (!_pivotSatellites.contains({ code, obsType })) // No pivot for this code yet
1565 {
1566 _pivotSatellites.insert(std::make_pair(std::make_pair(code, obsType), satSigId));
1567 if (!pivotChange)
1568 {
1569 LOG_DATA("{}: [{}] Setting [{}] as new pivot for obsType [{}] (elevation {:.4}°, no previous pivot on this code)", nameId(),
1570 _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), satSigId, obsType, rad2deg(satElevation));
1571 pivotChange = RtkSolution::PivotChange{ .reason = Reason::NewCode,
1572 .obsType = obsType,
1573 .oldPivotSat = {},
1574 .oldPivotElevation = 0.0,
1575 .newPivotSat = satSigId,
1576 .newPivotElevation = satElevation };
1577 }
1578 else
1579 {
1580 LOG_DATA("{}: [{}] Setting [{}] as new pivot for obsType [{}] (elevation {:.4}°)", nameId(),
1581 _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), satSigId, obsType, rad2deg(satElevation));
1582 pivotChange->newPivotSat = satSigId;
1583 pivotChange->newPivotElevation = satElevation;
1584 }
1585 }
1586 else // Check if better pivot available
1587 {
1588 const auto& pivotSat = _pivotSatellites.at({ code, obsType });
1589
1590 if (!observations.signals.contains(pivotSat.satSigId)) // Old pivot satellite was not observed this epoch, so we have to choose another one
1591 {
1592 LOG_DATA("{}: [{}] Setting [{}] as new pivot for obsType [{}] (elevation {:.4}°, old pivot [{}] not observed this epoch)", nameId(),
1593 _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), satSigId, obsType, rad2deg(satElevation), _pivotSatellites.at({ code, obsType }).satSigId);
1594 pivotChange = RtkSolution::PivotChange{ .reason = Reason::PivotNotObservedInEpoch,
1595 .obsType = obsType,
1596 .oldPivotSat = _pivotSatellites.at({ code, obsType }).satSigId,
1597 .oldPivotElevation = 0.0,
1598 .newPivotSat = satSigId,
1599 .newPivotElevation = satElevation };
1600 _pivotSatellites.at({ code, obsType }) = PivotSatellite(satSigId);
1601 continue;
1602 }
1603
1604 const auto& pivotObs = observations.signals.at(pivotSat.satSigId);
1605 double pivElevation = std::accumulate(pivotObs.recvObs.begin(), pivotObs.recvObs.end(), 0.0, sumRecvElevation) // NOLINT(boost-use-ranges,modernize-use-ranges) // There is no ranges::accumulate
1606 / static_cast<double>(pivotObs.recvObs.size());
1607
1608 if (satElevation > pivElevation)
1609 {
1610 LOG_DATA("{}: [{}] Setting [{}] as new pivot for obsType [{}] (elevation {:.4}°, larger than previous pivot [{}] elevation {:.4}°)", nameId(),
1611 _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), satSigId, obsType, rad2deg(satElevation), _pivotSatellites.at({ code, obsType }).satSigId, rad2deg(pivElevation));
1612 if (!pivotChange)
1613 {
1614 pivotChange = RtkSolution::PivotChange{ .reason = Reason::HigherElevationFound,
1615 .obsType = obsType,
1616 .oldPivotSat = _pivotSatellites.at({ code, obsType }).satSigId,
1617 .oldPivotElevation = pivElevation,
1618 .newPivotSat = satSigId,
1619 .newPivotElevation = satElevation };
1620 }
1621 else
1622 {
1623 pivotChange->newPivotSat = satSigId;
1624 pivotChange->newPivotElevation = satElevation;
1625 }
1626 _pivotSatellites.at({ code, obsType }) = PivotSatellite(satSigId);
1627 }
1628 }
1629 }
1630
1631 // React on changed pivot satellite
1632 if (pivotChange)
1633 {
1634 INS_ASSERT_USER_ERROR(!_pivotSatellites.contains({ code, obsType })
1635 || _pivotSatellites.at({ code, obsType }).satSigId == pivotChange->newPivotSat,
1636 "The new pivot satellite and current one must match.");
1637 INS_ASSERT_USER_ERROR(pivotChange->oldPivotSat != pivotChange->newPivotSat
1638 || pivotChange->newPivotSat == SatSigId{},
1639 "Old and new pivot satellite have to be different.");
1640
1641 LOG_TRACE("{}: [{}] {}", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), *pivotChange, obsType);
1642
1643 rtkSol->changedPivotSatellites.emplace(std::make_pair(code, obsType), *pivotChange);
1644 _nPivotChange++;
1645 addEventToGui(rtkSol, fmt::format("{}", *pivotChange));
1646
1647 if (pivotChange->newPivotSat == SatSigId{}) // No new pivot selected
1648 {
1649 observations.removeMeasurementsFor(pivotChange->oldPivotSat.code, obsType, nameId());
1650 if (obsType == GnssObs::Carrier)
1651 {
1652 for (size_t i = States::KFStates_COUNT; i < _kalmanFilter.x.rowKeys().size(); i++) // 0-2 Pos, 3-5 Vel
1653 {
1654 const auto& key = _kalmanFilter.x.rowKeys().at(i);
1655 if (const auto* ambDD = std::get_if<States::AmbiguityDD>(&key))
1656 {
1657 if (ambDD->satSigId.code != pivotChange->oldPivotSat.code) { continue; }
1658
1659 LOG_DATA("{}: [{}] Removing Amibguity as no pivot anymore {}", nameId(), ambDD->satSigId, key);
1660 if (_outputStateEvents) { addEventToGui(rtkSol, fmt::format("State [{}] removed (not observed anymore)", key)); }
1661 _ambiguitiesHold.erase(ambDD->satSigId);
1662 _kalmanFilter.removeState(key); // After this 'key' is invalidated
1663 i--;
1664 }
1665 }
1666 }
1667 }
1668 else if (pivotChange->reason != Reason::NewCode && obsType == GnssObs::Carrier)
1669 {
1670 updateKalmanFilterAmbiguitiesForPivotChange(pivotChange->newPivotSat, pivotChange->oldPivotSat,
1671 pivotChange->reason != Reason::PivotNotObservedInEpoch
1672 && pivotChange->reason != Reason::PivotCycleSlip
1673 && pivotChange->reason != Reason::PivotOutlier,
1674 rtkSol);
1675 }
1676 }
1677 return pivotChange;
1678}
1679
1681{
1682 LOG_DATA("{}: Current pivot satellites:", nameId());
1683 for ([[maybe_unused]] const auto& [key, pivot] : _pivotSatellites)
1684 {
1685 LOG_DATA("{}: [{} - {}]: {}", nameId(), key.first, key.second, pivot.satSigId);
1686 if (key.second != GnssObs::Carrier) { continue; }
1687 if (auto state = States::AmbiguityDD{ pivot.satSigId };
1688 _kalmanFilter.x.hasRow(state))
1689 {
1690 LOG_ERROR("{}: [{}] The Kalman Filter has the state [{}] but this is a pivot. Pivots should not be in the state.", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), state);
1691 }
1692 }
1693}
1694
1695void RealTimeKinematic::updatePivotSatellites(Observations& observations, const std::shared_ptr<RtkSolution>& rtkSol)
1696{
1698
1699 std::set<Code> codes;
1700 for (const auto& [satSigId, observation] : observations.signals)
1701 {
1702 codes.insert(satSigId.code);
1703 }
1704 bool anyPivotChanged = false;
1705 for (const auto& code : codes)
1706 {
1707 for (const auto& obsType : _obsFilter.getUsedObservationTypes())
1708 {
1710
1711 if (_pivotSatellites.contains({ code, obsType }))
1712 {
1713 const auto& pivotSat = _pivotSatellites.at({ code, obsType });
1714 const auto& pivotSatSigId = pivotSat.satSigId;
1715 auto obs = std::ranges::find_if(observations.signals, [&](const auto& obs) {
1716 size_t nFound = 0;
1717 for (const auto& recvObs : obs.second.recvObs)
1718 {
1719 if (recvObs.second->obs.contains(obsType)) { nFound++; }
1720 }
1721 return obs.first == pivotSatSigId && nFound == obs.second.recvObs.size();
1722 });
1723 if (obs == observations.signals.end())
1724 {
1725 LOG_DATA("{}: [{}] Removing pivot satellite [{}] on observable [{}] because not observed this epoch.", nameId(),
1726 _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), pivotSat.satSigId, obsType);
1728 }
1729 }
1730
1731 anyPivotChanged |= updatePivotSatellite(code, obsType, observations, rtkSol, reason).has_value();
1732 }
1733 }
1734
1735 LOG_DATA("{}: {}", nameId(), anyPivotChanged ? "Pivot satellite changed" : "No pivot satellite change");
1736 if (anyPivotChanged)
1737 {
1738 removeSingleObservations(observations, &rtkSol->filtered, rtkSol);
1739 printPivotSatellites();
1740 }
1741}
1742
1743void RealTimeKinematic::updateKalmanFilterAmbiguitiesForPivotChange(const SatSigId& newPivotSatSigId, const SatSigId& oldPivotSatSigId,
1744 bool oldPivotObservedInEpoch, const std::shared_ptr<RtkSolution>& rtkSol)
1745{
1746 auto newPivotKey = States::AmbiguityDD{ newPivotSatSigId };
1747 if (_kalmanFilter.x.hasRow(newPivotKey))
1748 {
1749 std::vector<States::StateKeyType> ambiguitiesToChange;
1750 for (size_t i = States::KFStates_COUNT; i < _kalmanFilter.x.rowKeys().size(); i++)
1751 {
1752 const auto* ambDD = std::get_if<States::AmbiguityDD>(&_kalmanFilter.x.rowKeys().at(i));
1753 if (ambDD && newPivotSatSigId.code == ambDD->satSigId.code
1754 && (oldPivotObservedInEpoch || newPivotSatSigId != ambDD->satSigId))
1755 {
1756 ambiguitiesToChange.emplace_back(*ambDD);
1757 }
1758 }
1759 LOG_TRACE("{}: [{}] New pivot [{}] adapts ambiguities [{}]", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST),
1760 newPivotSatSigId, fmt::join(ambiguitiesToChange, ", "));
1761
1762 auto nStates = static_cast<int>(_kalmanFilter.x.rowKeys().size());
1763
1764 KeyedMatrixXd<States::StateKeyType> D(Eigen::MatrixXd::Identity(nStates, nStates),
1765 _kalmanFilter.x.rowKeys(), _kalmanFilter.x.rowKeys());
1766 D(ambiguitiesToChange, newPivotKey).setConstant(-1.0);
1767 if (!oldPivotObservedInEpoch) { D(newPivotKey, newPivotKey) = 0.0; }
1768 LOG_DATA("{}: D = \n{}", nameId(), D);
1769
1770 LOG_DATA("{}: x_old = \n{}", nameId(), _kalmanFilter.x.transposed());
1771 _kalmanFilter.x(all) = D(all, all) * _kalmanFilter.x(all);
1772 LOG_DATA("{}: D * x_old = \n{}", nameId(), _kalmanFilter.x.transposed());
1773
1774 LOG_DATA("{}: P_old = \n{}", nameId(), _kalmanFilter.P);
1775 // Error propagation
1776 _kalmanFilter.P(all, all) = D(all, all) * _kalmanFilter.P(all, all) * D(all, all).transpose();
1777
1778#if LOG_LEVEL <= LOG_LEVEL_DATA
1779 {
1780 std::string ambHold;
1781 ambHold.reserve(_ambiguitiesHold.size() * 17);
1782 for (const auto& amb : ambiguitiesToChange)
1783 {
1784 if (const auto* ambDD = std::get_if<States::AmbiguityDD>(&amb))
1785 {
1786 if (_ambiguitiesHold.contains(ambDD->satSigId))
1787 {
1788 ambHold += fmt::format("[{} = {}]", ambDD->satSigId, _ambiguitiesHold.at(ambDD->satSigId));
1789 }
1790 }
1791 }
1792 LOG_DATA("{}: ambHold (pre ) = {}", nameId(), ambHold);
1793 }
1794#endif
1795
1796 for (const auto& amb : ambiguitiesToChange)
1797 {
1798 if (const auto* ambDD = std::get_if<States::AmbiguityDD>(&amb))
1799 {
1800 if (_ambiguitiesHold.contains(ambDD->satSigId))
1801 {
1802 _ambiguitiesHold.at(ambDD->satSigId) = _kalmanFilter.x(amb);
1803 }
1804 }
1805 }
1806
1807 if (oldPivotObservedInEpoch)
1808 {
1809 auto oldPivotKey = States::AmbiguityDD{ oldPivotSatSigId };
1811 && _ambiguitiesHold.contains(newPivotSatSigId))
1812 {
1813 _ambiguitiesHold[oldPivotSatSigId] = _ambiguitiesHold.at(newPivotSatSigId);
1814 _ambiguitiesHold.erase(newPivotSatSigId);
1815 }
1816 _kalmanFilter.replaceState(newPivotKey, oldPivotKey);
1818 {
1819 addEventToGui(rtkSol, fmt::format("State [{}] replaced with [{}] (because [{}] is new pivot satellite)", newPivotKey, oldPivotKey, newPivotSatSigId));
1820 }
1821 LOG_TRACE("{}: [{}] State [{}] replaced with [{}] (because [{}] is new pivot satellite)", nameId(),
1822 _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), newPivotKey, oldPivotKey, newPivotSatSigId);
1823 }
1824 else
1825 {
1826 _ambiguitiesHold.erase(newPivotSatSigId);
1827 _kalmanFilter.removeState(newPivotKey);
1828 for (auto& detector : _cycleSlipDetector)
1829 {
1830 detector.resetSignal(newPivotSatSigId);
1831 detector.resetSignal(oldPivotSatSigId);
1832 }
1834 {
1835 addEventToGui(rtkSol, fmt::format("State [{}] removed (because new pivot satellite)", newPivotKey));
1836 }
1837 LOG_TRACE("{}: [{}] State [{}] removed (because new pivot satellite)", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), newPivotKey);
1838 }
1839 LOG_DATA("{}: x_new = \n{}", nameId(), _kalmanFilter.x.transposed());
1840 LOG_DATA("{}: P_new = \n{}", nameId(), _kalmanFilter.P);
1841
1842#if LOG_LEVEL <= LOG_LEVEL_DATA
1843 {
1844 std::erase_if(ambiguitiesToChange, [&newPivotSatSigId](const auto& amb) {
1845 if (const auto* ambDD = std::get_if<States::AmbiguityDD>(&amb))
1846 {
1847 return ambDD->satSigId == newPivotSatSigId;
1848 }
1849 return false;
1850 });
1851 if (oldPivotObservedInEpoch)
1852 {
1853 ambiguitiesToChange.emplace_back(States::AmbiguityDD{ oldPivotSatSigId });
1854 }
1855
1856 std::string ambHold;
1857 ambHold.reserve(_ambiguitiesHold.size() * 17);
1858 for (const auto& amb : ambiguitiesToChange)
1859 {
1860 if (const auto* ambDD = std::get_if<States::AmbiguityDD>(&amb))
1861 {
1862 if (_ambiguitiesHold.contains(ambDD->satSigId))
1863 {
1864 ambHold += fmt::format("[{} = {}]", ambDD->satSigId, _ambiguitiesHold.at(ambDD->satSigId));
1865 }
1866 }
1867 }
1868 LOG_DATA("{}: ambHold (post) = {}", nameId(), ambHold);
1869 }
1870#endif
1871 }
1872}
1873
1874void RealTimeKinematic::addOrRemoveKalmanFilterAmbiguities(const Observations& observations, const std::shared_ptr<RtkSolution>& rtkSol)
1875{
1876 bool anyStateChanged = false;
1877
1878 auto addEvent = [&](const std::string& text) {
1879 if (_outputStateEvents) { addEventToGui(rtkSol, text); }
1880 };
1881
1882 std::vector<States::StateKeyType> newAddedAmbiguities;
1883
1884 for (const auto& [satSigId, observation] : observations.signals)
1885 {
1886 if (!observation.recvObs.at(Rover)->obs.contains(GnssObs::Carrier)
1887 || !observation.recvObs.at(Base)->obs.contains(GnssObs::Carrier)) { continue; }
1888
1889 auto key = States::AmbiguityDD{ satSigId };
1890 if (!_kalmanFilter.x.hasRow(key) // Ambiguity not in state yet
1891 && _pivotSatellites.contains({ satSigId.code, GnssObs::Carrier }) // There should be a pivot satellite for this observation
1892 && _pivotSatellites.at({ satSigId.code, GnssObs::Carrier }).satSigId != satSigId) // Don't add a ambiguity for pivot satellite
1893 {
1894 anyStateChanged = true;
1895 LOG_TRACE("{}: [{}] Adding state: {}", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), key);
1896 _kalmanFilter.addState(key);
1897 newAddedAmbiguities.emplace_back(key);
1898 if (std::ranges::find(rtkSol->newEstimatedAmbiguity, satSigId)
1899 == rtkSol->newEstimatedAmbiguity.end())
1900 {
1901 rtkSol->newEstimatedAmbiguity.push_back(satSigId);
1902 }
1903
1904 // F: Entries are all 0
1905 // Ambiguities are modeled as RW with very small noise to keep numerical stability
1906 _kalmanFilter.G(key, key) = 1;
1907 _kalmanFilter.Phi(key, key) = 1; // This is set, because we only calculate the matrix for PosVel
1908
1910
1911 const auto& pivotSat = _pivotSatellites.at({ satSigId.code, GnssObs::Carrier });
1912 LOG_DATA("{}: {} - pivot {}", nameId(), satSigId, pivotSat.satSigId);
1913 if (!observations.signals.contains(pivotSat.satSigId))
1914 {
1915 LOG_CRITICAL("{}: [{}] The pivot satellite [{}] does not have a carrier measurement. It should have been switched before.",
1916 nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), pivotSat.satSigId);
1917 }
1918 const auto& pivotObs = observations.signals.at(pivotSat.satSigId);
1919
1920 // Initialize with difference of 1/lambda (carrier-phase_DD - pseudorange_DD) measurement.
1921 double lambda_j = InsConst::C / satSigId.freq().getFrequency(observation.freqNum());
1922
1923 // Carrier is always there, otherwise we would not initialize the ambiguity
1924 const auto& phi_r_1 = pivotObs.recvObs.at(Rover)->obs.at(GnssObs::Carrier);
1925 const auto& phi_b_1 = pivotObs.recvObs.at(Base)->obs.at(GnssObs::Carrier);
1926 const auto& phi_r_s = observation.recvObs.at(Rover)->obs.at(GnssObs::Carrier);
1927 const auto& phi_b_s = observation.recvObs.at(Base)->obs.at(GnssObs::Carrier);
1928
1929 double phi_br_s_meas = phi_r_s.measurement - phi_b_s.measurement;
1930 double phi_br_1_meas = phi_r_1.measurement - phi_b_1.measurement;
1931 double phi_br_1s_meas = phi_br_s_meas - phi_br_1_meas;
1932
1933 double varCarrierPseudorange = std::numeric_limits<double>::infinity();
1934 bool forceCarrierPseudorange = false;
1935
1936 // Pseudorange could be missing
1937 if (observation.recvObs.at(Rover)->obs.contains(GnssObs::Pseudorange)
1938 && observation.recvObs.at(Base)->obs.contains(GnssObs::Pseudorange)
1939 && _pivotSatellites.contains({ satSigId.code, GnssObs::Pseudorange }))
1940 {
1941 const auto& pivotSatPsr = _pivotSatellites.at({ satSigId.code, GnssObs::Pseudorange });
1942 if (pivotSatPsr.satSigId == satSigId) // We cannot initialize the ambiguity with different pivot satellites
1943 {
1944 const auto& pivotObsPsr = observations.signals.at(pivotSatPsr.satSigId);
1945 const auto& p_r_1 = pivotObsPsr.recvObs.at(Rover)->obs.at(GnssObs::Pseudorange);
1946 const auto& p_b_1 = pivotObsPsr.recvObs.at(Base)->obs.at(GnssObs::Pseudorange);
1947 const auto& p_r_s = observation.recvObs.at(Rover)->obs.at(GnssObs::Pseudorange);
1948 const auto& p_b_s = observation.recvObs.at(Base)->obs.at(GnssObs::Pseudorange);
1949
1950 varCarrierPseudorange = phi_r_s.measVar + p_r_s.measVar + phi_b_s.measVar + p_b_s.measVar
1951 + phi_r_1.measVar + p_r_1.measVar + phi_b_1.measVar + p_b_1.measVar;
1952
1953 double posUncertanty = _kalmanFilter.P.block<3>(States::Pos, States::Pos).diagonal().cwiseSqrt().norm();
1954 forceCarrierPseudorange = posUncertanty > 0.06; // Always use the Carrier-Pseudorange difference when position is not good (stdDev 5cm)
1955 }
1956 }
1957
1958 Eigen::Vector3d e_pLOS_1s = pivotObs.recvObs.at(Rover)->e_pLOS(_receiver[Rover].e_posMarker)
1959 - observation.recvObs.at(Rover)->e_pLOS(_receiver[Rover].e_posMarker);
1960 const double varCarrierGeometry = phi_r_s.measVar + phi_b_s.measVar + phi_r_1.measVar + phi_b_1.measVar
1961 + e_pLOS_1s.transpose() * _kalmanFilter.P.block<3>(States::Pos, States::Pos) * e_pLOS_1s;
1962 LOG_DATA("{}: varCarrierGeometry = {:.2g} [m], varCarrierPseudorange = {:.2g} [m]", nameId(), varCarrierGeometry, varCarrierPseudorange);
1963
1964 if (varCarrierPseudorange < varCarrierGeometry || forceCarrierPseudorange)
1965 {
1966 const auto& p_r_1 = pivotObs.recvObs.at(Rover)->obs.at(GnssObs::Pseudorange);
1967 const auto& p_b_1 = pivotObs.recvObs.at(Base)->obs.at(GnssObs::Pseudorange);
1968 const auto& p_r_s = observation.recvObs.at(Rover)->obs.at(GnssObs::Pseudorange);
1969 const auto& p_b_s = observation.recvObs.at(Base)->obs.at(GnssObs::Pseudorange);
1970
1971 double p_br_s_meas = p_r_s.measurement - p_b_s.measurement;
1972 double p_br_1_meas = p_r_1.measurement - p_b_1.measurement;
1973 double p_br_1s_meas = p_br_s_meas - p_br_1_meas;
1974
1975 _kalmanFilter.x(key) = (phi_br_1s_meas - p_br_1s_meas) / lambda_j;
1976
1977 _kalmanFilter.P(key, key) = varCarrierPseudorange * 3.0 / std::pow(lambda_j, 2); // Safety factor of 3 and division by lambda^2, because state in [cycles]
1978 auto msg = fmt::format("ddCP({:.1f}m) - ddPR({:.1f}m) = {:.1f} [cycles], std = {:.2f} [cycles]",
1979 phi_br_1s_meas, p_br_1s_meas, _kalmanFilter.x(key), std::sqrt(_kalmanFilter.P(key, key)));
1980 LOG_DATA("{}: [{}] Init Amibguity: {}", nameId(), satSigId, msg);
1981
1982 addEvent(fmt::format("State [{}] added ({})", key, msg));
1983 }
1984 else // Carrier-Geometry Difference
1985 {
1986 double phi_br_s_est = phi_r_s.estimate - phi_b_s.estimate;
1987 double phi_br_1_est = phi_r_1.estimate - phi_b_1.estimate;
1988 double phi_br_1s_est = phi_br_s_est - phi_br_1_est;
1989
1990 _kalmanFilter.x(key) = (phi_br_1s_meas - phi_br_1s_est) / lambda_j;
1991
1992 _kalmanFilter.P(key, key) = varCarrierGeometry * 3.0 / std::pow(lambda_j, 2); // Safety factor of 3 and division by lambda^2, because state in [cycles]
1993 auto msg = fmt::format("ddCP({:.1f}m) - ddEst({:.1f}m) = {:.1f} [cycles], std = {:.2f} [cycles]",
1994 phi_br_1s_meas, phi_br_1s_est, _kalmanFilter.x(key), std::sqrt(_kalmanFilter.P(key, key)));
1995 LOG_DATA("{}: [{}] Init Amibguity: {}", nameId(), satSigId, msg);
1996
1997 addEvent(fmt::format("State [{}] added ({})", key, msg));
1998 }
1999 }
2000 }
2001
2002 for (size_t i = States::KFStates_COUNT; i < _kalmanFilter.x.rowKeys().size(); i++) // 0-2 Pos, 3-5 Vel
2003 {
2004 const auto& key = _kalmanFilter.x.rowKeys().at(i);
2005 if (const auto* ambDD = std::get_if<States::AmbiguityDD>(&key))
2006 {
2007 if (observations.countObservations(ambDD->satSigId, GnssObs::Carrier) == ReceiverType_COUNT) { continue; }
2008
2009 LOG_DATA("{}: [{}] Removing Ambiguity as not observed anymore {}", nameId(), ambDD->satSigId, key);
2010 _ambiguitiesHold.erase(ambDD->satSigId);
2011 addEvent(fmt::format("State [{}] removed (not observed anymore)", key));
2012 _kalmanFilter.removeState(key); // After this 'key' is invalidated
2013 i--;
2014 }
2015 }
2016
2017 if (anyStateChanged)
2018 {
2019 LOG_DATA("{}: x =\n{}", nameId(), _kalmanFilter.x.transposed());
2020 LOG_DATA("{}: P =\n{}", nameId(), _kalmanFilter.P);
2021 LOG_DATA("{}: G =\n{}", nameId(), _kalmanFilter.G);
2022 LOG_DATA("{}: W =\n{}", nameId(), _kalmanFilter.W);
2023 }
2024}
2025
2026RealTimeKinematic::Differences RealTimeKinematic::calcSingleDifferences(const Observations& observations) const // NOLINT(readability-convert-member-functions-to-static)
2027{
2028 Differences singleDifferences;
2029 singleDifferences.reserve(observations.signals.size());
2030
2031 LOG_DATA("{}: Calculating single differences (rover - base):", nameId());
2032 for (const auto& [satSigId, observation] : observations.signals)
2033 {
2034 const auto& baseObservations = observation.recvObs.at(Base)->obs;
2035 const auto& roverObservations = observation.recvObs.at(Rover)->obs;
2036
2037 for (const auto& [obsType, baseObs] : baseObservations)
2038 {
2039 const auto& roverObs = roverObservations.at(obsType);
2040
2041 singleDifferences[satSigId][obsType].estimate = roverObs.estimate - baseObs.estimate;
2042 singleDifferences[satSigId].at(obsType).measurement = roverObs.measurement - baseObs.measurement;
2043
2044 LOG_DATA("{}: [{}][{:11}][Meas] r {:.3f} - b {:.3f} = {:.3f}", nameId(), satSigId, obsType,
2045 roverObs.measurement, baseObs.measurement, singleDifferences[satSigId][obsType].measurement);
2046 LOG_DATA("{}: [{}][{:11}][Est ] r {:.3f} - b {:.3f} = {:.3f} (diff to meas {:.3e})", nameId(), satSigId, obsType,
2047 roverObs.estimate, baseObs.estimate, singleDifferences[satSigId][obsType].estimate,
2048 singleDifferences[satSigId][obsType].measurement - singleDifferences[satSigId][obsType].estimate);
2049 }
2050 }
2051
2052 return singleDifferences;
2053}
2054
2056{
2057 Differences doubleDifferences;
2058 if (singleDifferences.empty()) { return doubleDifferences; }
2059 doubleDifferences.reserve(singleDifferences.size() - 1);
2060
2061 LOG_DATA("{}: [{}] Calculating double differences (sat - pivot):", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST));
2062 for (const auto& [satSigId_s, singleDiff_s] : singleDifferences)
2063 {
2064 for (const auto& [obsType, sDiff_s] : singleDiff_s)
2065 {
2066 if (!_pivotSatellites.contains({ satSigId_s.code, obsType }))
2067 {
2068 LOG_DATA("{}: [{}] Not calculating double difference for [{} {}] because no pivot satellite.", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), satSigId_s.code, obsType);
2069 continue;
2070 }
2071 const auto& satSigId_1 = _pivotSatellites.at({ satSigId_s.code, obsType }).satSigId;
2072 if (satSigId_s == satSigId_1) { continue; } // No double difference with itself
2073
2074 if (!singleDifferences.contains(satSigId_1))
2075 {
2076 LOG_WARN("{}: [{}] Pivot [{}] has no single difference. This is a bug.", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), satSigId_1);
2077 continue;
2078 }
2079 if (!singleDifferences.at(satSigId_1).contains(obsType))
2080 {
2081 LOG_WARN("{}: [{}] Pivot [{}] has no observation type [{}]. This is a bug.", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), satSigId_1, obsType);
2082 continue;
2083 }
2084
2085 const auto& singleDiff_1 = singleDifferences.at(satSigId_1);
2086
2087 const auto& sDiff_1 = singleDiff_1.at(obsType);
2088
2089 doubleDifferences[satSigId_s][obsType].estimate = sDiff_s.estimate - sDiff_1.estimate;
2090 doubleDifferences[satSigId_s].at(obsType).measurement = sDiff_s.measurement - sDiff_1.measurement;
2091 LOG_DATA("{}: [{} - {}][{:11}][Meas] sat {:.3f} - piv {:.3f} = {:.3f}", nameId(), satSigId_s, satSigId_1, obsType,
2092 sDiff_s.measurement, sDiff_1.measurement, doubleDifferences[satSigId_s][obsType].measurement);
2093
2094 if (obsType == GnssObs::Carrier) // Pivot satellite ambiguity is 0 and not in state
2095 {
2096 double lambda_j_1 = InsConst::C / satSigId_1.freq().getFrequency(observations.signals.at(satSigId_1).freqNum());
2097 double N_br_1s = _kalmanFilter.x(States::AmbiguityDD{ satSigId_s });
2098 doubleDifferences[satSigId_s].at(obsType).estimate += lambda_j_1 * N_br_1s;
2099 }
2100 LOG_DATA("{}: [{} - {}][{:11}][Est ] sat {:.3f} - piv {:.3f} = {:.3f} (diff to meas {:.3e})", nameId(), satSigId_s, satSigId_1, obsType,
2101 sDiff_s.estimate, sDiff_1.estimate, doubleDifferences[satSigId_s][obsType].estimate,
2102 doubleDifferences[satSigId_s][obsType].measurement - doubleDifferences[satSigId_s][obsType].estimate);
2103 }
2104 }
2105
2106 return doubleDifferences;
2107}
2108
2110 RealTimeKinematic::calcSingleObsMeasurementNoiseMatrices(const Observations& observations) const // NOLINT(readability-convert-member-functions-to-static)
2111{
2113 for (const auto& [satSigId, observation] : observations.signals)
2114 {
2115 for (const auto& [obsType, baseObs] : observation.recvObs.at(Base)->obs)
2116 {
2117 measKeys[obsType].emplace_back(satSigId, Rover, obsType);
2118 measKeys[obsType].emplace_back(satSigId, Base, obsType);
2119 }
2120 }
2121
2123 for (const auto& [obsType, keys] : measKeys)
2124 {
2125 Rtilde[obsType] = KeyedMatrixXd<RTK::Meas::SingleObs<ReceiverType>>(Eigen::MatrixXd::Zero(static_cast<int>(keys.size()), static_cast<int>(keys.size())), keys);
2126 }
2127
2128 for (const auto& [satSigId, observation] : observations.signals)
2129 {
2130 const auto& baseObservations = observation.recvObs.at(Base)->obs;
2131 const auto& roverObservations = observation.recvObs.at(Rover)->obs;
2132
2133 for (const auto& [obsType, baseObs] : baseObservations)
2134 {
2135 const auto& roverObs = roverObservations.at(obsType);
2136
2137 RTK::Meas::SingleObs<ReceiverType> roverKey(satSigId, Rover, obsType);
2138 RTK::Meas::SingleObs<ReceiverType> baseKey(satSigId, Base, obsType);
2139
2140 Rtilde.at(obsType)(roverKey, roverKey) = roverObs.measVar;
2141 Rtilde.at(obsType)(baseKey, baseKey) = baseObs.measVar;
2142 }
2143 }
2144
2145 // for ([[maybe_unused]] const auto& [obsType, R] : Rtilde)
2146 // {
2147 // LOG_DATA("{}: R_tilde({}) =\n{}", nameId(), obsType, R);
2148 // }
2149
2150 return Rtilde;
2151}
2152
2153void RealTimeKinematic::calcKalmanUpdateMatrices(const Observations& observations, const Differences& doubleDifferences,
2155{
2156 // Update the Measurement sensitivity Matrix (𝐇), the Measurement noise covariance matrix (𝐑) and the Measurement vector (𝐳)
2157
2158 std::vector<Meas::MeasKeyTypes> measKeys;
2159 measKeys.reserve(doubleDifferences.size() * _obsFilter.getUsedObservationTypes().size());
2160 for (size_t i = 0; i < GnssObs::ObservationType_COUNT; i++)
2161 {
2163
2164 for (const auto& [satSigId, doubleDiff] : doubleDifferences)
2165 {
2166 for (const auto& [obsType, diff] : doubleDiff)
2167 {
2168 if (obsType == oType)
2169 {
2170 switch (obsType)
2171 {
2173 measKeys.emplace_back(Meas::PsrDD{ satSigId });
2174 break;
2175 case GnssObs::Carrier:
2176 measKeys.emplace_back(Meas::CarrierDD{ satSigId });
2177 break;
2178 case GnssObs::Doppler:
2179 measKeys.emplace_back(Meas::DopplerDD{ satSigId });
2180 break;
2182 LOG_CRITICAL("{}: ObservationType_COUNT is not a valid value", nameId());
2183 break;
2184 }
2185 }
2186 }
2187 }
2188 }
2189
2190 LOG_DATA("{}: Setting measurement keys: {}", nameId(), joinToString(measKeys));
2191 _kalmanFilter.setMeasurements(measKeys);
2192
2193 // R matrix
2194 for (const auto& [obsType, R] : Rtilde)
2195 {
2196 std::vector<Meas::MeasKeyTypes> obsTypeMeasKeys;
2197 for (const auto& key : measKeys)
2198 {
2199 if (std::holds_alternative<Meas::PsrDD>(key) && obsType == GnssObs::Pseudorange) { obsTypeMeasKeys.push_back(key); }
2200 else if (std::holds_alternative<Meas::CarrierDD>(key) && obsType == GnssObs::Carrier) { obsTypeMeasKeys.push_back(key); }
2201 else if (std::holds_alternative<Meas::DopplerDD>(key) && obsType == GnssObs::Doppler) { obsTypeMeasKeys.push_back(key); }
2202 }
2203 KeyedMatrixXd<RTK::Meas::MeasKeyTypes, RTK::Meas::SingleObs<ReceiverType>> J(Eigen::MatrixXd::Zero(static_cast<int>(obsTypeMeasKeys.size()), R.rows()),
2204 obsTypeMeasKeys, R.rowKeys());
2205
2206 for (const auto& rowKey : obsTypeMeasKeys)
2207 {
2208 SatSigId satSigId_s;
2209 switch (obsType)
2210 {
2212 satSigId_s = std::get<Meas::PsrDD>(rowKey).satSigId;
2213 break;
2214 case GnssObs::Carrier:
2215 satSigId_s = std::get<Meas::CarrierDD>(rowKey).satSigId;
2216 break;
2217 case GnssObs::Doppler:
2218 satSigId_s = std::get<Meas::DopplerDD>(rowKey).satSigId;
2219 break;
2221 LOG_CRITICAL("{}: ObservationType_COUNT is not a valid value", nameId());
2222 break;
2223 }
2224
2225 const auto& satSigId_1 = _pivotSatellites.at({ satSigId_s.code, obsType }).satSigId;
2226
2227 J(rowKey, RTK::Meas::SingleObs<ReceiverType>(satSigId_s, Rover, obsType)) = 1;
2228 J(rowKey, RTK::Meas::SingleObs<ReceiverType>(satSigId_s, Base, obsType)) = -1;
2229 J(rowKey, RTK::Meas::SingleObs<ReceiverType>(satSigId_1, Rover, obsType)) = -1;
2230 J(rowKey, RTK::Meas::SingleObs<ReceiverType>(satSigId_1, Base, obsType)) = 1;
2231 }
2232 // LOG_DATA("{}: J({}) =\n{}", nameId(), obsType, J);
2233
2234 _kalmanFilter.R(obsTypeMeasKeys, obsTypeMeasKeys) = J(all, all) * R(all, all) * J(all, all).transpose();
2235 }
2236 LOG_DATA("{}: R =\n{}", nameId(), _kalmanFilter.R);
2237
2238 for (const auto& [satSigId_s, doubleDiff] : doubleDifferences)
2239 {
2240 const auto& obs_s = observations.signals.at(satSigId_s);
2241
2242 double lambda_j = InsConst::C / satSigId_s.freq().getFrequency(obs_s.freqNum());
2243
2244 const auto& e_pLOS_s = obs_s.recvObs.at(Rover)->e_pLOS(_receiver[Rover].e_posMarker);
2245
2246 for (const auto& [obsType, obs] : doubleDiff)
2247 {
2248 const auto& satSigId_1 = _pivotSatellites.at({ satSigId_s.code, obsType }).satSigId;
2249 const auto& obs_1 = observations.signals.at(satSigId_1);
2250 const auto& e_pLOS_1 = obs_1.recvObs.at(Rover)->e_pLOS(_receiver[Rover].e_posMarker);
2251
2252 switch (obsType)
2253 {
2255 _kalmanFilter.z(Meas::PsrDD{ satSigId_s }) = obs.measurement - obs.estimate;
2256 _kalmanFilter.H.block<3>(Meas::PsrDD{ satSigId_s }, States::Pos) = (e_pLOS_1 - e_pLOS_s).transpose();
2257 break;
2258 case GnssObs::Carrier:
2259 _kalmanFilter.z(Meas::CarrierDD{ satSigId_s }) = obs.measurement - obs.estimate;
2260 _kalmanFilter.H.block<3>(Meas::CarrierDD{ satSigId_s }, States::Pos) = (e_pLOS_1 - e_pLOS_s).transpose();
2261 _kalmanFilter.H(Meas::CarrierDD{ satSigId_s }, States::AmbiguityDD{ satSigId_s }) = lambda_j;
2262 break;
2263 case GnssObs::Doppler:
2264 {
2265 _kalmanFilter.z(Meas::DopplerDD{ satSigId_s }) = obs.measurement - obs.estimate;
2266 const auto& e_vLOS_1 = obs_1.recvObs.at(Rover)->e_vLOS(_receiver[Rover].e_posMarker, _receiver[Rover].e_vel);
2267 const auto& e_vLOS_s = obs_s.recvObs.at(Rover)->e_vLOS(_receiver[Rover].e_posMarker, _receiver[Rover].e_vel);
2268
2269 _kalmanFilter.H.block<3>(Meas::DopplerDD{ satSigId_s }, States::Pos) = Eigen::RowVector3d(
2270 -e_vLOS_1.x() * e_pLOS_1.x() * e_pLOS_1.x() + e_vLOS_1.x() + e_vLOS_s.x() * e_pLOS_s.x() * e_pLOS_s.x() - e_vLOS_s.x() - e_vLOS_1.y() * e_pLOS_1.x() * e_pLOS_1.y() + e_vLOS_s.y() * e_pLOS_s.x() * e_pLOS_s.y() - e_vLOS_1.z() * e_pLOS_1.x() * e_pLOS_1.z() + e_vLOS_s.z() * e_pLOS_s.x() * e_pLOS_s.z(),
2271 -e_vLOS_1.x() * e_pLOS_1.x() * e_pLOS_1.y() + e_vLOS_s.x() * e_pLOS_s.x() * e_pLOS_s.y() - e_vLOS_1.y() * e_pLOS_1.y() * e_pLOS_1.y() + e_vLOS_1.y() + e_vLOS_s.y() * e_pLOS_s.y() * e_pLOS_s.y() - e_vLOS_s.y() - e_vLOS_1.z() * e_pLOS_1.y() * e_pLOS_1.z() + e_vLOS_s.z() * e_pLOS_s.y() * e_pLOS_s.z(),
2272 -e_vLOS_1.x() * e_pLOS_1.x() * e_pLOS_1.z() + e_vLOS_s.x() * e_pLOS_s.x() * e_pLOS_s.z() - e_vLOS_1.y() * e_pLOS_1.y() * e_pLOS_1.z() + e_vLOS_s.y() * e_pLOS_s.y() * e_pLOS_s.z() - e_vLOS_1.z() * e_pLOS_1.z() * e_pLOS_1.z() + e_vLOS_1.z() + e_vLOS_s.z() * e_pLOS_s.z() * e_pLOS_s.z() - e_vLOS_s.z());
2273 _kalmanFilter.H.block<3>(Meas::DopplerDD{ satSigId_s }, States::Vel) = (e_pLOS_1 - e_pLOS_s).transpose();
2274 break;
2275 }
2277 LOG_CRITICAL("{}: ObservationType_COUNT is not a valid value", nameId());
2278 break;
2279 }
2280 }
2281 }
2282
2283 LOG_DATA("{}: z =\n{}", nameId(), _kalmanFilter.z.transposed());
2284 LOG_DATA("{}: H =\n{}", nameId(), _kalmanFilter.H);
2285}
2286
2287std::vector<RealTimeKinematic::OutlierInfo> RealTimeKinematic::removeOutlier(Observations& observations, const std::shared_ptr<RtkSolution>& rtkSol)
2288{
2290 / _kalmanFilter.S(all, all).diagonal().array().abs().sqrt())
2291 .matrix(),
2292 _kalmanFilter.z.rowKeys());
2293 if (normInno(all).hasNaN())
2294 {
2295 LOG_ERROR("{}: NIS check has NaN values. Skipping check this epoch.", nameId());
2296 LOG_DATA("{}: normInno has NaN", nameId());
2297 LOG_DATA("{}: S =\n{}", nameId(), _kalmanFilter.S);
2298 return {};
2299 }
2300
2301 LOG_DATA("{}: normInno = \n{}", nameId(), normInno.transposed());
2302 LOG_DATA("{}: dz (post-fit) = \n{}", nameId(), _kalmanFilter.z.transposed());
2303 LOG_DATA("{}: dz (innovation) = \n{}", nameId(), _kalmanFilter.savedPreUpdate().z.transposed());
2304
2305 // Check pivot satellites for outlier by calculating mean / stdev
2306 OutlierInfo faultyMeas;
2307
2308 for (auto& [pivotCodeObsType, pivot] : _pivotSatellites)
2309 {
2310 auto code = pivotCodeObsType.first;
2311 auto obsType = pivotCodeObsType.second;
2312 auto filterMeasurements = [&](const Meas::MeasKeyTypes& key) -> bool {
2313 SatSigId satSigId;
2315 if (const auto* meas = std::get_if<Meas::PsrDD>(&key))
2316 {
2317 obsType2 = GnssObs::Pseudorange;
2318 satSigId = meas->satSigId;
2319 }
2320 else if (const auto* meas = std::get_if<Meas::CarrierDD>(&key))
2321 {
2322 obsType2 = GnssObs::Carrier;
2323 satSigId = meas->satSigId;
2324 }
2325 else if (const auto* meas = std::get_if<Meas::DopplerDD>(&key))
2326 {
2327 obsType2 = GnssObs::Doppler;
2328 satSigId = meas->satSigId;
2329 }
2330 return satSigId.code != code || obsType2 != obsType;
2331 };
2332 double mean = 0.0;
2333 size_t amount = 0;
2334 for (const auto& key : _kalmanFilter.savedPreUpdate().z.rowKeys())
2335 {
2336 if (filterMeasurements(key)) { continue; }
2337 mean += _kalmanFilter.savedPreUpdate().z(key);
2338 amount++;
2339 }
2340 if (amount <= 1) { continue; }
2341 mean /= static_cast<double>(amount);
2342
2343 double variance = 0.0;
2344 for (const auto& key : _kalmanFilter.savedPreUpdate().z.rowKeys())
2345 {
2346 if (filterMeasurements(key)) { continue; }
2347 variance += std::pow(std::abs(_kalmanFilter.savedPreUpdate().z(key) - mean), 2);
2348 }
2349 variance *= 1.0 / (static_cast<double>(amount) - 1.0);
2350
2351 LOG_DATA("{}: pivot [{}][{}]: {:.3f} / {:.3f} = {:.3f}", nameId(), code, obsType, mean, std::sqrt(variance), mean / std::sqrt(variance));
2352
2353 if (std::abs(mean / std::sqrt(variance)) > 5.0) // 3 or 5 sigma (but t-distributed https://en.wikipedia.org/wiki/Student%27s_t-distribution)
2354 {
2355 switch (obsType)
2356 {
2358 faultyMeas.key = Meas::PsrDD{ pivot.satSigId };
2359 break;
2360 case GnssObs::Carrier:
2361 faultyMeas.key = Meas::CarrierDD{ pivot.satSigId };
2362 break;
2363 case GnssObs::Doppler:
2364 faultyMeas.key = Meas::DopplerDD{ pivot.satSigId };
2365 break;
2367 break;
2368 }
2369 faultyMeas.obsType = obsType;
2370 faultyMeas.satSigId = pivot.satSigId;
2372
2373 break; // Pivot is problematic throw out and then break. Only one pivot change at a time
2374 }
2375 }
2376 if (faultyMeas.obsType == GnssObs::ObservationType_COUNT)
2377 {
2378 int maxIdx = 0;
2379 normInno(all).maxCoeff(&maxIdx);
2380 faultyMeas.key = normInno.rowKeys().at(static_cast<size_t>(maxIdx));
2381 LOG_DATA("{}: Largest post-fit innovation: {} ({:.1f})", nameId(), faultyMeas.key, normInno(faultyMeas.key));
2382
2383 if (const auto* meas = std::get_if<Meas::PsrDD>(&faultyMeas.key))
2384 {
2385 faultyMeas.obsType = GnssObs::Pseudorange;
2386 faultyMeas.satSigId = meas->satSigId;
2387 }
2388 else if (const auto* meas = std::get_if<Meas::CarrierDD>(&faultyMeas.key))
2389 {
2390 faultyMeas.obsType = GnssObs::Carrier;
2391 faultyMeas.satSigId = meas->satSigId;
2392 }
2393 else if (const auto* meas = std::get_if<Meas::DopplerDD>(&faultyMeas.key))
2394 {
2395 faultyMeas.obsType = GnssObs::Doppler;
2396 faultyMeas.satSigId = meas->satSigId;
2397 }
2398
2399 auto& pivot = _pivotSatellites.at({ faultyMeas.satSigId.code, faultyMeas.obsType });
2400 LOG_DATA("{}: Pivot for [{}] is [{}]", nameId(), faultyMeas.satSigId, pivot.satSigId);
2401 if (pivot.satSigId == faultyMeas.satSigId)
2402 {
2404 LOG_DATA("{}: Pivot [{}][{}] with outlier.", nameId(), pivot.satSigId, faultyMeas.obsType);
2405 }
2406 }
2407
2408 LOG_DATA("{}: faultyMeas = {}", nameId(), faultyMeas.key);
2409
2411 {
2412 addEventToGui(rtkSol, "Stopped doing NIS check, as minimum amount of pseudorange observables reached.");
2413 return {};
2414 }
2415
2416 std::vector<RealTimeKinematic::OutlierInfo> faulty{ faultyMeas };
2417 // Exclude Pseudorange and Carrier toghether
2418 if (faultyMeas.obsType == GnssObs::Pseudorange && _kalmanFilter.z.hasRow(Meas::CarrierDD{ faultyMeas.satSigId }))
2419 {
2420 LOG_DATA("{}: Also flagging [{}][{}] as outlier.", nameId(), faultyMeas.satSigId, GnssObs::Carrier);
2421 faulty.push_back(faultyMeas);
2422 faulty.back().obsType = GnssObs::Carrier;
2423 faulty.back().key = Meas::CarrierDD{ faulty.back().satSigId };
2424 }
2425 else if (faultyMeas.obsType == GnssObs::Carrier && _kalmanFilter.z.hasRow(Meas::PsrDD{ faultyMeas.satSigId }))
2426 {
2427 LOG_DATA("{}: Also flagging [{}][{}] as outlier.", nameId(), faultyMeas.satSigId, GnssObs::Pseudorange);
2428 faulty.push_back(faultyMeas);
2429 faulty.back().obsType = GnssObs::Pseudorange;
2430 faulty.back().key = Meas::PsrDD{ faulty.back().satSigId };
2431 }
2432 if (faulty.size() == 2)
2433 {
2434 if (auto pivotKey = std::make_pair(faulty.back().satSigId.code, faulty.back().obsType);
2435 _pivotSatellites.contains(pivotKey) && _pivotSatellites.at(pivotKey).satSigId == faulty.back().satSigId)
2436 {
2438 }
2439 else
2440 {
2441 faulty.back().pivot.reset();
2442 }
2443 }
2444
2446 for (const auto& faultyMeas : faulty)
2447 {
2448 LOG_DATA("{}: Erasing receiver observation [{}][{}], because outlier (NIS check).", nameId(), faultyMeas.satSigId, faultyMeas.obsType);
2449
2450 if (faultyMeas.pivot)
2451 {
2452 LOG_DATA("{}: Removing pivot satellite [{}][{}] because outlier detected.", nameId(), faultyMeas.satSigId, faultyMeas.obsType);
2453 updatePivotSatellite(faultyMeas.satSigId.code, faultyMeas.obsType, observations, rtkSol, faultyMeas.pivot.value());
2454 }
2455 else
2456 {
2457 if (States::AmbiguityDD ambStateKey{ faultyMeas.satSigId };
2458 faultyMeas.obsType == GnssObs::Carrier && _kalmanFilter.hasState(ambStateKey))
2459 {
2460 LOG_TRACE("{}: Removing state [{}]", nameId(), ambStateKey);
2461 _kalmanFilter.removeState(ambStateKey);
2462 _ambiguitiesHold.erase(faultyMeas.satSigId);
2464 {
2465 addEventToGui(rtkSol, fmt::format("State [{}] removed (because outlier detected in carrier measurement [{}])", ambStateKey, faultyMeas.key));
2466 }
2467 }
2468 _kalmanFilter.removeMeasurement(faultyMeas.key);
2469
2470 LOG_DATA("{}: x = \n{}", nameId(), _kalmanFilter.x.transposed());
2471 LOG_DATA("{}: z = \n{}", nameId(), _kalmanFilter.z.transposed());
2472 }
2473
2474 observations.removeSignal(faultyMeas.satSigId, faultyMeas.obsType, nameId());
2475 _obsFilter.excludeSignalTemporarily(faultyMeas.satSigId, _outlierRemoveEpochs - 1);
2476 addEventToGui(rtkSol, fmt::format("Erasing receiver observation [{}][{}], because outlier (NIS check)", faultyMeas.satSigId, faultyMeas.obsType));
2477
2479 rtkSol->nisRemovedCnt++;
2480 rtkSol->outliers.emplace_back(RtkSolution::Outlier::Type::NIS, faultyMeas.satSigId, faultyMeas.obsType);
2481 }
2482 return faulty;
2483}
2484
2486{
2487 UpdateStatus solutionValid;
2488 if (_kalmanFilter.z.rows() == 0)
2489 {
2490 solutionValid.valid = false;
2491 }
2492 else
2493 {
2494 auto dt = static_cast<double>((_receiver[Rover].gnssObs->insTime - _lastUpdate).count());
2495 solutionValid.threshold = std::max({ 50.0,
2496 10.0 * _receiver[Rover].e_vel.norm() * dt,
2497 _kalmanFilter.P(States::Pos, States::Pos).diagonal().cwiseSqrt().maxCoeff() });
2498
2499 _kalmanFilter.savePreUpdate();
2500 _kalmanFilter.correctWithMeasurementInnovation();
2501
2502 LOG_DATA("{}: x (a posteriori, t = {}) =\n{}", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), _kalmanFilter.x.transposed());
2503 LOG_DATA("{}: P (a posteriori, t = {}) =\n{}", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), _kalmanFilter.P);
2504
2505 LOG_DATA("{}: dx_ecef (a posteriori - a priori ) = {}", nameId(), (_kalmanFilter.x.segment<3>(States::Pos) - _receiver[Rover].e_posMarker).transpose());
2506 LOG_DATA("{}: dv_ecef (a posteriori - a priori ) = {}", nameId(), (_kalmanFilter.x.segment<3>(States::Vel) - _receiver[Rover].e_vel).transpose());
2507
2508 solutionValid.dx = (_kalmanFilter.x.segment<3>(States::Pos) - _receiver[Rover].e_posMarker).norm();
2509 solutionValid.valid = solutionValid.dx < solutionValid.threshold;
2510 }
2511
2512 if (solutionValid.valid)
2513 {
2514 _receiver[Rover].e_posMarker = _kalmanFilter.x.segment<3>(States::Pos);
2515 _receiver[Rover].e_vel = _kalmanFilter.x.segment<3>(States::Vel);
2516 _receiver[Rover].lla_posMarker = trafo::ecef2lla_WGS84(_receiver[Rover].e_posMarker);
2517
2518 if (_kalmanFilter.isPreUpdateSaved()) { _kalmanFilter.discardPreUpdate(); }
2519 }
2520 else
2521 {
2522 std::string text = _kalmanFilter.z.rows() == 0
2523 ? std::string("Update rejected - no observations left")
2524 : fmt::format("Update rejected - position change due to update is {:.2f} [m] > {:.2f} [m]", solutionValid.dx, solutionValid.threshold);
2525
2526 LOG_WARN("{}: [{}] {}", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), text);
2527 addEventToGui(rtkSol, text);
2528 if (_kalmanFilter.isPreUpdateSaved()) { _kalmanFilter.restorePreUpdate(); }
2529 }
2530
2531 return solutionValid;
2532}
2533
2534bool RealTimeKinematic::resolveAmbiguities(size_t nCarrierMeasUniqueSatellite, const std::shared_ptr<RtkSolution>& rtkSol)
2535{
2536 std::vector<States::StateKeyType> ambKeys;
2537 std::vector<Meas::MeasKeyTypes> ambMeasKeys;
2538 for (size_t i = States::KFStates_COUNT; i < _kalmanFilter.x.rowKeys().size(); i++) // 0-2 Pos, 3-5 Vel
2539 {
2540 if (const auto* ambDD = std::get_if<States::AmbiguityDD>(&_kalmanFilter.x.rowKeys().at(i)))
2541 {
2542 ambKeys.emplace_back(*ambDD);
2543 ambMeasKeys.emplace_back(*ambDD);
2544 }
2545 }
2546 size_t nAmbs = ambKeys.size();
2547
2548 if (nCarrierMeasUniqueSatellite < _nMinSatForAmbFix)
2549 {
2550 LOG_TRACE("{}: [{}] Not fixing ambiguities because only {} satellites but minimum {} needed.",
2551 nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), nCarrierMeasUniqueSatellite, _nMinSatForAmbFix);
2553 {
2554 addEventToGui(rtkSol, fmt::format("Not fixing ambiguities anymore because\nonly {} satellites but minimum {} needed.", nCarrierMeasUniqueSatellite, _nMinSatForAmbFix));
2555 }
2557
2558 return false;
2559 }
2561 {
2563 addEventToGui(rtkSol, fmt::format("Resuming ambiguity fixing as\nminimum amount of satellites reached."));
2564 }
2565
2566 if (double posVar = _kalmanFilter.P(States::Pos, States::Pos).diagonal().sum() / 3.0;
2567 posVar > _maxPosVar)
2568 {
2569 LOG_TRACE("{}: [{}] Not fixing ambiguities because position variance is {:.4f}m which is higher than {:.4f}m",
2570 nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), posVar, _maxPosVar);
2572 {
2573 addEventToGui(rtkSol, fmt::format("Not fixing ambiguities anymore because\nposition variance is {:.4f}m^2 which is higher than {:.4f}m^2.", posVar, _maxPosVar));
2574 }
2575 _maxPosVarTriggered = true;
2576 return false;
2577 }
2578 else if (_maxPosVarTriggered) // NOLINT(readability-else-after-return,llvm-else-after-return)
2579 {
2580 _maxPosVarTriggered = false;
2581 addEventToGui(rtkSol, fmt::format("Resuming ambiguity fixing as\nposition variance smaller than limit."));
2582 }
2583
2584 if (ambKeys.empty()) { return false; } // No ambiguities to estimate
2585 if (nCarrierMeasUniqueSatellite >= _nMinSatForAmbHold
2586 && _ambiguityResolutionStrategy == AmbiguityResolutionStrategy::FixAndHold && ambKeys.size() == _ambiguitiesHold.size()) // No new ambiguities to estimate
2587 {
2588#if LOG_LEVEL <= LOG_LEVEL_DATA
2589 {
2590 std::set<SatSigId> ambStates;
2591 for (const auto& key : ambKeys)
2592 {
2593 auto amb = std::get<States::AmbiguityDD>(key);
2594 if (ambStates.contains(amb.satSigId))
2595 {
2596 LOG_CRITICAL("{}: The KF state has [{}] more than once.", nameId(), amb);
2597 }
2598 ambStates.insert(amb.satSigId);
2599 }
2600 LOG_DATA("{}: ambKeys: [{}]", nameId(), fmt::join(ambStates, ", "));
2601
2602 std::set<SatSigId> ambHold;
2603 for (const auto& hold : _ambiguitiesHold)
2604 {
2605 if (ambHold.contains(hold.first))
2606 {
2607 LOG_CRITICAL("{}: _ambiguitiesHold has [{}] more than once.", nameId(), hold.first);
2608 }
2609 ambHold.insert(hold.first);
2610 }
2611 LOG_DATA("{}: ambHold: [{}]", nameId(), fmt::join(ambHold, ", "));
2612
2613 if (ambStates != ambHold)
2614 {
2615 std::vector<SatSigId> diff;
2616 std::set_difference(ambStates.begin(), ambStates.end(), ambHold.begin(), ambHold.end(), std::back_inserter(diff)); // NOLINT(boost-use-ranges,modernize-use-ranges) // There is no ranges::set_difference
2617 LOG_DATA("{}: Amb not in hold: [{}]", nameId(), fmt::join(diff, ", "));
2618 diff.clear();
2619 std::set_difference(ambHold.begin(), ambHold.end(), ambStates.begin(), ambStates.end(), std::back_inserter(diff)); // NOLINT(boost-use-ranges,modernize-use-ranges) // There is no ranges::set_difference
2620 LOG_DATA("{}: Amb not in states: [{}]", nameId(), fmt::join(diff, ", "));
2621 }
2622 }
2623#endif
2624 Eigen::VectorXd fixedAmb = Eigen::VectorXd::Zero(static_cast<int>(ambKeys.size()));
2625 // Apply exact integers (after update it is still float in the late digits)
2626 for (size_t k = 0; k < ambKeys.size(); k++)
2627 {
2628 const auto& key = ambKeys.at(k);
2629 auto satSigId = std::get<States::AmbiguityDD>(key).satSigId;
2630 LOG_DATA("{}: FixAndHold: [{}] Holding {} to {} (after update)", nameId(), satSigId, _kalmanFilter.x.hasRow(key), _ambiguitiesHold.contains(satSigId));
2631 LOG_DATA("{}: FixAndHold: [{}] Holding {} to {} (after update)", nameId(), satSigId, _kalmanFilter.x(key), _ambiguitiesHold.at(satSigId));
2632 fixedAmb(static_cast<int>(k)) = _ambiguitiesHold.at(satSigId);
2633 }
2634 rtkSol->nAmbiguitiesFixed = ambKeys.size() + 1; // + 1 to also count the pivot
2635 applyFixedAmbiguities(fixedAmb, ambKeys, ambMeasKeys);
2636
2638 {
2640 addEventToGui(rtkSol, fmt::format("Resuming ambiguity holding as\nminimum amount of satellites reached."));
2641 }
2642
2643 return true;
2644 }
2646 {
2647 LOG_TRACE("{}: [{}] Not holding ambiguities because only {} satellites but minimum {} needed.",
2648 nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), nCarrierMeasUniqueSatellite, _nMinSatForAmbHold);
2650 {
2651 addEventToGui(rtkSol, fmt::format("Not holding ambiguities anymore because\nonly {} satellites but minimum {} needed.", nCarrierMeasUniqueSatellite, _nMinSatForAmbHold));
2652 }
2654 }
2655
2656 LOG_DATA("{}: [{}] Estimating ambiguities", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST));
2657 auto floatKeys = States::PosVel;
2658 size_t partialFixTries = 0;
2659 do {
2660 LOG_DATA("{}: [{}] floatKeys = {}", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), fmt::join(floatKeys, ", "));
2661 LOG_DATA("{}: [{}] ambKeys = {}", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), fmt::join(ambKeys, ", "));
2662 auto fixed = ResolveAmbiguities(_kalmanFilter.x(ambKeys), _kalmanFilter.P(ambKeys, ambKeys),
2663 _kalmanFilter.x(floatKeys), _kalmanFilter.P(floatKeys, floatKeys),
2664 _kalmanFilter.P(ambKeys, floatKeys), _kalmanFilter.P(floatKeys, ambKeys),
2666 rtkSol->ambiguityResolutionFailure = fixed.failure;
2667 rtkSol->ambiguityCriticalValueRatio = fixed.ambiguityCriticalValueRatio;
2668
2669 if (fixed.failure == AmbiguityResolutionFailure::None)
2670 {
2671 _kalmanFilter.x(floatKeys) = fixed.b;
2672 LOG_DATA("{}: x(fixed.b) =\n{}", nameId(), _kalmanFilter.x.transposed());
2673
2674 if (_applyFixedAmbiguitiesWithUpdate) { applyFixedAmbiguities(fixed.fixedAmb.front().a, ambKeys, ambMeasKeys); }
2675 else
2676 {
2677 _kalmanFilter.x(ambKeys) = fixed.fixedAmb.front().a;
2678 // _kalmanFilter.P(floatKeys, floatKeys) = fixed.Qb;
2679 // LOG_DATA("{}: P(fixed.Qb) =\n{}", nameId(), _kalmanFilter.P);
2680 }
2681
2682 LOG_DATA("{}: dx_ecef (fix, update - fix ) = {}", nameId(), (_kalmanFilter.x.segment<3>(States::Pos) - _receiver[Rover].e_posMarker).transpose());
2683 LOG_DATA("{}: dv_ecef (fix, update - fix ) = {}", nameId(), (_kalmanFilter.x.segment<3>(States::Vel) - _receiver[Rover].e_vel).transpose());
2684 _receiver[Rover].e_posMarker = _kalmanFilter.x.segment<3>(States::Pos);
2685 _receiver[Rover].e_vel = _kalmanFilter.x.segment<3>(States::Vel);
2686 _receiver[Rover].lla_posMarker = trafo::ecef2lla_WGS84(_receiver[Rover].e_posMarker);
2687
2688 rtkSol->ambiguityCriticalValueRatio = fixed.ambiguityCriticalValueRatio;
2689 rtkSol->nAmbiguitiesFixed = fixed.nFixed + 1; // + 1 to also count the pivot
2690
2691 for (const auto& key_ : ambKeys)
2692 {
2693 const auto key = std::get<States::AmbiguityDD>(key_);
2694
2696 && _ambiguitiesHold.contains(key.satSigId)
2697 && std::abs(_kalmanFilter.x(key) - _ambiguitiesHold.at(key.satSigId)) > 0.1
2698 && fixed.nFixed == nAmbs && partialFixTries == 0)
2699 {
2700 LOG_WARN("{}: Ambiguity [{}] changed from {} to {} (despite being FixAndHold)", nameId(), key.satSigId,
2701 _kalmanFilter.x(key), _ambiguitiesHold.at(key.satSigId));
2702 }
2703 _ambiguitiesHold[key.satSigId] = _kalmanFilter.x(key);
2704 }
2705
2706 return partialFixTries == 0;
2707 }
2708 LOG_DATA("{}: Fixing failed. partialFixTries = {}", nameId(), partialFixTries);
2709
2710 if (_ambiguityResolutionParameters.partialFixing)
2711 {
2712 int maxAmb = 0;
2713 _kalmanFilter.P(ambKeys, ambKeys).diagonal().maxCoeff(&maxAmb);
2714
2715 LOG_DATA("{}: [{}] Trying partial fixing by not fixing [{}] with highest variance of {}",
2716 nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST),
2717 ambKeys.at(static_cast<size_t>(maxAmb)),
2718 _kalmanFilter.P(ambKeys.at(static_cast<size_t>(maxAmb)), ambKeys.at(static_cast<size_t>(maxAmb))));
2719 floatKeys.push_back(ambKeys.at(static_cast<size_t>(maxAmb)));
2720 ambKeys.erase(std::next(ambKeys.begin(), maxAmb));
2721 ambMeasKeys.erase(std::next(ambMeasKeys.begin(), maxAmb));
2722 partialFixTries++;
2723 }
2724 } while (_ambiguityResolutionParameters.partialFixing && ambKeys.size() > 1 && partialFixTries < 6);
2725
2727 {
2728 LOG_TRACE("{}: [{}] Ambiguity resolution failed", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST));
2729 _ambiguitiesHold.clear();
2730 }
2731
2732 return false;
2733}
2734
2735void RealTimeKinematic::applyFixedAmbiguities(const Eigen::VectorXd& fixedAmb, const std::vector<States::StateKeyType>& ambKeys, const std::vector<Meas::MeasKeyTypes>& ambMeasKeys)
2736{
2737#if LOG_LEVEL <= LOG_LEVEL_DATA
2738 if (((_kalmanFilter.x(ambKeys).array() * 1e2).round() * 1e-2).matrix() != fixedAmb)
2739 {
2741 (Eigen::MatrixXd(static_cast<int>(ambKeys.size()), 2) << _kalmanFilter.x(ambKeys), fixedAmb).finished(),
2742 ambKeys, std::vector<States::StateKeyType>{ States::AmbiguityDD(SatSigId(Code::G1C, 200)), States::AmbiguityDD(SatSigId(Code::G1C, 201)) });
2743 LOG_DATA("{}: [{}] Ambiguity estimate changed (relative to pivot) (200 = prev, 201 = new)\n{}", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), ambPrint.transposed());
2744 LOG_DATA("P(amb) =\n{}", _kalmanFilter.P(ambKeys, ambKeys));
2745 }
2746#endif
2747
2748 // Make a KF update with the fixed ambiguities as observation in order to adapt the P matrix
2749 _kalmanFilter.setMeasurements(ambMeasKeys);
2750
2751 // Update the Measurement sensitivity Matrix (𝐇), the Measurement noise covariance matrix (𝐑) and the Measurement vector (𝐳)
2752 _kalmanFilter.z.segment(ambMeasKeys) = fixedAmb - _kalmanFilter.x(ambKeys);
2753 _kalmanFilter.H(ambMeasKeys, ambKeys).setIdentity();
2754
2755 // R matrix
2756 _kalmanFilter.R(all, all).diagonal().setConstant(_ambFixUpdateVariance);
2757
2758 LOG_DATA("{}: z =\n{}", nameId(), _kalmanFilter.z.transposed());
2759 LOG_DATA("{}: H =\n{}", nameId(), _kalmanFilter.H);
2760 LOG_DATA("{}: R =\n{}", nameId(), _kalmanFilter.R);
2761
2762 _kalmanFilter.correctWithMeasurementInnovation();
2763
2764 LOG_DATA("{}: x (fix, update , t = {}) = (Ambiguity update)\n{}", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), _kalmanFilter.x.transposed());
2765 // Apply the integer values, otherwise values not exact
2766 _kalmanFilter.x(ambKeys) = fixedAmb;
2767 LOG_DATA("{}: x (fix, update , t = {}) = (Ambiguity integers)\n{}", nameId(), _receiver[Rover].gnssObs->insTime.toYMDHMS(GPST), _kalmanFilter.x.transposed());
2768}
2769
2770const char* to_string(const RealTimeKinematic::ReceiverType& receiver)
2771{
2772 switch (receiver)
2773 {
2775 return "Base";
2777 return "Rover";
2779 return "";
2780 }
2781 return "";
2782}
2783
2784} // namespace NAV
Ambiguity resolution algorithms.
#define INS_ASSERT_USER_ERROR(_EXP, _MSG)
Assert function with message.
Definition Assert.h:21
Code definitions.
Holds all Constants.
Combination of different cycle-slip detection algorithms.
Ambiguity decorrelation algorithms.
Combo representing an enumeration.
Save/Load the Nodes.
nlohmann::json json
json namespace
Frequency definition for different satellite systems.
GNSS helper functions.
Keys for the RTK algorithm for use inside the KeyedMatrices.
GNSS Observation messages.
Text Help Marker (?) with Tooltip.
Defines Widgets which allow the input of values and the selection of the unit.
The class is responsible for all time-related tasks.
void from_json(const json &j, ImColor &color)
Converts the provided json object into a color.
Definition Json.cpp:26
void to_json(json &j, const ImColor &color)
Converts the provided color into a json object.
Definition Json.cpp:17
Defines how to save certain datatypes to json.
Least squares with keyed states.
Least Squares Algorithm.
Utility class for logging to console and file.
#define LOG_CRITICAL(...)
Critical Event, which causes the program to work entirely and throws an exception.
Definition Logger.hpp:75
#define LOG_DEBUG
Debug information. Should not be called on functions which receive observations (spamming)
Definition Logger.hpp:67
#define LOG_DATA
All output which occurs repeatedly every time observations are received.
Definition Logger.hpp:29
#define LOG_ERROR
Error occurred, which stops part of the program to work, but not everything.
Definition Logger.hpp:73
#define LOG_WARN
Error occurred, but a fallback option exists and program continues to work normally.
Definition Logger.hpp:71
#define LOG_TRACE
Detailled info to trace the execution of the program. Should not be called on functions which receive...
Definition Logger.hpp:65
Node Class.
Observation data used for calculations.
Position Storage Class.
Real-Time Kinematic (RTK) carrier-phase DGNSS.
RTK Node/Algorithm output.
Structs identifying a unique satellite.
Ambiguity Search algorithms.
Time System defintions.
Values with an uncertainty (Standard Deviation)
std::unordered_map< Key, T > unordered_map
Unordered map type.
Vector Utility functions.
Enumerate for GNSS Codes.
Definition Code.hpp:89
Cycle-slip detector.
bool isEnabled(const Detector &detector) const
Is the cycle-slip detector enabled?
void resetSignal(const SatSigId &satSigId)
Resets all data related to the provided signal.
@ LLI
Loss-of-Lock Indicator check.
@ SingleFrequency
Single frequency detector.
@ DualFrequency
Dual frequency detector.
static std::string type()
Returns the type of the data class.
ObservationType
Observation types.
Definition GnssObs.hpp:38
@ Doppler
Doppler (Pseudorange rate)
Definition GnssObs.hpp:41
@ ObservationType_COUNT
Count.
Definition GnssObs.hpp:42
@ Carrier
Carrier-Phase.
Definition GnssObs.hpp:40
@ Pseudorange
Pseudorange.
Definition GnssObs.hpp:39
static std::string type()
Returns the type of the data class.
Definition GnssObs.hpp:151
Input pins of nodes.
Definition Pin.hpp:491
TsDeque< std::shared_ptr< const NAV::NodeData > > NodeDataQueue
Node data queue type.
Definition Pin.hpp:707
static constexpr double C
Speed of light [m/s].
Definition Constants.hpp:34
ImVec2 _guiConfigDefaultWindowSize
Definition Node.hpp:522
Node(std::string name)
Constructor.
Definition Node.cpp:29
std::optional< InputPin::IncomingLink::ValueWrapper< T > > getInputValue(size_t portIndex) const
Get Input Value connected on the pin. Only const data types.
Definition Node.hpp:290
std::vector< InputPin > inputPins
List of input pins.
Definition Node.hpp:509
OutputPin * CreateOutputPin(const char *name, Pin::Type pinType, const std::vector< std::string > &dataIdentifier, OutputPin::PinData data=static_cast< void * >(nullptr), int idx=-1)
Create an Output Pin object.
Definition Node.cpp:278
std::string nameId() const
Node name and id.
Definition Node.cpp:323
std::string name
Name of the Node.
Definition Node.hpp:507
InputPin * CreateInputPin(const char *name, Pin::Type pinType, const std::vector< std::string > &dataIdentifier={}, InputPin::Callback callback=static_cast< InputPin::FlowFirableCallbackFunc >(nullptr), InputPin::FlowFirableCheckFunc firable=nullptr, int priority=0, int idx=-1)
Create an Input Pin object.
Definition Node.cpp:252
void invokeCallbacks(size_t portIndex, const std::shared_ptr< const NodeData > &data)
Calls all registered callbacks on the specified output port.
Definition Node.cpp:179
bool DeleteInputPin(size_t pinIndex)
Deletes the input pin. Invalidates the pin reference given.
Definition Node.cpp:310
bool _hasConfig
Flag if the config window should be shown.
Definition Node.hpp:525
@ DoubleDifference
Double Difference.
void disableFilter()
Opens all settings to the maximum, disabling the filter.
void selectObservationsForCalculation(ReceiverType receiverType, const Eigen::MatrixBase< DerivedPe > &e_posMarker, const Eigen::MatrixBase< DerivedPn > &lla_posMarker, const std::shared_ptr< const GnssObs > &gnssObs, const std::vector< const GnssNavInfo * > &gnssNavInfos, Observations &observations, Filtered *filtered, const std::string &nameId, bool ignoreElevationMask=false)
Returns a list of satellites and observations filtered by GUI settings & NAV data available & ....
static std::string type()
Returns the type of the data class.
Definition Pos.hpp:36
ObservationFilter _obsFilter
Observation Filter.
double _dataInterval
Data interval [s].
StdevAccelUnits _gui_stdevAccelUnits
Gui selection for the Unit of the input stdev_accel parameter for the StDev due to acceleration due t...
void updateKalmanFilterAmbiguitiesForPivotChange(const SatSigId &newPivotSatSigId, const SatSigId &oldPivotSatSigId, bool oldPivotObservedInEpoch, const std::shared_ptr< RtkSolution > &rtkSol)
Adapts the Kalman Filter if a pivot satellite signal was changed.
StdevAmbiguityUnits _gui_stdevAmbiguityFloatUnits
Gui selection for the Unit of the input for the StDev of the ambiguities (while float solution)
void recvRoverGnssObs(InputPin::NodeDataQueue &queue, size_t pinIdx)
Receive Function for the RoverGNSS Observations.
void printPivotSatellites()
Prints all pivot satellites.
std::vector< std::pair< InsTime, std::vector< std::string > > > _events
List of event texts (pivot changes, cycle-slips)
size_t nFixSolutions
Percentage of fix solutions.
bool _outputStateEvents
Whether to output state change events.
size_t _nCycleSlipsDual
Cycle-slip detection count (Dual)
void guiConfig() override
ImGui config window which is shown on double click.
static std::string category()
String representation of the Class Category.
std::shared_ptr< RtkSolution > calcFallbackSppSolution()
Calculates a SPP solution as fallback in case no base data is available.
bool initialize() override
Initialize the node.
size_t _outlierMinSat
Minumum amount of satellites for doing a NIS check.
KeyedKalmanFilterD< RTK::States::StateKeyType, RTK::Meas::MeasKeyTypes > _kalmanFilter
Kalman Filter representation.
bool _maxPosVarTriggered
Trigger state of the check.
std::unordered_map< SatSigId, double > _ambiguitiesHold
Ambiguities which are fixed and hold. Values in [cycles].
void calcKalmanUpdateMatrices(const Observations &observations, const Differences &doubleDifferences, const unordered_map< GnssObs::ObservationType, KeyedMatrixXd< RTK::Meas::SingleObs< ReceiverType > > > &Rtilde)
Calculates the Measurement vector 𝐳, Measurement sensitivity matrix 𝐇 and Measurement noise covarianc...
StdevAmbiguityUnits _gui_ambFixUpdateStdDevUnits
Gui selection for the Unit of the input for the StDev of the ambiguities (while float solution)
std::vector< OutlierInfo > removeOutlier(Observations &observations, const std::shared_ptr< RtkSolution > &rtkSol)
Performs the NIS check.
double _ambiguityFloatProcessNoiseVariance
Process noise (variance) for the ambiguities (while float solution) in [cycles^2] (Value used for cal...
std::array< double, 2 > _gui_stdevAccel
GUI selection for the Standard deviation of the acceleration 𝜎_a due to user motion in horizontal and...
double _ambFixUpdateVariance
Measurement noise variance used when fixing ambiguities in [cycles^2] (Value used for calculation)
double _maxPosVar
Do not attempt to fix if the position variance is above the threshold.
size_t _nMinSatForAmbFix
Minimum amount of satellites with carrier observations to try fixing the solution.
double _maxTimeBetweenBaseRoverForRTK
Maximum time between base and rover observations to calculate the RTK solution.
size_t _nMinSatForAmbHold
Minimum amount of satellites with carrier observations for holding the ambiguities.
AmbiguityResolutionParameters _ambiguityResolutionParameters
Ambiguity resolution algorithms and parameters to use.
static void pinAddCallback(Node *node)
Function to call to add a new pin.
size_t _nPivotChange
Amount of pivot changes performed.
std::array< CycleSlipDetector, ReceiverType_COUNT > _cycleSlipDetector
Cycle-slip detector.
gui::widgets::DynamicInputPins _dynamicInputPins
Dynamic input pins.
double _outlierMaxPosVarStartup
Do not attempt to remove outlier if the position variance is above the threshold in the startup phase...
RtkSolution::SolutionType _lastSolutionStatus
Solution type of last epoch.
std::string _eventFilterRegex
Regex for filtering events.
void addEventToGui(const std::shared_ptr< RtkSolution > &rtkSol, const std::string &text)
Adds the event to the GUI list.
size_t _nCycleSlipsSingle
Cycle-slip detection count (Single)
RealTimeKinematic()
Default constructor.
void calcRealTimeKinematicSolution()
Calculates the RTK solution.
size_t _nCycleSlipsLLI
Cycle-slip detection count (LLI)
void assignSolutionToFilter(const std::shared_ptr< NAV::SppSolution > &sppSol)
Assign the SPP solution to the RTK filter.
unordered_map< SatSigId, unordered_map< GnssObs::ObservationType, Difference > > Differences
Difference storage type.
size_t nFloatSolutions
Percentage of float solutions.
@ ReceiverType_COUNT
Amount of receiver types.
void recvBaseGnssObs(InputPin::NodeDataQueue &queue, size_t pinIdx)
Receive Function for the Base GNSS Observations.
unordered_map< GnssObs::ObservationType, KeyedMatrixXd< RTK::Meas::SingleObs< ReceiverType > > > calcSingleObsMeasurementNoiseMatrices(const Observations &observations) const
Calculate the measurement noise matrices for each observation type.
double _gui_ambiguityFloatProcessNoiseStDev
Process noise (standard deviation) for the ambiguities (while float solution) (Selection in GUI)
StdevAmbiguityUnits _gui_stdevAmbiguityFixUnits
Gui selection for the Unit of the input for the StDev of the ambiguities (while fix solution)
void addOrRemoveKalmanFilterAmbiguities(const Observations &observations, const std::shared_ptr< RtkSolution > &rtkSol)
Adds or remove Ambiguities to/from the Kalman Filter state depending on the received observations.
size_t _nMeasExcludedNIS
Measurement excluded due to NIS test count.
static void pinDeleteCallback(Node *node, size_t pinIdx)
Function to call to delete a pin.
void printObservations(const Observations &observations)
Prints the observations for logging.
std::string type() const override
String representation of the Class Type.
NAV::Receiver< ReceiverType > Receiver
Receiver.
AmbiguityResolutionStrategy _ambiguityResolutionStrategy
Ambiguity resolution strategy.
SPP::Algorithm _sppAlgorithm
SPP algorithm.
bool _calcSPPIfNoBase
Wether to output a SPP solution if no base observations are available.
double _ambiguityFixProcessNoiseVariance
Process noise (standard deviation) for the ambiguities (while fix solution) in [cycles^2] (Value used...
size_t _outlierMinPsrObsKeep
Minumum amount of pseudorange observables to leave when doing a NIS check.
bool _outlierMaxPosVarStartupTriggered
Trigger state of the check.
Differences calcDoubleDifferences(const Observations &observations, const Differences &singleDifferences) const
Calculates the double difference of the measurements and estimates.
void kalmanFilterPrediction()
Does the Kalman Filter prediction.
bool _baseObsReceivedThisEpoch
Flag, whether the observation was received this epoch.
size_t _outlierRemoveEpochs
Amount of epochs to remove an outlier after being found (1 = only current)
size_t nSingleSolutions
Percentage of float solutions.
static constexpr size_t OUTPUT_PORT_INDEX_RTKSOL
Flow (RtkSol)
InsTime _lastUpdate
Last update time.
double _gui_ambiguityFixProcessNoiseStDev
Process noise (standard deviation) for the ambiguities (while fix solution) (Selection in GUI)
ObservationEstimator _obsEstimator
Observation Estimator.
unordered_map< std::pair< Code, GnssObs::ObservationType >, PivotSatellite > _pivotSatellites
Pivot satellites for each constellation.
std::optional< RtkSolution::PivotChange > updatePivotSatellite(Code code, GnssObs::ObservationType obsType, Observations &observations, const std::shared_ptr< RtkSolution > &rtkSol, const RtkSolution::PivotChange::Reason &reason)
Update the specified pivot satellite and also does a pivot change in the Kalman filter state if neces...
bool _nMinSatForAmbHoldTriggered
Trigger state of the check.
double _gui_ambFixUpdateStdDev
Measurement noise standard deviation used when fixing ambiguities in [cycles] (GUI value)
~RealTimeKinematic() override
Destructor.
size_t _maxRemoveOutlier
Maximum amount of outliers to remove per epoch.
void recvBasePos(InputPin::NodeDataQueue &queue, size_t pinIdx)
Receive Function for the Base Position.
void deinitialize() override
Deinitialize the node.
UpdateStatus kalmanFilterUpdate(const std::shared_ptr< RtkSolution > &rtkSol)
Does the Kalman Filter update.
static constexpr size_t INPUT_PORT_INDEX_GNSS_NAV_INFO
GnssNavInfo.
void restore(const json &j) override
Restores the node from a json object.
void removeSingleObservations(Observations &observations, ObservationFilter::Filtered *filtered, const std::shared_ptr< RtkSolution > &rtkSol)
Removes observations which do not have a second one to do double differences.
void applyFixedAmbiguities(const Eigen::VectorXd &fixedAmb, const std::vector< RTK::States::StateKeyType > &ambKeys, const std::vector< RTK::Meas::MeasKeyTypes > &ambMeasKeys)
Apply the fixed ambiguities to the kalman filter state.
void checkForCycleSlip(Observations &observations, const std::shared_ptr< RtkSolution > &rtkSol)
Checks for cycle-slips.
Differences calcSingleDifferences(const Observations &observations) const
Calculates the single difference of the measurements and estimates.
bool resolveAmbiguities(size_t nCarrierMeasUniqueSatellite, const std::shared_ptr< RtkSolution > &rtkSol)
Resolves the ambiguities in the Kalman filter and updates the state and covariance matrix.
std::array< Receiver, ReceiverType::ReceiverType_COUNT > _receiver
Receivers.
static std::string typeStatic()
String representation of the Class Type.
void updatePivotSatellites(Observations &observations, const std::shared_ptr< RtkSolution > &rtkSol)
Update the pivot satellites for each constellation.
bool _nMinSatForAmbFixTriggered
Trigger state of the check.
json save() const override
Saves the node into a json object.
@ Predicted
Only predicted by Kalman Filter.
@ RTK_Fixed
RTK solution with fixed ambiguities to integers.
@ RTK_Float
RTK solution with floating point ambiguities.
@ SPP
Solution calculated via SPP algorithm because of missing data for RTK.
static std::string type()
Returns the type of the data class.
auto extract_front()
Returns a copy of the first element in the container and removes it from the container.
Definition TsDeque.hpp:494
static float windowFontRatio()
Ratio to multiply for GUI window elements.
const std::vector< RowKeyType > & rowKeys() const
Returns the row keys.
ImGui extensions.
bool InputIntL(const char *label, int *v, int v_min, int v_max, int step, int step_fast, ImGuiInputTextFlags flags)
Shows a value limited InputText GUI element for 'int'.
Definition imgui_ex.cpp:242
bool InputDoubleL(const char *label, double *v, double v_min, double v_max, double step, double step_fast, const char *format, ImGuiInputTextFlags flags)
Shows a value limited InputText GUI element for 'double'.
Definition imgui_ex.cpp:294
constexpr std::array< StateKeyType, 3 > Pos
All position keys.
constexpr std::array< StateKeyType, 6 > PosVel
Vector with all position and velocity keys.
constexpr std::array< StateKeyType, 3 > Vel
All velocity keys.
std::variant< PsrDD, CarrierDD, DopplerDD, States::AmbiguityDD > MeasKeyTypes
Alias for the measurement key type.
Definition Keys.hpp:98
static const std::vector< StateKeyType > Vel
All velocity keys.
Definition Keys.hpp:62
@ KFStates_COUNT
Count.
Definition Keys.hpp:39
std::variant< KFStates, AmbiguityDD > StateKeyType
Alias for the state key type.
Definition Keys.hpp:55
static const std::vector< StateKeyType > Pos
All position keys.
Definition Keys.hpp:60
static const std::vector< StateKeyType > PosVel
Vector with all position and velocity state keys.
Definition Keys.hpp:57
void ApplyChanges()
Signals that there have been changes to the flow.
InputWithUnitChange InputDoubleWithUnit(const char *label, float itemWidth, float unitWidth, double *v, U &combo_current_item, const char *combo_items_separated_by_zeros, double step=0.0, double step_fast=0.0, const char *format="%.6f", ImGuiInputTextFlags flags=0, int combo_popup_max_height_in_items=-1)
Shows an InputText GUI element to modify the provided value and also set its unit.
InputWithUnitChange InputDouble2WithUnit(const char *label, float itemWidth, float unitWidth, double v[2], U &combo_current_item, const char *combo_items_separated_by_zeros, const char *format="%.6f", ImGuiInputTextFlags flags=0, int combo_popup_max_height_in_items=-1)
Shows an InputText GUI element to modify the provided value and also set its unit.
bool EnumCombo(const char *label, T &enumeration, size_t startIdx=0)
Combo representing an enumeration.
Definition EnumCombo.hpp:30
void from_json(const json &j, DynamicInputPins &obj, Node *node)
Converts the provided json object into a node object.
void HelpMarker(const char *desc, const char *symbol="(?)")
Text Help Marker, e.g. '(?)', with Tooltip.
Eigen::Vector3< typename Derived::Scalar > ecef2lla_WGS84(const Eigen::MatrixBase< Derived > &e_position)
Converts Earth-centered-Earth-fixed coordinates into latitude, longitude and altitude using WGS84.
Eigen::Quaternion< Scalar > e_Quat_n(const Scalar &latitude, const Scalar &longitude)
Quaternion for rotations from navigation to Earth-fixed frame.
std::string joinToString(const T &container, const char *delimiter=", ", const std::string &elementFormat="")
Joins the container to a string.
Definition STL.hpp:30
@ GPST
GPS Time.
std::pair< typename DerivedF::PlainObject, typename DerivedF::PlainObject > calcPhiAndQWithVanLoanMethod(const Eigen::MatrixBase< DerivedF > &F, const Eigen::MatrixBase< DerivedG > &G, const Eigen::MatrixBase< DerivedW > &W, double dt)
Numerical Method to calculate the State transition matrix 𝚽 and System/Process noise covariance matri...
Definition VanLoan.hpp:65
@ FixAndHold
Do not change the ambiguity once it is fixed.
const char * to_string(gui::widgets::PositionWithFrame::ReferenceFrame refFrame)
Converts the enum to a string.
KeyedVectorX< double, RowKeyType > KeyedVectorXd
Dynamic size KeyedVector with double types.
KeyedKalmanFilter< double, StateKeyType, MeasKeyType > KeyedKalmanFilterD
Keyed Kalman Filter class with double as type.
AmbiguityResolutionResult< typename DerivedA::Scalar, DerivedA::RowsAtCompileTime, DerivedB::RowsAtCompileTime > ResolveAmbiguities(const Eigen::MatrixBase< DerivedA > &a, const Eigen::MatrixBase< DerivedQa > &Qa, const Eigen::MatrixBase< DerivedB > &b, const Eigen::MatrixBase< DerivedQb > &Qb, const Eigen::MatrixBase< DerivedQab > &Qab, const Eigen::MatrixBase< DerivedQba > &Qba, const AmbiguityResolutionParameters &params, const std::string &nameId)
Tries resolving the ambiguities.
static const internal::all_t all
Used to request all rows or columns in KeyedMatrices.
KeyedMatrixX< double, RowKeyType, ColKeyType > KeyedMatrixXd
Dynamic size KeyedMatrix with double types.
bool CycleSlipDetectorGui(const char *label, CycleSlipDetector &cycleSlipDetector, float width, bool dualFrequencyAvailable)
Shows a GUI for advanced configuration of the CycleSlipDetector.
constexpr auto rad2deg(const T &rad)
Convert Radians to Degree.
Definition Units.hpp:39
bool GuiAmbiguityResolution(const char *id, AmbiguityResolutionParameters &params, float width)
Shows a ComboBox to select the ambiguity resolution algorithms.
Satellite observations ordered per satellite.
std::vector< SatSigId > singleObservation
Only signal for this code/type (relevant for double differences)
Observation storage type.
size_t countObservations(const SatSigId &satSigId, const GnssObs::ObservationType &obsType) const
Counts how many observations for the specified signal ant type exist.
bool removeSignal(const SatSigId &satSigId, const std::string &nameId)
Remove the signal from the observations.
bool removeMeasurementsFor(const Code &code, const GnssObs::ObservationType &obsType, const std::string &nameId)
Remove all measurements for the provided code and observation type.
std::array< size_t, GnssObs::ObservationType_COUNT > nObservables
Number of observables.
void recalcObservableCounts(const std::string &nameId)
Calculates/Recalculates the number of observables.
std::unordered_set< SatId > satellites
Satellites used.
std::array< size_t, GnssObs::ObservationType_COUNT > nObservablesUniqueSatellite
Number of observables (counted once for each satellite)
unordered_map< SatSigId, SignalObservation > signals
Observations and calculated data for each signal.
@ Flow
NodeData Trigger.
Definition Pin.hpp:52
@ Object
Generic Object.
Definition Pin.hpp:57
Double differenced carrier-phase measurement phi_br^1s [m] (one for each satellite signal,...
Definition Keys.hpp:80
Double differenced range-rate (doppler) measurement d_br^1s [m/s] (one for each satellite signal,...
Definition Keys.hpp:89
Double differenced pseudorange measurement psr_br^1s [m] (one for each satellite signal,...
Definition Keys.hpp:71
Single Observation key.
Definition Keys.hpp:103
Double differenced N_br^1s = N_br^s - N_br^1 ambiguity [cycles] (one for each satellite signal,...
Definition Keys.hpp:43
std::optional< RtkSolution::PivotChange::Reason > pivot
Pivot change reason, if it is a pivot.
SatSigId satSigId
Satellit signal id.
RTK::Meas::MeasKeyTypes key
Measurement key of the outlier.
GnssObs::ObservationType obsType
Observation type.
bool valid
Flag if the solution is valid.
double dx
Position change due to the update.
double threshold
Allowed position change.
Ambiguity double differences.
@ NIS
Normalized Innovation Squared (NIS)
Pivot Change information.
Reason
Possible reasons for a pivot change.
@ PivotNotObservedInEpoch
Old pivot satellite was not observed this epoch.
@ PivotCycleSlip
The pivot satellite had a cycle-slip.
@ PivotOutlier
Old pivot satellite was flagged as outlier.
Satellite specific data.
Identifies a satellite signal (satellite frequency and number)
Value with standard deviation.