Changeset 11117 in ntrip for trunk/BNC/src/ssrQc.cpp


Ignore:
Timestamp:
Oct 22, 2026, 5:22:15 PM (2 weeks ago)
Author:
stuerze
Message:

VTEC check added in ssr qc

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/BNC/src/ssrQc.cpp

    r11091 r11117  
    6060  const double DEV_MIN        = 0.05;  // [m] deviating satellites: RMS above max(DEV_MIN,
    6161  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]
    6267
    6368  double tKey(const bncTime& tt) {
    6469    return tt.gpsw() * 604800.0 + tt.gpssec();
    6570  }
    66   const char*  TYPE_NAME[]    = {"ORBIT", "CLOCK", "CODE_BIAS", "PHASE_BIAS"};
     71  const char*  TYPE_NAME[]    = {"ORBIT", "CLOCK", "CODE_BIAS", "PHASE_BIAS", "VTEC"};
    6772
    6873  double median(QVector<double> vv) {
    … …  
    155160  connect(BNC_CORE, SIGNAL(newPhaseBiases(QList<t_satPhaseBias>)),
    156161          this, SLOT(slotNewPhaseBiases(QList<t_satPhaseBias>)), Qt::DirectConnection);
     162  connect(BNC_CORE, SIGNAL(newTec(t_vTec)),
     163          this, SLOT(slotNewTec(t_vTec)), Qt::DirectConnection);
    157164  // combined corrections (stream INTERNAL)
    158165  if (bncComb::exists()) {
    … …  
    175182////////////////////////////////////////////////////////////////////////////
    176183QString 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]$");
    178185  QRegularExpressionMatch mm = re.match(staID);
    179186  return mm.hasMatch() ? mm.captured(1) : staID;
    … …  
    185192////////////////////////////////////////////////////////////////////////////
    186193QString 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]$");
    188195  QRegularExpressionMatch mm = re.match(staID);
    189196  return mm.hasMatch() ? mm.captured(2) + mm.captured(1) : staID;
    … …  
    222229           << " completeness w.r.t. healthy broadcast ephemerides," << endl
    223230           << "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
    225232           << "Reference signals of the clocks: " << t_ssrRefSig::currentString().toStdString() << endl;
    226233    }
    … …  
    278285    compareEpoch(_nextCmpEpoch);
    279286    _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    }
    280298  }
    281299  if (time - _intervalStart >= _interval) {
    … …  
    9831001}
    9841002
     1003// Ionospheric model (VTEC)
     1004////////////////////////////////////////////////////////////////////////////
     1005void 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////////////////////////////////////////////////////////////////////////////
     1034double 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////////////////////////////////////////////////////////////////////////////
     1065void 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
    9851155// Write the report of the current interval and reset the statistics
    9861156////////////////////////////////////////////////////////////////////////////
    … …  
    10291199          << "  Latency med/max [s]  Sats" << endl;
    10301200    for (char sys : systems) {
    1031       for (int type = ORB; type <= PBIAS; type++) {
     1201      for (int type = ORB; type <= VTEC; type++) {
    10321202        QString kk = QString("%1 %2 %3").arg(staID).arg(sys).arg(type);
    10331203        if (!_epoStat.contains(kk)) {
    … …  
    10591229    // Per system: completeness, ephemeris, biases, plausibility
    10601230    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      }
    10611275      QMap<QString, unsigned> missOrb, missClk, missCB, missPB, iodUnknown, unhealthy, noEph,
    10621276                              jumps, missRef, noYaw, yawPartly;
    … …  
    12661480  _cmpStat.clear();
    12671481  _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  }
    12681487
    12691488  // reset, keeping the state needed across intervals
Note: See TracChangeset for help on using the changeset viewer.