/* * lagrange.h -- Circular restricted three-body problem (CR3BP) solver * * Computes the five Lagrange equilibrium points for any gravitational * two-body system. The solver is pure C with no PostgreSQL dependency, * no global state, and no memory allocation. * * The CR3BP uses the mass parameter mu = M_secondary / (M_primary + M_secondary). * In the co-rotating frame normalized to unit separation, L1/L2/L3 lie * on the x-axis and L4/L5 form equilateral triangles. * * L1/L2/L3 positions come from Newton-Raphson on the quintic * equilibrium polynomial. L4/L5 are exact analytic. * * References: * Szebehely V., "Theory of Orbits" (1967), Academic Press * Murray & Dermott, "Solar System Dynamics" (1999), Cambridge */ #ifndef PG_ORRERY_LAGRANGE_H #define PG_ORRERY_LAGRANGE_H #include /* ── Lagrange point identifiers ────────────────────────── */ #define LAGRANGE_L1 1 #define LAGRANGE_L2 2 #define LAGRANGE_L3 3 #define LAGRANGE_L4 4 #define LAGRANGE_L5 5 /* ── Sun-planet mass ratios ────────────────────────────── */ /* * GM_sun / GM_planet ratios. Convert to CR3BP mu via: * mu = 1.0 / (1.0 + ratio) * * Sources: IAU 2012 nominal masses, JPL DE441 constants. * The Earth ratio includes the Moon (Earth+Moon system barycenter). */ #define SUN_MERCURY_RATIO 6023682.155 #define SUN_VENUS_RATIO 408523.7187 #define SUN_EARTH_RATIO 332946.0487 /* Earth+Moon system */ #define SUN_MARS_RATIO 3098703.59 #define SUN_JUPITER_RATIO 1047.348644 #define SUN_SATURN_RATIO 3497.9018 #define SUN_URANUS_RATIO 22902.98 #define SUN_NEPTUNE_RATIO 19412.26 /* ── Earth-Moon mass ratio ─────────────────────────────── */ /* * M_earth / M_moon. From DE441 EMRAT constant. * mu = 1.0 / (1.0 + EARTH_MOON_EMRAT) */ #define EARTH_MOON_EMRAT 81.300568 /* ── Planet-moon GM ratios ─────────────────────────────── */ /* * GM_planet / GM_moon from spacecraft-derived values. * mu = 1.0 / (1.0 + ratio) * * Galilean moons (Schubert et al. 2004, Anderson et al. 1996-2001): */ #define JUPITER_IO_RATIO 22423.9 /* GM_Jup / GM_Io */ #define JUPITER_EUROPA_RATIO 39478.0 /* GM_Jup / GM_Europa */ #define JUPITER_GANYMEDE_RATIO 12716.0 /* GM_Jup / GM_Ganymede */ #define JUPITER_CALLISTO_RATIO 17350.0 /* GM_Jup / GM_Callisto */ /* * Saturn moons (Jacobson et al. 2006): */ #define SATURN_MIMAS_RATIO 15108611.0 #define SATURN_ENCELADUS_RATIO 4955938.0 #define SATURN_TETHYS_RATIO 6137851.0 #define SATURN_DIONE_RATIO 3430825.0 #define SATURN_RHEA_RATIO 1629997.0 #define SATURN_TITAN_RATIO 4226.5 /* Titan is massive */ #define SATURN_IAPETUS_RATIO 3148296.0 #define SATURN_HYPERION_RATIO 6.821e9 /* tiny */ /* * Uranus moons (Jacobson et al. 1992): */ #define URANUS_MIRANDA_RATIO 1311870.0 #define URANUS_ARIEL_RATIO 65229.0 #define URANUS_UMBRIEL_RATIO 72449.0 #define URANUS_TITANIA_RATIO 24399.0 #define URANUS_OBERON_RATIO 25399.0 /* * Mars moons (Jacobson 2014): */ #define MARS_PHOBOS_RATIO 5.8775e7 #define MARS_DEIMOS_RATIO 3.919e8 /* ── Maximum Newton-Raphson iterations ─────────────────── */ #define LAGRANGE_MAX_ITER 50 /* ── Core API ──────────────────────────────────────────── */ /* * Solve for a Lagrange point in the normalized co-rotating frame. * * mu: mass parameter = M2 / (M1 + M2), must be in (0, 0.5] * point_id: LAGRANGE_L1 through LAGRANGE_L5 * x, y: output co-rotating coordinates (normalized to unit separation) * Origin at barycenter. Primary at (-mu, 0), secondary at (1-mu, 0). * * Returns 0 on success, -1 on invalid input or convergence failure. */ static inline int lagrange_corotating(double mu, int point_id, double *x, double *y) { double gamma, f, fp, gamma_new; int i; if (mu <= 0.0 || mu > 0.5 || point_id < LAGRANGE_L1 || point_id > LAGRANGE_L5) return -1; switch (point_id) { case LAGRANGE_L1: /* * L1: between primary and secondary. * Solve: gamma^5 - (3-mu)*gamma^4 + (3-2*mu)*gamma^3 * - mu*gamma^2 + 2*mu*gamma - mu = 0 * where gamma = distance from secondary toward primary. * Initial guess: Hill sphere approximation. */ gamma = cbrt(mu / 3.0); for (i = 0; i < LAGRANGE_MAX_ITER; i++) { double g2 = gamma * gamma; double g3 = g2 * gamma; double g4 = g3 * gamma; double g5 = g4 * gamma; f = g5 - (3.0 - mu) * g4 + (3.0 - 2.0 * mu) * g3 - mu * g2 + 2.0 * mu * gamma - mu; fp = 5.0 * g4 - 4.0 * (3.0 - mu) * g3 + 3.0 * (3.0 - 2.0 * mu) * g2 - 2.0 * mu * gamma + 2.0 * mu; if (fabs(fp) < 1e-30) return -1; gamma_new = gamma - f / fp; if (gamma_new <= 0.0) gamma_new = gamma * 0.5; /* keep gamma positive */ if (fabs(gamma_new - gamma) < 1e-15) break; gamma = gamma_new; } if (i == LAGRANGE_MAX_ITER) return -1; *x = 1.0 - mu - gamma; *y = 0.0; break; case LAGRANGE_L2: /* * L2: beyond secondary, away from primary. * Solve: gamma^5 + (3-mu)*gamma^4 + (3-2*mu)*gamma^3 * - mu*gamma^2 - 2*mu*gamma - mu = 0 */ gamma = cbrt(mu / 3.0); for (i = 0; i < LAGRANGE_MAX_ITER; i++) { double g2 = gamma * gamma; double g3 = g2 * gamma; double g4 = g3 * gamma; double g5 = g4 * gamma; f = g5 + (3.0 - mu) * g4 + (3.0 - 2.0 * mu) * g3 - mu * g2 - 2.0 * mu * gamma - mu; fp = 5.0 * g4 + 4.0 * (3.0 - mu) * g3 + 3.0 * (3.0 - 2.0 * mu) * g2 - 2.0 * mu * gamma - 2.0 * mu; if (fabs(fp) < 1e-30) return -1; gamma_new = gamma - f / fp; if (gamma_new <= 0.0) gamma_new = gamma * 0.5; /* keep gamma positive */ if (fabs(gamma_new - gamma) < 1e-15) break; gamma = gamma_new; } if (i == LAGRANGE_MAX_ITER) return -1; *x = 1.0 - mu + gamma; *y = 0.0; break; case LAGRANGE_L3: /* * L3: opposite side from secondary, beyond primary. * Solve: gamma^5 + (2+mu)*gamma^4 + (1+2*mu)*gamma^3 * - (1-mu)*gamma^2 - 2*(1-mu)*gamma - (1-mu) = 0 * where gamma = distance from primary. */ gamma = 1.0 - 7.0 * mu / 12.0; /* Szebehely approximation */ for (i = 0; i < LAGRANGE_MAX_ITER; i++) { double g2 = gamma * gamma; double g3 = g2 * gamma; double g4 = g3 * gamma; double g5 = g4 * gamma; double one_minus_mu = 1.0 - mu; f = g5 + (2.0 + mu) * g4 + (1.0 + 2.0 * mu) * g3 - one_minus_mu * g2 - 2.0 * one_minus_mu * gamma - one_minus_mu; fp = 5.0 * g4 + 4.0 * (2.0 + mu) * g3 + 3.0 * (1.0 + 2.0 * mu) * g2 - 2.0 * one_minus_mu * gamma - 2.0 * one_minus_mu; if (fabs(fp) < 1e-30) return -1; gamma_new = gamma - f / fp; if (gamma_new <= 0.0) gamma_new = gamma * 0.5; /* keep gamma positive */ if (fabs(gamma_new - gamma) < 1e-15) break; gamma = gamma_new; } if (i == LAGRANGE_MAX_ITER) return -1; *x = -mu - gamma; *y = 0.0; break; case LAGRANGE_L4: /* Equilateral triangle, leading */ *x = 0.5 - mu; *y = sqrt(3.0) / 2.0; break; case LAGRANGE_L5: /* Equilateral triangle, trailing */ *x = 0.5 - mu; *y = -sqrt(3.0) / 2.0; break; default: return -1; } return 0; } /* * Transform a co-rotating Lagrange point to physical ecliptic J2000. * * The co-rotating frame has origin at the barycenter, x-axis along * the primary→secondary direction, z-axis along the orbital angular * momentum. We construct this frame from the instantaneous positions * and velocity of the secondary relative to the primary. * * primary[3]: heliocentric position of primary (AU, ecliptic J2000) * secondary[3]: heliocentric position of secondary (AU, ecliptic J2000) * sec_vel[3]: velocity of secondary relative to primary (AU/day) * mu: mass parameter M2/(M1+M2) * point_id: LAGRANGE_L1..L5 * result[3]: output heliocentric position (AU, ecliptic J2000) * * Returns 0 on success, -1 on failure. */ static inline int lagrange_position(const double primary[3], const double secondary[3], const double sec_vel[3], double mu, int point_id, double result[3]) { double d[3], sep, e_x[3], e_z[3], e_y[3]; double hx, hy, hz, hmag; double x_co, y_co; int rc; /* Displacement: primary → secondary */ d[0] = secondary[0] - primary[0]; d[1] = secondary[1] - primary[1]; d[2] = secondary[2] - primary[2]; sep = sqrt(d[0]*d[0] + d[1]*d[1] + d[2]*d[2]); if (sep < 1e-30) return -1; /* Unit vector along primary→secondary */ e_x[0] = d[0] / sep; e_x[1] = d[1] / sep; e_x[2] = d[2] / sep; /* Angular momentum direction: h = d x v */ hx = d[1] * sec_vel[2] - d[2] * sec_vel[1]; hy = d[2] * sec_vel[0] - d[0] * sec_vel[2]; hz = d[0] * sec_vel[1] - d[1] * sec_vel[0]; hmag = sqrt(hx*hx + hy*hy + hz*hz); if (hmag < 1e-30) return -1; e_z[0] = hx / hmag; e_z[1] = hy / hmag; e_z[2] = hz / hmag; /* e_y = e_z x e_x (completes right-handed frame) */ e_y[0] = e_z[1] * e_x[2] - e_z[2] * e_x[1]; e_y[1] = e_z[2] * e_x[0] - e_z[0] * e_x[2]; e_y[2] = e_z[0] * e_x[1] - e_z[1] * e_x[0]; /* Solve for co-rotating coordinates */ rc = lagrange_corotating(mu, point_id, &x_co, &y_co); if (rc != 0) return -1; /* * Physical position relative to barycenter: * P_bary = primary + mu * d (barycenter location) * L_phys = P_bary + sep * (x_co * e_x + y_co * e_y) * * But x_co is already relative to barycenter (origin in co-rotating * frame), so: * L_phys = primary + mu * d + sep * (x_co * e_x + y_co * e_y) */ result[0] = primary[0] + mu * d[0] + sep * (x_co * e_x[0] + y_co * e_y[0]); result[1] = primary[1] + mu * d[1] + sep * (x_co * e_x[1] + y_co * e_y[1]); result[2] = primary[2] + mu * d[2] + sep * (x_co * e_x[2] + y_co * e_y[2]); return 0; } /* * Hill sphere radius. * * separation_au: distance between primary and secondary (AU) * mu: mass parameter M2/(M1+M2) * * Returns Hill radius in AU. */ static inline double lagrange_hill_radius(double separation_au, double mu) { return separation_au * cbrt(mu / 3.0); } /* * Libration zone radius (approximate). * * For L1/L2: same as Hill radius (zone extends ~r_Hill from L-point). * For L4/L5: horseshoe/tadpole width ~ separation * sqrt(mu) (Dermott 1981). * For L3: ~ separation * (7/12) * mu (very narrow). * * separation_au: distance between primary and secondary (AU) * mu: mass parameter * point_id: LAGRANGE_L1..L5 * * Returns approximate zone radius in AU, or -1.0 on error. */ static inline double lagrange_zone_radius(double separation_au, double mu, int point_id) { switch (point_id) { case LAGRANGE_L1: case LAGRANGE_L2: return lagrange_hill_radius(separation_au, mu); case LAGRANGE_L3: return separation_au * (7.0 / 12.0) * mu; case LAGRANGE_L4: case LAGRANGE_L5: return separation_au * sqrt(mu); default: return -1.0; } } /* * Look up the Sun-planet mass ratio for a pg_orrery body_id. * * body_id: 1=Mercury..8=Neptune (pg_orrery convention) * Returns the GM_sun/GM_planet ratio, or -1.0 for invalid body_id. */ static inline double sun_planet_ratio(int body_id) { switch (body_id) { case 1: return SUN_MERCURY_RATIO; case 2: return SUN_VENUS_RATIO; case 3: return SUN_EARTH_RATIO; case 4: return SUN_MARS_RATIO; case 5: return SUN_JUPITER_RATIO; case 6: return SUN_SATURN_RATIO; case 7: return SUN_URANUS_RATIO; case 8: return SUN_NEPTUNE_RATIO; default: return -1.0; } } /* * Compute mu from a Sun/planet GM ratio. * mu = 1 / (1 + ratio) */ static inline double mu_from_ratio(double ratio) { return 1.0 / (1.0 + ratio); } /* * Look up planet-moon GM ratio for a specific moon. * * family: 'g' (Galilean), 's' (Saturn), 'u' (Uranus), 'm' (Mars) * moon_id: 0-based index within family * Returns ratio, or -1.0 for invalid. */ static inline double planet_moon_ratio(char family, int moon_id) { switch (family) { case 'g': /* Galilean */ switch (moon_id) { case 0: return JUPITER_IO_RATIO; case 1: return JUPITER_EUROPA_RATIO; case 2: return JUPITER_GANYMEDE_RATIO; case 3: return JUPITER_CALLISTO_RATIO; default: return -1.0; } case 's': /* Saturn */ switch (moon_id) { case 0: return SATURN_MIMAS_RATIO; case 1: return SATURN_ENCELADUS_RATIO; case 2: return SATURN_TETHYS_RATIO; case 3: return SATURN_DIONE_RATIO; case 4: return SATURN_RHEA_RATIO; case 5: return SATURN_TITAN_RATIO; case 6: return SATURN_IAPETUS_RATIO; case 7: return SATURN_HYPERION_RATIO; default: return -1.0; } case 'u': /* Uranus */ switch (moon_id) { case 0: return URANUS_MIRANDA_RATIO; case 1: return URANUS_ARIEL_RATIO; case 2: return URANUS_UMBRIEL_RATIO; case 3: return URANUS_TITANIA_RATIO; case 4: return URANUS_OBERON_RATIO; default: return -1.0; } case 'm': /* Mars */ switch (moon_id) { case 0: return MARS_PHOBOS_RATIO; case 1: return MARS_DEIMOS_RATIO; default: return -1.0; } default: return -1.0; } } #endif /* PG_ORRERY_LAGRANGE_H */