Program Listing for File time_conversions.cc

Return to documentation for file (conversions/time_conversions.cc)

#include "lupnt/conversions/time_conversions.h"

#include <algorithm>
#include <array>
#include <iostream>
#include <map>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "lupnt/core/error.h"
#include "lupnt/data/eop.h"
#include "lupnt/data/kernels.h"
#include "lupnt/data/tai_utc.h"
#include "lupnt/environment/body.h"
#include "lupnt/environment/solar_system.h"
#include "lupnt/numerics/cheby_fit.h"
#include "lupnt/numerics/math_utils.h"

#define TIME_CONVERSION(from, to, func) \
  {{Time::from, Time::to}, [](Real t) -> Real { return func(t); }}


namespace lupnt {

  // Julian Epoch          -4712-01-01T12:00:00 TT
  // Modified Julian Epoch  1858-11-17T00:00:00 TT
  // Fifties Epoch          1950-01-01T00:00:00 TT
  // CCSDS Epoch            1958-01-01T00:00:00 TAI
  // Coordinate Time Epoch  1977-01-01T00:00:00 TAI
  // Galileo Epoch          1999-08-22T00:00:00 UTC
  // GPS Epoch              1980-01-06T00:00:00 UTC
  // J2000 Epoch            2000-01-01T12:00:00 TT

  const std::map<Time, std::string> time_to_string
      = {{Time::UT1, "UT1"},     {Time::UTC, "UTC"},      {Time::TAI, "TAI"}, {Time::TDB, "TDB"},
         {Time::TT, "TT"},       {Time::TCG, "TCG"},      {Time::TCB, "TCB"}, {Time::GPS, "GPS"},
         {Time::JD_TT, "JDTDT"}, {Time::JD_TDB, "JDTDB"}, {Time::TCL, "TCL"}, {Time::LT, "LT"}};

  const std::map<std::string, Time> string_to_time
      = {{"UT1", Time::UT1},     {"UTC", Time::UTC},      {"TAI", Time::TAI}, {"TDB", Time::TDB},
         {"TT", Time::TT},       {"TCG", Time::TCG},      {"TCB", Time::TCB}, {"GPS", Time::GPS},
         {"JDTDT", Time::JD_TT}, {"JDTDB", Time::JD_TDB}, {"TCL", Time::TCL}, {"LT", Time::LT}};

  Real ConvertTime(Real t, Time from, Time to) {
    if (from == to) return t;
    switch (from) {
      case Time::UT1: return ConvertTime(Ut1ToUtc(t), Time::UTC, to);
      case Time::UTC: {
        if (to == Time::UT1) return UtcToUt1(t);
        return ConvertTime(UtcToTai(t), Time::TAI, to);
      }
      case Time::TAI: {
        switch (to) {
          // If it reaches TAI, direct conversions to other systems are possible
          case Time::TAI: return t;
          case Time::GPS: return TaiToGps(t);
          case Time::UTC: return TaiToUtc(t);
          case Time::UT1: return UtcToUt1(TaiToUtc(t));
          case Time::TT: return TaiToTt(t);
          case Time::TCG: return TtToTcg(TaiToTt(t));
          case Time::TDB: return TtToTdb(TaiToTt(t));
          case Time::TCB: return TtToTcb(TaiToTt(t));
          case Time::TCL: return TcbToTcl(TtToTcb(TaiToTt(t)));
          default: break;
        }
        break;
      }
      case Time::TDB: {
        switch (to) {
          case Time::TT: return TDBToTt(t);
          case Time::TCB: return TtToTcb(TDBToTt(t));
          case Time::TCG: return TtToTcg(TDBToTt(t));
          case Time::TAI: return TtToTai(TDBToTt(t));
          case Time::LT: return TdbToLt(t);
          default: return ConvertTime(TtToTai(TDBToTt(t)), Time::TAI, to);
        }
      }
      case Time::TT: {
        switch (to) {
          case Time::TAI: return TtToTai(t);
          case Time::TDB: return TtToTdb(t);
          case Time::TCG: return TtToTcg(t);
          case Time::TCB: return TtToTcb(t);
          case Time::UTC: return TaiToUtc(TtToTai(t));
          case Time::UT1: return UtcToUt1(TaiToUtc(TtToTai(t)));
          case Time::GPS: return TaiToGps(TtToTai(t));
          case Time::LT: return TtToLt(t);
          default: return ConvertTime(TtToTai(t), Time::TAI, to);
        }
        break;
      }
      case Time::TCG: {
        switch (to) {
          default: return ConvertTime(TcgToTt(t), Time::TT, to);
        }
      }
      case Time::TCB: {
        if (to == Time::TCL) return TcbToTcl(t);
        return ConvertTime(TcbToTdb(t), Time::TDB, to);
      }
      case Time::GPS: return ConvertTime(GpsToTai(t), Time::TAI, to);
      case Time::TCL: {
        switch (to) {
          case Time::LT: return TclToLt(t);
          case Time::TCB: return TclToTcb(t);
          default: return ConvertTime(TclToTcb(t), Time::TCB, to);
        }
      }
      case Time::LT: {
        switch (to) {
          case Time::TCL: return LtToTcl(t);
          case Time::TDB: return LtToTdb(t);
          case Time::TT: return LtToTt(t);
          default: return ConvertTime(LtToTdb(t), Time::TDB, to);
        }
      }
    }
    throw std::invalid_argument("Invalid time conversion");
    return 0;
  }

  bool IsCoordinateTimeScale(Time time) {
    switch (time) {
      case Time::TT:
      case Time::TCG:
      case Time::TDB:
      case Time::TCB:
      case Time::TCL:
      case Time::LT: return true;
      default: return false;
    }
  }

  Real ConvertCoordinateTime(Real t, Time from, Time to) {
    LUPNT_CHECK(IsCoordinateTimeScale(from), "Input time scale is not a coordinate time scale",
                "ConvertCoordinateTime");
    LUPNT_CHECK(IsCoordinateTimeScale(to), "Output time scale is not a coordinate time scale",
                "ConvertCoordinateTime");
    return ConvertTime(t, from, to);
  }

  Real ConvertCoordinateTime(Real t, Time from, Time to, const Vec3& x_bcrs) {
    LUPNT_CHECK(IsCoordinateTimeScale(from), "Input time scale is not a coordinate time scale",
                "ConvertCoordinateTime");
    LUPNT_CHECK(IsCoordinateTimeScale(to), "Output time scale is not a coordinate time scale",
                "ConvertCoordinateTime");
    if (from == to) return t;
    if (from == Time::TDB && to == Time::TT) return TDBToTt(t, x_bcrs);
    if (from == Time::TT && to == Time::TDB) return TtToTdb(t, x_bcrs);
    if (from == Time::TT && to == Time::TCB) return TtToTcb(t, x_bcrs);
    if (from == Time::TCB && to == Time::TT) return TcbToTt(t, x_bcrs);
    if (from == Time::TDB && to == Time::TCB) return TtToTcb(TDBToTt(t, x_bcrs), x_bcrs);
    if (from == Time::TCB && to == Time::TDB) return TtToTdb(TcbToTt(t, x_bcrs), x_bcrs);
    if (from == Time::TCG && to == Time::TDB) return TtToTdb(TcgToTt(t), x_bcrs);
    if (from == Time::TCG && to == Time::TCB) return TtToTcb(TcgToTt(t), x_bcrs);
    if (from == Time::TDB && to == Time::TCG) return TtToTcg(TDBToTt(t, x_bcrs));
    if (from == Time::TCB && to == Time::TCG) return TtToTcg(TcbToTt(t, x_bcrs));
    if (from == Time::TCB && to == Time::TCL) return TcbToTcl(t, x_bcrs);
    if (from == Time::TCL && to == Time::TCB) return TclToTcb(t, x_bcrs);
    if (to == Time::TCL) return TcbToTcl(ConvertCoordinateTime(t, from, Time::TCB, x_bcrs), x_bcrs);
    if (from == Time::TCL) return ConvertCoordinateTime(TclToTcb(t, x_bcrs), Time::TCB, to, x_bcrs);
    return ConvertCoordinateTime(t, from, to);
  }

  VecX ConvertCoordinateTime(const VecX& t, Time from, Time to) {
    LUPNT_CHECK(IsCoordinateTimeScale(from), "Input time scale is not a coordinate time scale",
                "ConvertCoordinateTime");
    LUPNT_CHECK(IsCoordinateTimeScale(to), "Output time scale is not a coordinate time scale",
                "ConvertCoordinateTime");
    VecX t_out(t.size());
    for (int i = 0; i < t.size(); ++i) {
      t_out(i) = ConvertCoordinateTime(t(i), from, to);
    }
    return t_out;
  }

  VecX ConvertCoordinateTime(const VecX& t, Time from, Time to, const MatX3& x_bcrs) {
    LUPNT_CHECK(IsCoordinateTimeScale(from), "Input time scale is not a coordinate time scale",
                "ConvertCoordinateTime");
    LUPNT_CHECK(IsCoordinateTimeScale(to), "Output time scale is not a coordinate time scale",
                "ConvertCoordinateTime");
    LUPNT_CHECK(t.size() == x_bcrs.rows(), "Position row count must match time vector size",
                "ConvertCoordinateTime");
    VecX t_out(t.size());
    for (int i = 0; i < t.size(); ++i) {
      t_out(i) = ConvertCoordinateTime(t(i), from, to, x_bcrs.row(i).transpose());
    }
    return t_out;
  }

  VecX ConvertTime(const VecX& t, Time from, Time to) {
    VecX t_out(t.size());
    if (to == Time::TCL) {
      if (from == Time::LT) {
        return LtToTcl(t);
      } else if (from == Time::TCB) {
        return TcbToTcl(t);
      }
      return TcbToTcl(ConvertTime(t, from, Time::TCB));
    }
    if (from == Time::TCL) {
      if (to == Time::LT) {
        return TclToLt(t);
      } else if (to == Time::TCB) {
        return TclToTcb(t);
      }
      return ConvertTime(TclToTcb(t), Time::TCB, to);
    }
    for (int i = 0; i < t.size(); i++) {
      t_out(i) = ConvertTime(t(i), from, to);
    }
    return t_out;
  }

  Real UtcToUt1(Real t_utc) {
    Real mjd_utc = t_utc / SECS_DAY + MJD_J2000_TT;
    Real ut1_utc = GetUt1UtcDifference(mjd_utc);
    Real t_ut1 = t_utc + ut1_utc;
    return t_ut1;
  }

  Real Ut1ToUtc(Real t_ut1) {
    Real mjd_ut1 = t_ut1 / SECS_DAY + MJD_J2000_TT;
    Real ut1_utc = GetUt1UtcDifference(mjd_ut1);
    Real t_utc = t_ut1 - ut1_utc;
    return t_utc;
  }

  Real UtcToTai(Real t_utc) {
    Real mjd_utc = t_utc / SECS_DAY + MJD_J2000_TT;
    Real tai_utc = GetTaiUtcDifference(mjd_utc.val());
    Real t_tai = t_utc + tai_utc;
    return t_tai;
  }

  Real TaiToUtc(Real t_tai) {
    Real mjd_tai = t_tai / SECS_DAY + MJD_J2000_TT;
    Real tai_utc = GetTaiUtcDifference(mjd_tai.val());
    Real t_utc = t_tai - tai_utc;
    return t_utc;
  }

  Real TaiToTt(Real t_tai) { return t_tai + TT_TAI_OFFSET; }

  Real TtToTai(Real t_tt) { return t_tt - TT_TAI_OFFSET; }

  Real TtToTcg(Real t_tt) {
    Real mjd_tt = TimeToMjd(t_tt);
    Real tt_tcg = -L_G / (1.0 - L_G) * (mjd_tt - MJD_COORDINATE_TT_TCG_TCB) * SECS_DAY;
    return t_tt - tt_tcg;
  }

  Real TcgToTt(Real t_tcg) {
    Real mjd_tcg = TimeToMjd(t_tcg);
    Real tt_tcg = -L_G * (mjd_tcg - MJD_COORDINATE_TT_TCG_TCB) * SECS_DAY;
    return t_tcg + tt_tcg;
  }

  namespace {

    constexpr std::array<BodyId, 9> kTtExternalBodies = {
        BodyId::SUN,     BodyId::MERCURY, BodyId::VENUS,  BodyId::MOON,    BodyId::MARS,
        BodyId::JUPITER, BodyId::SATURN,  BodyId::URANUS, BodyId::NEPTUNE,
    };

    Real GetEarthExternalPotentialForTdb(Real t_tdb) {
      Vec3 x_earth = GetBodyPos(t_tdb, BodyId::SSB, BodyId::EARTH, Frame::GCRF);
      Real w_ext = 0.0;
      for (BodyId body : kTtExternalBodies) {
        Vec3 x_body = GetBodyPos(t_tdb, BodyId::SSB, body, Frame::GCRF);
        w_ext += GetBodyGM(body) / (x_earth - x_body).norm();
      }
      return w_ext;
    }

    Real TdbToTtIntegrandC2(Real t_tdb) {
      Vec6 rv_earth = GetBodyPosVel(t_tdb, BodyId::SSB, BodyId::EARTH, Frame::GCRF);
      Real v_earth_2 = rv_earth.tail<3>().squaredNorm();
      Real w_ext = GetEarthExternalPotentialForTdb(t_tdb);
      return 0.5 * v_earth_2 + w_ext;
    }

    Real TdbToTtIntegrandC4(Real t_tdb) {
      Vec6 rv_earth = GetBodyPosVel(t_tdb, BodyId::SSB, BodyId::EARTH, Frame::GCRF);
      Real v_earth_2 = rv_earth.tail<3>().squaredNorm();
      Real w_ext = GetEarthExternalPotentialForTdb(t_tdb);
      return 0.125 * v_earth_2 * v_earth_2 + 1.5 * v_earth_2 * w_ext - 0.5 * w_ext * w_ext;
    }

    std::pair<Real, Real> IntegrateTdbToTtTerms(Real t_tdb) {
      const Real t0_tdb = MjdToTime(MJD_COORDINATE_TT_TCG_TCB) + TDB_0;
      const double t_span = (t_tdb - t0_tdb).val();
      if (std::abs(t_span) < 1.0e-15) return {0.0, 0.0};

      const double sign = t_span >= 0.0 ? 1.0 : -1.0;
      const Real dt_nominal = sign * 0.01 * SECS_DAY;

      Real t_current = t0_tdb;
      Real f2_prev = TdbToTtIntegrandC2(t_current);
      Real f4_prev = TdbToTtIntegrandC4(t_current);
      Real i2 = 0.0;
      Real i4 = 0.0;

      while ((sign > 0.0 && t_current < t_tdb) || (sign < 0.0 && t_current > t_tdb)) {
        Real t_next = t_current + dt_nominal;
        if ((sign > 0.0 && t_next > t_tdb) || (sign < 0.0 && t_next < t_tdb)) {
          t_next = t_tdb;
        }

        Real dt = t_next - t_current;
        Real f2 = TdbToTtIntegrandC2(t_next);
        Real f4 = TdbToTtIntegrandC4(t_next);
        i2 += 0.5 * (f2_prev + f2) * dt;
        i4 += 0.5 * (f4_prev + f4) * dt;

        t_current = t_next;
        f2_prev = f2;
        f4_prev = f4;
      }

      return {i2, i4};
    }

    Real TdbToTtPositionTerm(Real t_tdb, const Vec3& x_bcrs) {
      Vec6 rv_earth = GetBodyPosVel(t_tdb, BodyId::SSB, BodyId::EARTH, Frame::GCRF);
      Vec3 x_earth = rv_earth.head<3>();
      Vec3 v_earth = rv_earth.tail<3>();
      Vec3 r_earth = x_bcrs - x_earth;

      Real v_dot_r = v_earth.dot(r_earth);
      Real v_earth_2 = v_earth.squaredNorm();
      Real w_ext = GetEarthExternalPotentialForTdb(t_tdb);
      const double c2 = C * C;
      const double c4 = c2 * c2;

      return -v_dot_r / c2 - (0.5 * v_earth_2 + 3.0 * w_ext) * v_dot_r / c4;
    }

    Real TdbToTtMinusTdbEq21(Real t_tdb, const Vec3& x_bcrs) {
      auto [i2, i4] = IntegrateTdbToTtTerms(t_tdb);
      const Real t0 = MjdToTime(MJD_COORDINATE_TT_TCG_TCB);
      const double c2 = C * C;
      const double c4 = c2 * c2;
      const Real position = -TdbToTtPositionTerm(t_tdb, x_bcrs);
      const Real bracket = TDB_0 + i2 / c2 + i4 / c4 + position;
      return (L_B - L_G) / (1.0 - L_B) * (t_tdb - t0) - (1.0 - L_G) / (1.0 - L_B) * bracket;
    }

  }  // namespace

  Real TDBToTt(Real t_tdb) {
    double k = 1.657e-3;
    double eb = 1.671e-2;
    Real mean_anom = 6.239996 + 1.99096871e-7 * t_tdb;
    Real ecc_anom = mean_anom + eb * sin(mean_anom);
    Real t_tt = t_tdb - k * sin(ecc_anom);
    return t_tt;
  }

  Real TDBToTt(Real t_tdb, const Vec3& x_bcrs) {
    return t_tdb + TdbToTtMinusTdbEq21(t_tdb, x_bcrs);
  }

  Real TtToTdb(Real t_tt) {
    Real days_j2000_tt = t_tt / SECS_DAY;
    Real me = (357.53 + 0.9856003 * days_j2000_tt) * RAD;
    Real t_tdb = t_tt + 0.001658 * sin(me) + 0.000014 * sin(2 * me);
    return t_tdb;
  }

  Real TtToTdb(Real t_tt, const Vec3& x_bcrs) {
    Real t_tdb = TtToTdb(t_tt);
    for (int iter = 0; iter < 10; ++iter) {
      Real delta = t_tt - TDBToTt(t_tdb, x_bcrs);
      t_tdb += delta;
      if (std::abs(delta.val()) < 1.0e-12) break;
    }
    return t_tdb;
  }

  Real TaiToGps(Real t_tai) {
    Real t_gps = t_tai - 19.0;
    return t_gps;
  }

  Real GpsToTai(Real t_gps) {
    Real t_tai = t_gps + 19.0;
    return t_tai;
  }

  Real TcbToTdb(Real t_tcb) {
    Real mjd_tcb = TimeToMjd(t_tcb);
    Real t_tdb = t_tcb - L_B * (mjd_tcb - MJD_COORDINATE_TT_TCG_TCB) * SECS_DAY + TDB_0;
    return t_tdb;
  }

  Real TtToTcb(Real t_tt) {
    Real t_tdb = TtToTdb(t_tt);
    const Real t0_tcb = MjdToTime(MJD_COORDINATE_TT_TCG_TCB);
    return (t_tdb - L_B * t0_tcb - TDB_0) / (1.0 - L_B);
  }

  Real TtToTcb(Real t_tt, const Vec3& x_bcrs) {
    Real t_tdb = TtToTdb(t_tt, x_bcrs);
    const Real t0_tcb = MjdToTime(MJD_COORDINATE_TT_TCG_TCB);
    return (t_tdb - L_B * t0_tcb - TDB_0) / (1.0 - L_B);
  }

  Real TcbToTt(Real t_tcb, const Vec3& x_bcrs) { return TDBToTt(TcbToTdb(t_tcb), x_bcrs); }

  Real EarthRotationAngle(Real t_ut1) {
    double theta_0 = 0.7790572732640;
    double dtheta_dt = 1.00273781191135448;
    Real theta_era = TWO_PI * (theta_0 + dtheta_dt * t_ut1 / SECS_DAY);
    return WrapToPi(theta_era);
  }

  Real GregorianToMjd(int year, int month, int day, int hour, int min, Real sec) {
    if (month <= 2) {
      month += 12;
      year--;
    }
    int b;
    if ((10000L * year + 100L * month + day) <= 15821004L)
      b = -2 + ((year + 4716) / 4) - 1179;  // Julian calendar
    else
      b = (year / 400) - (year / 100) + (year / 4);  // Gregorian calendar

    double mjd_midnight = 365L * year - 679004L + b + int(30.6001 * (month + 1)) + day;
    Real frac_of_day = (hour + min / 60.0 + sec / 3600.0) / 24.0;
    return mjd_midnight + frac_of_day;
  }

  std::tuple<int, int, int, int, int, Real> MjdToGregorian(Real mjd) {
    long a, b, c, d, e, f;
    a = long(mjd + 2400001.0);  // Convert Julian day number to calendar date
    if (a < 2299161) {          // Julian calendar
      b = 0;
      c = a + 1524;
    } else {  // Gregorian calendar
      b = long((a - 1867216.25) / 36524.25);
      c = a + b - (b / 4) + 1525;
    }
    d = long((c - 122.1) / 365.25);
    e = 365 * d + d / 4;
    f = long((c - e) / 30.6001);
    int day = c - e - int(30.6001 * f);
    int month = f - 1 - 12 * (f / 14);
    int year = d - 4715 - ((7 + month) / 10);
    Real hours = HOURS_DAY * (mjd - floor(mjd));
    int hour = int(hours);
    Real x = (hours - hour) * MINS_HOUR;
    int min = int(x);
    Real sec = (x - min) * SECS_MINUTE;
    return std::make_tuple(year, month, day, hour, min, sec);
  }

  Real GregorianToTime(int year, int month, int day, int hour, int min, Real sec) {
    Real mjd = GregorianToMjd(year, month, day, hour, min, sec);
    return MjdToTime(mjd);
  }

  Real GregorianToTime(const std::string& date) {
    int year, month, day, hour, min;
    double sec;
    sscanf(date.c_str(), "%d-%d-%dT%d:%d:%lf", &year, &month, &day, &hour, &min, &sec);
    return GregorianToTime(year, month, day, hour, min, sec);
  }

  Real GreenwichMeanSiderealTime(Real mjd_ut1) {
    Real mjd0 = floor(mjd_ut1);
    Real ut1 = SECS_DAY * (mjd_ut1 - mjd0);  // [s]
    Real T0 = (mjd0 - MJD_J2000_TT) / DAYS_CENTURY;
    Real T = (mjd_ut1 - MJD_J2000_TT) / DAYS_CENTURY;

    Real gmst = 24110.54841 + 8640184.812866 * T0 + 1.002737909350795 * ut1
                + (0.093104 - 6.2e-6 * T) * T * T;  // [s]

    return TWO_PI * frac(gmst / SECS_DAY);  // [rad]
  }

  Real MjdToTime(Real mjd) { return (mjd - MJD_J2000_TT) * SECS_DAY; }

  Real TimeToMjd(Real t) { return t / SECS_DAY + MJD_J2000_TT; }

  Real JdToTime(Real jd) { return (jd - JD_J2000_TT) * SECS_DAY; }

  Real TimeToJd(Real t) { return t / SECS_DAY + JD_J2000_TT; }

  std::string MjdToGregorianString(Real mjd, int precision) {
    double pow10 = pow(10, precision);
    Real mjd_round = (round(mjd * SECS_DAY * pow10, precision) + 0.1) / (SECS_DAY * pow10);
    auto [year, month, day, hour, min, sec] = MjdToGregorian(mjd_round);
    std::stringstream ss;
    sec = round(sec, precision);
    ss << year << "-";
    ss << std::setw(2) << std::setfill('0') << month << "-";
    ss << std::setw(2) << std::setfill('0') << day << "T";
    ss << std::setw(2) << std::setfill('0') << hour << ":";
    ss << std::setw(2) << std::setfill('0') << min << ":";
    ss << std::setw(2) << std::setfill('0') << floor(sec);
    if (precision > 0) {
      ss << "." << std::fixed << std::setprecision(0) << std::setw(precision) << std::setfill('0')
         << floor((sec - floor(sec)) * pow(10, precision));
    }
    return ss.str();
  }

  std::string TimeToGregorianString(Real t, int precision) {
    Real mjd = TimeToMjd(t);
    return MjdToGregorianString(mjd, precision);
  }

  Real GreenwichApparentSiderealTime(Real mjd_ut1) {
    return mod(GreenwichMeanSiderealTime(mjd_ut1) + EquinoxEquation(mjd_ut1), TWO_PI);
  }

  namespace {

    constexpr std::array<BodyId, 9> kTclExternalBodies = {
        BodyId::SUN,     BodyId::MERCURY, BodyId::VENUS,  BodyId::EARTH,   BodyId::MARS,
        BodyId::JUPITER, BodyId::SATURN,  BodyId::URANUS, BodyId::NEPTUNE,
    };

    Vec6 GetMoonBcrsStateForTcb(Real t_tcb) {
      return GetBodyPosVel(TcbToTdb(t_tcb), BodyId::SSB, BodyId::MOON, Frame::GCRF);
    }

    Real GetLunarExternalPotentialForTcb(Real t_tcb) {
      Real t_tdb = TcbToTdb(t_tcb);
      Vec3 x_moon = GetBodyPos(t_tdb, BodyId::SSB, BodyId::MOON, Frame::GCRF);
      Real w_ext = 0.0;

      for (BodyId body : kTclExternalBodies) {
        Vec3 x_body = GetBodyPos(t_tdb, BodyId::SSB, body, Frame::GCRF);
        w_ext += GetBodyGM(body) / (x_moon - x_body).norm();
      }
      return w_ext;
    }

    Real TcbToTclIntegrandC2(Real t_tcb) {
      Vec6 rv_moon = GetMoonBcrsStateForTcb(t_tcb);
      Real v_moon_2 = rv_moon.tail<3>().squaredNorm();
      Real w_ext = GetLunarExternalPotentialForTcb(t_tcb);
      return 0.5 * v_moon_2 + w_ext;
    }

    Real TcbToTclIntegrandC4(Real t_tcb) {
      Vec6 rv_moon = GetMoonBcrsStateForTcb(t_tcb);
      Real v_moon_2 = rv_moon.tail<3>().squaredNorm();
      Real w_ext = GetLunarExternalPotentialForTcb(t_tcb);
      return 0.125 * v_moon_2 * v_moon_2 + 1.5 * v_moon_2 * w_ext - 0.5 * w_ext * w_ext;
    }

    std::pair<Real, Real> IntegrateTcbToTclTerms(Real t_tcb) {
      const Real t0_tcb = MjdToTime(MJD_COORDINATE_TT_TCG_TCB);
      const double t_span = (t_tcb - t0_tcb).val();
      if (std::abs(t_span) < 1.0e-15) return {0.0, 0.0};

      const double sign = t_span >= 0.0 ? 1.0 : -1.0;
      const Real dt_nominal = sign * 0.01 * SECS_DAY;

      Real t_current = t0_tcb;
      Real f2_prev = TcbToTclIntegrandC2(t_current);
      Real f4_prev = TcbToTclIntegrandC4(t_current);
      Real i2 = 0.0;
      Real i4 = 0.0;

      while ((sign > 0.0 && t_current < t_tcb) || (sign < 0.0 && t_current > t_tcb)) {
        Real t_next = t_current + dt_nominal;
        if ((sign > 0.0 && t_next > t_tcb) || (sign < 0.0 && t_next < t_tcb)) {
          t_next = t_tcb;
        }

        Real dt = t_next - t_current;
        Real f2 = TcbToTclIntegrandC2(t_next);
        Real f4 = TcbToTclIntegrandC4(t_next);
        i2 += 0.5 * (f2_prev + f2) * dt;
        i4 += 0.5 * (f4_prev + f4) * dt;

        t_current = t_next;
        f2_prev = f2;
        f4_prev = f4;
      }

      return {i2, i4};
    }

    Real TcbToTclPositionTerm(Real t_tcb, const Vec3& x_bcrs) {
      Vec6 rv_moon = GetMoonBcrsStateForTcb(t_tcb);
      Vec3 x_moon = rv_moon.head<3>();
      Vec3 v_moon = rv_moon.tail<3>();
      Vec3 r_moon = x_bcrs - x_moon;

      Real v_dot_r = v_moon.dot(r_moon);
      Real v_moon_2 = v_moon.squaredNorm();
      Real w_ext = GetLunarExternalPotentialForTcb(t_tcb);
      const double c2 = C * C;
      const double c4 = c2 * c2;

      return -v_dot_r / c2 - (0.5 * v_moon_2 + 3.0 * w_ext) * v_dot_r / c4;
    }

  }  // namespace

  Real TcbToTcl(Real t_tcb) {
    Vec3 x_moon = GetMoonBcrsStateForTcb(t_tcb).head<3>();
    return TcbToTcl(t_tcb, x_moon);
  }

  Real TcbToTcl(Real t_tcb, const Vec3& x_bcrs) {
    auto [i2, i4] = IntegrateTcbToTclTerms(t_tcb);
    const double c2 = C * C;
    const double c4 = c2 * c2;
    return t_tcb - i2 / c2 - i4 / c4 + TcbToTclPositionTerm(t_tcb, x_bcrs);
  }

  VecX TcbToTcl(const VecX& t_tcb) {
    VecX t_tcl(t_tcb.size());
    for (int i = 0; i < t_tcb.size(); ++i) {
      t_tcl(i) = TcbToTcl(t_tcb(i));
    }
    return t_tcl;
  }

  VecX TcbToTcl(const VecX& t_tcb, const MatX3& x_bcrs) {
    LUPNT_CHECK(t_tcb.size() == x_bcrs.rows(), "Position row count must match time vector size",
                "TcbToTcl");
    VecX t_tcl(t_tcb.size());
    for (int i = 0; i < t_tcb.size(); ++i) {
      t_tcl(i) = TcbToTcl(t_tcb(i), x_bcrs.row(i).transpose());
    }
    return t_tcl;
  }

  Real TclToTcb(Real t_tcl) {
    Real t_tcb = t_tcl;
    for (int iter = 0; iter < 10; ++iter) {
      Real delta = t_tcl - TcbToTcl(t_tcb);
      t_tcb += delta;
      if (std::abs(delta.val()) < 1.0e-12) break;
    }
    return t_tcb;
  }

  Real TclToTcb(Real t_tcl, const Vec3& x_bcrs) {
    Real t_tcb = t_tcl;
    for (int iter = 0; iter < 10; ++iter) {
      Real delta = t_tcl - TcbToTcl(t_tcb, x_bcrs);
      t_tcb += delta;
      if (std::abs(delta.val()) < 1.0e-12) break;
    }
    return t_tcb;
  }

  VecX TclToTcb(const VecX& t_tcl) {
    VecX t_tcb(t_tcl.size());
    for (int i = 0; i < t_tcl.size(); ++i) {
      t_tcb(i) = TclToTcb(t_tcl(i));
    }
    return t_tcb;
  }

  VecX TclToTcb(const VecX& t_tcl, const MatX3& x_bcrs) {
    LUPNT_CHECK(t_tcl.size() == x_bcrs.rows(), "Position row count must match time vector size",
                "TclToTcb");
    VecX t_tcb(t_tcl.size());
    for (int i = 0; i < t_tcl.size(); ++i) {
      t_tcb(i) = TclToTcb(t_tcl(i), x_bcrs.row(i).transpose());
    }
    return t_tcb;
  }

  Real LtToTcl(Real t_lt) {
    Real mjd_lt = TimeToMjd(t_lt);
    Real lt_tcl = -L_L / (1.0 - L_L) * (mjd_lt - MJD_COORDINATE_TT_TCG_TCB) * SECS_DAY;
    return t_lt - lt_tcl;
  }

  Real TclToLt(Real t_tcl) {
    Real mjd_tcl = TimeToMjd(t_tcl);
    Real lt_tcl = -L_L * (mjd_tcl - MJD_COORDINATE_TT_TCG_TCB) * SECS_DAY;
    return t_tcl + lt_tcl;
  }

  Real GetProperTimeCorrectionTcl(Real t_tcg, const Vec3& x_mci) {
    Real t_tdb = TtToTdb(TcgToTt(t_tcg));
    Vec6 rv_LE = GetBodyPosVel(t_tdb, BodyId::EARTH, BodyId::MOON, Frame::GCRF);
    Vec3 r_vec_x_gcrf = ConvertFrame(t_tdb, x_mci, Frame::MOON_CI, Frame::GCRF);
    Vec3 r_vec_LE = rv_LE.head<3>();
    Vec3 v_vec_LE = rv_LE.tail<3>();
    Real proper_time_correction = -v_vec_LE.dot(r_vec_x_gcrf - r_vec_LE) / (C * C);
    return proper_time_correction;
  }

  VecX GetProperTimeCorrectionTcl(const VecX& t_tcg, const MatX& x_mci) {
    VecX proper_time_corrections(t_tcg.size());
    for (int i = 0; i < t_tcg.size(); i++) {
      proper_time_corrections(i) = GetProperTimeCorrectionTcl(t_tcg(i), x_mci.row(i).transpose());
    }
    return proper_time_corrections;
  }

  // =========================================================================
  // TL − TT conversion (Turyshev 2026, ApJ 997:97, Eq. 57)
  // =========================================================================
  //
  // Module-level optional Chebyshev fit of TL−TT as a function of TDB.
  // Populated by InitLtMinusTtFit(); cleared by ClearLtMinusTtFit().
  // When populated, TdbToLtMinusTt() uses it for fast evaluation; otherwise
  // it falls back to direct numerical integration from T_0.
  namespace {
    std::optional<ChebyshevFitModel> s_lt_minus_tt_fit;
  }

  static Real TdbToLtMinusTtIntegrandC2(Real t_tdb) {
    Vec6 rv_EM = GetBodyPosVel(t_tdb, BodyId::EARTH, BodyId::MOON, Frame::GCRF);
    Vec6 rv_ES = GetBodyPosVel(t_tdb, BodyId::SUN, BodyId::EARTH, Frame::GCRF);

    Vec3 r_EM = rv_EM.head<3>();
    Vec3 v_EM = rv_EM.tail<3>();
    Vec3 r_ES = rv_ES.head<3>();

    Real r_EM_norm = r_EM.norm();
    Real r_SE_norm = r_ES.norm();

    // v²_EM / 2
    Real kin = 0.5 * v_EM.squaredNorm();

    // (GM_E − 2GM_M) / r_EM  [note the factor of 2: −GM_M from TCL−TCG plus
    //  an additional −GM_M from the L_L scaling of TL = TCL − L_L·(TCL−T_0)]
    Real grav = (GM_EARTH - 2.0 * GM_MOON) / r_EM_norm;

    // Solar ℓ=2 tidal W⊙_EM = (3/2) GM_S / r_SE^5 · [(r_ES·r_EM)² − r_SE²r_EM²/3]
    Real dot_ESEM = r_ES.dot(r_EM);
    Real tidal = 1.5 * GM_SUN / pow(r_SE_norm, 5)
                 * (dot_ESEM * dot_ESEM - r_SE_norm * r_SE_norm * r_EM_norm * r_EM_norm / 3.0);

    // L_G correction: L_G · (3/2) · GM_S / r_SE
    Real sun_corr = L_G * 1.5 * GM_SUN / r_SE_norm;

    return kin + grav + tidal + sun_corr;
  }

  static Real TdbToLtMinusTtIntegrandC4(Real t_tdb) {
    Vec6 rv_EM = GetBodyPosVel(t_tdb, BodyId::EARTH, BodyId::MOON, Frame::GCRF);
    Vec6 rv_ES = GetBodyPosVel(t_tdb, BodyId::SUN, BodyId::EARTH, Frame::GCRF);
    Vec6 rv_E_bcrs = GetBodyPosVel(t_tdb, BodyId::SSB, BodyId::EARTH, Frame::GCRF);

    Vec3 v_EM = rv_EM.tail<3>();
    Vec3 v_E = rv_E_bcrs.tail<3>();
    Real r_SE_norm = rv_ES.head<3>().norm();

    return 3.0 * GM_SUN / r_SE_norm * v_E.dot(v_EM);
  }

  static Real TdbToLtMinusTtEarthMoonEndpoint(Real t_tdb) {
    Vec6 rv_EM = GetBodyPosVel(t_tdb, BodyId::EARTH, BodyId::MOON, Frame::GCRF);
    Vec6 rv_E_bcrs = GetBodyPosVel(t_tdb, BodyId::SSB, BodyId::EARTH, Frame::GCRF);
    return rv_E_bcrs.tail<3>().dot(rv_EM.head<3>());
  }

  Real TdbToLtMinusTt(Real t_tdb) {
    // Fast path: use pre-fitted Chebyshev model if available
    {
      VecX val(1);
      bool ok = false;
#pragma omp critical
      {
        ok = s_lt_minus_tt_fit.has_value() && s_lt_minus_tt_fit->Eval(t_tdb, &val, nullptr);
      }
      if (ok) return val(0);
    }

    // Slow path: numerical integration from T₀ + TDB₀
    const double t0_s
        = (MJD_COORDINATE_TT_TCG_TCB - MJD_J2000_TT) * SECS_DAY + TDB_0;  // [s from J2000]
    const double t_end = t_tdb.val();

    if (t_end <= t0_s) {
      Real endpoint
          = (TdbToLtMinusTtEarthMoonEndpoint(t0_s) - TdbToLtMinusTtEarthMoonEndpoint(t_tdb))
            / (C * C);
      return (L_G - L_L) / (1.0 - L_B) * (t_tdb - t0_s) + endpoint;
    }

    const double dt_nom = 0.01 * SECS_DAY;  // 864 s nominal step
    double t_cur = t0_s;
    double int_c2 = 0.0, int_c4 = 0.0;
    double f_c2_prev = TdbToLtMinusTtIntegrandC2(t_cur).val();
    double f_c4_prev = TdbToLtMinusTtIntegrandC4(t_cur).val();

    while (t_cur < t_end) {
      double t_next = std::min(t_cur + dt_nom, t_end);
      double dt = t_next - t_cur;

      double f_c2 = TdbToLtMinusTtIntegrandC2(t_next).val();
      double f_c4 = TdbToLtMinusTtIntegrandC4(t_next).val();

      int_c2 += 0.5 * (f_c2 + f_c2_prev) * dt;
      int_c4 += 0.5 * (f_c4 + f_c4_prev) * dt;

      f_c2_prev = f_c2;
      f_c4_prev = f_c4;
      t_cur = t_next;
    }

    const double C2 = C * C;
    const double C4 = C2 * C2;
    Real secular = (L_G - L_L) / (1.0 - L_B) * (t_tdb - t0_s);
    Real endpoint
        = (TdbToLtMinusTtEarthMoonEndpoint(t0_s) - TdbToLtMinusTtEarthMoonEndpoint(t_tdb)) / C2;
    return secular - int_c2 / C2 - int_c4 / C4 + endpoint;
  }

  Real TdbToLt(Real t_tdb) { return TDBToTt(t_tdb) + TdbToLtMinusTt(t_tdb); }

  Real LtToTdb(Real t_lt) {
    Real t_tdb = t_lt;  // initial guess
    for (int iter = 0; iter < 10; iter++) {
      Real t_lt_est = TdbToLt(t_tdb);
      Real delta = t_lt - t_lt_est;
      t_tdb = t_tdb + delta;
      if (std::abs(delta.val()) < 1e-12) break;
    }
    return t_tdb;
  }

  Real TtToLt(Real t_tt) { return TdbToLt(TtToTdb(t_tt)); }

  Real LtToTt(Real t_lt) { return TDBToTt(LtToTdb(t_lt)); }

  void InitLtMinusTtFit(Real t_start_tdb, Real t_end_tdb, double segment_length, int num_coeffs) {
    const double t_start = t_start_tdb.val();
    const double t_end = t_end_tdb.val();

    if (!(t_end > t_start)) throw std::runtime_error("InitLtMinusTtFit: t_end must be > t_start");

    const double t0_s = (MJD_COORDINATE_TT_TCG_TCB - MJD_J2000_TT) * SECS_DAY + TDB_0;
    const double t_sweep_start = std::min(t0_s, t_start);

    double span = t_end - t_start;
    int num_segs = std::max(1, static_cast<int>(std::ceil(span / segment_length)));
    double seg_len = span / num_segs;

    // -----------------------------------------------------------------------
    // 1. Collect ALL Chebyshev-Gauss node times across all segments (sorted).
    // -----------------------------------------------------------------------
    struct NodeSpec {
      double t;
      int seg_idx;
      int k;  // DCT index
    };
    std::vector<NodeSpec> all_nodes;
    all_nodes.reserve(num_segs * num_coeffs);

    for (int s = 0; s < num_segs; s++) {
      double seg_start = t_start + s * seg_len;
      double seg_end = (s == num_segs - 1) ? t_end : seg_start + seg_len;
      double mid = 0.5 * (seg_start + seg_end);
      double radius = 0.5 * (seg_end - seg_start);
      for (int k = 0; k < num_coeffs; k++) {
        double theta = (2.0 * k + 1.0) * PI / (2.0 * num_coeffs);
        double t = mid + radius * std::cos(theta);
        all_nodes.push_back({t, s, k});
      }
    }
    std::sort(all_nodes.begin(), all_nodes.end(),
              [](const NodeSpec& a, const NodeSpec& b) { return a.t < b.t; });

    // -----------------------------------------------------------------------
    // 2. Single forward sweep from t_sweep_start to t_end.
    //    Record TL−TT at each node (node_vals[seg][k]).
    // -----------------------------------------------------------------------
    const double dt_nom = 0.01 * SECS_DAY;  // 864 s integration step

    std::vector<std::vector<double>> node_vals(num_segs, std::vector<double>(num_coeffs, 0.0));

    double t_cur = t_sweep_start;
    double int_c2 = 0.0, int_c4 = 0.0;
    double f_c2_prev = TdbToLtMinusTtIntegrandC2(t_cur).val();
    double f_c4_prev = TdbToLtMinusTtIntegrandC4(t_cur).val();

    const double C2 = C * C;
    const double C4 = C2 * C2;

    auto lt_minus_tt_at = [&](double t_node) -> double {
      double secular = (L_G - L_L) / (1.0 - L_B) * (t_node - t0_s);
      double endpoint = (TdbToLtMinusTtEarthMoonEndpoint(t0_s).val()
                         - TdbToLtMinusTtEarthMoonEndpoint(t_node).val())
                        / C2;
      return secular - int_c2 / C2 - int_c4 / C4 + endpoint;
    };

    int node_idx = 0;
    int total_nodes = static_cast<int>(all_nodes.size());

    while (node_idx < total_nodes) {
      double t_node = all_nodes[node_idx].t;

      // Advance integration to t_node
      while (t_cur < t_node) {
        double t_step = std::min(t_cur + dt_nom, t_node);
        double dt = t_step - t_cur;

        double f_c2 = TdbToLtMinusTtIntegrandC2(t_step).val();
        double f_c4 = TdbToLtMinusTtIntegrandC4(t_step).val();

        int_c2 += 0.5 * (f_c2 + f_c2_prev) * dt;
        int_c4 += 0.5 * (f_c4 + f_c4_prev) * dt;

        f_c2_prev = f_c2;
        f_c4_prev = f_c4;
        t_cur = t_step;
      }

      // Record all nodes at exactly t_cur
      double val = lt_minus_tt_at(t_cur);
      while (node_idx < total_nodes && all_nodes[node_idx].t <= t_cur) {
        node_vals[all_nodes[node_idx].seg_idx][all_nodes[node_idx].k] = val;
        node_idx++;
      }
    }

    // -----------------------------------------------------------------------
    // 3. Build Chebyshev fit model from node values via DCT.
    // -----------------------------------------------------------------------
    ChebyshevFitModel model;
    model.t_start = t_start;
    model.t_end = t_end;
    model.num_dims = 1;
    model.num_coeffs = num_coeffs;
    model.segments.reserve(num_segs);

    for (int s = 0; s < num_segs; s++) {
      double seg_start = t_start + s * seg_len;
      double seg_end = (s == num_segs - 1) ? t_end : seg_start + seg_len;
      double mid = 0.5 * (seg_start + seg_end);
      double radius = 0.5 * (seg_end - seg_start);

      ChebyshevFitSegment seg;
      seg.t_mid = mid;
      seg.t_radius = radius;
      seg.coeffs.assign(1, std::vector<double>(num_coeffs, 0.0));

      for (int j = 0; j < num_coeffs; j++) {
        double w = (j == 0) ? 1.0 : 2.0;
        for (int k = 0; k < num_coeffs; k++) {
          double c = std::cos((2.0 * k + 1.0) * PI * j / (2.0 * num_coeffs));
          seg.coeffs[0][j] += node_vals[s][k] * c;
        }
        seg.coeffs[0][j] *= w / num_coeffs;
      }
      model.segments.push_back(std::move(seg));
    }

#pragma omp critical
    {
      s_lt_minus_tt_fit = std::move(model);
    }
  }

  void ClearLtMinusTtFit() {
#pragma omp critical
    {
      s_lt_minus_tt_fit.reset();
    }
  }

  bool HasFittedLtMinusTt(Real t_tdb) {
    bool ok = false;
#pragma omp critical
    {
      ok = s_lt_minus_tt_fit.has_value() && t_tdb.val() >= s_lt_minus_tt_fit->t_start
           && t_tdb.val() <= s_lt_minus_tt_fit->t_end;
    }
    return ok;
  }

  VEC_IMP_REAL(UtcToUt1)
  VEC_IMP_REAL(Ut1ToUtc)
  VEC_IMP_REAL(TaiToUtc)
  VEC_IMP_REAL(UtcToTai)
  VEC_IMP_REAL(TaiToTt)
  VEC_IMP_REAL(TtToTai)
  VEC_IMP_REAL(TcgToTt)
  VEC_IMP_REAL(TtToTcg)
  VEC_IMP_REAL(TtToTdb)
  VEC_IMP_REAL(TDBToTt)
  VEC_IMP_REAL(TaiToGps)
  VEC_IMP_REAL(GpsToTai)
  VEC_IMP_REAL(TcbToTdb)
  VEC_IMP_REAL(TtToTcb)

  VEC_IMP_REAL(MjdToTime)
  VEC_IMP_REAL(TimeToMjd)
  VEC_IMP_REAL(JdToTime)
  VEC_IMP_REAL(TimeToJd)
  VEC_IMP_REAL(TclToLt)
  VEC_IMP_REAL(LtToTcl)
  VEC_IMP_REAL(TdbToLtMinusTt)
  VEC_IMP_REAL(TdbToLt)
  VEC_IMP_REAL(LtToTdb)
  VEC_IMP_REAL(TtToLt)
  VEC_IMP_REAL(LtToTt)

}  // namespace lupnt