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