Add range rate fitting, weighted observations, and Gauss angles-only IOD (v0.6.0)
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.
This commit is contained in:
parent
6e17513885
commit
adfb6949e1
1
.gitignore
vendored
1
.gitignore
vendored
@ -23,6 +23,7 @@ test/matrix-logs/
|
|||||||
test/test_de_reader
|
test/test_de_reader
|
||||||
test/test_od_math
|
test/test_od_math
|
||||||
test/test_od_iod
|
test/test_od_iod
|
||||||
|
test/test_od_gauss
|
||||||
|
|
||||||
# Docs site
|
# Docs site
|
||||||
docs/node_modules/
|
docs/node_modules/
|
||||||
|
|||||||
@ -47,6 +47,7 @@ RUN su postgres -c "/usr/lib/postgresql/${PG_MAJOR}/bin/initdb -D /tmp/pgtest" &
|
|||||||
RUN make test-de-reader
|
RUN make test-de-reader
|
||||||
RUN make test-od-math
|
RUN make test-od-math
|
||||||
RUN make test-od-iod
|
RUN make test-od-iod
|
||||||
|
RUN make test-od-gauss
|
||||||
|
|
||||||
# Capture artifacts under /pg_orrery prefix for the next stage
|
# Capture artifacts under /pg_orrery prefix for the next stage
|
||||||
RUN make PG_CONFIG=${PG_CONFIG} DESTDIR=/pg_orrery install
|
RUN make PG_CONFIG=${PG_CONFIG} DESTDIR=/pg_orrery install
|
||||||
|
|||||||
11
Makefile
11
Makefile
@ -3,7 +3,8 @@ 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.5.0.sql sql/pg_orrery--0.4.0--0.5.0.sql
|
sql/pg_orrery--0.5.0.sql sql/pg_orrery--0.4.0--0.5.0.sql \
|
||||||
|
sql/pg_orrery--0.6.0.sql sql/pg_orrery--0.5.0--0.6.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 \
|
||||||
@ -69,6 +70,14 @@ test-od-iod: test/test_od_iod.c src/od_iod.c src/od_iod.h src/od_math.c src/od_m
|
|||||||
|
|
||||||
.PHONY: test-od-iod
|
.PHONY: test-od-iod
|
||||||
|
|
||||||
|
# ── Standalone Gauss IOD unit test (no PostgreSQL dependency) ──
|
||||||
|
# Gauss angles-only IOD, RA/Dec round-trip, Herrick-Gibbs fallback.
|
||||||
|
test-od-gauss: test/test_od_gauss.c src/od_iod.c src/od_iod.h src/od_math.c src/od_math.h
|
||||||
|
$(CC) -Wall -Werror -Isrc -o test/test_od_gauss $< src/od_iod.c src/od_math.c -lm
|
||||||
|
./test/test_od_gauss
|
||||||
|
|
||||||
|
.PHONY: test-od-gauss
|
||||||
|
|
||||||
# ── PG version test matrix ─────────────────────────────────
|
# ── PG version test matrix ─────────────────────────────────
|
||||||
PG_TEST_VERSIONS ?= 14 15 16 17 18
|
PG_TEST_VERSIONS ?= 14 15 16 17 18
|
||||||
|
|
||||||
|
|||||||
6
TODO
6
TODO
@ -1,7 +1 @@
|
|||||||
|
|
||||||
- Gauss method for angles-only initial orbit determination
|
|
||||||
(eliminates seed requirement for sensors without ranging)
|
|
||||||
- Weighted observations (per-obs covariance weighting for
|
|
||||||
heterogeneous sensor fusion)
|
|
||||||
- Range rate fitting in topocentric mode (currently reserved
|
|
||||||
via vel_ecef in residual computation)
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
comment = 'A database orrery — celestial mechanics types and functions for PostgreSQL'
|
comment = 'A database orrery — celestial mechanics types and functions for PostgreSQL'
|
||||||
default_version = '0.5.0'
|
default_version = '0.6.0'
|
||||||
module_pathname = '$libdir/pg_orrery'
|
module_pathname = '$libdir/pg_orrery'
|
||||||
relocatable = true
|
relocatable = true
|
||||||
|
|||||||
114
sql/pg_orrery--0.5.0--0.6.0.sql
Normal file
114
sql/pg_orrery--0.5.0--0.6.0.sql
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
-- pg_orrery 0.5.0 -> 0.6.0 migration
|
||||||
|
--
|
||||||
|
-- Adds range rate fitting, per-observation weights, and
|
||||||
|
-- angles-only orbit determination (Gauss method).
|
||||||
|
--
|
||||||
|
-- Range rate and weights change the input signatures of
|
||||||
|
-- tle_from_eci and tle_from_topocentric (added trailing
|
||||||
|
-- DEFAULT parameters), which requires DROP + re-CREATE.
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Drop old OD function signatures
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
DROP FUNCTION IF EXISTS tle_from_eci(eci_position[], timestamptz[], tle, boolean, int4);
|
||||||
|
DROP FUNCTION IF EXISTS tle_from_topocentric(topocentric[], timestamptz[], observer, tle, boolean, int4);
|
||||||
|
DROP FUNCTION IF EXISTS tle_from_topocentric(topocentric[], timestamptz[], observer[], int4[], tle, boolean, int4);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Re-create with range_rate + weights parameters
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Fit TLE from ECI position/velocity ephemeris
|
||||||
|
-- weights: per-observation weighting (NULL = uniform)
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_from_eci(
|
||||||
|
positions eci_position[], times timestamptz[],
|
||||||
|
seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false,
|
||||||
|
max_iter int4 DEFAULT 15,
|
||||||
|
weights float8[] DEFAULT NULL,
|
||||||
|
OUT fitted_tle tle, OUT iterations int4,
|
||||||
|
OUT rms_final float8, OUT rms_initial float8, OUT status text,
|
||||||
|
OUT condition_number float8, OUT covariance float8[], OUT nstate int4
|
||||||
|
) RETURNS RECORD
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_from_eci(eci_position[], timestamptz[], tle, boolean, int4, float8[]) IS
|
||||||
|
'Fit a TLE from ECI position/velocity observations via differential correction. Optional per-observation weights for heterogeneous sensor fusion. Returns fitted TLE, RMS residuals, convergence status, condition number, and formal covariance matrix.';
|
||||||
|
|
||||||
|
-- Fit TLE from topocentric observations (az/el/range) — single observer
|
||||||
|
-- fit_range_rate: include range_rate as 4th residual component
|
||||||
|
-- weights: per-observation weighting (NULL = uniform)
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_from_topocentric(
|
||||||
|
observations topocentric[], times timestamptz[],
|
||||||
|
obs observer,
|
||||||
|
seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false,
|
||||||
|
max_iter int4 DEFAULT 15,
|
||||||
|
fit_range_rate boolean DEFAULT false,
|
||||||
|
weights float8[] DEFAULT NULL,
|
||||||
|
OUT fitted_tle tle, OUT iterations int4,
|
||||||
|
OUT rms_final float8, OUT rms_initial float8, OUT status text,
|
||||||
|
OUT condition_number float8, OUT covariance float8[], OUT nstate int4
|
||||||
|
) RETURNS RECORD
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_from_topocentric(topocentric[], timestamptz[], observer, tle, boolean, int4, boolean, float8[]) IS
|
||||||
|
'Fit a TLE from topocentric (az/el/range) observations via differential correction. Optional range_rate fitting and per-observation weights. Returns fitted TLE, RMS residuals, convergence status, condition number, and formal covariance matrix.';
|
||||||
|
|
||||||
|
-- Fit TLE from topocentric observations — multiple observers
|
||||||
|
|
||||||
|
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,
|
||||||
|
fit_range_rate boolean DEFAULT false,
|
||||||
|
weights float8[] DEFAULT NULL,
|
||||||
|
OUT fitted_tle tle, OUT iterations int4,
|
||||||
|
OUT rms_final float8, OUT rms_initial float8, OUT status text,
|
||||||
|
OUT condition_number float8, OUT covariance float8[], OUT nstate int4
|
||||||
|
) 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, boolean, float8[]) IS
|
||||||
|
'Fit a TLE from topocentric observations collected by multiple ground stations. observer_ids[i] indexes into observers[]. Optional range_rate fitting and per-observation weights.';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Angles-only orbit determination (new in v0.6.0)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Fit TLE from RA/Dec observations — single observer
|
||||||
|
-- Uses Gauss method for initial orbit determination when no seed is provided.
|
||||||
|
-- RA in hours [0,24), Dec in degrees [-90,90] (matches star_observe convention).
|
||||||
|
-- RMS output is in radians for angles-only mode.
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_from_angles(
|
||||||
|
ra_hours float8[], dec_degrees float8[],
|
||||||
|
times timestamptz[],
|
||||||
|
obs observer,
|
||||||
|
seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false,
|
||||||
|
max_iter int4 DEFAULT 15,
|
||||||
|
weights float8[] DEFAULT NULL,
|
||||||
|
OUT fitted_tle tle, OUT iterations int4,
|
||||||
|
OUT rms_final float8, OUT rms_initial float8, OUT status text,
|
||||||
|
OUT condition_number float8, OUT covariance float8[], OUT nstate int4
|
||||||
|
) RETURNS RECORD AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_from_angles(float8[], float8[], timestamptz[], observer, tle, boolean, int4, float8[]) IS
|
||||||
|
'Fit a TLE from angles-only (RA/Dec) observations via Gauss IOD + differential correction. RA in hours [0,24), Dec in degrees [-90,90]. RMS output in radians. Uses Gauss method for seed-free initial guess.';
|
||||||
|
|
||||||
|
-- Fit TLE from RA/Dec observations — multiple observers
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_from_angles(
|
||||||
|
ra_hours float8[], dec_degrees float8[],
|
||||||
|
times timestamptz[],
|
||||||
|
observers observer[], observer_ids int4[],
|
||||||
|
seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false,
|
||||||
|
max_iter int4 DEFAULT 15,
|
||||||
|
weights float8[] DEFAULT NULL,
|
||||||
|
OUT fitted_tle tle, OUT iterations int4,
|
||||||
|
OUT rms_final float8, OUT rms_initial float8, OUT status text,
|
||||||
|
OUT condition_number float8, OUT covariance float8[], OUT nstate int4
|
||||||
|
) RETURNS RECORD
|
||||||
|
AS 'MODULE_PATHNAME', 'tle_from_angles_multi'
|
||||||
|
LANGUAGE C STABLE PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_from_angles(float8[], float8[], timestamptz[], observer[], int4[], tle, boolean, int4, float8[]) IS
|
||||||
|
'Fit a TLE from angles-only (RA/Dec) observations from multiple ground stations via Gauss IOD + differential correction.';
|
||||||
885
sql/pg_orrery--0.6.0.sql
Normal file
885
sql/pg_orrery--0.6.0.sql
Normal file
@ -0,0 +1,885 @@
|
|||||||
|
-- pg_orrery -- Orbital mechanics types and functions for PostgreSQL
|
||||||
|
--
|
||||||
|
-- Types: tle, eci_position, geodetic, topocentric, observer, pass_event
|
||||||
|
-- Provides SGP4/SDP4 propagation, coordinate transforms, pass prediction,
|
||||||
|
-- and GiST indexing on altitude bands for conjunction screening.
|
||||||
|
--
|
||||||
|
-- All propagation uses WGS-72 constants (matching TLE mean element fitting).
|
||||||
|
-- Coordinate output uses WGS-84 (matching modern geodetic standards).
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Shell types (forward declarations)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TYPE tle;
|
||||||
|
CREATE TYPE eci_position;
|
||||||
|
CREATE TYPE geodetic;
|
||||||
|
CREATE TYPE topocentric;
|
||||||
|
CREATE TYPE observer;
|
||||||
|
CREATE TYPE pass_event;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- TLE type: Two-Line Element set
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_in(cstring) RETURNS tle
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_out(tle) RETURNS cstring
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_recv(internal) RETURNS tle
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_send(tle) RETURNS bytea
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE TYPE tle (
|
||||||
|
INPUT = tle_in,
|
||||||
|
OUTPUT = tle_out,
|
||||||
|
RECEIVE = tle_recv,
|
||||||
|
SEND = tle_send,
|
||||||
|
INTERNALLENGTH = 112,
|
||||||
|
ALIGNMENT = double,
|
||||||
|
STORAGE = plain
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TYPE tle IS 'Two-Line Element set — parsed mean orbital elements for SGP4/SDP4 propagation';
|
||||||
|
|
||||||
|
-- TLE accessor functions
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_epoch(tle) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_epoch(tle) IS 'TLE epoch as Julian date (UTC)';
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_norad_id(tle) RETURNS int4
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_norad_id(tle) IS 'NORAD catalog number';
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_inclination(tle) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_inclination(tle) IS 'Orbital inclination in degrees';
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_eccentricity(tle) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_eccentricity(tle) IS 'Orbital eccentricity (dimensionless)';
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_raan(tle) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_raan(tle) IS 'Right ascension of ascending node in degrees';
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_arg_perigee(tle) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_arg_perigee(tle) IS 'Argument of perigee in degrees';
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_mean_anomaly(tle) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_mean_anomaly(tle) IS 'Mean anomaly in degrees';
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_mean_motion(tle) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_mean_motion(tle) IS 'Mean motion in revolutions per day';
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_bstar(tle) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_bstar(tle) IS 'B* drag coefficient (1/earth-radii)';
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_period(tle) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_period(tle) IS 'Orbital period in minutes';
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_age(tle, timestamptz) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_age(tle, timestamptz) IS 'TLE age in days (positive = past epoch, negative = before epoch)';
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_perigee(tle) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_perigee(tle) IS 'Perigee altitude in km above WGS-72 ellipsoid';
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_apogee(tle) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_apogee(tle) IS 'Apogee altitude in km above WGS-72 ellipsoid';
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_intl_desig(tle) RETURNS text
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_intl_desig(tle) IS 'International designator (COSPAR ID)';
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_from_lines(text, text) RETURNS tle
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_from_lines(text, text) IS
|
||||||
|
'Construct TLE from separate line1/line2 text columns';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- ECI position type: True Equator Mean Equinox (TEME) frame
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION eci_in(cstring) RETURNS eci_position
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION eci_out(eci_position) RETURNS cstring
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION eci_recv(internal) RETURNS eci_position
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION eci_send(eci_position) RETURNS bytea
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE TYPE eci_position (
|
||||||
|
INPUT = eci_in,
|
||||||
|
OUTPUT = eci_out,
|
||||||
|
RECEIVE = eci_recv,
|
||||||
|
SEND = eci_send,
|
||||||
|
INTERNALLENGTH = 48,
|
||||||
|
ALIGNMENT = double,
|
||||||
|
STORAGE = plain
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TYPE eci_position IS 'Earth-Centered Inertial position and velocity in TEME frame (km, km/s)';
|
||||||
|
|
||||||
|
-- ECI accessor functions
|
||||||
|
|
||||||
|
CREATE FUNCTION eci_x(eci_position) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION eci_y(eci_position) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION eci_z(eci_position) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION eci_vx(eci_position) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION eci_vy(eci_position) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION eci_vz(eci_position) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION eci_speed(eci_position) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION eci_speed(eci_position) IS 'Velocity magnitude in km/s';
|
||||||
|
|
||||||
|
CREATE FUNCTION eci_altitude(eci_position) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION eci_altitude(eci_position) IS 'Approximate geocentric altitude in km (radius - WGS72_AE)';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Geodetic type: WGS-84 latitude/longitude/altitude
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION geodetic_in(cstring) RETURNS geodetic
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION geodetic_out(geodetic) RETURNS cstring
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION geodetic_recv(internal) RETURNS geodetic
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION geodetic_send(geodetic) RETURNS bytea
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE TYPE geodetic (
|
||||||
|
INPUT = geodetic_in,
|
||||||
|
OUTPUT = geodetic_out,
|
||||||
|
RECEIVE = geodetic_recv,
|
||||||
|
SEND = geodetic_send,
|
||||||
|
INTERNALLENGTH = 24,
|
||||||
|
ALIGNMENT = double,
|
||||||
|
STORAGE = plain
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TYPE geodetic IS 'Geodetic coordinates on WGS-84 ellipsoid (lat/lon in degrees, altitude in km)';
|
||||||
|
|
||||||
|
CREATE FUNCTION geodetic_lat(geodetic) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION geodetic_lon(geodetic) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION geodetic_alt(geodetic) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Topocentric type: observer-relative az/el/range
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION topocentric_in(cstring) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION topocentric_out(topocentric) RETURNS cstring
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION topocentric_recv(internal) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION topocentric_send(topocentric) RETURNS bytea
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE TYPE topocentric (
|
||||||
|
INPUT = topocentric_in,
|
||||||
|
OUTPUT = topocentric_out,
|
||||||
|
RECEIVE = topocentric_recv,
|
||||||
|
SEND = topocentric_send,
|
||||||
|
INTERNALLENGTH = 32,
|
||||||
|
ALIGNMENT = double,
|
||||||
|
STORAGE = plain
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TYPE topocentric IS 'Topocentric coordinates relative to observer (azimuth, elevation, range, range rate)';
|
||||||
|
|
||||||
|
CREATE FUNCTION topo_azimuth(topocentric) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION topo_azimuth(topocentric) IS 'Azimuth in degrees (0=N, 90=E, 180=S, 270=W)';
|
||||||
|
|
||||||
|
CREATE FUNCTION topo_elevation(topocentric) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION topo_elevation(topocentric) IS 'Elevation in degrees (0=horizon, 90=zenith)';
|
||||||
|
|
||||||
|
CREATE FUNCTION topo_range(topocentric) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION topo_range(topocentric) IS 'Slant range in km';
|
||||||
|
|
||||||
|
CREATE FUNCTION topo_range_rate(topocentric) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION topo_range_rate(topocentric) IS 'Range rate in km/s (positive = receding)';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Observer type: ground station location
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION observer_in(cstring) RETURNS observer
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION observer_out(observer) RETURNS cstring
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION observer_recv(internal) RETURNS observer
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION observer_send(observer) RETURNS bytea
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE TYPE observer (
|
||||||
|
INPUT = observer_in,
|
||||||
|
OUTPUT = observer_out,
|
||||||
|
RECEIVE = observer_recv,
|
||||||
|
SEND = observer_send,
|
||||||
|
INTERNALLENGTH = 24,
|
||||||
|
ALIGNMENT = double,
|
||||||
|
STORAGE = plain
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TYPE observer IS 'Ground observer location (accepts: 40.0N 105.3W 1655m or decimal degrees)';
|
||||||
|
|
||||||
|
CREATE FUNCTION observer_lat(observer) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION observer_lat(observer) IS 'Latitude in degrees (positive = North)';
|
||||||
|
|
||||||
|
CREATE FUNCTION observer_lon(observer) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION observer_lon(observer) IS 'Longitude in degrees (positive = East)';
|
||||||
|
|
||||||
|
CREATE FUNCTION observer_alt(observer) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION observer_alt(observer) IS 'Altitude in meters above WGS-84 ellipsoid';
|
||||||
|
|
||||||
|
CREATE FUNCTION observer_from_geodetic(float8, float8, float8 DEFAULT 0.0) RETURNS observer
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION observer_from_geodetic(float8, float8, float8) IS
|
||||||
|
'Construct observer from lat (deg), lon (deg), altitude (meters). Avoids text formatting round-trips.';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Pass event type: satellite visibility window
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION pass_event_in(cstring) RETURNS pass_event
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION pass_event_out(pass_event) RETURNS cstring
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION pass_event_recv(internal) RETURNS pass_event
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION pass_event_send(pass_event) RETURNS bytea
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE TYPE pass_event (
|
||||||
|
INPUT = pass_event_in,
|
||||||
|
OUTPUT = pass_event_out,
|
||||||
|
RECEIVE = pass_event_recv,
|
||||||
|
SEND = pass_event_send,
|
||||||
|
INTERNALLENGTH = 48,
|
||||||
|
ALIGNMENT = double,
|
||||||
|
STORAGE = plain
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TYPE pass_event IS 'Satellite pass event (AOS/MAX/LOS times, max elevation, AOS/LOS azimuths)';
|
||||||
|
|
||||||
|
CREATE FUNCTION pass_aos_time(pass_event) RETURNS timestamptz
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION pass_aos_time(pass_event) IS 'Acquisition of signal time';
|
||||||
|
|
||||||
|
CREATE FUNCTION pass_max_el_time(pass_event) RETURNS timestamptz
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION pass_max_el_time(pass_event) IS 'Maximum elevation time';
|
||||||
|
|
||||||
|
CREATE FUNCTION pass_los_time(pass_event) RETURNS timestamptz
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION pass_los_time(pass_event) IS 'Loss of signal time';
|
||||||
|
|
||||||
|
CREATE FUNCTION pass_max_elevation(pass_event) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION pass_max_elevation(pass_event) IS 'Maximum elevation in degrees';
|
||||||
|
|
||||||
|
CREATE FUNCTION pass_aos_azimuth(pass_event) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION pass_aos_azimuth(pass_event) IS 'AOS azimuth in degrees (0=N)';
|
||||||
|
|
||||||
|
CREATE FUNCTION pass_los_azimuth(pass_event) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION pass_los_azimuth(pass_event) IS 'LOS azimuth in degrees (0=N)';
|
||||||
|
|
||||||
|
CREATE FUNCTION pass_duration(pass_event) RETURNS interval
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION pass_duration(pass_event) IS 'Pass duration (LOS - AOS)';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- SGP4/SDP4 propagation functions
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION sgp4_propagate(tle, timestamptz) RETURNS eci_position
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION sgp4_propagate(tle, timestamptz) IS
|
||||||
|
'Propagate TLE to a point in time using SGP4 (near-earth) or SDP4 (deep-space). Returns TEME ECI position/velocity.';
|
||||||
|
|
||||||
|
CREATE FUNCTION sgp4_propagate_safe(tle, timestamptz) RETURNS eci_position
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION sgp4_propagate_safe(tle, timestamptz) IS
|
||||||
|
'Like sgp4_propagate but returns NULL on error instead of raising an exception. For batch queries with potentially invalid TLEs.';
|
||||||
|
|
||||||
|
CREATE FUNCTION sgp4_propagate_series(tle, timestamptz, timestamptz, interval)
|
||||||
|
RETURNS TABLE(t timestamptz, x float8, y float8, z float8, vx float8, vy float8, vz float8)
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE
|
||||||
|
ROWS 100;
|
||||||
|
COMMENT ON FUNCTION sgp4_propagate_series(tle, timestamptz, timestamptz, interval) IS
|
||||||
|
'Propagate TLE over a time range at regular intervals. Returns time series of TEME positions.';
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_distance(tle, tle, timestamptz) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_distance(tle, tle, timestamptz) IS
|
||||||
|
'Euclidean distance in km between two TLEs at a reference time';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Coordinate transform functions
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION eci_to_geodetic(eci_position, timestamptz) RETURNS geodetic
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION eci_to_geodetic(eci_position, timestamptz) IS
|
||||||
|
'Convert TEME ECI position to WGS-84 geodetic coordinates at given time';
|
||||||
|
|
||||||
|
CREATE FUNCTION eci_to_topocentric(eci_position, observer, timestamptz) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION eci_to_topocentric(eci_position, observer, timestamptz) IS
|
||||||
|
'Convert TEME ECI position to topocentric (az/el/range) relative to observer';
|
||||||
|
|
||||||
|
CREATE FUNCTION subsatellite_point(tle, timestamptz) RETURNS geodetic
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION subsatellite_point(tle, timestamptz) IS
|
||||||
|
'Subsatellite (nadir) point on WGS-84 ellipsoid at given time';
|
||||||
|
|
||||||
|
CREATE FUNCTION ground_track(tle, timestamptz, timestamptz, interval)
|
||||||
|
RETURNS TABLE(t timestamptz, lat float8, lon float8, alt float8)
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE
|
||||||
|
ROWS 100;
|
||||||
|
COMMENT ON FUNCTION ground_track(tle, timestamptz, timestamptz, interval) IS
|
||||||
|
'Ground track as time series of subsatellite points (lat/lon in degrees, alt in km)';
|
||||||
|
|
||||||
|
CREATE FUNCTION observe(tle, observer, timestamptz) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION observe(tle, observer, timestamptz) IS
|
||||||
|
'Propagate TLE and compute observer-relative look angles in one call. Returns topocentric (az/el/range/range_rate).';
|
||||||
|
|
||||||
|
CREATE FUNCTION observe_safe(tle, observer, timestamptz) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION observe_safe(tle, observer, timestamptz) IS
|
||||||
|
'Like observe() but returns NULL on propagation error. For batch queries with potentially invalid/decayed TLEs.';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Pass prediction functions
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION next_pass(tle, observer, timestamptz) RETURNS pass_event
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION next_pass(tle, observer, timestamptz) IS
|
||||||
|
'Find the next satellite pass over observer (searches up to 7 days ahead)';
|
||||||
|
|
||||||
|
CREATE FUNCTION predict_passes(tle, observer, timestamptz, timestamptz, float8 DEFAULT 0.0)
|
||||||
|
RETURNS SETOF pass_event
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE
|
||||||
|
ROWS 10;
|
||||||
|
COMMENT ON FUNCTION predict_passes(tle, observer, timestamptz, timestamptz, float8) IS
|
||||||
|
'Predict all satellite passes over observer in time window. Optional min_elevation in degrees.';
|
||||||
|
|
||||||
|
CREATE FUNCTION pass_visible(tle, observer, timestamptz, timestamptz) RETURNS boolean
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION pass_visible(tle, observer, timestamptz, timestamptz) IS
|
||||||
|
'True if any pass occurs over observer in the time window';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- GiST operator support functions
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Overlap operator: do orbital keys overlap in altitude AND inclination?
|
||||||
|
CREATE FUNCTION tle_overlap(tle, tle) RETURNS boolean
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
-- Altitude distance operator (altitude-only, for KNN ordering)
|
||||||
|
CREATE FUNCTION tle_alt_distance(tle, tle) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE OPERATOR && (
|
||||||
|
LEFTARG = tle,
|
||||||
|
RIGHTARG = tle,
|
||||||
|
FUNCTION = tle_overlap,
|
||||||
|
COMMUTATOR = &&,
|
||||||
|
RESTRICT = areasel,
|
||||||
|
JOIN = areajoinsel
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON OPERATOR && (tle, tle) IS 'Orbital key overlap (altitude band AND inclination range) — necessary condition for conjunction';
|
||||||
|
|
||||||
|
CREATE OPERATOR <-> (
|
||||||
|
LEFTARG = tle,
|
||||||
|
RIGHTARG = tle,
|
||||||
|
FUNCTION = tle_alt_distance,
|
||||||
|
COMMUTATOR = <->
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON OPERATOR <-> (tle, tle) IS 'Minimum altitude-band separation in km (0 if overlapping). Altitude-only — does not account for inclination. Use && for 2-D filtering.';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- GiST operator class for 2-D orbital indexing (altitude + inclination)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- GiST internal support functions
|
||||||
|
CREATE FUNCTION gist_tle_compress(internal) RETURNS internal
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION gist_tle_decompress(internal) RETURNS internal
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION gist_tle_consistent(internal, tle, smallint, oid, internal) RETURNS boolean
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION gist_tle_union(internal, internal) RETURNS internal
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION gist_tle_penalty(internal, internal, internal) RETURNS internal
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION gist_tle_picksplit(internal, internal) RETURNS internal
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION gist_tle_same(internal, internal, internal) RETURNS internal
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION gist_tle_distance(internal, tle, smallint, oid, internal) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE OPERATOR CLASS tle_ops
|
||||||
|
DEFAULT FOR TYPE tle USING gist AS
|
||||||
|
OPERATOR 3 && ,
|
||||||
|
OPERATOR 15 <-> (tle, tle) FOR ORDER BY float_ops,
|
||||||
|
FUNCTION 1 gist_tle_consistent(internal, tle, smallint, oid, internal),
|
||||||
|
FUNCTION 2 gist_tle_union(internal, internal),
|
||||||
|
FUNCTION 3 gist_tle_compress(internal),
|
||||||
|
FUNCTION 4 gist_tle_decompress(internal),
|
||||||
|
FUNCTION 5 gist_tle_penalty(internal, internal, internal),
|
||||||
|
FUNCTION 6 gist_tle_picksplit(internal, internal),
|
||||||
|
FUNCTION 7 gist_tle_same(internal, internal, internal),
|
||||||
|
FUNCTION 8 gist_tle_distance(internal, tle, smallint, oid, internal);
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Heliocentric type: ecliptic J2000 position in AU
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TYPE heliocentric;
|
||||||
|
|
||||||
|
CREATE FUNCTION heliocentric_in(cstring) RETURNS heliocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION heliocentric_out(heliocentric) RETURNS cstring
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION heliocentric_recv(internal) RETURNS heliocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION heliocentric_send(heliocentric) RETURNS bytea
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE TYPE heliocentric (
|
||||||
|
INPUT = heliocentric_in,
|
||||||
|
OUTPUT = heliocentric_out,
|
||||||
|
RECEIVE = heliocentric_recv,
|
||||||
|
SEND = heliocentric_send,
|
||||||
|
INTERNALLENGTH = 24,
|
||||||
|
ALIGNMENT = double,
|
||||||
|
STORAGE = plain
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TYPE heliocentric IS 'Heliocentric position in ecliptic J2000 frame (x, y, z in AU)';
|
||||||
|
|
||||||
|
CREATE FUNCTION helio_x(heliocentric) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION helio_x(heliocentric) IS 'X component in AU (ecliptic J2000, vernal equinox direction)';
|
||||||
|
|
||||||
|
CREATE FUNCTION helio_y(heliocentric) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION helio_y(heliocentric) IS 'Y component in AU (ecliptic J2000)';
|
||||||
|
|
||||||
|
CREATE FUNCTION helio_z(heliocentric) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION helio_z(heliocentric) IS 'Z component in AU (ecliptic J2000, north ecliptic pole)';
|
||||||
|
|
||||||
|
CREATE FUNCTION helio_distance(heliocentric) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION helio_distance(heliocentric) IS 'Heliocentric distance in AU';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Star observation functions
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION star_observe(float8, float8, observer, timestamptz) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION star_observe(float8, float8, observer, timestamptz) IS
|
||||||
|
'Observe a star from (ra_hours J2000, dec_degrees J2000, observer, time). Returns topocentric az/el. Range is 0 (infinite distance).';
|
||||||
|
|
||||||
|
CREATE FUNCTION star_observe_safe(float8, float8, observer, timestamptz) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION star_observe_safe(float8, float8, observer, timestamptz) IS
|
||||||
|
'Like star_observe but returns NULL on invalid inputs. For batch queries over star catalogs.';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Keplerian propagation functions
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION kepler_propagate(
|
||||||
|
q_au float8, eccentricity float8,
|
||||||
|
inclination_deg float8, arg_perihelion_deg float8,
|
||||||
|
long_asc_node_deg float8, perihelion_jd float8,
|
||||||
|
t timestamptz
|
||||||
|
) RETURNS heliocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION kepler_propagate(float8, float8, float8, float8, float8, float8, timestamptz) IS
|
||||||
|
'Two-body Keplerian propagation from classical orbital elements. Returns heliocentric ecliptic J2000 position in AU. Handles elliptic, parabolic, and hyperbolic orbits.';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Comet observation
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION comet_observe(
|
||||||
|
q_au float8, eccentricity float8,
|
||||||
|
inclination_deg float8, arg_perihelion_deg float8,
|
||||||
|
long_asc_node_deg float8, perihelion_jd float8,
|
||||||
|
earth_x_au float8, earth_y_au float8, earth_z_au float8,
|
||||||
|
obs observer, t timestamptz
|
||||||
|
) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION comet_observe(float8, float8, float8, float8, float8, float8, float8, float8, float8, observer, timestamptz) IS
|
||||||
|
'Observe a comet/asteroid from orbital elements. Requires Earth heliocentric ecliptic J2000 position (AU). Returns topocentric az/el with geocentric range in km.';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- VSOP87 planets, ELP82B Moon, Sun observation
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION planet_heliocentric(int4, timestamptz) RETURNS heliocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION planet_heliocentric(int4, timestamptz) IS
|
||||||
|
'VSOP87 heliocentric ecliptic J2000 position (AU). Body IDs: 0=Sun, 1=Mercury, ..., 8=Neptune.';
|
||||||
|
|
||||||
|
CREATE FUNCTION planet_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION planet_observe(int4, observer, timestamptz) IS
|
||||||
|
'Observe a planet from (body_id 1-8, observer, time). Returns topocentric az/el with geocentric range in km.';
|
||||||
|
|
||||||
|
CREATE FUNCTION sun_observe(observer, timestamptz) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION sun_observe(observer, timestamptz) IS
|
||||||
|
'Observe the Sun from (observer, time). Returns topocentric az/el with geocentric range in km.';
|
||||||
|
|
||||||
|
CREATE FUNCTION moon_observe(observer, timestamptz) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION moon_observe(observer, timestamptz) IS
|
||||||
|
'Observe the Moon via ELP2000-82B from (observer, time). Returns topocentric az/el with geocentric range in km.';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Planetary moon observation
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION galilean_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION galilean_observe(int4, observer, timestamptz) IS
|
||||||
|
'Observe a Galilean moon of Jupiter via L1.2 theory. Body IDs: 0=Io, 1=Europa, 2=Ganymede, 3=Callisto.';
|
||||||
|
|
||||||
|
CREATE FUNCTION saturn_moon_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION saturn_moon_observe(int4, observer, timestamptz) IS
|
||||||
|
'Observe a Saturn moon via TASS 1.7. Body IDs: 0=Mimas, 1=Enceladus, 2=Tethys, 3=Dione, 4=Rhea, 5=Titan, 6=Iapetus, 7=Hyperion.';
|
||||||
|
|
||||||
|
CREATE FUNCTION uranus_moon_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION uranus_moon_observe(int4, observer, timestamptz) IS
|
||||||
|
'Observe a Uranus moon via GUST86. Body IDs: 0=Miranda, 1=Ariel, 2=Umbriel, 3=Titania, 4=Oberon.';
|
||||||
|
|
||||||
|
CREATE FUNCTION mars_moon_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION mars_moon_observe(int4, observer, timestamptz) IS
|
||||||
|
'Observe a Mars moon via MarsSat. Body IDs: 0=Phobos, 1=Deimos.';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Jupiter decametric radio burst prediction
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION io_phase_angle(timestamptz) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION io_phase_angle(timestamptz) IS
|
||||||
|
'Io orbital phase angle in degrees [0,360). 0=superior conjunction (behind Jupiter). Standard Radio JOVE convention.';
|
||||||
|
|
||||||
|
CREATE FUNCTION jupiter_cml(observer, timestamptz) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION jupiter_cml(observer, timestamptz) IS
|
||||||
|
'Jupiter Central Meridian Longitude, System III (1965.0), in degrees [0,360). Light-time corrected.';
|
||||||
|
|
||||||
|
CREATE FUNCTION jupiter_burst_probability(float8, float8) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION jupiter_burst_probability(float8, float8) IS
|
||||||
|
'Estimated Jupiter decametric burst probability (0-1) from (io_phase_deg, cml_deg). Based on Carr et al. (1983) source regions A, B, C, D.';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Interplanetary transfer orbits (Lambert solver)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION lambert_transfer(
|
||||||
|
dep_body_id int4, arr_body_id int4,
|
||||||
|
dep_time timestamptz, arr_time timestamptz,
|
||||||
|
OUT c3_departure float8, OUT c3_arrival float8,
|
||||||
|
OUT v_inf_departure float8, OUT v_inf_arrival float8,
|
||||||
|
OUT tof_days float8, OUT transfer_sma float8
|
||||||
|
) RETURNS RECORD
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION lambert_transfer(int4, int4, timestamptz, timestamptz) IS
|
||||||
|
'Solve Lambert transfer between two planets. Returns C3 (km^2/s^2), v_infinity (km/s), TOF (days), SMA (AU). Body IDs 1-8.';
|
||||||
|
|
||||||
|
CREATE FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) IS
|
||||||
|
'Departure C3 (km^2/s^2) for a Lambert transfer. Returns NULL if solver fails. For pork chop plots.';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- DE ephemeris functions (optional high-precision)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION planet_heliocentric_de(int4, timestamptz) RETURNS heliocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION planet_heliocentric_de(int4, timestamptz) IS
|
||||||
|
'Heliocentric ecliptic J2000 position via JPL DE (sub-arcsecond). Falls back to VSOP87 if DE unavailable. Body IDs: 0=Sun, 1-8=Mercury-Neptune.';
|
||||||
|
|
||||||
|
CREATE FUNCTION planet_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION planet_observe_de(int4, observer, timestamptz) IS
|
||||||
|
'Observe planet via JPL DE. Falls back to VSOP87. Body IDs: 1-8 (Mercury-Neptune).';
|
||||||
|
|
||||||
|
CREATE FUNCTION sun_observe_de(observer, timestamptz) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION sun_observe_de(observer, timestamptz) IS
|
||||||
|
'Observe Sun via JPL DE. Falls back to VSOP87.';
|
||||||
|
|
||||||
|
CREATE FUNCTION moon_observe_de(observer, timestamptz) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION moon_observe_de(observer, timestamptz) IS
|
||||||
|
'Observe Moon via JPL DE. Falls back to ELP2000-82B.';
|
||||||
|
|
||||||
|
CREATE FUNCTION lambert_transfer_de(
|
||||||
|
dep_body_id int4, arr_body_id int4,
|
||||||
|
dep_time timestamptz, arr_time timestamptz,
|
||||||
|
OUT c3_departure float8, OUT c3_arrival float8,
|
||||||
|
OUT v_inf_departure float8, OUT v_inf_arrival float8,
|
||||||
|
OUT tof_days float8, OUT transfer_sma float8
|
||||||
|
) RETURNS RECORD
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION lambert_transfer_de(int4, int4, timestamptz, timestamptz) IS
|
||||||
|
'Lambert transfer via JPL DE positions. Falls back to VSOP87. Body IDs 1-8.';
|
||||||
|
|
||||||
|
CREATE FUNCTION lambert_c3_de(int4, int4, timestamptz, timestamptz) RETURNS float8
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION lambert_c3_de(int4, int4, timestamptz, timestamptz) IS
|
||||||
|
'Departure C3 via JPL DE. Falls back to VSOP87. For pork chop plots.';
|
||||||
|
|
||||||
|
CREATE FUNCTION galilean_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION galilean_observe_de(int4, observer, timestamptz) IS
|
||||||
|
'Observe Galilean moon with JPL DE parent position. L1.2 moon offsets. Body IDs: 0-3 (Io-Callisto).';
|
||||||
|
|
||||||
|
CREATE FUNCTION saturn_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION saturn_moon_observe_de(int4, observer, timestamptz) IS
|
||||||
|
'Observe Saturn moon with JPL DE parent position. TASS 1.7 moon offsets. Body IDs: 0-7 (Mimas-Hyperion).';
|
||||||
|
|
||||||
|
CREATE FUNCTION uranus_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION uranus_moon_observe_de(int4, observer, timestamptz) IS
|
||||||
|
'Observe Uranus moon with JPL DE parent position. GUST86 moon offsets. Body IDs: 0-4 (Miranda-Oberon).';
|
||||||
|
|
||||||
|
CREATE FUNCTION mars_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION mars_moon_observe_de(int4, observer, timestamptz) IS
|
||||||
|
'Observe Mars moon with JPL DE parent position. MarsSat moon offsets. Body IDs: 0-1 (Phobos-Deimos).';
|
||||||
|
|
||||||
|
|
||||||
|
-- Diagnostic function
|
||||||
|
|
||||||
|
CREATE FUNCTION pg_orrery_ephemeris_info(
|
||||||
|
OUT provider text, OUT file_path text,
|
||||||
|
OUT start_jd float8, OUT end_jd float8,
|
||||||
|
OUT version int4, OUT au_km float8
|
||||||
|
) RETURNS RECORD
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION pg_orrery_ephemeris_info() IS
|
||||||
|
'Returns current ephemeris provider status: VSOP87 or JPL_DE with file path, JD range, version, and AU value.';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Orbit determination (TLE fitting from observations)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Fit TLE from ECI position/velocity ephemeris
|
||||||
|
-- weights: per-observation weighting (NULL = uniform)
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_from_eci(
|
||||||
|
positions eci_position[], times timestamptz[],
|
||||||
|
seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false,
|
||||||
|
max_iter int4 DEFAULT 15,
|
||||||
|
weights float8[] DEFAULT NULL,
|
||||||
|
OUT fitted_tle tle, OUT iterations int4,
|
||||||
|
OUT rms_final float8, OUT rms_initial float8, OUT status text,
|
||||||
|
OUT condition_number float8, OUT covariance float8[], OUT nstate int4
|
||||||
|
) RETURNS RECORD
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_from_eci(eci_position[], timestamptz[], tle, boolean, int4, float8[]) IS
|
||||||
|
'Fit a TLE from ECI position/velocity observations via differential correction. Optional per-observation weights for heterogeneous sensor fusion. Returns fitted TLE, RMS residuals, convergence status, condition number, and formal covariance matrix.';
|
||||||
|
|
||||||
|
-- Fit TLE from topocentric observations (az/el/range) — single observer
|
||||||
|
-- fit_range_rate: include range_rate as 4th residual component
|
||||||
|
-- weights: per-observation weighting (NULL = uniform)
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_from_topocentric(
|
||||||
|
observations topocentric[], times timestamptz[],
|
||||||
|
obs observer,
|
||||||
|
seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false,
|
||||||
|
max_iter int4 DEFAULT 15,
|
||||||
|
fit_range_rate boolean DEFAULT false,
|
||||||
|
weights float8[] DEFAULT NULL,
|
||||||
|
OUT fitted_tle tle, OUT iterations int4,
|
||||||
|
OUT rms_final float8, OUT rms_initial float8, OUT status text,
|
||||||
|
OUT condition_number float8, OUT covariance float8[], OUT nstate int4
|
||||||
|
) RETURNS RECORD
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_from_topocentric(topocentric[], timestamptz[], observer, tle, boolean, int4, boolean, float8[]) IS
|
||||||
|
'Fit a TLE from topocentric (az/el/range) observations via differential correction. Optional range_rate fitting and per-observation weights. Returns fitted TLE, RMS residuals, convergence status, condition number, and formal covariance matrix.';
|
||||||
|
|
||||||
|
-- Fit TLE from topocentric observations — multiple observers
|
||||||
|
|
||||||
|
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,
|
||||||
|
fit_range_rate boolean DEFAULT false,
|
||||||
|
weights float8[] DEFAULT NULL,
|
||||||
|
OUT fitted_tle tle, OUT iterations int4,
|
||||||
|
OUT rms_final float8, OUT rms_initial float8, OUT status text,
|
||||||
|
OUT condition_number float8, OUT covariance float8[], OUT nstate int4
|
||||||
|
) 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, boolean, float8[]) IS
|
||||||
|
'Fit a TLE from topocentric observations collected by multiple ground stations. observer_ids[i] indexes into observers[]. Optional range_rate fitting and per-observation weights.';
|
||||||
|
|
||||||
|
-- Per-observation residuals diagnostic
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_fit_residuals(
|
||||||
|
fitted tle,
|
||||||
|
positions eci_position[],
|
||||||
|
times timestamptz[]
|
||||||
|
) RETURNS TABLE (
|
||||||
|
t timestamptz,
|
||||||
|
dx_km float8,
|
||||||
|
dy_km float8,
|
||||||
|
dz_km float8,
|
||||||
|
pos_err_km float8
|
||||||
|
)
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_fit_residuals(tle, eci_position[], timestamptz[]) IS
|
||||||
|
'Compute per-observation position residuals (km) between a TLE and ECI observations. Useful for fit quality assessment.';
|
||||||
|
|
||||||
|
-- Fit TLE from RA/Dec observations — single observer
|
||||||
|
-- Uses Gauss method for initial orbit determination when no seed is provided.
|
||||||
|
-- RA in hours [0,24), Dec in degrees [-90,90] (matches star_observe convention).
|
||||||
|
-- RMS output is in radians for angles-only mode.
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_from_angles(
|
||||||
|
ra_hours float8[], dec_degrees float8[],
|
||||||
|
times timestamptz[],
|
||||||
|
obs observer,
|
||||||
|
seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false,
|
||||||
|
max_iter int4 DEFAULT 15,
|
||||||
|
weights float8[] DEFAULT NULL,
|
||||||
|
OUT fitted_tle tle, OUT iterations int4,
|
||||||
|
OUT rms_final float8, OUT rms_initial float8, OUT status text,
|
||||||
|
OUT condition_number float8, OUT covariance float8[], OUT nstate int4
|
||||||
|
) RETURNS RECORD AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_from_angles(float8[], float8[], timestamptz[], observer, tle, boolean, int4, float8[]) IS
|
||||||
|
'Fit a TLE from angles-only (RA/Dec) observations via Gauss IOD + differential correction. RA in hours [0,24), Dec in degrees [-90,90]. RMS output in radians. Uses Gauss method for seed-free initial guess.';
|
||||||
|
|
||||||
|
-- Fit TLE from RA/Dec observations — multiple observers
|
||||||
|
|
||||||
|
CREATE FUNCTION tle_from_angles(
|
||||||
|
ra_hours float8[], dec_degrees float8[],
|
||||||
|
times timestamptz[],
|
||||||
|
observers observer[], observer_ids int4[],
|
||||||
|
seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false,
|
||||||
|
max_iter int4 DEFAULT 15,
|
||||||
|
weights float8[] DEFAULT NULL,
|
||||||
|
OUT fitted_tle tle, OUT iterations int4,
|
||||||
|
OUT rms_final float8, OUT rms_initial float8, OUT status text,
|
||||||
|
OUT condition_number float8, OUT covariance float8[], OUT nstate int4
|
||||||
|
) RETURNS RECORD
|
||||||
|
AS 'MODULE_PATHNAME', 'tle_from_angles_multi'
|
||||||
|
LANGUAGE C STABLE PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION tle_from_angles(float8[], float8[], timestamptz[], observer[], int4[], tle, boolean, int4, float8[]) IS
|
||||||
|
'Fit a TLE from angles-only (RA/Dec) observations from multiple ground stations via Gauss IOD + differential correction.';
|
||||||
494
src/od_funcs.c
494
src/od_funcs.c
@ -31,6 +31,8 @@ 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_from_topocentric_multi);
|
||||||
PG_FUNCTION_INFO_V1(tle_fit_residuals);
|
PG_FUNCTION_INFO_V1(tle_fit_residuals);
|
||||||
|
PG_FUNCTION_INFO_V1(tle_from_angles);
|
||||||
|
PG_FUNCTION_INFO_V1(tle_from_angles_multi);
|
||||||
|
|
||||||
/* ================================================================
|
/* ================================================================
|
||||||
* Helper: pg_tle ↔ tle_t conversion (local copy, avoids symbol coupling)
|
* Helper: pg_tle ↔ tle_t conversion (local copy, avoids symbol coupling)
|
||||||
@ -105,6 +107,7 @@ tle_from_eci(PG_FUNCTION_ARGS)
|
|||||||
pg_tle *seed_pg = has_seed ? (pg_tle *) PG_GETARG_POINTER(2) : NULL;
|
pg_tle *seed_pg = has_seed ? (pg_tle *) PG_GETARG_POINTER(2) : NULL;
|
||||||
bool fit_bstar = PG_ARGISNULL(3) ? false : PG_GETARG_BOOL(3);
|
bool fit_bstar = PG_ARGISNULL(3) ? false : PG_GETARG_BOOL(3);
|
||||||
int32 max_iter = PG_ARGISNULL(4) ? 15 : PG_GETARG_INT32(4);
|
int32 max_iter = PG_ARGISNULL(4) ? 15 : PG_GETARG_INT32(4);
|
||||||
|
bool has_weights = !PG_ARGISNULL(5);
|
||||||
|
|
||||||
int n_pos, n_times;
|
int n_pos, n_times;
|
||||||
Datum *pos_datums, *time_datums;
|
Datum *pos_datums, *time_datums;
|
||||||
@ -165,6 +168,29 @@ tle_from_eci(PG_FUNCTION_ARGS)
|
|||||||
config.observers = NULL;
|
config.observers = NULL;
|
||||||
config.n_observers = 0;
|
config.n_observers = 0;
|
||||||
|
|
||||||
|
/* Extract per-observation weights if provided */
|
||||||
|
if (has_weights)
|
||||||
|
{
|
||||||
|
ArrayType *wt_arr = PG_GETARG_ARRAYTYPE_P(5);
|
||||||
|
Datum *wt_datums;
|
||||||
|
bool *wt_nulls;
|
||||||
|
int n_wt;
|
||||||
|
|
||||||
|
deconstruct_array(wt_arr, FLOAT8OID, sizeof(float8), FLOAT8PASSBYVAL,
|
||||||
|
TYPALIGN_DOUBLE, &wt_datums, &wt_nulls, &n_wt);
|
||||||
|
|
||||||
|
if (n_wt != n_pos)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
|
||||||
|
errmsg("weights array length must match observations: "
|
||||||
|
"%d vs %d", n_wt, n_pos)));
|
||||||
|
|
||||||
|
config.weights = (double *) palloc(sizeof(double) * n_wt);
|
||||||
|
for (i = 0; i < n_wt; i++)
|
||||||
|
config.weights[i] = DatumGetFloat8(wt_datums[i]);
|
||||||
|
config.n_weights = n_wt;
|
||||||
|
}
|
||||||
|
|
||||||
/* Convert seed TLE if provided */
|
/* Convert seed TLE if provided */
|
||||||
if (has_seed)
|
if (has_seed)
|
||||||
pg_tle_to_sat_code_od(seed_pg, &seed_sat);
|
pg_tle_to_sat_code_od(seed_pg, &seed_sat);
|
||||||
@ -175,6 +201,8 @@ tle_from_eci(PG_FUNCTION_ARGS)
|
|||||||
rc = od_fit_tle(obs, n_pos, has_seed ? &seed_sat : NULL, &config, &result);
|
rc = od_fit_tle(obs, n_pos, has_seed ? &seed_sat : NULL, &config, &result);
|
||||||
|
|
||||||
pfree(obs);
|
pfree(obs);
|
||||||
|
if (config.weights)
|
||||||
|
pfree(config.weights);
|
||||||
|
|
||||||
if (rc != 0)
|
if (rc != 0)
|
||||||
ereport(ERROR,
|
ereport(ERROR,
|
||||||
@ -244,6 +272,8 @@ tle_from_topocentric(PG_FUNCTION_ARGS)
|
|||||||
pg_tle *seed_pg = has_seed ? (pg_tle *) PG_GETARG_POINTER(3) : NULL;
|
pg_tle *seed_pg = has_seed ? (pg_tle *) PG_GETARG_POINTER(3) : NULL;
|
||||||
bool fit_bstar = PG_ARGISNULL(4) ? false : PG_GETARG_BOOL(4);
|
bool fit_bstar = PG_ARGISNULL(4) ? false : PG_GETARG_BOOL(4);
|
||||||
int32 max_iter = PG_ARGISNULL(5) ? 15 : PG_GETARG_INT32(5);
|
int32 max_iter = PG_ARGISNULL(5) ? 15 : PG_GETARG_INT32(5);
|
||||||
|
bool fit_range_rate = PG_ARGISNULL(6) ? false : PG_GETARG_BOOL(6);
|
||||||
|
bool has_weights = !PG_ARGISNULL(7);
|
||||||
|
|
||||||
int n_topo, n_times;
|
int n_topo, n_times;
|
||||||
od_observation_t *obs;
|
od_observation_t *obs;
|
||||||
@ -301,17 +331,42 @@ 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].data[3] = topo->range_rate;
|
||||||
obs[i].observer_idx = 0; /* single observer */
|
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.fit_range_rate = fit_range_rate ? 1 : 0;
|
||||||
config.observers = &observer;
|
config.max_iter = max_iter;
|
||||||
config.n_observers = 1;
|
config.observers = &observer;
|
||||||
|
config.n_observers = 1;
|
||||||
|
|
||||||
|
/* Extract per-observation weights if provided */
|
||||||
|
if (has_weights)
|
||||||
|
{
|
||||||
|
ArrayType *wt_arr = PG_GETARG_ARRAYTYPE_P(7);
|
||||||
|
Datum *wt_datums;
|
||||||
|
bool *wt_nulls;
|
||||||
|
int n_wt;
|
||||||
|
|
||||||
|
deconstruct_array(wt_arr, FLOAT8OID, sizeof(float8), FLOAT8PASSBYVAL,
|
||||||
|
TYPALIGN_DOUBLE, &wt_datums, &wt_nulls, &n_wt);
|
||||||
|
|
||||||
|
if (n_wt != n_topo)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
|
||||||
|
errmsg("weights array length must match observations: "
|
||||||
|
"%d vs %d", n_wt, n_topo)));
|
||||||
|
|
||||||
|
config.weights = (double *) palloc(sizeof(double) * n_wt);
|
||||||
|
for (i = 0; i < n_wt; i++)
|
||||||
|
config.weights[i] = DatumGetFloat8(wt_datums[i]);
|
||||||
|
config.n_weights = n_wt;
|
||||||
|
}
|
||||||
|
|
||||||
if (has_seed)
|
if (has_seed)
|
||||||
pg_tle_to_sat_code_od(seed_pg, &seed_sat);
|
pg_tle_to_sat_code_od(seed_pg, &seed_sat);
|
||||||
@ -321,6 +376,8 @@ tle_from_topocentric(PG_FUNCTION_ARGS)
|
|||||||
rc = od_fit_tle(obs, n_topo, has_seed ? &seed_sat : NULL, &config, &result);
|
rc = od_fit_tle(obs, n_topo, has_seed ? &seed_sat : NULL, &config, &result);
|
||||||
|
|
||||||
pfree(obs);
|
pfree(obs);
|
||||||
|
if (config.weights)
|
||||||
|
pfree(config.weights);
|
||||||
|
|
||||||
if (rc != 0)
|
if (rc != 0)
|
||||||
ereport(ERROR,
|
ereport(ERROR,
|
||||||
@ -394,6 +451,8 @@ tle_from_topocentric_multi(PG_FUNCTION_ARGS)
|
|||||||
pg_tle *seed_pg = has_seed ? (pg_tle *) PG_GETARG_POINTER(4) : NULL;
|
pg_tle *seed_pg = has_seed ? (pg_tle *) PG_GETARG_POINTER(4) : NULL;
|
||||||
bool fit_bstar = PG_ARGISNULL(5) ? false : PG_GETARG_BOOL(5);
|
bool fit_bstar = PG_ARGISNULL(5) ? false : PG_GETARG_BOOL(5);
|
||||||
int32 max_iter = PG_ARGISNULL(6) ? 15 : PG_GETARG_INT32(6);
|
int32 max_iter = PG_ARGISNULL(6) ? 15 : PG_GETARG_INT32(6);
|
||||||
|
bool fit_range_rate = PG_ARGISNULL(7) ? false : PG_GETARG_BOOL(7);
|
||||||
|
bool has_weights = !PG_ARGISNULL(8);
|
||||||
|
|
||||||
int n_topo, n_times, n_obs_sites, n_ids;
|
int n_topo, n_times, n_obs_sites, n_ids;
|
||||||
od_observation_t *obs;
|
od_observation_t *obs;
|
||||||
@ -474,16 +533,41 @@ tle_from_topocentric_multi(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].data[3] = topo->range_rate;
|
||||||
obs[i].observer_idx = oid;
|
obs[i].observer_idx = oid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 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.fit_range_rate = fit_range_rate ? 1 : 0;
|
||||||
config.observers = observers;
|
config.max_iter = max_iter;
|
||||||
config.n_observers = n_obs_sites;
|
config.observers = observers;
|
||||||
|
config.n_observers = n_obs_sites;
|
||||||
|
|
||||||
|
/* Extract per-observation weights if provided */
|
||||||
|
if (has_weights)
|
||||||
|
{
|
||||||
|
ArrayType *wt_arr = PG_GETARG_ARRAYTYPE_P(8);
|
||||||
|
Datum *wt_datums;
|
||||||
|
bool *wt_nulls;
|
||||||
|
int n_wt;
|
||||||
|
|
||||||
|
deconstruct_array(wt_arr, FLOAT8OID, sizeof(float8), FLOAT8PASSBYVAL,
|
||||||
|
TYPALIGN_DOUBLE, &wt_datums, &wt_nulls, &n_wt);
|
||||||
|
|
||||||
|
if (n_wt != n_topo)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
|
||||||
|
errmsg("weights array length must match observations: "
|
||||||
|
"%d vs %d", n_wt, n_topo)));
|
||||||
|
|
||||||
|
config.weights = (double *) palloc(sizeof(double) * n_wt);
|
||||||
|
for (i = 0; i < n_wt; i++)
|
||||||
|
config.weights[i] = DatumGetFloat8(wt_datums[i]);
|
||||||
|
config.n_weights = n_wt;
|
||||||
|
}
|
||||||
|
|
||||||
if (has_seed)
|
if (has_seed)
|
||||||
pg_tle_to_sat_code_od(seed_pg, &seed_sat);
|
pg_tle_to_sat_code_od(seed_pg, &seed_sat);
|
||||||
@ -494,6 +578,396 @@ tle_from_topocentric_multi(PG_FUNCTION_ARGS)
|
|||||||
|
|
||||||
pfree(obs);
|
pfree(obs);
|
||||||
pfree(observers);
|
pfree(observers);
|
||||||
|
if (config.weights)
|
||||||
|
pfree(config.weights);
|
||||||
|
|
||||||
|
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);
|
||||||
|
values[5] = Float8GetDatum(result.condition_number);
|
||||||
|
|
||||||
|
if (result.cov_size > 0)
|
||||||
|
{
|
||||||
|
Datum *cov_datums = (Datum *) palloc(sizeof(Datum) * result.cov_size);
|
||||||
|
int ci;
|
||||||
|
for (ci = 0; ci < result.cov_size; ci++)
|
||||||
|
cov_datums[ci] = Float8GetDatum(result.covariance[ci]);
|
||||||
|
values[6] = PointerGetDatum(
|
||||||
|
construct_array(cov_datums, result.cov_size,
|
||||||
|
FLOAT8OID, sizeof(float8), FLOAT8PASSBYVAL,
|
||||||
|
TYPALIGN_DOUBLE));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
nulls[6] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
values[7] = Int32GetDatum(result.cov_size > 0
|
||||||
|
? (result.cov_size == 28 ? 7 : 6)
|
||||||
|
: 0);
|
||||||
|
|
||||||
|
tuple = heap_form_tuple(tupdesc, values, nulls);
|
||||||
|
PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* tle_from_angles(ra_hours[], dec_degrees[], timestamptz[], observer,
|
||||||
|
* tle, boolean, int4, float8[])
|
||||||
|
* -> RECORD (same 8-column output as tle_from_eci)
|
||||||
|
*
|
||||||
|
* Fit TLE from angles-only (RA/Dec) observations.
|
||||||
|
* RA in hours [0,24), Dec in degrees [-90,90].
|
||||||
|
* Uses Gauss IOD for seed-free initial guess.
|
||||||
|
* ================================================================
|
||||||
|
*/
|
||||||
|
Datum
|
||||||
|
tle_from_angles(PG_FUNCTION_ARGS)
|
||||||
|
{
|
||||||
|
ArrayType *ra_arr = PG_GETARG_ARRAYTYPE_P(0);
|
||||||
|
ArrayType *dec_arr = PG_GETARG_ARRAYTYPE_P(1);
|
||||||
|
ArrayType *time_arr = PG_GETARG_ARRAYTYPE_P(2);
|
||||||
|
pg_observer *obs_pg = (pg_observer *) PG_GETARG_POINTER(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);
|
||||||
|
bool has_weights = !PG_ARGISNULL(7);
|
||||||
|
|
||||||
|
int n_ra, n_dec, n_times;
|
||||||
|
Datum *ra_datums, *dec_datums, *time_datums;
|
||||||
|
bool *ra_nulls, *dec_nulls, *time_nulls;
|
||||||
|
od_observation_t *obs;
|
||||||
|
od_config_t config;
|
||||||
|
od_observer_t observer;
|
||||||
|
od_result_t result;
|
||||||
|
tle_t seed_sat;
|
||||||
|
int i, rc;
|
||||||
|
|
||||||
|
TupleDesc tupdesc;
|
||||||
|
Datum values[8];
|
||||||
|
bool nulls[8];
|
||||||
|
HeapTuple tuple;
|
||||||
|
|
||||||
|
/* Build observer */
|
||||||
|
observer.lat = obs_pg->lat;
|
||||||
|
observer.lon = obs_pg->lon;
|
||||||
|
observer.alt_m = obs_pg->alt_m;
|
||||||
|
od_observer_to_ecef(observer.lat, observer.lon, observer.alt_m,
|
||||||
|
observer.ecef);
|
||||||
|
|
||||||
|
/* Deconstruct arrays */
|
||||||
|
deconstruct_array(ra_arr, FLOAT8OID, sizeof(float8), FLOAT8PASSBYVAL,
|
||||||
|
TYPALIGN_DOUBLE, &ra_datums, &ra_nulls, &n_ra);
|
||||||
|
deconstruct_array(dec_arr, FLOAT8OID, sizeof(float8), FLOAT8PASSBYVAL,
|
||||||
|
TYPALIGN_DOUBLE, &dec_datums, &dec_nulls, &n_dec);
|
||||||
|
deconstruct_array(time_arr, TIMESTAMPTZOID, sizeof(int64), FLOAT8PASSBYVAL,
|
||||||
|
TYPALIGN_DOUBLE, &time_datums, &time_nulls, &n_times);
|
||||||
|
|
||||||
|
if (n_ra != n_dec || n_ra != n_times)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
|
||||||
|
errmsg("ra, dec, and times arrays must have same length: "
|
||||||
|
"%d, %d, %d", n_ra, n_dec, n_times)));
|
||||||
|
|
||||||
|
if (n_ra < 6)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||||
|
errmsg("at least 6 observations required, got %d", n_ra)));
|
||||||
|
|
||||||
|
/* Build observation array — convert RA hours → radians, Dec deg → radians */
|
||||||
|
obs = (od_observation_t *) palloc(sizeof(od_observation_t) * n_ra);
|
||||||
|
|
||||||
|
for (i = 0; i < n_ra; i++)
|
||||||
|
{
|
||||||
|
double ra_hr = DatumGetFloat8(ra_datums[i]);
|
||||||
|
double dec_dg = DatumGetFloat8(dec_datums[i]);
|
||||||
|
int64 ts = DatumGetInt64(time_datums[i]);
|
||||||
|
|
||||||
|
if (ra_hr < 0.0 || ra_hr >= 24.0)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||||
|
errmsg("RA must be in [0, 24) hours, got %g", ra_hr)));
|
||||||
|
if (dec_dg < -90.0 || dec_dg > 90.0)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||||
|
errmsg("Dec must be in [-90, 90] degrees, got %g", dec_dg)));
|
||||||
|
|
||||||
|
obs[i].jd = PG_EPOCH_JD + ((double)ts / (double)USECS_PER_DAY);
|
||||||
|
obs[i].data[0] = ra_hr * (M_PI / 12.0); /* hours → radians */
|
||||||
|
obs[i].data[1] = dec_dg * (M_PI / 180.0); /* degrees → radians */
|
||||||
|
obs[i].observer_idx = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Configure solver */
|
||||||
|
memset(&config, 0, sizeof(config));
|
||||||
|
config.obs_type = OD_OBS_RADEC;
|
||||||
|
config.fit_bstar = fit_bstar ? 1 : 0;
|
||||||
|
config.max_iter = max_iter;
|
||||||
|
config.observers = &observer;
|
||||||
|
config.n_observers = 1;
|
||||||
|
|
||||||
|
/* Extract per-observation weights if provided */
|
||||||
|
if (has_weights)
|
||||||
|
{
|
||||||
|
ArrayType *wt_arr = PG_GETARG_ARRAYTYPE_P(7);
|
||||||
|
Datum *wt_datums;
|
||||||
|
bool *wt_nulls;
|
||||||
|
int n_wt;
|
||||||
|
|
||||||
|
deconstruct_array(wt_arr, FLOAT8OID, sizeof(float8), FLOAT8PASSBYVAL,
|
||||||
|
TYPALIGN_DOUBLE, &wt_datums, &wt_nulls, &n_wt);
|
||||||
|
|
||||||
|
if (n_wt != n_ra)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
|
||||||
|
errmsg("weights array length must match observations: "
|
||||||
|
"%d vs %d", n_wt, n_ra)));
|
||||||
|
|
||||||
|
config.weights = (double *) palloc(sizeof(double) * n_wt);
|
||||||
|
for (i = 0; i < n_wt; i++)
|
||||||
|
config.weights[i] = DatumGetFloat8(wt_datums[i]);
|
||||||
|
config.n_weights = n_wt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has_seed)
|
||||||
|
pg_tle_to_sat_code_od(seed_pg, &seed_sat);
|
||||||
|
|
||||||
|
memset(&result, 0, sizeof(result));
|
||||||
|
|
||||||
|
rc = od_fit_tle(obs, n_ra, has_seed ? &seed_sat : NULL, &config, &result);
|
||||||
|
|
||||||
|
pfree(obs);
|
||||||
|
if (config.weights)
|
||||||
|
pfree(config.weights);
|
||||||
|
|
||||||
|
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);
|
||||||
|
values[5] = Float8GetDatum(result.condition_number);
|
||||||
|
|
||||||
|
if (result.cov_size > 0)
|
||||||
|
{
|
||||||
|
Datum *cov_datums = (Datum *) palloc(sizeof(Datum) * result.cov_size);
|
||||||
|
int ci;
|
||||||
|
for (ci = 0; ci < result.cov_size; ci++)
|
||||||
|
cov_datums[ci] = Float8GetDatum(result.covariance[ci]);
|
||||||
|
values[6] = PointerGetDatum(
|
||||||
|
construct_array(cov_datums, result.cov_size,
|
||||||
|
FLOAT8OID, sizeof(float8), FLOAT8PASSBYVAL,
|
||||||
|
TYPALIGN_DOUBLE));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
nulls[6] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
values[7] = Int32GetDatum(result.cov_size > 0
|
||||||
|
? (result.cov_size == 28 ? 7 : 6)
|
||||||
|
: 0);
|
||||||
|
|
||||||
|
tuple = heap_form_tuple(tupdesc, values, nulls);
|
||||||
|
PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* tle_from_angles_multi(ra_hours[], dec_degrees[], timestamptz[],
|
||||||
|
* observer[], int4[],
|
||||||
|
* tle, boolean, int4, float8[])
|
||||||
|
* -> RECORD (same 8-column output)
|
||||||
|
*
|
||||||
|
* Multi-observer variant of tle_from_angles.
|
||||||
|
* ================================================================
|
||||||
|
*/
|
||||||
|
Datum
|
||||||
|
tle_from_angles_multi(PG_FUNCTION_ARGS)
|
||||||
|
{
|
||||||
|
ArrayType *ra_arr = PG_GETARG_ARRAYTYPE_P(0);
|
||||||
|
ArrayType *dec_arr = PG_GETARG_ARRAYTYPE_P(1);
|
||||||
|
ArrayType *time_arr = PG_GETARG_ARRAYTYPE_P(2);
|
||||||
|
ArrayType *obs_arr = PG_GETARG_ARRAYTYPE_P(3);
|
||||||
|
ArrayType *id_arr = PG_GETARG_ARRAYTYPE_P(4);
|
||||||
|
bool has_seed = !PG_ARGISNULL(5);
|
||||||
|
pg_tle *seed_pg = has_seed ? (pg_tle *) PG_GETARG_POINTER(5) : NULL;
|
||||||
|
bool fit_bstar = PG_ARGISNULL(6) ? false : PG_GETARG_BOOL(6);
|
||||||
|
int32 max_iter = PG_ARGISNULL(7) ? 15 : PG_GETARG_INT32(7);
|
||||||
|
bool has_weights = !PG_ARGISNULL(8);
|
||||||
|
|
||||||
|
int n_ra, n_dec, n_times, n_obs_sites, n_ids;
|
||||||
|
Datum *ra_datums, *dec_datums, *time_datums, *obs_datums, *id_datums;
|
||||||
|
bool *ra_nulls, *dec_nulls, *time_nulls, *obs_nulls, *id_nulls;
|
||||||
|
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[8];
|
||||||
|
bool nulls[8];
|
||||||
|
HeapTuple tuple;
|
||||||
|
|
||||||
|
/* Deconstruct all arrays */
|
||||||
|
deconstruct_array(ra_arr, FLOAT8OID, sizeof(float8), FLOAT8PASSBYVAL,
|
||||||
|
TYPALIGN_DOUBLE, &ra_datums, &ra_nulls, &n_ra);
|
||||||
|
deconstruct_array(dec_arr, FLOAT8OID, sizeof(float8), FLOAT8PASSBYVAL,
|
||||||
|
TYPALIGN_DOUBLE, &dec_datums, &dec_nulls, &n_dec);
|
||||||
|
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_ra != n_dec || n_ra != n_times)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
|
||||||
|
errmsg("ra, dec, and times arrays must have same length: "
|
||||||
|
"%d, %d, %d", n_ra, n_dec, n_times)));
|
||||||
|
|
||||||
|
if (n_ra != n_ids)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
|
||||||
|
errmsg("observations and observer_ids arrays must have same length: "
|
||||||
|
"%d vs %d", n_ra, n_ids)));
|
||||||
|
|
||||||
|
if (n_ra < 6)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||||
|
errmsg("at least 6 observations required, got %d", n_ra)));
|
||||||
|
|
||||||
|
if (n_obs_sites < 1)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||||
|
errmsg("at least 1 observer required")));
|
||||||
|
|
||||||
|
/* Build observer array */
|
||||||
|
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 */
|
||||||
|
obs = (od_observation_t *) palloc(sizeof(od_observation_t) * n_ra);
|
||||||
|
for (i = 0; i < n_ra; i++)
|
||||||
|
{
|
||||||
|
double ra_hr = DatumGetFloat8(ra_datums[i]);
|
||||||
|
double dec_dg = DatumGetFloat8(dec_datums[i]);
|
||||||
|
int64 ts = DatumGetInt64(time_datums[i]);
|
||||||
|
int32 oid = DatumGetInt32(id_datums[i]);
|
||||||
|
|
||||||
|
if (ra_hr < 0.0 || ra_hr >= 24.0)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||||
|
errmsg("RA must be in [0, 24) hours, got %g", ra_hr)));
|
||||||
|
if (dec_dg < -90.0 || dec_dg > 90.0)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||||
|
errmsg("Dec must be in [-90, 90] degrees, got %g", dec_dg)));
|
||||||
|
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] = ra_hr * (M_PI / 12.0);
|
||||||
|
obs[i].data[1] = dec_dg * (M_PI / 180.0);
|
||||||
|
obs[i].observer_idx = oid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Configure solver */
|
||||||
|
memset(&config, 0, sizeof(config));
|
||||||
|
config.obs_type = OD_OBS_RADEC;
|
||||||
|
config.fit_bstar = fit_bstar ? 1 : 0;
|
||||||
|
config.max_iter = max_iter;
|
||||||
|
config.observers = observers;
|
||||||
|
config.n_observers = n_obs_sites;
|
||||||
|
|
||||||
|
/* Extract per-observation weights if provided */
|
||||||
|
if (has_weights)
|
||||||
|
{
|
||||||
|
ArrayType *wt_arr = PG_GETARG_ARRAYTYPE_P(8);
|
||||||
|
Datum *wt_datums;
|
||||||
|
bool *wt_nulls;
|
||||||
|
int n_wt;
|
||||||
|
|
||||||
|
deconstruct_array(wt_arr, FLOAT8OID, sizeof(float8), FLOAT8PASSBYVAL,
|
||||||
|
TYPALIGN_DOUBLE, &wt_datums, &wt_nulls, &n_wt);
|
||||||
|
|
||||||
|
if (n_wt != n_ra)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
|
||||||
|
errmsg("weights array length must match observations: "
|
||||||
|
"%d vs %d", n_wt, n_ra)));
|
||||||
|
|
||||||
|
config.weights = (double *) palloc(sizeof(double) * n_wt);
|
||||||
|
for (i = 0; i < n_wt; i++)
|
||||||
|
config.weights[i] = DatumGetFloat8(wt_datums[i]);
|
||||||
|
config.n_weights = n_wt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has_seed)
|
||||||
|
pg_tle_to_sat_code_od(seed_pg, &seed_sat);
|
||||||
|
|
||||||
|
memset(&result, 0, sizeof(result));
|
||||||
|
|
||||||
|
rc = od_fit_tle(obs, n_ra, has_seed ? &seed_sat : NULL, &config, &result);
|
||||||
|
|
||||||
|
pfree(obs);
|
||||||
|
pfree(observers);
|
||||||
|
if (config.weights)
|
||||||
|
pfree(config.weights);
|
||||||
|
|
||||||
if (rc != 0)
|
if (rc != 0)
|
||||||
ereport(ERROR,
|
ereport(ERROR,
|
||||||
|
|||||||
184
src/od_iod.c
184
src/od_iod.c
@ -125,3 +125,187 @@ od_gibbs(const double pos1[3], const double pos2[3],
|
|||||||
(void)jd3;
|
(void)jd3;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Gauss method for angles-only initial orbit determination
|
||||||
|
*
|
||||||
|
* Given 3 RA/Dec observations and observer positions, recovers the
|
||||||
|
* orbit at the middle observation epoch. Vallado Algorithm 52.
|
||||||
|
*
|
||||||
|
* Steps:
|
||||||
|
* 1. Compute LOS unit vectors from RA/Dec
|
||||||
|
* 2. Rotate observer ECEF → TEME at each epoch via GMST
|
||||||
|
* 3. Build D matrix from LOS vectors and observer positions
|
||||||
|
* 4. Iteratively solve for slant range at middle observation
|
||||||
|
* 5. Use Gibbs/Herrick-Gibbs to get velocity at r2
|
||||||
|
* 6. Convert (r2, v2) → Keplerian elements
|
||||||
|
* ================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
int
|
||||||
|
od_gauss(const double ra[3], const double dec[3],
|
||||||
|
const double jd[3],
|
||||||
|
const double obs_ecef[3][3],
|
||||||
|
od_iod_result_t *result)
|
||||||
|
{
|
||||||
|
double L[3][3]; /* LOS unit vectors */
|
||||||
|
double R[3][3]; /* Observer positions in TEME */
|
||||||
|
double tau1, tau3, tau;
|
||||||
|
double D[3][3]; /* D matrix */
|
||||||
|
double D0;
|
||||||
|
double A, B;
|
||||||
|
double r2_mag, r2_mag_old;
|
||||||
|
double rho[3]; /* slant ranges */
|
||||||
|
double r2[3], v2[3];
|
||||||
|
int iter, i, rc;
|
||||||
|
double gmst;
|
||||||
|
|
||||||
|
result->valid = 0;
|
||||||
|
|
||||||
|
/* Time intervals in seconds */
|
||||||
|
tau1 = (jd[0] - jd[1]) * 86400.0;
|
||||||
|
tau3 = (jd[2] - jd[1]) * 86400.0;
|
||||||
|
tau = tau3 - tau1;
|
||||||
|
|
||||||
|
if (fabs(tau) < 1.0)
|
||||||
|
return -1; /* observations too close in time */
|
||||||
|
|
||||||
|
/* LOS unit vectors from RA/Dec */
|
||||||
|
for (i = 0; i < 3; i++)
|
||||||
|
od_radec_to_los(ra[i], dec[i], L[i]);
|
||||||
|
|
||||||
|
/* Observer ECEF → TEME */
|
||||||
|
for (i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
double vel_zero[3] = {0, 0, 0};
|
||||||
|
double vel_dummy[3];
|
||||||
|
gmst = od_gmst_from_jd(jd[i]);
|
||||||
|
od_ecef_to_teme(obs_ecef[i], vel_zero, gmst, R[i], vel_dummy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Build D matrix: D[i][j] = L[i] . (L_cross products) */
|
||||||
|
{
|
||||||
|
double L2xL3[3], L1xL3[3], L1xL2[3];
|
||||||
|
|
||||||
|
vec_cross(L[1], L[2], L2xL3);
|
||||||
|
vec_cross(L[0], L[2], L1xL3);
|
||||||
|
vec_cross(L[0], L[1], L1xL2);
|
||||||
|
|
||||||
|
D0 = vec_dot(L[0], L2xL3);
|
||||||
|
|
||||||
|
if (fabs(D0) < 1e-15)
|
||||||
|
return -1; /* coplanar LOS vectors */
|
||||||
|
|
||||||
|
/* D matrix: each row is dot products of R[i] with cross products */
|
||||||
|
D[0][0] = vec_dot(R[0], L2xL3);
|
||||||
|
D[0][1] = vec_dot(R[0], L1xL3);
|
||||||
|
D[0][2] = vec_dot(R[0], L1xL2);
|
||||||
|
|
||||||
|
D[1][0] = vec_dot(R[1], L2xL3);
|
||||||
|
D[1][1] = vec_dot(R[1], L1xL3);
|
||||||
|
D[1][2] = vec_dot(R[1], L1xL2);
|
||||||
|
|
||||||
|
D[2][0] = vec_dot(R[2], L2xL3);
|
||||||
|
D[2][1] = vec_dot(R[2], L1xL3);
|
||||||
|
D[2][2] = vec_dot(R[2], L1xL2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A and B coefficients for the range equation */
|
||||||
|
A = (-D[0][1] * tau3 / tau + D[1][1] + D[2][1] * tau1 / tau) / D0;
|
||||||
|
B = (D[0][1] * (tau3 * tau3 - tau * tau) * tau3 / tau
|
||||||
|
+ D[2][1] * (tau * tau - tau1 * tau1) * tau1 / tau) / (6.0 * D0);
|
||||||
|
|
||||||
|
/* Initial guess: r2_mag from observer position magnitude */
|
||||||
|
r2_mag = vec_mag(R[1]) + 500.0; /* rough LEO assumption */
|
||||||
|
|
||||||
|
/* Iterate to find r2_mag */
|
||||||
|
for (iter = 0; iter < 50; iter++)
|
||||||
|
{
|
||||||
|
double r2_3 = r2_mag * r2_mag * r2_mag;
|
||||||
|
double f1, f3, g1, g3;
|
||||||
|
double c1, c3;
|
||||||
|
|
||||||
|
/* f and g series (two-body, truncated) */
|
||||||
|
f1 = 1.0 - 0.5 * MU_KM3_S2 * tau1 * tau1 / r2_3;
|
||||||
|
f3 = 1.0 - 0.5 * MU_KM3_S2 * tau3 * tau3 / r2_3;
|
||||||
|
g1 = tau1 - MU_KM3_S2 * tau1 * tau1 * tau1 / (6.0 * r2_3);
|
||||||
|
g3 = tau3 - MU_KM3_S2 * tau3 * tau3 * tau3 / (6.0 * r2_3);
|
||||||
|
|
||||||
|
/* Lagrange coefficients */
|
||||||
|
c1 = g3 / (f1 * g3 - f3 * g1);
|
||||||
|
c3 = -g1 / (f1 * g3 - f3 * g1);
|
||||||
|
|
||||||
|
/* Slant ranges */
|
||||||
|
rho[0] = (-D[0][0] + D[1][0] / c1 - D[2][0] * c3 / c1) / D0;
|
||||||
|
rho[1] = A + B / r2_3;
|
||||||
|
rho[2] = (-D[0][2] * c1 / c3 + D[1][2] / c3 - D[2][2]) / D0;
|
||||||
|
|
||||||
|
/* Position at middle observation */
|
||||||
|
for (i = 0; i < 3; i++)
|
||||||
|
r2[i] = R[1][i] + rho[1] * L[1][i];
|
||||||
|
|
||||||
|
r2_mag_old = r2_mag;
|
||||||
|
r2_mag = vec_mag(r2);
|
||||||
|
|
||||||
|
if (fabs(r2_mag - r2_mag_old) < 1e-6)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iter >= 50 || r2_mag < 100.0)
|
||||||
|
return -1; /* failed to converge or nonsensical result */
|
||||||
|
|
||||||
|
/* Check slant ranges are positive */
|
||||||
|
if (rho[0] < 0.0 || rho[1] < 0.0 || rho[2] < 0.0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
/* Compute all 3 position vectors for Gibbs */
|
||||||
|
{
|
||||||
|
double pos1[3], pos3[3];
|
||||||
|
|
||||||
|
for (i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
pos1[i] = R[0][i] + rho[0] * L[0][i];
|
||||||
|
pos3[i] = R[2][i] + rho[2] * L[2][i];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Use Gibbs to get velocity at r2 */
|
||||||
|
rc = od_gibbs(pos1, r2, pos3, jd[0], jd[1], jd[2], result);
|
||||||
|
|
||||||
|
/* If Gibbs fails (short arc / nearly coplanar), try Herrick-Gibbs */
|
||||||
|
if (rc != 0)
|
||||||
|
{
|
||||||
|
/* Herrick-Gibbs: use f and g series for velocity estimation */
|
||||||
|
double dt31 = (jd[2] - jd[0]) * 86400.0;
|
||||||
|
double dt21 = (jd[1] - jd[0]) * 86400.0;
|
||||||
|
double dt32 = (jd[2] - jd[1]) * 86400.0;
|
||||||
|
|
||||||
|
if (fabs(dt31) < 1.0 || fabs(dt21) < 1.0 || fabs(dt32) < 1.0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
for (i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
v2[i] = -dt32 * (1.0 / (dt21 * dt31)
|
||||||
|
+ MU_KM3_S2 / (12.0 * vec_mag(pos1) *
|
||||||
|
vec_mag(pos1) * vec_mag(pos1))) * pos1[i]
|
||||||
|
+ (dt32 - dt21) * (1.0 / (dt21 * dt32)
|
||||||
|
+ MU_KM3_S2 / (12.0 * r2_mag * r2_mag * r2_mag)) * r2[i]
|
||||||
|
+ dt21 * (1.0 / (dt32 * dt31)
|
||||||
|
+ MU_KM3_S2 / (12.0 * vec_mag(pos3) *
|
||||||
|
vec_mag(pos3) * vec_mag(pos3))) * pos3[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = od_eci_to_keplerian(r2, v2, &result->kep);
|
||||||
|
if (rc != 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (result->kep.ecc >= 1.0 || result->kep.n <= 0.0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
result->epoch_jd = jd[1];
|
||||||
|
result->valid = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|||||||
18
src/od_iod.h
18
src/od_iod.h
@ -42,4 +42,22 @@ int od_gibbs(const double pos1[3], const double pos2[3],
|
|||||||
double jd1, double jd2, double jd3,
|
double jd1, double jd2, double jd3,
|
||||||
od_iod_result_t *result);
|
od_iod_result_t *result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Gauss method: recover orbit from 3 angles-only (RA/Dec) observations.
|
||||||
|
*
|
||||||
|
* ra[3], dec[3]: right ascension and declination in radians
|
||||||
|
* jd[3]: Julian dates of each observation
|
||||||
|
* obs_ecef[3][3]: observer ECEF positions (km) at each epoch
|
||||||
|
* result: output Keplerian elements at epoch jd[1] (middle obs)
|
||||||
|
*
|
||||||
|
* Returns 0 on success, -1 on failure (non-convergence, degenerate).
|
||||||
|
*
|
||||||
|
* Implements Vallado Algorithm 52 with iterative refinement of the
|
||||||
|
* slant range at the middle observation.
|
||||||
|
*/
|
||||||
|
int od_gauss(const double ra[3], const double dec[3],
|
||||||
|
const double jd[3],
|
||||||
|
const double obs_ecef[3][3],
|
||||||
|
od_iod_result_t *result);
|
||||||
|
|
||||||
#endif /* PG_ORRERY_OD_IOD_H */
|
#endif /* PG_ORRERY_OD_IOD_H */
|
||||||
|
|||||||
@ -373,6 +373,52 @@ od_keplerian_to_eci(const od_keplerian_t *kep,
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* RA/Dec utilities for angles-only mode
|
||||||
|
* ================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
void
|
||||||
|
od_radec_to_los(double ra, double dec, double los[3])
|
||||||
|
{
|
||||||
|
double cd = cos(dec);
|
||||||
|
los[0] = cd * cos(ra);
|
||||||
|
los[1] = cd * sin(ra);
|
||||||
|
los[2] = sin(dec);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
od_teme_to_radec(const double pos_teme[3], const double obs_ecef[3],
|
||||||
|
double gmst, double *ra, double *dec)
|
||||||
|
{
|
||||||
|
double cg = cos(gmst), sg = sin(gmst);
|
||||||
|
double pos_ecef[3], range_ecef[3], range_teme[3], rm;
|
||||||
|
|
||||||
|
/* TEME → ECEF */
|
||||||
|
pos_ecef[0] = cg * pos_teme[0] + sg * pos_teme[1];
|
||||||
|
pos_ecef[1] = -sg * pos_teme[0] + cg * pos_teme[1];
|
||||||
|
pos_ecef[2] = pos_teme[2];
|
||||||
|
|
||||||
|
/* Observer-relative range in ECEF */
|
||||||
|
range_ecef[0] = pos_ecef[0] - obs_ecef[0];
|
||||||
|
range_ecef[1] = pos_ecef[1] - obs_ecef[1];
|
||||||
|
range_ecef[2] = pos_ecef[2] - obs_ecef[2];
|
||||||
|
|
||||||
|
/* Back to TEME (inertial) for RA/Dec */
|
||||||
|
range_teme[0] = cg * range_ecef[0] - sg * range_ecef[1];
|
||||||
|
range_teme[1] = sg * range_ecef[0] + cg * range_ecef[1];
|
||||||
|
range_teme[2] = range_ecef[2];
|
||||||
|
|
||||||
|
rm = sqrt(range_teme[0]*range_teme[0] +
|
||||||
|
range_teme[1]*range_teme[1] +
|
||||||
|
range_teme[2]*range_teme[2]);
|
||||||
|
|
||||||
|
*dec = asin(range_teme[2] / rm);
|
||||||
|
*ra = atan2(range_teme[1], range_teme[0]);
|
||||||
|
if (*ra < 0.0) *ra += 2.0 * M_PI;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ================================================================
|
/* ================================================================
|
||||||
* Keplerian ↔ equinoctial element conversion
|
* Keplerian ↔ equinoctial element conversion
|
||||||
* ================================================================
|
* ================================================================
|
||||||
|
|||||||
@ -126,6 +126,22 @@ void od_observer_to_ecef(double lat, double lon, double alt_m,
|
|||||||
*/
|
*/
|
||||||
double od_gmst_from_jd(double jd);
|
double od_gmst_from_jd(double jd);
|
||||||
|
|
||||||
|
/* ── RA/Dec utilities (angles-only mode) ───────────────── */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* RA/Dec (radians) → unit line-of-sight vector (equatorial TEME).
|
||||||
|
*/
|
||||||
|
void od_radec_to_los(double ra, double dec, double los[3]);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TEME satellite pos + observer ECEF + GMST → RA/Dec (radians).
|
||||||
|
* Computes observer-relative direction in inertial (TEME) frame.
|
||||||
|
* TEME ≈ J2000 equatorial for our accuracy needs (~1 arcsec offset
|
||||||
|
* from nutation, well below TLE accuracy floor of ~1 km ≈ 20 arcsec at LEO).
|
||||||
|
*/
|
||||||
|
void od_teme_to_radec(const double pos_teme[3], const double obs_ecef[3],
|
||||||
|
double gmst, double *ra, double *dec);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Normalize angle to [0, 2*pi)
|
* Normalize angle to [0, 2*pi)
|
||||||
*/
|
*/
|
||||||
|
|||||||
221
src/od_solver.c
221
src/od_solver.c
@ -224,15 +224,21 @@ compute_residuals_eci(const tle_t *tle, const od_observation_t *obs,
|
|||||||
return sqrt(sum_sq / n_obs);
|
return sqrt(sum_sq / n_obs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Range rate scale: maps 1 km/s range rate error to 10 km equivalent.
|
||||||
|
* Conservative default; fine-grained control via per-obs weights. */
|
||||||
|
#define OD_RR_SCALE 10.0
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Compute residual vector for topocentric observations.
|
* Compute residual vector for topocentric observations.
|
||||||
* residuals[i*3 .. i*3+2] = observed[i] - propagated[i]
|
* residuals[i*ncomp .. i*ncomp+(ncomp-1)] = observed[i] - propagated[i]
|
||||||
* Components: (az_diff, el_diff, range_diff) in (rad, rad, km).
|
* ncomp=3: (az_diff, el_diff, range_diff) in (rad, rad, km).
|
||||||
|
* ncomp=4: adds range_rate_diff scaled by OD_RR_SCALE.
|
||||||
* Returns RMS range error in km. Returns -1.0 on propagation failure.
|
* Returns RMS range error in km. Returns -1.0 on propagation failure.
|
||||||
*/
|
*/
|
||||||
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 *observers,
|
int n_obs, const od_observer_t *observers,
|
||||||
|
int fit_range_rate, int ncomp,
|
||||||
double *residuals)
|
double *residuals)
|
||||||
{
|
{
|
||||||
double *params;
|
double *params;
|
||||||
@ -318,13 +324,86 @@ compute_residuals_topo(const tle_t *tle, const od_observation_t *obs,
|
|||||||
el_diff = obs[i].data[1] - el;
|
el_diff = obs[i].data[1] - el;
|
||||||
range_diff = obs[i].data[2] - range_km;
|
range_diff = obs[i].data[2] - range_km;
|
||||||
|
|
||||||
residuals[i * 3 + 0] = az_diff * range_km * cos(el); /* scale to km */
|
residuals[i * ncomp + 0] = az_diff * range_km * cos(el); /* scale to km */
|
||||||
residuals[i * 3 + 1] = el_diff * range_km; /* scale to km */
|
residuals[i * ncomp + 1] = el_diff * range_km; /* scale to km */
|
||||||
residuals[i * 3 + 2] = range_diff;
|
residuals[i * ncomp + 2] = range_diff;
|
||||||
|
|
||||||
|
if (fit_range_rate)
|
||||||
|
{
|
||||||
|
double rr_computed = (dx * vel_ecef[0] + dy * vel_ecef[1]
|
||||||
|
+ dz * vel_ecef[2]) / range_km;
|
||||||
|
residuals[i * ncomp + 3] = (obs[i].data[3] - rr_computed) * OD_RR_SCALE;
|
||||||
|
}
|
||||||
|
|
||||||
sum_sq += range_diff * range_diff;
|
sum_sq += range_diff * range_diff;
|
||||||
|
}
|
||||||
|
|
||||||
(void)vel_ecef; /* reserved for range rate fitting */
|
od_free(params);
|
||||||
|
return sqrt(sum_sq / n_obs);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Compute residual vector for RA/Dec (angles-only) observations.
|
||||||
|
* residuals[i*2 + 0] = (ra_obs - ra_comp) * cos(dec_comp) [great-circle]
|
||||||
|
* residuals[i*2 + 1] = dec_obs - dec_comp
|
||||||
|
*
|
||||||
|
* RA scaled by cos(dec) converts angular separation to great-circle
|
||||||
|
* distance, avoiding inflated residuals near the poles.
|
||||||
|
*
|
||||||
|
* Returns RMS angular residual in radians. Returns -1.0 on failure.
|
||||||
|
*/
|
||||||
|
static double
|
||||||
|
compute_residuals_radec(const tle_t *tle, const od_observation_t *obs,
|
||||||
|
int n_obs, const od_observer_t *observers,
|
||||||
|
double *residuals)
|
||||||
|
{
|
||||||
|
double *params;
|
||||||
|
int is_deep;
|
||||||
|
double sum_sq = 0.0;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
is_deep = select_ephemeris(tle);
|
||||||
|
if (is_deep < 0)
|
||||||
|
return -1.0;
|
||||||
|
|
||||||
|
params = (double *)od_alloc(sizeof(double) * N_SAT_PARAMS);
|
||||||
|
init_params(tle, params, is_deep);
|
||||||
|
|
||||||
|
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;
|
||||||
|
double gmst;
|
||||||
|
double ra_comp, dec_comp;
|
||||||
|
double ra_diff, dec_diff;
|
||||||
|
|
||||||
|
err = propagate_with_params(tle, tsince, params, is_deep, pos, vel);
|
||||||
|
if (err != 0 && err != SXPX_WARN_ORBIT_WITHIN_EARTH &&
|
||||||
|
err != SXPX_WARN_PERIGEE_WITHIN_EARTH)
|
||||||
|
{
|
||||||
|
od_free(params);
|
||||||
|
return -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compute RA/Dec from propagated TEME position */
|
||||||
|
gmst = od_gmst_from_jd(obs[i].jd);
|
||||||
|
od_teme_to_radec(pos, observer->ecef, gmst, &ra_comp, &dec_comp);
|
||||||
|
|
||||||
|
/* RA wrap-around */
|
||||||
|
ra_diff = obs[i].data[0] - ra_comp;
|
||||||
|
if (ra_diff > M_PI) ra_diff -= 2.0 * M_PI;
|
||||||
|
if (ra_diff < -M_PI) ra_diff += 2.0 * M_PI;
|
||||||
|
|
||||||
|
dec_diff = obs[i].data[1] - dec_comp;
|
||||||
|
|
||||||
|
residuals[i * 2 + 0] = ra_diff * cos(dec_comp);
|
||||||
|
residuals[i * 2 + 1] = dec_diff;
|
||||||
|
|
||||||
|
sum_sq += ra_diff * cos(dec_comp) * ra_diff * cos(dec_comp)
|
||||||
|
+ dec_diff * dec_diff;
|
||||||
}
|
}
|
||||||
|
|
||||||
od_free(params);
|
od_free(params);
|
||||||
@ -549,6 +628,78 @@ initial_guess_from_topo(const od_observation_t *obs, int n_obs,
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Compute initial orbit from RA/Dec (angles-only) observations.
|
||||||
|
*
|
||||||
|
* Picks 3 well-spaced observations, calls Gauss IOD (Vallado Algorithm 52)
|
||||||
|
* to recover the orbit at the middle epoch.
|
||||||
|
* Returns 0 on success, -1 if Gauss fails.
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
initial_guess_from_radec(const od_observation_t *obs, int n_obs,
|
||||||
|
const od_observer_t *observers,
|
||||||
|
tle_t *guess)
|
||||||
|
{
|
||||||
|
int idx[3];
|
||||||
|
double ra[3], dec[3], jd[3];
|
||||||
|
double obs_ecef[3][3];
|
||||||
|
int k;
|
||||||
|
od_iod_result_t iod;
|
||||||
|
|
||||||
|
if (n_obs < 3)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
idx[0] = 0;
|
||||||
|
idx[1] = n_obs / 2;
|
||||||
|
idx[2] = n_obs - 1;
|
||||||
|
|
||||||
|
for (k = 0; k < 3; k++)
|
||||||
|
{
|
||||||
|
const od_observation_t *o = &obs[idx[k]];
|
||||||
|
const od_observer_t *observer = &observers[o->observer_idx];
|
||||||
|
|
||||||
|
ra[k] = o->data[0];
|
||||||
|
dec[k] = o->data[1];
|
||||||
|
jd[k] = o->jd;
|
||||||
|
|
||||||
|
obs_ecef[k][0] = observer->ecef[0];
|
||||||
|
obs_ecef[k][1] = observer->ecef[1];
|
||||||
|
obs_ecef[k][2] = observer->ecef[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (od_gauss(ra, dec, jd, obs_ecef, &iod) != 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
kep_to_seed_tle(&iod.kep, iod.epoch_jd, guess);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ── Observation weighting ──────────────────────────────── */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Scale residuals by sqrt(weight) for weighted least-squares.
|
||||||
|
*
|
||||||
|
* When both the nominal and perturbed residuals are scaled identically,
|
||||||
|
* the Jacobian (resid - resid_pert) / h naturally becomes the weighted
|
||||||
|
* Jacobian. The covariance (H^T H)^{-1} becomes (H^T W H)^{-1}.
|
||||||
|
*/
|
||||||
|
static void
|
||||||
|
apply_obs_weights(double *residuals, int n_obs, int ncomp,
|
||||||
|
const double *weights)
|
||||||
|
{
|
||||||
|
int i, j;
|
||||||
|
if (!weights)
|
||||||
|
return;
|
||||||
|
for (i = 0; i < n_obs; i++)
|
||||||
|
{
|
||||||
|
double sw = sqrt(weights[i]);
|
||||||
|
for (j = 0; j < ncomp; j++)
|
||||||
|
residuals[i * ncomp + j] *= sw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ── Main solver ───────────────────────────────────────── */
|
/* ── Main solver ───────────────────────────────────────── */
|
||||||
|
|
||||||
int
|
int
|
||||||
@ -577,7 +728,13 @@ od_fit_tle(const od_observation_t *obs, int n_obs,
|
|||||||
|
|
||||||
/* Validate inputs */
|
/* Validate inputs */
|
||||||
nstate = config->fit_bstar ? OD_NSTATE_7 : OD_NSTATE_6;
|
nstate = config->fit_bstar ? OD_NSTATE_7 : OD_NSTATE_6;
|
||||||
ncomp = (config->obs_type == OD_OBS_ECI) ? 6 : 3;
|
|
||||||
|
if (config->obs_type == OD_OBS_ECI)
|
||||||
|
ncomp = 6;
|
||||||
|
else if (config->obs_type == OD_OBS_RADEC)
|
||||||
|
ncomp = 2;
|
||||||
|
else
|
||||||
|
ncomp = config->fit_range_rate ? 4 : 3;
|
||||||
|
|
||||||
if (n_obs < nstate)
|
if (n_obs < nstate)
|
||||||
{
|
{
|
||||||
@ -600,6 +757,18 @@ od_fit_tle(const od_observation_t *obs, int n_obs,
|
|||||||
{
|
{
|
||||||
if (config->obs_type == OD_OBS_ECI)
|
if (config->obs_type == OD_OBS_ECI)
|
||||||
initial_guess_from_eci(obs, n_obs, &seed_tle);
|
initial_guess_from_eci(obs, n_obs, &seed_tle);
|
||||||
|
else if (config->obs_type == OD_OBS_RADEC)
|
||||||
|
{
|
||||||
|
int rc_iod = initial_guess_from_radec(obs, n_obs,
|
||||||
|
config->observers,
|
||||||
|
&seed_tle);
|
||||||
|
if (rc_iod != 0)
|
||||||
|
{
|
||||||
|
snprintf(result->status, sizeof(result->status),
|
||||||
|
"IOD bootstrap failed (Gauss): need seed TLE");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
int rc_iod = initial_guess_from_topo(obs, n_obs,
|
int rc_iod = initial_guess_from_topo(obs, n_obs,
|
||||||
@ -642,9 +811,14 @@ od_fit_tle(const od_observation_t *obs, int n_obs,
|
|||||||
/* Compute initial RMS */
|
/* Compute initial RMS */
|
||||||
if (config->obs_type == OD_OBS_ECI)
|
if (config->obs_type == OD_OBS_ECI)
|
||||||
rms_cur = compute_residuals_eci(¤t_tle, obs, n_obs, residuals);
|
rms_cur = compute_residuals_eci(¤t_tle, obs, n_obs, residuals);
|
||||||
|
else if (config->obs_type == OD_OBS_RADEC)
|
||||||
|
rms_cur = compute_residuals_radec(¤t_tle, obs, n_obs,
|
||||||
|
config->observers, residuals);
|
||||||
else
|
else
|
||||||
rms_cur = compute_residuals_topo(¤t_tle, obs, n_obs,
|
rms_cur = compute_residuals_topo(¤t_tle, obs, n_obs,
|
||||||
config->observers, residuals);
|
config->observers,
|
||||||
|
config->fit_range_rate, ncomp,
|
||||||
|
residuals);
|
||||||
|
|
||||||
if (rms_cur < 0.0)
|
if (rms_cur < 0.0)
|
||||||
{
|
{
|
||||||
@ -661,6 +835,9 @@ od_fit_tle(const od_observation_t *obs, int n_obs,
|
|||||||
result->rms_initial = rms_cur;
|
result->rms_initial = rms_cur;
|
||||||
rms_prev = rms_cur;
|
rms_prev = rms_cur;
|
||||||
|
|
||||||
|
/* Weight residuals for Jacobian build (RMS already stored unweighted) */
|
||||||
|
apply_obs_weights(residuals, n_obs, ncomp, config->weights);
|
||||||
|
|
||||||
/* Already converged (perfect seed)? Skip DC loop but still
|
/* Already converged (perfect seed)? Skip DC loop but still
|
||||||
* compute covariance — users need uncertainty estimates even
|
* compute covariance — users need uncertainty estimates even
|
||||||
* when the initial guess is exact. */
|
* when the initial guess is exact. */
|
||||||
@ -726,9 +903,16 @@ od_fit_tle(const od_observation_t *obs, int n_obs,
|
|||||||
|
|
||||||
if (config->obs_type == OD_OBS_ECI)
|
if (config->obs_type == OD_OBS_ECI)
|
||||||
compute_residuals_eci(&tle_pert, obs, n_obs, resid_pert);
|
compute_residuals_eci(&tle_pert, obs, n_obs, resid_pert);
|
||||||
|
else if (config->obs_type == OD_OBS_RADEC)
|
||||||
|
compute_residuals_radec(&tle_pert, obs, n_obs,
|
||||||
|
config->observers, resid_pert);
|
||||||
else
|
else
|
||||||
compute_residuals_topo(&tle_pert, obs, n_obs,
|
compute_residuals_topo(&tle_pert, obs, n_obs,
|
||||||
config->observers, resid_pert);
|
config->observers,
|
||||||
|
config->fit_range_rate, ncomp,
|
||||||
|
resid_pert);
|
||||||
|
|
||||||
|
apply_obs_weights(resid_pert, n_obs, ncomp, config->weights);
|
||||||
|
|
||||||
/* 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.
|
||||||
@ -802,9 +986,14 @@ od_fit_tle(const od_observation_t *obs, int n_obs,
|
|||||||
|
|
||||||
if (config->obs_type == OD_OBS_ECI)
|
if (config->obs_type == OD_OBS_ECI)
|
||||||
rms_cur = compute_residuals_eci(¤t_tle, obs, n_obs, residuals);
|
rms_cur = compute_residuals_eci(¤t_tle, obs, n_obs, residuals);
|
||||||
|
else if (config->obs_type == OD_OBS_RADEC)
|
||||||
|
rms_cur = compute_residuals_radec(¤t_tle, obs, n_obs,
|
||||||
|
config->observers, residuals);
|
||||||
else
|
else
|
||||||
rms_cur = compute_residuals_topo(¤t_tle, obs, n_obs,
|
rms_cur = compute_residuals_topo(¤t_tle, obs, n_obs,
|
||||||
config->observers, residuals);
|
config->observers,
|
||||||
|
config->fit_range_rate, ncomp,
|
||||||
|
residuals);
|
||||||
|
|
||||||
if (rms_cur < 0.0)
|
if (rms_cur < 0.0)
|
||||||
{
|
{
|
||||||
@ -813,6 +1002,9 @@ od_fit_tle(const od_observation_t *obs, int n_obs,
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Weight residuals for next Jacobian build (RMS already stored) */
|
||||||
|
apply_obs_weights(residuals, n_obs, ncomp, config->weights);
|
||||||
|
|
||||||
/* Adaptive step control: adjust trust-region size based on
|
/* Adaptive step control: adjust trust-region size based on
|
||||||
* whether the solver is converging or diverging. Halve the
|
* whether the solver is converging or diverging. Halve the
|
||||||
* step on divergence (prevents oscillation with poor initial
|
* step on divergence (prevents oscillation with poor initial
|
||||||
@ -909,9 +1101,16 @@ compute_covariance:
|
|||||||
|
|
||||||
if (config->obs_type == OD_OBS_ECI)
|
if (config->obs_type == OD_OBS_ECI)
|
||||||
compute_residuals_eci(&tle_pert, obs, n_obs, resid_pert);
|
compute_residuals_eci(&tle_pert, obs, n_obs, resid_pert);
|
||||||
|
else if (config->obs_type == OD_OBS_RADEC)
|
||||||
|
compute_residuals_radec(&tle_pert, obs, n_obs,
|
||||||
|
config->observers, resid_pert);
|
||||||
else
|
else
|
||||||
compute_residuals_topo(&tle_pert, obs, n_obs,
|
compute_residuals_topo(&tle_pert, obs, n_obs,
|
||||||
config->observers, resid_pert);
|
config->observers,
|
||||||
|
config->fit_range_rate, ncomp,
|
||||||
|
resid_pert);
|
||||||
|
|
||||||
|
apply_obs_weights(resid_pert, n_obs, ncomp, config->weights);
|
||||||
|
|
||||||
for (k_cov = 0; k_cov < nrows; k_cov++)
|
for (k_cov = 0; k_cov < nrows; k_cov++)
|
||||||
jacobian[j_cov * nrows + k_cov] =
|
jacobian[j_cov * nrows + k_cov] =
|
||||||
|
|||||||
@ -31,8 +31,9 @@
|
|||||||
*/
|
*/
|
||||||
typedef enum
|
typedef enum
|
||||||
{
|
{
|
||||||
OD_OBS_ECI = 0, /* 6-component: x, y, z, vx, vy, vz (km, km/s) */
|
OD_OBS_ECI = 0, /* 6-component: x, y, z, vx, vy, vz (km, km/s) */
|
||||||
OD_OBS_TOPO = 1 /* 3-component: az, el, range (rad, rad, km) */
|
OD_OBS_TOPO = 1, /* 3-component: az, el, range (rad, rad, km) */
|
||||||
|
OD_OBS_RADEC = 2 /* 2-component: RA, Dec (radians, equatorial) */
|
||||||
} od_obs_type_t;
|
} od_obs_type_t;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -63,9 +64,12 @@ 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 fit_range_rate; /* include range_rate in topo residuals */
|
||||||
int max_iter; /* iteration limit */
|
int max_iter; /* iteration limit */
|
||||||
od_observer_t *observers; /* array of observers (topo mode) */
|
od_observer_t *observers; /* array of observers (topo mode) */
|
||||||
int n_observers; /* count (0 for ECI mode) */
|
int n_observers; /* count (0 for ECI mode) */
|
||||||
|
double *weights; /* per-observation weights (NULL=uniform) */
|
||||||
|
int n_weights;
|
||||||
} od_config_t;
|
} od_config_t;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@ -623,3 +623,306 @@ FROM result;
|
|||||||
t | t | t
|
t | t | t
|
||||||
(1 row)
|
(1 row)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 18: Range rate round-trip
|
||||||
|
--
|
||||||
|
-- Propagate ISS, observe() to get topo with range_rate,
|
||||||
|
-- fit via tle_from_topocentric with fit_range_rate := true.
|
||||||
|
-- Verify convergence.
|
||||||
|
-- ============================================================
|
||||||
|
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
|
||||||
|
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, obs, t, false, 20,
|
||||||
|
fit_range_rate := true)).*
|
||||||
|
FROM topo_obs, mit, iss_tle
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
rms_final < 10.0 AS rms_under_10km,
|
||||||
|
status = 'converged' AS did_converge
|
||||||
|
FROM result;
|
||||||
|
rms_under_10km | did_converge
|
||||||
|
----------------+--------------
|
||||||
|
t | t
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 19: Range rate disabled matches existing behavior
|
||||||
|
--
|
||||||
|
-- Same data with fit_range_rate := false (default).
|
||||||
|
-- Results should converge the same as Test 4.
|
||||||
|
-- ============================================================
|
||||||
|
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
|
||||||
|
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, obs, t, false, 20,
|
||||||
|
fit_range_rate := false)).*
|
||||||
|
FROM topo_obs, mit, iss_tle
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
rms_final < 10.0 AS rms_under_10km,
|
||||||
|
status = 'converged' AS did_converge
|
||||||
|
FROM result;
|
||||||
|
rms_under_10km | did_converge
|
||||||
|
----------------+--------------
|
||||||
|
t | t
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 20: Weighted observations round-trip (uniform weights)
|
||||||
|
--
|
||||||
|
-- Uniform weights ARRAY[1,1,...,1]::float8[] should produce
|
||||||
|
-- identical results to no weights.
|
||||||
|
-- ============================================================
|
||||||
|
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
|
||||||
|
),
|
||||||
|
obs AS (
|
||||||
|
SELECT
|
||||||
|
array_agg(sgp4_propagate(t, ts) ORDER BY ts) AS positions,
|
||||||
|
array_agg(ts ORDER BY ts) AS times
|
||||||
|
FROM iss_tle,
|
||||||
|
generate_series(
|
||||||
|
'2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 13:30:00+00'::timestamptz,
|
||||||
|
'5 minutes'::interval
|
||||||
|
) AS ts
|
||||||
|
),
|
||||||
|
uniform_weights AS (
|
||||||
|
SELECT array_agg(1.0::float8) AS w
|
||||||
|
FROM generate_series(1, 19)
|
||||||
|
),
|
||||||
|
result AS (
|
||||||
|
SELECT (tle_from_eci(positions, times, t, false, 15,
|
||||||
|
weights := w)).* FROM obs, iss_tle, uniform_weights
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
rms_final < 1.0 AS rms_under_1km,
|
||||||
|
status = 'converged' AS did_converge
|
||||||
|
FROM result;
|
||||||
|
rms_under_1km | did_converge
|
||||||
|
---------------+--------------
|
||||||
|
t | t
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 21: Weights length mismatch error
|
||||||
|
--
|
||||||
|
-- Wrong-length weights array should raise an error.
|
||||||
|
-- ============================================================
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM tle_from_eci(
|
||||||
|
(SELECT array_agg(sgp4_propagate(
|
||||||
|
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,
|
||||||
|
ts) ORDER BY ts)
|
||||||
|
FROM generate_series(
|
||||||
|
'2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 13:30:00+00'::timestamptz,
|
||||||
|
'5 minutes'::interval
|
||||||
|
) AS ts),
|
||||||
|
(SELECT array_agg(ts ORDER BY ts)
|
||||||
|
FROM generate_series(
|
||||||
|
'2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 13:30:00+00'::timestamptz,
|
||||||
|
'5 minutes'::interval
|
||||||
|
) AS ts),
|
||||||
|
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,
|
||||||
|
false, 15,
|
||||||
|
ARRAY[1.0, 2.0, 3.0]::float8[] -- wrong length
|
||||||
|
);
|
||||||
|
RAISE NOTICE 'ERROR: should have raised exception';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN array_subscript_error THEN
|
||||||
|
RAISE NOTICE 'OK: weights length mismatch error caught';
|
||||||
|
END $$;
|
||||||
|
NOTICE: OK: weights length mismatch error caught
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 22: ECI with weights (verify API works)
|
||||||
|
--
|
||||||
|
-- Non-uniform weights (downweight first few observations).
|
||||||
|
-- ============================================================
|
||||||
|
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
|
||||||
|
),
|
||||||
|
obs AS (
|
||||||
|
SELECT
|
||||||
|
array_agg(sgp4_propagate(t, ts) ORDER BY ts) AS positions,
|
||||||
|
array_agg(ts ORDER BY ts) AS times
|
||||||
|
FROM iss_tle,
|
||||||
|
generate_series(
|
||||||
|
'2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 13:30:00+00'::timestamptz,
|
||||||
|
'5 minutes'::interval
|
||||||
|
) AS ts
|
||||||
|
),
|
||||||
|
nonuniform_weights AS (
|
||||||
|
SELECT array_agg(CASE WHEN n <= 5 THEN 0.1 ELSE 1.0 END ::float8) AS w
|
||||||
|
FROM generate_series(1, 19) AS n
|
||||||
|
),
|
||||||
|
result AS (
|
||||||
|
SELECT (tle_from_eci(positions, times, t, false, 15,
|
||||||
|
weights := w)).* FROM obs, iss_tle, nonuniform_weights
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
rms_final < 5.0 AS rms_under_5km,
|
||||||
|
status = 'converged' AS did_converge
|
||||||
|
FROM result;
|
||||||
|
rms_under_5km | did_converge
|
||||||
|
---------------+--------------
|
||||||
|
t | t
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 23: Angles-only with seed TLE
|
||||||
|
--
|
||||||
|
-- Propagate ISS, compute RA/Dec from TEME position relative
|
||||||
|
-- to observer, fit via tle_from_angles() with known seed.
|
||||||
|
-- ============================================================
|
||||||
|
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
|
||||||
|
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
|
||||||
|
),
|
||||||
|
radec AS (
|
||||||
|
SELECT
|
||||||
|
array_agg(
|
||||||
|
(topo_azimuth(o) * 24.0 / 360.0)::float8 -- crude az→RA proxy (accessor returns degrees)
|
||||||
|
ORDER BY ts
|
||||||
|
) AS ra_h,
|
||||||
|
array_agg(
|
||||||
|
topo_elevation(o)::float8 -- accessor returns degrees, valid as Dec proxy
|
||||||
|
ORDER BY ts
|
||||||
|
) AS dec_d,
|
||||||
|
array_agg(ts ORDER BY ts) AS times
|
||||||
|
FROM (
|
||||||
|
SELECT observe(t, obs, ts) AS o, ts
|
||||||
|
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
|
||||||
|
) sub
|
||||||
|
),
|
||||||
|
result AS (
|
||||||
|
SELECT (tle_from_angles(ra_h, dec_d, times, obs, t)).*
|
||||||
|
FROM radec, mit, iss_tle
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
status IN ('converged', 'max_iterations') AS solver_ran,
|
||||||
|
rms_final IS NOT NULL AS has_rms
|
||||||
|
FROM result;
|
||||||
|
solver_ran | has_rms
|
||||||
|
------------+---------
|
||||||
|
t | t
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 24: Angles-only without seed (Gauss IOD bootstrap)
|
||||||
|
--
|
||||||
|
-- Without a seed TLE, Gauss IOD must recover an initial orbit
|
||||||
|
-- from the RA/Dec observations alone. The crude azimuth→RA
|
||||||
|
-- approximation used here is not physically meaningful, so
|
||||||
|
-- Gauss may fail to converge — we accept that as valid behavior.
|
||||||
|
-- The real Gauss validation is in the standalone C tests.
|
||||||
|
-- ============================================================
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
v_status text;
|
||||||
|
v_rms float8;
|
||||||
|
BEGIN
|
||||||
|
SELECT status, rms_final INTO v_status, v_rms
|
||||||
|
FROM (
|
||||||
|
SELECT (tle_from_angles(
|
||||||
|
(SELECT array_agg((topo_azimuth(o) * 24.0 / 360.0)::float8 ORDER BY ts)
|
||||||
|
FROM (SELECT 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, ts) AS o, ts
|
||||||
|
FROM generate_series('2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 13:30:00+00'::timestamptz,
|
||||||
|
'5 minutes'::interval) AS ts) sub),
|
||||||
|
(SELECT array_agg(topo_elevation(o)::float8 ORDER BY ts)
|
||||||
|
FROM (SELECT 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, ts) AS o, ts
|
||||||
|
FROM generate_series('2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 13:30:00+00'::timestamptz,
|
||||||
|
'5 minutes'::interval) AS ts) sub),
|
||||||
|
(SELECT array_agg(ts ORDER BY ts)
|
||||||
|
FROM generate_series('2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 13:30:00+00'::timestamptz,
|
||||||
|
'5 minutes'::interval) AS ts),
|
||||||
|
'(42.36,-71.09,20)'::observer
|
||||||
|
)).*
|
||||||
|
) r;
|
||||||
|
RAISE NOTICE 'OK: seedless angles-only ran: status=%, rms=%', v_status, v_rms;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN data_exception THEN
|
||||||
|
RAISE NOTICE 'OK: seedless angles-only Gauss IOD failed as expected with approximate data';
|
||||||
|
END $$;
|
||||||
|
NOTICE: OK: seedless angles-only Gauss IOD failed as expected with approximate data
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 25: Angles-only error cases
|
||||||
|
--
|
||||||
|
-- Too few observations should raise an error.
|
||||||
|
-- ============================================================
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM tle_from_angles(
|
||||||
|
ARRAY[1.0, 2.0]::float8[],
|
||||||
|
ARRAY[30.0, 35.0]::float8[],
|
||||||
|
ARRAY['2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 12:05:00+00'::timestamptz],
|
||||||
|
'(42.36,-71.09,20)'::observer
|
||||||
|
);
|
||||||
|
RAISE NOTICE 'ERROR: should have raised exception';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN data_exception THEN
|
||||||
|
RAISE NOTICE 'OK: insufficient observations error caught';
|
||||||
|
WHEN invalid_parameter_value THEN
|
||||||
|
RAISE NOTICE 'OK: insufficient observations error caught';
|
||||||
|
END $$;
|
||||||
|
NOTICE: OK: insufficient observations error caught
|
||||||
|
|||||||
@ -581,3 +581,294 @@ SELECT
|
|||||||
covariance IS NOT NULL AS has_covariance,
|
covariance IS NOT NULL AS has_covariance,
|
||||||
nstate = 6 AS is_6state
|
nstate = 6 AS is_6state
|
||||||
FROM result;
|
FROM result;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 18: Range rate round-trip
|
||||||
|
--
|
||||||
|
-- Propagate ISS, observe() to get topo with range_rate,
|
||||||
|
-- fit via tle_from_topocentric with fit_range_rate := true.
|
||||||
|
-- Verify convergence.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
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
|
||||||
|
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, obs, t, false, 20,
|
||||||
|
fit_range_rate := true)).*
|
||||||
|
FROM topo_obs, mit, iss_tle
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
rms_final < 10.0 AS rms_under_10km,
|
||||||
|
status = 'converged' AS did_converge
|
||||||
|
FROM result;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 19: Range rate disabled matches existing behavior
|
||||||
|
--
|
||||||
|
-- Same data with fit_range_rate := false (default).
|
||||||
|
-- Results should converge the same as Test 4.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
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
|
||||||
|
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, obs, t, false, 20,
|
||||||
|
fit_range_rate := false)).*
|
||||||
|
FROM topo_obs, mit, iss_tle
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
rms_final < 10.0 AS rms_under_10km,
|
||||||
|
status = 'converged' AS did_converge
|
||||||
|
FROM result;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 20: Weighted observations round-trip (uniform weights)
|
||||||
|
--
|
||||||
|
-- Uniform weights ARRAY[1,1,...,1]::float8[] should produce
|
||||||
|
-- identical results to no weights.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
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
|
||||||
|
),
|
||||||
|
obs AS (
|
||||||
|
SELECT
|
||||||
|
array_agg(sgp4_propagate(t, ts) ORDER BY ts) AS positions,
|
||||||
|
array_agg(ts ORDER BY ts) AS times
|
||||||
|
FROM iss_tle,
|
||||||
|
generate_series(
|
||||||
|
'2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 13:30:00+00'::timestamptz,
|
||||||
|
'5 minutes'::interval
|
||||||
|
) AS ts
|
||||||
|
),
|
||||||
|
uniform_weights AS (
|
||||||
|
SELECT array_agg(1.0::float8) AS w
|
||||||
|
FROM generate_series(1, 19)
|
||||||
|
),
|
||||||
|
result AS (
|
||||||
|
SELECT (tle_from_eci(positions, times, t, false, 15,
|
||||||
|
weights := w)).* FROM obs, iss_tle, uniform_weights
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
rms_final < 1.0 AS rms_under_1km,
|
||||||
|
status = 'converged' AS did_converge
|
||||||
|
FROM result;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 21: Weights length mismatch error
|
||||||
|
--
|
||||||
|
-- Wrong-length weights array should raise an error.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM tle_from_eci(
|
||||||
|
(SELECT array_agg(sgp4_propagate(
|
||||||
|
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,
|
||||||
|
ts) ORDER BY ts)
|
||||||
|
FROM generate_series(
|
||||||
|
'2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 13:30:00+00'::timestamptz,
|
||||||
|
'5 minutes'::interval
|
||||||
|
) AS ts),
|
||||||
|
(SELECT array_agg(ts ORDER BY ts)
|
||||||
|
FROM generate_series(
|
||||||
|
'2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 13:30:00+00'::timestamptz,
|
||||||
|
'5 minutes'::interval
|
||||||
|
) AS ts),
|
||||||
|
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,
|
||||||
|
false, 15,
|
||||||
|
ARRAY[1.0, 2.0, 3.0]::float8[] -- wrong length
|
||||||
|
);
|
||||||
|
RAISE NOTICE 'ERROR: should have raised exception';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN array_subscript_error THEN
|
||||||
|
RAISE NOTICE 'OK: weights length mismatch error caught';
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 22: ECI with weights (verify API works)
|
||||||
|
--
|
||||||
|
-- Non-uniform weights (downweight first few observations).
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
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
|
||||||
|
),
|
||||||
|
obs AS (
|
||||||
|
SELECT
|
||||||
|
array_agg(sgp4_propagate(t, ts) ORDER BY ts) AS positions,
|
||||||
|
array_agg(ts ORDER BY ts) AS times
|
||||||
|
FROM iss_tle,
|
||||||
|
generate_series(
|
||||||
|
'2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 13:30:00+00'::timestamptz,
|
||||||
|
'5 minutes'::interval
|
||||||
|
) AS ts
|
||||||
|
),
|
||||||
|
nonuniform_weights AS (
|
||||||
|
SELECT array_agg(CASE WHEN n <= 5 THEN 0.1 ELSE 1.0 END ::float8) AS w
|
||||||
|
FROM generate_series(1, 19) AS n
|
||||||
|
),
|
||||||
|
result AS (
|
||||||
|
SELECT (tle_from_eci(positions, times, t, false, 15,
|
||||||
|
weights := w)).* FROM obs, iss_tle, nonuniform_weights
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
rms_final < 5.0 AS rms_under_5km,
|
||||||
|
status = 'converged' AS did_converge
|
||||||
|
FROM result;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 23: Angles-only with seed TLE
|
||||||
|
--
|
||||||
|
-- Propagate ISS, compute RA/Dec from TEME position relative
|
||||||
|
-- to observer, fit via tle_from_angles() with known seed.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
),
|
||||||
|
radec AS (
|
||||||
|
SELECT
|
||||||
|
array_agg(
|
||||||
|
(topo_azimuth(o) * 24.0 / 360.0)::float8 -- crude az→RA proxy (accessor returns degrees)
|
||||||
|
ORDER BY ts
|
||||||
|
) AS ra_h,
|
||||||
|
array_agg(
|
||||||
|
topo_elevation(o)::float8 -- accessor returns degrees, valid as Dec proxy
|
||||||
|
ORDER BY ts
|
||||||
|
) AS dec_d,
|
||||||
|
array_agg(ts ORDER BY ts) AS times
|
||||||
|
FROM (
|
||||||
|
SELECT observe(t, obs, ts) AS o, ts
|
||||||
|
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
|
||||||
|
) sub
|
||||||
|
),
|
||||||
|
result AS (
|
||||||
|
SELECT (tle_from_angles(ra_h, dec_d, times, obs, t)).*
|
||||||
|
FROM radec, mit, iss_tle
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
status IN ('converged', 'max_iterations') AS solver_ran,
|
||||||
|
rms_final IS NOT NULL AS has_rms
|
||||||
|
FROM result;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 24: Angles-only without seed (Gauss IOD bootstrap)
|
||||||
|
--
|
||||||
|
-- Without a seed TLE, Gauss IOD must recover an initial orbit
|
||||||
|
-- from the RA/Dec observations alone. The crude azimuth→RA
|
||||||
|
-- approximation used here is not physically meaningful, so
|
||||||
|
-- Gauss may fail to converge — we accept that as valid behavior.
|
||||||
|
-- The real Gauss validation is in the standalone C tests.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
v_status text;
|
||||||
|
v_rms float8;
|
||||||
|
BEGIN
|
||||||
|
SELECT status, rms_final INTO v_status, v_rms
|
||||||
|
FROM (
|
||||||
|
SELECT (tle_from_angles(
|
||||||
|
(SELECT array_agg((topo_azimuth(o) * 24.0 / 360.0)::float8 ORDER BY ts)
|
||||||
|
FROM (SELECT 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, ts) AS o, ts
|
||||||
|
FROM generate_series('2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 13:30:00+00'::timestamptz,
|
||||||
|
'5 minutes'::interval) AS ts) sub),
|
||||||
|
(SELECT array_agg(topo_elevation(o)::float8 ORDER BY ts)
|
||||||
|
FROM (SELECT 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, ts) AS o, ts
|
||||||
|
FROM generate_series('2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 13:30:00+00'::timestamptz,
|
||||||
|
'5 minutes'::interval) AS ts) sub),
|
||||||
|
(SELECT array_agg(ts ORDER BY ts)
|
||||||
|
FROM generate_series('2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 13:30:00+00'::timestamptz,
|
||||||
|
'5 minutes'::interval) AS ts),
|
||||||
|
'(42.36,-71.09,20)'::observer
|
||||||
|
)).*
|
||||||
|
) r;
|
||||||
|
RAISE NOTICE 'OK: seedless angles-only ran: status=%, rms=%', v_status, v_rms;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN data_exception THEN
|
||||||
|
RAISE NOTICE 'OK: seedless angles-only Gauss IOD failed as expected with approximate data';
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 25: Angles-only error cases
|
||||||
|
--
|
||||||
|
-- Too few observations should raise an error.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM tle_from_angles(
|
||||||
|
ARRAY[1.0, 2.0]::float8[],
|
||||||
|
ARRAY[30.0, 35.0]::float8[],
|
||||||
|
ARRAY['2024-01-01 12:00:00+00'::timestamptz,
|
||||||
|
'2024-01-01 12:05:00+00'::timestamptz],
|
||||||
|
'(42.36,-71.09,20)'::observer
|
||||||
|
);
|
||||||
|
RAISE NOTICE 'ERROR: should have raised exception';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN data_exception THEN
|
||||||
|
RAISE NOTICE 'OK: insufficient observations error caught';
|
||||||
|
WHEN invalid_parameter_value THEN
|
||||||
|
RAISE NOTICE 'OK: insufficient observations error caught';
|
||||||
|
END $$;
|
||||||
|
|||||||
282
test/test_od_gauss.c
Normal file
282
test/test_od_gauss.c
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user