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

Last change on this file was 11091, checked in by stuerze, 6 days ago

ssrqs: consider if an AC has more than one solution

File size: 51.0 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 "ssrRefSig.h"
45#include "combination/bncbiassnx.h"
46#include "bnccore.h"
47#include "bncutils.h"
48#include "bncconst.h"
49#include "bncsettings.h"
50#include "combination/bnccomb.h"
51
52using namespace std;
53
54namespace {
55 const double MAX_CLK_JUMP = 0.5; // [m] clock change beyond its rate between consecutive epochs
56 const double EPH_WARMUP = 120.0; // [s] no ephemeris checks at the start
57 const double CMP_SAMPL = 30.0; // [s] sampling of the stream comparison
58 const double CMP_DELAY = 60.0; // [s] epochs compared when data this much newer arrived
59 const double HIST_LENGTH = 300.0; // [s] corrections kept for the comparison
60 const double DEV_MIN = 0.05; // [m] deviating satellites: RMS above max(DEV_MIN,
61 const double DEV_FACTOR = 3.0; // DEV_FACTOR x median RMS of the system)
62
63 double tKey(const bncTime& tt) {
64 return tt.gpsw() * 604800.0 + tt.gpssec();
65 }
66 const char* TYPE_NAME[] = {"ORBIT", "CLOCK", "CODE_BIAS", "PHASE_BIAS"};
67
68 double median(QVector<double> vv) {
69 if (vv.isEmpty()) {
70 return 0.0;
71 }
72 std::sort(vv.begin(), vv.end());
73 int nn = vv.size();
74 return (nn % 2) ? vv[nn/2] : 0.5 * (vv[nn/2-1] + vv[nn/2]);
75 }
76
77 bncTime currentTime() {
78 int week = 0;
79 double sec = 0.0;
80 currentGPSWeeks(week, sec);
81 return bncTime(week, sec);
82 }
83
84 // satellite list "G01 G05(12)", counts shown if not all epochs concerned
85 QString satList(const QMap<QString, unsigned>& sats, unsigned maxNum = 20) {
86 QString str;
87 unsigned num = 0;
88 QMapIterator<QString, unsigned> it(sats);
89 while (it.hasNext()) {
90 it.next();
91 if (++num > maxNum) {
92 str += QString(" ... (%1 satellites)").arg(sats.size());
93 break;
94 }
95 str += " " + it.key();
96 if (it.value() > 0) {
97 str += QString("(%1)").arg(it.value());
98 }
99 }
100 return str.isEmpty() ? " -" : str;
101 }
102}
103
104// Constructor
105////////////////////////////////////////////////////////////////////////////
106t_ssrQc::t_ssrQc(const QString& dirName, double interval, const QStringList& streams,
107 const QString& reference) : _ephUser(true) {
108 _dirName = dirName;
109 _reference = reference.isEmpty() ? QString("CONSENSUS") : reference;
110 _interval = interval > 0.0 ? interval : 3600.0;
111 // "ALL" or a list of streams; streams with a leading '!' are excluded
112 // (from the reports and from the consensus)
113 bool all = false;
114 for (const QString& sta : streams) {
115 if (sta.startsWith('!')) {
116 _excluded.insert(sta.mid(1));
117 }
118 else if (sta.toUpper() == "ALL") {
119 all = true;
120 }
121 else {
122 _streams.insert(sta);
123 }
124 }
125 if (all) {
126 _streams.clear();
127 }
128
129 connect(this, SIGNAL(newMessage(QByteArray,bool)), BNC_CORE, SLOT(slotMessage(const QByteArray,bool)));
130 for (const QString& err : t_ssrRefSig::init()) {
131 emit newMessage(("SSR QC: " + err).toLatin1(), true);
132 }
133 // reference code biases from a Bias SINEX file (optional)
134 _bsxRef = 0;
135 bncSettings settings;
136 QString bsxFile = settings.value("ssrQcBiasFile").toString();
137 expandEnvVar(bsxFile);
138 if (!bsxFile.isEmpty()) {
139 _bsxRef = new bncBiasSnx(true);
140 if (_bsxRef->readFile(bsxFile) == success) {
141 _bsxName = QFileInfo(bsxFile).fileName();
142 }
143 else {
144 emit newMessage(("SSR QC: wrong Bias SINEX file " + bsxFile).toLatin1(), true);
145 delete _bsxRef;
146 _bsxRef = 0;
147 }
148 }
149 connect(BNC_CORE, SIGNAL(newOrbCorrections(QList<t_orbCorr>)),
150 this, SLOT(slotNewOrbCorrections(QList<t_orbCorr>)), Qt::DirectConnection);
151 connect(BNC_CORE, SIGNAL(newClkCorrections(QList<t_clkCorr>)),
152 this, SLOT(slotNewClkCorrections(QList<t_clkCorr>)), Qt::DirectConnection);
153 connect(BNC_CORE, SIGNAL(newCodeBiases(QList<t_satCodeBias>)),
154 this, SLOT(slotNewCodeBiases(QList<t_satCodeBias>)), Qt::DirectConnection);
155 connect(BNC_CORE, SIGNAL(newPhaseBiases(QList<t_satPhaseBias>)),
156 this, SLOT(slotNewPhaseBiases(QList<t_satPhaseBias>)), Qt::DirectConnection);
157 // combined corrections (stream INTERNAL)
158 if (bncComb::exists()) {
159 connect(BNC_CMB, SIGNAL(newOrbCorrections(QList<t_orbCorr>)),
160 this, SLOT(slotNewOrbCorrections(QList<t_orbCorr>)), Qt::DirectConnection);
161 connect(BNC_CMB, SIGNAL(newClkCorrections(QList<t_clkCorr>)),
162 this, SLOT(slotNewClkCorrections(QList<t_clkCorr>)), Qt::DirectConnection);
163 }
164}
165
166// Stream selected for the QC
167////////////////////////////////////////////////////////////////////////////
168bool t_ssrQc::selected(const string& staID) const {
169 QString sta = QString::fromStdString(staID);
170 return !_excluded.contains(sta) && (_streams.isEmpty() || _streams.contains(sta));
171}
172
173// Analysis center of a stream: the 3 characters of the AC in mountpoints
174// like SSRA00CNE1 or OSBC00WHU1, otherwise the stream name itself
175////////////////////////////////////////////////////////////////////////////
176QString t_ssrQc::acOf(const QString& staID) {
177 static const QRegularExpression re("^(?:SSR|OSB)[AC][0-9]{2}([A-Z0-9]{3})[0-9]$");
178 QRegularExpressionMatch mm = re.match(staID);
179 return mm.hasMatch() ? mm.captured(1) : staID;
180}
181
182// Solution of an AC: AC and the two digits of the mountpoint, e.g. GFZ00 for
183// SSRA00GFZ0/SSRC00GFZ1 and GFZ01 for SSRA01GFZ0; the streams of a solution
184// (formats, APC/CoM) have one vote in the consensus
185////////////////////////////////////////////////////////////////////////////
186QString t_ssrQc::solOf(const QString& staID) {
187 static const QRegularExpression re("^(?:SSR|OSB)[AC]([0-9]{2})([A-Z0-9]{3})[0-9]$");
188 QRegularExpressionMatch mm = re.match(staID);
189 return mm.hasMatch() ? mm.captured(2) + mm.captured(1) : staID;
190}
191
192// AC of a solution (GFZ01 -> GFZ)
193////////////////////////////////////////////////////////////////////////////
194QString t_ssrQc::acOfSol(const QString& sol) {
195 static const QRegularExpression re("^([A-Z0-9]{3})[0-9]{2}$");
196 QRegularExpressionMatch mm = re.match(sol);
197 return mm.hasMatch() ? mm.captured(1) : sol;
198}
199
200// Stream part of the consensus: not excluded, and not a combination product
201// (INTERNAL, IGS combinations), as these are derived from the other streams
202////////////////////////////////////////////////////////////////////////////
203bool t_ssrQc::votes(const QString& staID) const {
204 return !_excluded.contains(staID) && staID != "INTERNAL" && acOf(staID) != "IGS";
205}
206
207// Daily QC file of a stream: <staID>_S_<YYYYDDD>0000_01D_QC.txt
208////////////////////////////////////////////////////////////////////////////
209ofstream& t_ssrQc::streamOut(const QString& staID, const bncTime& time) {
210 unsigned year, month, day;
211 time.civil_date(year, month, day);
212 QDate date(year, month, day);
213 QString name = QDir(_dirName).filePath(QString("%1_S_%2%3%4_01D_QC.txt").arg(staID)
214 .arg(year).arg(date.dayOfYear(), 3, 10, QChar('0')).arg("0000"));
215 if (_outNames.value(staID) != name) {
216 delete _outs.value(staID, 0);
217 bool exists = QFile::exists(name);
218 ofstream* out = new ofstream(name.toLocal8Bit().data(), ios_base::out | ios_base::app);
219 if (!exists) {
220 *out << "SSR QC of stream " << staID.toStdString() << ", report interval " << _interval << " s" << endl
221 << "Checks: epochs and update intervals, gaps (interval > 2 x max(declared, median)), latency,"
222 << " completeness w.r.t. healthy broadcast ephemerides," << endl
223 << "IOD matching, code and phase bias signals, yaw angle, code biases of the reference signals, clock jumps > "
224 << MAX_CLK_JUMP << " m, largest orbit corrections" << endl
225 << "Reference signals of the clocks: " << t_ssrRefSig::currentString().toStdString() << endl;
226 }
227 _outs[staID] = out;
228 _outNames[staID] = name;
229 }
230 return *_outs[staID];
231}
232
233// Destructor
234////////////////////////////////////////////////////////////////////////////
235t_ssrQc::~t_ssrQc() {
236 QMutexLocker locker(&_mutex);
237 writeReport(true);
238 for (ofstream* out : _outs) {
239 delete out;
240 }
241 delete _bsxRef;
242}
243
244// Key of the epoch statistics
245////////////////////////////////////////////////////////////////////////////
246QString t_ssrQc::key(const string& staID, char sys, e_type type) {
247 return QString("%1 %2 %3").arg(staID.c_str()).arg(sys).arg(int(type));
248}
249
250// Reference signals of the clocks (as in the combination, option ssrRefSignals)
251////////////////////////////////////////////////////////////////////////////
252bool t_ssrQc::refSignals(char sys, QString& sig1, QString& sig2,
253 t_frequency::type& frq1, t_frequency::type& frq2) {
254 t_ssrRefSig::t_pair pair;
255 if (!t_ssrRefSig::get(sys, pair)) {
256 return false;
257 }
258 sig1 = QString::fromStdString(pair.sig1());
259 sig2 = QString::fromStdString(pair.sig2());
260 frq1 = pair.frq1;
261 frq2 = pair.frq2;
262 return true;
263}
264
265// Start a new report interval if the epoch is beyond the current one
266////////////////////////////////////////////////////////////////////////////
267void t_ssrQc::checkInterval(const bncTime& time) {
268 if (!_intervalStart.valid()) {
269 double sec = floor(time.gpssec() / _interval) * _interval;
270 _intervalStart = bncTime(time.gpsw(), sec);
271 }
272 // stream comparison of the epochs already complete for all streams
273 if (!_nextCmpEpoch.valid()) {
274 double sec = ceil(time.gpssec() / CMP_SAMPL) * CMP_SAMPL;
275 _nextCmpEpoch = bncTime(time.gpsw(), sec);
276 }
277 while (time - _nextCmpEpoch >= CMP_DELAY) {
278 compareEpoch(_nextCmpEpoch);
279 _nextCmpEpoch = _nextCmpEpoch + CMP_SAMPL;
280 }
281 if (time - _intervalStart >= _interval) {
282 writeReport(false);
283 double sec = floor(time.gpssec() / _interval) * _interval;
284 _intervalStart = bncTime(time.gpsw(), sec);
285 }
286 if (!_lastTime.valid() || _lastTime < time) {
287 _lastTime = time;
288 }
289}
290
291// Epoch statistics
292////////////////////////////////////////////////////////////////////////////
293void t_ssrQc::newEpoch(const string& staID, char sys, e_type type, const bncTime& time,
294 unsigned updateInt, const QString& prn) {
295 QString sta = QString::fromStdString(staID);
296 if (!_staStart.contains(sta)) {
297 _staStart[sta] = time;
298 }
299 if (!_staLast.contains(sta) || _staLast[sta] < time) {
300 _staLast[sta] = time;
301 }
302 t_epoStat& stat = _epoStat[key(staID, sys, type)];
303 if (updateInt < unsigned(ssrUpdateInt.size())) {
304 stat.nominal = ssrUpdateInt[updateInt];
305 }
306 stat.sats.insert(prn);
307 if (!stat.last.valid() || stat.last < time) {
308 if (stat.last.valid()) {
309 stat.intervals.push_back(time - stat.last);
310 }
311 stat.last = time;
312 stat.numEpo += 1;
313 stat.latencies.push_back(currentTime() - time);
314 }
315 else if (time < stat.last) {
316 stat.numOutOfOrder += 1;
317 }
318}
319
320// Broadcast ephemeris the correction refers to
321////////////////////////////////////////////////////////////////////////////
322void t_ssrQc::checkEph(t_satStat& stat, const QString& prnInt, unsigned iod, const bncTime& time,
323 const string& staID) {
324 // not before all broadcast ephemerides may have been received
325 if (time - _staStart[QString::fromStdString(staID)] < EPH_WARMUP) {
326 return;
327 }
328 t_eph* ephLast = _ephUser.ephLast(prnInt);
329 t_eph* ephPrev = _ephUser.ephPrev(prnInt);
330 t_eph* eph = 0;
331 if (ephLast && ephLast->IOD() == iod) eph = ephLast;
332 else if (ephPrev && ephPrev->IOD() == iod) eph = ephPrev;
333 if (!ephLast) {
334 stat.noEph += 1;
335 }
336 else if (!eph) {
337 stat.iodUnknown += 1;
338 }
339 else if (eph->checkState() == t_eph::unhealthy || eph->checkState() == t_eph::bad) {
340 stat.ephUnhealthy += 1;
341 }
342}
343
344// Orbit corrections
345////////////////////////////////////////////////////////////////////////////
346void t_ssrQc::slotNewOrbCorrections(QList<t_orbCorr> orbCorrections) {
347 QMutexLocker locker(&_mutex);
348 for (const t_orbCorr& corr : orbCorrections) {
349 if (!_excluded.contains(QString::fromStdString(corr._staID)) || corr._staID == _reference.toStdString()) {
350 storeHist(&corr, 0);
351 }
352 if (!selected(corr._staID)) continue;
353 QString prn = QString::fromStdString(corr._prn.toString());
354 checkInterval(corr._time);
355 newEpoch(corr._staID, corr._prn.system(), ORB, corr._time, corr._updateInt, prn);
356 t_satStat& stat = _satStat[QString::fromStdString(corr._staID) + ' ' + prn];
357 stat.numOrb += 1;
358 checkEph(stat, QString::fromStdString(corr._prn.toInternalString()), corr._iod, corr._time, corr._staID);
359 double radial = fabs(corr._xr[0]);
360 double alongCross = max(fabs(corr._xr[1]), fabs(corr._xr[2]));
361 stat.maxRadial = max(stat.maxRadial, radial);
362 stat.maxAlongCross = max(stat.maxAlongCross, alongCross);
363 }
364}
365
366// Clock corrections
367////////////////////////////////////////////////////////////////////////////
368void t_ssrQc::slotNewClkCorrections(QList<t_clkCorr> clkCorrections) {
369 QMutexLocker locker(&_mutex);
370 for (const t_clkCorr& corr : clkCorrections) {
371 if (!_excluded.contains(QString::fromStdString(corr._staID)) || corr._staID == _reference.toStdString()) {
372 storeHist(0, &corr);
373 }
374 if (!selected(corr._staID)) continue;
375 QString prn = QString::fromStdString(corr._prn.toString());
376 checkInterval(corr._time);
377 newEpoch(corr._staID, corr._prn.system(), CLK, corr._time, corr._updateInt, prn);
378 t_satStat& stat = _satStat[QString::fromStdString(corr._staID) + ' ' + prn];
379 stat.numClk += 1;
380 checkEph(stat, QString::fromStdString(corr._prn.toInternalString()), corr._iod, corr._time, corr._staID);
381
382 // jump: change between consecutive epochs (same IOD) beyond the clock rate
383 double clk = corr._dClk * t_CST::c;
384 if (stat.lastClkTime.valid() && stat.lastIod == corr._iod) {
385 double dt = corr._time - stat.lastClkTime;
386 int nominal = (corr._updateInt < unsigned(ssrUpdateInt.size())) ? ssrUpdateInt[corr._updateInt] : 0;
387 if (dt > 0.0 && dt <= max(2.0 * nominal, 60.0)) {
388 double jump = fabs(clk - stat.lastClk - corr._dotDClk * t_CST::c * dt);
389 stat.maxJump = max(stat.maxJump, jump);
390 if (jump > MAX_CLK_JUMP) {
391 stat.clkJumps += 1;
392 }
393 }
394 }
395 stat.lastClkTime = corr._time;
396 stat.lastIod = corr._iod;
397 stat.lastClk = clk;
398 }
399}
400
401// Code biases
402////////////////////////////////////////////////////////////////////////////
403void t_ssrQc::slotNewCodeBiases(QList<t_satCodeBias> codeBiases) {
404 QMutexLocker locker(&_mutex);
405 for (const t_satCodeBias& bias : codeBiases) {
406 bool sel = selected(bias._staID);
407 if (_excluded.contains(QString::fromStdString(bias._staID)) && bias._staID != _reference.toStdString()) continue;
408 char sys = bias._prn.system();
409 QString prn = QString::fromStdString(bias._prn.toString());
410 if (sel) {
411 checkInterval(bias._time);
412 newEpoch(bias._staID, sys, CBIAS, bias._time, bias._updateInt, prn);
413 }
414 QString staSys = QString("%1 %2").arg(bias._staID.c_str()).arg(sys);
415 QMap<QString, double> values;
416 for (const t_frqCodeBias& frqBias : bias._bias) {
417 QString sig = QString::fromStdString(frqBias._rnxType2ch);
418 _codeSignals[staSys].insert(sig);
419 values[sig] = frqBias._value;
420 }
421 if (!values.isEmpty()) {
422 _cbLast[QString::fromStdString(bias._staID)][prn] = values;
423 }
424 t_satStat& stat = _satStat[QString::fromStdString(bias._staID) + ' ' + prn];
425 QString sig1, sig2;
426 t_frequency::type frq1, frq2;
427 if (refSignals(sys, sig1, sig2, frq1, frq2)) {
428 stat.missingRefSig.clear();
429 if (!values.contains(sig1)) stat.missingRefSig.insert("C" + sig1);
430 if (!values.contains(sig2)) stat.missingRefSig.insert("C" + sig2);
431 stat.refBiasOK = stat.missingRefSig.isEmpty();
432 if (stat.refBiasOK) {
433 double channel = 0.0;
434 if (sys == 'R') {
435 t_eph* eph = _ephUser.ephLast(QString::fromStdString(bias._prn.toInternalString()));
436 if (!eph) {
437 stat.refBiasOK = false;
438 continue;
439 }
440 channel = eph->slotNum();
441 }
442 double f1 = t_CST::freq(frq1, channel);
443 double f2 = t_CST::freq(frq2, channel);
444 stat.biasIF = (f1 * f1 * values[sig1] - f2 * f2 * values[sig2]) / (f1 * f1 - f2 * f2);
445 }
446 }
447 }
448}
449
450// Phase biases
451////////////////////////////////////////////////////////////////////////////
452void t_ssrQc::slotNewPhaseBiases(QList<t_satPhaseBias> phaseBiases) {
453 QMutexLocker locker(&_mutex);
454 for (const t_satPhaseBias& bias : phaseBiases) {
455 if (!selected(bias._staID)) continue;
456 checkInterval(bias._time);
457 newEpoch(bias._staID, bias._prn.system(), PBIAS, bias._time, bias._updateInt,
458 QString::fromStdString(bias._prn.toString()));
459 QString staSys = QString("%1 %2").arg(bias._staID.c_str()).arg(bias._prn.system());
460 for (const t_frqPhaseBias& frqBias : bias._bias) {
461 _phaseSignals[staSys].insert(QString::fromStdString(frqBias._rnxType2ch));
462 }
463 // Yaw angle: new RTCM-SSR has an indicator, old RTCM-SSR and IGS-SSR always
464 // carry the field, providers without attitude information send zeros
465 t_satStat& stat = _satStat[QString::fromStdString(bias._staID) + ' ' +
466 QString::fromStdString(bias._prn.toString())];
467 stat.numPB++;
468 bool yawProvided = (bias._ssrFormat == ssrRtcmNew) ? (bias._satYawInfoInd != 0)
469 : (bias._yaw != 0.0 || bias._yawRate != 0.0);
470 if (yawProvided) {
471 stat.numYaw++;
472 }
473 }
474}
475
476// Reference DCBs (bias(base) - bias(signal)) of a satellite: median of the
477// other ACs (one vote per AC, own AC left out, combinations not voting) or
478// the selected reference stream, or from the Bias SINEX file
479////////////////////////////////////////////////////////////////////////////
480bool t_ssrQc::refDcbs(const QString& staID, const QString& prn, const QString& base, bool fromFile,
481 QMap<QString, double>& dcbs, const QString& onlyAC, QMap<QString, int>* numVotes) {
482 dcbs.clear();
483 if (numVotes) {
484 numVotes->clear();
485 }
486 if (fromFile) {
487 if (!_bsxRef) {
488 return false;
489 }
490 char sys = prn[0].toLatin1();
491 t_ssrRefSig::t_pair pair;
492 if (!t_ssrRefSig::get(sys, pair)) {
493 return false;
494 }
495 // the DCBs do not depend on the coefficients (as long as they add up to 1)
496 double f1 = t_CST::freq(pair.frq1, 0.0);
497 double f2 = t_CST::freq(pair.frq2, 0.0);
498 double aIF1 = f1 * f1 / (f1 * f1 - f2 * f2);
499 double aIF2 = -f2 * f2 / (f1 * f1 - f2 * f2);
500 t_satCodeBias satCodeBias;
501 _bsxRef->determineSsrSatCodeBiases(prn, aIF1, aIF2, satCodeBias);
502 QMap<QString, double> osb;
503 for (const t_frqCodeBias& bb : satCodeBias._bias) {
504 osb[QString::fromStdString(bb._rnxType2ch)] = bb._value;
505 }
506 if (!osb.contains(base)) {
507 return false;
508 }
509 for (auto it = osb.constBegin(); it != osb.constEnd(); ++it) {
510 if (it.key() != base) {
511 dcbs[it.key()] = osb[base] - it.value();
512 }
513 }
514 return !dcbs.isEmpty();
515 }
516
517 // per AC: DCBs of its streams
518 QMap<QString, QMap<QString, QVector<double> > > acDcbs; // AC, signal
519 for (auto itS = _cbLast.constBegin(); itS != _cbLast.constEnd(); ++itS) {
520 const QString& sta = itS.key();
521 if (sta == staID) {
522 continue;
523 }
524 if (_reference == "CONSENSUS") {
525 if (!votes(sta)) {
526 continue;
527 }
528 if (onlyAC.isEmpty() ? (acOf(sta) == acOf(staID))
529 : (solOf(sta) != onlyAC || solOf(sta) == solOf(staID))) {
530 continue;
531 }
532 }
533 else if (sta != _reference) {
534 continue;
535 }
536 if (!itS.value().contains(prn)) {
537 continue;
538 }
539 const QMap<QString, double>& bb = itS.value()[prn];
540 if (!bb.contains(base)) {
541 continue;
542 }
543 for (auto it = bb.constBegin(); it != bb.constEnd(); ++it) {
544 if (it.key() != base) {
545 acDcbs[solOf(sta)][it.key()].append(bb[base] - it.value());
546 }
547 }
548 }
549 QMap<QString, QVector<double> > votesPerSig;
550 for (auto itA = acDcbs.constBegin(); itA != acDcbs.constEnd(); ++itA) {
551 for (auto it = itA.value().constBegin(); it != itA.value().constEnd(); ++it) {
552 votesPerSig[it.key()].append(median(it.value()));
553 }
554 }
555 for (auto it = votesPerSig.constBegin(); it != votesPerSig.constEnd(); ++it) {
556 dcbs[it.key()] = median(it.value());
557 if (numVotes) {
558 (*numVotes)[it.key()] = it.value().size();
559 }
560 }
561 return !dcbs.isEmpty();
562}
563
564// Code bias check: the DCBs of the stream (relative to the first reference
565// signal, otherwise to another common signal) compared with a reference;
566// DCBs do not depend on the clock datum. A sign opposite to the reference
567// for most DCBs indicates the SINEX instead of the SSR sign convention.
568// References: the Bias SINEX file, the selected reference stream, or each
569// other AC separately (a median of two ACs cannot decide which one is wrong)
570////////////////////////////////////////////////////////////////////////////
571void t_ssrQc::compareCodeBiases(const QString& staID, char sys, QStringList& lines) {
572 const double MIN_DCB = 0.3; // [m] DCBs used for the sign check
573 const double MIN_DEV = 0.5; // [m] deviating DCBs: > max(MIN_DEV, DEV_FACTOR x median)
574 const int MIN_VOTE = 3; // ACs needed for a consensus DCB (deviating satellites)
575 if (!_cbLast.contains(staID)) {
576 return;
577 }
578 t_ssrRefSig::t_pair pair;
579 if (!t_ssrRefSig::get(sys, pair)) {
580 return;
581 }
582 const QString ref1 = QString::fromStdString(pair.sig1());
583
584 class t_cmp {
585 public:
586 unsigned numSat = 0, numSign = 0, numOpp = 0;
587 double sumSqr = 0.0;
588 QVector<double> absDiffs;
589 QMap<QString, QPair<QString, double> > maxDiff; // PRN: signal, difference
590 QSet<QString> bases;
591 bool opposite() const {return numSign >= 5 && numOpp > numSign / 2;}
592 double rms() const {return absDiffs.isEmpty() ? 0.0 : sqrt(sumSqr / absDiffs.size());}
593 };
594 // mode 0: file, 1: reference stream or median of the other ACs, 2: one other AC
595 auto compare = [&](int mode, const QString& onlyAC) {
596 t_cmp cc;
597 for (auto itP = _cbLast[staID].constBegin(); itP != _cbLast[staID].constEnd(); ++itP) {
598 const QString& prn = itP.key();
599 if (prn[0].toLatin1() != sys) {
600 continue;
601 }
602 const QMap<QString, double>& bb = itP.value();
603 QStringList candidates = QStringList() << ref1 << bb.keys();
604 QMap<QString, double> dcbs;
605 QMap<QString, int> numVotes;
606 QString base;
607 for (const QString& cand : candidates) {
608 if (bb.contains(cand) && refDcbs(staID, prn, cand, mode == 0, dcbs, onlyAC, &numVotes)) {
609 base = cand;
610 break;
611 }
612 }
613 if (base.isEmpty()) {
614 continue;
615 }
616 bool used = false;
617 for (auto it = dcbs.constBegin(); it != dcbs.constEnd(); ++it) {
618 if (!bb.contains(it.key())) {
619 continue;
620 }
621 if (mode == 1 && _reference == "CONSENSUS" && numVotes.value(it.key()) < MIN_VOTE) {
622 continue;
623 }
624 double dcb = bb[base] - bb[it.key()];
625 double diff = dcb - it.value();
626 used = true;
627 cc.sumSqr += diff * diff;
628 cc.absDiffs.append(fabs(diff));
629 if (fabs(diff) > fabs(cc.maxDiff.value(prn).second)) {
630 cc.maxDiff[prn] = qMakePair("C" + it.key(), diff);
631 }
632 if (fabs(it.value()) >= MIN_DCB) {
633 cc.numSign++;
634 if (dcb * it.value() < 0.0) {
635 cc.numOpp++;
636 }
637 }
638 }
639 if (used) {
640 cc.numSat++;
641 cc.bases.insert("C" + base);
642 }
643 }
644 return cc;
645 };
646 auto baseStr = [](const t_cmp& cc) {
647 QStringList ll = cc.bases.values();
648 ll.sort();
649 return ll.join("/");
650 };
651 auto deviating = [&](const t_cmp& cc, const QString& refName) {
652 if (cc.absDiffs.isEmpty() || cc.opposite()) {
653 return;
654 }
655 double limit = std::max(MIN_DEV, DEV_FACTOR * median(cc.absDiffs));
656 QStringList devs;
657 for (auto it = cc.maxDiff.constBegin(); it != cc.maxDiff.constEnd(); ++it) {
658 if (fabs(it.value().second) > limit) {
659 devs << QString("%1(%2 %3)").arg(it.key()).arg(it.value().first).arg(it.value().second, 0, 'f', 2);
660 }
661 }
662 if (!devs.isEmpty()) {
663 lines << QString("satellites with DCBs deviating from %1 [m]: %2").arg(refName).arg(devs.join(" "));
664 }
665 };
666
667 // Bias SINEX file or the selected reference stream
668 for (int mode = 0; mode < 2; mode++) {
669 if ((mode == 0 && !_bsxRef) || (mode == 1 && _reference == "CONSENSUS")) {
670 continue;
671 }
672 QString refName = (mode == 0) ? QString("Bias SINEX file %1").arg(_bsxName) : _reference;
673 t_cmp cc = compare(mode, QString());
674 if (cc.absDiffs.isEmpty()) {
675 continue;
676 }
677 lines << QString("code biases compared with %1 (DCBs to %2, %3 satellites, %4 DCBs): RMS of the differences "
678 "%5 m, opposite sign for %6 of %7 DCBs (|DCB| >= %8 m)")
679 .arg(refName).arg(baseStr(cc)).arg(cc.numSat).arg(cc.absDiffs.size())
680 .arg(cc.rms(), 0, 'f', 3).arg(cc.numOpp).arg(cc.numSign).arg(MIN_DCB, 0, 'f', 1);
681 if (cc.opposite()) {
682 lines << QString("code biases with the opposite sign of %1 for %2 of %3 DCBs - "
683 "SINEX sign convention instead of the SSR one?").arg(refName).arg(cc.numOpp).arg(cc.numSign);
684 }
685 deviating(cc, refName);
686 }
687
688 // Each other AC separately, and the median of the other ACs for deviating satellites
689 if (_reference == "CONSENSUS") {
690 QSet<QString> acs;
691 for (auto itS = _cbLast.constBegin(); itS != _cbLast.constEnd(); ++itS) {
692 if (itS.key() != staID && votes(itS.key()) && solOf(itS.key()) != solOf(staID)) {
693 acs.insert(solOf(itS.key()));
694 }
695 }
696 QStringList acList = acs.values();
697 acList.sort();
698 QStringList items;
699 QStringList oppACs;
700 int numACs = 0;
701 QSet<QString> bases;
702 for (const QString& ac : acList) {
703 t_cmp cc = compare(2, ac);
704 if (cc.absDiffs.isEmpty()) {
705 continue;
706 }
707 bases.unite(cc.bases);
708 items << QString("%1 RMS %2 m, opposite sign %3/%4").arg(ac).arg(cc.rms(), 0, 'f', 2)
709 .arg(cc.numOpp).arg(cc.numSign);
710 // the sign rule counts the solutions of other ACs only
711 if (acOfSol(ac) != acOf(staID)) {
712 numACs++;
713 if (cc.opposite()) {
714 oppACs << ac;
715 }
716 }
717 }
718 if (!items.isEmpty()) {
719 QStringList ll = bases.values();
720 ll.sort();
721 lines << QString("code biases compared with the other solutions (DCBs to %1): %2").arg(ll.join("/")).arg(items.join("; "));
722 if (numACs >= 2 && oppACs.size() > numACs / 2) {
723 lines << QString("code biases with the opposite sign of %1 of %2 solutions of other ACs (%3) - "
724 "SINEX sign convention instead of the SSR one?")
725 .arg(oppACs.size()).arg(numACs).arg(oppACs.join(" "));
726 }
727 deviating(compare(1, QString()), QString("the median of the other ACs (at least %1 solutions)").arg(MIN_VOTE));
728 }
729 }
730}
731
732// Reference point of the orbits of a stream: APC (SSRA...), CoM (SSRC...);
733// the combined stream INTERNAL refers to the APC
734////////////////////////////////////////////////////////////////////////////
735char t_ssrQc::refPoint(const QString& staID) const {
736 if (staID == "INTERNAL") {
737 return 'A'; // the combined corrections always refer to the APC
738 }
739 const QString& sta = staID;
740 if (sta.startsWith("SSRA")) return 'A';
741 if (sta.startsWith("SSRC")) return 'C';
742 return '?';
743}
744
745// Keep the recent orbit and clock corrections of a stream
746////////////////////////////////////////////////////////////////////////////
747void t_ssrQc::storeHist(const t_orbCorr* orb, const t_clkCorr* clk) {
748 if (orb) {
749 QMap<double, t_orbCorr>& hist = _orbHist[QString::fromStdString(orb->_staID)]
750 [QString::fromStdString(orb->_prn.toInternalString())];
751 hist[tKey(orb->_time)] = *orb;
752 while (!hist.isEmpty() && hist.firstKey() < tKey(orb->_time) - HIST_LENGTH) {
753 hist.erase(hist.begin());
754 }
755 }
756 if (clk) {
757 QMap<double, t_clkCorr>& hist = _clkHist[QString::fromStdString(clk->_staID)]
758 [QString::fromStdString(clk->_prn.toInternalString())];
759 hist[tKey(clk->_time)] = *clk;
760 while (!hist.isEmpty() && hist.firstKey() < tKey(clk->_time) - HIST_LENGTH) {
761 hist.erase(hist.begin());
762 }
763 }
764}
765
766// Compare the orbits and clocks of all streams at epoch tt
767////////////////////////////////////////////////////////////////////////////
768void t_ssrQc::compareEpoch(const bncTime& tt) {
769 double tk = tKey(tt);
770
771 // Satellite positions and clocks of all streams (latest corrections <= tt)
772 QMap<QString, QMap<QString, t_satCrd> > crd; // stream, PRN (external)
773 QMapIterator<QString, QMap<QString, QMap<double, t_clkCorr> > > itSta(_clkHist);
774 while (itSta.hasNext()) {
775 itSta.next();
776 const QString& staID = itSta.key();
777 QMapIterator<QString, QMap<double, t_clkCorr> > itPrn(itSta.value());
778 while (itPrn.hasNext()) {
779 itPrn.next();
780 const QString& prnInt = itPrn.key();
781 QMap<double, t_clkCorr>::const_iterator itC = itPrn.value().upperBound(tk);
782 if (itC == itPrn.value().constBegin()) {
783 continue;
784 }
785 const t_clkCorr& clk = (--itC).value();
786 const QMap<double, t_orbCorr>& orbHist = _orbHist[staID][prnInt];
787 QMap<double, t_orbCorr>::const_iterator itO = orbHist.upperBound(tk);
788 if (itO == orbHist.constBegin()) {
789 continue;
790 }
791 const t_orbCorr& orb = (--itO).value();
792 t_eph* eph = 0;
793 t_eph* ephLast = _ephUser.ephLast(prnInt);
794 t_eph* ephPrev = _ephUser.ephPrev(prnInt);
795 if (ephLast && ephLast->IOD() == orb._iod) eph = ephLast;
796 else if (ephPrev && ephPrev->IOD() == orb._iod) eph = ephPrev;
797 if (!eph || clk._iod != orb._iod) {
798 continue;
799 }
800 eph->setOrbCorr(&orb);
801 eph->setClkCorr(&clk);
802 ColumnVector xc(6), vv(3);
803 if (eph->getCrd(tt, xc, vv, true) != success) {
804 continue;
805 }
806 t_satCrd& sc = crd[staID][QString::fromStdString(clk._prn.toString())];
807 sc.xx = xc.Rows(1,3);
808 sc.vv = vv;
809 sc.clk = xc(4) * t_CST::c;
810 sc.refPoint = refPoint(staID);
811 // clock referred to the reference signals (as in the combination):
812 // + B_IF of the stream's code biases; the combination (INTERNAL)
813 // refers to them already
814 sc.clkOK = (staID == "INTERNAL");
815 QString satKey = staID + ' ' + QString::fromStdString(clk._prn.toString());
816 if (!sc.clkOK && _satStat.contains(satKey) && _satStat[satKey].refBiasOK) {
817 sc.clk += _satStat[satKey].biasIF;
818 sc.clkOK = true;
819 }
820 }
821 }
822
823 // Satellites per system
824 QMap<char, QSet<QString> > satsSys;
825 QMapIterator<QString, QMap<QString, t_satCrd> > itCrd(crd);
826 while (itCrd.hasNext()) {
827 itCrd.next();
828 for (const QString& prn : itCrd.value().keys()) {
829 satsSys[prn[0].toLatin1()].insert(prn);
830 }
831 }
832
833 QMapIterator<char, QSet<QString> > itSys(satsSys);
834 while (itSys.hasNext()) {
835 itSys.next();
836 const QSet<QString>& sats = itSys.value();
837 QStringList staList;
838 for (const QString& staID : crd.keys()) {
839 for (const QString& prn : sats) {
840 if (crd[staID].contains(prn)) {
841 staList << staID;
842 break;
843 }
844 }
845 }
846
847 // Consensus: one value per AC (median of its streams), so that an AC
848 // with several streams (formats, APC/CoM) has one vote; the streams of
849 // the AC compared are left out (leave-one-AC-out)
850 auto acClocks = [&](const QString& prn, const QMap<QString, double>& off, bool useOff) {
851 QMap<QString, QVector<double> > perAC;
852 for (const QString& oo : staList) {
853 if (!votes(oo) || !crd[oo].contains(prn) || !crd[oo][prn].clkOK) continue;
854 if (useOff && !off.contains(oo)) continue;
855 perAC[solOf(oo)] << crd[oo][prn].clk - (useOff ? off[oo] : 0.0);
856 }
857 QMap<QString, double> res;
858 for (const QString& ac : perAC.keys()) res[ac] = median(perAC[ac]);
859 return res;
860 };
861
862 // Clock datum: offset of each stream w.r.t. the per-satellite consensus
863 // of all ACs (or w.r.t. the reference stream)
864 QMap<QString, double> offset;
865 QMap<QString, double> noOff;
866 QMap<QString, QMap<QString, double> > rawAC; // PRN, AC
867 if (_reference == "CONSENSUS") {
868 for (const QString& prn : sats) rawAC[prn] = acClocks(prn, noOff, false);
869 }
870 for (const QString& staID : staList) {
871 QVector<double> diffs;
872 for (const QString& prn : sats) {
873 if (!crd[staID].contains(prn) || !crd[staID][prn].clkOK) continue;
874 if (_reference == "CONSENSUS") {
875 if (rawAC[prn].size() >= 3) diffs << crd[staID][prn].clk - median(rawAC[prn].values().toVector());
876 }
877 else if (crd.value(_reference).contains(prn) && crd[_reference][prn].clkOK) {
878 diffs << crd[staID][prn].clk - crd[_reference][prn].clk;
879 }
880 }
881 if (!diffs.isEmpty()) {
882 offset[staID] = median(diffs);
883 }
884 }
885 QMap<QString, QMap<QString, double> > alignedAC; // PRN, AC
886 if (_reference == "CONSENSUS") {
887 for (const QString& prn : sats) alignedAC[prn] = acClocks(prn, offset, true);
888 }
889
890 // Differences of each selected stream to its reference
891 for (const QString& staID : staList) {
892 if (!selected(staID.toStdString()) || staID == _reference) {
893 continue;
894 }
895 const QString ownAC = acOf(staID);
896 QMap<QString, double> dClk;
897 QMap<QString, ColumnVector> dRac;
898 for (const QString& prn : sats) {
899 if (!crd[staID].contains(prn)) continue;
900 const t_satCrd& sc = crd[staID][prn];
901 QVector<double> clks, xs[3];
902 if (_reference == "CONSENSUS") {
903 QMapIterator<QString, double> itA(alignedAC[prn]);
904 while (itA.hasNext()) {
905 itA.next();
906 if (acOfSol(itA.key()) != ownAC) clks << itA.value();
907 }
908 // orbits: per AC the median of its streams with the same reference point
909 QMap<QString, QVector<double> > perAC[3];
910 for (const QString& oo : staList) {
911 if (!votes(oo) || acOf(oo) == ownAC || !crd[oo].contains(prn)) continue;
912 const t_satCrd& rc = crd[oo][prn];
913 if (rc.refPoint != sc.refPoint || sc.refPoint == '?') continue;
914 for (int ii = 0; ii < 3; ii++) perAC[ii][solOf(oo)] << rc.xx[ii];
915 }
916 for (const QString& ac : perAC[0].keys()) {
917 for (int ii = 0; ii < 3; ii++) xs[ii] << median(perAC[ii][ac]);
918 }
919 if (clks.size() < 2) clks.clear();
920 if (xs[0].size() < 2) for (int ii = 0; ii < 3; ii++) xs[ii].clear();
921 }
922 else {
923 if (!crd.value(_reference).contains(prn)) continue;
924 const t_satCrd& rc = crd[_reference][prn];
925 if (rc.clkOK) clks << rc.clk;
926 if (rc.refPoint == sc.refPoint && sc.refPoint != '?') {
927 for (int ii = 0; ii < 3; ii++) xs[ii] << rc.xx[ii];
928 }
929 }
930 if (sc.clkOK && offset.contains(staID) && !clks.isEmpty()) {
931 dClk[prn] = sc.clk - offset[staID] - median(clks);
932 }
933 else if (!sc.clkOK) {
934 _cmpStat[staID][prn].clkNotReferred += 1;
935 }
936 if (!xs[0].isEmpty()) {
937 ColumnVector dx(3);
938 for (int ii = 0; ii < 3; ii++) dx[ii] = sc.xx[ii] - median(xs[ii]);
939 ColumnVector er = sc.xx / sc.xx.NormFrobenius();
940 ColumnVector ec = crossproduct(sc.xx, sc.vv);
941 ec /= ec.NormFrobenius();
942 ColumnVector ea = crossproduct(ec, er);
943 ColumnVector rac(3);
944 rac[0] = DotProduct(dx, er);
945 rac[1] = DotProduct(dx, ea);
946 rac[2] = DotProduct(dx, ec);
947 dRac[prn] = rac;
948 }
949 }
950 if (dClk.isEmpty() && dRac.isEmpty()) {
951 continue;
952 }
953 // remaining common clock offset of this stream and epoch
954 double off = dClk.isEmpty() ? 0.0 : median(dClk.values().toVector());
955 _cmpEpochs[staID] += 1;
956 QSet<QString> prns;
957 for (const QString& prn : dClk.keys()) prns.insert(prn);
958 for (const QString& prn : dRac.keys()) prns.insert(prn);
959 for (const QString& prn : prns) {
960 t_cmpStat& stat = _cmpStat[staID][prn];
961 if (dRac.contains(prn)) {
962 const ColumnVector& rac = dRac[prn];
963 stat.numOrb += 1;
964 stat.sR += rac[0] * rac[0];
965 stat.sA += rac[1] * rac[1];
966 stat.sC += rac[2] * rac[2];
967 }
968 if (dClk.contains(prn)) {
969 double dc = dClk[prn] - off;
970 stat.numClk += 1;
971 stat.sClk += dc * dc;
972 stat.mClk += dc;
973 if (dRac.contains(prn)) {
974 double sis = dRac[prn][0] - dc;
975 stat.numSis += 1;
976 stat.sSis += sis * sis;
977 stat.mSis += sis;
978 }
979 }
980 }
981 }
982 }
983}
984
985// Write the report of the current interval and reset the statistics
986////////////////////////////////////////////////////////////////////////////
987void t_ssrQc::writeReport(bool final) {
988 if (_epoStat.isEmpty()) {
989 return;
990 }
991 bncTime endTime = final ? _lastTime : _intervalStart + _interval;
992
993 // Healthy and current broadcast ephemerides per system
994 QMap<char, QSet<QString> > ephSats;
995 for (const QString& prnInt : _ephUser.prnList()) {
996 t_eph* eph = _ephUser.ephLast(prnInt);
997 if (eph && eph->checkState() == t_eph::ok && !outDatedBcep(eph, endTime)) {
998 ephSats[eph->prn().system()].insert(QString::fromStdString(eph->prn().toString()));
999 }
1000 }
1001
1002 // Streams and systems
1003 QMap<QString, QSet<char> > streams;
1004 QMapIterator<QString, t_epoStat> itEpo(_epoStat);
1005 while (itEpo.hasNext()) {
1006 itEpo.next();
1007 QStringList hlp = itEpo.key().split(' ');
1008 streams[hlp[0]].insert(hlp[1][0].toLatin1());
1009 }
1010
1011 QMapIterator<QString, QSet<char> > itSta(streams);
1012 while (itSta.hasNext()) {
1013 itSta.next();
1014 const QString& staID = itSta.key();
1015 QList<char> systems = itSta.value().values();
1016 std::sort(systems.begin(), systems.end());
1017 ofstream& out = streamOut(staID, _intervalStart);
1018 if (final) {
1019 endTime = _staLast.value(staID, _lastTime);
1020 if (endTime < _intervalStart) { // stream without data in the last interval
1021 endTime = _lastTime;
1022 }
1023 }
1024 out << endl << "=== SSR QC " << staID.toStdString() << ' '
1025 << _intervalStart.datestr() << ' ' << _intervalStart.timestr(0)
1026 << " - " << endTime.datestr() << ' ' << endTime.timestr(0)
1027 << (final ? " (end of data)" : "") << " ===" << endl
1028 << " Sys Type Epochs Interval nom/med/max [s] Gaps Out-of-order"
1029 << " Latency med/max [s] Sats" << endl;
1030 for (char sys : systems) {
1031 for (int type = ORB; type <= PBIAS; type++) {
1032 QString kk = QString("%1 %2 %3").arg(staID).arg(sys).arg(type);
1033 if (!_epoStat.contains(kk)) {
1034 continue;
1035 }
1036 const t_epoStat& stat = _epoStat[kk];
1037 double medInt = median(stat.intervals);
1038 double maxInt = stat.intervals.isEmpty() ? 0.0 :
1039 *std::max_element(stat.intervals.begin(), stat.intervals.end());
1040 double ref = max(double(stat.nominal), medInt);
1041 unsigned gaps = 0;
1042 for (double dt : stat.intervals) {
1043 if (ref > 0.0 && dt > 2.0 * ref) {
1044 ++gaps;
1045 }
1046 }
1047 double maxLat = stat.latencies.isEmpty() ? 0.0 :
1048 *std::max_element(stat.latencies.begin(), stat.latencies.end());
1049 out << " " << sys << " " << left << setw(10) << TYPE_NAME[type] << right
1050 << setw(8) << stat.numEpo
1051 << setw(10) << stat.nominal << " /" << setw(6) << setprecision(1) << fixed << medInt
1052 << " /" << setw(7) << maxInt
1053 << setw(6) << gaps << setw(14) << stat.numOutOfOrder
1054 << setw(10) << median(stat.latencies) << " /" << setw(7) << maxLat
1055 << setw(7) << stat.sats.size() << endl;
1056 }
1057 }
1058
1059 // Per system: completeness, ephemeris, biases, plausibility
1060 for (char sys : systems) {
1061 QMap<QString, unsigned> missOrb, missClk, missCB, missPB, iodUnknown, unhealthy, noEph,
1062 jumps, missRef, noYaw, yawPartly;
1063 unsigned numPB = 0, numYaw = 0, yawSats = 0;
1064 double maxRadial = 0.0, maxAlongCross = 0.0;
1065 QString maxRadialPrn, maxAlongCrossPrn;
1066 QVector<double> biasIF;
1067 const QSet<QString>& ephs = ephSats[sys];
1068 for (int type = ORB; type <= PBIAS; type++) {
1069 QString kk = QString("%1 %2 %3").arg(staID).arg(sys).arg(type);
1070 if (!_epoStat.contains(kk)) {
1071 continue;
1072 }
1073 QMap<QString, unsigned>& miss = (type == ORB) ? missOrb : (type == CLK) ? missClk :
1074 (type == CBIAS) ? missCB : missPB;
1075 for (const QString& prn : ephs) {
1076 if (!_epoStat[kk].sats.contains(prn)) {
1077 miss[prn] = 0;
1078 }
1079 }
1080 }
1081 QMapIterator<QString, t_satStat> itSat(_satStat);
1082 while (itSat.hasNext()) {
1083 itSat.next();
1084 QStringList hlp = itSat.key().split(' ');
1085 if (hlp[0] != staID || hlp[1][0].toLatin1() != sys) {
1086 continue;
1087 }
1088 const QString& prn = hlp[1];
1089 const t_satStat& stat = itSat.value();
1090 if (stat.iodUnknown) iodUnknown[prn] = stat.iodUnknown;
1091 if (stat.ephUnhealthy) unhealthy[prn] = stat.ephUnhealthy;
1092 if (stat.noEph) noEph[prn] = stat.noEph;
1093 if (stat.clkJumps) jumps[prn] = stat.clkJumps;
1094 if (stat.maxRadial > maxRadial) {
1095 maxRadial = stat.maxRadial;
1096 maxRadialPrn = prn;
1097 }
1098 if (stat.maxAlongCross > maxAlongCross) {
1099 maxAlongCross = stat.maxAlongCross;
1100 maxAlongCrossPrn = prn;
1101 }
1102 if (!stat.missingRefSig.isEmpty()) {
1103 QStringList sigs = stat.missingRefSig.values();
1104 sigs.sort();
1105 missRef[prn + "[" + sigs.join("/") + "]"] = 0;
1106 }
1107 if (stat.refBiasOK) {
1108 biasIF.push_back(stat.biasIF);
1109 }
1110 if (stat.numPB) {
1111 numPB += stat.numPB;
1112 numYaw += stat.numYaw;
1113 if (stat.numYaw) {
1114 yawSats++;
1115 if (stat.numYaw < stat.numPB) {
1116 yawPartly[prn] = stat.numPB - stat.numYaw;
1117 }
1118 }
1119 else {
1120 noYaw[prn] = 0;
1121 }
1122 }
1123 }
1124 // only findings are listed
1125 QStringList lines;
1126 for (int type = ORB; type <= PBIAS; type++) {
1127 QString kk = QString("%1 %2 %3").arg(staID).arg(sys).arg(type);
1128 if (_epoStat.contains(kk)) {
1129 const t_epoStat& stat = _epoStat[kk];
1130 double medInt = median(stat.intervals);
1131 if (stat.nominal > 0 && stat.intervals.size() > 1 &&
1132 (medInt > 1.5 * stat.nominal || medInt < stat.nominal / 1.5)) {
1133 lines << QString("%1: declared update interval %2 s, actual %3 s")
1134 .arg(TYPE_NAME[type]).arg(stat.nominal).arg(medInt, 0, 'f', 1);
1135 }
1136 }
1137 }
1138 auto addSats = [&lines](const QString& text, const QMap<QString, unsigned>& sats) {
1139 if (!sats.isEmpty()) {
1140 lines << text + ":" + satList(sats);
1141 }
1142 };
1143 addSats("orbit corrections missing (healthy broadcast ephemeris available)", missOrb);
1144 addSats("clock corrections missing (healthy broadcast ephemeris available)", missClk);
1145 addSats("code biases missing (healthy broadcast ephemeris available)", missCB);
1146 addSats("phase biases missing (healthy broadcast ephemeris available)", missPB);
1147 addSats("corrections referring to an IOD of no received broadcast ephemeris", iodUnknown);
1148 addSats("corrections for unhealthy broadcast ephemeris", unhealthy);
1149 addSats("corrections for satellites without broadcast ephemeris", noEph);
1150 QString staSys = QString("%1 %2").arg(staID).arg(sys);
1151 if (_codeSignals.contains(staSys)) {
1152 QStringList sigs = _codeSignals[staSys].values();
1153 sigs.sort();
1154 lines << "code bias signals: " + sigs.join(" ");
1155 addSats("code biases of the reference signals missing", missRef);
1156 if (biasIF.size() > 1) {
1157 double mean = 0.0;
1158 for (double bb : biasIF) mean += bb / biasIF.size();
1159 double var = 0.0;
1160 for (double bb : biasIF) var += (bb - mean) * (bb - mean) / biasIF.size();
1161 double maxAbs = max(fabs(*std::min_element(biasIF.begin(), biasIF.end())),
1162 fabs(*std::max_element(biasIF.begin(), biasIF.end())));
1163 lines << QString("ionosphere-free combination B_IF of the reference signal biases: "
1164 "std. dev. %1 m, max |B_IF| %2 m (%3 satellites)")
1165 .arg(sqrt(var), 0, 'f', 3).arg(maxAbs, 0, 'f', 3).arg(biasIF.size());
1166 }
1167 }
1168 compareCodeBiases(staID, sys, lines);
1169 if (_phaseSignals.contains(staSys)) {
1170 QStringList sigs = _phaseSignals[staSys].values();
1171 sigs.sort();
1172 lines << "phase bias signals: " + sigs.join(" ");
1173 }
1174 if (numPB) {
1175 if (numYaw == 0) {
1176 lines << "yaw angle: not provided";
1177 }
1178 else {
1179 lines << QString("yaw angle: provided for %1 of %2 satellites (%3 % of the phase bias records)")
1180 .arg(yawSats).arg(yawSats + noYaw.size()).arg(100.0 * numYaw / numPB, 0, 'f', 1);
1181 addSats("phase biases without yaw angle", noYaw);
1182 addSats("phase biases partly without yaw angle (records)", yawPartly);
1183 }
1184 }
1185 addSats(QString("clock jumps > %1 m").arg(MAX_CLK_JUMP), jumps);
1186 if (!maxRadialPrn.isEmpty()) {
1187 lines << QString("largest orbit corrections: radial %1 m (%2), along/cross %3 m (%4)")
1188 .arg(maxRadial, 0, 'f', 2).arg(maxRadialPrn).arg(maxAlongCross, 0, 'f', 2).arg(maxAlongCrossPrn);
1189 }
1190 // stream comparison
1191 if (_cmpStat.contains(staID)) {
1192 unsigned nSat = 0, nClk = 0, nOrb = 0, nSis = 0;
1193 double sClk = 0.0, sR = 0.0, sA = 0.0, sC = 0.0, sSis = 0.0;
1194 double vClk = 0.0, vSis = 0.0; // sums of squares about the satellite means
1195 QMap<QString, double> rmsSis, rmsClk, rmsOrb;
1196 QMap<QString, unsigned> notReferred;
1197 QMapIterator<QString, t_cmpStat> itC(_cmpStat[staID]);
1198 while (itC.hasNext()) {
1199 itC.next();
1200 if (itC.key()[0].toLatin1() != sys) continue;
1201 const t_cmpStat& st = itC.value();
1202 if (st.numClk == 0 && st.numOrb == 0) {
1203 if (st.clkNotReferred) notReferred[itC.key()] = 0;
1204 continue;
1205 }
1206 ++nSat;
1207 if (st.clkNotReferred) notReferred[itC.key()] = 0;
1208 nClk += st.numClk; sClk += st.sClk;
1209 nOrb += st.numOrb; sR += st.sR; sA += st.sA; sC += st.sC;
1210 nSis += st.numSis; sSis += st.sSis;
1211 if (st.numClk) vClk += st.sClk - st.mClk * st.mClk / st.numClk;
1212 if (st.numSis) vSis += st.sSis - st.mSis * st.mSis / st.numSis;
1213 if (st.numSis) rmsSis[itC.key()] = sqrt(st.sSis / st.numSis);
1214 else if (st.numClk) rmsClk[itC.key()] = sqrt(st.sClk / st.numClk);
1215 if (st.numOrb) rmsOrb[itC.key()] = sqrt((st.sR + st.sA + st.sC) / st.numOrb);
1216 }
1217 if (nSat) {
1218 QString refText = (_reference == "CONSENSUS") ? QString("consensus of the other ACs")
1219 : QString("stream %1").arg(_reference);
1220 QStringList parts;
1221 if (nClk) parts << QString("clock %1 (STD %2)").arg(sqrt(sClk / nClk), 0, 'f', 3)
1222 .arg(sqrt(max(0.0, vClk) / nClk), 0, 'f', 3);
1223 if (nOrb) parts << QString("radial %1, along %2, cross %3")
1224 .arg(sqrt(sR / nOrb), 0, 'f', 3).arg(sqrt(sA / nOrb), 0, 'f', 3)
1225 .arg(sqrt(sC / nOrb), 0, 'f', 3);
1226 if (nSis) parts << QString("SIS (radial - clock) %1 (STD %2)").arg(sqrt(sSis / nSis), 0, 'f', 3)
1227 .arg(sqrt(max(0.0, vSis) / nSis), 0, 'f', 3);
1228 lines << QString("comparison with %1 (%2 satellites), RMS (STD about the satellite means) [m]: %3")
1229 .arg(refText).arg(nSat).arg(parts.join(", "));
1230 if (!nOrb) {
1231 lines << "orbits not compared: no other stream with the same reference point (APC/CoM)";
1232 }
1233 addSats("clocks not compared (code biases of the reference signals missing)", notReferred);
1234 auto deviating = [&lines](const QString& what, const QMap<QString, double>& rms) {
1235 if (rms.size() < 2) return;
1236 double med = median(rms.values().toVector());
1237 QStringList dev;
1238 QMapIterator<QString, double> itR(rms);
1239 while (itR.hasNext()) {
1240 itR.next();
1241 if (itR.value() > max(DEV_MIN, DEV_FACTOR * med)) {
1242 dev << QString("%1(%2)").arg(itR.key()).arg(itR.value(), 0, 'f', 2);
1243 }
1244 }
1245 if (!dev.isEmpty()) {
1246 lines << QString("satellites deviating from the reference, RMS %1 [m]: %2").arg(what).arg(dev.join(" "));
1247 }
1248 };
1249 deviating("SIS", rmsSis);
1250 deviating("clock", rmsClk);
1251 deviating("orbit (3D)", rmsOrb);
1252 }
1253 }
1254 for (const QString& line : lines) {
1255 out << " " << sys << " " << line.toStdString() << endl;
1256 }
1257 }
1258 out.flush();
1259 }
1260
1261 emit newMessage(QString("SSR QC: report %1 %2 of %3 stream(s) written to %4")
1262 .arg(QString::fromStdString(_intervalStart.datestr()))
1263 .arg(QString::fromStdString(_intervalStart.timestr(0)))
1264 .arg(streams.size()).arg(_dirName).toLatin1(), false);
1265
1266 _cmpStat.clear();
1267 _cmpEpochs.clear();
1268
1269 // reset, keeping the state needed across intervals
1270 for (QMap<QString, t_epoStat>::iterator it = _epoStat.begin(); it != _epoStat.end(); ++it) {
1271 t_epoStat& stat = it.value();
1272 bncTime last = stat.last;
1273 int nominal = stat.nominal;
1274 stat = t_epoStat();
1275 stat.last = last;
1276 stat.nominal = nominal;
1277 }
1278 for (QMap<QString, t_satStat>::iterator it = _satStat.begin(); it != _satStat.end(); ++it) {
1279 t_satStat& stat = it.value();
1280 t_satStat fresh;
1281 fresh.lastClkTime = stat.lastClkTime;
1282 fresh.lastIod = stat.lastIod;
1283 fresh.lastClk = stat.lastClk;
1284 fresh.missingRefSig = stat.missingRefSig;
1285 fresh.refBiasOK = stat.refBiasOK;
1286 fresh.biasIF = stat.biasIF;
1287 stat = fresh;
1288 }
1289}
Note: See TracBrowser for help on using the repository browser.