Improved agreement of precision among the 4 supported languages.

Before making these changes, I had the following discrepancies
between the calculations made by the different programming
language implementations of Astronomy Engine:

    C vs C#: 5.55112e-17, worst line number = 6
    C vs JS: 2.78533e-12, worst line number = 196936
    C vs PY: 1.52767e-12, worst line number = 159834

Now the results are:

    Diffing calculations: C vs C#
    ctest(Diff): Maximum numeric difference = 5.55112e-17, worst line number = 5

    Diffing calculations: C vs JS
    ctest(Diff): Maximum numeric difference = 1.02318e-12, worst line number = 133677

    Diffing calculations: C vs PY
    ctest(Diff): Maximum numeric difference = 5.68434e-14, worst line number = 49066

    Diffing calculations: JS vs PY
    ctest(Diff): Maximum numeric difference = 1.02318e-12, worst line number = 133677

Here is how I did this:

1. Use new constants HOUR2RAD, RAD2HOUR that directly convert between radians and sidereal hours.
   This reduces tiny roundoff errors in the conversions.

2. In VSOP longitude calculations, keep clamping the angular sum to
   the range [-2pi, +2pi], to prevent it from accumulating thousands
   of radians. This reduces the accumulated error in the final result
   before it is fed into trig functions.

The remaining discrepancies are largely because of an "azimuth amplification" effect:
When converting equatorial coordinates to horizontal coordinates, an object near
the zenith (or nadir) has an azimuth that is highly sensitive to the input
equatorial coordinates. A tiny change in right ascension (RA) can cause a much
larger change in azimuth.

I tracked down the RA discrepancy, and it is due to a different behavior
of the atan2 function in C and JavaScript. There are cases where the least
significant decimal digit is off by 1, as if due to a difference of opinion
about rounding policy.

My best thought is to go back and have a more nuanced diffcalc that
applies less strict tests for azimuth values than the other calculated values.
It seems like every other computed quantity is less sensitive, because solar
system bodies tend to stay away from "poles" of other angular coordinate
systems: their ecliptic latitudes and equatorial declinations are usually
reasonably close to zero. Therefore, right ascensions and ecliptic longitudes
are usually insensitive to changes in the cartesian coordinates they
are calculated from.
This commit is contained in:
Don Cross
2021-04-18 21:15:17 -04:00
parent 6b01510b33
commit cbcacc4b57
32 changed files with 11067 additions and 10503 deletions

View File

@@ -56,6 +56,8 @@ CALLISTO_RADIUS_KM = 2410.3 #<const> The mean radius of Jupiter's moon Calli
_CalcMoonCount = 0
_RAD2HOUR = 3.819718634205488 # 12/pi = factor to convert radians to sidereal hours
_HOUR2RAD = 0.2617993877991494365 # pi/12 = factor to convert sidereal hours to radians
_DAYS_PER_TROPICAL_YEAR = 365.24217
_PI2 = 2.0 * math.pi
_EPOCH = datetime.datetime(2000, 1, 1, 12)
@@ -1465,7 +1467,7 @@ def _vector2radec(pos, time):
else:
dec = +90.0
else:
ra = math.degrees(math.atan2(pos[1], pos[0])) / 15.0
ra = _RAD2HOUR * math.atan2(pos[1], pos[0])
if ra < 0:
ra += 24
dec = math.degrees(math.atan2(pos[2], math.sqrt(xyproj)))
@@ -3085,11 +3087,16 @@ _vsop = [
],
]
def _VsopFormula(formula, t):
def _VsopFormula(formula, t, clamp_angle):
tpower = 1.0
coord = 0.0
for series in formula:
coord += tpower * sum(A * math.cos(B + C*t) for (A, B, C) in series)
incr = tpower * sum(A * math.cos(B + C*t) for (A, B, C) in series)
if clamp_angle:
# Longitude angles can be hundreds of radians.
# Improve precision by keeping each increment within [-2*pi, +2*pi].
incr = math.fmod(incr, _PI2)
coord += incr
tpower *= t
return coord
@@ -3136,7 +3143,9 @@ def _VsopSphereToRect(lon, lat, rad):
def _CalcVsop(model, time):
t = time.tt / _DAYS_PER_MILLENNIUM
(lon, lat, rad) = [_VsopFormula(formula, t) for formula in model]
lon = _VsopFormula(model[0], t, True)
lat = _VsopFormula(model[1], t, False)
rad = _VsopFormula(model[2], t, False)
eclip = _VsopSphereToRect(lon, lat, rad)
return _VsopRotate(eclip).ToAstroVector(time)
@@ -3149,7 +3158,10 @@ class _body_state_t:
def _CalcVsopPosVel(model, tt):
t = tt / _DAYS_PER_MILLENNIUM
(lon, lat, rad) = [_VsopFormula(formula, t) for formula in model]
lon = _VsopFormula(model[0], t, True)
lat = _VsopFormula(model[1], t, False)
rad = _VsopFormula(model[2], t, False)
(dlon_dt, dlat_dt, drad_dt) = [_VsopDeriv(formula, t) for formula in model]
# Use spherical coords and spherical derivatives to calculate
@@ -3209,7 +3221,7 @@ def _VsopHelioDistance(model, time):
# The caller only wants to know the distance between the planet and the Sun.
# So we only need to calculate the radial component of the spherical coordinates.
# There is no need to translate coordinates.
return _VsopFormula(model[2], time.tt / _DAYS_PER_MILLENNIUM)
return _VsopFormula(model[2], time.tt / _DAYS_PER_MILLENNIUM, False)
def _CalcEarth(time):
return _CalcVsop(_vsop[Body.Earth.value], time)
@@ -4314,7 +4326,7 @@ def Horizon(time, observer, ra, dec, refraction):
latrad = math.radians(observer.latitude)
lonrad = math.radians(observer.longitude)
decrad = math.radians(dec)
rarad = math.radians(ra * 15.0)
rarad = ra * _HOUR2RAD
sinlat = math.sin(latrad)
coslat = math.cos(latrad)
@@ -4407,7 +4419,7 @@ def Horizon(time, observer, ra, dec, refraction):
pr = [(((p[j] - coszd0 * uz[j]) / sinzd0)*sinzd + uz[j]*coszd) for j in range(3)]
proj = math.sqrt(pr[0]*pr[0] + pr[1]*pr[1])
if proj > 0:
hor_ra = math.degrees(math.atan2(pr[1], pr[0])) / 15
hor_ra = _RAD2HOUR * math.atan2(pr[1], pr[0])
if hor_ra < 0:
hor_ra += 24
else: