Rise/Set Prediction
pg_orrery computes rise and set times for the Sun, Moon, and all eight planets. You pass an observer and a starting timestamp, and get back a timestamptz for the next crossing of the horizon. Refracted variants account for atmospheric bending, matching what you actually see. Status diagnostics explain why a body might not cross the horizon at all.
How you do it today
Section titled “How you do it today”Finding when things rise and set involves a few approaches:
- Stellarium: Shows rise/set times in the object info panel. One object at a time, not scriptable or queryable from your database.
- Skyfield: Computes rise/set events by searching for horizon crossings. Well-designed API, but finding events for many bodies across many days means writing nested loops.
- Astropy + astroplan: The
Observerclass computes rise/set/transit times. Handles refraction and horizon altitude. Per-object Python calls; batch queries over a table of targets need iteration. - JPL Horizons: Outputs rise/transit/set tables as part of its ephemeris service. One request per body, rate-limited, results live outside your database.
The common pattern: compute rise/set times externally, then import the results into your scheduling table or observation log. If you want to answer “which planets are visible tonight?” in a single SQL query, you assemble the answer from pieces.
What changes with pg_orrery
Section titled “What changes with pg_orrery”Rise and set times are SQL function calls. The functions search forward from a given timestamp using bisection (0.1-second precision) adapted from the satellite pass prediction algorithm. Three tiers cover different needs:
| Tier | Functions | Threshold | Version |
|---|---|---|---|
| Geometric | sun_next_rise, sun_next_set, moon_next_rise, moon_next_set, planet_next_rise, planet_next_set | 0.0 deg | v0.13.0 |
| Refracted | sun_next_rise_refracted, sun_next_set_refracted, moon_next_rise_refracted, moon_next_set_refracted, planet_next_rise_refracted, planet_next_set_refracted | varies | v0.14.0 |
| Diagnostic | sun_rise_set_status, moon_rise_set_status, planet_rise_set_status | — | v0.15.0 |
All functions are STABLE STRICT PARALLEL SAFE. The search window is 7 days. If the body does not cross the threshold within that window, the function returns NULL.
Refraction thresholds
Section titled “Refraction thresholds”Refracted functions use the same thresholds as the USNO and most almanacs:
| Body | Threshold | Why |
|---|---|---|
| Sun | -0.833 deg | 34 arcmin atmospheric refraction + 16 arcmin solar semidiameter |
| Moon | -0.833 deg | 34 arcmin refraction + ~15.5 arcmin mean lunar semidiameter |
| Planets | -0.569 deg | 34 arcmin refraction only (point sources — even Jupiter at opposition subtends just 0.4 arcmin) |
The difference between geometric and refracted sunrise is typically 2-5 minutes. At extreme latitudes near the solstices, the difference can be much larger because the Sun’s path intersects the horizon at a shallow angle.
What pg_orrery does not replace
Section titled “What pg_orrery does not replace”- No topographic horizon. All thresholds assume a flat, unobstructed horizon at sea level. Mountains, buildings, and terrain features are not considered.
- No civil/nautical/astronomical twilight. The functions compute when the Sun’s center crosses the specified threshold, not when it reaches -6, -12, or -18 degrees. You can approximate these by sampling
topo_elevation(sun_observe(...))at the desired threshold. - No lunar limb correction. Moon rise/set uses a fixed mean semidiameter (15.5 arcmin). The actual semidiameter varies by about 1.5 arcmin between perigee and apogee, introducing up to ~15 seconds of timing error.
- No UT1-UTC correction. Times are in UTC. The difference from UT1 (which governs the actual rotation of the Earth) is at most 0.9 seconds.
Try it
Section titled “Try it”Basic sunrise and sunset
Section titled “Basic sunrise and sunset”The simplest rise/set query. Eagle, Idaho on the 2024 summer solstice:
SELECT sun_next_rise('43.7N 116.4W 800m'::observer, '2024-06-21 12:00:00+00') AS sunrise, sun_next_set('43.7N 116.4W 800m'::observer, '2024-06-21 12:00:00+00') AS sunset;The observer format is lat lon altitude — latitude as degrees with N/S suffix, longitude with E/W suffix, altitude in meters. The start time should be before the expected event. Starting at noon UTC (6am MDT) catches the next sunrise and sunset for a Mountain Time observer.
Geometric vs refracted
Section titled “Geometric vs refracted”Atmospheric refraction lifts the Sun’s apparent position near the horizon. Refracted sunrise is earlier; refracted sunset is later:
SELECT sun_next_rise('43.7N 116.4W 800m'::observer, '2024-06-21 12:00:00+00') AS geometric_rise, sun_next_rise_refracted('43.7N 116.4W 800m'::observer, '2024-06-21 12:00:00+00') AS refracted_rise, sun_next_rise('43.7N 116.4W 800m'::observer, '2024-06-21 12:00:00+00') - sun_next_rise_refracted('43.7N 116.4W 800m'::observer, '2024-06-21 12:00:00+00') AS difference;The difference is typically 2-5 minutes for mid-latitude locations. This is why newspaper sunrise times differ from the geometric horizon crossing.
SELECT moon_next_rise('43.7N 116.4W 800m'::observer, '2024-06-21 12:00:00+00') AS geometric_rise, moon_next_rise_refracted('43.7N 116.4W 800m'::observer, '2024-06-21 12:00:00+00') AS refracted_rise, moon_next_rise('43.7N 116.4W 800m'::observer, '2024-06-21 12:00:00+00') - moon_next_rise_refracted('43.7N 116.4W 800m'::observer, '2024-06-21 12:00:00+00') AS difference;The Moon uses the same -0.833 deg threshold as the Sun because its mean semidiameter is nearly identical.
SELECT planet_next_rise(5, '43.7N 116.4W 800m'::observer, '2024-06-21 12:00:00+00') AS geometric_rise, planet_next_rise_refracted(5, '43.7N 116.4W 800m'::observer, '2024-06-21 12:00:00+00') AS refracted_rise, planet_next_rise(5, '43.7N 116.4W 800m'::observer, '2024-06-21 12:00:00+00') - planet_next_rise_refracted(5, '43.7N 116.4W 800m'::observer, '2024-06-21 12:00:00+00') AS difference;Planets use a shallower threshold (-0.569 deg) because they are point sources — no semidiameter to add.
What is visible tonight?
Section titled “What is visible tonight?”Sweep all planets and check which ones rise before midnight local time:
WITH obs AS (SELECT '43.7N 116.4W 800m'::observer AS o), t0 AS (SELECT '2024-06-21 12:00:00+00'::timestamptz AS t)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 planet, planet_next_rise_refracted(body_id, obs.o, t0.t) AS rises, planet_next_set_refracted(body_id, obs.o, t0.t) AS setsFROM generate_series(1, 8) AS body_id, obs, t0WHERE body_id != 3 -- cannot observe Earth from Earth AND planet_next_rise_refracted(body_id, obs.o, t0.t) IS NOT NULLORDER BY planet_next_rise_refracted(body_id, obs.o, t0.t);Body IDs follow the VSOP87 convention: 1=Mercury through 8=Neptune. Body 3 (Earth) and body 0 (Sun) are invalid for planet_next_rise and will raise an error.
Extreme latitude: midnight sun
Section titled “Extreme latitude: midnight sun”At 70 degrees north during the June solstice, the Sun never sets. Both rise/set functions express this by returning NULL:
SELECT sun_next_rise('70.0N 25.0E 10m'::observer, '2024-06-21 12:00:00+00') AS sunrise, sun_next_set('70.0N 25.0E 10m'::observer, '2024-06-21 12:00:00+00') AS sunset, sun_rise_set_status('70.0N 25.0E 10m'::observer, '2024-06-21 12:00:00+00') AS status;The sunrise column will have a value (the Sun is up and will “rise” again after the brief polar dip near midnight), but sunset returns NULL. The status function returns 'circumpolar', explaining why.
Polar night
Section titled “Polar night”The inverse case. At 70 degrees north during the December solstice, the Sun never rises:
SELECT sun_next_rise('70.0N 25.0E 10m'::observer, '2024-12-21 12:00:00+00') AS sunrise, sun_next_set('70.0N 25.0E 10m'::observer, '2024-12-21 12:00:00+00') AS sunset, sun_rise_set_status('70.0N 25.0E 10m'::observer, '2024-12-21 12:00:00+00') AS status;Here sunrise is NULL, sunset may also be NULL (Sun is already below the horizon), and status returns 'never_rises'.
Observation window planning
Section titled “Observation window planning”How long can you observe Jupiter tonight? Compute rise-to-set duration:
WITH obs AS (SELECT '43.7N 116.4W 800m'::observer AS o), t0 AS (SELECT '2024-06-21 12:00:00+00'::timestamptz AS t), events AS ( SELECT planet_next_rise_refracted(5, obs.o, t0.t) AS jupiter_rise, planet_next_set_refracted(5, obs.o, t0.t) AS jupiter_set FROM obs, t0 )SELECT jupiter_rise, jupiter_set, jupiter_set - jupiter_rise AS observation_windowFROM eventsWHERE jupiter_rise IS NOT NULL AND jupiter_set IS NOT NULL AND jupiter_set > jupiter_rise;The WHERE jupiter_set > jupiter_rise clause handles the case where the body is already above the horizon — the next set comes before the next rise. In that case, you would swap the logic: observe from now until the next set.
Moonrise for the next week
Section titled “Moonrise for the next week”Generate a daily moonrise table using generate_series:
SELECT day::date AS date, moon_next_rise_refracted('43.7N 116.4W 800m'::observer, day) AS moonriseFROM generate_series( '2024-06-21 12:00:00+00'::timestamptz, '2024-06-28 12:00:00+00'::timestamptz, interval '1 day') AS day;The Moon’s orbital period is about 24 hours and 50 minutes, so moonrise drifts later by roughly 50 minutes each day. Some days may show NULL if the Moon does not rise during the search window starting from noon UTC.
All rise/set events for one night
Section titled “All rise/set events for one night”Combine Sun, Moon, and all visible planets into a single timeline:
WITH obs AS (SELECT '43.7N 116.4W 800m'::observer AS o), t0 AS (SELECT '2024-06-21 12:00:00+00'::timestamptz AS t), events AS ( SELECT 'Sun' AS body, 'rise' AS event, sun_next_rise_refracted(obs.o, t0.t) AS time FROM obs, t0 UNION ALL SELECT 'Sun', 'set', sun_next_set_refracted(obs.o, t0.t) FROM obs, t0 UNION ALL SELECT 'Moon', 'rise', moon_next_rise_refracted(obs.o, t0.t) FROM obs, t0 UNION ALL SELECT 'Moon', 'set', moon_next_set_refracted(obs.o, t0.t) FROM obs, t0 UNION ALL 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, 'rise', planet_next_rise_refracted(body_id, obs.o, t0.t) FROM generate_series(1, 8) AS body_id, obs, t0 WHERE body_id != 3 UNION ALL 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, 'set', planet_next_set_refracted(body_id, obs.o, t0.t) FROM generate_series(1, 8) AS body_id, obs, t0 WHERE body_id != 3 )SELECT body, event, timeFROM eventsWHERE time IS NOT NULLORDER BY time;This produces a chronological timeline of every rise and set event for the Sun, Moon, and all seven visible planets from Eagle, Idaho. NULL events (circumpolar or never-rising bodies) are filtered out.
Diagnosing NULL results across all bodies
Section titled “Diagnosing NULL results across all bodies”When planning observations at extreme latitudes, check every body’s status at once:
WITH obs AS (SELECT '70.0N 25.0E 10m'::observer AS o), t0 AS (SELECT '2024-06-21 12:00:00+00'::timestamptz AS t)SELECT 'Sun' AS body, sun_rise_set_status(obs.o, t0.t) AS statusFROM obs, t0
UNION ALL
SELECT 'Moon', moon_rise_set_status(obs.o, t0.t)FROM obs, t0
UNION ALL
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, planet_rise_set_status(body_id, obs.o, t0.t)FROM generate_series(1, 8) AS body_id, obs, t0WHERE body_id != 3ORDER BY body;At 70 degrees north on the summer solstice, the Sun is circumpolar. Some planets may also be circumpolar or never-rising depending on their current declination. The status functions classify each body with a single 24-hour scan (48 samples at 30-minute spacing), so this query is lightweight.