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.