Skip to content

Planetary Moons

pg_orbit computes positions for 19 planetary moons across four systems: the four Galilean moons of Jupiter, eight moons of Saturn, five moons of Uranus, and two moons of Mars. Each uses a dedicated analytic theory optimized for that system.

Observing planetary moons usually means one of:

  • Stellarium: Renders moon positions graphically. Good for identifying which moon is which at the eyepiece. Not scriptable.
  • JPL Horizons: Computes precise ephemerides for any solar system body, including all natural satellites. One query per moon, rate-limited web API.
  • Skyfield: Can load JPL satellite ephemeris kernels (BSP files) for high-precision moon positions. Requires downloading and managing kernel files.
  • IMCCE: Provides specialized ephemeris services for natural satellites. Web-based, per-body queries.

The common limitation: getting positions for many moons at many times means many separate requests or script iterations. Comparing moon positions across systems (all of Jupiter’s moons vs. all of Saturn’s) requires stitching results together outside the ephemeris tool.

Four observation functions cover all 19 moons:

FunctionTheoryMoonsAccuracy
galilean_observe(body_id, observer, time)L1.2 (Lieske, 1998)4 (Io, Europa, Ganymede, Callisto)~1 arcsecond
saturn_moon_observe(body_id, observer, time)TASS 1.7 (Vienne & Duriez, 1995)8 (Mimas through Hyperion)~1-5 arcseconds
uranus_moon_observe(body_id, observer, time)GUST86 (Laskar & Jacobson, 1987)5 (Miranda through Oberon)~2-10 arcseconds
mars_moon_observe(body_id, observer, time)MarsSat (Jacobson, 2010)2 (Phobos, Deimos)~1-5 arcseconds

All functions return the same topocentric type. Every moon is identified by a system-specific body ID (integer).

IDMoonOrbital PeriodNotes
0Io1.77 daysVolcanic, drives Jupiter radio bursts
1Europa3.55 daysSubsurface ocean candidate
2Ganymede7.15 daysLargest moon in the solar system
3Callisto16.69 daysMost heavily cratered body
  • Not sub-arcsecond. The analytic theories produce positions accurate to a few arcseconds at best. For astrometric reduction or spacecraft navigation, use JPL ephemerides via SPICE or Skyfield.
  • No mutual events. pg_orbit does not predict eclipses, occultations, or transits between moons. Use IMCCE’s MULTISAT service for mutual event predictions.
  • No libration or physical ephemerides. The functions return topocentric position only — no rotation state, no sub-observer longitude, no apparent disk size.
  • 19 moons, not hundreds. Only the major moons with well-characterized analytic theories are included. Irregular satellites, small inner moons, and ring-embedded moonlets are not covered.
SELECT CASE moon_id
WHEN 0 THEN 'Io' WHEN 1 THEN 'Europa'
WHEN 2 THEN 'Ganymede' WHEN 3 THEN 'Callisto'
END AS moon,
round(topo_azimuth(obs)::numeric, 1) AS az,
round(topo_elevation(obs)::numeric, 1) AS el,
round(topo_range(obs)::numeric, 0) AS range_km
FROM generate_series(0, 3) AS moon_id,
LATERAL galilean_observe(moon_id,
'40.0N 105.3W 1655m'::observer,
'2024-03-15 03:00:00+00') obs;

The range values should cluster near Jupiter’s range (about 4-6 AU or 600-900 million km), since the Galilean moons orbit within 0.013 AU of Jupiter.

Verify that the moons are near their parent planet:

SELECT 'Jupiter' AS body,
round(topo_range(planet_observe(5, '40.0N 105.3W 1655m'::observer,
'2024-03-15 03:00:00+00'))::numeric, -4) AS range_km
UNION ALL
SELECT CASE moon_id
WHEN 0 THEN 'Io' WHEN 1 THEN 'Europa'
WHEN 2 THEN 'Ganymede' WHEN 3 THEN 'Callisto'
END,
round(topo_range(galilean_observe(moon_id,
'40.0N 105.3W 1655m'::observer,
'2024-03-15 03:00:00+00'))::numeric, -4)
FROM generate_series(0, 3) AS moon_id;

The moon ranges should differ from Jupiter’s by at most a few million km. Io orbits closest; Callisto, the farthest Galilean moon, sits about 1.9 million km from Jupiter.

SELECT CASE moon_id
WHEN 0 THEN 'Mimas' WHEN 1 THEN 'Enceladus'
WHEN 2 THEN 'Tethys' WHEN 3 THEN 'Dione'
WHEN 4 THEN 'Rhea' WHEN 5 THEN 'Titan'
WHEN 6 THEN 'Iapetus' WHEN 7 THEN 'Hyperion'
END AS moon,
round(topo_azimuth(obs)::numeric, 1) AS az,
round(topo_elevation(obs)::numeric, 1) AS el,
round(topo_range(obs)::numeric, 0) AS range_km
FROM generate_series(0, 7) AS moon_id,
LATERAL saturn_moon_observe(moon_id,
'40.0N 105.3W 1655m'::observer,
'2024-06-15 03:00:00+00') obs;

The five major Uranian moons are faint targets, but their positions are still useful for planning deep imaging sessions:

SELECT CASE moon_id
WHEN 0 THEN 'Miranda' WHEN 1 THEN 'Ariel'
WHEN 2 THEN 'Umbriel' WHEN 3 THEN 'Titania'
WHEN 4 THEN 'Oberon'
END AS moon,
round(topo_azimuth(obs)::numeric, 1) AS az,
round(topo_elevation(obs)::numeric, 1) AS el,
round(topo_range(obs)::numeric, 0) AS range_km
FROM generate_series(0, 4) AS moon_id,
LATERAL uranus_moon_observe(moon_id,
'40.0N 105.3W 1655m'::observer,
'2024-06-15 03:00:00+00') obs;

Phobos and Deimos are challenging visual targets due to Mars glare, but their positions are computed at every epoch:

SELECT CASE moon_id
WHEN 0 THEN 'Phobos'
WHEN 1 THEN 'Deimos'
END AS moon,
round(topo_azimuth(obs)::numeric, 1) AS az,
round(topo_elevation(obs)::numeric, 1) AS el,
round(topo_range(obs)::numeric, 0) AS range_km
FROM generate_series(0, 1) AS moon_id,
LATERAL mars_moon_observe(moon_id,
'40.0N 105.3W 1655m'::observer,
'2024-01-15 06:00:00+00') obs;

A single query that observes every supported moon in the solar system:

WITH all_moons AS (
-- Galilean moons (Jupiter)
SELECT 'Jupiter' AS parent,
CASE id WHEN 0 THEN 'Io' WHEN 1 THEN 'Europa'
WHEN 2 THEN 'Ganymede' WHEN 3 THEN 'Callisto'
END AS moon,
galilean_observe(id, '40.0N 105.3W 1655m'::observer,
'2024-06-15 03:00:00+00') AS obs
FROM generate_series(0, 3) AS id
UNION ALL
-- Saturn moons
SELECT 'Saturn',
CASE id WHEN 0 THEN 'Mimas' WHEN 1 THEN 'Enceladus'
WHEN 2 THEN 'Tethys' WHEN 3 THEN 'Dione'
WHEN 4 THEN 'Rhea' WHEN 5 THEN 'Titan'
WHEN 6 THEN 'Iapetus' WHEN 7 THEN 'Hyperion'
END,
saturn_moon_observe(id, '40.0N 105.3W 1655m'::observer,
'2024-06-15 03:00:00+00')
FROM generate_series(0, 7) AS id
UNION ALL
-- Uranus moons
SELECT 'Uranus',
CASE id WHEN 0 THEN 'Miranda' WHEN 1 THEN 'Ariel'
WHEN 2 THEN 'Umbriel' WHEN 3 THEN 'Titania'
WHEN 4 THEN 'Oberon'
END,
uranus_moon_observe(id, '40.0N 105.3W 1655m'::observer,
'2024-06-15 03:00:00+00')
FROM generate_series(0, 4) AS id
UNION ALL
-- Mars moons
SELECT 'Mars',
CASE id WHEN 0 THEN 'Phobos' WHEN 1 THEN 'Deimos' END,
mars_moon_observe(id, '40.0N 105.3W 1655m'::observer,
'2024-06-15 03:00:00+00')
FROM generate_series(0, 1) AS id
)
SELECT parent,
moon,
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_au
FROM all_moons
WHERE topo_elevation(obs) > 0
ORDER BY parent, moon;

This returns every visible moon from Boulder at the specified time. 19 moons, 19 function calls, one result set.

Watch Io complete part of its 1.77-day orbit around Jupiter:

SELECT t,
round(topo_range(galilean_observe(0, '40.0N 105.3W 1655m'::observer, t))::numeric, 0) AS io_range_km,
round(topo_range(planet_observe(5, '40.0N 105.3W 1655m'::observer, t))::numeric, 0) AS jupiter_range_km,
round((topo_range(galilean_observe(0, '40.0N 105.3W 1655m'::observer, t))
- topo_range(planet_observe(5, '40.0N 105.3W 1655m'::observer, t)))::numeric, 0) AS separation_km
FROM generate_series(
'2024-03-15 00:00:00+00'::timestamptz,
'2024-03-16 18:00:00+00'::timestamptz,
interval '2 hours'
) AS t;

The separation_km column shows Io oscillating between being closer and farther than Jupiter as seen from Earth — the projection of its orbit along the line of sight.

Find when Titan is above the horizon over a week:

SELECT t::date AS date,
round(topo_elevation(
saturn_moon_observe(5, '40.0N 105.3W 1655m'::observer, t)
)::numeric, 1) AS titan_el
FROM generate_series(
'2024-06-15 00:00:00+00'::timestamptz,
'2024-06-22 00:00:00+00'::timestamptz,
interval '1 hour'
) AS t
WHERE topo_elevation(
saturn_moon_observe(5, '40.0N 105.3W 1655m'::observer, t)
) > 10
ORDER BY t;

Titan (body ID 5 in the Saturn system) is the only moon in the solar system with a thick atmosphere, making it a frequent target for amateur imaging.