pg_orrery/test/test_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

221 lines
6.4 KiB
C

/*
* test_od_iod.c -- Standalone unit tests for Gibbs IOD
*
* No PostgreSQL dependency. Exercises:
* - ISS-like 3-position Gibbs recovery
* - GEO-like orbit with wide time separation
* - Coplanar failure detection
* - Near-circular orbit
*
* Build: cc -Wall -Werror -Isrc -o test/test_od_iod \
* test/test_od_iod.c src/od_iod.c src/od_math.c -lm
* Run: ./test/test_od_iod
*/
#include "od_iod.h"
#include "od_math.h"
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
/* ── Test harness ───────────────────────────────────────── */
static int n_run, n_pass;
#define RUN(cond, msg) do { \
n_run++; \
if (!(cond)) \
fprintf(stderr, "FAIL: %s [line %d]\n", (msg), __LINE__); \
else { n_pass++; fprintf(stderr, " ok: %s\n", (msg)); } \
} while (0)
#define CLOSE(a, b, tol, msg) do { \
n_run++; \
double _a = (a), _b = (b); \
if (fabs(_a - _b) > (tol)) \
fprintf(stderr, "FAIL: %s: %.15g vs %.15g (diff %.3e) [line %d]\n", \
(msg), _a, _b, fabs(_a - _b), __LINE__); \
else { n_pass++; fprintf(stderr, " ok: %s\n", (msg)); } \
} while (0)
/* ── Test: ISS-like orbit ──────────────────────────────── */
static void
test_gibbs_iss(void)
{
/* ISS-like orbit: 408 km altitude, 51.6 deg inclination.
* Generate 3 positions from known Keplerian elements. */
od_keplerian_t kep;
double pos1[3], vel1[3], pos2[3], vel2[3], pos3[3], vel3[3];
od_iod_result_t result;
int rc;
double n_rad_s, period_s, dt;
fprintf(stderr, "\n--- Gibbs: ISS-like orbit ---\n");
kep.n = 0.001127; /* ~15.5 rev/day in rad/min */
kep.ecc = 0.0007;
kep.inc = 0.9012; /* ~51.6 deg */
kep.raan = 3.0;
kep.argp = 0.5;
kep.M = 0.0;
kep.bstar = 0.0;
/* Period in seconds, time step = period/6 */
n_rad_s = kep.n / 60.0;
period_s = 2.0 * M_PI / n_rad_s;
dt = period_s / 6.0; /* ~15 min */
od_keplerian_to_eci(&kep, pos1, vel1);
/* Advance M by dt */
kep.M = od_normalize_angle(kep.M + kep.n * (dt / 60.0));
od_keplerian_to_eci(&kep, pos2, vel2);
kep.M = od_normalize_angle(kep.M + kep.n * (dt / 60.0));
od_keplerian_to_eci(&kep, pos3, vel3);
/* Julian dates (arbitrary epoch + dt in days) */
rc = od_gibbs(pos1, pos2, pos3,
2451545.0,
2451545.0 + dt / 86400.0,
2451545.0 + 2.0 * dt / 86400.0,
&result);
RUN(rc == 0, "Gibbs ISS returns success");
RUN(result.valid == 1, "result is valid");
CLOSE(result.kep.ecc, 0.0007, 0.01, "eccentricity recovered");
CLOSE(result.kep.inc, 0.9012, 0.02, "inclination recovered");
RUN(result.kep.n > 0.0, "positive mean motion");
CLOSE(result.kep.n, 0.001127, 0.0001, "mean motion recovered");
}
/* ── Test: GEO-like orbit ──────────────────────────────── */
static void
test_gibbs_geo(void)
{
od_keplerian_t kep;
double pos1[3], vel1[3], pos2[3], vel2[3], pos3[3], vel3[3];
od_iod_result_t result;
int rc;
double dt;
fprintf(stderr, "\n--- Gibbs: GEO-like orbit ---\n");
kep.n = 0.0000729; /* ~1 rev/day */
kep.ecc = 0.0001;
kep.inc = 0.001; /* near-equatorial */
kep.raan = 0.0;
kep.argp = 0.0;
kep.M = 0.0;
kep.bstar = 0.0;
/* 2 hours between observations */
dt = 7200.0;
od_keplerian_to_eci(&kep, pos1, vel1);
kep.M = od_normalize_angle(kep.M + kep.n * (dt / 60.0));
od_keplerian_to_eci(&kep, pos2, vel2);
kep.M = od_normalize_angle(kep.M + kep.n * (dt / 60.0));
od_keplerian_to_eci(&kep, pos3, vel3);
rc = od_gibbs(pos1, pos2, pos3,
2451545.0,
2451545.0 + dt / 86400.0,
2451545.0 + 2.0 * dt / 86400.0,
&result);
RUN(rc == 0, "Gibbs GEO returns success");
RUN(result.valid == 1, "result is valid");
RUN(result.kep.ecc < 0.01, "near-circular eccentricity");
CLOSE(result.kep.n, 0.0000729, 0.00001, "GEO mean motion recovered");
}
/* ── Test: coplanar failure ────────────────────────────── */
static void
test_gibbs_coplanar_fail(void)
{
/* Three vectors that are NOT coplanar (large out-of-plane angle) */
double pos1[3] = {6778.0, 0.0, 0.0};
double pos2[3] = {0.0, 6778.0, 0.0};
double pos3[3] = {0.0, 0.0, 6778.0};
od_iod_result_t result;
int rc;
fprintf(stderr, "\n--- Gibbs: coplanarity failure ---\n");
rc = od_gibbs(pos1, pos2, pos3,
2451545.0, 2451545.01, 2451545.02,
&result);
RUN(rc != 0, "non-coplanar vectors rejected");
RUN(result.valid == 0, "result marked invalid");
}
/* ── Test: near-circular orbit ─────────────────────────── */
static void
test_gibbs_circular(void)
{
od_keplerian_t kep;
double pos1[3], vel1[3], pos2[3], vel2[3], pos3[3], vel3[3];
od_iod_result_t result;
int rc;
double dt;
fprintf(stderr, "\n--- Gibbs: near-circular orbit ---\n");
kep.n = 0.001127;
kep.ecc = 0.00001; /* nearly perfect circle */
kep.inc = 0.5;
kep.raan = 1.0;
kep.argp = 0.0;
kep.M = 0.0;
kep.bstar = 0.0;
dt = 900.0; /* 15 min */
od_keplerian_to_eci(&kep, pos1, vel1);
kep.M = od_normalize_angle(kep.M + kep.n * (dt / 60.0));
od_keplerian_to_eci(&kep, pos2, vel2);
kep.M = od_normalize_angle(kep.M + kep.n * (dt / 60.0));
od_keplerian_to_eci(&kep, pos3, vel3);
rc = od_gibbs(pos1, pos2, pos3,
2451545.0,
2451545.0 + dt / 86400.0,
2451545.0 + 2.0 * dt / 86400.0,
&result);
RUN(rc == 0, "Gibbs circular returns success");
RUN(result.kep.ecc < 0.001, "eccentricity near zero");
CLOSE(result.kep.n, 0.001127, 0.0001, "mean motion recovered");
}
int
main(void)
{
fprintf(stderr, "pg_orrery IOD unit tests\n");
fprintf(stderr, "========================\n");
test_gibbs_iss();
test_gibbs_geo();
test_gibbs_coplanar_fail();
test_gibbs_circular();
fprintf(stderr, "\n%d/%d tests passed.\n", n_pass, n_run);
return (n_pass == n_run) ? 0 : 1;
}