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