From 59fd8ba74344bf941d7c0154573c85d157ece6bd Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Tue, 17 Feb 2026 15:59:11 -0700 Subject: [PATCH] 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. --- Makefile | 3 +- sql/pg_orrery--0.4.0--0.5.0.sql | 21 ++++ src/od_funcs.c | 177 ++++++++++++++++++++++++++++++-- src/od_solver.c | 9 +- src/od_solver.h | 4 +- test/expected/od_fit.out | 156 ++++++++++++++++++++++++++++ test/sql/od_fit.sql | 151 +++++++++++++++++++++++++++ 7 files changed, 507 insertions(+), 14 deletions(-) create mode 100644 sql/pg_orrery--0.4.0--0.5.0.sql diff --git a/Makefile b/Makefile index d270f69..689e024 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,8 @@ MODULE_big = 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 \ 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 OBJS = src/pg_orrery.o src/tle_type.o src/eci_type.o src/observer_type.o \ diff --git a/sql/pg_orrery--0.4.0--0.5.0.sql b/sql/pg_orrery--0.4.0--0.5.0.sql new file mode 100644 index 0000000..388fdcb --- /dev/null +++ b/sql/pg_orrery--0.4.0--0.5.0.sql @@ -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.'; diff --git a/src/od_funcs.c b/src/od_funcs.c index 0e58019..76dcd64 100644 --- a/src/od_funcs.c +++ b/src/od_funcs.c @@ -26,6 +26,7 @@ PG_FUNCTION_INFO_V1(tle_from_eci); PG_FUNCTION_INFO_V1(tle_from_topocentric); +PG_FUNCTION_INFO_V1(tle_from_topocentric_multi); PG_FUNCTION_INFO_V1(tle_fit_residuals); /* ================================================================ @@ -155,10 +156,11 @@ tle_from_eci(PG_FUNCTION_ARGS) /* Configure solver */ memset(&config, 0, sizeof(config)); - config.obs_type = OD_OBS_ECI; - config.fit_bstar = fit_bstar ? 1 : 0; - config.max_iter = max_iter; - config.observer = NULL; + config.obs_type = OD_OBS_ECI; + config.fit_bstar = fit_bstar ? 1 : 0; + config.max_iter = max_iter; + config.observers = NULL; + config.n_observers = 0; /* Convert seed TLE if provided */ if (has_seed) @@ -274,15 +276,17 @@ tle_from_topocentric(PG_FUNCTION_ARGS) obs[i].data[0] = topo->azimuth; obs[i].data[1] = topo->elevation; obs[i].data[2] = topo->range_km; + obs[i].observer_idx = 0; /* single observer */ } } /* 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.observer = &observer; + config.obs_type = OD_OBS_TOPO; + config.fit_bstar = fit_bstar ? 1 : 0; + config.max_iter = max_iter; + config.observers = &observer; + config.n_observers = 1; /* Seed TLE required for topocentric */ 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[]) * -> TABLE (t timestamptz, dx_km float8, dy_km float8, dz_km float8, diff --git a/src/od_solver.c b/src/od_solver.c index de8be32..69efad5 100644 --- a/src/od_solver.c +++ b/src/od_solver.c @@ -223,7 +223,7 @@ compute_residuals_eci(const tle_t *tle, const od_observation_t *obs, */ static double 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 *params; @@ -240,6 +240,7 @@ compute_residuals_topo(const tle_t *tle, const od_observation_t *obs, 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 pos[3], vel[3]; 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); else rms_cur = compute_residuals_topo(¤t_tle, obs, n_obs, - config->observer, residuals); + config->observers, residuals); 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); else compute_residuals_topo(&tle_pert, obs, n_obs, - config->observer, resid_pert); + config->observers, resid_pert); /* Jacobian column j (column-major for LAPACK) * 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); else rms_cur = compute_residuals_topo(¤t_tle, obs, n_obs, - config->observer, residuals); + config->observers, residuals); if (rms_cur < 0.0) { diff --git a/src/od_solver.h b/src/od_solver.h index 0119389..c56b03c 100644 --- a/src/od_solver.h +++ b/src/od_solver.h @@ -42,6 +42,7 @@ typedef struct { double jd; /* Julian date of observation */ 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; /* @@ -63,7 +64,8 @@ typedef struct od_obs_type_t obs_type; /* ECI or topocentric */ int fit_bstar; /* include B* as 7th state */ 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; /* diff --git a/test/expected/od_fit.out b/test/expected/od_fit.out index 856eb56..de4d409 100644 --- a/test/expected/od_fit.out +++ b/test/expected/od_fit.out @@ -3,6 +3,7 @@ -- Tests tle_from_eci(), tle_from_topocentric(), and tle_fit_residuals(). -- Uses round-trip methodology: propagate known TLE → fit from obs → compare. 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) -- @@ -256,3 +257,158 @@ FROM result; t | t (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 diff --git a/test/sql/od_fit.sql b/test/sql/od_fit.sql index ea0d582..882b218 100644 --- a/test/sql/od_fit.sql +++ b/test/sql/od_fit.sql @@ -4,6 +4,7 @@ -- Uses round-trip methodology: propagate known TLE → fit from obs → compare. 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) @@ -237,3 +238,153 @@ SELECT rms_final < 2.0 AS rms_under_2km, status = 'converged' AS did_converge 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 $$;