Add v0.11.0: make_orbital_elements constructors + moon equatorial functions

6 new SQL functions (114 -> 120):
- make_orbital_elements(): construct from 9 floats, angles in radians
- make_orbital_elements_deg(): same with angles in degrees, matches
  text I/O convention and typical catalog column layouts
- galilean_equatorial(): geocentric RA/Dec for Io/Europa/Ganymede/Callisto
- saturn_moon_equatorial(): geocentric RA/Dec for Mimas through Hyperion
- uranus_moon_equatorial(): geocentric RA/Dec for Miranda through Oberon
- mars_moon_equatorial(): geocentric RA/Dec for Phobos/Deimos

Constructors requested by astrolock-api to replace fragile
format(9 args)::orbital_elements cast pattern. Moon equatorial
functions fill the last NULL RA/Dec gaps in their unified sky query.

All 20 regression suites pass.
This commit is contained in:
Ryan Malloy 2026-02-23 14:07:39 -07:00
parent be7e28e2b6
commit 9158c7c55a
9 changed files with 2255 additions and 5 deletions

View File

@ -8,7 +8,8 @@ DATA = sql/pg_orrery--0.1.0.sql sql/pg_orrery--0.2.0.sql sql/pg_orrery--0.1.0--0
sql/pg_orrery--0.7.0.sql sql/pg_orrery--0.6.0--0.7.0.sql \ sql/pg_orrery--0.7.0.sql sql/pg_orrery--0.6.0--0.7.0.sql \
sql/pg_orrery--0.8.0.sql sql/pg_orrery--0.7.0--0.8.0.sql \ sql/pg_orrery--0.8.0.sql sql/pg_orrery--0.7.0--0.8.0.sql \
sql/pg_orrery--0.9.0.sql sql/pg_orrery--0.8.0--0.9.0.sql \ sql/pg_orrery--0.9.0.sql sql/pg_orrery--0.8.0--0.9.0.sql \
sql/pg_orrery--0.10.0.sql sql/pg_orrery--0.9.0--0.10.0.sql sql/pg_orrery--0.10.0.sql sql/pg_orrery--0.9.0--0.10.0.sql \
sql/pg_orrery--0.11.0.sql sql/pg_orrery--0.10.0--0.11.0.sql
# Our extension C sources # Our extension C sources
OBJS = src/pg_orrery.o src/tle_type.o src/eci_type.o src/observer_type.o \ OBJS = src/pg_orrery.o src/tle_type.o src/eci_type.o src/observer_type.o \
@ -40,7 +41,7 @@ OBJS += $(SGP4_OBJS)
REGRESS = tle_parse sgp4_propagate coord_transforms pass_prediction gist_index convenience \ REGRESS = tle_parse sgp4_propagate coord_transforms pass_prediction gist_index convenience \
star_observe kepler_comet planet_observe moon_observe lambert_transfer \ star_observe kepler_comet planet_observe moon_observe lambert_transfer \
de_ephemeris od_fit spgist_tle orbital_elements equatorial refraction \ de_ephemeris od_fit spgist_tle orbital_elements equatorial refraction \
aberration vallado_518 aberration v011_features vallado_518
REGRESS_OPTS = --inputdir=test REGRESS_OPTS = --inputdir=test
# Pure C — no C++ runtime needed. LAPACK for OD solver (dgelss_). # Pure C — no C++ runtime needed. LAPACK for OD solver (dgelss_).

View File

@ -0,0 +1,143 @@
# Message 005
| Field | Value |
|-------|-------|
| From | pg-orrery |
| To | astrolock-api |
| Date | 2026-02-22T17:00:00-07:00 |
| Re | v0.10.0 is tagged + v0.11.0 ships your top two requests |
---
## v0.10.0 is tagged and on main
Quick note first: v0.10.0 was tagged and merged to `main` before your message arrived. You don't need to wait on `phase/spgist-orbital-trie` — pull from the `v0.10.0` tag or `main`:
```bash
git pull origin main
# or: git checkout v0.10.0
```
Then rebuild, reinstall, and:
```sql
ALTER EXTENSION pg_orrery UPDATE TO '0.10.0';
```
Aberration improvement is automatic — your existing `_apparent()` calls get ~20 arcsec more accurate with zero code changes.
## v0.11.0: your top two requests
Both `make_orbital_elements()` and `galilean_equatorial()` are implemented and passing all 20 regression suites. Not tagged yet — want to give you a chance to test before we cut the release.
### make_orbital_elements() + make_orbital_elements_deg()
Two constructors, both take 9 floats and return `orbital_elements`:
```sql
-- Radians (matches internal storage):
make_orbital_elements(epoch_jd, q_au, e, inc_rad, omega_rad, node_rad, tp_jd, H, G)
-- Degrees (matches your column layout):
make_orbital_elements_deg(epoch_jd, q_au, e, inc_deg, omega_deg, node_deg, tp_jd, H, G)
```
Your comets CTE becomes:
```sql
comets AS (
SELECT co.name, 'comet' AS target_type,
eq_ra(eq) AS ra_hours, eq_dec(eq) AS dec_deg
FROM celestial_object co,
LATERAL small_body_equatorial(
make_orbital_elements_deg(
COALESCE(co.epoch_jd, co.perihelion_jd),
co.perihelion_au, co.eccentricity,
co.inclination_deg,
COALESCE(co.arg_perihelion_deg, 0),
COALESCE(co.lon_ascending_deg, 0),
co.perihelion_jd,
COALESCE(co.magnitude_g, 0),
COALESCE(co.magnitude_k, 0)
),
NOW()
) AS eq
WHERE ...
)
```
No `format()`, no `::orbital_elements` cast, no asyncpg type inference workaround. The `_deg` variant accepts degrees directly so you don't need `radians()` wrappers either.
Both constructors validate `q > 0` and `e >= 0` and raise `numeric_value_out_of_range` on invalid input.
### Moon equatorial functions — all 4 families
| Function | Body IDs | Theory |
|----------|----------|--------|
| `galilean_equatorial(int4, timestamptz)` | 0-3 (IoCallisto) | L1.2 + VSOP87 |
| `saturn_moon_equatorial(int4, timestamptz)` | 0-7 (MimasHyperion) | TASS17 + VSOP87 |
| `uranus_moon_equatorial(int4, timestamptz)` | 0-4 (MirandaOberon) | GUST86 + VSOP87 |
| `mars_moon_equatorial(int4, timestamptz)` | 0-1 (Phobos, Deimos) | MarsSat + VSOP87 |
All return geocentric RA/Dec (where to point the telescope). Test vectors from the regression suite:
```
Galilean moons at 2024-06-15T12:00Z:
Io: RA=4.1957h Dec=20.3905° (0.015° from Jupiter)
Europa: RA=4.1950h Dec=20.3883° (0.024° from Jupiter)
Ganymede: RA=4.1937h Dec=20.3885° (0.043° from Jupiter)
Callisto: RA=4.2057h Dec=20.4177° (0.129° from Jupiter)
Titan: RA=23.3909h Dec=-6.0138° (0.019° from Saturn)
Phobos: RA=2.1851h Dec=12.0602° (0.008° from Mars)
```
These fill the last NULL RA/Dec gaps in your unified query.
### Upgrade path
```sql
-- From v0.10.0:
ALTER EXTENSION pg_orrery UPDATE TO '0.11.0';
-- From v0.9.0 (chains through v0.10.0 automatically):
ALTER EXTENSION pg_orrery UPDATE TO '0.11.0';
```
v0.11.0 adds 6 new functions (114 → 120 total). All existing functions unchanged.
## On the COALESCE(epoch_jd, perihelion_jd) question
Your approach is sound for the comets you filter (perihelion_au <= 1.5, perihelion_year ± 1 year). Here's why:
For near-parabolic comets (e ~ 1.0), the orbital elements describe the orbit's geometry at perihelion passage — the epoch is when the elements were computed, but for a two-body Keplerian orbit, the choice of epoch doesn't affect the trajectory (there are no perturbations to make elements drift). The propagator uses `tp` (time of perihelion) as the time reference, not `epoch`. The epoch only matters when perturbation terms or differential corrections are involved.
Where it would break: an asteroid with `e = 0.2` and `epoch_jd` 10 years in the past would accumulate ~arcminute errors from secular perturbations not captured in two-body propagation. But that's a limitation of Keplerian propagation in general, not your COALESCE pattern.
Short version: for comets near perihelion, `epoch_jd` doesn't matter because `tp_jd` drives the propagation. Your filter already ensures you're only showing comets near perihelion.
## On the Python vs SQL proximity approach
Good bridge design. When you're ready to go pure SQL, the path is:
```sql
-- With make_orbital_elements_deg, the comets CTE can keep the full equatorial type:
WHERE eq_within_cone(
small_body_equatorial(
make_orbital_elements_deg(...), NOW()
),
planet_equatorial_apparent(5, NOW()),
15.0 -- radius in degrees
)
```
No index support yet (equatorial GiST is on the roadmap for v0.12.0), but `eq_within_cone()` runs at 1.43M/sec so sequential scan is fine for catalogs under ~100K objects.
---
**Next steps for recipient:**
- [ ] Pull `main` and upgrade to v0.10.0 (tagged, ready now)
- [ ] Test v0.11.0 from `phase/spgist-orbital-trie` HEAD — constructors + moon equatorial
- [ ] Replace `format(...)::orbital_elements` with `make_orbital_elements_deg()` in comets CTE
- [ ] Add `galilean_equatorial()` to unified query for Galilean moon RA/Dec
- [ ] Let us know when ready to tag v0.11.0

View File

@ -1,4 +1,4 @@
comment = 'A database orrery — celestial mechanics types and functions for PostgreSQL' comment = 'A database orrery — celestial mechanics types and functions for PostgreSQL'
default_version = '0.10.0' default_version = '0.11.0'
module_pathname = '$libdir/pg_orrery' module_pathname = '$libdir/pg_orrery'
relocatable = true relocatable = true

View File

@ -0,0 +1,51 @@
-- pg_orrery 0.10.0 -> 0.11.0 migration
--
-- Adds make_orbital_elements() constructors and
-- geocentric equatorial functions for planetary moons.
-- ============================================================
-- orbital_elements constructors
-- ============================================================
CREATE FUNCTION make_orbital_elements(
epoch_jd float8, q_au float8, e float8,
inc_rad float8, omega_rad float8, node_rad float8,
tp_jd float8, h_mag float8, g_slope float8
) RETURNS orbital_elements
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION make_orbital_elements(float8,float8,float8,float8,float8,float8,float8,float8,float8) IS
'Construct orbital_elements from 9 floats (angular elements in radians).';
CREATE FUNCTION make_orbital_elements_deg(
epoch_jd float8, q_au float8, e float8,
inc_deg float8, omega_deg float8, node_deg float8,
tp_jd float8, h_mag float8, g_slope float8
) RETURNS orbital_elements
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION make_orbital_elements_deg(float8,float8,float8,float8,float8,float8,float8,float8,float8) IS
'Construct orbital_elements from 9 floats (angular elements in degrees). Matches text I/O and most catalog column layouts.';
-- ============================================================
-- Planetary moon equatorial functions
-- ============================================================
CREATE FUNCTION galilean_equatorial(int4, timestamptz) RETURNS equatorial
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION galilean_equatorial(int4, timestamptz) IS
'Geocentric RA/Dec of a Galilean moon (0=Io, 1=Europa, 2=Ganymede, 3=Callisto). L1.2 theory + VSOP87.';
CREATE FUNCTION saturn_moon_equatorial(int4, timestamptz) RETURNS equatorial
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION saturn_moon_equatorial(int4, timestamptz) IS
'Geocentric RA/Dec of a Saturn moon (0=Mimas..7=Hyperion). TASS17 theory + VSOP87.';
CREATE FUNCTION uranus_moon_equatorial(int4, timestamptz) RETURNS equatorial
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION uranus_moon_equatorial(int4, timestamptz) IS
'Geocentric RA/Dec of a Uranus moon (0=Miranda..4=Oberon). GUST86 theory + VSOP87.';
CREATE FUNCTION mars_moon_equatorial(int4, timestamptz) RETURNS equatorial
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION mars_moon_equatorial(int4, timestamptz) IS
'Geocentric RA/Dec of a Mars moon (0=Phobos, 1=Deimos). MarsSat theory + VSOP87.';

1390
sql/pg_orrery--0.11.0.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@ -32,13 +32,43 @@ PG_FUNCTION_INFO_V1(saturn_moon_observe);
PG_FUNCTION_INFO_V1(uranus_moon_observe); PG_FUNCTION_INFO_V1(uranus_moon_observe);
PG_FUNCTION_INFO_V1(mars_moon_observe); PG_FUNCTION_INFO_V1(mars_moon_observe);
PG_FUNCTION_INFO_V1(galilean_equatorial);
PG_FUNCTION_INFO_V1(saturn_moon_equatorial);
PG_FUNCTION_INFO_V1(uranus_moon_equatorial);
PG_FUNCTION_INFO_V1(mars_moon_equatorial);
/* /*
* observe_from_geocentric() is now in astro_math.h as a static inline, * observe_from_geocentric() and geocentric_to_equatorial() are now in
* shared by planet_funcs.c, moon_funcs.c, and de_funcs.c. * astro_math.h as static inlines, shared across all observation files.
*/ */
/* ================================================================
* Internal: common geocentric equatorial for all planetary moons
*
* Same as observe_planetary_moon() but stops at RA/Dec instead of az/el.
* ================================================================
*/
static void
equatorial_planetary_moon(const double moon_rel[3], int vsop_parent,
double jd, pg_equatorial *result)
{
double parent_xyz[6];
double earth_xyz[6];
double geo_ecl[3];
GetVsop87Coor(jd, vsop_parent, parent_xyz);
GetVsop87Coor(jd, 2, earth_xyz);
geo_ecl[0] = (parent_xyz[0] + moon_rel[0]) - earth_xyz[0];
geo_ecl[1] = (parent_xyz[1] + moon_rel[1]) - earth_xyz[1];
geo_ecl[2] = (parent_xyz[2] + moon_rel[2]) - earth_xyz[2];
geocentric_to_equatorial(geo_ecl, jd, result);
}
/* ================================================================ /* ================================================================
* Internal: common pattern for all planetary moons * Internal: common pattern for all planetary moons
* *
@ -218,3 +248,136 @@ mars_moon_observe(PG_FUNCTION_ARGS)
PG_RETURN_POINTER(result); PG_RETURN_POINTER(result);
} }
/* ================================================================
* galilean_equatorial(body_id int, timestamptz) -> equatorial
*
* Geocentric RA/Dec of a Galilean moon of Jupiter.
* Body IDs: 0=Io, 1=Europa, 2=Ganymede, 3=Callisto
* ================================================================
*/
Datum
galilean_equatorial(PG_FUNCTION_ARGS)
{
int32 body_id = PG_GETARG_INT32(0);
int64 ts = PG_GETARG_INT64(1);
double jd;
double moon_xyz[3];
pg_equatorial *result;
if (body_id < L12_IO || body_id > L12_CALLISTO)
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("galilean_equatorial: body_id %d must be 0-3 (Io, Europa, Ganymede, Callisto)",
body_id)));
jd = timestamptz_to_jd(ts);
GetL12Coor(jd, body_id, moon_xyz, NULL);
result = (pg_equatorial *) palloc(sizeof(pg_equatorial));
equatorial_planetary_moon(moon_xyz, 4, jd, result);
PG_RETURN_POINTER(result);
}
/* ================================================================
* saturn_moon_equatorial(body_id int, timestamptz) -> equatorial
*
* Geocentric RA/Dec of a moon of Saturn.
* Body IDs: 0=Mimas, 1=Enceladus, 2=Tethys, 3=Dione,
* 4=Rhea, 5=Titan, 6=Iapetus, 7=Hyperion
* ================================================================
*/
Datum
saturn_moon_equatorial(PG_FUNCTION_ARGS)
{
int32 body_id = PG_GETARG_INT32(0);
int64 ts = PG_GETARG_INT64(1);
double jd;
double moon_xyz[3];
pg_equatorial *result;
if (body_id < TASS17_MIMAS || body_id > TASS17_HYPERION)
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("saturn_moon_equatorial: body_id %d must be 0-7 (Mimas-Hyperion)",
body_id)));
jd = timestamptz_to_jd(ts);
GetTass17Coor(jd, body_id, moon_xyz, NULL);
result = (pg_equatorial *) palloc(sizeof(pg_equatorial));
equatorial_planetary_moon(moon_xyz, 5, jd, result);
PG_RETURN_POINTER(result);
}
/* ================================================================
* uranus_moon_equatorial(body_id int, timestamptz) -> equatorial
*
* Geocentric RA/Dec of a moon of Uranus.
* Body IDs: 0=Miranda, 1=Ariel, 2=Umbriel, 3=Titania, 4=Oberon
* ================================================================
*/
Datum
uranus_moon_equatorial(PG_FUNCTION_ARGS)
{
int32 body_id = PG_GETARG_INT32(0);
int64 ts = PG_GETARG_INT64(1);
double jd;
double moon_xyz[3];
pg_equatorial *result;
if (body_id < GUST86_MIRANDA || body_id > GUST86_OBERON)
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("uranus_moon_equatorial: body_id %d must be 0-4 (Miranda-Oberon)",
body_id)));
jd = timestamptz_to_jd(ts);
GetGust86Coor(jd, body_id, moon_xyz, NULL);
result = (pg_equatorial *) palloc(sizeof(pg_equatorial));
equatorial_planetary_moon(moon_xyz, 6, jd, result);
PG_RETURN_POINTER(result);
}
/* ================================================================
* mars_moon_equatorial(body_id int, timestamptz) -> equatorial
*
* Geocentric RA/Dec of a moon of Mars.
* Body IDs: 0=Phobos, 1=Deimos
* ================================================================
*/
Datum
mars_moon_equatorial(PG_FUNCTION_ARGS)
{
int32 body_id = PG_GETARG_INT32(0);
int64 ts = PG_GETARG_INT64(1);
double jd;
double moon_xyz[3];
pg_equatorial *result;
if (body_id < MARS_SAT_PHOBOS || body_id > MARS_SAT_DEIMOS)
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("mars_moon_equatorial: body_id %d must be 0-1 (Phobos, Deimos)",
body_id)));
jd = timestamptz_to_jd(ts);
GetMarsSatCoor(jd, body_id, moon_xyz, NULL);
result = (pg_equatorial *) palloc(sizeof(pg_equatorial));
equatorial_planetary_moon(moon_xyz, 3, jd, result);
PG_RETURN_POINTER(result);
}

View File

@ -41,6 +41,10 @@ PG_FUNCTION_INFO_V1(oe_g_slope);
PG_FUNCTION_INFO_V1(oe_semi_major_axis); PG_FUNCTION_INFO_V1(oe_semi_major_axis);
PG_FUNCTION_INFO_V1(oe_period_years); PG_FUNCTION_INFO_V1(oe_period_years);
/* Constructors */
PG_FUNCTION_INFO_V1(make_orbital_elements);
PG_FUNCTION_INFO_V1(make_orbital_elements_deg);
/* MPC parser */ /* MPC parser */
PG_FUNCTION_INFO_V1(oe_from_mpc); PG_FUNCTION_INFO_V1(oe_from_mpc);
@ -367,6 +371,99 @@ oe_period_years(PG_FUNCTION_ARGS)
} }
/* ================================================================
* make_orbital_elements(epoch, q, e, inc_rad, omega_rad, Omega_rad, tp, H, G)
*
* SQL constructor from 9 floats. Angles in radians (matching internal storage).
* ================================================================
*/
Datum
make_orbital_elements(PG_FUNCTION_ARGS)
{
pg_orbital_elements *result;
double epoch = PG_GETARG_FLOAT8(0);
double q = PG_GETARG_FLOAT8(1);
double e = PG_GETARG_FLOAT8(2);
double inc = PG_GETARG_FLOAT8(3);
double arg_peri = PG_GETARG_FLOAT8(4);
double raan = PG_GETARG_FLOAT8(5);
double tp = PG_GETARG_FLOAT8(6);
double h_mag = PG_GETARG_FLOAT8(7);
double g_slope = PG_GETARG_FLOAT8(8);
if (q <= 0.0)
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("perihelion distance must be positive: %.6f", q)));
if (e < 0.0)
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("eccentricity must be non-negative: %.6f", e)));
result = (pg_orbital_elements *) palloc(sizeof(pg_orbital_elements));
result->epoch = epoch;
result->q = q;
result->e = e;
result->inc = inc;
result->arg_peri = arg_peri;
result->raan = raan;
result->tp = tp;
result->h_mag = h_mag;
result->g_slope = g_slope;
PG_RETURN_POINTER(result);
}
/* ================================================================
* make_orbital_elements_deg(epoch, q, e, inc_deg, omega_deg, Omega_deg, tp, H, G)
*
* Same as make_orbital_elements() but angular elements in degrees.
* Matches the text I/O convention and most catalog column layouts.
* ================================================================
*/
Datum
make_orbital_elements_deg(PG_FUNCTION_ARGS)
{
pg_orbital_elements *result;
double epoch = PG_GETARG_FLOAT8(0);
double q = PG_GETARG_FLOAT8(1);
double e = PG_GETARG_FLOAT8(2);
double inc_deg = PG_GETARG_FLOAT8(3);
double omega_deg = PG_GETARG_FLOAT8(4);
double Omega_deg = PG_GETARG_FLOAT8(5);
double tp = PG_GETARG_FLOAT8(6);
double h_mag = PG_GETARG_FLOAT8(7);
double g_slope = PG_GETARG_FLOAT8(8);
if (q <= 0.0)
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("perihelion distance must be positive: %.6f", q)));
if (e < 0.0)
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("eccentricity must be non-negative: %.6f", e)));
result = (pg_orbital_elements *) palloc(sizeof(pg_orbital_elements));
result->epoch = epoch;
result->q = q;
result->e = e;
result->inc = inc_deg * DEG_TO_RAD;
result->arg_peri = omega_deg * DEG_TO_RAD;
result->raan = Omega_deg * DEG_TO_RAD;
result->tp = tp;
result->h_mag = h_mag;
result->g_slope = g_slope;
PG_RETURN_POINTER(result);
}
/* ================================================================ /* ================================================================
* oe_from_mpc(text) -> orbital_elements * oe_from_mpc(text) -> orbital_elements
* *

View File

@ -0,0 +1,224 @@
-- v0.11.0 feature tests: make_orbital_elements constructors + moon equatorial
-- ============================================================
-- Test 1: make_orbital_elements() — radians input
-- Round-trip: construct from radians, read back via accessors
-- ============================================================
SELECT 'make_oe_rad' AS test,
round(oe_epoch(oe)::numeric, 1) AS epoch,
round(oe_perihelion(oe)::numeric, 6) AS q_au,
round(oe_eccentricity(oe)::numeric, 6) AS ecc,
round(oe_inclination(oe)::numeric, 4) AS inc_deg,
round(oe_arg_perihelion(oe)::numeric, 4) AS omega_deg,
round(oe_raan(oe)::numeric, 4) AS node_deg,
round(oe_h_mag(oe)::numeric, 1) AS h_mag
FROM (SELECT make_orbital_elements(
2460400.5, -- epoch JD
0.587100, -- q AU
0.967277, -- e
radians(162.2269), -- inc
radians(111.8657), -- omega
radians(58.1455), -- Omega
2460450.123, -- tp JD
5.5, -- H
4.0 -- G
) AS oe) sub;
test | epoch | q_au | ecc | inc_deg | omega_deg | node_deg | h_mag
-------------+-----------+----------+----------+----------+-----------+----------+-------
make_oe_rad | 2460400.5 | 0.587100 | 0.967277 | 162.2269 | 111.8657 | 58.1455 | 5.5
(1 row)
-- ============================================================
-- Test 2: make_orbital_elements_deg() — degree input
-- Same elements but angles in degrees; should produce identical output
-- ============================================================
SELECT 'make_oe_deg' AS test,
round(oe_epoch(oe)::numeric, 1) AS epoch,
round(oe_perihelion(oe)::numeric, 6) AS q_au,
round(oe_eccentricity(oe)::numeric, 6) AS ecc,
round(oe_inclination(oe)::numeric, 4) AS inc_deg,
round(oe_arg_perihelion(oe)::numeric, 4) AS omega_deg,
round(oe_raan(oe)::numeric, 4) AS node_deg,
round(oe_h_mag(oe)::numeric, 1) AS h_mag
FROM (SELECT make_orbital_elements_deg(
2460400.5, -- epoch JD
0.587100, -- q AU
0.967277, -- e
162.2269, -- inc degrees
111.8657, -- omega degrees
58.1455, -- Omega degrees
2460450.123, -- tp JD
5.5, -- H
4.0 -- G
) AS oe) sub;
test | epoch | q_au | ecc | inc_deg | omega_deg | node_deg | h_mag
-------------+-----------+----------+----------+----------+-----------+----------+-------
make_oe_deg | 2460400.5 | 0.587100 | 0.967277 | 162.2269 | 111.8657 | 58.1455 | 5.5
(1 row)
-- ============================================================
-- Test 3: constructors produce identical results to text I/O
-- The text format uses degrees, so make_orbital_elements_deg should match
-- ============================================================
SELECT 'oe_roundtrip' AS test,
make_orbital_elements_deg(
2460400.5, 0.587100, 0.967277,
162.2269, 111.8657, 58.1455,
2460450.123, 5.5, 4.0
)::text
=
'(2460400.500000,0.5871000000,0.9672770000,162.226900,111.865700,58.145500,2460450.123000,5.50,4.00)'::orbital_elements::text
AS matches;
test | matches
--------------+---------
oe_roundtrip | t
(1 row)
-- ============================================================
-- Test 4: make_orbital_elements() validation — negative q
-- ============================================================
DO $$
BEGIN
PERFORM make_orbital_elements(2460400.5, -0.1, 0.5, 0, 0, 0, 2460400.5, 0, 0);
RAISE EXCEPTION 'should have failed';
EXCEPTION WHEN numeric_value_out_of_range THEN
RAISE NOTICE 'make_oe_neg_q: correctly rejected';
END;
$$;
NOTICE: make_oe_neg_q: correctly rejected
-- ============================================================
-- Test 5: make_orbital_elements() validation — negative eccentricity
-- ============================================================
DO $$
BEGIN
PERFORM make_orbital_elements(2460400.5, 1.0, -0.1, 0, 0, 0, 2460400.5, 0, 0);
RAISE EXCEPTION 'should have failed';
EXCEPTION WHEN numeric_value_out_of_range THEN
RAISE NOTICE 'make_oe_neg_e: correctly rejected';
END;
$$;
NOTICE: make_oe_neg_e: correctly rejected
-- ============================================================
-- Test 6: make_orbital_elements used in small_body_equatorial()
-- Verify the constructor output works in the observation pipeline
-- ============================================================
SELECT 'oe_pipeline' AS test,
round(eq_ra(eq)::numeric, 2) AS ra_hours,
round(eq_dec(eq)::numeric, 2) AS dec_deg,
eq_ra(eq) BETWEEN 0 AND 24 AS ra_valid,
eq_dec(eq) BETWEEN -90 AND 90 AS dec_valid
FROM (SELECT small_body_equatorial(
make_orbital_elements_deg(
2460400.5, 0.587100, 0.967277,
162.2269, 111.8657, 58.1455,
2460450.123, 5.5, 4.0
),
'2024-06-15 12:00:00+00'::timestamptz
) AS eq) sub;
test | ra_hours | dec_deg | ra_valid | dec_valid
-------------+----------+---------+----------+-----------
oe_pipeline | 9.27 | 17.78 | t | t
(1 row)
-- ============================================================
-- Test 7: galilean_equatorial — all 4 moons
-- Io, Europa, Ganymede, Callisto should all return valid RA/Dec
-- near Jupiter's position
-- ============================================================
SELECT 'galilean_eq' AS test,
moon_id,
round(eq_ra(eq)::numeric, 4) AS ra_hours,
round(eq_dec(eq)::numeric, 4) AS dec_deg,
eq_ra(eq) BETWEEN 0 AND 24 AS ra_valid,
eq_dec(eq) BETWEEN -90 AND 90 AS dec_valid
FROM generate_series(0, 3) AS moon_id,
LATERAL galilean_equatorial(moon_id, '2024-06-15 12:00:00+00'::timestamptz) AS eq
ORDER BY moon_id;
test | moon_id | ra_hours | dec_deg | ra_valid | dec_valid
-------------+---------+----------+---------+----------+-----------
galilean_eq | 0 | 4.1957 | 20.3905 | t | t
galilean_eq | 1 | 4.1950 | 20.3883 | t | t
galilean_eq | 2 | 4.1937 | 20.3885 | t | t
galilean_eq | 3 | 4.2057 | 20.4177 | t | t
(4 rows)
-- ============================================================
-- Test 8: galilean moons should be near Jupiter
-- All 4 Galilean moons within 0.5 degrees of Jupiter
-- ============================================================
SELECT 'galilean_near_jupiter' AS test,
moon_id,
round((galilean_equatorial(moon_id, '2024-06-15 12:00:00+00') <->
planet_equatorial_apparent(5, '2024-06-15 12:00:00+00'))::numeric, 4)
AS sep_deg,
(galilean_equatorial(moon_id, '2024-06-15 12:00:00+00') <->
planet_equatorial_apparent(5, '2024-06-15 12:00:00+00')) < 0.5
AS within_half_deg
FROM generate_series(0, 3) AS moon_id
ORDER BY moon_id;
test | moon_id | sep_deg | within_half_deg
-----------------------+---------+---------+-----------------
galilean_near_jupiter | 0 | 0.0149 | t
galilean_near_jupiter | 1 | 0.0241 | t
galilean_near_jupiter | 2 | 0.0426 | t
galilean_near_jupiter | 3 | 0.1293 | t
(4 rows)
-- ============================================================
-- Test 9: saturn_moon_equatorial — Titan (id=5)
-- Should be near Saturn, within ~0.5 degrees
-- ============================================================
SELECT 'saturn_titan_eq' AS test,
round(eq_ra(eq)::numeric, 4) AS ra_hours,
round(eq_dec(eq)::numeric, 4) AS dec_deg,
round((eq <-> planet_equatorial_apparent(6, '2024-06-15 12:00:00+00'))::numeric, 4) AS sep_from_saturn,
(eq <-> planet_equatorial_apparent(6, '2024-06-15 12:00:00+00')) < 0.5 AS near_saturn
FROM saturn_moon_equatorial(5, '2024-06-15 12:00:00+00'::timestamptz) AS eq;
test | ra_hours | dec_deg | sep_from_saturn | near_saturn
-----------------+----------+---------+-----------------+-------------
saturn_titan_eq | 23.3909 | -6.0138 | 0.0187 | t
(1 row)
-- ============================================================
-- Test 10: uranus_moon_equatorial — Titania (id=3)
-- ============================================================
SELECT 'uranus_titania_eq' AS test,
round(eq_ra(eq)::numeric, 4) AS ra_hours,
round(eq_dec(eq)::numeric, 4) AS dec_deg,
eq_ra(eq) BETWEEN 0 AND 24 AS ra_valid,
eq_dec(eq) BETWEEN -90 AND 90 AS dec_valid
FROM uranus_moon_equatorial(3, '2024-06-15 12:00:00+00'::timestamptz) AS eq;
test | ra_hours | dec_deg | ra_valid | dec_valid
-------------------+----------+---------+----------+-----------
uranus_titania_eq | 3.5124 | 18.7450 | t | t
(1 row)
-- ============================================================
-- Test 11: mars_moon_equatorial — Phobos and Deimos
-- Both should be near Mars, within ~0.02 degrees (very close moons)
-- ============================================================
SELECT 'mars_moons_eq' AS test,
moon_id,
round(eq_ra(eq)::numeric, 4) AS ra_hours,
round(eq_dec(eq)::numeric, 4) AS dec_deg,
round((eq <-> planet_equatorial_apparent(4, '2024-06-15 12:00:00+00'))::numeric, 4) AS sep_from_mars
FROM generate_series(0, 1) AS moon_id,
LATERAL mars_moon_equatorial(moon_id, '2024-06-15 12:00:00+00'::timestamptz) AS eq
ORDER BY moon_id;
test | moon_id | ra_hours | dec_deg | sep_from_mars
---------------+---------+----------+---------+---------------
mars_moons_eq | 0 | 2.1851 | 12.0602 | 0.0075
mars_moons_eq | 1 | 2.1851 | 12.0572 | 0.0059
(2 rows)
-- ============================================================
-- Test 12: galilean_equatorial error — invalid body_id
-- ============================================================
DO $$
BEGIN
PERFORM galilean_equatorial(5, '2024-06-15 12:00:00+00');
RAISE EXCEPTION 'should have failed';
EXCEPTION WHEN numeric_value_out_of_range THEN
RAISE NOTICE 'galilean_eq_invalid: correctly rejected';
END;
$$;
NOTICE: galilean_eq_invalid: correctly rejected

181
test/sql/v011_features.sql Normal file
View File

@ -0,0 +1,181 @@
-- v0.11.0 feature tests: make_orbital_elements constructors + moon equatorial
-- ============================================================
-- Test 1: make_orbital_elements() — radians input
-- Round-trip: construct from radians, read back via accessors
-- ============================================================
SELECT 'make_oe_rad' AS test,
round(oe_epoch(oe)::numeric, 1) AS epoch,
round(oe_perihelion(oe)::numeric, 6) AS q_au,
round(oe_eccentricity(oe)::numeric, 6) AS ecc,
round(oe_inclination(oe)::numeric, 4) AS inc_deg,
round(oe_arg_perihelion(oe)::numeric, 4) AS omega_deg,
round(oe_raan(oe)::numeric, 4) AS node_deg,
round(oe_h_mag(oe)::numeric, 1) AS h_mag
FROM (SELECT make_orbital_elements(
2460400.5, -- epoch JD
0.587100, -- q AU
0.967277, -- e
radians(162.2269), -- inc
radians(111.8657), -- omega
radians(58.1455), -- Omega
2460450.123, -- tp JD
5.5, -- H
4.0 -- G
) AS oe) sub;
-- ============================================================
-- Test 2: make_orbital_elements_deg() — degree input
-- Same elements but angles in degrees; should produce identical output
-- ============================================================
SELECT 'make_oe_deg' AS test,
round(oe_epoch(oe)::numeric, 1) AS epoch,
round(oe_perihelion(oe)::numeric, 6) AS q_au,
round(oe_eccentricity(oe)::numeric, 6) AS ecc,
round(oe_inclination(oe)::numeric, 4) AS inc_deg,
round(oe_arg_perihelion(oe)::numeric, 4) AS omega_deg,
round(oe_raan(oe)::numeric, 4) AS node_deg,
round(oe_h_mag(oe)::numeric, 1) AS h_mag
FROM (SELECT make_orbital_elements_deg(
2460400.5, -- epoch JD
0.587100, -- q AU
0.967277, -- e
162.2269, -- inc degrees
111.8657, -- omega degrees
58.1455, -- Omega degrees
2460450.123, -- tp JD
5.5, -- H
4.0 -- G
) AS oe) sub;
-- ============================================================
-- Test 3: constructors produce identical results to text I/O
-- The text format uses degrees, so make_orbital_elements_deg should match
-- ============================================================
SELECT 'oe_roundtrip' AS test,
make_orbital_elements_deg(
2460400.5, 0.587100, 0.967277,
162.2269, 111.8657, 58.1455,
2460450.123, 5.5, 4.0
)::text
=
'(2460400.500000,0.5871000000,0.9672770000,162.226900,111.865700,58.145500,2460450.123000,5.50,4.00)'::orbital_elements::text
AS matches;
-- ============================================================
-- Test 4: make_orbital_elements() validation — negative q
-- ============================================================
DO $$
BEGIN
PERFORM make_orbital_elements(2460400.5, -0.1, 0.5, 0, 0, 0, 2460400.5, 0, 0);
RAISE EXCEPTION 'should have failed';
EXCEPTION WHEN numeric_value_out_of_range THEN
RAISE NOTICE 'make_oe_neg_q: correctly rejected';
END;
$$;
-- ============================================================
-- Test 5: make_orbital_elements() validation — negative eccentricity
-- ============================================================
DO $$
BEGIN
PERFORM make_orbital_elements(2460400.5, 1.0, -0.1, 0, 0, 0, 2460400.5, 0, 0);
RAISE EXCEPTION 'should have failed';
EXCEPTION WHEN numeric_value_out_of_range THEN
RAISE NOTICE 'make_oe_neg_e: correctly rejected';
END;
$$;
-- ============================================================
-- Test 6: make_orbital_elements used in small_body_equatorial()
-- Verify the constructor output works in the observation pipeline
-- ============================================================
SELECT 'oe_pipeline' AS test,
round(eq_ra(eq)::numeric, 2) AS ra_hours,
round(eq_dec(eq)::numeric, 2) AS dec_deg,
eq_ra(eq) BETWEEN 0 AND 24 AS ra_valid,
eq_dec(eq) BETWEEN -90 AND 90 AS dec_valid
FROM (SELECT small_body_equatorial(
make_orbital_elements_deg(
2460400.5, 0.587100, 0.967277,
162.2269, 111.8657, 58.1455,
2460450.123, 5.5, 4.0
),
'2024-06-15 12:00:00+00'::timestamptz
) AS eq) sub;
-- ============================================================
-- Test 7: galilean_equatorial — all 4 moons
-- Io, Europa, Ganymede, Callisto should all return valid RA/Dec
-- near Jupiter's position
-- ============================================================
SELECT 'galilean_eq' AS test,
moon_id,
round(eq_ra(eq)::numeric, 4) AS ra_hours,
round(eq_dec(eq)::numeric, 4) AS dec_deg,
eq_ra(eq) BETWEEN 0 AND 24 AS ra_valid,
eq_dec(eq) BETWEEN -90 AND 90 AS dec_valid
FROM generate_series(0, 3) AS moon_id,
LATERAL galilean_equatorial(moon_id, '2024-06-15 12:00:00+00'::timestamptz) AS eq
ORDER BY moon_id;
-- ============================================================
-- Test 8: galilean moons should be near Jupiter
-- All 4 Galilean moons within 0.5 degrees of Jupiter
-- ============================================================
SELECT 'galilean_near_jupiter' AS test,
moon_id,
round((galilean_equatorial(moon_id, '2024-06-15 12:00:00+00') <->
planet_equatorial_apparent(5, '2024-06-15 12:00:00+00'))::numeric, 4)
AS sep_deg,
(galilean_equatorial(moon_id, '2024-06-15 12:00:00+00') <->
planet_equatorial_apparent(5, '2024-06-15 12:00:00+00')) < 0.5
AS within_half_deg
FROM generate_series(0, 3) AS moon_id
ORDER BY moon_id;
-- ============================================================
-- Test 9: saturn_moon_equatorial — Titan (id=5)
-- Should be near Saturn, within ~0.5 degrees
-- ============================================================
SELECT 'saturn_titan_eq' AS test,
round(eq_ra(eq)::numeric, 4) AS ra_hours,
round(eq_dec(eq)::numeric, 4) AS dec_deg,
round((eq <-> planet_equatorial_apparent(6, '2024-06-15 12:00:00+00'))::numeric, 4) AS sep_from_saturn,
(eq <-> planet_equatorial_apparent(6, '2024-06-15 12:00:00+00')) < 0.5 AS near_saturn
FROM saturn_moon_equatorial(5, '2024-06-15 12:00:00+00'::timestamptz) AS eq;
-- ============================================================
-- Test 10: uranus_moon_equatorial — Titania (id=3)
-- ============================================================
SELECT 'uranus_titania_eq' AS test,
round(eq_ra(eq)::numeric, 4) AS ra_hours,
round(eq_dec(eq)::numeric, 4) AS dec_deg,
eq_ra(eq) BETWEEN 0 AND 24 AS ra_valid,
eq_dec(eq) BETWEEN -90 AND 90 AS dec_valid
FROM uranus_moon_equatorial(3, '2024-06-15 12:00:00+00'::timestamptz) AS eq;
-- ============================================================
-- Test 11: mars_moon_equatorial — Phobos and Deimos
-- Both should be near Mars, within ~0.02 degrees (very close moons)
-- ============================================================
SELECT 'mars_moons_eq' AS test,
moon_id,
round(eq_ra(eq)::numeric, 4) AS ra_hours,
round(eq_dec(eq)::numeric, 4) AS dec_deg,
round((eq <-> planet_equatorial_apparent(4, '2024-06-15 12:00:00+00'))::numeric, 4) AS sep_from_mars
FROM generate_series(0, 1) AS moon_id,
LATERAL mars_moon_equatorial(moon_id, '2024-06-15 12:00:00+00'::timestamptz) AS eq
ORDER BY moon_id;
-- ============================================================
-- Test 12: galilean_equatorial error — invalid body_id
-- ============================================================
DO $$
BEGIN
PERFORM galilean_equatorial(5, '2024-06-15 12:00:00+00');
RAISE EXCEPTION 'should have failed';
EXCEPTION WHEN numeric_value_out_of_range THEN
RAISE NOTICE 'galilean_eq_invalid: correctly rejected';
END;
$$;