Program Listing for File gnss_constellation.cc¶

↰ Return to documentation for file (agents/gnss_constellation.cc)

#include "lupnt/agents/gnss_constellation.h"

#include <algorithm>
#include <cmath>

#include "lupnt/conversions/frame_converter.h"
#include "lupnt/conversions/time_conversions.h"
#include "lupnt/core/error.h"
#include "lupnt/core/file.h"
#include "lupnt/interfaces/antex_loader.h"
#include "lupnt/interfaces/sp3_loader.h"
#include "lupnt/measurements/comms_utils.h"
#include "lupnt/numerics/interpolation.h"

namespace lupnt {

  namespace {
    ChebyshevFitModel FitStateHistoryChebyshev(const VecXd& t_tai, const MatXd& rv_eci) {
      double t_start = t_tai(0);
      double t_end = t_tai(t_tai.size() - 1);
      double min_step = t_end - t_start;
      for (int i = 1; i < t_tai.size(); i++) {
        min_step = std::min(min_step, t_tai(i) - t_tai(i - 1));
      }

      return FitChebyshevModel(
          [&t_tai, &rv_eci](double t) {
            VecXd state(6);
            for (int k = 0; k < 6; k++) {
              VecXd col = rv_eci.col(k);
              state(k) = LinearInterp1d(t_tai, col, t);
            }
            return state;
          },
          t_start, t_end, 6, std::max(1.0, min_step), 2);
    }
  }  // namespace

  // Constructors ***************************************************************

  GnssConstellation::GnssConstellation(GnssConst gnss_const) : gnss_const_(gnss_const) {
    name_ = std::string(enum_name(gnss_const_));
  }

  GnssConstellation::GnssConstellation(Config& config) {
    gnss_const_ = enum_cast<GnssConst>(config["gnss_const"].as<std::string>()).value();
    name_ = config["name"] ? config["name"].as<std::string>() : std::string(enum_name(gnss_const_));
  }

  // Setup **********************************************************************

  void GnssConstellation::SetSatelliteStates(const std::vector<int>& prns, const VecXd& t_tai,
                                             const std::vector<MatXd>& rv_eci) {
    LUPNT_CHECK(prns.size() == rv_eci.size(), "Number of PRNs must match number of state histories",
                "GnssConstellation");
    LUPNT_CHECK(t_tai.size() >= 2, "Ephemeris must contain at least 2 epochs", "GnssConstellation");

    prns_ = prns;
    t_tai_ = t_tai;
    rv_eci_.clear();
    rv_eci_cheby_.clear();
    for (size_t i = 0; i < prns.size(); i++) {
      LUPNT_CHECK(rv_eci[i].rows() == t_tai.size() && rv_eci[i].cols() == 6,
                  "Each satellite state history must be [N_epochs x 6]", "GnssConstellation");
      rv_eci_[prns[i]] = rv_eci[i];
      rv_eci_cheby_[prns[i]] = FitStateHistoryChebyshev(t_tai_, rv_eci[i]);
    }
  }

  void GnssConstellation::LoadEphemeris(const std::filesystem::path& filepath) {
    LUPNT_CHECK(std::filesystem::exists(filepath),
                "GNSS ephemeris file not found: " + filepath.string(), "GnssConstellation");

    H5Easy::File file(filepath.string(), H5Easy::File::ReadOnly);
    LUPNT_CHECK(
        file.exist("/prns") && file.exist("/t_tai"),
        "Ephemeris file is missing required datasets '/prns' and/or '/t_tai': " + filepath.string(),
        "GnssConstellation");

    std::vector<int> prns = H5Easy::load<std::vector<int>>(file, "/prns");
    VecXd t_tai = H5Easy::load<VecXd>(file, "/t_tai");

    std::vector<MatXd> rv_eci;
    rv_eci.reserve(prns.size());
    for (int prn : prns) {
      std::string dset = "/rv_eci/" + std::to_string(prn);
      LUPNT_CHECK(file.exist(dset),
                  "Ephemeris file is missing dataset '" + dset + "': " + filepath.string(),
                  "GnssConstellation");
      rv_eci.push_back(H5Easy::load<MatXd>(file, dset));
    }

    SetSatelliteStates(prns, t_tai, rv_eci);
  }

  void GnssConstellation::SaveEphemeris(const std::filesystem::path& filepath) const {
    LUPNT_CHECK(!prns_.empty(), "No satellite states to save (call SetSatelliteStates first)",
                "GnssConstellation");

    H5Easy::File file(filepath.string(), H5Easy::File::Overwrite);
    H5Easy::dump(file, "/prns", prns_);
    H5Easy::dump(file, "/t_tai", t_tai_);
    for (int prn : prns_) {
      H5Easy::dump(file, "/rv_eci/" + std::to_string(prn), rv_eci_.at(prn));
    }
    file.flush();
  }

  void GnssConstellation::SetupSatelliteStatesFromFiles(
      const std::vector<std::filesystem::path>& sp3_filepaths,
      const std::filesystem::path& antex_filepath, const VecXd& t_tai, GnssFreq freq,
      const std::vector<int>& prns_in) {
    LUPNT_CHECK(t_tai.size() >= 2, "Ephemeris must contain at least 2 epochs", "GnssConstellation");
    LUPNT_CHECK(!sp3_filepaths.empty(), "At least one SP3 file path must be provided",
                "GnssConstellation");

    Sp3Loader sp3(sp3_filepaths);
    AntexLoader antex(antex_filepath);

    std::string letter = AntexLoader::GnssLetter(gnss_const_);

    // Determine the PRN list: explicit, or every PRN of `gnss_const_` present
    // in the loaded SP3 ephemeris (and -- to avoid runtime errors -- also
    // present in the ANTEX file).
    std::vector<int> prns = prns_in;
    if (prns.empty()) {
      for (const std::string& sat_id : sp3.GetSatellites()) {
        if (sat_id.size() == 3 && sat_id[0] == letter[0]) {
          int prn = std::stoi(sat_id.substr(1));
          if (antex.HasSatellite(gnss_const_, prn)) prns.push_back(prn);
        }
      }
      std::sort(prns.begin(), prns.end());
    }
    LUPNT_CHECK(!prns.empty(),
                "No PRNs of the requested constellation found in the SP3/ANTEX files",
                "GnssConstellation");

    int n_epochs = static_cast<int>(t_tai.size());
    std::vector<MatXd> rv_eci_list;
    rv_eci_list.reserve(prns.size());

    for (int prn : prns) {
      std::string sat_id = AntexLoader::SatId(gnss_const_, prn);
      MatXd rv_eci(n_epochs, 6);

      for (int k = 0; k < n_epochs; k++) {
        Real t = t_tai(k);

        // SP3 (center-of-mass) ECEF position/velocity
        Vec6 rv_sp3_ecef = sp3.GetPosVel(sat_id, t);
        Vec3d pos_sp3_ecef(rv_sp3_ecef(0).val(), rv_sp3_ecef(1).val(), rv_sp3_ecef(2).val());

        // Antenna phase-center offset (NEU, meters) -> apply correction in ECEF
        Vec3d pco_neu_m = antex.GetPco(gnss_const_, prn, freq, t);
        Vec3d pos_corrected_ecef = AntexLoader::ApplyPcoCorrectionEcef(t, pos_sp3_ecef, pco_neu_m);

        // Retain the (uncorrected) SP3 velocity -- the PCO is constant in the
        // satellite body frame, so its ECEF-velocity contribution is negligible.
        Vec6 rv_corrected_ecef;
        rv_corrected_ecef << Real(pos_corrected_ecef(0)), Real(pos_corrected_ecef(1)),
            Real(pos_corrected_ecef(2)), rv_sp3_ecef(3), rv_sp3_ecef(4), rv_sp3_ecef(5);

        Real t_tdb = ConvertTime(t, Time::TAI, Time::TDB);
        Vec6 rv_eci_k = ConvertFrame(t_tdb, rv_corrected_ecef, Frame::ECEF, Frame::ECI, false);
        for (int c = 0; c < 6; c++) rv_eci(k, c) = rv_eci_k(c).val();
      }

      rv_eci_list.push_back(rv_eci);
    }

    SetSatelliteStates(prns, t_tai, rv_eci_list);
  }

  void GnssConstellation::SetupTransmitters() {
    LUPNT_CHECK(!prns_.empty(),
                "Satellite states must be set (SetSatelliteStates / LoadEphemeris) "
                "before SetupTransmitters",
                "GnssConstellation");

    antennas_.clear();
    P_tx_.clear();
    freq_list_.clear();

    for (int prn : prns_) {
      // Reuse the existing GnssTransmitter device, which already loads the
      // antenna gain pattern(s) and transmit power(s) for the given GNSS
      // constellation and PRN (see `GnssTransmitter::InitGps` / `InitGalileo`
      // / `InitQzss` in `cpp/lupnt/devices/space_comms.cc`).
      GnssTransmitter tx(gnss_const_, prn);
      antennas_[prn] = tx.GetAntennas();
      P_tx_[prn] = tx.GetTransmitPower();
      if (freq_list_.empty()) freq_list_ = tx.GetFreqList();
    }
  }

  // Queries ********************************************************************

  bool GnssConstellation::IsFaultPrn(int prn) const {
    return std::find(fault_prns_.begin(), fault_prns_.end(), prn) != fault_prns_.end();
  }

  Vec6 GnssConstellation::GetSatelliteStateEci(int prn, Real t_tai) const {
    auto it = rv_eci_.find(prn);
    LUPNT_CHECK(it != rv_eci_.end(), "PRN " + std::to_string(prn) + " not found in constellation",
                "GnssConstellation");

    auto it_cheby = rv_eci_cheby_.find(prn);
    LUPNT_CHECK(it_cheby != rv_eci_cheby_.end(),
                "Chebyshev ephemeris missing for PRN " + std::to_string(prn), "GnssConstellation");

    VecX state_x;
    LUPNT_CHECK(it_cheby->second.Eval(t_tai, &state_x, nullptr),
                "Ephemeris interpolation time is out of range", "GnssConstellation");
    Vec6 state = state_x;
    return state;
  }

  Real GnssConstellation::ComputeRange(int prn, const Vec3& r_rx_eci, Real t_tai) const {
    Vec6 rv_tx = GetSatelliteStateEci(prn, t_tai);
    return (r_rx_eci - rv_tx.head(3)).norm();
  }

  Real GnssConstellation::ComputeRangeRate(int prn, const Vec6& rv_rx_eci, Real t_tai) const {
    Vec6 rv_tx = GetSatelliteStateEci(prn, t_tai);
    Vec3 dr = rv_rx_eci.head(3) - rv_tx.head(3);
    Vec3 dv = rv_rx_eci.tail(3) - rv_tx.tail(3);
    Real range = dr.norm();
    return dr.dot(dv) / range;
  }

  const MatXd& GnssConstellation::GetSatelliteStateHistoryEci(int prn) const {
    auto it = rv_eci_.find(prn);
    LUPNT_CHECK(it != rv_eci_.end(), "PRN " + std::to_string(prn) + " not found in constellation",
                "GnssConstellation");
    return it->second;
  }

  const ChebyshevFitModel& GnssConstellation::GetSatelliteStateChebyshevEci(int prn) const {
    auto it = rv_eci_cheby_.find(prn);
    LUPNT_CHECK(it != rv_eci_cheby_.end(),
                "Chebyshev ephemeris missing for PRN " + std::to_string(prn), "GnssConstellation");
    return it->second;
  }

  bool GnssConstellation::HasTransmitterInfo(int prn, GnssFreq freq) const {
    auto it_ant = antennas_.find(prn);
    auto it_pow = P_tx_.find(prn);
    if (it_ant == antennas_.end() || it_pow == P_tx_.end()) return false;
    return it_ant->second.find(freq) != it_ant->second.end()
           && it_pow->second.find(freq) != it_pow->second.end();
  }

  const Antenna& GnssConstellation::GetTransmitterAntenna(int prn, GnssFreq freq) const {
    LUPNT_CHECK(HasTransmitterInfo(prn, freq),
                "Antenna metadata not available for PRN " + std::to_string(prn),
                "GnssConstellation");
    return antennas_.at(prn).at(freq);
  }

  Real GnssConstellation::GetTransmitPowerDbw(int prn, GnssFreq freq) const {
    LUPNT_CHECK(HasTransmitterInfo(prn, freq),
                "Transmit-power metadata not available for PRN " + std::to_string(prn),
                "GnssConstellation");
    return P_tx_.at(prn).at(freq);
  }

  // Measurement noise models ***************************************************

  // Pseudorange (DLL) noise: direct port of `compute_gnss_pseudorange_noise`,
  // reusing the case-based formula already implemented in `SigmaDll`
  // (comms_utils.cc; the case boundaries and per-case expressions are
  // identical -- `Tc = 1/R_c` makes `1/(Tc*B_fe) = R_c/B_fe`), then applying
  // the `* (C * Tc)` chip-to-meters scaling that the Python version applies
  // on top of the dimensionless code-tracking jitter.
  Real GnssConstellation::ComputeSigmaRange(Real cn0_dbhz, GnssFreq freq) const {
    Real CN0_w = DecibelToDecimal(cn0_dbhz);
    auto it = GNSS_RC_MAP.find(freq);
    LUPNT_CHECK(it != GNSS_RC_MAP.end(), "Unknown GNSS frequency", "GnssConstellation");
    Real Rc = it->second;
    Real Tc = 1.0 / Rc;

    DllParams params;
    params.B_dll = rx_params_.Bn;
    params.T_i = rx_params_.T;
    params.d = rx_params_.D;
    params.Tc = Tc;
    params.B_fe = rx_params_.b * Rc;

    return SigmaDll(params, CN0_w) * (Real(C) * Tc);
  }

  // Pseudorange-rate (FLL) noise: direct port of
  // `compute_gnss_pseudorangerate_noise`.
  Real GnssConstellation::ComputeSigmaRangeRate(Real cn0_dbhz, GnssFreq freq) const {
    Real CN0_w = DecibelToDecimal(cn0_dbhz);
    auto it = GNSS_FREQ_MAP.find(freq);
    LUPNT_CHECK(it != GNSS_FREQ_MAP.end(), "Unknown GNSS frequency", "GnssConstellation");
    Real lambda = Real(C) / it->second;

    Real Bf = rx_params_.Bf;
    Real T = rx_params_.T;

    return lambda / (2.0 * PI * T) * sqrt(4.0 * Bf / CN0_w * (1.0 + 1.0 / (T * CN0_w)));
  }

  // Carrier-phase (PLL) noise: direct port of
  // `compute_gnss_carrier_phase_noise`. The dimensionless phase-jitter term
  // `sqrt(Bp/CN0 * (1 + 1/(2*T*CN0)))` is identical to `SigmaPll`.
  Real GnssConstellation::ComputeSigmaCarrierPhase(Real cn0_dbhz, GnssFreq freq) const {
    Real CN0_w = DecibelToDecimal(cn0_dbhz);
    auto it = GNSS_FREQ_MAP.find(freq);
    LUPNT_CHECK(it != GNSS_FREQ_MAP.end(), "Unknown GNSS frequency", "GnssConstellation");
    Real lambda = Real(C) / it->second;

    PllParams params;
    params.B_pll = rx_params_.Bp;
    params.T_i = rx_params_.T;

    return lambda / (2.0 * PI) * SigmaPll(params, CN0_w);
  }

  // Constellation interface ****************************************************

  void GnssConstellation::Setup() {
    if (antennas_.empty() && !prns_.empty()) SetupTransmitters();
  }

  void GnssConstellation::Step(Real t) {
    (void)t;  // GNSS ephemerides are precomputed; nothing to propagate here.
  }

}  // namespace lupnt