pg_orrery/src/radio_funcs.c
Ryan Malloy ad7209d0db Phase 3: Planetary moons and Jupiter radio burst prediction
Add observation functions for 19 planetary moons across four systems:
- Galilean moons (Io, Europa, Ganymede, Callisto) via clean-room L1.2 theory
- Saturn moons (Mimas through Hyperion) via TASS 1.7
- Uranus moons (Miranda through Oberon) via GUST86
- Mars moons (Phobos, Deimos) via MarsSat

Add Jupiter decametric radio burst prediction for Radio JOVE operators:
- io_phase_angle() — Io orbital phase from superior conjunction
- jupiter_cml() — System III Central Meridian Longitude with light-time correction
- jupiter_burst_probability() — Carr et al. (1983) source regions A, B, C, D

L1.2 Galilean theory is a clean-room MIT implementation from the published
IMCCE FORTRAN coefficients. All other ephemeris libraries are MIT-licensed
extractions from Stellarium with static caching removed for PARALLEL SAFE.

All 10 regression tests pass. Extension .so grows from 2.4MB to 2.5MB.
2026-02-16 01:55:13 -07:00

263 lines
8.2 KiB
C

/*
* radio_funcs.c -- Jupiter decametric radio burst prediction
*
* Jupiter emits intense radio bursts at 4-39.5 MHz, modulated by:
* 1. Io's orbital phase around Jupiter (Io-related storms)
* 2. Jupiter's Central Meridian Longitude (CML, System III)
*
* Radio JOVE operators use CML-vs-Io-phase lookup charts to predict
* burst probability. This module computes both parameters from SQL,
* enabling batch prediction queries over time series.
*
* References:
* Carr, Desch & Alexander (1983), "Phenomenology of Magnetospheric
* Radio Emissions", in Physics of the Jovian Magnetosphere.
* Radio JOVE project: https://radiojove.gsfc.nasa.gov/
*/
#include "postgres.h"
#include "fmgr.h"
#include "funcapi.h"
#include "utils/timestamp.h"
#include "types.h"
#include "astro_math.h"
#include "vsop87.h"
#include "l12.h"
#include <math.h>
PG_FUNCTION_INFO_V1(io_phase_angle);
PG_FUNCTION_INFO_V1(jupiter_cml);
PG_FUNCTION_INFO_V1(jupiter_burst_probability);
/* ================================================================
* io_phase_angle(timestamptz) -> float8
*
* Returns Io's orbital phase angle in degrees [0, 360).
* Measured from superior geocentric conjunction:
* 0 = Io behind Jupiter (as seen from Earth)
* 90 = Io east of Jupiter (western elongation)
* 180 = Io in front of Jupiter (inferior conjunction)
* 270 = Io west of Jupiter (eastern elongation)
*
* This is the standard convention used by Radio JOVE charts.
* ================================================================
*/
Datum
io_phase_angle(PG_FUNCTION_ARGS)
{
int64 ts = PG_GETARG_INT64(0);
double jd;
double io_xyz[3];
double jup_xyz[6], earth_xyz[6];
double jup_geo[3];
double io_rel_geo[3];
double phase;
double proj_x, proj_y;
double jup_dist;
double east_x, east_y, east_z;
double north_x, north_y, north_z;
double io_east, io_north;
jd = timestamptz_to_jd(ts);
/* Io position relative to Jupiter (VSOP87 ecliptic J2000, AU) */
GetL12Coor(jd, L12_IO, io_xyz, NULL);
/* Jupiter and Earth heliocentric positions */
GetVsop87Coor(jd, 4, jup_xyz); /* VSOP87 body 4 = Jupiter */
GetVsop87Coor(jd, 2, earth_xyz); /* VSOP87 body 2 = Earth */
/* Jupiter geocentric direction */
jup_geo[0] = jup_xyz[0] - earth_xyz[0];
jup_geo[1] = jup_xyz[1] - earth_xyz[1];
jup_geo[2] = jup_xyz[2] - earth_xyz[2];
jup_dist = sqrt(jup_geo[0] * jup_geo[0] +
jup_geo[1] * jup_geo[1] +
jup_geo[2] * jup_geo[2]);
/* Normalize Jupiter geocentric direction */
jup_geo[0] /= jup_dist;
jup_geo[1] /= jup_dist;
jup_geo[2] /= jup_dist;
/*
* Project Io's position onto the plane perpendicular to
* the Earth-Jupiter line of sight. We need two orthogonal
* axes in this plane: "east" (position angle) and "north"
* (toward north ecliptic pole).
*
* North ecliptic pole = (0, 0, 1) in ecliptic J2000.
* East = North x LineOfSight (cross product)
*/
north_x = -(jup_geo[2] * jup_geo[0]); /* north - (north.los)*los */
north_y = -(jup_geo[2] * jup_geo[1]);
north_z = 1.0 - jup_geo[2] * jup_geo[2];
{
double nmag = sqrt(north_x * north_x + north_y * north_y + north_z * north_z);
if (nmag > 1e-10) {
north_x /= nmag;
north_y /= nmag;
north_z /= nmag;
}
}
/* East = North x LOS */
east_x = north_y * jup_geo[2] - north_z * jup_geo[1];
east_y = north_z * jup_geo[0] - north_x * jup_geo[2];
east_z = north_x * jup_geo[1] - north_y * jup_geo[0];
/* Project Io relative position onto east/north */
io_east = io_xyz[0] * east_x + io_xyz[1] * east_y + io_xyz[2] * east_z;
io_north = io_xyz[0] * north_x + io_xyz[1] * north_y + io_xyz[2] * north_z;
/* Along LOS component (positive = behind Jupiter from Earth) */
proj_x = io_xyz[0] * jup_geo[0] + io_xyz[1] * jup_geo[1] + io_xyz[2] * jup_geo[2];
proj_y = io_east;
/*
* Phase angle convention (Radio JOVE):
* 0 = superior conjunction (Io behind Jupiter)
* Measured counter-clockwise as seen from north ecliptic pole
*/
phase = atan2(-proj_y, proj_x) * RAD_TO_DEG;
if (phase < 0.0)
phase += 360.0;
PG_RETURN_FLOAT8(phase);
}
/* ================================================================
* jupiter_cml(observer, timestamptz) -> float8
*
* Jupiter's Central Meridian Longitude in System III (1965.0).
* Returns degrees [0, 360).
*
* System III is defined by Jupiter's magnetic field rotation,
* with period 9h 55m 29.711s (IAU).
*
* The CML is the System III longitude of the sub-observer point.
* For Earth-based observation, this is approximately the
* sub-Earth longitude.
*
* Reference: IAU (1976) System III longitude definition.
* ================================================================
*/
Datum
jupiter_cml(PG_FUNCTION_ARGS)
{
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
int64 ts = PG_GETARG_INT64(1);
double jd;
double jup_xyz[6], earth_xyz[6];
double dx, dy, dz, geo_dist;
double light_time_days;
double jd_lt;
double d;
double cml;
(void)obs; /* observer not needed for CML (geocentric is sufficient) */
jd = timestamptz_to_jd(ts);
/* Jupiter and Earth positions */
GetVsop87Coor(jd, 4, jup_xyz);
GetVsop87Coor(jd, 2, earth_xyz);
/* Geocentric distance for light-time correction */
dx = jup_xyz[0] - earth_xyz[0];
dy = jup_xyz[1] - earth_xyz[1];
dz = jup_xyz[2] - earth_xyz[2];
geo_dist = sqrt(dx * dx + dy * dy + dz * dz);
/* Light-time correction (speed of light = 173.1446 AU/day) */
light_time_days = geo_dist / 173.1446327;
jd_lt = jd - light_time_days;
/*
* System III longitude of central meridian.
*
* Reference epoch: J1965.0 = JD 2438761.5
* Initial longitude: 305.38 deg (IAU 1976 convention)
* Rotation rate: 870.536 deg/day (System III, 9h 55m 29.711s period)
*
* The sub-Earth longitude decreases with time (Jupiter rotates
* prograde), so CML = initial + rate * elapsed_days.
*/
d = jd_lt - 2438761.5; /* Days since J1965.0 */
cml = 305.38 + 870.536 * d;
/* Normalize to [0, 360) */
cml = fmod(cml, 360.0);
if (cml < 0.0)
cml += 360.0;
PG_RETURN_FLOAT8(cml);
}
/* ================================================================
* jupiter_burst_probability(io_phase float8, cml float8) -> float8
*
* Estimated probability of Jupiter decametric radio burst
* based on Io phase angle and CML (System III).
*
* Returns a value 0.0-1.0 representing relative likelihood.
*
* Based on the empirical source regions from Carr et al. (1983):
* Source A: CML 200-260, Io phase 195-265 (Io-A)
* Source B: CML 90-200, Io phase 70-110 (Io-B)
* Source C: CML 290-10, Io phase 215-260 (Io-C)
* Source D: CML 0-50, no Io dependence (non-Io-D)
*
* This is a simplified model. Real-world burst probability
* depends on additional factors (frequency, solar activity,
* antenna orientation).
* ================================================================
*/
Datum
jupiter_burst_probability(PG_FUNCTION_ARGS)
{
double io_phase = PG_GETARG_FLOAT8(0);
double cml = PG_GETARG_FLOAT8(1);
double p;
/* Normalize inputs */
io_phase = fmod(io_phase, 360.0);
if (io_phase < 0.0) io_phase += 360.0;
cml = fmod(cml, 360.0);
if (cml < 0.0) cml += 360.0;
p = 0.0;
/* Source A (Io-A): strongest, most predictable */
if (cml >= 200.0 && cml <= 260.0 && io_phase >= 195.0 && io_phase <= 265.0)
p = 0.8;
/* Source B (Io-B): second strongest */
if (cml >= 90.0 && cml <= 200.0 && io_phase >= 70.0 && io_phase <= 110.0)
{
double pb = 0.6;
if (pb > p) p = pb;
}
/* Source C (Io-C): wraps around 360/0 boundary */
if ((cml >= 290.0 || cml <= 10.0) && io_phase >= 215.0 && io_phase <= 260.0)
{
double pc = 0.5;
if (pc > p) p = pc;
}
/* Non-Io-D: CML-dependent only, weaker */
if (cml >= 0.0 && cml <= 50.0)
{
double pd = 0.15;
if (pd > p) p = pd;
}
PG_RETURN_FLOAT8(p);
}