| 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 |
|
|---|
| 53 | using namespace std;
|
|---|
| 54 |
|
|---|
| 55 | namespace {
|
|---|
| 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 123 | t_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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 199 | bool 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 207 | QString 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 217 | QString 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 225 | QString 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 234 | bool 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 240 | ofstream& 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 266 | t_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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 278 | QString 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 284 | bool 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 299 | void 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 336 | void 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 365 | void 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 406 | void 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 435 | void 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 458 | void 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 494 | void 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 543 | void 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 579 | bool 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 670 | void 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 834 | char 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 846 | void 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 867 | void 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 1094 | void 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 1199 | void 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 1258 | void 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 1287 | double 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 1318 | void 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 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 1410 | void 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 | }
|
|---|