Range rate: topocentric residuals now include an optional 4th component (dot(Δr, v_ecef) / |Δr|) with OD_RR_SCALE=10.0 for unit balancing. Controlled via fit_range_rate parameter on tle_from_topocentric(). Weighted observations: per-observation weights applied as √w scaling to both residuals and Jacobian rows, producing the weighted normal equations H'WH without explicit W construction. Weights parameter added to tle_from_eci, tle_from_topocentric, and tle_from_angles. Gauss angles-only IOD: Vallado Algorithm 52 implementation for seed-free orbit recovery from 3+ RA/Dec observations. New RA/Dec residual function with cos(dec) scaling and wrap-around handling. New tle_from_angles() and tle_from_angles_multi() SQL functions accepting RA in hours [0,24), Dec in degrees [-90,90]. New standalone test suite: test_od_gauss (17 assertions). New regression tests: Tests 18-25 covering range rate, weights, angles-only with/without seed, and error cases.
283 lines
8.4 KiB
C
283 lines
8.4 KiB
C
/*
|
|
* test_od_gauss.c -- Standalone unit tests for Gauss angles-only IOD
|
|
*
|
|
* No PostgreSQL dependency. Exercises:
|
|
* - ISS-like orbit: generate RA/Dec from known state, recover via Gauss
|
|
* - GEO orbit with wider time spacing
|
|
* - Degenerate: observations too close in time
|
|
* - Gauss feeding into Gibbs/Herrick-Gibbs
|
|
*
|
|
* Build: cc -Wall -Werror -Isrc -o test/test_od_gauss \
|
|
* test/test_od_gauss.c src/od_iod.c src/od_math.c -lm
|
|
* Run: ./test/test_od_gauss
|
|
*/
|
|
|
|
#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)
|
|
|
|
|
|
/* ── Helper: compute RA/Dec from TEME pos + observer ECEF ── */
|
|
|
|
/*
|
|
* Simulates an observation by computing RA/Dec of a satellite
|
|
* from a ground observer. Uses od_teme_to_radec().
|
|
*/
|
|
static void
|
|
simulate_radec(const double pos_teme[3], const double obs_ecef[3],
|
|
double jd, double *ra, double *dec)
|
|
{
|
|
double gmst = od_gmst_from_jd(jd);
|
|
od_teme_to_radec(pos_teme, obs_ecef, gmst, ra, dec);
|
|
}
|
|
|
|
|
|
/* ── Test: ISS-like orbit ──────────────────────────────── */
|
|
|
|
static void
|
|
test_gauss_iss(void)
|
|
{
|
|
od_keplerian_t kep;
|
|
double pos[3][3], vel[3][3];
|
|
double ra[3], dec[3], jd[3];
|
|
double obs_ecef[3][3];
|
|
od_iod_result_t result;
|
|
int rc, i;
|
|
double dt;
|
|
|
|
fprintf(stderr, "\n--- Gauss: ISS-like orbit ---\n");
|
|
|
|
/* Known ISS-like orbit */
|
|
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;
|
|
|
|
/* 30 minutes between observations — wider arc improves Gauss conditioning */
|
|
dt = 1800.0;
|
|
jd[0] = 2451545.0;
|
|
jd[1] = jd[0] + dt / 86400.0;
|
|
jd[2] = jd[0] + 2.0 * dt / 86400.0;
|
|
|
|
/* Generate 3 positions */
|
|
od_keplerian_to_eci(&kep, pos[0], vel[0]);
|
|
|
|
kep.M = od_normalize_angle(kep.M + kep.n * (dt / 60.0));
|
|
od_keplerian_to_eci(&kep, pos[1], vel[1]);
|
|
|
|
kep.M = od_normalize_angle(kep.M + kep.n * (dt / 60.0));
|
|
od_keplerian_to_eci(&kep, pos[2], vel[2]);
|
|
|
|
/* Observer at lat=40N, lon=0, alt=0 — compute ECEF once for all obs */
|
|
od_observer_to_ecef(40.0 * M_PI / 180.0, 0.0, 0.0, obs_ecef[0]);
|
|
obs_ecef[1][0] = obs_ecef[0][0];
|
|
obs_ecef[1][1] = obs_ecef[0][1];
|
|
obs_ecef[1][2] = obs_ecef[0][2];
|
|
obs_ecef[2][0] = obs_ecef[0][0];
|
|
obs_ecef[2][1] = obs_ecef[0][1];
|
|
obs_ecef[2][2] = obs_ecef[0][2];
|
|
|
|
/* Compute RA/Dec for each observation */
|
|
for (i = 0; i < 3; i++)
|
|
simulate_radec(pos[i], obs_ecef[i], jd[i], &ra[i], &dec[i]);
|
|
|
|
/* Run Gauss */
|
|
rc = od_gauss(ra, dec, jd, obs_ecef, &result);
|
|
|
|
RUN(rc == 0, "Gauss ISS returns success");
|
|
RUN(result.valid == 1, "result is valid");
|
|
RUN(result.kep.ecc < 1.0, "eccentricity is bound");
|
|
RUN(result.kep.n > 0.0, "positive mean motion");
|
|
|
|
/* Gauss accuracy is inherently low (angles-only loses range info).
|
|
* It only needs to be close enough for the DC solver to converge.
|
|
* A factor of 5x in mean motion is acceptable for a seed orbit. */
|
|
RUN(result.kep.n > 0.0002 && result.kep.n < 0.006,
|
|
"mean motion in plausible range");
|
|
CLOSE(result.kep.inc, 0.9012, 0.3, "inclination within ~17 deg");
|
|
}
|
|
|
|
|
|
/* ── Test: MEO-like orbit ──────────────────────────────── */
|
|
|
|
static void
|
|
test_gauss_meo(void)
|
|
{
|
|
od_keplerian_t kep;
|
|
double pos[3][3], vel[3][3];
|
|
double ra[3], dec[3], jd[3];
|
|
double obs_ecef[3][3];
|
|
od_iod_result_t result;
|
|
int rc, i;
|
|
double dt;
|
|
|
|
fprintf(stderr, "\n--- Gauss: MEO-like orbit ---\n");
|
|
|
|
/* GPS-like altitude, moderate inclination */
|
|
kep.n = 0.000262; /* ~2 rev/day */
|
|
kep.ecc = 0.01;
|
|
kep.inc = 0.96; /* ~55 deg */
|
|
kep.raan = 1.0;
|
|
kep.argp = 0.0;
|
|
kep.M = 0.0;
|
|
kep.bstar = 0.0;
|
|
|
|
/* 2 hours between observations — wider arc for higher altitude */
|
|
dt = 7200.0;
|
|
jd[0] = 2451545.0;
|
|
jd[1] = jd[0] + dt / 86400.0;
|
|
jd[2] = jd[0] + 2.0 * dt / 86400.0;
|
|
|
|
od_keplerian_to_eci(&kep, pos[0], vel[0]);
|
|
|
|
kep.M = od_normalize_angle(kep.M + kep.n * (dt / 60.0));
|
|
od_keplerian_to_eci(&kep, pos[1], vel[1]);
|
|
|
|
kep.M = od_normalize_angle(kep.M + kep.n * (dt / 60.0));
|
|
od_keplerian_to_eci(&kep, pos[2], vel[2]);
|
|
|
|
od_observer_to_ecef(35.0 * M_PI / 180.0, -106.0 * M_PI / 180.0,
|
|
1600.0, obs_ecef[0]);
|
|
for (i = 1; i < 3; i++)
|
|
{
|
|
obs_ecef[i][0] = obs_ecef[0][0];
|
|
obs_ecef[i][1] = obs_ecef[0][1];
|
|
obs_ecef[i][2] = obs_ecef[0][2];
|
|
}
|
|
|
|
for (i = 0; i < 3; i++)
|
|
simulate_radec(pos[i], obs_ecef[i], jd[i], &ra[i], &dec[i]);
|
|
|
|
rc = od_gauss(ra, dec, jd, obs_ecef, &result);
|
|
|
|
RUN(rc == 0, "Gauss MEO returns success");
|
|
RUN(result.valid == 1, "result is valid");
|
|
RUN(result.kep.n > 0.0, "positive mean motion");
|
|
RUN(result.kep.n > 0.00005 && result.kep.n < 0.002,
|
|
"mean motion in plausible MEO range");
|
|
}
|
|
|
|
|
|
/* ── Test: too-close observations fail ───────────────────── */
|
|
|
|
static void
|
|
test_gauss_too_close(void)
|
|
{
|
|
double ra[3] = {1.0, 1.001, 1.002};
|
|
double dec[3] = {0.5, 0.501, 0.502};
|
|
double jd[3] = {2451545.0, 2451545.000005, 2451545.00001};
|
|
double obs_ecef[3][3];
|
|
od_iod_result_t result;
|
|
int rc;
|
|
|
|
fprintf(stderr, "\n--- Gauss: too-close observations ---\n");
|
|
|
|
od_observer_to_ecef(40.0 * M_PI / 180.0, 0.0, 0.0, obs_ecef[0]);
|
|
obs_ecef[1][0] = obs_ecef[0][0];
|
|
obs_ecef[1][1] = obs_ecef[0][1];
|
|
obs_ecef[1][2] = obs_ecef[0][2];
|
|
obs_ecef[2][0] = obs_ecef[0][0];
|
|
obs_ecef[2][1] = obs_ecef[0][1];
|
|
obs_ecef[2][2] = obs_ecef[0][2];
|
|
|
|
rc = od_gauss(ra, dec, jd, obs_ecef, &result);
|
|
|
|
RUN(rc != 0, "too-close observations rejected");
|
|
RUN(result.valid == 0, "result marked invalid");
|
|
}
|
|
|
|
|
|
/* ── Test: radec_to_los / teme_to_radec round-trip ─────── */
|
|
|
|
static void
|
|
test_radec_roundtrip(void)
|
|
{
|
|
double ra_in = 1.5; /* ~86 degrees */
|
|
double dec_in = 0.3; /* ~17 degrees */
|
|
double los[3];
|
|
double ra_out, dec_out;
|
|
double rm;
|
|
|
|
fprintf(stderr, "\n--- RA/Dec ↔ LOS round-trip ---\n");
|
|
|
|
/* RA/Dec → LOS unit vector */
|
|
od_radec_to_los(ra_in, dec_in, los);
|
|
|
|
rm = sqrt(los[0]*los[0] + los[1]*los[1] + los[2]*los[2]);
|
|
CLOSE(rm, 1.0, 1e-12, "LOS is unit vector");
|
|
|
|
/* LOS → RA/Dec (inverse) */
|
|
dec_out = asin(los[2]);
|
|
ra_out = atan2(los[1], los[0]);
|
|
if (ra_out < 0.0) ra_out += 2.0 * M_PI;
|
|
|
|
CLOSE(ra_out, ra_in, 1e-12, "RA round-trip");
|
|
CLOSE(dec_out, dec_in, 1e-12, "Dec round-trip");
|
|
}
|
|
|
|
|
|
/* ── Test: teme_to_radec consistency ──────────────────── */
|
|
|
|
static void
|
|
test_teme_to_radec(void)
|
|
{
|
|
/* Place a satellite at known TEME position, compute RA/Dec from
|
|
* a ground observer, verify it's in reasonable range */
|
|
double pos_teme[3] = {6778.0, 0.0, 0.0}; /* on X-axis, LEO alt */
|
|
double obs_ecef[3];
|
|
double ra, dec;
|
|
double jd = 2451545.0;
|
|
|
|
fprintf(stderr, "\n--- teme_to_radec consistency ---\n");
|
|
|
|
od_observer_to_ecef(0.0, 0.0, 0.0, obs_ecef); /* equator, prime meridian */
|
|
|
|
od_teme_to_radec(pos_teme, obs_ecef, od_gmst_from_jd(jd), &ra, &dec);
|
|
|
|
RUN(ra >= 0.0 && ra < 2.0 * M_PI, "RA in [0, 2pi)");
|
|
RUN(dec >= -M_PI / 2.0 && dec <= M_PI / 2.0, "Dec in [-pi/2, pi/2]");
|
|
}
|
|
|
|
|
|
int
|
|
main(void)
|
|
{
|
|
fprintf(stderr, "pg_orrery Gauss IOD unit tests\n");
|
|
fprintf(stderr, "==============================\n");
|
|
|
|
test_gauss_iss();
|
|
test_gauss_meo();
|
|
test_gauss_too_close();
|
|
test_radec_roundtrip();
|
|
test_teme_to_radec();
|
|
|
|
fprintf(stderr, "\n%d/%d tests passed.\n", n_pass, n_run);
|
|
return (n_pass == n_run) ? 0 : 1;
|
|
}
|