Files
astronomy/generate/ctest.c
Don Cross 1b0ba300d8 Removed all references to GitHub Pages, because it has been disabled.
I'm not going to use GitHub Pages after all, because it is
causing more problems than it is helping. All I really wanted
was a way to host live JavaScript browser examples.
I will find my own way of hosting just those.

The main problem is that GitHub pages uses a different flavor
of Markdown than GitHub. This makes it really difficult to get
something that works right across both.  In general, it doubles
how much stuff I have to look at when I make a cosmetic change.

So I have already turned off GitHub Pages on this repo,
and this commit removes all links and references to it.
2019-07-09 19:49:28 -04:00

1746 lines
57 KiB
C

/*
ctest.c - Don Cross <cosinekitty.com>
C langauge unit test for Astronomy Engine project.
https://github.com/cosinekitty/astronomy
*/
#include <stdio.h>
#include <string.h>
#include <math.h>
#include "astronomy.h"
#define PI 3.14159265358979323846
#define CHECK(x) do{if(0 != (error = (x))) goto fail;}while(0)
static int CheckVector(int lnum, astro_vector_t v)
{
if (v.status != ASTRO_SUCCESS)
{
fprintf(stderr, "FAILURE at ctest.c[%d]: vector status = %d\n", lnum, v.status);
return 1;
}
return 0;
}
static int CheckEquator(int lnum, astro_equatorial_t equ)
{
if (equ.status != ASTRO_SUCCESS)
{
fprintf(stderr, "FAILURE at ctest.c[%d]: equatorial status = %d\n", lnum, equ.status);
return 1;
}
return 0;
}
#define CHECK_VECTOR(var,expr) CHECK(CheckVector(__LINE__, ((var) = (expr))))
#define CHECK_EQU(var,expr) CHECK(CheckEquator(__LINE__, ((var) = (expr))))
static int Issue46(void);
static int Issue48(void);
static int Test_AstroTime(void);
static int AstroCheck(void);
static int Diff(const char *c_filename, const char *js_filename);
static int DiffLine(int lnum, const char *cline, const char *jline, double *maxdiff, int *worst_lnum);
static int SeasonsTest(const char *filename);
static int MoonPhase(const char *filename);
static int RiseSet(const char *filename);
static int LunarApsis(const char *filename);
static int ElongationTest(void);
static int MagnitudeTest(void);
static int MoonTest(void);
static int TestMaxMag(astro_body_t body, const char *filename);
static const char *ParseJplHorizonsDateTime(const char *text, astro_time_t *time);
int main(int argc, const char *argv[])
{
int error = 1;
if (argc == 1)
{
CHECK(Test_AstroTime());
CHECK(AstroCheck());
goto success;
}
if (argc == 2)
{
const char *verb = argv[1];
if (!strcmp(verb, "elongation"))
{
CHECK(ElongationTest());
goto success;
}
if (!strcmp(verb, "magnitude"))
{
CHECK(MagnitudeTest());
goto success;
}
if (!strcmp(verb, "moon"))
{
CHECK(MoonTest());
goto success;
}
if (!strcmp(verb, "issue46"))
{
CHECK(Issue46());
return 0; /* prevent extra "ctest exiting with 0" output. */
}
if (!strcmp(verb, "issue48"))
{
CHECK(Issue48());
return 0; /* prevent extra "ctest exiting with 0" output. */
}
}
if (argc == 3)
{
const char *verb = argv[1];
const char *filename = argv[2];
if (!strcmp(verb, "seasons"))
{
CHECK(SeasonsTest(filename));
goto success;
}
if (!strcmp(verb, "moonphase"))
{
CHECK(MoonPhase(filename));
goto success;
}
if (!strcmp(verb, "riseset"))
{
CHECK(RiseSet(filename));
goto success;
}
if (!strcmp(verb, "apsis"))
{
CHECK(LunarApsis(filename));
goto success;
}
}
if (argc == 4)
{
if (!strcmp(argv[1], "diff"))
{
const char *c_filename = argv[2];
const char *js_filename = argv[3];
CHECK(Diff(c_filename, js_filename));
goto success;
}
}
fprintf(stderr, "Invalid command line arguments.\n");
error = 1;
goto fail;
success:
error = 0;
fail:
fprintf(stderr, "ctest exiting with %d\n", error);
return error;
}
static int Test_AstroTime(void)
{
astro_time_t time;
astro_utc_t utc;
const double expected_ut = 6910.270978506945;
const double expected_tt = 6910.271779431480;
double diff;
const int year = 2018;
const int month = 12;
const int day = 2;
const int hour = 18;
const int minute = 30;
const double second = 12.543;
time = Astronomy_MakeTime(year, month, day, hour, minute, second);
printf("Test_AstroTime: ut=%0.6lf, tt=%0.6lf\n", time.ut, time.tt);
diff = time.ut - expected_ut;
if (fabs(diff) > 1.0e-12)
{
fprintf(stderr, "Test_AstroTime: excessive UT error %lg\n", diff);
return 1;
}
diff = time.tt - expected_tt;
if (fabs(diff) > 1.0e-12)
{
fprintf(stderr, "Test_AstroTime: excessive TT error %lg\n", diff);
return 1;
}
utc = Astronomy_UtcFromTime(time);
if (utc.year != year || utc.month != month || utc.day != day || utc.minute != minute || utc.hour != hour)
{
fprintf(stderr, "Test_AstroTime: UtcFromTime FAILURE - Expected %04d-%02d-%02dT%02d:%02dZ, found %04d-%02d-%02dT%02d:%02dZ\n",
year, month, day, hour, minute,
utc.year, utc.month, utc.day, utc.hour, utc.minute);
return 1;
}
diff = utc.second - second;
if (fabs(diff) > 2.0e-5)
{
fprintf(stderr, "Test_AstroTime: excessive UTC second error %lg\n", diff);
return 1;
}
return 0;
}
static int AstroCheck(void)
{
int error = 1;
FILE *outfile = NULL;
const char *filename = "temp/c_check.txt";
astro_time_t time;
astro_time_t stop;
astro_body_t body;
astro_vector_t pos;
astro_equatorial_t j2000;
astro_equatorial_t ofdate;
astro_horizon_t hor;
astro_observer_t observer = Astronomy_MakeObserver(29.0, -81.0, 10.0);
int b;
static const astro_body_t bodylist[] = /* match the order in the JavaScript unit test */
{
BODY_SUN, BODY_MERCURY, BODY_VENUS, BODY_EARTH, BODY_MARS,
BODY_JUPITER, BODY_SATURN, BODY_URANUS, BODY_NEPTUNE, BODY_PLUTO
};
static int nbodies = sizeof(bodylist) / sizeof(bodylist[0]);
outfile = fopen(filename, "wt");
if (outfile == NULL)
{
fprintf(stderr, "AstroCheck: Cannot open output file: %s\n", filename);
error = 1;
goto fail;
}
fprintf(outfile, "o %lf %lf %lf\n", observer.latitude, observer.longitude, observer.height);
time = Astronomy_MakeTime(1700, 1, 1, 0, 0, 0.0);
stop = Astronomy_MakeTime(2200, 1, 1, 0, 0, 0.0);
while (time.tt < stop.tt)
{
for (b=0; b < nbodies; ++b)
{
body = bodylist[b];
CHECK_VECTOR(pos, Astronomy_HelioVector(body, time));
fprintf(outfile, "v %s %0.16lf %0.16lf %0.16lf %0.16lf\n", Astronomy_BodyName(body), pos.t.tt, pos.x, pos.y, pos.z);
if (body != BODY_EARTH)
{
CHECK_EQU(j2000, Astronomy_Equator(body, &time, observer, EQUATOR_J2000, NO_ABERRATION));
CHECK_EQU(ofdate, Astronomy_Equator(body, &time, observer, EQUATOR_OF_DATE, ABERRATION));
hor = Astronomy_Horizon(&time, observer, ofdate.ra, ofdate.dec, REFRACTION_NONE);
fprintf(outfile, "s %s %0.16lf %0.16lf %0.16lf %0.16lf %0.16lf %0.16lf %0.16lf\n",
Astronomy_BodyName(body), time.tt, time.ut, j2000.ra, j2000.dec, j2000.dist, hor.azimuth, hor.altitude);
}
}
CHECK_VECTOR(pos, Astronomy_GeoVector(BODY_MOON, time, NO_ABERRATION));
fprintf(outfile, "v GM %0.16lf %0.16lf %0.16lf %0.16lf\n", pos.t.tt, pos.x, pos.y, pos.z);
CHECK_EQU(j2000, Astronomy_Equator(BODY_MOON, &time, observer, EQUATOR_J2000, NO_ABERRATION));
CHECK_EQU(ofdate, Astronomy_Equator(BODY_MOON, &time, observer, EQUATOR_OF_DATE, ABERRATION));
hor = Astronomy_Horizon(&time, observer, ofdate.ra, ofdate.dec, REFRACTION_NONE);
fprintf(outfile, "s GM %0.16lf %0.16lf %0.16lf %0.16lf %0.16lf %0.16lf %0.16lf\n",
time.tt, time.ut, j2000.ra, j2000.dec, j2000.dist, hor.azimuth, hor.altitude);
time = Astronomy_AddDays(time, 10.0 + PI/100.0);
}
fail:
if (outfile != NULL)
fclose(outfile);
return error;
}
/*-----------------------------------------------------------------------------------------------------------*/
static int Diff(const char *c_filename, const char *js_filename)
{
int error = 1;
int lnum;
FILE *cfile = NULL;
FILE *jfile = NULL;
char cline[200];
char jline[200];
char *cread;
char *jread;
double maxdiff = 0.0;
int worst_lnum = 0;
cfile = fopen(c_filename, "rt");
if (cfile == NULL)
{
fprintf(stderr, "ctest(Diff): Cannot open input file: %s\n", c_filename);
error = 1;
goto fail;
}
jfile = fopen(js_filename, "rt");
if (jfile == NULL)
{
fprintf(stderr, "ctest(Diff): Cannot open input file: %s\n", js_filename);
error = 1;
goto fail;
}
lnum = 0;
for(;;)
{
cread = fgets(cline, sizeof(cline), cfile);
jread = fgets(jline, sizeof(jline), jfile);
if (cread==NULL && jread==NULL)
break; /* normal end of both files */
if (cread==NULL || jread==NULL)
{
fprintf(stderr, "ctest(Diff): Files do not have same number of lines: %s and %s\n", c_filename, js_filename);
error = 1;
goto fail;
}
++lnum;
CHECK(DiffLine(lnum, cline, jline, &maxdiff, &worst_lnum));
}
printf("ctest(Diff): Maximum numeric difference = %lg, worst line number = %d\n", maxdiff, worst_lnum);
if (maxdiff > 1.819e-12)
{
fprintf(stderr, "ERROR: Excessive error comparing files %s and %s\n", c_filename, js_filename);
error = 1;
goto fail;
}
error = 0;
fail:
if (cfile != NULL) fclose(cfile);
if (jfile != NULL) fclose(jfile);
return error;
}
static int DiffLine(int lnum, const char *cline, const char *jline, double *maxdiff, int *worst_lnum)
{
int error = 1;
char cbody[10];
char jbody[10];
double cdata[7];
double jdata[7];
double diff;
int i, nc, nj, nrequired = -1;
/* be paranoid: make sure we can't possibly have a fake match. */
memset(cdata, 0xdc, sizeof(cdata));
memset(jdata, 0xce, sizeof(jdata));
/* Make sure the two data records are the same type. */
if (cline[0] != jline[0])
{
fprintf(stderr, "ctest(DiffLine): Line %d mismatch record type: '%c' vs '%c'.\n", lnum, cline[0], jline[0]);
error = 1;
goto fail;
}
switch (cline[0])
{
case 'o': /* observer */
nc = sscanf(cline, "o %lf %lf %lf", &cdata[0], &cdata[1], &cdata[2]);
nj = sscanf(jline, "o %lf %lf %lf", &jdata[0], &jdata[1], &jdata[2]);
cbody[0] = jbody[0] = '\0';
nrequired = 3;
break;
case 'v': /* heliocentric vector */
nc = sscanf(cline, "v %9[A-Za-z] %lf %lf %lf", cbody, &cdata[0], &cdata[1], &cdata[2]);
nj = sscanf(jline, "v %9[A-Za-z] %lf %lf %lf", jbody, &jdata[0], &jdata[1], &jdata[2]);
nrequired = 4;
break;
case 's': /* sky coords: ecliptic and horizontal */
nc = sscanf(cline, "s %9[A-Za-z] %lf %lf %lf %lf %lf %lf %lf", cbody, &cdata[0], &cdata[1], &cdata[2], &cdata[3], &cdata[4], &cdata[5], &cdata[6]);
nj = sscanf(jline, "s %9[A-Za-z] %lf %lf %lf %lf %lf %lf %lf", jbody, &jdata[0], &jdata[1], &jdata[2], &jdata[3], &jdata[4], &jdata[5], &jdata[6]);
nrequired = 8;
break;
default:
fprintf(stderr, "ctest(DiffLine): Line %d type '%c' is not a valid record type.\n", lnum, cline[0]);
error = 1;
goto fail;
}
if (nc != nj)
{
fprintf(stderr, "ctest(DiffLine): Line %d mismatch data counts: %d vs %d\n", lnum, nc, nj);
error = 1;
goto fail;
}
if (nc != nrequired)
{
fprintf(stderr, "ctest(DiffLine): Line %d incorrect number of scanned arguments: %d\n", lnum, nc);
error = 1;
goto fail;
}
if (strcmp(cbody, jbody))
{
fprintf(stderr, "ctest(DiffLine): Line %d body mismatch: '%s' vs '%s'\n.", lnum, cbody, jbody);
error = 1;
goto fail;
}
if (cbody[0])
{
/* This is one of the record types that contains a body name. */
/* Therefore, we need to correct the number of numeric data. */
--nrequired;
}
/* Verify all the numeric data are very close. */
for (i=0; i < nrequired; ++i)
{
diff = fabs(cdata[i] - jdata[i]);
if (diff > *maxdiff)
{
*maxdiff = diff;
*worst_lnum = lnum;
}
}
error = 0;
fail:
return error;
}
/*-----------------------------------------------------------------------------------------------------------*/
static int SeasonsTest(const char *filename)
{
int error = 1;
int lnum;
FILE *infile = NULL;
char line[200];
int nscanned, year, month, day, hour, minute;
int current_year = 0;
char name[20];
astro_time_t correct_time;
astro_time_t calc_time;
astro_seasons_t seasons;
double diff_minutes, max_minutes = 0.0;
int mar_count=0, jun_count=0, sep_count=0, dec_count=0;
memset(&seasons, 0, sizeof(seasons));
infile = fopen(filename, "rt");
if (infile == NULL)
{
fprintf(stderr, "SeasonsTest: Cannot open input file: %s\n", filename);
error = 1;
goto fail;
}
lnum = 0;
while (fgets(line, sizeof(line), infile))
{
++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
*/
nscanned = sscanf(line, "%d-%d-%dT%d:%dZ %10[A-Za-z]", &year, &month, &day, &hour, &minute, name);
if (nscanned != 6)
{
fprintf(stderr, "SeasonsTest: %s line %d : scanned %d, expected 6\n", filename, lnum, nscanned);
error = 1;
goto fail;
}
if (year != current_year)
{
current_year = year;
seasons = Astronomy_Seasons(year);
if (seasons.status != ASTRO_SUCCESS)
{
fprintf(stderr, "SeasonsTest: Astronomy_Seasons(%d) returned %d\n", year, seasons.status);
error = 1;
goto fail;
}
}
memset(&calc_time, 0xcd, sizeof(calc_time));
correct_time = Astronomy_MakeTime(year, month, day, hour, minute, 0.0);
if (!strcmp(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:
fprintf(stderr, "SeasonsTest: Invalid equinox date in test data: %s line %d\n", filename, lnum);
error = 1;
goto fail;
}
}
else if (!strcmp(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:
fprintf(stderr, "SeasonsTest: Invalid solstice date in test data: %s line %d\n", filename, lnum);
error = 1;
goto fail;
}
}
else if (!strcmp(name, "Aphelion"))
{
/* not yet calculated */
continue;
}
else if (!strcmp(name, "Perihelion"))
{
/* not yet calculated */
continue;
}
else
{
fprintf(stderr, "SeasonsTest: %s line %d: unknown event type '%s'\n", filename, lnum, name);
error = 1;
goto fail;
}
/* Verify that the calculated time matches the correct time for this event. */
diff_minutes = (24.0 * 60.0) * fabs(calc_time.tt - correct_time.tt);
if (diff_minutes > max_minutes)
max_minutes = diff_minutes;
if (diff_minutes > 1.7)
{
fprintf(stderr, "SeasonsTest: %s line %d: excessive error (%s): %lf minutes.\n", filename, lnum, name, diff_minutes);
error = 1;
goto fail;
}
}
printf("SeasonsTest: verified %d lines from file %s : max error minutes = %0.3lf\n", lnum, filename, max_minutes);
printf("SeasonsTest: Event counts: mar=%d, jun=%d, sep=%d, dec=%d\n", mar_count, jun_count, sep_count, dec_count);
error = 0;
fail:
if (infile != NULL) fclose(infile);
return error;
}
/*-----------------------------------------------------------------------------------------------------------*/
static int MoonPhase(const char *filename)
{
int error = 1;
FILE *infile = NULL;
int lnum, nscanned;
int quarter, year, month, day, hour, minute;
int expected_quarter, quarter_count = 0;
int prev_year = 0;
double second, expected_elong;
astro_time_t expected_time, start_time;
astro_angle_result_t result;
double degree_error, arcmin, max_arcmin = 0.0;
double diff_seconds, maxdiff = 0.0;
const double threshold_seconds = 120.0; /* max tolerable prediction error in seconds */
astro_moon_quarter_t mq;
char line[200];
memset(&mq, 0xcd, sizeof(mq));
infile = fopen(filename, "rt");
if (infile == NULL)
{
fprintf(stderr, "MoonPhase: Cannot open input file '%s'\n", filename);
error = 1;
goto fail;
}
/*
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
*/
lnum = 0;
while (fgets(line, sizeof(line), infile))
{
++lnum;
nscanned = sscanf(line, "%d %d-%d-%dT%d:%d:%lfZ", &quarter, &year, &month, &day, &hour, &minute, &second);
if (nscanned != 7)
{
fprintf(stderr, "MoonPhase(%s line %d): Invalid data format\n", filename, lnum);
error = 1;
goto fail;
}
if (quarter < 0 || quarter > 3)
{
fprintf(stderr, "MoonPhase(%s line %d): Invalid quarter %d\n", filename, lnum, quarter);
error = 1;
goto fail;
}
expected_elong = 90.0 * quarter;
expected_time = Astronomy_MakeTime(year, month, day, hour, minute, second);
result = Astronomy_MoonPhase(expected_time);
degree_error = fabs(result.angle - expected_elong);
if (degree_error > 180.0)
degree_error = 360 - degree_error;
arcmin = 60.0 * degree_error;
if (arcmin > 1.0)
{
fprintf(stderr, "MoonPhase(%s line %d): EXCESSIVE ANGULAR ERROR: %lg arcmin\n", filename, lnum, arcmin);
error = 1;
goto fail;
}
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. */
start_time = Astronomy_MakeTime(year, 1, 1, 0, 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)
{
fprintf(stderr, "MoonPhase(%s line %d): Astronomy_SearchMoonQuarter returned quarter %d, but expected %d\n", filename, lnum, mq.quarter, expected_quarter);
error = 1;
goto fail;
}
}
if (mq.status != ASTRO_SUCCESS)
{
fprintf(stderr, "MoonPhase(%s line %d): Astronomy_SearchMoonQuarter returned %d\n", filename, lnum, mq.status);
error = 1;
goto fail;
}
++quarter_count;
/* Make sure the time matches what we expect. */
diff_seconds = fabs(mq.time.tt - expected_time.tt) * (24.0 * 3600.0);
if (diff_seconds > threshold_seconds)
{
fprintf(stderr, "MoonPhase(%s line %d): excessive time error %0.3lf seconds\n", filename, lnum, diff_seconds);
error = 1;
goto fail;
}
if (diff_seconds > maxdiff)
maxdiff = diff_seconds;
}
printf("MoonPhase: passed %d lines for file %s : max_arcmin = %0.6lf, maxdiff = %0.3lf seconds, %d quarters\n", lnum, filename, max_arcmin, maxdiff, quarter_count);
error = 0;
fail:
if (infile != NULL) fclose(infile);
return error;
}
/*-----------------------------------------------------------------------------------------------------------*/
static int TestElongFile(const char *filename, double targetRelLon)
{
int error = 1;
FILE *infile = NULL;
int lnum;
char line[100];
char name[20];
int year, month, day, hour, minute;
int nscanned;
astro_time_t search_date, expected_time;
astro_body_t body;
astro_search_result_t search_result;
double diff_minutes;
infile = fopen(filename, "rt");
if (infile == NULL)
{
fprintf(stderr, "TestElongFile: Cannot open input file: %s\n", filename);
error = 1;
goto fail;
}
lnum = 0;
while (fgets(line, sizeof(line), infile))
{
++lnum;
/* 2018-05-09T00:28Z Jupiter */
nscanned = sscanf(line, "%d-%d-%dT%d:%dZ %9[A-Za-z]", &year, &month, &day, &hour, &minute, name);
if (nscanned != 6)
{
fprintf(stderr, "TestElongFile(%s line %d): Invalid data format.\n", filename, lnum);
error = 1;
goto fail;
}
body = Astronomy_BodyCode(name);
if (body == BODY_INVALID)
{
fprintf(stderr, "TestElongFile(%s line %d): Invalid body name '%s'\n", filename, lnum, name);
error = 1;
goto fail;
}
search_date = Astronomy_MakeTime(year, 1, 1, 0, 0, 0.0);
expected_time = Astronomy_MakeTime(year, month, day, hour, minute, 0.0);
search_result = Astronomy_SearchRelativeLongitude(body, targetRelLon, search_date);
if (search_result.status != ASTRO_SUCCESS)
{
fprintf(stderr, "TestElongFile(%s line %d): SearchRelativeLongitude returned %d\n", filename, lnum, search_result.status);
error = 1;
goto fail;
}
diff_minutes = (24.0 * 60.0) * (search_result.time.tt - expected_time.tt);
printf("TestElongFile: %-7s error = %6.3lf minutes\n", name, diff_minutes);
if (fabs(diff_minutes) > 15.0)
{
fprintf(stderr, "TestElongFile(%s line %d): EXCESSIVE ERROR\n", filename, lnum);
error = 1;
goto fail;
}
}
printf("TestElongFile: passed %d rows of data\n", lnum);
error = 0;
fail:
if (infile != NULL) fclose(infile);
return error;
}
typedef struct
{
astro_body_t body;
const char *searchDate;
const char *eventDate;
double angle;
astro_visibility_t visibility;
}
elong_test_t;
static const elong_test_t ElongTestData[] =
{
/* Max elongation data obtained from: */
/* http://www.skycaramba.com/greatest_elongations.shtml */
{ BODY_MERCURY, "2010-01-17T05:22Z", "2010-01-27T05:22Z", 24.80, VISIBLE_MORNING },
{ BODY_MERCURY, "2010-05-16T02:15Z", "2010-05-26T02:15Z", 25.10, VISIBLE_MORNING },
{ BODY_MERCURY, "2010-09-09T17:24Z", "2010-09-19T17:24Z", 17.90, VISIBLE_MORNING },
{ BODY_MERCURY, "2010-12-30T14:33Z", "2011-01-09T14:33Z", 23.30, VISIBLE_MORNING },
{ BODY_MERCURY, "2011-04-27T19:03Z", "2011-05-07T19:03Z", 26.60, VISIBLE_MORNING },
{ BODY_MERCURY, "2011-08-24T05:52Z", "2011-09-03T05:52Z", 18.10, VISIBLE_MORNING },
{ BODY_MERCURY, "2011-12-13T02:56Z", "2011-12-23T02:56Z", 21.80, VISIBLE_MORNING },
{ BODY_MERCURY, "2012-04-08T17:22Z", "2012-04-18T17:22Z", 27.50, VISIBLE_MORNING },
{ BODY_MERCURY, "2012-08-06T12:04Z", "2012-08-16T12:04Z", 18.70, VISIBLE_MORNING },
{ BODY_MERCURY, "2012-11-24T22:55Z", "2012-12-04T22:55Z", 20.60, VISIBLE_MORNING },
{ BODY_MERCURY, "2013-03-21T22:02Z", "2013-03-31T22:02Z", 27.80, VISIBLE_MORNING },
{ BODY_MERCURY, "2013-07-20T08:51Z", "2013-07-30T08:51Z", 19.60, VISIBLE_MORNING },
{ BODY_MERCURY, "2013-11-08T02:28Z", "2013-11-18T02:28Z", 19.50, VISIBLE_MORNING },
{ BODY_MERCURY, "2014-03-04T06:38Z", "2014-03-14T06:38Z", 27.60, VISIBLE_MORNING },
{ BODY_MERCURY, "2014-07-02T18:22Z", "2014-07-12T18:22Z", 20.90, VISIBLE_MORNING },
{ BODY_MERCURY, "2014-10-22T12:36Z", "2014-11-01T12:36Z", 18.70, VISIBLE_MORNING },
{ BODY_MERCURY, "2015-02-14T16:20Z", "2015-02-24T16:20Z", 26.70, VISIBLE_MORNING },
{ BODY_MERCURY, "2015-06-14T17:10Z", "2015-06-24T17:10Z", 22.50, VISIBLE_MORNING },
{ BODY_MERCURY, "2015-10-06T03:20Z", "2015-10-16T03:20Z", 18.10, VISIBLE_MORNING },
{ BODY_MERCURY, "2016-01-28T01:22Z", "2016-02-07T01:22Z", 25.60, VISIBLE_MORNING },
{ BODY_MERCURY, "2016-05-26T08:45Z", "2016-06-05T08:45Z", 24.20, VISIBLE_MORNING },
{ BODY_MERCURY, "2016-09-18T19:27Z", "2016-09-28T19:27Z", 17.90, VISIBLE_MORNING },
{ BODY_MERCURY, "2017-01-09T09:42Z", "2017-01-19T09:42Z", 24.10, VISIBLE_MORNING },
{ BODY_MERCURY, "2017-05-07T23:19Z", "2017-05-17T23:19Z", 25.80, VISIBLE_MORNING },
{ BODY_MERCURY, "2017-09-02T10:14Z", "2017-09-12T10:14Z", 17.90, VISIBLE_MORNING },
{ BODY_MERCURY, "2017-12-22T19:48Z", "2018-01-01T19:48Z", 22.70, VISIBLE_MORNING },
{ BODY_MERCURY, "2018-04-19T18:17Z", "2018-04-29T18:17Z", 27.00, VISIBLE_MORNING },
{ BODY_MERCURY, "2018-08-16T20:35Z", "2018-08-26T20:35Z", 18.30, VISIBLE_MORNING },
{ BODY_MERCURY, "2018-12-05T11:34Z", "2018-12-15T11:34Z", 21.30, VISIBLE_MORNING },
{ BODY_MERCURY, "2019-04-01T19:40Z", "2019-04-11T19:40Z", 27.70, VISIBLE_MORNING },
{ BODY_MERCURY, "2019-07-30T23:08Z", "2019-08-09T23:08Z", 19.00, VISIBLE_MORNING },
{ BODY_MERCURY, "2019-11-18T10:31Z", "2019-11-28T10:31Z", 20.10, VISIBLE_MORNING },
{ BODY_MERCURY, "2010-03-29T23:32Z", "2010-04-08T23:32Z", 19.40, VISIBLE_EVENING },
{ BODY_MERCURY, "2010-07-28T01:03Z", "2010-08-07T01:03Z", 27.40, VISIBLE_EVENING },
{ BODY_MERCURY, "2010-11-21T15:42Z", "2010-12-01T15:42Z", 21.50, VISIBLE_EVENING },
{ BODY_MERCURY, "2011-03-13T01:07Z", "2011-03-23T01:07Z", 18.60, VISIBLE_EVENING },
{ BODY_MERCURY, "2011-07-10T04:56Z", "2011-07-20T04:56Z", 26.80, VISIBLE_EVENING },
{ BODY_MERCURY, "2011-11-04T08:40Z", "2011-11-14T08:40Z", 22.70, VISIBLE_EVENING },
{ BODY_MERCURY, "2012-02-24T09:39Z", "2012-03-05T09:39Z", 18.20, VISIBLE_EVENING },
{ BODY_MERCURY, "2012-06-21T02:00Z", "2012-07-01T02:00Z", 25.70, VISIBLE_EVENING },
{ BODY_MERCURY, "2012-10-16T21:59Z", "2012-10-26T21:59Z", 24.10, VISIBLE_EVENING },
{ BODY_MERCURY, "2013-02-06T21:24Z", "2013-02-16T21:24Z", 18.10, VISIBLE_EVENING },
{ BODY_MERCURY, "2013-06-02T16:45Z", "2013-06-12T16:45Z", 24.30, VISIBLE_EVENING },
{ BODY_MERCURY, "2013-09-29T09:59Z", "2013-10-09T09:59Z", 25.30, VISIBLE_EVENING },
{ BODY_MERCURY, "2014-01-21T10:00Z", "2014-01-31T10:00Z", 18.40, VISIBLE_EVENING },
{ BODY_MERCURY, "2014-05-15T07:06Z", "2014-05-25T07:06Z", 22.70, VISIBLE_EVENING },
{ BODY_MERCURY, "2014-09-11T22:20Z", "2014-09-21T22:20Z", 26.40, VISIBLE_EVENING },
{ BODY_MERCURY, "2015-01-04T20:26Z", "2015-01-14T20:26Z", 18.90, VISIBLE_EVENING },
{ BODY_MERCURY, "2015-04-27T04:46Z", "2015-05-07T04:46Z", 21.20, VISIBLE_EVENING },
{ BODY_MERCURY, "2015-08-25T10:20Z", "2015-09-04T10:20Z", 27.10, VISIBLE_EVENING },
{ BODY_MERCURY, "2015-12-19T03:11Z", "2015-12-29T03:11Z", 19.70, VISIBLE_EVENING },
{ BODY_MERCURY, "2016-04-08T14:00Z", "2016-04-18T14:00Z", 19.90, VISIBLE_EVENING },
{ BODY_MERCURY, "2016-08-06T21:24Z", "2016-08-16T21:24Z", 27.40, VISIBLE_EVENING },
{ BODY_MERCURY, "2016-12-01T04:36Z", "2016-12-11T04:36Z", 20.80, VISIBLE_EVENING },
{ BODY_MERCURY, "2017-03-22T10:24Z", "2017-04-01T10:24Z", 19.00, VISIBLE_EVENING },
{ BODY_MERCURY, "2017-07-20T04:34Z", "2017-07-30T04:34Z", 27.20, VISIBLE_EVENING },
{ BODY_MERCURY, "2017-11-14T00:32Z", "2017-11-24T00:32Z", 22.00, VISIBLE_EVENING },
{ BODY_MERCURY, "2018-03-05T15:07Z", "2018-03-15T15:07Z", 18.40, VISIBLE_EVENING },
{ BODY_MERCURY, "2018-07-02T05:24Z", "2018-07-12T05:24Z", 26.40, VISIBLE_EVENING },
{ BODY_MERCURY, "2018-10-27T15:25Z", "2018-11-06T15:25Z", 23.30, VISIBLE_EVENING },
{ BODY_MERCURY, "2019-02-17T01:23Z", "2019-02-27T01:23Z", 18.10, VISIBLE_EVENING },
{ BODY_MERCURY, "2019-06-13T23:14Z", "2019-06-23T23:14Z", 25.20, VISIBLE_EVENING },
{ BODY_MERCURY, "2019-10-10T04:00Z", "2019-10-20T04:00Z", 24.60, VISIBLE_EVENING },
{ BODY_VENUS, "2010-12-29T15:57Z", "2011-01-08T15:57Z", 47.00, VISIBLE_MORNING },
{ BODY_VENUS, "2012-08-05T08:59Z", "2012-08-15T08:59Z", 45.80, VISIBLE_MORNING },
{ BODY_VENUS, "2014-03-12T19:25Z", "2014-03-22T19:25Z", 46.60, VISIBLE_MORNING },
{ BODY_VENUS, "2015-10-16T06:57Z", "2015-10-26T06:57Z", 46.40, VISIBLE_MORNING },
{ BODY_VENUS, "2017-05-24T13:09Z", "2017-06-03T13:09Z", 45.90, VISIBLE_MORNING },
{ BODY_VENUS, "2018-12-27T04:24Z", "2019-01-06T04:24Z", 47.00, VISIBLE_MORNING },
{ BODY_VENUS, "2010-08-10T03:19Z", "2010-08-20T03:19Z", 46.00, VISIBLE_EVENING },
{ BODY_VENUS, "2012-03-17T08:03Z", "2012-03-27T08:03Z", 46.00, VISIBLE_EVENING },
{ BODY_VENUS, "2013-10-22T08:00Z", "2013-11-01T08:00Z", 47.10, VISIBLE_EVENING },
{ BODY_VENUS, "2015-05-27T18:46Z", "2015-06-06T18:46Z", 45.40, VISIBLE_EVENING },
{ BODY_VENUS, "2017-01-02T13:19Z", "2017-01-12T13:19Z", 47.10, VISIBLE_EVENING },
{ BODY_VENUS, "2018-08-07T17:02Z", "2018-08-17T17:02Z", 45.90, VISIBLE_EVENING }
};
static const int ElongTestCount = sizeof(ElongTestData) / sizeof(ElongTestData[0]);
static int ParseDate(const char *text, astro_time_t *time)
{
int year, month, day, hour, minute, nscanned;
nscanned = sscanf(text, "%d-%d-%dT%d:%dZ", &year, &month, &day, &hour, &minute);
if (nscanned != 5)
{
fprintf(stderr, "ParseDate: Invalid date text '%s'\n", text);
time->ut = time->tt = NAN;
return 1;
}
*time = Astronomy_MakeTime(year, month, day, hour, minute, 0.0);
return 0;
}
static int TestMaxElong(const elong_test_t *test)
{
int error;
astro_time_t searchTime, eventTime;
astro_elongation_t evt;
double hour_diff, arcmin_diff;
const char *name = NULL;
const char *vis = NULL;
switch (test->body)
{
case BODY_MERCURY: name = "Mercury"; break;
case BODY_VENUS: name = "Venus"; break;
default:
fprintf(stderr, "TestMaxElong: invalid body %d in test data.\n", test->body);
error = 1;
goto fail;
}
switch (test->visibility)
{
case VISIBLE_MORNING: vis = "morning"; break;
case VISIBLE_EVENING: vis = "evening"; break;
default:
fprintf(stderr, "TestMaxElong: invalid visibility %d in test data.\n", test->visibility);
error = 1;
goto fail;
}
CHECK(ParseDate(test->searchDate, &searchTime));
CHECK(ParseDate(test->eventDate, &eventTime));
evt = Astronomy_SearchMaxElongation(test->body, searchTime);
if (evt.status != ASTRO_SUCCESS)
{
fprintf(stderr, "TestMaxElong(%s %s): SearchMaxElongation returned %d\n", name, test->searchDate, evt.status);
error = 1;
goto fail;
}
hour_diff = 24.0 * fabs(evt.time.tt - eventTime.tt);
arcmin_diff = 60.0 * fabs(evt.elongation - test->angle);
printf("TestMaxElong: %-7s %-7s elong=%5.2lf (%4.2lf arcmin, %5.3lf hours)\n", name, vis, evt.elongation, arcmin_diff, hour_diff);
if (hour_diff > 0.6)
{
fprintf(stderr, "TestMaxElong(%s %s): excessive hour error.\n", name, test->searchDate);
error = 1;
goto fail;
}
if (arcmin_diff > 3.4)
{
fprintf(stderr, "TestMaxElong(%s %s): excessive arcmin error.\n", name, test->searchDate);
error = 1;
goto fail;
}
fail:
return error;
}
static int SearchElongTest()
{
int error = 1;
int i;
for (i=0; i < ElongTestCount; ++i)
CHECK(TestMaxElong(&ElongTestData[i]));
printf("SearchElongTest: Passed %d rows\n", ElongTestCount);
error = 0;
fail:
return error;
}
static int TestPlanetLongitudes(
astro_body_t body,
const char *outFileName,
const char *zeroLonEventName)
{
int error = 1;
const int startYear = 1700;
const int stopYear = 2200;
astro_time_t time, stopTime;
double rlon = 0.0;
const char *event;
astro_search_result_t search_result;
int count = 0;
double day_diff, min_diff = 1.0e+99, max_diff = 1.0e+99, sum_diff = 0.0;
astro_vector_t geo;
double dist;
FILE *outfile = NULL;
const char *name;
double ratio, thresh;
name = Astronomy_BodyName(body);
if (!name[0])
{
fprintf(stderr, "TestPlanetLongitudes: Invalid body code %d\n", body);
error = 1;
goto fail;
}
outfile = fopen(outFileName, "wt");
if (outfile == NULL)
{
fprintf(stderr, "TestPlanetLongitudes: Cannot open output file: %s\n", outFileName);
error = 1;
goto fail;
}
time = Astronomy_MakeTime(startYear, 1, 1, 0, 0, 0.0);
stopTime = Astronomy_MakeTime(stopYear, 1, 1, 0, 0, 0.0);
while (time.tt < stopTime.tt)
{
++count;
event = (rlon == 0.0) ? zeroLonEventName : "sup";
search_result = Astronomy_SearchRelativeLongitude(body, rlon, time);
if (search_result.status != ASTRO_SUCCESS)
{
fprintf(stderr, "TestPlanetLongitudes(%s): SearchRelativeLongitude returned %d\n", name, search_result.status);
error = 1;
goto fail;
}
if (count >= 2)
{
/* Check for consistent intervals. */
/* Mainly I don't want to skip over an event! */
day_diff = search_result.time.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;
}
}
geo = Astronomy_GeoVector(body, search_result.time, ABERRATION);
if (geo.status != ASTRO_SUCCESS)
{
fprintf(stderr, "TestPlanetLongitudes(%s): GeoVector returned %d\n", name, geo.status);
error = 1;
goto fail;
}
dist = Astronomy_VectorLength(geo);
fprintf(outfile, "e %s %s %0.16lf %0.16lf\n", name, event, search_result.time.tt, dist);
/* Search for the opposite longitude event next time. */
time = search_result.time;
rlon = 180.0 - rlon;
}
switch (body)
{
case BODY_MERCURY: thresh = 1.65; break;
case BODY_MARS: thresh = 1.30; break;
default: thresh = 1.07; break;
}
ratio = max_diff / min_diff;
printf("TestPlanetLongitudes(%-7s): %5d events, ratio=%5.3lf, file: %s\n", name, count, ratio, outFileName);
if (ratio > thresh)
{
fprintf(stderr, "TestPlanetLongitudes(%s): excessive event interval ratio.\n", name);
error = 1;
goto fail;
}
error = 0;
fail:
if (outfile != NULL) fclose(outfile);
return error;
}
static int ElongationTest(void)
{
int error;
CHECK(TestElongFile("longitude/opposition_2018.txt", 0.0));
CHECK(TestPlanetLongitudes(BODY_MERCURY, "temp/c_longitude_Mercury.txt", "inf"));
CHECK(TestPlanetLongitudes(BODY_VENUS, "temp/c_longitude_Venus.txt", "inf"));
CHECK(TestPlanetLongitudes(BODY_MARS, "temp/c_longitude_Mars.txt", "opp"));
CHECK(TestPlanetLongitudes(BODY_JUPITER, "temp/c_longitude_Jupiter.txt", "opp"));
CHECK(TestPlanetLongitudes(BODY_SATURN, "temp/c_longitude_Saturn.txt", "opp"));
CHECK(TestPlanetLongitudes(BODY_URANUS, "temp/c_longitude_Uranus.txt", "opp"));
CHECK(TestPlanetLongitudes(BODY_NEPTUNE, "temp/c_longitude_Neptune.txt", "opp"));
CHECK(TestPlanetLongitudes(BODY_PLUTO, "temp/c_longitude_Pluto.txt", "opp"));
CHECK(SearchElongTest());
fail:
return error;
}
/*-----------------------------------------------------------------------------------------------------------*/
static int RiseSet(const char *filename)
{
int error = 1;
FILE *infile = NULL;
char line[100];
char name[20];
double longitude, latitude;
int year, month, day, hour, minute;
char kind[2]; /* "r" or "s" for rise or set */
int direction; /* +1 for rise, -1 for set */
int lnum, nscanned;
astro_time_t correct_date;
astro_body_t body, current_body;
astro_observer_t observer;
astro_time_t r_search_date;
astro_search_result_t r_evt, s_evt; /* rise event, set event: search results */
astro_search_result_t a_evt, b_evt; /* chronologically first and second events */
astro_time_t s_search_date;
int a_dir = 0, b_dir = 0;
double error_minutes, rms_minutes;
double sum_minutes = 0.0;
double max_minutes = 0.0;
const double nudge_days = 0.01;
observer.latitude = observer.longitude = observer.height = NAN;
current_body = BODY_INVALID;
a_evt.status = b_evt.status = r_evt.status = s_evt.status = ASTRO_NOT_INITIALIZED;
b_evt.time.tt = b_evt.time.ut = a_evt.time.tt = a_evt.time.ut = NAN;
infile = fopen(filename, "rt");
if (infile == NULL)
{
fprintf(stderr, "RiseSet: cannot open input file: %s\n", filename);
error = 1;
goto fail;
}
lnum = 0;
while (fgets(line, sizeof(line), infile))
{
++lnum;
// Moon 103 -61 1944-01-02T17:08Z s
// Moon 103 -61 1944-01-03T05:47Z r
nscanned = sscanf(line, "%9[A-Za-z] %lf %lf %d-%d-%dT%d:%dZ %1[rs]",
name, &longitude, &latitude, &year, &month, &day, &hour, &minute, kind);
if (nscanned != 9)
{
fprintf(stderr, "RiseSet(%s line %d): invalid format\n", filename, lnum);
error = 1;
goto fail;
}
correct_date = Astronomy_MakeTime(year, month, day, hour, minute, 0.0);
if (!strcmp(kind, "r"))
direction = +1;
else if (!strcmp(kind, "s"))
direction = -1;
else
{
fprintf(stderr, "RiseSet(%s line %d): invalid kind '%s'\n", filename, lnum, kind);
error = 1;
goto fail;
}
body = Astronomy_BodyCode(name);
if (body == BODY_INVALID)
{
fprintf(stderr, "RiseSet(%s line %d): invalid body name '%s'", filename, lnum, name);
error = 1;
goto fail;
}
/* 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.latitude != latitude || observer.longitude != longitude || current_body != body)
{
current_body = body;
observer = Astronomy_MakeObserver(latitude, longitude, 0.0);
r_search_date = s_search_date = Astronomy_MakeTime(year, 1, 1, 0, 0, 0.0);
b_evt.time.tt = b_evt.time.ut = NAN;
b_evt.status = ASTRO_NOT_INITIALIZED;
printf("RiseSet: %-7s lat=%0.1lf lon=%0.1lf\n", name, latitude, longitude);
}
if (b_evt.status == ASTRO_SUCCESS) /* has b_evt been initialized? (does it contain a valid event?) */
{
/* Recycle the second event from the previous iteration as the first event. */
a_evt = b_evt;
a_dir = b_dir;
b_evt.status = ASTRO_NOT_INITIALIZED; /* invalidate b_evt for the next time around the loop */
}
else
{
r_evt = Astronomy_SearchRiseSet(body, observer, DIRECTION_RISE, r_search_date, 366.0);
if (r_evt.status != ASTRO_SUCCESS)
{
fprintf(stderr, "RiseSet(%s line %d): did not find %s rise event.\n", filename, lnum, name);
error = 1;
goto fail;
}
s_evt = Astronomy_SearchRiseSet(body, observer, DIRECTION_SET, s_search_date, 366.0);
if (s_evt.status != ASTRO_SUCCESS)
{
fprintf(stderr, "RiseSet(%s line %d): did not find %s set event.\n", filename, lnum, name);
error = 1;
goto fail;
}
/* Expect the current event to match the earlier of the found dates. */
if (r_evt.time.tt < s_evt.time.tt)
{
a_evt = r_evt;
b_evt = s_evt;
a_dir = +1;
b_dir = -1;
}
else
{
a_evt = s_evt;
b_evt = r_evt;
a_dir = -1;
b_dir = +1;
}
/* Nudge the event times forward a tiny amount. */
r_search_date = Astronomy_AddDays(r_evt.time, nudge_days);
s_search_date = Astronomy_AddDays(s_evt.time, nudge_days);
}
if (a_dir != direction)
{
fprintf(stderr, "RiseSet(%s line %d): expected dir=%d but found %d\n", filename, lnum, a_dir, direction);
error = 1;
goto fail;
}
error_minutes = (24.0 * 60.0) * fabs(a_evt.time.tt - correct_date.tt);
sum_minutes += error_minutes * error_minutes;
if (error_minutes > max_minutes)
max_minutes = error_minutes;
if (error_minutes > 0.56)
{
fprintf(stderr, "RiseSet(%s line %d): excessive prediction time error = %lg minutes.\n", filename, lnum, error_minutes);
error = 1;
goto fail;
}
}
rms_minutes = sqrt(sum_minutes / lnum);
printf("RiseSet: passed %d lines: time errors in minutes: rms=%0.4lf, max=%0.4lf\n", lnum, rms_minutes, max_minutes);
error = 0;
fail:
if (infile != NULL) fclose(infile);
return error;
}
/*-----------------------------------------------------------------------------------------------------------*/
static int CheckMagnitudeData(astro_body_t body, const char *filename)
{
int error = 1;
int lnum, count;
FILE *infile = NULL;
char line[200];
const char *rest;
astro_time_t time;
astro_illum_t illum;
int nscanned;
double mag, sbrt, dist, rdot, delta, deldot, phase_angle;
double diff, diff_lo = NAN, diff_hi = NAN, sum_squared_diff = 0.0, rms;
const double limit = 0.012;
infile = fopen(filename, "rt");
if (infile == NULL)
{
fprintf(stderr, "CheckMagnitudeData: cannot open input file: %s\n", filename);
error = 1;
goto fail;
}
count = lnum = 0;
while (fgets(line, sizeof(line), infile))
{
++lnum;
rest = ParseJplHorizonsDateTime(line, &time);
/* Ignore non-data rows and data rows that contain "n.a." */
if (rest && !strstr(rest, "n.a."))
{
nscanned = sscanf(rest, "%lf %lf %lf %lf %lf %lf %lf",
&mag, &sbrt, &dist, &rdot, &delta, &deldot, &phase_angle);
if (nscanned != 7)
{
fprintf(stderr, "CheckMagnitudeData(%s line %d): invalid data format\n", filename, lnum);
error = 1;
goto fail;
}
illum = Astronomy_Illumination(body, time);
if (illum.status != ASTRO_SUCCESS)
{
fprintf(stderr, "CheckMagnitudeData(%s line %d): Astronomy_Illumination returned %d\n", filename, lnum, illum.status);
error = 1;
goto fail;
}
diff = illum.mag - mag;
if (fabs(diff) > limit)
{
fprintf(stderr, "CheckMagnitudeData(%s line %d): EXCESSIVE ERROR: correct mag=%lf, calc mag=%lf, diff=%lf\n", filename, lnum, mag, illum.mag, diff);
error = 1;
goto fail;
}
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)
{
fprintf(stderr, "CheckMagnitudeData: Did not find any data in file: %s\n", filename);
error = 1;
goto fail;
}
rms = sqrt(sum_squared_diff / count);
printf("CheckMagnitudeData: %-21s %5d rows diff_lo=%0.4lf diff_hi=%0.4lf rms=%0.4lf\n", filename, count, diff_lo, diff_hi, rms);
error = 0;
fail:
if (infile != NULL) fclose(infile);
return error;
}
static int 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 */
static const struct test_case
{
const char *date;
double mag;
double tilt;
}
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 }
};
static const int ncases = sizeof(data) / sizeof(data[0]);
int i;
astro_illum_t illum;
astro_time_t time;
double mag_diff, tilt_diff;
for (i=0; i < ncases; ++i)
{
int error = ParseDate(data[i].date, &time);
if (error)
return 1;
illum = Astronomy_Illumination(BODY_SATURN, time);
if (illum.status != ASTRO_SUCCESS)
{
fprintf(stderr, "CheckSaturn(%d): Illumination returned %d\n", i, illum.status);
return 1;
}
printf("Saturn: date=%s calc mag=%12.8lf ring_tilt=%12.8lf\n", data[i].date, illum.mag, illum.ring_tilt);
mag_diff = fabs(illum.mag - data[i].mag);
if (mag_diff > 1.0e-8)
{
fprintf(stderr, "ERROR: Excessive magnitude error %lg", mag_diff);
return 1;
}
tilt_diff = fabs(illum.ring_tilt - data[i].tilt);
if (tilt_diff > 1.0e-8)
{
fprintf(stderr, "ERROR: Excessive ring tilt error %lg\n", tilt_diff);
return 1;
}
}
return 0;
}
static int MoonTest(void)
{
astro_time_t time = Astronomy_MakeTime(2019, 6, 24, 15, 45, 37.0);
astro_vector_t vec = Astronomy_GeoMoon(time);
double dx, dy, dz, diff;
if (vec.status != ASTRO_SUCCESS)
{
fprintf(stderr, "MoonTest: ERROR: vec.status = %d\n", vec.status);
return 1;
}
printf("MoonTest: %0.16lg %0.16lg %0.16lg\n", vec.x, vec.y, vec.z);
dx = vec.x - (+0.002674036155459549);
dy = vec.y - (-0.0001531716308218381);
dz = vec.z - (-0.0003150201604895409);
diff = sqrt(dx*dx + dy*dy + dz*dz);
printf("MoonTest: diff = %lg\n", diff);
if (diff > 4.34e-19)
{
fprintf(stderr, "MoonTest: EXCESSIVE ERROR\n");
return 1;
}
return 0;
}
static int MagnitudeTest(void)
{
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 += CheckSaturn();
nfailed += CheckMagnitudeData(BODY_JUPITER, "magnitude/Jupiter.txt");
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)
fprintf(stderr, "MagnitudeTest: FAILED %d test(s).\n", nfailed);
return nfailed;
}
static int TestMaxMag(astro_body_t body, const char *filename)
{
int error = 1;
FILE *infile = NULL;
int lnum, nscanned;
int year1, month1, day1, hour1, minute1;
int year2, month2, day2, hour2, minute2;
double correct_mag, correct_angle1, correct_angle2;
char line[100];
astro_time_t search_time, time1, time2, center_time;
astro_illum_t illum;
double mag_diff, hours_diff;
/*
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.
*/
infile = fopen(filename, "rt");
if (infile == NULL)
{
fprintf(stderr, "TestMaxMag: Cannot open input file: %s\n", filename);
error = 1;
goto fail;
}
lnum = 0;
search_time = Astronomy_MakeTime(2001, 1, 1, 0, 0, 0.0);
while (fgets(line, sizeof(line), infile))
{
++lnum;
nscanned = sscanf(line, "%d-%d-%dT%d:%dZ %d-%d-%dT%d:%dZ %lf %lf %lf\n",
&year1, &month1, &day1, &hour1, &minute1,
&year2, &month2, &day2, &hour2, &minute2,
&correct_angle1, &correct_angle2, &correct_mag);
if (nscanned != 13)
{
fprintf(stderr, "TestMaxMag(%s line %d): invalid data format.\n", filename, lnum);
error = 1;
goto fail;
}
time1 = Astronomy_MakeTime(year1, month1, day1, hour1, minute1, 0.0);
time2 = Astronomy_MakeTime(year2, month2, day2, hour2, minute2, 0.0);
center_time = Astronomy_AddDays(time1, 0.5*(time2.ut - time1.ut));
illum = Astronomy_SearchPeakMagnitude(body, search_time);
if (illum.status != ASTRO_SUCCESS)
{
fprintf(stderr, "TestMaxMag(%s line %d): SearchPeakMagnitude returned %d\n", filename, lnum, illum.status);
error = 1;
goto fail;
}
mag_diff = fabs(illum.mag - correct_mag);
hours_diff = 24.0 * fabs(illum.time.ut - center_time.ut);
printf("TestMaxMag: mag_diff=%0.3lf, hours_diff=%0.3lf\n", mag_diff, hours_diff);
if (hours_diff > 7.1)
{
fprintf(stderr, "TestMaxMag(%s line %d): EXCESSIVE TIME DIFFERENCE.\n", filename, lnum);
error = 1;
goto fail;
}
if (mag_diff > 0.005)
{
fprintf(stderr, "TestMaxMag(%s line %d): EXCESSIVE MAGNITUDE DIFFERENCE.\n", filename, lnum);
error = 1;
goto fail;
}
search_time = time2;
}
printf("TestMaxMag: processed %d lines from file %s\n", lnum, filename);
error = 0;
fail:
if (infile != NULL) fclose(infile);
return error;
}
/*-----------------------------------------------------------------------------------------------------------*/
static const char *ParseJplHorizonsDateTime(const char *text, astro_time_t *time)
{
int year, month, day, hour, minute;
int nscanned;
char mtext[4];
char verify[30];
size_t length;
time->ut = time->tt = NAN;
while (*text == ' ' && *text != '\0')
++text;
nscanned = sscanf(text, "%d-%3[A-Za-z]-%d %d:%d", &year, mtext, &day, &hour, &minute);
if (nscanned != 5)
return NULL;
if (year < 1000 || year > 9999)
return NULL; /* if not a 4-digit year, we are skipping wrong number of chars */
if (day < 1 || day > 31)
return NULL;
if (hour < 0 || hour > 23)
return NULL;
if (minute < 0 || minute > 59)
return NULL;
if (!strcmp(mtext, "Jan"))
month = 1;
else if (!strcmp(mtext, "Feb"))
month = 2;
else if (!strcmp(mtext, "Mar"))
month = 3;
else if (!strcmp(mtext, "Apr"))
month = 4;
else if (!strcmp(mtext, "May"))
month = 5;
else if (!strcmp(mtext, "Jun"))
month = 6;
else if (!strcmp(mtext, "Jul"))
month = 7;
else if (!strcmp(mtext, "Aug"))
month = 8;
else if (!strcmp(mtext, "Sep"))
month = 9;
else if (!strcmp(mtext, "Oct"))
month = 10;
else if (!strcmp(mtext, "Nov"))
month = 11;
else if (!strcmp(mtext, "Dec"))
month = 12;
else
return NULL;
/* Make absolutely sure we know how many characters the date text is. */
/* If anything is fishy, fail! */
snprintf(verify, sizeof(verify), "%04d-%s-%02d %02d:%02d", year, mtext, day, hour, minute);
length = strlen(verify);
if (length != 17)
return NULL;
if (memcmp(verify, text, length))
return NULL;
*time = Astronomy_MakeTime(year, month, day, hour, minute, 0.0);
return text + length; /* return remaining portion of string */
}
/*-----------------------------------------------------------------------------------------------------------*/
static int LunarApsis(const char *filename)
{
int error = 1;
FILE *infile = NULL;
int lnum, nscanned;
char line[100];
int kind, year, month, day, hour, minute;
double dist_km;
astro_time_t start_time, correct_time;
astro_apsis_t apsis;
double diff_minutes, diff_km;
double max_minutes = 0.0, max_km = 0.0;
infile = fopen(filename, "rt");
if (infile == NULL)
{
fprintf(stderr, "LunarApsis: Cannot open input file: %s\n", filename);
error = 1;
goto fail;
}
/*
0 2001-01-10T08:59Z 357132
1 2001-01-24T19:02Z 406565
*/
start_time = Astronomy_MakeTime(2001, 1, 1, 0, 0, 0.0);
lnum = 0;
while (fgets(line, sizeof(line), infile))
{
++lnum;
if (lnum == 1)
apsis = Astronomy_SearchLunarApsis(start_time);
else
apsis = Astronomy_NextLunarApsis(apsis);
if (apsis.status != ASTRO_SUCCESS)
{
fprintf(stderr, "LunarApsis(%s line %d): Failed to find apsis.\n", filename, lnum);
error = 1;
goto fail;
}
nscanned = sscanf(line, "%d %d-%d-%dT%d:%dZ %lf", &kind, &year, &month, &day, &hour, &minute, &dist_km);
if (nscanned != 7)
{
fprintf(stderr, "LunarApsis(%s line %d): invalid data format\n", filename, lnum);
error = 1;
goto fail;
}
correct_time = Astronomy_MakeTime(year, month, day, hour, minute, 0.0);
diff_minutes = (24.0 * 60.0) * fabs(apsis.time.ut - correct_time.ut);
diff_km = fabs(apsis.dist_km - dist_km);
if (diff_minutes > 35.0)
{
fprintf(stderr, "LunarApsis(%s line %d): Excessive time error: %lf minutes.\n", filename, lnum, diff_minutes);
error = 1;
goto fail;
}
if (diff_km > 25.0)
{
fprintf(stderr, "LunarApsis(%s line %d): Excessive distance error: %lf km.\n", filename, lnum, diff_km);
error = 1;
goto fail;
}
if (diff_minutes > max_minutes)
max_minutes = diff_minutes;
if (diff_km > max_km)
max_km = diff_km;
}
printf("LunarApsis: found %d events, max time error = %0.3lf minutes, max distance error = %0.3lf km.\n", lnum, max_minutes, max_km);
error = 0;
fail:
if (infile != NULL) fclose(infile);
return error;
}
/*-----------------------------------------------------------------------------------------------------------*/
static int MathCheck(astro_body_t body, double ut)
{
astro_observer_t observer = Astronomy_MakeObserver(29, -81, 10);
astro_time_t time = Astronomy_TimeFromDays(ut);
astro_equatorial_t j2000;
astro_equatorial_t ofdate;
astro_horizon_t hor;
int error = 1;
printf("time.ut = %0.16lf\n", time.ut);
printf("time.tt = %0.16lf\n", time.tt);
CHECK_EQU(j2000, Astronomy_Equator(body, &time, observer, EQUATOR_J2000, NO_ABERRATION));
printf("j2000 ra = %0.16lf\n", j2000.ra);
printf("j2000 dec = %0.16lf\n", j2000.dec);
CHECK_EQU(ofdate, Astronomy_Equator(body, &time, observer, EQUATOR_OF_DATE, ABERRATION));
printf("ofdate ra = %0.16lf\n", ofdate.ra);
printf("ofdate dec = %0.16lf\n", ofdate.dec);
hor = Astronomy_Horizon(&time, observer, ofdate.ra, ofdate.dec, REFRACTION_NONE);
printf("azimuth = %0.16lf\n", hor.azimuth);
printf("altitude = %0.16lf\n", hor.altitude);
error = 0;
fail:
return error;
}
static int Issue46(void)
{
/* https://github.com/cosinekitty/astronomy/issues/46 */
return MathCheck(BODY_SUN, -93692.7685882873047376);
}
static int Issue48(void)
{
/* https://github.com/cosinekitty/astronomy/issues/48 */
return MathCheck(BODY_VENUS, -39864.1907264927140204);
}
/*-----------------------------------------------------------------------------------------------------------*/