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