Add multi-observer support for topocentric fitting
Extend od_observation_t with observer_idx so each observation can reference a different ground station. Config now holds an array of observers instead of a single pointer. The existing single-observer tle_from_topocentric() is unchanged (sets observer_idx=0 for all obs). New overload: tle_from_topocentric(topo[], ts[], observer[], int4[], ...) accepts parallel observer_ids array indexing into the observers array. PG function overloading resolves by argument types. Tests 9-11: two-station fit converges, single-station via multi-observer API matches, out-of-range observer_id raises error.
This commit is contained in:
parent
9b0634725b
commit
59fd8ba743
3
Makefile
3
Makefile
@ -2,7 +2,8 @@ MODULE_big = pg_orrery
|
|||||||
EXTENSION = pg_orrery
|
EXTENSION = pg_orrery
|
||||||
DATA = sql/pg_orrery--0.1.0.sql sql/pg_orrery--0.2.0.sql sql/pg_orrery--0.1.0--0.2.0.sql \
|
DATA = sql/pg_orrery--0.1.0.sql sql/pg_orrery--0.2.0.sql sql/pg_orrery--0.1.0--0.2.0.sql \
|
||||||
sql/pg_orrery--0.3.0.sql sql/pg_orrery--0.2.0--0.3.0.sql \
|
sql/pg_orrery--0.3.0.sql sql/pg_orrery--0.2.0--0.3.0.sql \
|
||||||
sql/pg_orrery--0.4.0.sql sql/pg_orrery--0.3.0--0.4.0.sql
|
sql/pg_orrery--0.4.0.sql sql/pg_orrery--0.3.0--0.4.0.sql \
|
||||||
|
sql/pg_orrery--0.4.0--0.5.0.sql
|
||||||
|
|
||||||
# Our extension C sources
|
# Our extension C sources
|
||||||
OBJS = src/pg_orrery.o src/tle_type.o src/eci_type.o src/observer_type.o \
|
OBJS = src/pg_orrery.o src/tle_type.o src/eci_type.o src/observer_type.o \
|
||||||
|
|||||||
21
sql/pg_orrery--0.4.0--0.5.0.sql
Normal file
21
sql/pg_orrery--0.4.0--0.5.0.sql
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
-- pg_orrery 0.4.0 -> 0.5.0 migration
|
||||||
|
--
|
||||||
|
-- Adds multi-observer support, IOD bootstrap (seed-free fitting),
|
||||||
|
-- and covariance output for uncertainty estimation.
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Multi-observer topocentric fitting
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_from_topocentric(
|
||||||
|
observations topocentric[], times timestamptz[],
|
||||||
|
observers observer[], observer_ids int4[],
|
||||||
|
seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false,
|
||||||
|
max_iter int4 DEFAULT 15,
|
||||||
|
OUT fitted_tle tle, OUT iterations int4,
|
||||||
|
OUT rms_final float8, OUT rms_initial float8, OUT status text
|
||||||
|
) RETURNS RECORD
|
||||||
|
AS 'MODULE_PATHNAME', 'tle_from_topocentric_multi'
|
||||||
|
LANGUAGE C STABLE PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_from_topocentric(topocentric[], timestamptz[], observer[], int4[], tle, boolean, int4) IS
|
||||||
|
'Fit a TLE from topocentric observations collected by multiple ground stations. observer_ids[i] indexes into observers[]. Requires seed TLE and >= 6 observations.';
|
||||||
177
src/od_funcs.c
177
src/od_funcs.c
@ -26,6 +26,7 @@
|
|||||||
|
|
||||||
PG_FUNCTION_INFO_V1(tle_from_eci);
|
PG_FUNCTION_INFO_V1(tle_from_eci);
|
||||||
PG_FUNCTION_INFO_V1(tle_from_topocentric);
|
PG_FUNCTION_INFO_V1(tle_from_topocentric);
|
||||||
|
PG_FUNCTION_INFO_V1(tle_from_topocentric_multi);
|
||||||
PG_FUNCTION_INFO_V1(tle_fit_residuals);
|
PG_FUNCTION_INFO_V1(tle_fit_residuals);
|
||||||
|
|
||||||
/* ================================================================
|
/* ================================================================
|
||||||
@ -155,10 +156,11 @@ tle_from_eci(PG_FUNCTION_ARGS)
|
|||||||
|
|
||||||
/* Configure solver */
|
/* Configure solver */
|
||||||
memset(&config, 0, sizeof(config));
|
memset(&config, 0, sizeof(config));
|
||||||
config.obs_type = OD_OBS_ECI;
|
config.obs_type = OD_OBS_ECI;
|
||||||
config.fit_bstar = fit_bstar ? 1 : 0;
|
config.fit_bstar = fit_bstar ? 1 : 0;
|
||||||
config.max_iter = max_iter;
|
config.max_iter = max_iter;
|
||||||
config.observer = NULL;
|
config.observers = NULL;
|
||||||
|
config.n_observers = 0;
|
||||||
|
|
||||||
/* Convert seed TLE if provided */
|
/* Convert seed TLE if provided */
|
||||||
if (has_seed)
|
if (has_seed)
|
||||||
@ -274,15 +276,17 @@ tle_from_topocentric(PG_FUNCTION_ARGS)
|
|||||||
obs[i].data[0] = topo->azimuth;
|
obs[i].data[0] = topo->azimuth;
|
||||||
obs[i].data[1] = topo->elevation;
|
obs[i].data[1] = topo->elevation;
|
||||||
obs[i].data[2] = topo->range_km;
|
obs[i].data[2] = topo->range_km;
|
||||||
|
obs[i].observer_idx = 0; /* single observer */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Configure solver */
|
/* Configure solver */
|
||||||
memset(&config, 0, sizeof(config));
|
memset(&config, 0, sizeof(config));
|
||||||
config.obs_type = OD_OBS_TOPO;
|
config.obs_type = OD_OBS_TOPO;
|
||||||
config.fit_bstar = fit_bstar ? 1 : 0;
|
config.fit_bstar = fit_bstar ? 1 : 0;
|
||||||
config.max_iter = max_iter;
|
config.max_iter = max_iter;
|
||||||
config.observer = &observer;
|
config.observers = &observer;
|
||||||
|
config.n_observers = 1;
|
||||||
|
|
||||||
/* Seed TLE required for topocentric */
|
/* Seed TLE required for topocentric */
|
||||||
if (!has_seed)
|
if (!has_seed)
|
||||||
@ -328,6 +332,163 @@ tle_from_topocentric(PG_FUNCTION_ARGS)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* tle_from_topocentric_multi(topocentric[], timestamptz[],
|
||||||
|
* observer[], int4[],
|
||||||
|
* tle, boolean, int4)
|
||||||
|
* -> RECORD (same as tle_from_eci)
|
||||||
|
*
|
||||||
|
* Multi-observer variant: observations from different ground stations.
|
||||||
|
* observer_ids[i] indexes into the observers[] array.
|
||||||
|
* ================================================================
|
||||||
|
*/
|
||||||
|
Datum
|
||||||
|
tle_from_topocentric_multi(PG_FUNCTION_ARGS)
|
||||||
|
{
|
||||||
|
ArrayType *topo_arr = PG_GETARG_ARRAYTYPE_P(0);
|
||||||
|
ArrayType *time_arr = PG_GETARG_ARRAYTYPE_P(1);
|
||||||
|
ArrayType *obs_arr = PG_GETARG_ARRAYTYPE_P(2);
|
||||||
|
ArrayType *id_arr = PG_GETARG_ARRAYTYPE_P(3);
|
||||||
|
bool has_seed = !PG_ARGISNULL(4);
|
||||||
|
pg_tle *seed_pg = has_seed ? (pg_tle *) PG_GETARG_POINTER(4) : NULL;
|
||||||
|
bool fit_bstar = PG_ARGISNULL(5) ? false : PG_GETARG_BOOL(5);
|
||||||
|
int32 max_iter = PG_ARGISNULL(6) ? 15 : PG_GETARG_INT32(6);
|
||||||
|
|
||||||
|
int n_topo, n_times, n_obs_sites, n_ids;
|
||||||
|
od_observation_t *obs;
|
||||||
|
od_config_t config;
|
||||||
|
od_observer_t *observers;
|
||||||
|
od_result_t result;
|
||||||
|
tle_t seed_sat;
|
||||||
|
int i, rc;
|
||||||
|
|
||||||
|
TupleDesc tupdesc;
|
||||||
|
Datum values[5];
|
||||||
|
bool nulls[5];
|
||||||
|
HeapTuple tuple;
|
||||||
|
|
||||||
|
/* Deconstruct all arrays */
|
||||||
|
Datum *topo_datums, *time_datums, *obs_datums, *id_datums;
|
||||||
|
bool *topo_nulls, *time_nulls, *obs_nulls, *id_nulls;
|
||||||
|
|
||||||
|
deconstruct_array(topo_arr, topo_arr->elemtype, sizeof(pg_topocentric),
|
||||||
|
false, TYPALIGN_DOUBLE,
|
||||||
|
&topo_datums, &topo_nulls, &n_topo);
|
||||||
|
deconstruct_array(time_arr, TIMESTAMPTZOID, sizeof(int64), FLOAT8PASSBYVAL,
|
||||||
|
TYPALIGN_DOUBLE, &time_datums, &time_nulls, &n_times);
|
||||||
|
deconstruct_array(obs_arr, obs_arr->elemtype, sizeof(pg_observer),
|
||||||
|
false, TYPALIGN_DOUBLE,
|
||||||
|
&obs_datums, &obs_nulls, &n_obs_sites);
|
||||||
|
deconstruct_array(id_arr, INT4OID, sizeof(int32), true,
|
||||||
|
TYPALIGN_INT, &id_datums, &id_nulls, &n_ids);
|
||||||
|
|
||||||
|
if (n_topo != n_times)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
|
||||||
|
errmsg("observations and times arrays must have same length: "
|
||||||
|
"%d vs %d", n_topo, n_times)));
|
||||||
|
|
||||||
|
if (n_topo != n_ids)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
|
||||||
|
errmsg("observations and observer_ids arrays must have same length: "
|
||||||
|
"%d vs %d", n_topo, n_ids)));
|
||||||
|
|
||||||
|
if (n_topo < 6)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||||
|
errmsg("at least 6 observations required, got %d", n_topo)));
|
||||||
|
|
||||||
|
if (n_obs_sites < 1)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||||
|
errmsg("at least 1 observer required")));
|
||||||
|
|
||||||
|
/* Build observer array (pre-compute ECEF for each) */
|
||||||
|
observers = (od_observer_t *) palloc(sizeof(od_observer_t) * n_obs_sites);
|
||||||
|
for (i = 0; i < n_obs_sites; i++)
|
||||||
|
{
|
||||||
|
pg_observer *op = (pg_observer *) DatumGetPointer(obs_datums[i]);
|
||||||
|
observers[i].lat = op->lat;
|
||||||
|
observers[i].lon = op->lon;
|
||||||
|
observers[i].alt_m = op->alt_m;
|
||||||
|
od_observer_to_ecef(op->lat, op->lon, op->alt_m, observers[i].ecef);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Build observation array with per-observation observer index */
|
||||||
|
obs = (od_observation_t *) palloc(sizeof(od_observation_t) * n_topo);
|
||||||
|
for (i = 0; i < n_topo; i++)
|
||||||
|
{
|
||||||
|
pg_topocentric *topo = (pg_topocentric *) DatumGetPointer(topo_datums[i]);
|
||||||
|
int64 ts = DatumGetInt64(time_datums[i]);
|
||||||
|
int32 oid = DatumGetInt32(id_datums[i]);
|
||||||
|
|
||||||
|
if (oid < 0 || oid >= n_obs_sites)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||||
|
errmsg("observer_id %d out of range [0, %d)",
|
||||||
|
oid, n_obs_sites)));
|
||||||
|
|
||||||
|
obs[i].jd = PG_EPOCH_JD + ((double)ts / (double)USECS_PER_DAY);
|
||||||
|
obs[i].data[0] = topo->azimuth;
|
||||||
|
obs[i].data[1] = topo->elevation;
|
||||||
|
obs[i].data[2] = topo->range_km;
|
||||||
|
obs[i].observer_idx = oid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Configure solver */
|
||||||
|
memset(&config, 0, sizeof(config));
|
||||||
|
config.obs_type = OD_OBS_TOPO;
|
||||||
|
config.fit_bstar = fit_bstar ? 1 : 0;
|
||||||
|
config.max_iter = max_iter;
|
||||||
|
config.observers = observers;
|
||||||
|
config.n_observers = n_obs_sites;
|
||||||
|
|
||||||
|
/* Seed TLE required for topocentric */
|
||||||
|
if (!has_seed)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||||
|
errmsg("seed TLE required for topocentric fitting")));
|
||||||
|
|
||||||
|
pg_tle_to_sat_code_od(seed_pg, &seed_sat);
|
||||||
|
|
||||||
|
memset(&result, 0, sizeof(result));
|
||||||
|
|
||||||
|
rc = od_fit_tle(obs, n_topo, &seed_sat, &config, &result);
|
||||||
|
|
||||||
|
pfree(obs);
|
||||||
|
pfree(observers);
|
||||||
|
|
||||||
|
if (rc != 0)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_DATA_EXCEPTION),
|
||||||
|
errmsg("TLE fitting failed: %s", result.status)));
|
||||||
|
|
||||||
|
/* Build result tuple */
|
||||||
|
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||||
|
errmsg("function returning record called in context "
|
||||||
|
"that cannot accept type record")));
|
||||||
|
tupdesc = BlessTupleDesc(tupdesc);
|
||||||
|
|
||||||
|
memset(nulls, 0, sizeof(nulls));
|
||||||
|
|
||||||
|
{
|
||||||
|
pg_tle *fitted = (pg_tle *) palloc(sizeof(pg_tle));
|
||||||
|
sat_code_to_pg_tle(&result.fitted_tle, fitted);
|
||||||
|
values[0] = PointerGetDatum(fitted);
|
||||||
|
}
|
||||||
|
values[1] = Int32GetDatum(result.iterations);
|
||||||
|
values[2] = Float8GetDatum(result.rms_final);
|
||||||
|
values[3] = Float8GetDatum(result.rms_initial);
|
||||||
|
values[4] = CStringGetTextDatum(result.status);
|
||||||
|
|
||||||
|
tuple = heap_form_tuple(tupdesc, values, nulls);
|
||||||
|
PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ================================================================
|
/* ================================================================
|
||||||
* tle_fit_residuals(tle, eci_position[], timestamptz[])
|
* tle_fit_residuals(tle, eci_position[], timestamptz[])
|
||||||
* -> TABLE (t timestamptz, dx_km float8, dy_km float8, dz_km float8,
|
* -> TABLE (t timestamptz, dx_km float8, dy_km float8, dz_km float8,
|
||||||
|
|||||||
@ -223,7 +223,7 @@ compute_residuals_eci(const tle_t *tle, const od_observation_t *obs,
|
|||||||
*/
|
*/
|
||||||
static double
|
static double
|
||||||
compute_residuals_topo(const tle_t *tle, const od_observation_t *obs,
|
compute_residuals_topo(const tle_t *tle, const od_observation_t *obs,
|
||||||
int n_obs, const od_observer_t *observer,
|
int n_obs, const od_observer_t *observers,
|
||||||
double *residuals)
|
double *residuals)
|
||||||
{
|
{
|
||||||
double *params;
|
double *params;
|
||||||
@ -240,6 +240,7 @@ compute_residuals_topo(const tle_t *tle, const od_observation_t *obs,
|
|||||||
|
|
||||||
for (i = 0; i < n_obs; i++)
|
for (i = 0; i < n_obs; i++)
|
||||||
{
|
{
|
||||||
|
const od_observer_t *observer = &observers[obs[i].observer_idx];
|
||||||
double tsince = (obs[i].jd - tle->epoch) * 1440.0;
|
double tsince = (obs[i].jd - tle->epoch) * 1440.0;
|
||||||
double pos[3], vel[3];
|
double pos[3], vel[3];
|
||||||
int err;
|
int err;
|
||||||
@ -547,7 +548,7 @@ od_fit_tle(const od_observation_t *obs, int n_obs,
|
|||||||
rms_cur = compute_residuals_eci(¤t_tle, obs, n_obs, residuals);
|
rms_cur = compute_residuals_eci(¤t_tle, obs, n_obs, residuals);
|
||||||
else
|
else
|
||||||
rms_cur = compute_residuals_topo(¤t_tle, obs, n_obs,
|
rms_cur = compute_residuals_topo(¤t_tle, obs, n_obs,
|
||||||
config->observer, residuals);
|
config->observers, residuals);
|
||||||
|
|
||||||
if (rms_cur < 0.0)
|
if (rms_cur < 0.0)
|
||||||
{
|
{
|
||||||
@ -639,7 +640,7 @@ od_fit_tle(const od_observation_t *obs, int n_obs,
|
|||||||
compute_residuals_eci(&tle_pert, obs, n_obs, resid_pert);
|
compute_residuals_eci(&tle_pert, obs, n_obs, resid_pert);
|
||||||
else
|
else
|
||||||
compute_residuals_topo(&tle_pert, obs, n_obs,
|
compute_residuals_topo(&tle_pert, obs, n_obs,
|
||||||
config->observer, resid_pert);
|
config->observers, resid_pert);
|
||||||
|
|
||||||
/* Jacobian column j (column-major for LAPACK)
|
/* Jacobian column j (column-major for LAPACK)
|
||||||
* H = dG/dx where G is the computed observation function.
|
* H = dG/dx where G is the computed observation function.
|
||||||
@ -715,7 +716,7 @@ od_fit_tle(const od_observation_t *obs, int n_obs,
|
|||||||
rms_cur = compute_residuals_eci(¤t_tle, obs, n_obs, residuals);
|
rms_cur = compute_residuals_eci(¤t_tle, obs, n_obs, residuals);
|
||||||
else
|
else
|
||||||
rms_cur = compute_residuals_topo(¤t_tle, obs, n_obs,
|
rms_cur = compute_residuals_topo(¤t_tle, obs, n_obs,
|
||||||
config->observer, residuals);
|
config->observers, residuals);
|
||||||
|
|
||||||
if (rms_cur < 0.0)
|
if (rms_cur < 0.0)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -42,6 +42,7 @@ typedef struct
|
|||||||
{
|
{
|
||||||
double jd; /* Julian date of observation */
|
double jd; /* Julian date of observation */
|
||||||
double data[6]; /* ECI: [x,y,z,vx,vy,vz], topo: [az,el,range,...] */
|
double data[6]; /* ECI: [x,y,z,vx,vy,vz], topo: [az,el,range,...] */
|
||||||
|
int observer_idx; /* index into config->observers[] (topo mode) */
|
||||||
} od_observation_t;
|
} od_observation_t;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -63,7 +64,8 @@ typedef struct
|
|||||||
od_obs_type_t obs_type; /* ECI or topocentric */
|
od_obs_type_t obs_type; /* ECI or topocentric */
|
||||||
int fit_bstar; /* include B* as 7th state */
|
int fit_bstar; /* include B* as 7th state */
|
||||||
int max_iter; /* iteration limit */
|
int max_iter; /* iteration limit */
|
||||||
od_observer_t *observer; /* non-NULL for topocentric mode */
|
od_observer_t *observers; /* array of observers (topo mode) */
|
||||||
|
int n_observers; /* count (0 for ECI mode) */
|
||||||
} od_config_t;
|
} od_config_t;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
-- Tests tle_from_eci(), tle_from_topocentric(), and tle_fit_residuals().
|
-- Tests tle_from_eci(), tle_from_topocentric(), and tle_fit_residuals().
|
||||||
-- Uses round-trip methodology: propagate known TLE → fit from obs → compare.
|
-- Uses round-trip methodology: propagate known TLE → fit from obs → compare.
|
||||||
CREATE EXTENSION IF NOT EXISTS pg_orrery;
|
CREATE EXTENSION IF NOT EXISTS pg_orrery;
|
||||||
|
ALTER EXTENSION pg_orrery UPDATE TO '0.5.0';
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- Test 1: ECI round-trip (ISS-like LEO orbit)
|
-- Test 1: ECI round-trip (ISS-like LEO orbit)
|
||||||
--
|
--
|
||||||
@ -256,3 +257,158 @@ FROM result;
|
|||||||
t | t
|
t | t
|
||||||
(1 row)
|
(1 row)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 9: Multi-observer topocentric (MIT + Boulder observe ISS)
|
||||||
|
--
|
||||||
|
-- Two ground stations observe ISS, fit via multi-observer API.
|
||||||
|
-- Uses the overloaded tle_from_topocentric(topo[], ts[], observer[], int4[], ...).
|
||||||
|
-- ============================================================
|
||||||
|
WITH iss_tle AS (
|
||||||
|
SELECT E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS t
|
||||||
|
),
|
||||||
|
stations AS (
|
||||||
|
SELECT
|
||||||
|
'(42.36,-71.09,20)'::observer AS mit,
|
||||||
|
'(40.01,-105.27,1655)'::observer AS boulder
|
||||||
|
),
|
||||||
|
topo_obs AS (
|
||||||
|
SELECT
|
||||||
|
array_agg(topo ORDER BY rn) AS observations,
|
||||||
|
array_agg(ts ORDER BY rn) AS times,
|
||||||
|
array_agg(obs_id ORDER BY rn) AS observer_ids
|
||||||
|
FROM (
|
||||||
|
-- MIT observations (observer_id = 0)
|
||||||
|
SELECT observe(t, mit, ts) AS topo, ts, 0 AS obs_id,
|
||||||
|
row_number() OVER (ORDER BY ts) AS rn
|
||||||
|
FROM iss_tle, stations,
|
||||||
|
generate_series(
|
||||||
|
'2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 13:00:00+00'::timestamptz,
|
||||||
|
'5 minutes'::interval
|
||||||
|
) AS ts
|
||||||
|
UNION ALL
|
||||||
|
-- Boulder observations (observer_id = 1)
|
||||||
|
SELECT observe(t, boulder, ts) AS topo, ts, 1 AS obs_id,
|
||||||
|
row_number() OVER (ORDER BY ts) + 100 AS rn
|
||||||
|
FROM iss_tle, stations,
|
||||||
|
generate_series(
|
||||||
|
'2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 13:00:00+00'::timestamptz,
|
||||||
|
'5 minutes'::interval
|
||||||
|
) AS ts
|
||||||
|
) sub
|
||||||
|
),
|
||||||
|
result AS (
|
||||||
|
SELECT (tle_from_topocentric(
|
||||||
|
observations, times,
|
||||||
|
ARRAY[mit, boulder], observer_ids,
|
||||||
|
t, false, 20
|
||||||
|
)).*
|
||||||
|
FROM topo_obs, stations, iss_tle
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
status = 'converged' AS did_converge,
|
||||||
|
rms_final < 10.0 AS rms_under_10km
|
||||||
|
FROM result;
|
||||||
|
did_converge | rms_under_10km
|
||||||
|
--------------+----------------
|
||||||
|
t | t
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 10: Single observer via multi-observer API
|
||||||
|
--
|
||||||
|
-- Verify the multi-observer path produces equivalent results
|
||||||
|
-- when all observations come from one station.
|
||||||
|
-- ============================================================
|
||||||
|
WITH iss_tle AS (
|
||||||
|
SELECT E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS t
|
||||||
|
),
|
||||||
|
mit AS (
|
||||||
|
SELECT '(42.36,-71.09,20)'::observer AS obs
|
||||||
|
),
|
||||||
|
topo_obs AS (
|
||||||
|
SELECT
|
||||||
|
array_agg(observe(t, obs, ts) ORDER BY ts) AS observations,
|
||||||
|
array_agg(ts ORDER BY ts) AS times,
|
||||||
|
array_agg(0) AS observer_ids
|
||||||
|
FROM iss_tle, mit,
|
||||||
|
generate_series(
|
||||||
|
'2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 13:30:00+00'::timestamptz,
|
||||||
|
'5 minutes'::interval
|
||||||
|
) AS ts
|
||||||
|
),
|
||||||
|
result AS (
|
||||||
|
SELECT (tle_from_topocentric(
|
||||||
|
observations, times,
|
||||||
|
ARRAY[obs], observer_ids,
|
||||||
|
t, false, 20
|
||||||
|
)).*
|
||||||
|
FROM topo_obs, mit, iss_tle
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
status = 'converged' AS did_converge,
|
||||||
|
rms_final < 10.0 AS rms_under_10km
|
||||||
|
FROM result;
|
||||||
|
did_converge | rms_under_10km
|
||||||
|
--------------+----------------
|
||||||
|
t | t
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 11: Error - observer_id out of range
|
||||||
|
-- ============================================================
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM tle_from_topocentric(
|
||||||
|
ARRAY[
|
||||||
|
observe(
|
||||||
|
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle,
|
||||||
|
'(42.36,-71.09,20)'::observer,
|
||||||
|
'2024-01-01 12:00:00+00'::timestamptz
|
||||||
|
),
|
||||||
|
observe(
|
||||||
|
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle,
|
||||||
|
'(42.36,-71.09,20)'::observer,
|
||||||
|
'2024-01-01 12:05:00+00'::timestamptz
|
||||||
|
),
|
||||||
|
observe(
|
||||||
|
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle,
|
||||||
|
'(42.36,-71.09,20)'::observer,
|
||||||
|
'2024-01-01 12:10:00+00'::timestamptz
|
||||||
|
),
|
||||||
|
observe(
|
||||||
|
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle,
|
||||||
|
'(42.36,-71.09,20)'::observer,
|
||||||
|
'2024-01-01 12:15:00+00'::timestamptz
|
||||||
|
),
|
||||||
|
observe(
|
||||||
|
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle,
|
||||||
|
'(42.36,-71.09,20)'::observer,
|
||||||
|
'2024-01-01 12:20:00+00'::timestamptz
|
||||||
|
),
|
||||||
|
observe(
|
||||||
|
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle,
|
||||||
|
'(42.36,-71.09,20)'::observer,
|
||||||
|
'2024-01-01 12:25:00+00'::timestamptz
|
||||||
|
)
|
||||||
|
],
|
||||||
|
ARRAY[
|
||||||
|
'2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 12:05:00+00'::timestamptz,
|
||||||
|
'2024-01-01 12:10:00+00'::timestamptz,
|
||||||
|
'2024-01-01 12:15:00+00'::timestamptz,
|
||||||
|
'2024-01-01 12:20:00+00'::timestamptz,
|
||||||
|
'2024-01-01 12:25:00+00'::timestamptz
|
||||||
|
],
|
||||||
|
ARRAY['(42.36,-71.09,20)'::observer],
|
||||||
|
ARRAY[5, 0, 0, 0, 0, 0], -- observer_id 5 out of range
|
||||||
|
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle
|
||||||
|
);
|
||||||
|
RAISE NOTICE 'ERROR: should have raised exception';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN invalid_parameter_value THEN
|
||||||
|
RAISE NOTICE 'OK: observer_id out of range error caught';
|
||||||
|
END $$;
|
||||||
|
NOTICE: OK: observer_id out of range error caught
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
-- Uses round-trip methodology: propagate known TLE → fit from obs → compare.
|
-- Uses round-trip methodology: propagate known TLE → fit from obs → compare.
|
||||||
|
|
||||||
CREATE EXTENSION IF NOT EXISTS pg_orrery;
|
CREATE EXTENSION IF NOT EXISTS pg_orrery;
|
||||||
|
ALTER EXTENSION pg_orrery UPDATE TO '0.5.0';
|
||||||
|
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- Test 1: ECI round-trip (ISS-like LEO orbit)
|
-- Test 1: ECI round-trip (ISS-like LEO orbit)
|
||||||
@ -237,3 +238,153 @@ SELECT
|
|||||||
rms_final < 2.0 AS rms_under_2km,
|
rms_final < 2.0 AS rms_under_2km,
|
||||||
status = 'converged' AS did_converge
|
status = 'converged' AS did_converge
|
||||||
FROM result;
|
FROM result;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 9: Multi-observer topocentric (MIT + Boulder observe ISS)
|
||||||
|
--
|
||||||
|
-- Two ground stations observe ISS, fit via multi-observer API.
|
||||||
|
-- Uses the overloaded tle_from_topocentric(topo[], ts[], observer[], int4[], ...).
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
WITH iss_tle AS (
|
||||||
|
SELECT E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS t
|
||||||
|
),
|
||||||
|
stations AS (
|
||||||
|
SELECT
|
||||||
|
'(42.36,-71.09,20)'::observer AS mit,
|
||||||
|
'(40.01,-105.27,1655)'::observer AS boulder
|
||||||
|
),
|
||||||
|
topo_obs AS (
|
||||||
|
SELECT
|
||||||
|
array_agg(topo ORDER BY rn) AS observations,
|
||||||
|
array_agg(ts ORDER BY rn) AS times,
|
||||||
|
array_agg(obs_id ORDER BY rn) AS observer_ids
|
||||||
|
FROM (
|
||||||
|
-- MIT observations (observer_id = 0)
|
||||||
|
SELECT observe(t, mit, ts) AS topo, ts, 0 AS obs_id,
|
||||||
|
row_number() OVER (ORDER BY ts) AS rn
|
||||||
|
FROM iss_tle, stations,
|
||||||
|
generate_series(
|
||||||
|
'2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 13:00:00+00'::timestamptz,
|
||||||
|
'5 minutes'::interval
|
||||||
|
) AS ts
|
||||||
|
UNION ALL
|
||||||
|
-- Boulder observations (observer_id = 1)
|
||||||
|
SELECT observe(t, boulder, ts) AS topo, ts, 1 AS obs_id,
|
||||||
|
row_number() OVER (ORDER BY ts) + 100 AS rn
|
||||||
|
FROM iss_tle, stations,
|
||||||
|
generate_series(
|
||||||
|
'2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 13:00:00+00'::timestamptz,
|
||||||
|
'5 minutes'::interval
|
||||||
|
) AS ts
|
||||||
|
) sub
|
||||||
|
),
|
||||||
|
result AS (
|
||||||
|
SELECT (tle_from_topocentric(
|
||||||
|
observations, times,
|
||||||
|
ARRAY[mit, boulder], observer_ids,
|
||||||
|
t, false, 20
|
||||||
|
)).*
|
||||||
|
FROM topo_obs, stations, iss_tle
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
status = 'converged' AS did_converge,
|
||||||
|
rms_final < 10.0 AS rms_under_10km
|
||||||
|
FROM result;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 10: Single observer via multi-observer API
|
||||||
|
--
|
||||||
|
-- Verify the multi-observer path produces equivalent results
|
||||||
|
-- when all observations come from one station.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
WITH iss_tle AS (
|
||||||
|
SELECT E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS t
|
||||||
|
),
|
||||||
|
mit AS (
|
||||||
|
SELECT '(42.36,-71.09,20)'::observer AS obs
|
||||||
|
),
|
||||||
|
topo_obs AS (
|
||||||
|
SELECT
|
||||||
|
array_agg(observe(t, obs, ts) ORDER BY ts) AS observations,
|
||||||
|
array_agg(ts ORDER BY ts) AS times,
|
||||||
|
array_agg(0) AS observer_ids
|
||||||
|
FROM iss_tle, mit,
|
||||||
|
generate_series(
|
||||||
|
'2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 13:30:00+00'::timestamptz,
|
||||||
|
'5 minutes'::interval
|
||||||
|
) AS ts
|
||||||
|
),
|
||||||
|
result AS (
|
||||||
|
SELECT (tle_from_topocentric(
|
||||||
|
observations, times,
|
||||||
|
ARRAY[obs], observer_ids,
|
||||||
|
t, false, 20
|
||||||
|
)).*
|
||||||
|
FROM topo_obs, mit, iss_tle
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
status = 'converged' AS did_converge,
|
||||||
|
rms_final < 10.0 AS rms_under_10km
|
||||||
|
FROM result;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 11: Error - observer_id out of range
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM tle_from_topocentric(
|
||||||
|
ARRAY[
|
||||||
|
observe(
|
||||||
|
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle,
|
||||||
|
'(42.36,-71.09,20)'::observer,
|
||||||
|
'2024-01-01 12:00:00+00'::timestamptz
|
||||||
|
),
|
||||||
|
observe(
|
||||||
|
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle,
|
||||||
|
'(42.36,-71.09,20)'::observer,
|
||||||
|
'2024-01-01 12:05:00+00'::timestamptz
|
||||||
|
),
|
||||||
|
observe(
|
||||||
|
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle,
|
||||||
|
'(42.36,-71.09,20)'::observer,
|
||||||
|
'2024-01-01 12:10:00+00'::timestamptz
|
||||||
|
),
|
||||||
|
observe(
|
||||||
|
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle,
|
||||||
|
'(42.36,-71.09,20)'::observer,
|
||||||
|
'2024-01-01 12:15:00+00'::timestamptz
|
||||||
|
),
|
||||||
|
observe(
|
||||||
|
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle,
|
||||||
|
'(42.36,-71.09,20)'::observer,
|
||||||
|
'2024-01-01 12:20:00+00'::timestamptz
|
||||||
|
),
|
||||||
|
observe(
|
||||||
|
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle,
|
||||||
|
'(42.36,-71.09,20)'::observer,
|
||||||
|
'2024-01-01 12:25:00+00'::timestamptz
|
||||||
|
)
|
||||||
|
],
|
||||||
|
ARRAY[
|
||||||
|
'2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 12:05:00+00'::timestamptz,
|
||||||
|
'2024-01-01 12:10:00+00'::timestamptz,
|
||||||
|
'2024-01-01 12:15:00+00'::timestamptz,
|
||||||
|
'2024-01-01 12:20:00+00'::timestamptz,
|
||||||
|
'2024-01-01 12:25:00+00'::timestamptz
|
||||||
|
],
|
||||||
|
ARRAY['(42.36,-71.09,20)'::observer],
|
||||||
|
ARRAY[5, 0, 0, 0, 0, 0], -- observer_id 5 out of range
|
||||||
|
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle
|
||||||
|
);
|
||||||
|
RAISE NOTICE 'ERROR: should have raised exception';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN invalid_parameter_value THEN
|
||||||
|
RAISE NOTICE 'OK: observer_id out of range error caught';
|
||||||
|
END $$;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user