Changeset 10955 in ntrip for trunk/BNC/src/bncantex.cpp


Ignore:
Timestamp:
Jul 3, 2026, 11:10:42 AM (26 hours ago)
Author:
stuerze
Message:

Possibility to select how satellite attitude is modelled when converting Antenna Phase Center (APC) corrections to
Center-of-Mass (CoM) positions required for SP3 output

File:
1 edited

Legend:

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

    r10951 r10955  
    192192            line.indexOf("NavIC") == 0 ){
    193193          newAntMap->antName = line.mid(20,3);
     194          if (line.indexOf("BLOCK I") == 0) {
     195            // Extract GPS block type: "BLOCK IIF   " → "IIF"
     196            QString bt = line.mid(0, 20).trimmed();
     197            if (bt.startsWith("BLOCK "))
     198              newAntMap->blockType = bt.mid(6);
     199          }
     200          else if (line.indexOf("GALILEO") == 0) {
     201            // "GALILEO-1" → "1" (IOV), "GALILEO-2" → "2" (FOC)
     202            QString bt = line.mid(0, 20).trimmed();
     203            if (bt.startsWith("GALILEO-"))
     204              newAntMap->blockType = bt.mid(8);
     205          }
     206          else if (line.indexOf("BEIDOU") == 0) {
     207            // "BEIDOU-2M" → "2M", "BEIDOU-3M-CAST" → "3M-CAST", etc.
     208            QString bt = line.mid(0, 20).trimmed();
     209            if (bt.startsWith("BEIDOU-"))
     210              newAntMap->blockType = bt.mid(7);
     211          }
    194212        }
    195213        else {
     
    448466}
    449467
     468// GPS satellite yaw angle during nominal tracking and noon/midnight turns.
     469//
     470// GPS satellites perform a "noon turn" (and, for some blocks, a "midnight
     471// turn") when the Sun's elevation angle above the orbital plane (beta) is
     472// small: the required nominal yaw rate then exceeds the satellite's
     473// mechanical maximum, so the satellite yaws at that maximum rate until it
     474// catches back up to the nominal Sun-pointing orientation (Kouba 2009/2015,
     475// Bar-Sever 1996).
     476//
     477// The max yaw rates below are the best published estimates per block type:
     478//   IIA:   0.12 °/s (Kouba 2009)
     479//   IIR:   0.20 °/s (Bar-Sever 1996)
     480//   IIR-M: 0.20 °/s
     481//   IIF:   0.11 °/s (Kouba 2015)
     482//   IIIA:  0.15 °/s (tentative)
     483//
     484// Returns the effective yaw angle [rad] in the velocity-referenced frame,
     485// for use with the same Rodrigues rotation as the GLONASS model. During
     486// nominal tracking this equals psiNom and the result is identical to the
     487// simple sz×xSun formula.
     488////////////////////////////////////////////////////////////////////////////
     489double bncAntex::gpsYawAngle(const QString& prn, const QString& blockType,
     490                              double Mjd,
     491                              const ColumnVector& xSat,
     492                              const ColumnVector& vSat,
     493                              const ColumnVector& xSun) {
     494
     495  // Max yaw rate [rad/s] by GPS block type
     496  double psiDotMax;
     497  if      (blockType == "IIA")   psiDotMax = 0.12 * M_PI / 180.0;
     498  else if (blockType == "IIR")   psiDotMax = 0.20 * M_PI / 180.0;
     499  else if (blockType == "IIR-M") psiDotMax = 0.20 * M_PI / 180.0;
     500  else if (blockType == "IIF")   psiDotMax = 0.11 * M_PI / 180.0;
     501  else if (blockType == "IIIA")  psiDotMax = 0.15 * M_PI / 180.0;
     502  else return 0.0; // unknown block: caller uses simple Sun-pointing
     503
     504  const double MAX_CALL_GAP = 1800.0 / 86400.0; // 30 min in days
     505
     506  // Inertial velocity
     507  ColumnVector Omega(3); Omega(1) = 0.0; Omega(2) = 0.0; Omega(3) = t_CST::omega;
     508  ColumnVector vInert = vSat + crossproduct(Omega, xSat);
     509
     510  // Orbital angular momentum vector → orbital rate
     511  ColumnVector h     = crossproduct(xSat, vInert);
     512  double       hNorm = sqrt(DotProduct(h, h));
     513  ColumnVector orbNormal = h / hNorm;
     514  double       r    = sqrt(DotProduct(xSat, xSat));
     515  double       nRate = hNorm / (r * r); // [rad/s]
     516
     517  // Beta angle
     518  double beta = asin(DotProduct(orbNormal, xSun));
     519
     520  // Mu: orbit angle from midnight (same geometry as GLONASS)
     521  ColumnVector sunProj = xSun - DotProduct(xSun, orbNormal) * orbNormal;
     522  sunProj /= sqrt(DotProduct(sunProj, sunProj));
     523  ColumnVector eX = -1.0 * sunProj; // midnight direction
     524  ColumnVector eY = crossproduct(orbNormal, eX);
     525  ColumnVector rHat = xSat / r;
     526  double mu = atan2(DotProduct(rHat, eY), DotProduct(rHat, eX));
     527
     528  // Nominal yaw and its rate
     529  double tanBeta = tan(beta);
     530  double sinMu   = sin(mu);
     531  double psiNom  = atan2(-tanBeta, sinMu);
     532  double denom   = tanBeta * tanBeta + sinMu * sinMu;
     533  // |dPsi/dt| (always non-negative, sign comes from sign of tanBeta*cos(mu))
     534  double psiDotNomAbs = (denom > 1e-12)
     535                        ? nRate * fabs(tanBeta * cos(mu)) / denom
     536                        : 1e9;
     537  // Sign of the nominal yaw rate: dPsi/dt = nRate * tanBeta * cos(mu) / denom
     538  double psiDotNomSign = (tanBeta * cos(mu) >= 0.0) ? 1.0 : -1.0;
     539
     540  t_gpsYaw& st      = _gpsYaw[prn];
     541  double    callGap = st.valid ? (Mjd - st.lastCallMjd) : 0.0;
     542
     543  // Stale state: reset if there has been a gap in calls
     544  if (st.valid && callGap > MAX_CALL_GAP) {
     545    _gpsYawLog += QString().asprintf(
     546      "%s gpsYaw STALE-RESET  Mjd=%.6f beta=%6.3f mu=%7.2f"
     547      " old=%7.2f new=%7.2f gap=%.1fmin\n",
     548      prn.toLatin1().data(), Mjd, beta*180.0/M_PI, mu*180.0/M_PI,
     549      st.yaw*180.0/M_PI, psiNom*180.0/M_PI, callGap*1440.0);
     550    st.valid  = false;
     551    st.inTurn = false;
     552  }
     553
     554  double psiEff;
     555  if (psiDotNomAbs > psiDotMax) {
     556    // Constrained: satellite yaws at psiDotMax in the nominal direction
     557    if (!st.valid || !st.inTurn) {
     558      // Entering a noon/midnight turn
     559      psiEff = st.valid ? st.yaw : psiNom;
     560      _gpsYawLog += QString().asprintf(
     561        "%s gpsYaw TURN-ENTER   Mjd=%.6f beta=%6.3f mu=%7.2f"
     562        " psiNom=%7.2f psiEff=%7.2f psiDotNom=%6.3f max=%5.3f\n",
     563        prn.toLatin1().data(), Mjd, beta*180.0/M_PI, mu*180.0/M_PI,
     564        psiNom*180.0/M_PI, psiEff*180.0/M_PI,
     565        psiDotNomAbs*psiDotNomSign*180.0/M_PI, psiDotMax*180.0/M_PI);
     566      st.inTurn = true;
     567    } else {
     568      // Continuing the turn: integrate at constrained rate
     569      double dt  = callGap * 86400.0; // [s]
     570      psiEff = st.yaw + psiDotNomSign * psiDotMax * dt;
     571    }
     572    // Wrap to [-pi, pi]
     573    while (psiEff >  M_PI) psiEff -= 2.0 * M_PI;
     574    while (psiEff < -M_PI) psiEff += 2.0 * M_PI;
     575  }
     576  else {
     577    // Nominal tracking
     578    psiEff = psiNom;
     579    if (st.inTurn) {
     580      _gpsYawLog += QString().asprintf(
     581        "%s gpsYaw TURN-EXIT    Mjd=%.6f beta=%6.3f mu=%7.2f"
     582        " frozen=%7.2f nominal=%7.2f\n",
     583        prn.toLatin1().data(), Mjd, beta*180.0/M_PI, mu*180.0/M_PI,
     584        st.yaw*180.0/M_PI, psiNom*180.0/M_PI);
     585      st.inTurn = false;
     586    }
     587    st.yaw = psiNom;
     588  }
     589
     590  st.yaw         = psiEff;
     591  st.lastCallMjd = Mjd;
     592  st.valid       = true;
     593
     594  return psiEff;
     595}
     596
     597//
     598// Orbit-Normal Mode Yaw Angle — shared model for Galileo and BDS
     599//
     600// When |beta| < betaThr the satellite rotates toward yaw = 0 (orbit-normal)
     601// at the block-specific maximum yaw rate. When |beta| >= betaThr it returns
     602// to nominal yaw-steering (psiNom). Transitions are rate-limited in both
     603// directions to match physical satellite behaviour.
     604//
     605// References:
     606//   Galileo IOV/FOC: Kouba (2017), Steigenberger et al. (2018)
     607//   BDS MEO/IGSO:    Dai et al. (2015), Wang et al. (2018)
     608////////////////////////////////////////////////////////////////////////////
     609double bncAntex::onModeYawAngle(const QString& prn,
     610                                 double betaThr, double psiDotMax, double Mjd,
     611                                 const ColumnVector& xSat,
     612                                 const ColumnVector& vSat,
     613                                 const ColumnVector& xSun) {
     614
     615  const double MAX_CALL_GAP = 1800.0 / 86400.0;  // 30 min [days]
     616
     617  // Inertial velocity
     618  ColumnVector Omega(3); Omega(1) = 0.0; Omega(2) = 0.0; Omega(3) = t_CST::omega;
     619  ColumnVector vInert = vSat + crossproduct(Omega, xSat);
     620
     621  // Orbit geometry
     622  ColumnVector h        = crossproduct(xSat, vInert);
     623  double       hNorm    = sqrt(DotProduct(h, h));
     624  ColumnVector orbNormal = h / hNorm;
     625  double       r        = sqrt(DotProduct(xSat, xSat));
     626
     627  double beta = asin(DotProduct(orbNormal, xSun));
     628
     629  ColumnVector sunProj = xSun - DotProduct(xSun, orbNormal) * orbNormal;
     630  sunProj /= sqrt(DotProduct(sunProj, sunProj));
     631  ColumnVector eX   = -1.0 * sunProj;
     632  ColumnVector eY   = crossproduct(orbNormal, eX);
     633  ColumnVector rHat = xSat / r;
     634  double mu     = atan2(DotProduct(rHat, eY), DotProduct(rHat, eX));
     635  double psiNom = atan2(-tan(beta), sin(mu));
     636
     637  // Target yaw: orbit-normal (0) when below threshold, nominal otherwise
     638  bool   wantsON   = (fabs(beta) < betaThr);
     639  double psiTarget = wantsON ? 0.0 : psiNom;
     640
     641  t_onYaw& st      = _onYaw[prn];
     642  double   callGap = st.valid ? (Mjd - st.lastCallMjd) : 0.0;
     643
     644  if (st.valid && callGap > MAX_CALL_GAP) {
     645    _onYawLog += QString().asprintf(
     646      "%s onYaw STALE-RESET  Mjd=%.6f beta=%5.2f gap=%.1fmin\n",
     647      prn.toLatin1().data(), Mjd, beta*180.0/M_PI, callGap*1440.0);
     648    st.valid = false;
     649  }
     650
     651  double psiEff;
     652  if (!st.valid) {
     653    // Cold start: initialise at psiNom regardless of mode
     654    psiEff    = psiNom;
     655    st.inON   = false;
     656  }
     657  else {
     658    double dt      = callGap * 86400.0;       // [s]
     659    double dpsi    = psiTarget - st.yaw;
     660    while (dpsi >  M_PI) dpsi -= 2.0 * M_PI;
     661    while (dpsi < -M_PI) dpsi += 2.0 * M_PI;
     662    double maxDpsi = psiDotMax * dt;
     663
     664    if (fabs(dpsi) <= maxDpsi) {
     665      // Target reached this step
     666      psiEff = psiTarget;
     667      bool wasON = st.inON;
     668      st.inON    = wantsON;
     669      if (!wasON && wantsON && fabs(st.yaw) < 0.5*M_PI/180.0) {
     670        _onYawLog += QString().asprintf(
     671          "%s onYaw ON-ENTER   Mjd=%.6f beta=%5.2f psiNom=%7.2f\n",
     672          prn.toLatin1().data(), Mjd, beta*180.0/M_PI, psiNom*180.0/M_PI);
     673      }
     674      else if (wasON && !wantsON) {
     675        _onYawLog += QString().asprintf(
     676          "%s onYaw ON-EXIT    Mjd=%.6f beta=%5.2f psiNom=%7.2f\n",
     677          prn.toLatin1().data(), Mjd, beta*180.0/M_PI, psiNom*180.0/M_PI);
     678        st.inON = false;
     679      }
     680    }
     681    else {
     682      // Still rotating toward target
     683      psiEff = st.yaw + (dpsi > 0.0 ? 1.0 : -1.0) * maxDpsi;
     684    }
     685  }
     686
     687  while (psiEff >  M_PI) psiEff -= 2.0 * M_PI;
     688  while (psiEff < -M_PI) psiEff += 2.0 * M_PI;
     689
     690  st.yaw         = psiEff;
     691  st.lastCallMjd = Mjd;
     692  st.valid       = true;
     693
     694  return psiEff;
     695}
     696
     697//
     698////////////////////////////////////////////////////////////////////////////
     699double bncAntex::galileoYawAngle(const QString& prn, const QString& blockType,
     700                                  double Mjd,
     701                                  const ColumnVector& xSat,
     702                                  const ColumnVector& vSat,
     703                                  const ColumnVector& xSun) {
     704  // IOV (type "1") and FOC (type "2"): orbit-normal for |beta| < 2 deg.
     705  // Max yaw rate 0.20 deg/s applies to both generations.
     706  // Kouba (2017), Steigenberger et al. (2018)
     707  Q_UNUSED(blockType);
     708  const double betaThr   = 2.0 * M_PI / 180.0;
     709  const double psiDotMax = 0.20 * M_PI / 180.0;
     710  return onModeYawAngle(prn, betaThr, psiDotMax, Mjd, xSat, vSat, xSun);
     711}
     712
     713//
     714////////////////////////////////////////////////////////////////////////////
     715double bncAntex::bdsYawAngle(const QString& prn, const QString& blockType,
     716                              double Mjd,
     717                              const ColumnVector& xSat,
     718                              const ColumnVector& vSat,
     719                              const ColumnVector& xSun) {
     720  // GEO satellites are always in orbit-normal mode (yaw = 0).
     721  if (blockType == "2G" || blockType == "3G-CAST")
     722    return 0.0;
     723
     724  double betaThr, psiDotMax;
     725  if (blockType == "2M" || blockType == "2I") {
     726    // BDS-2 MEO / IGSO: orbit-normal for |beta| < 4 deg (Dai et al. 2015)
     727    betaThr   = 4.0 * M_PI / 180.0;
     728    psiDotMax = 0.10 * M_PI / 180.0;
     729  }
     730  else {
     731    // BDS-3 (CAST and SECM MEO/IGSO): orbit-normal for |beta| < 3 deg
     732    betaThr   = 3.0 * M_PI / 180.0;
     733    psiDotMax = 0.15 * M_PI / 180.0;
     734  }
     735  return onModeYawAngle(prn, betaThr, psiDotMax, Mjd, xSat, vSat, xSun);
     736}
     737
     738//
    450739// Satellite Antenna Offset
    451740////////////////////////////////////////////////////////////////////////////
    452741t_irc bncAntex::satCoMcorrection(const QString& prn, double Mjd,
    453742                                 const ColumnVector& xSat,
    454                                  const ColumnVector& vSat, ColumnVector& dx) {
     743                                 const ColumnVector& vSat, ColumnVector& dx,
     744                                 e_attMode mode, double externalYaw) {
    455745
    456746  t_frequency::type frqType = t_frequency::dummy;
     
    495785      ColumnVector sy, sx;
    496786
    497       // GLONASS: override the nominal Sun-pointing attitude near the
    498       // orbit noon/midnight points when the beta angle is small (see
    499       // glonassYawAngle() above). Elsewhere GLONASS-M follows the same
    500       // nominal law as the other constellations, so the result is
    501       // identical to the direct Sun-pointing computation used below.
    502       // -----------------------------------------------------------------
    503       if (prn[0] == 'R' && vSat.size() == 3) {
    504         double psi = glonassYawAngle(prn, Mjd, xSat, vSat, xSun);
    505 
    506         ColumnVector vInert = vSat;
     787      // Determine the satellite body frame orientation.
     788      //
     789      // ATT_NOMINAL: simple nominal Sun-pointing for all systems.
     790      // ATT_COMPUTED or ATT_EXTERNAL: use per-system attitude models.
     791      //   GLONASS  → yaw-fixed model (Dilssner et al. 2011)
     792      //   GPS      → noon/midnight turn model (Kouba 2009/2015)
     793      //   others   → simple nominal Sun-pointing
     794      // ATT_EXTERNAL: caller supplies the yaw angle [rad] directly
     795      //   (velocity-referenced frame, same convention as GLONASS/GPS models).
     796      // -----------------------------------------------------------------------
     797      bool useVelocityFrame = false;
     798      double psiEff = 0.0;
     799
     800      if (mode == ATT_COMPUTED && prn[0] == 'R' && vSat.size() == 3) {
     801        psiEff = glonassYawAngle(prn, Mjd, xSat, vSat, xSun);
     802        useVelocityFrame = true;
     803      }
     804      else if (mode == ATT_COMPUTED && prn[0] == 'G' && vSat.size() == 3
     805               && !map->blockType.isEmpty()) {
     806        psiEff = gpsYawAngle(prn, map->blockType, Mjd, xSat, vSat, xSun);
     807        useVelocityFrame = true;
     808      }
     809      else if (mode == ATT_COMPUTED && prn[0] == 'E' && vSat.size() == 3
     810               && !map->blockType.isEmpty()) {
     811        psiEff = galileoYawAngle(prn, map->blockType, Mjd, xSat, vSat, xSun);
     812        useVelocityFrame = true;
     813      }
     814      else if (mode == ATT_COMPUTED && prn[0] == 'C' && vSat.size() == 3
     815               && !map->blockType.isEmpty()) {
     816        psiEff = bdsYawAngle(prn, map->blockType, Mjd, xSat, vSat, xSun);
     817        useVelocityFrame = true;
     818      }
     819      else if (mode == ATT_EXTERNAL && vSat.size() == 3) {
     820        psiEff = externalYaw;
     821        useVelocityFrame = true;
     822      }
     823
     824      if (useVelocityFrame) {
    507825        ColumnVector Omega(3); Omega(1) = 0.0; Omega(2) = 0.0; Omega(3) = t_CST::omega;
    508         vInert += crossproduct(Omega, xSat);
     826        ColumnVector vInert = vSat + crossproduct(Omega, xSat);
    509827
    510828        ColumnVector sy0 = crossproduct(sz, vInert);
     
    512830        ColumnVector sx0 = crossproduct(sy0, sz);
    513831
    514         // Rodrigues rotation of (sx0, sy0) around sz by angle psi
    515         double cosY = cos(psi);
    516         double sinY = sin(psi);
     832        // Rodrigues rotation of (sx0, sy0) around sz by psiEff
     833        double cosY = cos(psiEff);
     834        double sinY = sin(psiEff);
    517835        sx = sx0 * cosY + crossproduct(sz, sx0) * sinY;
    518836        sy = sy0 * cosY + crossproduct(sz, sy0) * sinY;
    519837      }
    520838      else {
     839        // Nominal Sun-pointing: direct formula (ATT_NOMINAL, or computed
     840        // with no block-type-specific model available)
    521841        sy = crossproduct(sz, xSun);
    522842        sy /= sqrt(DotProduct(sy,sy));
Note: See TracChangeset for help on using the changeset viewer.