Changeset 11129 in ntrip


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

attitude check added to the SSR QC

Location:
trunk/BNC/src
Files:
5 edited

Legend:

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

    r11128 r11129  
    144144        setValue_p("ssrQcIntr", "1 hour");
    145145        setValue_p("ssrQcBiasFile", "");
     146        setValue_p("ssrQcAntexFile", "");
    146147        setValue_p("ssrRefSignals", "");
    147148        // Feed Engine
  • trunk/BNC/src/bncwindow.cpp

    r11126 r11129  
    303303  _ssrQcBiasFileChooser = new qtFileChooser(0, qtFileChooser::File);
    304304  _ssrQcBiasFileChooser->setFileName(settings.value("ssrQcBiasFile").toString());
     305  _ssrQcAntexFileChooser = new qtFileChooser(0, qtFileChooser::File);
     306  _ssrQcAntexFileChooser->setFileName(settings.value("ssrQcAntexFile").toString());
    305307  _ssrQcIntrComboBox = new QComboBox();
    306308  _ssrQcIntrComboBox->setEditable(false);
    … …  
    933935  cLayout->addWidget(new QLabel("QC Bias File"), 8, 0);
    934936  cLayout->addWidget(_ssrQcBiasFileChooser, 8, 1, 1, 30);
    935   cLayout->addWidget(new QLabel(""), 9, 1);
    936   cLayout->setRowStretch(8, 999);
     937  cLayout->addWidget(new QLabel("QC ANTEX File"), 9, 0);
     938  cLayout->addWidget(_ssrQcAntexFileChooser, 9, 1, 1, 30);
     939  cLayout->addWidget(new QLabel(""), 10, 1);
     940  cLayout->setRowStretch(9, 999);
    937941  cgroup->setLayout(cLayout);
    938942
    … …  
    16061610  _ssrQcRefLineEdit->setWhatsThis(tr("<p>Specify the reference for the comparison of the orbits and clocks of the checked streams: 'CONSENSUS' for the median of all other streams, or the name of a stream, e.g. 'INTERNAL' for the combination.</p><p>Default is 'CONSENSUS'. <i>[key: ssrQcRef]</i></p>"));
    16071611  _ssrQcStreamsLineEdit->setWhatsThis(tr("<p>Specify the Broadcast Correction streams to be checked, as a comma separated list of stream names (e.g. SSRA00CNE1,SSRA00GMV0), or 'ALL' for all decoded streams. A stream with a leading '!' is excluded from the QC and from the consensus, e.g. 'ALL,!SIRGAS200001' for streams in another reference frame.</p><p>Default is 'ALL'. <i>[key: ssrQcStreams]</i></p>"));
     1612  _ssrQcAntexFileChooser->setWhatsThis(tr("<p>Specify an ANTEX file (e.g. the current igs20.atx) to enable the attitude check of the SSR QC: for streams sending the satellite yaw angle, the clock change of the stream relative to the other ACs during noon and midnight turns is compared with the change predicted from the difference of its yaw angle to BNC's attitude model ('Computed', see section 'Combination'). The ANTEX file provides the satellite block types the model needs.</p><p>Default is an empty option field, meaning no attitude check. <i>[key: ssrQcAntexFile]</i></p>"));
    16081613  _ssrQcBiasFileChooser->setWhatsThis(tr("<p>Specify a Bias SINEX file (OSB or DSB, e.g. a CAS DCB or a GFZ OSB product) as reference for the code bias check of the SSR QC: the DCBs of each stream are compared with the DCBs of this file, in addition to the comparison with the other ACs. A sign opposite to the reference for most DCBs indicates a stream using the SINEX instead of the SSR sign convention.</p><p>Default is an empty option field, meaning the code biases are compared with the other ACs only. <i>[key: ssrQcBiasFile]</i></p>"));
    16091614  _ssrQcIntrComboBox->setWhatsThis(tr("<p>Select the interval of the SSR QC reports. <i>[key: ssrQcIntr]</i></p>"));
    … …  
    18871892  delete _ssrQcIntrComboBox;
    18881893  delete _ssrQcBiasFileChooser;
     1894  delete _ssrQcAntexFileChooser;
    18891895  delete _corrPortLineEdit;
    18901896  delete _outPortLineEdit;
    … …  
    24192425  settings.setValue("ssrQcIntr", _ssrQcIntrComboBox->currentText());
    24202426  settings.setValue("ssrQcBiasFile", _ssrQcBiasFileChooser->fileName());
     2427  settings.setValue("ssrQcAntexFile", _ssrQcAntexFileChooser->fileName());
    24212428  settings.setValue("corrPort", _corrPortLineEdit->text());
    24222429  // Feed Engine
    … …  
    29592966    enableWidget(!_ssrQcPathLineEdit->text().isEmpty(), _ssrQcRefLineEdit);
    29602967    enableWidget(!_ssrQcPathLineEdit->text().isEmpty(), _ssrQcBiasFileChooser);
     2968    enableWidget(!_ssrQcPathLineEdit->text().isEmpty(), _ssrQcAntexFileChooser);
    29612969  }
    29622970
  • trunk/BNC/src/bncwindow.h

    r11087 r11129  
    155155    QLineEdit* _ssrQcRefLineEdit;
    156156    qtFileChooser* _ssrQcBiasFileChooser;
     157    qtFileChooser* _ssrQcAntexFileChooser;
    157158    QLineEdit* _miscMountLineEdit;
    158159    QLineEdit* _miscPortLineEdit;
  • 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;
  • trunk/BNC/src/ssrQc.h

    r11117 r11129  
    1616////////////////////////////////////////////////////////////////////////////
    1717class bncBiasSnx;
     18class bncAntex;
    1819
    1920class t_ssrQc : public QObject {
    … …  
    171172  QMap<QString, t_vtecStat>            _vtecStat;  // stream
    172173  bncTime                              _nextVtecCmp;
     174
     175  // Attitude check: for streams sending the yaw angle, the clock change of
     176  // the stream relative to the consensus during a turn (yaw difference to
     177  // BNC's attitude model changing by more than ATT_ONSET) is compared with
     178  // the change predicted by the phase wind-up, lambda_IF * dpsi / 2pi
     179  class t_yawLast {          // latest SSR yaw angle of a stream and satellite
     180   public:
     181    bncTime time;
     182    double  yaw  = 0.0;      // [rad]
     183    double  rate = 0.0;      // [rad/s]
     184  };
     185  class t_attSample {
     186   public:
     187    double t;                // [s] (tKey)
     188    double dc;               // clock difference to the consensus [m]
     189    double p;                // predicted clock difference, unwrapped [m]
     190    double dpsi;             // yaw difference model - stream, unwrapped [rad]
     191  };
     192  class t_attTrack {         // per stream and satellite
     193   public:
     194    bool                 active   = false;
     195    bool                 hasLast  = false;
     196    double               lastDPsi = 0.0;
     197    double               lastT    = 0.0;
     198    QVector<t_attSample> hist;   // recent samples before a turn
     199    QVector<t_attSample> event;  // samples of the current turn
     200    double               dc0 = 0.0, p0 = 0.0, dpsi0 = 0.0;  // before the turn
     201  };
     202  void   attitudeSample(const QString& staID, const QString& prn, const bncTime& tt,
     203                        const ColumnVector& xx, const ColumnVector& vv, double dc);
     204  void   attitudeEvent(const QString& staID, const QString& prn, t_attTrack& tr, const QString& end);
     205  bncAntex*                                       _antex;      // option ssrQcAntexFile
     206  QMap<QString, QMap<QString, t_yawLast> >        _yawLast;    // stream, PRN
     207  QMap<QString, QMap<QString, t_attTrack> >       _attTrack;   // stream, PRN
     208  QMap<QString, double>                           _psiModel;   // PRN: model yaw at _psiModelT
     209  double                                          _psiModelT = 0.0;
     210  QMap<QString, QMap<char, QStringList> >         _attEvents;  // stream, system: report lines
    173211};
    174212
Note: See TracChangeset for help on using the changeset viewer.