Changeset 11117 in ntrip for trunk/BNC/src/ssrQc.cpp
- Timestamp:
- Oct 22, 2026, 5:22:15 PM (2 weeks ago)
- File:
-
- 1 edited
-
trunk/BNC/src/ssrQc.cpp (modified) (10 diffs)
Legend:
- Unmodified
- Added
- Removed
-
trunk/BNC/src/ssrQc.cpp
r11091 r11117 60 60 const double DEV_MIN = 0.05; // [m] deviating satellites: RMS above max(DEV_MIN, 61 61 const double DEV_FACTOR = 3.0; // DEV_FACTOR x median RMS of the system) 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] 62 67 63 68 double tKey(const bncTime& tt) { 64 69 return tt.gpsw() * 604800.0 + tt.gpssec(); 65 70 } 66 const char* TYPE_NAME[] = {"ORBIT", "CLOCK", "CODE_BIAS", "PHASE_BIAS"}; 71 const char* TYPE_NAME[] = {"ORBIT", "CLOCK", "CODE_BIAS", "PHASE_BIAS", "VTEC"}; 67 72 68 73 double median(QVector<double> vv) { … … 155 160 connect(BNC_CORE, SIGNAL(newPhaseBiases(QList<t_satPhaseBias>)), 156 161 this, SLOT(slotNewPhaseBiases(QList<t_satPhaseBias>)), Qt::DirectConnection); 162 connect(BNC_CORE, SIGNAL(newTec(t_vTec)), 163 this, SLOT(slotNewTec(t_vTec)), Qt::DirectConnection); 157 164 // combined corrections (stream INTERNAL) 158 165 if (bncComb::exists()) { … … 175 182 //////////////////////////////////////////////////////////////////////////// 176 183 QString t_ssrQc::acOf(const QString& staID) { 177 static const QRegularExpression re("^(?:SSR |OSB)[AC][0-9]{2}([A-Z0-9]{3})[0-9]$");184 static const QRegularExpression re("^(?:SSR[AC]|OSB[AC]|IONO)[0-9]{2}([A-Z0-9]{3})[0-9]$"); 178 185 QRegularExpressionMatch mm = re.match(staID); 179 186 return mm.hasMatch() ? mm.captured(1) : staID; … … 185 192 //////////////////////////////////////////////////////////////////////////// 186 193 QString t_ssrQc::solOf(const QString& staID) { 187 static const QRegularExpression re("^(?:SSR |OSB)[AC]([0-9]{2})([A-Z0-9]{3})[0-9]$");194 static const QRegularExpression re("^(?:SSR[AC]|OSB[AC]|IONO)([0-9]{2})([A-Z0-9]{3})[0-9]$"); 188 195 QRegularExpressionMatch mm = re.match(staID); 189 196 return mm.hasMatch() ? mm.captured(2) + mm.captured(1) : staID; … … 222 229 << " completeness w.r.t. healthy broadcast ephemerides," << endl 223 230 << "IOD matching, code and phase bias signals, yaw angle, code biases of the reference signals, clock jumps > " 224 << MAX_CLK_JUMP << " m, largest orbit corrections" << endl 231 << MAX_CLK_JUMP << " m, largest orbit corrections, VTEC models (system V)" << endl 225 232 << "Reference signals of the clocks: " << t_ssrRefSig::currentString().toStdString() << endl; 226 233 } … … 278 285 compareEpoch(_nextCmpEpoch); 279 286 _nextCmpEpoch = _nextCmpEpoch + CMP_SAMPL; 287 } 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 } 280 298 } 281 299 if (time - _intervalStart >= _interval) { … … 983 1001 } 984 1002 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 985 1155 // Write the report of the current interval and reset the statistics 986 1156 //////////////////////////////////////////////////////////////////////////// … … 1029 1199 << " Latency med/max [s] Sats" << endl; 1030 1200 for (char sys : systems) { 1031 for (int type = ORB; type <= PBIAS; type++) {1201 for (int type = ORB; type <= VTEC; type++) { 1032 1202 QString kk = QString("%1 %2 %3").arg(staID).arg(sys).arg(type); 1033 1203 if (!_epoStat.contains(kk)) { … … 1059 1229 // Per system: completeness, ephemeris, biases, plausibility 1060 1230 for (char sys : systems) { 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 } 1061 1275 QMap<QString, unsigned> missOrb, missClk, missCB, missPB, iodUnknown, unhealthy, noEph, 1062 1276 jumps, missRef, noYaw, yawPartly; … … 1266 1480 _cmpStat.clear(); 1267 1481 _cmpEpochs.clear(); 1482 for (QMap<QString, t_vtecStat>::iterator it = _vtecStat.begin(); it != _vtecStat.end(); ++it) { 1483 t_vtecStat fresh; 1484 fresh.model = it.value().model; 1485 it.value() = fresh; 1486 } 1268 1487 1269 1488 // reset, keeping the state needed across intervals
Note:
See TracChangeset
for help on using the changeset viewer.
