Skip to content

JPL DE Ephemeris

pg_orrery v0.3.0 adds optional support for JPL Development Ephemeris files (DE440/DE441), bringing positional accuracy from VSOP87’s ~1 arcsecond to DE441’s ~0.1 milliarcsecond. The upgrade path is designed so that nothing changes unless you opt in.

VSOP87 is sufficient for most purposes --- visual observation planning, rise/set times, conjunction identification, telescope pointing at optical wavelengths. You need DE when:

  • GHz dish pointing. A 10m dish at 10 GHz has a ~0.1 degree beamwidth. VSOP87’s 1 arcsecond error is well within the beam, but systematic campaigns benefit from the tighter DE error budget.
  • Precision astrometry. Comparing CCD field positions against computed planet positions at the sub-arcsecond level.
  • Occultation timing. Predicting when a planet or asteroid will occult a star requires milliarcsecond precision in the planet’s geocentric position.
  • Interplanetary mission design. Lambert transfer C3 values are sensitive to planet position accuracy. DE441 eliminates VSOP87 as an error source.
  1. Obtain a DE file. Download DE441 (~3.1 GB, covers -13200 to +17191) or DE440 (~115 MB, covers 1550 to 2650) from JPL:

    Terminal window
    # DE440 (smaller, covers 1550-2650, sufficient for most uses)
    curl -O https://ssd.jpl.nasa.gov/ftp/eph/planets/Linux/de440/linux_p1550p2650.440
    # DE441 (full range, large file)
    curl -O https://ssd.jpl.nasa.gov/ftp/eph/planets/Linux/de441/linux_m13000p17000.441
  2. Place the file where PostgreSQL can read it:

    Terminal window
    sudo mkdir -p /var/lib/postgres/pg_orrery
    sudo cp linux_p1550p2650.440 /var/lib/postgres/pg_orrery/de440.bin
    sudo chown postgres:postgres /var/lib/postgres/pg_orrery/de440.bin
  3. Set the GUC (requires superuser):

    ALTER SYSTEM SET pg_orrery.ephemeris_path = '/var/lib/postgres/pg_orrery/de440.bin';
    SELECT pg_reload_conf();
  4. Verify:

    SELECT * FROM pg_orrery_ephemeris_info();

    You should see provider = 'JPL_DE' with the file’s JD range and version.

Every VSOP87 function has a _de() counterpart with the same signature:

-- Always VSOP87, always IMMUTABLE
SELECT topo_azimuth(planet_observe(4, '40.0N 105.3W 1655m'::observer, now())) AS mars_az;

The _de() variants share the same parameter types, return types, and body ID conventions. You can swap between them by adding or removing _de from the function name.

SELECT body_id,
CASE body_id
WHEN 1 THEN 'Mercury' WHEN 2 THEN 'Venus'
WHEN 4 THEN 'Mars' WHEN 5 THEN 'Jupiter'
WHEN 6 THEN 'Saturn' WHEN 7 THEN 'Uranus'
WHEN 8 THEN 'Neptune'
END AS planet,
round(topo_azimuth(planet_observe_de(body_id, '40.0N 105.3W 1655m'::observer, now()))::numeric, 4) AS az,
round(topo_elevation(planet_observe_de(body_id, '40.0N 105.3W 1655m'::observer, now()))::numeric, 4) AS el
FROM unnest(ARRAY[1,2,4,5,6,7,8]) AS body_id
WHERE topo_elevation(planet_observe_de(body_id, '40.0N 105.3W 1655m'::observer, now())) > 0
ORDER BY el DESC;
SELECT round(topo_azimuth(moon_observe_de('40.0N 105.3W 1655m'::observer, now()))::numeric, 2) AS az,
round(topo_elevation(moon_observe_de('40.0N 105.3W 1655m'::observer, now()))::numeric, 2) AS el,
round(topo_range(moon_observe_de('40.0N 105.3W 1655m'::observer, now()))::numeric, 0) AS range_km;
-- Earth-Mars transfer with DE-quality planet positions
SELECT * FROM lambert_transfer_de(3, 4, '2026-05-01 00:00:00+00', '2027-01-15 00:00:00+00');

Compare DE and VSOP87 results to see the sub-arcsecond difference:

SELECT body_id,
round(helio_distance(planet_heliocentric(body_id, '2024-06-21 12:00:00+00'))::numeric, 10) AS vsop87_r,
round(helio_distance(planet_heliocentric_de(body_id, '2024-06-21 12:00:00+00'))::numeric, 10) AS de_r,
round((helio_distance(planet_heliocentric_de(body_id, '2024-06-21 12:00:00+00'))
- helio_distance(planet_heliocentric(body_id, '2024-06-21 12:00:00+00')))::numeric * 149597870.7, 2) AS diff_km
FROM generate_series(1, 8) AS body_id;
ProviderTheoryAccuracyVolatilityData Source
VSOP87Fourier series (Bretagnon 1988)~1 arcsecond (inner planets), up to 40 arcseconds (Neptune, +/- 4000 yr)IMMUTABLECompiled-in coefficients
ELP2000-82BFourier series (Chapront 1983)~2 arcseconds (longitude)IMMUTABLECompiled-in coefficients
JPL DE440Numerical integration~0.1 milliarcsecondSTABLEExternal binary file (115 MB)
JPL DE441Numerical integration~0.1 milliarcsecondSTABLEExternal binary file (3.1 GB)

The pg_orrery_ephemeris_info() function reports the current state:

SELECT * FROM pg_orrery_ephemeris_info();
ColumnTypeDescription
providertext'VSOP87' or 'JPL_DE'
file_pathtextPath to DE file (empty if VSOP87-only)
start_jdfloat8First Julian Date in the DE file
end_jdfloat8Last Julian Date in the DE file
versionint4DE version number (440, 441, etc.)
au_kmfloat8AU value from the DE header

When no DE file is configured, provider returns 'VSOP87' and the remaining columns are defaults.

DE functions never fail silently. The fallback rules:

  1. No GUC set (default): Fall back to VSOP87/ELP2000-82B silently. No NOTICE, no overhead.
  2. GUC set, file works: Use DE positions.
  3. GUC set, query-time failure (JD out of range, I/O error): Emit NOTICE explaining the fallback, then use VSOP87/ELP2000-82B.

This means _de() functions always return a valid result. They never abort due to DE-specific issues --- at worst, they degrade to VSOP87 accuracy with a NOTICE explaining why.