Changeset 11117 in ntrip


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

VTEC check added in ssr qc

Location:
trunk/BNC
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/BNC/CHANGELOG.md

    r11114 r11117  
    44- ADDED: **SSR QC** of decoded Broadcast Correction streams (options 'QC Directory', 'QC Streams', 'QC Reference', 'QC Interval'; example configuration '31_SsrQC.bnc'). One daily report file per stream with epochs, declared and actual update intervals, gaps, latency, completeness w.r.t. healthy broadcast ephemerides, IOD matching, code and phase bias signals, yaw angle availability, missing code biases of the reference signals, clock jumps and largest orbit corrections
    55- ADDED: Stream comparison in the SSR QC: orbits (radial, along, cross), clocks referred to the reference signals and signal-in-space differences (SIS = radial - clock) of each stream compared with the consensus of the other ACs (one vote per AC, own AC left out, combination products not voting) or with a selected stream (e.g. 'INTERNAL'), as RMS and standard deviation per satellite and system
     6- ADDED: VTEC check in the SSR QC (pseudo system 'V'): update interval, gaps and latency of the VTEC messages, model (layers, heights, degree/order), VTEC range on a global grid and comparison with the consensus of the other AC solutions (or the 'QC Reference' stream) in TECU; ionosphere streams (IONO...) are assigned to their AC solution
    67- ADDED: Code bias check in the SSR QC: the DCBs of each stream are compared with each other AC and optionally with a Bias SINEX file (option 'QC Bias File'); a stream with the opposite sign for most DCBs (SINEX instead of SSR sign convention) is reported
    78- ADDED: SIS (radial - clock, satellite-specific clock offset included) per epoch and SISRMS/SISSig per satellite and system in the SP3 comparison
    … …  
    3940- FIXED: GPS attitude model ('Computed'): at the end of a noon/midnight turn the yaw jumped to the nominal yaw (e.g. by 95 deg); the satellite now keeps yawing at its maximum rate until it has caught up with the nominal yaw
    4041- FIXED: GLONASS attitude model ('Computed'): the yaw was frozen while the nominal yaw rate exceeded the maximum rate and then jumped to the nominal yaw; now following Dilssner et al. (2011), the satellite yaws at the maximum rate (0.25 deg/s) during noon turns until it has caught up, and in the Earth's shadow until it reaches the nominal yaw at shadow exit
     42- FIXED: Source table window: streams with an incomplete source table entry (e.g. without the fields authentication, fee, bitrate, misc) were not listed; they are now shown as long as the entry contains the fields up to 'nmea'
    4143- FIXED: Upload of raw streams lost data chunks
    4244- FIXED: RTCM3 decoder: a 0xD3 byte followed by a zero length field at the end of the received data caused an endless loop until std::bad_alloc, and the stream was deleted
  • trunk/BNC/src/bnchelp.html

    r11113 r11117  
    52355235    clock 0.689 (STD 0.008), radial 0.020, along 0.036, cross 0.022, SIS (radial - clock) 0.687 (STD 0.008)
    52365236  G satellites deviating from the reference, RMS SIS [m]: G02(0.38) G16(0.33) G20(0.91) G26(3.36)
     5237</pre>
     5238  <p>
     5239    <b>Ionospheric models (VTEC):</b> Streams with VTEC messages (e.g. the ionosphere streams 'IONO...', or SSR
     5240    streams also carrying VTEC) get the pseudo system 'V' (VTEC) in the report: epochs, declared and actual update interval,
     5241    gaps and latency of the VTEC messages, the model (number of layers, layer heights, degree and order of the
     5242    spherical harmonics), and the range of the VTEC values on a global grid (10 degrees in latitude from -80 to 80
     5243    degrees, 15 degrees in longitude), including the percentage of negative values, which are set to zero as in PPP.
     5244    Every 300 seconds (with a delay of 120 seconds), the latest model of each stream not older than twice its update
     5245    interval (at least 120 seconds, at most one hour) is evaluated on this grid and compared with the median of the
     5246    models of the other AC solutions (one vote per solution, own AC left out, combinations like 'IONO00IGS1' not
     5247    voting; at least one other solution), or with the selected 'QC Reference' stream. The mean difference (a bias
     5248    of the model) and the RMS are given in TEC units, e.g.
     5249  </p>
     5250  <pre>
     5251  V   VTEC            60        60 /  60.0 /   60.0     0             0       1.0 /    5.0      1
     5252  V VTEC model: 1 layer(s): 450 km (degree/order 15/15)
     5253  V VTEC on a global 10 x 15 deg grid: 2.1 ... 65.7 TECU, negative (set to zero) 0.0 %
     5254  V VTEC comparison with consensus of the other ACs (1.0 solutions on average), 12 epochs: mean 3.47 TECU, RMS 7.55 TECU, max |diff| 24.0 TECU
    52375255</pre>
    52385256  <p>
  • 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
  • trunk/BNC/src/ssrQc.h

    r11091 r11117  
    1010// Quality control of the decoded SSR streams: epochs and update intervals,
    1111// latency, completeness with respect to the broadcast ephemerides, IOD
    12 // matching, code bias signals, clock jumps and orbit correction size. A
     12// matching, code bias signals, clock jumps, orbit correction size and the
     13// ionospheric (VTEC) models. A
    1314// report per stream and system is written to the QC logfile at the end of
    1415// each interval and at the end of the program.
    … …  
    2930  void slotNewCodeBiases(QList<t_satCodeBias> codeBiases);
    3031  void slotNewPhaseBiases(QList<t_satPhaseBias> phaseBiases);
     32  void slotNewTec(t_vTec vTec);
    3133
    3234 signals:
    … …  
    3436
    3537 private:
    36   enum e_type {ORB = 0, CLK = 1, CBIAS = 2, PBIAS = 3};
     38  enum e_type {ORB = 0, CLK = 1, CBIAS = 2, PBIAS = 3, VTEC = 4};
    3739
    3840  class t_epoStat {          // per stream, system and message type
    … …  
    147149  bncBiasSnx*               _bsxRef;    // reference code biases (option ssrQcBiasFile)
    148150  QString                   _bsxName;
     151
     152  // VTEC check: model content, values on a global grid and comparison with
     153  // the consensus of the other ACs (or the reference stream)
     154  class t_vtecStat {         // per stream
     155   public:
     156    unsigned numCmp  = 0;     // compared epochs
     157    unsigned numSol  = 0;     // sum of the solutions in the consensus
     158    unsigned numDiff = 0;     // compared grid values
     159    double   sDiff   = 0.0;   // sum of the differences [TECU]
     160    double   sDiff2  = 0.0;   // sum of squares [TECU^2]
     161    double   maxAbs  = 0.0;   // [TECU]
     162    unsigned numGrid = 0;     // evaluated grid values
     163    unsigned numNeg  = 0;     // negative grid values
     164    double   minVal  = 0.0;   // [TECU]
     165    double   maxVal  = 0.0;
     166    QString  model;           // layers, heights, degree/order of the latest model
     167  };
     168  void          compareVtec(const bncTime& tt);
     169  static double vtecAt(const t_vTec& vTec, double lat, double lon, const bncTime& tt, bool& negative);
     170  QMap<QString, QMap<double, t_vTec> > _vtecHist;  // stream, time
     171  QMap<QString, t_vtecStat>            _vtecStat;  // stream
     172  bncTime                              _nextVtecCmp;
    149173};
    150174
Note: See TracChangeset for help on using the changeset viewer.