← Astronomy 11 / 18

Orbits

NodesAndApsides answers what a single position does not: not just where a body is, but the whole path it is tracing. Where that path crosses the ecliptic, where it comes closest to the Sun and where it swings farthest, what shape it has, and whereabouts along it the body sits right now.

It is three questions answered together on purpose. Once you have a body's position and velocity at one instant, the nodes, the apsides, the three anomalies, the mean motion and the three periods are all algebra on that same state. Asking for them separately would mean paying for the same five-point velocity derivative three times over.

use Astronomy\Body;
use Astronomy\NodesAndApsides;
use Astronomy\Time;

[$jdTT] = Time::fromClock(new DateTimeImmutable('2026-09-19', new DateTimeZone('UTC')));

$orbit = NodesAndApsides::of(Body::Pluto, $jdTT);

printf("ascending node   %.3f degrees, at %.3f AU from the Sun\n", $orbit->ascendingNode, $orbit->ascendingNodeDistance);
printf("descending node  %.3f degrees, at %.3f AU\n", $orbit->descendingNode(), $orbit->descendingNodeDistance);
printf("perihelion       %.3f degrees, at %.3f AU\n", $orbit->perihelion, $orbit->perihelionDistance);
printf("aphelion         %.3f degrees, at %.3f AU\n", $orbit->aphelion(), $orbit->aphelionDistance);
printf("e %.5f  i %.3f  sidereal period %.0f days\n", $orbit->eccentricity, $orbit->inclination, $orbit->siderealPeriod);
printf("tropical period %.0f days, synodic period %.0f days\n", $orbit->tropicalPeriod, $orbit->synodicPeriod);
printf("mean anomaly %.3f  true anomaly %.3f  eccentric anomaly %.3f\n", $orbit->meanAnomaly, $orbit->trueAnomaly, $orbit->eccentricAnomaly);
printf("mean longitude %.3f  perihelion longitude (ϖ) %.3f\n", $orbit->meanLongitude, $orbit->perihelionLongitude());
echo Time::toClock($orbit->perihelionPassage)->format('Y-m-d'), "  last perihelion passage\n";
echo Time::toClock($orbit->nextPerihelionPassage())->format('Y-m-d'), "  next one\n";
ascending node   110.701 degrees, at 40.900 AU from the Sun
descending node  290.701 degrees, at 33.656 AU
perihelion       224.730 degrees, at 29.590 AU
aphelion         44.730 degrees, at 49.100 AU
e 0.24794  i 17.173  sidereal period 90142 days
tropical period 89287 days, synodic period 367 days
mean anomaly 54.320  true anomaly 81.373  eccentric anomaly 67.438
mean longitude 278.093  perihelion longitude (ϖ) 223.773
1989-06-23  last perihelion passage
2236-04-11  next one

Longitudes come out in the true ecliptic of date, the same frame Ephemeris::position() leaves the planets in, so a body's node and the body itself read off the same wheel without translating anything.

The Sun sits at one focus, not at the centre

The two nodes are half a turn apart, and it is tempting to assume they are also equidistant from the Sun. They are not, because the orbit is an ellipse and the Sun sits at one of its foci:

printf("ascending node distance   %.2f AU\n", $orbit->ascendingNodeDistance);
printf("descending node distance  %.2f AU\n", $orbit->descendingNodeDistance);
ascending node distance   40.90 AU
descending node distance  33.66 AU

Seven astronomical units of difference on the same body, purely from where the ellipse happens to put its perihelion relative to the line of nodes.

Three anomalies, three ways of telling the same time

The mean, true and eccentric anomalies all measure how far the body has travelled since perihelion, and they only agree at perihelion and at aphelion. The mean one is the clock's: it grows at a constant rate and corresponds to no angle you could point a telescope at. The true one is the real angle, seen from the Sun, so it runs fast near perihelion and slow at aphelion. The eccentric one is the auxiliary angle Kepler's equation is written in, and it exists to join the other two. On Pluto, with eccentricity 0.25, the three sit ten degrees apart; on a near-circular orbit they barely separate.

The order they are worked out in matters: the true anomaly comes first, the eccentric one from that, and the mean one last, so any eccentricity error amplifies towards the mean anomaly rather than staying put.

of(), mean() and barycentric() are three different questions

of() gives the osculating orbit: the ellipse the body would trace if, at this exact instant, every other planet stopped pulling on it. It comes out entirely of the position and velocity of that one instant, so it changes from one day to the next and carries every perturbation folded into it.

mean() gives the averaged orbit, with the periodic perturbations taken back out. It does not come from any state at all, it comes from a table of elements, MeanElements, read from Simon and others (1994) as published in ERFA's plan94.c. In the outer planets the two views drift a long way apart:

use Astronomy\Body;
use Astronomy\NodesAndApsides;
use Astronomy\Time;

[$jdTT] = Time::fromClock(new DateTimeImmutable('2000-01-01 12:00:00', new DateTimeZone('UTC')));

$osculating = NodesAndApsides::of(Body::Neptune, $jdTT);
$mean = NodesAndApsides::mean(Body::Neptune, $jdTT);

printf("osculating perihelion  %.1f degrees\n", $osculating->perihelion);
printf("mean perihelion        %.1f degrees\n", $mean->perihelion);
printf("difference              %.1f degrees\n", abs($osculating->perihelion - $mean->perihelion));
osculating perihelion  37.4 degrees
mean perihelion        48.1 degrees
difference              10.7 degrees

Nearly eleven degrees, a third of a sign, on the same planet at the same instant, depending only on whether the periodic tugs of the other planets are left in or averaged out. Neither number is wrong; they answer different questions.

mean() only exists for the eight planets, because that table is the only source of published mean elements this engine will read without copying a proprietary implementation. Asking it for anything else throws instead of silently falling back to the osculating numbers:

use Astronomy\Body;
use Astronomy\NodesAndApsides;
use Astronomy\Time;

[$jdTT] = Time::fromClock(new DateTimeImmutable('2000-01-01 12:00:00', new DateTimeZone('UTC')));

try {
    NodesAndApsides::mean(Body::Pluto, $jdTT);
} catch (\LogicException $e) {
    echo $e->getMessage(), "\n";
}
There are no published mean elements for Pluto: the table by Simon and others (1994) carries the eight planets and nobody else, not Pluto, not the asteroids, not the fictitious ones. Its osculating orbit is there, in `of()`, and the Moon mean elements in `LunarPoints`.

barycentric() is the osculating orbit again, but measured from the barycentre of the solar system rather than from the centre of the Sun, which is not standing still:

use Astronomy\Body;
use Astronomy\NodesAndApsides;
use Astronomy\Time;

[$jdTT] = Time::fromClock(new DateTimeImmutable('2000-01-01 12:00:00', new DateTimeZone('UTC')));

$heliocentric = NodesAndApsides::of(Body::Jupiter, $jdTT);
$barycentric = NodesAndApsides::barycentric(Body::Jupiter, $jdTT);

printf("around the Sun         a %.4f AU  e %.4f\n", $heliocentric->semiMajorAxis, $heliocentric->eccentricity);
printf("around the barycentre  a %.4f AU  e %.4f\n", $barycentric->semiMajorAxis, $barycentric->eccentricity);
around the Sun         a 5.2043 AU  e 0.0488
around the barycentre  a 5.1889 AU  e 0.0476

Jupiter drags the Sun around their common centre of mass by 743,000 kilometres, more than a solar radius, so an orbit measured from the Sun's own centre carries that wobble inside it. Checked against the "Keplerian GM" JPL Horizons itself prints for CENTER='500@0', the gravitational constant this uses for a barycentric orbit, μ = (M − m)³ / M², agrees to nine significant figures: 2.96263547e-4 computed against 2.96263547e-4 published, for Neptune.

barycentric() matters most for bodies far enough out that they orbit the barycentre rather than the Sun in any meaningful sense: published orbital elements for the most distant known objects are barycentric for exactly that reason, and comparing them against a heliocentric orbit is comparing two different things.

Five bodies that have no orbit to give back

The Sun is the origin of the frame, and the Earth is the degenerate case: the ecliptic is, by definition, the plane of the Earth's own orbit, so its inclination is zero but for a wobble of a few arcseconds and its node wanders tens of degrees in a season, a number that exists and means nothing. The Moon's nodes and apogee are of its orbit around the Earth, not around the Sun, and live in LunarPoints instead. The lunar nodes and Lilith are themselves orbital elements, not bodies, so they have no orbit of their own to take nodes and apsides from. All five throw rather than returning a number that looks plausible and says nothing:

use Astronomy\Body;
use Astronomy\NodesAndApsides;
use Astronomy\Time;

[$jdTT] = Time::fromClock(new DateTimeImmutable('2000-01-01 12:00:00', new DateTimeZone('UTC')));

try {
    NodesAndApsides::of(Body::Earth, $jdTT);
} catch (\LogicException $e) {
    echo $e->getMessage(), "\n";
}
The orbit of the Earth has no nodes on the ecliptic: the ecliptic IS that orbit, so its inclination is zero but for a wobble of a couple of arcseconds, and the line of nodes that comes out of it is noise that looks like data.

The foci nobody plots

OsculatingOrbit::secondFocusDistance() returns the distance to the ellipse's other focus, the empty one: not a curiosity, but a real point on the major axis, 2ae from the Sun in the direction of the aphelion. It matters for two reasons: the sum of the distances to both foci is always 2a, and a body's angular speed as seen from the empty focus is almost constant, which is the other definition an apogee-like point can carry.

printf("second focus at %.3f degrees, %.3f AU away\n", $orbit->aphelion(), $orbit->secondFocusDistance());
second focus at 44.730 degrees, 19.510 AU away

ExtremeDistances: how close, how far, and where today falls between them

NodesAndApsides::distances() answers a different question: not where a body's own orbit takes it, but how close and how far it can ever get from the Earth, and where today's distance sits between those two extremes.

use Astronomy\Body;
use Astronomy\NodesAndApsides;
use Astronomy\Time;

[$jdTT] = Time::fromClock(new DateTimeImmutable('2026-09-19', new DateTimeZone('UTC')));

$mars = NodesAndApsides::distances(Body::Mars, $jdTT);

printf("closest possible   %.3f AU\n", $mars->minimum);
printf("farthest possible  %.3f AU\n", $mars->maximum);
printf("right now           %.3f AU\n", $mars->current);
printf("fraction of the way from closest to farthest: %.2f\n", $mars->fraction());
closest possible   0.373 AU
farthest possible  2.676 AU
right now           1.744 AU
fraction of the way from closest to farthest: 0.60

maximum and minimum are the true extrema between the two ellipses, Mars's and the Earth's, searched with both inclinations in place, and not the aphelion of one plus the aphelion of the other. Two aphelia only add up if they happen to fall in opposite directions, and the lines of apsides are wherever they are. On Jupiter in the year 2000 the shortcut gives 6.4748 astronomical units and the real search gives 6.4574: seventeen thousandths of an astronomical unit of difference from assuming the two farthest points line up when they do not.

fraction() is what turns the three numbers into a sentence: 0 is as close as the two orbits can ever bring the bodies, 1 is as far, and it says nothing about how often either extreme actually happens. Mars's closest possible approach to the Earth needs a near-perfect opposition at its own perihelion, which occurs roughly every fifteen to seventeen years and not every time.

Precision, against JPL Horizons

Against the osculating elements Horizons itself publishes (EPHEM_TYPE='ELEMENTS', centre 500@10, ecliptic of J2000), fourteen bodies compared in 1700, 2000 and 2300:

worst difference
Node, tabulated bodies (Pluto, Chiron, Pholus, the four asteroids) 0.06″
Node, planets from the analytic series 5.8″ (Uranus, 2300)
Apsides, tabulated bodies 0.60″
Apsides, Mercury through Uranus 8.4″
Apsides, Neptune 64.7″ (2300)
Perihelion latitude 0.65″
Inclination 0.09″
Eccentricity 6.6e-6
Semi-major axis 1.2e-4 AU
Perihelion and aphelion distance 2.4e-4 AU
Three anomalies and perihelion passage 0.018° (Neptune, 2300)
Mean motion and sidereal period 5.9e-6 relative

What is read in degrees there is not the raw error, it is the error divided by the eccentricity: the eccentricity vector, where an apside comes from, stays below 6.7e-6 astronomical units across all forty-two cases, but Neptune's own eccentricity is only 0.0085, so that same vector error turns into 64.7 arcseconds of apside once it is divided down to an angle. An almost-round orbit has an ill-defined apside for the same reason Lilith's apogee is ill-defined on a near-circular lunar orbit: the geometry itself, not the computation, is what is uncertain. The nodes never have this problem, because they come from the angular momentum, which is not divided by anything.

For barycentric(), checked against Horizons's own CENTER='500@0' elements across Jupiter, Saturn, Neptune and Pluto: node within 3.5″, eccentricity vector within 6.6e-6, inclination within 0.09″, semi-major axis within 1.2e-4 AU.

See Precision for how the rest of the engine is checked, and Crossings and retrogrades for what is built on top of a single longitude rather than a whole orbit.