Eliminates the seed TLE requirement for topocentric fitting by computing an initial orbit estimate from 3 well-spaced observations using the Gibbs method. ECI fitting retains the single-observation r,v approach (exact for two-body) with Gibbs as fallback.
635 lines
22 KiB
C
635 lines
22 KiB
C
/*
|
|
* od_funcs.c -- SQL bindings for TLE fitting functions
|
|
*
|
|
* Exposes od_solver.c to PostgreSQL as SQL-callable functions:
|
|
* tle_from_eci() -- fit TLE from ECI ephemeris
|
|
* tle_from_topocentric() -- fit TLE from topocentric observations
|
|
* tle_fit_residuals() -- per-observation residuals diagnostic
|
|
*
|
|
* Placeholder: full implementation in Commit 3.
|
|
*/
|
|
|
|
#include "postgres.h"
|
|
#include "fmgr.h"
|
|
#include "funcapi.h"
|
|
#include "utils/timestamp.h"
|
|
#include "utils/array.h"
|
|
#include "utils/builtins.h"
|
|
#include "catalog/pg_type.h"
|
|
#include "norad.h"
|
|
#include "types.h"
|
|
#include "od_solver.h"
|
|
#include "od_math.h"
|
|
|
|
#include <math.h>
|
|
#include <string.h>
|
|
|
|
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);
|
|
|
|
/* ================================================================
|
|
* Helper: pg_tle ↔ tle_t conversion (local copy, avoids symbol coupling)
|
|
* ================================================================
|
|
*/
|
|
|
|
static void
|
|
pg_tle_to_sat_code_od(const pg_tle *src, tle_t *dst)
|
|
{
|
|
memset(dst, 0, sizeof(tle_t));
|
|
|
|
dst->epoch = src->epoch;
|
|
dst->xincl = src->inclination;
|
|
dst->xnodeo = src->raan;
|
|
dst->eo = src->eccentricity;
|
|
dst->omegao = src->arg_perigee;
|
|
dst->xmo = src->mean_anomaly;
|
|
dst->xno = src->mean_motion;
|
|
dst->xndt2o = src->mean_motion_dot;
|
|
dst->xndd6o = src->mean_motion_ddot;
|
|
dst->bstar = src->bstar;
|
|
|
|
dst->norad_number = src->norad_id;
|
|
dst->bulletin_number = src->elset_num;
|
|
dst->revolution_number = src->rev_num;
|
|
dst->classification = src->classification;
|
|
dst->ephemeris_type = src->ephemeris_type;
|
|
|
|
memcpy(dst->intl_desig, src->intl_desig, 9);
|
|
}
|
|
|
|
static void
|
|
sat_code_to_pg_tle(const tle_t *src, pg_tle *dst)
|
|
{
|
|
memset(dst, 0, sizeof(pg_tle));
|
|
|
|
dst->epoch = src->epoch;
|
|
dst->inclination = src->xincl;
|
|
dst->raan = src->xnodeo;
|
|
dst->eccentricity = src->eo;
|
|
dst->arg_perigee = src->omegao;
|
|
dst->mean_anomaly = src->xmo;
|
|
dst->mean_motion = src->xno;
|
|
dst->mean_motion_dot = src->xndt2o;
|
|
dst->mean_motion_ddot = src->xndd6o;
|
|
dst->bstar = src->bstar;
|
|
|
|
dst->norad_id = src->norad_number;
|
|
dst->elset_num = src->bulletin_number;
|
|
dst->rev_num = src->revolution_number;
|
|
dst->classification = src->classification;
|
|
dst->ephemeris_type = src->ephemeris_type;
|
|
|
|
memcpy(dst->intl_desig, src->intl_desig, 9);
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* tle_from_eci(eci_position[], timestamptz[], tle, boolean, int4)
|
|
* -> RECORD (fitted_tle tle, iterations int4, rms_final float8,
|
|
* rms_initial float8, status text)
|
|
*
|
|
* Fit a TLE from an array of ECI position/velocity observations.
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
tle_from_eci(PG_FUNCTION_ARGS)
|
|
{
|
|
ArrayType *pos_arr = PG_GETARG_ARRAYTYPE_P(0);
|
|
ArrayType *time_arr = PG_GETARG_ARRAYTYPE_P(1);
|
|
bool has_seed = !PG_ARGISNULL(2);
|
|
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);
|
|
|
|
int n_pos, n_times;
|
|
Datum *pos_datums, *time_datums;
|
|
bool *pos_nulls, *time_nulls;
|
|
od_observation_t *obs;
|
|
od_config_t config;
|
|
od_result_t result;
|
|
tle_t seed_sat;
|
|
int i, rc;
|
|
|
|
TupleDesc tupdesc;
|
|
Datum values[5];
|
|
bool nulls[5];
|
|
HeapTuple tuple;
|
|
|
|
/* Deconstruct arrays.
|
|
* pg_eci is 48 bytes = pass-by-reference (byval=false).
|
|
* timestamptz is int64 = pass-by-value on 64-bit. */
|
|
deconstruct_array(pos_arr, pos_arr->elemtype, sizeof(pg_eci), false,
|
|
TYPALIGN_DOUBLE, &pos_datums, &pos_nulls, &n_pos);
|
|
deconstruct_array(time_arr, TIMESTAMPTZOID, sizeof(int64), FLOAT8PASSBYVAL,
|
|
TYPALIGN_DOUBLE, &time_datums, &time_nulls, &n_times);
|
|
|
|
if (n_pos != n_times)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
|
|
errmsg("positions and times arrays must have same length: "
|
|
"%d vs %d", n_pos, n_times)));
|
|
|
|
if (n_pos < 6)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
|
errmsg("at least 6 observations required, got %d", n_pos)));
|
|
|
|
/* Build observation array */
|
|
obs = (od_observation_t *) palloc(sizeof(od_observation_t) * n_pos);
|
|
|
|
for (i = 0; i < n_pos; i++)
|
|
{
|
|
pg_eci *eci = (pg_eci *) DatumGetPointer(pos_datums[i]);
|
|
int64 ts = DatumGetInt64(time_datums[i]);
|
|
double jd = PG_EPOCH_JD + ((double)ts / (double)USECS_PER_DAY);
|
|
|
|
obs[i].jd = jd;
|
|
obs[i].data[0] = eci->x;
|
|
obs[i].data[1] = eci->y;
|
|
obs[i].data[2] = eci->z;
|
|
obs[i].data[3] = eci->vx; /* already km/s */
|
|
obs[i].data[4] = eci->vy;
|
|
obs[i].data[5] = eci->vz;
|
|
}
|
|
|
|
/* Configure solver */
|
|
memset(&config, 0, sizeof(config));
|
|
config.obs_type = OD_OBS_ECI;
|
|
config.fit_bstar = fit_bstar ? 1 : 0;
|
|
config.max_iter = max_iter;
|
|
config.observers = NULL;
|
|
config.n_observers = 0;
|
|
|
|
/* Convert seed TLE if provided */
|
|
if (has_seed)
|
|
pg_tle_to_sat_code_od(seed_pg, &seed_sat);
|
|
|
|
memset(&result, 0, sizeof(result));
|
|
|
|
/* Run solver */
|
|
rc = od_fit_tle(obs, n_pos, has_seed ? &seed_sat : NULL, &config, &result);
|
|
|
|
pfree(obs);
|
|
|
|
if (rc != 0)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_DATA_EXCEPTION),
|
|
errmsg("TLE fitting failed: %s", result.status)));
|
|
|
|
/* Build result tuple */
|
|
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("function returning record called in context "
|
|
"that cannot accept type record")));
|
|
tupdesc = BlessTupleDesc(tupdesc);
|
|
|
|
memset(nulls, 0, sizeof(nulls));
|
|
|
|
{
|
|
pg_tle *fitted = (pg_tle *) palloc(sizeof(pg_tle));
|
|
sat_code_to_pg_tle(&result.fitted_tle, fitted);
|
|
values[0] = PointerGetDatum(fitted);
|
|
}
|
|
values[1] = Int32GetDatum(result.iterations);
|
|
values[2] = Float8GetDatum(result.rms_final);
|
|
values[3] = Float8GetDatum(result.rms_initial);
|
|
values[4] = CStringGetTextDatum(result.status);
|
|
|
|
tuple = heap_form_tuple(tupdesc, values, nulls);
|
|
PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* tle_from_topocentric(topocentric[], timestamptz[], observer,
|
|
* tle, boolean, int4)
|
|
* -> RECORD (same as tle_from_eci)
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
tle_from_topocentric(PG_FUNCTION_ARGS)
|
|
{
|
|
ArrayType *topo_arr = PG_GETARG_ARRAYTYPE_P(0);
|
|
ArrayType *time_arr = PG_GETARG_ARRAYTYPE_P(1);
|
|
pg_observer *obs_pg = (pg_observer *) PG_GETARG_POINTER(2);
|
|
bool has_seed = !PG_ARGISNULL(3);
|
|
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);
|
|
|
|
int n_topo, n_times;
|
|
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[5];
|
|
bool nulls[5];
|
|
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 */
|
|
{
|
|
Datum *topo_datums;
|
|
bool *topo_nulls;
|
|
Datum *time_datums;
|
|
bool *time_nulls;
|
|
|
|
deconstruct_array(topo_arr, topo_arr->elemtype, sizeof(pg_topocentric),
|
|
false, TYPALIGN_DOUBLE,
|
|
&topo_datums, &topo_nulls, &n_topo);
|
|
deconstruct_array(time_arr, TIMESTAMPTZOID, sizeof(int64), FLOAT8PASSBYVAL,
|
|
TYPALIGN_DOUBLE, &time_datums, &time_nulls, &n_times);
|
|
|
|
if (n_topo != n_times)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
|
|
errmsg("observations and times arrays must have same length: "
|
|
"%d vs %d", n_topo, n_times)));
|
|
|
|
if (n_topo < 6)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
|
errmsg("at least 6 observations required, got %d", n_topo)));
|
|
|
|
obs = (od_observation_t *) palloc(sizeof(od_observation_t) * n_topo);
|
|
|
|
for (i = 0; i < n_topo; i++)
|
|
{
|
|
pg_topocentric *topo = (pg_topocentric *) DatumGetPointer(topo_datums[i]);
|
|
int64 ts = DatumGetInt64(time_datums[i]);
|
|
double jd = PG_EPOCH_JD + ((double)ts / (double)USECS_PER_DAY);
|
|
|
|
obs[i].jd = jd;
|
|
obs[i].data[0] = topo->azimuth;
|
|
obs[i].data[1] = topo->elevation;
|
|
obs[i].data[2] = topo->range_km;
|
|
obs[i].observer_idx = 0; /* single observer */
|
|
}
|
|
}
|
|
|
|
/* Configure solver */
|
|
memset(&config, 0, sizeof(config));
|
|
config.obs_type = OD_OBS_TOPO;
|
|
config.fit_bstar = fit_bstar ? 1 : 0;
|
|
config.max_iter = max_iter;
|
|
config.observers = &observer;
|
|
config.n_observers = 1;
|
|
|
|
if (has_seed)
|
|
pg_tle_to_sat_code_od(seed_pg, &seed_sat);
|
|
|
|
memset(&result, 0, sizeof(result));
|
|
|
|
rc = od_fit_tle(obs, n_topo, has_seed ? &seed_sat : NULL, &config, &result);
|
|
|
|
pfree(obs);
|
|
|
|
if (rc != 0)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_DATA_EXCEPTION),
|
|
errmsg("TLE fitting failed: %s", result.status)));
|
|
|
|
/* Build result tuple */
|
|
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("function returning record called in context "
|
|
"that cannot accept type record")));
|
|
tupdesc = BlessTupleDesc(tupdesc);
|
|
|
|
memset(nulls, 0, sizeof(nulls));
|
|
|
|
{
|
|
pg_tle *fitted = (pg_tle *) palloc(sizeof(pg_tle));
|
|
sat_code_to_pg_tle(&result.fitted_tle, fitted);
|
|
values[0] = PointerGetDatum(fitted);
|
|
}
|
|
values[1] = Int32GetDatum(result.iterations);
|
|
values[2] = Float8GetDatum(result.rms_final);
|
|
values[3] = Float8GetDatum(result.rms_initial);
|
|
values[4] = CStringGetTextDatum(result.status);
|
|
|
|
tuple = heap_form_tuple(tupdesc, values, nulls);
|
|
PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* tle_from_topocentric_multi(topocentric[], timestamptz[],
|
|
* observer[], int4[],
|
|
* tle, boolean, int4)
|
|
* -> RECORD (same as tle_from_eci)
|
|
*
|
|
* Multi-observer variant: observations from different ground stations.
|
|
* observer_ids[i] indexes into the observers[] array.
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
tle_from_topocentric_multi(PG_FUNCTION_ARGS)
|
|
{
|
|
ArrayType *topo_arr = PG_GETARG_ARRAYTYPE_P(0);
|
|
ArrayType *time_arr = PG_GETARG_ARRAYTYPE_P(1);
|
|
ArrayType *obs_arr = PG_GETARG_ARRAYTYPE_P(2);
|
|
ArrayType *id_arr = PG_GETARG_ARRAYTYPE_P(3);
|
|
bool has_seed = !PG_ARGISNULL(4);
|
|
pg_tle *seed_pg = has_seed ? (pg_tle *) PG_GETARG_POINTER(4) : NULL;
|
|
bool fit_bstar = PG_ARGISNULL(5) ? false : PG_GETARG_BOOL(5);
|
|
int32 max_iter = PG_ARGISNULL(6) ? 15 : PG_GETARG_INT32(6);
|
|
|
|
int n_topo, n_times, n_obs_sites, n_ids;
|
|
od_observation_t *obs;
|
|
od_config_t config;
|
|
od_observer_t *observers;
|
|
od_result_t result;
|
|
tle_t seed_sat;
|
|
int i, rc;
|
|
|
|
TupleDesc tupdesc;
|
|
Datum values[5];
|
|
bool nulls[5];
|
|
HeapTuple tuple;
|
|
|
|
/* Deconstruct all arrays */
|
|
Datum *topo_datums, *time_datums, *obs_datums, *id_datums;
|
|
bool *topo_nulls, *time_nulls, *obs_nulls, *id_nulls;
|
|
|
|
deconstruct_array(topo_arr, topo_arr->elemtype, sizeof(pg_topocentric),
|
|
false, TYPALIGN_DOUBLE,
|
|
&topo_datums, &topo_nulls, &n_topo);
|
|
deconstruct_array(time_arr, TIMESTAMPTZOID, sizeof(int64), FLOAT8PASSBYVAL,
|
|
TYPALIGN_DOUBLE, &time_datums, &time_nulls, &n_times);
|
|
deconstruct_array(obs_arr, obs_arr->elemtype, sizeof(pg_observer),
|
|
false, TYPALIGN_DOUBLE,
|
|
&obs_datums, &obs_nulls, &n_obs_sites);
|
|
deconstruct_array(id_arr, INT4OID, sizeof(int32), true,
|
|
TYPALIGN_INT, &id_datums, &id_nulls, &n_ids);
|
|
|
|
if (n_topo != n_times)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
|
|
errmsg("observations and times arrays must have same length: "
|
|
"%d vs %d", n_topo, n_times)));
|
|
|
|
if (n_topo != n_ids)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
|
|
errmsg("observations and observer_ids arrays must have same length: "
|
|
"%d vs %d", n_topo, n_ids)));
|
|
|
|
if (n_topo < 6)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
|
errmsg("at least 6 observations required, got %d", n_topo)));
|
|
|
|
if (n_obs_sites < 1)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
|
errmsg("at least 1 observer required")));
|
|
|
|
/* Build observer array (pre-compute ECEF for each) */
|
|
observers = (od_observer_t *) palloc(sizeof(od_observer_t) * n_obs_sites);
|
|
for (i = 0; i < n_obs_sites; i++)
|
|
{
|
|
pg_observer *op = (pg_observer *) DatumGetPointer(obs_datums[i]);
|
|
observers[i].lat = op->lat;
|
|
observers[i].lon = op->lon;
|
|
observers[i].alt_m = op->alt_m;
|
|
od_observer_to_ecef(op->lat, op->lon, op->alt_m, observers[i].ecef);
|
|
}
|
|
|
|
/* Build observation array with per-observation observer index */
|
|
obs = (od_observation_t *) palloc(sizeof(od_observation_t) * n_topo);
|
|
for (i = 0; i < n_topo; i++)
|
|
{
|
|
pg_topocentric *topo = (pg_topocentric *) DatumGetPointer(topo_datums[i]);
|
|
int64 ts = DatumGetInt64(time_datums[i]);
|
|
int32 oid = DatumGetInt32(id_datums[i]);
|
|
|
|
if (oid < 0 || oid >= n_obs_sites)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
|
errmsg("observer_id %d out of range [0, %d)",
|
|
oid, n_obs_sites)));
|
|
|
|
obs[i].jd = PG_EPOCH_JD + ((double)ts / (double)USECS_PER_DAY);
|
|
obs[i].data[0] = topo->azimuth;
|
|
obs[i].data[1] = topo->elevation;
|
|
obs[i].data[2] = topo->range_km;
|
|
obs[i].observer_idx = oid;
|
|
}
|
|
|
|
/* Configure solver */
|
|
memset(&config, 0, sizeof(config));
|
|
config.obs_type = OD_OBS_TOPO;
|
|
config.fit_bstar = fit_bstar ? 1 : 0;
|
|
config.max_iter = max_iter;
|
|
config.observers = observers;
|
|
config.n_observers = n_obs_sites;
|
|
|
|
if (has_seed)
|
|
pg_tle_to_sat_code_od(seed_pg, &seed_sat);
|
|
|
|
memset(&result, 0, sizeof(result));
|
|
|
|
rc = od_fit_tle(obs, n_topo, has_seed ? &seed_sat : NULL, &config, &result);
|
|
|
|
pfree(obs);
|
|
pfree(observers);
|
|
|
|
if (rc != 0)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_DATA_EXCEPTION),
|
|
errmsg("TLE fitting failed: %s", result.status)));
|
|
|
|
/* Build result tuple */
|
|
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("function returning record called in context "
|
|
"that cannot accept type record")));
|
|
tupdesc = BlessTupleDesc(tupdesc);
|
|
|
|
memset(nulls, 0, sizeof(nulls));
|
|
|
|
{
|
|
pg_tle *fitted = (pg_tle *) palloc(sizeof(pg_tle));
|
|
sat_code_to_pg_tle(&result.fitted_tle, fitted);
|
|
values[0] = PointerGetDatum(fitted);
|
|
}
|
|
values[1] = Int32GetDatum(result.iterations);
|
|
values[2] = Float8GetDatum(result.rms_final);
|
|
values[3] = Float8GetDatum(result.rms_initial);
|
|
values[4] = CStringGetTextDatum(result.status);
|
|
|
|
tuple = heap_form_tuple(tupdesc, values, nulls);
|
|
PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* tle_fit_residuals(tle, eci_position[], timestamptz[])
|
|
* -> TABLE (t timestamptz, dx_km float8, dy_km float8, dz_km float8,
|
|
* pos_err_km float8)
|
|
*
|
|
* Compute per-observation residuals between a TLE and ECI observations.
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
tle_fit_residuals(PG_FUNCTION_ARGS)
|
|
{
|
|
FuncCallContext *funcctx;
|
|
|
|
if (SRF_IS_FIRSTCALL())
|
|
{
|
|
MemoryContext oldctx;
|
|
pg_tle *tle;
|
|
ArrayType *pos_arr;
|
|
ArrayType *time_arr;
|
|
int n_pos, n_times;
|
|
Datum *pos_datums;
|
|
bool *pos_nulls;
|
|
Datum *time_datums;
|
|
bool *time_nulls;
|
|
TupleDesc tupdesc;
|
|
|
|
/* Context for residual data (persists across calls) */
|
|
typedef struct {
|
|
tle_t sat;
|
|
double *params;
|
|
int is_deep;
|
|
int n_obs;
|
|
double *jds; /* Julian dates */
|
|
double *obs_pos; /* observed positions (3 * n_obs) */
|
|
} residual_ctx;
|
|
|
|
residual_ctx *ctx;
|
|
int i;
|
|
|
|
funcctx = SRF_FIRSTCALL_INIT();
|
|
oldctx = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
|
|
|
|
tle = (pg_tle *) PG_GETARG_POINTER(0);
|
|
pos_arr = PG_GETARG_ARRAYTYPE_P(1);
|
|
time_arr = PG_GETARG_ARRAYTYPE_P(2);
|
|
|
|
deconstruct_array(pos_arr, pos_arr->elemtype, sizeof(pg_eci), false,
|
|
TYPALIGN_DOUBLE, &pos_datums, &pos_nulls, &n_pos);
|
|
deconstruct_array(time_arr, TIMESTAMPTZOID, sizeof(int64), FLOAT8PASSBYVAL,
|
|
TYPALIGN_DOUBLE, &time_datums, &time_nulls, &n_times);
|
|
|
|
if (n_pos != n_times)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
|
|
errmsg("positions and times arrays must have same length")));
|
|
|
|
ctx = (residual_ctx *) palloc0(sizeof(residual_ctx));
|
|
pg_tle_to_sat_code_od(tle, &ctx->sat);
|
|
|
|
ctx->is_deep = select_ephemeris(&ctx->sat);
|
|
if (ctx->is_deep < 0)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_DATA_EXCEPTION),
|
|
errmsg("invalid TLE for residual computation")));
|
|
|
|
ctx->params = (double *) palloc(sizeof(double) * N_SAT_PARAMS);
|
|
if (ctx->is_deep)
|
|
SDP4_init(ctx->params, &ctx->sat);
|
|
else
|
|
SGP4_init(ctx->params, &ctx->sat);
|
|
|
|
ctx->n_obs = n_pos;
|
|
ctx->jds = (double *) palloc(sizeof(double) * n_pos);
|
|
ctx->obs_pos = (double *) palloc(sizeof(double) * 3 * n_pos);
|
|
|
|
for (i = 0; i < n_pos; i++)
|
|
{
|
|
pg_eci *eci = (pg_eci *) DatumGetPointer(pos_datums[i]);
|
|
int64 ts = DatumGetInt64(time_datums[i]);
|
|
|
|
ctx->jds[i] = PG_EPOCH_JD + ((double)ts / (double)USECS_PER_DAY);
|
|
ctx->obs_pos[i * 3 + 0] = eci->x;
|
|
ctx->obs_pos[i * 3 + 1] = eci->y;
|
|
ctx->obs_pos[i * 3 + 2] = eci->z;
|
|
}
|
|
|
|
funcctx->max_calls = n_pos;
|
|
funcctx->user_fctx = ctx;
|
|
|
|
tupdesc = CreateTemplateTupleDesc(5);
|
|
TupleDescInitEntry(tupdesc, 1, "t", TIMESTAMPTZOID, -1, 0);
|
|
TupleDescInitEntry(tupdesc, 2, "dx_km", FLOAT8OID, -1, 0);
|
|
TupleDescInitEntry(tupdesc, 3, "dy_km", FLOAT8OID, -1, 0);
|
|
TupleDescInitEntry(tupdesc, 4, "dz_km", FLOAT8OID, -1, 0);
|
|
TupleDescInitEntry(tupdesc, 5, "pos_err_km", FLOAT8OID, -1, 0);
|
|
|
|
funcctx->tuple_desc = BlessTupleDesc(tupdesc);
|
|
|
|
MemoryContextSwitchTo(oldctx);
|
|
}
|
|
|
|
funcctx = SRF_PERCALL_SETUP();
|
|
|
|
if (funcctx->call_cntr < funcctx->max_calls)
|
|
{
|
|
typedef struct {
|
|
tle_t sat;
|
|
double *params;
|
|
int is_deep;
|
|
int n_obs;
|
|
double *jds;
|
|
double *obs_pos;
|
|
} residual_ctx;
|
|
|
|
residual_ctx *ctx = (residual_ctx *) funcctx->user_fctx;
|
|
int idx = funcctx->call_cntr;
|
|
double tsince, pos[3], vel[3];
|
|
double dx, dy, dz, pos_err;
|
|
int err;
|
|
int64 ts;
|
|
Datum values[5];
|
|
bool nulls[5];
|
|
HeapTuple tuple;
|
|
|
|
tsince = (ctx->jds[idx] - ctx->sat.epoch) * 1440.0;
|
|
|
|
if (ctx->is_deep)
|
|
err = SDP4(tsince, &ctx->sat, ctx->params, pos, vel);
|
|
else
|
|
err = SGP4(tsince, &ctx->sat, ctx->params, pos, vel);
|
|
|
|
dx = ctx->obs_pos[idx * 3 + 0] - pos[0];
|
|
dy = ctx->obs_pos[idx * 3 + 1] - pos[1];
|
|
dz = ctx->obs_pos[idx * 3 + 2] - pos[2];
|
|
pos_err = sqrt(dx * dx + dy * dy + dz * dz);
|
|
|
|
ts = (int64)((ctx->jds[idx] - PG_EPOCH_JD) * (double)USECS_PER_DAY);
|
|
|
|
memset(nulls, 0, sizeof(nulls));
|
|
values[0] = Int64GetDatum(ts);
|
|
values[1] = Float8GetDatum(dx);
|
|
values[2] = Float8GetDatum(dy);
|
|
values[3] = Float8GetDatum(dz);
|
|
values[4] = Float8GetDatum(pos_err);
|
|
|
|
(void)err;
|
|
|
|
tuple = heap_form_tuple(funcctx->tuple_desc, values, nulls);
|
|
SRF_RETURN_NEXT(funcctx, HeapTupleGetDatum(tuple));
|
|
}
|
|
|
|
SRF_RETURN_DONE(funcctx);
|
|
}
|