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_od_math
|
||||
test/test_od_iod
|
||||
test/test_od_gauss
|
||||
|
||||
# Docs site
|
||||
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-od-math
|
||||
RUN make test-od-iod
|
||||
RUN make test-od-gauss
|
||||
|
||||
# Capture artifacts under /pg_orrery prefix for the next stage
|
||||
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 \
|
||||
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.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
|
||||
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
|
||||
|
||||
# ── 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_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'
|
||||
default_version = '0.5.0'
|
||||
default_version = '0.6.0'
|
||||
module_pathname = '$libdir/pg_orrery'
|
||||
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.';
|
||||
474
src/od_funcs.c
474
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_multi);
|
||||
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)
|
||||
@ -105,6 +107,7 @@ tle_from_eci(PG_FUNCTION_ARGS)
|
||||
pg_tle *seed_pg = has_seed ? (pg_tle *) PG_GETARG_POINTER(2) : NULL;
|
||||
bool fit_bstar = PG_ARGISNULL(3) ? false : PG_GETARG_BOOL(3);
|
||||
int32 max_iter = PG_ARGISNULL(4) ? 15 : PG_GETARG_INT32(4);
|
||||
bool has_weights = !PG_ARGISNULL(5);
|
||||
|
||||
int n_pos, n_times;
|
||||
Datum *pos_datums, *time_datums;
|
||||
@ -165,6 +168,29 @@ tle_from_eci(PG_FUNCTION_ARGS)
|
||||
config.observers = NULL;
|
||||
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 */
|
||||
if (has_seed)
|
||||
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);
|
||||
|
||||
pfree(obs);
|
||||
if (config.weights)
|
||||
pfree(config.weights);
|
||||
|
||||
if (rc != 0)
|
||||
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;
|
||||
bool fit_bstar = PG_ARGISNULL(4) ? false : PG_GETARG_BOOL(4);
|
||||
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;
|
||||
od_observation_t *obs;
|
||||
@ -301,6 +331,7 @@ tle_from_topocentric(PG_FUNCTION_ARGS)
|
||||
obs[i].data[0] = topo->azimuth;
|
||||
obs[i].data[1] = topo->elevation;
|
||||
obs[i].data[2] = topo->range_km;
|
||||
obs[i].data[3] = topo->range_rate;
|
||||
obs[i].observer_idx = 0; /* single observer */
|
||||
}
|
||||
}
|
||||
@ -309,10 +340,34 @@ tle_from_topocentric(PG_FUNCTION_ARGS)
|
||||
memset(&config, 0, sizeof(config));
|
||||
config.obs_type = OD_OBS_TOPO;
|
||||
config.fit_bstar = fit_bstar ? 1 : 0;
|
||||
config.fit_range_rate = fit_range_rate ? 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_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)
|
||||
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);
|
||||
|
||||
pfree(obs);
|
||||
if (config.weights)
|
||||
pfree(config.weights);
|
||||
|
||||
if (rc != 0)
|
||||
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;
|
||||
bool fit_bstar = PG_ARGISNULL(5) ? false : PG_GETARG_BOOL(5);
|
||||
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;
|
||||
od_observation_t *obs;
|
||||
@ -474,6 +533,7 @@ tle_from_topocentric_multi(PG_FUNCTION_ARGS)
|
||||
obs[i].data[0] = topo->azimuth;
|
||||
obs[i].data[1] = topo->elevation;
|
||||
obs[i].data[2] = topo->range_km;
|
||||
obs[i].data[3] = topo->range_rate;
|
||||
obs[i].observer_idx = oid;
|
||||
}
|
||||
|
||||
@ -481,10 +541,34 @@ tle_from_topocentric_multi(PG_FUNCTION_ARGS)
|
||||
memset(&config, 0, sizeof(config));
|
||||
config.obs_type = OD_OBS_TOPO;
|
||||
config.fit_bstar = fit_bstar ? 1 : 0;
|
||||
config.fit_range_rate = fit_range_rate ? 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_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)
|
||||
pg_tle_to_sat_code_od(seed_pg, &seed_sat);
|
||||
|
||||
@ -494,6 +578,396 @@ tle_from_topocentric_multi(PG_FUNCTION_ARGS)
|
||||
|
||||
pfree(obs);
|
||||
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)
|
||||
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;
|
||||
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,
|
||||
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 */
|
||||
|
||||
@ -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
|
||||
* ================================================================
|
||||
|
||||
@ -126,6 +126,22 @@ void od_observer_to_ecef(double lat, double lon, double alt_m,
|
||||
*/
|
||||
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)
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/* 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.
|
||||
* residuals[i*3 .. i*3+2] = observed[i] - propagated[i]
|
||||
* Components: (az_diff, el_diff, range_diff) in (rad, rad, km).
|
||||
* residuals[i*ncomp .. i*ncomp+(ncomp-1)] = observed[i] - propagated[i]
|
||||
* 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.
|
||||
*/
|
||||
static double
|
||||
compute_residuals_topo(const tle_t *tle, const od_observation_t *obs,
|
||||
int n_obs, const od_observer_t *observers,
|
||||
int fit_range_rate, int ncomp,
|
||||
double *residuals)
|
||||
{
|
||||
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;
|
||||
range_diff = obs[i].data[2] - range_km;
|
||||
|
||||
residuals[i * 3 + 0] = az_diff * range_km * cos(el); /* scale to km */
|
||||
residuals[i * 3 + 1] = el_diff * range_km; /* scale to km */
|
||||
residuals[i * 3 + 2] = range_diff;
|
||||
residuals[i * ncomp + 0] = az_diff * range_km * cos(el); /* scale to km */
|
||||
residuals[i * ncomp + 1] = el_diff * range_km; /* scale to km */
|
||||
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;
|
||||
}
|
||||
|
||||
(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);
|
||||
@ -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 ───────────────────────────────────────── */
|
||||
|
||||
int
|
||||
@ -577,7 +728,13 @@ od_fit_tle(const od_observation_t *obs, int n_obs,
|
||||
|
||||
/* Validate inputs */
|
||||
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)
|
||||
{
|
||||
@ -600,6 +757,18 @@ od_fit_tle(const od_observation_t *obs, int n_obs,
|
||||
{
|
||||
if (config->obs_type == OD_OBS_ECI)
|
||||
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
|
||||
{
|
||||
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 */
|
||||
if (config->obs_type == OD_OBS_ECI)
|
||||
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
|
||||
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)
|
||||
{
|
||||
@ -661,6 +835,9 @@ od_fit_tle(const od_observation_t *obs, int n_obs,
|
||||
result->rms_initial = 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
|
||||
* compute covariance — users need uncertainty estimates even
|
||||
* 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)
|
||||
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
|
||||
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)
|
||||
* 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)
|
||||
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
|
||||
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)
|
||||
{
|
||||
@ -813,6 +1002,9 @@ od_fit_tle(const od_observation_t *obs, int n_obs,
|
||||
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
|
||||
* whether the solver is converging or diverging. Halve the
|
||||
* step on divergence (prevents oscillation with poor initial
|
||||
@ -909,9 +1101,16 @@ compute_covariance:
|
||||
|
||||
if (config->obs_type == OD_OBS_ECI)
|
||||
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
|
||||
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++)
|
||||
jacobian[j_cov * nrows + k_cov] =
|
||||
|
||||
@ -32,7 +32,8 @@
|
||||
typedef enum
|
||||
{
|
||||
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;
|
||||
|
||||
/*
|
||||
@ -63,9 +64,12 @@ typedef struct
|
||||
{
|
||||
od_obs_type_t obs_type; /* ECI or topocentric */
|
||||
int fit_bstar; /* include B* as 7th state */
|
||||
int fit_range_rate; /* include range_rate in topo residuals */
|
||||
int max_iter; /* iteration limit */
|
||||
od_observer_t *observers; /* array of observers (topo mode) */
|
||||
int n_observers; /* count (0 for ECI mode) */
|
||||
double *weights; /* per-observation weights (NULL=uniform) */
|
||||
int n_weights;
|
||||
} od_config_t;
|
||||
|
||||
/*
|
||||
|
||||
@ -623,3 +623,306 @@ FROM result;
|
||||
t | t | t
|
||||
(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,
|
||||
nstate = 6 AS is_6state
|
||||
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