Index: /trunk/BNC/src/bnchelp.html
===================================================================
--- /trunk/BNC/src/bnchelp.html	(revision 11080)
+++ /trunk/BNC/src/bnchelp.html	(revision 11081)
@@ -5083,5 +5083,6 @@
     ('--file'). Specify a 'QC Directory' to activate this function. 'QC Streams' selects the streams to be checked:
     'ALL' (default) for all decoded Broadcast Correction streams, or a comma separated list of stream names, e.g.
-    'SSRA00CNE1,SSRA00GMV0'. 'QC Reference' selects the reference of the stream comparison (see below). Select
+    'SSRA00CNE1,SSRA00GMV0'. A stream name with a leading '!' excludes the stream from the QC and from the consensus,
+    e.g. 'ALL,!SIRGAS200001' for a stream referring to another reference frame than the ITRF. 'QC Reference' selects the reference of the stream comparison (see below). Select
     the 'QC Interval' for the reports (default: 1 hour). One daily file
     '&lt;stream&gt;_S_&lt;YYYYDDD&gt;0000_01D_QC.txt' is written per stream; at the end of each interval and at the
@@ -5146,7 +5147,11 @@
   </p>
   <ul>
-    <li>'CONSENSUS' (default): per satellite the median of all other streams (at least two). A single erroneous stream
-      does not affect it, and no combination is needed. The combined stream 'INTERNAL' is not part of the consensus,
-      as it is derived from the other streams.</li>
+    <li>'CONSENSUS' (default): per satellite the median of the other analysis centers (at least two). All decoded
+      streams contribute, also those not selected under 'QC Streams' (except the excluded ones). An AC with several
+      streams (IGS-SSR and RTCM-SSR format, APC and CoM) has one vote, the median of its streams; the AC is derived
+      from the stream name (e.g. 'CNE' from 'SSRA00CNE1'). A stream is compared without the streams of its own AC.
+      Combination products ('INTERNAL' and the IGS combinations '...IGS.') do not vote, as they are derived from the
+      ACs, but are compared with the consensus. A single erroneous AC does not affect the consensus, and no
+      combination is needed.</li>
     <li>the name of a stream, e.g. 'INTERNAL' for the result of the combination (see section 'Combination') or the
       stream of a selected AC. Note that an AC contributing to the combination is compared partly with itself, and
Index: /trunk/BNC/src/bncmain.cpp
===================================================================
--- /trunk/BNC/src/bncmain.cpp	(revision 11080)
+++ /trunk/BNC/src/bncmain.cpp	(revision 11081)
@@ -211,5 +211,5 @@
         "   corrPort {Output port [integer number]}\n"
         "   ssrQcPath    {Directory for SSR QC files, one per stream [character string]}\n"
-        "   ssrQcStreams {Streams to check [character string: ALL or comma separated list of stream names]}\n"
+        "   ssrQcStreams {Streams to check [character string: ALL or comma separated list of stream names; '!' excludes a stream, e.g. ALL,!SIRGAS200001]}\n"
         "   ssrQcRef     {Reference of the stream comparison [character string: CONSENSUS or a stream name, e.g. INTERNAL]}\n"
         "   ssrQcIntr    {SSR QC report interval [character string: 5 min|15 min|30 min|1 hour|1 day]}\n"
Index: /trunk/BNC/src/bncwindow.cpp
===================================================================
--- /trunk/BNC/src/bncwindow.cpp	(revision 11080)
+++ /trunk/BNC/src/bncwindow.cpp	(revision 11081)
@@ -1596,5 +1596,5 @@
   _ssrQcPathLineEdit->setWhatsThis(tr("<p>Specify a directory for the quality control of decoded Broadcast Correction streams: epochs, update intervals, gaps, latency, completeness, IODs, code and phase bias signals, code biases of the reference signals, clock jumps and orbit correction size. One daily file is written per stream.</p><p>Default is an empty option field, meaning that no SSR QC is done. <i>[key: ssrQcPath]</i></p>"));
   _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>"));
-  _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.</p><p>Default is 'ALL'. <i>[key: ssrQcStreams]</i></p>"));
+  _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>"));
   _ssrQcIntrComboBox->setWhatsThis(tr("<p>Select the interval of the SSR QC reports. <i>[key: ssrQcIntr]</i></p>"));
   _corrPortLineEdit->setWhatsThis(tr("<p>BNC can produce Broadcast Ephemeris Corrections on your local host through an IP port.</p><p>Specify a port number here to activate this function. <i>[key: corrPort]</i></p>"));
Index: /trunk/BNC/src/ssrQc.cpp
===================================================================
--- /trunk/BNC/src/ssrQc.cpp	(revision 11080)
+++ /trunk/BNC/src/ssrQc.cpp	(revision 11081)
@@ -107,10 +107,20 @@
   _reference = reference.isEmpty() ? QString("CONSENSUS") : reference;
   _interval = interval > 0.0 ? interval : 3600.0;
+  // "ALL" or a list of streams; streams with a leading '!' are excluded
+  // (from the reports and from the consensus)
+  bool all = false;
   for (const QString& sta : streams) {
-    if (sta.toUpper() == "ALL") {
-      _streams.clear();
-      break;
-    }
-    _streams.insert(sta);
+    if (sta.startsWith('!')) {
+      _excluded.insert(sta.mid(1));
+    }
+    else if (sta.toUpper() == "ALL") {
+      all = true;
+    }
+    else {
+      _streams.insert(sta);
+    }
+  }
+  if (all) {
+    _streams.clear();
   }
 
@@ -136,5 +146,22 @@
 ////////////////////////////////////////////////////////////////////////////
 bool t_ssrQc::selected(const string& staID) const {
-  return _streams.isEmpty() || _streams.contains(QString::fromStdString(staID));
+  QString sta = QString::fromStdString(staID);
+  return !_excluded.contains(sta) && (_streams.isEmpty() || _streams.contains(sta));
+}
+
+// Analysis center of a stream: the 3 characters of the AC in mountpoints
+// like SSRA00CNE1, otherwise the stream name itself
+////////////////////////////////////////////////////////////////////////////
+QString t_ssrQc::acOf(const QString& staID) {
+  static const QRegularExpression re("^SSR[AC][0-9]{2}([A-Z0-9]{3})[0-9]$");
+  QRegularExpressionMatch mm = re.match(staID);
+  return mm.hasMatch() ? mm.captured(1) : staID;
+}
+
+// Stream part of the consensus: not excluded, and not a combination product
+// (INTERNAL, IGS combinations), as these are derived from the other streams
+////////////////////////////////////////////////////////////////////////////
+bool t_ssrQc::votes(const QString& staID) const {
+  return !_excluded.contains(staID) && staID != "INTERNAL" && acOf(staID) != "IGS";
 }
 
@@ -278,5 +305,5 @@
   QMutexLocker locker(&_mutex);
   for (const t_orbCorr& corr : orbCorrections) {
-    if (selected(corr._staID) || corr._staID == _reference.toStdString()) {
+    if (!_excluded.contains(QString::fromStdString(corr._staID)) || corr._staID == _reference.toStdString()) {
       storeHist(&corr, 0);
     }
@@ -300,5 +327,5 @@
   QMutexLocker locker(&_mutex);
   for (const t_clkCorr& corr : clkCorrections) {
-    if (selected(corr._staID) || corr._staID == _reference.toStdString()) {
+    if (!_excluded.contains(QString::fromStdString(corr._staID)) || corr._staID == _reference.toStdString()) {
       storeHist(0, &corr);
     }
@@ -336,5 +363,5 @@
   for (const t_satCodeBias& bias : codeBiases) {
     bool sel = selected(bias._staID);
-    if (!sel && bias._staID != _reference.toStdString()) continue;
+    if (_excluded.contains(QString::fromStdString(bias._staID)) && bias._staID != _reference.toStdString()) continue;
     char sys = bias._prn.system();
     QString prn = QString::fromStdString(bias._prn.toString());
@@ -507,7 +534,27 @@
     }
 
-    // Clock datum: offset of each stream w.r.t. the per-satellite median of
-    // all streams (or w.r.t. the reference stream)
+    // Consensus: one value per AC (median of its streams), so that an AC
+    // with several streams (formats, APC/CoM) has one vote; the streams of
+    // the AC compared are left out (leave-one-AC-out)
+    auto acClocks = [&](const QString& prn, const QMap<QString, double>& off, bool useOff) {
+      QMap<QString, QVector<double> > perAC;
+      for (const QString& oo : staList) {
+        if (!votes(oo) || !crd[oo].contains(prn) || !crd[oo][prn].clkOK) continue;
+        if (useOff && !off.contains(oo)) continue;
+        perAC[acOf(oo)] << crd[oo][prn].clk - (useOff ? off[oo] : 0.0);
+      }
+      QMap<QString, double> res;
+      for (const QString& ac : perAC.keys()) res[ac] = median(perAC[ac]);
+      return res;
+    };
+
+    // Clock datum: offset of each stream w.r.t. the per-satellite consensus
+    // of all ACs (or w.r.t. the reference stream)
     QMap<QString, double> offset;
+    QMap<QString, double> noOff;
+    QMap<QString, QMap<QString, double> > rawAC;  // PRN, AC
+    if (_reference == "CONSENSUS") {
+      for (const QString& prn : sats) rawAC[prn] = acClocks(prn, noOff, false);
+    }
     for (const QString& staID : staList) {
       QVector<double> diffs;
@@ -515,9 +562,5 @@
         if (!crd[staID].contains(prn) || !crd[staID][prn].clkOK) continue;
         if (_reference == "CONSENSUS") {
-          QVector<double> all;
-          for (const QString& oo : staList) {
-            if (oo != "INTERNAL" && crd[oo].contains(prn) && crd[oo][prn].clkOK) all << crd[oo][prn].clk;
-          }
-          if (all.size() >= 3) diffs << crd[staID][prn].clk - median(all);
+          if (rawAC[prn].size() >= 3) diffs << crd[staID][prn].clk - median(rawAC[prn].values().toVector());
         }
         else if (crd.value(_reference).contains(prn) && crd[_reference][prn].clkOK) {
@@ -528,4 +571,8 @@
         offset[staID] = median(diffs);
       }
+    }
+    QMap<QString, QMap<QString, double> > alignedAC;  // PRN, AC
+    if (_reference == "CONSENSUS") {
+      for (const QString& prn : sats) alignedAC[prn] = acClocks(prn, offset, true);
     }
 
@@ -535,4 +582,5 @@
         continue;
       }
+      const QString ownAC = acOf(staID);
       QMap<QString, double> dClk;
       QMap<QString, ColumnVector> dRac;
@@ -540,33 +588,34 @@
         if (!crd[staID].contains(prn)) continue;
         const t_satCrd& sc = crd[staID][prn];
-        QStringList refs;
+        QVector<double> clks, xs[3];
         if (_reference == "CONSENSUS") {
+          QMapIterator<QString, double> itA(alignedAC[prn]);
+          while (itA.hasNext()) {
+            itA.next();
+            if (itA.key() != ownAC) clks << itA.value();
+          }
+          // orbits: per AC the median of its streams with the same reference point
+          QMap<QString, QVector<double> > perAC[3];
           for (const QString& oo : staList) {
-            // the combination (INTERNAL) is derived from the other streams
-            if (oo != staID && oo != "INTERNAL" && crd[oo].contains(prn)) refs << oo;
-          }
-          if (refs.size() < 2) continue;
-          refs.clear();
-          for (const QString& oo : staList) {
-            if (oo != staID && oo != "INTERNAL" && crd[oo].contains(prn)) refs << oo;
-          }
+            if (!votes(oo) || acOf(oo) == ownAC || !crd[oo].contains(prn)) continue;
+            const t_satCrd& rc = crd[oo][prn];
+            if (rc.refPoint != sc.refPoint || sc.refPoint == '?') continue;
+            for (int ii = 0; ii < 3; ii++) perAC[ii][acOf(oo)] << rc.xx[ii];
+          }
+          for (const QString& ac : perAC[0].keys()) {
+            for (int ii = 0; ii < 3; ii++) xs[ii] << median(perAC[ii][ac]);
+          }
+          if (clks.size() < 2) clks.clear();
+          if (xs[0].size() < 2) for (int ii = 0; ii < 3; ii++) xs[ii].clear();
         }
         else {
           if (!crd.value(_reference).contains(prn)) continue;
-          refs << _reference;
-        }
-        QVector<double> clks, xs[3];
-        for (const QString& oo : refs) {
-          const t_satCrd& rc = crd[oo][prn];
-          if (rc.clkOK && (oo == _reference || offset.contains(oo))) {
-            clks << rc.clk - (oo == _reference ? 0.0 : offset[oo]);
-          }
+          const t_satCrd& rc = crd[_reference][prn];
+          if (rc.clkOK) clks << rc.clk;
           if (rc.refPoint == sc.refPoint && sc.refPoint != '?') {
             for (int ii = 0; ii < 3; ii++) xs[ii] << rc.xx[ii];
           }
         }
-        bool clkCmp = sc.clkOK && offset.contains(staID) &&
-                      (_reference == "CONSENSUS" ? clks.size() >= 2 : clks.size() == 1);
-        if (clkCmp) {
+        if (sc.clkOK && offset.contains(staID) && !clks.isEmpty()) {
           dClk[prn] = sc.clk - offset[staID] - median(clks);
         }
@@ -574,5 +623,5 @@
           _cmpStat[staID][prn].clkNotReferred += 1;
         }
-        if (xs[0].size() == refs.size() || (xs[0].size() >= 2 && _reference == "CONSENSUS")) {
+        if (!xs[0].isEmpty()) {
           ColumnVector dx(3);
           for (int ii = 0; ii < 3; ii++) dx[ii] = sc.xx[ii] - median(xs[ii]);
@@ -827,5 +876,5 @@
         }
         if (nSat) {
-          QString refText = (_reference == "CONSENSUS") ? QString("consensus of the other streams")
+          QString refText = (_reference == "CONSENSUS") ? QString("consensus of the other ACs")
                                                         : QString("stream %1").arg(_reference);
           QStringList parts;
Index: /trunk/BNC/src/ssrQc.h
===================================================================
--- /trunk/BNC/src/ssrQc.h	(revision 11080)
+++ /trunk/BNC/src/ssrQc.h	(revision 11081)
@@ -80,4 +80,6 @@
   QMutex                    _mutex;
   bool           selected(const std::string& staID) const;
+  bool           votes(const QString& staID) const;
+  static QString acOf(const QString& staID);
 
   // Stream comparison: every CMP_SAMPL seconds, the orbits and clocks of each
@@ -116,4 +118,5 @@
   QString                   _dirName;
   QSet<QString>             _streams;   // selected streams, empty: all
+  QSet<QString>             _excluded;  // excluded streams ('!' in the selection)
   QMap<QString, std::ofstream*> _outs;  // per stream
   QMap<QString, QString>    _outNames;  // per stream
