using System; using System.IO; using System.Text.RegularExpressions; using CosineKitty; namespace csharp_test { class Program { static int Main(string[] args) { try { Console.WriteLine("csharp_test: starting"); if (TestTime() != 0) return 1; if (MoonTest() != 0) return 1; if (RiseSetTest("../../riseset/riseset.txt") != 0) return 1; if (SeasonsTest("../../seasons/seasons.txt") != 0) return 1; if (MoonPhaseTest("../../moonphase/moonphases.txt") != 0) return 1; if (ElongationTest() != 0) return 1; if (LunarApsisTest("../../apsides/moon.txt") != 0) return 1; if (MagnitudeTest() != 0) return 1; if (AstroCheck() != 0) return 1; Console.WriteLine("csharp_test: PASS"); return 0; } catch (Exception ex) { Console.WriteLine("charp_test: EXCEPTION: {0}", ex); return 1; } } static int TestTime() { const int year = 2018; const int month = 12; const int day = 2; const int hour = 18; const int minute = 30; const int second = 12; const int milli = 543; DateTime d = new DateTime(year, month, day, hour, minute, second, milli, DateTimeKind.Utc); AstroTime time = new AstroTime(d); Console.WriteLine("TestTime: text={0}, ut={1}, tt={2}", time.ToString(), time.ut.ToString("F6"), time.tt.ToString("F6")); const double expected_ut = 6910.270978506945; double diff = time.ut - expected_ut; if (Math.Abs(diff) > 1.0e-12) { Console.WriteLine("TestTime: ERROR - excessive UT error {0}", diff); return 1; } const double expected_tt = 6910.271779431480; diff = time.tt - expected_tt; if (Math.Abs(diff) > 1.0e-12) { Console.WriteLine("TestTime: ERROR - excessive TT error {0}", diff); return 1; } DateTime utc = time.ToUtcDateTime(); if (utc.Year != year || utc.Month != month || utc.Day != day || utc.Hour != hour || utc.Minute != minute || utc.Second != second || utc.Millisecond != milli) { Console.WriteLine("TestTime: ERROR - Expected {0:o}, found {1:o}", d, utc); return 1; } return 0; } static int MoonTest() { var time = new AstroTime(2019, 6, 24, 15, 45, 37); AstroVector vec = Astronomy.GeoVector(Body.Moon, time, Aberration.None); Console.WriteLine("MoonTest: {0} {1} {2}", vec.x.ToString("f17"), vec.y.ToString("f17"), vec.z.ToString("f17")); double dx = vec.x - (+0.002674036155459549); double dy = vec.y - (-0.0001531716308218381); double dz = vec.z - (-0.0003150201604895409); double diff = Math.Sqrt(dx*dx + dy*dy + dz*dz); Console.WriteLine("MoonTest: diff = {0}", diff.ToString("g5")); if (diff > 4.34e-19) { Console.WriteLine("MoonTest: EXCESSIVE ERROR"); return 1; } return 0; } static int AstroCheck() { const string filename = "csharp_check.txt"; using (StreamWriter outfile = File.CreateText(filename)) { var bodylist = new Body[] { Body.Sun, Body.Mercury, Body.Venus, Body.Earth, Body.Mars, Body.Jupiter, Body.Saturn, Body.Uranus, Body.Neptune, Body.Pluto }; var observer = new Observer(29.0, -81.0, 10.0); var time = new AstroTime(new DateTime(1700, 1, 1, 0, 0, 0, DateTimeKind.Utc)); var stop = new AstroTime(new DateTime(2200, 1, 1, 0, 0, 0, DateTimeKind.Utc)); AstroVector pos; Equatorial j2000, ofdate; Topocentric hor; outfile.WriteLine("o {0} {1} {2}", observer.latitude, observer.longitude, observer.height); while (time.tt < stop.tt) { foreach (Body body in bodylist) { pos = Astronomy.HelioVector(body, time); outfile.WriteLine("v {0} {1} {2} {3} {4}", body, pos.t.tt.ToString("G17"), pos.x.ToString("G17"), pos.y.ToString("G17"), pos.z.ToString("G17")); if (body != Body.Earth) { j2000 = Astronomy.Equator(body, time, observer, EquatorEpoch.J2000, Aberration.None); ofdate = Astronomy.Equator(body, time, observer, EquatorEpoch.OfDate, Aberration.Corrected); hor = Astronomy.Horizon(time, observer, ofdate.ra, ofdate.dec, Refraction.None); outfile.WriteLine("s {0} {1} {2} {3} {4} {5} {6} {7}", body, time.tt.ToString("G17"), time.ut.ToString("G17"), j2000.ra.ToString("G17"), j2000.dec.ToString("G17"), j2000.dist.ToString("G17"), hor.azimuth.ToString("G17"), hor.altitude.ToString("G17")); } } pos = Astronomy.GeoVector(Body.Moon, time, Aberration.None); outfile.WriteLine("v GM {0} {1} {2} {3}", pos.t.tt.ToString("G17"), pos.x.ToString("G17"), pos.y.ToString("G17"), pos.z.ToString("G17")); j2000 = Astronomy.Equator(Body.Moon, time, observer, EquatorEpoch.J2000, Aberration.None); ofdate = Astronomy.Equator(Body.Moon, time, observer, EquatorEpoch.OfDate, Aberration.Corrected); hor = Astronomy.Horizon(time, observer, ofdate.ra, ofdate.dec, Refraction.None); outfile.WriteLine("s GM {0} {1} {2} {3} {4} {5} {6}", time.tt.ToString("G17"), time.ut.ToString("G17"), j2000.ra.ToString("G17"), j2000.dec.ToString("G17"), j2000.dist.ToString("G17"), hor.azimuth.ToString("G17"), hor.altitude.ToString("G17")); time = time.AddDays(10.0 + Math.PI/100.0); } } Console.WriteLine("AstroCheck: finished"); return 0; } static int SeasonsTest(string filename) { var re = new Regex(@"^(\d+)-(\d+)-(\d+)T(\d+):(\d+)Z\s+([A-Za-z]+)\s*$"); using (StreamReader infile = File.OpenText(filename)) { string line; int lnum = 0; int current_year = 0; int mar_count=0, jun_count=0, sep_count=0, dec_count=0; double max_minutes = 0.0; SeasonsInfo seasons = new SeasonsInfo(); while (null != (line = infile.ReadLine())) { ++lnum; /* 2019-01-03T05:20Z Perihelion 2019-03-20T21:58Z Equinox 2019-06-21T15:54Z Solstice 2019-07-04T22:11Z Aphelion 2019-09-23T07:50Z Equinox 2019-12-22T04:19Z Solstice */ Match m = re.Match(line); if (!m.Success) { Console.WriteLine("SeasonsTest: ERROR {0} line {1}: cannot parse", filename, lnum); return 1; } int year = int.Parse(m.Groups[1].Value); int month = int.Parse(m.Groups[2].Value); int day = int.Parse(m.Groups[3].Value); int hour = int.Parse(m.Groups[4].Value); int minute = int.Parse(m.Groups[5].Value); string name = m.Groups[6].Value; var correct_time = new AstroTime(year, month, day, hour, minute, 0); if (year != current_year) { current_year = year; seasons = Astronomy.Seasons(year); } AstroTime calc_time = null; if (name == "Equinox") { switch (month) { case 3: calc_time = seasons.mar_equinox; ++mar_count; break; case 9: calc_time = seasons.sep_equinox; ++sep_count; break; default: Console.WriteLine("SeasonsTest: {0} line {1}: Invalid equinox date in test data.", filename, lnum); return 1; } } else if (name == "Solstice") { switch (month) { case 6: calc_time = seasons.jun_solstice; ++jun_count; break; case 12: calc_time = seasons.dec_solstice; ++dec_count; break; default: Console.WriteLine("SeasonsTest: {0} line {1}: Invalid solstice date in test data.", filename, lnum); return 1; } } else if (name == "Aphelion") { /* not yet calculated */ continue; } else if (name == "Perihelion") { /* not yet calculated */ continue; } else { Console.WriteLine("SeasonsTest: {0} line {1}: unknown event type {2}", filename, lnum, name); return 1; } /* Verify that the calculated time matches the correct time for this event. */ double diff_minutes = (24.0 * 60.0) * Math.Abs(calc_time.tt - correct_time.tt); if (diff_minutes > max_minutes) max_minutes = diff_minutes; if (diff_minutes > 1.7) { Console.WriteLine("SeasonsTest: %s line %d: excessive error (%s): %lf minutes.\n", filename, lnum, name, diff_minutes); return 1; } } Console.WriteLine("SeasonsTest: verified {0} lines from file {1} : max error minutes = {2:0.000}", lnum, filename, max_minutes); Console.WriteLine("SeasonsTest: Event counts: mar={0}, jun={1}, sep={2}, dec={3}", mar_count, jun_count, sep_count, dec_count); return 0; } } static int MoonPhaseTest(string filename) { using (StreamReader infile = File.OpenText(filename)) { const double threshold_seconds = 120.0; int lnum = 0; string line; double max_arcmin = 0.0; int prev_year = 0; int expected_quarter = 0; int quarter_count = 0; double maxdiff = 0.0; MoonQuarterInfo mq = new MoonQuarterInfo(); var re = new Regex(@"^([0-3])\s+(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)\.000Z$"); while (null != (line = infile.ReadLine())) { ++lnum; /* 0 1800-01-25T03:21:00.000Z 1 1800-02-01T20:40:00.000Z 2 1800-02-09T17:26:00.000Z 3 1800-02-16T15:49:00.000Z */ Match m = re.Match(line); if (!m.Success) { Console.WriteLine("MoonPhaseTest: ERROR {0} line {1}: cannot parse", filename, lnum); return 1; } int quarter = int.Parse(m.Groups[1].Value); int year = int.Parse(m.Groups[2].Value); int month = int.Parse(m.Groups[3].Value); int day = int.Parse(m.Groups[4].Value); int hour = int.Parse(m.Groups[5].Value); int minute = int.Parse(m.Groups[6].Value); int second = int.Parse(m.Groups[7].Value); double expected_elong = 90.0 * quarter; AstroTime expected_time = new AstroTime(year, month, day, hour, minute, second); double calc_elong = Astronomy.MoonPhase(expected_time); double degree_error = Math.Abs(calc_elong - expected_elong); if (degree_error > 180.0) degree_error = 360.0 - degree_error; double arcmin = 60.0 * degree_error; if (arcmin > 1.0) { Console.WriteLine("MoonPhaseTest({0} line {1}): EXCESSIVE ANGULAR ERROR: {2} arcmin", filename, lnum, arcmin); return 1; } if (arcmin > max_arcmin) 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. */ AstroTime start_time = new AstroTime(year, 1, 1, 0, 0, 0); mq = Astronomy.SearchMoonQuarter(start_time); expected_quarter = -1; /* we have no idea what the quarter should be */ } else { /* Yet another lunar quarter in the same year. */ expected_quarter = (1 + mq.quarter) % 4; mq = Astronomy.NextMoonQuarter(mq); /* Make sure we find the next expected quarter. */ if (expected_quarter != mq.quarter) { Console.WriteLine("MoonPhaseTest({0} line {1}): SearchMoonQuarter returned quarter {2}, but expected {3}", filename, lnum, mq.quarter, expected_quarter); return 1; } } ++quarter_count; /* Make sure the time matches what we expect. */ double diff_seconds = Math.Abs(mq.time.tt - expected_time.tt) * (24.0 * 3600.0); if (diff_seconds > threshold_seconds) { Console.WriteLine("MoonPhaseTest({0} line {1}): excessive time error {2:0.000} seconds", filename, lnum, diff_seconds); return 1; } if (diff_seconds > maxdiff) maxdiff = diff_seconds; } Console.WriteLine("MoonPhaseTest: passed {0} lines for file {1} : max_arcmin = {2:0.000000}, maxdiff = {3:0.000} seconds, {4} quarters", lnum, filename, max_arcmin, maxdiff, quarter_count); return 0; } } static int RiseSetTest(string filename) { using (StreamReader infile = File.OpenText(filename)) { int lnum = 0; string line; var re = new Regex(@"^([A-Za-z]+)\s+([\-\+]?\d+\.?\d*)\s+([\-\+]?\d+\.?\d*)\s+(\d+)-(\d+)-(\d+)T(\d+):(\d+)Z\s+([rs])\s*$"); Body current_body = Body.Invalid; Observer observer = null; AstroTime r_search_date = null, s_search_date = null; AstroTime r_evt = null, s_evt = null; /* rise event, set event: search results */ AstroTime a_evt = null, b_evt = null; /* chronologically first and second events */ Direction a_dir = Direction.Rise, b_dir = Direction.Rise; const double nudge_days = 0.01; double sum_minutes = 0.0; double max_minutes = 0.0; while (null != (line = infile.ReadLine())) { ++lnum; // Moon 103 -61 1944-01-02T17:08Z s // Moon 103 -61 1944-01-03T05:47Z r Match m = re.Match(line); if (!m.Success) { Console.WriteLine("RiseSetTest({0} line {1}): invalid input format", filename, lnum); return 1; } Body body = Enum.Parse(m.Groups[1].Value); double longitude = double.Parse(m.Groups[2].Value); double latitude = double.Parse(m.Groups[3].Value); int year = int.Parse(m.Groups[4].Value); int month = int.Parse(m.Groups[5].Value); int day = int.Parse(m.Groups[6].Value); int hour = int.Parse(m.Groups[7].Value); int minute = int.Parse(m.Groups[8].Value); Direction direction = (m.Groups[9].Value == "r") ? Direction.Rise : Direction.Set; var correct_date = new AstroTime(year, month, day, hour, minute, 0); /* Every time we see a new geographic location or body, start a new iteration */ /* of finding all rise/set times for that UTC calendar year. */ if (observer == null || observer.latitude != latitude || observer.longitude != longitude || current_body != body) { current_body = body; observer = new Observer(latitude, longitude, 0.0); r_search_date = s_search_date = new AstroTime(year, 1, 1, 0, 0, 0); b_evt = null; Console.WriteLine("RiseSetTest: {0} lat={1} lon={2}", body, latitude, longitude); } if (b_evt != null) { a_evt = b_evt; a_dir = b_dir; b_evt = null; } else { r_evt = Astronomy.SearchRiseSet(body, observer, Direction.Rise, r_search_date, 366.0); if (r_evt == null) { Console.WriteLine("RiseSetTest({0} line {1}): Did not find {2} rise event.", filename, lnum, body); return 1; } s_evt = Astronomy.SearchRiseSet(body, observer, Direction.Set, s_search_date, 366.0); if (s_evt == null) { Console.WriteLine("RiseSetTest({0} line {1}): Did not find {2} rise event.", filename, lnum, body); return 1; } /* Expect the current event to match the earlier of the found dates. */ if (r_evt.tt < s_evt.tt) { a_evt = r_evt; b_evt = s_evt; a_dir = Direction.Rise; b_dir = Direction.Set; } else { a_evt = s_evt; b_evt = r_evt; a_dir = Direction.Set; b_dir = 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) { Console.WriteLine("RiseSetTest({0} line {1}): expected dir={2} but found {3}", filename, lnum, a_dir, direction); return 1; } double error_minutes = (24.0 * 60.0) * Math.Abs(a_evt.tt - correct_date.tt); sum_minutes += error_minutes * error_minutes; if (error_minutes > max_minutes) max_minutes = error_minutes; if (error_minutes > 0.56) { Console.WriteLine("RiseSetTest({0} line {1}): excessive prediction time error = {2} minutes.\n", filename, lnum, error_minutes); return 1; } } double rms_minutes = Math.Sqrt(sum_minutes / lnum); Console.WriteLine("RiseSetTest: passed {0} lines: time errors in minutes: rms={1}, max={2}", lnum, rms_minutes, max_minutes); return 0; } } static int TestElongFile(string filename, double targetRelLon) { using (StreamReader infile = File.OpenText(filename)) { int lnum = 0; string line; var re = new Regex(@"^(\d+)-(\d+)-(\d+)T(\d+):(\d+)Z\s+([A-Z][a-z]+)\s*$"); while (null != (line = infile.ReadLine())) { ++lnum; /* 2018-05-09T00:28Z Jupiter */ Match m = re.Match(line); if (!m.Success) { Console.WriteLine("C# TestElongFile({0} line {1}): invalid data format.", filename, lnum); return 1; } int year = int.Parse(m.Groups[1].Value); int month = int.Parse(m.Groups[2].Value); int day = int.Parse(m.Groups[3].Value); int hour = int.Parse(m.Groups[4].Value); int minute = int.Parse(m.Groups[5].Value); Body body = Enum.Parse(m.Groups[6].Value); var search_date = new AstroTime(year, 1, 1, 0, 0, 0); var expected_time = new AstroTime(year, month, day, hour, minute, 0); AstroTime search_result = Astronomy.SearchRelativeLongitude(body, targetRelLon, search_date); if (search_result == null) { Console.WriteLine("C# TestElongFile({0} line {1}): SearchRelativeLongitude returned null.", filename, lnum); return 1; } double diff_minutes = (24.0 * 60.0) * (search_result.tt - expected_time.tt); Console.WriteLine("{0} error = {1} minutes.", body, diff_minutes.ToString("f3")); if (Math.Abs(diff_minutes) > 15.0) { Console.WriteLine("C# TestElongFile({0} line {1}): EXCESSIVE ERROR.", filename, lnum); return 1; } } Console.WriteLine("C# TestElongFile: passed {0} rows of data.", lnum); return 0; } } static int TestPlanetLongitudes(Body body, string outFileName, string zeroLonEventName) { const int startYear = 1700; const int stopYear = 2200; int count = 0; double rlon = 0.0; double min_diff = 1.0e+99; double max_diff = 1.0e+99; double sum_diff = 0.0; using (StreamWriter outfile = File.CreateText(outFileName)) { var time = new AstroTime(startYear, 1, 1, 0, 0, 0); var stopTime = new AstroTime(stopYear, 1, 1, 0, 0, 0); while (time.tt < stopTime.tt) { ++count; string event_name = (rlon == 0.0) ? zeroLonEventName : "sup"; AstroTime search_result = Astronomy.SearchRelativeLongitude(body, rlon, time); if (search_result == null) { Console.WriteLine("C# TestPlanetLongitudes({0}): SearchRelativeLongitude returned null.", body); return 1; } if (count >= 2) { /* Check for consistent intervals. */ /* Mainly I don't want to skip over an event! */ double day_diff = search_result.tt - time.tt; sum_diff += day_diff; if (count == 2) { min_diff = max_diff = day_diff; } else { if (day_diff < min_diff) min_diff = day_diff; if (day_diff > max_diff) max_diff = day_diff; } } AstroVector geo = Astronomy.GeoVector(body, search_result, Aberration.Corrected); double dist = geo.Length(); outfile.WriteLine("e {0} {1} {2} {3}", body, event_name, search_result.tt.ToString("g17"), dist.ToString("g17")); /* Search for the opposite longitude event next time. */ time = search_result; rlon = 180.0 - rlon; } } double thresh; switch (body) { case Body.Mercury: thresh = 1.65; break; case Body.Mars: thresh = 1.30; break; default: thresh = 1.07; break; } double ratio = max_diff / min_diff; Console.WriteLine("TestPlanetLongitudes({0,7}): {1,5} events, ratio={2,5}, file: {3}", body, count, ratio.ToString("f3"), outFileName); if (ratio > thresh) { Console.WriteLine("TestPlanetLongitudes({0}): excessive event interval ratio.\n", body); return 1; } return 0; } static int ElongationTest() { if (0 != TestElongFile("../../longitude/opposition_2018.txt", 0.0)) return 1; if (0 != TestPlanetLongitudes(Body.Mercury, "csharp_longitude_Mercury.txt", "inf")) return 1; if (0 != TestPlanetLongitudes(Body.Venus, "csharp_longitude_Venus.txt", "inf")) return 1; if (0 != TestPlanetLongitudes(Body.Mars, "csharp_longitude_Mars.txt", "opp")) return 1; if (0 != TestPlanetLongitudes(Body.Jupiter, "csharp_longitude_Jupiter.txt", "opp")) return 1; if (0 != TestPlanetLongitudes(Body.Saturn, "csharp_longitude_Saturn.txt", "opp")) return 1; if (0 != TestPlanetLongitudes(Body.Uranus, "csharp_longitude_Uranus.txt", "opp")) return 1; if (0 != TestPlanetLongitudes(Body.Neptune, "csharp_longitude_Neptune.txt", "opp")) return 1; if (0 != TestPlanetLongitudes(Body.Pluto, "csharp_longitude_Pluto.txt", "opp")) return 1; foreach (elong_test_t et in ElongTestData) if (0 != TestMaxElong(et)) return 1; return 0; } static readonly Regex regexDate = new Regex(@"^(\d+)-(\d+)-(\d+)T(\d+):(\d+)Z$"); static AstroTime ParseDate(string text) { Match m = regexDate.Match(text); if (!m.Success) throw new Exception(string.Format("ParseDate failed for string: '{0}'", text)); int year = int.Parse(m.Groups[1].Value); int month = int.Parse(m.Groups[2].Value); int day = int.Parse(m.Groups[3].Value); int hour = int.Parse(m.Groups[4].Value); int minute = int.Parse(m.Groups[5].Value); return new AstroTime(year, month, day, hour, minute, 0); } static int TestMaxElong(elong_test_t test) { AstroTime searchTime = ParseDate(test.searchDate); AstroTime eventTime = ParseDate(test.eventDate); ElongationInfo evt = Astronomy.SearchMaxElongation(test.body, searchTime); double hour_diff = 24.0 * Math.Abs(evt.time.tt - eventTime.tt); double arcmin_diff = 60.0 * Math.Abs(evt.elongation - test.angle); Console.WriteLine("C# TestMaxElong: {0,7} {1,7} elong={2,5} ({3} arcmin, {4} hours)", test.body, test.visibility, evt.elongation, arcmin_diff, hour_diff); if (hour_diff > 0.6) { Console.WriteLine("C# TestMaxElong({0} {1}): excessive hour error.", test.body, test.searchDate); return 1; } if (arcmin_diff > 3.4) { Console.WriteLine("C# TestMaxElong({0} {1}): excessive arcmin error.", test.body, test.searchDate); return 1; } return 0; } struct elong_test_t { public Body body; public string searchDate; public string eventDate; public double angle; public Visibility visibility; public elong_test_t(Body body, string searchDate, string eventDate, double angle, Visibility visibility) { this.body = body; this.searchDate = searchDate; this.eventDate = eventDate; this.angle = angle; this.visibility = visibility; } } static readonly elong_test_t[] ElongTestData = new elong_test_t[] { /* Max elongation data obtained from: */ /* http://www.skycaramba.com/greatest_elongations.shtml */ new elong_test_t( Body.Mercury, "2010-01-17T05:22Z", "2010-01-27T05:22Z", 24.80, Visibility.Morning ), new elong_test_t( Body.Mercury, "2010-05-16T02:15Z", "2010-05-26T02:15Z", 25.10, Visibility.Morning ), new elong_test_t( Body.Mercury, "2010-09-09T17:24Z", "2010-09-19T17:24Z", 17.90, Visibility.Morning ), new elong_test_t( Body.Mercury, "2010-12-30T14:33Z", "2011-01-09T14:33Z", 23.30, Visibility.Morning ), new elong_test_t( Body.Mercury, "2011-04-27T19:03Z", "2011-05-07T19:03Z", 26.60, Visibility.Morning ), new elong_test_t( Body.Mercury, "2011-08-24T05:52Z", "2011-09-03T05:52Z", 18.10, Visibility.Morning ), new elong_test_t( Body.Mercury, "2011-12-13T02:56Z", "2011-12-23T02:56Z", 21.80, Visibility.Morning ), new elong_test_t( Body.Mercury, "2012-04-08T17:22Z", "2012-04-18T17:22Z", 27.50, Visibility.Morning ), new elong_test_t( Body.Mercury, "2012-08-06T12:04Z", "2012-08-16T12:04Z", 18.70, Visibility.Morning ), new elong_test_t( Body.Mercury, "2012-11-24T22:55Z", "2012-12-04T22:55Z", 20.60, Visibility.Morning ), new elong_test_t( Body.Mercury, "2013-03-21T22:02Z", "2013-03-31T22:02Z", 27.80, Visibility.Morning ), new elong_test_t( Body.Mercury, "2013-07-20T08:51Z", "2013-07-30T08:51Z", 19.60, Visibility.Morning ), new elong_test_t( Body.Mercury, "2013-11-08T02:28Z", "2013-11-18T02:28Z", 19.50, Visibility.Morning ), new elong_test_t( Body.Mercury, "2014-03-04T06:38Z", "2014-03-14T06:38Z", 27.60, Visibility.Morning ), new elong_test_t( Body.Mercury, "2014-07-02T18:22Z", "2014-07-12T18:22Z", 20.90, Visibility.Morning ), new elong_test_t( Body.Mercury, "2014-10-22T12:36Z", "2014-11-01T12:36Z", 18.70, Visibility.Morning ), new elong_test_t( Body.Mercury, "2015-02-14T16:20Z", "2015-02-24T16:20Z", 26.70, Visibility.Morning ), new elong_test_t( Body.Mercury, "2015-06-14T17:10Z", "2015-06-24T17:10Z", 22.50, Visibility.Morning ), new elong_test_t( Body.Mercury, "2015-10-06T03:20Z", "2015-10-16T03:20Z", 18.10, Visibility.Morning ), new elong_test_t( Body.Mercury, "2016-01-28T01:22Z", "2016-02-07T01:22Z", 25.60, Visibility.Morning ), new elong_test_t( Body.Mercury, "2016-05-26T08:45Z", "2016-06-05T08:45Z", 24.20, Visibility.Morning ), new elong_test_t( Body.Mercury, "2016-09-18T19:27Z", "2016-09-28T19:27Z", 17.90, Visibility.Morning ), new elong_test_t( Body.Mercury, "2017-01-09T09:42Z", "2017-01-19T09:42Z", 24.10, Visibility.Morning ), new elong_test_t( Body.Mercury, "2017-05-07T23:19Z", "2017-05-17T23:19Z", 25.80, Visibility.Morning ), new elong_test_t( Body.Mercury, "2017-09-02T10:14Z", "2017-09-12T10:14Z", 17.90, Visibility.Morning ), new elong_test_t( Body.Mercury, "2017-12-22T19:48Z", "2018-01-01T19:48Z", 22.70, Visibility.Morning ), new elong_test_t( Body.Mercury, "2018-04-19T18:17Z", "2018-04-29T18:17Z", 27.00, Visibility.Morning ), new elong_test_t( Body.Mercury, "2018-08-16T20:35Z", "2018-08-26T20:35Z", 18.30, Visibility.Morning ), new elong_test_t( Body.Mercury, "2018-12-05T11:34Z", "2018-12-15T11:34Z", 21.30, Visibility.Morning ), new elong_test_t( Body.Mercury, "2019-04-01T19:40Z", "2019-04-11T19:40Z", 27.70, Visibility.Morning ), new elong_test_t( Body.Mercury, "2019-07-30T23:08Z", "2019-08-09T23:08Z", 19.00, Visibility.Morning ), new elong_test_t( Body.Mercury, "2019-11-18T10:31Z", "2019-11-28T10:31Z", 20.10, Visibility.Morning ), new elong_test_t( Body.Mercury, "2010-03-29T23:32Z", "2010-04-08T23:32Z", 19.40, Visibility.Evening ), new elong_test_t( Body.Mercury, "2010-07-28T01:03Z", "2010-08-07T01:03Z", 27.40, Visibility.Evening ), new elong_test_t( Body.Mercury, "2010-11-21T15:42Z", "2010-12-01T15:42Z", 21.50, Visibility.Evening ), new elong_test_t( Body.Mercury, "2011-03-13T01:07Z", "2011-03-23T01:07Z", 18.60, Visibility.Evening ), new elong_test_t( Body.Mercury, "2011-07-10T04:56Z", "2011-07-20T04:56Z", 26.80, Visibility.Evening ), new elong_test_t( Body.Mercury, "2011-11-04T08:40Z", "2011-11-14T08:40Z", 22.70, Visibility.Evening ), new elong_test_t( Body.Mercury, "2012-02-24T09:39Z", "2012-03-05T09:39Z", 18.20, Visibility.Evening ), new elong_test_t( Body.Mercury, "2012-06-21T02:00Z", "2012-07-01T02:00Z", 25.70, Visibility.Evening ), new elong_test_t( Body.Mercury, "2012-10-16T21:59Z", "2012-10-26T21:59Z", 24.10, Visibility.Evening ), new elong_test_t( Body.Mercury, "2013-02-06T21:24Z", "2013-02-16T21:24Z", 18.10, Visibility.Evening ), new elong_test_t( Body.Mercury, "2013-06-02T16:45Z", "2013-06-12T16:45Z", 24.30, Visibility.Evening ), new elong_test_t( Body.Mercury, "2013-09-29T09:59Z", "2013-10-09T09:59Z", 25.30, Visibility.Evening ), new elong_test_t( Body.Mercury, "2014-01-21T10:00Z", "2014-01-31T10:00Z", 18.40, Visibility.Evening ), new elong_test_t( Body.Mercury, "2014-05-15T07:06Z", "2014-05-25T07:06Z", 22.70, Visibility.Evening ), new elong_test_t( Body.Mercury, "2014-09-11T22:20Z", "2014-09-21T22:20Z", 26.40, Visibility.Evening ), new elong_test_t( Body.Mercury, "2015-01-04T20:26Z", "2015-01-14T20:26Z", 18.90, Visibility.Evening ), new elong_test_t( Body.Mercury, "2015-04-27T04:46Z", "2015-05-07T04:46Z", 21.20, Visibility.Evening ), new elong_test_t( Body.Mercury, "2015-08-25T10:20Z", "2015-09-04T10:20Z", 27.10, Visibility.Evening ), new elong_test_t( Body.Mercury, "2015-12-19T03:11Z", "2015-12-29T03:11Z", 19.70, Visibility.Evening ), new elong_test_t( Body.Mercury, "2016-04-08T14:00Z", "2016-04-18T14:00Z", 19.90, Visibility.Evening ), new elong_test_t( Body.Mercury, "2016-08-06T21:24Z", "2016-08-16T21:24Z", 27.40, Visibility.Evening ), new elong_test_t( Body.Mercury, "2016-12-01T04:36Z", "2016-12-11T04:36Z", 20.80, Visibility.Evening ), new elong_test_t( Body.Mercury, "2017-03-22T10:24Z", "2017-04-01T10:24Z", 19.00, Visibility.Evening ), new elong_test_t( Body.Mercury, "2017-07-20T04:34Z", "2017-07-30T04:34Z", 27.20, Visibility.Evening ), new elong_test_t( Body.Mercury, "2017-11-14T00:32Z", "2017-11-24T00:32Z", 22.00, Visibility.Evening ), new elong_test_t( Body.Mercury, "2018-03-05T15:07Z", "2018-03-15T15:07Z", 18.40, Visibility.Evening ), new elong_test_t( Body.Mercury, "2018-07-02T05:24Z", "2018-07-12T05:24Z", 26.40, Visibility.Evening ), new elong_test_t( Body.Mercury, "2018-10-27T15:25Z", "2018-11-06T15:25Z", 23.30, Visibility.Evening ), new elong_test_t( Body.Mercury, "2019-02-17T01:23Z", "2019-02-27T01:23Z", 18.10, Visibility.Evening ), new elong_test_t( Body.Mercury, "2019-06-13T23:14Z", "2019-06-23T23:14Z", 25.20, Visibility.Evening ), new elong_test_t( Body.Mercury, "2019-10-10T04:00Z", "2019-10-20T04:00Z", 24.60, Visibility.Evening ), new elong_test_t( Body.Venus, "2010-12-29T15:57Z", "2011-01-08T15:57Z", 47.00, Visibility.Morning ), new elong_test_t( Body.Venus, "2012-08-05T08:59Z", "2012-08-15T08:59Z", 45.80, Visibility.Morning ), new elong_test_t( Body.Venus, "2014-03-12T19:25Z", "2014-03-22T19:25Z", 46.60, Visibility.Morning ), new elong_test_t( Body.Venus, "2015-10-16T06:57Z", "2015-10-26T06:57Z", 46.40, Visibility.Morning ), new elong_test_t( Body.Venus, "2017-05-24T13:09Z", "2017-06-03T13:09Z", 45.90, Visibility.Morning ), new elong_test_t( Body.Venus, "2018-12-27T04:24Z", "2019-01-06T04:24Z", 47.00, Visibility.Morning ), new elong_test_t( Body.Venus, "2010-08-10T03:19Z", "2010-08-20T03:19Z", 46.00, Visibility.Evening ), new elong_test_t( Body.Venus, "2012-03-17T08:03Z", "2012-03-27T08:03Z", 46.00, Visibility.Evening ), new elong_test_t( Body.Venus, "2013-10-22T08:00Z", "2013-11-01T08:00Z", 47.10, Visibility.Evening ), new elong_test_t( Body.Venus, "2015-05-27T18:46Z", "2015-06-06T18:46Z", 45.40, Visibility.Evening ), new elong_test_t( Body.Venus, "2017-01-02T13:19Z", "2017-01-12T13:19Z", 47.10, Visibility.Evening ), new elong_test_t( Body.Venus, "2018-08-07T17:02Z", "2018-08-17T17:02Z", 45.90, Visibility.Evening ) }; static int LunarApsisTest(string inFileName) { using (StreamReader infile = File.OpenText(inFileName)) { int lnum = 0; string line; var start_time = new AstroTime(2001,1, 1, 0, 0, 0); ApsisInfo apsis = new ApsisInfo(); double max_minutes = 0.0; double max_km = 0.0; /* 0 2001-01-10T08:59Z 357132 1 2001-01-24T19:02Z 406565 */ var regex = new Regex(@"^\s*([01])\s+(\d+)-(\d+)-(\d+)T(\d+):(\d+)Z\s+(\d+)\s*$"); while (null != (line = infile.ReadLine())) { ++lnum; Match m = regex.Match(line); if (!m.Success) { Console.WriteLine("LunarApsisTest({0} line {1}): invalid data format.", inFileName, lnum); return 1; } ApsisKind kind = (m.Groups[1].Value == "0") ? ApsisKind.Pericenter : ApsisKind.Apocenter; int year = int.Parse(m.Groups[2].Value); int month = int.Parse(m.Groups[3].Value); int day = int.Parse(m.Groups[4].Value); int hour = int.Parse(m.Groups[5].Value); int minute = int.Parse(m.Groups[6].Value); double dist_km = double.Parse(m.Groups[7].Value); var correct_time = new AstroTime(year, month, day, hour, minute, 0); if (lnum == 1) apsis = Astronomy.SearchLunarApsis(start_time); else apsis = Astronomy.NextLunarApsis(apsis); if (kind != apsis.kind) { Console.WriteLine("LunarApsisTest({0} line {1}): expected apsis kind {2} but found {3}", inFileName, lnum, kind, apsis.kind); return 1; } double diff_minutes = (24.0 * 60.0) * Math.Abs(apsis.time.ut - correct_time.ut); if (diff_minutes > 35.0) { Console.WriteLine("LunarApsisTest({0} line {1}): excessive time error: {2} minutes", inFileName, lnum, diff_minutes); return 1; } double diff_km = Math.Abs(apsis.dist_km - dist_km); if (diff_km > 25.0) { Console.WriteLine("LunarApsisTest({0} line {1}): excessive distance error: {2} km", inFileName, lnum, diff_km); return 1; } if (diff_minutes > max_minutes) max_minutes = diff_minutes; if (diff_km > max_km) max_km = diff_km; } Console.WriteLine("C# LunarApsisTest: Found {0} events, max time error = {1} minutes, max distance error = {2} km.", lnum, max_minutes, max_km); return 0; } } class JplDateTime { public string Rest; public AstroTime Time; } static readonly Regex JplRegex = new Regex(@"^\s*(\d{4})-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d{2})\s+(\d{2}):(\d{2})\s+(.*)"); static readonly char[] TokenSeparators = new char[] { ' ', '\t', '\r', '\n' }; static string[] Tokenize(string line) { return line.Split(TokenSeparators, StringSplitOptions.RemoveEmptyEntries); } static JplDateTime ParseJplHorizonsDateTime(string line) { Match m = JplRegex.Match(line); if (!m.Success) return null; int year = int.Parse(m.Groups[1].Value); string mtext = m.Groups[2].Value; int day = int.Parse(m.Groups[3].Value); int hour = int.Parse(m.Groups[4].Value); int minute = int.Parse(m.Groups[5].Value); string rest = m.Groups[6].Value; int month; switch (mtext) { case "Jan": month = 1; break; case "Feb": month = 2; break; case "Mar": month = 3; break; case "Apr": month = 4; break; case "May": month = 5; break; case "Jun": month = 6; break; case "Jul": month = 7; break; case "Aug": month = 8; break; case "Sep": month = 9; break; case "Oct": month = 10; break; case "Nov": month = 11; break; case "Dec": month = 12; break; default: throw new Exception(string.Format("Internal error: unexpected month name '{0}'", mtext)); } AstroTime time = new AstroTime(year, month, day, hour, minute, 0); return new JplDateTime { Rest=rest, Time=time }; } static int CheckMagnitudeData(Body body, string filename) { using (StreamReader infile = File.OpenText(filename)) { const double limit = 0.012; double diff_lo = 0.0; double diff_hi = 0.0; double sum_squared_diff = 0.0; int lnum = 0; int count = 0; string line; while (null != (line = infile.ReadLine())) { ++lnum; JplDateTime jpl = ParseJplHorizonsDateTime(line); if (jpl == null) continue; string[] token = Tokenize(jpl.Rest); if (token.Length > 0 && token[0] == "n.a.") continue; if (token.Length != 7) { Console.WriteLine("CheckMagnitudeData({0} line {1}): invalid data format", lnum, filename); return 1; } double mag; if (!double.TryParse(token[0], out mag)) { Console.WriteLine("CheckMagnitudeData({0} line {1}): cannot parse number from '{2}'", filename, lnum, token[0]); return 1; } var illum = Astronomy.Illumination(body, jpl.Time); double diff = illum.mag - mag; if (Math.Abs(diff) > limit) { Console.WriteLine("CheckMagnitudeData({0} line {1}): EXCESSIVE ERROR: correct mag={0}, calc mag={1}, diff={2}", mag, illum.mag, diff); return 1; } sum_squared_diff += diff * diff; if (count == 0) { diff_lo = diff_hi = diff; } else { if (diff < diff_lo) diff_lo = diff; if (diff > diff_hi) diff_hi = diff; } ++count; } if (count == 0) { Console.WriteLine("CheckMagnitudeData: Data not find any data in file: {0}", filename); return 1; } double rms = Math.Sqrt(sum_squared_diff / count); Console.WriteLine("CheckMagnitudeData: {0} {1} rows diff_lo={2} diff_hi={3} rms={4}", filename, count, diff_lo, diff_hi, rms); return 0; } } struct saturn_test_case { public readonly string date; public readonly double mag; public readonly double tilt; public saturn_test_case(string date, double mag, double tilt) { this.date = date; this.mag = mag; this.tilt = tilt; } } /* 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 */ static saturn_test_case[] saturn_data = new saturn_test_case[] { new saturn_test_case("1972-01-01T00:00Z", -0.31904865, +24.50061220), new saturn_test_case("1980-01-01T00:00Z", +0.85213663, -1.85761461), new saturn_test_case("2009-09-04T00:00Z", +1.01626809, +0.08380716), new saturn_test_case("2017-06-15T00:00Z", -0.12318790, -26.60871409), new saturn_test_case("2019-05-01T00:00Z", +0.32954097, -23.53880802), new saturn_test_case("2025-09-25T00:00Z", +0.51286575, +1.52327932), new saturn_test_case("2032-05-15T00:00Z", -0.04652109, +26.95717765) }; static int CheckSaturn() { foreach (saturn_test_case data in saturn_data) { AstroTime time = ParseDate(data.date); IllumInfo illum = Astronomy.Illumination(Body.Saturn, time); Console.WriteLine("Saturn: date={0} calc mag={1} ring_tilt={2}\n", data.date, illum.mag, illum.ring_tilt); double mag_diff = Math.Abs(illum.mag - data.mag); if (mag_diff > 1.0e-8) { Console.WriteLine("ERROR: Excessive magnitude error {0}", mag_diff); return 1; } double tilt_diff = Math.Abs(illum.ring_tilt - data.tilt); if (tilt_diff > 1.0e-8) { Console.WriteLine("ERROR: Excessive ring tilt error {0}\n", tilt_diff); return 1; } } return 0; } static int TestMaxMag(Body body, string 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. */ using (StreamReader infile = File.OpenText(filename)) { int lnum = 0; string line; var search_time = new AstroTime(2001, 1, 1, 0, 0, 0); while (null != (line = infile.ReadLine())) { ++lnum; string[] token = Tokenize(line); if (token.Length != 5) { Console.WriteLine("TestMaxMag({0} line {1}): invalid data format", filename, lnum); return 1; } AstroTime time1 = ParseDate(token[0]); AstroTime time2 = ParseDate(token[1]); double correct_angle1 = double.Parse(token[2]); double correct_angle2 = double.Parse(token[3]); double correct_mag = double.Parse(token[4]); AstroTime center_time = time1.AddDays(0.5*(time2.ut - time1.ut)); IllumInfo illum = Astronomy.SearchPeakMagnitude(body, search_time); double mag_diff = Math.Abs(illum.mag - correct_mag); double hours_diff = 24.0 * Math.Abs(illum.time.ut - center_time.ut); Console.WriteLine("C# TestMaxMag: mag_diff={0}, hours_diff={1}", mag_diff, hours_diff); if (hours_diff > 7.1) { Console.WriteLine("TestMaxMag({0} line {1}): EXCESSIVE TIME DIFFERENCE.", filename, lnum); return 1; } if (mag_diff > 0.005) { Console.WriteLine("TestMaxMag({0} line {1}): EXCESSIVE MAGNITUDE DIFFERENCE.", filename, lnum); return 1; } search_time = time2; } Console.WriteLine("TestMaxMag: Processed {0} lines from file {1}", lnum, filename); return 0; } } static int MagnitudeTest() { int nfailed = 0; nfailed += CheckMagnitudeData(Body.Sun, "../../magnitude/Sun.txt"); nfailed += CheckMagnitudeData(Body.Moon, "../../magnitude/Moon.txt"); nfailed += CheckMagnitudeData(Body.Mercury, "../../magnitude/Mercury.txt"); nfailed += CheckMagnitudeData(Body.Venus, "../../magnitude/Venus.txt"); nfailed += CheckMagnitudeData(Body.Mars, "../../magnitude/Mars.txt"); nfailed += CheckMagnitudeData(Body.Jupiter, "../../magnitude/Jupiter.txt"); nfailed += CheckSaturn(); nfailed += CheckMagnitudeData(Body.Uranus, "../../magnitude/Uranus.txt"); nfailed += CheckMagnitudeData(Body.Neptune, "../../magnitude/Neptune.txt"); nfailed += CheckMagnitudeData(Body.Pluto, "../../magnitude/Pluto.txt"); nfailed += TestMaxMag(Body.Venus, "../../magnitude/maxmag_Venus.txt"); if (nfailed > 0) Console.WriteLine("MagnitudeTest: FAILED {0} test(s).", nfailed); return nfailed; } } }