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


Ignore:
Timestamp:
Oct 28, 2026, 9:31:18 AM (37 hours ago)
Author:
stuerze
Message:

attitude check added to the SSR QC

File:
1 edited

Legend:

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

    r11119 r11129  
    4949#include "bncsettings.h"
    5050#include "combination/bnccomb.h"
     51#include "bncantex.h"
    5152
    5253using namespace std;
    … …  
    6061  const double DEV_MIN        = 0.05;  // [m] deviating satellites: RMS above max(DEV_MIN,
    6162  const double DEV_FACTOR     = 3.0;   //     DEV_FACTOR x median RMS of the system)
     63  const double ATT_ONSET      = 20.0 * M_PI / 180.0; // [rad] change of the yaw difference starting a turn
     64  const double ATT_STABLE     =  2.0 * M_PI / 180.0; // [rad] yaw difference stable again ...
     65  const double ATT_STABLE_T   = 600.0;   // [s] ... for this time: end of the turn
     66  const double ATT_REF_FROM   = 900.0;   // [s] reference before the turn: samples from 15 min
     67  const double ATT_REF_TO     = 300.0;   //     to 5 min before the current epoch
     68  const double ATT_MAX_GAP    = 600.0;   // [s] longer gaps interrupt a turn
     69  const double ATT_MAX_TURN   = 10800.0; // [s] a turn is closed after 3 h at the latest
     70  const double ATT_MIN_PRED   = 0.03;    // [m] smaller predicted clock changes are not assessed
    6271  const double VTEC_CMP_SAMPL = 300.0;  // [s] sampling of the VTEC comparison
    6372  const double VTEC_CMP_DELAY = 120.0;  // [s] VTEC epochs compared when data this much newer arrived
    … …  
    150159      delete _bsxRef;
    151160      _bsxRef = 0;
     161    }
     162  }
     163  _antex = 0;
     164  QString antexFile = settings.value("ssrQcAntexFile").toString();
     165  expandEnvVar(antexFile);
     166  if (!antexFile.isEmpty()) {
     167    _antex = new bncAntex();
     168    if (_antex->readFile(antexFile) != success || _antex->numSatPrns() == 0) {
     169      emit newMessage(("SSR QC: wrong ANTEX file " + antexFile + " - no attitude check").toLatin1(), true);
     170      delete _antex;
     171      _antex = 0;
    152172    }
    153173  }
    … …  
    247267  }
    248268  delete _bsxRef;
     269  delete _antex;
    249270}
    250271
    … …  
    488509    if (yawProvided) {
    489510      stat.numYaw++;
     511      t_yawLast& yl = _yawLast[QString::fromStdString(bias._staID)][QString::fromStdString(bias._prn.toString())];
     512      double dt = 0.0;
     513      if (bias._updateInt < unsigned(ssrUpdateInt.size())) {
     514        dt = 0.5 * ssrUpdateInt[bias._updateInt];   // reference time in the middle of the interval
     515      }
     516      yl.time = bias._time + dt;
     517      yl.yaw  = bias._yaw;
     518      yl.rate = bias._yawRate;
    490519    }
    491520  }
    … …  
    9861015        if (dClk.contains(prn)) {
    9871016          double dc = dClk[prn] - off;
     1017          if (_antex && _yawLast.value(staID).contains(prn)) {
     1018            attitudeSample(staID, prn, tt, crd[staID][prn].xx, crd[staID][prn].vv, dc);
     1019          }
    9881020          stat.numClk += 1;
    9891021          stat.sClk   += dc * dc;
    … …  
    9991031    }
    10001032  }
     1033}
     1034
     1035// Attitude check: one sample per comparison epoch for a stream sending the
     1036// yaw angle. The yaw difference of BNC's attitude model ('Computed') minus
     1037// the stream is followed continuously over time; the predicted clock
     1038// difference of the stream to an AC following the model is
     1039// lambda_IF * dpsi / 2pi (see combination, dC_att). A turn starts when the
     1040// yaw difference changes by more than ATT_ONSET against the samples 5-15
     1041// minutes before, and ends when it is stable again for ATT_STABLE_T.
     1042////////////////////////////////////////////////////////////////////////////
     1043void t_ssrQc::attitudeSample(const QString& staID, const QString& prn, const bncTime& tt,
     1044                             const ColumnVector& xx, const ColumnVector& vv, double dc) {
     1045  const t_yawLast& yl = _yawLast[staID][prn];
     1046  double age = tt - yl.time;
     1047  if (!yl.time.valid() || fabs(age) > 120.0) {
     1048    return;
     1049  }
     1050  double psiAC = yl.yaw + yl.rate * age;
     1051
     1052  // model yaw, once per satellite and epoch (model state in time order)
     1053  double tk = tKey(tt);
     1054  if (tk != _psiModelT) {
     1055    _psiModel.clear();
     1056    _psiModelT = tk;
     1057  }
     1058  if (!_psiModel.contains(prn)) {
     1059    ColumnVector dx(3);
     1060    double psiM = 0.0;
     1061    double Mjd = tt.mjd() + tt.daysec() / 86400.0;
     1062    if (_antex->satCoMcorrection(prn, Mjd, xx, vv, dx, bncAntex::ATT_COMPUTED, 0.0,
     1063                                 "ssrQc", &psiM) != success) {
     1064      return;
     1065    }
     1066    _psiModel[prn] = psiM;
     1067  }
     1068
     1069  // ionosphere-free wavelength of the reference signals, c / (f1 + f2)
     1070  QString sig1, sig2;
     1071  t_frequency::type frq1, frq2;
     1072  if (!refSignals(prn[0].toLatin1(), sig1, sig2, frq1, frq2)) {
     1073    return;
     1074  }
     1075  double lambda = t_CST::c / (t_CST::freq(frq1, 0) + t_CST::freq(frq2, 0));
     1076
     1077  t_attTrack& tr = _attTrack[staID][prn];
     1078  if (tr.hasLast && tk - tr.lastT > ATT_MAX_GAP) {
     1079    if (tr.active) {
     1080      attitudeEvent(staID, prn, tr, "interrupted by a gap");
     1081    }
     1082    tr = t_attTrack();
     1083  }
     1084  double dpsi = remainder(_psiModel[prn] - psiAC, 2.0 * M_PI);
     1085  if (tr.hasLast) {
     1086    dpsi = tr.lastDPsi + remainder(dpsi - tr.lastDPsi, 2.0 * M_PI);
     1087  }
     1088  tr.hasLast  = true;
     1089  tr.lastDPsi = dpsi;
     1090  tr.lastT    = tk;
     1091  t_attSample smp;
     1092  smp.t    = tk;
     1093  smp.dc   = dc;
     1094  smp.p    = lambda * dpsi / (2.0 * M_PI);
     1095  smp.dpsi = dpsi;
     1096
     1097  if (!tr.active) {
     1098    tr.hist << smp;
     1099    while (!tr.hist.isEmpty() && tk - tr.hist.first().t > ATT_REF_FROM) {
     1100      tr.hist.removeFirst();
     1101    }
     1102    // reference: samples 5 to 15 minutes before
     1103    QVector<double> rDc, rP, rPsi;
     1104    for (const t_attSample& hh : tr.hist) {
     1105      if (tk - hh.t >= ATT_REF_TO) {
     1106        rDc << hh.dc; rP << hh.p; rPsi << hh.dpsi;
     1107      }
     1108    }
     1109    if (rPsi.size() >= 3 && fabs(dpsi - median(rPsi)) > ATT_ONSET) {
     1110      tr.active = true;
     1111      tr.dc0    = median(rDc);
     1112      tr.p0     = median(rP);
     1113      tr.dpsi0  = median(rPsi);
     1114      for (const t_attSample& hh : tr.hist) {
     1115        if (tk - hh.t < ATT_REF_TO) tr.event << hh;  // the beginning of the turn
     1116      }
     1117      tr.hist.clear();
     1118    }
     1119    return;
     1120  }
     1121
     1122  tr.event << smp;
     1123  // end: yaw difference stable for ATT_STABLE_T, or the turn too long
     1124  double tStart = tr.event.first().t;
     1125  bool   stable = (tk - tStart >= ATT_STABLE_T);
     1126  if (stable) {
     1127    for (int ii = tr.event.size() - 1; ii >= 0 && tk - tr.event[ii].t <= ATT_STABLE_T; ii--) {
     1128      if (fabs(tr.event[ii].dpsi - dpsi) > ATT_STABLE) {
     1129        stable = false;
     1130        break;
     1131      }
     1132    }
     1133  }
     1134  if (stable || tk - tStart > ATT_MAX_TURN) {
     1135    attitudeEvent(staID, prn, tr, stable ? "" : "closed after 3 h");
     1136    double keepPsi = tr.lastDPsi, keepT = tr.lastT;
     1137    tr = t_attTrack();
     1138    tr.hasLast  = true;
     1139    tr.lastDPsi = keepPsi;
     1140    tr.lastT    = keepT;
     1141    tr.hist << smp;
     1142  }
     1143}
     1144
     1145// Assessment of a turn: observed clock change of the stream relative to the
     1146// consensus against the predicted one (regression slope, RMS reduction)
     1147////////////////////////////////////////////////////////////////////////////
     1148void t_ssrQc::attitudeEvent(const QString& staID, const QString& prn, t_attTrack& tr,
     1149                            const QString& end) {
     1150  if (tr.event.size() < 3) {
     1151    return;
     1152  }
     1153  double sxx = 0.0, sxy = 0.0, syy = 0.0, srr = 0.0, maxX = 0.0, maxPsi = 0.0;
     1154  for (const t_attSample& ss : tr.event) {
     1155    double x = ss.p  - tr.p0;
     1156    double y = ss.dc - tr.dc0;
     1157    sxx += x * x; sxy += x * y; syy += y * y; srr += (y - x) * (y - x);
     1158    if (fabs(x) > fabs(maxX)) maxX = x;
     1159    if (fabs(ss.dpsi - tr.dpsi0) > fabs(maxPsi)) maxPsi = ss.dpsi - tr.dpsi0;
     1160  }
     1161  int    nn  = tr.event.size();
     1162  // changes at the end of the turn: mean of the last 5 minutes
     1163  double tEnd = tr.event.last().t, xEnd = 0.0, yEnd = 0.0;
     1164  int    nEnd = 0;
     1165  for (const t_attSample& ss : tr.event) {
     1166    if (tEnd - ss.t <= 300.0) {
     1167      xEnd += ss.p - tr.p0; yEnd += ss.dc - tr.dc0; ++nEnd;
     1168    }
     1169  }
     1170  xEnd /= nEnd; yEnd /= nEnd;
     1171  double rmsObs = sqrt(syy / nn);
     1172  double rmsRes = sqrt(srr / nn);
     1173  double slope  = (sxx > 0.0) ? sxy / sxx : 0.0;
     1174
     1175  bncTime t1, t2;
     1176  t1.set(int(tr.event.first().t / 604800.0), fmod(tr.event.first().t, 604800.0));
     1177  t2.set(int(tEnd / 604800.0), fmod(tEnd, 604800.0));
     1178  QString verdict;
     1179  if (fabs(maxX) < ATT_MIN_PRED) {
     1180    // the consensus is assumed to follow BNC's attitude model; a larger clock
     1181    // change shows that the other ACs model this turn differently
     1182    verdict = (rmsObs >= ATT_MIN_PRED) ? "clock change not explained by the yaw difference "
     1183                                         "(the other ACs model this turn differently?)"
     1184                                       : "predicted change too small to assess";
     1185  }
     1186  else if (slope > 0.7 && slope < 1.3 && rmsRes < 0.5 * rmsObs) {
     1187    verdict = "SSR yaw consistent with the clocks";
     1188  }
     1189  else {
     1190    verdict = "SSR yaw NOT consistent with the clocks";
     1191  }
     1192  QString line = QString("attitude check %1 %2-%3: yaw difference to the model %4 deg, clock change vs. "
     1193                         "consensus %5 cm, predicted %6 cm (max %7 cm), slope %8, clock RMS %9 -> %10 cm: %11")
     1194                 .arg(prn).arg(QString::fromStdString(t1.timestr(0))).arg(QString::fromStdString(t2.timestr(0)))
     1195                 .arg(maxPsi * 180.0 / M_PI, 0, 'f', 0)
     1196                 .arg(yEnd * 100.0, 0, 'f', 1).arg(xEnd * 100.0, 0, 'f', 1).arg(maxX * 100.0, 0, 'f', 1)
     1197                 .arg(slope, 0, 'f', 2).arg(rmsObs * 100.0, 0, 'f', 1).arg(rmsRes * 100.0, 0, 'f', 1)
     1198                 .arg(verdict);
     1199  if (!end.isEmpty()) {
     1200    line += " (" + end + ")";
     1201  }
     1202  _attEvents[staID][prn[0].toLatin1()] << line;
    10011203}
    10021204
    … …  
    14061608        }
    14071609      }
     1610      for (const QString& line : _attEvents.value(staID).value(sys)) {
     1611        lines << line;
     1612      }
    14081613      addSats(QString("clock jumps > %1 m").arg(MAX_CLK_JUMP), jumps);
    14091614      if (!maxRadialPrn.isEmpty()) {
    … …  
    14891694  _cmpStat.clear();
    14901695  _cmpEpochs.clear();
     1696  _attEvents.clear();
    14911697  for (QMap<QString, t_vtecStat>::iterator it = _vtecStat.begin(); it != _vtecStat.end(); ++it) {
    14921698    t_vtecStat fresh;
Note: See TracChangeset for help on using the changeset viewer.