Your procedure is sound! Here is the same calculation, using Skyfield's ability to build a vector of all the hours of the year 2030, and NumPy's ability to find where the maximum and minimum occur in a sequence — and it gives the same odd answer as your script:
import numpy as np
from skyfield.api import load
ts = load.timescale()
t = ts.utc(2030, 1, 1, range(365 * 24))
eph = load('de440.bsp')
sun = eph['sun']
earth = eph['earth']
difference = (earth - sun).at(t).distance().km
i = np.argmax(difference)
j = np.argmin(difference)
print('Aphelion:')
print(t[i].utc_strftime())
print(difference[i], 'km')
print()
print('Perihelion:')
print(t[j].utc_strftime())
print(difference[j], 'km')
The output:
Aphelion:
2030-07-04 13:00:00 UTC
152099543.53658378 km
Perihelion:
2030-12-31 23:00:00 UTC
147101197.3727886 km
So what's going on?
The problem is that the perihelion of 2031 happens to be a much "deeper" perihelion than that of 2030. So, even though 2030-12-31 23:00:00 UTC
is still several days away from the 2031 perihelion, the Earth at that point is already so close to the sun that it "wins" the contest against the 2030 perihelion and gets returned instead.
What you really want for perihelion is "the moment that is closer than the two adjacent moments", so that we rule out returning a point that's at the very end of our range. We want only a point where the Sun distance is less than both of the points around it. Here's one way NumPy can do that:
import numpy as np
from skyfield.api import load
ts = load.timescale()
t = ts.utc(2030, 1, 1, range(365 * 24))
eph = load('de440.bsp')
sun = eph['sun']
earth = eph['earth']
difference = (earth - sun).at(t).distance().km
dprev = difference[:-2]
d = difference[1:-1]
dnext = difference[2:]
smaller_than_previous = (d < dprev)
smaller_than_next = (d < dnext)
both = (smaller_than_previous & smaller_than_next)
i = np.argmax(both) + 1 # the '1' corrects for our trimming the array
print('Perihelion:')
print(t[i].utc_strftime())
print(difference[i], 'km')
The result is the moment of perihelion:
Perihelion:
2030-01-03 10:00:00 UTC
147105837.56789857 km
There might be even more elegant ways to do this in NumPy; a web search for 'numpy local minima' should suggest several.
Skyfield also has maxima and minima search functions, if you want to try them out. Maybe I should add built-in perihelion and aphelion routines someday?
find_minima
andfind_maxima
functions to hunt down apoapses and periapses. It pulls2030-01-03 10:12:26 +0000
and2030-07-04 12:57:38 +0000
, when set to search 2030. but it's also usingde421.bsp
$\endgroup$