Files
astronomy/generate/test.py
Don Cross cec908e52c Fixed #137 - more carefully scale SSB errors.
The reason SSB vector errors were larger than other bodies
is because the Sun/Barycenter relationship does not have
position and velocity vectors of a "typical" size.
The distance of a planet from the SSB is fairly constant,
and the speed a planet travels is fairly constant.
Therefore, comparing errors by dividing by the magnitude
of the correct vector usually makes sense for scaling.
But for the barycentric Sun (or the heliocentric SSB),
the magnitude of the vectors can become arbitrarily small,
nearly zero in fact, resulting in surprisingly large ratios.

I compensated for this in all the tests by adding a new rule.
When the error thresholds r_thresh and v_thresh are negative,
it is a flag that indicates they are absolute, not relative.
In other words, r_thresh < 0 indicates that abs(r_thresh) is
the maximum number of astronomical units allowed in position
errors, and v_thresh < 0 specifies maximum AU/day.

This results in more consistent numbers that give confidence
the errors are indeed very small and not worth worrying about.
2021-11-20 21:51:08 -05:00

2296 lines
103 KiB
Python
Executable File

#!/usr/bin/env python3
import sys
import math
import re
import os
sys.path.append('../source/python')
import astronomy
#-----------------------------------------------------------------------------------------------------------
Verbose = False
def Debug(text):
if Verbose:
print(text)
def v(x):
# Verify that a number is really numeric
if not isinstance(x, (int, float)):
raise Exception('Not a numeric type: {}'.format(x))
if not math.isfinite(x):
raise Exception('Not a finite numeric value: {}'.format(x))
return x
def vabs(x):
return abs(v(x))
def vmax(a, b):
return max(v(a), v(b))
def vmin(a, b):
return min(v(a), v(b))
def sqrt(x):
return v(math.sqrt(v(x)))
def AssertGoodTime(text, correct):
time = astronomy.Time.Parse(text)
check = str(time)
if check != correct:
print('Python AssertGoodTime FAILURE: parsed "{}", got "{}", expected "{}"'.format(text, check, correct))
sys.exit(1)
Debug('PY AssertGoodTime: "{}" OK'.format(text))
def AssertBadTime(text):
try:
astronomy.Time.Parse(text)
except astronomy.DateTimeFormatError:
Debug('PY AssertBadTime: "{}" OK'.format(text))
else:
print('PY AssertBadTime FAILURE: should not have parsed "{}"'.format(text))
sys.exit(1)
def AstroTime():
expected_ut = 6910.270978506945
expected_tt = 6910.271800214368
time = astronomy.Time.Make(2018, 12, 2, 18, 30, 12.543)
diff = time.ut - expected_ut
if vabs(diff) > 1.0e-12:
print('PY AstroTime: excessive UT error {}'.format(diff))
sys.exit(1)
diff = time.tt - expected_tt
if vabs(diff) > 1.0e-12:
print('PY AstroTime: excessive TT error {}'.format(diff))
sys.exit(1)
s = str(time.Utc())
if s != '2018-12-02 18:30:12.543000':
print('PY AstroTime: Utc() returned incorrect string "{}"'.format(s))
sys.exit(1)
time = astronomy.Time.Make(2018, 12, 31, 23, 59, 59.9994)
s = str(time)
if s != '2018-12-31T23:59:59.999Z':
print('PY AstroTime: expected 2018-12-31T23:59:59.999Z but found {}'.format(s))
sys.exit(1)
time = astronomy.Time.Make(2018, 12, 31, 23, 59, 59.9995)
s = str(time)
if s != '2019-01-01T00:00:00.000Z':
print('PY AstroTime: expected 2019-01-01T00:00:00.000Z but found {}'.format(s))
sys.exit(1)
print('PY Current time =', astronomy.Time.Now())
AssertGoodTime('2015-12-31', '2015-12-31T00:00:00.000Z')
AssertGoodTime('2015-12-31T23:45Z', '2015-12-31T23:45:00.000Z')
AssertGoodTime('2015-01-02T23:45:17Z', '2015-01-02T23:45:17.000Z')
AssertGoodTime('1971-03-17T03:30:55.976Z', '1971-03-17T03:30:55.976Z')
AssertBadTime('')
AssertBadTime('1971-13-01')
AssertBadTime('1971-12-32')
AssertBadTime('1971-12-31T24:00:00Z')
AssertBadTime('1971-12-31T23:60:00Z')
AssertBadTime('1971-12-31T23:00:60Z')
AssertBadTime('1971-03-17T03:30:55.976')
return 0
#-----------------------------------------------------------------------------------------------------------
def GeoMoon():
time = astronomy.Time.Make(2019, 6, 24, 15, 45, 37)
vec = astronomy.GeoMoon(time)
print('PY GeoMoon: vec = {:0.16f}, {:0.16f}, {:0.16f}'.format(vec.x, vec.y, vec.z))
# Correct values obtained from C version of GeoMoon calculation
cx, cy, cz = +0.002674037026701135, -0.0001531610316600666, -0.0003150159927069429
dx, dy, dz = vec.x - cx, vec.y - cy, vec.z - cz
diff = sqrt(dx*dx + dy*dy + dz*dz)
print('PY GeoMoon: diff = {}'.format(diff))
if diff > 4.34e-19:
print('PY GeoMoon: EXCESSIVE ERROR')
return 1
return 0
#-----------------------------------------------------------------------------------------------------------
def AstroCheck(printflag):
time = astronomy.Time.Make(1700, 1, 1, 0, 0, 0)
stop = astronomy.Time.Make(2200, 1, 1, 0, 0, 0)
observer = astronomy.Observer(29, -81, 10)
if printflag:
print('o {:0.6f} {:0.6f} {:0.6f}'.format(observer.latitude, observer.longitude, observer.height))
dt = 10 + math.pi/100
bodylist = [
astronomy.Body.Sun, astronomy.Body.Moon, astronomy.Body.Mercury, astronomy.Body.Venus,
astronomy.Body.Earth, astronomy.Body.Mars, astronomy.Body.Jupiter, astronomy.Body.Saturn,
astronomy.Body.Uranus, astronomy.Body.Neptune, astronomy.Body.Pluto,
astronomy.Body.SSB, astronomy.Body.EMB
]
while time.tt < stop.tt:
for body in bodylist:
name = body.name
if body != astronomy.Body.Moon:
pos = astronomy.HelioVector(body, time)
if printflag:
print('v {} {:0.18e} {:0.18e} {:0.18e} {:0.18e}'.format(name, pos.t.tt, pos.x, pos.y, pos.z))
if body != astronomy.Body.Earth and body != astronomy.Body.EMB and body != astronomy.Body.SSB:
j2000 = astronomy.Equator(body, time, observer, False, False)
ofdate = astronomy.Equator(body, time, observer, True, True)
hor = astronomy.Horizon(time, observer, ofdate.ra, ofdate.dec, astronomy.Refraction.Airless)
if printflag:
print('s {} {:0.18e} {:0.18e} {:0.18e} {:0.18e} {:0.18e} {:0.18e} {:0.18e}'.format(name, time.tt, time.ut, j2000.ra, j2000.dec, j2000.dist, hor.azimuth, hor.altitude))
pos = astronomy.GeoMoon(time)
if printflag:
print('v GM {:0.18e} {:0.18e} {:0.18e} {:0.18e}'.format(pos.t.tt, pos.x, pos.y, pos.z))
j2000 = astronomy.Equator(astronomy.Body.Moon, time, observer, False, False)
ofdate = astronomy.Equator(astronomy.Body.Moon, time, observer, True, True)
hor = astronomy.Horizon(time, observer, ofdate.ra, ofdate.dec, astronomy.Refraction.Airless)
if printflag:
print('s GM {:0.18e} {:0.18e} {:0.18e} {:0.18e} {:0.18e} {:0.18e} {:0.18e}'.format(time.tt, time.ut, j2000.ra, j2000.dec, j2000.dist, hor.azimuth, hor.altitude))
jm = astronomy.JupiterMoons(time)
if printflag:
for mindex in range(4):
moon = jm.moon[mindex]
print('j {:d} {:0.18e} {:0.18e} {:0.18e} {:0.18e} {:0.18e} {:0.18e} {:0.18e} {:0.18e}'.format(mindex, time.tt, time.ut, moon.x, moon.y, moon.z, moon.vx, moon.vy, moon.vz))
time = time.AddDays(dt)
return 0
#-----------------------------------------------------------------------------------------------------------
def Seasons(filename = 'seasons/seasons.txt'):
with open(filename, 'rt') as infile:
lnum = 0
current_year = 0
mar_count = sep_count = jun_count = dec_count = 0
max_minutes = 0.0
for line in infile:
lnum += 1
line = line.strip()
m = re.match(r'^(\d+)-(\d+)-(\d+)T(\d+):(\d+)Z\s+([A-Za-z]+)$', line)
if not m:
print('PY Seasons: Invalid data on line {} of file {}'.format(lnum, filename))
return 1
year = int(m.group(1))
month = int(m.group(2))
day = int(m.group(3))
hour = int(m.group(4))
minute = int(m.group(5))
name = m.group(6)
if year != current_year:
current_year = current_year
seasons = astronomy.Seasons(year)
correct_time = astronomy.Time.Make(year, month, day, hour, minute, 0)
if name == 'Equinox':
if month == 3:
calc_time = seasons.mar_equinox
mar_count += 1
elif month == 9:
calc_time = seasons.sep_equinox
sep_count += 1
else:
print('PY Seasons: {} line {}: Invalid equinox date in test data'.format(filename, lnum))
return 1
elif name == 'Solstice':
if month == 6:
calc_time = seasons.jun_solstice
jun_count += 1
elif month == 12:
calc_time = seasons.dec_solstice
dec_count += 1
else:
print('PY Seasons: {} line {}: Invalid solstice date in test data'.format(filename, lnum))
return 1
elif name == 'Aphelion':
continue # not yet calculated
elif name == 'Perihelion':
continue # not yet calculated
else:
print('PY Seasons: {} line {}: unknown event type {}'.format(filename, lnum, name))
return 1
# Verify that the calculated time matches the correct time for this event.
diff_minutes = (24.0 * 60.0) * vabs(calc_time.tt - correct_time.tt)
if diff_minutes > max_minutes:
max_minutes = diff_minutes
if diff_minutes > 2.37:
print('PY Seasons: {} line {}: excessive error ({}): {} minutes.'.format(filename, lnum, name, diff_minutes))
return 1
print('PY Seasons: verified {} lines from file {} : max error minutes = {:0.3f}'.format(lnum, filename, max_minutes))
print('PY Seasons: Event counts: mar={}, jun={}, sep={}, dec={}'.format(mar_count, jun_count, sep_count, dec_count))
return 0
#-----------------------------------------------------------------------------------------------------------
def MoonPhase(filename = 'moonphase/moonphases.txt'):
threshold_seconds = 120.0 # max tolerable prediction error in seconds
max_arcmin = 0.0
maxdiff = 0.0
quarter_count = 0
with open(filename, 'rt') as infile:
lnum = 0
prev_year = 0
for line in infile:
lnum += 1
line = line.strip()
m = re.match(r'^([0-3]) (\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+\.\d+)Z$', line)
if not m:
print('PY MoonPhase: invalid data format in {} line {}'.format(filename, lnum))
return 1
quarter = int(m.group(1))
year = int(m.group(2))
month = int(m.group(3))
day = int(m.group(4))
hour = int(m.group(5))
minute = int(m.group(6))
second = float(m.group(7))
expected_elong = 90.0 * quarter
expected_time = astronomy.Time.Make(year, month, day, hour, minute, second)
angle = astronomy.MoonPhase(expected_time)
degree_error = vabs(angle - expected_elong)
if degree_error > 180.0:
degree_error = 360.0 - degree_error
arcmin = 60.0 * degree_error
if arcmin > 1.0:
print('PY MoonPhase({} line {}): EXCESSIVE ANGULAR ERROR: {} arcmin'.format(filename, lnum, arcmin))
return 1
max_arcmin = vmax(max_arcmin, arcmin)
if year != prev_year:
prev_year = year
# The test data contains a single year's worth of data for every 10 years.
# Every time we see the year value change, it breaks continuity of the phases.
# Start the search over again.
start_time = astronomy.Time.Make(year, 1, 1, 0, 0, 0.0)
mq = astronomy.SearchMoonQuarter(start_time)
else:
# Yet another lunar quarter in the same year.
expected_quarter = (1 + mq.quarter) % 4
mq = astronomy.NextMoonQuarter(mq)
# Expect the next consecutive quarter.
if expected_quarter != mq.quarter:
print('PY MoonPhase({} line {}): SearchMoonQuarter returned quarter {}, but expected {}.'.format(filename, lnum, mq.quarter, expected_quarter))
return 1
quarter_count += 1
# Make sure the time matches what we expect.
diff_seconds = vabs(mq.time.tt - expected_time.tt) * (24.0 * 3600.0)
if diff_seconds > threshold_seconds:
print('PY MoonPhase({} line {}): excessive time error {:0.3f} seconds.'.format(filename, lnum, diff_seconds))
return 1
maxdiff = vmax(maxdiff, diff_seconds)
print('PY MoonPhase: passed {} lines for file {} : max_arcmin = {:0.6f}, maxdiff = {:0.3f} seconds, {} quarters.'
.format(lnum, filename, max_arcmin, maxdiff, quarter_count))
return 0
#-----------------------------------------------------------------------------------------------------------
def TestElongFile(filename, targetRelLon):
with open(filename, 'rt') as infile:
lnum = 0
for line in infile:
lnum += 1
line = line.strip()
m = re.match(r'^(\d+)-(\d+)-(\d+)T(\d+):(\d+)Z ([A-Za-z]+)$', line)
if not m:
print('PY TestElongFile({} line {}): invalid data format'.format(filename, lnum))
return 1
year = int(m.group(1))
month = int(m.group(2))
day = int(m.group(3))
hour = int(m.group(4))
minute = int(m.group(5))
name = m.group(6)
body = astronomy.BodyCode(name)
if body.value == astronomy.Body.Invalid:
print('PY TestElongFile({} line {}): invalid body name "{}"'.format(filename, lnum, name))
return 1
search_time = astronomy.Time.Make(year, 1, 1, 0, 0, 0)
expected_time = astronomy.Time.Make(year, month, day, hour, minute, 0)
found_time = astronomy.SearchRelativeLongitude(body, targetRelLon, search_time)
if found_time is None:
print('PY TestElongFile({} line {}): SearchRelativeLongitude failed.'.format(filename, lnum))
return 1
diff_minutes = (24.0 * 60.0) * (found_time.tt - expected_time.tt)
Debug('PY TestElongFile: {:<7s} error = {:6.3} minutes'.format(name, diff_minutes))
if vabs(diff_minutes) > 15.0:
print('PY TestElongFile({} line {}): EXCESSIVE ERROR.'.format(filename, lnum))
return 1
print('PY TestElongFile: passed {} rows of data'.format(lnum))
return 0
def TestPlanetLongitudes(body, outFileName, zeroLonEventName):
startYear = 1700
stopYear = 2200
rlon = 0.0
sum_diff = 0.0
count = 0
name = body.name
with open(outFileName, 'wt') as outfile:
time = astronomy.Time.Make(startYear, 1, 1, 0, 0, 0)
stopTime = astronomy.Time.Make(stopYear, 1, 1, 0, 0, 0)
while time.tt < stopTime.tt:
count += 1
event = zeroLonEventName if rlon == 0.0 else 'sup'
found_time = astronomy.SearchRelativeLongitude(body, rlon, time)
if found_time is None:
print('PY TestPlanetLongitudes({}): SearchRelativeLongitudes failed'.format(name))
return 1
if count >= 2:
# Check for consistent intervals.
# Mainly I don't want to skip over an event!
day_diff = found_time.tt - time.tt
sum_diff += day_diff
if count == 2:
min_diff = max_diff = day_diff
else:
min_diff = vmin(min_diff, day_diff)
max_diff = vmax(max_diff, day_diff)
geo = astronomy.GeoVector(body, found_time, True)
dist = geo.Length()
outfile.write('e {} {} {:0.16f} {:0.16f}\n'.format(name, event, found_time.tt, dist))
# Search for the opposite longitude vent next time.
time = found_time
rlon = 180.0 - rlon
if body == astronomy.Body.Mercury:
thresh = 1.65
elif body == astronomy.Body.Mars:
thresh = 1.30
else:
thresh = 1.07
ratio = max_diff / min_diff
Debug('PY TestPlanetLongitudes({:<7s}): {:5d} events, ratio={:5.3f}, file: {}'.format(name, count, ratio, outFileName))
if ratio > thresh:
print('PY TestPlanetLongitudes({}): EXCESSIVE EVENT INTERVAL RATIO'.format(name))
return 1
return 0
ElongTestData = [
# Max elongation data obtained from:
# http://www.skycaramba.com/greatest_elongations.shtml
( astronomy.Body.Mercury, "2010-01-17T05:22Z", "2010-01-27T05:22Z", 24.80, 'morning' ),
( astronomy.Body.Mercury, "2010-05-16T02:15Z", "2010-05-26T02:15Z", 25.10, 'morning' ),
( astronomy.Body.Mercury, "2010-09-09T17:24Z", "2010-09-19T17:24Z", 17.90, 'morning' ),
( astronomy.Body.Mercury, "2010-12-30T14:33Z", "2011-01-09T14:33Z", 23.30, 'morning' ),
( astronomy.Body.Mercury, "2011-04-27T19:03Z", "2011-05-07T19:03Z", 26.60, 'morning' ),
( astronomy.Body.Mercury, "2011-08-24T05:52Z", "2011-09-03T05:52Z", 18.10, 'morning' ),
( astronomy.Body.Mercury, "2011-12-13T02:56Z", "2011-12-23T02:56Z", 21.80, 'morning' ),
( astronomy.Body.Mercury, "2012-04-08T17:22Z", "2012-04-18T17:22Z", 27.50, 'morning' ),
( astronomy.Body.Mercury, "2012-08-06T12:04Z", "2012-08-16T12:04Z", 18.70, 'morning' ),
( astronomy.Body.Mercury, "2012-11-24T22:55Z", "2012-12-04T22:55Z", 20.60, 'morning' ),
( astronomy.Body.Mercury, "2013-03-21T22:02Z", "2013-03-31T22:02Z", 27.80, 'morning' ),
( astronomy.Body.Mercury, "2013-07-20T08:51Z", "2013-07-30T08:51Z", 19.60, 'morning' ),
( astronomy.Body.Mercury, "2013-11-08T02:28Z", "2013-11-18T02:28Z", 19.50, 'morning' ),
( astronomy.Body.Mercury, "2014-03-04T06:38Z", "2014-03-14T06:38Z", 27.60, 'morning' ),
( astronomy.Body.Mercury, "2014-07-02T18:22Z", "2014-07-12T18:22Z", 20.90, 'morning' ),
( astronomy.Body.Mercury, "2014-10-22T12:36Z", "2014-11-01T12:36Z", 18.70, 'morning' ),
( astronomy.Body.Mercury, "2015-02-14T16:20Z", "2015-02-24T16:20Z", 26.70, 'morning' ),
( astronomy.Body.Mercury, "2015-06-14T17:10Z", "2015-06-24T17:10Z", 22.50, 'morning' ),
( astronomy.Body.Mercury, "2015-10-06T03:20Z", "2015-10-16T03:20Z", 18.10, 'morning' ),
( astronomy.Body.Mercury, "2016-01-28T01:22Z", "2016-02-07T01:22Z", 25.60, 'morning' ),
( astronomy.Body.Mercury, "2016-05-26T08:45Z", "2016-06-05T08:45Z", 24.20, 'morning' ),
( astronomy.Body.Mercury, "2016-09-18T19:27Z", "2016-09-28T19:27Z", 17.90, 'morning' ),
( astronomy.Body.Mercury, "2017-01-09T09:42Z", "2017-01-19T09:42Z", 24.10, 'morning' ),
( astronomy.Body.Mercury, "2017-05-07T23:19Z", "2017-05-17T23:19Z", 25.80, 'morning' ),
( astronomy.Body.Mercury, "2017-09-02T10:14Z", "2017-09-12T10:14Z", 17.90, 'morning' ),
( astronomy.Body.Mercury, "2017-12-22T19:48Z", "2018-01-01T19:48Z", 22.70, 'morning' ),
( astronomy.Body.Mercury, "2018-04-19T18:17Z", "2018-04-29T18:17Z", 27.00, 'morning' ),
( astronomy.Body.Mercury, "2018-08-16T20:35Z", "2018-08-26T20:35Z", 18.30, 'morning' ),
( astronomy.Body.Mercury, "2018-12-05T11:34Z", "2018-12-15T11:34Z", 21.30, 'morning' ),
( astronomy.Body.Mercury, "2019-04-01T19:40Z", "2019-04-11T19:40Z", 27.70, 'morning' ),
( astronomy.Body.Mercury, "2019-07-30T23:08Z", "2019-08-09T23:08Z", 19.00, 'morning' ),
( astronomy.Body.Mercury, "2019-11-18T10:31Z", "2019-11-28T10:31Z", 20.10, 'morning' ),
( astronomy.Body.Mercury, "2010-03-29T23:32Z", "2010-04-08T23:32Z", 19.40, 'evening' ),
( astronomy.Body.Mercury, "2010-07-28T01:03Z", "2010-08-07T01:03Z", 27.40, 'evening' ),
( astronomy.Body.Mercury, "2010-11-21T15:42Z", "2010-12-01T15:42Z", 21.50, 'evening' ),
( astronomy.Body.Mercury, "2011-03-13T01:07Z", "2011-03-23T01:07Z", 18.60, 'evening' ),
( astronomy.Body.Mercury, "2011-07-10T04:56Z", "2011-07-20T04:56Z", 26.80, 'evening' ),
( astronomy.Body.Mercury, "2011-11-04T08:40Z", "2011-11-14T08:40Z", 22.70, 'evening' ),
( astronomy.Body.Mercury, "2012-02-24T09:39Z", "2012-03-05T09:39Z", 18.20, 'evening' ),
( astronomy.Body.Mercury, "2012-06-21T02:00Z", "2012-07-01T02:00Z", 25.70, 'evening' ),
( astronomy.Body.Mercury, "2012-10-16T21:59Z", "2012-10-26T21:59Z", 24.10, 'evening' ),
( astronomy.Body.Mercury, "2013-02-06T21:24Z", "2013-02-16T21:24Z", 18.10, 'evening' ),
( astronomy.Body.Mercury, "2013-06-02T16:45Z", "2013-06-12T16:45Z", 24.30, 'evening' ),
( astronomy.Body.Mercury, "2013-09-29T09:59Z", "2013-10-09T09:59Z", 25.30, 'evening' ),
( astronomy.Body.Mercury, "2014-01-21T10:00Z", "2014-01-31T10:00Z", 18.40, 'evening' ),
( astronomy.Body.Mercury, "2014-05-15T07:06Z", "2014-05-25T07:06Z", 22.70, 'evening' ),
( astronomy.Body.Mercury, "2014-09-11T22:20Z", "2014-09-21T22:20Z", 26.40, 'evening' ),
( astronomy.Body.Mercury, "2015-01-04T20:26Z", "2015-01-14T20:26Z", 18.90, 'evening' ),
( astronomy.Body.Mercury, "2015-04-27T04:46Z", "2015-05-07T04:46Z", 21.20, 'evening' ),
( astronomy.Body.Mercury, "2015-08-25T10:20Z", "2015-09-04T10:20Z", 27.10, 'evening' ),
( astronomy.Body.Mercury, "2015-12-19T03:11Z", "2015-12-29T03:11Z", 19.70, 'evening' ),
( astronomy.Body.Mercury, "2016-04-08T14:00Z", "2016-04-18T14:00Z", 19.90, 'evening' ),
( astronomy.Body.Mercury, "2016-08-06T21:24Z", "2016-08-16T21:24Z", 27.40, 'evening' ),
( astronomy.Body.Mercury, "2016-12-01T04:36Z", "2016-12-11T04:36Z", 20.80, 'evening' ),
( astronomy.Body.Mercury, "2017-03-22T10:24Z", "2017-04-01T10:24Z", 19.00, 'evening' ),
( astronomy.Body.Mercury, "2017-07-20T04:34Z", "2017-07-30T04:34Z", 27.20, 'evening' ),
( astronomy.Body.Mercury, "2017-11-14T00:32Z", "2017-11-24T00:32Z", 22.00, 'evening' ),
( astronomy.Body.Mercury, "2018-03-05T15:07Z", "2018-03-15T15:07Z", 18.40, 'evening' ),
( astronomy.Body.Mercury, "2018-07-02T05:24Z", "2018-07-12T05:24Z", 26.40, 'evening' ),
( astronomy.Body.Mercury, "2018-10-27T15:25Z", "2018-11-06T15:25Z", 23.30, 'evening' ),
( astronomy.Body.Mercury, "2019-02-17T01:23Z", "2019-02-27T01:23Z", 18.10, 'evening' ),
( astronomy.Body.Mercury, "2019-06-13T23:14Z", "2019-06-23T23:14Z", 25.20, 'evening' ),
( astronomy.Body.Mercury, "2019-10-10T04:00Z", "2019-10-20T04:00Z", 24.60, 'evening' ),
( astronomy.Body.Venus, "2010-12-29T15:57Z", "2011-01-08T15:57Z", 47.00, 'morning' ),
( astronomy.Body.Venus, "2012-08-05T08:59Z", "2012-08-15T08:59Z", 45.80, 'morning' ),
( astronomy.Body.Venus, "2014-03-12T19:25Z", "2014-03-22T19:25Z", 46.60, 'morning' ),
( astronomy.Body.Venus, "2015-10-16T06:57Z", "2015-10-26T06:57Z", 46.40, 'morning' ),
( astronomy.Body.Venus, "2017-05-24T13:09Z", "2017-06-03T13:09Z", 45.90, 'morning' ),
( astronomy.Body.Venus, "2018-12-27T04:24Z", "2019-01-06T04:24Z", 47.00, 'morning' ),
( astronomy.Body.Venus, "2010-08-10T03:19Z", "2010-08-20T03:19Z", 46.00, 'evening' ),
( astronomy.Body.Venus, "2012-03-17T08:03Z", "2012-03-27T08:03Z", 46.00, 'evening' ),
( astronomy.Body.Venus, "2013-10-22T08:00Z", "2013-11-01T08:00Z", 47.10, 'evening' ),
( astronomy.Body.Venus, "2015-05-27T18:46Z", "2015-06-06T18:46Z", 45.40, 'evening' ),
( astronomy.Body.Venus, "2017-01-02T13:19Z", "2017-01-12T13:19Z", 47.10, 'evening' ),
( astronomy.Body.Venus, "2018-08-07T17:02Z", "2018-08-17T17:02Z", 45.90, 'evening' )
]
def TestMaxElong(body, searchText, eventText, angle, visibility):
name = body.name
searchTime = astronomy.Time.Parse(searchText)
eventTime = astronomy.Time.Parse(eventText)
evt = astronomy.SearchMaxElongation(body, searchTime)
if evt is None:
print('PY TestMaxElong({} {}): SearchMaxElongation failed.'.format(name, searchText))
return 1
if evt.visibility != visibility:
print('PY TestMaxElong({} {}): SearchMaxElongation returned visibility {}, but expected {}'.format(name, searchText, evt.visibility.name, visibility.name))
return 1
hour_diff = 24.0 * vabs(evt.time.tt - eventTime.tt)
arcmin_diff = 60.0 * vabs(evt.elongation - angle)
Debug('PY TestMaxElong: {:<7s} {:<7s} elong={:5.2f} ({:4.2f} arcmin, {:5.3f} hours)'.format(name, visibility.name, evt.elongation, arcmin_diff, hour_diff))
if hour_diff > 0.603:
print('PY TestMaxElong({} {}): EXCESSIVE HOUR ERROR.'.format(name, searchText))
return 1
if arcmin_diff > 3.4:
print('PY TestMaxElong({} {}): EXCESSIVE ARCMIN ERROR.'.format(name, searchText))
return 1
return 0
def SearchElongTest():
for (body, searchText, eventText, angle, visibility) in ElongTestData:
if 0 != TestMaxElong(body, searchText, eventText, angle, astronomy.Visibility[visibility.title()]):
return 1
return 0
def Elongation():
if 0 != TestElongFile('longitude/opposition_2018.txt', 0.0): return 1
if 0 != TestPlanetLongitudes(astronomy.Body.Mercury, "temp/py_longitude_Mercury.txt", "inf"): return 1
if 0 != TestPlanetLongitudes(astronomy.Body.Venus, "temp/py_longitude_Venus.txt", "inf"): return 1
if 0 != TestPlanetLongitudes(astronomy.Body.Mars, "temp/py_longitude_Mars.txt", "opp"): return 1
if 0 != TestPlanetLongitudes(astronomy.Body.Jupiter, "temp/py_longitude_Jupiter.txt", "opp"): return 1
if 0 != TestPlanetLongitudes(astronomy.Body.Saturn, "temp/py_longitude_Saturn.txt", "opp"): return 1
if 0 != TestPlanetLongitudes(astronomy.Body.Uranus, "temp/py_longitude_Uranus.txt", "opp"): return 1
if 0 != TestPlanetLongitudes(astronomy.Body.Neptune, "temp/py_longitude_Neptune.txt", "opp"): return 1
if 0 != TestPlanetLongitudes(astronomy.Body.Pluto, "temp/py_longitude_Pluto.txt", "opp"): return 1
if 0 != SearchElongTest(): return 1
print('PY Elongation: PASS')
return 0
#-----------------------------------------------------------------------------------------------------------
def MonthNumber(mtext):
return 1 + ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'].index(mtext)
def ParseJplHorizonsDateTime(line):
m = re.match(r'^\s*(\d{4})-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d{2})\s(\d{2}):(\d{2})\s+(.*)$', line)
if not m:
return None, None
year = int(m.group(1))
month = MonthNumber(m.group(2))
day = int(m.group(3))
hour = int(m.group(4))
minute = int(m.group(5))
rest = m.group(6)
time = astronomy.Time.Make(year, month, day, hour, minute, 0)
return time, rest
def CheckMagnitudeData(body, filename):
limit = 0.012
sum_squared_diff = 0.0
with open(filename, 'rt') as infile:
count = lnum = 0
for line in infile:
lnum += 1
line = line.strip()
(time, rest) = ParseJplHorizonsDateTime(line)
if (time is not None) and (rest is not None) and not ('n.a.' in rest):
data = [float(t) for t in rest.split()]
if len(data) != 7:
print('PY CheckMagnitudeData({} line {}): invalid data format'.format(filename, lnum))
return 1
(mag, sbrt, dist, rdot, delta, deldot, phase_angle) = data
illum = astronomy.Illumination(body, time)
diff = illum.mag - mag
if vabs(diff) > limit:
print('PY CheckMagnitudeData({} line {}): EXCESSIVE ERROR: correct mag={}, calc mag={}'.format(filename, lnum, mag, illum.mag))
return 1
sum_squared_diff += diff * diff
if count == 0:
diff_lo = diff_hi = diff
else:
diff_lo = vmin(diff_lo, diff)
diff_hi = vmax(diff_hi, diff)
count += 1
if count == 0:
print('PY CheckMagnitudeData: Did not find any data in file: {}'.format(filename))
return 1
rms = sqrt(sum_squared_diff / count)
Debug('PY CheckMagnitudeData: {:<21s} {:5d} rows diff_lo={:0.4f} diff_hi={:0.4f} rms={:0.4f}'.format(filename, count, diff_lo, diff_hi, rms))
return 0
def CheckSaturn():
# JPL Horizons does not include Saturn's rings in its magnitude models.
# I still don't have authoritative test data for Saturn's magnitude.
# For now, I just test for consistency with Paul Schlyter's formulas at:
# http://www.stjarnhimlen.se/comp/ppcomp.html#15
data = [
( "1972-01-01T00:00Z", -0.31904865, +24.50061220 ),
( "1980-01-01T00:00Z", +0.85213663, -1.85761461 ),
( "2009-09-04T00:00Z", +1.01626809, +0.08380716 ),
( "2017-06-15T00:00Z", -0.12318790, -26.60871409 ),
( "2019-05-01T00:00Z", +0.32954097, -23.53880802 ),
( "2025-09-25T00:00Z", +0.51286575, +1.52327932 ),
( "2032-05-15T00:00Z", -0.04652109, +26.95717765 )
]
error = 0
for (dtext, mag, tilt) in data:
time = astronomy.Time.Parse(dtext)
illum = astronomy.Illumination(astronomy.Body.Saturn, time)
Debug('PY Saturn: date={} calc mag={:12.8f} ring_tilt={:12.8f}'.format(dtext, illum.mag, illum.ring_tilt))
mag_diff = vabs(illum.mag - mag)
if mag_diff > 1.0e-4:
print('PY CheckSaturn: Excessive magnitude error {}'.format(mag_diff))
error = 1
tilt_diff = vabs(illum.ring_tilt - tilt)
if (tilt_diff > 3.0e-5):
print('PY CheckSaturn: Excessive ring tilt error {}'.format(tilt_diff))
error = 1
return error
def TestMaxMag(body, filename):
# Example of input data:
#
# 2001-02-21T08:00Z 2001-02-27T08:00Z 23.17 19.53 -4.84
#
# JPL Horizons test data has limited floating point precision in the magnitude values.
# There is a pair of dates for the beginning and end of the max magnitude period,
# given the limited precision. We pick the point halfway between as the supposed max magnitude time.
with open(filename, 'rt') as infile:
lnum = 0
search_time = astronomy.Time.Make(2001, 1, 1, 0, 0, 0)
for line in infile:
lnum += 1
line = line.strip()
tokenlist = line.split()
if len(tokenlist) == 5:
time1 = astronomy.Time.Parse(tokenlist[0])
time2 = astronomy.Time.Parse(tokenlist[1])
if time1 and time2:
center_time = time1.AddDays(0.5*(time2.ut - time1.ut))
correct_mag = float(tokenlist[4])
illum = astronomy.SearchPeakMagnitude(body, search_time)
mag_diff = vabs(illum.mag - correct_mag)
hours_diff = 24.0 * vabs(illum.time.ut - center_time.ut)
Debug('PY TestMaxMag: mag_diff={:0.3f}, hours_diff={:0.3f}'.format(mag_diff, hours_diff))
if hours_diff > 7.1:
print('PY TestMaxMag({} line {}): EXCESSIVE TIME DIFFERENCE.'.format(filename, lnum))
return 1
if mag_diff > 0.005:
print('PY TestMaxMag({} line {}): EXCESSIVE MAGNITUDE DIFFERENCE.'.format(filename, lnum))
return 1
search_time = time2
Debug('PY TestMaxMag: processed {} lines from file {}'.format(lnum, filename))
return 0
def Magnitude():
nfailed = 0
nfailed += CheckMagnitudeData(astronomy.Body.Sun, 'magnitude/Sun.txt')
nfailed += CheckMagnitudeData(astronomy.Body.Moon, 'magnitude/Moon.txt')
nfailed += CheckMagnitudeData(astronomy.Body.Mercury, 'magnitude/Mercury.txt')
nfailed += CheckMagnitudeData(astronomy.Body.Venus, 'magnitude/Venus.txt')
nfailed += CheckMagnitudeData(astronomy.Body.Mars, 'magnitude/Mars.txt')
nfailed += CheckMagnitudeData(astronomy.Body.Jupiter, 'magnitude/Jupiter.txt')
nfailed += CheckSaturn()
nfailed += CheckMagnitudeData(astronomy.Body.Uranus, 'magnitude/Uranus.txt')
nfailed += CheckMagnitudeData(astronomy.Body.Neptune, 'magnitude/Neptune.txt')
nfailed += CheckMagnitudeData(astronomy.Body.Pluto, 'magnitude/Pluto.txt')
nfailed += TestMaxMag(astronomy.Body.Venus, 'magnitude/maxmag_Venus.txt')
if nfailed == 0:
print('PY Magnitude: PASS')
else:
print('PY Magnitude: failed {} test(s).'.format(nfailed))
return 1
return 0
#-----------------------------------------------------------------------------------------------------------
def RiseSet(filename = 'riseset/riseset.txt'):
sum_minutes = 0.0
max_minutes = 0.0
nudge_days = 0.01
observer = None
current_body = None
a_dir = 0
b_dir = 0
with open(filename, 'rt') as infile:
lnum = 0
for line in infile:
lnum += 1
line = line.strip()
# Moon 103 -61 1944-01-02T17:08Z s
# Moon 103 -61 1944-01-03T05:47Z r
m = re.match(r'^([A-Za-z]+)\s+(-?[0-9\.]+)\s+(-?[0-9\.]+)\s+(\d+)-(\d+)-(\d+)T(\d+):(\d+)Z\s+([sr])$', line)
if not m:
print('PY RiseSet({} line {}): invalid data format'.format(filename, lnum))
return 1
name = m.group(1)
longitude = float(m.group(2))
latitude = float(m.group(3))
year = int(m.group(4))
month = int(m.group(5))
day = int(m.group(6))
hour = int(m.group(7))
minute = int(m.group(8))
kind = m.group(9)
correct_time = astronomy.Time.Make(year, month, day, hour, minute, 0)
direction = astronomy.Direction.Rise if kind == 'r' else astronomy.Direction.Set
body = astronomy.BodyCode(name)
if body == astronomy.Body.Invalid:
print('PY RiseSet({} line {}): invalid body name "{}"'.format(filename, lnum, name))
return 1
# Every time we see a new geographic location, start a new iteration
# of finding all rise/set times for that UTC calendar year.
if (observer is None) or (observer.latitude != latitude) or (observer.longitude != longitude) or (current_body != body):
current_body = body
observer = astronomy.Observer(latitude, longitude, 0)
r_search_date = s_search_date = astronomy.Time.Make(year, 1, 1, 0, 0, 0)
b_evt = None
Debug('PY RiseSet: {:<7s} lat={:0.1f} lon={:0.1f}'.format(name, latitude, longitude))
if b_evt is not None:
# Recycle the second event from the previous iteration as the first event.
a_evt = b_evt
a_dir = b_dir
b_evt = None
else:
r_evt = astronomy.SearchRiseSet(body, observer, astronomy.Direction.Rise, r_search_date, 366.0)
if r_evt is None:
print('PY RiseSet({} line {}): rise search failed'.format(filename, lnum))
return 1
s_evt = astronomy.SearchRiseSet(body, observer, astronomy.Direction.Set, s_search_date, 366.0)
if s_evt is None:
print('PY RiseSet({} line {}): set search failed'.format(filename, lnum))
return 1
# Expect the current event to match the earlier of the found times.
if r_evt.tt < s_evt.tt:
a_evt = r_evt
b_evt = s_evt
a_dir = astronomy.Direction.Rise
b_dir = astronomy.Direction.Set
else:
a_evt = s_evt
b_evt = r_evt
a_dir = astronomy.Direction.Set
b_dir = astronomy.Direction.Rise
# Nudge the event times forward a tiny amount.
r_search_date = r_evt.AddDays(nudge_days)
s_search_date = s_evt.AddDays(nudge_days)
if a_dir != direction:
print('PY RiseSet({} line {}): expected dir={} but found {}'.format(filename, lnum, a_dir, direction))
return 1
error_minutes = (24.0 * 60.0) * vabs(a_evt.tt - correct_time.tt)
sum_minutes += error_minutes ** 2
max_minutes = vmax(max_minutes, error_minutes)
if error_minutes > 0.57:
print('PY RiseSet({} line {}): excessive prediction time error = {} minutes.'.format(filename, lnum, error_minutes))
print(' correct = {}, calculated = {}'.format(correct_time, a_evt))
return 1
rms_minutes = sqrt(sum_minutes / lnum)
print('PY RiseSet: passed {} lines: time errors in minutes: rms={:0.4f}, max={:0.4f}'.format(lnum, rms_minutes, max_minutes))
return 0
#-----------------------------------------------------------------------------------------------------------
def LunarApsis(filename = 'apsides/moon.txt'):
max_minutes = 0.0
max_km = 0.0
with open(filename, 'rt') as infile:
start_time = astronomy.Time.Make(2001, 1, 1, 0, 0, 0)
lnum = 0
for line in infile:
lnum += 1
if lnum == 1:
apsis = astronomy.SearchLunarApsis(start_time)
else:
apsis = astronomy.NextLunarApsis(apsis)
tokenlist = line.split()
if len(tokenlist) != 3:
print('PY LunarApsis({} line {}): invalid data format'.format(filename, lnum))
return 1
correct_time = astronomy.Time.Parse(tokenlist[1])
if not correct_time:
print('PY LunarApsis({} line {}): invalid time'.format(filename, lnum))
return 1
kind = astronomy.ApsisKind(int(tokenlist[0]))
if apsis.kind != kind:
print('PY LunarApsis({} line {}): Expected kind {} but found {}'.format(filename, lnum, kind, apsis.kind))
return 1
dist_km = float(tokenlist[2])
diff_minutes = (24.0 * 60.0) * vabs(apsis.time.ut - correct_time.ut)
diff_km = vabs(apsis.dist_km - dist_km)
if diff_minutes > 35.0:
print('PY LunarApsis({} line {}): Excessive time error = {} minutes.'.format(filename, lnum, diff_minutes))
return 1
if diff_km > 25.0:
print('PY LunarApsis({} line {}): Excessive distance error = {} km.'.format(filename, lnum, diff_km))
return 1
max_minutes = vmax(max_minutes, diff_minutes)
max_km = vmax(max_km, diff_km)
print('PY LunarApsis: found {} events, max time error = {:0.3f} minutes, max distance error = {:0.3f} km.'.format(lnum, max_minutes, max_km))
return 0
#-----------------------------------------------------------------------------------------------------------
def CompareMatrices(caller, a, b, tolerance):
for i in range(3):
for j in range(3):
diff = vabs(a.rot[i][j] - b.rot[i][j])
if diff > tolerance:
print('PY CompareMatrices ERROR({}): matrix[{}][{}] = {}, expected {}, diff {}'.format(caller, i, j, a.rot[i][j], b.rot[i][j], diff))
sys.exit(1)
def CompareVectors(caller, a, b, tolerance):
diff = vabs(a.x - b.x)
if diff > tolerance:
print('PY CompareVectors ERROR({}): vector x = {}, expected {}, diff {}'.format(caller, a.x, b.x, diff))
sys.exit(1)
diff = vabs(a.y - b.y)
if diff > tolerance:
print('PY CompareVectors ERROR({}): vector y = {}, expected {}, diff {}'.format(caller, a.y, b.y, diff))
sys.exit(1)
diff = vabs(a.z - b.z)
if diff > tolerance:
print('PY CompareVectors ERROR({}): vector z = {}, expected {}, diff {}'.format(caller, a.z, b.z, diff))
sys.exit(1)
def Rotation_MatrixInverse():
a = astronomy.RotationMatrix([
[1, 4, 7],
[2, 5, 8],
[3, 6, 9]
])
v = astronomy.RotationMatrix([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
])
b = astronomy.InverseRotation(a)
CompareMatrices('Rotation_MatrixInverse', b, v, 0)
def Rotation_MatrixMultiply():
a = astronomy.RotationMatrix([
[1, 4, 7],
[2, 5, 8],
[3, 6, 9]
])
b = astronomy.RotationMatrix([
[10, 13, 16],
[11, 14, 17],
[12, 15, 18]
])
v = astronomy.RotationMatrix([
[84, 201, 318],
[90, 216, 342],
[96, 231, 366]
])
c = astronomy.CombineRotation(b, a)
CompareMatrices('Rotation_MatrixMultiply', c, v, 0)
def VectorDiff(a, b):
dx = a.x - b.x
dy = a.y - b.y
dz = a.z - b.z
return sqrt(dx*dx + dy*dy + dz*dz)
def Test_EQJ_ECL():
r = astronomy.Rotation_EQJ_ECL()
# Calculate heliocentric Earth position at a test time.
time = astronomy.Time.Make(2019, 12, 8, 19, 39, 15)
ev = astronomy.HelioVector(astronomy.Body.Earth, time)
# Use the existing astronomy.Ecliptic() to calculate ecliptic vector and angles.
ecl = astronomy.Ecliptic(ev)
Debug('PY Test_EQJ_ECL ecl = ({}, {}, {})'.format(ecl.vec.x, ecl.vec.y, ecl.vec.z))
# Now compute the same vector via rotation matrix.
ee = astronomy.RotateVector(r, ev)
dx = ee.x - ecl.vec.x
dy = ee.y - ecl.vec.y
dz = ee.z - ecl.vec.z
diff = sqrt(dx*dx + dy*dy + dz*dz)
Debug('PY Test_EQJ_ECL ee = ({}, {}, {}); diff = {}'.format(ee.x, ee.y, ee.z, diff))
if diff > 1.0e-16:
print('PY Test_EQJ_ECL: EXCESSIVE VECTOR ERROR')
sys.exit(1)
# Reverse the test: go from ecliptic back to equatorial.
ir = astronomy.Rotation_ECL_EQJ()
et = astronomy.RotateVector(ir, ee)
idiff = VectorDiff(et, ev)
Debug('PY Test_EQJ_ECL ev diff = {}'.format(idiff))
if idiff > 2.3e-16:
print('PY Test_EQJ_ECL: EXCESSIVE REVERSE ROTATION ERROR')
sys.exit(1)
def Test_GAL_EQJ_NOVAS(filename):
THRESHOLD_SECONDS = 8.8
rot = astronomy.Rotation_EQJ_GAL()
time = astronomy.Time(0.0) # placeholder time - value does not matter
with open(filename, 'rt') as infile:
lnum = 0
max_diff = 0.0
for line in infile:
lnum += 1
token = line.split()
if len(token) != 4:
print('PY Test_GAL_EQJ_NOVAS({} line {}): Wrong number of tokens.'.format(filename, lnum))
sys.exit(1)
ra = float(token[0])
dec = float(token[1])
glon = float(token[2])
glat = float(token[3])
eqj_sphere = astronomy.Spherical(dec, 15.0*ra, 1.0)
eqj_vec = astronomy.VectorFromSphere(eqj_sphere, time)
gal_vec = astronomy.RotateVector(rot, eqj_vec)
gal_sphere = astronomy.SphereFromVector(gal_vec)
dlat = gal_sphere.lat - glat
dlon = math.cos(math.radians(glat)) * (gal_sphere.lon - glon)
diff = 3600.0 * math.sqrt(dlon*dlon + dlat*dlat)
if diff > THRESHOLD_SECONDS:
print('PY Test_GAL_EQJ_NOVAS({} line {}): EXCESSIVE ERROR = {:0.3f}'.format(filename, lnum, diff))
sys.exit(1)
if diff > max_diff:
max_diff = diff
Debug('PY Test_GAL_EQJ_NOVAS: PASS. max_diff = {:0.3f} arcseconds.'.format(max_diff))
return 0
def Test_EQJ_EQD(body):
# Verify conversion of equatorial J2000 to equatorial of-date, and back.
# Use established functions to calculate spherical coordinates for the body, in both EQJ and EQD.
time = astronomy.Time.Make(2019, 12, 8, 20, 50, 0)
observer = astronomy.Observer(+35, -85, 0)
eq2000 = astronomy.Equator(body, time, observer, False, True)
eqdate = astronomy.Equator(body, time, observer, True, True)
# Convert EQJ spherical coordinates to vector.
v2000 = eq2000.vec
# Find rotation matrix.
r = astronomy.Rotation_EQJ_EQD(time)
# Rotate EQJ vector to EQD vector.
vdate = astronomy.RotateVector(r, v2000)
# Convert vector back to angular equatorial coordinates.
equcheck = astronomy.EquatorFromVector(vdate)
# Compare the result with the eqdate.
ra_diff = vabs(equcheck.ra - eqdate.ra)
dec_diff = vabs(equcheck.dec - eqdate.dec)
dist_diff = vabs(equcheck.dist - eqdate.dist)
Debug('PY Test_EQJ_EQD: {} ra={}, dec={}, dist={}, ra_diff={}, dec_diff={}, dist_diff={}'.format(
body.name, eqdate.ra, eqdate.dec, eqdate.dist, ra_diff, dec_diff, dist_diff
))
if ra_diff > 1.0e-14 or dec_diff > 1.0e-14 or dist_diff > 4.0e-15:
print('PY Test_EQJ_EQD: EXCESSIVE ERROR')
sys.exit(1)
# Perform the inverse conversion back to equatorial J2000 coordinates.
ir = astronomy.Rotation_EQD_EQJ(time)
t2000 = astronomy.RotateVector(ir, vdate)
diff = VectorDiff(t2000, v2000)
Debug('PY Test_EQJ_EQD: {} inverse diff = {}'.format(body.name, diff))
if diff > 5.0e-15:
print('PY Test_EQJ_EQD: EXCESSIVE INVERSE ERROR')
sys.exit(1)
def Test_EQD_HOR(body):
# Use existing functions to calculate horizontal coordinates of the body for the time+observer.
time = astronomy.Time.Make(1970, 12, 13, 5, 15, 0)
observer = astronomy.Observer(-37, +45, 0)
eqd = astronomy.Equator(body, time, observer, True, True)
Debug('PY Test_EQD_HOR {}: OFDATE ra={}, dec={}'.format(body.name, eqd.ra, eqd.dec))
hor = astronomy.Horizon(time, observer, eqd.ra, eqd.dec, astronomy.Refraction.Normal)
# Calculate the position of the body as an equatorial vector of date.
vec_eqd = eqd.vec
# Calculate rotation matrix to convert equatorial J2000 vector to horizontal vector.
rot = astronomy.Rotation_EQD_HOR(time, observer)
# Rotate the equator of date vector to a horizontal vector.
vec_hor = astronomy.RotateVector(rot, vec_eqd)
# Convert the horizontal vector to horizontal angular coordinates.
xsphere = astronomy.HorizonFromVector(vec_hor, astronomy.Refraction.Normal)
diff_alt = vabs(xsphere.lat - hor.altitude)
diff_az = vabs(xsphere.lon - hor.azimuth)
Debug('PY Test_EQD_HOR {}: trusted alt={}, az={}; test alt={}, az={}; diff_alt={}, diff_az={}'.format(
body.name, hor.altitude, hor.azimuth, xsphere.lat, xsphere.lon, diff_alt, diff_az))
if diff_alt > 4.0e-14 or diff_az > 1.2e-13:
print('PY Test_EQD_HOR: EXCESSIVE HORIZONTAL ERROR.')
sys.exit(1)
# Confirm that we can convert back to horizontal vector.
check_hor = astronomy.VectorFromHorizon(xsphere, time, astronomy.Refraction.Normal)
diff = VectorDiff(check_hor, vec_hor)
Debug('PY Test_EQD_HOR {}: horizontal recovery: diff = {}'.format(body.name, diff))
if diff > 3.0e-15:
print('PY Test_EQD_HOR: EXCESSIVE ERROR IN HORIZONTAL RECOVERY.')
sys.exit(1)
# Verify the inverse translation from horizontal vector to equatorial of-date vector.
irot = astronomy.Rotation_HOR_EQD(time, observer)
check_eqd = astronomy.RotateVector(irot, vec_hor)
diff = VectorDiff(check_eqd, vec_eqd)
Debug('PY Test_EQD_HOR {}: OFDATE inverse rotation diff = {}'.format(body.name, diff))
if diff > 2.7e-15:
print('PY Test_EQD_HOR: EXCESSIVE OFDATE INVERSE HORIZONTAL ERROR.')
sys.exit(1)
# Exercise HOR to EQJ translation.
eqj = astronomy.Equator(body, time, observer, False, True)
vec_eqj = eqj.vec
yrot = astronomy.Rotation_HOR_EQJ(time, observer)
check_eqj = astronomy.RotateVector(yrot, vec_hor)
diff = VectorDiff(check_eqj, vec_eqj)
Debug('PY Test_EQD_HOR {}: J2000 inverse rotation diff = {}'.format(body.name, diff))
if diff > 5.0e-15:
print('PY Test_EQD_HOR: EXCESSIVE J2000 INVERSE HORIZONTAL ERROR.')
sys.exit(1)
# Verify the inverse translation: EQJ to HOR.
zrot = astronomy.Rotation_EQJ_HOR(time, observer)
another_hor = astronomy.RotateVector(zrot, vec_eqj)
diff = VectorDiff(another_hor, vec_hor)
Debug('PY Test_EQD_HOR {}: EQJ inverse rotation diff = {}'.format(body.name, diff))
if diff > 6.0e-15:
print('PY Test_EQD_HOR: EXCESSIVE EQJ INVERSE HORIZONTAL ERROR.')
sys.exit(1)
IdentityMatrix = astronomy.RotationMatrix([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
def CheckInverse(aname, bname, arot, brot):
crot = astronomy.CombineRotation(arot, brot)
caller = 'CheckInverse({},{})'.format(aname, bname)
CompareMatrices(caller, crot, IdentityMatrix, 2.0e-15)
def CheckCycle(cyclename, arot, brot, crot):
xrot = astronomy.CombineRotation(arot, brot)
irot = astronomy.InverseRotation(xrot)
CompareMatrices(cyclename, crot, irot, 2.0e-15)
def Test_RotRoundTrip():
# In each round trip, calculate a forward rotation and a backward rotation.
# Verify the two are inverse matrices.
time = astronomy.Time.Make(2067, 5, 30, 14, 45, 0)
observer = astronomy.Observer(+28, -82, 0)
# Round trip #1: EQJ <==> EQD.
eqj_eqd = astronomy.Rotation_EQJ_EQD(time)
eqd_eqj = astronomy.Rotation_EQD_EQJ(time)
CheckInverse('eqj_eqd', 'eqd_eqj', eqj_eqd, eqd_eqj)
# Round trip #2: EQJ <==> ECL.
eqj_ecl = astronomy.Rotation_EQJ_ECL()
ecl_eqj = astronomy.Rotation_ECL_EQJ()
CheckInverse('eqj_ecl', 'ecl_eqj', eqj_ecl, ecl_eqj)
# Round trip #3: EQJ <==> HOR.
eqj_hor = astronomy.Rotation_EQJ_HOR(time, observer)
hor_eqj = astronomy.Rotation_HOR_EQJ(time, observer)
CheckInverse('eqj_hor', 'hor_eqj', eqj_hor, hor_eqj)
# Round trip #4: EQD <==> HOR.
eqd_hor = astronomy.Rotation_EQD_HOR(time, observer)
hor_eqd = astronomy.Rotation_HOR_EQD(time, observer)
CheckInverse('eqd_hor', 'hor_eqd', eqd_hor, hor_eqd)
# Round trip #5: EQD <==> ECL.
eqd_ecl = astronomy.Rotation_EQD_ECL(time)
ecl_eqd = astronomy.Rotation_ECL_EQD(time)
CheckInverse('eqd_ecl', 'ecl_eqd', eqd_ecl, ecl_eqd)
# Round trip #6: HOR <==> ECL.
hor_ecl = astronomy.Rotation_HOR_ECL(time, observer)
ecl_hor = astronomy.Rotation_ECL_HOR(time, observer)
CheckInverse('hor_ecl', 'ecl_hor', hor_ecl, ecl_hor)
# Verify that combining different sequences of rotations result
# in the expected combination.
# For example, (EQJ ==> HOR ==> ECL) must be the same matrix as (EQJ ==> ECL).
# Each of these is a "triangle" of relationships between 3 orientations.
# There are 4 possible ways to pick 3 orientations from the 4 to form a triangle.
# Because we have just proved that each transformation is reversible,
# we only need to verify the triangle in one cyclic direction.
CheckCycle('eqj_ecl, ecl_eqd, eqd_eqj', eqj_ecl, ecl_eqd, eqd_eqj) # excluded corner = HOR
CheckCycle('eqj_hor, hor_ecl, ecl_eqj', eqj_hor, hor_ecl, ecl_eqj) # excluded corner = EQD
CheckCycle('eqj_hor, hor_eqd, eqd_eqj', eqj_hor, hor_eqd, eqd_eqj) # excluded corner = ECL
CheckCycle('ecl_eqd, eqd_hor, hor_ecl', ecl_eqd, eqd_hor, hor_ecl) # excluded corner = EQJ
Debug('PY Test_RotRoundTrip: PASS')
def Rotation_Pivot():
tolerance = 1.0e-15
# Start with an identity matrix.
ident = astronomy.IdentityMatrix()
# Pivot 90 degrees counterclockwise around the z-axis.
r = astronomy.Pivot(ident, 2, +90.0)
# Put the expected answer in 'a'.
a = astronomy.RotationMatrix([
[ 0, +1, 0],
[-1, 0, 0],
[ 0, 0, +1],
])
# Compare actual 'r' with expected 'a'.
CompareMatrices('Rotation_Pivot #1', r, a, tolerance)
# Pivot again, -30 degrees around the x-axis.
r = astronomy.Pivot(r, 0, -30.0)
# Pivot a third time, 180 degrees around the y-axis.
r = astronomy.Pivot(r, 1, +180.0)
# Use the 'r' matrix to rotate a vector.
v1 = astronomy.Vector(1, 2, 3, astronomy.Time(0))
v2 = astronomy.RotateVector(r, v1)
# Initialize the expected vector 've'.
ve = astronomy.Vector(+2.0, +2.3660254037844390, -2.0980762113533156, v1.t)
CompareVectors('Rotation_Pivot #2', v2, ve, tolerance)
Debug('PY Rotation_Pivot: PASS')
def Rotation():
Rotation_MatrixInverse()
Rotation_MatrixMultiply()
Rotation_Pivot()
Test_EQJ_ECL()
Test_GAL_EQJ_NOVAS('temp/galeqj.txt')
Test_EQJ_EQD(astronomy.Body.Mercury)
Test_EQJ_EQD(astronomy.Body.Venus)
Test_EQJ_EQD(astronomy.Body.Mars)
Test_EQJ_EQD(astronomy.Body.Jupiter)
Test_EQJ_EQD(astronomy.Body.Saturn)
Test_EQD_HOR(astronomy.Body.Mercury)
Test_EQD_HOR(astronomy.Body.Venus)
Test_EQD_HOR(astronomy.Body.Mars)
Test_EQD_HOR(astronomy.Body.Jupiter)
Test_EQD_HOR(astronomy.Body.Saturn)
Test_RotRoundTrip()
print('PY Rotation: PASS')
return 0
#-----------------------------------------------------------------------------------------------------------
def Refraction():
alt = -90.1
while alt <= +90.1:
refr = astronomy.RefractionAngle(astronomy.Refraction.Normal, alt)
corrected = alt + refr
inv_refr = astronomy.InverseRefractionAngle(astronomy.Refraction.Normal, corrected)
check_alt = corrected + inv_refr
diff = vabs(check_alt - alt)
if diff > 2.0e-14:
print('PY Refraction: ERROR - excessive error: alt={}, refr={}, diff={}'.format(alt, refr, diff))
return 1
alt += 0.001
print('PY Refraction: PASS')
return 0
#-----------------------------------------------------------------------------------------------------------
def PlanetApsis():
start_time = astronomy.Time.Make(1700, 1, 1, 0, 0, 0)
body = astronomy.Body.Mercury
while body.value <= astronomy.Body.Pluto.value:
count = 1
period = astronomy._PlanetOrbitalPeriod[body.value]
filename = os.path.join('apsides', 'apsis_{}.txt'.format(body.value))
min_interval = -1.0
max_diff_days = 0.0
max_dist_ratio = 0.0
apsis = astronomy.SearchPlanetApsis(body, start_time)
with open(filename, 'rt') as infile:
for line in infile:
token = line.split()
if len(token) != 3:
print('PY PlanetApsis({} line {}): Invalid data format: {} tokens'.format(filename, count, len(token)))
return 1
expected_kind = astronomy.ApsisKind(int(token[0]))
expected_time = astronomy.Time.Parse(token[1])
expected_distance = float(token[2])
if apsis.kind != expected_kind:
print('PY PlanetApsis({} line {}): WRONG APSIS KIND: expected {}, found {}'.format(filename, count, expected_kind, apsis.kind))
return 1
diff_days = vabs(expected_time.tt - apsis.time.tt)
max_diff_days = vmax(max_diff_days, diff_days)
diff_degrees = (diff_days / period) * 360
if body == astronomy.Body.Pluto:
degree_threshold = 0.262
else:
degree_threshold = 0.1
if diff_degrees > degree_threshold:
print('PY PlanetApsis: FAIL - {} exceeded angular threshold ({} vs {} degrees)'.format(body.name, diff_degrees, degree_threshold))
return 1
diff_dist_ratio = vabs(expected_distance - apsis.dist_au) / expected_distance
max_dist_ratio = vmax(max_dist_ratio, diff_dist_ratio)
if diff_dist_ratio > 1.05e-4:
print('PY PlanetApsis({} line {}): distance ratio {} is too large.'.format(filename, count, diff_dist_ratio))
return 1
# Calculate the next apsis.
prev_time = apsis.time
apsis = astronomy.NextPlanetApsis(body, apsis)
count += 1
interval = apsis.time.tt - prev_time.tt
if min_interval < 0.0:
min_interval = max_interval = interval
else:
min_interval = vmin(min_interval, interval)
max_interval = vmax(max_interval, interval)
if count < 2:
print('PY PlanetApsis: FAILED to find apsides for {}'.format(body))
return 1
Debug('PY PlanetApsis: {:4d} apsides for {:<9s} -- intervals: min={:0.2f}, max={:0.2f}, ratio={:0.6f}; max day={:0.3f}, degrees={:0.3f}, dist ratio={:0.6f}'.format(
count,
body.name,
min_interval, max_interval, max_interval / min_interval,
max_diff_days,
(max_diff_days / period) * 360.0,
max_dist_ratio
))
body = astronomy.Body(body.value + 1)
print('PY PlanetApsis: PASS')
return 0
#-----------------------------------------------------------------------------------------------------------
def Constellation():
inFileName = 'constellation/test_input.txt'
lnum = 0
failcount = 0
with open(inFileName, 'rt') as infile:
for line in infile:
lnum += 1
m = re.match(r'^\s*(\d+)\s+(\S+)\s+(\S+)\s+([A-Z][a-zA-Z]{2})\s*$', line)
if not m:
print('PY Constellation: invalid line {} in file {}'.format(lnum, inFileName))
return 1
id = int(m.group(1))
ra = float(m.group(2))
dec = float(m.group(3))
symbol = m.group(4)
constel = astronomy.Constellation(ra, dec)
if constel.symbol != symbol:
print('Star {:6d}: expected {}, found {} at B1875 RA={:10.6f}, DEC={:10.6f}'.format(id, symbol, constel.symbol, constel.ra1875, constel.dec1875))
failcount += 1
if failcount > 0:
print('PY Constellation: {} failures'.format(failcount))
return 1
print('PY Constellation: PASS (verified {})'.format(lnum))
return 0
#-----------------------------------------------------------------------------------------------------------
def LunarEclipseIssue78():
# https://github.com/cosinekitty/astronomy/issues/78
eclipse = astronomy.SearchLunarEclipse(astronomy.Time.Make(2020, 12, 19, 0, 0, 0))
expected_peak = astronomy.Time.Make(2021, 5, 26, 11, 18, 42) # https://www.timeanddate.com/eclipse/lunar/2021-may-26
dt = (expected_peak.tt - eclipse.peak.tt) * (24.0 * 3600.0)
if vabs(dt) > 40.0:
print('LunarEclipseIssue78: Excessive prediction error = {} seconds.'.format(dt))
return 1
if eclipse.kind != astronomy.EclipseKind.Total:
print('Expected total eclipse; found: {}'.format(eclipse.kind))
return 1
print('PY LunarEclipseIssue78: PASS')
return 0
#-----------------------------------------------------------------------------------------------------------
def LunarEclipse():
astronomy._CalcMoonCount = 0
filename = 'eclipse/lunar_eclipse.txt'
with open(filename, 'rt') as infile:
eclipse = astronomy.SearchLunarEclipse(astronomy.Time.Make(1701, 1, 1, 0, 0, 0))
lnum = 0
skip_count = 0
diff_count = 0
sum_diff_minutes = 0.0
max_diff_minutes = 0.0
diff_limit = 2.0
for line in infile:
lnum += 1
if len(line) < 17:
print('PY LunarEclipse({} line {}): line is too short.'.format(filename, lnum))
return 1
time_text = line[0:17]
peak_time = astronomy.Time.Parse(time_text)
token = line[17:].split()
if len(token) != 2:
print('PY LunarEclipse({} line {}): wrong number of tokens.'.format(filename, lnum))
return 1
partial_minutes = float(token[0])
total_minutes = float(token[1])
valid = False
# Verify that the calculated eclipse semi-durations are consistent with the kind.
if eclipse.kind == astronomy.EclipseKind.Penumbral:
valid = (eclipse.sd_penum > 0.0) and (eclipse.sd_partial == 0.0) and (eclipse.sd_total == 0.0)
elif eclipse.kind == astronomy.EclipseKind.Partial:
valid = (eclipse.sd_penum > 0.0) and (eclipse.sd_partial > 0.0) and (eclipse.sd_total == 0.0)
elif eclipse.kind == astronomy.EclipseKind.Total:
valid = (eclipse.sd_penum > 0.0) and (eclipse.sd_partial > 0.0) and (eclipse.sd_total > 0.0)
else:
print('PY LunarEclipse({} line {}): invalid eclipse kind {}.'.format(filename, lnum, eclipse.kind))
return 1
if not valid:
print('PY LunarEclipse({} line {}): invalid semidurations.'.format(filename, lnum))
return 1
# Check eclipse peak time.
diff_days = eclipse.peak.ut - peak_time.ut
# Tolerate missing penumbral eclipses - skip to next input line without calculating next eclipse.
if partial_minutes == 0.0 and diff_days > 20.0:
skip_count += 1
continue
diff_minutes = (24.0 * 60.0) * vabs(diff_days)
sum_diff_minutes += diff_minutes
diff_count += 1
if diff_minutes > diff_limit:
print("PY LunarEclipse expected center: {}".format(peak_time))
print("PY LunarEclipse found center: {}".format(eclipse.peak))
print("PY LunarEclipse({} line {}): EXCESSIVE center time error = {} minutes ({} days).".format(filename, lnum, diff_minutes, diff_days))
return 1
if diff_minutes > max_diff_minutes:
max_diff_minutes = diff_minutes
# check partial eclipse duration
diff_minutes = vabs(partial_minutes - eclipse.sd_partial)
sum_diff_minutes += diff_minutes
diff_count += 1
if diff_minutes > diff_limit:
print("PY LunarEclipse({} line {}): EXCESSIVE partial eclipse semiduration error: {} minutes".format(filename, lnum, diff_minutes))
return 1
if diff_minutes > max_diff_minutes:
max_diff_minutes = diff_minutes
# check total eclipse duration
diff_minutes = vabs(total_minutes - eclipse.sd_total)
sum_diff_minutes += diff_minutes
diff_count += 1
if diff_minutes > diff_limit:
print("PY LunarEclipse({} line {}): EXCESSIVE total eclipse semiduration error: {} minutes".format(filename, lnum, diff_minutes))
return 1
if diff_minutes > max_diff_minutes:
max_diff_minutes = diff_minutes
# calculate for next iteration
eclipse = astronomy.NextLunarEclipse(eclipse.peak)
print("PY LunarEclipse: PASS (verified {}, skipped {}, max_diff_minutes = {}, avg_diff_minutes = {}, moon calcs = {})".format(lnum, skip_count, max_diff_minutes, (sum_diff_minutes / diff_count), astronomy._CalcMoonCount))
return 0
#-----------------------------------------------------------------------------------------------------------
def VectorFromAngles(lat, lon):
rlat = math.radians(v(lat))
rlon = math.radians(v(lon))
coslat = math.cos(rlat)
return [
math.cos(rlon) * coslat,
math.sin(rlon) * coslat,
math.sin(rlat)
]
def AngleDiff(alat, alon, blat, blon):
a = VectorFromAngles(alat, alon)
b = VectorFromAngles(blat, blon)
dot = a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
if dot <= -1.0:
return 180.0
if dot >= +1.0:
return 0.0
return v(math.degrees(math.acos(dot)))
def KindFromChar(typeChar):
return {
'P': astronomy.EclipseKind.Partial,
'A': astronomy.EclipseKind.Annular,
'T': astronomy.EclipseKind.Total,
'H': astronomy.EclipseKind.Total,
}[typeChar]
def GlobalSolarEclipse():
expected_count = 1180
max_minutes = 0.0
max_angle = 0.0
skip_count = 0
eclipse = astronomy.SearchGlobalSolarEclipse(astronomy.Time.Make(1701, 1, 1, 0, 0, 0))
filename = 'eclipse/solar_eclipse.txt'
with open(filename, 'rt') as infile:
lnum = 0
for line in infile:
lnum += 1
# 1889-12-22T12:54:15Z -6 T -12.7 -12.8
token = line.split()
if len(token) != 5:
print('PY GlobalSolarEclipse({} line {}): invalid token count = {}'.format(filename, lnum, len(token)))
return 1
peak = astronomy.Time.Parse(token[0])
expected_kind = KindFromChar(token[2])
lat = float(token[3])
lon = float(token[4])
diff_days = eclipse.peak.tt - peak.tt
# Sometimes we find marginal eclipses that aren't listed in the test data.
# Ignore them if the distance between the Sun/Moon shadow axis and the Earth's center is large.
while diff_days < -25.0 and eclipse.distance > 9000.0:
skip_count += 1
eclipse = astronomy.NextGlobalSolarEclipse(eclipse.peak)
diff_days = eclipse.peak.ut - peak.ut
# Validate the eclipse prediction.
diff_minutes = (24 * 60) * vabs(diff_days)
if diff_minutes > 6.93:
print('PY GlobalSolarEclipse({} line {}): EXCESSIVE TIME ERROR = {} minutes'.format(filename, lnum, diff_minutes))
return 1
if diff_minutes > max_minutes:
max_minutes = diff_minutes
# Validate the eclipse kind, but only when it is not a "glancing" eclipse.
if (eclipse.distance < 6360) and (eclipse.kind != expected_kind):
print('PY GlobalSolarEclipse({} line {}): WRONG ECLIPSE KIND: expected {}, found {}'.format(filename, lnum, expected_kind, eclipse.kind))
return 1
if eclipse.kind == astronomy.EclipseKind.Total or eclipse.kind == astronomy.EclipseKind.Annular:
# When the distance between the Moon's shadow ray and the Earth's center is beyond 6100 km,
# it creates a glancing blow whose geographic coordinates are excessively sensitive to
# slight changes in the ray. Therefore, it is unreasonable to count large errors there.
if eclipse.distance < 6100.0:
diff_angle = AngleDiff(lat, lon, eclipse.latitude, eclipse.longitude)
if diff_angle > 0.247:
print('PY GlobalSolarEclipse({} line {}): EXCESSIVE GEOGRAPHIC LOCATION ERROR = {} degrees'.format(filename, lnum, diff_angle))
return 1
if diff_angle > max_angle:
max_angle = diff_angle
eclipse = astronomy.NextGlobalSolarEclipse(eclipse.peak)
if lnum != expected_count:
print('PY GlobalSolarEclipse: WRONG LINE COUNT = {}, expected {}'.format(lnum, expected_count))
return 1
if skip_count > 2:
print('PY GlobalSolarEclipse: EXCESSIVE SKIP COUNT = {}'.format(skip_count))
return 1
print('PY GlobalSolarEclipse: PASS ({} verified, {} skipped, max minutes = {}, max angle = {})'.format(lnum, skip_count, max_minutes, max_angle))
return 0
#-----------------------------------------------------------------------------------------------------------
def LocalSolarEclipse1():
expected_count = 1180
max_minutes = 0.0
skip_count = 0
filename = 'eclipse/solar_eclipse.txt'
with open(filename, 'rt') as infile:
lnum = 0
for line in infile:
lnum += 1
# 1889-12-22T12:54:15Z -6 T -12.7 -12.8
token = line.split()
if len(token) != 5:
print('PY LocalSolarEclipse1({} line {}): invalid token count = {}'.format(filename, lnum, len(token)))
return 1
peak = astronomy.Time.Parse(token[0])
#typeChar = token[2]
lat = float(token[3])
lon = float(token[4])
observer = astronomy.Observer(lat, lon, 0.0)
# Start the search 20 days before we know the eclipse should peak.
search_start = peak.AddDays(-20)
eclipse = astronomy.SearchLocalSolarEclipse(search_start, observer)
# Validate the predicted eclipse peak time.
diff_days = eclipse.peak.time.tt - peak.tt
if diff_days > 20:
skip_count += 1
continue
diff_minutes = (24 * 60) * vabs(diff_days)
if diff_minutes > 7.14:
print('PY LocalSolarEclipse1({} line {}): EXCESSIVE TIME ERROR = {} minutes'.format(filename, lnum, diff_minutes))
return 1
if diff_minutes > max_minutes:
max_minutes = diff_minutes
if lnum != expected_count:
print('PY LocalSolarEclipse1: WRONG LINE COUNT = {}, expected {}'.format(lnum, expected_count))
return 1
if skip_count > 6:
print('PY LocalSolarEclipse1: EXCESSIVE SKIP COUNT = {}'.format(skip_count))
return 1
print('PY LocalSolarEclipse1: PASS ({} verified, {} skipped, max minutes = {})'.format(lnum, skip_count, max_minutes))
return 0
def TrimLine(line):
# Treat '#' as a comment character.
poundIndex = line.find('#')
if poundIndex >= 0:
line = line[:poundIndex]
return line.strip()
def ParseEvent(time_str, alt_str, required):
if required:
time = astronomy.Time.Parse(time_str)
altitude = float(alt_str)
return astronomy.EclipseEvent(time, altitude)
if time_str != '-':
raise Exception('Expected event time to be "-" but found "{}"'.format(time_str))
return None
def LocalSolarEclipse2():
# Test ability to calculate local solar eclipse conditions away from
# the peak position on the Earth.
filename = 'eclipse/local_solar_eclipse.txt'
lnum = 0
verify_count = 0
max_minutes = 0.0
max_degrees = 0.0
def CheckEvent(calc, expect):
nonlocal max_minutes, max_degrees
diff_minutes = (24 * 60) * vabs(expect.time.ut - calc.time.ut)
if diff_minutes > max_minutes:
max_minutes = diff_minutes
if diff_minutes > 1.0:
raise Exception('CheckEvent({} line {}): EXCESSIVE TIME ERROR: {} minutes.'.format(filename, lnum, diff_minutes))
diff_alt = vabs(expect.altitude - calc.altitude)
if diff_alt > max_degrees:
max_degrees = diff_alt
if diff_alt > 0.5:
raise Exception('CheckEvent({} line {}): EXCESSIVE ALTITUDE ERROR: {} degrees.'.format(filename, lnum, diff_alt))
with open(filename, 'rt') as infile:
for line in infile:
lnum += 1
line = TrimLine(line)
if line == '':
continue
token = line.split()
if len(token) != 13:
print('PY LocalSolarEclipse2({} line {}): Incorrect token count = {}'.format(filename, lnum, len(token)))
return 1
latitude = float(token[0])
longitude = float(token[1])
observer = astronomy.Observer(latitude, longitude, 0)
expected_kind = KindFromChar(token[2])
is_umbral = (expected_kind != astronomy.EclipseKind.Partial)
p1 = ParseEvent(token[3], token[4], True)
t1 = ParseEvent(token[5], token[6], is_umbral)
peak = ParseEvent(token[7], token[8], True)
t2 = ParseEvent(token[9], token[10], is_umbral)
p2 = ParseEvent(token[11], token[12], True)
search_time = p1.time.AddDays(-20)
eclipse = astronomy.SearchLocalSolarEclipse(search_time, observer)
if eclipse.kind != expected_kind:
print('PY LocalSolarEclipse2({} line {}): expected eclipse kind "{}" but found "{}".'.format(
filename, lnum, expected_kind, eclipse.kind
))
return 1
CheckEvent(eclipse.peak, peak)
CheckEvent(eclipse.partial_begin, p1)
CheckEvent(eclipse.partial_end, p2)
if is_umbral:
CheckEvent(eclipse.total_begin, t1)
CheckEvent(eclipse.total_end, t2)
verify_count += 1
print('PY LocalSolarEclipse2: PASS ({} verified, max_minutes = {}, max_degrees = {})'.format(verify_count, max_minutes, max_degrees))
return 0
def LocalSolarEclipse():
if 0 != LocalSolarEclipse1():
return 1
if 0 != LocalSolarEclipse2():
return 1
return 0
#-----------------------------------------------------------------------------------------------------------
def TransitFile(body, filename, limit_minutes, limit_sep):
lnum = 0
max_minutes = 0
max_sep = 0
with open(filename, 'rt') as infile:
transit = astronomy.SearchTransit(body, astronomy.Time.Make(1600, 1, 1, 0, 0, 0))
for line in infile:
lnum += 1
token = line.strip().split()
# 22:17 1881-11-08T00:57Z 03:38 3.8633
if len(token) != 4:
print('PY TransitFile({} line {}): bad data format.'.format(filename, lnum))
return 1
textp = token[1]
text1 = textp[0:11] + token[0] + 'Z'
text2 = textp[0:11] + token[2] + 'Z'
timep = astronomy.Time.Parse(textp)
time1 = astronomy.Time.Parse(text1)
time2 = astronomy.Time.Parse(text2)
separation = float(token[3])
# If the start time is after the peak time, it really starts on the previous day.
if time1.ut > timep.ut:
time1 = time1.AddDays(-1.0)
# If the finish time is before the peak time, it really starts on the following day.
if time2.ut < timep.ut:
time2 = time2.AddDays(+1.0)
diff_start = (24.0 * 60.0) * vabs(time1.ut - transit.start.ut )
diff_peak = (24.0 * 60.0) * vabs(timep.ut - transit.peak.ut )
diff_finish = (24.0 * 60.0) * vabs(time2.ut - transit.finish.ut)
diff_sep = vabs(separation - transit.separation)
max_minutes = vmax(max_minutes, diff_start)
max_minutes = vmax(max_minutes, diff_peak)
max_minutes = vmax(max_minutes, diff_finish)
if max_minutes > limit_minutes:
print('PY TransitFile({} line {}): EXCESSIVE TIME ERROR = {} minutes.'.format(filename, lnum, max_minutes))
return 1
max_sep = vmax(max_sep, diff_sep)
if max_sep > limit_sep:
print('PY TransitFile({} line {}): EXCESSIVE SEPARATION ERROR = {} arcminutes.'.format(filename, lnum, max_sep))
return 1
transit = astronomy.NextTransit(body, transit.finish)
print('PY TransitFile({}): PASS - verified {}, max minutes = {}, max sep arcmin = {}'.format(filename, lnum, max_minutes, max_sep))
return 0
def Transit():
if 0 != TransitFile(astronomy.Body.Mercury, 'eclipse/mercury.txt', 10.710, 0.2121):
return 1
if 0 != TransitFile(astronomy.Body.Venus, 'eclipse/venus.txt', 9.109, 0.6772):
return 1
return 0
#-----------------------------------------------------------------------------------------------------------
def PlutoCheckDate(ut, arcmin_tolerance, x, y, z):
time = astronomy.Time(ut)
try:
timeText = str(time)
except OverflowError:
timeText = "???"
Debug('PY PlutoCheck: {} = {} UT = {} TT'.format(timeText, time.ut, time.tt))
vector = astronomy.HelioVector(astronomy.Body.Pluto, time)
dx = v(vector.x - x)
dy = v(vector.y - y)
dz = v(vector.z - z)
diff = sqrt(dx*dx + dy*dy + dz*dz)
dist = sqrt(x*x + y*y + z*z) - 1.0
arcmin = (diff / dist) * (180.0 * 60.0 / math.pi)
Debug('PY PlutoCheck: calc pos = [{}, {}, {}]'.format(vector.x, vector.y, vector.z))
Debug('PY PlutoCheck: ref pos = [{}, {}, {}]'.format(x, y, z))
Debug('PY PlutoCheck: del pos = [{}, {}, {}]'.format(vector.x - x, vector.y - y, vector.z - z))
Debug('PY PlutoCheck: diff = {} AU, {} arcmin'.format(diff, arcmin))
if v(arcmin) > arcmin_tolerance:
print('PY PlutoCheck: EXCESSIVE ERROR')
return 1
Debug('')
return 0
def PlutoCheck():
if PlutoCheckDate( +18250.0, 0.089, +37.4377303523676090, -10.2466292454075898, -14.4773101310875809): return 1
if PlutoCheckDate( -856493.0, 4.067, +23.4292113199166252, +42.1452685817740829, +6.0580908436642940): return 1
if PlutoCheckDate( +435633.0, 0.016, -27.3178902095231813, +18.5887022581070305, +14.0493896259306936): return 1
if PlutoCheckDate( 0.0, 8e-9, -9.8753673425269000, -27.9789270580402771, -5.7537127596369588): return 1
if PlutoCheckDate( +800916.0, 2.286, -29.5266052645301365, +12.0554287322176474, +12.6878484911631091): return 1
print("PY PlutoCheck: PASS")
return 0
#-----------------------------------------------------------------------------------------------------------
def GeoidTestCase(time, observer, ofdate):
topo_moon = astronomy.Equator(astronomy.Body.Moon, time, observer, ofdate, False)
surface = astronomy.ObserverVector(time, observer, ofdate)
geo_moon = astronomy.GeoVector(astronomy.Body.Moon, time, False)
if ofdate:
# GeoVector() returns J2000 coordinates. Convert to equator-of-date coordinates.
rot = astronomy.Rotation_EQJ_EQD(time)
geo_moon = astronomy.RotateVector(rot, geo_moon)
dx = astronomy.KM_PER_AU * v((geo_moon.x - surface.x) - topo_moon.vec.x)
dy = astronomy.KM_PER_AU * v((geo_moon.y - surface.y) - topo_moon.vec.y)
dz = astronomy.KM_PER_AU * v((geo_moon.z - surface.z) - topo_moon.vec.z)
diff = sqrt(dx*dx + dy*dy + dz*dz)
Debug('PY GeoidTestCase: ofdate={}, time={}, obs={}, surface=({}, {}, {}), diff = {} km'.format(
ofdate,
time,
observer,
astronomy.KM_PER_AU * surface.x,
astronomy.KM_PER_AU * surface.y,
astronomy.KM_PER_AU * surface.z,
diff
))
# Require 1 millimeter accuracy! (one millionth of a kilometer).
if diff > 1.0e-6:
print('PY GeoidTestCase: EXCESSIVE POSITION ERROR.')
return 1
# Verify that we can convert the surface vector back to an observer.
vobs = astronomy.VectorObserver(surface, ofdate)
lat_diff = vabs(vobs.latitude - observer.latitude)
# Longitude is meaningless at the poles, so don't bother checking it there.
if -89.99 <= observer.latitude <= +89.99:
lon_diff = vabs(vobs.longitude - observer.longitude)
if lon_diff > 180.0:
lon_diff = 360.0 - lon_diff
lon_diff = vabs(lon_diff * math.cos(math.degrees(observer.latitude)))
if lon_diff > 1.0e-6:
print('PY GeoidTestCase: EXCESSIVE longitude check error = {}'.format(lon_diff))
return 1
else:
lon_diff = 0.0
h_diff = vabs(vobs.height - observer.height)
Debug('PY GeoidTestCase: vobs={}, lat_diff={}, lon_diff={}, h_diff={}'.format(vobs, lat_diff, lon_diff, h_diff))
if lat_diff > 1.0e-6:
print('PY GeoidTestCase: EXCESSIVE latitude check error = {}'.format(lat_diff))
return 1
if h_diff > 0.001:
print('PY GeoidTestCase: EXCESSIVE height check error = {}'.format(h_diff))
return 1
return 0
def Geoid():
time_list = [
astronomy.Time.Parse('1066-09-27T18:00:00Z'),
astronomy.Time.Parse('1970-12-13T15:42:00Z'),
astronomy.Time.Parse('1970-12-13T15:43:00Z'),
astronomy.Time.Parse('2015-03-05T02:15:45Z')
]
observer_list = [
astronomy.Observer( 0.0, 0.0, 0.0),
astronomy.Observer( +1.5, +2.7, 7.4),
astronomy.Observer( -1.5, -2.7, 7.4),
astronomy.Observer(-53.7, +141.7, 100.0),
astronomy.Observer(+30.0, -85.2, -50.0),
astronomy.Observer(+90.0, +45.0, -50.0),
astronomy.Observer(-90.0, -180.0, 0.0),
astronomy.Observer(-89.0, -81.0, 1234.0),
astronomy.Observer(+89.0, -103.4, 279.8),
astronomy.Observer(+48.2, 24.5, 2019.0),
astronomy.Observer(+28.5, -82.3, -3.4)
]
# Test hand-crafted locations.
for observer in observer_list:
for time in time_list:
if GeoidTestCase(time, observer, False):
return 1
if GeoidTestCase(time, observer, True):
return 1
# More exhaustive tests for a single time value across many different geographic coordinates.
# Solving for latitude is the most complicated part of VectorObserver, so
# I test for every 1-degree increment of latitude, but with 5-degree increments for longitude.
time = astronomy.Time.Parse('2021-06-20T15:08:00Z')
lat = -90
while lat <= +90:
lon = -175
while lon <= +180:
observer = astronomy.Observer(lat, lon, 0.0)
if GeoidTestCase(time, observer, True):
return 1
lon += 5
lat += 1
print('PY GeoidTest: PASS')
return 0
#-----------------------------------------------------------------------------------------------------------
def JupiterMoons_CheckJpl(mindex, tt, pos, vel):
pos_tolerance = 9.0e-4
vel_tolerance = 9.0e-4
time = astronomy.Time.FromTerrestrialTime(tt)
jm = astronomy.JupiterMoons(time)
dx = v(pos[0] - jm.moon[mindex].x)
dy = v(pos[1] - jm.moon[mindex].y)
dz = v(pos[2] - jm.moon[mindex].z)
mag = sqrt(pos[0]*pos[0] + pos[1]*pos[1] + pos[2]*pos[2])
pos_diff = sqrt(dx*dx + dy*dy + dz*dz) / mag
if pos_diff > pos_tolerance:
print('PY JupiterMoons_CheckJpl(mindex={}, tt={}): excessive position error {}'.format(mindex, tt, pos_diff))
return 1
dx = v(vel[0] - jm.moon[mindex].vx)
dy = v(vel[1] - jm.moon[mindex].vy)
dz = v(vel[2] - jm.moon[mindex].vz)
mag = sqrt(vel[0]*vel[0] + vel[1]*vel[1] + vel[2]*vel[2])
vel_diff = sqrt(dx*dx + dy*dy + dz*dz) / mag
if vel_diff > vel_tolerance:
print('PY JupiterMoons_CheckJpl(mindex={}, tt={}): excessive velocity error {}'.format(mindex, tt, vel_diff))
return 1
Debug('PY JupiterMoons_CheckJpl: mindex={}, tt={}, pos_diff={}, vel_diff={}'.format(mindex, tt, pos_diff, vel_diff))
return 0
def JupiterMoons():
for mindex in range(4):
filename = 'jupiter_moons/horizons/jm{}.txt'.format(mindex)
with open(filename, 'rt') as infile:
lnum = 0
found = False
part = -1
expected_count = 5001
count = 0
for line in infile:
line = line.rstrip()
lnum += 1
if not found:
if line == '$$SOE':
found = True
part = 0
elif line.startswith('Revised:'):
check_mindex = int(line[76:]) - 501
if mindex != check_mindex:
print('PY JupiterMoons({} line {}): moon index does not match: check={}, mindex={}'.format(filename, lnum, check_mindex, mindex))
return 1
elif line == '$$EOE':
break
else:
if part == 0:
# 2446545.000000000 = A.D. 1986-Apr-24 12:00:00.0000 TDB
tt = float(line.split()[0]) - 2451545.0 # convert JD to J2000 TT
elif part == 1:
# X = 1.134408131605554E-03 Y =-2.590904586750408E-03 Z =-7.490427225904720E-05
match = re.match(r'\s*X =\s*(\S+) Y =\s*(\S+) Z =\s*(\S+)', line)
if not match:
print('PY JupiterMoons({} line {}): cannot parse position vector.'.format(filename, lnum))
return 1
pos = [ float(match.group(1)), float(match.group(2)), float(match.group(3)) ]
else: # part == 2
# VX= 9.148038778472862E-03 VY= 3.973823407182510E-03 VZ= 2.765660368640458E-04
match = re.match(r'\s*VX=\s*(\S+) VY=\s*(\S+) VZ=\s*(\S+)', line)
if not match:
print('PY JupiterMoons({} line {}): cannot parse velocity vector.'.format(filename, lnum))
return 1
vel = [ float(match.group(1)), float(match.group(2)), float(match.group(3)) ]
if JupiterMoons_CheckJpl(mindex, tt, pos, vel):
print('PY JupiterMoons({} line {}): FAILED VERIFICATION.'.format(filename, lnum))
return 1
count += 1
part = (part + 1) % 3
if count != expected_count:
print('PY JupiterMoons: expected {} test cases, but found {}'.format(expected_count, count))
return 1
print('PY JupiterMoons: PASS')
return 0
#-----------------------------------------------------------------------------------------------------------
def Issue103():
# https://github.com/cosinekitty/astronomy/issues/103
observer = astronomy.Observer(29, -81, 10)
ut = -8.817548982869034808e+04
time = astronomy.Time(ut)
body = astronomy.Body.Venus
ofdate = astronomy.Equator(body, time, observer, True, True)
hor = astronomy.Horizon(time, observer, ofdate.ra, ofdate.dec, astronomy.Refraction.Airless)
print('tt = {:23.16f}'.format(time.tt))
print('az = {:23.16f}'.format(hor.azimuth))
print('alt = {:23.16f}'.format(hor.altitude))
return 0
#-----------------------------------------------------------------------------------------------------------
class _bary_stats_t:
def __init__(self):
self.max_rdiff = 0.0
self.max_vdiff = 0.0
def StateVectorDiff(relative, vec, x, y, z):
dx = v(vec[0] - x)
dy = v(vec[1] - y)
dz = v(vec[2] - z)
diff_squared = dx*dx + dy*dy + dz*dz
if relative:
diff_squared /= (vec[0]*vec[0] + vec[1]*vec[1] + vec[2]*vec[2])
return sqrt(diff_squared)
#-----------------------------------------------------------------------------------------------------------
def VerifyState(func, stats, body, filename, lnum, time, pos, vel, r_thresh, v_thresh):
state = func(body, time)
rdiff = StateVectorDiff((r_thresh > 0.0), pos, state.x, state.y, state.z)
if rdiff > stats.max_rdiff:
stats.max_rdiff = rdiff
vdiff = StateVectorDiff((v_thresh > 0.0), vel, state.vx, state.vy, state.vz)
if vdiff > stats.max_vdiff:
stats.max_vdiff = vdiff
if rdiff > abs(r_thresh):
print('PY VerifyState({} line {}): EXCESSIVE position error = {:0.4e}'.format(filename, lnum, rdiff))
return 1
if vdiff > abs(v_thresh):
print('PY VerifyState({} line {}): EXCESSIVE velocity error = {:0.4e}'.format(filename, lnum, vdiff))
return 1
return 0
def VerifyStateBody(func, body, filename, r_thresh, v_thresh):
with open(filename, 'rt') as infile:
lnum = 0
part = 0
count = 0
found_begin = False
stats = _bary_stats_t()
for line in infile:
line = line.rstrip()
lnum += 1
if not found_begin:
if line == '$$SOE':
found_begin = True
elif line == '$$EOE':
break
else:
if part == 0:
# 2446545.000000000 = A.D. 1986-Apr-24 12:00:00.0000 TDB
tt = float(line.split()[0]) - 2451545.0 # convert JD to J2000 TT
time = astronomy.Time.FromTerrestrialTime(tt)
elif part == 1:
# X = 1.134408131605554E-03 Y =-2.590904586750408E-03 Z =-7.490427225904720E-05
match = re.match(r'\s*X =\s*(\S+) Y =\s*(\S+) Z =\s*(\S+)', line)
if not match:
print('PY VerifyStateBody({} line {}): cannot parse position vector.'.format(filename, lnum))
return 1
pos = [ float(match.group(1)), float(match.group(2)), float(match.group(3)) ]
else: # part == 2
# VX= 9.148038778472862E-03 VY= 3.973823407182510E-03 VZ= 2.765660368640458E-04
match = re.match(r'\s*VX=\s*(\S+) VY=\s*(\S+) VZ=\s*(\S+)', line)
if not match:
print('PY VerifyStateBody({} line {}): cannot parse velocity vector.'.format(filename, lnum))
return 1
vel = [ float(match.group(1)), float(match.group(2)), float(match.group(3)) ]
if VerifyState(func, stats, body, filename, lnum, time, pos, vel, r_thresh, v_thresh):
print('PY VerifyStateBody({} line {}): FAILED VERIFICATION.'.format(filename, lnum))
return 1
count += 1
part = (part + 1) % 3
Debug('PY VerifyStateBody({}): PASS - Tested {} cases. max rdiff={:0.3e}, vdiff={:0.3e}'.format(filename, count, stats.max_rdiff, stats.max_vdiff))
return 0
#-----------------------------------------------------------------------------------------------------------
# Constants for use inside unit tests only; they doesn't make sense for public consumption.
_Body_GeoMoon = -100
_Body_Geo_EMB = -101
def BaryStateFunc(body, time):
if body == _Body_GeoMoon:
return astronomy.GeoMoonState(time)
if body == _Body_Geo_EMB:
return astronomy.GeoEmbState(time)
return astronomy.BaryState(body, time)
def BaryState():
if VerifyStateBody(BaryStateFunc, astronomy.Body.Sun, 'barystate/Sun.txt', -1.224e-05, -1.134e-07): return 1
if VerifyStateBody(BaryStateFunc, astronomy.Body.Mercury, 'barystate/Mercury.txt', 1.672e-04, 2.698e-04): return 1
if VerifyStateBody(BaryStateFunc, astronomy.Body.Venus, 'barystate/Venus.txt', 4.123e-05, 4.308e-05): return 1
if VerifyStateBody(BaryStateFunc, astronomy.Body.Earth, 'barystate/Earth.txt', 2.296e-05, 6.359e-05): return 1
if VerifyStateBody(BaryStateFunc, astronomy.Body.Mars, 'barystate/Mars.txt', 3.107e-05, 5.550e-05): return 1
if VerifyStateBody(BaryStateFunc, astronomy.Body.Jupiter, 'barystate/Jupiter.txt', 7.389e-05, 2.471e-04): return 1
if VerifyStateBody(BaryStateFunc, astronomy.Body.Saturn, 'barystate/Saturn.txt', 1.067e-04, 3.220e-04): return 1
if VerifyStateBody(BaryStateFunc, astronomy.Body.Uranus, 'barystate/Uranus.txt', 9.035e-05, 2.519e-04): return 1
if VerifyStateBody(BaryStateFunc, astronomy.Body.Neptune, 'barystate/Neptune.txt', 9.838e-05, 4.446e-04): return 1
if VerifyStateBody(BaryStateFunc, astronomy.Body.Pluto, 'barystate/Pluto.txt', 4.259e-05, 7.827e-05): return 1
if VerifyStateBody(BaryStateFunc, astronomy.Body.Moon, "barystate/Moon.txt", 2.354e-05, 6.604e-05): return 1
if VerifyStateBody(BaryStateFunc, astronomy.Body.EMB, "barystate/EMB.txt", 2.353e-05, 6.511e-05): return 1
if VerifyStateBody(BaryStateFunc, _Body_GeoMoon, "barystate/GeoMoon.txt", 4.086e-05, 5.347e-05): return 1
if VerifyStateBody(BaryStateFunc, _Body_Geo_EMB, "barystate/GeoEMB.txt", 4.076e-05, 5.335e-05): return 1
print('PY BaryState: PASS')
return 0
#-----------------------------------------------------------------------------------------------------------
def HelioState():
if VerifyStateBody(astronomy.HelioState, astronomy.Body.SSB, 'heliostate/SSB.txt', -1.209e-05, -1.125e-07): return 1
if VerifyStateBody(astronomy.HelioState, astronomy.Body.Mercury, 'heliostate/Mercury.txt', 1.481e-04, 2.756e-04): return 1
if VerifyStateBody(astronomy.HelioState, astronomy.Body.Venus, 'heliostate/Venus.txt', 3.528e-05, 4.485e-05): return 1
if VerifyStateBody(astronomy.HelioState, astronomy.Body.Earth, 'heliostate/Earth.txt', 1.476e-05, 6.105e-05): return 1
if VerifyStateBody(astronomy.HelioState, astronomy.Body.Mars, 'heliostate/Mars.txt', 3.154e-05, 5.603e-05): return 1
if VerifyStateBody(astronomy.HelioState, astronomy.Body.Jupiter, 'heliostate/Jupiter.txt', 7.455e-05, 2.562e-04): return 1
if VerifyStateBody(astronomy.HelioState, astronomy.Body.Saturn, 'heliostate/Saturn.txt', 1.066e-04, 3.150e-04): return 1
if VerifyStateBody(astronomy.HelioState, astronomy.Body.Uranus, 'heliostate/Uranus.txt', 9.034e-05, 2.712e-04): return 1
if VerifyStateBody(astronomy.HelioState, astronomy.Body.Neptune, 'heliostate/Neptune.txt', 9.834e-05, 4.534e-04): return 1
if VerifyStateBody(astronomy.HelioState, astronomy.Body.Pluto, 'heliostate/Pluto.txt', 4.271e-05, 1.198e-04): return 1
if VerifyStateBody(astronomy.HelioState, astronomy.Body.Moon, 'heliostate/Moon.txt', 1.477e-05, 6.195e-05): return 1
if VerifyStateBody(astronomy.HelioState, astronomy.Body.EMB, 'heliostate/EMB.txt', 1.476e-05, 6.106e-05): return 1
print('PY HelioState: PASS')
return 0
#-----------------------------------------------------------------------------------------------------------
def TopoStateFunc(body, time):
observer = astronomy.Observer(30.0, -80.0, 1000.0)
observer_state = astronomy.ObserverState(time, observer, False)
if body == _Body_Geo_EMB:
state = astronomy.GeoEmbState(time)
elif body == astronomy.Body.Earth:
state = astronomy.StateVector(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, time)
else:
raise Exception('PY TopoStateFunction: unsupported body ' + body)
state.x -= observer_state.x
state.y -= observer_state.y
state.z -= observer_state.z
state.vx -= observer_state.vx
state.vy -= observer_state.vy
state.vz -= observer_state.vz
return state
def TopoState():
if VerifyStateBody(TopoStateFunc, astronomy.Body.Earth, 'topostate/Earth_N30_W80_1000m.txt', 2.108e-04, 2.430e-04): return 1
if VerifyStateBody(TopoStateFunc, _Body_Geo_EMB, 'topostate/EMB_N30_W80_1000m.txt', 7.195e-04, 2.497e-04): return 1
print('PY TopoState: PASS')
return 0
#-----------------------------------------------------------------------------------------------------------
def Aberration():
THRESHOLD_SECONDS = 0.4
filename = 'equatorial/Mars_j2000_ofdate_aberration.txt'
count = 0
with open(filename, 'rt') as infile:
lnum = 0
found_begin = False
max_diff_seconds = 0.0
for line in infile:
lnum += 1
line = line.rstrip()
if not found_begin:
if line == '$$SOE':
found_begin = True
elif line == '$$EOE':
break
else:
# 2459371.500000000 * 118.566080210 22.210647456 118.874086738 22.155784122
token = line.split()
if len(token) < 5:
print('PY Aberration({} line {}): not enough tokens'.format(filename, lnum))
return 1
jd = float(token[0])
jra = float(token[-4])
jdec = float(token[-3])
dra = float(token[-2])
ddec = float(token[-1])
# Convert julian day value to AstroTime.
time = astronomy.Time(jd - 2451545.0)
# Convert EQJ angular coordinates (jra, jdec) to an EQJ vector.
# Make the maginitude of the vector the speed of light,
# to prepare for aberration correction.
eqj_sphere = astronomy.Spherical(jdec, jra, astronomy.C_AUDAY)
eqj_vec = astronomy.VectorFromSphere(eqj_sphere, time)
# Aberration correction: calculate the Earth's barycentric
# velocity vector in EQJ coordinates.
eqj_earth = astronomy.BaryState(astronomy.Body.Earth, time)
# Use non-relativistic approximation: add light vector to Earth velocity vector.
# This gives aberration-corrected apparent position of the start in EQJ.
eqj_vec.x += eqj_earth.vx
eqj_vec.y += eqj_earth.vy
eqj_vec.z += eqj_earth.vz
# Calculate the rotation matrix that converts J2000 coordinates to of-date coordinates.
rot = astronomy.Rotation_EQJ_EQD(time)
# Use the rotation matrix to re-orient the EQJ vector to an EQD vector.
eqd_vec = astronomy.RotateVector(rot, eqj_vec)
# Convert the EQD vector back to spherical angular coordinates.
eqd_sphere = astronomy.SphereFromVector(eqd_vec)
# Calculate the differences in RA and DEC between expected and calculated values.
factor = math.cos(math.radians(v(eqd_sphere.lat))) # RA errors are less important toward the poles.
xra = factor * vabs(eqd_sphere.lon - dra)
xdec = vabs(eqd_sphere.lat - ddec)
diff_seconds = 3600.0 * sqrt(xra*xra + xdec*xdec)
Debug('PY Aberration({} line {}): xra={:0.6f} deg, xdec={:0.6f} deg, diff_seconds={:0.3f}.'.format(filename, lnum, xra, xdec, diff_seconds))
if diff_seconds > THRESHOLD_SECONDS:
print('PY Aberration({} line {}): EXCESSIVE ANGULAR ERROR = {:0.3f} seconds.'.format(filename, lnum, diff_seconds));
return 1
if diff_seconds > max_diff_seconds:
max_diff_seconds = diff_seconds
# We have completed one more test case.
count += 1
print('PY AberrationTest({}): PASS - Tested {} cases. max_diff_seconds = {:0.3f}'.format(filename, count, max_diff_seconds))
return 0
#-----------------------------------------------------------------------------------------------------------
def Twilight():
tolerance_seconds = 60.0
max_diff = 0.0
filename = 'riseset/twilight.txt'
with open(filename, 'rt') as infile:
lnum = 0
for line in infile:
lnum += 1
tokens = line.split()
if len(tokens) != 9:
print('PY Twilight({} line {}): incorrect number of tokens = {}'.format(filename, lnum, len(tokens)))
return 1
observer = astronomy.Observer(float(tokens[0]), float(tokens[1]), 0.0)
searchDate = astronomy.Time.Parse(tokens[2])
correctTimes = [astronomy.Time.Parse(t) for t in tokens[3:]]
calcTimes = [
astronomy.SearchAltitude(astronomy.Body.Sun, observer, astronomy.Direction.Rise, searchDate, 1.0, -18.0), # astronomical dawn
astronomy.SearchAltitude(astronomy.Body.Sun, observer, astronomy.Direction.Rise, searchDate, 1.0, -12.0), # nautical dawn
astronomy.SearchAltitude(astronomy.Body.Sun, observer, astronomy.Direction.Rise, searchDate, 1.0, -6.0), # civil dawn
astronomy.SearchAltitude(astronomy.Body.Sun, observer, astronomy.Direction.Set, searchDate, 1.0, -6.0), # civil dusk
astronomy.SearchAltitude(astronomy.Body.Sun, observer, astronomy.Direction.Set, searchDate, 1.0, -12.0), # nautical dusk
astronomy.SearchAltitude(astronomy.Body.Sun, observer, astronomy.Direction.Set, searchDate, 1.0, -18.0) # astronomical dusk
]
for i in range(6):
correct = correctTimes[i]
calc = calcTimes[i]
diff = 86400.0 * vabs(calc.ut - correct.ut)
if diff > tolerance_seconds:
print('PY Twilight({} line {}): EXCESSIVE ERROR = {} seconds for case {}'.format(filename, lnum, diff, i))
return 1
if diff > max_diff:
max_diff = diff
print('PY Twilight: PASS ({} test cases, max error = {} seconds)'.format(lnum, max_diff))
return 0
#-----------------------------------------------------------------------------------------------------------
def LibrationFile(filename):
max_diff_elon = 0.0
max_diff_elat = 0.0
max_diff_distance = 0.0
max_diff_diam = 0.0
count = 0
with open(filename, 'rt') as infile:
lnum = 0
for line in infile:
lnum += 1
token = line.split()
if lnum == 1:
if line != " Date Time Phase Age Diam Dist RA Dec Slon Slat Elon Elat AxisA\n":
print('PY LibrationFile({} line {}): unexpected header line'.format(filename, lnum))
return 1
else:
if len(token) != 16:
print('PY LibrationFile({} line {}): expected 16 tokens, found {}'.format(filename, lnum, len(token)))
return 1
day = int(token[0])
month = MonthNumber(token[1])
year = int(token[2])
hmtoken = token[3].split(':')
if len(hmtoken) != 2:
print('PY LibrationFile({} line {}): expected hh:mm but found "{}"'.format(filename, lnum, hmtoken))
return 1
hour = int(hmtoken[0])
minute = int(hmtoken[1])
time = astronomy.Time.Make(year, month, day, hour, minute, 0.0)
diam = float(token[7]) / 3600.0
dist = float(token[8])
elon = float(token[13])
elat = float(token[14])
lib = astronomy.Libration(time)
diff_elon = 60.0 * vabs(lib.elon - elon)
if diff_elon > max_diff_elon:
max_diff_elon = diff_elon
diff_elat = 60.0 * vabs(lib.elat - elat)
if diff_elat > max_diff_elat:
max_diff_elat = diff_elat
diff_distance = vabs(lib.dist_km - dist)
if diff_distance > max_diff_distance:
max_diff_distance = diff_distance
diff_diam = vabs(lib.diam_deg - diam)
if diff_diam > max_diff_diam:
max_diff_diam = diff_diam
if diff_elon > 0.130:
print('PY LibrationFile({} line {}): EXCESSIVE diff_elon = {}'.format(filename, lnum, diff_elon))
return 1
if diff_elat > 1.666:
print('PY LibrationFile({} line {}): EXCESSIVE diff_elat = {}'.format(filename, lnum, diff_elat))
return 1
if diff_distance > 53.9:
print('PY LibrationFile({} line {}): EXCESSIVE diff_distance = {}'.format(filename, lnum, diff_distance))
return 1
count += 1
print('PY Libration({}): PASS ({} test cases, max_diff_elon = {} arcmin, max_diff_elat = {} arcmin, max_diff_distance = {} km, max_diff_diam = {} deg)'.format(
filename, count, max_diff_elon, max_diff_elat, max_diff_distance, max_diff_diam
))
return 0
def Libration():
if 0 != LibrationFile('libration/mooninfo_2020.txt'):
return 1
if 0 != LibrationFile('libration/mooninfo_2021.txt'):
return 1
return 0
#-----------------------------------------------------------------------------------------------------------
UnitTests = {
'aberration': Aberration,
'barystate': BaryState,
'constellation': Constellation,
'elongation': Elongation,
'geoid': Geoid,
'global_solar_eclipse': GlobalSolarEclipse,
'heliostate': HelioState,
'issue_103': Issue103,
'jupiter_moons': JupiterMoons,
'libration': Libration,
'local_solar_eclipse': LocalSolarEclipse,
'lunar_apsis': LunarApsis,
'lunar_eclipse': LunarEclipse,
'lunar_eclipse_78': LunarEclipseIssue78,
'magnitude': Magnitude,
'moon': GeoMoon,
'moonphase': MoonPhase,
'planet_apsis': PlanetApsis,
'pluto': PlutoCheck,
'refraction': Refraction,
'riseset': RiseSet,
'rotation': Rotation,
'seasons': Seasons,
'time': AstroTime,
'topostate': TopoState,
'transit': Transit,
'twilight': Twilight,
}
#-----------------------------------------------------------------------------------------------------------
if __name__ == '__main__':
if len(sys.argv) > 1 and sys.argv[1] == '-v':
sys.argv = sys.argv[1:]
Verbose = True
if len(sys.argv) == 2:
name = sys.argv[1]
if name in UnitTests:
sys.exit(UnitTests[name]())
if name in ['astro_check', 'astro_profile']:
sys.exit(AstroCheck(sys.argv[1] == 'astro_check'))
if name == 'all':
for name in sorted(UnitTests.keys()):
func = UnitTests[name]
Debug('test.py: Starting test "{}"'.format(name))
rc = func()
Debug('test.py: Test "{}" returned {}'.format(name, rc))
if rc != 0:
sys.exit(1)
print('test.py: ALL PASS')
sys.exit(0)
print('test.py: Invalid command line arguments.')
sys.exit(1)