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

Last change on this file was 11148, checked in by stuerze, 15 hours ago

minor changes regarding GLONASS health and SSR QC

File size: 71.1 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#include "bncantex.h"
52
53using namespace std;
54
55namespace {
56 const double MAX_CLK_JUMP = 0.5; // [m] clock change beyond its rate between consecutive epochs
57 const double EPH_WARMUP = 120.0; // [s] no ephemeris checks at the start
58 const double EPH_EARLY_MAX = 300.0; // [s] IOD received this much later still counted as early
59 const double EPH_IOD_REUSE = 21600.0; // [s] an outdated data set this far from the epoch: IOD reused
60 const unsigned EPH_KEPT = 30; // data sets kept per satellite
61 const double CMP_SAMPL = 30.0; // [s] sampling of the stream comparison
62 const double CMP_DELAY = 60.0; // [s] epochs compared when data this much newer arrived
63 const double HIST_LENGTH = 300.0; // [s] corrections kept for the comparison
64 const double DEV_MIN = 0.05; // [m] deviating satellites: RMS above max(DEV_MIN,
65 const double DEV_FACTOR = 3.0; // DEV_FACTOR x median RMS of the system)
66 const double ATT_ONSET = 20.0 * M_PI / 180.0; // [rad] change of the yaw difference starting a turn
67 const double ATT_STABLE = 2.0 * M_PI / 180.0; // [rad] yaw difference stable again ...
68 const double ATT_STABLE_T = 600.0; // [s] ... for this time: end of the turn
69 const double ATT_REF_FROM = 900.0; // [s] reference before the turn: samples from 15 min
70 const double ATT_REF_TO = 300.0; // to 5 min before the current epoch
71 const double ATT_MAX_GAP = 600.0; // [s] longer gaps interrupt a turn
72 const double ATT_MAX_TURN = 10800.0; // [s] a turn is closed after 3 h at the latest
73 const double ATT_MIN_PRED = 0.03; // [m] smaller predicted clock changes are not assessed
74 const double VTEC_CMP_SAMPL = 300.0; // [s] sampling of the VTEC comparison
75 const double VTEC_CMP_DELAY = 120.0; // [s] VTEC epochs compared when data this much newer arrived
76 const double VTEC_HIST = 3600.0; // [s] VTEC models kept for the comparison
77 const double VTEC_GRID_LAT = 10.0; // [deg] grid of the VTEC evaluation, latitudes -80 ... 80
78 const double VTEC_GRID_LON = 15.0; // [deg]
79
80 double tKey(const bncTime& tt) {
81 return tt.gpsw() * 604800.0 + tt.gpssec();
82 }
83 const char* TYPE_NAME[] = {"ORBIT", "CLOCK", "CODE_BIAS", "PHASE_BIAS", "VTEC"};
84
85 double median(QVector<double> vv) {
86 if (vv.isEmpty()) {
87 return 0.0;
88 }
89 std::sort(vv.begin(), vv.end());
90 int nn = vv.size();
91 return (nn % 2) ? vv[nn/2] : 0.5 * (vv[nn/2-1] + vv[nn/2]);
92 }
93
94 bncTime currentTime() {
95 int week = 0;
96 double sec = 0.0;
97 currentGPSWeeks(week, sec);
98 return bncTime(week, sec);
99 }
100
101 // satellite list "G01 G05(12)", counts shown if not all epochs concerned
102 QString satList(const QMap<QString, unsigned>& sats, unsigned maxNum = 20) {
103 QString str;
104 unsigned num = 0;
105 QMapIterator<QString, unsigned> it(sats);
106 while (it.hasNext()) {
107 it.next();
108 if (++num > maxNum) {
109 str += QString(" ... (%1 satellites)").arg(sats.size());
110 break;
111 }
112 str += " " + it.key();
113 if (it.value() > 0) {
114 str += QString("(%1)").arg(it.value());
115 }
116 }
117 return str.isEmpty() ? " -" : str;
118 }
119}
120
121// Constructor
122////////////////////////////////////////////////////////////////////////////
123t_ssrQc::t_ssrQc(const QString& dirName, double interval, const QStringList& streams,
124 const QString& reference) : _ephUser(true, EPH_KEPT) {
125 _ephUser.useEphMounts("SSR QC");
126 _dirName = dirName;
127 _reference = reference.isEmpty() ? QString("CONSENSUS") : reference;
128 _interval = interval > 0.0 ? interval : 3600.0;
129 // "ALL" or a list of streams; streams with a leading '!' are excluded
130 // (from the reports and from the consensus)
131 bool all = false;
132 for (const QString& sta : streams) {
133 if (sta.startsWith('!')) {
134 _excluded.insert(sta.mid(1));
135 }
136 else if (sta.toUpper() == "ALL") {
137 all = true;
138 }
139 else {
140 _streams.insert(sta);
141 }
142 }
143 if (all) {
144 _streams.clear();
145 }
146
147 connect(this, SIGNAL(newMessage(QByteArray,bool)), BNC_CORE, SLOT(slotMessage(const QByteArray,bool)));
148 for (const QString& err : t_ssrRefSig::init()) {
149 emit newMessage(("SSR QC: " + err).toLatin1(), true);
150 }
151 // reference code biases from a Bias SINEX file (optional)
152 _bsxRef = 0;
153 bncSettings settings;
154 QString bsxFile = settings.value("ssrQcBiasFile").toString();
155 expandEnvVar(bsxFile);
156 if (!bsxFile.isEmpty()) {
157 _bsxRef = new bncBiasSnx(true);
158 if (_bsxRef->readFile(bsxFile) == success) {
159 _bsxName = QFileInfo(bsxFile).fileName();
160 }
161 else {
162 emit newMessage(("SSR QC: wrong Bias SINEX file " + bsxFile).toLatin1(), true);
163 delete _bsxRef;
164 _bsxRef = 0;
165 }
166 }
167 _antex = 0;
168 QString antexFile = settings.value("ssrQcAntexFile").toString();
169 expandEnvVar(antexFile);
170 if (!antexFile.isEmpty()) {
171 _antex = new bncAntex();
172 if (_antex->readFile(antexFile) != success || _antex->numSatPrns() == 0) {
173 emit newMessage(("SSR QC: wrong ANTEX file " + antexFile + " - no attitude check").toLatin1(), true);
174 delete _antex;
175 _antex = 0;
176 }
177 }
178 connect(BNC_CORE, SIGNAL(newOrbCorrections(QList<t_orbCorr>)),
179 this, SLOT(slotNewOrbCorrections(QList<t_orbCorr>)), Qt::DirectConnection);
180 connect(BNC_CORE, SIGNAL(newClkCorrections(QList<t_clkCorr>)),
181 this, SLOT(slotNewClkCorrections(QList<t_clkCorr>)), Qt::DirectConnection);
182 connect(BNC_CORE, SIGNAL(newCodeBiases(QList<t_satCodeBias>)),
183 this, SLOT(slotNewCodeBiases(QList<t_satCodeBias>)), Qt::DirectConnection);
184 connect(BNC_CORE, SIGNAL(newPhaseBiases(QList<t_satPhaseBias>)),
185 this, SLOT(slotNewPhaseBiases(QList<t_satPhaseBias>)), Qt::DirectConnection);
186 connect(BNC_CORE, SIGNAL(newTec(t_vTec)),
187 this, SLOT(slotNewTec(t_vTec)), Qt::DirectConnection);
188 // combined corrections (stream INTERNAL)
189 if (bncComb::exists()) {
190 connect(BNC_CMB, SIGNAL(newOrbCorrections(QList<t_orbCorr>)),
191 this, SLOT(slotNewOrbCorrections(QList<t_orbCorr>)), Qt::DirectConnection);
192 connect(BNC_CMB, SIGNAL(newClkCorrections(QList<t_clkCorr>)),
193 this, SLOT(slotNewClkCorrections(QList<t_clkCorr>)), Qt::DirectConnection);
194 }
195}
196
197// Stream selected for the QC
198////////////////////////////////////////////////////////////////////////////
199bool t_ssrQc::selected(const string& staID) const {
200 QString sta = QString::fromStdString(staID);
201 return !_excluded.contains(sta) && (_streams.isEmpty() || _streams.contains(sta));
202}
203
204// Analysis center of a stream: the 3 characters of the AC in mountpoints
205// like SSRA00CNE1 or OSBC00WHU1, otherwise the stream name itself
206////////////////////////////////////////////////////////////////////////////
207QString t_ssrQc::acOf(const QString& staID) {
208 static const QRegularExpression re("^(?:SSR[AC]|OSB[AC]|IONO)[0-9]{2}([A-Z0-9]{3})[0-9]$");
209 QRegularExpressionMatch mm = re.match(staID);
210 return mm.hasMatch() ? mm.captured(1) : staID;
211}
212
213// Solution of an AC: AC and the two digits of the mountpoint, e.g. GFZ00 for
214// SSRA00GFZ0/SSRC00GFZ1 and GFZ01 for SSRA01GFZ0; the streams of a solution
215// (formats, APC/CoM) have one vote in the consensus
216////////////////////////////////////////////////////////////////////////////
217QString t_ssrQc::solOf(const QString& staID) {
218 static const QRegularExpression re("^(?:SSR[AC]|OSB[AC]|IONO)([0-9]{2})([A-Z0-9]{3})[0-9]$");
219 QRegularExpressionMatch mm = re.match(staID);
220 return mm.hasMatch() ? mm.captured(2) + mm.captured(1) : staID;
221}
222
223// AC of a solution (GFZ01 -> GFZ)
224////////////////////////////////////////////////////////////////////////////
225QString t_ssrQc::acOfSol(const QString& sol) {
226 static const QRegularExpression re("^([A-Z0-9]{3})[0-9]{2}$");
227 QRegularExpressionMatch mm = re.match(sol);
228 return mm.hasMatch() ? mm.captured(1) : sol;
229}
230
231// Stream part of the consensus: not excluded, and not a combination product
232// (INTERNAL, IGS combinations), as these are derived from the other streams
233////////////////////////////////////////////////////////////////////////////
234bool t_ssrQc::votes(const QString& staID) const {
235 return !_excluded.contains(staID) && staID != "INTERNAL" && acOf(staID) != "IGS";
236}
237
238// Daily QC file of a stream: <staID>_S_<YYYYDDD>0000_01D_QC.txt
239////////////////////////////////////////////////////////////////////////////
240ofstream& t_ssrQc::streamOut(const QString& staID, const bncTime& time) {
241 unsigned year, month, day;
242 time.civil_date(year, month, day);
243 QDate date(year, month, day);
244 QString name = QDir(_dirName).filePath(QString("%1_S_%2%3%4_01D_QC.txt").arg(staID)
245 .arg(year).arg(date.dayOfYear(), 3, 10, QChar('0')).arg("0000"));
246 if (_outNames.value(staID) != name) {
247 delete _outs.value(staID, 0);
248 bool exists = QFile::exists(name);
249 ofstream* out = new ofstream(name.toLocal8Bit().data(), ios_base::out | ios_base::app);
250 if (!exists) {
251 *out << "SSR QC of stream " << staID.toStdString() << ", report interval " << _interval << " s" << endl
252 << "Checks: epochs and update intervals, gaps (interval > 2 x max(declared, median)), latency,"
253 << " completeness w.r.t. healthy broadcast ephemerides," << endl
254 << "IOD matching, code and phase bias signals, yaw angle, code biases of the reference signals, clock jumps > "
255 << MAX_CLK_JUMP << " m, largest orbit corrections, VTEC models (system V)" << endl
256 << "Reference signals of the clocks: " << t_ssrRefSig::currentString().toStdString() << endl;
257 }
258 _outs[staID] = out;
259 _outNames[staID] = name;
260 }
261 return *_outs[staID];
262}
263
264// Destructor
265////////////////////////////////////////////////////////////////////////////
266t_ssrQc::~t_ssrQc() {
267 QMutexLocker locker(&_mutex);
268 writeReport(true);
269 for (ofstream* out : _outs) {
270 delete out;
271 }
272 delete _bsxRef;
273 delete _antex;
274}
275
276// Key of the epoch statistics
277////////////////////////////////////////////////////////////////////////////
278QString t_ssrQc::key(const string& staID, char sys, e_type type) {
279 return QString("%1 %2 %3").arg(staID.c_str()).arg(sys).arg(int(type));
280}
281
282// Reference signals of the clocks (as in the combination, option ssrRefSignals)
283////////////////////////////////////////////////////////////////////////////
284bool t_ssrQc::refSignals(char sys, QString& sig1, QString& sig2,
285 t_frequency::type& frq1, t_frequency::type& frq2) {
286 t_ssrRefSig::t_pair pair;
287 if (!t_ssrRefSig::get(sys, pair)) {
288 return false;
289 }
290 sig1 = QString::fromStdString(pair.sig1());
291 sig2 = QString::fromStdString(pair.sig2());
292 frq1 = pair.frq1;
293 frq2 = pair.frq2;
294 return true;
295}
296
297// Start a new report interval if the epoch is beyond the current one
298////////////////////////////////////////////////////////////////////////////
299void t_ssrQc::checkInterval(const bncTime& time) {
300 if (!_intervalStart.valid()) {
301 double sec = floor(time.gpssec() / _interval) * _interval;
302 _intervalStart = bncTime(time.gpsw(), sec);
303 }
304 // stream comparison of the epochs already complete for all streams
305 if (!_nextCmpEpoch.valid()) {
306 double sec = ceil(time.gpssec() / CMP_SAMPL) * CMP_SAMPL;
307 _nextCmpEpoch = bncTime(time.gpsw(), sec);
308 }
309 while (time - _nextCmpEpoch >= CMP_DELAY) {
310 compareEpoch(_nextCmpEpoch);
311 _nextCmpEpoch = _nextCmpEpoch + CMP_SAMPL;
312 }
313 // VTEC comparison
314 if (!_vtecHist.isEmpty()) {
315 if (!_nextVtecCmp.valid()) {
316 double sec = ceil(time.gpssec() / VTEC_CMP_SAMPL) * VTEC_CMP_SAMPL;
317 _nextVtecCmp = bncTime(time.gpsw(), sec);
318 }
319 while (time - _nextVtecCmp >= VTEC_CMP_DELAY) {
320 compareVtec(_nextVtecCmp);
321 _nextVtecCmp = _nextVtecCmp + VTEC_CMP_SAMPL;
322 }
323 }
324 if (time - _intervalStart >= _interval) {
325 writeReport(false);
326 double sec = floor(time.gpssec() / _interval) * _interval;
327 _intervalStart = bncTime(time.gpsw(), sec);
328 }
329 if (!_lastTime.valid() || _lastTime < time) {
330 _lastTime = time;
331 }
332}
333
334// Epoch statistics
335////////////////////////////////////////////////////////////////////////////
336void t_ssrQc::newEpoch(const string& staID, char sys, e_type type, const bncTime& time,
337 unsigned updateInt, const QString& prn) {
338 QString sta = QString::fromStdString(staID);
339 if (!_staStart.contains(sta)) {
340 _staStart[sta] = time;
341 }
342 if (!_staLast.contains(sta) || _staLast[sta] < time) {
343 _staLast[sta] = time;
344 }
345 t_epoStat& stat = _epoStat[key(staID, sys, type)];
346 if (updateInt < unsigned(ssrUpdateInt.size())) {
347 stat.nominal = ssrUpdateInt[updateInt];
348 }
349 stat.sats.insert(prn);
350 if (!stat.last.valid() || stat.last < time) {
351 if (stat.last.valid()) {
352 stat.intervals.push_back(time - stat.last);
353 }
354 stat.last = time;
355 stat.numEpo += 1;
356 stat.latencies.push_back(currentTime() - time);
357 }
358 else if (time < stat.last) {
359 stat.numOutOfOrder += 1;
360 }
361}
362
363// Broadcast ephemeris the correction refers to
364////////////////////////////////////////////////////////////////////////////
365void t_ssrQc::checkEph(t_satStat& stat, const QString& satKey, const QString& prnInt,
366 unsigned iod, const bncTime& time, const string& staID) {
367 // not before all broadcast ephemerides may have been received
368 if (time - _staStart[QString::fromStdString(staID)] < EPH_WARMUP) {
369 return;
370 }
371 if (!_ephUser.ephLast(prnInt)) {
372 stat.noEph += 1;
373 return;
374 }
375 // any data set kept, not only the last two: Galileo has a new IODnav every
376 // 10 minutes, and ACs keep one for up to an hour
377 t_eph* eph = _ephUser.ephWithIOD(prnInt, iod);
378 // an IOD used again later (GLONASS tb every day, GPS IODE) is another data set
379 if (eph && outDatedBcep(eph, time) && fabs(time - eph->TOC()) > EPH_IOD_REUSE) {
380 eph = 0;
381 }
382 if (!eph) {
383 // received later from the ephemeris stream(s)? decided in resolvePendingIod
384 t_pendingIod& pending = _pendingIod[satKey][iod];
385 if (pending.num == 0) {
386 pending.prnInt = prnInt;
387 pending.first = currentTime();
388 }
389 pending.num += 1;
390 }
391 else if (eph->checkState() == t_eph::unhealthy || eph->checkState() == t_eph::bad) {
392 stat.ephUnhealthy += 1;
393 }
394 else if (outDatedBcep(eph, time)) {
395 stat.ephOutdated += 1;
396 }
397 if (_pendingIod.contains(satKey)) {
398 resolvePendingIod(satKey, false);
399 }
400}
401
402// Corrections referring to an IOD not received before: counted as early if
403// the ephemeris stream(s) delivered the data set later, as unknown if not
404// within EPH_EARLY_MAX (or at the end of the report interval, final)
405////////////////////////////////////////////////////////////////////////////
406void t_ssrQc::resolvePendingIod(const QString& satKey, bool final) {
407 t_satStat& stat = _satStat[satKey];
408 QMap<unsigned, t_pendingIod>& pend = _pendingIod[satKey];
409 QMutableMapIterator<unsigned, t_pendingIod> it(pend);
410 while (it.hasNext()) {
411 it.next();
412 const t_pendingIod& pending = it.value();
413 t_eph* eph = _ephUser.ephWithIOD(pending.prnInt, it.key());
414 if (eph && eph->receptDateTime().isValid()) {
415 bncTime recept(eph->receptDateTime().toString(Qt::ISODate).toStdString());
416 if (recept >= pending.first) {
417 stat.iodEarly += pending.num;
418 stat.maxEarly = max(stat.maxEarly, recept - pending.first);
419 it.remove();
420 continue;
421 }
422 }
423 if (final || currentTime() - pending.first > EPH_EARLY_MAX) {
424 stat.iodUnknown += pending.num;
425 it.remove();
426 }
427 }
428 if (pend.isEmpty()) {
429 _pendingIod.remove(satKey);
430 }
431}
432
433// Orbit corrections
434////////////////////////////////////////////////////////////////////////////
435void t_ssrQc::slotNewOrbCorrections(QList<t_orbCorr> orbCorrections) {
436 QMutexLocker locker(&_mutex);
437 for (const t_orbCorr& corr : orbCorrections) {
438 if (!_excluded.contains(QString::fromStdString(corr._staID)) || corr._staID == _reference.toStdString()) {
439 storeHist(&corr, 0);
440 }
441 if (!selected(corr._staID)) continue;
442 QString prn = QString::fromStdString(corr._prn.toString());
443 checkInterval(corr._time);
444 newEpoch(corr._staID, corr._prn.system(), ORB, corr._time, corr._updateInt, prn);
445 t_satStat& stat = _satStat[QString::fromStdString(corr._staID) + ' ' + prn];
446 stat.numOrb += 1;
447 checkEph(stat, QString::fromStdString(corr._staID) + ' ' + prn,
448 QString::fromStdString(corr._prn.toInternalString()), corr._iod, corr._time, corr._staID);
449 double radial = fabs(corr._xr[0]);
450 double alongCross = max(fabs(corr._xr[1]), fabs(corr._xr[2]));
451 stat.maxRadial = max(stat.maxRadial, radial);
452 stat.maxAlongCross = max(stat.maxAlongCross, alongCross);
453 }
454}
455
456// Clock corrections
457////////////////////////////////////////////////////////////////////////////
458void t_ssrQc::slotNewClkCorrections(QList<t_clkCorr> clkCorrections) {
459 QMutexLocker locker(&_mutex);
460 for (const t_clkCorr& corr : clkCorrections) {
461 if (!_excluded.contains(QString::fromStdString(corr._staID)) || corr._staID == _reference.toStdString()) {
462 storeHist(0, &corr);
463 }
464 if (!selected(corr._staID)) continue;
465 QString prn = QString::fromStdString(corr._prn.toString());
466 checkInterval(corr._time);
467 newEpoch(corr._staID, corr._prn.system(), CLK, corr._time, corr._updateInt, prn);
468 t_satStat& stat = _satStat[QString::fromStdString(corr._staID) + ' ' + prn];
469 stat.numClk += 1;
470 checkEph(stat, QString::fromStdString(corr._staID) + ' ' + prn,
471 QString::fromStdString(corr._prn.toInternalString()), corr._iod, corr._time, corr._staID);
472
473 // jump: change between consecutive epochs (same IOD) beyond the clock rate
474 double clk = corr._dClk * t_CST::c;
475 if (stat.lastClkTime.valid() && stat.lastIod == corr._iod) {
476 double dt = corr._time - stat.lastClkTime;
477 int nominal = (corr._updateInt < unsigned(ssrUpdateInt.size())) ? ssrUpdateInt[corr._updateInt] : 0;
478 if (dt > 0.0 && dt <= max(2.0 * nominal, 60.0)) {
479 double jump = fabs(clk - stat.lastClk - corr._dotDClk * t_CST::c * dt);
480 stat.maxJump = max(stat.maxJump, jump);
481 if (jump > MAX_CLK_JUMP) {
482 stat.clkJumps += 1;
483 }
484 }
485 }
486 stat.lastClkTime = corr._time;
487 stat.lastIod = corr._iod;
488 stat.lastClk = clk;
489 }
490}
491
492// Code biases
493////////////////////////////////////////////////////////////////////////////
494void t_ssrQc::slotNewCodeBiases(QList<t_satCodeBias> codeBiases) {
495 QMutexLocker locker(&_mutex);
496 for (const t_satCodeBias& bias : codeBiases) {
497 bool sel = selected(bias._staID);
498 if (_excluded.contains(QString::fromStdString(bias._staID)) && bias._staID != _reference.toStdString()) continue;
499 char sys = bias._prn.system();
500 QString prn = QString::fromStdString(bias._prn.toString());
501 if (sel) {
502 checkInterval(bias._time);
503 newEpoch(bias._staID, sys, CBIAS, bias._time, bias._updateInt, prn);
504 }
505 QString staSys = QString("%1 %2").arg(bias._staID.c_str()).arg(sys);
506 QMap<QString, double> values;
507 for (const t_frqCodeBias& frqBias : bias._bias) {
508 QString sig = QString::fromStdString(frqBias._rnxType2ch);
509 _codeSignals[staSys].insert(sig);
510 values[sig] = frqBias._value;
511 }
512 if (!values.isEmpty()) {
513 _cbLast[QString::fromStdString(bias._staID)][prn] = values;
514 }
515 t_satStat& stat = _satStat[QString::fromStdString(bias._staID) + ' ' + prn];
516 QString sig1, sig2;
517 t_frequency::type frq1, frq2;
518 if (refSignals(sys, sig1, sig2, frq1, frq2)) {
519 stat.missingRefSig.clear();
520 if (!values.contains(sig1)) stat.missingRefSig.insert("C" + sig1);
521 if (!values.contains(sig2)) stat.missingRefSig.insert("C" + sig2);
522 stat.refBiasOK = stat.missingRefSig.isEmpty();
523 if (stat.refBiasOK) {
524 double channel = 0.0;
525 if (sys == 'R') {
526 t_eph* eph = _ephUser.ephLast(QString::fromStdString(bias._prn.toInternalString()));
527 if (!eph) {
528 stat.refBiasOK = false;
529 continue;
530 }
531 channel = eph->slotNum();
532 }
533 double f1 = t_CST::freq(frq1, channel);
534 double f2 = t_CST::freq(frq2, channel);
535 stat.biasIF = (f1 * f1 * values[sig1] - f2 * f2 * values[sig2]) / (f1 * f1 - f2 * f2);
536 }
537 }
538 }
539}
540
541// Phase biases
542////////////////////////////////////////////////////////////////////////////
543void t_ssrQc::slotNewPhaseBiases(QList<t_satPhaseBias> phaseBiases) {
544 QMutexLocker locker(&_mutex);
545 for (const t_satPhaseBias& bias : phaseBiases) {
546 if (!selected(bias._staID)) continue;
547 checkInterval(bias._time);
548 newEpoch(bias._staID, bias._prn.system(), PBIAS, bias._time, bias._updateInt,
549 QString::fromStdString(bias._prn.toString()));
550 QString staSys = QString("%1 %2").arg(bias._staID.c_str()).arg(bias._prn.system());
551 for (const t_frqPhaseBias& frqBias : bias._bias) {
552 _phaseSignals[staSys].insert(QString::fromStdString(frqBias._rnxType2ch));
553 }
554 // Yaw angle: new RTCM-SSR has an indicator, old RTCM-SSR and IGS-SSR always
555 // carry the field, providers without attitude information send zeros
556 t_satStat& stat = _satStat[QString::fromStdString(bias._staID) + ' ' +
557 QString::fromStdString(bias._prn.toString())];
558 stat.numPB++;
559 bool yawProvided = (bias._ssrFormat == ssrRtcmNew) ? (bias._satYawInfoInd != 0)
560 : (bias._yaw != 0.0 || bias._yawRate != 0.0);
561 if (yawProvided) {
562 stat.numYaw++;
563 t_yawLast& yl = _yawLast[QString::fromStdString(bias._staID)][QString::fromStdString(bias._prn.toString())];
564 double dt = 0.0;
565 if (bias._updateInt < unsigned(ssrUpdateInt.size())) {
566 dt = 0.5 * ssrUpdateInt[bias._updateInt]; // reference time in the middle of the interval
567 }
568 yl.time = bias._time + dt;
569 yl.yaw = bias._yaw;
570 yl.rate = bias._yawRate;
571 }
572 }
573}
574
575// Reference DCBs (bias(base) - bias(signal)) of a satellite: median of the
576// other ACs (one vote per AC, own AC left out, combinations not voting) or
577// the selected reference stream, or from the Bias SINEX file
578////////////////////////////////////////////////////////////////////////////
579bool t_ssrQc::refDcbs(const QString& staID, const QString& prn, const QString& base, bool fromFile,
580 QMap<QString, double>& dcbs, const QString& onlyAC, QMap<QString, int>* numVotes) {
581 dcbs.clear();
582 if (numVotes) {
583 numVotes->clear();
584 }
585 if (fromFile) {
586 if (!_bsxRef) {
587 return false;
588 }
589 char sys = prn[0].toLatin1();
590 t_ssrRefSig::t_pair pair;
591 if (!t_ssrRefSig::get(sys, pair)) {
592 return false;
593 }
594 // the DCBs do not depend on the coefficients (as long as they add up to 1)
595 double f1 = t_CST::freq(pair.frq1, 0.0);
596 double f2 = t_CST::freq(pair.frq2, 0.0);
597 double aIF1 = f1 * f1 / (f1 * f1 - f2 * f2);
598 double aIF2 = -f2 * f2 / (f1 * f1 - f2 * f2);
599 t_satCodeBias satCodeBias;
600 _bsxRef->determineSsrSatCodeBiases(prn, aIF1, aIF2, satCodeBias);
601 QMap<QString, double> osb;
602 for (const t_frqCodeBias& bb : satCodeBias._bias) {
603 osb[QString::fromStdString(bb._rnxType2ch)] = bb._value;
604 }
605 if (!osb.contains(base)) {
606 return false;
607 }
608 for (auto it = osb.constBegin(); it != osb.constEnd(); ++it) {
609 if (it.key() != base) {
610 dcbs[it.key()] = osb[base] - it.value();
611 }
612 }
613 return !dcbs.isEmpty();
614 }
615
616 // per AC: DCBs of its streams
617 QMap<QString, QMap<QString, QVector<double> > > acDcbs; // AC, signal
618 for (auto itS = _cbLast.constBegin(); itS != _cbLast.constEnd(); ++itS) {
619 const QString& sta = itS.key();
620 if (sta == staID) {
621 continue;
622 }
623 if (_reference == "CONSENSUS") {
624 if (!votes(sta)) {
625 continue;
626 }
627 if (onlyAC.isEmpty() ? (acOf(sta) == acOf(staID))
628 : (solOf(sta) != onlyAC || solOf(sta) == solOf(staID))) {
629 continue;
630 }
631 }
632 else if (sta != _reference) {
633 continue;
634 }
635 if (!itS.value().contains(prn)) {
636 continue;
637 }
638 const QMap<QString, double>& bb = itS.value()[prn];
639 if (!bb.contains(base)) {
640 continue;
641 }
642 for (auto it = bb.constBegin(); it != bb.constEnd(); ++it) {
643 if (it.key() != base) {
644 acDcbs[solOf(sta)][it.key()].append(bb[base] - it.value());
645 }
646 }
647 }
648 QMap<QString, QVector<double> > votesPerSig;
649 for (auto itA = acDcbs.constBegin(); itA != acDcbs.constEnd(); ++itA) {
650 for (auto it = itA.value().constBegin(); it != itA.value().constEnd(); ++it) {
651 votesPerSig[it.key()].append(median(it.value()));
652 }
653 }
654 for (auto it = votesPerSig.constBegin(); it != votesPerSig.constEnd(); ++it) {
655 dcbs[it.key()] = median(it.value());
656 if (numVotes) {
657 (*numVotes)[it.key()] = it.value().size();
658 }
659 }
660 return !dcbs.isEmpty();
661}
662
663// Code bias check: the DCBs of the stream (relative to the first reference
664// signal, otherwise to another common signal) compared with a reference;
665// DCBs do not depend on the clock datum. A sign opposite to the reference
666// for most DCBs indicates the SINEX instead of the SSR sign convention.
667// References: the Bias SINEX file, the selected reference stream, or each
668// other AC separately (a median of two ACs cannot decide which one is wrong)
669////////////////////////////////////////////////////////////////////////////
670void t_ssrQc::compareCodeBiases(const QString& staID, char sys, QStringList& lines) {
671 const double MIN_DCB = 0.3; // [m] DCBs used for the sign check
672 const double MIN_DEV = 0.5; // [m] deviating DCBs: > max(MIN_DEV, DEV_FACTOR x median)
673 const int MIN_VOTE = 3; // ACs needed for a consensus DCB (deviating satellites)
674 if (!_cbLast.contains(staID)) {
675 return;
676 }
677 t_ssrRefSig::t_pair pair;
678 if (!t_ssrRefSig::get(sys, pair)) {
679 return;
680 }
681 const QString ref1 = QString::fromStdString(pair.sig1());
682
683 class t_cmp {
684 public:
685 unsigned numSat = 0, numSign = 0, numOpp = 0;
686 double sumSqr = 0.0;
687 QVector<double> absDiffs;
688 QMap<QString, QPair<QString, double> > maxDiff; // PRN: signal, difference
689 QSet<QString> bases;
690 bool opposite() const {return numSign >= 5 && numOpp > numSign / 2;}
691 double rms() const {return absDiffs.isEmpty() ? 0.0 : sqrt(sumSqr / absDiffs.size());}
692 };
693 // mode 0: file, 1: reference stream or median of the other ACs, 2: one other AC
694 auto compare = [&](int mode, const QString& onlyAC) {
695 t_cmp cc;
696 for (auto itP = _cbLast[staID].constBegin(); itP != _cbLast[staID].constEnd(); ++itP) {
697 const QString& prn = itP.key();
698 if (prn[0].toLatin1() != sys) {
699 continue;
700 }
701 const QMap<QString, double>& bb = itP.value();
702 QStringList candidates = QStringList() << ref1 << bb.keys();
703 QMap<QString, double> dcbs;
704 QMap<QString, int> numVotes;
705 QString base;
706 for (const QString& cand : candidates) {
707 if (bb.contains(cand) && refDcbs(staID, prn, cand, mode == 0, dcbs, onlyAC, &numVotes)) {
708 base = cand;
709 break;
710 }
711 }
712 if (base.isEmpty()) {
713 continue;
714 }
715 bool used = false;
716 for (auto it = dcbs.constBegin(); it != dcbs.constEnd(); ++it) {
717 if (!bb.contains(it.key())) {
718 continue;
719 }
720 if (mode == 1 && _reference == "CONSENSUS" && numVotes.value(it.key()) < MIN_VOTE) {
721 continue;
722 }
723 double dcb = bb[base] - bb[it.key()];
724 double diff = dcb - it.value();
725 used = true;
726 cc.sumSqr += diff * diff;
727 cc.absDiffs.append(fabs(diff));
728 if (fabs(diff) > fabs(cc.maxDiff.value(prn).second)) {
729 cc.maxDiff[prn] = qMakePair("C" + it.key(), diff);
730 }
731 if (fabs(it.value()) >= MIN_DCB) {
732 cc.numSign++;
733 if (dcb * it.value() < 0.0) {
734 cc.numOpp++;
735 }
736 }
737 }
738 if (used) {
739 cc.numSat++;
740 cc.bases.insert("C" + base);
741 }
742 }
743 return cc;
744 };
745 auto baseStr = [](const t_cmp& cc) {
746 QStringList ll = cc.bases.values();
747 ll.sort();
748 return ll.join("/");
749 };
750 auto deviating = [&](const t_cmp& cc, const QString& refName) {
751 if (cc.absDiffs.isEmpty() || cc.opposite()) {
752 return;
753 }
754 double limit = std::max(MIN_DEV, DEV_FACTOR * median(cc.absDiffs));
755 QStringList devs;
756 for (auto it = cc.maxDiff.constBegin(); it != cc.maxDiff.constEnd(); ++it) {
757 if (fabs(it.value().second) > limit) {
758 devs << QString("%1(%2 %3)").arg(it.key()).arg(it.value().first).arg(it.value().second, 0, 'f', 2);
759 }
760 }
761 if (!devs.isEmpty()) {
762 lines << QString("satellites with DCBs deviating from %1 [m]: %2").arg(refName).arg(devs.join(" "));
763 }
764 };
765
766 // Bias SINEX file or the selected reference stream
767 for (int mode = 0; mode < 2; mode++) {
768 if ((mode == 0 && !_bsxRef) || (mode == 1 && _reference == "CONSENSUS")) {
769 continue;
770 }
771 QString refName = (mode == 0) ? QString("Bias SINEX file %1").arg(_bsxName) : _reference;
772 t_cmp cc = compare(mode, QString());
773 if (cc.absDiffs.isEmpty()) {
774 continue;
775 }
776 lines << QString("code biases compared with %1 (DCBs to %2, %3 satellites, %4 DCBs): RMS of the differences "
777 "%5 m, opposite sign for %6 of %7 DCBs (|DCB| >= %8 m)")
778 .arg(refName).arg(baseStr(cc)).arg(cc.numSat).arg(cc.absDiffs.size())
779 .arg(cc.rms(), 0, 'f', 3).arg(cc.numOpp).arg(cc.numSign).arg(MIN_DCB, 0, 'f', 1);
780 if (cc.opposite()) {
781 lines << QString("code biases with the opposite sign of %1 for %2 of %3 DCBs - "
782 "SINEX sign convention instead of the SSR one?").arg(refName).arg(cc.numOpp).arg(cc.numSign);
783 }
784 deviating(cc, refName);
785 }
786
787 // Each other AC separately, and the median of the other ACs for deviating satellites
788 if (_reference == "CONSENSUS") {
789 QSet<QString> acs;
790 for (auto itS = _cbLast.constBegin(); itS != _cbLast.constEnd(); ++itS) {
791 if (itS.key() != staID && votes(itS.key()) && solOf(itS.key()) != solOf(staID)) {
792 acs.insert(solOf(itS.key()));
793 }
794 }
795 QStringList acList = acs.values();
796 acList.sort();
797 QStringList items;
798 QStringList oppACs;
799 int numACs = 0;
800 QSet<QString> bases;
801 for (const QString& ac : acList) {
802 t_cmp cc = compare(2, ac);
803 if (cc.absDiffs.isEmpty()) {
804 continue;
805 }
806 bases.unite(cc.bases);
807 items << QString("%1 RMS %2 m, opposite sign %3/%4").arg(ac).arg(cc.rms(), 0, 'f', 2)
808 .arg(cc.numOpp).arg(cc.numSign);
809 // the sign rule counts the solutions of other ACs only
810 if (acOfSol(ac) != acOf(staID)) {
811 numACs++;
812 if (cc.opposite()) {
813 oppACs << ac;
814 }
815 }
816 }
817 if (!items.isEmpty()) {
818 QStringList ll = bases.values();
819 ll.sort();
820 lines << QString("code biases compared with the other solutions (DCBs to %1): %2").arg(ll.join("/")).arg(items.join("; "));
821 if (numACs >= 2 && oppACs.size() > numACs / 2) {
822 lines << QString("code biases with the opposite sign of %1 of %2 solutions of other ACs (%3) - "
823 "SINEX sign convention instead of the SSR one?")
824 .arg(oppACs.size()).arg(numACs).arg(oppACs.join(" "));
825 }
826 deviating(compare(1, QString()), QString("the median of the other ACs (at least %1 solutions)").arg(MIN_VOTE));
827 }
828 }
829}
830
831// Reference point of the orbits of a stream: APC (SSRA...), CoM (SSRC...);
832// the combined stream INTERNAL refers to the APC
833////////////////////////////////////////////////////////////////////////////
834char t_ssrQc::refPoint(const QString& staID) const {
835 if (staID == "INTERNAL") {
836 return 'A'; // the combined corrections always refer to the APC
837 }
838 const QString& sta = staID;
839 if (sta.startsWith("SSRA")) return 'A';
840 if (sta.startsWith("SSRC")) return 'C';
841 return '?';
842}
843
844// Keep the recent orbit and clock corrections of a stream
845////////////////////////////////////////////////////////////////////////////
846void t_ssrQc::storeHist(const t_orbCorr* orb, const t_clkCorr* clk) {
847 if (orb) {
848 QMap<double, t_orbCorr>& hist = _orbHist[QString::fromStdString(orb->_staID)]
849 [QString::fromStdString(orb->_prn.toInternalString())];
850 hist[tKey(orb->_time)] = *orb;
851 while (!hist.isEmpty() && hist.firstKey() < tKey(orb->_time) - HIST_LENGTH) {
852 hist.erase(hist.begin());
853 }
854 }
855 if (clk) {
856 QMap<double, t_clkCorr>& hist = _clkHist[QString::fromStdString(clk->_staID)]
857 [QString::fromStdString(clk->_prn.toInternalString())];
858 hist[tKey(clk->_time)] = *clk;
859 while (!hist.isEmpty() && hist.firstKey() < tKey(clk->_time) - HIST_LENGTH) {
860 hist.erase(hist.begin());
861 }
862 }
863}
864
865// Compare the orbits and clocks of all streams at epoch tt
866////////////////////////////////////////////////////////////////////////////
867void t_ssrQc::compareEpoch(const bncTime& tt) {
868 double tk = tKey(tt);
869
870 // Satellite positions and clocks of all streams (latest corrections <= tt)
871 QMap<QString, QMap<QString, t_satCrd> > crd; // stream, PRN (external)
872 QMapIterator<QString, QMap<QString, QMap<double, t_clkCorr> > > itSta(_clkHist);
873 while (itSta.hasNext()) {
874 itSta.next();
875 const QString& staID = itSta.key();
876 QMapIterator<QString, QMap<double, t_clkCorr> > itPrn(itSta.value());
877 while (itPrn.hasNext()) {
878 itPrn.next();
879 const QString& prnInt = itPrn.key();
880 QMap<double, t_clkCorr>::const_iterator itC = itPrn.value().upperBound(tk);
881 if (itC == itPrn.value().constBegin()) {
882 continue;
883 }
884 const t_clkCorr& clk = (--itC).value();
885 const QMap<double, t_orbCorr>& orbHist = _orbHist[staID][prnInt];
886 QMap<double, t_orbCorr>::const_iterator itO = orbHist.upperBound(tk);
887 if (itO == orbHist.constBegin()) {
888 continue;
889 }
890 const t_orbCorr& orb = (--itO).value();
891 t_eph* eph = _ephUser.ephWithIOD(prnInt, orb._iod);
892 if (eph && outDatedBcep(eph, tt) && fabs(tt - eph->TOC()) > EPH_IOD_REUSE) {
893 eph = 0;
894 }
895 if (!eph || clk._iod != orb._iod) {
896 continue;
897 }
898 eph->setOrbCorr(&orb);
899 eph->setClkCorr(&clk);
900 ColumnVector xc(6), vv(3);
901 if (eph->getCrd(tt, xc, vv, true) != success) {
902 continue;
903 }
904 t_satCrd& sc = crd[staID][QString::fromStdString(clk._prn.toString())];
905 sc.xx = xc.Rows(1,3);
906 sc.vv = vv;
907 sc.clk = xc(4) * t_CST::c;
908 sc.refPoint = refPoint(staID);
909 // clock referred to the reference signals (as in the combination):
910 // + B_IF of the stream's code biases; the combination (INTERNAL)
911 // refers to them already
912 sc.clkOK = (staID == "INTERNAL");
913 QString satKey = staID + ' ' + QString::fromStdString(clk._prn.toString());
914 if (!sc.clkOK && _satStat.contains(satKey) && _satStat[satKey].refBiasOK) {
915 sc.clk += _satStat[satKey].biasIF;
916 sc.clkOK = true;
917 }
918 }
919 }
920
921 // Satellites per system
922 QMap<char, QSet<QString> > satsSys;
923 QMapIterator<QString, QMap<QString, t_satCrd> > itCrd(crd);
924 while (itCrd.hasNext()) {
925 itCrd.next();
926 for (const QString& prn : itCrd.value().keys()) {
927 satsSys[prn[0].toLatin1()].insert(prn);
928 }
929 }
930
931 QMapIterator<char, QSet<QString> > itSys(satsSys);
932 while (itSys.hasNext()) {
933 itSys.next();
934 const QSet<QString>& sats = itSys.value();
935 QStringList staList;
936 for (const QString& staID : crd.keys()) {
937 for (const QString& prn : sats) {
938 if (crd[staID].contains(prn)) {
939 staList << staID;
940 break;
941 }
942 }
943 }
944
945 // Consensus: one value per AC (median of its streams), so that an AC
946 // with several streams (formats, APC/CoM) has one vote; the streams of
947 // the AC compared are left out (leave-one-AC-out)
948 auto acClocks = [&](const QString& prn, const QMap<QString, double>& off, bool useOff) {
949 QMap<QString, QVector<double> > perAC;
950 for (const QString& oo : staList) {
951 if (!votes(oo) || !crd[oo].contains(prn) || !crd[oo][prn].clkOK) continue;
952 if (useOff && !off.contains(oo)) continue;
953 perAC[solOf(oo)] << crd[oo][prn].clk - (useOff ? off[oo] : 0.0);
954 }
955 QMap<QString, double> res;
956 for (const QString& ac : perAC.keys()) res[ac] = median(perAC[ac]);
957 return res;
958 };
959
960 // Clock datum: offset of each stream w.r.t. the per-satellite consensus
961 // of all ACs (or w.r.t. the reference stream)
962 QMap<QString, double> offset;
963 QMap<QString, double> noOff;
964 QMap<QString, QMap<QString, double> > rawAC; // PRN, AC
965 if (_reference == "CONSENSUS") {
966 for (const QString& prn : sats) rawAC[prn] = acClocks(prn, noOff, false);
967 }
968 for (const QString& staID : staList) {
969 QVector<double> diffs;
970 for (const QString& prn : sats) {
971 if (!crd[staID].contains(prn) || !crd[staID][prn].clkOK) continue;
972 if (_reference == "CONSENSUS") {
973 if (rawAC[prn].size() >= 3) diffs << crd[staID][prn].clk - median(rawAC[prn].values().toVector());
974 }
975 else if (crd.value(_reference).contains(prn) && crd[_reference][prn].clkOK) {
976 diffs << crd[staID][prn].clk - crd[_reference][prn].clk;
977 }
978 }
979 if (!diffs.isEmpty()) {
980 offset[staID] = median(diffs);
981 }
982 }
983 QMap<QString, QMap<QString, double> > alignedAC; // PRN, AC
984 if (_reference == "CONSENSUS") {
985 for (const QString& prn : sats) alignedAC[prn] = acClocks(prn, offset, true);
986 }
987
988 // Differences of each selected stream to its reference
989 for (const QString& staID : staList) {
990 if (!selected(staID.toStdString()) || staID == _reference) {
991 continue;
992 }
993 const QString ownAC = acOf(staID);
994 QMap<QString, double> dClk;
995 QMap<QString, ColumnVector> dRac;
996 for (const QString& prn : sats) {
997 if (!crd[staID].contains(prn)) continue;
998 const t_satCrd& sc = crd[staID][prn];
999 QVector<double> clks, xs[3];
1000 if (_reference == "CONSENSUS") {
1001 QMapIterator<QString, double> itA(alignedAC[prn]);
1002 while (itA.hasNext()) {
1003 itA.next();
1004 if (acOfSol(itA.key()) != ownAC) clks << itA.value();
1005 }
1006 // orbits: per AC the median of its streams with the same reference point
1007 QMap<QString, QVector<double> > perAC[3];
1008 for (const QString& oo : staList) {
1009 if (!votes(oo) || acOf(oo) == ownAC || !crd[oo].contains(prn)) continue;
1010 const t_satCrd& rc = crd[oo][prn];
1011 if (rc.refPoint != sc.refPoint || sc.refPoint == '?') continue;
1012 for (int ii = 0; ii < 3; ii++) perAC[ii][solOf(oo)] << rc.xx[ii];
1013 }
1014 for (const QString& ac : perAC[0].keys()) {
1015 for (int ii = 0; ii < 3; ii++) xs[ii] << median(perAC[ii][ac]);
1016 }
1017 if (clks.size() < 2) clks.clear();
1018 if (xs[0].size() < 2) for (int ii = 0; ii < 3; ii++) xs[ii].clear();
1019 }
1020 else {
1021 if (!crd.value(_reference).contains(prn)) continue;
1022 const t_satCrd& rc = crd[_reference][prn];
1023 if (rc.clkOK) clks << rc.clk;
1024 if (rc.refPoint == sc.refPoint && sc.refPoint != '?') {
1025 for (int ii = 0; ii < 3; ii++) xs[ii] << rc.xx[ii];
1026 }
1027 }
1028 if (sc.clkOK && offset.contains(staID) && !clks.isEmpty()) {
1029 dClk[prn] = sc.clk - offset[staID] - median(clks);
1030 }
1031 else if (!sc.clkOK) {
1032 _cmpStat[staID][prn].clkNotReferred += 1;
1033 }
1034 if (!xs[0].isEmpty()) {
1035 ColumnVector dx(3);
1036 for (int ii = 0; ii < 3; ii++) dx[ii] = sc.xx[ii] - median(xs[ii]);
1037 ColumnVector er = sc.xx / sc.xx.NormFrobenius();
1038 ColumnVector ec = crossproduct(sc.xx, sc.vv);
1039 ec /= ec.NormFrobenius();
1040 ColumnVector ea = crossproduct(ec, er);
1041 ColumnVector rac(3);
1042 rac[0] = DotProduct(dx, er);
1043 rac[1] = DotProduct(dx, ea);
1044 rac[2] = DotProduct(dx, ec);
1045 dRac[prn] = rac;
1046 }
1047 }
1048 if (dClk.isEmpty() && dRac.isEmpty()) {
1049 continue;
1050 }
1051 // remaining common clock offset of this stream and epoch
1052 double off = dClk.isEmpty() ? 0.0 : median(dClk.values().toVector());
1053 _cmpEpochs[staID] += 1;
1054 QSet<QString> prns;
1055 for (const QString& prn : dClk.keys()) prns.insert(prn);
1056 for (const QString& prn : dRac.keys()) prns.insert(prn);
1057 for (const QString& prn : prns) {
1058 t_cmpStat& stat = _cmpStat[staID][prn];
1059 if (dRac.contains(prn)) {
1060 const ColumnVector& rac = dRac[prn];
1061 stat.numOrb += 1;
1062 stat.sR += rac[0] * rac[0];
1063 stat.sA += rac[1] * rac[1];
1064 stat.sC += rac[2] * rac[2];
1065 }
1066 if (dClk.contains(prn)) {
1067 double dc = dClk[prn] - off;
1068 if (_antex && _yawLast.value(staID).contains(prn)) {
1069 attitudeSample(staID, prn, tt, crd[staID][prn].xx, crd[staID][prn].vv, dc);
1070 }
1071 stat.numClk += 1;
1072 stat.sClk += dc * dc;
1073 stat.mClk += dc;
1074 if (dRac.contains(prn)) {
1075 double sis = dRac[prn][0] - dc;
1076 stat.numSis += 1;
1077 stat.sSis += sis * sis;
1078 stat.mSis += sis;
1079 }
1080 }
1081 }
1082 }
1083 }
1084}
1085
1086// Attitude check: one sample per comparison epoch for a stream sending the
1087// yaw angle. The yaw difference of BNC's attitude model ('Computed') minus
1088// the stream is followed continuously over time; the predicted clock
1089// difference of the stream to an AC following the model is
1090// lambda_IF * dpsi / 2pi (see combination, dC_att). A turn starts when the
1091// yaw difference changes by more than ATT_ONSET against the samples 5-15
1092// minutes before, and ends when it is stable again for ATT_STABLE_T.
1093////////////////////////////////////////////////////////////////////////////
1094void t_ssrQc::attitudeSample(const QString& staID, const QString& prn, const bncTime& tt,
1095 const ColumnVector& xx, const ColumnVector& vv, double dc) {
1096 const t_yawLast& yl = _yawLast[staID][prn];
1097 double age = tt - yl.time;
1098 if (!yl.time.valid() || fabs(age) > 120.0) {
1099 return;
1100 }
1101 double psiAC = yl.yaw + yl.rate * age;
1102
1103 // model yaw, once per satellite and epoch (model state in time order)
1104 double tk = tKey(tt);
1105 if (tk != _psiModelT) {
1106 _psiModel.clear();
1107 _psiModelT = tk;
1108 }
1109 if (!_psiModel.contains(prn)) {
1110 ColumnVector dx(3);
1111 double psiM = 0.0;
1112 double Mjd = tt.mjd() + tt.daysec() / 86400.0;
1113 if (_antex->satCoMcorrection(prn, Mjd, xx, vv, dx, bncAntex::ATT_COMPUTED, 0.0,
1114 "ssrQc", &psiM) != success) {
1115 return;
1116 }
1117 _psiModel[prn] = psiM;
1118 }
1119
1120 // ionosphere-free wavelength of the reference signals, c / (f1 + f2)
1121 QString sig1, sig2;
1122 t_frequency::type frq1, frq2;
1123 if (!refSignals(prn[0].toLatin1(), sig1, sig2, frq1, frq2)) {
1124 return;
1125 }
1126 double lambda = t_CST::c / (t_CST::freq(frq1, 0) + t_CST::freq(frq2, 0));
1127
1128 t_attTrack& tr = _attTrack[staID][prn];
1129 if (tr.hasLast && tk - tr.lastT > ATT_MAX_GAP) {
1130 if (tr.active) {
1131 attitudeEvent(staID, prn, tr, "interrupted by a gap");
1132 }
1133 tr = t_attTrack();
1134 }
1135 double dpsi = remainder(_psiModel[prn] - psiAC, 2.0 * M_PI);
1136 if (tr.hasLast) {
1137 dpsi = tr.lastDPsi + remainder(dpsi - tr.lastDPsi, 2.0 * M_PI);
1138 }
1139 tr.hasLast = true;
1140 tr.lastDPsi = dpsi;
1141 tr.lastT = tk;
1142 t_attSample smp;
1143 smp.t = tk;
1144 smp.dc = dc;
1145 smp.p = lambda * dpsi / (2.0 * M_PI);
1146 smp.dpsi = dpsi;
1147
1148 if (!tr.active) {
1149 tr.hist << smp;
1150 while (!tr.hist.isEmpty() && tk - tr.hist.first().t > ATT_REF_FROM) {
1151 tr.hist.removeFirst();
1152 }
1153 // reference: samples 5 to 15 minutes before
1154 QVector<double> rDc, rP, rPsi;
1155 for (const t_attSample& hh : tr.hist) {
1156 if (tk - hh.t >= ATT_REF_TO) {
1157 rDc << hh.dc; rP << hh.p; rPsi << hh.dpsi;
1158 }
1159 }
1160 if (rPsi.size() >= 3 && fabs(dpsi - median(rPsi)) > ATT_ONSET) {
1161 tr.active = true;
1162 tr.dc0 = median(rDc);
1163 tr.p0 = median(rP);
1164 tr.dpsi0 = median(rPsi);
1165 for (const t_attSample& hh : tr.hist) {
1166 if (tk - hh.t < ATT_REF_TO) tr.event << hh; // the beginning of the turn
1167 }
1168 tr.hist.clear();
1169 }
1170 return;
1171 }
1172
1173 tr.event << smp;
1174 // end: yaw difference stable for ATT_STABLE_T, or the turn too long
1175 double tStart = tr.event.first().t;
1176 bool stable = (tk - tStart >= ATT_STABLE_T);
1177 if (stable) {
1178 for (int ii = tr.event.size() - 1; ii >= 0 && tk - tr.event[ii].t <= ATT_STABLE_T; ii--) {
1179 if (fabs(tr.event[ii].dpsi - dpsi) > ATT_STABLE) {
1180 stable = false;
1181 break;
1182 }
1183 }
1184 }
1185 if (stable || tk - tStart > ATT_MAX_TURN) {
1186 attitudeEvent(staID, prn, tr, stable ? "" : "closed after 3 h");
1187 double keepPsi = tr.lastDPsi, keepT = tr.lastT;
1188 tr = t_attTrack();
1189 tr.hasLast = true;
1190 tr.lastDPsi = keepPsi;
1191 tr.lastT = keepT;
1192 tr.hist << smp;
1193 }
1194}
1195
1196// Assessment of a turn: observed clock change of the stream relative to the
1197// consensus against the predicted one (regression slope, RMS reduction)
1198////////////////////////////////////////////////////////////////////////////
1199void t_ssrQc::attitudeEvent(const QString& staID, const QString& prn, t_attTrack& tr,
1200 const QString& end) {
1201 if (tr.event.size() < 3) {
1202 return;
1203 }
1204 double sxx = 0.0, sxy = 0.0, syy = 0.0, srr = 0.0, maxX = 0.0, maxPsi = 0.0;
1205 for (const t_attSample& ss : tr.event) {
1206 double x = ss.p - tr.p0;
1207 double y = ss.dc - tr.dc0;
1208 sxx += x * x; sxy += x * y; syy += y * y; srr += (y - x) * (y - x);
1209 if (fabs(x) > fabs(maxX)) maxX = x;
1210 if (fabs(ss.dpsi - tr.dpsi0) > fabs(maxPsi)) maxPsi = ss.dpsi - tr.dpsi0;
1211 }
1212 int nn = tr.event.size();
1213 // changes at the end of the turn: mean of the last 5 minutes
1214 double tEnd = tr.event.last().t, xEnd = 0.0, yEnd = 0.0;
1215 int nEnd = 0;
1216 for (const t_attSample& ss : tr.event) {
1217 if (tEnd - ss.t <= 300.0) {
1218 xEnd += ss.p - tr.p0; yEnd += ss.dc - tr.dc0; ++nEnd;
1219 }
1220 }
1221 xEnd /= nEnd; yEnd /= nEnd;
1222 double rmsObs = sqrt(syy / nn);
1223 double rmsRes = sqrt(srr / nn);
1224 double slope = (sxx > 0.0) ? sxy / sxx : 0.0;
1225
1226 bncTime t1, t2;
1227 t1.set(int(tr.event.first().t / 604800.0), fmod(tr.event.first().t, 604800.0));
1228 t2.set(int(tEnd / 604800.0), fmod(tEnd, 604800.0));
1229 QString verdict;
1230 if (fabs(maxX) < ATT_MIN_PRED) {
1231 // the consensus is assumed to follow BNC's attitude model; a larger clock
1232 // change shows that the other ACs model this turn differently
1233 verdict = (rmsObs >= ATT_MIN_PRED) ? "clock change not explained by the yaw difference "
1234 "(the other ACs model this turn differently?)"
1235 : "predicted change too small to assess";
1236 }
1237 else if (slope > 0.7 && slope < 1.3 && rmsRes < 0.5 * rmsObs) {
1238 verdict = "SSR yaw consistent with the clocks";
1239 }
1240 else {
1241 verdict = "SSR yaw NOT consistent with the clocks";
1242 }
1243 QString line = QString("attitude check %1 %2-%3: yaw difference to the model %4 deg, clock change vs. "
1244 "consensus %5 cm, predicted %6 cm (max %7 cm), slope %8, clock RMS %9 -> %10 cm: %11")
1245 .arg(prn).arg(QString::fromStdString(t1.timestr(0))).arg(QString::fromStdString(t2.timestr(0)))
1246 .arg(maxPsi * 180.0 / M_PI, 0, 'f', 0)
1247 .arg(yEnd * 100.0, 0, 'f', 1).arg(xEnd * 100.0, 0, 'f', 1).arg(maxX * 100.0, 0, 'f', 1)
1248 .arg(slope, 0, 'f', 2).arg(rmsObs * 100.0, 0, 'f', 1).arg(rmsRes * 100.0, 0, 'f', 1)
1249 .arg(verdict);
1250 if (!end.isEmpty()) {
1251 line += " (" + end + ")";
1252 }
1253 _attEvents[staID][prn[0].toLatin1()] << line;
1254}
1255
1256// Ionospheric model (VTEC)
1257////////////////////////////////////////////////////////////////////////////
1258void t_ssrQc::slotNewTec(t_vTec vTec) {
1259 QMutexLocker locker(&_mutex);
1260 if (vTec._layers.empty()) {
1261 return;
1262 }
1263 QString sta = QString::fromStdString(vTec._staID);
1264 if (!_excluded.contains(sta) || sta == _reference) {
1265 QMap<double, t_vTec>& hist = _vtecHist[sta];
1266 hist[tKey(vTec._time)] = vTec;
1267 while (!hist.isEmpty() && tKey(vTec._time) - hist.firstKey() > VTEC_HIST) {
1268 hist.erase(hist.begin());
1269 }
1270 }
1271 if (selected(vTec._staID)) {
1272 newEpoch(vTec._staID, 'V', VTEC, vTec._time, vTec._updateInt, "VTEC");
1273 QStringList layers;
1274 for (const t_vTecLayer& layer : vTec._layers) {
1275 layers << QString("%1 km (degree/order %2/%3)").arg(layer._height / 1000.0, 0, 'f', 0)
1276 .arg(layer._C.Nrows() - 1).arg(layer._C.Ncols() - 1);
1277 }
1278 _vtecStat[sta].model = QString("%1 layer(s): %2").arg(vTec._layers.size()).arg(layers.join(", "));
1279 }
1280 checkInterval(vTec._time);
1281}
1282
1283// VTEC [TECU] of a model at a geocentric latitude and longitude [rad] and
1284// epoch: spherical harmonics in the sun-fixed frame as used in PPP, sum of
1285// the layers (negative layer values set to zero as in PPP)
1286////////////////////////////////////////////////////////////////////////////
1287double t_ssrQc::vtecAt(const t_vTec& vTec, double lat, double lon, const bncTime& tt, bool& negative) {
1288 double epoch = fmod(tt.gpssec(), 86400.0);
1289 double lonS = fmod(lon + (epoch - 50400.0) * M_PI / 43200.0, 2.0 * M_PI);
1290 double vtec = 0.0;
1291 negative = false;
1292 for (const t_vTecLayer& layer : vTec._layers) {
1293 int N = layer._C.Nrows() - 1;
1294 int M = layer._C.Ncols() - 1;
1295 double vv = 0.0;
1296 for (int n = 0; n <= N; n++) {
1297 for (int m = 0; m <= min(n, M); m++) {
1298 double pnm = associatedLegendreFunction(n, m, sin(lat));
1299 double fac = (m == 0) ? sqrt(2.0 * n + 1.0)
1300 : sqrt(2.0 * (2.0 * n + 1.0) * factorial(n - m) / factorial(n + m));
1301 vv += (layer._C[n][m] * cos(m * lonS) + layer._S[n][m] * sin(m * lonS)) * pnm * fac;
1302 }
1303 }
1304 if (vv < 0.0) {
1305 negative = true;
1306 vv = 0.0;
1307 }
1308 vtec += vv;
1309 }
1310 return vtec;
1311}
1312
1313// VTEC comparison: each model (latest one not older than twice its update
1314// interval) evaluated on a global grid and compared with the median of the
1315// other AC solutions (one vote per solution, own AC left out, combination
1316// products not voting) or with the reference stream
1317////////////////////////////////////////////////////////////////////////////
1318void t_ssrQc::compareVtec(const bncTime& tt) {
1319 double tk = tKey(tt);
1320
1321 QMap<QString, const t_vTec*> models;
1322 QMapIterator<QString, QMap<double, t_vTec> > itH(_vtecHist);
1323 while (itH.hasNext()) {
1324 itH.next();
1325 QMap<double, t_vTec>::const_iterator it = itH.value().upperBound(tk);
1326 if (it == itH.value().constBegin()) {
1327 continue;
1328 }
1329 --it;
1330 const t_vTec& vTec = it.value();
1331 double interval = (vTec._updateInt < unsigned(ssrUpdateInt.size()) && ssrUpdateInt[vTec._updateInt] > 0)
1332 ? ssrUpdateInt[vTec._updateInt] : VTEC_CMP_SAMPL;
1333 double maxAge = min(3600.0, max(120.0, 2.0 * interval));
1334 if (tk - it.key() <= maxAge) {
1335 models[itH.key()] = &vTec;
1336 }
1337 }
1338 if (models.isEmpty()) {
1339 return;
1340 }
1341
1342 // grid values of all models
1343 QVector<QPair<double, double> > grid;
1344 for (double lat = -80.0; lat <= 80.0 + 1e-6; lat += VTEC_GRID_LAT) {
1345 for (double lon = -180.0; lon < 180.0 - 1e-6; lon += VTEC_GRID_LON) {
1346 grid << QPair<double, double>(lat * M_PI / 180.0, lon * M_PI / 180.0);
1347 }
1348 }
1349 QMap<QString, QVector<double> > val;
1350 QMapIterator<QString, const t_vTec*> itM(models);
1351 while (itM.hasNext()) {
1352 itM.next();
1353 QVector<double>& vv = val[itM.key()];
1354 vv.resize(grid.size());
1355 bool selectedSta = selected(itM.key().toStdString());
1356 t_vtecStat* st = selectedSta ? &_vtecStat[itM.key()] : 0;
1357 for (int ii = 0; ii < grid.size(); ii++) {
1358 bool negative = false;
1359 vv[ii] = vtecAt(*itM.value(), grid[ii].first, grid[ii].second, tt, negative);
1360 if (st) {
1361 if (st->numGrid == 0 || vv[ii] < st->minVal) st->minVal = vv[ii];
1362 if (st->numGrid == 0 || vv[ii] > st->maxVal) st->maxVal = vv[ii];
1363 st->numGrid += 1;
1364 if (negative) st->numNeg += 1;
1365 }
1366 }
1367 }
1368
1369 // comparison
1370 for (const QString& staID : models.keys()) {
1371 if (!selected(staID.toStdString())) {
1372 continue;
1373 }
1374 QMap<QString, QStringList> perSol; // solution: streams
1375 for (const QString& oo : models.keys()) {
1376 if (oo == staID) continue;
1377 if (_reference == "CONSENSUS") {
1378 if (!votes(oo) || acOf(oo) == acOf(staID)) continue;
1379 }
1380 else if (oo != _reference) {
1381 continue;
1382 }
1383 perSol[solOf(oo)] << oo;
1384 }
1385 if (perSol.isEmpty()) {
1386 continue;
1387 }
1388 t_vtecStat& st = _vtecStat[staID];
1389 st.numCmp += 1;
1390 st.numSol += perSol.size();
1391 for (int ii = 0; ii < grid.size(); ii++) {
1392 QVector<double> solVal;
1393 for (const QStringList& streams : perSol) {
1394 QVector<double> vv;
1395 for (const QString& oo : streams) vv << val[oo][ii];
1396 solVal << median(vv);
1397 }
1398 double diff = val[staID][ii] - median(solVal);
1399 st.numDiff += 1;
1400 st.sDiff += diff;
1401 st.sDiff2 += diff * diff;
1402 st.maxAbs = max(st.maxAbs, fabs(diff));
1403 }
1404 }
1405}
1406
1407
1408// Write the report of the current interval and reset the statistics
1409////////////////////////////////////////////////////////////////////////////
1410void t_ssrQc::writeReport(bool final) {
1411 if (_epoStat.isEmpty()) {
1412 return;
1413 }
1414 bncTime endTime = final ? _lastTime : _intervalStart + _interval;
1415
1416 // IODs not received: early or unknown (pending ones younger than
1417 // EPH_EARLY_MAX are decided in the next interval)
1418 for (const QString& satKey : _pendingIod.keys()) {
1419 resolvePendingIod(satKey, final);
1420 }
1421
1422 // Healthy and current broadcast ephemerides per system
1423 QMap<char, QSet<QString> > ephSats;
1424 for (const QString& prnInt : _ephUser.prnList()) {
1425 t_eph* eph = _ephUser.ephLast(prnInt);
1426 if (eph && eph->checkState() == t_eph::ok && !outDatedBcep(eph, endTime)) {
1427 ephSats[eph->prn().system()].insert(QString::fromStdString(eph->prn().toString()));
1428 }
1429 }
1430
1431 // Streams and systems
1432 QMap<QString, QSet<char> > streams;
1433 QMapIterator<QString, t_epoStat> itEpo(_epoStat);
1434 while (itEpo.hasNext()) {
1435 itEpo.next();
1436 QStringList hlp = itEpo.key().split(' ');
1437 streams[hlp[0]].insert(hlp[1][0].toLatin1());
1438 }
1439
1440 QMapIterator<QString, QSet<char> > itSta(streams);
1441 while (itSta.hasNext()) {
1442 itSta.next();
1443 const QString& staID = itSta.key();
1444 QList<char> systems = itSta.value().values();
1445 std::sort(systems.begin(), systems.end());
1446 ofstream& out = streamOut(staID, _intervalStart);
1447 if (final) {
1448 endTime = _staLast.value(staID, _lastTime);
1449 if (endTime < _intervalStart) { // stream without data in the last interval
1450 endTime = _lastTime;
1451 }
1452 }
1453 out << endl << "=== SSR QC " << staID.toStdString() << ' '
1454 << _intervalStart.datestr() << ' ' << _intervalStart.timestr(0)
1455 << " - " << endTime.datestr() << ' ' << endTime.timestr(0)
1456 << (final ? " (end of data)" : "") << " ===" << endl
1457 << " Sys Type Epochs Interval nom/med/max [s] Gaps Out-of-order"
1458 << " Latency med/max [s] Sats" << endl;
1459 for (char sys : systems) {
1460 for (int type = ORB; type <= VTEC; type++) {
1461 QString kk = QString("%1 %2 %3").arg(staID).arg(sys).arg(type);
1462 if (!_epoStat.contains(kk)) {
1463 continue;
1464 }
1465 const t_epoStat& stat = _epoStat[kk];
1466 double medInt = median(stat.intervals);
1467 double maxInt = stat.intervals.isEmpty() ? 0.0 :
1468 *std::max_element(stat.intervals.begin(), stat.intervals.end());
1469 double ref = max(double(stat.nominal), medInt);
1470 unsigned gaps = 0;
1471 for (double dt : stat.intervals) {
1472 if (ref > 0.0 && dt > 2.0 * ref) {
1473 ++gaps;
1474 }
1475 }
1476 double maxLat = stat.latencies.isEmpty() ? 0.0 :
1477 *std::max_element(stat.latencies.begin(), stat.latencies.end());
1478 out << " " << sys << " " << left << setw(10) << TYPE_NAME[type] << right
1479 << setw(8) << stat.numEpo
1480 << setw(10) << stat.nominal << " /" << setw(6) << setprecision(1) << fixed << medInt
1481 << " /" << setw(7) << maxInt
1482 << setw(6) << gaps << setw(14) << stat.numOutOfOrder
1483 << setw(10) << median(stat.latencies) << " /" << setw(7) << maxLat
1484 << setw(7) << stat.sats.size() << endl;
1485 }
1486 }
1487
1488 // Per system: completeness, ephemeris, biases, plausibility
1489 for (char sys : systems) {
1490 // Ionosphere (VTEC): model, values on the grid, comparison
1491 if (sys == 'V') {
1492 QStringList lines;
1493 QString kk = QString("%1 %2 %3").arg(staID).arg(sys).arg(int(VTEC));
1494 if (_epoStat.contains(kk)) {
1495 const t_epoStat& stat = _epoStat[kk];
1496 double medInt = median(stat.intervals);
1497 if (stat.nominal > 0 && stat.intervals.size() > 1 &&
1498 (medInt > 1.5 * stat.nominal || medInt < stat.nominal / 1.5)) {
1499 lines << QString("VTEC: declared update interval %1 s, actual %2 s")
1500 .arg(stat.nominal).arg(medInt, 0, 'f', 1);
1501 }
1502 }
1503 if (_vtecStat.contains(staID)) {
1504 const t_vtecStat& vs = _vtecStat[staID];
1505 if (!vs.model.isEmpty()) {
1506 lines << "VTEC model: " + vs.model;
1507 }
1508 if (vs.numGrid) {
1509 lines << QString("VTEC on a global %1 x %2 deg grid: %3 ... %4 TECU, negative (set to zero) %5 %")
1510 .arg(VTEC_GRID_LAT, 0, 'f', 0).arg(VTEC_GRID_LON, 0, 'f', 0)
1511 .arg(vs.minVal, 0, 'f', 1).arg(vs.maxVal, 0, 'f', 1)
1512 .arg(100.0 * vs.numNeg / vs.numGrid, 0, 'f', 1);
1513 }
1514 if (vs.numDiff) {
1515 QString refText = (_reference == "CONSENSUS")
1516 ? QString("consensus of the other ACs (%1 solutions on average)")
1517 .arg(double(vs.numSol) / vs.numCmp, 0, 'f', 1)
1518 : QString("stream %1").arg(_reference);
1519 double mean = vs.sDiff / vs.numDiff;
1520 double rms = sqrt(vs.sDiff2 / vs.numDiff);
1521 lines << QString("VTEC comparison with %1, %2 epochs: mean %3 TECU, RMS %4 TECU, max |diff| %5 TECU")
1522 .arg(refText).arg(vs.numCmp).arg(mean, 0, 'f', 2).arg(rms, 0, 'f', 2)
1523 .arg(vs.maxAbs, 0, 'f', 1);
1524 }
1525 else {
1526 lines << "VTEC not compared: no other model available";
1527 }
1528 }
1529 for (const QString& line : lines) {
1530 out << " " << sys << " " << line.toStdString() << endl;
1531 }
1532 continue;
1533 }
1534 QMap<QString, unsigned> iodEarly, outdated;
1535 double maxEarly = 0.0;
1536 QMap<QString, unsigned> missOrb, missClk, missCB, missPB, iodUnknown, unhealthy, noEph,
1537 jumps, missRef, noYaw, yawPartly;
1538 unsigned numPB = 0, numYaw = 0, yawSats = 0;
1539 double maxRadial = 0.0, maxAlongCross = 0.0;
1540 QString maxRadialPrn, maxAlongCrossPrn;
1541 QVector<double> biasIF;
1542 double maxBiasIF = 0.0;
1543 QString biasIFPrn;
1544 const QSet<QString>& ephs = ephSats[sys];
1545 for (int type = ORB; type <= PBIAS; type++) {
1546 QString kk = QString("%1 %2 %3").arg(staID).arg(sys).arg(type);
1547 if (!_epoStat.contains(kk)) {
1548 continue;
1549 }
1550 QMap<QString, unsigned>& miss = (type == ORB) ? missOrb : (type == CLK) ? missClk :
1551 (type == CBIAS) ? missCB : missPB;
1552 for (const QString& prn : ephs) {
1553 if (!_epoStat[kk].sats.contains(prn)) {
1554 miss[prn] = 0;
1555 }
1556 }
1557 }
1558 QMapIterator<QString, t_satStat> itSat(_satStat);
1559 while (itSat.hasNext()) {
1560 itSat.next();
1561 QStringList hlp = itSat.key().split(' ');
1562 if (hlp[0] != staID || hlp[1][0].toLatin1() != sys) {
1563 continue;
1564 }
1565 const QString& prn = hlp[1];
1566 const t_satStat& stat = itSat.value();
1567 if (stat.iodUnknown) iodUnknown[prn] = stat.iodUnknown;
1568 if (stat.iodEarly) {
1569 iodEarly[prn] = stat.iodEarly;
1570 maxEarly = max(maxEarly, stat.maxEarly);
1571 }
1572 if (stat.ephOutdated) outdated[prn] = stat.ephOutdated;
1573 if (stat.ephUnhealthy) unhealthy[prn] = stat.ephUnhealthy;
1574 if (stat.noEph) noEph[prn] = stat.noEph;
1575 if (stat.clkJumps) jumps[prn] = stat.clkJumps;
1576 if (stat.maxRadial > maxRadial) {
1577 maxRadial = stat.maxRadial;
1578 maxRadialPrn = prn;
1579 }
1580 if (stat.maxAlongCross > maxAlongCross) {
1581 maxAlongCross = stat.maxAlongCross;
1582 maxAlongCrossPrn = prn;
1583 }
1584 // only satellites with corrections (providers list satellites
1585 // without corrections with zero biases, e.g. GMV E14/E18)
1586 if (!stat.missingRefSig.isEmpty() && (stat.numClk || stat.numOrb)) {
1587 QStringList sigs = stat.missingRefSig.values();
1588 sigs.sort();
1589 missRef[prn + "[" + sigs.join("/") + "]"] = 0;
1590 }
1591 if (stat.refBiasOK) {
1592 biasIF.push_back(stat.biasIF);
1593 if (biasIFPrn.isEmpty() || fabs(stat.biasIF) > maxBiasIF) {
1594 maxBiasIF = fabs(stat.biasIF);
1595 biasIFPrn = prn;
1596 }
1597 }
1598 if (stat.numPB) {
1599 numPB += stat.numPB;
1600 numYaw += stat.numYaw;
1601 if (stat.numYaw) {
1602 yawSats++;
1603 if (stat.numYaw < stat.numPB) {
1604 yawPartly[prn] = stat.numPB - stat.numYaw;
1605 }
1606 }
1607 else {
1608 noYaw[prn] = 0;
1609 }
1610 }
1611 }
1612 // only findings are listed
1613 QStringList lines;
1614 for (int type = ORB; type <= PBIAS; type++) {
1615 QString kk = QString("%1 %2 %3").arg(staID).arg(sys).arg(type);
1616 if (_epoStat.contains(kk)) {
1617 const t_epoStat& stat = _epoStat[kk];
1618 double medInt = median(stat.intervals);
1619 if (stat.nominal > 0 && stat.intervals.size() > 1 &&
1620 (medInt > 1.5 * stat.nominal || medInt < stat.nominal / 1.5)) {
1621 lines << QString("%1: declared update interval %2 s, actual %3 s")
1622 .arg(TYPE_NAME[type]).arg(stat.nominal).arg(medInt, 0, 'f', 1);
1623 }
1624 }
1625 }
1626 auto addSats = [&lines](const QString& text, const QMap<QString, unsigned>& sats) {
1627 if (!sats.isEmpty()) {
1628 lines << text + ":" + satList(sats);
1629 }
1630 };
1631 addSats("orbit corrections missing (healthy broadcast ephemeris available)", missOrb);
1632 addSats("clock corrections missing (healthy broadcast ephemeris available)", missClk);
1633 addSats("code biases missing (healthy broadcast ephemeris available)", missCB);
1634 addSats("phase biases missing (healthy broadcast ephemeris available)", missPB);
1635 addSats("corrections referring to an IOD of no received broadcast ephemeris", iodUnknown);
1636 if (!iodEarly.isEmpty()) {
1637 addSats(QString("corrections referring to an IOD received from the ephemeris stream(s) "
1638 "only later (up to %1 s)").arg(maxEarly, 0, 'f', 0), iodEarly);
1639 }
1640 addSats("corrections referring to a broadcast ephemeris outside its validity", outdated);
1641 addSats("corrections for unhealthy broadcast ephemeris", unhealthy);
1642 addSats("corrections for satellites without broadcast ephemeris", noEph);
1643 QString staSys = QString("%1 %2").arg(staID).arg(sys);
1644 if (_codeSignals.contains(staSys)) {
1645 QStringList sigs = _codeSignals[staSys].values();
1646 sigs.sort();
1647 lines << "code bias signals: " + sigs.join(" ");
1648 addSats("code biases of the reference signals missing", missRef);
1649 if (biasIF.size() > 1) {
1650 double mean = 0.0;
1651 for (double bb : biasIF) mean += bb / biasIF.size();
1652 double var = 0.0;
1653 for (double bb : biasIF) var += (bb - mean) * (bb - mean) / biasIF.size();
1654 // the median shows whether the clocks refer to other signals (all
1655 // satellites) or single satellites have inconsistent biases
1656 QVector<double> absIF;
1657 for (double bb : biasIF) absIF << fabs(bb);
1658 lines << QString("ionosphere-free combination B_IF of the reference signal biases: "
1659 "std. dev. %1 m, median |B_IF| %2 m, max |B_IF| %3 m (%4) (%5 satellites)")
1660 .arg(sqrt(var), 0, 'f', 3).arg(median(absIF), 0, 'f', 3).arg(maxBiasIF, 0, 'f', 3)
1661 .arg(biasIFPrn).arg(biasIF.size());
1662 }
1663 }
1664 compareCodeBiases(staID, sys, lines);
1665 if (_phaseSignals.contains(staSys)) {
1666 QStringList sigs = _phaseSignals[staSys].values();
1667 sigs.sort();
1668 lines << "phase bias signals: " + sigs.join(" ");
1669 }
1670 if (numPB) {
1671 if (numYaw == 0) {
1672 lines << "yaw angle: not provided";
1673 }
1674 else {
1675 lines << QString("yaw angle: provided for %1 of %2 satellites (%3 % of the phase bias records)")
1676 .arg(yawSats).arg(yawSats + noYaw.size()).arg(100.0 * numYaw / numPB, 0, 'f', 1);
1677 addSats("phase biases without yaw angle", noYaw);
1678 addSats("phase biases partly without yaw angle (records)", yawPartly);
1679 }
1680 }
1681 for (const QString& line : _attEvents.value(staID).value(sys)) {
1682 lines << line;
1683 }
1684 addSats(QString("clock jumps > %1 m").arg(MAX_CLK_JUMP), jumps);
1685 if (!maxRadialPrn.isEmpty()) {
1686 lines << QString("largest orbit corrections: radial %1 m (%2), along/cross %3 m (%4)")
1687 .arg(maxRadial, 0, 'f', 2).arg(maxRadialPrn).arg(maxAlongCross, 0, 'f', 2).arg(maxAlongCrossPrn);
1688 }
1689 // stream comparison
1690 if (_cmpStat.contains(staID)) {
1691 unsigned nSat = 0, nClk = 0, nOrb = 0, nSis = 0;
1692 double sClk = 0.0, sR = 0.0, sA = 0.0, sC = 0.0, sSis = 0.0;
1693 double vClk = 0.0, vSis = 0.0; // sums of squares about the satellite means
1694 QMap<QString, double> rmsSis, rmsClk, rmsOrb;
1695 QMap<QString, unsigned> notReferred;
1696 QMapIterator<QString, t_cmpStat> itC(_cmpStat[staID]);
1697 while (itC.hasNext()) {
1698 itC.next();
1699 if (itC.key()[0].toLatin1() != sys) continue;
1700 const t_cmpStat& st = itC.value();
1701 if (st.numClk == 0 && st.numOrb == 0) {
1702 if (st.clkNotReferred) notReferred[itC.key()] = 0;
1703 continue;
1704 }
1705 ++nSat;
1706 if (st.clkNotReferred) notReferred[itC.key()] = 0;
1707 nClk += st.numClk; sClk += st.sClk;
1708 nOrb += st.numOrb; sR += st.sR; sA += st.sA; sC += st.sC;
1709 nSis += st.numSis; sSis += st.sSis;
1710 if (st.numClk) vClk += st.sClk - st.mClk * st.mClk / st.numClk;
1711 if (st.numSis) vSis += st.sSis - st.mSis * st.mSis / st.numSis;
1712 if (st.numSis) rmsSis[itC.key()] = sqrt(st.sSis / st.numSis);
1713 else if (st.numClk) rmsClk[itC.key()] = sqrt(st.sClk / st.numClk);
1714 if (st.numOrb) rmsOrb[itC.key()] = sqrt((st.sR + st.sA + st.sC) / st.numOrb);
1715 }
1716 if (nSat) {
1717 QString refText = (_reference == "CONSENSUS") ? QString("consensus of the other ACs")
1718 : QString("stream %1").arg(_reference);
1719 QStringList parts;
1720 if (nClk) parts << QString("clock %1 (STD %2)").arg(sqrt(sClk / nClk), 0, 'f', 3)
1721 .arg(sqrt(max(0.0, vClk) / nClk), 0, 'f', 3);
1722 if (nOrb) parts << QString("radial %1, along %2, cross %3")
1723 .arg(sqrt(sR / nOrb), 0, 'f', 3).arg(sqrt(sA / nOrb), 0, 'f', 3)
1724 .arg(sqrt(sC / nOrb), 0, 'f', 3);
1725 if (nSis) parts << QString("SIS (radial - clock) %1 (STD %2)").arg(sqrt(sSis / nSis), 0, 'f', 3)
1726 .arg(sqrt(max(0.0, vSis) / nSis), 0, 'f', 3);
1727 lines << QString("comparison with %1 (%2 satellites), RMS (STD about the satellite means) [m]: %3")
1728 .arg(refText).arg(nSat).arg(parts.join(", "));
1729 if (!nOrb) {
1730 lines << "orbits not compared: no other stream with the same reference point (APC/CoM)";
1731 }
1732 addSats("clocks not compared (code biases of the reference signals missing)", notReferred);
1733 auto deviating = [&lines](const QString& what, const QMap<QString, double>& rms) {
1734 if (rms.size() < 2) return;
1735 double med = median(rms.values().toVector());
1736 QStringList dev;
1737 QMapIterator<QString, double> itR(rms);
1738 while (itR.hasNext()) {
1739 itR.next();
1740 if (itR.value() > max(DEV_MIN, DEV_FACTOR * med)) {
1741 dev << QString("%1(%2)").arg(itR.key()).arg(itR.value(), 0, 'f', 2);
1742 }
1743 }
1744 if (!dev.isEmpty()) {
1745 lines << QString("satellites deviating from the reference, RMS %1 [m]: %2").arg(what).arg(dev.join(" "));
1746 }
1747 };
1748 deviating("SIS", rmsSis);
1749 deviating("clock", rmsClk);
1750 deviating("orbit (3D)", rmsOrb);
1751 }
1752 }
1753 for (const QString& line : lines) {
1754 out << " " << sys << " " << line.toStdString() << endl;
1755 }
1756 }
1757 out.flush();
1758 }
1759
1760 emit newMessage(QString("SSR QC: report %1 %2 of %3 stream(s) written to %4")
1761 .arg(QString::fromStdString(_intervalStart.datestr()))
1762 .arg(QString::fromStdString(_intervalStart.timestr(0)))
1763 .arg(streams.size()).arg(_dirName).toLatin1(), false);
1764
1765 _cmpStat.clear();
1766 _cmpEpochs.clear();
1767 _attEvents.clear();
1768 for (QMap<QString, t_vtecStat>::iterator it = _vtecStat.begin(); it != _vtecStat.end(); ++it) {
1769 t_vtecStat fresh;
1770 fresh.model = it.value().model;
1771 it.value() = fresh;
1772 }
1773
1774 // reset, keeping the state needed across intervals
1775 for (QMap<QString, t_epoStat>::iterator it = _epoStat.begin(); it != _epoStat.end(); ++it) {
1776 t_epoStat& stat = it.value();
1777 bncTime last = stat.last;
1778 int nominal = stat.nominal;
1779 stat = t_epoStat();
1780 stat.last = last;
1781 stat.nominal = nominal;
1782 }
1783 for (QMap<QString, t_satStat>::iterator it = _satStat.begin(); it != _satStat.end(); ++it) {
1784 t_satStat& stat = it.value();
1785 t_satStat fresh;
1786 fresh.lastClkTime = stat.lastClkTime;
1787 fresh.lastIod = stat.lastIod;
1788 fresh.lastClk = stat.lastClk;
1789 fresh.missingRefSig = stat.missingRefSig;
1790 fresh.refBiasOK = stat.refBiasOK;
1791 fresh.biasIF = stat.biasIF;
1792 stat = fresh;
1793 }
1794}
Note: See TracBrowser for help on using the repository browser.