DEV Community

Neither-Pangolin-120
Neither-Pangolin-120

Posted on

Retrograde motion is a derivative, and 0 Aries will lie to you

I maintain a small astronomy calculator site, and one of the pages answers a question that sounds like a database lookup: which planets were retrograde on the day you were born?

There's no retrograde column in an ephemeris. Retrograde isn't a stored fact, it's a property of the first derivative of a position. A planet is retrograde when its apparent geocentric ecliptic longitude is decreasing. So you compute longitude at two instants, subtract, check the sign. Easy.

The subtraction is where it gets you. Written the obvious way it invents a Saturn reversal in April 1996, and I want to lead with that, because it generalises to anything you compute on a circular quantity.

The bug

Here's the obvious implementation:

const retrograde = lon(t + step) - lon(t) < 0;
Enter fullscreen mode Exit fullscreen mode

And here are real values out of my ephemeris, Saturn, two days apart:

λ(1996-04-07T00:00:00Z) = 359.95541°
λ(1996-04-09T00:00:00Z) =   0.19549°
Enter fullscreen mode Exit fullscreen mode

Saturn moved forward by 0.24008°, which is its perfectly ordinary direct speed for that decade. The subtraction returns −359.75992°. The detector sees a huge negative velocity, decides Saturn has reversed, and opens a retrograde period. Two days later the next interval comes back positive, so it closes it again. You get a Saturn retrograde lasting exactly one sampling step, on a date when Saturn was doing nothing at all.

What actually happened at 1996-04-07T08:53:37Z is that Saturn crossed 0° Aries, which is the origin of the coordinate system. Longitude wrapped from 359.9 to 0.1 and the raw difference fell off a cliff.

The fix is to stop treating longitude as a number and start treating it as an angle. Signed difference, normalised into (−180, 180]:

/** Normalize any degree value into [0, 360). */
export function normalizeDegrees(deg: number): number {
  const d = ((deg % 360) + 360) % 360;
  // Guard the float edge where a tiny negative rounds up to exactly 360.
  return d === 360 ? 0 : d;
}

/** Signed shortest angular difference a−b, in (−180, 180]. */
export function angularDelta(a: number, b: number): number {
  const d = normalizeDegrees(a - b);
  return d > 180 ? d - 360 : d;
}
Enter fullscreen mode Exit fullscreen mode

angularDelta(0.19549, 359.95541) gives +0.24008. Right sign, right magnitude.

Here's the same 1996 scan with each version of the difference, printing every station the detector finds:

Saturn 1996, naive (b - a):
  { mid: '1996-04-07T00:00', toRetro: true  }   <- invented
  { mid: '1996-04-09T00:00', toRetro: false }   <- invented
  { mid: '1996-07-20T00:00', toRetro: true  }
  { mid: '1996-12-05T00:00', toRetro: false }

Saturn 1996, angularDelta:
  { mid: '1996-07-20T00:00', toRetro: true  }
  { mid: '1996-12-05T00:00', toRetro: false }
Enter fullscreen mode Exit fullscreen mode

Saturn's real 1996 retrograde ran 20 July to 5 December, 138 days. The naive version gets that one right and then bolts a phantom onto the front of the year.

This isn't a corner case you can wave away. Counting forward crossings of 0° Aries from 1 January 1900 to 31 December 2100, for the eight planets that can go retrograde: Mercury 224, Venus 208, Mars 109, Jupiter 22, Saturn 10, Uranus 6, Neptune 2, Pluto 3. That's 584 phantom retrograde periods across the span, every one of them landing on somebody's actual birthday. (Trim the window to an even 200 years and you get 582, because Mercury and Venus each cross about once a year. Worth stating the endpoints if you go reproducing this.) Rare enough to survive your spot checks, common enough to be wrong for thousands of users. My least favourite combination.

The general shape of it: any derivative, difference, interpolation or comparison on a wrapped quantity needs the wrap-aware version. Longitude, heading, phase angle, hue, time of day. If your code has a - b in it and a is an angle, it's probably wrong somewhere.

Getting the longitude right in the first place

Two words in "apparent geocentric ecliptic longitude" are doing real work.

Geocentric, because retrograde motion doesn't exist heliocentrically. No planet ever reverses its orbit. Retrograde is an artifact of the observer's own motion: Earth overtakes an outer planet on the inside lane, or gets overtaken by an inner one, and for a few weeks the target appears to slide backward against the stars. Compute from the Sun's point of view and the effect disappears completely.

Apparent, meaning corrected for light-travel time from the body and for aberration caused by Earth's own velocity. Where the planet appears to be, not where it geometrically is.

I use astronomy-engine (MIT, ^2.1.19). I picked it over Swiss Ephemeris specifically because Swiss Ephemeris is AGPL and I wanted the whole thing to run client-side without licensing entanglement. The entire position layer is four lines:

import { Body, Ecliptic, GeoVector, MakeTime } from "astronomy-engine";

/** Apparent geocentric ecliptic longitude (true equinox of date), deg [0,360). */
export function geocentricEclipticLongitude(body: PlanetBody, date: Date): number {
  const eqj = GeoVector(Body[body], MakeTime(date), true);
  return normalizeDegrees(Ecliptic(eqj).elon);
}
Enter fullscreen mode Exit fullscreen mode

GeoVector(body, time, true) returns apparent geocentric position in the J2000 equatorial frame, and that true flag is what switches on the light-time and aberration corrections. Ecliptic() converts to the true ecliptic and equinox of date, so precession and nutation get applied.

One trap worth naming out loud: the library also exports EclipticLongitude(), whose name reads exactly like the thing you want. It's heliocentric. Call it and you get a planet that is never retrograde, which is a satisfying way to spend an afternoon.

Finding the exact station

The instant a planet's apparent motion changes sign is called a station. Finding it is a root-find on the velocity.

I sample longitude on a grid at a body-appropriate step (0.5 days for Mercury, 1 for Venus and Mars, 2 for Jupiter outward) and attach each interval's mean velocity to that interval's midpoint. A sign flip between consecutive intervals then brackets a station between two midpoints:

const lon = (ms: number): number => geocentricEclipticLongitude(body, new Date(ms));
const vel = (ms: number): number => angularDelta(lon(ms + halfStepMs), lon(ms - halfStepMs));
Enter fullscreen mode Exit fullscreen mode

Then bisect. The velocity function used for refinement is the same centred difference with h = step/2, so it agrees exactly with the grid values at the bracket endpoints and the bracket is guaranteed valid:

while (hiMs - loMs > REFINE_MS) {
  const midMs = (loMs + hiMs) / 2;
  const vMid = vel(midMs);
  if (vLo < 0 !== vMid < 0) {
    hiMs = midMs;
  } else {
    loMs = midMs;
    vLo = vMid;
  }
}
Enter fullscreen mode Exit fullscreen mode

REFINE_MS is 5000, so it converges to within five seconds, which is about 15 bisection steps from a two-day bracket. The contract the tests actually enforce is one minute. The extra headroom is free, so I take it.

Two details that only showed up once I was running this for real. The scan runs 230 days past each end of the requested range, because Pluto's retrograde is about 185 days long and a period straddling a boundary has to be seen whole rather than clipped. And the Sun and Moon return [] immediately, since their apparent geocentric longitude only ever increases and they can never station. That early return kills a whole class of nonsense output.

What the distribution looks like

Once station detection is right you can ask a question I couldn't find a published answer to: how many planets is a person typically born with retrograde?

I scanned every calendar day from 1900 to 2100, 73,414 dates sampled at 12:00 UTC, counting how many of the eight planets sat between a retrograde station and the next direct station. These get recomputed from scratch on every build of the natal retrograde page on my site, astrocalcs.com, so they're never a table I typed in once and stopped checking.

Retrograde planets Share of dates
0 6.8%
1 19.8%
2 31.0%
3 25.7%
4 12.4%
5 or more 4.3%

Mean is 2.3, and 93.2% of dates have at least one. The bit that surprises everyone: a chart with zero retrograde planets (6.8%) is rarer than one with four (12.4%).

Per planet, share of dates on which it's retrograde:

Planet Share Mean retrograde length
Venus 7.2% 42 days
Mars 9.4% 74 days
Mercury 19.2% 22 days
Jupiter 30.2% 121 days
Saturn 36.4% 138 days
Uranus 41.2% 152 days
Neptune 42.9% 158 days
Pluto 44.1% 162 days

Why the gradient exists

That six-fold spread has a clean physical explanation, and it's the part I find genuinely satisfying.

The share is just retrograde arc length over synodic period, the time between successive alignments with Earth. Check it against known synodic periods and the whole table falls out: Venus 42/584 = 7.2%, Mars 74/780 = 9.5%, Jupiter 121/399 = 30.3%, Saturn 138/378 = 36.5%, Pluto 162/367 = 44.2%. Every one lands within a few tenths of the measured column, which is a nice independent check that the detector isn't drifting.

Now take the limit. As a planet's orbital period grows, its synodic period with Earth collapses toward one year, because a body that barely moves is one we lap once per orbit of our own. Uranus is at 370 days, Neptune 367, Pluto 367.

And consider the endpoint of that limit: a fixed star. The only thing moving is Earth, and Earth still moves the star. Parallax displaces a nearby star by an amount that shrinks with distance, but stellar aberration swings every star through an ellipse roughly 20.5 arcseconds across no matter how far away it is, because that one depends on Earth's velocity rather than its position. Either way the star's apparent ecliptic longitude oscillates about its mean once a year, and a sinusoid decreases exactly half the time. A fixed star is retrograde 50% of the year, by an amount far too small for anyone to care about. That's the ceiling, and it's entirely a description of our own motion.

Every real planet falls short of that ceiling by exactly as much as its own prograde motion contributes. Mars moves fast enough to cancel most of the parallactic swing and lands at 9.4%. Pluto crawls, cancels almost nothing, gets to 44.1%. The gradient from Mars outward is a direct readout of how much of what we're seeing is us.

Mercury and Venus sit outside that ordering because they're interior to Earth and retrograde around inferior conjunction rather than opposition. Same ratio still governs them. Mercury's synodic period is only 116 days, so it retrogrades three or four times a year, but each pass is short, which gives 19.2%. Venus takes 584 days between passes with a 42-day arc, and that makes Venus retrograde the rarest natal placement of the eight.

Error bars

Where this is uncertain, and I'd rather say it than have someone find it:

Frame convention. Everything here is apparent position referred to the true equinox of date. Software using astrometric or mean-equinox positions will put stations minutes away from mine. Neither is wrong, they're answering slightly different questions, and any comparison across tools has to fix the convention first.

Precision isn't accuracy. The bisection converges to five seconds of the model's station. astronomy-engine agrees with JPL Horizons to within 0.05° for planets at the anchor instants I test against. Near a station the planet is barely moving, so a small position error turns into a much bigger timing error than it would anywhere else in the orbit. Don't read those five seconds as five seconds of physical truth. There's more on the frame and the validation set on my methodology page.

Sampling resolution. The distribution samples one instant per calendar day. It's a day-resolution proxy for "birth dates", not a time-integrated measure of how much of the century each configuration occupies. The unit suite pins the per-planet shares against an independently computed, time-integrated run over 1950 to 2009 and requires agreement within 0.4 percentage points.

A date isn't an instant. Allow for every time of day and every zone from UTC+14 to UTC−12 and a birth date covers roughly a 50-hour window. If a station falls inside it, the answer genuinely depends on birth time, and the honest output is "can't be determined from the date alone" rather than a confident guess.

None of this touches astrology, which I have no opinion about here. Whether a planet's apparent direction at your birth means anything is a question about a tradition. Whether it was moving backward is a question about a number going down, and that one has an exact answer, as long as you subtract your angles correctly.

Top comments (0)