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


Ignore:
Timestamp:
Oct 5, 2026, 6:39:54 PM (5 days ago)
Author:
stuerze
Message:

ssr qc

File:
1 edited

Legend:

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

    r11073 r11075  
    4545#include "bncutils.h"
    4646#include "bncconst.h"
     47#include "bncsettings.h"
     48#include "combination/bnccomb.h"
    4749
    4850using namespace std;
    … …  
    5153  const double MAX_CLK_JUMP   = 0.5;  // [m] clock change beyond its rate between consecutive epochs
    5254  const double EPH_WARMUP     = 120.0; // [s] no ephemeris checks at the start
     55  const double CMP_SAMPL      = 30.0;  // [s] sampling of the stream comparison
     56  const double CMP_DELAY      = 60.0;  // [s] epochs compared when data this much newer arrived
     57  const double HIST_LENGTH    = 300.0; // [s] corrections kept for the comparison
     58  const double DEV_MIN        = 0.05;  // [m] deviating satellites: RMS above max(DEV_MIN,
     59  const double DEV_FACTOR     = 3.0;   //     DEV_FACTOR x median RMS of the system)
     60
     61  double tKey(const bncTime& tt) {
     62    return tt.gpsw() * 604800.0 + tt.gpssec();
     63  }
    5364  const char*  TYPE_NAME[]    = {"ORBIT", "CLOCK", "CODE_BIAS", "PHASE_BIAS"};
    5465
    … …  
    91102// Constructor
    92103////////////////////////////////////////////////////////////////////////////
    93 t_ssrQc::t_ssrQc(const QString& fileName, double interval) : _ephUser(true) {
    94   _fileName = fileName;
     104t_ssrQc::t_ssrQc(const QString& dirName, double interval, const QStringList& streams,
     105                 const QString& reference) : _ephUser(true) {
     106  _dirName   = dirName;
     107  _reference = reference.isEmpty() ? QString("CONSENSUS") : reference;
     108  bncSettings settings;
     109  QStringList cmbStreams = settings.value("cmbStreams").toStringList();
     110  if (!cmbStreams.isEmpty()) {
     111    _cmbMaster = cmbStreams[0].split(' ', Qt::SkipEmptyParts).value(0);
     112  }
    95113  _interval = interval > 0.0 ? interval : 3600.0;
    96   _out      = new ofstream(fileName.toLocal8Bit().data());
     114  for (const QString& sta : streams) {
     115    if (sta.toUpper() == "ALL") {
     116      _streams.clear();
     117      break;
     118    }
     119    _streams.insert(sta);
     120  }
    97121
    98122  connect(this, SIGNAL(newMessage(QByteArray,bool)), BNC_CORE, SLOT(slotMessage(const QByteArray,bool)));
    … …  
    105129  connect(BNC_CORE, SIGNAL(newPhaseBiases(QList<t_satPhaseBias>)),
    106130          this, SLOT(slotNewPhaseBiases(QList<t_satPhaseBias>)), Qt::DirectConnection);
    107 
    108   *_out << "SSR QC of decoded correction streams, report interval " << _interval << " s" << endl
    109         << "Checks: epochs and update intervals, gaps (interval > 2 x max(declared, median)), latency,"
    110         << " completeness w.r.t. healthy broadcast ephemerides," << endl
    111         << "IOD matching, code biases of the reference signals, clock jumps > " << MAX_CLK_JUMP
    112         << " m, largest orbit corrections" << endl;
     131  // combined corrections (stream INTERNAL)
     132  if (bncComb::exists()) {
     133    connect(BNC_CMB, SIGNAL(newOrbCorrections(QList<t_orbCorr>)),
     134            this, SLOT(slotNewOrbCorrections(QList<t_orbCorr>)), Qt::DirectConnection);
     135    connect(BNC_CMB, SIGNAL(newClkCorrections(QList<t_clkCorr>)),
     136            this, SLOT(slotNewClkCorrections(QList<t_clkCorr>)), Qt::DirectConnection);
     137  }
     138}
     139
     140// Stream selected for the QC
     141////////////////////////////////////////////////////////////////////////////
     142bool t_ssrQc::selected(const string& staID) const {
     143  return _streams.isEmpty() || _streams.contains(QString::fromStdString(staID));
     144}
     145
     146// Daily QC file of a stream: <staID>_S_<YYYYDDD>0000_01D_QC.txt
     147////////////////////////////////////////////////////////////////////////////
     148ofstream& t_ssrQc::streamOut(const QString& staID, const bncTime& time) {
     149  unsigned year, month, day;
     150  time.civil_date(year, month, day);
     151  QDate date(year, month, day);
     152  QString name = QDir(_dirName).filePath(QString("%1_S_%2%3%4_01D_QC.txt").arg(staID)
     153                 .arg(year).arg(date.dayOfYear(), 3, 10, QChar('0')).arg("0000"));
     154  if (_outNames.value(staID) != name) {
     155    delete _outs.value(staID, 0);
     156    bool exists = QFile::exists(name);
     157    ofstream* out = new ofstream(name.toLocal8Bit().data(), ios_base::out | ios_base::app);
     158    if (!exists) {
     159      *out << "SSR QC of stream " << staID.toStdString() << ", report interval " << _interval << " s" << endl
     160           << "Checks: epochs and update intervals, gaps (interval > 2 x max(declared, median)), latency,"
     161           << " completeness w.r.t. healthy broadcast ephemerides," << endl
     162           << "IOD matching, code and phase bias signals, code biases of the reference signals, clock jumps > "
     163           << MAX_CLK_JUMP << " m, largest orbit corrections" << endl;
     164    }
     165    _outs[staID]     = out;
     166    _outNames[staID] = name;
     167  }
     168  return *_outs[staID];
    113169}
    114170
    … …  
    118174  QMutexLocker locker(&_mutex);
    119175  writeReport(true);
    120   delete _out;
     176  for (ofstream* out : _outs) {
     177    delete out;
     178  }
    121179}
    122180
    … …  
    144202////////////////////////////////////////////////////////////////////////////
    145203void t_ssrQc::checkInterval(const bncTime& time) {
    146   if (!_startTime.valid()) {
    147     _startTime = time;
    148   }
    149204  if (!_intervalStart.valid()) {
    150205    double sec = floor(time.gpssec() / _interval) * _interval;
    151206    _intervalStart = bncTime(time.gpsw(), sec);
     207  }
     208  // stream comparison of the epochs already complete for all streams
     209  if (!_nextCmpEpoch.valid()) {
     210    double sec = ceil(time.gpssec() / CMP_SAMPL) * CMP_SAMPL;
     211    _nextCmpEpoch = bncTime(time.gpsw(), sec);
     212  }
     213  while (time - _nextCmpEpoch >= CMP_DELAY) {
     214    compareEpoch(_nextCmpEpoch);
     215    _nextCmpEpoch = _nextCmpEpoch + CMP_SAMPL;
    152216  }
    153217  if (time - _intervalStart >= _interval) {
    … …  
    165229void t_ssrQc::newEpoch(const string& staID, char sys, e_type type, const bncTime& time,
    166230                       unsigned updateInt, const QString& prn) {
     231  QString sta = QString::fromStdString(staID);
     232  if (!_staStart.contains(sta)) {
     233    _staStart[sta] = time;
     234  }
     235  if (!_staLast.contains(sta) || _staLast[sta] < time) {
     236    _staLast[sta] = time;
     237  }
    167238  t_epoStat& stat = _epoStat[key(staID, sys, type)];
    168239  if (updateInt < unsigned(ssrUpdateInt.size())) {
    … …  
    185256// Broadcast ephemeris the correction refers to
    186257////////////////////////////////////////////////////////////////////////////
    187 void t_ssrQc::checkEph(t_satStat& stat, const QString& prnInt, unsigned iod, const bncTime& time) {
     258void t_ssrQc::checkEph(t_satStat& stat, const QString& prnInt, unsigned iod, const bncTime& time,
     259                       const string& staID) {
    188260  // not before all broadcast ephemerides may have been received
    189   if (time - _startTime < EPH_WARMUP) {
     261  if (time - _staStart[QString::fromStdString(staID)] < EPH_WARMUP) {
    190262    return;
    191263  }
    … …  
    211283  QMutexLocker locker(&_mutex);
    212284  for (const t_orbCorr& corr : orbCorrections) {
     285    if (selected(corr._staID) || corr._staID == _reference.toStdString()) {
     286      storeHist(&corr, 0);
     287    }
     288    if (!selected(corr._staID)) continue;
    213289    QString prn = QString::fromStdString(corr._prn.toString());
    214290    checkInterval(corr._time);
    … …  
    216292    t_satStat& stat = _satStat[QString::fromStdString(corr._staID) + ' ' + prn];
    217293    stat.numOrb += 1;
    218     checkEph(stat, QString::fromStdString(corr._prn.toInternalString()), corr._iod, corr._time);
     294    checkEph(stat, QString::fromStdString(corr._prn.toInternalString()), corr._iod, corr._time, corr._staID);
    219295    double radial = fabs(corr._xr[0]);
    220296    double alongCross = max(fabs(corr._xr[1]), fabs(corr._xr[2]));
    … …  
    229305  QMutexLocker locker(&_mutex);
    230306  for (const t_clkCorr& corr : clkCorrections) {
     307    if (selected(corr._staID) || corr._staID == _reference.toStdString()) {
     308      storeHist(0, &corr);
     309    }
     310    if (!selected(corr._staID)) continue;
    231311    QString prn = QString::fromStdString(corr._prn.toString());
    232312    checkInterval(corr._time);
    … …  
    234314    t_satStat& stat = _satStat[QString::fromStdString(corr._staID) + ' ' + prn];
    235315    stat.numClk += 1;
    236     checkEph(stat, QString::fromStdString(corr._prn.toInternalString()), corr._iod, corr._time);
     316    checkEph(stat, QString::fromStdString(corr._prn.toInternalString()), corr._iod, corr._time, corr._staID);
    237317
    238318    // jump: change between consecutive epochs (same IOD) beyond the clock rate
    … …  
    260340  QMutexLocker locker(&_mutex);
    261341  for (const t_satCodeBias& bias : codeBiases) {
     342    bool sel = selected(bias._staID);
     343    if (!sel && bias._staID != _reference.toStdString()) continue;
    262344    char sys = bias._prn.system();
    263345    QString prn = QString::fromStdString(bias._prn.toString());
    264     checkInterval(bias._time);
    265     newEpoch(bias._staID, sys, CBIAS, bias._time, bias._updateInt, prn);
     346    if (sel) {
     347      checkInterval(bias._time);
     348      newEpoch(bias._staID, sys, CBIAS, bias._time, bias._updateInt, prn);
     349    }
    266350    QString staSys = QString("%1 %2").arg(bias._staID.c_str()).arg(sys);
    267351    QMap<QString, double> values;
    … …  
    302386  QMutexLocker locker(&_mutex);
    303387  for (const t_satPhaseBias& bias : phaseBiases) {
     388    if (!selected(bias._staID)) continue;
    304389    checkInterval(bias._time);
    305390    newEpoch(bias._staID, bias._prn.system(), PBIAS, bias._time, bias._updateInt,
    … …  
    312397}
    313398
     399// Reference point of the orbits of a stream: APC (SSRA...), CoM (SSRC...);
     400// the combined stream INTERNAL refers to that of its master AC (the first
     401// AC of the combination)
     402////////////////////////////////////////////////////////////////////////////
     403char t_ssrQc::refPoint(const QString& staID) const {
     404  QString sta = (staID == "INTERNAL") ? _cmbMaster : staID;
     405  if (sta.startsWith("SSRA")) return 'A';
     406  if (sta.startsWith("SSRC")) return 'C';
     407  return '?';
     408}
     409
     410// Keep the recent orbit and clock corrections of a stream
     411////////////////////////////////////////////////////////////////////////////
     412void t_ssrQc::storeHist(const t_orbCorr* orb, const t_clkCorr* clk) {
     413  if (orb) {
     414    QMap<double, t_orbCorr>& hist = _orbHist[QString::fromStdString(orb->_staID)]
     415                                            [QString::fromStdString(orb->_prn.toInternalString())];
     416    hist[tKey(orb->_time)] = *orb;
     417    while (!hist.isEmpty() && hist.firstKey() < tKey(orb->_time) - HIST_LENGTH) {
     418      hist.erase(hist.begin());
     419    }
     420  }
     421  if (clk) {
     422    QMap<double, t_clkCorr>& hist = _clkHist[QString::fromStdString(clk->_staID)]
     423                                            [QString::fromStdString(clk->_prn.toInternalString())];
     424    hist[tKey(clk->_time)] = *clk;
     425    while (!hist.isEmpty() && hist.firstKey() < tKey(clk->_time) - HIST_LENGTH) {
     426      hist.erase(hist.begin());
     427    }
     428  }
     429}
     430
     431// Compare the orbits and clocks of all streams at epoch tt
     432////////////////////////////////////////////////////////////////////////////
     433void t_ssrQc::compareEpoch(const bncTime& tt) {
     434  double tk = tKey(tt);
     435
     436  // Satellite positions and clocks of all streams (latest corrections <= tt)
     437  QMap<QString, QMap<QString, t_satCrd> > crd;   // stream, PRN (external)
     438  QMapIterator<QString, QMap<QString, QMap<double, t_clkCorr> > > itSta(_clkHist);
     439  while (itSta.hasNext()) {
     440    itSta.next();
     441    const QString& staID = itSta.key();
     442    QMapIterator<QString, QMap<double, t_clkCorr> > itPrn(itSta.value());
     443    while (itPrn.hasNext()) {
     444      itPrn.next();
     445      const QString& prnInt = itPrn.key();
     446      QMap<double, t_clkCorr>::const_iterator itC = itPrn.value().upperBound(tk);
     447      if (itC == itPrn.value().constBegin()) {
     448        continue;
     449      }
     450      const t_clkCorr& clk = (--itC).value();
     451      const QMap<double, t_orbCorr>& orbHist = _orbHist[staID][prnInt];
     452      QMap<double, t_orbCorr>::const_iterator itO = orbHist.upperBound(tk);
     453      if (itO == orbHist.constBegin()) {
     454        continue;
     455      }
     456      const t_orbCorr& orb = (--itO).value();
     457      t_eph* eph = 0;
     458      t_eph* ephLast = _ephUser.ephLast(prnInt);
     459      t_eph* ephPrev = _ephUser.ephPrev(prnInt);
     460      if      (ephLast && ephLast->IOD() == orb._iod) eph = ephLast;
     461      else if (ephPrev && ephPrev->IOD() == orb._iod) eph = ephPrev;
     462      if (!eph || clk._iod != orb._iod) {
     463        continue;
     464      }
     465      eph->setOrbCorr(&orb);
     466      eph->setClkCorr(&clk);
     467      ColumnVector xc(6), vv(3);
     468      if (eph->getCrd(tt, xc, vv, true) != success) {
     469        continue;
     470      }
     471      t_satCrd& sc = crd[staID][QString::fromStdString(clk._prn.toString())];
     472      sc.xx  = xc.Rows(1,3);
     473      sc.vv  = vv;
     474      sc.clk = xc(4) * t_CST::c;
     475      sc.refPoint = refPoint(staID);
     476      // clock referred to the reference signals (as in the combination):
     477      // + B_IF of the stream's code biases; the combination (INTERNAL)
     478      // refers to them already
     479      sc.clkOK = (staID == "INTERNAL");
     480      QString satKey = staID + ' ' + QString::fromStdString(clk._prn.toString());
     481      if (!sc.clkOK && _satStat.contains(satKey) && _satStat[satKey].refBiasOK) {
     482        sc.clk  += _satStat[satKey].biasIF;
     483        sc.clkOK = true;
     484      }
     485    }
     486  }
     487
     488  // Satellites per system
     489  QMap<char, QSet<QString> > satsSys;
     490  QMapIterator<QString, QMap<QString, t_satCrd> > itCrd(crd);
     491  while (itCrd.hasNext()) {
     492    itCrd.next();
     493    for (const QString& prn : itCrd.value().keys()) {
     494      satsSys[prn[0].toLatin1()].insert(prn);
     495    }
     496  }
     497
     498  QMapIterator<char, QSet<QString> > itSys(satsSys);
     499  while (itSys.hasNext()) {
     500    itSys.next();
     501    const QSet<QString>& sats = itSys.value();
     502    QStringList staList;
     503    for (const QString& staID : crd.keys()) {
     504      for (const QString& prn : sats) {
     505        if (crd[staID].contains(prn)) {
     506          staList << staID;
     507          break;
     508        }
     509      }
     510    }
     511
     512    // Clock datum: offset of each stream w.r.t. the per-satellite median of
     513    // all streams (or w.r.t. the reference stream)
     514    QMap<QString, double> offset;
     515    for (const QString& staID : staList) {
     516      QVector<double> diffs;
     517      for (const QString& prn : sats) {
     518        if (!crd[staID].contains(prn) || !crd[staID][prn].clkOK) continue;
     519        if (_reference == "CONSENSUS") {
     520          QVector<double> all;
     521          for (const QString& oo : staList) {
     522            if (oo != "INTERNAL" && crd[oo].contains(prn) && crd[oo][prn].clkOK) all << crd[oo][prn].clk;
     523          }
     524          if (all.size() >= 3) diffs << crd[staID][prn].clk - median(all);
     525        }
     526        else if (crd.value(_reference).contains(prn) && crd[_reference][prn].clkOK) {
     527          diffs << crd[staID][prn].clk - crd[_reference][prn].clk;
     528        }
     529      }
     530      if (!diffs.isEmpty()) {
     531        offset[staID] = median(diffs);
     532      }
     533    }
     534
     535    // Differences of each selected stream to its reference
     536    for (const QString& staID : staList) {
     537      if (!selected(staID.toStdString()) || staID == _reference) {
     538        continue;
     539      }
     540      QMap<QString, double> dClk;
     541      QMap<QString, ColumnVector> dRac;
     542      for (const QString& prn : sats) {
     543        if (!crd[staID].contains(prn)) continue;
     544        const t_satCrd& sc = crd[staID][prn];
     545        QStringList refs;
     546        if (_reference == "CONSENSUS") {
     547          for (const QString& oo : staList) {
     548            // the combination (INTERNAL) is derived from the other streams
     549            if (oo != staID && oo != "INTERNAL" && crd[oo].contains(prn)) refs << oo;
     550          }
     551          if (refs.size() < 2) continue;
     552          refs.clear();
     553          for (const QString& oo : staList) {
     554            if (oo != staID && oo != "INTERNAL" && crd[oo].contains(prn)) refs << oo;
     555          }
     556        }
     557        else {
     558          if (!crd.value(_reference).contains(prn)) continue;
     559          refs << _reference;
     560        }
     561        QVector<double> clks, xs[3];
     562        for (const QString& oo : refs) {
     563          const t_satCrd& rc = crd[oo][prn];
     564          if (rc.clkOK && (oo == _reference || offset.contains(oo))) {
     565            clks << rc.clk - (oo == _reference ? 0.0 : offset[oo]);
     566          }
     567          if (rc.refPoint == sc.refPoint && sc.refPoint != '?') {
     568            for (int ii = 0; ii < 3; ii++) xs[ii] << rc.xx[ii];
     569          }
     570        }
     571        bool clkCmp = sc.clkOK && offset.contains(staID) &&
     572                      (_reference == "CONSENSUS" ? clks.size() >= 2 : clks.size() == 1);
     573        if (clkCmp) {
     574          dClk[prn] = sc.clk - offset[staID] - median(clks);
     575        }
     576        else if (!sc.clkOK) {
     577          _cmpStat[staID][prn].clkNotReferred += 1;
     578        }
     579        if (xs[0].size() == refs.size() || (xs[0].size() >= 2 && _reference == "CONSENSUS")) {
     580          ColumnVector dx(3);
     581          for (int ii = 0; ii < 3; ii++) dx[ii] = sc.xx[ii] - median(xs[ii]);
     582          ColumnVector er = sc.xx / sc.xx.NormFrobenius();
     583          ColumnVector ec = crossproduct(sc.xx, sc.vv);
     584          ec /= ec.NormFrobenius();
     585          ColumnVector ea = crossproduct(ec, er);
     586          ColumnVector rac(3);
     587          rac[0] = DotProduct(dx, er);
     588          rac[1] = DotProduct(dx, ea);
     589          rac[2] = DotProduct(dx, ec);
     590          dRac[prn] = rac;
     591        }
     592      }
     593      if (dClk.isEmpty() && dRac.isEmpty()) {
     594        continue;
     595      }
     596      // remaining common clock offset of this stream and epoch
     597      double off = dClk.isEmpty() ? 0.0 : median(dClk.values().toVector());
     598      _cmpEpochs[staID] += 1;
     599      QSet<QString> prns;
     600      for (const QString& prn : dClk.keys()) prns.insert(prn);
     601      for (const QString& prn : dRac.keys()) prns.insert(prn);
     602      for (const QString& prn : prns) {
     603        t_cmpStat& stat = _cmpStat[staID][prn];
     604        if (dRac.contains(prn)) {
     605          const ColumnVector& rac = dRac[prn];
     606          stat.numOrb += 1;
     607          stat.sR   += rac[0] * rac[0];
     608          stat.sA   += rac[1] * rac[1];
     609          stat.sC   += rac[2] * rac[2];
     610        }
     611        if (dClk.contains(prn)) {
     612          double dc = dClk[prn] - off;
     613          stat.numClk += 1;
     614          stat.sClk   += dc * dc;
     615          stat.mClk   += dc;
     616          if (dRac.contains(prn)) {
     617            double sis = dRac[prn][0] - dc;
     618            stat.numSis += 1;
     619            stat.sSis   += sis * sis;
     620            stat.mSis   += sis;
     621          }
     622        }
     623      }
     624    }
     625  }
     626}
     627
    314628// Write the report of the current interval and reset the statistics
    315629////////////////////////////////////////////////////////////////////////////
    316630void t_ssrQc::writeReport(bool final) {
    317   if (!_out || _epoStat.isEmpty()) {
     631  if (_epoStat.isEmpty()) {
    318632    return;
    319633  }
    320634  bncTime endTime = final ? _lastTime : _intervalStart + _interval;
    321   *_out << endl << "=== SSR QC " << _intervalStart.datestr() << ' ' << _intervalStart.timestr(0)
    322         << " - " << endTime.datestr() << ' ' << endTime.timestr(0)
    323         << (final ? " (end of data)" : "") << " ===" << endl;
    324635
    325636  // Healthy and current broadcast ephemerides per system
    … …  
    347658    QList<char> systems = itSta.value().values();
    348659    std::sort(systems.begin(), systems.end());
    349     *_out << endl << "Stream " << staID.toStdString() << endl
    350           << "  Sys Type        Epochs  Interval nom/med/max [s]  Gaps  Out-of-order"
     660    ofstream& out = streamOut(staID, _intervalStart);
     661    if (final) {
     662      endTime = _staLast.value(staID, _lastTime);
     663    }
     664    out << endl << "=== SSR QC " << staID.toStdString() << ' '
     665        << _intervalStart.datestr() << ' ' << _intervalStart.timestr(0)
     666        << " - " << endTime.datestr() << ' ' << endTime.timestr(0)
     667        << (final ? " (end of data)" : "") << " ===" << endl
     668        << "  Sys Type        Epochs  Interval nom/med/max [s]  Gaps  Out-of-order"
    351669          << "  Latency med/max [s]  Sats" << endl;
    352670    for (char sys : systems) {
    … …  
    369687        double maxLat = stat.latencies.isEmpty() ? 0.0 :
    370688                        *std::max_element(stat.latencies.begin(), stat.latencies.end());
    371         *_out << "  " << sys << "   " << left << setw(10) << TYPE_NAME[type] << right
     689        out << "  " << sys << "   " << left << setw(10) << TYPE_NAME[type] << right
    372690              << setw(8) << stat.numEpo
    373691              << setw(10) << stat.nominal << " /" << setw(6) << setprecision(1) << fixed << medInt
    … …  
    484802                 .arg(maxRadial, 0, 'f', 2).arg(maxRadialPrn).arg(maxAlongCross, 0, 'f', 2).arg(maxAlongCrossPrn);
    485803      }
     804      // stream comparison
     805      if (_cmpStat.contains(staID)) {
     806        unsigned nSat = 0, nClk = 0, nOrb = 0, nSis = 0;
     807        double sClk = 0.0, sR = 0.0, sA = 0.0, sC = 0.0, sSis = 0.0;
     808        double vClk = 0.0, vSis = 0.0;  // sums of squares about the satellite means
     809        QMap<QString, double> rmsSis, rmsClk, rmsOrb;
     810        QMap<QString, unsigned> notReferred;
     811        QMapIterator<QString, t_cmpStat> itC(_cmpStat[staID]);
     812        while (itC.hasNext()) {
     813          itC.next();
     814          if (itC.key()[0].toLatin1() != sys) continue;
     815          const t_cmpStat& st = itC.value();
     816          if (st.numClk == 0 && st.numOrb == 0) {
     817            if (st.clkNotReferred) notReferred[itC.key()] = 0;
     818            continue;
     819          }
     820          ++nSat;
     821          if (st.clkNotReferred) notReferred[itC.key()] = 0;
     822          nClk += st.numClk; sClk += st.sClk;
     823          nOrb += st.numOrb; sR += st.sR; sA += st.sA; sC += st.sC;
     824          nSis += st.numSis; sSis += st.sSis;
     825          if (st.numClk) vClk += st.sClk - st.mClk * st.mClk / st.numClk;
     826          if (st.numSis) vSis += st.sSis - st.mSis * st.mSis / st.numSis;
     827          if (st.numSis) rmsSis[itC.key()] = sqrt(st.sSis / st.numSis);
     828          else if (st.numClk) rmsClk[itC.key()] = sqrt(st.sClk / st.numClk);
     829          if (st.numOrb) rmsOrb[itC.key()] = sqrt((st.sR + st.sA + st.sC) / st.numOrb);
     830        }
     831        if (nSat) {
     832          QString refText = (_reference == "CONSENSUS") ? QString("consensus of the other streams")
     833                                                        : QString("stream %1").arg(_reference);
     834          QStringList parts;
     835          if (nClk) parts << QString("clock %1 (STD %2)").arg(sqrt(sClk / nClk), 0, 'f', 3)
     836                                                          .arg(sqrt(max(0.0, vClk) / nClk), 0, 'f', 3);
     837          if (nOrb) parts << QString("radial %1, along %2, cross %3")
     838                             .arg(sqrt(sR / nOrb), 0, 'f', 3).arg(sqrt(sA / nOrb), 0, 'f', 3)
     839                             .arg(sqrt(sC / nOrb), 0, 'f', 3);
     840          if (nSis) parts << QString("SIS (radial - clock) %1 (STD %2)").arg(sqrt(sSis / nSis), 0, 'f', 3)
     841                                                                         .arg(sqrt(max(0.0, vSis) / nSis), 0, 'f', 3);
     842          lines << QString("comparison with %1 (%2 satellites), RMS (STD about the satellite means) [m]: %3")
     843                   .arg(refText).arg(nSat).arg(parts.join(", "));
     844          if (!nOrb) {
     845            lines << "orbits not compared: no other stream with the same reference point (APC/CoM)";
     846          }
     847          addSats("clocks not compared (code biases of the reference signals missing)", notReferred);
     848          auto deviating = [&lines](const QString& what, const QMap<QString, double>& rms) {
     849            if (rms.size() < 2) return;
     850            double med = median(rms.values().toVector());
     851            QStringList dev;
     852            QMapIterator<QString, double> itR(rms);
     853            while (itR.hasNext()) {
     854              itR.next();
     855              if (itR.value() > max(DEV_MIN, DEV_FACTOR * med)) {
     856                dev << QString("%1(%2)").arg(itR.key()).arg(itR.value(), 0, 'f', 2);
     857              }
     858            }
     859            if (!dev.isEmpty()) {
     860              lines << QString("satellites deviating from the reference, RMS %1 [m]: %2").arg(what).arg(dev.join(" "));
     861            }
     862          };
     863          deviating("SIS", rmsSis);
     864          deviating("clock", rmsClk);
     865          deviating("orbit (3D)", rmsOrb);
     866        }
     867      }
    486868      for (const QString& line : lines) {
    487         *_out << "  " << sys << " " << line.toStdString() << endl;
    488       }
    489     }
    490   }
    491   _out->flush();
    492 
    493   emit newMessage(QString("SSR QC: report %1 %2 written to %3")
     869        out << "  " << sys << " " << line.toStdString() << endl;
     870      }
     871    }
     872    out.flush();
     873  }
     874
     875  emit newMessage(QString("SSR QC: report %1 %2 of %3 stream(s) written to %4")
    494876                  .arg(QString::fromStdString(_intervalStart.datestr()))
    495877                  .arg(QString::fromStdString(_intervalStart.timestr(0)))
    496                   .arg(_fileName).toLatin1(), false);
     878                  .arg(streams.size()).arg(_dirName).toLatin1(), false);
     879
     880  _cmpStat.clear();
     881  _cmpEpochs.clear();
    497882
    498883  // reset, keeping the state needed across intervals
Note: See TracChangeset for help on using the changeset viewer.