Observing the Solar System
pg_orbit computes positions for all eight planets (VSOP87), the Sun, and the Moon (ELP2000-82B). Every observation returns the same topocentric type: azimuth, elevation, range, and range rate from a given observer at a given time. The solar system becomes queryable with standard SQL.
How you do it today
Section titled “How you do it today”Knowing where planets are involves one of a few approaches:
- Stellarium gives you a beautiful real-time sky view. You scrub time, click objects, read coordinates. Not scriptable, not batch-queryable.
- JPL Horizons computes high-precision ephemerides via web form or API. Accurate to milliarcseconds. One object per request, rate-limited.
- Skyfield (Python) loads JPL DE441 ephemerides and computes positions with sub-arcsecond accuracy. Excellent for one-off scripts; batch processing over large time ranges or many observers means writing loops.
- Astropy provides coordinate frames, time systems, and ERFA wrappers. Powerful, but computing “what’s above the horizon right now” requires assembling several components.
All of these produce results that live outside your database. If you want to correlate planet positions with weather data, observation logs, or satellite passes, you export, import, and join.
What changes with pg_orbit
Section titled “What changes with pg_orbit”All planets, the Sun, and the Moon are available as SQL function calls. The functions take an observer and a timestamp, and return topocentric coordinates. You can sweep all eight planets, generate time series, filter by elevation, and join with other tables in the same query.
Key functions:
| Function | What it computes |
|---|---|
planet_observe(body_id, observer, time) | Topocentric az/el/range for a planet |
planet_heliocentric(body_id, time) | Heliocentric ecliptic J2000 position (AU) |
sun_observe(observer, time) | Topocentric Sun position |
moon_observe(observer, time) | Topocentric Moon position (ELP2000-82B) |
Body IDs follow the VSOP87 convention: 1=Mercury, 2=Venus, 3=Earth, 4=Mars, 5=Jupiter, 6=Saturn, 7=Uranus, 8=Neptune. Body 0 returns the Sun at the heliocentric origin (all zeros).
What pg_orbit does not replace
Section titled “What pg_orbit does not replace”- VSOP87 accuracy is about 1 arcsecond. JPL DE441 (used by Skyfield and SPICE) achieves 0.001 arcsecond. For visual observation planning, 1 arcsecond is more than sufficient. For pointing a dish at GHz frequencies or precision astrometry, use SPICE.
- ELP2000-82B accuracy is about 10 arcseconds for the Moon. Good enough for knowing when the Moon is up, what phase it is in, and whether it will interfere with observations. Not sufficient for occultation timing.
- No light-time iteration. pg_orbit computes geometric positions, not apparent positions. The difference matters at the milliarcsecond level.
- No atmospheric refraction. Objects near the horizon appear slightly higher than their geometric position. pg_orbit does not apply refraction corrections.
Try it
Section titled “Try it”Where is Jupiter right now?
Section titled “Where is Jupiter right now?”The simplest possible observation query:
SELECT topo_azimuth(t) AS azimuth, topo_elevation(t) AS elevation, topo_range(t) / 149597870.7 AS distance_auFROM planet_observe(5, '40.0N 105.3W 1655m'::observer, now()) t;Body ID 5 is Jupiter. The range comes back in km; dividing by 149,597,870.7 converts to AU.
What is up tonight?
Section titled “What is up tonight?”Sweep all eight planets plus the Sun and Moon. Filter to objects above the horizon:
SELECT CASE body_id WHEN 1 THEN 'Mercury' WHEN 2 THEN 'Venus' WHEN 3 THEN 'Earth' 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(obs)::numeric, 1) AS az, round(topo_elevation(obs)::numeric, 1) AS el, round((topo_range(obs) / 149597870.7)::numeric, 3) AS dist_auFROM generate_series(1, 8) AS body_id, LATERAL planet_observe(body_id, '40.0N 105.3W 1655m'::observer, '2024-06-21 04:00:00+00') obsWHERE body_id != 3 -- cannot observe Earth from Earth AND topo_elevation(obs) > 0ORDER BY topo_elevation(obs) DESC;-- Combine planets, Sun, and Moon into one result setWITH observations AS ( -- All planets except Earth SELECT 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 name, planet_observe(body_id, '40.0N 105.3W 1655m'::observer, '2024-06-21 04:00:00+00') AS obs FROM generate_series(1, 8) AS body_id WHERE body_id != 3
UNION ALL
SELECT 'Sun', sun_observe('40.0N 105.3W 1655m'::observer, '2024-06-21 04:00:00+00')
UNION ALL
SELECT 'Moon', moon_observe('40.0N 105.3W 1655m'::observer, '2024-06-21 04:00:00+00'))SELECT name, round(topo_azimuth(obs)::numeric, 1) AS az, round(topo_elevation(obs)::numeric, 1) AS el, round((topo_range(obs) / 149597870.7)::numeric, 4) AS dist_auFROM observationsWHERE topo_elevation(obs) > 0ORDER BY topo_elevation(obs) DESC;Solar system status: heliocentric distances
Section titled “Solar system status: heliocentric distances”See where every planet is relative to the Sun:
SELECT body_id, CASE body_id WHEN 1 THEN 'Mercury' WHEN 2 THEN 'Venus' WHEN 3 THEN 'Earth' WHEN 4 THEN 'Mars' WHEN 5 THEN 'Jupiter' WHEN 6 THEN 'Saturn' WHEN 7 THEN 'Uranus' WHEN 8 THEN 'Neptune' END AS planet, round(helio_distance(planet_heliocentric(body_id, now()))::numeric, 4) AS dist_au, round(helio_x(planet_heliocentric(body_id, now()))::numeric, 4) AS x_au, round(helio_y(planet_heliocentric(body_id, now()))::numeric, 4) AS y_au, round(helio_z(planet_heliocentric(body_id, now()))::numeric, 4) AS z_auFROM generate_series(1, 8) AS body_id;The heliocentric coordinates are in the ecliptic J2000 frame. X points toward the vernal equinox, Z toward the north ecliptic pole.
Planet elevation over one night
Section titled “Planet elevation over one night”Track Jupiter’s elevation from sunset to sunrise:
SELECT t, round(topo_elevation( planet_observe(5, '40.0N 105.3W 1655m'::observer, t) )::numeric, 1) AS jupiter_el, round(topo_azimuth( planet_observe(5, '40.0N 105.3W 1655m'::observer, t) )::numeric, 1) AS jupiter_azFROM generate_series( '2024-06-21 02:00:00+00'::timestamptz, -- ~8pm MDT '2024-06-21 12:00:00+00'::timestamptz, -- ~6am MDT interval '30 minutes') AS tWHERE topo_elevation( planet_observe(5, '40.0N 105.3W 1655m'::observer, t)) > 0;This produces a time series of Jupiter’s position through the night, filtered to only the hours it is above the horizon. Replace body ID 5 with any other planet.
Sun position through the day
Section titled “Sun position through the day”Useful for solar panel analysis, sunrise/sunset approximation, or photography planning:
SELECT t, round(topo_azimuth(sun_observe('40.0N 105.3W 1655m'::observer, t))::numeric, 1) AS az, round(topo_elevation(sun_observe('40.0N 105.3W 1655m'::observer, t))::numeric, 1) AS elFROM generate_series( '2024-06-21 12:00:00+00'::timestamptz, -- ~6am MDT '2024-06-22 03:00:00+00'::timestamptz, -- ~9pm MDT interval '15 minutes') AS t;At the summer solstice from Boulder, the Sun reaches about 73 degrees elevation at local noon, rising in the northeast and setting in the northwest.
Moon range check
Section titled “Moon range check”The Moon’s distance varies between about 356,000 km (perigee) and 407,000 km (apogee):
SELECT t::date AS date, round(topo_range(moon_observe('40.0N 105.3W 1655m'::observer, t))::numeric, 0) AS range_kmFROM generate_series( '2024-01-01'::timestamptz, '2024-02-01'::timestamptz, interval '1 day') AS t;Multi-observer comparison
Section titled “Multi-observer comparison”Compare planet visibility from two different locations:
WITH observers AS ( SELECT 'Boulder, CO' AS location, '40.0N 105.3W 1655m'::observer AS obs UNION ALL SELECT 'Sydney, AU', '33.9S 151.2E 58m'::observer)SELECT o.location, CASE body_id WHEN 5 THEN 'Jupiter' WHEN 6 THEN 'Saturn' END AS planet, round(topo_elevation( planet_observe(body_id, o.obs, '2024-06-21 10:00:00+00') )::numeric, 1) AS elevationFROM observers o, generate_series(5, 6) AS body_idORDER BY o.location, body_id;Earth heliocentric sanity check
Section titled “Earth heliocentric sanity check”Earth’s distance from the Sun should be about 0.983 AU at perihelion (early January) and 1.017 AU at aphelion (early July):
SELECT 'perihelion' AS point, round(helio_distance( planet_heliocentric(3, '2024-01-03 12:00:00+00') )::numeric, 4) AS earth_auUNION ALLSELECT 'aphelion', round(helio_distance( planet_heliocentric(3, '2024-07-05 12:00:00+00') )::numeric, 4);This is a useful sanity check when verifying the extension is installed correctly.
Solar-terrestrial geometry
Section titled “Solar-terrestrial geometry”When does the Sun cross specific elevation thresholds? Find solar noon and the elevation at specific times:
-- Sample the Sun every minute around local noon to find peak elevationSELECT t, round(topo_elevation(sun_observe('40.0N 105.3W 1655m'::observer, t))::numeric, 2) AS elFROM generate_series( '2024-06-21 17:30:00+00'::timestamptz, '2024-06-21 18:30:00+00'::timestamptz, interval '1 minute') AS tORDER BY topo_elevation(sun_observe('40.0N 105.3W 1655m'::observer, t)) DESCLIMIT 5;The highest elevation reading approximates solar noon. For Boulder at the summer solstice, expect about 73 degrees.