pg_orrery/src/transfer_funcs.c
Ryan Malloy 70420c3b4f Phase 4: Lambert transfer orbit solver for interplanetary trajectories
Add Universal Variable Lambert solver for computing transfer orbits
between any two planets. Enables pork chop plot generation as SQL:

  SELECT dep_date, arr_date, lambert_c3(3, 4, dep_date, arr_date)
  FROM generate_series(...) dep CROSS JOIN generate_series(...) arr;

New functions:
- lambert_transfer(dep_body, arr_body, dep_time, arr_time) → RECORD
  Returns C3 departure/arrival (km^2/s^2), v_infinity (km/s),
  time of flight (days), and transfer orbit SMA (AU).
- lambert_c3(dep_body, arr_body, dep_time, arr_time) → float8
  Convenience: departure C3 only, NULL on solver failure.

The solver uses Stumpff functions for unified elliptic/parabolic/hyperbolic
handling, with Newton-Raphson iteration and bisection fallback.
Each solve is sub-millisecond; PARALLEL SAFE for batch computation.

All 11 regression tests pass.
2026-02-16 02:00:09 -07:00

227 lines
7.3 KiB
C

/*
* transfer_funcs.c -- Interplanetary transfer orbit SQL functions
*
* Wraps the Lambert solver for use in PostgreSQL queries.
* Enables pork chop plots and transfer window analysis as SQL:
*
* SELECT dep_date, arr_date, c3_departure, c3_arrival, delta_v
* FROM generate_series(...) dep_date
* CROSS JOIN generate_series(...) arr_date
* CROSS JOIN LATERAL lambert_transfer(3, 4, dep_date, arr_date) t
* WHERE t IS NOT NULL;
*
* All functions are IMMUTABLE STRICT PARALLEL SAFE.
*/
#include "postgres.h"
#include "fmgr.h"
#include "funcapi.h"
#include "catalog/pg_type.h"
#include "utils/builtins.h"
#include "utils/timestamp.h"
#include "types.h"
#include "vsop87.h"
#include "lambert.h"
#include <math.h>
PG_FUNCTION_INFO_V1(lambert_transfer);
PG_FUNCTION_INFO_V1(lambert_c3);
/*
* Compute planet heliocentric velocity via numerical differentiation.
* Central difference: v = (pos(t+dt) - pos(t-dt)) / (2*dt)
* dt = 0.01 day ~ 14 minutes, gives ~6 significant digits.
*/
static void
planet_velocity(int vsop_body, double jd, double vel[3])
{
double pos_fwd[6], pos_bwd[6];
double dt = 0.01; /* days */
GetVsop87Coor(jd + dt, vsop_body, pos_fwd);
GetVsop87Coor(jd - dt, vsop_body, pos_bwd);
vel[0] = (pos_fwd[0] - pos_bwd[0]) / (2.0 * dt);
vel[1] = (pos_fwd[1] - pos_bwd[1]) / (2.0 * dt);
vel[2] = (pos_fwd[2] - pos_bwd[2]) / (2.0 * dt);
}
/*
* lambert_transfer(dep_body_id int4, arr_body_id int4,
* dep_time timestamptz, arr_time timestamptz)
* RETURNS RECORD (c3_departure float8, c3_arrival float8,
* v_inf_dep float8, v_inf_arr float8,
* tof_days float8, sma float8)
*
* Solves Lambert's problem for a transfer between two planets.
* Returns NULL if the solver fails to converge (e.g., TOF too short).
*
* C3 = v_infinity^2 (km^2/s^2), the characteristic energy.
* v_inf = hyperbolic excess velocity at departure/arrival (km/s).
* tof_days = time of flight in days.
* sma = transfer orbit semi-major axis (AU).
*/
Datum
lambert_transfer(PG_FUNCTION_ARGS)
{
int32 dep_body = PG_GETARG_INT32(0);
int32 arr_body = PG_GETARG_INT32(1);
int64 dep_ts = PG_GETARG_INT64(2);
int64 arr_ts = PG_GETARG_INT64(3);
double dep_jd, arr_jd, tof_days;
double r1[6], r2[6];
double v_planet_dep[3], v_planet_arr[3];
double v_inf_dep[3], v_inf_arr[3];
double v_inf_dep_mag, v_inf_arr_mag;
double c3_dep, c3_arr;
int dep_vsop, arr_vsop;
lambert_result lr;
TupleDesc tupdesc;
Datum values[6];
bool nulls[6];
HeapTuple tuple;
double au_per_day_to_km_per_s;
int k;
/* Validate body IDs */
if (dep_body < 1 || dep_body > 8)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("lambert_transfer: dep_body_id %d must be 1-8", dep_body)));
if (arr_body < 1 || arr_body > 8)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("lambert_transfer: arr_body_id %d must be 1-8", arr_body)));
if (dep_body == arr_body)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("lambert_transfer: departure and arrival bodies must differ")));
dep_jd = timestamptz_to_jd(dep_ts);
arr_jd = timestamptz_to_jd(arr_ts);
tof_days = arr_jd - dep_jd;
if (tof_days <= 0.0)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("lambert_transfer: arrival must be after departure")));
/* VSOP87 uses 0-based: Mercury=0, ..., Neptune=7 */
dep_vsop = dep_body - 1;
arr_vsop = arr_body - 1;
/* Get planet positions */
GetVsop87Coor(dep_jd, dep_vsop, r1);
GetVsop87Coor(arr_jd, arr_vsop, r2);
/* Solve Lambert's problem (prograde, short-way) */
if (!lambert_solve_uv(r1, r2, tof_days, GAUSS_K2, 1, &lr))
PG_RETURN_NULL();
/* Planet velocities via numerical differentiation */
planet_velocity(dep_vsop, dep_jd, v_planet_dep);
planet_velocity(arr_vsop, arr_jd, v_planet_arr);
/* Hyperbolic excess velocity = transfer velocity - planet velocity */
/* Convert AU/day to km/s: 1 AU/day = 149597870.7 / 86400 km/s */
au_per_day_to_km_per_s = AU_KM / 86400.0;
for (k = 0; k < 3; k++) {
v_inf_dep[k] = (lr.v1[k] - v_planet_dep[k]) * au_per_day_to_km_per_s;
v_inf_arr[k] = (lr.v2[k] - v_planet_arr[k]) * au_per_day_to_km_per_s;
}
v_inf_dep_mag = sqrt(v_inf_dep[0]*v_inf_dep[0] +
v_inf_dep[1]*v_inf_dep[1] +
v_inf_dep[2]*v_inf_dep[2]);
v_inf_arr_mag = sqrt(v_inf_arr[0]*v_inf_arr[0] +
v_inf_arr[1]*v_inf_arr[1] +
v_inf_arr[2]*v_inf_arr[2]);
/* C3 = v_infinity^2 */
c3_dep = v_inf_dep_mag * v_inf_dep_mag;
c3_arr = v_inf_arr_mag * v_inf_arr_mag;
/* Build composite return */
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));
values[0] = Float8GetDatum(c3_dep);
values[1] = Float8GetDatum(c3_arr);
values[2] = Float8GetDatum(v_inf_dep_mag);
values[3] = Float8GetDatum(v_inf_arr_mag);
values[4] = Float8GetDatum(tof_days);
values[5] = Float8GetDatum(lr.sma);
tuple = heap_form_tuple(tupdesc, values, nulls);
PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
}
/*
* lambert_c3(dep_body_id int4, arr_body_id int4,
* dep_time timestamptz, arr_time timestamptz)
* RETURNS float8
*
* Convenience function: returns departure C3 only (km^2/s^2).
* NULL if solver doesn't converge.
* Useful for pork chop plot coloring.
*/
Datum
lambert_c3(PG_FUNCTION_ARGS)
{
int32 dep_body = PG_GETARG_INT32(0);
int32 arr_body = PG_GETARG_INT32(1);
int64 dep_ts = PG_GETARG_INT64(2);
int64 arr_ts = PG_GETARG_INT64(3);
double dep_jd, arr_jd, tof_days;
double r1[6], r2[6];
double v_planet_dep[3];
double v_inf_dep[3];
double c3_dep;
int dep_vsop, arr_vsop;
lambert_result lr;
double au_per_day_to_km_per_s;
int k;
if (dep_body < 1 || dep_body > 8 || arr_body < 1 || arr_body > 8)
PG_RETURN_NULL();
if (dep_body == arr_body)
PG_RETURN_NULL();
dep_jd = timestamptz_to_jd(dep_ts);
arr_jd = timestamptz_to_jd(arr_ts);
tof_days = arr_jd - dep_jd;
if (tof_days <= 0.0)
PG_RETURN_NULL();
dep_vsop = dep_body - 1;
arr_vsop = arr_body - 1;
GetVsop87Coor(dep_jd, dep_vsop, r1);
GetVsop87Coor(arr_jd, arr_vsop, r2);
if (!lambert_solve_uv(r1, r2, tof_days, GAUSS_K2, 1, &lr))
PG_RETURN_NULL();
planet_velocity(dep_vsop, dep_jd, v_planet_dep);
au_per_day_to_km_per_s = AU_KM / 86400.0;
for (k = 0; k < 3; k++)
v_inf_dep[k] = (lr.v1[k] - v_planet_dep[k]) * au_per_day_to_km_per_s;
c3_dep = v_inf_dep[0]*v_inf_dep[0] +
v_inf_dep[1]*v_inf_dep[1] +
v_inf_dep[2]*v_inf_dep[2];
PG_RETURN_FLOAT8(c3_dep);
}