mars91
Published © MIT

WhatsUp? A GPS Keychain That Shows Overhead Planets

This small circuit uses GPS and 9 RGB LEDs that light up when the eight planets (and Pluto and the Moon) are overhead

IntermediateFull instructions provided2 hours224
WhatsUp? A GPS Keychain That Shows Overhead Planets

Things used in this project

Hardware components

RGB LED
https://www.lcsc.com/product-detail/C965555.html
×1
10uF capacitors - 0603
×4
LED - any color 0603
×1
10k resistors - 0603
×4
ATTINY3216
Microchip ATTINY3216
×1
220uF capacitor
https://www.digikey.com/en/products/detail/kemet/T520B227M006ATE045/2230995
×1
SAM-M10Q
https://www.digikey.com/en/products/detail/u-blox/SAM-M10Q-00B/16672678
×1
.1uF capacitors - 0603
×4
AO3413
https://www.digikey.com/en/products/detail/alpha-omega-semiconductor-inc/AO3413/1855780
×1
metal buttons
https://www.digikey.com/en/products/detail/same-sky-formerly-cui-devices/TS18-5-25-SL-160-SMT-TR/16588311
×2
IRLML6402
https://www.digikey.com/en/products/detail/alpha-omega-semiconductor-inc/AO3413/1855780
×1
47k resistors - 0603
×1
LS14250 case
https://www.digikey.com/en/products/detail/keystone-electronics/1016/2137844
×1
LS14250
https://www.amazon.com/dp/B0CNSZ5JXK?th=1
×1
2.2k resistors - 0603
×1
TPS7A0230
https://www.digikey.com/en/products/detail/texas-instruments/TPS7A0230PDBVR/12165118
×1
MMBT3904
https://www.digikey.com/en/products/detail/diotec-semiconductor/MMBT3904/13163698
×1
100k resistors 0603
×2
PCBWay Custom PCB
PCBWay Custom PCB
https://www.pcbway.com/project/shareproject/W775378ASE188_ublox_gps_planet_12c8586f.html
×1

Software apps and online services

Arduino IDE
Arduino IDE
python

Hand tools and fabrication machines

hot plate
any normal hot plate will work
solder
https://www.amazon.com/dp/B0186IM0P0
UPDI programmer
https://www.digikey.com/en/products/detail/adafruit-industries-llc/5879/22596413
Stencil Squeegee
https://www.digikey.com/en/products/detail/chip-quik-inc/SS-METAL-0-2-68X36/16896798
Solder Paste, Tack Flux
Solder Paste, Tack Flux

Story

Read more

Code

SAMM10Q.h

C/C++
// ublox gps code - big thanks to sparkfun 
// https://github.com/sparkfun/SparkFun_u-blox_SAM-M10Q
// needed to add extra delays to work with attiny3216

#include <Wire.h>
#define UBLOX_ADDR 0x42

const uint8_t UBX_SYNCH_1   = 0xB5;
const uint8_t UBX_SYNCH_2   = 0x62;
const uint8_t UBX_CLASS_NAV = 0x01;
const uint8_t UBX_NAV_PVT   = 0x07;

typedef struct {
  uint8_t  fixType;
  uint8_t  numSV;
  int32_t  lon;
  int32_t  lat;
  uint32_t unixTime;
  bool     dataReceived;
} GPSData_t;

extern GPSData_t gpsData;

bool gpsFound() {
  Wire.beginTransmission(UBLOX_ADDR);
  uint8_t error = Wire.endTransmission();
  delay(20);
  return (error == 0);
}

uint16_t getBytesAvailable() {
  Wire.beginTransmission(UBLOX_ADDR);
  Wire.write(0xFD);
  uint8_t err = Wire.endTransmission(false);
  if (err != 0) {
    Wire.endTransmission();
    delay(10);
    return 0;
  }

  delay(5);
  uint8_t bytesRead = Wire.requestFrom(UBLOX_ADDR, (uint8_t)2, (uint8_t)1);
  delay(3);

  if (bytesRead < 2) return 0;

  uint8_t msb = Wire.read();
  uint8_t lsb = Wire.read();
  delay(2);

  return ((uint16_t)msb << 8) | lsb;
}

uint16_t readBytesChunk(uint8_t *buffer, uint16_t length) {
  if (length > 32) length = 32;

  Wire.beginTransmission(UBLOX_ADDR);
  Wire.write(0xFF);
  uint8_t err = Wire.endTransmission(false);
  if (err != 0) {
    Wire.endTransmission();
    delay(10);
    return 0;
  }

  delay(5);
  uint8_t bytesRead = Wire.requestFrom(UBLOX_ADDR, (uint8_t)length, (uint8_t)1);
  delay(3);

  for (uint8_t i = 0; i < bytesRead; i++) {
    if (Wire.available()) buffer[i] = Wire.read();
  }

  delay(2);
  return bytesRead;
}

// Convert date/time to Unix timestamp (UTC)
uint32_t dateTimeToUnix(uint16_t year, uint8_t month, uint8_t day,
                        uint8_t hour, uint8_t min, uint8_t sec)
{
  const uint8_t daysInMonth[] = {31,28,31,30,31,30,31,31,30,31,30,31};
  uint32_t days = 0;

  // Days from 1970 to current year
  for (uint16_t y = 1970; y < year; y++) {
    days += 365;
    if ((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0)) {
      days++; // leap year
    }
  }

  // Days from months this year
  for (uint8_t m = 1; m < month; m++) {
    days += daysInMonth[m - 1];
    if (m == 2) { // February leap day
      if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
        days++;
      }
    }
  }

  days += (day - 1);

  return days * 86400UL + hour * 3600UL + min * 60UL + sec;
}



bool readUBXMessage() {
  uint16_t available = getBytesAvailable();
  if (available < 8) return false;

  uint8_t buffer[120];
  uint16_t bufferIndex = 0;

  while (available > 0 && bufferIndex < 100) {
    uint16_t toRead = (available > 32) ? 32 : available;
    uint16_t read = readBytesChunk(&buffer[bufferIndex], toRead);
    if (read == 0) break;
    bufferIndex += read;
    available -= read;
    delay(10);
  }

  if (bufferIndex < 10) return false;

  int syncPos = -1;
  for (int i = 0; i < bufferIndex - 1; i++) {
    if (buffer[i] == UBX_SYNCH_1 && buffer[i+1] == UBX_SYNCH_2) {
      syncPos = i;
      break;
    }
  }

  if (syncPos < 0 || syncPos + 8 > bufferIndex) return false;

  uint8_t msgClass   = buffer[syncPos + 2];
  uint8_t msgID      = buffer[syncPos + 3];
  uint16_t payloadLen = buffer[syncPos + 4] | ((uint16_t)buffer[syncPos + 5] << 8);

  if (payloadLen > 100 || syncPos + 8 + payloadLen > bufferIndex) return false;

  uint8_t checksumA = 0, checksumB = 0;
  for (int i = 2; i < 6 + payloadLen; i++) {
    checksumA += buffer[syncPos + i];
    checksumB += checksumA;
  }

  uint8_t ckA = buffer[syncPos + 6 + payloadLen];
  uint8_t ckB = buffer[syncPos + 7 + payloadLen];
  if (checksumA != ckA || checksumB != ckB) return false;

  // NAV-PVT message
  if (msgClass == UBX_CLASS_NAV && msgID == UBX_NAV_PVT && payloadLen == 92) {
    int p = syncPos + 6;

    uint16_t year  = buffer[p + 4] | ((uint16_t)buffer[p + 5] << 8);
    uint8_t  month = buffer[p + 6];
    uint8_t  day   = buffer[p + 7];
    uint8_t  hour  = buffer[p + 8];
    uint8_t  min   = buffer[p + 9];
    uint8_t  sec   = buffer[p + 10];
    gpsData.unixTime = dateTimeToUnix(year, month, day, hour, min, sec);

    gpsData.fixType = buffer[p + 20];
    gpsData.numSV   = buffer[p + 23];

    gpsData.lon = (int32_t)(
      buffer[p + 24] |
      ((uint32_t)buffer[p + 25] << 8) |
      ((uint32_t)buffer[p + 26] << 16) |
      ((uint32_t)buffer[p + 27] << 24));

    gpsData.lat = (int32_t)(
      buffer[p + 28] |
      ((uint32_t)buffer[p + 29] << 8) |
      ((uint32_t)buffer[p + 30] << 16) |
      ((uint32_t)buffer[p + 31] << 24));

    gpsData.dataReceived = true;
    return true;
  }

  return false;
}

// static uint8_t ubxBuffer[120];
// bool readUBXMessage() {
//   uint16_t available = getBytesAvailable();
//   if (available < 8) return false;

//   // uint8_t ubxBuffer[120];
//   uint16_t bufferIndex = 0;

//   // while (available > 0 && bufferIndex < 100) {
//   while (available > 0 && bufferIndex < sizeof(ubxBuffer)) {
//     uint16_t toRead = (available > 32) ? 32 : available;
//     uint16_t read = readBytesChunk(&ubxBuffer[bufferIndex], toRead);
//     if (read == 0) break;
//     bufferIndex += read;
//     available -= read;
//     delay(10);
//   }

//   if (bufferIndex < 10) return false;

//   int syncPos = -1;
//   for (int i = 0; i < bufferIndex - 1; i++) {
//     if (ubxBuffer[i] == UBX_SYNCH_1 && ubxBuffer[i+1] == UBX_SYNCH_2) {
//       syncPos = i;
//       break;
//     }
//   }

//   if (syncPos < 0 || syncPos + 8 > bufferIndex) return false;

//   uint8_t msgClass   = ubxBuffer[syncPos + 2];
//   uint8_t msgID      = ubxBuffer[syncPos + 3];
//   uint16_t payloadLen = ubxBuffer[syncPos + 4] | ((uint16_t)ubxBuffer[syncPos + 5] << 8);

//   if (payloadLen > 100 || syncPos + 8 + payloadLen > bufferIndex) return false;

//   uint8_t checksumA = 0, checksumB = 0;
//   for (int i = 2; i < 6 + payloadLen; i++) {
//     checksumA += ubxBuffer[syncPos + i];
//     checksumB += checksumA;
//   }

//   uint8_t ckA = ubxBuffer[syncPos + 6 + payloadLen];
//   uint8_t ckB = ubxBuffer[syncPos + 7 + payloadLen];
//   if (checksumA != ckA || checksumB != ckB) return false;

//   // NAV-PVT message
//   if (msgClass == UBX_CLASS_NAV && msgID == UBX_NAV_PVT && payloadLen == 92) {
//     int p = syncPos + 6;

//     uint16_t year  = ubxBuffer[p + 4] | ((uint16_t)ubxBuffer[p + 5] << 8);
//     uint8_t  month = ubxBuffer[p + 6];
//     uint8_t  day   = ubxBuffer[p + 7];
//     uint8_t  hour  = ubxBuffer[p + 8];
//     uint8_t  min   = ubxBuffer[p + 9];
//     uint8_t  sec   = ubxBuffer[p + 10];
//     gpsData.unixTime = dateTimeToUnix(year, month, day, hour, min, sec);

//     gpsData.fixType = ubxBuffer[p + 20];
//     gpsData.numSV   = ubxBuffer[p + 23];

//     gpsData.lon = (int32_t)(
//       ubxBuffer[p + 24] |
//       ((uint32_t)ubxBuffer[p + 25] << 8) |
//       ((uint32_t)ubxBuffer[p + 26] << 16) |
//       ((uint32_t)ubxBuffer[p + 27] << 24));

//     gpsData.lat = (int32_t)(
//       ubxBuffer[p + 28] |
//       ((uint32_t)ubxBuffer[p + 29] << 8) |
//       ((uint32_t)ubxBuffer[p + 30] << 16) |
//       ((uint32_t)ubxBuffer[p + 31] << 24));

//     gpsData.dataReceived = true;
//     return true;
//   }

//   return false;
// }

bool configureNAVPVT() {
  delay(200);
  
  uint8_t cfg[] = {
    0xB5, 0x62,  0x06, 0x01,  0x08, 0x00,
    0x01, 0x07,  0x01, 0x00,  0x00, 0x00,
    0x00, 0x00,  0x00, 0x00
  };
  
  uint8_t ckA = 0, ckB = 0;
  for (int i = 2; i < 14; i++) {
    ckA += cfg[i];
    ckB += ckA;
  }
  cfg[14] = ckA;
  cfg[15] = ckB;
  
  Wire.beginTransmission(UBLOX_ADDR);
  Wire.write(0xFF);
  for (int i = 0; i < 16; i++) {
    Wire.write(cfg[i]);
  }
  uint8_t result = Wire.endTransmission();
  
  delay(300);
  return (result == 0);
}

attiny3216_WORKING_GPS_LATCH.ino

C/C++
/////////// space stuf
//////////////////////////////////////////////////////////////////////////
#include "ephemeris_tables.h"

static const double SECONDS_PER_DAY = 86400.0;
static const double JD_UNIX_EPOCH   = 2440587.5;
static const double G_AU3_DAY2      = 2.9591220828559093e-4; // AU^3 / day^2
static const double MU_SUN          = G_AU3_DAY2 * 1.0;

static const double MU_EARTH_KM = 398600.4418;
static const double AU_KM       = 149597870.700;
static const double DAY_SEC     = 86400.0;
static const double MOON_DT_SEC = 300.0;

static inline void accel_sun(const double r[3], double a[3]) {
  double d2 = r[0]*r[0] + r[1]*r[1] + r[2]*r[2];
  double d  = sqrt(d2);
  double denom = (d*d*d) + 1e-12;
  double k = -MU_SUN / denom;
  a[0] = k * r[0];
  a[1] = k * r[1];
  a[2] = k * r[2];
}

static inline void euler_step(double r[3], double v[3], double dt_days) {
  double a[3];
  accel_sun(r, a);
  r[0] += v[0] * dt_days;
  r[1] += v[1] * dt_days;
  r[2] += v[2] * dt_days;
  v[0] += a[0] * dt_days;
  v[1] += a[1] * dt_days;
  v[2] += a[2] * dt_days;
}

static inline void eci_to_ecef_target_timestamp(const double r_eci[3], double r_ecef[3], double theta) {
  double c = cos(theta);
  double s = sin(theta);
  r_ecef[0] =  c * r_eci[0] + s * r_eci[1];
  r_ecef[1] = -s * r_eci[0] + c * r_eci[1];
  r_ecef[2] =  r_eci[2];
}

double earth_rotation_angle_from_unix(int64_t t_unix) {
  double JD = JD_UNIX_EPOCH + (double)t_unix / SECONDS_PER_DAY;
  double d  = JD - 2451545.0;

  double theta = 2.0 * M_PI * (
    0.7790572732640 +
    1.00273781191135448 * d
  );
  theta = fmod(theta, 2.0 * M_PI);
  if (theta < 0.0) theta += 2.0 * M_PI;
  return theta;
}

static inline void latlon_to_unit_ecef(double lat_deg, double lon_deg, double out[3]) {
  double lat = lat_deg * (M_PI / 180.0);
  double lon = lon_deg * (M_PI / 180.0);
  double clat = cos(lat);
  out[0] = clat * cos(lon);
  out[1] = clat * sin(lon);
  out[2] = sin(lat);
}

static inline bool is_visible_est(const double planet_ecef[3], double user_lat, double user_lon) {
  double user_ecef[3];
  latlon_to_unit_ecef(user_lat, user_lon, user_ecef);
  double dot = planet_ecef[0]*user_ecef[0] + planet_ecef[1]*user_ecef[1] + planet_ecef[2]*user_ecef[2];
  return (dot > 0.0);
}

int closest_time_index(const int64_t *unix_time, int n, int64_t target) {
  if (n <= 0) return 0;
  if (target <= unix_time[0]) return 0;
  if (target >= unix_time[n - 1]) return n - 1;

  int left = 0;
  int right = n - 1;
  while (left <= right) {
    int mid = (left + right) >> 1;
    int64_t tmid = unix_time[mid];
    if (tmid == target) return mid;
    if (tmid < target) left = mid + 1;
    else right = mid - 1;
  }

  int64_t dr = target - unix_time[right];
  int64_t dl = unix_time[left] - target;
  return (dr <= dl) ? right : left;
}

void propagate_planet_grcs(int idx, double dt_days_dbl,
                           const float planet_table[EPHEMERIS_SAMPLES][6],
                           double r_geo_out[3], double r_earth[3])
{

  double r_p[3] = {
    (double)planet_table[idx][0] + (double)sun_sv[idx][0],
    (double)planet_table[idx][1] + (double)sun_sv[idx][1],
    (double)planet_table[idx][2] + (double)sun_sv[idx][2]
  };
  double v_p[3] = {
    (double)planet_table[idx][3] + (double)sun_sv[idx][3],
    (double)planet_table[idx][4] + (double)sun_sv[idx][4],
    (double)planet_table[idx][5] + (double)sun_sv[idx][5]
  };

  euler_step(r_p, v_p, dt_days_dbl);

  r_geo_out[0] = r_p[0] - r_earth[0];
  r_geo_out[1] = r_p[1] - r_earth[1];
  r_geo_out[2] = r_p[2] - r_earth[2];
}

void propagate_moon_grcs_fine(int idx, int64_t dt_sec_i64,
                              const float moon_table[EPHEMERIS_SAMPLES][6],
                              double r_moon_out_au[3])
{
  // initial in AU / AU/day
  double r_km[3] = {
    (double)moon_table[idx][0] * AU_KM,
    (double)moon_table[idx][1] * AU_KM,
    (double)moon_table[idx][2] * AU_KM
  };
  double v_km_s[3] = {
    (double)moon_table[idx][3] * AU_KM / DAY_SEC,
    (double)moon_table[idx][4] * AU_KM / DAY_SEC,
    (double)moon_table[idx][5] * AU_KM / DAY_SEC
  };

  double dt_remaining = (double)dt_sec_i64;
  double sign = (dt_remaining >= 0.0) ? 1.0 : -1.0;
  dt_remaining = fabs(dt_remaining);

  while (dt_remaining > 0.0) {
    double h = MOON_DT_SEC;
    if (h > dt_remaining) h = dt_remaining;
    h *= sign;
    rk4_step_moon(r_km, v_km_s, h);
    dt_remaining -= fabs(h);
  }

  r_moon_out_au[0] = r_km[0] / AU_KM;
  r_moon_out_au[1] = r_km[1] / AU_KM;
  r_moon_out_au[2] = r_km[2] / AU_KM;
}

#include <math.h>
#include <stdint.h>

static inline double dot3(const double a[3], const double b[3]) {
  return a[0]*b[0] + a[1]*b[1] + a[2]*b[2];
}
static inline double norm3(const double a[3]) {
  return sqrt(dot3(a,a));
}

// Stumpff
static inline double stumpff_C(double z) {
  if (z > 0.0) {
    double s = sqrt(z);
    return (1.0 - cos(s)) / z;
  } else if (z < 0.0) {
    double s = sqrt(-z);
    return (cosh(s) - 1.0) / (-z);
  } else {
    return 0.5;
  }
}

// Stumpff z
static inline double stumpff_S(double z) {
  if (z > 0.0) {
    double s = sqrt(z);
    double s3 = s*s*s;
    return (s - sin(s)) / s3;
  } else if (z < 0.0) {
    double s = sqrt(-z);
    double s3 = s*s*s;
    return (sinh(s) - s) / s3;
  } else {
    return 1.0 / 6.0;
  }
}


//// This method isnt discussed in the Instructables. I tested the RK4 approach on a more powerful chip when i wrote the article.
//// I also accidentally uploaded the fast, inaccurate Euler based Moon version in my demo but it just happened to be right when i was testing...
//// On the ATtiny3216 I switched to the rk4 and it slowed things down noticeably. This Kepler method (Im not 100% sure how all of it works) 
//// achieves about 98 to 99% accuracy for the Moon and runs much faster.


void propagate_moon_grcs_fine_kepler(int idx, int64_t dt_sec_i64,
                              const float moon_table[EPHEMERIS_SAMPLES][6],
                              double r_moon_out_au[3])
{
  const double dt = (double)dt_sec_i64;

  if (fabs(dt) < 1e-6) {
    r_moon_out_au[0] = (double)moon_table[idx][0];
    r_moon_out_au[1] = (double)moon_table[idx][1];
    r_moon_out_au[2] = (double)moon_table[idx][2];
    return;
  }

  double r0[3] = {
    (double)moon_table[idx][0] * AU_KM,
    (double)moon_table[idx][1] * AU_KM,
    (double)moon_table[idx][2] * AU_KM
  };
  double v0[3] = {
    (double)moon_table[idx][3] * AU_KM / DAY_SEC,
    (double)moon_table[idx][4] * AU_KM / DAY_SEC,
    (double)moon_table[idx][5] * AU_KM / DAY_SEC
  };

  const double mu = MU_EARTH_KM;

  const double r0_norm = norm3(r0);
  const double v0_norm = norm3(v0);
  const double vr0 = dot3(r0, v0) / r0_norm; // radial

  // 1/semimajor axis
  const double alpha = 2.0 / r0_norm - (v0_norm * v0_norm) / mu;

  // init guess
  const double sqrt_mu = sqrt(mu);
  double chi;
  if (alpha != 0.0) chi = sqrt_mu * fabs(alpha) * dt;
  else              chi = sqrt_mu * dt / r0_norm;

  for (int it = 0; it < 6; ++it) {
    const double z = alpha * chi * chi;
    const double C = stumpff_C(z);
    const double S = stumpff_S(z);

    const double chi2 = chi * chi;
    const double chi3 = chi2 * chi;

    const double F =
      (r0_norm * vr0 / sqrt_mu) * chi2 * C +
      (1.0 - alpha * r0_norm)   * chi3 * S +
      r0_norm * chi -
      sqrt_mu * dt;

    const double dF =
      (r0_norm * vr0 / sqrt_mu) * chi * (1.0 - z * S) +
      (1.0 - alpha * r0_norm)   * chi2 * C +
      r0_norm;

    if (fabs(dF) < 1e-14) break;
    chi -= F / dF;
  }

  const double z = alpha * chi * chi;
  const double C = stumpff_C(z);
  const double S = stumpff_S(z);

  const double f = 1.0 - (chi * chi / r0_norm) * C;
  const double g = dt  - (chi * chi * chi / sqrt_mu) * S;

  double r[3] = {
    f * r0[0] + g * v0[0],
    f * r0[1] + g * v0[1],
    f * r0[2] + g * v0[2]
  };

  r_moon_out_au[0] = r[0] / AU_KM;
  r_moon_out_au[1] = r[1] / AU_KM;
  r_moon_out_au[2] = r[2] / AU_KM;
}


static inline void rk4_step_moon(double r_km[3], double v_km_s[3], double dt_sec) {
  // k1
  double a1[3]; accel_earth_moon(r_km, a1);
  double k1_r[3] = { v_km_s[0], v_km_s[1], v_km_s[2] };
  double k1_v[3] = { a1[0], a1[1], a1[2] };

  // k2
  double r2[3] = { r_km[0] + 0.5*dt_sec*k1_r[0], r_km[1] + 0.5*dt_sec*k1_r[1], r_km[2] + 0.5*dt_sec*k1_r[2] };
  double v2[3] = { v_km_s[0] + 0.5*dt_sec*k1_v[0], v_km_s[1] + 0.5*dt_sec*k1_v[1], v_km_s[2] + 0.5*dt_sec*k1_v[2] };
  double a2[3]; accel_earth_moon(r2, a2);
  double k2_r[3] = { v2[0], v2[1], v2[2] };
  double k2_v[3] = { a2[0], a2[1], a2[2] };

  // k3
  double r3[3] = { r_km[0] + 0.5*dt_sec*k2_r[0], r_km[1] + 0.5*dt_sec*k2_r[1], r_km[2] + 0.5*dt_sec*k2_r[2] };
  double v3[3] = { v_km_s[0] + 0.5*dt_sec*k2_v[0], v_km_s[1] + 0.5*dt_sec*k2_v[1], v_km_s[2] + 0.5*dt_sec*k2_v[2] };
  double a3[3]; accel_earth_moon(r3, a3);
  double k3_r[3] = { v3[0], v3[1], v3[2] };
  double k3_v[3] = { a3[0], a3[1], a3[2] };

  // k4
  double r4[3] = { r_km[0] + dt_sec*k3_r[0], r_km[1] + dt_sec*k3_r[1], r_km[2] + dt_sec*k3_r[2] };
  double v4[3] = { v_km_s[0] + dt_sec*k3_v[0], v_km_s[1] + dt_sec*k3_v[1], v_km_s[2] + dt_sec*k3_v[2] };
  double a4[3]; accel_earth_moon(r4, a4);
  double k4_r[3] = { v4[0], v4[1], v4[2] };
  double k4_v[3] = { a4[0], a4[1], a4[2] };

  // combine
  r_km[0] += (dt_sec / 6.0) * (k1_r[0] + 2.0*k2_r[0] + 2.0*k3_r[0] + k4_r[0]);
  r_km[1] += (dt_sec / 6.0) * (k1_r[1] + 2.0*k2_r[1] + 2.0*k3_r[1] + k4_r[1]);
  r_km[2] += (dt_sec / 6.0) * (k1_r[2] + 2.0*k2_r[2] + 2.0*k3_r[2] + k4_r[2]);

  v_km_s[0] += (dt_sec / 6.0) * (k1_v[0] + 2.0*k2_v[0] + 2.0*k3_v[0] + k4_v[0]);
  v_km_s[1] += (dt_sec / 6.0) * (k1_v[1] + 2.0*k2_v[1] + 2.0*k3_v[1] + k4_v[1]);
  v_km_s[2] += (dt_sec / 6.0) * (k1_v[2] + 2.0*k2_v[2] + 2.0*k3_v[2] + k4_v[2]);
}

static inline void accel_earth_moon(const double r_km[3], double a_km[3]) {
  double d2 = r_km[0]*r_km[0] + r_km[1]*r_km[1] + r_km[2]*r_km[2];
  double d  = sqrt(d2);
  double denom = (d*d*d) + 1e-12;
  double k = -MU_EARTH_KM / denom;
  a_km[0] = k * r_km[0];
  a_km[1] = k * r_km[1];
  a_km[2] = k * r_km[2];
}

static inline bool normalize3(double v[3]) {
  double n2 = v[0]*v[0] + v[1]*v[1] + v[2]*v[2];
  if (n2 < 1e-18) return false;
  double inv = 1.0 / sqrt(n2);
  v[0] *= inv;
  v[1] *= inv;
  v[2] *= inv;
  return true;
}

/////////// board stuf
//////////////////////////////////////////////////////////////////////////

#include <Wire.h>
#include <tinyNeoPixel.h>
#include "SAMM10Q.h"
GPSData_t gpsData; 

#define LED_PIN     16
#define LED_COUNT   9
#define LATCH_PIN   14
#define OFF_BUTTON  7

#define RED_R   10
#define RED_G   0
#define RED_B   0


tinyNeoPixel strip = tinyNeoPixel(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);

// flash
bool flashState   = false;
unsigned long lastFlash = 0;
const unsigned long FLASH_INTERVAL = 500;   // ms

/////////// led stuf
//////////////////////////////////////////////////////////////////////////

void clearLEDs() {for (int i = 0; i < LED_COUNT; i++) {strip.setPixelColor(i, 0, 0, 0);}}
void setLEDOverhead(int i, bool overhead, int r, int g, int b) {
  if (overhead) strip.setPixelColor(i, r, g, b);
  else          strip.setPixelColor(i, 0,   0,   0);
}

void updateLEDs() {

  clearLEDs();
  if (!gpsData.dataReceived || gpsData.numSV < 3) {
    if (flashState) {for (int i = 0; i < LED_COUNT; i++) {strip.setPixelColor(i, RED_R, RED_G, RED_B);}}    
    return;
  }

  // uint64_t unix_time = (uint64_t)gpsData.unixTime;
  int64_t unix_time = (int64_t)gpsData.unixTime;

  // for testing with python
  // const uint64_t SECONDS_212_DAYS = 60ULL * 60ULL * 24ULL * 212ULL;
  // unix_time += SECONDS_212_DAYS;

  double currentLat = gpsData.lat / 10000000.0;
  double currentLon = gpsData.lon / 10000000.0;

  int idx = closest_time_index(time_table, 8, unix_time);

  int64_t t0 = time_table[idx];
  int64_t dt_sec_i64 = (int64_t)unix_time - t0;
  double dt_days = (double)dt_sec_i64 / SECONDS_PER_DAY;


  double earth_theta = earth_rotation_angle_from_unix(unix_time);

  // Propagate earth
  double r_e[3] = { (double)sun_sv[idx][0], (double)sun_sv[idx][1], (double)sun_sv[idx][2] };
  double v_e[3] = { (double)sun_sv[idx][3], (double)sun_sv[idx][4], (double)sun_sv[idx][5] };
  euler_step(r_e, v_e, dt_days);

  double r_geo[3], r_ecef[3];


  propagate_planet_grcs(idx, dt_days, mercury_sv, r_geo, r_e);
  normalize3(r_geo); // normalize r_geo, its in units of AU it's small, this was a hard bug to find....
  eci_to_ecef_target_timestamp(r_geo, r_ecef, earth_theta);
  setLEDOverhead(0, flashState * is_visible_est(r_ecef, currentLat, currentLon),
                 MERCURY_R, MERCURY_G, MERCURY_B);

  propagate_planet_grcs(idx, dt_days, venus_sv, r_geo, r_e);
  normalize3(r_geo);
  eci_to_ecef_target_timestamp(r_geo, r_ecef, earth_theta);
  setLEDOverhead(1, flashState * is_visible_est(r_ecef, currentLat, currentLon),
                 VENUS_R, VENUS_G, VENUS_B);

  propagate_planet_grcs(idx, dt_days, mars_sv, r_geo, r_e);
  normalize3(r_geo);
  eci_to_ecef_target_timestamp(r_geo, r_ecef, earth_theta);
  setLEDOverhead(2, flashState * is_visible_est(r_ecef, currentLat, currentLon),
                 MARS_R, MARS_G, MARS_B);

  propagate_planet_grcs(idx, dt_days, jupiter_barycenter_sv, r_geo, r_e);
  normalize3(r_geo);
  eci_to_ecef_target_timestamp(r_geo, r_ecef, earth_theta);
  setLEDOverhead(3, flashState * is_visible_est(r_ecef, currentLat, currentLon),
                 JUPITER_R, JUPITER_G, JUPITER_B);

  propagate_planet_grcs(idx, dt_days, saturn_barycenter_sv, r_geo, r_e);
  normalize3(r_geo);
  eci_to_ecef_target_timestamp(r_geo, r_ecef, earth_theta);
  setLEDOverhead(4, flashState * is_visible_est(r_ecef, currentLat, currentLon),
                 SATURN_R, SATURN_G, SATURN_B);

  propagate_planet_grcs(idx, dt_days, uranus_barycenter_sv, r_geo, r_e);
  normalize3(r_geo);
  eci_to_ecef_target_timestamp(r_geo, r_ecef, earth_theta);
  setLEDOverhead(5, flashState * is_visible_est(r_ecef, currentLat, currentLon),
                 URANUS_R, URANUS_G, URANUS_B);

  propagate_planet_grcs(idx, dt_days, neptune_barycenter_sv, r_geo, r_e);
  normalize3(r_geo);
  eci_to_ecef_target_timestamp(r_geo, r_ecef, earth_theta);
  setLEDOverhead(6, flashState * is_visible_est(r_ecef, currentLat, currentLon),
                 NEPTUNE_R, NEPTUNE_G, NEPTUNE_B);

  propagate_planet_grcs(idx, dt_days, pluto_barycenter_sv, r_geo, r_e);
  normalize3(r_geo);
  eci_to_ecef_target_timestamp(r_geo, r_ecef, earth_theta);
  setLEDOverhead(7, flashState * is_visible_est(r_ecef, currentLat, currentLon),
                 PLUTO_R, PLUTO_G, PLUTO_B);

  propagate_moon_grcs_fine_kepler(idx, dt_sec_i64, moon_sv, r_geo);
  normalize3(r_geo);
  eci_to_ecef_target_timestamp(r_geo, r_ecef, earth_theta);
  setLEDOverhead(8, flashState * is_visible_est(r_ecef, currentLat, currentLon),
                 MOON_R, MOON_G, MOON_B);


  // propagate_moon_grcs_fine(idx, dt_sec_i64, moon_sv, r_geo);
  // normalize3(r_geo);
  // eci_to_ecef_target_timestamp(r_geo, r_ecef, earth_theta);
  // setLEDOverhead(8, flashState * is_visible_est(r_ecef, currentLat, currentLon),
  //                MOON_R, MOON_G, MOON_B);

  strip.show();
}

void hackerStartScreen() {
  const int loops = 20;
  const int frameDelay = 70;

  strip.clear();
  strip.show();

  for (int t = 0; t < loops; t++) {
    for (int i = 0; i < LED_COUNT; i++) {
      int g = random(5, 15);
      strip.setPixelColor(i, 0, g, 0);
    }

    int head = t % LED_COUNT;
    strip.setPixelColor(head, 0, 24, 0);

    strip.show();
    delay(frameDelay);

    if (t % 5 == 0) {
      for (int i = 0; i < LED_COUNT; i++) {
        strip.setPixelColor(
          i,
          random(5, 20),
          random(5, 20),
          random(5, 20)
        );
      }
      strip.show();
      delay(50);
    }

    int pulse = (t % 6) * 4;
    strip.setPixelColor(4, pulse, 0, pulse);
    strip.show();
    delay(frameDelay);
  }

  for (int k = 0; k < 2; k++) {
    for (int i = 0; i < LED_COUNT; i++) {
      strip.setPixelColor(i, 0, 20, 5);
    }
    strip.show();
    delay(150);

    strip.clear();
    strip.show();
    delay(150);
  }
  strip.clear();
  strip.show();
}



void setup() {


  pinMode(LATCH_PIN, OUTPUT);
  digitalWrite(LATCH_PIN, HIGH); 

  strip.begin();
  strip.show();

  Wire.begin();
  delay(1000);

  gpsData.dataReceived = false;
  gpsData.numSV = 0;

  // wit for GPS
  int attempts = 0;
  while (!gpsFound() && attempts < 10) {
    delay(300);
    attempts++;
  }


  if (attempts >= 10) {
    for (int i = 0; i < LED_COUNT; i++) {
      strip.setPixelColor(i, RED_R, RED_G, RED_B);
    }
    strip.show();
    while (1);
  }

  configureNAVPVT();
  delay(2000);

  hackerStartScreen();


}

void loop() {


  if (digitalRead(OFF_BUTTON) == LOW) {
    delay(50); 
    if (digitalRead(OFF_BUTTON) == LOW) {
      digitalWrite(LATCH_PIN, LOW);
      while (true) {
        // wait for power to cut
      }
    }
  }


  uint16_t avail = getBytesAvailable();

  if (avail > 8) {
    readUBXMessage();
  }

  if (millis() - lastFlash >= FLASH_INTERVAL) {
    lastFlash = millis();
    flashState = !flashState;
    updateLEDs();
    strip.show();
  }


  delay(50);
}

ephemeris_tables.h

C/C++
// AUTO-GENERATED FILE  DO NOT EDIT
// Ephemeris lookup tables (GCRS, Earth-centered)

#pragma once

#include <stdint.h>

#define EPHEMERIS_SAMPLES 8
#define EPHEMERIS_COLUMNS 6

const int64_t time_table[8] = {
  1767225600,
  1770854400,
  1774483200,
  1778112000,
  1781740800,
  1785369600,
  1788998400,
  1792627200
};

const float sun_sv[8][6] = {
  { 1.742952168e-01f, -8.879228234e-01f, -3.848968446e-01f, 1.720450260e-02f, 2.859745407e-03f, 1.239592675e-03f },
  { 7.873948216e-01f, -5.462082624e-01f, -2.367793471e-01f, 1.066171192e-02f, 1.264979504e-02f, 5.483536050e-03f },
  { 9.936382771e-01f, 7.945988327e-02f, 3.444101661e-02f, -1.221224898e-03f, 1.578411460e-02f, 6.841792259e-03f },
  { 6.996138096e-01f, 6.669433117e-01f, 2.891010344e-01f, -1.211037394e-02f, 1.100643165e-02f, 4.771576263e-03f },
  { 6.317487359e-02f, 9.303498864e-01f, 4.032905400e-01f, -1.689830422e-02f, 1.037285198e-03f, 4.491088039e-04f },
  { -6.039854884e-01f, 7.486671209e-01f, 3.245364130e-01f, -1.354425680e-02f, -9.328189306e-03f, -4.042850342e-03f },
  { -9.811158776e-01f, 2.086497098e-01f, 9.045208991e-02f, -3.609358566e-03f, -1.532396395e-02f, -6.643372588e-03f },
  { -8.769258857e-01f, -4.320504665e-01f, -1.872791946e-01f, 8.420635015e-03f, -1.384192612e-02f, -5.999573506e-03f }
};

const float moon_sv[8][6] = {
  { 9.645412792e-04f, 1.935964799e-03f, 1.070691971e-03f, -5.800898070e-04f, 2.216364373e-04f, 9.959529416e-05f },
  { -4.772208340e-04f, -2.328889677e-03f, -1.273929141e-03f, 5.529845366e-04f, -8.678845916e-05f, -2.591292650e-05f },
  { -3.250438604e-04f, 2.174365567e-03f, 1.158295781e-03f, -6.047159550e-04f, -4.661893763e-05f, -4.821577386e-05f },
  { 6.479381700e-04f, -2.323614666e-03f, -1.210430637e-03f, 5.403386313e-04f, 1.214328804e-04f, 8.690256072e-05f },
  { -1.465585781e-03f, 1.755741658e-03f, 8.596066036e-04f, -5.156788975e-04f, -2.952546638e-04f, -1.808125817e-04f },
  { 1.744159497e-03f, -1.815445954e-03f, -8.715497097e-04f, 4.193811910e-04f, 3.346354060e-04f, 1.995692437e-04f },
  { -2.204057062e-03f, 1.070239814e-03f, 4.519470967e-04f, -3.008191125e-04f, -4.632714845e-04f, -2.618523140e-04f },
  { 2.371959854e-03f, -9.981868789e-04f, -3.976827138e-04f, 2.060530969e-04f, 4.775166453e-04f, 2.621846797e-04f }
};

const float mercury_sv[8][6] = {
  { -4.104369879e-02f, -1.257843256e+00f, -5.601915121e-01f, 3.643174842e-02f, -6.834022235e-03f, -5.931565538e-03f },
  { 1.062130451e+00f, -3.843180537e-01f, -1.787703782e-01f, -9.749501944e-03f, 3.410912305e-02f, 1.906258240e-02f },
  { 7.077797651e-01f, -2.435207665e-01f, -1.084704101e-01f, 1.494452823e-02f, 1.865368569e-03f, -2.269055462e-03f },
  { 1.036048770e+00f, 7.350151539e-01f, 2.905976474e-01f, -2.273311839e-02f, 3.620269895e-02f, 1.933244802e-02f },
  { -2.790478766e-01f, 6.703414917e-01f, 2.998603284e-01f, -4.844259471e-03f, -1.668756641e-02f, -1.026884839e-02f },
  { -2.441948354e-01f, 7.148861289e-01f, 2.692026198e-01f, -1.488841139e-02f, 1.636843570e-02f, 9.823712520e-03f },
  { -1.361233592e+00f, 2.562854253e-02f, 3.207571805e-02f, 3.227943322e-03f, -3.623490408e-02f, -1.852270216e-02f },
  { -5.279079676e-01f, -5.648458004e-01f, -2.943904102e-01f, 1.491479110e-02f, 1.004681177e-02f, 6.088876165e-03f }
};

const float venus_sv[8][6] = {
  { 2.629914880e-01f, -1.544166684e+00f, -6.857994199e-01f, 3.714368492e-02f, 5.501963198e-03f, 1.167043345e-03f },
  { 1.483586073e+00f, -7.186988592e-01f, -3.584403694e-01f, 1.631101221e-02f, 3.038175777e-02f, 1.310501155e-02f },
  { 1.453569412e+00f, 5.979443192e-01f, 2.386465222e-01f, -1.687984727e-02f, 2.709124237e-02f, 1.292033866e-02f },
  { 3.598686159e-01f, 1.236015916e+00f, 5.666619539e-01f, -2.999785542e-02f, 1.764163608e-03f, 1.744478010e-03f },
  { -6.516706347e-01f, 8.405032158e-01f, 4.080871046e-01f, -1.475982182e-02f, -1.732626557e-02f, -7.949272171e-03f },
  { -8.075063229e-01f, 1.084998176e-01f, 4.935474321e-02f, 5.733605009e-03f, -1.413454022e-02f, -7.425196934e-03f },
  { -4.225521982e-01f, -2.031365335e-01f, -1.301778704e-01f, 9.212384932e-03f, -9.307927103e-04f, -9.780081455e-04f },
  { -2.316401005e-01f, -1.182118356e-01f, -8.688353002e-02f, -8.059090469e-04f, 2.311748452e-03f, 1.852902467e-03f }
};

const float mars_sv[8][6] = {
  { 5.146895647e-01f, -2.145644426e+00f, -9.709649086e-01f, 3.132436797e-02f, 7.123501506e-03f, 2.814435167e-03f },
  { 1.671723962e+00f, -1.516398549e+00f, -7.056344151e-01f, 2.200628817e-02f, 2.192665264e-02f, 9.432631545e-03f },
  { 2.254023314e+00f, -4.205077291e-01f, -2.288766652e-01f, 5.024842918e-03f, 2.854785137e-02f, 1.252775732e-02f },
  { 2.091573238e+00f, 7.341658473e-01f, 2.823916674e-01f, -1.216569263e-02f, 2.480435371e-02f, 1.110185962e-02f },
  { 1.323478818e+00f, 1.552977681e+00f, 6.548838019e-01f, -2.294147201e-02f, 1.329389866e-02f, 6.233928259e-03f },
  { 3.018189073e-01f, 1.818280816e+00f, 7.907131314e-01f, -2.406553738e-02f, -5.448129959e-04f, 2.696523443e-04f },
  { -5.758664608e-01f, 1.555323482e+00f, 6.972102523e-01f, -1.657368988e-02f, -1.100257412e-02f, -4.311593715e-03f },
  { -1.031886697e+00f, 9.985242486e-01f, 4.730715752e-01f, -4.975771066e-03f, -1.413780451e-02f, -5.773981102e-03f }
};

const float jupiter_barycenter_sv[8][6] = {
  { -1.519536495e+00f, 3.627354383e+00f, 1.591704130e+00f, 9.973803535e-03f, 8.640224696e-04f, 5.601781304e-04f },
  { -1.206818938e+00f, 3.876997232e+00f, 1.707669139e+00f, 3.592328401e-03f, 1.026295684e-02f, 4.632556345e-03f },
  { -1.293651223e+00f, 4.394422054e+00f, 1.939627528e+00f, -8.104946464e-03f, 1.301821321e-02f, 5.823818501e-03f },
  { -1.872457743e+00f, 4.858017921e+00f, 2.148118258e+00f, -1.878513768e-02f, 7.874692790e-03f, 3.591709305e-03f },
  { -2.784439802e+00f, 4.982466698e+00f, 2.209454060e+00f, -2.334192023e-02f, -2.445992548e-03f, -8.870662423e-04f },
  { -3.717002392e+00f, 4.647373676e+00f, 2.071404934e+00f, -1.973577403e-02f, -1.314769499e-02f, -5.529277958e-03f },
  { -4.348541260e+00f, 3.940153122e+00f, 1.771845579e+00f, -9.529049508e-03f, -1.946357824e-02f, -8.273623884e-03f },
  { -4.486959457e+00f, 3.119149446e+00f, 1.422737002e+00f, 2.791308099e-03f, -1.828474738e-02f, -7.766852621e-03f }
};

const float saturn_barycenter_sv[8][6] = {
  { 9.681663513e+00f, -4.994135201e-01f, -6.338292956e-01f, 1.674159989e-02f, 7.996461354e-03f, 3.381074872e-03f },
  { 1.027244473e+01f, 5.787989125e-02f, -3.957130909e-01f, 1.006169803e-02f, 1.777900755e-02f, 7.627824321e-03f },
  { 1.045060635e+01f, 8.987553120e-01f, -3.439926729e-02f, -1.958517358e-03f, 2.090263553e-02f, 8.987574838e-03f },
  { 1.012273026e+01f, 1.700943232e+00f, 3.103949428e-01f, -1.298500970e-02f, 1.611110009e-02f, 6.917550229e-03f },
  { 9.446669579e+00f, 2.178416252e+00f, 5.147034526e-01f, -1.791024953e-02f, 6.124947220e-03f, 2.593969461e-03f },
  { 8.734120369e+00f, 2.210023642e+00f, 5.259960294e-01f, -1.469346974e-02f, -4.260656890e-03f, -1.900391653e-03f },
  { 8.305838585e+00f, 1.882378101e+00f, 3.818273842e-01f, -4.895741120e-03f, -1.027978212e-02f, -4.504651297e-03f },
  { 8.353122711e+00f, 1.452987552e+00f, 1.938207746e-01f, 6.997304969e-03f, -8.824359626e-03f, -3.865946550e-03f }
};

const float uranus_barycenter_sv[8][6] = {
  { 1.005497169e+01f, 1.455175686e+01f, 6.237381935e+00f, 1.377814356e-02f, 4.500451963e-03f, 2.006680239e-03f },
  { 1.052383137e+01f, 1.496182156e+01f, 6.417477131e+00f, 7.218939718e-03f, 1.426410954e-02f, 6.239303853e-03f },
  { 1.058515072e+01f, 1.565472698e+01f, 6.720197678e+00f, -4.680149723e-03f, 1.737186126e-02f, 7.586157415e-03f },
  { 1.014552498e+01f, 1.630833244e+01f, 7.005878448e+00f, -1.558517013e-02f, 1.256748103e-02f, 5.504475906e-03f },
  { 9.362815857e+00f, 1.663673973e+01f, 7.150608540e+00f, -2.038867958e-02f, 2.571536228e-03f, 1.170490519e-03f },
  { 8.548727036e+00f, 1.651893616e+01f, 7.101912022e+00f, -1.704999804e-02f, -7.820792496e-03f, -3.333012341e-03f },
  { 8.024021149e+00f, 1.604166985e+01f, 6.897400379e+00f, -7.130291779e-03f, -1.384356152e-02f, -5.945140496e-03f },
  { 7.980004787e+00f, 1.546258163e+01f, 6.648752213e+00f, 4.884717520e-03f, -1.238868758e-02f, -5.313022528e-03f }
};

const float neptune_barycenter_sv[8][6] = {
  { 3.004643822e+01f, -1.342440248e-01f, -8.200629354e-01f, 1.712276414e-02f, 5.781460553e-03f, 2.437465591e-03f },
  { 3.065581322e+01f, 3.301571012e-01f, -6.216380596e-01f, 1.056613494e-02f, 1.557077002e-02f, 6.681447849e-03f },
  { 3.085775375e+01f, 1.078485250e+00f, -3.001068532e-01f, -1.330606872e-03f, 1.870425791e-02f, 8.039702661e-03f },
  { 3.055884552e+01f, 1.788601041e+00f, 4.866841249e-03f, -1.223350596e-02f, 1.392570138e-02f, 5.969464779e-03f },
  { 2.991694641e+01f, 2.174608469e+00f, 1.693712026e-01f, -1.703512110e-02f, 3.955663648e-03f, 1.646964811e-03f },
  { 2.924375153e+01f, 2.115490913e+00f, 1.409313232e-01f, -1.369477250e-02f, -6.410674658e-03f, -2.845011186e-03f },
  { 2.886001205e+01f, 1.697996855e+00f, -4.284161702e-02f, -3.773627104e-03f, -1.240737084e-02f, -5.445572082e-03f },
  { 2.895701218e+01f, 1.179771543e+00f, -2.702671885e-01f, 8.242589422e-03f, -1.092634629e-02f, -4.801847506e-03f }
};

const float pluto_barycenter_sv[8][6] = {
  { 1.940164185e+01f, -2.714402199e+01f, -1.437037659e+01f, 1.991874538e-02f, 4.141388927e-03f, 8.197210263e-04f },
  { 2.012862778e+01f, -2.674833298e+01f, -1.423981380e+01f, 1.337065361e-02f, 1.393841114e-02f, 5.067424849e-03f },
  { 2.044854355e+01f, -2.606839371e+01f, -1.398599339e+01f, 1.482431893e-03f, 1.707963832e-02f, 6.429419853e-03f },
  { 2.026797104e+01f, -2.542634964e+01f, -1.374857616e+01f, -9.411965497e-03f, 1.230884250e-02f, 4.362941254e-03f },
  { 1.974476624e+01f, -2.510809135e+01f, -1.365147209e+01f, -1.420509815e-02f, 2.346591093e-03f, 4.421894118e-05f },
  { 1.919061279e+01f, -2.523463631e+01f, -1.374715233e+01f, -1.085628569e-02f, -8.011937141e-03f, -4.443960730e-03f },
  { 1.892626190e+01f, -2.571922874e+01f, -1.399800301e+01f, -9.266966372e-04f, -1.400079951e-02f, -7.040707394e-03f },
  { 1.914299965e+01f, -2.630421638e+01f, -1.429234219e+01f, 1.109794434e-02f, -1.251191739e-02f, -6.393149961e-03f }
};

// planet colors

#define MERCURY_R  18
#define MERCURY_G  18
#define MERCURY_B  18    // gray

#define VENUS_R   30
#define VENUS_G   24
#define VENUS_B    6    // warm yellow

#define MARS_R    30
#define MARS_G     6
#define MARS_B     0    // red/orange

#define JUPITER_R  24
#define JUPITER_G  15
#define JUPITER_B   6    // tan

#define SATURN_R   24
#define SATURN_G   21
#define SATURN_B    9    // pale gold

#define URANUS_R    6
#define URANUS_G   24
#define URANUS_B   24    // cyan

#define NEPTUNE_R   3
#define NEPTUNE_G   6
#define NEPTUNE_B  30    // deep blue

#define PLUTO_R    15
#define PLUTO_G    12
#define PLUTO_B     9    // brown/gray

#define MOON_R     24
#define MOON_G     24
#define MOON_B     24    // bright gray / white

planet_overhead_est.py

Python
import numpy as np
from datetime import datetime, timezone
from planet_overhead import is_planet_overhead

SECONDS_PER_DAY = 86400.0
JD_UNIX_EPOCH = 2440587.5

G = 2.9591220828559093e-4  # AU^3 / day^2
MASS_SUN = 1.0
MU_SUN = G * MASS_SUN

MU_EARTH = 398600.4418  # km^3 / s^2
AU_KM = 149597870.700
DAY_SEC = 86400.0
MOON_DT = 300.0


def accel_sun(r):
    d = np.linalg.norm(r)
    return -MU_SUN * r / (d ** 3 + 1e-12)


def euler_step(r, v, dt_days):
    a = accel_sun(r)
    return r + v * dt_days, v + a * dt_days


def accel_earth_moon(r_km):
    d = np.linalg.norm(r_km)
    return -MU_EARTH * r_km / (d ** 3 + 1e-12)


def rk4_step(r, v, dt, accel_fn):
    def f(r, v):
        return v, accel_fn(r)

    k1r, k1v = f(r, v)
    k2r, k2v = f(r + 0.5 * dt * k1r, v + 0.5 * dt * k1v)
    k3r, k3v = f(r + 0.5 * dt * k2r, v + 0.5 * dt * k2v)
    k4r, k4v = f(r + dt * k3r, v + dt * k3v)

    r_new = r + (dt / 6) * (k1r + 2 * k2r + 2 * k3r + k4r)
    v_new = v + (dt / 6) * (k1v + 2 * k2v + 2 * k3v + k4v)
    return r_new, v_new


def propagate_planet_grcs(idx, unix_time, target_unix, planet_table, sun_data):
    dt_days = (target_unix - unix_time) / SECONDS_PER_DAY

    r_p = planet_table[idx, :3] + sun_data[idx, :3]
    v_p = planet_table[idx, 3:6] + sun_data[idx, 3:6]

    r_e = sun_data[idx, :3]
    v_e = sun_data[idx, 3:6]

    if abs(dt_days) < 1e-9:
        return planet_table[idx, :3].copy()

    r_p, v_p = euler_step(r_p, v_p, dt_days)
    r_e, v_e = euler_step(r_e, v_e, dt_days)

    return r_p - r_e


def propagate_moon_grcs_fine(idx, unix_time, target_unix, moon_table):
    dt_total = target_unix - unix_time
    if abs(dt_total) < 1e-6:
        return moon_table[idx, :3].copy()

    r = moon_table[idx, :3] * AU_KM
    v = moon_table[idx, 3:] * AU_KM / DAY_SEC

    sign = np.sign(dt_total)
    remaining = abs(dt_total)

    while remaining > 0:
        h = min(MOON_DT, remaining) * sign
        r, v = rk4_step(r, v, h, accel_earth_moon)
        remaining -= abs(h)

    return r / AU_KM


def earth_rotation_angle_from_unix(t_unix):
    JD = JD_UNIX_EPOCH + t_unix / SECONDS_PER_DAY
    d = JD - 2451545.0
    theta = 2 * np.pi * (0.7790572732640 + 1.00273781191135448 * d)
    return theta % (2 * np.pi)


def eci_to_ecef(r_eci, t_unix):
    theta = earth_rotation_angle_from_unix(t_unix)
    c, s = np.cos(theta), np.sin(theta)
    R = np.array([[c, s, 0], [-s, c, 0], [0, 0, 1]])
    return R @ r_eci


def latlon_to_ecef(lat, lon, radius=6371.0):
    lat, lon = np.radians(lat), np.radians(lon)
    return np.array([
        radius * np.cos(lat) * np.cos(lon),
        radius * np.cos(lat) * np.sin(lon),
        radius * np.sin(lat),
    ])


def is_visible_est(planet_ecef, user_lat, user_lon):
    user = latlon_to_ecef(user_lat, user_lon)
    return np.dot(planet_ecef, user) > 0


def closest_time_index(times, target):
    return np.searchsorted(times, target, side="left").clip(0, len(times) - 1)


def check_planet_visibility(
        planet_name: str,
        utc_timestamp,
        user_lat: float,
        user_lon: float,
        data_dir: str = "data",
):
    """
    Args:
        planet_name: "mars", "moon", "venus", etc
        utc_timestamp:
            - unix seconds (float or int), OR
            - timezone-aware datetime in UTC
        user_lat, user_lon: degrees

    Returns:
        dict with:
            est_overhead   -> bool (your estimator)
            true_overhead  -> bool (skyfield)
            match          -> bool
    """

    if isinstance(utc_timestamp, datetime):
        if utc_timestamp.tzinfo is None:
            raise ValueError("datetime must be timezone-aware UTC")
        unix_time = utc_timestamp.timestamp()
        dt_utc = utc_timestamp.astimezone(timezone.utc)
    else:
        unix_time = float(utc_timestamp)
        dt_utc = datetime.fromtimestamp(unix_time, tz=timezone.utc)

    planet_table = np.load(f"{data_dir}/{planet_name}_sv_table.npy")
    time_table = np.load(f"{data_dir}/time_table.npy")
    sun_data = np.load(f"{data_dir}/sun_sv_table.npy")

    idx = closest_time_index(time_table, unix_time)

    if planet_name.lower() == "moon":
        r_eci = propagate_moon_grcs_fine(idx, time_table[idx], unix_time, planet_table)
    else:
        r_eci = propagate_planet_grcs(idx, time_table[idx], unix_time, planet_table, sun_data)

    planet_ecef = eci_to_ecef(r_eci, unix_time)
    est_overhead = is_visible_est(planet_ecef, user_lat, user_lon)

    true_overhead = is_planet_overhead(
        planet_name.lower(),
        dt_utc,
        user_lat,
        user_lon,
    )

    return {
        "planet": planet_name.lower(),
        "est_overhead": bool(est_overhead),
        "true_overhead": bool(true_overhead),
        "match": bool(est_overhead == true_overhead),
    }

testMyCity.py

Python
from planet_overhead_est import check_planet_visibility, closest_time_index, earth_rotation_angle_from_unix, \
    SECONDS_PER_DAY
from datetime import datetime, timezone
import numpy as np

PLANETS = [
    "mercury",
    "venus",
    "mars",
    "jupiter barycenter",
    "saturn barycenter",
    "uranus barycenter",
    "neptune barycenter",
    "pluto barycenter",
    "moon"
]

now_utc = datetime.now(timezone.utc).timestamp()
lat = 0
lon = 0

planet_name = PLANETS[0]
time_table = np.load(f"data/time_table.npy")
sun_data = np.load(f"data/sun_sv_table.npy")
idx = closest_time_index(time_table, now_utc)
print(idx)
print(now_utc - time_table[idx])
print(earth_rotation_angle_from_unix(now_utc))
print((now_utc - time_table[idx]) / SECONDS_PER_DAY)

for name in PLANETS:
    name = name.replace(' ', '_')
    result = check_planet_visibility(name, now_utc, lat, lon)
    print(result)

build_ephemeris_tables.py

Python
"""
build sparse ephemeris lookup table builder (GCRS, Earth-centered!).
"""

from datetime import datetime, timedelta, timezone
import numpy as np
from skyfield.api import load
from tqdm import tqdm

BODIES = [
    "sun",
    "moon",
    "mercury",
    "venus",
    "mars",
    "jupiter BARYCENTER",
    "saturn BARYCENTER",
    "uranus BARYCENTER",
    "neptune BARYCENTER",
    "pluto BARYCENTER",
]

START_YEAR = 2026
START_MONTH = 1
START_DAY = 1
START_HOUR = 0
START_MINUTE = 0
START_SECOND = 0

YEARS = 1

# sample spacing (seconds)
RESOLUTION_SECONDS = 60 * 60 * 24 * 7 * 6  # 6 weeks

OUTPUT_DIR = "data"
C_HEADER_PATH = f"{OUTPUT_DIR}/ephemeris_tables.h"


def c_identifier(name: str) -> str:
    return name.lower().replace(" ", "_")


def write_c_array_1d(f, ctype, name, array):
    f.write(f"const {ctype} {name}[{len(array)}] = {{\n")
    for i, v in enumerate(array):
        if ctype == "int64_t":
            f.write(f"  {int(v)}")
        else:
            f.write(f"  {float(v):.9e}f")
        if i < len(array) - 1:
            f.write(",")
        f.write("\n")
    f.write("};\n\n")


def write_c_array_2d(f, ctype, name, array):
    rows, cols = array.shape
    f.write(f"const {ctype} {name}[{rows}][{cols}] = {{\n")
    for r in range(rows):
        f.write("  { ")
        for c in range(cols):
            v = float(array[r, c])
            f.write(f"{v:.9e}f")
            if c < cols - 1:
                f.write(", ")
        f.write(" }")
        if r < rows - 1:
            f.write(",")
        f.write("\n")
    f.write("};\n\n")


ts = load.timescale()
eph = load("de421.bsp")
earth = eph["earth"]

start_dt = datetime(
    START_YEAR,
    START_MONTH,
    START_DAY,
    START_HOUR,
    START_MINUTE,
    START_SECOND,
    tzinfo=timezone.utc,
)

num_samples = int(YEARS * 365 * 24 * 3600 // RESOLUTION_SECONDS)

datetimes = [
    start_dt + timedelta(seconds=i * RESOLUTION_SECONDS)
    for i in range(num_samples)
]

times = ts.from_datetimes(datetimes)

unix_time = np.array(
    [int(dt.timestamp()) for dt in datetimes],
    dtype=np.int64
)

np.save(f"{OUTPUT_DIR}/time_table.npy", unix_time)

tables = {}

for body_name in tqdm(BODIES):
    print(f"Building table for {body_name} ...")

    body = eph[body_name]
    obs = earth.at(times).observe(body)

    table = np.empty((num_samples, 6), dtype=np.float32)
    table[:, 0:3] = obs.position.au.T
    table[:, 3:6] = obs.velocity.au_per_d.T

    key = c_identifier(body_name)
    tables[key] = table

    np.save(f"{OUTPUT_DIR}/{key}_sv_table.npy", table)

print("ephemeris complete :)")

print("Writing C header...")

with open(C_HEADER_PATH, "w") as f:
    f.write("// AUTO-GENERATED FILE  DO NOT EDIT\n")
    f.write("// Ephemeris lookup tables (GCRS, Earth-centered)\n\n")

    f.write("#pragma once\n\n")
    f.write("#include <stdint.h>\n\n")

    f.write(f"#define EPHEMERIS_SAMPLES {num_samples}\n")
    f.write(f"#define EPHEMERIS_COLUMNS 6\n\n")

    write_c_array_1d(
        f,
        ctype="int64_t",
        name="time_table",
        array=unix_time,
    )

    for name, table in tables.items():
        write_c_array_2d(
            f,
            ctype="float",
            name=f"{name}_sv",
            array=table,
        )

print(f"C .h written done")

planet_overhead.py

Python
from datetime import datetime, timezone
from skyfield.api import load, wgs84

ts = load.timescale()
eph = load("de421.bsp")
earth = eph["earth"]


def is_planet_overhead(
        planet_name: str,
        utc_datetime: datetime,
        latitude_deg: float,
        longitude_deg: float,
) -> bool:
    if utc_datetime.tzinfo is None:
        raise ValueError("utc_datetime must be timezone-aware UTC")
    planet = eph[planet_name.lower()]
    observer = wgs84.latlon(latitude_deg, longitude_deg)
    t = ts.from_datetime(utc_datetime)
    astrometric = (earth + observer).at(t).observe(planet)
    apparent = astrometric.apparent()
    alt, az, distance = apparent.altaz()
    # above horizon
    # print(f"{planet_name} is {alt.degrees} over horizon")
    return alt.degrees > 0.0

customWhatsUp.py

Python
import numpy as np
import random
from planet_overhead import is_planet_overhead
from datetime import datetime, timezone
from tqdm import tqdm

random.seed(42)

names = ["sun", "moon", "mercury", "venus", "mars",
         "jupiter BARYCENTER", "saturn BARYCENTER", "uranus BARYCENTER",
         "neptune BARYCENTER", "pluto BARYCENTER"]

test_planet = "mercury"
USER_LAT = 0
USER_LON = 0
trials = 1000

"""
FIRST RUN build_ephemeris_tables.py

This is a code script to test your tables for accuracy 

what this code does:
    we have saved sparse tables of inertial planet (and moon) state vectors (position and velocity).
    given a random user time, lat, and lon, we grab the closest planet state vector, sun vector,
    and unix timestamp.

    using these vectors we approximate the planets position, usually using a simple linear
    approximation and the sun. the sun has the biggest effect on planet motion

    we then see where these approximated planet locations (xyz) are relative to Earth, and then
    spin them around based on Earths rotation. (ECI to ECEF)

    once we have our planet locations spun to match the rotating Earth, confirming whether its
    overhead is easy. lat / lon are also stuck on the spinning Earth.

"""

SECONDS_PER_DAY = 86400.0  # sidereal
JD_UNIX_EPOCH = 2440587.5  # julian date at unix epoch
G = 2.9591220828559093e-4  # units of au^3 / (day^2)
MASS_EARTH = 3.003e-06  # solar mass
MASS_MOON_RATIO = 0.0123000
MASS_SUN = 1.0
SOLAR_MASS_FRACTION = {
    "sun": 1.0,
    "mercury": 1.66e-7,
    "venus": 2.45e-6,
    "earth": 3.03e-6,
    "mars": 3.23e-7,
    "jupiter barycenter": 9.55e-4,
    "saturn barycenter": 2.86e-5,
    "uranus barycenter": 4.37e-6,
    "neptune barycenter": 5.15e-5,
    "pluto barycenter": 7.340e-9
}

MU_SUN = G * MASS_SUN


def accel_sun(r):
    """f = ma => a = F / m => a = r * G * M_sun / |r|^3"""
    # thank you, newton
    d = np.linalg.norm(r)
    return -MU_SUN * r / (d ** 3 + 1e-12)


def euler_step(r, v, dt_days):
    """euler integration  linear."""
    a = accel_sun(r)
    r_new = r + v * dt_days
    v_new = v + a * dt_days
    return r_new, v_new


def propagate_planet_grcs(idx, unix_time, target_unix, planet_table):
    """
    Take one big linear step to move the planet to where it should ~be.

    We need the planet vector to have Earth at its origin, so we also move Earth with a big step,
    then subtract Earths new position from the propagated planet position. This effectively
    slides the frame back to where it should be.

    The convenient thing about the GCRS frame is that Earth, by definition, is always at
    xyz = [0, 0, 0] and d(xyz)/dt = [0, 0, 0] (at the time the table was made),
    so we dont really need Earths state vector to move it  we just use the sun table.

    """

    dt_sec = target_unix - unix_time
    dt_days = dt_sec / SECONDS_PER_DAY

    au_xyz = planet_table[idx, 0:3]
    au_per_d = planet_table[idx, 3:6]

    if abs(dt_days) < 1e-9:
        return au_xyz.copy()

    r_p = au_xyz.copy() + sun_data[idx, 0:3]
    v_p = au_per_d.copy() + sun_data[idx, 3:6]

    # earth helio = sun wrt earth
    r_e = sun_data[idx, 0:3].copy()
    v_e = sun_data[idx, 3:6].copy()

    r_p, v_p = euler_step(r_p, v_p, dt_days)
    r_e, v_e = euler_step(r_e, v_e, dt_days)

    # r_p, v_p = rk4_step_sun_planet_system(r_p, v_p, dt_days)
    # r_e, v_e = rk4_step_sun_planet_system(r_e, v_e, dt_days)

    r_geo = r_p - r_e
    return r_geo


### moon stuff
MU_EARTH = 398600.4418


def accel_earth_moon(r_km):
    """f = ma => a = F / m => a = r * G * M_sun / |r|^3"""
    d = np.linalg.norm(r_km)
    return -MU_EARTH * r_km / (d ** 3 + 1e-12)


def euler_step_earth_moon(r, v, dt_days):
    """euler integration  linear."""
    a = accel_earth_moon(r)
    # v_new = v + a * dt_days
    # r_new = r + v_new * dt_days
    r_new = r + v * dt_days
    v_new = v + a * dt_days
    return r_new, v_new


AU_KM = 149597870.700
DAY_SEC = 86400.0
MOON_DT = 100.0  # the moon needs special care  smaller time steps and higher-order integration


def rk4_step_moon_earth_system(r, v, dt):
    """
    rk4 integrator for approximating 2nd-order differential equations.
    """

    def f(r, v):
        return v, accel_earth_moon(r)

    k1_r, k1_v = f(r, v)
    k2_r, k2_v = f(
        r + 0.5 * dt * k1_r,
        v + 0.5 * dt * k1_v
    )
    k3_r, k3_v = f(
        r + 0.5 * dt * k2_r,
        v + 0.5 * dt * k2_v
    )
    k4_r, k4_v = f(
        r + dt * k3_r,
        v + dt * k3_v
    )
    r_new = r + (dt / 6.0) * (k1_r + 2 * k2_r + 2 * k3_r + k4_r)
    v_new = v + (dt / 6.0) * (k1_v + 2 * k2_v + 2 * k3_v + k4_v)
    return r_new, v_new


def rk4_step_sun_planet_system(r, v, dt):
    """
    rk4 integrator for approximating 2nd-order differential equations.
    """

    def f(r, v):
        return v, accel_sun(r)

    k1_r, k1_v = f(r, v)
    k2_r, k2_v = f(
        r + 0.5 * dt * k1_r,
        v + 0.5 * dt * k1_v
    )
    k3_r, k3_v = f(
        r + 0.5 * dt * k2_r,
        v + 0.5 * dt * k2_v
    )
    k4_r, k4_v = f(
        r + dt * k3_r,
        v + dt * k3_v
    )
    r_new = r + (dt / 6.0) * (k1_r + 2 * k2_r + 2 * k3_r + k4_r)
    v_new = v + (dt / 6.0) * (k1_v + 2 * k2_v + 2 * k3_v + k4_v)
    return r_new, v_new


def propagate_moon_grcs_fine_old(idx, unix_time, target_unix, moon_table):
    """
    propagate the Moon around Earth using rk4.
    Earth's gravity completely dominates for the moon compared to everything else.
    """

    au_xyz = moon_table[idx][0:3]
    au_per_d = moon_table[idx][3:]

    dt_total = target_unix - unix_time
    if abs(dt_total) < 1e-6:
        return au_xyz.copy()

    r = au_xyz * AU_KM
    v = au_per_d * AU_KM / DAY_SEC

    sign = np.sign(dt_total)
    dt_remaining = abs(dt_total)

    dt = MOON_DT

    while dt_remaining > 0.0:
        h = min(dt, dt_remaining) * sign
        # r, v = euler_step_earth_moon(r, v, h)
        r, v = rk4_step_moon_earth_system(r, v, h)
        dt_remaining -= abs(h)

    return r / AU_KM


def propagate_moon_grcs_fine_kelper(idx, unix_time, target_unix, moon_table):
    """
    Closed-form 2-body Kepler propagation of the Moon around Earth.
    Assumes Earth-only gravity (MU_EARTH), which matches your model.
    """

    # --- early exit ---
    dt = target_unix - unix_time
    if abs(dt) < 1e-6:
        return moon_table[idx, 0:3].copy()

    # --- initial state (km, km/s) ---
    r0 = moon_table[idx, 0:3] * AU_KM
    v0 = moon_table[idx, 3:6] * AU_KM / DAY_SEC
    dt = float(dt)

    r0_norm = np.linalg.norm(r0)
    v0_norm = np.linalg.norm(v0)
    vr0 = np.dot(r0, v0) / r0_norm

    mu = MU_EARTH

    # reciprocal of semi-major axis
    alpha = 2.0 / r0_norm - (v0_norm ** 2) / mu

    # --- solve universal Kepler equation for chi ---
    chi = np.sqrt(mu) * abs(alpha) * dt if alpha != 0 else np.sqrt(mu) * dt / r0_norm

    def stumpff_C(z):
        if z > 0:
            s = np.sqrt(z)
            return (1.0 - np.cos(s)) / z
        elif z < 0:
            s = np.sqrt(-z)
            return (np.cosh(s) - 1.0) / (-z)
        else:
            return 0.5

    def stumpff_S(z):
        if z > 0:
            s = np.sqrt(z)
            return (s - np.sin(s)) / (s ** 3)
        elif z < 0:
            s = np.sqrt(-z)
            return (np.sinh(s) - s) / (s ** 3)
        else:
            return 1.0 / 6.0

    # Newton solve
    for _ in range(6):
        z = alpha * chi * chi
        C = stumpff_C(z)
        S = stumpff_S(z)

        F = (
                r0_norm * vr0 / np.sqrt(mu) * chi * chi * C
                + (1.0 - alpha * r0_norm) * chi ** 3 * S
                + r0_norm * chi
                - np.sqrt(mu) * dt
        )

        dF = (
                r0_norm * vr0 / np.sqrt(mu) * chi * (1.0 - z * S)
                + (1.0 - alpha * r0_norm) * chi * chi * C
                + r0_norm
        )

        chi -= F / dF

    # --- f and g functions ---
    z = alpha * chi * chi
    C = stumpff_C(z)
    S = stumpff_S(z)

    f = 1.0 - (chi * chi / r0_norm) * C
    g = dt - (chi ** 3 / np.sqrt(mu)) * S

    r = f * r0 + g * v0

    # return in AU
    return r / AU_KM


###########################################################

## earth rotate

def eci_to_ecef_target_timestamp(r_eci, t_unix_start, t_unix_target):
    """
    Our GCRS vectors dont care about the spinning Earth (if they did,
    classical physics approximations wouldnt work).

    GCRS shares the same origin (0, 0, 0) with ECEF, so we just rotate the vectors to match
    Earths current spin.

    IMHO I cant believe how accurate earth_rotation_angle_from_unix is. This was the part I was most worried about,
    but grabbing some numbers online actually worked surprisingly well.
    """
    theta = earth_rotation_angle_from_unix(t_unix_start + (t_unix_target - t_unix_start))
    c = np.cos(theta)
    s = np.sin(theta)
    R = np.array([
        [c, s, 0.0],
        [-s, c, 0.0],
        [0.0, 0.0, 1.0]
    ])
    return R @ r_eci


def earth_rotation_angle_from_unix(t_unix):
    # unix seconds to Julian Date
    JD = JD_UNIX_EPOCH + t_unix / SECONDS_PER_DAY

    d = JD - 2451545.0

    theta = 2.0 * np.pi * (
            0.7790572732640 +
            1.00273781191135448 * d
    )

    return theta % (2.0 * np.pi)


def latlon_to_ecef(lat, lon, radius=6371.0):
    # classic equation - z axis goes thru the north pole
    lat_rad = np.radians(lat)
    lon_rad = np.radians(lon)
    x = radius * np.cos(lat_rad) * np.cos(lon_rad)
    y = radius * np.cos(lat_rad) * np.sin(lon_rad)
    z = radius * np.sin(lat_rad)
    return np.array([x, y, z])


def is_visible_est(planet_ecef, user_lat, user_lon):
    # once agin the dot product is king
    user_ecef = latlon_to_ecef(user_lat, user_lon)
    return np.dot(planet_ecef, user_ecef) > 0


#########################

def closest_time_index(unix_time, n, target):
    # given our sparse lookup tables (ephemeris), find the closest saved timestamp
    if target <= unix_time[0]:
        return 0
    if target >= unix_time[n - 1]:
        return n - 1
    left = 0
    right = n - 1
    while left <= right:
        mid = (left + right) // 2
        if unix_time[mid] == target:
            return mid
        elif unix_time[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    if (target - unix_time[right]) <= (unix_time[left] - target):
        return right
    else:
        return left


filename = f"data/{test_planet}_sv_table.npy"
test_planet_table = np.load(filename)
time_table = np.load("data/time_table.npy")
sun_data = np.load("data/sun_sv_table.npy")

accuracy = 0

for i in tqdm(range(trials)):

    USER_TIMESTAMP = random.uniform(time_table[0], time_table[-1])

    idx = closest_time_index(time_table, len(time_table), USER_TIMESTAMP)

    if test_planet == "moon":
        r_prop = propagate_moon_grcs_fine_old(idx, time_table[idx], USER_TIMESTAMP, test_planet_table)
    else:
        r_prop = propagate_planet_grcs(idx, time_table[idx], USER_TIMESTAMP, test_planet_table)

    itrs_test_position_est = eci_to_ecef_target_timestamp(r_prop, time_table[idx], USER_TIMESTAMP)
    person_lat_lon_est = latlon_to_ecef(USER_LAT, USER_LON)
    est_overhead = is_visible_est(itrs_test_position_est, USER_LAT, USER_LON)  ## bool

    ## skyfield truth
    dt_utc = datetime.fromtimestamp(USER_TIMESTAMP, tz=timezone.utc)
    planet_name_sf = test_planet.lower()

    true_overhead = is_planet_overhead(
        planet_name_sf,
        dt_utc,
        USER_LAT,
        USER_LON,
    )

    accuracy += (est_overhead == true_overhead)

print(f"accuracy {100 * accuracy / trials}%")

Credits

mars91
1 project • 0 followers

Comments