Skip to content

Lagrange Equilibrium Points

Lagrange points are the five positions in a two-body gravitational system where a third, much smaller body experiences zero net acceleration in the co-rotating frame. Three of them — the collinear points L1, L2, L3 — were identified by Euler in 1767. The remaining two equilateral points L4 and L5 were found by Lagrange in 1772. The physical reality matches the mathematics: SOHO stares at the Sun from Earth-Sun L1, JWST observes from the cold shadow of L2, and several thousand Trojan asteroids share Jupiter’s orbit clustered around L4 and L5.

pg_orrery v0.20.0 adds 37 functions for computing Lagrange point positions across every gravitational system the extension already models: Sun-planet (8 planets, each with 5 L-points), Earth-Moon (5 points), and 19 planetary moons spanning the Galilean, Saturn, Uranus, and Mars families. The solver uses the Circular Restricted Three-Body Problem (CR3BP): Newton-Raphson on the quintic equilibrium polynomial for the collinear points, the classical equilateral geometry for L4/L5, all projected from the co-rotating frame into heliocentric ecliptic J2000 coordinates via the instantaneous orbital geometry.

Every L-point can be queried as a heliocentric position, a topocentric observation, or an equatorial RA/Dec. Distances from asteroids to any L-point let you identify Trojans in bulk. Hill radii define gravitational spheres of influence. The total is 140 equilibrium positions — 40 Sun-planet, 5 Earth-Moon, 95 planetary moon — all accessible with a single function call.

Computing Lagrange point positions requires solving the CR3BP for the specific mass ratio of the system, then projecting from the co-rotating frame into a physical coordinate system:

  • JPL Horizons: Supports specific L-points as targets (e.g., @L2 for Sun-Earth L2). Limited to Sun-planet systems. No planetary moon L-points. Web and email interface, not designed for batch queries.
  • Skyfield (Python): No built-in Lagrange point support. You can manually compute CR3BP positions, but it requires rolling your own quintic solver and coordinate frame rotation.
  • GMAT: Full CR3BP module for mission design — computes libration point orbits, manifold transfers, station-keeping budgets. Essential for trajectory design, but overkill for “where is L2 on the sky tonight?”
  • STK/Astrogator: Commercial. Full three-body dynamics with halo orbit families. Not designed for batch surveys across all planets and moon systems.

For all of these, the workflow is: pick a specific system (usually Sun-Earth), request one L-point at a time, get the result in one coordinate frame. Building a survey across all planets and moon systems requires scripting loops and managing coordinate transforms.

Six function families cover the complete Lagrange point problem:

FamilyFunctionsSystemsUse case
Sun-planetlagrange_heliocentric, lagrange_observe, lagrange_equatorial8 planets x 5 L-pointsWhere are the Sun-planet equilibrium positions?
Earth-Moonlunar_lagrange_observe, lunar_lagrange_equatorial5 L-pointsCislunar equilibrium for Artemis-era planning
Planetary moonsgalilean_lagrange_*, saturn_moon_lagrange_*, uranus_moon_lagrange_*, mars_moon_lagrange_*19 moons x 5 L-pointsEvery moon system pg_orrery tracks
Distancelagrange_distance, lagrange_distance_oeAny Sun-planet L-pointTrojan asteroid identification
Hill spherehill_radius, hill_radius_lunar, lagrange_zone_radiusAll systemsGravitational influence boundaries
Conveniencelagrange_mass_ratio, lagrange_point_nameDiagnosticCR3BP parameters, human-readable labels

Body IDs follow the existing conventions: Sun-planet uses 1=Mercury through 8=Neptune, Galilean moons 0-3 (Io-Callisto), Saturn moons 0-7 (Mimas-Hyperion), Uranus moons 0-4 (Miranda-Oberon), Mars moons 0-1 (Phobos-Deimos). Point IDs are 1-5 for L1-L5.

All IMMUTABLE functions also have DE variants (_de suffix) that use JPL DE440/441 positions when configured. See the DE Ephemeris guide.

  • No station-keeping. Real spacecraft at L1/L2 require periodic maneuvers to maintain their halo or Lissajous orbits. pg_orrery computes the equilibrium point, not the orbit around it.
  • No halo or Lissajous orbits. JWST doesn’t sit at L2 --- it orbits L2 in a halo orbit with a roughly 400,000 km radius. The extension returns the point itself.
  • No manifold transfers. The stable/unstable manifolds of L1/L2 are the backbone of low-energy transfer design. For trajectory optimization, use GMAT or NASA’s MONTE.
  • No four-body effects. The three-body approximation breaks down when multiple large bodies interact (e.g., Sun-Jupiter-Saturn near conjunction). The L-point positions are instantaneous geometric solutions.
  • No libration orbit families. The extension computes the static equilibrium point, not the family of periodic orbits around it (Lyapunov, halo, vertical, butterfly).

For mission design beyond “where is the L-point?”, use GMAT with its CR3BP module or MONTE for multi-body dynamics.

Sun-Earth L2 sits about 1.5 million km anti-sunward of Earth. JWST has been there since January 2022. The L2 heliocentric distance should be slightly beyond Earth’s orbital radius:

-- Sun-Earth L1 and L2 heliocentric distances
SELECT lagrange_point_name(p) AS point,
round(helio_distance(lagrange_heliocentric(3, p, '2000-01-01 12:00:00+00'))::numeric, 2) AS sun_dist_au
FROM generate_series(1, 2) AS p;

L1 is at roughly 0.97 AU (sunward of Earth) and L2 at roughly 0.99 AU (anti-sunward). Both are within about 0.01 AU --- around 1.5 million km --- of Earth’s position.

-- L2 sky position (always near the anti-solar point)
SELECT round(eq_ra(lagrange_equatorial(3, 2, now()))::numeric, 4) AS ra_hours,
round(eq_dec(lagrange_equatorial(3, 2, now()))::numeric, 4) AS dec_deg,
constellation(lagrange_equatorial(3, 2, now())) AS constellation;

Sun-Earth L2 is always approximately 12 hours of RA offset from the Sun. Its constellation changes throughout the year as the Earth orbits.

Map all five Sun-Earth Lagrange points at once:

SELECT lagrange_point_name(p) AS point,
round(helio_distance(lagrange_heliocentric(3, p, now()))::numeric, 4) AS sun_dist_au,
round(eq_ra(lagrange_equatorial(3, p, now()))::numeric, 4) AS ra_hours,
round(eq_dec(lagrange_equatorial(3, p, now()))::numeric, 4) AS dec_deg,
constellation(lagrange_equatorial(3, p, now())) AS constellation
FROM generate_series(1, 5) AS p;

L4 leads Earth by roughly 60 degrees in its orbit; L5 trails by roughly 60 degrees. L3 is on the opposite side of the Sun. L1 and L2 are close to Earth, straddling it along the Sun-Earth line.

The L1 point for each planet lies between the Sun and the planet. Its heliocentric distance scales with the planet’s orbital radius:

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(lagrange_heliocentric(body_id, 1, '2000-01-01 12:00:00+00'))::numeric, 2) AS l1_sun_dist_au
FROM generate_series(1, 8) AS body_id
ORDER BY body_id;

For a reference, verified values at J2000: Mercury 0.46, Venus 0.71, Earth 0.97, Mars 1.38, Jupiter 4.63, Saturn 8.77, Uranus 19.44, Neptune 29.35 AU.

Jupiter’s L4 and L5 host the largest known populations of Trojan asteroids. With lagrange_distance_oe, you can measure how close an asteroid with known orbital elements is to a Lagrange point:

-- (588) Achilles — the first discovered Trojan, near Jupiter L4
SELECT round(lagrange_distance_oe(
5, 4,
oe_from_mpc('00588 14.39 0.15 K249V 41.50128 169.10254 334.19917 13.04512 0.0760428 0.22963720 5.1763803 0 MPO752723 4285 88 1992-2024 0.49 M-v 30h MPCW 0000 (588) Achilles 20240913'),
'2024-06-21 12:00:00+00'
)::numeric, 2) AS dist_to_l4_au;

For a bulk survey, load an MPC catalog into a table and query every asteroid’s distance to Jupiter L4 and L5:

-- Find objects within 1 AU of Jupiter L4 (Trojan candidates)
SELECT name,
round(lagrange_distance_oe(5, 4, oe, '2024-06-21 12:00:00+00')::numeric, 3) AS dist_au
FROM mpc_asteroids
WHERE lagrange_distance_oe(5, 4, oe, '2024-06-21 12:00:00+00') < 1.0
ORDER BY dist_au
LIMIT 20;

The lagrange_distance function works with raw heliocentric positions if you already have them, while lagrange_distance_oe accepts orbital_elements directly and handles the Keplerian propagation internally.

Earth-Moon L1 sits between the Earth and Moon at roughly 326,000 km from Earth. Artemis Gateway is planned for a near-rectilinear halo orbit around the Moon, but Earth-Moon L1 and L2 are natural waypoints for cislunar logistics:

-- Earth-Moon L1 distance and sky position
SELECT round(eq_distance(lunar_lagrange_equatorial(1, now()))::numeric, 0) AS dist_km,
round(eq_ra(lunar_lagrange_equatorial(1, now()))::numeric, 4) AS ra_hours,
round(eq_dec(lunar_lagrange_equatorial(1, now()))::numeric, 4) AS dec_deg;

The distance should fall between 300,000 and 360,000 km, varying with the Moon’s orbital eccentricity. The sky position tracks the Moon’s motion, offset slightly toward Earth.

-- All 5 Earth-Moon L-points from Boulder
SELECT lagrange_point_name(p) AS point,
round(topo_elevation(lunar_lagrange_observe(p, '40.0N 105.3W 1655m'::observer, now()))::numeric, 2) AS el_deg,
round(topo_azimuth(lunar_lagrange_observe(p, '40.0N 105.3W 1655m'::observer, now()))::numeric, 2) AS az_deg
FROM generate_series(1, 5) AS p;

Every moon system pg_orrery tracks has Lagrange points. The Galilean moons of Jupiter are the most accessible:

-- Jupiter-Io L4 and L5 (leading and trailing Io by ~60 degrees)
SELECT lagrange_point_name(p) AS point,
round(eq_ra(galilean_lagrange_equatorial(0, p, now()))::numeric, 4) AS ra_hours,
round(eq_dec(galilean_lagrange_equatorial(0, p, now()))::numeric, 4) AS dec_deg
FROM generate_series(4, 5) AS p;
-- Saturn-Titan L1 from Greenwich
SELECT round(topo_elevation(saturn_moon_lagrange_observe(5, 1, '51.4769N 0.0005W 0m'::observer, now()))::numeric, 2) AS el_deg;

Titan is the most massive Saturn moon (GM ratio 4226.5, compared to millions for the icy moons), so its Lagrange points are the most physically significant in the Saturn system. For context, Saturn’s Tethys actually has co-orbital companions near its L4 and L5 --- Telesto and Calypso.

-- All four Galilean moon families: one L4 each
SELECT 'Io' AS moon, round(eq_ra(galilean_lagrange_equatorial(0, 4, now()))::numeric, 4) AS l4_ra
UNION ALL
SELECT 'Europa', round(eq_ra(galilean_lagrange_equatorial(1, 4, now()))::numeric, 4)
UNION ALL
SELECT 'Ganymede', round(eq_ra(galilean_lagrange_equatorial(2, 4, now()))::numeric, 4)
UNION ALL
SELECT 'Callisto', round(eq_ra(galilean_lagrange_equatorial(3, 4, now()))::numeric, 4);

The Hill radius defines the gravitational sphere of influence for each planet. Inside this radius, the planet’s gravity dominates over the Sun’s:

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(hill_radius(body_id, now())::numeric, 4) AS hill_au,
round((hill_radius(body_id, now()) * 149597870.7)::numeric, 0) AS hill_km
FROM generate_series(1, 8) AS body_id;

Jupiter has the largest Hill sphere at roughly 0.35 AU (about 53 million km). Earth’s is roughly 0.01 AU (about 1.5 million km) --- L1 and L2 sit right at the Hill sphere boundary, which is not a coincidence: the Hill radius and the L1 distance are both derived from the same cubic approximation of the CR3BP.

-- Earth-Moon Hill radius (Moon's gravitational influence)
SELECT round(hill_radius_lunar(now())::numeric, 6) AS lunar_hill_au,
round((hill_radius_lunar(now()) * 149597870.7)::numeric, 0) AS lunar_hill_km;

The Moon’s Hill radius is much smaller --- roughly 60,000 km. Objects within this radius are gravitationally bound to the Moon rather than the Earth.

The lagrange_zone_radius function estimates the approximate extent of stable libration around each L-point. The physics differs by point type: L1/L2 zones scale with the Hill radius, L4/L5 zones scale with the square root of the mass ratio (horseshoe/tadpole orbit widths from Dermott 1981), and L3’s zone is extremely narrow:

SELECT lagrange_point_name(p) AS point,
round(lagrange_zone_radius(5, p, now())::numeric, 4) AS zone_au
FROM generate_series(1, 5) AS p;

Jupiter’s L4/L5 zones are the widest, which explains why they collect so many Trojans. The L3 zone is vanishingly small for all planets.

Verify the solver produces physically consistent results:

-- L-point distance to itself should be exactly zero
SELECT round(lagrange_distance(
5, 4,
lagrange_heliocentric(5, 4, '2000-01-01 12:00:00+00'),
'2000-01-01 12:00:00+00'
)::numeric, 10) AS self_distance;
-- L4 and L5 should be equidistant from the Sun (equilateral triangle)
SELECT abs(
helio_distance(lagrange_heliocentric(5, 4, '2000-01-01 12:00:00+00'))
-
helio_distance(lagrange_heliocentric(5, 5, '2000-01-01 12:00:00+00'))
) < 0.001 AS l4_l5_equidistant;
-- L1 is always closer to the Sun than L2
SELECT helio_distance(lagrange_heliocentric(3, 1, now()))
< helio_distance(lagrange_heliocentric(3, 2, now()))
AS l1_closer_than_l2;