← Astronomy 6 / 18

Reference frames

Ephemeris has five ways of placing a body, and all five are the same physics with the observer moved: position() from the centre of the Earth, heliocentric() from the Sun, barycentric() from the centre of mass of the whole solar system, topocentric() from a point on the ground, and planetocentric() from another body entirely. What changes between them is not the arithmetic, it is where light time, aberration and deflection are measured from, and that turns out to matter more than it sounds.

The same body from four origins

use Astronomy\Ephemeris;
use Astronomy\Body;
use Astronomy\Time;

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

$seenFrom = [
    'the Earth'      => Ephemeris::position(Body::Mars, $jdTT),
    'the Sun'        => Ephemeris::heliocentric(Body::Mars, $jdTT),
    'the barycentre' => Ephemeris::barycentric(Body::Mars, $jdTT),
    'Jupiter'        => Ephemeris::planetocentric(Body::Mars, Body::Jupiter, $jdTT),
];

foreach ($seenFrom as $origin => $mars) {
    printf("Mars from %-15s %10.4f deg  %8.4f AU  speed %8.4f deg/day\n",
        $origin, $mars->longitude, $mars->distance, $mars->speed);
}
Mars from the Earth          54.7553 deg    2.1336 AU  speed   0.7211 deg/day
Mars from the Sun            30.4901 deg    1.4317 AU  speed   0.5907 deg/day
Mars from the barycentre     30.3419 deg    1.4274 AU  speed   0.5925 deg/day
Mars from Jupiter           318.3291 deg    5.5329 AU  speed   0.1308 deg/day

Four different longitudes, four different distances and four different speeds for the same planet at the same instant, and the heliocentric and planetocentric columns are not the geocentric one shifted by some fixed offset: light time and aberration are measured from wherever the observer actually stands, and each of those has to be recomputed from scratch rather than derived from the Earth's own answer.

Body::Earth: only in two of the five

position(), topocentric() and planetocentric() all refuse Body::Earth, and for the same reason each time: a geocentric Earth is not zero, it does not exist, because the Earth is the observer in those three frames rather than something being looked at.

try {
    Ephemeris::position(Body::Earth, $jdTT);
} catch (\LogicException $e) {
    echo $e->getMessage(), "\n";
}

echo Ephemeris::heliocentric(Body::Earth, $jdTT)->formatted(), " (the Earth, seen from the Sun)\n";
The Earth has no geocentric position: it is the observer. Ask for it heliocentric or barycentric.
0° 08' 34" Capricorn (the Earth, seen from the Sun)

Body::Earth only exists in heliocentric() and barycentric(), where it stops being the observer and becomes a body like any other, opposite the Sun from wherever it is being looked at from.

Heliocentric and barycentric: the Sun that does move

The Sun has no heliocentric position either, for the mirror reason: it is the origin of that frame.

try {
    Ephemeris::heliocentric(Body::Sun, $jdTT);
} catch (\LogicException $e) {
    echo $e->getMessage(), "\n";
}
The Sun has no heliocentric position: it is the origin of that frame.

But it does have a barycentric one, because the barycentre is the centre of mass of the whole solar system and the Sun does not sit exactly there: Jupiter alone pulls it about a tenth of a solar radius off centre.

$sunBary = Ephemeris::barycentric(Body::Sun, $jdTT);

printf("Sun, barycentric distance from the barycentre: %.6f AU\n", $sunBary->distance);
printf("Sun, barycentric longitude:                    %.4f deg\n", $sunBary->longitude);
Sun, barycentric distance from the barycentre: 0.005681 AU
Sun, barycentric longitude:                    251.0359 deg

Half a hundredth of an astronomical unit here, which is small against a planet's own orbit and not remotely small against the Sun's own radius: it is why a barycentric ephemeris exists at all rather than being the heliocentric one with a fixed shift applied.

Topocentric: parallax, and why the Moon needs it and Pluto does not

topocentric() is the apparent position from a point on the ground rather than from the centre of the Earth, and the gap between the two is parallax, which only matters for something close:

$geocentric = Ephemeris::position(Body::Moon, $jdTT);
$topocentric = Ephemeris::topocentric(Body::Moon, $jdTT, latitude: 40.4165, geographicLongitude: -3.7026);

printf("Moon, geocentric:  %s\n", $geocentric->formatted());
printf("Moon, topocentric: %s\n", $topocentric->formatted());
printf("parallax: %.4f deg (%.1f arcmin)\n",
    abs($geocentric->longitude - $topocentric->longitude),
    abs($geocentric->longitude - $topocentric->longitude) * 60);

$plutoGeo = Ephemeris::position(Body::Pluto, $jdTT);
$plutoTopo = Ephemeris::topocentric(Body::Pluto, $jdTT, latitude: 40.4165, geographicLongitude: -3.7026);

printf("\nPluto, geocentric minus topocentric: %.6f arcsec\n",
    abs($plutoGeo->longitude - $plutoTopo->longitude) * 3600);
Moon, geocentric:  25° 13' 01" Virgo
Moon, topocentric: 26° 07' 20" Virgo
parallax: 0.9053 deg (54.3 arcmin)

Pluto, geocentric minus topocentric: 0.341117 arcsec

Fifty four arcminutes for the Moon on this date, close to the full degree parallax can reach depending on where it sits in the sky; a third of an arcsecond for Pluto, which is the same six thousand kilometres of baseline looked at from thirty five astronomical units away instead of from four hundred thousand kilometres. It is why anything to do with a real horizon, a rising, an eclipse, a transit of the Sun's meridian, goes topocentric for the Moon and would not gain anything from it for the outer planets.

Planetocentric: aberration belongs to whoever is looking

planetocentric() sounds like a curiosity and is the same calculation as the other four with one assumption taken out: that the observer is the Earth. That assumption sits inside both light time, which has to be measured to the new origin and not to the Earth, and aberration, which tilts the incoming light by the observer's velocity, not by the Earth's.

use Astronomy\PositionType;

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

// The Earth seen from Mars: the aberration here is Mars's own orbital speed, about 13 km/s
// where the Earth runs at 30.
$apparent = Ephemeris::planetocentric(Body::Earth, Body::Mars, $jdTT);
$noAberration = Ephemeris::planetocentric(Body::Earth, Body::Mars, $jdTT, PositionType::NoAberration);
$marsAberration = ($apparent->longitude - $noAberration->longitude) * 3600;

// The Sun seen from the Earth, for comparison: the same kind of correction, a different
// observer's speed behind it.
$sunFromEarth = Ephemeris::position(Body::Sun, $jdTT);
$sunFromEarthNoAb = Ephemeris::position(Body::Sun, $jdTT, PositionType::NoAberration);
$earthAberration = ($sunFromEarth->longitude - $sunFromEarthNoAb->longitude) * 3600;

printf("Earth from Mars: aberration from Mars's own speed  = %.2f arcsec\n", $marsAberration);
printf("Sun from Earth:  aberration from Earth's own speed = %.2f arcsec\n", $earthAberration);
Earth from Mars: aberration from Mars's own speed  = -15.75 arcsec
Sun from Earth:  aberration from Earth's own speed = -20.84 arcsec

The two numbers are close enough to be the same order of magnitude and different enough to prove the point: aberration is not a fixed twenty arcseconds attached to a body, it is the observer's own orbital speed doing the tilting, and Mars moves slower than the Earth does. Leaving the Earth's velocity in place by mistake when the observer is actually Mars does not throw, it gives a number with a perfectly plausible shape; the engine's own measurements across fifteen such pairs put the resulting error as high as 29.8 arcseconds, worst in the case that is easiest to get backwards, the Earth seen from Mars.

Deflection carries the same correction the other way: what bends the light is passing close to the Sun on its way to this observer, so all three points of that triangle, the body, the Sun and wherever the observer is standing, have to be the real ones. From most vantage points that adds up to less than a thousandth of an arcsecond, because nothing happens to be right up against the Sun; the moment something is, from any origin, it shows up exactly the way it does geocentrically.

Diurnal aberration: the same tilt, from spinning rather than orbiting

topocentric() carries one more correction the other four frames do not need: the observer on the ground is also moving, at up to 465 metres per second at the equator, purely from the Earth's own rotation. It is the same kind of tilt as annual aberration, smaller by three orders of magnitude, and it is isolated from the parallax it travels alongside by comparing the apparent topocentric position against the one with aberration removed:

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

$max = null;
for ($h = 0; $h < 24; $h++) {
    $jd = $jdTT + $h / 24;
    $apparentTopo = Ephemeris::topocentric(Body::Neptune, $jd, latitude: 0.0, geographicLongitude: 0.0);
    $noAbTopo = Ephemeris::topocentric(Body::Neptune, $jd, latitude: 0.0, geographicLongitude: 0.0, type: PositionType::NoAberration);
    $geoApparent = Ephemeris::position(Body::Neptune, $jd);
    $geoNoAb = Ephemeris::position(Body::Neptune, $jd, PositionType::NoAberration);

    // Subtracting the geocentric (annual only) shift leaves just the diurnal part.
    $diurnalOnly = ($apparentTopo->longitude - $noAbTopo->longitude) * 3600
        - ($geoApparent->longitude - $geoNoAb->longitude) * 3600;

    if ($max === null || abs($diurnalOnly) > abs($max)) { $max = $diurnalOnly; }
}
printf("max diurnal aberration alone over 24h at the equator: %.4f arcsec\n", $max);
max diurnal aberration alone over 24h at the equator: -0.2927 arcsec

Under a third of an arcsecond, scanned over a full day at the one latitude where the rotation speed is largest. It rides on top of the annual correction rather than replacing it, which is why PositionType::NoAberration strips both at once on a topocentric position: the two are the same physics at two different speeds, not two separate switches.

Horizon::equatorialWithSpeed() and eclipticWithSpeed(): a velocity is not a second position

Rotating a position from the ecliptic to the equator, or back, is a single rotation. Rotating a velocity along with it is not the same rotation applied twice: angles are not a vector, and treating a rate of change as if it were itself a point on the sky gives an answer with no relation to the real one, not even in sign.

use Astronomy\Horizon;

[$jdTT] = Time::fromClock(new DateTimeImmutable('1981-05-11 07:15:00', new DateTimeZone('UTC')));
$mars = Ephemeris::position(Body::Mars, $jdTT);

$ecliptic = [$mars->longitude, $mars->latitude, $mars->distance, $mars->speed, $mars->latitudeSpeed, $mars->distanceSpeed];

[$ra, $dec, $dist, $vRa, $vDec, $vDist] = Horizon::equatorialWithSpeed($ecliptic, $jdTT);

printf("right ascension speed: %.6f deg/day\n", $vRa);

$back = Horizon::eclipticWithSpeed([$ra, $dec, $dist, $vRa, $vDec, $vDist], $jdTT);
printf("round trip longitude speed: %.10f (original %.10f)\n", $back[3], $mars->speed);
right ascension speed: 0.723463 deg/day
round trip longitude speed: 0.7370855112 (original 0.7370855112)

The round trip is exact, because equatorialWithSpeed() and eclipticWithSpeed() are the same rotation run forward and backward on the rectangular vector and its derivative, then converted back to angles by the chain rule. The naive alternative, rotating the pair of speeds through the same formula used for a position, only looks reasonable near the ecliptic; near a pole it stops looking reasonable at all:

$eps = Time::trueObliquity(Time::centuries($jdTT));

// A synthetic point near the ecliptic pole, moving fast in longitude: chosen to make the
// failure obvious rather than to represent a real body.
$syntheticEcliptic = [10.0, 89.0, 1.0, 13.2, 0.0, 0.0];
[, , , $correctRaSpeed] = Horizon::equatorialWithSpeed($syntheticEcliptic, $jdTT);

$l = deg2rad($syntheticEcliptic[3]);
$b = deg2rad($syntheticEcliptic[4]);
$x = cos($b) * cos($l);
$y = cos($b) * sin($l);
$z = sin($b);
$ye = $y * cos($eps) - $z * sin($eps);
$naiveRaSpeed = rad2deg(atan2($ye, $x));

printf("correct RA speed: %.4f deg/day\n", $correctRaSpeed);
printf("naive RA speed (velocity rotated as if it were a position): %.4f\n", $naiveRaSpeed);
correct RA speed: -0.0782 deg/day
naive RA speed (velocity rotated as if it were a position): 12.1444

Neither the size nor the sign survives the naive approach at high latitude, which is exactly where the wrong answer looks most like a real one: at everyday, low latitudes the two are close enough that a mistake here is the harder kind to catch, a believable number quietly attached to the wrong sign of the latitude.

Where to go next

Positions is PositionType and ReferenceEcliptic in full, which every frame in this chapter accepts the same way. Time and calendars is where jdUt comes from for the topocentric examples above. Precision has the frame by frame comparison against JPL Horizons that these numbers are checked against.