source: ntrip/trunk/BNC/src/ssrQc.cpp@ 11081

Last change on this file since 11081 was 11081, checked in by stuerze, 9 days ago

SSR QC improved

File size: 38.6 KB
Line 
1// Part of BNC, a utility for retrieving decoding and
2// converting GNSS data streams from NTRIP broadcasters.
3//
4// Copyright (C) 2007
5// German Federal Agency for Cartography and Geodesy (BKG)
6// http://www.bkg.bund.de
7// Czech Technical University Prague, Department of Geodesy
8// http://www.fsv.cvut.cz
9//
10// Email: euref-ip@bkg.bund.de
11//
12// This program is free software; you can redistribute it and/or
13// modify it under the terms of the GNU General Public License
14// as published by the Free Software Foundation, version 2.
15//
16// This program is distributed in the hope that it will be useful,
17// but WITHOUT ANY WARRANTY; without even the implied warranty of
18// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19// GNU General Public License for more details.
20//
21// You should have received a copy of the GNU General Public License
22// along with this program; if not, write to the Free Software
23// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
24
25/* -------------------------------------------------------------------------
26 * BKG NTRIP Client
27 * -------------------------------------------------------------------------
28 *
29 * Class: t_ssrQc
30 *
31 * Purpose: Quality control of decoded SSR streams
32 *
33 * Created: 05-Oct-2026
34 *
35 * Changes:
36 *
37 * -----------------------------------------------------------------------*/
38
39#include <algorithm>
40#include <cmath>
41#include <iomanip>
42
43#include "ssrQc.h"
44#include "bnccore.h"
45#include "bncutils.h"
46#include "bncconst.h"
47#include "bncsettings.h"
48#include "combination/bnccomb.h"
49
50using namespace std;
51
52namespace {
53 const double MAX_CLK_JUMP = 0.5; // [m] clock change beyond its rate between consecutive epochs
54 const double EPH_WARMUP = 120.0; // [s] no ephemeris checks at the start
55 const double CMP_SAMPL = 30.0; // [s] sampling of the stream comparison
56 const double CMP_DELAY = 60.0; // [s] epochs compared when data this much newer arrived
57 const double HIST_LENGTH = 300.0; // [s] corrections kept for the comparison
58 const double DEV_MIN = 0.05; // [m] deviating satellites: RMS above max(DEV_MIN,
59 const double DEV_FACTOR = 3.0; // DEV_FACTOR x median RMS of the system)
60
61 double tKey(const bncTime& tt) {
62 return tt.gpsw() * 604800.0 + tt.gpssec();
63 }
64 const char* TYPE_NAME[] = {"ORBIT", "CLOCK", "CODE_BIAS", "PHASE_BIAS"};
65
66 double median(QVector<double> vv) {
67 if (vv.isEmpty()) {
68 return 0.0;
69 }
70 std::sort(vv.begin(), vv.end());
71 int nn = vv.size();
72 return (nn % 2) ? vv[nn/2] : 0.5 * (vv[nn/2-1] + vv[nn/2]);
73 }
74
75 bncTime currentTime() {
76 int week = 0;
77 double sec = 0.0;
78 currentGPSWeeks(week, sec);
79 return bncTime(week, sec);
80 }
81
82 // satellite list "G01 G05(12)", counts shown if not all epochs concerned
83 QString satList(const QMap<QString, unsigned>& sats, unsigned maxNum = 20) {
84 QString str;
85 unsigned num = 0;
86 QMapIterator<QString, unsigned> it(sats);
87 while (it.hasNext()) {
88 it.next();
89 if (++num > maxNum) {
90 str += QString(" ... (%1 satellites)").arg(sats.size());
91 break;
92 }
93 str += " " + it.key();
94 if (it.value() > 0) {
95 str += QString("(%1)").arg(it.value());
96 }
97 }
98 return str.isEmpty() ? " -" : str;
99 }
100}
101
102// Constructor
103////////////////////////////////////////////////////////////////////////////
104t_ssrQc::t_ssrQc(const QString& dirName, double interval, const QStringList& streams,
105 const QString& reference) : _ephUser(true) {
106 _dirName = dirName;
107 _reference = reference.isEmpty() ? QString("CONSENSUS") : reference;
108 _interval = interval > 0.0 ? interval : 3600.0;
109 // "ALL" or a list of streams; streams with a leading '!' are excluded
110 // (from the reports and from the consensus)
111 bool all = false;
112 for (const QString& sta : streams) {
113 if (sta.startsWith('!')) {
114 _excluded.insert(sta.mid(1));
115 }
116 else if (sta.toUpper() == "ALL") {
117 all = true;
118 }
119 else {
120 _streams.insert(sta);
121 }
122 }
123 if (all) {
124 _streams.clear();
125 }
126
127 connect(this, SIGNAL(newMessage(QByteArray,bool)), BNC_CORE, SLOT(slotMessage(const QByteArray,bool)));
128 connect(BNC_CORE, SIGNAL(newOrbCorrections(QList<t_orbCorr>)),
129 this, SLOT(slotNewOrbCorrections(QList<t_orbCorr>)), Qt::DirectConnection);
130 connect(BNC_CORE, SIGNAL(newClkCorrections(QList<t_clkCorr>)),
131 this, SLOT(slotNewClkCorrections(QList<t_clkCorr>)), Qt::DirectConnection);
132 connect(BNC_CORE, SIGNAL(newCodeBiases(QList<t_satCodeBias>)),
133 this, SLOT(slotNewCodeBiases(QList<t_satCodeBias>)), Qt::DirectConnection);
134 connect(BNC_CORE, SIGNAL(newPhaseBiases(QList<t_satPhaseBias>)),
135 this, SLOT(slotNewPhaseBiases(QList<t_satPhaseBias>)), Qt::DirectConnection);
136 // combined corrections (stream INTERNAL)
137 if (bncComb::exists()) {
138 connect(BNC_CMB, SIGNAL(newOrbCorrections(QList<t_orbCorr>)),
139 this, SLOT(slotNewOrbCorrections(QList<t_orbCorr>)), Qt::DirectConnection);
140 connect(BNC_CMB, SIGNAL(newClkCorrections(QList<t_clkCorr>)),
141 this, SLOT(slotNewClkCorrections(QList<t_clkCorr>)), Qt::DirectConnection);
142 }
143}
144
145// Stream selected for the QC
146////////////////////////////////////////////////////////////////////////////
147bool t_ssrQc::selected(const string& staID) const {
148 QString sta = QString::fromStdString(staID);
149 return !_excluded.contains(sta) && (_streams.isEmpty() || _streams.contains(sta));
150}
151
152// Analysis center of a stream: the 3 characters of the AC in mountpoints
153// like SSRA00CNE1, otherwise the stream name itself
154////////////////////////////////////////////////////////////////////////////
155QString t_ssrQc::acOf(const QString& staID) {
156 static const QRegularExpression re("^SSR[AC][0-9]{2}([A-Z0-9]{3})[0-9]$");
157 QRegularExpressionMatch mm = re.match(staID);
158 return mm.hasMatch() ? mm.captured(1) : staID;
159}
160
161// Stream part of the consensus: not excluded, and not a combination product
162// (INTERNAL, IGS combinations), as these are derived from the other streams
163////////////////////////////////////////////////////////////////////////////
164bool t_ssrQc::votes(const QString& staID) const {
165 return !_excluded.contains(staID) && staID != "INTERNAL" && acOf(staID) != "IGS";
166}
167
168// Daily QC file of a stream: <staID>_S_<YYYYDDD>0000_01D_QC.txt
169////////////////////////////////////////////////////////////////////////////
170ofstream& t_ssrQc::streamOut(const QString& staID, const bncTime& time) {
171 unsigned year, month, day;
172 time.civil_date(year, month, day);
173 QDate date(year, month, day);
174 QString name = QDir(_dirName).filePath(QString("%1_S_%2%3%4_01D_QC.txt").arg(staID)
175 .arg(year).arg(date.dayOfYear(), 3, 10, QChar('0')).arg("0000"));
176 if (_outNames.value(staID) != name) {
177 delete _outs.value(staID, 0);
178 bool exists = QFile::exists(name);
179 ofstream* out = new ofstream(name.toLocal8Bit().data(), ios_base::out | ios_base::app);
180 if (!exists) {
181 *out << "SSR QC of stream " << staID.toStdString() << ", report interval " << _interval << " s" << endl
182 << "Checks: epochs and update intervals, gaps (interval > 2 x max(declared, median)), latency,"
183 << " completeness w.r.t. healthy broadcast ephemerides," << endl
184 << "IOD matching, code and phase bias signals, code biases of the reference signals, clock jumps > "
185 << MAX_CLK_JUMP << " m, largest orbit corrections" << endl;
186 }
187 _outs[staID] = out;
188 _outNames[staID] = name;
189 }
190 return *_outs[staID];
191}
192
193// Destructor
194////////////////////////////////////////////////////////////////////////////
195t_ssrQc::~t_ssrQc() {
196 QMutexLocker locker(&_mutex);
197 writeReport(true);
198 for (ofstream* out : _outs) {
199 delete out;
200 }
201}
202
203// Key of the epoch statistics
204////////////////////////////////////////////////////////////////////////////
205QString t_ssrQc::key(const string& staID, char sys, e_type type) {
206 return QString("%1 %2 %3").arg(staID.c_str()).arg(sys).arg(int(type));
207}
208
209// Reference signals (IGS conventions, as in the combination)
210////////////////////////////////////////////////////////////////////////////
211bool t_ssrQc::refSignals(char sys, QString& sig1, QString& sig2,
212 t_frequency::type& frq1, t_frequency::type& frq2) {
213 switch (sys) {
214 case 'G': sig1 = "1W"; sig2 = "2W"; frq1 = t_frequency::G1; frq2 = t_frequency::G2; return true;
215 case 'R': sig1 = "1P"; sig2 = "2P"; frq1 = t_frequency::R1; frq2 = t_frequency::R2; return true;
216 case 'E': sig1 = "1C"; sig2 = "5Q"; frq1 = t_frequency::E1; frq2 = t_frequency::E5; return true;
217 case 'C': sig1 = "2I"; sig2 = "6I"; frq1 = t_frequency::C2; frq2 = t_frequency::C6; return true;
218 case 'J': sig1 = "1C"; sig2 = "2L"; frq1 = t_frequency::J1; frq2 = t_frequency::J2; return true;
219 }
220 return false;
221}
222
223// Start a new report interval if the epoch is beyond the current one
224////////////////////////////////////////////////////////////////////////////
225void t_ssrQc::checkInterval(const bncTime& time) {
226 if (!_intervalStart.valid()) {
227 double sec = floor(time.gpssec() / _interval) * _interval;
228 _intervalStart = bncTime(time.gpsw(), sec);
229 }
230 // stream comparison of the epochs already complete for all streams
231 if (!_nextCmpEpoch.valid()) {
232 double sec = ceil(time.gpssec() / CMP_SAMPL) * CMP_SAMPL;
233 _nextCmpEpoch = bncTime(time.gpsw(), sec);
234 }
235 while (time - _nextCmpEpoch >= CMP_DELAY) {
236 compareEpoch(_nextCmpEpoch);
237 _nextCmpEpoch = _nextCmpEpoch + CMP_SAMPL;
238 }
239 if (time - _intervalStart >= _interval) {
240 writeReport(false);
241 double sec = floor(time.gpssec() / _interval) * _interval;
242 _intervalStart = bncTime(time.gpsw(), sec);
243 }
244 if (!_lastTime.valid() || _lastTime < time) {
245 _lastTime = time;
246 }
247}
248
249// Epoch statistics
250////////////////////////////////////////////////////////////////////////////
251void t_ssrQc::newEpoch(const string& staID, char sys, e_type type, const bncTime& time,
252 unsigned updateInt, const QString& prn) {
253 QString sta = QString::fromStdString(staID);
254 if (!_staStart.contains(sta)) {
255 _staStart[sta] = time;
256 }
257 if (!_staLast.contains(sta) || _staLast[sta] < time) {
258 _staLast[sta] = time;
259 }
260 t_epoStat& stat = _epoStat[key(staID, sys, type)];
261 if (updateInt < unsigned(ssrUpdateInt.size())) {
262 stat.nominal = ssrUpdateInt[updateInt];
263 }
264 stat.sats.insert(prn);
265 if (!stat.last.valid() || stat.last < time) {
266 if (stat.last.valid()) {
267 stat.intervals.push_back(time - stat.last);
268 }
269 stat.last = time;
270 stat.numEpo += 1;
271 stat.latencies.push_back(currentTime() - time);
272 }
273 else if (time < stat.last) {
274 stat.numOutOfOrder += 1;
275 }
276}
277
278// Broadcast ephemeris the correction refers to
279////////////////////////////////////////////////////////////////////////////
280void t_ssrQc::checkEph(t_satStat& stat, const QString& prnInt, unsigned iod, const bncTime& time,
281 const string& staID) {
282 // not before all broadcast ephemerides may have been received
283 if (time - _staStart[QString::fromStdString(staID)] < EPH_WARMUP) {
284 return;
285 }
286 t_eph* ephLast = _ephUser.ephLast(prnInt);
287 t_eph* ephPrev = _ephUser.ephPrev(prnInt);
288 t_eph* eph = 0;
289 if (ephLast && ephLast->IOD() == iod) eph = ephLast;
290 else if (ephPrev && ephPrev->IOD() == iod) eph = ephPrev;
291 if (!ephLast) {
292 stat.noEph += 1;
293 }
294 else if (!eph) {
295 stat.iodUnknown += 1;
296 }
297 else if (eph->checkState() == t_eph::unhealthy || eph->checkState() == t_eph::bad) {
298 stat.ephUnhealthy += 1;
299 }
300}
301
302// Orbit corrections
303////////////////////////////////////////////////////////////////////////////
304void t_ssrQc::slotNewOrbCorrections(QList<t_orbCorr> orbCorrections) {
305 QMutexLocker locker(&_mutex);
306 for (const t_orbCorr& corr : orbCorrections) {
307 if (!_excluded.contains(QString::fromStdString(corr._staID)) || corr._staID == _reference.toStdString()) {
308 storeHist(&corr, 0);
309 }
310 if (!selected(corr._staID)) continue;
311 QString prn = QString::fromStdString(corr._prn.toString());
312 checkInterval(corr._time);
313 newEpoch(corr._staID, corr._prn.system(), ORB, corr._time, corr._updateInt, prn);
314 t_satStat& stat = _satStat[QString::fromStdString(corr._staID) + ' ' + prn];
315 stat.numOrb += 1;
316 checkEph(stat, QString::fromStdString(corr._prn.toInternalString()), corr._iod, corr._time, corr._staID);
317 double radial = fabs(corr._xr[0]);
318 double alongCross = max(fabs(corr._xr[1]), fabs(corr._xr[2]));
319 stat.maxRadial = max(stat.maxRadial, radial);
320 stat.maxAlongCross = max(stat.maxAlongCross, alongCross);
321 }
322}
323
324// Clock corrections
325////////////////////////////////////////////////////////////////////////////
326void t_ssrQc::slotNewClkCorrections(QList<t_clkCorr> clkCorrections) {
327 QMutexLocker locker(&_mutex);
328 for (const t_clkCorr& corr : clkCorrections) {
329 if (!_excluded.contains(QString::fromStdString(corr._staID)) || corr._staID == _reference.toStdString()) {
330 storeHist(0, &corr);
331 }
332 if (!selected(corr._staID)) continue;
333 QString prn = QString::fromStdString(corr._prn.toString());
334 checkInterval(corr._time);
335 newEpoch(corr._staID, corr._prn.system(), CLK, corr._time, corr._updateInt, prn);
336 t_satStat& stat = _satStat[QString::fromStdString(corr._staID) + ' ' + prn];
337 stat.numClk += 1;
338 checkEph(stat, QString::fromStdString(corr._prn.toInternalString()), corr._iod, corr._time, corr._staID);
339
340 // jump: change between consecutive epochs (same IOD) beyond the clock rate
341 double clk = corr._dClk * t_CST::c;
342 if (stat.lastClkTime.valid() && stat.lastIod == corr._iod) {
343 double dt = corr._time - stat.lastClkTime;
344 int nominal = (corr._updateInt < unsigned(ssrUpdateInt.size())) ? ssrUpdateInt[corr._updateInt] : 0;
345 if (dt > 0.0 && dt <= max(2.0 * nominal, 60.0)) {
346 double jump = fabs(clk - stat.lastClk - corr._dotDClk * t_CST::c * dt);
347 stat.maxJump = max(stat.maxJump, jump);
348 if (jump > MAX_CLK_JUMP) {
349 stat.clkJumps += 1;
350 }
351 }
352 }
353 stat.lastClkTime = corr._time;
354 stat.lastIod = corr._iod;
355 stat.lastClk = clk;
356 }
357}
358
359// Code biases
360////////////////////////////////////////////////////////////////////////////
361void t_ssrQc::slotNewCodeBiases(QList<t_satCodeBias> codeBiases) {
362 QMutexLocker locker(&_mutex);
363 for (const t_satCodeBias& bias : codeBiases) {
364 bool sel = selected(bias._staID);
365 if (_excluded.contains(QString::fromStdString(bias._staID)) && bias._staID != _reference.toStdString()) continue;
366 char sys = bias._prn.system();
367 QString prn = QString::fromStdString(bias._prn.toString());
368 if (sel) {
369 checkInterval(bias._time);
370 newEpoch(bias._staID, sys, CBIAS, bias._time, bias._updateInt, prn);
371 }
372 QString staSys = QString("%1 %2").arg(bias._staID.c_str()).arg(sys);
373 QMap<QString, double> values;
374 for (const t_frqCodeBias& frqBias : bias._bias) {
375 QString sig = QString::fromStdString(frqBias._rnxType2ch);
376 _codeSignals[staSys].insert(sig);
377 values[sig] = frqBias._value;
378 }
379 t_satStat& stat = _satStat[QString::fromStdString(bias._staID) + ' ' + prn];
380 QString sig1, sig2;
381 t_frequency::type frq1, frq2;
382 if (refSignals(sys, sig1, sig2, frq1, frq2)) {
383 stat.missingRefSig.clear();
384 if (!values.contains(sig1)) stat.missingRefSig.insert("C" + sig1);
385 if (!values.contains(sig2)) stat.missingRefSig.insert("C" + sig2);
386 stat.refBiasOK = stat.missingRefSig.isEmpty();
387 if (stat.refBiasOK) {
388 double channel = 0.0;
389 if (sys == 'R') {
390 t_eph* eph = _ephUser.ephLast(QString::fromStdString(bias._prn.toInternalString()));
391 if (!eph) {
392 stat.refBiasOK = false;
393 continue;
394 }
395 channel = eph->slotNum();
396 }
397 double f1 = t_CST::freq(frq1, channel);
398 double f2 = t_CST::freq(frq2, channel);
399 stat.biasIF = (f1 * f1 * values[sig1] - f2 * f2 * values[sig2]) / (f1 * f1 - f2 * f2);
400 }
401 }
402 }
403}
404
405// Phase biases
406////////////////////////////////////////////////////////////////////////////
407void t_ssrQc::slotNewPhaseBiases(QList<t_satPhaseBias> phaseBiases) {
408 QMutexLocker locker(&_mutex);
409 for (const t_satPhaseBias& bias : phaseBiases) {
410 if (!selected(bias._staID)) continue;
411 checkInterval(bias._time);
412 newEpoch(bias._staID, bias._prn.system(), PBIAS, bias._time, bias._updateInt,
413 QString::fromStdString(bias._prn.toString()));
414 QString staSys = QString("%1 %2").arg(bias._staID.c_str()).arg(bias._prn.system());
415 for (const t_frqPhaseBias& frqBias : bias._bias) {
416 _phaseSignals[staSys].insert(QString::fromStdString(frqBias._rnxType2ch));
417 }
418 }
419}
420
421// Reference point of the orbits of a stream: APC (SSRA...), CoM (SSRC...);
422// the combined stream INTERNAL refers to the APC
423////////////////////////////////////////////////////////////////////////////
424char t_ssrQc::refPoint(const QString& staID) const {
425 if (staID == "INTERNAL") {
426 return 'A'; // the combined corrections always refer to the APC
427 }
428 const QString& sta = staID;
429 if (sta.startsWith("SSRA")) return 'A';
430 if (sta.startsWith("SSRC")) return 'C';
431 return '?';
432}
433
434// Keep the recent orbit and clock corrections of a stream
435////////////////////////////////////////////////////////////////////////////
436void t_ssrQc::storeHist(const t_orbCorr* orb, const t_clkCorr* clk) {
437 if (orb) {
438 QMap<double, t_orbCorr>& hist = _orbHist[QString::fromStdString(orb->_staID)]
439 [QString::fromStdString(orb->_prn.toInternalString())];
440 hist[tKey(orb->_time)] = *orb;
441 while (!hist.isEmpty() && hist.firstKey() < tKey(orb->_time) - HIST_LENGTH) {
442 hist.erase(hist.begin());
443 }
444 }
445 if (clk) {
446 QMap<double, t_clkCorr>& hist = _clkHist[QString::fromStdString(clk->_staID)]
447 [QString::fromStdString(clk->_prn.toInternalString())];
448 hist[tKey(clk->_time)] = *clk;
449 while (!hist.isEmpty() && hist.firstKey() < tKey(clk->_time) - HIST_LENGTH) {
450 hist.erase(hist.begin());
451 }
452 }
453}
454
455// Compare the orbits and clocks of all streams at epoch tt
456////////////////////////////////////////////////////////////////////////////
457void t_ssrQc::compareEpoch(const bncTime& tt) {
458 double tk = tKey(tt);
459
460 // Satellite positions and clocks of all streams (latest corrections <= tt)
461 QMap<QString, QMap<QString, t_satCrd> > crd; // stream, PRN (external)
462 QMapIterator<QString, QMap<QString, QMap<double, t_clkCorr> > > itSta(_clkHist);
463 while (itSta.hasNext()) {
464 itSta.next();
465 const QString& staID = itSta.key();
466 QMapIterator<QString, QMap<double, t_clkCorr> > itPrn(itSta.value());
467 while (itPrn.hasNext()) {
468 itPrn.next();
469 const QString& prnInt = itPrn.key();
470 QMap<double, t_clkCorr>::const_iterator itC = itPrn.value().upperBound(tk);
471 if (itC == itPrn.value().constBegin()) {
472 continue;
473 }
474 const t_clkCorr& clk = (--itC).value();
475 const QMap<double, t_orbCorr>& orbHist = _orbHist[staID][prnInt];
476 QMap<double, t_orbCorr>::const_iterator itO = orbHist.upperBound(tk);
477 if (itO == orbHist.constBegin()) {
478 continue;
479 }
480 const t_orbCorr& orb = (--itO).value();
481 t_eph* eph = 0;
482 t_eph* ephLast = _ephUser.ephLast(prnInt);
483 t_eph* ephPrev = _ephUser.ephPrev(prnInt);
484 if (ephLast && ephLast->IOD() == orb._iod) eph = ephLast;
485 else if (ephPrev && ephPrev->IOD() == orb._iod) eph = ephPrev;
486 if (!eph || clk._iod != orb._iod) {
487 continue;
488 }
489 eph->setOrbCorr(&orb);
490 eph->setClkCorr(&clk);
491 ColumnVector xc(6), vv(3);
492 if (eph->getCrd(tt, xc, vv, true) != success) {
493 continue;
494 }
495 t_satCrd& sc = crd[staID][QString::fromStdString(clk._prn.toString())];
496 sc.xx = xc.Rows(1,3);
497 sc.vv = vv;
498 sc.clk = xc(4) * t_CST::c;
499 sc.refPoint = refPoint(staID);
500 // clock referred to the reference signals (as in the combination):
501 // + B_IF of the stream's code biases; the combination (INTERNAL)
502 // refers to them already
503 sc.clkOK = (staID == "INTERNAL");
504 QString satKey = staID + ' ' + QString::fromStdString(clk._prn.toString());
505 if (!sc.clkOK && _satStat.contains(satKey) && _satStat[satKey].refBiasOK) {
506 sc.clk += _satStat[satKey].biasIF;
507 sc.clkOK = true;
508 }
509 }
510 }
511
512 // Satellites per system
513 QMap<char, QSet<QString> > satsSys;
514 QMapIterator<QString, QMap<QString, t_satCrd> > itCrd(crd);
515 while (itCrd.hasNext()) {
516 itCrd.next();
517 for (const QString& prn : itCrd.value().keys()) {
518 satsSys[prn[0].toLatin1()].insert(prn);
519 }
520 }
521
522 QMapIterator<char, QSet<QString> > itSys(satsSys);
523 while (itSys.hasNext()) {
524 itSys.next();
525 const QSet<QString>& sats = itSys.value();
526 QStringList staList;
527 for (const QString& staID : crd.keys()) {
528 for (const QString& prn : sats) {
529 if (crd[staID].contains(prn)) {
530 staList << staID;
531 break;
532 }
533 }
534 }
535
536 // Consensus: one value per AC (median of its streams), so that an AC
537 // with several streams (formats, APC/CoM) has one vote; the streams of
538 // the AC compared are left out (leave-one-AC-out)
539 auto acClocks = [&](const QString& prn, const QMap<QString, double>& off, bool useOff) {
540 QMap<QString, QVector<double> > perAC;
541 for (const QString& oo : staList) {
542 if (!votes(oo) || !crd[oo].contains(prn) || !crd[oo][prn].clkOK) continue;
543 if (useOff && !off.contains(oo)) continue;
544 perAC[acOf(oo)] << crd[oo][prn].clk - (useOff ? off[oo] : 0.0);
545 }
546 QMap<QString, double> res;
547 for (const QString& ac : perAC.keys()) res[ac] = median(perAC[ac]);
548 return res;
549 };
550
551 // Clock datum: offset of each stream w.r.t. the per-satellite consensus
552 // of all ACs (or w.r.t. the reference stream)
553 QMap<QString, double> offset;
554 QMap<QString, double> noOff;
555 QMap<QString, QMap<QString, double> > rawAC; // PRN, AC
556 if (_reference == "CONSENSUS") {
557 for (const QString& prn : sats) rawAC[prn] = acClocks(prn, noOff, false);
558 }
559 for (const QString& staID : staList) {
560 QVector<double> diffs;
561 for (const QString& prn : sats) {
562 if (!crd[staID].contains(prn) || !crd[staID][prn].clkOK) continue;
563 if (_reference == "CONSENSUS") {
564 if (rawAC[prn].size() >= 3) diffs << crd[staID][prn].clk - median(rawAC[prn].values().toVector());
565 }
566 else if (crd.value(_reference).contains(prn) && crd[_reference][prn].clkOK) {
567 diffs << crd[staID][prn].clk - crd[_reference][prn].clk;
568 }
569 }
570 if (!diffs.isEmpty()) {
571 offset[staID] = median(diffs);
572 }
573 }
574 QMap<QString, QMap<QString, double> > alignedAC; // PRN, AC
575 if (_reference == "CONSENSUS") {
576 for (const QString& prn : sats) alignedAC[prn] = acClocks(prn, offset, true);
577 }
578
579 // Differences of each selected stream to its reference
580 for (const QString& staID : staList) {
581 if (!selected(staID.toStdString()) || staID == _reference) {
582 continue;
583 }
584 const QString ownAC = acOf(staID);
585 QMap<QString, double> dClk;
586 QMap<QString, ColumnVector> dRac;
587 for (const QString& prn : sats) {
588 if (!crd[staID].contains(prn)) continue;
589 const t_satCrd& sc = crd[staID][prn];
590 QVector<double> clks, xs[3];
591 if (_reference == "CONSENSUS") {
592 QMapIterator<QString, double> itA(alignedAC[prn]);
593 while (itA.hasNext()) {
594 itA.next();
595 if (itA.key() != ownAC) clks << itA.value();
596 }
597 // orbits: per AC the median of its streams with the same reference point
598 QMap<QString, QVector<double> > perAC[3];
599 for (const QString& oo : staList) {
600 if (!votes(oo) || acOf(oo) == ownAC || !crd[oo].contains(prn)) continue;
601 const t_satCrd& rc = crd[oo][prn];
602 if (rc.refPoint != sc.refPoint || sc.refPoint == '?') continue;
603 for (int ii = 0; ii < 3; ii++) perAC[ii][acOf(oo)] << rc.xx[ii];
604 }
605 for (const QString& ac : perAC[0].keys()) {
606 for (int ii = 0; ii < 3; ii++) xs[ii] << median(perAC[ii][ac]);
607 }
608 if (clks.size() < 2) clks.clear();
609 if (xs[0].size() < 2) for (int ii = 0; ii < 3; ii++) xs[ii].clear();
610 }
611 else {
612 if (!crd.value(_reference).contains(prn)) continue;
613 const t_satCrd& rc = crd[_reference][prn];
614 if (rc.clkOK) clks << rc.clk;
615 if (rc.refPoint == sc.refPoint && sc.refPoint != '?') {
616 for (int ii = 0; ii < 3; ii++) xs[ii] << rc.xx[ii];
617 }
618 }
619 if (sc.clkOK && offset.contains(staID) && !clks.isEmpty()) {
620 dClk[prn] = sc.clk - offset[staID] - median(clks);
621 }
622 else if (!sc.clkOK) {
623 _cmpStat[staID][prn].clkNotReferred += 1;
624 }
625 if (!xs[0].isEmpty()) {
626 ColumnVector dx(3);
627 for (int ii = 0; ii < 3; ii++) dx[ii] = sc.xx[ii] - median(xs[ii]);
628 ColumnVector er = sc.xx / sc.xx.NormFrobenius();
629 ColumnVector ec = crossproduct(sc.xx, sc.vv);
630 ec /= ec.NormFrobenius();
631 ColumnVector ea = crossproduct(ec, er);
632 ColumnVector rac(3);
633 rac[0] = DotProduct(dx, er);
634 rac[1] = DotProduct(dx, ea);
635 rac[2] = DotProduct(dx, ec);
636 dRac[prn] = rac;
637 }
638 }
639 if (dClk.isEmpty() && dRac.isEmpty()) {
640 continue;
641 }
642 // remaining common clock offset of this stream and epoch
643 double off = dClk.isEmpty() ? 0.0 : median(dClk.values().toVector());
644 _cmpEpochs[staID] += 1;
645 QSet<QString> prns;
646 for (const QString& prn : dClk.keys()) prns.insert(prn);
647 for (const QString& prn : dRac.keys()) prns.insert(prn);
648 for (const QString& prn : prns) {
649 t_cmpStat& stat = _cmpStat[staID][prn];
650 if (dRac.contains(prn)) {
651 const ColumnVector& rac = dRac[prn];
652 stat.numOrb += 1;
653 stat.sR += rac[0] * rac[0];
654 stat.sA += rac[1] * rac[1];
655 stat.sC += rac[2] * rac[2];
656 }
657 if (dClk.contains(prn)) {
658 double dc = dClk[prn] - off;
659 stat.numClk += 1;
660 stat.sClk += dc * dc;
661 stat.mClk += dc;
662 if (dRac.contains(prn)) {
663 double sis = dRac[prn][0] - dc;
664 stat.numSis += 1;
665 stat.sSis += sis * sis;
666 stat.mSis += sis;
667 }
668 }
669 }
670 }
671 }
672}
673
674// Write the report of the current interval and reset the statistics
675////////////////////////////////////////////////////////////////////////////
676void t_ssrQc::writeReport(bool final) {
677 if (_epoStat.isEmpty()) {
678 return;
679 }
680 bncTime endTime = final ? _lastTime : _intervalStart + _interval;
681
682 // Healthy and current broadcast ephemerides per system
683 QMap<char, QSet<QString> > ephSats;
684 for (const QString& prnInt : _ephUser.prnList()) {
685 t_eph* eph = _ephUser.ephLast(prnInt);
686 if (eph && eph->checkState() == t_eph::ok && !outDatedBcep(eph, endTime)) {
687 ephSats[eph->prn().system()].insert(QString::fromStdString(eph->prn().toString()));
688 }
689 }
690
691 // Streams and systems
692 QMap<QString, QSet<char> > streams;
693 QMapIterator<QString, t_epoStat> itEpo(_epoStat);
694 while (itEpo.hasNext()) {
695 itEpo.next();
696 QStringList hlp = itEpo.key().split(' ');
697 streams[hlp[0]].insert(hlp[1][0].toLatin1());
698 }
699
700 QMapIterator<QString, QSet<char> > itSta(streams);
701 while (itSta.hasNext()) {
702 itSta.next();
703 const QString& staID = itSta.key();
704 QList<char> systems = itSta.value().values();
705 std::sort(systems.begin(), systems.end());
706 ofstream& out = streamOut(staID, _intervalStart);
707 if (final) {
708 endTime = _staLast.value(staID, _lastTime);
709 }
710 out << endl << "=== SSR QC " << staID.toStdString() << ' '
711 << _intervalStart.datestr() << ' ' << _intervalStart.timestr(0)
712 << " - " << endTime.datestr() << ' ' << endTime.timestr(0)
713 << (final ? " (end of data)" : "") << " ===" << endl
714 << " Sys Type Epochs Interval nom/med/max [s] Gaps Out-of-order"
715 << " Latency med/max [s] Sats" << endl;
716 for (char sys : systems) {
717 for (int type = ORB; type <= PBIAS; type++) {
718 QString kk = QString("%1 %2 %3").arg(staID).arg(sys).arg(type);
719 if (!_epoStat.contains(kk)) {
720 continue;
721 }
722 const t_epoStat& stat = _epoStat[kk];
723 double medInt = median(stat.intervals);
724 double maxInt = stat.intervals.isEmpty() ? 0.0 :
725 *std::max_element(stat.intervals.begin(), stat.intervals.end());
726 double ref = max(double(stat.nominal), medInt);
727 unsigned gaps = 0;
728 for (double dt : stat.intervals) {
729 if (ref > 0.0 && dt > 2.0 * ref) {
730 ++gaps;
731 }
732 }
733 double maxLat = stat.latencies.isEmpty() ? 0.0 :
734 *std::max_element(stat.latencies.begin(), stat.latencies.end());
735 out << " " << sys << " " << left << setw(10) << TYPE_NAME[type] << right
736 << setw(8) << stat.numEpo
737 << setw(10) << stat.nominal << " /" << setw(6) << setprecision(1) << fixed << medInt
738 << " /" << setw(7) << maxInt
739 << setw(6) << gaps << setw(14) << stat.numOutOfOrder
740 << setw(10) << median(stat.latencies) << " /" << setw(7) << maxLat
741 << setw(7) << stat.sats.size() << endl;
742 }
743 }
744
745 // Per system: completeness, ephemeris, biases, plausibility
746 for (char sys : systems) {
747 QMap<QString, unsigned> missOrb, missClk, missCB, missPB, iodUnknown, unhealthy, noEph,
748 jumps, missRef;
749 double maxRadial = 0.0, maxAlongCross = 0.0;
750 QString maxRadialPrn, maxAlongCrossPrn;
751 QVector<double> biasIF;
752 const QSet<QString>& ephs = ephSats[sys];
753 for (int type = ORB; type <= PBIAS; type++) {
754 QString kk = QString("%1 %2 %3").arg(staID).arg(sys).arg(type);
755 if (!_epoStat.contains(kk)) {
756 continue;
757 }
758 QMap<QString, unsigned>& miss = (type == ORB) ? missOrb : (type == CLK) ? missClk :
759 (type == CBIAS) ? missCB : missPB;
760 for (const QString& prn : ephs) {
761 if (!_epoStat[kk].sats.contains(prn)) {
762 miss[prn] = 0;
763 }
764 }
765 }
766 QMapIterator<QString, t_satStat> itSat(_satStat);
767 while (itSat.hasNext()) {
768 itSat.next();
769 QStringList hlp = itSat.key().split(' ');
770 if (hlp[0] != staID || hlp[1][0].toLatin1() != sys) {
771 continue;
772 }
773 const QString& prn = hlp[1];
774 const t_satStat& stat = itSat.value();
775 if (stat.iodUnknown) iodUnknown[prn] = stat.iodUnknown;
776 if (stat.ephUnhealthy) unhealthy[prn] = stat.ephUnhealthy;
777 if (stat.noEph) noEph[prn] = stat.noEph;
778 if (stat.clkJumps) jumps[prn] = stat.clkJumps;
779 if (stat.maxRadial > maxRadial) {
780 maxRadial = stat.maxRadial;
781 maxRadialPrn = prn;
782 }
783 if (stat.maxAlongCross > maxAlongCross) {
784 maxAlongCross = stat.maxAlongCross;
785 maxAlongCrossPrn = prn;
786 }
787 if (!stat.missingRefSig.isEmpty()) {
788 QStringList sigs = stat.missingRefSig.values();
789 sigs.sort();
790 missRef[prn + "[" + sigs.join("/") + "]"] = 0;
791 }
792 if (stat.refBiasOK) {
793 biasIF.push_back(stat.biasIF);
794 }
795 }
796 // only findings are listed
797 QStringList lines;
798 for (int type = ORB; type <= PBIAS; type++) {
799 QString kk = QString("%1 %2 %3").arg(staID).arg(sys).arg(type);
800 if (_epoStat.contains(kk)) {
801 const t_epoStat& stat = _epoStat[kk];
802 double medInt = median(stat.intervals);
803 if (stat.nominal > 0 && stat.intervals.size() > 1 &&
804 (medInt > 1.5 * stat.nominal || medInt < stat.nominal / 1.5)) {
805 lines << QString("%1: declared update interval %2 s, actual %3 s")
806 .arg(TYPE_NAME[type]).arg(stat.nominal).arg(medInt, 0, 'f', 1);
807 }
808 }
809 }
810 auto addSats = [&lines](const QString& text, const QMap<QString, unsigned>& sats) {
811 if (!sats.isEmpty()) {
812 lines << text + ":" + satList(sats);
813 }
814 };
815 addSats("orbit corrections missing (healthy broadcast ephemeris available)", missOrb);
816 addSats("clock corrections missing (healthy broadcast ephemeris available)", missClk);
817 addSats("code biases missing (healthy broadcast ephemeris available)", missCB);
818 addSats("phase biases missing (healthy broadcast ephemeris available)", missPB);
819 addSats("corrections referring to an IOD of no received broadcast ephemeris", iodUnknown);
820 addSats("corrections for unhealthy broadcast ephemeris", unhealthy);
821 addSats("corrections for satellites without broadcast ephemeris", noEph);
822 QString staSys = QString("%1 %2").arg(staID).arg(sys);
823 if (_codeSignals.contains(staSys)) {
824 QStringList sigs = _codeSignals[staSys].values();
825 sigs.sort();
826 lines << "code bias signals: " + sigs.join(" ");
827 addSats("code biases of the reference signals missing", missRef);
828 if (biasIF.size() > 1) {
829 double mean = 0.0;
830 for (double bb : biasIF) mean += bb / biasIF.size();
831 double var = 0.0;
832 for (double bb : biasIF) var += (bb - mean) * (bb - mean) / biasIF.size();
833 double maxAbs = max(fabs(*std::min_element(biasIF.begin(), biasIF.end())),
834 fabs(*std::max_element(biasIF.begin(), biasIF.end())));
835 lines << QString("ionosphere-free combination B_IF of the reference signal biases: "
836 "std. dev. %1 m, max |B_IF| %2 m (%3 satellites)")
837 .arg(sqrt(var), 0, 'f', 3).arg(maxAbs, 0, 'f', 3).arg(biasIF.size());
838 }
839 }
840 if (_phaseSignals.contains(staSys)) {
841 QStringList sigs = _phaseSignals[staSys].values();
842 sigs.sort();
843 lines << "phase bias signals: " + sigs.join(" ");
844 }
845 addSats(QString("clock jumps > %1 m").arg(MAX_CLK_JUMP), jumps);
846 if (!maxRadialPrn.isEmpty()) {
847 lines << QString("largest orbit corrections: radial %1 m (%2), along/cross %3 m (%4)")
848 .arg(maxRadial, 0, 'f', 2).arg(maxRadialPrn).arg(maxAlongCross, 0, 'f', 2).arg(maxAlongCrossPrn);
849 }
850 // stream comparison
851 if (_cmpStat.contains(staID)) {
852 unsigned nSat = 0, nClk = 0, nOrb = 0, nSis = 0;
853 double sClk = 0.0, sR = 0.0, sA = 0.0, sC = 0.0, sSis = 0.0;
854 double vClk = 0.0, vSis = 0.0; // sums of squares about the satellite means
855 QMap<QString, double> rmsSis, rmsClk, rmsOrb;
856 QMap<QString, unsigned> notReferred;
857 QMapIterator<QString, t_cmpStat> itC(_cmpStat[staID]);
858 while (itC.hasNext()) {
859 itC.next();
860 if (itC.key()[0].toLatin1() != sys) continue;
861 const t_cmpStat& st = itC.value();
862 if (st.numClk == 0 && st.numOrb == 0) {
863 if (st.clkNotReferred) notReferred[itC.key()] = 0;
864 continue;
865 }
866 ++nSat;
867 if (st.clkNotReferred) notReferred[itC.key()] = 0;
868 nClk += st.numClk; sClk += st.sClk;
869 nOrb += st.numOrb; sR += st.sR; sA += st.sA; sC += st.sC;
870 nSis += st.numSis; sSis += st.sSis;
871 if (st.numClk) vClk += st.sClk - st.mClk * st.mClk / st.numClk;
872 if (st.numSis) vSis += st.sSis - st.mSis * st.mSis / st.numSis;
873 if (st.numSis) rmsSis[itC.key()] = sqrt(st.sSis / st.numSis);
874 else if (st.numClk) rmsClk[itC.key()] = sqrt(st.sClk / st.numClk);
875 if (st.numOrb) rmsOrb[itC.key()] = sqrt((st.sR + st.sA + st.sC) / st.numOrb);
876 }
877 if (nSat) {
878 QString refText = (_reference == "CONSENSUS") ? QString("consensus of the other ACs")
879 : QString("stream %1").arg(_reference);
880 QStringList parts;
881 if (nClk) parts << QString("clock %1 (STD %2)").arg(sqrt(sClk / nClk), 0, 'f', 3)
882 .arg(sqrt(max(0.0, vClk) / nClk), 0, 'f', 3);
883 if (nOrb) parts << QString("radial %1, along %2, cross %3")
884 .arg(sqrt(sR / nOrb), 0, 'f', 3).arg(sqrt(sA / nOrb), 0, 'f', 3)
885 .arg(sqrt(sC / nOrb), 0, 'f', 3);
886 if (nSis) parts << QString("SIS (radial - clock) %1 (STD %2)").arg(sqrt(sSis / nSis), 0, 'f', 3)
887 .arg(sqrt(max(0.0, vSis) / nSis), 0, 'f', 3);
888 lines << QString("comparison with %1 (%2 satellites), RMS (STD about the satellite means) [m]: %3")
889 .arg(refText).arg(nSat).arg(parts.join(", "));
890 if (!nOrb) {
891 lines << "orbits not compared: no other stream with the same reference point (APC/CoM)";
892 }
893 addSats("clocks not compared (code biases of the reference signals missing)", notReferred);
894 auto deviating = [&lines](const QString& what, const QMap<QString, double>& rms) {
895 if (rms.size() < 2) return;
896 double med = median(rms.values().toVector());
897 QStringList dev;
898 QMapIterator<QString, double> itR(rms);
899 while (itR.hasNext()) {
900 itR.next();
901 if (itR.value() > max(DEV_MIN, DEV_FACTOR * med)) {
902 dev << QString("%1(%2)").arg(itR.key()).arg(itR.value(), 0, 'f', 2);
903 }
904 }
905 if (!dev.isEmpty()) {
906 lines << QString("satellites deviating from the reference, RMS %1 [m]: %2").arg(what).arg(dev.join(" "));
907 }
908 };
909 deviating("SIS", rmsSis);
910 deviating("clock", rmsClk);
911 deviating("orbit (3D)", rmsOrb);
912 }
913 }
914 for (const QString& line : lines) {
915 out << " " << sys << " " << line.toStdString() << endl;
916 }
917 }
918 out.flush();
919 }
920
921 emit newMessage(QString("SSR QC: report %1 %2 of %3 stream(s) written to %4")
922 .arg(QString::fromStdString(_intervalStart.datestr()))
923 .arg(QString::fromStdString(_intervalStart.timestr(0)))
924 .arg(streams.size()).arg(_dirName).toLatin1(), false);
925
926 _cmpStat.clear();
927 _cmpEpochs.clear();
928
929 // reset, keeping the state needed across intervals
930 for (QMap<QString, t_epoStat>::iterator it = _epoStat.begin(); it != _epoStat.end(); ++it) {
931 t_epoStat& stat = it.value();
932 bncTime last = stat.last;
933 int nominal = stat.nominal;
934 stat = t_epoStat();
935 stat.last = last;
936 stat.nominal = nominal;
937 }
938 for (QMap<QString, t_satStat>::iterator it = _satStat.begin(); it != _satStat.end(); ++it) {
939 t_satStat& stat = it.value();
940 t_satStat fresh;
941 fresh.lastClkTime = stat.lastClkTime;
942 fresh.lastIod = stat.lastIod;
943 fresh.lastClk = stat.lastClk;
944 fresh.missingRefSig = stat.missingRefSig;
945 fresh.refBiasOK = stat.refBiasOK;
946 fresh.biasIF = stat.biasIF;
947 stat = fresh;
948 }
949}
Note: See TracBrowser for help on using the repository browser.