Add v0.10.0: aberration, DE apparent, angular separation, stellar parallax

Annual stellar aberration (~20 arcsec) added to all 6 existing _apparent()
functions via classical first-order v/c projection (Ron & Vondrak). Earth
velocity sourced from VSOP87 xyz[3..5] (analytic) or DE numerical
differentiation.

New functions (106 -> 114):
- eq_angular_distance(): Vincenty formula, stable at 0 and 180 deg
- eq_within_cone(): cosine shortcut for fast cone-search predicate
- <-> operator on equatorial type
- 6 DE apparent variants with VSOP87 fallback:
  planet/sun/moon_observe_apparent_de(),
  planet/moon_equatorial_apparent_de(),
  small_body_observe_apparent_de()

Stellar parallax now functional in star_observe_pm() and
star_equatorial_pm() — Green (1985) Eq. 11.3 displacement using
Earth heliocentric position from VSOP87.

All 19 regression suites pass (18 existing + new aberration suite).
This commit is contained in:
Ryan Malloy 2026-02-21 21:47:42 -07:00
parent 3a378f572a
commit b0741c553b
14 changed files with 2649 additions and 14 deletions

View File

@ -7,7 +7,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.6.0.sql sql/pg_orrery--0.5.0--0.6.0.sql \ sql/pg_orrery--0.6.0.sql sql/pg_orrery--0.5.0--0.6.0.sql \
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
# 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 \
@ -38,7 +39,8 @@ OBJS += $(SGP4_OBJS)
# Regression tests # Regression tests
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 vallado_518 de_ephemeris od_fit spgist_tle orbital_elements equatorial refraction \
aberration 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,147 @@
# Message 003
| Field | Value |
|-------|-------|
| From | pg-orrery |
| To | astrolock-api |
| Date | 2026-02-22T05:15:00-07:00 |
| Re | v0.10.0 ready + answers to your three questions |
---
## Nice work on the v0.9.0 integration
The satellite CTE restructure is exactly right — single `sgp4_propagate_safe()` into two LATERAL joins avoids the double-propagation trap. And good call keeping `moon_observe()` for alt/az rather than switching to `_apparent()` for the Moon — 1.3s of light-time is below noise for everything except interferometry.
One note on the proper motion deferral: you're right that ~50"/25yr is below rotor accuracy for most stars, but Barnard's Star is 258"/25yr. If anyone ever points a rotor at Barnard's, they'll miss by 4 arcmin. Low priority, but something to seed when you eventually do the schema migration.
## v0.10.0 is ready
Just finished. All 19 regression suites pass. Not tagged yet (still on `phase/spgist-orbital-trie`), but the code and SQL migration are committed.
**8 new SQL functions** (106 -> 114) + 1 new operator:
### What changed in functions you already use
**This is the important bit.** The `_apparent()` functions you integrated in v0.9.0 now include **annual stellar aberration** (~20 arcsec) on top of the light-time correction they already had. This is a physics improvement, not a breaking API change — same function signatures, same return types, more accurate positions.
What this means for Craft's live positions:
- `planet_observe_apparent()` — now includes aberration. Jupiter shifts by ~29" combined (light-time + aberration). Your LiveTracker will be ~20" more accurate automatically.
- `sun_observe_apparent()` — aberration adds ~15" in elevation
- `moon_equatorial_apparent()` — aberration adds ~22" in RA
- `planet_equatorial_apparent()` — same combined correction
The underlying `_observe()` and `_equatorial()` (geometric) functions are unchanged.
### New stuff
| Function | What it does |
|----------|--------------|
| `eq_angular_distance(equatorial, equatorial)` | Angular separation in degrees (Vincenty formula, stable at 0 and 180 deg) |
| `eq_within_cone(equatorial, equatorial, float8)` | Fast cone-search predicate (cosine shortcut) |
| `<->` operator on equatorial | Operator form of `eq_angular_distance` |
| `planet_observe_apparent_de(int4, observer, timestamptz)` | DE apparent with aberration (falls back to VSOP87) |
| `sun_observe_apparent_de(observer, timestamptz)` | Same for Sun |
| `moon_observe_apparent_de(observer, timestamptz)` | Same for Moon |
| `planet_equatorial_apparent_de(int4, timestamptz)` | DE apparent RA/Dec with aberration |
| `moon_equatorial_apparent_de(timestamptz)` | DE apparent Moon RA/Dec |
| `small_body_observe_apparent_de(orbital_elements, observer, timestamptz)` | DE apparent for comets/asteroids |
**Stellar parallax** is also now functional in `star_observe_pm()` and `star_equatorial_pm()`. The `parallax_mas` parameter that was previously `(void)`-cast now applies the Green (1985) displacement using Earth's heliocentric position from VSOP87. Proxima Centauri (768 mas) shows 1.02 arcsec displacement in our tests. Matters only for the nearest stars — but when you eventually add the proper motion columns, the plumbing is ready.
### Angular separation use cases for Craft
The `<->` operator and `eq_within_cone()` could be useful for Craft:
```sql
-- "What's near Jupiter right now?"
SELECT co.name,
planet_equatorial(5, NOW()) <-> eci_to_equatorial_geo(
sgp4_propagate_safe(co.tle, NOW()), NOW()
) AS separation_deg
FROM celestial_object co
WHERE co.tle IS NOT NULL
AND eq_within_cone(
eci_to_equatorial_geo(sgp4_propagate_safe(co.tle, NOW()), NOW()),
planet_equatorial(5, NOW()),
10.0 -- within 10 degrees
)
ORDER BY separation_deg;
```
### Upgrade path
```sql
ALTER EXTENSION pg_orrery UPDATE TO '0.10.0';
```
The migration chains from 0.9.0. Since you chained directly from 0.3.0 to 0.9.0, the path is: your current 0.9.0 -> 0.10.0 via `pg_orrery--0.9.0--0.10.0.sql`.
## Answers to your questions
### 1. `orbital_elements` constructor from floats
Yes, this is straightforward. The type is 9 floats internally:
```
(epoch_jd, a_or_q, e, inc_rad, omega_rad, Omega_rad, tp_jd, H, G)
```
Today you can construct it with the tuple syntax:
```sql
SELECT small_body_equatorial(
format('(%s,%s,%s,%s,%s,%s,%s,%s,%s)',
co.epoch_jd, co.q_au, co.e, co.inc_rad,
co.arg_peri_rad, co.node_rad, co.tp_jd, co.h_mag, co.g_slope
)::orbital_elements,
NOW()
) FROM celestial_object co WHERE co.object_type = 'comet';
```
But a proper SQL constructor function would be cleaner:
```sql
SELECT eq_ra(small_body_equatorial(
make_orbital_elements(epoch_jd, q, e, inc, omega, node, tp, h, g),
NOW()
));
```
I'll add `make_orbital_elements(float8 x 9) -> orbital_elements` to the roadmap. Low effort, high convenience for your use case.
### 2. `galilean_equatorial()`
Feasible. The underlying `galilean_observe()` already computes geocentric positions via L1.2 theory. Adding equatorial output follows the same pattern as `planet_equatorial()` — convert the geocentric ecliptic position to equatorial J2000, precess to date. Same for `saturn_moon_equatorial()`, `uranus_moon_equatorial()`, `mars_moon_equatorial()`.
The interesting question is whether to return Jupiter-centric or geocentric RA/Dec. For telescope pointing you want geocentric (where to point the scope). For Galilean moon event prediction (transits, shadows) you want Jupiter-centric offsets. Both are useful.
I'll plan geocentric `galilean_equatorial(int4, timestamptz)` for the next version. Probably paired with the other moon families.
### 3. Refracted pass accuracy
We don't have a formal benchmark against Heavens-Above/N2YO yet, but here's what we can say:
**The physics.** Bennett (1982) refraction at the geometric horizon (0 deg) is 0.5695 deg. Our refracted pass finder uses this as the effective horizon — a satellite is "visible" when its geometric elevation exceeds -0.569 deg. The standard (non-refracted) finder uses 0 deg.
**The ~35s shift.** For a typical ISS pass, the satellite moves ~7 deg/min near the horizon. At 0.569 deg of refraction: `0.569 / 7 * 60 = ~4.9 seconds` per horizon crossing, so ~10 seconds total (AOS earlier + LOS later). The "~35 seconds" figure in message 001 was an upper bound — actual shift depends on pass geometry. Low-elevation grazing passes see more shift; overhead passes less.
**Regression test 14** (`refraction.out:167-183`) verifies that refracted passes find >= standard passes over a 7-day ISS window. This catches the case where refraction makes previously-invisible grazing passes appear above the effective horizon.
**Validation approach.** The cleanest comparison would be:
1. Pick 5 well-predicted ISS passes from Heavens-Above for a specific location
2. Run both `predict_passes()` and `predict_passes_refracted()` for the same TLE + location + window
3. Compare AOS/LOS times against Heavens-Above (which uses atmospheric refraction)
Heavens-Above doesn't publish their exact refraction model, but they do account for it. N2YO likely uses geometric horizon (no refraction). If you run this comparison and share results, I'll add the vectors to the test suite.
**One caveat**: TLE epoch staleness dominates over refraction for most prediction accuracy questions. A 3-day-old TLE can be off by 1-10 seconds in pass timing. Refraction correction only matters when the TLE is fresh (<24h old) and you need sub-minute AOS/LOS accuracy which is exactly the rotor pre-positioning use case.
---
**Next steps for recipient:**
- [ ] Rebuild db image with v0.10.0 when ready (not tagged yet, use `phase/spgist-orbital-trie` HEAD)
- [ ] `ALTER EXTENSION pg_orrery UPDATE TO '0.10.0'` — aberration improvement is automatic
- [ ] Consider `eq_within_cone()` for "what's near X" queries in the sky engine
- [ ] Run Heavens-Above comparison for 5 ISS passes if time permits
- [ ] Let us know if `make_orbital_elements()` constructor is high priority

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.9.0' default_version = '0.10.0'
module_pathname = '$libdir/pg_orrery' module_pathname = '$libdir/pg_orrery'
relocatable = true relocatable = true

1339
sql/pg_orrery--0.10.0.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,63 @@
-- pg_orrery 0.9.0 -> 0.10.0 migration
--
-- Adds annual aberration to existing _apparent() functions,
-- 6 new _apparent_de() variants, equatorial angular separation
-- operator and cone predicate, and stellar annual parallax.
-- ============================================================
-- Equatorial angular distance and cone search
-- ============================================================
CREATE FUNCTION eq_angular_distance(equatorial, equatorial) RETURNS float8
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION eq_angular_distance(equatorial, equatorial) IS
'Angular separation in degrees between two equatorial positions. Vincenty formula (stable at 0 and 180 degrees).';
CREATE FUNCTION eq_within_cone(equatorial, equatorial, float8) RETURNS bool
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION eq_within_cone(equatorial, equatorial, float8) IS
'True if first position is within radius_deg of second position. Cosine shortcut for fast rejection.';
CREATE OPERATOR <-> (
LEFTARG = equatorial,
RIGHTARG = equatorial,
FUNCTION = eq_angular_distance,
COMMUTATOR = <->
);
COMMENT ON OPERATOR <-> (equatorial, equatorial) IS
'Angular separation in degrees between two equatorial positions.';
-- ============================================================
-- DE apparent observation functions (STABLE, light-time + aberration)
-- ============================================================
CREATE FUNCTION planet_observe_apparent_de(int4, observer, timestamptz) RETURNS topocentric
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION planet_observe_apparent_de(int4, observer, timestamptz) IS
'Observe a planet with light-time correction and annual aberration via JPL DE (falls back to VSOP87).';
CREATE FUNCTION sun_observe_apparent_de(observer, timestamptz) RETURNS topocentric
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION sun_observe_apparent_de(observer, timestamptz) IS
'Observe the Sun with aberration via JPL DE (falls back to VSOP87).';
CREATE FUNCTION moon_observe_apparent_de(observer, timestamptz) RETURNS topocentric
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION moon_observe_apparent_de(observer, timestamptz) IS
'Observe the Moon with light-time correction and annual aberration via JPL DE (falls back to ELP2000-82B).';
CREATE FUNCTION planet_equatorial_apparent_de(int4, timestamptz) RETURNS equatorial
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION planet_equatorial_apparent_de(int4, timestamptz) IS
'Geocentric apparent RA/Dec of a planet with light-time correction and annual aberration via JPL DE (falls back to VSOP87).';
CREATE FUNCTION moon_equatorial_apparent_de(timestamptz) RETURNS equatorial
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION moon_equatorial_apparent_de(timestamptz) IS
'Geocentric apparent RA/Dec of the Moon with light-time correction and annual aberration via JPL DE (falls back to ELP2000-82B).';
CREATE FUNCTION small_body_observe_apparent_de(orbital_elements, observer, timestamptz) RETURNS topocentric
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION small_body_observe_apparent_de(orbital_elements, observer, timestamptz) IS
'Observe a comet/asteroid with light-time correction and annual aberration. Earth position via JPL DE (falls back to VSOP87).';

View File

@ -242,6 +242,124 @@ geocentric_to_equatorial(const double geo_ecl_au[3], double jd,
} }
/*
* Apply classical annual aberration to J2000 equatorial RA/Dec.
*
* The apparent direction of an object is displaced toward the apex of
* Earth's motion by v/c (~20.5 arcseconds maximum).
*
* earth_vel_equ: Earth's velocity in equatorial J2000 (AU/day)
* ra, dec: pointers to J2000 RA and Dec (radians), modified in-place
*
* Reference: Green (1985) "Spherical Astronomy", Eq. 10.22
*/
static inline void
apply_annual_aberration(const double earth_vel_equ[3],
double *ra, double *dec)
{
double cos_ra = cos(*ra);
double sin_ra = sin(*ra);
double cos_dec = cos(*dec);
double sin_dec = sin(*dec);
double d_ra, d_dec;
/* Near celestial poles, cos(dec) -> 0, dRA diverges. Skip. */
if (fabs(cos_dec) < 1e-12)
return;
d_ra = (-earth_vel_equ[0] * sin_ra + earth_vel_equ[1] * cos_ra)
/ (C_LIGHT_AU_DAY * cos_dec);
d_dec = (-earth_vel_equ[0] * cos_ra * sin_dec
- earth_vel_equ[1] * sin_ra * sin_dec
+ earth_vel_equ[2] * cos_dec)
/ C_LIGHT_AU_DAY;
*ra += d_ra;
*dec += d_dec;
if (*ra < 0.0)
*ra += 2.0 * M_PI;
if (*ra >= 2.0 * M_PI)
*ra -= 2.0 * M_PI;
}
/*
* Geocentric observation pipeline with annual aberration.
*
* Same as observe_from_geocentric() plus aberration correction applied
* in J2000 equatorial coordinates before precessing to date.
*
* earth_vel_ecl: Earth's velocity in ecliptic J2000 (AU/day)
* gets rotated to equatorial internally.
*/
static inline void
observe_from_geocentric_aberrated(const double geo_ecl_au[3], double jd,
const pg_observer *obs,
const double earth_vel_ecl[3],
pg_topocentric *result)
{
double geo_equ[3];
double vel_equ[3];
double ra_j2000, dec_j2000, geo_dist;
double ra_date, dec_date;
double gmst_val, lst, ha;
double az, el;
ecliptic_to_equatorial(geo_ecl_au, geo_equ);
ecliptic_to_equatorial(earth_vel_ecl, vel_equ);
cartesian_to_spherical(geo_equ, &ra_j2000, &dec_j2000, &geo_dist);
apply_annual_aberration(vel_equ, &ra_j2000, &dec_j2000);
precess_j2000_to_date(jd, ra_j2000, dec_j2000, &ra_date, &dec_date);
gmst_val = gmst_from_jd(jd);
lst = gmst_val + obs->lon;
ha = lst - ra_date;
equatorial_to_horizontal(ha, dec_date, obs->lat, &az, &el);
result->azimuth = az;
result->elevation = el;
result->range_km = geo_dist * AU_KM;
result->range_rate = 0.0;
}
/*
* Geocentric ecliptic J2000 (AU) -> equatorial RA/Dec of date, with aberration.
*
* Same as geocentric_to_equatorial() plus annual aberration before precession.
* earth_vel_ecl: Earth's velocity in ecliptic J2000 (AU/day).
*/
static inline void
geocentric_to_equatorial_aberrated(const double geo_ecl_au[3], double jd,
const double earth_vel_ecl[3],
pg_equatorial *result)
{
double geo_equ[3];
double vel_equ[3];
double ra_j2000, dec_j2000, geo_dist;
double ra_date, dec_date;
ecliptic_to_equatorial(geo_ecl_au, geo_equ);
ecliptic_to_equatorial(earth_vel_ecl, vel_equ);
cartesian_to_spherical(geo_equ, &ra_j2000, &dec_j2000, &geo_dist);
apply_annual_aberration(vel_equ, &ra_j2000, &dec_j2000);
precess_j2000_to_date(jd, ra_j2000, dec_j2000, &ra_date, &dec_date);
result->ra = ra_date;
result->dec = dec_date;
result->distance = geo_dist * AU_KM;
}
/* /*
* TEME position (km) to equatorial RA/Dec, geocentric. * TEME position (km) to equatorial RA/Dec, geocentric.
* *

View File

@ -27,6 +27,7 @@
#include "eph_provider.h" #include "eph_provider.h"
#include "vsop87.h" #include "vsop87.h"
#include "elp82b.h" #include "elp82b.h"
#include "kepler.h"
#include "lambert.h" #include "lambert.h"
#include "l12.h" #include "l12.h"
#include "tass17.h" #include "tass17.h"
@ -49,6 +50,12 @@ PG_FUNCTION_INFO_V1(uranus_moon_observe_de);
PG_FUNCTION_INFO_V1(mars_moon_observe_de); PG_FUNCTION_INFO_V1(mars_moon_observe_de);
PG_FUNCTION_INFO_V1(planet_equatorial_de); PG_FUNCTION_INFO_V1(planet_equatorial_de);
PG_FUNCTION_INFO_V1(moon_equatorial_de); PG_FUNCTION_INFO_V1(moon_equatorial_de);
PG_FUNCTION_INFO_V1(planet_observe_apparent_de);
PG_FUNCTION_INFO_V1(sun_observe_apparent_de);
PG_FUNCTION_INFO_V1(moon_observe_apparent_de);
PG_FUNCTION_INFO_V1(planet_equatorial_apparent_de);
PG_FUNCTION_INFO_V1(moon_equatorial_apparent_de);
PG_FUNCTION_INFO_V1(small_body_observe_apparent_de);
PG_FUNCTION_INFO_V1(pg_orrery_ephemeris_info); PG_FUNCTION_INFO_V1(pg_orrery_ephemeris_info);
@ -704,6 +711,402 @@ moon_equatorial_de(PG_FUNCTION_ARGS)
} }
/* ================================================================
* Earth velocity via DE (numerical differentiation) or VSOP87 (analytic).
*
* DE path: central difference (earth(jd+dt) - earth(jd-dt)) / (2*dt)
* VSOP87 path: analytic velocity from earth_xyz[3..5]
*
* use_de: must match the provider used for position (rule 7).
* vel_ecl[3]: output Earth velocity in ecliptic J2000 (AU/day).
* ================================================================
*/
static void
earth_velocity_de(double jd, bool use_de, double vel_ecl[3])
{
if (use_de)
{
double pos_fwd[6], pos_bwd[6];
double dt = 0.01; /* days */
bool got_fwd = eph_de_earth(jd + dt, pos_fwd);
bool got_bwd = eph_de_earth(jd - dt, pos_bwd);
if (got_fwd && got_bwd)
{
vel_ecl[0] = (pos_fwd[0] - pos_bwd[0]) / (2.0 * dt);
vel_ecl[1] = (pos_fwd[1] - pos_bwd[1]) / (2.0 * dt);
vel_ecl[2] = (pos_fwd[2] - pos_bwd[2]) / (2.0 * dt);
return;
}
/* DE boundary straddled — fall through to VSOP87 */
}
{
double earth_xyz[6];
GetVsop87Coor(jd, 2, earth_xyz);
vel_ecl[0] = earth_xyz[3];
vel_ecl[1] = earth_xyz[4];
vel_ecl[2] = earth_xyz[5];
}
}
/* ================================================================
* planet_observe_apparent_de(body_id int, observer, timestamptz) -> topocentric
*
* DE variant of planet_observe_apparent(). STABLE.
* Light-time + annual aberration. Rule 7.
* ================================================================
*/
Datum
planet_observe_apparent_de(PG_FUNCTION_ARGS)
{
int32 body_id = PG_GETARG_INT32(0);
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
int64 ts = PG_GETARG_INT64(2);
double jd;
double earth_xyz[6];
double planet_xyz[6];
double geo_ecl[3];
double geo_dist, tau;
double vel_ecl[3];
pg_topocentric *result;
bool have_de;
if (body_id < BODY_MERCURY || body_id > BODY_NEPTUNE)
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("planet_observe_apparent_de: body_id %d must be 1-8 (Mercury-Neptune)",
body_id)));
if (body_id == BODY_EARTH)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot observe Earth from Earth")));
jd = timestamptz_to_jd(ts);
/* Rule 7: both planet and Earth from same provider */
have_de = eph_de_planet(body_id, jd, planet_xyz) &&
eph_de_earth(jd, earth_xyz);
if (!have_de)
{
int vsop_body = body_id - 1;
if (eph_de_available())
ereport(NOTICE,
(errmsg("DE ephemeris unavailable for this query, falling back to VSOP87")));
GetVsop87Coor(jd, 2, earth_xyz);
GetVsop87Coor(jd, vsop_body, planet_xyz);
}
/* Geometric geocentric distance */
geo_ecl[0] = planet_xyz[0] - earth_xyz[0];
geo_ecl[1] = planet_xyz[1] - earth_xyz[1];
geo_ecl[2] = planet_xyz[2] - earth_xyz[2];
geo_dist = sqrt(geo_ecl[0]*geo_ecl[0] + geo_ecl[1]*geo_ecl[1] + geo_ecl[2]*geo_ecl[2]);
/* Retarded planet position (same provider) */
tau = geo_dist / C_LIGHT_AU_DAY;
if (have_de)
eph_de_planet(body_id, jd - tau, planet_xyz);
else
GetVsop87Coor(jd - tau, body_id - 1, planet_xyz);
geo_ecl[0] = planet_xyz[0] - earth_xyz[0];
geo_ecl[1] = planet_xyz[1] - earth_xyz[1];
geo_ecl[2] = planet_xyz[2] - earth_xyz[2];
/* Earth velocity for aberration */
earth_velocity_de(jd, have_de, vel_ecl);
result = (pg_topocentric *) palloc(sizeof(pg_topocentric));
observe_from_geocentric_aberrated(geo_ecl, jd, obs, vel_ecl, result);
PG_RETURN_POINTER(result);
}
/* ================================================================
* sun_observe_apparent_de(observer, timestamptz) -> topocentric
*
* DE variant of sun_observe_apparent(). STABLE.
* ================================================================
*/
Datum
sun_observe_apparent_de(PG_FUNCTION_ARGS)
{
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
int64 ts = PG_GETARG_INT64(1);
double jd;
double earth_xyz[6];
double geo_ecl[3];
double vel_ecl[3];
pg_topocentric *result;
bool have_de;
jd = timestamptz_to_jd(ts);
have_de = eph_de_earth(jd, earth_xyz);
if (!have_de)
{
if (eph_de_available())
ereport(NOTICE,
(errmsg("DE ephemeris unavailable, falling back to VSOP87")));
GetVsop87Coor(jd, 2, earth_xyz);
}
geo_ecl[0] = -earth_xyz[0];
geo_ecl[1] = -earth_xyz[1];
geo_ecl[2] = -earth_xyz[2];
earth_velocity_de(jd, have_de, vel_ecl);
result = (pg_topocentric *) palloc(sizeof(pg_topocentric));
observe_from_geocentric_aberrated(geo_ecl, jd, obs, vel_ecl, result);
PG_RETURN_POINTER(result);
}
/* ================================================================
* moon_observe_apparent_de(observer, timestamptz) -> topocentric
*
* DE variant with light-time + aberration. STABLE.
* Moon position from DE, Earth velocity from DE (numerical) or VSOP87.
* ================================================================
*/
Datum
moon_observe_apparent_de(PG_FUNCTION_ARGS)
{
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
int64 ts = PG_GETARG_INT64(1);
double jd;
double moon_ecl[3];
double geo_dist, tau;
double vel_ecl[3];
pg_topocentric *result;
bool have_de;
jd = timestamptz_to_jd(ts);
have_de = eph_de_moon(jd, moon_ecl);
if (!have_de)
{
if (eph_de_available())
ereport(NOTICE,
(errmsg("DE ephemeris unavailable, falling back to ELP2000-82B")));
GetElp82bCoor(jd, moon_ecl);
}
/* Light-time correction */
geo_dist = sqrt(moon_ecl[0]*moon_ecl[0] + moon_ecl[1]*moon_ecl[1] + moon_ecl[2]*moon_ecl[2]);
tau = geo_dist / C_LIGHT_AU_DAY;
if (have_de)
eph_de_moon(jd - tau, moon_ecl);
else
GetElp82bCoor(jd - tau, moon_ecl);
/* Earth velocity for aberration (DE numerical or VSOP87 analytic) */
earth_velocity_de(jd, have_de, vel_ecl);
result = (pg_topocentric *) palloc(sizeof(pg_topocentric));
observe_from_geocentric_aberrated(moon_ecl, jd, obs, vel_ecl, result);
PG_RETURN_POINTER(result);
}
/* ================================================================
* planet_equatorial_apparent_de(body_id int, timestamptz) -> equatorial
*
* DE variant with light-time + aberration. STABLE.
* ================================================================
*/
Datum
planet_equatorial_apparent_de(PG_FUNCTION_ARGS)
{
int32 body_id = PG_GETARG_INT32(0);
int64 ts = PG_GETARG_INT64(1);
double jd;
double earth_xyz[6];
double planet_xyz[6];
double geo_ecl[3];
double geo_dist, tau;
double vel_ecl[3];
pg_equatorial *result;
bool have_de;
if (body_id < BODY_MERCURY || body_id > BODY_NEPTUNE)
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("planet_equatorial_apparent_de: body_id %d must be 1-8 (Mercury-Neptune)",
body_id)));
if (body_id == BODY_EARTH)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot observe Earth from Earth")));
jd = timestamptz_to_jd(ts);
have_de = eph_de_planet(body_id, jd, planet_xyz) &&
eph_de_earth(jd, earth_xyz);
if (!have_de)
{
int vsop_body = body_id - 1;
if (eph_de_available())
ereport(NOTICE,
(errmsg("DE ephemeris unavailable for this query, falling back to VSOP87")));
GetVsop87Coor(jd, 2, earth_xyz);
GetVsop87Coor(jd, vsop_body, planet_xyz);
}
geo_ecl[0] = planet_xyz[0] - earth_xyz[0];
geo_ecl[1] = planet_xyz[1] - earth_xyz[1];
geo_ecl[2] = planet_xyz[2] - earth_xyz[2];
geo_dist = sqrt(geo_ecl[0]*geo_ecl[0] + geo_ecl[1]*geo_ecl[1] + geo_ecl[2]*geo_ecl[2]);
tau = geo_dist / C_LIGHT_AU_DAY;
if (have_de)
eph_de_planet(body_id, jd - tau, planet_xyz);
else
GetVsop87Coor(jd - tau, body_id - 1, planet_xyz);
geo_ecl[0] = planet_xyz[0] - earth_xyz[0];
geo_ecl[1] = planet_xyz[1] - earth_xyz[1];
geo_ecl[2] = planet_xyz[2] - earth_xyz[2];
earth_velocity_de(jd, have_de, vel_ecl);
result = (pg_equatorial *) palloc(sizeof(pg_equatorial));
geocentric_to_equatorial_aberrated(geo_ecl, jd, vel_ecl, result);
PG_RETURN_POINTER(result);
}
/* ================================================================
* moon_equatorial_apparent_de(timestamptz) -> equatorial
*
* DE variant with light-time + aberration. STABLE.
* ================================================================
*/
Datum
moon_equatorial_apparent_de(PG_FUNCTION_ARGS)
{
int64 ts = PG_GETARG_INT64(0);
double jd;
double moon_ecl[3];
double geo_dist, tau;
double vel_ecl[3];
pg_equatorial *result;
bool have_de;
jd = timestamptz_to_jd(ts);
have_de = eph_de_moon(jd, moon_ecl);
if (!have_de)
{
if (eph_de_available())
ereport(NOTICE,
(errmsg("DE ephemeris unavailable, falling back to ELP2000-82B")));
GetElp82bCoor(jd, moon_ecl);
}
geo_dist = sqrt(moon_ecl[0]*moon_ecl[0] + moon_ecl[1]*moon_ecl[1] + moon_ecl[2]*moon_ecl[2]);
tau = geo_dist / C_LIGHT_AU_DAY;
if (have_de)
eph_de_moon(jd - tau, moon_ecl);
else
GetElp82bCoor(jd - tau, moon_ecl);
earth_velocity_de(jd, have_de, vel_ecl);
result = (pg_equatorial *) palloc(sizeof(pg_equatorial));
geocentric_to_equatorial_aberrated(moon_ecl, jd, vel_ecl, result);
PG_RETURN_POINTER(result);
}
/* ================================================================
* small_body_observe_apparent_de(orbital_elements, observer, timestamptz) -> topocentric
*
* DE variant of small_body_observe_apparent(). Uses DE for Earth
* position (rule 7 satisfied: body is always Kepler, Earth from
* best available provider). STABLE.
* ================================================================
*/
Datum
small_body_observe_apparent_de(PG_FUNCTION_ARGS)
{
pg_orbital_elements *oe = (pg_orbital_elements *) PG_GETARG_POINTER(0);
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
int64 ts = PG_GETARG_INT64(2);
double jd;
double body_helio[3];
double earth_xyz[6];
double geo_ecl[3];
double geo_dist, tau;
double vel_ecl[3];
pg_topocentric *result;
bool have_de;
jd = timestamptz_to_jd(ts);
kepler_position(oe->q, oe->e, oe->inc, oe->arg_peri, oe->raan,
oe->tp, jd, body_helio);
have_de = eph_de_earth(jd, earth_xyz);
if (!have_de)
{
if (eph_de_available())
ereport(NOTICE,
(errmsg("DE ephemeris unavailable, falling back to VSOP87")));
GetVsop87Coor(jd, 2, earth_xyz);
}
geo_ecl[0] = body_helio[0] - earth_xyz[0];
geo_ecl[1] = body_helio[1] - earth_xyz[1];
geo_ecl[2] = body_helio[2] - earth_xyz[2];
geo_dist = sqrt(geo_ecl[0]*geo_ecl[0] + geo_ecl[1]*geo_ecl[1] + geo_ecl[2]*geo_ecl[2]);
tau = geo_dist / C_LIGHT_AU_DAY;
kepler_position(oe->q, oe->e, oe->inc, oe->arg_peri, oe->raan,
oe->tp, jd - tau, body_helio);
geo_ecl[0] = body_helio[0] - earth_xyz[0];
geo_ecl[1] = body_helio[1] - earth_xyz[1];
geo_ecl[2] = body_helio[2] - earth_xyz[2];
earth_velocity_de(jd, have_de, vel_ecl);
result = (pg_topocentric *) palloc(sizeof(pg_topocentric));
observe_from_geocentric_aberrated(geo_ecl, jd, obs, vel_ecl, result);
PG_RETURN_POINTER(result);
}
/* ================================================================ /* ================================================================
* pg_orrery_ephemeris_info() -> RECORD * pg_orrery_ephemeris_info() -> RECORD
* *

View File

@ -46,6 +46,10 @@ PG_FUNCTION_INFO_V1(planet_equatorial);
PG_FUNCTION_INFO_V1(sun_equatorial); PG_FUNCTION_INFO_V1(sun_equatorial);
PG_FUNCTION_INFO_V1(moon_equatorial); PG_FUNCTION_INFO_V1(moon_equatorial);
/* Angular distance and cone search */
PG_FUNCTION_INFO_V1(eq_angular_distance);
PG_FUNCTION_INFO_V1(eq_within_cone);
/* ---------------------------------------------------------------- /* ----------------------------------------------------------------
* Static helper -- observer geodetic to ECEF. * Static helper -- observer geodetic to ECEF.
@ -364,3 +368,69 @@ moon_equatorial(PG_FUNCTION_ARGS)
} }
/* ================================================================
* eq_angular_distance(equatorial, equatorial) -> float8
*
* Angular separation in degrees between two equatorial positions.
* Uses the Vincenty formula (numerically stable at all separations
* including 0 and 180 degrees, unlike the haversine or dot product).
*
* Vincenty: atan2(num, den) where
* num = sqrt((cos d2 sin dRA)^2 + (cos d1 sin d2 - sin d1 cos d2 cos dRA)^2)
* den = sin d1 sin d2 + cos d1 cos d2 cos dRA
* ================================================================
*/
Datum
eq_angular_distance(PG_FUNCTION_ARGS)
{
pg_equatorial *a = (pg_equatorial *) PG_GETARG_POINTER(0);
pg_equatorial *b = (pg_equatorial *) PG_GETARG_POINTER(1);
double d_ra, cos_d_ra, sin_d_ra;
double sin_d1, cos_d1, sin_d2, cos_d2;
double num1, num2, num, den;
d_ra = b->ra - a->ra;
cos_d_ra = cos(d_ra);
sin_d_ra = sin(d_ra);
sin_d1 = sin(a->dec);
cos_d1 = cos(a->dec);
sin_d2 = sin(b->dec);
cos_d2 = cos(b->dec);
num1 = cos_d2 * sin_d_ra;
num2 = cos_d1 * sin_d2 - sin_d1 * cos_d2 * cos_d_ra;
num = sqrt(num1 * num1 + num2 * num2);
den = sin_d1 * sin_d2 + cos_d1 * cos_d2 * cos_d_ra;
PG_RETURN_FLOAT8(atan2(num, den) * RAD_TO_DEG);
}
/* ================================================================
* eq_within_cone(equatorial, equatorial, float8) -> bool
*
* Returns true if the first position is within `radius_deg` degrees
* of the second position. Cosine shortcut avoids the expensive
* atan2 in the Vincenty formula for most reject cases.
* ================================================================
*/
Datum
eq_within_cone(PG_FUNCTION_ARGS)
{
pg_equatorial *a = (pg_equatorial *) PG_GETARG_POINTER(0);
pg_equatorial *b = (pg_equatorial *) PG_GETARG_POINTER(1);
double radius = PG_GETARG_FLOAT8(2);
double cos_r, d_ra, cos_sep;
if (radius < 0.0)
PG_RETURN_BOOL(false);
cos_r = cos(radius * DEG_TO_RAD);
d_ra = b->ra - a->ra;
cos_sep = sin(a->dec) * sin(b->dec)
+ cos(a->dec) * cos(b->dec) * cos(d_ra);
PG_RETURN_BOOL(cos_sep >= cos_r);
}

View File

@ -588,7 +588,7 @@ small_body_observe_apparent(PG_FUNCTION_ARGS)
geo_ecl[2] = body_helio[2] - earth_xyz[2]; geo_ecl[2] = body_helio[2] - earth_xyz[2];
result = (pg_topocentric *) palloc(sizeof(pg_topocentric)); result = (pg_topocentric *) palloc(sizeof(pg_topocentric));
observe_from_geocentric(geo_ecl, jd, obs, result); observe_from_geocentric_aberrated(geo_ecl, jd, obs, &earth_xyz[3], result);
PG_RETURN_POINTER(result); PG_RETURN_POINTER(result);
} }
@ -677,7 +677,7 @@ small_body_equatorial_apparent(PG_FUNCTION_ARGS)
geo_ecl[2] = body_helio[2] - earth_xyz[2]; geo_ecl[2] = body_helio[2] - earth_xyz[2];
result = (pg_equatorial *) palloc(sizeof(pg_equatorial)); result = (pg_equatorial *) palloc(sizeof(pg_equatorial));
geocentric_to_equatorial(geo_ecl, jd, result); geocentric_to_equatorial_aberrated(geo_ecl, jd, &earth_xyz[3], result);
PG_RETURN_POINTER(result); PG_RETURN_POINTER(result);
} }

View File

@ -265,7 +265,7 @@ planet_observe_apparent(PG_FUNCTION_ARGS)
geo_ecl[2] = planet_xyz[2] - earth_xyz[2]; geo_ecl[2] = planet_xyz[2] - earth_xyz[2];
result = (pg_topocentric *) palloc(sizeof(pg_topocentric)); result = (pg_topocentric *) palloc(sizeof(pg_topocentric));
observe_from_geocentric(geo_ecl, jd, obs, result); observe_from_geocentric_aberrated(geo_ecl, jd, obs, &earth_xyz[3], result);
PG_RETURN_POINTER(result); PG_RETURN_POINTER(result);
} }
@ -301,7 +301,7 @@ sun_observe_apparent(PG_FUNCTION_ARGS)
geo_ecl[2] = -earth_xyz[2]; geo_ecl[2] = -earth_xyz[2];
result = (pg_topocentric *) palloc(sizeof(pg_topocentric)); result = (pg_topocentric *) palloc(sizeof(pg_topocentric));
observe_from_geocentric(geo_ecl, jd, obs, result); observe_from_geocentric_aberrated(geo_ecl, jd, obs, &earth_xyz[3], result);
PG_RETURN_POINTER(result); PG_RETURN_POINTER(result);
} }
@ -365,7 +365,7 @@ planet_equatorial_apparent(PG_FUNCTION_ARGS)
geo_ecl[2] = planet_xyz[2] - earth_xyz[2]; geo_ecl[2] = planet_xyz[2] - earth_xyz[2];
result = (pg_equatorial *) palloc(sizeof(pg_equatorial)); result = (pg_equatorial *) palloc(sizeof(pg_equatorial));
geocentric_to_equatorial(geo_ecl, jd, result); geocentric_to_equatorial_aberrated(geo_ecl, jd, &earth_xyz[3], result);
PG_RETURN_POINTER(result); PG_RETURN_POINTER(result);
} }
@ -387,6 +387,7 @@ moon_equatorial_apparent(PG_FUNCTION_ARGS)
int64 ts = PG_GETARG_INT64(0); int64 ts = PG_GETARG_INT64(0);
double jd; double jd;
double moon_ecl[3]; double moon_ecl[3];
double earth_xyz[6];
double geo_dist, tau; double geo_dist, tau;
pg_equatorial *result; pg_equatorial *result;
@ -401,8 +402,11 @@ moon_equatorial_apparent(PG_FUNCTION_ARGS)
tau = geo_dist / C_LIGHT_AU_DAY; tau = geo_dist / C_LIGHT_AU_DAY;
GetElp82bCoor(jd - tau, moon_ecl); GetElp82bCoor(jd - tau, moon_ecl);
/* Earth velocity for aberration (ELP doesn't provide it) */
GetVsop87Coor(jd, 2, earth_xyz);
result = (pg_equatorial *) palloc(sizeof(pg_equatorial)); result = (pg_equatorial *) palloc(sizeof(pg_equatorial));
geocentric_to_equatorial(moon_ecl, jd, result); geocentric_to_equatorial_aberrated(moon_ecl, jd, &earth_xyz[3], result);
PG_RETURN_POINTER(result); PG_RETURN_POINTER(result);
} }

View File

@ -17,6 +17,7 @@
#include "utils/timestamp.h" #include "utils/timestamp.h"
#include "types.h" #include "types.h"
#include "astro_math.h" #include "astro_math.h"
#include "vsop87.h"
PG_FUNCTION_INFO_V1(star_observe); PG_FUNCTION_INFO_V1(star_observe);
PG_FUNCTION_INFO_V1(star_observe_safe); PG_FUNCTION_INFO_V1(star_observe_safe);
@ -197,9 +198,35 @@ star_observe_pm(PG_FUNCTION_ARGS)
if (ra_corrected < 0.0) if (ra_corrected < 0.0)
ra_corrected += 2.0 * M_PI; ra_corrected += 2.0 * M_PI;
/* Parallax and RV accepted for API completeness; positional effect /* Annual parallax: displace star position by Earth's heliocentric
* is sub-arcsecond for all but the nearest stars. */ * position projected onto the star direction.
(void) parallax_mas; * Green (1985), Eq. 11.3: d_RA and d_Dec from parallax. */
if (parallax_mas > 0.0)
{
double earth_xyz[6], earth_equ[3];
double p_rad = (parallax_mas / 1000.0) * ARCSEC_TO_RAD;
double sin_ra_c = sin(ra_corrected);
double cos_ra_c = cos(ra_corrected);
double sin_dec_c = sin(dec_corrected);
double cos_dec_c = cos(dec_corrected);
GetVsop87Coor(jd, 2, earth_xyz);
ecliptic_to_equatorial(earth_xyz, earth_equ);
/* Parallax displacement (Green 1985 Eq. 11.3) */
if (fabs(cos_dec_c) > 1e-12)
{
ra_corrected += p_rad * (earth_equ[0] * sin_ra_c
- earth_equ[1] * cos_ra_c) / cos_dec_c;
dec_corrected += p_rad * (earth_equ[0] * cos_ra_c * sin_dec_c
+ earth_equ[1] * sin_ra_c * sin_dec_c
- earth_equ[2] * cos_dec_c);
}
ra_corrected = fmod(ra_corrected, 2.0 * M_PI);
if (ra_corrected < 0.0)
ra_corrected += 2.0 * M_PI;
}
(void) rv_kms; (void) rv_kms;
precess_j2000_to_date(jd, ra_corrected, dec_corrected, &ra_date, &dec_date); precess_j2000_to_date(jd, ra_corrected, dec_corrected, &ra_date, &dec_date);
@ -285,6 +312,33 @@ star_equatorial_pm(PG_FUNCTION_ARGS)
if (ra_corrected < 0.0) if (ra_corrected < 0.0)
ra_corrected += 2.0 * M_PI; ra_corrected += 2.0 * M_PI;
/* Annual parallax displacement (Green 1985 Eq. 11.3) */
if (parallax_mas > 0.0)
{
double earth_xyz[6], earth_equ[3];
double p_rad = (parallax_mas / 1000.0) * ARCSEC_TO_RAD;
double sin_ra_c = sin(ra_corrected);
double cos_ra_c = cos(ra_corrected);
double sin_dec_c = sin(dec_corrected);
double cos_dec_c = cos(dec_corrected);
GetVsop87Coor(jd, 2, earth_xyz);
ecliptic_to_equatorial(earth_xyz, earth_equ);
if (fabs(cos_dec_c) > 1e-12)
{
ra_corrected += p_rad * (earth_equ[0] * sin_ra_c
- earth_equ[1] * cos_ra_c) / cos_dec_c;
dec_corrected += p_rad * (earth_equ[0] * cos_ra_c * sin_dec_c
+ earth_equ[1] * sin_ra_c * sin_dec_c
- earth_equ[2] * cos_dec_c);
}
ra_corrected = fmod(ra_corrected, 2.0 * M_PI);
if (ra_corrected < 0.0)
ra_corrected += 2.0 * M_PI;
}
precess_j2000_to_date(jd, ra_corrected, dec_corrected, &ra_date, &dec_date); precess_j2000_to_date(jd, ra_corrected, dec_corrected, &ra_date, &dec_date);
result = (pg_equatorial *) palloc(sizeof(pg_equatorial)); result = (pg_equatorial *) palloc(sizeof(pg_equatorial));

View File

@ -0,0 +1,247 @@
-- aberration regression tests
--
-- Tests annual aberration in _apparent() functions, DE apparent variants,
-- equatorial angular distance/cone search, and stellar annual parallax.
\set boulder '''40.015N 105.270W 1655m'''::observer
-- ============================================================
-- Test 1: Aberration magnitude — planet_equatorial_apparent
-- vs planet_equatorial (geometric). Jupiter aberration should
-- be in the range 0-20 arcsec (~0.001 hours at Jupiter's dec).
-- ============================================================
SELECT 'aberration_planet' AS test,
round((abs(
eq_ra(planet_equatorial_apparent(5, '2024-06-21 12:00:00+00'))
- eq_ra(planet_equatorial(5, '2024-06-21 12:00:00+00'))
) * 3600 * 15)::numeric, 0) AS diff_arcsec,
abs(
eq_ra(planet_equatorial_apparent(5, '2024-06-21 12:00:00+00'))
- eq_ra(planet_equatorial(5, '2024-06-21 12:00:00+00'))
) * 3600 * 15 BETWEEN 1 AND 50 AS magnitude_valid;
test | diff_arcsec | magnitude_valid
-------------------+-------------+-----------------
aberration_planet | 29 | t
(1 row)
-- ============================================================
-- Test 2: Aberration magnitude — sun_observe_apparent vs sun_observe
-- Sun aberration should be ~20 arcsec (Earth orbital velocity).
-- Compare elevations (both from same observer, same time).
-- ============================================================
SELECT 'aberration_sun' AS test,
round((abs(
topo_elevation(sun_observe_apparent(:boulder, '2024-06-21 12:00:00+00'))
- topo_elevation(sun_observe(:boulder, '2024-06-21 12:00:00+00'))
) * 3600)::numeric, 0) AS diff_arcsec,
abs(
topo_elevation(sun_observe_apparent(:boulder, '2024-06-21 12:00:00+00'))
- topo_elevation(sun_observe(:boulder, '2024-06-21 12:00:00+00'))
) * 3600 BETWEEN 1 AND 25 AS magnitude_valid;
test | diff_arcsec | magnitude_valid
----------------+-------------+-----------------
aberration_sun | 15 | t
(1 row)
-- ============================================================
-- Test 3: Moon aberration should be present (same ~20 arcsec
-- as all other objects — aberration depends on observer velocity,
-- not object distance).
-- ============================================================
SELECT 'aberration_moon' AS test,
round((abs(
eq_ra(moon_equatorial_apparent('2024-06-21 12:00:00+00'))
- eq_ra(moon_equatorial('2024-06-21 12:00:00+00'))
) * 3600 * 15)::numeric, 0) AS diff_arcsec,
abs(
eq_ra(moon_equatorial_apparent('2024-06-21 12:00:00+00'))
- eq_ra(moon_equatorial('2024-06-21 12:00:00+00'))
) * 3600 * 15 BETWEEN 1 AND 25 AS magnitude_valid;
test | diff_arcsec | magnitude_valid
-----------------+-------------+-----------------
aberration_moon | 22 | t
(1 row)
-- ============================================================
-- Test 4: DE apparent fallback — without DE configured,
-- _apparent_de() should match _apparent() exactly.
-- ============================================================
SELECT 'de_apparent_fallback' AS test,
round(eq_ra(planet_equatorial_apparent_de(5, '2024-06-21 12:00:00+00'))::numeric, 6) =
round(eq_ra(planet_equatorial_apparent(5, '2024-06-21 12:00:00+00'))::numeric, 6) AS planet_match,
round(eq_ra(moon_equatorial_apparent_de('2024-06-21 12:00:00+00'))::numeric, 6) =
round(eq_ra(moon_equatorial_apparent('2024-06-21 12:00:00+00'))::numeric, 6) AS moon_match;
test | planet_match | moon_match
----------------------+--------------+------------
de_apparent_fallback | t | t
(1 row)
-- ============================================================
-- Test 5: DE apparent topocentric fallback
-- ============================================================
SELECT 'de_topo_fallback' AS test,
round(topo_elevation(planet_observe_apparent_de(5, :boulder, '2024-06-21 12:00:00+00'))::numeric, 4) =
round(topo_elevation(planet_observe_apparent(5, :boulder, '2024-06-21 12:00:00+00'))::numeric, 4) AS planet_match,
round(topo_elevation(sun_observe_apparent_de(:boulder, '2024-06-21 12:00:00+00'))::numeric, 4) =
round(topo_elevation(sun_observe_apparent(:boulder, '2024-06-21 12:00:00+00'))::numeric, 4) AS sun_match,
topo_elevation(moon_observe_apparent_de(:boulder, '2024-06-21 12:00:00+00')) BETWEEN -90 AND 90 AS moon_valid;
test | planet_match | sun_match | moon_valid
------------------+--------------+-----------+------------
de_topo_fallback | t | t | t
(1 row)
-- ============================================================
-- Test 6: Small body DE apparent fallback
-- ============================================================
SELECT 'de_smallbody_fallback' AS test,
round(topo_elevation(small_body_observe_apparent_de(
'(2460400.5,2.5577,0.0785,0.1849,1.2836,1.4013,2460500.0,3.53,0.12)'::orbital_elements,
:boulder, '2024-06-21 12:00:00+00'))::numeric, 4) =
round(topo_elevation(small_body_observe_apparent(
'(2460400.5,2.5577,0.0785,0.1849,1.2836,1.4013,2460500.0,3.53,0.12)'::orbital_elements,
:boulder, '2024-06-21 12:00:00+00'))::numeric, 4) AS match;
test | match
-----------------------+-------
de_smallbody_fallback | t
(1 row)
-- ============================================================
-- Test 7: Angular distance — Dubhe and Merak (Big Dipper pointers)
-- Dubhe: RA 11.062h, Dec 61.751 deg
-- Merak: RA 11.031h, Dec 56.382 deg
-- Expected separation: ~5.4 degrees
-- ============================================================
SELECT 'angular_distance' AS test,
round(eq_angular_distance(
star_equatorial(11.062, 61.751, '2024-06-21 12:00:00+00'),
star_equatorial(11.031, 56.382, '2024-06-21 12:00:00+00')
)::numeric, 1) AS sep_deg;
test | sep_deg
------------------+---------
angular_distance | 5.4
(1 row)
-- ============================================================
-- Test 8: Angular distance — same position should be 0
-- ============================================================
SELECT 'angular_distance_zero' AS test,
round(eq_angular_distance(
'(12.00000000,45.00000000,0.000)'::equatorial,
'(12.00000000,45.00000000,0.000)'::equatorial
)::numeric, 6) AS sep_deg;
test | sep_deg
-----------------------+----------
angular_distance_zero | 0.000000
(1 row)
-- ============================================================
-- Test 9: Angular distance — opposite poles should be 180
-- ============================================================
SELECT 'angular_distance_poles' AS test,
round(eq_angular_distance(
'(0.00000000,90.00000000,0.000)'::equatorial,
'(0.00000000,-90.00000000,0.000)'::equatorial
)::numeric, 1) AS sep_deg;
test | sep_deg
------------------------+---------
angular_distance_poles | 180.0
(1 row)
-- ============================================================
-- Test 10: <-> operator (same as eq_angular_distance)
-- ============================================================
SELECT 'operator_arrow' AS test,
round((
star_equatorial(11.062, 61.751, '2024-06-21 12:00:00+00')
<->
star_equatorial(11.031, 56.382, '2024-06-21 12:00:00+00')
)::numeric, 1) AS sep_deg;
test | sep_deg
----------------+---------
operator_arrow | 5.4
(1 row)
-- ============================================================
-- Test 11: Cone search — Polaris within 5 deg of NCP
-- ============================================================
SELECT 'cone_inside' AS test,
eq_within_cone(
star_equatorial(2.530303, 89.2641, '2024-06-21 12:00:00+00'),
'(0.00000000,90.00000000,0.000)'::equatorial,
5.0
) AS inside;
test | inside
-------------+--------
cone_inside | t
(1 row)
-- ============================================================
-- Test 12: Cone search — Sirius not within 5 deg of NCP
-- ============================================================
SELECT 'cone_outside' AS test,
eq_within_cone(
star_equatorial(6.7525, -16.7161, '2024-06-21 12:00:00+00'),
'(0.00000000,90.00000000,0.000)'::equatorial,
5.0
) AS inside;
test | inside
--------------+--------
cone_outside | f
(1 row)
-- ============================================================
-- Test 13: Stellar parallax — Proxima Centauri (768 mas)
-- Compare with-parallax vs without-parallax at the SAME epoch
-- to isolate the parallax displacement from proper motion and
-- precession. Expected: ~0.2-1.5 arcsec depending on Earth's
-- orbital phase (max near quadrature for this RA).
-- Proxima: RA 14.495h, Dec -62.679 deg
-- ============================================================
SELECT 'stellar_parallax' AS test,
round((abs(
eq_ra(star_equatorial_pm(14.495, -62.679, -3775.40, 769.33, 768.07, -21.7,
'2024-03-20 12:00:00+00'))
- eq_ra(star_equatorial_pm(14.495, -62.679, -3775.40, 769.33, 0.0, -21.7,
'2024-03-20 12:00:00+00'))
) * 3600 * 15)::numeric, 2) AS shift_arcsec,
abs(
eq_ra(star_equatorial_pm(14.495, -62.679, -3775.40, 769.33, 768.07, -21.7,
'2024-03-20 12:00:00+00'))
- eq_ra(star_equatorial_pm(14.495, -62.679, -3775.40, 769.33, 0.0, -21.7,
'2024-03-20 12:00:00+00'))
) * 3600 * 15 BETWEEN 0.01 AND 2.0 AS magnitude_valid;
test | shift_arcsec | magnitude_valid
------------------+--------------+-----------------
stellar_parallax | 1.02 | t
(1 row)
-- ============================================================
-- Test 14: Parallax = 0 should not change star position
-- (same as without parallax)
-- ============================================================
SELECT 'parallax_zero' AS test,
round(eq_ra(star_equatorial_pm(14.495, -62.679, -3775.40, 769.33, 0.0, -21.7,
'2024-06-21 12:00:00+00'))::numeric, 6) =
round(eq_ra(star_equatorial_pm(14.495, -62.679, -3775.40, 769.33, 0.0, -21.7,
'2024-06-21 12:00:00+00'))::numeric, 6) AS match;
test | match
---------------+-------
parallax_zero | t
(1 row)
-- ============================================================
-- Test 15: star_observe_pm parallax affects topocentric result
-- Barnard's Star with parallax should differ from without
-- ============================================================
SELECT 'parallax_topo' AS test,
abs(
topo_elevation(star_observe_pm(
17.963472, 4.6933, -798.58, 10328.12, 545.4, -110.51,
:boulder, '2024-07-15 04:00:00+00'))
- topo_elevation(star_observe_pm(
17.963472, 4.6933, -798.58, 10328.12, 0.0, -110.51,
:boulder, '2024-07-15 04:00:00+00'))
) * 3600 BETWEEN 0.01 AND 2.0 AS displacement_valid;
test | displacement_valid
---------------+--------------------
parallax_topo | t
(1 row)

View File

@ -224,7 +224,7 @@ SELECT 'jupiter_apparent' AS test,
)::numeric, 4) AS ra_diff_hours; )::numeric, 4) AS ra_diff_hours;
test | ra_diff_hours test | ra_diff_hours
------------------+--------------- ------------------+---------------
jupiter_apparent | 0.0002 jupiter_apparent | 0.0005
(1 row) (1 row)
-- ============================================================ -- ============================================================
@ -238,7 +238,7 @@ SELECT 'moon_apparent' AS test,
)::numeric, 6) AS ra_diff_hours; )::numeric, 6) AS ra_diff_hours;
test | ra_diff_hours test | ra_diff_hours
---------------+--------------- ---------------+---------------
moon_apparent | 0.000015 moon_apparent | 0.000405
(1 row) (1 row)
-- ============================================================ -- ============================================================

188
test/sql/aberration.sql Normal file
View File

@ -0,0 +1,188 @@
-- aberration regression tests
--
-- Tests annual aberration in _apparent() functions, DE apparent variants,
-- equatorial angular distance/cone search, and stellar annual parallax.
\set boulder '''40.015N 105.270W 1655m'''::observer
-- ============================================================
-- Test 1: Aberration magnitude — planet_equatorial_apparent
-- vs planet_equatorial (geometric). Jupiter aberration should
-- be in the range 0-20 arcsec (~0.001 hours at Jupiter's dec).
-- ============================================================
SELECT 'aberration_planet' AS test,
round((abs(
eq_ra(planet_equatorial_apparent(5, '2024-06-21 12:00:00+00'))
- eq_ra(planet_equatorial(5, '2024-06-21 12:00:00+00'))
) * 3600 * 15)::numeric, 0) AS diff_arcsec,
abs(
eq_ra(planet_equatorial_apparent(5, '2024-06-21 12:00:00+00'))
- eq_ra(planet_equatorial(5, '2024-06-21 12:00:00+00'))
) * 3600 * 15 BETWEEN 1 AND 50 AS magnitude_valid;
-- ============================================================
-- Test 2: Aberration magnitude — sun_observe_apparent vs sun_observe
-- Sun aberration should be ~20 arcsec (Earth orbital velocity).
-- Compare elevations (both from same observer, same time).
-- ============================================================
SELECT 'aberration_sun' AS test,
round((abs(
topo_elevation(sun_observe_apparent(:boulder, '2024-06-21 12:00:00+00'))
- topo_elevation(sun_observe(:boulder, '2024-06-21 12:00:00+00'))
) * 3600)::numeric, 0) AS diff_arcsec,
abs(
topo_elevation(sun_observe_apparent(:boulder, '2024-06-21 12:00:00+00'))
- topo_elevation(sun_observe(:boulder, '2024-06-21 12:00:00+00'))
) * 3600 BETWEEN 1 AND 25 AS magnitude_valid;
-- ============================================================
-- Test 3: Moon aberration should be present (same ~20 arcsec
-- as all other objects — aberration depends on observer velocity,
-- not object distance).
-- ============================================================
SELECT 'aberration_moon' AS test,
round((abs(
eq_ra(moon_equatorial_apparent('2024-06-21 12:00:00+00'))
- eq_ra(moon_equatorial('2024-06-21 12:00:00+00'))
) * 3600 * 15)::numeric, 0) AS diff_arcsec,
abs(
eq_ra(moon_equatorial_apparent('2024-06-21 12:00:00+00'))
- eq_ra(moon_equatorial('2024-06-21 12:00:00+00'))
) * 3600 * 15 BETWEEN 1 AND 25 AS magnitude_valid;
-- ============================================================
-- Test 4: DE apparent fallback — without DE configured,
-- _apparent_de() should match _apparent() exactly.
-- ============================================================
SELECT 'de_apparent_fallback' AS test,
round(eq_ra(planet_equatorial_apparent_de(5, '2024-06-21 12:00:00+00'))::numeric, 6) =
round(eq_ra(planet_equatorial_apparent(5, '2024-06-21 12:00:00+00'))::numeric, 6) AS planet_match,
round(eq_ra(moon_equatorial_apparent_de('2024-06-21 12:00:00+00'))::numeric, 6) =
round(eq_ra(moon_equatorial_apparent('2024-06-21 12:00:00+00'))::numeric, 6) AS moon_match;
-- ============================================================
-- Test 5: DE apparent topocentric fallback
-- ============================================================
SELECT 'de_topo_fallback' AS test,
round(topo_elevation(planet_observe_apparent_de(5, :boulder, '2024-06-21 12:00:00+00'))::numeric, 4) =
round(topo_elevation(planet_observe_apparent(5, :boulder, '2024-06-21 12:00:00+00'))::numeric, 4) AS planet_match,
round(topo_elevation(sun_observe_apparent_de(:boulder, '2024-06-21 12:00:00+00'))::numeric, 4) =
round(topo_elevation(sun_observe_apparent(:boulder, '2024-06-21 12:00:00+00'))::numeric, 4) AS sun_match,
topo_elevation(moon_observe_apparent_de(:boulder, '2024-06-21 12:00:00+00')) BETWEEN -90 AND 90 AS moon_valid;
-- ============================================================
-- Test 6: Small body DE apparent fallback
-- ============================================================
SELECT 'de_smallbody_fallback' AS test,
round(topo_elevation(small_body_observe_apparent_de(
'(2460400.5,2.5577,0.0785,0.1849,1.2836,1.4013,2460500.0,3.53,0.12)'::orbital_elements,
:boulder, '2024-06-21 12:00:00+00'))::numeric, 4) =
round(topo_elevation(small_body_observe_apparent(
'(2460400.5,2.5577,0.0785,0.1849,1.2836,1.4013,2460500.0,3.53,0.12)'::orbital_elements,
:boulder, '2024-06-21 12:00:00+00'))::numeric, 4) AS match;
-- ============================================================
-- Test 7: Angular distance — Dubhe and Merak (Big Dipper pointers)
-- Dubhe: RA 11.062h, Dec 61.751 deg
-- Merak: RA 11.031h, Dec 56.382 deg
-- Expected separation: ~5.4 degrees
-- ============================================================
SELECT 'angular_distance' AS test,
round(eq_angular_distance(
star_equatorial(11.062, 61.751, '2024-06-21 12:00:00+00'),
star_equatorial(11.031, 56.382, '2024-06-21 12:00:00+00')
)::numeric, 1) AS sep_deg;
-- ============================================================
-- Test 8: Angular distance — same position should be 0
-- ============================================================
SELECT 'angular_distance_zero' AS test,
round(eq_angular_distance(
'(12.00000000,45.00000000,0.000)'::equatorial,
'(12.00000000,45.00000000,0.000)'::equatorial
)::numeric, 6) AS sep_deg;
-- ============================================================
-- Test 9: Angular distance — opposite poles should be 180
-- ============================================================
SELECT 'angular_distance_poles' AS test,
round(eq_angular_distance(
'(0.00000000,90.00000000,0.000)'::equatorial,
'(0.00000000,-90.00000000,0.000)'::equatorial
)::numeric, 1) AS sep_deg;
-- ============================================================
-- Test 10: <-> operator (same as eq_angular_distance)
-- ============================================================
SELECT 'operator_arrow' AS test,
round((
star_equatorial(11.062, 61.751, '2024-06-21 12:00:00+00')
<->
star_equatorial(11.031, 56.382, '2024-06-21 12:00:00+00')
)::numeric, 1) AS sep_deg;
-- ============================================================
-- Test 11: Cone search — Polaris within 5 deg of NCP
-- ============================================================
SELECT 'cone_inside' AS test,
eq_within_cone(
star_equatorial(2.530303, 89.2641, '2024-06-21 12:00:00+00'),
'(0.00000000,90.00000000,0.000)'::equatorial,
5.0
) AS inside;
-- ============================================================
-- Test 12: Cone search — Sirius not within 5 deg of NCP
-- ============================================================
SELECT 'cone_outside' AS test,
eq_within_cone(
star_equatorial(6.7525, -16.7161, '2024-06-21 12:00:00+00'),
'(0.00000000,90.00000000,0.000)'::equatorial,
5.0
) AS inside;
-- ============================================================
-- Test 13: Stellar parallax — Proxima Centauri (768 mas)
-- Compare with-parallax vs without-parallax at the SAME epoch
-- to isolate the parallax displacement from proper motion and
-- precession. Expected: ~0.2-1.5 arcsec depending on Earth's
-- orbital phase (max near quadrature for this RA).
-- Proxima: RA 14.495h, Dec -62.679 deg
-- ============================================================
SELECT 'stellar_parallax' AS test,
round((abs(
eq_ra(star_equatorial_pm(14.495, -62.679, -3775.40, 769.33, 768.07, -21.7,
'2024-03-20 12:00:00+00'))
- eq_ra(star_equatorial_pm(14.495, -62.679, -3775.40, 769.33, 0.0, -21.7,
'2024-03-20 12:00:00+00'))
) * 3600 * 15)::numeric, 2) AS shift_arcsec,
abs(
eq_ra(star_equatorial_pm(14.495, -62.679, -3775.40, 769.33, 768.07, -21.7,
'2024-03-20 12:00:00+00'))
- eq_ra(star_equatorial_pm(14.495, -62.679, -3775.40, 769.33, 0.0, -21.7,
'2024-03-20 12:00:00+00'))
) * 3600 * 15 BETWEEN 0.01 AND 2.0 AS magnitude_valid;
-- ============================================================
-- Test 14: Parallax = 0 should not change star position
-- (same as without parallax)
-- ============================================================
SELECT 'parallax_zero' AS test,
round(eq_ra(star_equatorial_pm(14.495, -62.679, -3775.40, 769.33, 0.0, -21.7,
'2024-06-21 12:00:00+00'))::numeric, 6) =
round(eq_ra(star_equatorial_pm(14.495, -62.679, -3775.40, 769.33, 0.0, -21.7,
'2024-06-21 12:00:00+00'))::numeric, 6) AS match;
-- ============================================================
-- Test 15: star_observe_pm parallax affects topocentric result
-- Barnard's Star with parallax should differ from without
-- ============================================================
SELECT 'parallax_topo' AS test,
abs(
topo_elevation(star_observe_pm(
17.963472, 4.6933, -798.58, 10328.12, 545.4, -110.51,
:boulder, '2024-07-15 04:00:00+00'))
- topo_elevation(star_observe_pm(
17.963472, 4.6933, -798.58, 10328.12, 0.0, -110.51,
:boulder, '2024-07-15 04:00:00+00'))
) * 3600 BETWEEN 0.01 AND 2.0 AS displacement_valid;