pg_orrery/src/od_iod.c
Ryan Malloy 6e57071970 Add Gibbs IOD bootstrap for seed-free orbit determination
Eliminates the seed TLE requirement for topocentric fitting by
computing an initial orbit estimate from 3 well-spaced observations
using the Gibbs method. ECI fitting retains the single-observation
r,v approach (exact for two-body) with Gibbs as fallback.
2026-02-17 16:06:05 -07:00

128 lines
3.3 KiB
C

/*
* od_iod.c -- Gibbs method for initial orbit determination
*
* Given 3 position vectors, computes the velocity at the middle
* observation via vector cross-product geometry (Vallado Algorithm 54).
*
* The Gibbs method requires 3 coplanar position vectors (which all
* orbit positions are, modulo perturbations and measurement noise).
* It avoids iteration entirely -- pure linear algebra.
*
* Reference: Vallado, "Fundamentals of Astrodynamics", 4th ed., p.459
*/
#include "od_iod.h"
#include <math.h>
/* WGS-72 gravitational parameter -- must match SGP4 */
#define MU_KM3_S2 398600.8
/* Coplanarity tolerance: cos(angle) between r1 and (r2 x r3) plane.
* 0.05 corresponds to about 3 degrees, generous for noisy obs. */
#define COPLANAR_TOL 0.05
static double
vec_mag(const double v[3])
{
return sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
}
static void
vec_cross(const double a[3], const double b[3], double out[3])
{
out[0] = a[1]*b[2] - a[2]*b[1];
out[1] = a[2]*b[0] - a[0]*b[2];
out[2] = a[0]*b[1] - a[1]*b[0];
}
static double
vec_dot(const double a[3], const double b[3])
{
return a[0]*b[0] + a[1]*b[1] + a[2]*b[2];
}
int
od_gibbs(const double pos1[3], const double pos2[3],
const double pos3[3],
double jd1, double jd2, double jd3,
od_iod_result_t *result)
{
double r1_mag, r2_mag, r3_mag;
double z12[3], z23[3], z31[3];
double z23_mag;
double coplanar;
double D[3], N[3], S[3];
double D_mag, N_mag;
double v2[3];
double v2_coeff;
double D_cross_r2[3];
int i, rc;
result->valid = 0;
r1_mag = vec_mag(pos1);
r2_mag = vec_mag(pos2);
r3_mag = vec_mag(pos3);
if (r1_mag < 1.0 || r2_mag < 1.0 || r3_mag < 1.0)
return -1;
/* Cross products */
vec_cross(pos1, pos2, z12);
vec_cross(pos2, pos3, z23);
vec_cross(pos3, pos1, z31);
/* Coplanarity check: |r1 . z23| / (|r1| * |z23|) should be small */
z23_mag = vec_mag(z23);
if (z23_mag < 1e-10)
return -1; /* pos2 and pos3 are parallel (same direction) */
coplanar = fabs(vec_dot(pos1, z23)) / (r1_mag * z23_mag);
if (coplanar > COPLANAR_TOL)
return -1;
/* D = z12 + z23 + z31 */
for (i = 0; i < 3; i++)
D[i] = z12[i] + z23[i] + z31[i];
/* N = |r1| * z23 + |r2| * z31 + |r3| * z12 */
for (i = 0; i < 3; i++)
N[i] = r1_mag * z23[i] + r2_mag * z31[i] + r3_mag * z12[i];
/* S = (|r2| - |r3|) * r1 + (|r3| - |r1|) * r2 + (|r1| - |r2|) * r3 */
for (i = 0; i < 3; i++)
S[i] = (r2_mag - r3_mag) * pos1[i]
+ (r3_mag - r1_mag) * pos2[i]
+ (r1_mag - r2_mag) * pos3[i];
D_mag = vec_mag(D);
N_mag = vec_mag(N);
if (D_mag < 1e-10 || N_mag < 1e-10)
return -1;
/* v2 = sqrt(mu / (N_mag * D_mag)) * (D x r2 / |r2| + S) */
v2_coeff = sqrt(MU_KM3_S2 / (N_mag * D_mag));
vec_cross(D, pos2, D_cross_r2);
for (i = 0; i < 3; i++)
v2[i] = v2_coeff * (D_cross_r2[i] / r2_mag + S[i]);
/* Convert km/s velocity to the form expected by od_eci_to_keplerian */
rc = od_eci_to_keplerian(pos2, v2, &result->kep);
if (rc != 0)
return -1;
/* Sanity: eccentricity < 1 and positive mean motion */
if (result->kep.ecc >= 1.0 || result->kep.n <= 0.0)
return -1;
result->epoch_jd = jd2;
result->valid = 1;
(void)jd1;
(void)jd3;
return 0;
}