mirror of
https://github.com/cosinekitty/astronomy.git
synced 2025-12-23 23:58:15 -05:00
7777 lines
276 KiB
C
7777 lines
276 KiB
C
/*
|
|
ctest.c - Don Cross <cosinekitty.com>
|
|
|
|
C langauge unit test for Astronomy Engine project.
|
|
https://github.com/cosinekitty/astronomy
|
|
*/
|
|
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
#include <ctype.h>
|
|
#include "astronomy.h"
|
|
|
|
char *ReadLine(char *s, int n, FILE *f, const char *filename, int lnum)
|
|
{
|
|
char *ret = fgets(s, n, f);
|
|
if (ret != NULL)
|
|
{
|
|
/* If a line is longer than the buffer can hold, it can cause confusing problems. */
|
|
/* Detect that and fail immediately, for easier diagnosis. */
|
|
int i;
|
|
int found = 0;
|
|
for (i = 0; !found && s[i] != '\0'; ++i)
|
|
if (s[i] == '\n' || s[i] == '\r')
|
|
found = 1;
|
|
if (!found)
|
|
{
|
|
fprintf(stderr, "C %s(%s line %d): No EOLN character found. Is the line too long for buffer size %d?\n",
|
|
__func__, filename, lnum+1, n);
|
|
exit(1);
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
#define PI 3.14159265358979323846
|
|
#define JD_2000 2451545.0
|
|
|
|
#define SECONDS_PER_DAY (24 * 3600)
|
|
#define MINUTES_PER_DAY (24 * 60)
|
|
|
|
#define CHECK(x) do{if(0 != (error = (x))) goto fail;}while(0)
|
|
#define FAIL(...) do{printf(__VA_ARGS__); error = 1; goto fail;}while(0)
|
|
#define FAILRET(...) do{printf(__VA_ARGS__); return 1;}while(0)
|
|
|
|
#define FFAIL(...) do{printf("C %s: ", __func__); printf(__VA_ARGS__); error = 1; goto fail;}while(0)
|
|
#define FLNFAIL(...) do{printf("C %s(%s line %d): ", __func__, filename, lnum); printf(__VA_ARGS__); error = 1; goto fail;}while(0)
|
|
#define FPASS() do{printf("C %s: PASS\n", __func__); error = 0;}while(0)
|
|
#define FPASSA(...) do{printf("C %s: PASS - ", __func__); printf(__VA_ARGS__); error = 0;}while(0)
|
|
|
|
static int CheckInverse(const char *aname, const char *bname, astro_rotation_t arot, astro_rotation_t brot);
|
|
#define CHECK_INVERSE(a,b) CHECK(CheckInverse(#a, #b, a, b))
|
|
|
|
int Verbose = 0;
|
|
#define DEBUG(...) do{if(Verbose)printf(__VA_ARGS__);}while(0)
|
|
#define FDEBUG(...) do{if(Verbose){printf("C %s: ", __func__); printf(__VA_ARGS__);}}while(0)
|
|
|
|
static int CheckStatus(int lnum, const char *varname, astro_status_t status)
|
|
{
|
|
if (status != ASTRO_SUCCESS)
|
|
FAILRET("FAILURE at ctest.c[%d]: %s.status = %d\n", lnum, varname, status);
|
|
return 0;
|
|
}
|
|
|
|
static int CheckVector(int lnum, astro_vector_t v)
|
|
{
|
|
if (v.status != ASTRO_SUCCESS)
|
|
FAILRET("FAILURE at ctest.c[%d]: vector status = %d\n", lnum, v.status);
|
|
return 0;
|
|
}
|
|
|
|
static int CheckEquator(int lnum, astro_equatorial_t equ)
|
|
{
|
|
if (equ.status != ASTRO_SUCCESS)
|
|
FAILRET("FAILURE at ctest.c[%d]: equatorial status = %d\n", lnum, equ.status);
|
|
return 0;
|
|
}
|
|
|
|
#define CHECK_VECTOR(var,expr) CHECK(CheckVector(__LINE__, ((var) = (expr))))
|
|
#define CHECK_EQU(var,expr) CHECK(CheckEquator(__LINE__, ((var) = (expr))))
|
|
#define CHECK_STATUS(expr) CHECK(CheckStatus(__LINE__, #expr, (expr).status))
|
|
|
|
static double v(const char *filename, int lnum, double x)
|
|
{
|
|
if (!isfinite(x))
|
|
{
|
|
fprintf(stderr, "FATAL(%s line %d): non-numeric result.\n", filename, lnum);
|
|
exit(1);
|
|
}
|
|
return x;
|
|
}
|
|
|
|
#define V(x) v(__FILE__, __LINE__, (x))
|
|
#define ABS(x) fabs(V(x))
|
|
|
|
static double DegreeDiff(double a, double b)
|
|
{
|
|
double diff = ABS(a - b);
|
|
if (diff > 180.0 && diff < 360.0)
|
|
diff = 360.0 - diff; /* handle wraparound of range [0, 360).*/
|
|
return diff;
|
|
}
|
|
|
|
typedef struct
|
|
{
|
|
int lnum;
|
|
double a_value;
|
|
double b_value;
|
|
double factor;
|
|
double diff;
|
|
}
|
|
maxdiff_column_t;
|
|
|
|
#define NUM_V_COLUMNS 4
|
|
#define NUM_S_COLUMNS 7
|
|
#define NUM_J_COLUMNS 8
|
|
#define NUM_N_COLUMNS 2
|
|
#define NUM_M_COLUMNS 3
|
|
#define NUM_DIFF_COLUMNS (NUM_V_COLUMNS + NUM_S_COLUMNS + NUM_J_COLUMNS + NUM_N_COLUMNS + NUM_M_COLUMNS)
|
|
|
|
/*-------------------------------------------------------------------------------------------------*/
|
|
|
|
typedef struct
|
|
{
|
|
int size; /* number of array entries allocated */
|
|
int length; /* number of array entries that contain valid state vectors */
|
|
astro_state_vector_t *array;
|
|
}
|
|
state_vector_batch_t;
|
|
|
|
static state_vector_batch_t EmptyStateVectorBatch()
|
|
{
|
|
state_vector_batch_t batch;
|
|
batch.size = 0;
|
|
batch.length = 0;
|
|
batch.array = NULL;
|
|
return batch;
|
|
}
|
|
|
|
static void FreeStateVectorBatch(state_vector_batch_t *batch)
|
|
{
|
|
free(batch->array);
|
|
batch->array = NULL;
|
|
batch->size = 0;
|
|
batch->length = 0;
|
|
}
|
|
|
|
static int AppendStateVector(state_vector_batch_t *batch, astro_state_vector_t state)
|
|
{
|
|
int error;
|
|
|
|
if (state.status != ASTRO_SUCCESS)
|
|
FFAIL("attempt to append state with status = %d\n", state.status);
|
|
|
|
if (batch->array == NULL)
|
|
{
|
|
/* This is the first time appending a state vector. */
|
|
const int INIT_SIZE = 100;
|
|
batch->array = calloc((size_t)INIT_SIZE, sizeof(batch->array[0]));
|
|
if (batch->array == NULL)
|
|
FFAIL("failed initial memory allocation!\n");
|
|
batch->size = INIT_SIZE;
|
|
batch->length = 0;
|
|
}
|
|
else if (batch->length == batch->size)
|
|
{
|
|
/* The buffer is full. Allocate a larger one. */
|
|
int longer = 2 * batch->size;
|
|
astro_state_vector_t *bigger = calloc((size_t)longer, sizeof(batch->array[0]));
|
|
if (bigger == NULL)
|
|
FFAIL("failed to increase memory allocation!\n");
|
|
memcpy(bigger, batch->array, (size_t)(batch->size) * sizeof(batch->array[0]));
|
|
free(batch->array);
|
|
batch->array = bigger;
|
|
batch->size = longer;
|
|
}
|
|
batch->array[(batch->length)++] = state;
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
static int LoadStateVectors(state_vector_batch_t *batch, const char *filename);
|
|
|
|
/*-------------------------------------------------------------------------------------------------*/
|
|
|
|
|
|
static int Test_AstroTime(void);
|
|
static int AstroCheck(void);
|
|
static int Diff(double tolerance, const char *a_filename, const char *b_filename);
|
|
static int DiffLine(int lnum, const char *aline, const char *bline, maxdiff_column_t column[]);
|
|
static int SeasonsTest(void);
|
|
static int SeasonsIssue187(void);
|
|
static int MoonPhase(void);
|
|
static int MoonReverse(void);
|
|
static int MoonNodes(void);
|
|
static int MoonVector(void);
|
|
static int MoonEcliptic(void);
|
|
static int RiseSet(void);
|
|
static int RiseSetElevation(void);
|
|
static int RiseSetReverse(void);
|
|
static int LunarApsis(void);
|
|
static int EarthApsis(void);
|
|
static int PlanetApsis(void);
|
|
static int PlutoCheck(void);
|
|
static int ElongationTest(void);
|
|
static int MagnitudeTest(void);
|
|
static int MoonTest(void);
|
|
static int RotationTest(void);
|
|
static int TestMaxMag(astro_body_t body, const char *filename);
|
|
static const char *ParseJplHorizonsDateTime(const char *text, astro_time_t *time);
|
|
static int VectorDiff(astro_vector_t a, astro_vector_t b, double *diff);
|
|
static int RefractionTest(void);
|
|
static int ConstellationTest(void);
|
|
static int LunarEclipseIssue78(void);
|
|
static int LunarEclipseTest(void);
|
|
static int LunarFractionTest(void);
|
|
static int SolarFractionTest(void);
|
|
static int GlobalSolarEclipseTest(void);
|
|
static int GravitySimulatorTest(void);
|
|
static int PlotDeltaT(const char *outFileName);
|
|
static double AngleDiff(double alat, double alon, double blat, double blon);
|
|
static int LocalSolarEclipseTest(void);
|
|
static int LocalSolarEclipseTest1(void);
|
|
static int LocalSolarEclipseTest2(void);
|
|
static int Transit(void);
|
|
static int DistancePlot(astro_body_t body, double ut1, double ut2, const char *filename);
|
|
static int GeoidTest(void);
|
|
static int JupiterMoonsTest(void);
|
|
static int Issue103(void);
|
|
static int AberrationTest(void);
|
|
static int BaryStateTest(void);
|
|
static int HelioStateTest(void);
|
|
static int LagrangeTest(void);
|
|
static int LagrangeJplAnalysis(void);
|
|
static int TopoStateTest(void);
|
|
static int Twilight(void);
|
|
static int LibrationTest(void);
|
|
static int DE405_Check(void);
|
|
static int AxisTest(void);
|
|
static int SiderealTimeTest(void);
|
|
static int DatesIssue250(void);
|
|
static int StarRiseSetCulm(void);
|
|
static int MapPerformanceTest(void);
|
|
static int GeoMoonPerformance(void);
|
|
static int NutationPerformance(void);
|
|
static int EclipticTest(void);
|
|
static int HourAngleTest(void);
|
|
static int Atmosphere(void);
|
|
|
|
typedef int (* unit_test_func_t) (void);
|
|
|
|
typedef struct
|
|
{
|
|
const char *name;
|
|
unit_test_func_t func;
|
|
int exclude; /* nonzero to exclude the test from the "all" command. Used for "performance" tests. */
|
|
}
|
|
unit_test_t;
|
|
|
|
#define EXCLUDE_FROM_AUTOMATED_TESTS 1
|
|
|
|
static unit_test_t UnitTests[] =
|
|
{
|
|
{"aberration", AberrationTest},
|
|
{"atmosphere", Atmosphere},
|
|
{"axis", AxisTest},
|
|
{"barystate", BaryStateTest},
|
|
{"check", AstroCheck},
|
|
{"constellation", ConstellationTest},
|
|
{"dates250", DatesIssue250},
|
|
{"de405", DE405_Check},
|
|
{"earth_apsis", EarthApsis},
|
|
{"ecliptic", EclipticTest},
|
|
{"elongation", ElongationTest},
|
|
{"geoid", GeoidTest},
|
|
{"global_solar_eclipse", GlobalSolarEclipseTest},
|
|
{"gravsim", GravitySimulatorTest},
|
|
{"heliostate", HelioStateTest},
|
|
{"hour_angle", HourAngleTest},
|
|
{"issue_103", Issue103},
|
|
{"jupiter_moons", JupiterMoonsTest},
|
|
{"lagrange", LagrangeTest},
|
|
{"lagrange_jpl", LagrangeJplAnalysis},
|
|
{"libration", LibrationTest},
|
|
{"local_solar_eclipse", LocalSolarEclipseTest},
|
|
{"lunar_eclipse", LunarEclipseTest},
|
|
{"lunar_eclipse_78", LunarEclipseIssue78},
|
|
{"lunar_fraction", LunarFractionTest},
|
|
{"magnitude", MagnitudeTest},
|
|
{"map", MapPerformanceTest, EXCLUDE_FROM_AUTOMATED_TESTS},
|
|
{"moon", MoonTest},
|
|
{"moon_apsis", LunarApsis},
|
|
{"moon_ecm", MoonEcliptic},
|
|
{"moon_nodes", MoonNodes},
|
|
{"moon_performance", GeoMoonPerformance, EXCLUDE_FROM_AUTOMATED_TESTS},
|
|
{"moon_phase", MoonPhase},
|
|
{"moon_reverse", MoonReverse},
|
|
{"moon_vector", MoonVector},
|
|
{"nutation", NutationPerformance, EXCLUDE_FROM_AUTOMATED_TESTS},
|
|
{"planet_apsis", PlanetApsis},
|
|
{"pluto", PlutoCheck},
|
|
{"refraction", RefractionTest},
|
|
{"riseset", RiseSet},
|
|
{"riseset_elevation", RiseSetElevation},
|
|
{"riseset_reverse", RiseSetReverse},
|
|
{"rotation", RotationTest},
|
|
{"seasons", SeasonsTest},
|
|
{"seasons187", SeasonsIssue187},
|
|
{"sidereal", SiderealTimeTest},
|
|
{"solar_fraction", SolarFractionTest},
|
|
{"star_risesetculm", StarRiseSetCulm},
|
|
{"time", Test_AstroTime},
|
|
{"topostate", TopoStateTest},
|
|
{"transit", Transit},
|
|
{"twilight", Twilight}
|
|
};
|
|
|
|
#define NUM_UNIT_TESTS (sizeof(UnitTests) / sizeof(UnitTests[0]))
|
|
|
|
int main(int argc, const char *argv[])
|
|
{
|
|
int error = 1;
|
|
int i;
|
|
|
|
if (argc > 1 && !strcmp(argv[1], "-v"))
|
|
{
|
|
++argv;
|
|
--argc;
|
|
Verbose = 1;
|
|
}
|
|
|
|
if (argc == 1)
|
|
{
|
|
/* List available commands. */
|
|
printf("Run `ctest all` to run all tests.\n");
|
|
printf("Or run `ctest <name>` for a specific named test:\n");
|
|
for (i = 0; i < NUM_UNIT_TESTS; ++i)
|
|
printf(" %s\n", UnitTests[i].name);
|
|
return 1;
|
|
}
|
|
|
|
if (argc > 1)
|
|
{
|
|
const char *verb = argv[1];
|
|
if (argc == 2)
|
|
{
|
|
if (!strcmp(verb, "all"))
|
|
{
|
|
for (i=0; i < NUM_UNIT_TESTS; ++i)
|
|
if (!UnitTests[i].exclude && UnitTests[i].func())
|
|
FAIL("ctest: Unit test failed: %s\n", UnitTests[i].name);
|
|
goto success;
|
|
}
|
|
|
|
for (i=0; i < NUM_UNIT_TESTS; ++i)
|
|
{
|
|
if (!strcmp(verb, UnitTests[i].name))
|
|
{
|
|
CHECK(UnitTests[i].func());
|
|
goto success;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (argc == 3)
|
|
{
|
|
const char *filename = argv[2];
|
|
if (!strcmp(verb, "dtplot"))
|
|
{
|
|
CHECK(PlotDeltaT(filename));
|
|
goto success;
|
|
}
|
|
}
|
|
|
|
if (argc == 5)
|
|
{
|
|
if (!strcmp(verb, "diff"))
|
|
{
|
|
double tolerance = atof(argv[2]);
|
|
const char *c_filename = argv[3];
|
|
const char *js_filename = argv[4];
|
|
CHECK(Diff(tolerance, c_filename, js_filename));
|
|
goto success;
|
|
}
|
|
}
|
|
|
|
if (argc == 6)
|
|
{
|
|
if (!strcmp(verb, "distplot"))
|
|
{
|
|
/* ctest distplot name tt1 tt2 */
|
|
astro_body_t body;
|
|
double ut1, ut2;
|
|
|
|
body = Astronomy_BodyCode(argv[2]);
|
|
if (body == BODY_INVALID)
|
|
FAIL("Invalid body name '%s'\n", argv[2]);
|
|
|
|
if (1 != sscanf(argv[3], "%lf", &ut1))
|
|
FAIL("Invalid tt1 value '%s'\n", argv[3]);
|
|
|
|
if (1 != sscanf(argv[4], "%lf", &ut2))
|
|
FAIL("Invalid tt2 value '%s'\n", argv[4]);
|
|
|
|
CHECK(DistancePlot(body, ut1, ut2, argv[5]));
|
|
goto success;
|
|
}
|
|
}
|
|
}
|
|
|
|
FAIL("ctest: Invalid command line arguments.\n");
|
|
|
|
success:
|
|
error = 0;
|
|
|
|
fail:
|
|
Astronomy_Reset(); /* Free memory so valgrind doesn't see any leaks. */
|
|
fflush(stdout);
|
|
fflush(stderr);
|
|
return error;
|
|
}
|
|
|
|
|
|
static int CheckTimeFormat(
|
|
astro_time_t time,
|
|
astro_time_format_t format,
|
|
astro_status_t expected_status,
|
|
const char *expected_text)
|
|
{
|
|
int error = 1;
|
|
astro_status_t status;
|
|
char text[TIME_TEXT_BYTES];
|
|
|
|
status = Astronomy_FormatTime(time, format, text, sizeof(text));
|
|
if (status != expected_status)
|
|
FAIL("C %s(%s): expected status %d, got %d\n", __func__, expected_text, expected_status, status);
|
|
|
|
if (strcmp(text, expected_text))
|
|
FAIL("C %s(%s): computed wrong text '%s'\n", __func__, expected_text, text);
|
|
|
|
DEBUG("C %s(%s): PASS\n", __func__, expected_text);
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
|
|
static int CalendarCase(int year, int month, int day, int hour, int minute, double second)
|
|
{
|
|
int error;
|
|
astro_time_t time;
|
|
astro_utc_t utc;
|
|
double diff;
|
|
|
|
time = Astronomy_MakeTime(year, month, day, hour, minute, second);
|
|
|
|
utc = Astronomy_UtcFromTime(time);
|
|
if (utc.year != year || utc.month != month || utc.day != day || utc.hour != hour || utc.minute != minute)
|
|
{
|
|
FFAIL("UtcFromTime FAILURE - Expected %04d-%02d-%02dT%02d:%02dZ, found %04d-%02d-%02dT%02d:%02dZ, ut = %0.6lf\n",
|
|
year, month, day, hour, minute,
|
|
utc.year, utc.month, utc.day, utc.hour, utc.minute,
|
|
time.ut);
|
|
}
|
|
|
|
diff = utc.second - second;
|
|
if (ABS(diff) > 0.001)
|
|
FFAIL("excessive UTC second error %lg for %06d-%02d-%02d\n", diff, year, month, day);
|
|
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
|
|
static int Test_AstroTime(void)
|
|
{
|
|
int error = 1;
|
|
astro_time_t time, time2;
|
|
const double expected_ut = 6910.270978506945;
|
|
const double expected_tt = 6910.271800214368;
|
|
double diff;
|
|
int year, count;
|
|
|
|
time = Astronomy_MakeTime(2018, 12, 2, 18, 30, 12.543);
|
|
FDEBUG("ut=%0.12lf, tt=%0.12lf\n", time.ut, time.tt);
|
|
|
|
diff = time.ut - expected_ut;
|
|
if (ABS(diff) > 1.0e-12)
|
|
FFAIL("excessive UT error %lg\n", diff);
|
|
|
|
diff = time.tt - expected_tt;
|
|
if (ABS(diff) > 1.0e-12)
|
|
FFAIL("excessive TT error %lg\n", diff);
|
|
|
|
for (year = -999999; year <= +999999; ++year)
|
|
{
|
|
/* Check just before and after each potential leap day. */
|
|
CHECK(CalendarCase(year, 2, 28, 18, 30, 12.543));
|
|
CHECK(CalendarCase(year, 3, 1, 18, 30, 12.543));
|
|
}
|
|
|
|
time = Astronomy_MakeTime(2020, 12, 31, 23, 59, 59.4994);
|
|
CHECK(CheckTimeFormat(time, TIME_FORMAT_MILLI, ASTRO_SUCCESS, "2020-12-31T23:59:59.499Z"));
|
|
CHECK(CheckTimeFormat(time, TIME_FORMAT_SECOND, ASTRO_SUCCESS, "2020-12-31T23:59:59Z"));
|
|
CHECK(CheckTimeFormat(time, TIME_FORMAT_MINUTE, ASTRO_SUCCESS, "2021-01-01T00:00Z"));
|
|
CHECK(CheckTimeFormat(time, TIME_FORMAT_DAY, ASTRO_SUCCESS, "2020-12-31"));
|
|
|
|
time = Astronomy_MakeTime(2020, 12, 31, 23, 59, 59.500);
|
|
CHECK(CheckTimeFormat(time, TIME_FORMAT_MILLI, ASTRO_SUCCESS, "2020-12-31T23:59:59.500Z"));
|
|
CHECK(CheckTimeFormat(time, TIME_FORMAT_SECOND, ASTRO_SUCCESS, "2021-01-01T00:00:00Z"));
|
|
CHECK(CheckTimeFormat(time, TIME_FORMAT_MINUTE, ASTRO_SUCCESS, "2021-01-01T00:00Z"));
|
|
CHECK(CheckTimeFormat(time, TIME_FORMAT_DAY, ASTRO_SUCCESS, "2020-12-31"));
|
|
|
|
/* The verification texts are intentionally off by a millisecond or two in the following */
|
|
/* two test cases, because the extreme year values expose floating point precision limitions. */
|
|
time = Astronomy_MakeTime(-999999, 1, 14, 22, 55, 12.0);
|
|
CHECK(CheckTimeFormat(time, TIME_FORMAT_MILLI, ASTRO_SUCCESS, "-999999-01-14T22:55:11.998Z"));
|
|
|
|
time = Astronomy_MakeTime(+999999, 11, 30, 8, 15, 45.0);
|
|
CHECK(CheckTimeFormat(time, TIME_FORMAT_MILLI, ASTRO_SUCCESS, "+999999-11-30T08:15:44.999Z"));
|
|
|
|
/* Verify that the realtime clock supports fine-grained time resolution. */
|
|
time = Astronomy_CurrentTime();
|
|
count = 0;
|
|
do
|
|
{
|
|
/* Keep looping until the time changes. */
|
|
time2 = Astronomy_CurrentTime();
|
|
|
|
/* Prevent infinite looping, in case something goes bonkers. */
|
|
if (++count > 1000)
|
|
FFAIL("TAKING TOO LONG for Astronomy_CurrentTime() to change.\n");
|
|
} while (time2.ut == time.ut);
|
|
diff = V((time2.ut - time.ut)*SECONDS_PER_DAY);
|
|
if (diff <= 0.0)
|
|
FFAIL("IMPOSSIBLE realtime increment = %0.4lg seconds after %d iterations.\n", diff, count);
|
|
if (diff > 0.001)
|
|
FFAIL("EXCESSIVE realtime increment = %0.4lg seconds after %d iterations.\n", diff, count);
|
|
|
|
FPASSA("realtime increment = %0.4lg seconds after %d iterations.\n", diff, count);
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
static int WriteJupiterMoon(FILE *outfile, int moon_index, astro_state_vector_t moon)
|
|
{
|
|
int error;
|
|
|
|
CHECK_STATUS(moon);
|
|
|
|
fprintf(outfile, "j %d %0.18le %0.18le %0.18le %0.18le %0.18le %0.18le %0.18le %0.18le\n",
|
|
moon_index,
|
|
moon.t.tt, moon.t.ut,
|
|
moon.x, moon.y, moon.z,
|
|
moon.vx, moon.vy, moon.vz
|
|
);
|
|
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
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_jupiter_moons_t jm;
|
|
astro_spherical_t sphere;
|
|
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,
|
|
BODY_SSB, BODY_EMB
|
|
};
|
|
static int nbodies = sizeof(bodylist) / sizeof(bodylist[0]);
|
|
|
|
outfile = fopen(filename, "wt");
|
|
if (outfile == NULL)
|
|
FFAIL("Cannot open output file: %s\n", filename);
|
|
|
|
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.18le %0.18le %0.18le %0.18le\n", Astronomy_BodyName(body), pos.t.tt, pos.x, pos.y, pos.z);
|
|
|
|
if (body != BODY_EARTH && body != BODY_EMB && body != BODY_SSB)
|
|
{
|
|
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.18le %0.18le %0.18le %0.18le %0.18le %0.18le %0.18le\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.18le %0.18le %0.18le %0.18le\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.18le %0.18le %0.18le %0.18le %0.18le %0.18le %0.18le\n",
|
|
time.tt, time.ut, j2000.ra, j2000.dec, j2000.dist, hor.azimuth, hor.altitude);
|
|
|
|
/* Calculate Jupiter's moons for diff purposes only. */
|
|
jm = Astronomy_JupiterMoons(time);
|
|
WriteJupiterMoon(outfile, 0, jm.io);
|
|
WriteJupiterMoon(outfile, 1, jm.europa);
|
|
WriteJupiterMoon(outfile, 2, jm.ganymede);
|
|
WriteJupiterMoon(outfile, 3, jm.callisto);
|
|
|
|
/* Nutation calculations. */
|
|
fprintf(outfile, "n %0.18le %0.18le\n", time.psi, time.eps);
|
|
|
|
/* Moon's ecliptic coordinates in true equinox of date. */
|
|
sphere = Astronomy_EclipticGeoMoon(time);
|
|
CHECK_STATUS(sphere);
|
|
fprintf(outfile, "m %0.18le %0.18le %0.18le\n", sphere.lat, sphere.lon, sphere.dist);
|
|
|
|
time = Astronomy_AddDays(time, 10.0 + PI/100.0);
|
|
}
|
|
|
|
fail:
|
|
if (outfile != NULL)
|
|
fclose(outfile);
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static double OrbitRange(const char *name)
|
|
{
|
|
/* Return the minimum distance from a planet to the Sun, in AU. */
|
|
if (!strcmp(name, "Mercury")) return 0.307;
|
|
if (!strcmp(name, "Venus")) return 0.718;
|
|
if (!strcmp(name, "Earth")) return 0.983;
|
|
if (!strcmp(name, "EMB")) return 0.983;
|
|
if (!strcmp(name, "Mars")) return 1.382;
|
|
if (!strcmp(name, "Jupiter")) return 4.951;
|
|
if (!strcmp(name, "Saturn")) return 9.014;
|
|
if (!strcmp(name, "Uranus")) return 18.31;
|
|
if (!strcmp(name, "Neptune")) return 29.76;
|
|
if (!strcmp(name, "Pluto")) return 29.73;
|
|
|
|
/* GM is geocentric moon */
|
|
if (!strcmp(name, "GM")) return 0.00243;
|
|
|
|
/* For Solar System Barycenter, we use typical distance. */
|
|
if (!strcmp(name, "SSB")) return 0.005;
|
|
|
|
/* The Sun vector is always (0, 0, 0), so range doesn't matter. */
|
|
if (!strcmp(name, "Sun")) return 1.0;
|
|
|
|
/* Jovicentric distances for Jupiter's moons. */
|
|
if (!strcmp(name, "jm0")) return 0.00282; /* Io */
|
|
if (!strcmp(name, "jm1")) return 0.00448; /* Europa */
|
|
if (!strcmp(name, "jm2")) return 0.00716; /* Ganymede */
|
|
if (!strcmp(name, "jm3")) return 0.01259; /* Callisto */
|
|
|
|
fprintf(stderr, "FATAL(%s): unknown body name '%s'\n", __func__, name);
|
|
exit(1);
|
|
}
|
|
|
|
static double OrbitSpeed(const char *name)
|
|
{
|
|
/* Return the orbital speed (AU/day) of Jupiter's moons. */
|
|
if (!strcmp(name, "jm0")) return 0.0100; /* Io */
|
|
if (!strcmp(name, "jm1")) return 0.0079; /* Europa */
|
|
if (!strcmp(name, "jm2")) return 0.0063; /* Ganymede */
|
|
if (!strcmp(name, "jm3")) return 0.0047; /* Callisto */
|
|
|
|
fprintf(stderr, "FATAL(%s): unknown body name '%s'\n", __func__, name);
|
|
exit(1);
|
|
}
|
|
|
|
static double TopoRange(const char *name)
|
|
{
|
|
/* Return the minimum distance from a planet to the Earth, in AU. */
|
|
if (!strcmp(name, "Mercury")) return 1.0 - 0.307;
|
|
if (!strcmp(name, "Venus")) return 1.0 - 0.718;
|
|
if (!strcmp(name, "Earth")) return 1.0; /* we don't calculate topocentric Earth. */
|
|
if (!strcmp(name, "EMB")) return 1.0; /* we don't calculate topocentric EMB */
|
|
if (!strcmp(name, "Mars")) return 1.382 - 1.0;
|
|
if (!strcmp(name, "Jupiter")) return 4.951 - 1.0;
|
|
if (!strcmp(name, "Saturn")) return 9.014 - 1.0;
|
|
if (!strcmp(name, "Uranus")) return 18.31 - 1.0;
|
|
if (!strcmp(name, "Neptune")) return 29.76 - 1.0;
|
|
if (!strcmp(name, "Pluto")) return 29.73 - 1.0;
|
|
|
|
/* GM is geocentric moon, so use minimum distance to Earth. */
|
|
if (!strcmp(name, "GM")) return 0.00243;
|
|
|
|
/* For Solar System Barycenter, we use typical distance. */
|
|
if (!strcmp(name, "SSB")) return 1.0;
|
|
|
|
/* The Sun vector is always (0, 0, 0), so range doesn't matter. */
|
|
if (!strcmp(name, "Sun")) return 1.0;
|
|
|
|
fprintf(stderr, "FATAL(%s): unknown body name '%s'\n", __func__, name);
|
|
exit(1);
|
|
}
|
|
|
|
|
|
typedef struct
|
|
{
|
|
const char *name; /* the name of the column/field */
|
|
int cos_index; /* moderate longitude-like differences by the cosine of the indicated latitude-like field (or -1 if N/A) */
|
|
int wrap; /* the value at which an angular measure can wrap around, or 0 if N/A */
|
|
double range; /* moderate differences by dividing by this range of possible values (or special codes for 0, -1, -2) */
|
|
}
|
|
maxdiff_settings_t;
|
|
|
|
static const maxdiff_settings_t DiffSettings[NUM_DIFF_COLUMNS] =
|
|
{
|
|
/* 'v' lines */
|
|
{ "helio_tt", -1, 0, 1.0 }, /* 0 */
|
|
{ "helio_x", -1, 0, 0.0 }, /* 1 */
|
|
{ "helio_y", -1, 0, 0.0 }, /* 2 */
|
|
{ "helio_z", -1, 0, 0.0 }, /* 3 */
|
|
|
|
/* 's' lines */
|
|
{ "sky_tt", -1, 0, 1.0 }, /* 4 */
|
|
{ "sky_ut", -1, 0, 1.0 }, /* 5 */
|
|
{ "sky_j2000_ra", 7, 24, 24.0 }, /* 6 */
|
|
{ "sky_j2000_dec", -1, 0, 180.0 }, /* 7 */
|
|
{ "sky_j2000_dist", -1, 0, -1.0 }, /* 8 */
|
|
{ "sky_hor_az", 10, 360, 360.0 }, /* 9 */
|
|
{ "sky_hor_alt", -1, 0, 180.0 }, /* 10 */
|
|
|
|
/* 'j' lines */
|
|
{ "jm_tt", -1, 0, 1.0 }, /* 11 */
|
|
{ "jm_ut", -1, 0, 1.0 }, /* 12 */
|
|
{ "jm_x", -1, 0, 0.0 }, /* 13 */
|
|
{ "jm_y", -1, 0, 0.0 }, /* 14 */
|
|
{ "jm_z", -1, 0, 0.0 }, /* 15 */
|
|
{ "jm_vx", -1, 0, -2.0 }, /* 16 */
|
|
{ "jm_vy", -1, 0, -2.0 }, /* 17 */
|
|
{ "jm_vz", -1, 0, -2.0 }, /* 18 */
|
|
|
|
/* 'n' lines: nutation angles */
|
|
{ "psi", -1, 0, 10.0 }, /* 19 */
|
|
{ "eps", -1, 0, 10.0 }, /* 20 */
|
|
|
|
/* 'm' lines: EclipticGeoMoon calculations. */
|
|
{ "moon_lat", -1, 0, 180.0 }, /* 21 */
|
|
{ "moon_lon", 21, 360, 360.0 }, /* 22 */
|
|
{ "moon_dist", -1, 0, 0.003 } /* 23 */
|
|
};
|
|
|
|
static int Diff(double tolerance, const char *a_filename, const char *b_filename)
|
|
{
|
|
int error = 1;
|
|
int lnum;
|
|
int i;
|
|
FILE *afile = NULL;
|
|
FILE *bfile = NULL;
|
|
char aline[300];
|
|
char bline[300];
|
|
const char *aread;
|
|
const char *bread;
|
|
maxdiff_column_t columns[NUM_DIFF_COLUMNS];
|
|
double score = 0.0;
|
|
|
|
memset(columns, 0, sizeof(columns));
|
|
|
|
afile = fopen(a_filename, "rt");
|
|
if (afile == NULL)
|
|
FFAIL("Cannot open input file: %s\n", a_filename);
|
|
|
|
bfile = fopen(b_filename, "rt");
|
|
if (bfile == NULL)
|
|
FFAIL("Cannot open input file: %s\n", b_filename);
|
|
|
|
lnum = 0;
|
|
for(;;)
|
|
{
|
|
aread = ReadLine(aline, sizeof(aline), afile, a_filename, lnum);
|
|
bread = ReadLine(bline, sizeof(bline), bfile, b_filename, lnum);
|
|
if (aread==NULL && bread==NULL)
|
|
break; /* normal end of both files */
|
|
|
|
if (aread==NULL || bread==NULL)
|
|
FFAIL("Files do not have same number of lines: %s and %s\n", a_filename, b_filename);
|
|
|
|
++lnum;
|
|
CHECK(DiffLine(lnum, aline, bline, columns));
|
|
}
|
|
|
|
error = 0;
|
|
|
|
printf("First file: %s\n", a_filename);
|
|
printf("Second file: %s\n", b_filename);
|
|
printf("Tolerance = %0.3le\n\n", tolerance);
|
|
printf(" %10s %23s %23s %10s %10s %s\n", "lnum", "a_value", "b_value", "factor", "diff", "name");
|
|
for (i = 0; i < NUM_DIFF_COLUMNS; ++i)
|
|
{
|
|
if (columns[i].lnum > 0)
|
|
{
|
|
const char *result = "OK";
|
|
if (columns[i].diff > tolerance)
|
|
{
|
|
result = "FAIL";
|
|
error = 1;
|
|
}
|
|
|
|
if (columns[i].diff > score)
|
|
score = columns[i].diff;
|
|
|
|
printf("%4s %10d %23.16le %23.16le %10.5lf %10.3le %s\n",
|
|
result,
|
|
columns[i].lnum,
|
|
columns[i].a_value,
|
|
columns[i].b_value,
|
|
columns[i].factor,
|
|
columns[i].diff,
|
|
DiffSettings[i].name);
|
|
}
|
|
}
|
|
|
|
printf("\nScore = %0.3le\n", score);
|
|
if (error)
|
|
printf("ctest(Diff): EXCEEDED ERROR TOLERANCE.\n");
|
|
printf("----------------------------------------------------------------------------------------------------\n\n");
|
|
|
|
fail:
|
|
if (afile != NULL) fclose(afile);
|
|
if (bfile != NULL) fclose(bfile);
|
|
return error;
|
|
}
|
|
|
|
static int DiffLine(int lnum, const char *aline, const char *bline, maxdiff_column_t columns[])
|
|
{
|
|
int error = 1;
|
|
char abody[10];
|
|
char bbody[10];
|
|
double adata[8];
|
|
double bdata[8];
|
|
double diff;
|
|
int i, na, nb, nrequired = -1;
|
|
int colbase = -1;
|
|
int mindex_a, mindex_b; /* index of Jupiter's moon: 0..3 */
|
|
maxdiff_column_t *col;
|
|
|
|
/* be paranoid: make sure we can't possibly have a fake match. */
|
|
memset(adata, 0xdc, sizeof(adata));
|
|
memset(bdata, 0xce, sizeof(bdata));
|
|
|
|
/* Make sure the two data records are the same type. */
|
|
if (aline[0] != bline[0])
|
|
FFAIL("Line %d mismatch record type: '%c' vs '%c'.\n", lnum, aline[0], bline[0]);
|
|
|
|
abody[0] = bbody[0] = '\0'; /* default to no body name */
|
|
|
|
switch (aline[0])
|
|
{
|
|
case 'o': /* observer */
|
|
na = sscanf(aline, "o %lf %lf %lf", &adata[0], &adata[1], &adata[2]);
|
|
if (na != 3)
|
|
FFAIL("Bad observer on line %d of first file\n", lnum);
|
|
nb = sscanf(bline, "o %lf %lf %lf", &bdata[0], &bdata[1], &bdata[2]);
|
|
if (nb != 3)
|
|
FFAIL("Bad observer on line %d of second file\n", lnum);
|
|
if (adata[0] != bdata[0] || adata[1] != bdata[1] || adata[2] != bdata[2])
|
|
FFAIL("Observers are not identical on line %d\n", lnum);
|
|
return 0;
|
|
|
|
case 'v': /* heliocentric vector: tt x y z */
|
|
colbase = 0;
|
|
na = sscanf(aline, "v %9[A-Za-z] %lf %lf %lf %lf", abody, &adata[0], &adata[1], &adata[2], &adata[3]);
|
|
nb = sscanf(bline, "v %9[A-Za-z] %lf %lf %lf %lf", bbody, &bdata[0], &bdata[1], &bdata[2], &bdata[3]);
|
|
nrequired = 5;
|
|
break;
|
|
|
|
case 's': /* sky coords: ecliptic and horizontal */
|
|
/* Astronomy_BodyName(body), [0] time.tt, [1] time.ut, [2] j2000.ra, [3] j2000.dec, [4] j2000.dist, [5] hor.azimuth, [6] hor.altitude */
|
|
colbase = NUM_V_COLUMNS;
|
|
na = sscanf(aline, "s %9[A-Za-z] %lf %lf %lf %lf %lf %lf %lf", abody, &adata[0], &adata[1], &adata[2], &adata[3], &adata[4], &adata[5], &adata[6]);
|
|
nb = sscanf(bline, "s %9[A-Za-z] %lf %lf %lf %lf %lf %lf %lf", bbody, &bdata[0], &bdata[1], &bdata[2], &bdata[3], &bdata[4], &bdata[5], &bdata[6]);
|
|
nrequired = 8;
|
|
break;
|
|
|
|
case 'j': /* Jupiter's moons: j moon[0..3] tt ut x y z vx vy vz */
|
|
colbase = NUM_V_COLUMNS + NUM_S_COLUMNS;
|
|
na = sscanf(aline, "j %d %lf %lf %lf %lf %lf %lf %lf %lf", &mindex_a, &adata[0], &adata[1], &adata[2], &adata[3], &adata[4], &adata[5], &adata[6], &adata[7]);
|
|
nb = sscanf(bline, "j %d %lf %lf %lf %lf %lf %lf %lf %lf", &mindex_b, &bdata[0], &bdata[1], &bdata[2], &bdata[3], &bdata[4], &bdata[5], &bdata[6], &bdata[7]);
|
|
nrequired = 9;
|
|
if (mindex_a < 0 || mindex_a >= 4 || mindex_a != mindex_b)
|
|
FFAIL("Bad Jupiter moon index in line %d: mindex_a=%d, mindex_b=%d\n", lnum, mindex_a, mindex_b);
|
|
snprintf(abody, sizeof(abody), "jm%d", mindex_a);
|
|
snprintf(bbody, sizeof(bbody), "jm%d", mindex_b);
|
|
break;
|
|
|
|
case 'n': /* nutation angles */
|
|
colbase = NUM_V_COLUMNS + NUM_S_COLUMNS + NUM_J_COLUMNS;
|
|
na = sscanf(aline, "n %lf %lf", &adata[0], &adata[1]);
|
|
nb = sscanf(bline, "n %lf %lf", &bdata[0], &bdata[1]);
|
|
nrequired = 2;
|
|
break;
|
|
|
|
case 'm': /* EclipticGeoMoon */
|
|
colbase = NUM_V_COLUMNS + NUM_S_COLUMNS + NUM_J_COLUMNS + NUM_N_COLUMNS;
|
|
na = sscanf(aline, "m %lf %lf %lf", &adata[0], &adata[1], &adata[2]);
|
|
nb = sscanf(bline, "m %lf %lf %lf", &bdata[0], &bdata[1], &bdata[2]);
|
|
nrequired = 3;
|
|
break;
|
|
|
|
default:
|
|
FFAIL("Line %d type '%c' is not a valid record type.\n", lnum, aline[0]);
|
|
}
|
|
|
|
if (na != nb)
|
|
FFAIL("Line %d mismatch data counts: %d vs %d\n", lnum, na, nb);
|
|
|
|
if (na != nrequired)
|
|
FFAIL("Line %d incorrect number of scanned arguments: %d\n", lnum, na);
|
|
|
|
if (strcmp(abody, bbody))
|
|
FFAIL("Line %d body mismatch: '%s' vs '%s'\n.", lnum, abody, bbody);
|
|
|
|
if (abody[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)
|
|
{
|
|
double factor, denom;
|
|
int ci, w;
|
|
int k = colbase + i;
|
|
|
|
/* Life is too short to debug memory corruption errors. */
|
|
if (k < 0 || k >= NUM_DIFF_COLUMNS)
|
|
FFAIL("Internal error on line %d: k=%d\n", lnum, k);
|
|
|
|
ci = DiffSettings[k].cos_index;
|
|
w = DiffSettings[k].wrap;
|
|
|
|
denom = NAN;
|
|
if (DiffSettings[k].range == -2.0)
|
|
denom = OrbitSpeed(abody);
|
|
else if (DiffSettings[k].range == -1.0)
|
|
denom = TopoRange(abody);
|
|
else if (DiffSettings[k].range == 0.0)
|
|
denom = OrbitRange(abody);
|
|
else if (DiffSettings[k].range > 0.0)
|
|
denom = DiffSettings[k].range;
|
|
else
|
|
FFAIL("Invalid range value: %lf\n", DiffSettings[k].range);
|
|
|
|
if (!isfinite(denom) || denom <= 0.0)
|
|
FFAIL("Invalid denominator value: %lf\n", denom);
|
|
|
|
factor = V(1.0 / denom);
|
|
|
|
diff = ABS(adata[i] - bdata[i]);
|
|
if ((w > 0) && (diff > w / 2))
|
|
{
|
|
/* Tolerate differences in angular values that wrap around at a periodic limit. */
|
|
diff = ABS(w - diff);
|
|
}
|
|
|
|
if (ci >= 0)
|
|
{
|
|
/*
|
|
Longitude-like angles (right ascension and azimuth) become less significant as their
|
|
counterpart latitude-like angle gets closer to its poles.
|
|
For example, if horizontal altitude is close to 90 degrees (near the zenith),
|
|
then an azimuth error is not very important.
|
|
So we multiply errors for these types of numbers by the cosine of their counterpart.
|
|
Make sure we calculate identical values regardless of the order of the filenames.
|
|
Therefore, use the arithmetic mean of the two latitude angles.
|
|
*/
|
|
factor *= ABS(cos(DEG2RAD * ((adata[ci] + bdata[ci]) / 2.0)));
|
|
}
|
|
|
|
diff *= factor;
|
|
|
|
/* Remember the worst difference for each different type of calculation. */
|
|
col = &columns[k];
|
|
if (diff > col->diff)
|
|
{
|
|
col->diff = diff;
|
|
col->lnum = lnum;
|
|
col->a_value = adata[i];
|
|
col->b_value = bdata[i];
|
|
col->factor = factor;
|
|
}
|
|
}
|
|
error = 0;
|
|
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int SeasonsTest(void)
|
|
{
|
|
const char *filename = "seasons/seasons.txt";
|
|
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)
|
|
FFAIL("Cannot open input file: %s\n", filename);
|
|
|
|
lnum = 0;
|
|
while (ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++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)
|
|
FFAIL("%s line %d : scanned %d, expected 6\n", filename, lnum, nscanned);
|
|
|
|
if (year != current_year)
|
|
{
|
|
current_year = year;
|
|
seasons = Astronomy_Seasons(year);
|
|
if (seasons.status != ASTRO_SUCCESS)
|
|
FFAIL("Astronomy_Seasons(%d) returned %d\n", year, seasons.status);
|
|
}
|
|
|
|
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:
|
|
FFAIL("Invalid equinox date in test data: %s line %d\n", filename, lnum);
|
|
}
|
|
}
|
|
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:
|
|
FFAIL("Invalid solstice date in test data: %s line %d\n", filename, lnum);
|
|
}
|
|
}
|
|
else if (!strcmp(name, "Aphelion"))
|
|
{
|
|
/* not yet calculated */
|
|
continue;
|
|
}
|
|
else if (!strcmp(name, "Perihelion"))
|
|
{
|
|
/* not yet calculated */
|
|
continue;
|
|
}
|
|
else
|
|
FFAIL("%s line %d: unknown event type '%s'\n", filename, lnum, name);
|
|
|
|
/* Verify that the calculated time matches the correct time for this event. */
|
|
diff_minutes = (24.0 * 60.0) * ABS(calc_time.tt - correct_time.tt);
|
|
if (diff_minutes > max_minutes)
|
|
max_minutes = diff_minutes;
|
|
|
|
if (diff_minutes > 2.37)
|
|
FFAIL("%s line %d: excessive error (%s): %lf minutes.\n", filename, lnum, name, diff_minutes);
|
|
}
|
|
|
|
printf("C %s: verified %d lines from file %s : max error minutes = %0.3lf\n", __func__, lnum, filename, max_minutes);
|
|
printf("C %s: Event counts: mar=%d, jun=%d, sep=%d, dec=%d\n", __func__, mar_count, jun_count, sep_count, dec_count);
|
|
error = 0;
|
|
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
static int SeasonsIssue187(void)
|
|
{
|
|
int error, year;
|
|
astro_seasons_t seasons;
|
|
char text[TIME_TEXT_BYTES];
|
|
|
|
// This is a regression test for:
|
|
// https://github.com/cosinekitty/astronomy/issues/187
|
|
// For years far from the present, the seasons search was sometimes failing.
|
|
|
|
for (year = -2000; year <= +9999; ++year)
|
|
{
|
|
seasons = Astronomy_Seasons(year);
|
|
if (seasons.status != ASTRO_SUCCESS)
|
|
FFAIL("Search error %d for year %d.\n", (int)seasons.status, year);
|
|
|
|
if (Verbose && ((year > 0) && (year % 1000 == 999)))
|
|
{
|
|
printf("C %s: DEBUG", __func__);
|
|
Astronomy_FormatTime(seasons.mar_equinox, TIME_FORMAT_DAY, text, sizeof(text));
|
|
printf(" %s", text);
|
|
Astronomy_FormatTime(seasons.jun_solstice, TIME_FORMAT_DAY, text, sizeof(text));
|
|
printf(" %s", text);
|
|
Astronomy_FormatTime(seasons.sep_equinox, TIME_FORMAT_DAY, text, sizeof(text));
|
|
printf(" %s", text);
|
|
Astronomy_FormatTime(seasons.dec_solstice, TIME_FORMAT_DAY, text, sizeof(text));
|
|
printf(" %s\n", text);
|
|
}
|
|
}
|
|
|
|
FPASS();
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int MoonPhase(void)
|
|
{
|
|
const char *filename = "moonphase/moonphases.txt";
|
|
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 = 90.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)
|
|
FFAIL("Cannot open input file '%s'\n", filename);
|
|
|
|
/*
|
|
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 (ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++lnum;
|
|
nscanned = sscanf(line, "%d %d-%d-%dT%d:%d:%lfZ", &quarter, &year, &month, &day, &hour, &minute, &second);
|
|
if (nscanned != 7)
|
|
FLNFAIL("Invalid data format\n");
|
|
|
|
if (quarter < 0 || quarter > 3)
|
|
FLNFAIL("Invalid quarter %d\n", quarter);
|
|
|
|
expected_elong = 90.0 * quarter;
|
|
expected_time = Astronomy_MakeTime(year, month, day, hour, minute, second);
|
|
result = Astronomy_MoonPhase(expected_time);
|
|
degree_error = ABS(result.angle - expected_elong);
|
|
if (degree_error > 180.0)
|
|
degree_error = 360 - degree_error;
|
|
arcmin = 60.0 * degree_error;
|
|
|
|
if (arcmin > 1.0)
|
|
FLNFAIL("EXCESSIVE ANGULAR ERROR: %lg arcmin\n", arcmin);
|
|
|
|
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)
|
|
FLNFAIL("Astronomy_SearchMoonQuarter returned quarter %d, but expected %d\n", mq.quarter, expected_quarter);
|
|
}
|
|
|
|
if (mq.status != ASTRO_SUCCESS)
|
|
FLNFAIL("Astronomy_SearchMoonQuarter returned %d\n", mq.status);
|
|
|
|
++quarter_count;
|
|
|
|
/* Make sure the time matches what we expect. */
|
|
diff_seconds = ABS(mq.time.tt - expected_time.tt) * (24.0 * 3600.0);
|
|
if (diff_seconds > threshold_seconds)
|
|
FLNFAIL("excessive time error %0.3lf seconds\n", diff_seconds);
|
|
|
|
if (diff_seconds > maxdiff)
|
|
maxdiff = diff_seconds;
|
|
}
|
|
|
|
printf("C %s: passed %d lines for file %s : max_arcmin = %0.6lf, maxdiff = %0.3lf seconds, %d quarters\n", __func__, lnum, filename, max_arcmin, maxdiff, quarter_count);
|
|
error = 0;
|
|
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int MoonReversePhase(double longitude)
|
|
{
|
|
/* Verify that Astronomy_SearchMoonPhase works both forward and backward in time. */
|
|
|
|
const int nphases = 5000; /* approximately 400 years worth */
|
|
int error = 1;
|
|
int i;
|
|
astro_time_t time;
|
|
astro_search_result_t result;
|
|
double *utList = NULL;
|
|
double dtMin = +1000.0;
|
|
double dtMax = -1000.0;
|
|
double diff, maxDiff = 0.0;
|
|
const int nslots = 100;
|
|
int k;
|
|
double ut, ut1, ut2;
|
|
|
|
/* Allocate memory to hold times of consecutive moon phases. */
|
|
utList = (double *)calloc(nphases, sizeof(utList[0]));
|
|
if (utList == NULL)
|
|
FFAIL("cannot allocate memory for longitude %0.0lf\n", longitude);
|
|
|
|
/* Search forward in time from 1800 to find consecutive new moon events. */
|
|
time = Astronomy_MakeTime(1800, 1, 1, 0, 0, 0.0);
|
|
for (i = 0; i < nphases; ++i)
|
|
{
|
|
result = Astronomy_SearchMoonPhase(longitude, time, +40.0);
|
|
CHECK_STATUS(result);
|
|
utList[i] = result.time.ut;
|
|
if (i > 0)
|
|
{
|
|
/* Verify that consecutive events are reasonably close to the synodic period (29.5 days) apart. */
|
|
double dt = V(utList[i] - utList[i-1]);
|
|
if (dt < dtMin) dtMin = dt;
|
|
if (dt > dtMax) dtMax = dt;
|
|
}
|
|
time = Astronomy_AddDays(result.time, +0.1);
|
|
}
|
|
|
|
FDEBUG("longitude %0.0lf: dtMin=%0.3lf days, dtMax=%0.3lf days.\n", longitude, dtMin, dtMax);
|
|
if (dtMin < 29.175 || dtMax > 29.926)
|
|
FFAIL("Time between consecutive new moons is suspicious.\n");
|
|
|
|
/* Do a reverse chronological search and make sure the results are consistent with the forward search. */
|
|
time = Astronomy_AddDays(time, 20.0);
|
|
for (i = nphases-1; i >= 0; --i)
|
|
{
|
|
result = Astronomy_SearchMoonPhase(longitude, time, -40.0);
|
|
CHECK_STATUS(result);
|
|
diff = SECONDS_PER_DAY * ABS(result.time.ut - utList[i]);
|
|
if (diff > maxDiff) maxDiff = diff;
|
|
time = Astronomy_AddDays(result.time, -0.1);
|
|
}
|
|
|
|
FDEBUG("longitude %0.0lf: Maximum discrepancy in reverse search = %0.3lf seconds.\n", longitude, maxDiff);
|
|
if (maxDiff > 0.165)
|
|
FFAIL("EXCESSIVE DISCREPANCY in reverse search.\n");
|
|
|
|
/* Pick a pair of consecutive events from the middle of the list. */
|
|
/* Verify forward and backward searches work correctly from many intermediate times. */
|
|
k = nphases / 2;
|
|
ut1 = utList[k];
|
|
ut2 = utList[k+1];
|
|
for (i = 1; i < nslots; ++i)
|
|
{
|
|
ut = ut1 + ((double)i/nslots)*(ut2 - ut1);
|
|
time = Astronomy_TimeFromDays(ut);
|
|
|
|
result = Astronomy_SearchMoonPhase(longitude, time, -40.0);
|
|
CHECK_STATUS(result);
|
|
diff = SECONDS_PER_DAY * ABS(result.time.ut - ut1);
|
|
if (diff > 0.07)
|
|
FFAIL("longitude %0.0lf: backward search error = %0.4le seconds.\n", longitude, diff);
|
|
|
|
result = Astronomy_SearchMoonPhase(longitude, time, +40.0);
|
|
CHECK_STATUS(result);
|
|
diff = SECONDS_PER_DAY * ABS(result.time.ut - ut2);
|
|
if (diff > 0.07)
|
|
FFAIL("longitude %0.0lf: forward search error = %0.4le seconds.\n", longitude, diff);
|
|
}
|
|
|
|
printf("C %s(%0.0lf): PASS\n", __func__, longitude);
|
|
error = 0;
|
|
fail:
|
|
free(utList);
|
|
return error;
|
|
}
|
|
|
|
|
|
static int MoonReverse(void)
|
|
{
|
|
return
|
|
MoonReversePhase( 0.0) ||
|
|
MoonReversePhase( 90.0) ||
|
|
MoonReversePhase(180.0) ||
|
|
MoonReversePhase(270.0);
|
|
}
|
|
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
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)
|
|
FFAIL("Cannot open input file: %s\n", filename);
|
|
|
|
lnum = 0;
|
|
while (ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++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)
|
|
FLNFAIL("Invalid data format.\n");
|
|
|
|
body = Astronomy_BodyCode(name);
|
|
if (body == BODY_INVALID)
|
|
FLNFAIL("Invalid body name '%s'\n", name);
|
|
|
|
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)
|
|
FLNFAIL("SearchRelativeLongitude returned %d\n", search_result.status);
|
|
|
|
diff_minutes = (24.0 * 60.0) * (search_result.time.tt - expected_time.tt);
|
|
FDEBUG("%-7s error = %6.3lf minutes\n", name, diff_minutes);
|
|
if (ABS(diff_minutes) > 6.8)
|
|
FLNFAIL("EXCESSIVE ERROR\n");
|
|
}
|
|
|
|
FPASSA("%d rows of data\n", lnum);
|
|
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)
|
|
{
|
|
char zcheck;
|
|
int year, month, day, hour, minute, nscanned;
|
|
double second;
|
|
|
|
nscanned = sscanf(text, "%d-%d-%dT%d:%d:%lf%c", &year, &month, &day, &hour, &minute, &second, &zcheck);
|
|
if (nscanned != 7 || zcheck != 'Z')
|
|
{
|
|
second = 0.0;
|
|
nscanned = sscanf(text, "%d-%d-%dT%d:%d%c", &year, &month, &day, &hour, &minute, &zcheck);
|
|
if (nscanned != 6 || zcheck != 'Z')
|
|
{
|
|
fprintf(stderr, "C %s: Invalid date text '%s'\n", __func__, text);
|
|
time->ut = time->tt = NAN;
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
*time = Astronomy_MakeTime(year, month, day, hour, minute, second);
|
|
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:
|
|
FFAIL("invalid body %d in test data.\n", test->body);
|
|
}
|
|
|
|
switch (test->visibility)
|
|
{
|
|
case VISIBLE_MORNING: vis = "morning"; break;
|
|
case VISIBLE_EVENING: vis = "evening"; break;
|
|
default:
|
|
FFAIL("invalid visibility %d in test data.\n", test->visibility);
|
|
}
|
|
|
|
CHECK(ParseDate(test->searchDate, &searchTime));
|
|
CHECK(ParseDate(test->eventDate, &eventTime));
|
|
|
|
evt = Astronomy_SearchMaxElongation(test->body, searchTime);
|
|
if (evt.status != ASTRO_SUCCESS)
|
|
FAIL("C %s(%s %s): SearchMaxElongation returned %d\n", __func__, name, test->searchDate, evt.status);
|
|
|
|
hour_diff = 24.0 * ABS(evt.time.tt - eventTime.tt);
|
|
arcmin_diff = 60.0 * ABS(evt.elongation - test->angle);
|
|
|
|
FDEBUG("%-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)
|
|
FAIL("C %s(%s %s): excessive hour error.\n", __func__, name, test->searchDate);
|
|
|
|
if (arcmin_diff > 3.4)
|
|
FAIL("C %s(%s %s): excessive arcmin error.\n", __func__, name, test->searchDate);
|
|
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
static int SearchElongTest()
|
|
{
|
|
int error, i;
|
|
|
|
for (i=0; i < ElongTestCount; ++i)
|
|
CHECK(TestMaxElong(&ElongTestData[i]));
|
|
|
|
FPASSA("%d rows\n", ElongTestCount);
|
|
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;
|
|
astro_vector_t geo;
|
|
double dist;
|
|
FILE *outfile = NULL;
|
|
const char *name;
|
|
double ratio, thresh;
|
|
|
|
name = Astronomy_BodyName(body);
|
|
if (!name[0])
|
|
FFAIL("Invalid body code %d\n", body);
|
|
|
|
outfile = fopen(outFileName, "wt");
|
|
if (outfile == NULL)
|
|
FFAIL("Cannot open output file: %s\n", outFileName);
|
|
|
|
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)
|
|
FAIL("C %s(%s): SearchRelativeLongitude returned %d\n", __func__, name, search_result.status);
|
|
|
|
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;
|
|
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)
|
|
FAIL("C %s(%s): GeoVector returned %d\n", __func__, name, geo.status);
|
|
|
|
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;
|
|
DEBUG("C %s(%-7s): %5d events, ratio=%5.3lf, file: %s\n", __func__, name, count, ratio, outFileName);
|
|
|
|
if (ratio > thresh)
|
|
FAIL("C %s(%s): excessive event interval ratio.\n", __func__, name);
|
|
|
|
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(void)
|
|
{
|
|
const char *filename = "riseset/riseset.txt";
|
|
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 = 1.0e-5; /* just under 1 second */
|
|
extern int _AltitudeDiffCallCount; /* undocumented global var used for performance testing only */
|
|
extern int _FindAscentMaxRecursionDepth; /* undocumented global var used for performance testing only */
|
|
|
|
_AltitudeDiffCallCount = 0;
|
|
_FindAscentMaxRecursionDepth = -1;
|
|
|
|
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)
|
|
FFAIL("cannot open input file: %s\n", filename);
|
|
|
|
lnum = 0;
|
|
while (ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++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)
|
|
FLNFAIL("invalid format\n");
|
|
|
|
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
|
|
FLNFAIL("invalid kind '%s'\n", kind);
|
|
|
|
body = Astronomy_BodyCode(name);
|
|
if (body == BODY_INVALID)
|
|
FLNFAIL("invalid body name '%s'", name);
|
|
|
|
/* 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;
|
|
FDEBUG("%-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)
|
|
FLNFAIL("did not find %s rise event: status = %d\n", name, r_evt.status);
|
|
|
|
s_evt = Astronomy_SearchRiseSet(body, observer, DIRECTION_SET, s_search_date, 366.0);
|
|
if (s_evt.status != ASTRO_SUCCESS)
|
|
FLNFAIL("did not find %s set event: status = %d\n", name, s_evt.status);
|
|
|
|
/* 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);
|
|
}
|
|
|
|
error_minutes = (24.0 * 60.0) * ABS(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 > 1.18)
|
|
FLNFAIL("excessive prediction time error = %lg minutes.\n", error_minutes);
|
|
|
|
if (a_dir != direction)
|
|
FLNFAIL("expected dir=%d but found %d\n", direction, a_dir);
|
|
}
|
|
|
|
rms_minutes = V(sqrt(sum_minutes / lnum));
|
|
FPASSA("%d lines: time errors in minutes: rms=%0.4lf, max=%0.4lf, recur=%d, altcount=%d\n", lnum, rms_minutes, max_minutes, _FindAscentMaxRecursionDepth, _AltitudeDiffCallCount);
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int RiseSetSlot(double ut1, double ut2, astro_direction_t dir, astro_observer_t observer)
|
|
{
|
|
int error = 1;
|
|
const int nslots = 100;
|
|
astro_time_t time;
|
|
astro_search_result_t result;
|
|
int i;
|
|
double ut, diff, maxDiff = 0.0;
|
|
|
|
for (i = 1; i < nslots; ++i)
|
|
{
|
|
ut = ut1 + ((double)i / nslots)*(ut2 - ut1);
|
|
time = Astronomy_TimeFromDays(ut);
|
|
|
|
result = Astronomy_SearchRiseSet(BODY_SUN, observer, dir, time, -1.0);
|
|
CHECK_STATUS(result);
|
|
diff = SECONDS_PER_DAY * ABS(result.time.ut - ut1);
|
|
if (diff > maxDiff) maxDiff = diff;
|
|
|
|
result = Astronomy_SearchRiseSet(BODY_SUN, observer, dir, time, +1.0);
|
|
CHECK_STATUS(result);
|
|
diff = SECONDS_PER_DAY * ABS(result.time.ut - ut2);
|
|
if (diff > maxDiff) maxDiff = diff;
|
|
}
|
|
if (maxDiff > 0.13)
|
|
FAIL("C %s(dir=%d): EXCESSIVE slot-test discrepancy = %0.6lf seconds.\n", __func__, dir, maxDiff);
|
|
DEBUG("C %s(dir=%d): slot discrepancy = %0.6lf seconds.\n", __func__, dir, maxDiff);
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
|
|
static int RiseSetReverse(void)
|
|
{
|
|
/* Verify that the rise/set search works equally well forwards and backwards in time. */
|
|
|
|
const int nsamples = 5000;
|
|
const double nudge = 0.1;
|
|
int error = 1;
|
|
int i;
|
|
double *utList = NULL;
|
|
astro_observer_t observer = Astronomy_MakeObserver(30.5, -90.7, 0.0);
|
|
astro_direction_t dir;
|
|
astro_time_t time;
|
|
astro_search_result_t result;
|
|
double dt, dtMin = +1000.0, dtMax = -1000.0;
|
|
double diff, maxDiff = 0.0;
|
|
int k;
|
|
|
|
utList = (double *)calloc(nsamples, sizeof(utList[0]));
|
|
if (utList == NULL)
|
|
FFAIL("out of memory!\n");
|
|
|
|
/* Find alternating sunrise/sunset events in forward chronological order. */
|
|
dir = DIRECTION_RISE;
|
|
time = Astronomy_MakeTime(2022, 1, 1, 0, 0, 0.0);
|
|
for (i = 0; i < nsamples; ++i)
|
|
{
|
|
result = Astronomy_SearchRiseSet(BODY_SUN, observer, dir, time, +1.0);
|
|
CHECK_STATUS(result);
|
|
utList[i] = result.time.ut;
|
|
if (i > 0)
|
|
{
|
|
/* Check the time between consecutive sunrise/sunset events. */
|
|
/* These will vary considerably with the seasons, so just make sure we don't miss any entirely. */
|
|
dt = V(utList[i] - utList[i-1]);
|
|
if (dt < dtMin) dtMin = dt;
|
|
if (dt > dtMax) dtMax = dt;
|
|
}
|
|
dir *= -1; /* Toggle searching sunrise/sunset. */
|
|
time = Astronomy_AddDays(result.time, +nudge);
|
|
}
|
|
|
|
FDEBUG("dtMin=%0.6lf days, dtMax=%0.6lf days.\n", dtMin, dtMax);
|
|
if (dtMin < 0.411 || dtMax > 0.589)
|
|
FFAIL("Invalid intervals between sunrise/sunset.\n");
|
|
|
|
/* Perform the same search in reverse. Verify we get consistent rise/set times. */
|
|
for (i = nsamples-1; i >= 0; --i)
|
|
{
|
|
dir *= -1; /* Toggle searching sunrise/sunset. */
|
|
result = Astronomy_SearchRiseSet(BODY_SUN, observer, dir, time, -1.0);
|
|
CHECK_STATUS(result);
|
|
diff = SECONDS_PER_DAY * ABS(utList[i] - result.time.ut);
|
|
if (diff > maxDiff) maxDiff = diff;
|
|
time = Astronomy_AddDays(result.time, -nudge);
|
|
}
|
|
if (maxDiff > 0.1)
|
|
FFAIL("EXCESSIVE forward/backward discrepancy = %0.6lf seconds.\n", maxDiff);
|
|
FDEBUG("forward/backward discrepancy = %0.6lf seconds.\n", maxDiff);
|
|
|
|
/* All even indexes in utList hold sunrise times. */
|
|
/* All odd indexes in utList hold sunset times. */
|
|
/* Verify that forward/backward searches for consecutive sunrises/sunsets */
|
|
/* resolve correctly for 100 time slots between them. */
|
|
k = (nsamples / 2) & ~1;
|
|
CHECK(RiseSetSlot(utList[k+0], utList[k+2], DIRECTION_RISE, observer));
|
|
CHECK(RiseSetSlot(utList[k+1], utList[k+3], DIRECTION_SET, observer));
|
|
|
|
FPASS();
|
|
fail:
|
|
free(utList);
|
|
return error;
|
|
}
|
|
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
|
|
static int RiseSetElevationBodyCase(
|
|
astro_body_t body,
|
|
astro_observer_t observer,
|
|
astro_direction_t direction,
|
|
double metersAboveGround,
|
|
astro_time_t startTime,
|
|
double eventOffsetDays)
|
|
{
|
|
int error;
|
|
astro_search_result_t result;
|
|
double diff;
|
|
|
|
result = Astronomy_SearchRiseSetEx(body, observer, direction, startTime, 2.0, metersAboveGround);
|
|
CHECK_STATUS(result);
|
|
|
|
diff = V(result.time.ut - (startTime.ut + eventOffsetDays));
|
|
if (diff > 0.5)
|
|
diff -= 1.0; /* assume event actually takes place on the next day */
|
|
|
|
diff = ABS(MINUTES_PER_DAY * diff); /* convert signed days to absolute minutes */
|
|
FDEBUG("%-4s %s, diff = %0.3lf, expected day offset = %0.6lf, search time = %0.6lf calc event time = %0.6lf\n", Astronomy_BodyName(body), (direction > 0) ? "rise" : "set ", diff, eventOffsetDays, startTime.ut, result.time.ut);
|
|
if (diff > 0.5)
|
|
FFAIL("body=%s, direction=%c, EXCESSIVE diff = %0.3lf minutes\n", Astronomy_BodyName(body), (direction > 0) ? 'r' : 's', diff);
|
|
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
|
|
static int RiseSetElevationCase(
|
|
astro_observer_t observer,
|
|
double metersAboveGround,
|
|
astro_time_t time,
|
|
double sr, double ss, double mr, double ms)
|
|
{
|
|
int error;
|
|
CHECK(RiseSetElevationBodyCase(BODY_SUN, observer, DIRECTION_RISE, metersAboveGround, time, sr));
|
|
CHECK(RiseSetElevationBodyCase(BODY_SUN, observer, DIRECTION_SET, metersAboveGround, time, ss));
|
|
CHECK(RiseSetElevationBodyCase(BODY_MOON, observer, DIRECTION_RISE, metersAboveGround, time, mr));
|
|
CHECK(RiseSetElevationBodyCase(BODY_MOON, observer, DIRECTION_SET, metersAboveGround, time, ms));
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
|
|
static int RiseSetElevation(void)
|
|
{
|
|
const char *filename = "riseset/elevation.txt";
|
|
int error;
|
|
FILE *infile = NULL;
|
|
int lnum, nscanned, year, month, day;
|
|
int srh, srm, ssh, ssm, mrh, mrm, msh, msm;
|
|
astro_observer_t observer;
|
|
astro_time_t time;
|
|
double sr, ss, mr, ms; /* day offsets for sr, ss, mr, ms */
|
|
double metersAboveGround, dip;
|
|
char line[200];
|
|
|
|
infile = fopen(filename, "rt");
|
|
if (infile == NULL)
|
|
FFAIL("Cannot open input file: %s\n", filename);
|
|
|
|
lnum = 0;
|
|
while (ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
if (line[0] == '#')
|
|
continue;
|
|
|
|
nscanned = sscanf(line, "%d-%d-%d %lf %lf %lf %lf %d:%d %d:%d %d:%d %d:%d %lf",
|
|
&year, &month, &day,
|
|
&observer.latitude, &observer.longitude, &observer.height,
|
|
&metersAboveGround,
|
|
&srh, &srm, &ssh, &ssm,
|
|
&mrh, &mrm, &msh, &msm,
|
|
&dip
|
|
);
|
|
|
|
if (nscanned != 16)
|
|
FLNFAIL("Expected 16 tokens but found %d\n", nscanned);
|
|
|
|
/* Get search origin time. */
|
|
time = Astronomy_MakeTime(year, month, day, 0, 0, 0.0);
|
|
|
|
/* Convert scanned values into sunrise, sunset, moonrise, moonset day offsets. */
|
|
sr = (srh + srm/60.0) / 24.0;
|
|
ss = (ssh + ssm/60.0) / 24.0;
|
|
mr = (mrh + mrm/60.0) / 24.0;
|
|
ms = (msh + msm/60.0) / 24.0;
|
|
|
|
CHECK(RiseSetElevationCase(observer, metersAboveGround, time, sr, ss, mr, ms));
|
|
}
|
|
|
|
/* Correct rise/set times for an observer that is above the horizon plane. */
|
|
FPASS();
|
|
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)
|
|
FFAIL("cannot open input file: %s\n", filename);
|
|
|
|
count = lnum = 0;
|
|
while (ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++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)
|
|
FLNFAIL("invalid data format\n");
|
|
|
|
illum = Astronomy_Illumination(body, time);
|
|
if (illum.status != ASTRO_SUCCESS)
|
|
FLNFAIL("Astronomy_Illumination returned %d\n", illum.status);
|
|
|
|
diff = illum.mag - mag;
|
|
if (ABS(diff) > limit)
|
|
FLNFAIL("EXCESSIVE ERROR: correct mag=%lf, calc mag=%lf, diff=%lf\n", mag, illum.mag, diff);
|
|
|
|
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)
|
|
FFAIL("Did not find any data in file: %s\n", filename);
|
|
|
|
rms = V(sqrt(sum_squared_diff / count));
|
|
FDEBUG("%-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.31725492, +24.43386475 },
|
|
{ "1980-01-01T00:00Z", +0.85796177, -1.72627324 },
|
|
{ "2009-09-04T00:00Z", +1.01932560, +0.01834451 },
|
|
{ "2017-06-15T00:00Z", -0.12303373, -26.60068380 },
|
|
{ "2019-05-01T00:00Z", +0.33124502, -23.47173574 },
|
|
{ "2025-09-25T00:00Z", +0.50543708, +1.69118986 },
|
|
{ "2032-05-15T00:00Z", -0.04649573, +26.95238680 }
|
|
};
|
|
|
|
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)
|
|
FAILRET("C %s(%d): Illumination returned %d\n", __func__, i, illum.status);
|
|
|
|
FDEBUG("date=%s calc mag=%12.8lf ring_tilt=%12.8lf\n", data[i].date, illum.mag, illum.ring_tilt);
|
|
|
|
mag_diff = ABS(illum.mag - data[i].mag);
|
|
if (mag_diff > 1.0e-8)
|
|
FAILRET("C %s: ERROR: Excessive magnitude error %lg\n", __func__, mag_diff);
|
|
|
|
tilt_diff = ABS(illum.ring_tilt - data[i].tilt);
|
|
if (tilt_diff > 1.0e-8)
|
|
FAILRET("C %s: ERROR: Excessive ring tilt error %lg\n", __func__, tilt_diff);
|
|
}
|
|
|
|
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)
|
|
FAILRET("C %s: ERROR: vec.status = %d\n", __func__, vec.status);
|
|
|
|
printf("C %s: %0.16lg %0.16lg %0.16lg\n", __func__, vec.x, vec.y, vec.z);
|
|
|
|
|
|
dx = vec.x - (+0.002674037026701135);
|
|
dy = vec.y - (-0.0001531610316600666);
|
|
dz = vec.z - (-0.0003150159927069429);
|
|
diff = V(sqrt(dx*dx + dy*dy + dz*dz));
|
|
printf("C %s: diff = %lg\n", __func__, diff);
|
|
if (diff > 4.34e-19)
|
|
FAILRET("C %s: EXCESSIVE ERROR\n", __func__);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
typedef enum
|
|
{
|
|
COORD_EQJ,
|
|
COORD_ECL
|
|
}
|
|
coord_sys_t;
|
|
|
|
|
|
static int MoonVectorFile(const char *filename, coord_sys_t coords, double rms_angle_limit, double rms_length_limit)
|
|
{
|
|
int error = 1;
|
|
FILE *infile = NULL;
|
|
int lnum = 0;
|
|
int count = 0; /* number of test cases processed */
|
|
int toggle = 0;
|
|
int found_start = 0;
|
|
char line[200];
|
|
double jd = 0.0;
|
|
astro_vector_t expected;
|
|
astro_vector_t calculated;
|
|
astro_angle_result_t result;
|
|
double angle_square_sum = 0.0;
|
|
double expected_length;
|
|
double calculated_length;
|
|
double length_error;
|
|
double length_error_square_sum = 0.0;
|
|
double rms_length, rms_angle;
|
|
double max_length = 0.0, max_angle = 0.0;
|
|
astro_rotation_t rot;
|
|
|
|
rot = Astronomy_Rotation_EQJ_ECL();
|
|
CHECK_STATUS(rot);
|
|
|
|
infile = fopen(filename, "rt");
|
|
if (infile == NULL)
|
|
FFAIL("cannot open file: %s\n", filename);
|
|
|
|
while (ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++lnum;
|
|
if (!found_start)
|
|
{
|
|
if (strlen(line) > 5 && !memcmp(line, "$$SOE", 5))
|
|
found_start = 1;
|
|
}
|
|
else
|
|
{
|
|
if (strlen(line) > 5 && !memcmp(line, "$$EOE", 5))
|
|
break;
|
|
|
|
if (strlen(line) < 62)
|
|
FLNFAIL("line is too short.\n");
|
|
|
|
/*
|
|
2444239.500000000 = A.D. 1980-Jan-01 00:00:00.0000 TDB [del_T= 50.183943 s]
|
|
X = 2.933885115037269E-04 Y = 2.421938721618651E-03 Z = 8.195641195651288E-04
|
|
*/
|
|
if (toggle == 0)
|
|
{
|
|
if (1 != sscanf(line, "%lf", &jd))
|
|
FLNFAIL("cannot parse julian date.\n");
|
|
V(jd);
|
|
}
|
|
else
|
|
{
|
|
/* Complete the expected Moon position vector for this time. */
|
|
if (3 != sscanf(line, " X =%lf Y =%lf Z =%lf", &expected.x, &expected.y, &expected.z))
|
|
FLNFAIL("cannot parse position vector.\n");
|
|
expected.t = Astronomy_TerrestrialTime(jd - JD_2000);
|
|
expected.status = ASTRO_SUCCESS;
|
|
|
|
switch (coords)
|
|
{
|
|
case COORD_EQJ:
|
|
calculated = Astronomy_GeoMoon(expected.t);
|
|
CHECK_STATUS(calculated);
|
|
break;
|
|
|
|
case COORD_ECL:
|
|
calculated = Astronomy_GeoMoon(expected.t);
|
|
CHECK_STATUS(calculated);
|
|
calculated = Astronomy_RotateVector(rot, calculated);
|
|
CHECK_STATUS(calculated);
|
|
break;
|
|
|
|
default:
|
|
FLNFAIL("unknown coord system %d\n", coords);
|
|
}
|
|
|
|
/* Compare distances and angles between vectors. */
|
|
result = Astronomy_AngleBetween(expected, calculated);
|
|
CHECK_STATUS(result);
|
|
result.angle = ABS(result.angle);
|
|
if (result.angle > max_angle)
|
|
max_angle = result.angle;
|
|
angle_square_sum += (result.angle * result.angle);
|
|
|
|
expected_length = Astronomy_VectorLength(expected);
|
|
V(expected_length);
|
|
calculated_length = Astronomy_VectorLength(calculated);
|
|
V(calculated_length);
|
|
|
|
length_error = ABS(calculated_length - expected_length);
|
|
if (length_error > max_length)
|
|
max_length = length_error;
|
|
length_error_square_sum += (length_error * length_error);
|
|
|
|
++count;
|
|
}
|
|
|
|
toggle ^= 1;
|
|
}
|
|
}
|
|
|
|
if (count != 73050)
|
|
FAIL("C %s(%s): unexpected count = %d!\n", __func__, filename, count);
|
|
|
|
rms_angle = V(3600.0 * sqrt(angle_square_sum / count));
|
|
rms_length = V(KM_PER_AU * sqrt(length_error_square_sum / count));
|
|
max_angle = V(3600.0 * max_angle);
|
|
max_length = V(KM_PER_AU * max_length);
|
|
|
|
if (rms_angle > rms_angle_limit)
|
|
FAIL("C %s(%s): EXCESSIVE rms angular error = %0.6lf arcsec\n", __func__, filename, rms_angle);
|
|
|
|
if (rms_length > rms_length_limit)
|
|
FAIL("C %s(%s): EXCESSIVE rms distance error = %0.3lf km\n", __func__, filename, rms_length);
|
|
|
|
printf("C %s(%s): PASS (%d lines, %d cases, angular error rms=%0.6lf arcsec, max=%0.6lf arcsec; distance error rms=%0.3lf km, max=%0.3lf km)\n",
|
|
__func__, filename, lnum, count, rms_angle, max_angle, rms_length, max_length);
|
|
|
|
error = 0;
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
|
|
static int MoonVector()
|
|
{
|
|
int error;
|
|
CHECK(MoonVectorFile("moonphase/moon_eqj.txt", COORD_EQJ, 1.16, 10.633));
|
|
CHECK(MoonVectorFile("moonphase/moon_ecl.txt", COORD_ECL, 1.16, 10.633));
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
static int MoonEcliptic(void)
|
|
{
|
|
int error = 1;
|
|
FILE *infile = NULL;
|
|
int lnum = 0;
|
|
int count = 0;
|
|
int found_start = 0;
|
|
const char *filename = "moonphase/moon_ecm.txt";
|
|
char line[100];
|
|
double jdut, tt, deltat, lon, lat;
|
|
double diff_lat, diff_lon, max_lat = 0.0, max_lon = 0.0;
|
|
astro_time_t time;
|
|
astro_spherical_t sphere;
|
|
|
|
/* Verify Moon's ecliptic coordinates relative to mean equator of date. */
|
|
|
|
infile = fopen(filename, "rt");
|
|
if (infile == NULL)
|
|
FFAIL("Cannot open input file: %s\n", filename);
|
|
|
|
while (ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++lnum;
|
|
if (!found_start)
|
|
{
|
|
if (strlen(line) > 5 && !memcmp(line, "$$SOE", 5))
|
|
found_start = 1;
|
|
}
|
|
else
|
|
{
|
|
if (strlen(line) > 5 && !memcmp(line, "$$EOE", 5))
|
|
break;
|
|
|
|
if (strlen(line) < 79)
|
|
FLNFAIL("line is too short.\n");
|
|
|
|
/* Parse reference data from JPL Horizons. */
|
|
|
|
/* " Date__(UT)__HR:MN Date_________JDUT TDB-UT ObsEcLon ObsEcLat" */
|
|
/* " 1900-Jan-01 00:00 2415020.500000000 -1.944461 272.4162663 1.1082671" */
|
|
if (4 != sscanf(&line[19], "%lf %lf %lf %lf", &jdut, &deltat, &lon, &lat))
|
|
FLNFAIL("cannot parse data.\n");
|
|
V(jdut);
|
|
V(deltat);
|
|
V(lon);
|
|
V(lat);
|
|
|
|
/* Use JPL Horizons delta-t value to match their TT value as close as possible. */
|
|
/* We do this because their Delta-T formula differs from Espenak-Meeus, especially in the future. */
|
|
/* TT-UT = deltat ==> TT = UT + deltat */
|
|
|
|
tt = V((jdut - JD_2000) + (deltat / SECONDS_PER_DAY));
|
|
time = Astronomy_TerrestrialTime(tt);
|
|
|
|
/* Calculate Moon's ecliptic coordinates relative to mean equator of date. */
|
|
sphere = Astronomy_EclipticGeoMoon(time);
|
|
CHECK_STATUS(sphere);
|
|
V(sphere.lat);
|
|
V(sphere.lon);
|
|
|
|
/* Find maximum arcsecond discrepancy between latitudes and longitudes. */
|
|
diff_lat = 3600.0 * DegreeDiff(sphere.lat, lat);
|
|
diff_lon = 3600.0 * DegreeDiff(sphere.lon, lon);
|
|
if (diff_lat > max_lat) max_lat = diff_lat;
|
|
if (diff_lon > max_lon) max_lon = diff_lon;
|
|
++count;
|
|
}
|
|
}
|
|
|
|
if (count != 73050)
|
|
FFAIL("Unexpected count = %d\n", count);
|
|
|
|
if (max_lat > 1.826)
|
|
FFAIL("EXCESSIVE ecliptic latitude discrepancy = %0.6lf arcsec.\n", max_lat);
|
|
|
|
if (max_lon > 5.928)
|
|
FFAIL("EXCESSIVE ecliptic longitude discrepancy = %0.6lf arcsec.\n", max_lon);
|
|
|
|
FPASSA("count=%d, max lat=%0.3lf arcsec, max lon=%0.3lf arcsec.\n", count, max_lat, max_lon);
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
|
|
static int CheckIlluminationInvalidBody(astro_body_t body)
|
|
{
|
|
astro_illum_t illum;
|
|
astro_time_t time = Astronomy_MakeTime(2021, 11, 10, 0, 8, 16.0);
|
|
|
|
/* Verify that invalid bodies return errors. */
|
|
|
|
illum = Astronomy_Illumination(body, time);
|
|
if (illum.status == ASTRO_SUCCESS)
|
|
FAILRET("C %s: FAILURE -- incorrect success status for body %d\n", __func__, (int)body);
|
|
|
|
return 0; /* Correct behavior. */
|
|
}
|
|
|
|
static int MagnitudeTest(void)
|
|
{
|
|
int nfailed = 0;
|
|
|
|
nfailed += CheckIlluminationInvalidBody(BODY_INVALID);
|
|
nfailed += CheckIlluminationInvalidBody(BODY_EARTH);
|
|
nfailed += CheckIlluminationInvalidBody(BODY_EMB);
|
|
nfailed += CheckIlluminationInvalidBody(BODY_SSB);
|
|
nfailed += CheckIlluminationInvalidBody((astro_body_t)2112);
|
|
|
|
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)
|
|
printf("C %s: PASS\n", __func__);
|
|
else
|
|
fprintf(stderr, "C %s: FAILED %d test(s).\n", __func__, 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)
|
|
FFAIL("Cannot open input file: %s\n", filename);
|
|
|
|
lnum = 0;
|
|
search_time = Astronomy_MakeTime(2001, 1, 1, 0, 0, 0.0);
|
|
while (ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++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)
|
|
FLNFAIL("invalid data format.\n");
|
|
|
|
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)
|
|
FLNFAIL("SearchPeakMagnitude returned %d\n", illum.status);
|
|
|
|
mag_diff = ABS(illum.mag - correct_mag);
|
|
hours_diff = 24.0 * ABS(illum.time.ut - center_time.ut);
|
|
FDEBUG("mag_diff=%0.3lf, hours_diff=%0.3lf\n", mag_diff, hours_diff);
|
|
if (hours_diff > 7.1)
|
|
FLNFAIL("EXCESSIVE TIME DIFFERENCE.\n");
|
|
|
|
if (mag_diff > 0.005)
|
|
FLNFAIL("EXCESSIVE MAGNITUDE DIFFERENCE.\n");
|
|
|
|
search_time = time2;
|
|
}
|
|
|
|
FPASSA("processed %d lines from file %s\n", lnum, filename);
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int ParseMonthName(const char *mtext, int *month)
|
|
{
|
|
if (month == NULL)
|
|
return 1;
|
|
|
|
*month = -1;
|
|
|
|
if (mtext == NULL)
|
|
return 2;
|
|
|
|
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 3;
|
|
|
|
return 0; /* success */
|
|
}
|
|
|
|
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;
|
|
|
|
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(void)
|
|
{
|
|
const char *filename = "apsides/moon.txt";
|
|
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)
|
|
FFAIL("Cannot open input file: %s\n", filename);
|
|
|
|
/*
|
|
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 (ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++lnum;
|
|
|
|
if (lnum == 1)
|
|
apsis = Astronomy_SearchLunarApsis(start_time);
|
|
else
|
|
apsis = Astronomy_NextLunarApsis(apsis);
|
|
|
|
if (apsis.status != ASTRO_SUCCESS)
|
|
FLNFAIL("Failed to find apsis.\n");
|
|
|
|
nscanned = sscanf(line, "%d %d-%d-%dT%d:%dZ %lf", &kind, &year, &month, &day, &hour, &minute, &dist_km);
|
|
if (nscanned != 7)
|
|
FLNFAIL("invalid data format\n");
|
|
|
|
if (kind != apsis.kind)
|
|
FLNFAIL("expected apsis kind %d but found %d\n", kind, apsis.kind);
|
|
|
|
correct_time = Astronomy_MakeTime(year, month, day, hour, minute, 0.0);
|
|
|
|
diff_minutes = (24.0 * 60.0) * ABS(apsis.time.ut - correct_time.ut);
|
|
diff_km = ABS(apsis.dist_km - dist_km);
|
|
|
|
if (diff_minutes > 35.0)
|
|
FLNFAIL("Excessive time error: %lf minutes.\n", diff_minutes);
|
|
|
|
if (diff_km > 25.0)
|
|
FLNFAIL("Excessive distance error: %lf km.\n", diff_km);
|
|
|
|
if (diff_minutes > max_minutes)
|
|
max_minutes = diff_minutes;
|
|
|
|
if (diff_km > max_km)
|
|
max_km = diff_km;
|
|
}
|
|
|
|
FPASSA("found %d events, max time error = %0.3lf minutes, max distance error = %0.3lf km.\n", lnum, max_minutes, max_km);
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int EarthApsis(void)
|
|
{
|
|
const char *filename = "apsides/earth.txt";
|
|
int error = 1;
|
|
FILE *infile = NULL;
|
|
int lnum, nscanned;
|
|
char line[100];
|
|
int kind, year, month, day, hour, minute;
|
|
double dist_au;
|
|
astro_time_t start_time, correct_time;
|
|
astro_apsis_t apsis;
|
|
double diff_minutes, diff_au;
|
|
double max_minutes = 0.0, max_au = 0.0;
|
|
|
|
infile = fopen(filename, "rt");
|
|
if (infile == NULL)
|
|
FFAIL("Cannot open input file: %s\n", filename);
|
|
|
|
/*
|
|
0 2001-01-04T08:52Z 0.9832860
|
|
1 2001-07-04T13:37Z 1.0166426
|
|
*/
|
|
|
|
start_time = Astronomy_MakeTime(2001, 1, 1, 0, 0, 0.0);
|
|
lnum = 0;
|
|
while (ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++lnum;
|
|
|
|
if (lnum == 1)
|
|
apsis = Astronomy_SearchPlanetApsis(BODY_EARTH, start_time);
|
|
else
|
|
apsis = Astronomy_NextPlanetApsis(BODY_EARTH, apsis);
|
|
|
|
if (apsis.status != ASTRO_SUCCESS)
|
|
FLNFAIL("Failed to find apsis.\n");
|
|
|
|
nscanned = sscanf(line, "%d %d-%d-%dT%d:%dZ %lf", &kind, &year, &month, &day, &hour, &minute, &dist_au);
|
|
if (nscanned != 7)
|
|
FLNFAIL("invalid data format\n");
|
|
|
|
if (kind != apsis.kind)
|
|
FLNFAIL("expected apsis kind %d but found %d\n", kind, apsis.kind);
|
|
|
|
correct_time = Astronomy_MakeTime(year, month, day, hour, minute, 0.0);
|
|
|
|
diff_minutes = (24.0 * 60.0) * ABS(apsis.time.ut - correct_time.ut);
|
|
diff_au = ABS(apsis.dist_au - dist_au);
|
|
|
|
if (diff_minutes > 120.58)
|
|
FLNFAIL("Excessive time error: %lf minutes.\n", diff_minutes);
|
|
|
|
if (diff_au > 1.2e-5)
|
|
FLNFAIL("Excessive distance error: %lg AU.\n", diff_au);
|
|
|
|
if (diff_minutes > max_minutes)
|
|
max_minutes = diff_minutes;
|
|
|
|
if (diff_au > max_au)
|
|
max_au = diff_au;
|
|
}
|
|
|
|
FPASSA("found %d events, max time error = %0.3lf minutes, max distance error = %0.3lg AU.\n", lnum, max_minutes, max_au);
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
|
|
static int PlanetApsis(void)
|
|
{
|
|
int error;
|
|
astro_body_t body;
|
|
astro_time_t start_time, prev_time;
|
|
astro_apsis_t apsis;
|
|
astro_utc_t utc;
|
|
int count;
|
|
double interval, min_interval, max_interval;
|
|
FILE *infile = NULL;
|
|
char filename[100];
|
|
char line[100];
|
|
char expected_time_text[100];
|
|
astro_time_t expected_time;
|
|
int expected_kind;
|
|
double expected_distance;
|
|
double period;
|
|
double diff_days, diff_degrees, max_degrees=0.0, diff_dist_ratio;
|
|
const double degree_threshold = 0.1;
|
|
double max_diff_days, max_dist_ratio;
|
|
|
|
start_time = Astronomy_MakeTime(1700, 1, 1, 0, 0, 0.0);
|
|
|
|
for (body = BODY_MERCURY; body <= BODY_PLUTO; ++body)
|
|
{
|
|
period = Astronomy_PlanetOrbitalPeriod(body);
|
|
max_dist_ratio = 0.0;
|
|
max_diff_days = 0.0;
|
|
snprintf(filename, sizeof(filename)-1, "apsides/apsis_%d.txt", (int)body);
|
|
if (infile) fclose(infile);
|
|
infile = fopen(filename, "rt");
|
|
if (infile == NULL)
|
|
FFAIL("ERROR - cannot open input file: %s\n", filename);
|
|
|
|
min_interval = max_interval = -1.0;
|
|
apsis = Astronomy_SearchPlanetApsis(body, start_time);
|
|
if (apsis.status != ASTRO_SUCCESS)
|
|
FFAIL("ERROR %d finding first apsis for %s\n", apsis.status, Astronomy_BodyName(body));
|
|
|
|
count = 1;
|
|
while (ReadLine(line, sizeof(line), infile, filename, count-1))
|
|
{
|
|
/* Parse the line of test data. */
|
|
if ( (3 != sscanf(line, "%d %30s %lf", &expected_kind, expected_time_text, &expected_distance))
|
|
|| (expected_kind & ~1) /* must be either 0=perihelion or 1=aphelion */
|
|
|| (expected_distance <= 0.0)
|
|
|| ParseDate(expected_time_text, &expected_time))
|
|
{
|
|
FFAIL("INPUT SYNTAX ERROR (%s line %d): '%s'\n", filename, count, line);
|
|
}
|
|
|
|
/* Compare computed values against expected values. */
|
|
if (apsis.kind != expected_kind)
|
|
FFAIL("WRONG APSIS KIND (%s line %d)\n", filename, count);
|
|
|
|
diff_days = ABS(expected_time.tt - apsis.time.tt);
|
|
if (diff_days > max_diff_days) max_diff_days = diff_days;
|
|
diff_degrees = (diff_days / period) * 360.0;
|
|
if (diff_degrees > max_degrees)
|
|
max_degrees = diff_degrees;
|
|
|
|
if (diff_degrees > degree_threshold)
|
|
FFAIL("FAIL - %s exceeded angular threshold (%lg versus %lg degrees)\n", Astronomy_BodyName(body), max_degrees, degree_threshold);
|
|
|
|
diff_dist_ratio = ABS(expected_distance - apsis.dist_au) / expected_distance;
|
|
if (diff_dist_ratio > max_dist_ratio) max_dist_ratio = diff_dist_ratio;
|
|
if (diff_dist_ratio > 1.05e-4)
|
|
{
|
|
FFAIL("EXCESSIVE DISTANCE ERROR for %s (%s line %d): expected=%0.16lf, calculated=%0.16lf, error ratio=%lg\n",
|
|
Astronomy_BodyName(body), filename, count, expected_distance, apsis.dist_au, diff_dist_ratio);
|
|
}
|
|
|
|
/* Calculate the next apsis. */
|
|
prev_time = apsis.time;
|
|
utc = Astronomy_UtcFromTime(apsis.time);
|
|
apsis = Astronomy_NextPlanetApsis(body, apsis);
|
|
if (apsis.status != ASTRO_SUCCESS)
|
|
{
|
|
FFAIL("ERROR %d finding apsis for %s after %04d-%02d-%02d\n",
|
|
apsis.status, Astronomy_BodyName(body), utc.year, utc.month, utc.day);
|
|
}
|
|
|
|
/* Update statistics. */
|
|
++count;
|
|
interval = apsis.time.tt - prev_time.tt;
|
|
if (min_interval < 0.0)
|
|
{
|
|
min_interval = max_interval = interval;
|
|
}
|
|
else
|
|
{
|
|
if (interval < min_interval)
|
|
min_interval = interval;
|
|
if (interval > max_interval)
|
|
max_interval = interval;
|
|
}
|
|
}
|
|
|
|
if (count < 2)
|
|
FFAIL("FAILED to find apsides for %s\n", Astronomy_BodyName(body));
|
|
|
|
FDEBUG("%5d apsides for %-9s -- intervals: min=%9.2lf, max=%9.2lf, ratio=%8.6lf; max day=%lg, degrees=%0.3lf, dist ratio=%lg\n",
|
|
count, Astronomy_BodyName(body),
|
|
min_interval, max_interval, max_interval / min_interval,
|
|
max_diff_days,
|
|
(max_diff_days / period) * 360.0,
|
|
max_dist_ratio);
|
|
}
|
|
|
|
FPASS();
|
|
fail:
|
|
if (infile) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int CheckUnitVector(int lnum, const char *name, astro_rotation_t r, int i0, int j0, int di, int dj)
|
|
{
|
|
int k;
|
|
double x;
|
|
double sum = 0.0;
|
|
|
|
for (k=0; k<3; ++k)
|
|
{
|
|
x = r.rot[i0+k*di][j0+k*dj];
|
|
sum += x*x;
|
|
}
|
|
x = ABS(sum - 1.0);
|
|
if (x > 1.8e-15)
|
|
FAILRET("C CheckUnitVector ERROR(%s line %d): unit error = %lg for i0=%d, j0=%d, di=%d, dj=%d\n", name, lnum, x, i0, j0, di, dj);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int CheckRotationMatrix(int lnum, const char *name, astro_rotation_t r)
|
|
{
|
|
if (r.status != ASTRO_SUCCESS)
|
|
FAILRET("C CheckRotationMatrix ERROR(%s line %d): status = %d\n", name, lnum, r.status);
|
|
|
|
/* Verify that every row and every column is a unit vector. */
|
|
if (CheckUnitVector(lnum, name, r, 0, 0, 1, 0)) return 1;
|
|
if (CheckUnitVector(lnum, name, r, 0, 1, 1, 0)) return 1;
|
|
if (CheckUnitVector(lnum, name, r, 0, 2, 1, 0)) return 1;
|
|
if (CheckUnitVector(lnum, name, r, 0, 0, 0, 1)) return 1;
|
|
if (CheckUnitVector(lnum, name, r, 1, 0, 0, 1)) return 1;
|
|
if (CheckUnitVector(lnum, name, r, 2, 0, 0, 1)) return 1;
|
|
return 0;
|
|
}
|
|
|
|
#define CHECK_ROTMAT(r) CHECK(CheckRotationMatrix(__LINE__, #r, (r)))
|
|
|
|
static int CompareVectors(const char *caller, astro_vector_t a, astro_vector_t b, double tolerance)
|
|
{
|
|
double diff;
|
|
|
|
if (a.status != ASTRO_SUCCESS)
|
|
FAILRET("C CompareVectors ERROR(%s): a.status = %d\n", caller, a.status);
|
|
|
|
if (b.status != ASTRO_SUCCESS)
|
|
FAILRET("C CompareVectors ERROR(%s): b.status = %d\n", caller, b.status);
|
|
|
|
diff = ABS(a.x - b.x);
|
|
if (diff > tolerance)
|
|
FAILRET("C CompareVectors ERROR(%s): x=%lg, expected %lg, diff %lg\n", caller, a.x, b.x, diff);
|
|
|
|
diff = ABS(a.y - b.y);
|
|
if (diff > tolerance)
|
|
FAILRET("C CompareVectors ERROR(%s): y=%lg, expected %lg, diff %lg\n", caller, a.y, b.y, diff);
|
|
|
|
diff = ABS(a.z - b.z);
|
|
if (diff > tolerance)
|
|
FAILRET("C CompareVectors ERROR(%s): z=%lg, expected %lg, diff %lg\n", caller, a.z, b.z, diff);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int CompareMatrices(const char *caller, astro_rotation_t a, astro_rotation_t b, double tolerance)
|
|
{
|
|
int i, j;
|
|
|
|
if (a.status != ASTRO_SUCCESS)
|
|
FAILRET("C CompareMatrices ERROR(%s): a.status = %d\n", caller, a.status);
|
|
|
|
if (b.status != ASTRO_SUCCESS)
|
|
FAILRET("C CompareMatrices ERROR(%s): b.status = %d\n", caller, b.status);
|
|
|
|
for (i=0; i < 3; ++i)
|
|
{
|
|
for (j=0; j < 3; ++j)
|
|
{
|
|
double diff = ABS(a.rot[i][j] - b.rot[i][j]);
|
|
if (diff > tolerance)
|
|
FAILRET("C CompareMatrices ERROR(%s): matrix[%d][%d]=%lg, expected %lg, diff %lg\n", caller, i, j, a.rot[i][j], b.rot[i][j], diff);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int Rotation_MatrixInverse(void)
|
|
{
|
|
astro_rotation_t a, b, v;
|
|
int error;
|
|
|
|
a.status = ASTRO_SUCCESS;
|
|
a.rot[0][0] = 1.0; a.rot[1][0] = 2.0; a.rot[2][0] = 3.0;
|
|
a.rot[0][1] = 4.0; a.rot[1][1] = 5.0; a.rot[2][1] = 6.0;
|
|
a.rot[0][2] = 7.0; a.rot[1][2] = 8.0; a.rot[2][2] = 9.0;
|
|
|
|
v.status = ASTRO_SUCCESS;
|
|
v.rot[0][0] = 1.0; v.rot[1][0] = 4.0; v.rot[2][0] = 7.0;
|
|
v.rot[0][1] = 2.0; v.rot[1][1] = 5.0; v.rot[2][1] = 8.0;
|
|
v.rot[0][2] = 3.0; v.rot[1][2] = 6.0; v.rot[2][2] = 9.0;
|
|
|
|
b = Astronomy_InverseRotation(a);
|
|
CHECK(CompareMatrices("Rotation_MatrixInverse", b, v, 0.0));
|
|
|
|
FPASS();
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
static int Rotation_Pivot(void)
|
|
{
|
|
int error;
|
|
astro_rotation_t ident;
|
|
astro_rotation_t r;
|
|
astro_rotation_t a;
|
|
astro_vector_t v1, v2, ve;
|
|
const double tolerance = 1.0e-15;
|
|
|
|
/* Test #1 */
|
|
|
|
/* Start with an identity matrix. */
|
|
ident = Astronomy_IdentityMatrix();
|
|
|
|
/* Pivot 90 degrees counterclockwise around the z-axis. */
|
|
r = Astronomy_Pivot(ident, 2, +90.0);
|
|
CHECK_STATUS(r);
|
|
|
|
/* Put the expected answer in 'a'. */
|
|
a.status = ASTRO_SUCCESS;
|
|
a.rot[0][0] = 0.0; a.rot[1][0] = -1.0; a.rot[2][0] = 0.0;
|
|
a.rot[0][1] = +1.0; a.rot[1][1] = 0.0; a.rot[2][1] = 0.0;
|
|
a.rot[0][2] = 0.0; a.rot[1][2] = 0.0; a.rot[2][2] = 1.0;
|
|
|
|
/* Compare actual 'r' with expected 'a'. */
|
|
CHECK(CompareMatrices("Rotation_Pivot #1", r, a, tolerance));
|
|
|
|
/* Test #2. */
|
|
|
|
/* Pivot again, -30 degrees around the x-axis. */
|
|
r = Astronomy_Pivot(r, 0, -30.0);
|
|
CHECK_STATUS(r);
|
|
|
|
/* 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.status = ASTRO_SUCCESS;
|
|
v1.t = Astronomy_MakeTime(2000, 1, 1, 0, 0, 0.0);
|
|
v1.x = 1.0;
|
|
v1.y = 2.0;
|
|
v1.z = 3.0;
|
|
|
|
v2 = Astronomy_RotateVector(r, v1);
|
|
CHECK_STATUS(v2);
|
|
|
|
/* Initialize the expected vector 've'. */
|
|
ve.status = ASTRO_SUCCESS;
|
|
ve.t = v1.t;
|
|
ve.x = +2.0;
|
|
ve.y = +2.3660254037844390;
|
|
ve.z = -2.0980762113533156;
|
|
|
|
CHECK(CompareVectors("Rotation_Pivot #2", v2, ve, tolerance));
|
|
|
|
FPASS();
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
static int Rotation_MatrixMultiply(void)
|
|
{
|
|
astro_rotation_t a, b, c, v;
|
|
int error;
|
|
|
|
a.status = ASTRO_SUCCESS;
|
|
a.rot[0][0] = 1.0; a.rot[1][0] = 2.0; a.rot[2][0] = 3.0;
|
|
a.rot[0][1] = 4.0; a.rot[1][1] = 5.0; a.rot[2][1] = 6.0;
|
|
a.rot[0][2] = 7.0; a.rot[1][2] = 8.0; a.rot[2][2] = 9.0;
|
|
|
|
b.status = ASTRO_SUCCESS;
|
|
b.rot[0][0] = 10.0; b.rot[1][0] = 11.0; b.rot[2][0] = 12.0;
|
|
b.rot[0][1] = 13.0; b.rot[1][1] = 14.0; b.rot[2][1] = 15.0;
|
|
b.rot[0][2] = 16.0; b.rot[1][2] = 17.0; b.rot[2][2] = 18.0;
|
|
|
|
v.status = ASTRO_SUCCESS;
|
|
v.rot[0][0] = 84.0; v.rot[1][0] = 90.0; v.rot[2][0] = 96.0;
|
|
v.rot[0][1] = 201.0; v.rot[1][1] = 216.0; v.rot[2][1] = 231.0;
|
|
v.rot[0][2] = 318.0; v.rot[1][2] = 342.0; v.rot[2][2] = 366.0;
|
|
|
|
/* Let c = a*b. The order looks backwards in the call to make rotation semantics more intuitive. */
|
|
c = Astronomy_CombineRotation(b, a);
|
|
|
|
/* Verify that c = v. */
|
|
CHECK(CompareMatrices("Rotation_MatrixMultiply", c, v, 0.0));
|
|
|
|
FPASS();
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
static int TestVectorFromAngles(double lat, double lon, double x, double y, double z)
|
|
{
|
|
astro_spherical_t sphere;
|
|
astro_vector_t vector;
|
|
astro_time_t time;
|
|
double diff, dx, dy, dz;
|
|
|
|
/* Confirm the expected vector really is a unit vector. */
|
|
diff = ABS((x*x + y*y + z*z) - 1.0);
|
|
if (diff > 1.0e-16)
|
|
FAILRET("C %s: EXCESSIVE unit error = %lg\n", __func__, diff);
|
|
|
|
sphere.status = ASTRO_SUCCESS;
|
|
sphere.lat = lat;
|
|
sphere.lon = lon;
|
|
sphere.dist = 1.0;
|
|
|
|
time = Astronomy_MakeTime(2015, 3, 5, 12, 0, 0.0);
|
|
vector = Astronomy_VectorFromSphere(sphere, time);
|
|
|
|
if (vector.status != ASTRO_SUCCESS)
|
|
FAILRET("C ERROR(TestVectorFromAngles): vector.status = %d\n", vector.status);
|
|
|
|
dx = x - vector.x;
|
|
dy = y - vector.y;
|
|
dz = z - vector.z;
|
|
diff = V(sqrt(dx*dx + dy*dy + dz*dz));
|
|
|
|
DEBUG("C TestVectorFromAngles(%lf, %lf): diff = %lg\n", lat, lon, diff);
|
|
if (diff > 2.0e-16)
|
|
FAILRET("C %s: EXCESSIVE ERROR.\n", __func__);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int TestAnglesFromVector(double lat, double lon, double x, double y, double z)
|
|
{
|
|
astro_vector_t vector;
|
|
astro_spherical_t sphere;
|
|
double diff, latdiff, londiff;
|
|
|
|
/* Confirm the expected vector really is a unit vector. */
|
|
diff = ABS((x*x + y*y + z*z) - 1.0);
|
|
if (diff > 1.0e-16)
|
|
FAILRET("C TestAnglesFromVector(lat=%lf, lon=%lf, x=%lf, y=%lf, z=%lf): EXCESSIVE unit error = %lg\n", lat, lon, x, y, z, diff);
|
|
|
|
vector.status = ASTRO_SUCCESS;
|
|
vector.t = Astronomy_MakeTime(2015, 3, 5, 12, 0, 0.0);
|
|
vector.x = x;
|
|
vector.y = y;
|
|
vector.z = z;
|
|
|
|
sphere = Astronomy_SphereFromVector(vector);
|
|
if (sphere.status != ASTRO_SUCCESS)
|
|
FAILRET("C %s(lat=%lf, lon=%lf, x=%lf, y=%lf, z=%lf): sphere.status = %d\n", __func__, lat, lon, x, y, z, sphere.status);
|
|
|
|
latdiff = ABS(sphere.lat - lat);
|
|
londiff = ABS(sphere.lon - lon);
|
|
DEBUG("TestAnglesFromVector(x=%lf, y=%lf, z=%lf): latdiff=%lg, londiff=%lg\n", x, y, z, latdiff, londiff);
|
|
if (latdiff > 8.0e-15 || londiff > 8.0e-15)
|
|
FAILRET("C %s: EXCESSIVE ERROR\n", __func__);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static astro_rotation_t RotateX(double rot)
|
|
{
|
|
astro_rotation_t m;
|
|
double ca, sa;
|
|
|
|
/* https://en.wikipedia.org/wiki/Rotation_matrix */
|
|
|
|
ca = cos(rot * DEG2RAD);
|
|
sa = sin(rot * DEG2RAD);
|
|
m.status = ASTRO_SUCCESS;
|
|
m.rot[0][0] = 1.0; m.rot[1][0] = 0.0; m.rot[2][0] = 0.0;
|
|
m.rot[0][1] = 0.0; m.rot[1][1] = +ca; m.rot[2][1] = -sa;
|
|
m.rot[0][2] = 0.0; m.rot[1][2] = +sa; m.rot[2][2] = +ca;
|
|
|
|
return m;
|
|
}
|
|
|
|
static astro_rotation_t RotateY(double rot)
|
|
{
|
|
astro_rotation_t m;
|
|
double ca, sa;
|
|
|
|
/* https://en.wikipedia.org/wiki/Rotation_matrix */
|
|
|
|
ca = cos(rot * DEG2RAD);
|
|
sa = sin(rot * DEG2RAD);
|
|
m.status = ASTRO_SUCCESS;
|
|
m.rot[0][0] = +ca; m.rot[1][0] = 0.0; m.rot[2][0] = +sa;
|
|
m.rot[0][1] = 0.0; m.rot[1][1] = 1.0; m.rot[2][1] = 0.0;
|
|
m.rot[0][2] = -sa; m.rot[1][2] = 0.0; m.rot[2][2] = +ca;
|
|
|
|
return m;
|
|
}
|
|
|
|
static astro_rotation_t RotateZ(double rot)
|
|
{
|
|
astro_rotation_t m;
|
|
double ca, sa;
|
|
|
|
/* https://en.wikipedia.org/wiki/Rotation_matrix */
|
|
|
|
ca = cos(rot * DEG2RAD);
|
|
sa = sin(rot * DEG2RAD);
|
|
m.status = ASTRO_SUCCESS;
|
|
m.rot[0][0] = +ca; m.rot[1][0] = -sa; m.rot[2][0] = 0.0;
|
|
m.rot[0][1] = +sa; m.rot[1][1] = +ca; m.rot[2][1] = 0.0;
|
|
m.rot[0][2] = 0.0; m.rot[1][2] = 0.0; m.rot[2][2] = 1.0;
|
|
|
|
return m;
|
|
}
|
|
|
|
static int TestSpin(
|
|
double xrot, double yrot, double zrot,
|
|
double sx, double sy, double sz,
|
|
double tx, double ty, double tz)
|
|
{
|
|
astro_rotation_t mx, my, mz, m;
|
|
astro_vector_t sv, tv;
|
|
double diff, dx, dy, dz;
|
|
int error;
|
|
|
|
/* https://en.wikipedia.org/wiki/Rotation_matrix */
|
|
|
|
mx = RotateX(xrot);
|
|
CHECK_ROTMAT(mx);
|
|
my = RotateY(yrot);
|
|
CHECK_ROTMAT(my);
|
|
mz = RotateZ(zrot);
|
|
CHECK_ROTMAT(mz);
|
|
m = Astronomy_CombineRotation(mx, my);
|
|
CHECK_ROTMAT(m);
|
|
m = Astronomy_CombineRotation(m, mz);
|
|
CHECK_ROTMAT(m);
|
|
|
|
sv.status = ASTRO_SUCCESS;
|
|
sv.x = sx;
|
|
sv.y = sy;
|
|
sv.z = sz;
|
|
sv.t = Astronomy_MakeTime(2019, 5, 5, 12, 30, 0.0);
|
|
|
|
tv = Astronomy_RotateVector(m, sv);
|
|
if (tv.status != ASTRO_SUCCESS)
|
|
FAIL("C ERROR(TestSpin): tv.status = %d\n", tv.status);
|
|
|
|
if (tv.t.ut != sv.t.ut)
|
|
FAIL("C ERROR(TestSpin): tv time != sv time\n");
|
|
|
|
dx = tx - tv.x;
|
|
dy = ty - tv.y;
|
|
dz = tz - tv.z;
|
|
diff = V(sqrt(dx*dx + dy*dy + dz*dz));
|
|
DEBUG("C TestSpin(xrot=%0.0lf, yrot=%0.0lf, zrot=%0.0lf, sx=%0.0lf, sy=%0.0lf, sz=%0.0lf): diff = %lg\n", xrot, yrot, zrot, sx, sy, sz, diff);
|
|
if (diff > 1.0e-15)
|
|
FFAIL("EXCESSIVE ERROR\n");
|
|
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
static int Test_EQJ_GAL_NOVAS(const char *filename)
|
|
{
|
|
/* Compare against ICRS/GAL calculated by NOVAS C 3.1. */
|
|
const double THRESHOLD_SECONDS = 8.8;
|
|
int error;
|
|
double ra, dec, glon, glat;
|
|
double dlon, dlat, diff, max_diff;
|
|
FILE *infile = NULL;
|
|
int lnum, nscanned;
|
|
char line[100];
|
|
astro_rotation_t rot, inv;
|
|
astro_time_t time;
|
|
astro_spherical_t eqj_sphere, gal_sphere;
|
|
astro_vector_t eqj_vec, gal_vec;
|
|
|
|
time = Astronomy_MakeTime(2000, 1, 1, 0, 0, 0.0); /* placeholder time - value does not matter */
|
|
|
|
rot = Astronomy_Rotation_EQJ_GAL();
|
|
CHECK_STATUS(rot);
|
|
|
|
inv = Astronomy_Rotation_GAL_EQJ();
|
|
CHECK_STATUS(inv);
|
|
CHECK_INVERSE(rot, inv);
|
|
|
|
infile = fopen(filename, "rt");
|
|
if (infile == NULL)
|
|
FFAIL("Cannot open input file: %s\n", filename);
|
|
|
|
max_diff = 0.0;
|
|
lnum = 0;
|
|
while (ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++lnum;
|
|
nscanned = sscanf(line, "%lf %lf %lf %lf", &ra, &dec, &glon, &glat);
|
|
if (nscanned != 4)
|
|
FAIL("C Test_EQJ_GAL_NOVAS(%s line %d): bad input format.\n", filename, lnum);
|
|
V(ra);
|
|
V(dec);
|
|
V(glon);
|
|
V(glat);
|
|
|
|
/* Use Astronomy Engine to do the same EQJ/GAL conversion. */
|
|
eqj_sphere.status = ASTRO_SUCCESS;
|
|
eqj_sphere.dist = 1.0;
|
|
eqj_sphere.lon = 15.0 * ra; /* Convert from sidereal hours to degrees. */
|
|
eqj_sphere.lat = dec;
|
|
|
|
eqj_vec = Astronomy_VectorFromSphere(eqj_sphere, time);
|
|
CHECK_STATUS(eqj_vec);
|
|
|
|
gal_vec = Astronomy_RotateVector(rot, eqj_vec);
|
|
CHECK_STATUS(gal_vec);
|
|
|
|
gal_sphere = Astronomy_SphereFromVector(gal_vec);
|
|
CHECK_STATUS(gal_sphere);
|
|
|
|
dlat = V(gal_sphere.lat - glat);
|
|
dlon = cos(DEG2RAD * glat) * V(gal_sphere.lon - glon);
|
|
diff = V(3600.0 * sqrt(dlon*dlon + dlat*dlat)); /* error in arcseconds */
|
|
if (diff > THRESHOLD_SECONDS)
|
|
FAIL("C Test_EQJ_GAL_NOVAS(%s line %d): EXCESSIVE ERROR = %0.3lf arcseconds\n", filename, lnum, diff);
|
|
|
|
if (diff > max_diff)
|
|
max_diff = diff;
|
|
}
|
|
|
|
FDEBUG("PASS. max_diff = %0.3lf arcseconds.\n", max_diff);
|
|
error = 0;
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
static int Test_EQJ_GAL_JPL(const char *filename)
|
|
{
|
|
/*
|
|
Compare against ICRS/GAL calculated by JPL Horizons tool.
|
|
We get a fairly large error of 23 arcseconds due because I
|
|
can't figure out how to make JPL Horizons use the same
|
|
aberration settings in equatorial and galactic coordinates.
|
|
*/
|
|
const double THRESHOLD_SECONDS = 23.0;
|
|
int error, lnum, nscanned;
|
|
int found_begin = 0;
|
|
int found_end = 0;
|
|
double ra, dec, glon, glat;
|
|
FILE *infile = NULL;
|
|
char line[100];
|
|
astro_rotation_t rot;
|
|
astro_time_t time;
|
|
astro_spherical_t eqj_sphere, gal_sphere;
|
|
astro_vector_t eqj_vec, gal_vec;
|
|
double dlon, dlat, diff, max_diff = 0.0;
|
|
|
|
time = Astronomy_MakeTime(2000, 1, 1, 0, 0, 0.0); /* placeholder time - value does not matter */
|
|
|
|
rot = Astronomy_Rotation_EQJ_GAL();
|
|
CHECK_STATUS(rot);
|
|
|
|
infile = fopen(filename, "rt");
|
|
if (infile == NULL)
|
|
FFAIL("Cannot open input file: %s\n", filename);
|
|
|
|
lnum = 0;
|
|
while (!found_end && ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++lnum;
|
|
if (!found_begin)
|
|
{
|
|
if (strlen(line) >= 5 && !memcmp(line, "$$SOE", 5))
|
|
found_begin = 1;
|
|
}
|
|
else if (strlen(line) >= 5 && !memcmp(line, "$$EOE", 5))
|
|
{
|
|
found_end = 1;
|
|
}
|
|
else
|
|
{
|
|
if (strlen(line) < 18)
|
|
FAIL("C Test_EQJ_GAL_JPL(%s line %d): line is too short.\n", filename, lnum);
|
|
|
|
nscanned = sscanf(line+18, "%lf %lf %lf %lf", &ra, &dec, &glon, &glat);
|
|
if (nscanned != 4)
|
|
FAIL("C Test_EQJ_GAL_JPL(%s line %d): invalid data format.\n", filename, lnum);
|
|
|
|
V(ra);
|
|
V(dec);
|
|
V(glon);
|
|
V(glat);
|
|
|
|
/* Use Astronomy Engine to do the same EQJ/GAL conversion. */
|
|
eqj_sphere.status = ASTRO_SUCCESS;
|
|
eqj_sphere.dist = 1.0;
|
|
eqj_sphere.lon = ra; /* RA is already in degrees in the JPL Horizons input file. */
|
|
eqj_sphere.lat = dec;
|
|
|
|
eqj_vec = Astronomy_VectorFromSphere(eqj_sphere, time);
|
|
CHECK_STATUS(eqj_vec);
|
|
|
|
gal_vec = Astronomy_RotateVector(rot, eqj_vec);
|
|
CHECK_STATUS(gal_vec);
|
|
|
|
gal_sphere = Astronomy_SphereFromVector(gal_vec);
|
|
CHECK_STATUS(gal_sphere);
|
|
|
|
dlat = V(gal_sphere.lat - glat);
|
|
dlon = cos(DEG2RAD * glat) * V(gal_sphere.lon - glon);
|
|
diff = V(3600.0 * sqrt(dlon*dlon + dlat*dlat)); /* error in arcseconds */
|
|
if (diff > THRESHOLD_SECONDS)
|
|
FAIL("C Test_EQJ_GAL_JPL(%s line %d): EXCESSIVE ERROR = %0.3lf arcseconds\n", filename, lnum, diff);
|
|
|
|
if (diff > max_diff)
|
|
max_diff = diff;
|
|
}
|
|
}
|
|
|
|
if (!found_begin)
|
|
FFAIL("did not find begin-data marker in: %s\n", filename);
|
|
|
|
if (!found_end)
|
|
FFAIL("did not find end-data marker in: %s\n", filename);
|
|
|
|
FDEBUG("PASS. max_diff = %0.3lf arcseconds.\n", max_diff);
|
|
error = 0;
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
static int Test_EQJ_EQD(astro_body_t body)
|
|
{
|
|
astro_time_t time;
|
|
astro_observer_t observer;
|
|
astro_equatorial_t eq2000, eqdate, eqcheck;
|
|
astro_vector_t v2000, vdate, t2000;
|
|
astro_rotation_t r;
|
|
double ra_diff, dec_diff, dist_diff, diff;
|
|
int error;
|
|
|
|
/* 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_MakeTime(2019, 12, 8, 20, 50, 0.0);
|
|
observer = Astronomy_MakeObserver(35, -85, 0);
|
|
eq2000 = Astronomy_Equator(body, &time, observer, EQUATOR_J2000, ABERRATION);
|
|
CHECK_STATUS(eq2000);
|
|
eqdate = Astronomy_Equator(body, &time, observer, EQUATOR_OF_DATE, ABERRATION);
|
|
CHECK_STATUS(eqdate);
|
|
|
|
/* Convert EQJ angular coordinates to vector. */
|
|
v2000 = eq2000.vec;
|
|
CHECK_STATUS(v2000);
|
|
|
|
/* Find rotation matrix. */
|
|
r = Astronomy_Rotation_EQJ_EQD(&time);
|
|
CHECK_ROTMAT(r);
|
|
|
|
/* Rotate EQJ vector to EQD vector. */
|
|
vdate = Astronomy_RotateVector(r, v2000);
|
|
CHECK_STATUS(vdate);
|
|
|
|
/* Convert vector back to angular coordinates. */
|
|
eqcheck = Astronomy_EquatorFromVector(vdate);
|
|
CHECK_STATUS(eqcheck);
|
|
|
|
/* Compare the result with the eqdate. */
|
|
ra_diff = ABS(eqcheck.ra - eqdate.ra);
|
|
dec_diff = ABS(eqcheck.dec - eqdate.dec);
|
|
dist_diff = ABS(eqcheck.dist - eqdate.dist);
|
|
FDEBUG("%s ra=%0.3lf, dec=%0.3lf, dist=%0.3lf, ra_diff=%lg, dec_diff=%lg, dist_diff=%lg\n", Astronomy_BodyName(body), eqdate.ra, eqdate.dec, eqdate.dist, ra_diff, dec_diff, dist_diff);
|
|
if (ra_diff > 1.0e-14 || dec_diff > 1.0e-14 || dist_diff > 4.0e-15)
|
|
FFAIL("EXCESSIVE ERROR\n");
|
|
|
|
/* Perform the inverse conversion back to equatorial J2000 coordinates. */
|
|
r = Astronomy_Rotation_EQD_EQJ(&time);
|
|
CHECK_ROTMAT(r);
|
|
t2000 = Astronomy_RotateVector(r, vdate);
|
|
CHECK_STATUS(t2000);
|
|
CHECK(VectorDiff(t2000, v2000, &diff));
|
|
FDEBUG("%s inverse diff = %lg\n", Astronomy_BodyName(body), diff);
|
|
if (diff > 5.0e-15)
|
|
FFAIL("EXCESSIVE INVERSE ERROR\n");
|
|
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
static int Test_EQD_HOR(astro_body_t body)
|
|
{
|
|
astro_time_t time;
|
|
astro_observer_t observer;
|
|
astro_equatorial_t eqd, eqj;
|
|
astro_horizon_t hor;
|
|
astro_rotation_t rot;
|
|
astro_spherical_t sphere;
|
|
astro_vector_t vec_eqd, vec_eqj, vec_hor, check_eqd, check_eqj, check_hor;
|
|
double diff_az, diff_alt, diff;
|
|
int error;
|
|
|
|
/* Use existing functions to calculate horizontal coordinates of the body for the time+observer. */
|
|
time = Astronomy_MakeTime(1970, 12, 13, 5, 15, 0.0);
|
|
observer = Astronomy_MakeObserver(-37.0, +45.0, 0.0);
|
|
CHECK_EQU(eqd, Astronomy_Equator(body, &time, observer, EQUATOR_OF_DATE, ABERRATION));
|
|
DEBUG("C Test_EQD_HOR %s: OFDATE ra=%0.3lf, dec=%0.3lf\n", Astronomy_BodyName(body), eqd.ra, eqd.dec);
|
|
hor = Astronomy_Horizon(&time, observer, eqd.ra, eqd.dec, REFRACTION_NORMAL);
|
|
|
|
/* Calculate the position of the body as an equatorial vector of date. */
|
|
CHECK_VECTOR(vec_eqd, eqd.vec);
|
|
|
|
/* Calculate rotation matrix to convert equatorial J2000 vector to horizontal vector. */
|
|
rot = Astronomy_Rotation_EQD_HOR(&time, observer);
|
|
CHECK_ROTMAT(rot);
|
|
|
|
/* Rotate the equator of date vector to a horizontal vector. */
|
|
CHECK_VECTOR(vec_hor, Astronomy_RotateVector(rot, vec_eqd));
|
|
|
|
/* Convert the horizontal vector to horizontal angular coordinates. */
|
|
sphere = Astronomy_HorizonFromVector(vec_hor, REFRACTION_NORMAL);
|
|
CHECK_STATUS(sphere);
|
|
|
|
diff_alt = ABS(sphere.lat - hor.altitude);
|
|
diff_az = ABS(sphere.lon - hor.azimuth);
|
|
|
|
DEBUG("C Test_EQD_HOR %s: trusted alt=%0.3lf, az=%0.3lf; test alt=%0.3lf, az=%0.3lf; diff_alt=%lg, diff_az=%lg\n",
|
|
Astronomy_BodyName(body), hor.altitude, hor.azimuth, sphere.lat, sphere.lon, diff_alt, diff_az);
|
|
|
|
if (diff_alt > 3.6e-14 || diff_az > 1.2e-13)
|
|
FFAIL("EXCESSIVE HORIZONTAL ERROR.\n");
|
|
|
|
/* Confirm that we can convert back to horizontal vector. */
|
|
CHECK_VECTOR(check_hor, Astronomy_VectorFromHorizon(sphere, time, REFRACTION_NORMAL));
|
|
CHECK(VectorDiff(check_hor, vec_hor, &diff));
|
|
DEBUG("C Test_EQD_HOR %s: horizontal recovery: diff = %lg\n", Astronomy_BodyName(body), diff);
|
|
if (diff > 3.0e-15)
|
|
FFAIL("EXCESSIVE ERROR IN HORIZONTAL RECOVERY.\n");
|
|
|
|
/* Verify the inverse translation from horizontal vector to equatorial of-date vector. */
|
|
rot = Astronomy_Rotation_HOR_EQD(&time, observer);
|
|
CHECK_ROTMAT(rot);
|
|
CHECK_VECTOR(check_eqd, Astronomy_RotateVector(rot, vec_hor));
|
|
CHECK(VectorDiff(check_eqd, vec_eqd, &diff));
|
|
DEBUG("C Test_EQD_HOR %s: OFDATE inverse rotation diff = %lg\n", Astronomy_BodyName(body), diff);
|
|
if (diff > 2.67e-15)
|
|
FFAIL("EXCESSIVE OFDATE INVERSE HORIZONTAL ERROR.\n");
|
|
|
|
/* Exercise HOR to EQJ translation. */
|
|
CHECK_EQU(eqj, Astronomy_Equator(body, &time, observer, EQUATOR_J2000, ABERRATION));
|
|
CHECK_VECTOR(vec_eqj, eqj.vec);
|
|
|
|
rot = Astronomy_Rotation_HOR_EQJ(&time, observer);
|
|
CHECK_ROTMAT(rot);
|
|
CHECK_VECTOR(check_eqj, Astronomy_RotateVector(rot, vec_hor));
|
|
CHECK(VectorDiff(check_eqj, vec_eqj, &diff));
|
|
DEBUG("C Test_EQD_HOR %s: J2000 inverse rotation diff = %lg\n", Astronomy_BodyName(body), diff);
|
|
if (diff > 6.0e-15)
|
|
FFAIL("EXCESSIVE J2000 INVERSE HORIZONTAL ERROR.\n");
|
|
|
|
/* Verify the inverse translation: EQJ to HOR. */
|
|
rot = Astronomy_Rotation_EQJ_HOR(&time, observer);
|
|
CHECK_ROTMAT(rot);
|
|
CHECK_VECTOR(check_hor, Astronomy_RotateVector(rot, vec_eqj));
|
|
CHECK(VectorDiff(check_hor, vec_hor, &diff));
|
|
DEBUG("C Test_EQD_HOR %s: EQJ inverse rotation diff = %lg\n", Astronomy_BodyName(body), diff);
|
|
if (diff > 5.0e-15)
|
|
FFAIL("EXCESSIVE EQJ INVERSE HORIZONTAL ERROR.\n");
|
|
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
static int Test_EQD_ECT(void)
|
|
{
|
|
int error = 1;
|
|
int count = 0;
|
|
astro_time_t time = Astronomy_MakeTime(1900, 1, 1, 0, 0, 0.0);
|
|
astro_time_t stopTime = Astronomy_MakeTime(2100, 1, 1, 0, 0, 0.0);
|
|
astro_vector_t eqj, eqd, ect, check_ect;
|
|
astro_rotation_t eqj_eqd, eqd_ect;
|
|
astro_spherical_t sphere;
|
|
double diff, max_diff = 0.0;
|
|
|
|
while (time.ut <= stopTime.ut)
|
|
{
|
|
/* Get Moon's geocentric position in EQJ. */
|
|
eqj = Astronomy_GeoMoon(time);
|
|
CHECK_STATUS(eqj);
|
|
|
|
/* Convert EQJ to EQD. */
|
|
eqj_eqd = Astronomy_Rotation_EQJ_EQD(&time);
|
|
CHECK_ROTMAT(eqj_eqd);
|
|
eqd = Astronomy_RotateVector(eqj_eqd, eqj);
|
|
CHECK_STATUS(eqd);
|
|
|
|
/* Convert EQD to ECT. */
|
|
eqd_ect = Astronomy_Rotation_EQD_ECT(&time);
|
|
CHECK_ROTMAT(eqd_ect);
|
|
ect = Astronomy_RotateVector(eqd_ect, eqd);
|
|
CHECK_STATUS(ect);
|
|
|
|
/* Independently get the Moon's spherical coordinates in ECT. */
|
|
sphere = Astronomy_EclipticGeoMoon(time);
|
|
CHECK_STATUS(sphere);
|
|
|
|
/* Convert spherical coordinates to ECT vector. */
|
|
check_ect = Astronomy_VectorFromSphere(sphere, time);
|
|
CHECK_STATUS(check_ect);
|
|
|
|
/* Verify the two ECT vectors are identical, within tolerance. */
|
|
CHECK(VectorDiff(ect, check_ect, &diff));
|
|
if (diff > max_diff) max_diff = diff;
|
|
|
|
time = Astronomy_AddDays(time, 10.0);
|
|
++count;
|
|
}
|
|
|
|
if (max_diff > 3.743e-18)
|
|
FFAIL("excessive vector diff = %0.6le AU.\n", max_diff);
|
|
|
|
FDEBUG("PASS: count = %d, max_diff = %0.6le AU\n", count, max_diff);
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
|
|
static int EclipticTest(void)
|
|
{
|
|
int error = 1;
|
|
int count = 0;
|
|
astro_time_t time = Astronomy_MakeTime(1900, 1, 1, 0, 0, 0.0);
|
|
astro_time_t stopTime = Astronomy_MakeTime(2100, 1, 1, 0, 0, 0.0);
|
|
astro_vector_t eqj, check_ect;
|
|
astro_spherical_t sphere;
|
|
astro_ecliptic_t eclip;
|
|
double diff, max_vec_diff = 0.0, max_angle_diff = 0.0;
|
|
|
|
while (time.ut <= stopTime.ut)
|
|
{
|
|
/* Get Moon's geocentric position in EQJ. */
|
|
eqj = Astronomy_GeoMoon(time);
|
|
CHECK_STATUS(eqj);
|
|
|
|
/* Convert EQJ to ECT. */
|
|
eclip = Astronomy_Ecliptic(eqj);
|
|
CHECK_STATUS(eclip);
|
|
|
|
/* Confirm that the ecliptic angles and ecliptic vector are consistent.*/
|
|
CHECK_STATUS(eclip.vec);
|
|
V(eclip.vec.x);
|
|
V(eclip.vec.y);
|
|
V(eclip.vec.z);
|
|
sphere.dist = V(Astronomy_VectorLength(eclip.vec));
|
|
sphere.lat = V(eclip.elat);
|
|
sphere.lon = V(eclip.elon);
|
|
sphere.status = ASTRO_SUCCESS;
|
|
check_ect = Astronomy_VectorFromSphere(sphere, time);
|
|
CHECK(VectorDiff(eclip.vec, check_ect, &diff));
|
|
if (diff > max_angle_diff) max_angle_diff = diff;
|
|
|
|
/* Independently get the Moon's spherical coordinates in ECT. */
|
|
sphere = Astronomy_EclipticGeoMoon(time);
|
|
CHECK_STATUS(sphere);
|
|
|
|
/* Convert spherical coordinates to ECT vector. */
|
|
check_ect = Astronomy_VectorFromSphere(sphere, time);
|
|
CHECK_STATUS(check_ect);
|
|
|
|
/* Verify the two ECT vectors are identical, within tolerance. */
|
|
CHECK(VectorDiff(eclip.vec, check_ect, &diff));
|
|
if (diff > max_vec_diff) max_vec_diff = diff;
|
|
|
|
time = Astronomy_AddDays(time, 10.0);
|
|
++count;
|
|
}
|
|
|
|
if (max_angle_diff > 3.110e-18)
|
|
FFAIL("EXCESSIVE ANGLE DIFF = %0.6le AU.\n", max_angle_diff);
|
|
|
|
if (max_vec_diff > 3.388e-18)
|
|
FFAIL("EXCESSIVE VECTOR DIFF = %0.6le AU.\n", max_vec_diff);
|
|
|
|
FPASSA("PASS: count = %d, max_vec_diff = %0.6le AU, max_angle_diff = %0.6le AU.\n", count, max_vec_diff, max_angle_diff);
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
|
|
static int CheckInverse(const char *aname, const char *bname, astro_rotation_t arot, astro_rotation_t brot)
|
|
{
|
|
int error;
|
|
astro_rotation_t crot;
|
|
astro_rotation_t identity;
|
|
|
|
CHECK(CheckRotationMatrix(__LINE__, aname, arot));
|
|
CHECK(CheckRotationMatrix(__LINE__, bname, brot));
|
|
|
|
crot = Astronomy_CombineRotation(arot, brot);
|
|
CHECK_ROTMAT(crot);
|
|
|
|
identity.status = ASTRO_SUCCESS;
|
|
identity.rot[0][0] = 1; identity.rot[1][0] = 0; identity.rot[2][0] = 0;
|
|
identity.rot[0][1] = 0; identity.rot[1][1] = 1; identity.rot[2][1] = 0;
|
|
identity.rot[0][2] = 0; identity.rot[1][2] = 0; identity.rot[2][2] = 1;
|
|
|
|
CHECK(CompareMatrices("CheckInverse", crot, identity, 2.0e-15));
|
|
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
static int CheckCycle(
|
|
const char *cyclename,
|
|
astro_rotation_t arot, astro_rotation_t brot, astro_rotation_t crot)
|
|
{
|
|
int error;
|
|
astro_rotation_t xrot;
|
|
|
|
xrot = Astronomy_CombineRotation(arot, brot);
|
|
CHECK_ROTMAT(xrot);
|
|
xrot = Astronomy_InverseRotation(xrot);
|
|
CHECK(CompareMatrices(cyclename, crot, xrot, 2.0e-15));
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
#define CHECK_CYCLE(a,b,c) CHECK(CheckCycle(("CheckCycle: " #a ", " #b ", " #c), a, b, c))
|
|
|
|
static int Test_RotRoundTrip(void)
|
|
{
|
|
int error;
|
|
astro_time_t time;
|
|
astro_observer_t observer;
|
|
astro_rotation_t eqj_ecl, ecl_eqj;
|
|
astro_rotation_t eqj_hor, hor_eqj;
|
|
astro_rotation_t eqj_eqd, eqd_eqj;
|
|
astro_rotation_t hor_eqd, eqd_hor;
|
|
astro_rotation_t eqd_ecl, ecl_eqd;
|
|
astro_rotation_t hor_ecl, ecl_hor;
|
|
astro_rotation_t eqd_ect, ect_eqd;
|
|
astro_rotation_t eqj_ect, ect_eqj;
|
|
|
|
time = Astronomy_MakeTime(2067, 5, 30, 14, 45, 0.0);
|
|
observer = Astronomy_MakeObserver(+28.0, -82.0, 0.0);
|
|
|
|
/*
|
|
In each round trip, calculate a forward rotation and a backward rotation.
|
|
Verify the two are inverse matrices.
|
|
*/
|
|
|
|
/* Round trip #1: EQJ <==> EQD. */
|
|
eqj_eqd = Astronomy_Rotation_EQJ_EQD(&time);
|
|
eqd_eqj = Astronomy_Rotation_EQD_EQJ(&time);
|
|
CHECK_INVERSE(eqj_eqd, eqd_eqj);
|
|
|
|
/* Round trip #2: EQJ <==> ECL. */
|
|
eqj_ecl = Astronomy_Rotation_EQJ_ECL();
|
|
ecl_eqj = Astronomy_Rotation_ECL_EQJ();
|
|
CHECK_INVERSE(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);
|
|
CHECK_INVERSE(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);
|
|
CHECK_INVERSE(eqd_hor, hor_eqd);
|
|
|
|
/* Round trip #5: EQD <==> ECL. */
|
|
eqd_ecl = Astronomy_Rotation_EQD_ECL(&time);
|
|
ecl_eqd = Astronomy_Rotation_ECL_EQD(&time);
|
|
CHECK_INVERSE(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);
|
|
CHECK_INVERSE(hor_ecl, ecl_hor);
|
|
|
|
/* Round trip #7: EQD <==> ECT. */
|
|
eqd_ect = Astronomy_Rotation_EQD_ECT(&time);
|
|
ect_eqd = Astronomy_Rotation_ECT_EQD(&time);
|
|
CHECK_INVERSE(eqd_ect, ect_eqd);
|
|
|
|
/* Round trip #8: EQJ <==> ECT. */
|
|
eqj_ect = Astronomy_Rotation_EQJ_ECT(&time);
|
|
ect_eqj = Astronomy_Rotation_ECT_EQJ(&time);
|
|
CHECK_INVERSE(eqj_ect, ect_eqj);
|
|
|
|
/*
|
|
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).
|
|
*/
|
|
CHECK_CYCLE(eqj_ecl, ecl_eqd, eqd_eqj);
|
|
CHECK_CYCLE(eqj_hor, hor_ecl, ecl_eqj);
|
|
CHECK_CYCLE(eqj_hor, hor_eqd, eqd_eqj);
|
|
CHECK_CYCLE(ecl_eqd, eqd_hor, hor_ecl);
|
|
CHECK_CYCLE(eqj_eqd, eqd_ect, ect_eqj);
|
|
|
|
FDEBUG("PASS\n");
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
static int RotationTest(void)
|
|
{
|
|
int error;
|
|
CHECK(Rotation_MatrixInverse());
|
|
CHECK(Rotation_MatrixMultiply());
|
|
CHECK(Rotation_Pivot());
|
|
|
|
/* Verify conversion of spherical coordinates to vector. */
|
|
CHECK(TestVectorFromAngles(0.0, 0.0, 1.0, 0.0, 0.0));
|
|
CHECK(TestVectorFromAngles(0.0, 90.0, 0.0, 1.0, 0.0));
|
|
CHECK(TestVectorFromAngles(0.0, 180.0, -1.0, 0.0, 0.0));
|
|
CHECK(TestVectorFromAngles(0.0, 270.0, 0.0, -1.0, 0.0));
|
|
CHECK(TestVectorFromAngles(+90.0, 0.0, 0.0, 0.0, 1.0));
|
|
CHECK(TestVectorFromAngles(-90.0, 0.0, 0.0, 0.0, -1.0));
|
|
CHECK(TestVectorFromAngles(-30.0, +60.0, 0.43301270189221946, 0.75, -0.5));
|
|
|
|
/* Verify conversion of vector to spherical coordinates. */
|
|
CHECK(TestAnglesFromVector(0.0, 0.0, 1.0, 0.0, 0.0));
|
|
CHECK(TestAnglesFromVector(0.0, 90.0, 0.0, 1.0, 0.0));
|
|
CHECK(TestAnglesFromVector(0.0, 180.0, -1.0, 0.0, 0.0));
|
|
CHECK(TestAnglesFromVector(0.0, 270.0, 0.0, -1.0, 0.0));
|
|
CHECK(TestAnglesFromVector(+90.0, 0.0, 0.0, 0.0, 1.0));
|
|
CHECK(TestAnglesFromVector(-90.0, 0.0, 0.0, 0.0, -1.0));
|
|
CHECK(TestAnglesFromVector(-30.0, +60.0, 0.43301270189221946, 0.75, -0.5));
|
|
|
|
/* Verify rotation of vectors */
|
|
CHECK(TestSpin(0.0, 0.0, 90.0, +1, +2, +3, -2, +1, +3));
|
|
CHECK(TestSpin(0.0, 0.0, 0.0, 1.0, 2.0, -3.0, 1.0, 2.0, -3.0));
|
|
CHECK(TestSpin(90.0, 0.0, 0.0, +1, +2, +3, +1, -3, +2));
|
|
CHECK(TestSpin(0.0, 90.0, 0.0, +1, +2, +3, +3, +2, -1));
|
|
CHECK(TestSpin(180.0, 180.0, 180.0, +1, +2, +3, +1, +2, +3));
|
|
CHECK(TestSpin(0.0, 0.0, -45.0, +1, 0, 0, +0.7071067811865476, -0.7071067811865476, 0));
|
|
|
|
CHECK(Test_EQJ_GAL_NOVAS("temp/galeqj.txt"));
|
|
CHECK(Test_EQJ_GAL_JPL("galactic/mars.txt"));
|
|
|
|
CHECK(Test_EQJ_EQD(BODY_MERCURY));
|
|
CHECK(Test_EQJ_EQD(BODY_VENUS));
|
|
CHECK(Test_EQJ_EQD(BODY_MARS));
|
|
CHECK(Test_EQJ_EQD(BODY_JUPITER));
|
|
CHECK(Test_EQJ_EQD(BODY_SATURN));
|
|
|
|
CHECK(Test_EQD_HOR(BODY_MERCURY));
|
|
CHECK(Test_EQD_HOR(BODY_VENUS));
|
|
CHECK(Test_EQD_HOR(BODY_MARS));
|
|
CHECK(Test_EQD_HOR(BODY_JUPITER));
|
|
CHECK(Test_EQD_HOR(BODY_SATURN));
|
|
|
|
CHECK(Test_EQD_ECT());
|
|
|
|
CHECK(Test_RotRoundTrip());
|
|
|
|
FPASS();
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
static int VectorDiff(astro_vector_t a, astro_vector_t b, double *diff)
|
|
{
|
|
double dx, dy, dz;
|
|
|
|
*diff = NAN;
|
|
|
|
if (a.status != ASTRO_SUCCESS)
|
|
FAILRET("C %s: ERROR - first vector has status %d\n", __func__, a.status);
|
|
|
|
if (b.status != ASTRO_SUCCESS)
|
|
FAILRET("C %s: ERROR - second vector has status %d\n", __func__, b.status);
|
|
|
|
dx = a.x - b.x;
|
|
dy = a.y - b.y;
|
|
dz = a.z - b.z;
|
|
*diff = V(sqrt(dx*dx + dy*dy + dz*dz));
|
|
return 0;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int RefractionTest(void)
|
|
{
|
|
int error;
|
|
double alt, corrected, refr, inv_refr, check_alt, diff;
|
|
|
|
for (alt = -90.1; alt <= +90.1; alt += 0.001)
|
|
{
|
|
refr = Astronomy_Refraction(REFRACTION_NORMAL, alt);
|
|
corrected = alt + refr;
|
|
inv_refr = Astronomy_InverseRefraction(REFRACTION_NORMAL, corrected);
|
|
check_alt = corrected + inv_refr;
|
|
diff = ABS(check_alt - alt);
|
|
if (diff > 2.0e-14)
|
|
FFAIL("EXCESSIVE diff: alt=%8.3lf, refr=%10.6lf, diff=%lg\n", alt, refr, diff);
|
|
}
|
|
|
|
FPASS();
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int ConstellationTest(void)
|
|
{
|
|
int error = 1;
|
|
FILE *infile = NULL;
|
|
const char *inFileName = "constellation/test_input.txt";
|
|
char line[100];
|
|
double ra, dec;
|
|
char symbol[4];
|
|
int lnum, failcount, id;
|
|
astro_constellation_t constel;
|
|
|
|
infile = fopen(inFileName, "rt");
|
|
if (infile == NULL)
|
|
FFAIL("Cannot open input file: %s\n", inFileName);
|
|
|
|
lnum = 0;
|
|
failcount = 0;
|
|
while (ReadLine(line, sizeof(line), infile, inFileName, lnum))
|
|
{
|
|
++lnum;
|
|
if (4 != sscanf(line, "%d %lf %lf %3s", &id, &ra, &dec, symbol) || 3 != strlen(symbol))
|
|
FFAIL("Invalid test data in %s line %d\n", inFileName, lnum);
|
|
|
|
constel = Astronomy_Constellation(ra, dec);
|
|
if (constel.status != ASTRO_SUCCESS)
|
|
FFAIL("FAILED star %d with status %d: %s line %d\n", id, constel.status, inFileName, lnum);
|
|
|
|
if (constel.symbol == NULL || constel.name == NULL)
|
|
FFAIL("UNEXPECTED NULL name/symbol: star %d, %s line %d\n", id, inFileName, lnum);
|
|
|
|
if (strcmp(constel.symbol, symbol))
|
|
{
|
|
fprintf(stderr, "Star %6d: expected %s, found %s at B1875 RA=%10.6lf, DEC=%10.6lf\n", id, symbol, constel.symbol, constel.ra_1875, constel.dec_1875);
|
|
++failcount;
|
|
}
|
|
}
|
|
|
|
if (failcount > 0)
|
|
FFAIL("%d failures\n", failcount);
|
|
|
|
FPASSA("(verified %d)\n", lnum);
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static void PrintTime(astro_time_t time)
|
|
{
|
|
astro_utc_t utc = Astronomy_UtcFromTime(time);
|
|
printf("%04d-%02d-%02dT%02d:%02d:%06.3lfZ", utc.year, utc.month, utc.day, utc.hour, utc.minute, utc.second);
|
|
}
|
|
|
|
static int LunarEclipseIssue78(void)
|
|
{
|
|
int error;
|
|
astro_lunar_eclipse_t eclipse;
|
|
astro_time_t search_time = Astronomy_MakeTime(2020, 12, 19, 0, 0, 0.0);
|
|
astro_time_t expected_peak = Astronomy_MakeTime(2021, 5, 26, 11, 18, 42); /* https://www.timeanddate.com/eclipse/lunar/2021-may-26 */
|
|
double dt_seconds;
|
|
|
|
eclipse = Astronomy_SearchLunarEclipse(search_time);
|
|
CHECK_STATUS(eclipse);
|
|
|
|
dt_seconds = (24.0 * 3600.0) * ABS(eclipse.peak.tt - expected_peak.tt);
|
|
if (dt_seconds > 40.0)
|
|
FFAIL("Excessive prediction error = %lf seconds.\n", dt_seconds);
|
|
|
|
if (eclipse.kind != ECLIPSE_TOTAL)
|
|
FFAIL("Expected total eclipse; found %d\n", eclipse.kind);
|
|
|
|
FPASS();
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
static int LunarEclipseTest(void)
|
|
{
|
|
const char *filename = "eclipse/lunar_eclipse.txt";
|
|
const char *statsFileName = "eclipse/c_le_stats.csv";
|
|
FILE *infile = NULL;
|
|
FILE *outfile = NULL;
|
|
int lnum = 0;
|
|
int error = 1;
|
|
char line[100];
|
|
astro_time_t peak_time;
|
|
astro_lunar_eclipse_t eclipse;
|
|
double partial_minutes, total_minutes;
|
|
double diff_minutes, max_diff_minutes = 0.0, sum_diff_minutes = 0.0;
|
|
int diff_count = 0;
|
|
double diff_days;
|
|
int sd_valid = 0;
|
|
int frac_valid = 0;
|
|
int skip_count = 0;
|
|
const double diff_limit = 2.0;
|
|
extern int _CalcMoonCount; /* incremented by Astronomy Engine every time expensive CalcMoon() is called */
|
|
|
|
_CalcMoonCount = 0;
|
|
|
|
infile = fopen(filename, "rt");
|
|
if (infile == NULL)
|
|
FFAIL("Cannot open input file: %s\n", filename);
|
|
|
|
outfile = fopen(statsFileName, "wt");
|
|
if (outfile == NULL)
|
|
FFAIL("Cannot open output stats file: %s\n", statsFileName);
|
|
|
|
eclipse = Astronomy_SearchLunarEclipse(Astronomy_MakeTime(1701, 1, 1, 0, 0, 0.0));
|
|
CHECK_STATUS(eclipse);
|
|
|
|
fprintf(outfile, "\"utc\",\"center\",\"partial\",\"total\"\n");
|
|
while (ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++lnum;
|
|
|
|
/* Make sure numeric data are finite numbers. */
|
|
V(eclipse.obscuration);
|
|
V(eclipse.sd_partial);
|
|
V(eclipse.sd_penum);
|
|
V(eclipse.sd_total);
|
|
|
|
/* scan test data */
|
|
|
|
/* 2021-05-26T11:19Z 94 9 */
|
|
if (strlen(line) < 17)
|
|
FAIL("C LunarEclipseTest(%s line %d): line is too short.\n", filename, lnum);
|
|
|
|
line[17] = '\0';
|
|
CHECK(ParseDate(line, &peak_time));
|
|
if (2 != sscanf(line+18, "%lf %lf", &partial_minutes, &total_minutes))
|
|
FAIL("C LunarEclipseTest(%s line %d): invalid data format.\n", filename, lnum);
|
|
|
|
/* Verify that the calculated eclipse semi-durations are consistent with the kind. */
|
|
/* Verify that obscuration values also make sense for the kind. */
|
|
|
|
switch (eclipse.kind)
|
|
{
|
|
case ECLIPSE_PENUMBRAL:
|
|
sd_valid = (eclipse.sd_penum > 0.0) && (eclipse.sd_partial == 0.0) && (eclipse.sd_total == 0.0);
|
|
frac_valid = (eclipse.obscuration == 0.0);
|
|
break;
|
|
|
|
case ECLIPSE_PARTIAL:
|
|
sd_valid = (eclipse.sd_penum > 0.0) && (eclipse.sd_partial > 0.0) && (eclipse.sd_total == 0.0);
|
|
frac_valid = (eclipse.obscuration > 0.0) && (eclipse.obscuration < 1.0);
|
|
break;
|
|
|
|
case ECLIPSE_TOTAL:
|
|
sd_valid = (eclipse.sd_penum > 0.0) && (eclipse.sd_partial > 0.0) && (eclipse.sd_total > 0.0);
|
|
frac_valid = (eclipse.obscuration == 1.0);
|
|
break;
|
|
|
|
default:
|
|
FAIL("C LunarEclipseTest(%s line %d): invalid eclipse kind %d.\n", filename, lnum, eclipse.kind);
|
|
}
|
|
|
|
if (!sd_valid)
|
|
{
|
|
FAIL("C LunarEclipseTest(%s line %d): invalid semiduration(s) for kind %d: penum=%lf, partial=%lf, total=%lf.\n",
|
|
filename, lnum, eclipse.kind, eclipse.sd_penum, eclipse.sd_partial, eclipse.sd_total);
|
|
}
|
|
|
|
if (!frac_valid)
|
|
{
|
|
FAIL("C LunarEclipseTest(%s line %d): invalid obscuration = %0.16lf for kind %d\n",
|
|
filename, lnum, eclipse.obscuration, eclipse.kind);
|
|
}
|
|
|
|
/* 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 && diff_days > 20.0)
|
|
{
|
|
++skip_count;
|
|
continue;
|
|
}
|
|
|
|
fprintf(outfile, "\"%s\",%lf,%lf,%lf\n",
|
|
line, /* was already truncated at offset [17] above, so now has only UTC */
|
|
diff_days * (24.0 * 60.0),
|
|
eclipse.sd_partial - partial_minutes,
|
|
eclipse.sd_total - total_minutes
|
|
);
|
|
|
|
diff_minutes = (24.0 * 60.0) * ABS(diff_days);
|
|
sum_diff_minutes += diff_minutes;
|
|
++diff_count;
|
|
|
|
if (diff_minutes > diff_limit)
|
|
{
|
|
printf("C LunarEclipseTest expected peak: ");
|
|
PrintTime(peak_time);
|
|
printf("\n");
|
|
printf("C LunarEclipseTest found peak: ");
|
|
PrintTime(eclipse.peak);
|
|
printf("\n");
|
|
FAIL("C LunarEclipseTest(%s line %d): EXCESSIVE peak time error = %lf minutes (%lf days).\n", filename, lnum, diff_minutes, diff_days);
|
|
}
|
|
|
|
if (diff_minutes > max_diff_minutes)
|
|
max_diff_minutes = diff_minutes;
|
|
|
|
/* check partial eclipse duration */
|
|
|
|
diff_minutes = ABS(partial_minutes - eclipse.sd_partial);
|
|
sum_diff_minutes += diff_minutes;
|
|
++diff_count;
|
|
|
|
if (diff_minutes > diff_limit)
|
|
FAIL("C LunarEclipseTest(%s line %d): EXCESSIVE partial eclipse semiduration error: %lf minutes\n", filename, lnum, diff_minutes);
|
|
|
|
if (diff_minutes > max_diff_minutes)
|
|
max_diff_minutes = diff_minutes;
|
|
|
|
/* check total eclipse duration */
|
|
|
|
diff_minutes = ABS(total_minutes - eclipse.sd_total);
|
|
sum_diff_minutes += diff_minutes;
|
|
++diff_count;
|
|
|
|
if (diff_minutes > diff_limit)
|
|
FAIL("C LunarEclipseTest(%s line %d): EXCESSIVE total eclipse semiduration error: %lf minutes\n", filename, lnum, diff_minutes);
|
|
|
|
if (diff_minutes > max_diff_minutes)
|
|
max_diff_minutes = diff_minutes;
|
|
|
|
/* calculate for next iteration */
|
|
|
|
eclipse = Astronomy_NextLunarEclipse(eclipse.peak);
|
|
if (eclipse.status != ASTRO_SUCCESS)
|
|
FAIL("C LunarEclipseTest(%s line %d): Astronomy_NextLunarEclipse returned status %d\n", filename, lnum, eclipse.status);
|
|
}
|
|
|
|
sum_diff_minutes /= diff_count; /* convert to average error in minutes */
|
|
if (sum_diff_minutes > 0.274)
|
|
FFAIL("EXCESSIVE AVERAGE TIME ERROR: %lf\n", sum_diff_minutes);
|
|
|
|
if (skip_count > 9)
|
|
FFAIL("Skipped too many penumbral eclipses: %d\n", skip_count);
|
|
|
|
FPASSA("(verified %d, skipped %d, %d CalcMoons, max_diff_minutes = %0.3lf, avg_diff_minutes = %0.3lf)\n", lnum, skip_count, _CalcMoonCount, max_diff_minutes, sum_diff_minutes);
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
if (outfile != NULL) fclose(outfile);
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int LunarFractionCase(int year, int month, int day, double obscuration)
|
|
{
|
|
int error;
|
|
astro_time_t time;
|
|
astro_lunar_eclipse_t eclipse;
|
|
double dt, diff;
|
|
|
|
/* Search for the first lunar eclipse to occur after the given date. */
|
|
/* It should always happen within 24 hours of the given date. */
|
|
time = Astronomy_MakeTime(year, month, day, 0, 0, 0.0);
|
|
eclipse = Astronomy_SearchLunarEclipse(time);
|
|
CHECK_STATUS(eclipse);
|
|
|
|
if (eclipse.kind != ECLIPSE_PARTIAL)
|
|
FAIL("C LunarFractionCase(%04d-%02d-%02d): expected partial eclipse, but found %d.\n", year, month, day, eclipse.kind);
|
|
|
|
dt = V(eclipse.peak.ut - time.ut);
|
|
if (dt < 0.0 || dt > 1.0)
|
|
FAIL("C LunarFractionCase(%04d-%02d-%02d): eclipse occurs %0.4lf days after predicted date.\n", year, month, day, dt);
|
|
|
|
diff = V(eclipse.obscuration - obscuration);
|
|
if (ABS(diff) > 0.00763)
|
|
FAIL("C LunarFractionCase(%04d-%02d-%02d) FAIL: obscuration error = %0.8lf, expected = %0.3lf, actual = %0.8lf\n", year, month, day, diff, obscuration, eclipse.obscuration);
|
|
DEBUG("C LunarFractionCase(%04d-%02d-%02d) fraction error = %0.8lf\n", year, month, day, diff);
|
|
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
|
|
static int LunarFractionTest(void)
|
|
{
|
|
int error;
|
|
|
|
/* Verify calculation of the fraction of the Moon's disc covered by the Earth's umbra during a partial eclipse. */
|
|
/* Data for this is more tedious to gather, because Espenak data does not contain it. */
|
|
/* We already verify fraction=0.0 for penumbral eclipses and fraction=1.0 for total eclipses in LunarEclipseTest. */
|
|
|
|
CHECK(LunarFractionCase(2010, 6, 26, 0.506)); /* https://www.timeanddate.com/eclipse/lunar/2010-june-26 */
|
|
CHECK(LunarFractionCase(2012, 6, 4, 0.304)); /* https://www.timeanddate.com/eclipse/lunar/2012-june-4 */
|
|
CHECK(LunarFractionCase(2013, 4, 25, 0.003)); /* https://www.timeanddate.com/eclipse/lunar/2013-april-25 */
|
|
CHECK(LunarFractionCase(2017, 8, 7, 0.169)); /* https://www.timeanddate.com/eclipse/lunar/2017-august-7 */
|
|
CHECK(LunarFractionCase(2019, 7, 16, 0.654)); /* https://www.timeanddate.com/eclipse/lunar/2019-july-16 */
|
|
CHECK(LunarFractionCase(2021, 11, 19, 0.991)); /* https://www.timeanddate.com/eclipse/lunar/2021-november-19 */
|
|
CHECK(LunarFractionCase(2023, 10, 28, 0.060)); /* https://www.timeanddate.com/eclipse/lunar/2023-october-28 */
|
|
CHECK(LunarFractionCase(2024, 9, 18, 0.035)); /* https://www.timeanddate.com/eclipse/lunar/2024-september-18 */
|
|
CHECK(LunarFractionCase(2026, 8, 28, 0.962)); /* https://www.timeanddate.com/eclipse/lunar/2026-august-28 */
|
|
CHECK(LunarFractionCase(2028, 1, 12, 0.024)); /* https://www.timeanddate.com/eclipse/lunar/2028-january-12 */
|
|
CHECK(LunarFractionCase(2028, 7, 6, 0.325)); /* https://www.timeanddate.com/eclipse/lunar/2028-july-6 */
|
|
CHECK(LunarFractionCase(2030, 6, 15, 0.464)); /* https://www.timeanddate.com/eclipse/lunar/2030-june-15 */
|
|
|
|
FPASS();
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int GlobalSolarEclipseTest(void)
|
|
{
|
|
const int expected_count = 1180;
|
|
int error = 1;
|
|
FILE *infile = NULL;
|
|
const char *inFileName = "eclipse/solar_eclipse.txt";
|
|
int lnum;
|
|
char line[100];
|
|
char typeChar;
|
|
double deltaT, lat, lon;
|
|
astro_time_t peak;
|
|
astro_global_solar_eclipse_t eclipse;
|
|
astro_eclipse_kind_t expected_kind;
|
|
double diff_days, diff_minutes, max_minutes=0.0;
|
|
double diff_angle, max_angle=0.0;
|
|
int skip_count = 0;
|
|
extern int _CalcMoonCount; /* incremented by Astronomy Engine every time expensive CalcMoon() is called */
|
|
|
|
_CalcMoonCount = 0;
|
|
|
|
infile = fopen(inFileName, "rt");
|
|
if (infile == NULL)
|
|
FFAIL("Cannot open input file: %s\n", inFileName);
|
|
|
|
eclipse = Astronomy_SearchGlobalSolarEclipse(Astronomy_MakeTime(1701, 1, 1, 0, 0, 0.0));
|
|
|
|
lnum = 0;
|
|
while (ReadLine(line, sizeof(line), infile, inFileName, lnum))
|
|
{
|
|
++lnum;
|
|
|
|
if (eclipse.status != ASTRO_SUCCESS)
|
|
FAIL("C GlobalSolarEclipseTest(%s line %d): error %d searching for solar eclipse.\n", inFileName, lnum, eclipse.status);
|
|
|
|
/* 1889-12-22T12:54:15Z -6 T -12.7 -12.8 */
|
|
if (strlen(line) < 20)
|
|
FAIL("C GlobalSolarEclipseTest(%s line %d): line is too short.\n", inFileName, lnum);
|
|
|
|
line[20] = '\0'; /* terminate the date/time string */
|
|
if (ParseDate(line, &peak))
|
|
FAIL("C GlobalSolarEclipseTest(%s line %d): invalid date/time format: '%s'\n", inFileName, lnum, line);
|
|
|
|
if (4 != sscanf(line+21, "%lf %c %lf %lf", &deltaT, &typeChar, &lat, &lon))
|
|
FAIL("C GlobalSolarEclipseTest(%s line %d): invalid data format.\n", inFileName, lnum);
|
|
|
|
/*
|
|
The test data allows for "hybrid" eclipses, which means that some
|
|
observers see a total eclipse, others see an annular eclipse.
|
|
The global solar eclipse predictor determines the eclipse kind
|
|
for the peak observer only. Therefore, hybrid in the test data
|
|
matches a total eclipse from our predictor.
|
|
*/
|
|
switch (typeChar)
|
|
{
|
|
case 'P': expected_kind = ECLIPSE_PARTIAL; break;
|
|
case 'A': expected_kind = ECLIPSE_ANNULAR; break;
|
|
case 'T': expected_kind = ECLIPSE_TOTAL; break;
|
|
case 'H': expected_kind = ECLIPSE_TOTAL; break;
|
|
default:
|
|
FAIL("C GlobalSolarEclipseTest(%s line %d): invalid eclipse kind in test data.\n", inFileName, lnum);
|
|
}
|
|
|
|
diff_days = V(eclipse.peak.ut - peak.ut);
|
|
|
|
/* 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 && eclipse.distance > 9000.0)
|
|
{
|
|
++skip_count;
|
|
eclipse = Astronomy_NextGlobalSolarEclipse(eclipse.peak);
|
|
CHECK_STATUS(eclipse);
|
|
diff_days = V(eclipse.peak.ut - peak.ut);
|
|
}
|
|
|
|
/* Validate the eclipse prediction. */
|
|
diff_minutes = (24 * 60) * ABS(diff_days);
|
|
if (diff_minutes > 7.56)
|
|
{
|
|
printf("Expected: ");
|
|
PrintTime(peak);
|
|
printf("\nFound: ");
|
|
PrintTime(eclipse.peak);
|
|
printf("\n");
|
|
FAIL("C GlobalSolarEclipseTest(%s line %d): EXCESSIVE TIME ERROR = %0.2lf minutes\n", inFileName, lnum, diff_minutes);
|
|
}
|
|
|
|
if (diff_minutes > max_minutes)
|
|
max_minutes = diff_minutes;
|
|
|
|
/* Validate the eclipse kind, but only when it is not a "glancing" eclipse. */
|
|
if ((V(eclipse.distance) < 6360) && (eclipse.kind != expected_kind))
|
|
FAIL("C GlobalSolarEclipseTest(%s line %d): WRONG ECLIPSE KIND: expected %d, found %d\n", inFileName, lnum, expected_kind, eclipse.kind);
|
|
|
|
if (eclipse.kind == ECLIPSE_TOTAL || eclipse.kind == ECLIPSE_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)
|
|
FAIL("C GlobalSolarEclipseTest(%s line %d): EXCESSIVE GEOGRAPHIC LOCATION ERROR = %0.6lf degrees\n", inFileName, lnum, diff_angle);
|
|
if (diff_angle > max_angle)
|
|
max_angle = diff_angle;
|
|
}
|
|
}
|
|
|
|
/* Verify that the obscuration value is consistent with the eclipse kind. */
|
|
switch (eclipse.kind)
|
|
{
|
|
case ECLIPSE_PARTIAL:
|
|
if (!isnan(eclipse.obscuration))
|
|
FAIL("C GlobalSolarEclipseTest(%s line %d): expected NAN obscuration for partial eclipse, but found %lf\n", inFileName, lnum, eclipse.obscuration);
|
|
break;
|
|
|
|
case ECLIPSE_ANNULAR:
|
|
if (!isfinite(eclipse.obscuration))
|
|
FAIL("C GlobalSolarEclipseTest(%s line %d): expected finite obscuration for annular eclipse.\n", inFileName, lnum);
|
|
if (eclipse.obscuration < 0.8 || eclipse.obscuration >= 1.0)
|
|
FAIL("C GlobalSolarEclipseTest(%s line %d): obscuration = %0.6lf is out of range for annular eclipse.\n", inFileName, lnum, eclipse.obscuration);
|
|
break;
|
|
|
|
case ECLIPSE_TOTAL:
|
|
if (!isfinite(eclipse.obscuration))
|
|
FAIL("C GlobalSolarEclipseTest(%s line %d): expected finite obscuration for total eclipse.\n", inFileName, lnum);
|
|
if (eclipse.obscuration != 1.0)
|
|
FAIL("C GlobalSolarEclipseTest(%s line %d): obscuration = %0.6lf should have been 1.0 for total eclipse.\n", inFileName, lnum, eclipse.obscuration);
|
|
break;
|
|
|
|
default:
|
|
FAIL("C GlobalSolarEclipseTest(%s line %d): Unhandled eclipse kind %d for obscuration check.\n", inFileName, lnum, eclipse.kind);
|
|
}
|
|
|
|
eclipse = Astronomy_NextGlobalSolarEclipse(eclipse.peak);
|
|
}
|
|
|
|
if (lnum != expected_count)
|
|
FFAIL("WRONG LINE COUNT = %d, expected %d\n", lnum, expected_count);
|
|
|
|
if (skip_count > 2)
|
|
FFAIL("EXCESSIVE SKIP COUNT = %d\n", skip_count);
|
|
|
|
FPASSA("(%d verified, %d skipped, %d CalcMoons, max minutes = %0.3lf, max angle = %0.3lf)\n", lnum, skip_count, _CalcMoonCount, max_minutes, max_angle);
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static void VectorFromAngles(double v[3], double lat, double lon)
|
|
{
|
|
double coslat = cos(DEG2RAD * lat);
|
|
v[0] = cos(DEG2RAD * lon) * coslat;
|
|
v[1] = sin(DEG2RAD * lon) * coslat;
|
|
v[2] = sin(DEG2RAD * lat);
|
|
}
|
|
|
|
|
|
static double AngleDiff(double alat, double alon, double blat, double blon)
|
|
{
|
|
double a[3];
|
|
double b[3];
|
|
double dot;
|
|
|
|
/* Convert angles to vectors on a unit sphere. */
|
|
VectorFromAngles(a, alat, alon);
|
|
VectorFromAngles(b, blat, blon);
|
|
|
|
dot = V(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(RAD2DEG * acos(dot));
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int LocalSolarEclipseTest(void)
|
|
{
|
|
int error;
|
|
CHECK(LocalSolarEclipseTest1());
|
|
CHECK(LocalSolarEclipseTest2());
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
static int LocalSolarEclipseTest1(void)
|
|
{
|
|
/*
|
|
Re-use the test data for global solar eclipses, only feed the given coordinates
|
|
into the local solar eclipse predictor as the observer's location.
|
|
In each case, start the search 20 days before the expected eclipse.
|
|
Then verify that the peak time and eclipse type is correct in each case.
|
|
*/
|
|
|
|
int error = 1;
|
|
FILE *infile = NULL;
|
|
const char *inFileName = "eclipse/solar_eclipse.txt";
|
|
int lnum;
|
|
char line[100];
|
|
char typeChar;
|
|
double deltaT;
|
|
astro_observer_t observer;
|
|
astro_time_t peak, search_start;
|
|
astro_local_solar_eclipse_t eclipse;
|
|
double diff_days, diff_minutes, max_minutes=0.0;
|
|
int skip_count = 0;
|
|
extern int _CalcMoonCount; /* incremented by Astronomy Engine every time expensive CalcMoon() is called */
|
|
|
|
_CalcMoonCount = 0;
|
|
|
|
infile = fopen(inFileName, "rt");
|
|
if (infile == NULL)
|
|
FFAIL("Cannot open input file: %s\n", inFileName);
|
|
|
|
lnum = 0;
|
|
observer.height = 0.0;
|
|
while (ReadLine(line, sizeof(line), infile, inFileName, lnum))
|
|
{
|
|
++lnum;
|
|
|
|
/* 1889-12-22T12:54:15Z -6 T -12.7 -12.8 */
|
|
if (strlen(line) < 20)
|
|
FAIL("C LocalSolarEclipseTest1(%s line %d): line is too short.\n", inFileName, lnum);
|
|
|
|
line[20] = '\0'; /* terminate the date/time string */
|
|
if (ParseDate(line, &peak))
|
|
FAIL("C LocalSolarEclipseTest1(%s line %d): invalid date/time format: '%s'\n", inFileName, lnum, line);
|
|
|
|
if (4 != sscanf(line+21, "%lf %c %lf %lf", &deltaT, &typeChar, &observer.latitude, &observer.longitude))
|
|
FAIL("C LocalSolarEclipseTest1(%s line %d): invalid data format.\n", inFileName, lnum);
|
|
|
|
/* Start the search 20 days before we know the eclipse should peak. */
|
|
search_start = Astronomy_AddDays(peak, -20.0);
|
|
eclipse = Astronomy_SearchLocalSolarEclipse(search_start, observer);
|
|
if (eclipse.status != ASTRO_SUCCESS)
|
|
FAIL("C LocalSolarEclipseTest1(%s line %d): Astronomy_SearchLocalSolarEclipse returned %d\n", inFileName, lnum, eclipse.status);
|
|
|
|
/* Validate the predicted eclipse peak time. */
|
|
diff_days = eclipse.peak.time.ut - peak.ut;
|
|
if (diff_days > 20.0)
|
|
{
|
|
++skip_count;
|
|
continue;
|
|
}
|
|
|
|
diff_minutes = (24 * 60) * ABS(diff_days);
|
|
if (diff_minutes > 7.737)
|
|
{
|
|
printf("Expected: ");
|
|
PrintTime(peak);
|
|
printf("\nFound: ");
|
|
PrintTime(eclipse.peak.time);
|
|
printf(" ut=%0.6lf, tt=%0.6lf\n", eclipse.peak.time.ut, eclipse.peak.time.tt);
|
|
FAIL("C LocalSolarEclipseTest1(%s line %d): EXCESSIVE TIME ERROR = %0.6lf minutes\n", inFileName, lnum, diff_minutes);
|
|
}
|
|
|
|
if (diff_minutes > max_minutes)
|
|
max_minutes = diff_minutes;
|
|
|
|
/* Verify obscuration makes sense for this kind of eclipse. */
|
|
|
|
if (!isfinite(eclipse.obscuration))
|
|
FAIL("C LocalSolarEclipseTest1(%s line %d): obscuration is not finite\n", inFileName, lnum);
|
|
|
|
switch (eclipse.kind)
|
|
{
|
|
case ECLIPSE_ANNULAR:
|
|
case ECLIPSE_PARTIAL:
|
|
if (eclipse.obscuration <= 0.0 || eclipse.obscuration >= 1.0)
|
|
FAIL("C LocalSolarEclipseTest1(%s line %d): eclipse kind %d has invalid obscuration = %0.8lf\n", inFileName, lnum, eclipse.kind, eclipse.obscuration);
|
|
break;
|
|
|
|
case ECLIPSE_TOTAL:
|
|
if (eclipse.obscuration != 1.0)
|
|
FAIL("C LocalSolarEclipseTest1(%s line %d): total eclipse has invalid obscuration = %0.8lf\n", inFileName, lnum, eclipse.obscuration);
|
|
break;
|
|
|
|
default:
|
|
FAIL("C LocalSolarEclipseTest1(%s line %d): Invalid eclipse kind %d\n", inFileName, lnum, eclipse.kind);
|
|
}
|
|
}
|
|
|
|
if (skip_count > 6)
|
|
FFAIL("EXCESSIVE SKIP COUNT %d\n", skip_count);
|
|
|
|
FPASSA("(%d verified, %d skipped, %d CalcMoons, max minutes = %0.3lf)\n", lnum, skip_count, _CalcMoonCount, max_minutes);
|
|
error = 0;
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int ParseEvent(const char *str, int required, astro_time_t *time)
|
|
{
|
|
if (required)
|
|
return ParseDate(str, time);
|
|
|
|
if (strcmp(str, "-"))
|
|
{
|
|
fprintf(stderr, "ParseEvent: Expected '-' but found '%s'\n", str);
|
|
return 1;
|
|
}
|
|
|
|
time->ut = time->tt = NAN;
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int IgnoreLine(char *line)
|
|
{
|
|
int col, empty;
|
|
|
|
/* Treat '#' as a comment character. */
|
|
empty = 1;
|
|
for (col=0; line[col] != '\0' && line[col] != '#'; ++col)
|
|
if (!isspace(line[col]))
|
|
empty = 0;
|
|
|
|
line[col] = '\0';
|
|
return empty;
|
|
}
|
|
|
|
|
|
static int CheckEvent(
|
|
const char *inFileName,
|
|
int lnum,
|
|
const char *name,
|
|
astro_time_t expected_time,
|
|
double expected_altitude,
|
|
astro_eclipse_event_t evt,
|
|
double *max_minutes,
|
|
double *max_degrees)
|
|
{
|
|
int error = 1;
|
|
double diff_minutes, diff_alt;
|
|
|
|
diff_minutes = (24 * 60) * ABS(expected_time.ut - evt.time.ut);
|
|
|
|
if (diff_minutes > *max_minutes)
|
|
*max_minutes = diff_minutes;
|
|
|
|
if (diff_minutes > 1.0)
|
|
FAIL("CheckEvent(%s line %d): EXCESSIVE TIME ERROR: %0.3lf minutes\n", inFileName, lnum, diff_minutes);
|
|
|
|
/* Ignore discrepancies for negative altitudes, because of quirky and irrelevant differences in refraction models. */
|
|
if (expected_altitude >= 0.0)
|
|
{
|
|
diff_alt = ABS(expected_altitude - evt.altitude);
|
|
if (diff_alt > *max_degrees) *max_degrees = diff_alt;
|
|
if (diff_alt > 0.5)
|
|
FAIL("CheckEvent(%s line %d): EXCESSIVE ALTITUDE ERROR: %0.6lf degrees (expected = %0.6lf, calc = %0.6lf)\n", inFileName, lnum, diff_alt, expected_altitude, evt.altitude);
|
|
}
|
|
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
|
|
#define TIMESTRSIZE 31
|
|
|
|
static int LocalSolarEclipseTest2(void)
|
|
{
|
|
/*
|
|
Test ability to calculate local solar eclipse conditions away from
|
|
the peak position on the Earth.
|
|
*/
|
|
|
|
int error = 1;
|
|
FILE *infile = NULL;
|
|
const char *inFileName = "eclipse/local_solar_eclipse.txt";
|
|
int lnum, nscanned;
|
|
char line[300];
|
|
char p1str[TIMESTRSIZE], p2str[TIMESTRSIZE], t1str[TIMESTRSIZE], t2str[TIMESTRSIZE], peakstr[TIMESTRSIZE];
|
|
double p1alt, p2alt, t1alt, t2alt, peakalt;
|
|
astro_time_t p1, p2, t1, t2, peak, search_time;
|
|
char typeChar;
|
|
astro_observer_t observer;
|
|
double max_minutes=0.0, max_degrees=0.0;
|
|
int verify_count = 0;
|
|
astro_local_solar_eclipse_t eclipse;
|
|
astro_eclipse_kind_t expected_kind;
|
|
extern int _CalcMoonCount; /* incremented by Astronomy Engine every time expensive CalcMoon() is called */
|
|
|
|
_CalcMoonCount = 0;
|
|
|
|
infile = fopen(inFileName, "rt");
|
|
if (infile == NULL)
|
|
FFAIL("Cannot open input file: %s\n", inFileName);
|
|
|
|
lnum = 0;
|
|
observer.height = 0.0;
|
|
while (ReadLine(line, sizeof(line), infile, inFileName, lnum))
|
|
{
|
|
++lnum;
|
|
if (IgnoreLine(line)) continue;
|
|
|
|
nscanned = sscanf(line, "%lf %lf %c %30s %lf %30s %lf %30s %lf %30s %lf %30s %lf",
|
|
&observer.latitude, &observer.longitude,
|
|
&typeChar,
|
|
p1str, &p1alt,
|
|
t1str, &t1alt,
|
|
peakstr, &peakalt,
|
|
t2str, &t2alt,
|
|
p2str, &p2alt);
|
|
|
|
if (nscanned != 13)
|
|
FAIL("C LocalSolarEclipseTest2(%s line %d): Incorrect token count (scanned %d)\n", inFileName, lnum, nscanned);
|
|
|
|
switch (typeChar)
|
|
{
|
|
case 'P': expected_kind = ECLIPSE_PARTIAL; break;
|
|
case 'A': expected_kind = ECLIPSE_ANNULAR; break;
|
|
case 'T': expected_kind = ECLIPSE_TOTAL; break;
|
|
default:
|
|
FAIL("C LocalSolarEclipseTest2(%s line %d): invalid eclipse type '%c'\n", inFileName, lnum, typeChar);
|
|
}
|
|
|
|
CHECK(ParseEvent(p1str, 1, &p1));
|
|
CHECK(ParseEvent(p2str, 1, &p2));
|
|
CHECK(ParseEvent(peakstr, 1, &peak));
|
|
CHECK(ParseEvent(t1str, (typeChar != 'P'), &t1));
|
|
CHECK(ParseEvent(t2str, (typeChar != 'P'), &t2));
|
|
|
|
search_time = Astronomy_AddDays(p1, -20.0);
|
|
eclipse = Astronomy_SearchLocalSolarEclipse(search_time, observer);
|
|
if (eclipse.status != ASTRO_SUCCESS)
|
|
FAIL("C LocalSolarEclipseTest2(%s line %d): error %d searching for solar eclipse.\n", inFileName, lnum, eclipse.status);
|
|
|
|
if (eclipse.kind != expected_kind)
|
|
FAIL("C LocalSolarEclipseTest2(%s line %d): expected eclipse kind %d, found %d\n", inFileName, lnum, expected_kind, eclipse.kind);
|
|
|
|
CHECK(CheckEvent(inFileName, lnum, "peak", peak, peakalt, eclipse.peak, &max_minutes, &max_degrees));
|
|
CHECK(CheckEvent(inFileName, lnum, "partial_begin", p1, p1alt, eclipse.partial_begin, &max_minutes, &max_degrees));
|
|
CHECK(CheckEvent(inFileName, lnum, "partial_end", p2, p2alt, eclipse.partial_end, &max_minutes, &max_degrees));
|
|
if (typeChar != 'P')
|
|
{
|
|
CHECK(CheckEvent(inFileName, lnum, "total_begin", t1, t1alt, eclipse.total_begin, &max_minutes, &max_degrees));
|
|
CHECK(CheckEvent(inFileName, lnum, "total_end", t2, t2alt, eclipse.total_end, &max_minutes, &max_degrees));
|
|
}
|
|
|
|
++verify_count;
|
|
}
|
|
|
|
FPASSA("(%d verified, %d CalcMoons, max minutes = %0.3lf, max alt degrees = %0.3lf)\n", verify_count, _CalcMoonCount, max_minutes, max_degrees);
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int GlobalAnnularCase(int year, int month, int day, double obscuration)
|
|
{
|
|
int error;
|
|
astro_time_t time;
|
|
astro_global_solar_eclipse_t eclipse;
|
|
double dt, diff;
|
|
|
|
/* Search for the first solar eclipse that occurs after the given date. */
|
|
time = Astronomy_MakeTime(year, month, day, 0, 0, 0.0);
|
|
eclipse = Astronomy_SearchGlobalSolarEclipse(time);
|
|
CHECK_STATUS(eclipse);
|
|
|
|
/* Verify the eclipse is within 1 day after the search basis time. */
|
|
dt = V(eclipse.peak.ut - time.ut);
|
|
if (dt < 0.0 || dt > 1.0)
|
|
FAIL("C GlobalAnnularCase(%04d-%02d-%02d) FAIL: found eclipse %0.8lf days after search time.\n", year, month, day, dt);
|
|
|
|
/* Verify we found an annular solar eclipse. */
|
|
if (eclipse.kind != ECLIPSE_ANNULAR)
|
|
FAIL("C GlobalAnnularCase(%04d-%02d-%02d) FAIL: expected annular eclipse, found %d.\n", year, month, day, eclipse.kind);
|
|
|
|
/* Check how accurately we calculated obscuration. */
|
|
diff = V(eclipse.obscuration - obscuration);
|
|
if (ABS(diff) > 0.0000904)
|
|
FAIL("C GlobalAnnularCase(%04d-%02d-%02d) FAIL: excessive obscuration error = %0.8lf. expected = %0.8lf, calculated = %0.8lf\n", year, month, day, diff, obscuration, eclipse.obscuration);
|
|
DEBUG("C GlobalAnnularCase(%04d-%02d-%02d) obscuration error = %0.8lf, expected = %0.8lf, calculated = %0.8lf\n", year, month, day, diff, obscuration, eclipse.obscuration);
|
|
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
|
|
static int LocalSolarCase(
|
|
int year,
|
|
int month,
|
|
int day,
|
|
double latitude,
|
|
double longitude,
|
|
astro_eclipse_kind_t kind,
|
|
double obscuration,
|
|
double tolerance)
|
|
{
|
|
int error;
|
|
astro_time_t time;
|
|
astro_observer_t observer;
|
|
astro_local_solar_eclipse_t eclipse;
|
|
double dt, diff;
|
|
|
|
observer = Astronomy_MakeObserver(latitude, longitude, 0.0);
|
|
time = Astronomy_MakeTime(year, month, day, 0, 0, 0.0);
|
|
eclipse = Astronomy_SearchLocalSolarEclipse(time, observer);
|
|
CHECK_STATUS(eclipse);
|
|
|
|
dt = V(eclipse.peak.time.ut - time.ut);
|
|
if (dt < 0.0 || dt > 1.0)
|
|
FAIL("C LocalSolarCase(%04d-%02d-%02d) FAIL: elapsed time = %0.6lf days\n", year, month, day, dt);
|
|
|
|
if (eclipse.kind != kind)
|
|
FAIL("C LocalSolarCase(%04d-%02d-%02d) FAIL: expected kind %d, found %d\n", year, month, day, kind, eclipse.kind);
|
|
|
|
diff = V(eclipse.obscuration - obscuration);
|
|
if (ABS(diff) > tolerance)
|
|
FAIL("C LocalSolarCase(%04d-%02d-%02d) FAIL: obscuration diff = %11.8lf, expected = %11.8lf, calculated = %11.8lf\n", year, month, day, diff, obscuration, eclipse.obscuration);
|
|
DEBUG("C LocalSolarCase(%04d-%02d-%02d) obscuration diff = %11.8lf, expected = %11.8lf, calculated = %11.8lf\n", year, month, day, diff, obscuration, eclipse.obscuration);
|
|
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
|
|
static int SolarFractionTest(void)
|
|
{
|
|
int error;
|
|
|
|
/* Verify global solar eclipse obscurations for annular eclipses only. */
|
|
/* This is because they are the only nontrivial values for global solar eclipses. */
|
|
/* The trivial values are all validated exactly by GlobalSolarEclipseTest(). */
|
|
|
|
CHECK(GlobalAnnularCase(2023, 10, 14, 0.90638)); /* https://www.eclipsewise.com/solar/SEprime/2001-2100/SE2023Oct14Aprime.html */
|
|
CHECK(GlobalAnnularCase(2024, 10, 2, 0.86975)); /* https://www.eclipsewise.com/solar/SEprime/2001-2100/SE2024Oct02Aprime.html */
|
|
CHECK(GlobalAnnularCase(2027, 2, 6, 0.86139)); /* https://www.eclipsewise.com/solar/SEprime/2001-2100/SE2027Feb06Aprime.html */
|
|
CHECK(GlobalAnnularCase(2028, 1, 26, 0.84787)); /* https://www.eclipsewise.com/solar/SEprime/2001-2100/SE2028Jan26Aprime.html */
|
|
CHECK(GlobalAnnularCase(2030, 6, 1, 0.89163)); /* https://www.eclipsewise.com/solar/SEprime/2001-2100/SE2030Jun01Aprime.html */
|
|
|
|
/* Verify obscuration values for specific locations on the Earth. */
|
|
/* Local solar eclipse calculations include obscuration for all types of eclipse, not just annular and total. */
|
|
CHECK(LocalSolarCase(2023, 10, 14, 11.3683, -83.1017, ECLIPSE_ANNULAR, 0.90638, 0.000080)); /* https://www.eclipsewise.com/solar/SEprime/2001-2100/SE2023Oct14Aprime.html */
|
|
CHECK(LocalSolarCase(2023, 10, 14, 25.78, -80.22, ECLIPSE_PARTIAL, 0.578, 0.000023)); /* https://aa.usno.navy.mil/calculated/eclipse/solar?eclipse=22023&lat=25.78&lon=-80.22&label=Miami%2C+FL&height=0&submit=Get+Data */
|
|
CHECK(LocalSolarCase(2023, 10, 14, 30.2666, -97.7000, ECLIPSE_PARTIAL, 0.8867, 0.001016)); /* http://astro.ukho.gov.uk/eclipse/0332023/Austin_TX_United_States_2023Oct14.png */
|
|
CHECK(LocalSolarCase(2024, 4, 8, 25.2900, -104.1383, ECLIPSE_TOTAL, 1.0, 0.0)); /* https://www.eclipsewise.com/solar/SEprime/2001-2100/SE2024Apr08Tprime.html */
|
|
CHECK(LocalSolarCase(2024, 4, 8, 37.76, -122.44, ECLIPSE_PARTIAL, 0.340, 0.000604)); /* https://aa.usno.navy.mil/calculated/eclipse/solar?eclipse=12024&lat=37.76&lon=-122.44&label=San+Francisco%2C+CA&height=0&submit=Get+Data */
|
|
CHECK(LocalSolarCase(2024, 10, 2, -21.9533, -114.5083, ECLIPSE_ANNULAR, 0.86975, 0.000061)); /* https://www.eclipsewise.com/solar/SEprime/2001-2100/SE2024Oct02Aprime.html */
|
|
CHECK(LocalSolarCase(2024, 10, 2, -33.468, -70.636, ECLIPSE_PARTIAL, 0.436, 0.000980)); /* https://aa.usno.navy.mil/calculated/eclipse/solar?eclipse=22024&lat=-33.468&lon=-70.636&label=Santiago%2C+Chile&height=0&submit=Get+Data */
|
|
CHECK(LocalSolarCase(2030, 6, 1, 56.525, 80.0617, ECLIPSE_ANNULAR, 0.89163, 0.000067)); /* https://www.eclipsewise.com/solar/SEprime/2001-2100/SE2030Jun01Aprime.html */
|
|
CHECK(LocalSolarCase(2030, 6, 1, 40.388, 49.914, ECLIPSE_PARTIAL, 0.67240, 0.000599)); /* http://xjubier.free.fr/en/site_pages/SolarEclipseCalc_Diagram.html */
|
|
CHECK(LocalSolarCase(2030, 6, 1, 40.3667, 49.8333, ECLIPSE_PARTIAL, 0.6736, 0.001464)); /* http://astro.ukho.gov.uk/eclipse/0132030/Baku_Azerbaijan_2030Jun01.png */
|
|
|
|
FPASS();
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int PlotDeltaT(const char *outFileName)
|
|
{
|
|
int error = 1;
|
|
FILE *outfile = NULL;
|
|
int year;
|
|
double dt;
|
|
astro_time_t time;
|
|
|
|
outfile = fopen(outFileName, "wt");
|
|
if (outfile == NULL)
|
|
FAIL("ctest PlotDeltaT: Cannot open output file: %s\n", outFileName);
|
|
|
|
fprintf(outfile, "\"year\",\"delta_t\"\n");
|
|
for (year = 1500; year <= 2500; ++year)
|
|
{
|
|
time = Astronomy_MakeTime(year, 1, 1, 0, 0, 0.0);
|
|
dt = (24.0 * 3600.0) * (time.tt - time.ut);
|
|
fprintf(outfile, "%d,%lf\n", year, dt);
|
|
}
|
|
|
|
printf("ctest PlotDeltaT: Wrote file %s\n", outFileName);
|
|
error = 0;
|
|
fail:
|
|
if (outfile != NULL) fclose(outfile);
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int TransitFile(astro_body_t body, const char *filename, double limit_minutes, double limit_sep)
|
|
{
|
|
int error = 1;
|
|
FILE *infile = NULL;
|
|
char line[100];
|
|
int lnum;
|
|
astro_time_t time1, time2, timep;
|
|
astro_transit_t transit;
|
|
double separation;
|
|
double diff_start, diff_peak, diff_finish, diff_sep;
|
|
double max_minutes = 0.0, max_sep = 0.0;
|
|
const int START_YEAR = 1600;
|
|
|
|
infile = fopen(filename, "rt");
|
|
if (infile == NULL)
|
|
FFAIL("Cannot open input file: %s\n", filename);
|
|
|
|
transit = Astronomy_SearchTransit(body, Astronomy_MakeTime(START_YEAR, 1, 1, 0, 0, 0.0));
|
|
if (transit.status != ASTRO_SUCCESS)
|
|
FAIL("C TransitFile(%s): SearchTransit returned %d\n", filename, transit.status);
|
|
|
|
DEBUG("\n----------------------------------------------------------------\n\n");
|
|
FDEBUG("STARTING %s\n\n", filename);
|
|
|
|
lnum = 0;
|
|
while (ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++lnum;
|
|
|
|
/* 22:17 1881-11-08T00:57Z 03:38 3.8633 */
|
|
|
|
if (strlen(line) < 37)
|
|
FAIL("C TransitFile(%s line %d): line is too short.\n", filename, lnum);
|
|
|
|
if (line[5] != ' ' || line[23] != ' ' || line[29] != ' ')
|
|
FAIL("C TransitFile(%s line %d): invalid data format.\n", filename, lnum);
|
|
|
|
if (1 != sscanf(line+30, "%lf", &separation))
|
|
FAIL("C TransitFile(%s line %d): unable to parse angular separation.\n", filename, lnum);
|
|
|
|
line[23] = '\0';
|
|
if (ParseDate(line+6, &timep))
|
|
FAIL("C TransitFile(%s line %d): invalid date/time '%s'\n", filename, lnum, line+6);
|
|
|
|
/* Patch the start time and reparse. */
|
|
line[17] = line[0];
|
|
line[18] = line[1];
|
|
line[20] = line[3];
|
|
line[21] = line[4];
|
|
if (ParseDate(line+6, &time1))
|
|
FAIL("C TransitFile(%s line %d): failed to parse patched start time '%s'\n", filename, lnum, line+6);
|
|
|
|
/* If the start time is after the peak time, it really starts on the previous day. */
|
|
if (time1.ut > timep.ut)
|
|
time1 = Astronomy_AddDays(time1, -1.0);
|
|
|
|
/* Patch the finish time and reparse. */
|
|
line[17] = line[24];
|
|
line[18] = line[25];
|
|
line[20] = line[27];
|
|
line[21] = line[28];
|
|
if (ParseDate(line+6, &time2))
|
|
FAIL("C TransitFile(%s line %d): failed to parse patched finish time '%s'\n", filename, lnum, line+6);
|
|
|
|
/* If the finish time is before the peak time, it really starts on the next day. */
|
|
if (time2.ut < timep.ut)
|
|
time2 = Astronomy_AddDays(time2, +1.0);
|
|
|
|
/* Measure result errors. */
|
|
diff_start = (24.0 * 60.0) * (time1.ut - transit.start.ut );
|
|
diff_peak = (24.0 * 60.0) * (timep.ut - transit.peak.ut );
|
|
diff_finish = (24.0 * 60.0) * (time2.ut - transit.finish.ut);
|
|
diff_sep = separation - transit.separation;
|
|
|
|
if (Verbose)
|
|
{
|
|
printf("Expected: ");
|
|
PrintTime(time1);
|
|
printf(" ");
|
|
PrintTime(timep);
|
|
printf(" ");
|
|
PrintTime(time2);
|
|
printf(" %10.4lf\n", separation);
|
|
|
|
printf("Found: ");
|
|
PrintTime(transit.start);
|
|
printf(" ");
|
|
PrintTime(transit.peak);
|
|
printf(" ");
|
|
PrintTime(transit.finish);
|
|
printf(" %10.4lf\n", transit.separation);
|
|
|
|
printf("Diff: %24.3lf %24.3lf %24.3lf %10.4lf\n\n", diff_start, diff_peak, diff_finish, diff_sep);
|
|
}
|
|
|
|
diff_start = ABS(diff_start);
|
|
diff_peak = ABS(diff_peak);
|
|
diff_finish = ABS(diff_finish);
|
|
if (diff_start > max_minutes) max_minutes = diff_start;
|
|
if (diff_peak > max_minutes) max_minutes = diff_peak;
|
|
if (diff_finish > max_minutes) max_minutes = diff_finish;
|
|
if (max_minutes > limit_minutes)
|
|
FAIL("C TransitFile(%s line %d): EXCESSIVE TIME ERROR = %0.3lf minutes. (Run again with -v to debug.)\n", filename, lnum, max_minutes);
|
|
|
|
diff_sep = ABS(diff_sep);
|
|
if (diff_sep > max_sep) max_sep = diff_sep;
|
|
if (max_sep > limit_sep)
|
|
FAIL("C TransitFile(%s line %d): EXCESSIVE SEPARATION ERROR = %0.4lf arcminutes. (Run again with -v to debug.)\n", filename, lnum, max_sep);
|
|
|
|
/* Search for the next transit. */
|
|
transit = Astronomy_NextTransit(body, transit.finish);
|
|
if (transit.status != ASTRO_SUCCESS)
|
|
FAIL("C TransitFile(%s line %d): NextTransit returned %d\n", filename, lnum, transit.status);
|
|
}
|
|
|
|
printf("C TransitFile(%-20s): PASS - verified %2d, max minutes = %6.3lf, max sep arcmin = %6.4lf\n", filename, lnum, max_minutes, max_sep);
|
|
error = 0;
|
|
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
static int Transit(void)
|
|
{
|
|
int error;
|
|
|
|
CHECK(TransitFile(BODY_MERCURY, "eclipse/mercury.txt", 10.710, 0.2121));
|
|
CHECK(TransitFile(BODY_VENUS, "eclipse/venus.txt", 9.109, 0.6772));
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int DistancePlot(astro_body_t body, double ut1, double ut2, const char *filename)
|
|
{
|
|
const int npoints = 100000;
|
|
int error = 1;
|
|
int i;
|
|
double ut;
|
|
astro_time_t time, j2000;
|
|
astro_func_result_t dist;
|
|
FILE *outfile = NULL;
|
|
|
|
outfile = fopen(filename, "wt");
|
|
if (outfile == NULL)
|
|
FFAIL("Cannot open output file: %s\n", filename);
|
|
|
|
j2000 = Astronomy_MakeTime(2000, 1, 1, 12, 0, 0.0);
|
|
|
|
fprintf(outfile, "\"tt\",\"distance\"\n");
|
|
for (i=0; i < npoints; ++i)
|
|
{
|
|
ut = ut1 + (((double)i)/((double)(npoints-1)) * (ut2 - ut1));
|
|
time = Astronomy_AddDays(j2000, ut);
|
|
dist = Astronomy_HelioDistance(body, time);
|
|
CHECK_STATUS(dist);
|
|
fprintf(outfile, "%0.16lf,%0.16lg\n", time.tt, dist.value);
|
|
}
|
|
|
|
error = 0;
|
|
fail:
|
|
if (outfile != NULL) fclose(outfile);
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int PlutoCheckDate(double ut, double arcmin_tolerance, double x, double y, double z)
|
|
{
|
|
int error = 1;
|
|
astro_time_t time;
|
|
astro_vector_t vector;
|
|
double dx, dy, dz, diff, dist, arcmin;
|
|
|
|
time = Astronomy_TimeFromDays(ut);
|
|
|
|
DEBUG("C %s: ", __func__);
|
|
if (Verbose) PrintTime(time);
|
|
DEBUG(" = %0.6lf UT = %0.6lf TT\n", time.ut, time.tt);
|
|
|
|
vector = Astronomy_HelioVector(BODY_PLUTO, time);
|
|
if (vector.status != ASTRO_SUCCESS)
|
|
FFAIL("FAIL - Astronomy_HelioVector returned status = %d\n", vector.status);
|
|
|
|
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); /* worst-case distance between Pluto and Earth */
|
|
arcmin = (diff / dist) * (180.0 * 60.0 / PI);
|
|
FDEBUG("calc pos = [%20.16lf, %20.16lf, %20.16lf]\n", vector.x, vector.y, vector.z);
|
|
FDEBUG("ref pos = [%20.16lf, %20.16lf, %20.16lf]\n", x, y, z);
|
|
FDEBUG("del pos = [%20.16lf, %20.16lf, %20.16lf]\n", vector.x - x, vector.y - y, vector.z - z);
|
|
FDEBUG("diff = %le AU, %0.3lf arcmin\n", diff, arcmin);
|
|
DEBUG("\n");
|
|
|
|
if (V(arcmin) > arcmin_tolerance)
|
|
FFAIL("EXCESSIVE ERROR\n");
|
|
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
|
|
static int PlutoCheck(void)
|
|
{
|
|
int error;
|
|
CHECK(PlutoCheckDate( +18250.0, 0.089, +37.4377303523676090, -10.2466292454075898, -14.4773101310875809));
|
|
CHECK(PlutoCheckDate( -856493.0, 4.067, +23.4292113199166252, +42.1452685817740829, +6.0580908436642940));
|
|
CHECK(PlutoCheckDate( +435633.0, 0.016, -27.3178902095231813, +18.5887022581070305, +14.0493896259306936));
|
|
CHECK(PlutoCheckDate( 0.0, 8.e-9, -9.8753673425269000, -27.9789270580402771, -5.7537127596369588));
|
|
CHECK(PlutoCheckDate( +800916.0, 2.286, -29.5266052645301365, +12.0554287322176474, +12.6878484911631091));
|
|
|
|
FPASS();
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int GeoidTestCase(FILE *outfile, astro_time_t time, astro_observer_t observer, astro_equator_date_t equdate)
|
|
{
|
|
int error;
|
|
astro_state_vector_t state;
|
|
astro_vector_t surface;
|
|
astro_vector_t geo_moon;
|
|
astro_equatorial_t topo_moon;
|
|
double dx, dy, dz, diff, lat_diff, lon_diff, h_diff;
|
|
astro_observer_t vobs;
|
|
|
|
topo_moon = Astronomy_Equator(BODY_MOON, &time, observer, equdate, NO_ABERRATION);
|
|
CHECK_STATUS(topo_moon);
|
|
|
|
surface = Astronomy_ObserverVector(&time, observer, equdate);
|
|
CHECK_STATUS(surface);
|
|
|
|
state = Astronomy_ObserverState(&time, observer, equdate);
|
|
CHECK_STATUS(state);
|
|
|
|
/* Verify state vector's position exactly matches the lone position vector. */
|
|
if (state.x != surface.x) FFAIL("state vector x = %0.16le, but position vector x = %0.16le\n", state.x, surface.x);
|
|
if (state.y != surface.y) FFAIL("state vector x = %0.16le, but position vector x = %0.16le\n", state.y, surface.y);
|
|
if (state.z != surface.z) FFAIL("state vector x = %0.16le, but position vector x = %0.16le\n", state.z, surface.z);
|
|
|
|
if (outfile != NULL)
|
|
{
|
|
const double speedConvert = KM_PER_AU / 24.0; /* converts AU/day into km/hour */
|
|
|
|
fprintf(
|
|
outfile,
|
|
"%d %22.12lf %6.1lf %7.1lf %7.1lf %12.6lf %12.6lf %12.6lf %12.6lf %12.6lf %12.6lf\n",
|
|
(int)equdate,
|
|
time.ut,
|
|
observer.latitude,
|
|
observer.longitude,
|
|
observer.height,
|
|
KM_PER_AU * state.x,
|
|
KM_PER_AU * state.y,
|
|
KM_PER_AU * state.z,
|
|
speedConvert * state.vx,
|
|
speedConvert * state.vy,
|
|
speedConvert * state.vz
|
|
);
|
|
}
|
|
|
|
geo_moon = Astronomy_GeoMoon(time);
|
|
CHECK_STATUS(geo_moon);
|
|
|
|
if (equdate == EQUATOR_OF_DATE)
|
|
{
|
|
/* Astronomy_GeoMoon() returns J2000 coordinates. Convert to equator-of-date coordinates. */
|
|
astro_rotation_t rot = Astronomy_Rotation_EQJ_EQD(&time);
|
|
CHECK_STATUS(rot);
|
|
geo_moon = Astronomy_RotateVector(rot, geo_moon);
|
|
}
|
|
|
|
dx = KM_PER_AU * V((geo_moon.x - surface.x) - topo_moon.vec.x);
|
|
dy = KM_PER_AU * V((geo_moon.y - surface.y) - topo_moon.vec.y);
|
|
dz = KM_PER_AU * V((geo_moon.z - surface.z) - topo_moon.vec.z);
|
|
diff = sqrt(dx*dx + dy*dy + dz*dz);
|
|
FDEBUG("equ=%d, tt=%14.5lf, lat=%5.1lf, lon=%6.1lf, ht=%6.1lf, surface=(%12.6lf, %12.6lf, %12.6lf), diff = %9.6lf km\n",
|
|
(int)equdate,
|
|
time.tt,
|
|
observer.latitude,
|
|
observer.longitude,
|
|
observer.height,
|
|
KM_PER_AU * surface.x,
|
|
KM_PER_AU * surface.y,
|
|
KM_PER_AU * surface.z,
|
|
diff);
|
|
|
|
/* Require 1 millimeter accuracy! (one millionth of a kilometer). */
|
|
if (diff > 1.0e-6)
|
|
FFAIL("EXCESSIVE POSITION ERROR.\n");
|
|
|
|
/* Verify that we can convert the surface vector back to an observer. */
|
|
vobs = Astronomy_VectorObserver(&surface, equdate);
|
|
lat_diff = ABS(vobs.latitude - observer.latitude);
|
|
|
|
/* Longitude is meaningless at the poles, so don't bother checking it there. */
|
|
if (-89.99 <= observer.latitude && observer.latitude <= +89.99)
|
|
{
|
|
lon_diff = ABS(vobs.longitude - observer.longitude);
|
|
if (lon_diff > 180.0)
|
|
lon_diff = 360.0 - lon_diff;
|
|
lon_diff = ABS(lon_diff * cos(DEG2RAD * observer.latitude));
|
|
if (lon_diff > 1.0e-6)
|
|
FFAIL("EXCESSIVE longitude check error = %lf\n", lon_diff);
|
|
}
|
|
else
|
|
{
|
|
lon_diff = 0.0;
|
|
}
|
|
|
|
h_diff = ABS(vobs.height - observer.height);
|
|
FDEBUG("vobs=(lat=%lf, lon=%lf, height=%lf), lat_diff=%lg, lon_diff=%lg, h_diff=%lg\n",
|
|
vobs.latitude, vobs.longitude, vobs.height, lat_diff, lon_diff, h_diff);
|
|
|
|
if (lat_diff > 1.0e-6)
|
|
FFAIL("EXCESSIVE latitude check error = %lf\n", lat_diff);
|
|
|
|
if (h_diff > 0.001)
|
|
FFAIL("EXCESSIVE height check error = %lf\n", h_diff);
|
|
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
|
|
static int GeoidTest(void)
|
|
{
|
|
int error;
|
|
int tindex, oindex;
|
|
astro_time_t time;
|
|
astro_vector_t vec;
|
|
astro_observer_t observer;
|
|
int lat, lon;
|
|
FILE *outfile = NULL;
|
|
const char *outFileName = "temp/c_geoid.txt";
|
|
|
|
const astro_time_t time_list[] =
|
|
{
|
|
Astronomy_MakeTime(1066, 9, 27, 18, 0, 0.0),
|
|
Astronomy_MakeTime(1970, 12, 13, 15, 42, 0.0),
|
|
Astronomy_MakeTime(1970, 12, 13, 15, 43, 0.0),
|
|
Astronomy_MakeTime(2015, 3, 5, 2, 15, 45.0)
|
|
};
|
|
const int ntimes = sizeof(time_list) / sizeof(time_list[0]);
|
|
|
|
const astro_observer_t observer_list[] =
|
|
{
|
|
Astronomy_MakeObserver( +1.5, +2.7, 7.4),
|
|
Astronomy_MakeObserver(-53.7, +141.7, +100.0),
|
|
Astronomy_MakeObserver(+30.0, -85.2, -50.0),
|
|
Astronomy_MakeObserver(+90.0, +45.0, -50.0),
|
|
Astronomy_MakeObserver(-90.0, -180.0, 0.0)
|
|
};
|
|
const int nobs = sizeof(observer_list) / sizeof(observer_list[0]);
|
|
|
|
/* Make sure Astronomy_ObserverVector() checks for invalid equdate parameter. */
|
|
time = time_list[0];
|
|
vec = Astronomy_ObserverVector(&time, observer_list[0], (astro_equator_date_t)42);
|
|
if (vec.status != ASTRO_INVALID_PARAMETER)
|
|
FFAIL("Expected ASTRO_INVALID_PARAMETER (%d) but found %d\n", (int)ASTRO_INVALID_PARAMETER, (int)vec.status);
|
|
FDEBUG("Astronomy_ObserverVector correctly detected invalid astro_equator_date_t parameter.\n");
|
|
|
|
/* Test a variety of times and locations, in both supported orientation systems. */
|
|
/* Also write reference data for comparing with other language implementations. */
|
|
outfile = fopen(outFileName, "wt");
|
|
if (outfile == NULL)
|
|
FFAIL("FAIL - cannot open output file: %s\n", outFileName);
|
|
|
|
for (oindex = 0; oindex < nobs; ++oindex)
|
|
{
|
|
for (tindex = 0; tindex < ntimes; ++tindex)
|
|
{
|
|
CHECK(GeoidTestCase(outfile, time_list[tindex], observer_list[oindex], EQUATOR_J2000));
|
|
CHECK(GeoidTestCase(outfile, time_list[tindex], observer_list[oindex], EQUATOR_OF_DATE));
|
|
}
|
|
}
|
|
|
|
/* More exhaustive tests for a single time value across many different geographic coordinates. */
|
|
time = Astronomy_MakeTime(2021, 6, 20, 15, 8, 0.0);
|
|
for (lat = -90; lat <= +90; lat += 1)
|
|
{
|
|
for (lon = -175; lon <= +180; lon += 5)
|
|
{
|
|
observer = Astronomy_MakeObserver(lat, lon, 0.0);
|
|
CHECK(GeoidTestCase(NULL, time, observer, EQUATOR_OF_DATE));
|
|
}
|
|
}
|
|
|
|
FPASS();
|
|
fail:
|
|
if (outfile) fclose(outfile);
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static void TrimWhiteSpace(char *line)
|
|
{
|
|
int len = (int)strlen(line);
|
|
while (len > 0 && isspace(line[len-1]))
|
|
line[--len] = '\0';
|
|
}
|
|
|
|
|
|
static int StringStartsWith(const char *text, const char *prefix)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; prefix[i] != '\0'; ++i)
|
|
if (text[i] != prefix[i])
|
|
return 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
|
|
static astro_state_vector_t SelectJupiterMoon(const astro_jupiter_moons_t *jm, int mindex)
|
|
{
|
|
switch (mindex)
|
|
{
|
|
case 0: return jm->io;
|
|
case 1: return jm->europa;
|
|
case 2: return jm->ganymede;
|
|
case 3: return jm->callisto;
|
|
default:
|
|
fprintf(stderr, "C FATAL(%s): Invalid mindex = %d\n", __func__, mindex);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
|
|
static int JupiterMoons_CheckJpl(int mindex, double tt, const double pos[3], const double vel[3])
|
|
{
|
|
int error;
|
|
astro_jupiter_moons_t jm;
|
|
astro_state_vector_t moon;
|
|
astro_time_t time;
|
|
double dx, dy, dz, mag, diff;
|
|
const double pos_tolerance = 9.0e-4;
|
|
const double vel_tolerance = 9.0e-4;
|
|
|
|
time = Astronomy_TerrestrialTime(tt);
|
|
|
|
jm = Astronomy_JupiterMoons(time);
|
|
CHECK_STATUS(jm.io);
|
|
CHECK_STATUS(jm.europa);
|
|
CHECK_STATUS(jm.ganymede);
|
|
CHECK_STATUS(jm.callisto);
|
|
moon = SelectJupiterMoon(&jm, mindex);
|
|
|
|
dx = V(pos[0] - moon.x);
|
|
dy = V(pos[1] - moon.y);
|
|
dz = V(pos[2] - moon.z);
|
|
mag = V(sqrt(pos[0]*pos[0] + pos[1]*pos[1] + pos[2]*pos[2]));
|
|
diff = V(sqrt(dx*dx + dy*dy + dz*dz) / mag);
|
|
if (diff > pos_tolerance)
|
|
FFAIL("(mindex=%d, tt=%0.1lf): excessive position error %le\n", mindex, tt, diff);
|
|
|
|
dx = V(vel[0] - moon.vx);
|
|
dy = V(vel[1] - moon.vy);
|
|
dz = V(vel[2] - moon.vz);
|
|
mag = V(sqrt(vel[0]*vel[0] + vel[1]*vel[1] + vel[2]*vel[2]));
|
|
diff = V(sqrt(dx*dx + dy*dy + dz*dz) / mag);
|
|
if (diff > vel_tolerance)
|
|
FFAIL("(mindex=%d, tt=%0.1lf): excessive velocity error %le\n", mindex, tt, diff);
|
|
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
|
|
static int JupiterMoonsTest(void)
|
|
{
|
|
int error, mindex, check_mindex, lnum, found, part;
|
|
double tt = NAN, pos[3] = {NAN, NAN, NAN}, vel[3] = {NAN, NAN, NAN};
|
|
FILE *infile = NULL;
|
|
char filename[100];
|
|
char line[100];
|
|
const int expected_count = 5001;
|
|
int count;
|
|
|
|
for (mindex = 0; mindex < 4; ++mindex)
|
|
{
|
|
snprintf(filename, sizeof(filename), "jupiter_moons/horizons/jm%d.txt", mindex);
|
|
infile = fopen(filename, "rt");
|
|
if (infile == NULL)
|
|
FFAIL("Cannot open input file: %s\n", filename);
|
|
|
|
FDEBUG("Comparing against %s\n", filename);
|
|
|
|
lnum = 0;
|
|
found = 0;
|
|
part = -1;
|
|
count = 0;
|
|
while (ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++lnum;
|
|
TrimWhiteSpace(line);
|
|
if (!found)
|
|
{
|
|
if (!strcmp(line, "$$SOE"))
|
|
{
|
|
found = 1;
|
|
part = 0;
|
|
}
|
|
else if (StringStartsWith(line, "Revised:"))
|
|
{
|
|
if (strlen(line) != 79)
|
|
FAIL("C JupiterMoonsTest(%s line %d): unexpected line length.\n", filename, lnum);
|
|
check_mindex = atoi(&line[76]) - 501;
|
|
if (mindex != check_mindex)
|
|
FAIL("C JupiterMoonsTest(%s line %d): moon index does not match: check=%d, mindex=%d.\n", filename, lnum, check_mindex, mindex);
|
|
}
|
|
}
|
|
else if (!strcmp(line, "$$EOE"))
|
|
{
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
switch (part)
|
|
{
|
|
case 0:
|
|
/* 2446545.000000000 = A.D. 1986-Apr-24 12:00:00.0000 TDB */
|
|
if (1 != sscanf(line, "%lf", &tt))
|
|
FAIL("C JupiterMoonsTest(%s line %d): failed to read time stamp.\n", filename, lnum);
|
|
tt -= JD_2000; /* convert JD to J2000 TT */
|
|
break;
|
|
|
|
case 1:
|
|
/* X = 1.134408131605554E-03 Y =-2.590904586750408E-03 Z =-7.490427225904720E-05 */
|
|
if (3 != sscanf(line, " X =%lf Y =%lf Z =%lf", &pos[0], &pos[1], &pos[2]))
|
|
FAIL("C JupiterMoonsTest(%s line %d): cannot parse position vector.\n", filename, lnum);
|
|
break;
|
|
|
|
case 2:
|
|
/* VX= 9.148038778472862E-03 VY= 3.973823407182510E-03 VZ= 2.765660368640458E-04 */
|
|
if (3 != sscanf(line, " VX=%lf VY=%lf VZ=%lf", &vel[0], &vel[1], &vel[2]))
|
|
FAIL("C JupiterMoonsTest(%s line %d): cannot parse velocity vector.\n", filename, lnum);
|
|
if (JupiterMoons_CheckJpl(mindex, tt, pos, vel))
|
|
FAIL("C JupiterMoonsTest(%s line %d): FAILED VERIFICATION.\n", filename, lnum);
|
|
++count;
|
|
break;
|
|
|
|
default:
|
|
FAIL("C JupiterMoonsTest(%s line %d): unexpected part = %d.\n", filename, lnum, part);
|
|
}
|
|
part = (part + 1) % 3;
|
|
}
|
|
}
|
|
|
|
fclose(infile);
|
|
infile = NULL;
|
|
|
|
if (count != expected_count)
|
|
FAIL("C JupiterMoonsTest(%s): expected %d test cases, found %d\n", filename, expected_count, count);
|
|
}
|
|
|
|
FPASS();
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int Issue103(void)
|
|
{
|
|
/* https://github.com/cosinekitty/astronomy/issues/103 */
|
|
|
|
astro_observer_t observer = Astronomy_MakeObserver(29.0, -81.0, 10.0);
|
|
double ut = -8.817548982869034808e+04;
|
|
astro_time_t time = Astronomy_TimeFromDays(ut);
|
|
astro_body_t body = BODY_VENUS;
|
|
astro_equatorial_t ofdate = Astronomy_Equator(body, &time, observer, EQUATOR_OF_DATE, ABERRATION);
|
|
astro_horizon_t hor;
|
|
|
|
printf("tt = %23.16lf\n", time.tt);
|
|
printf("ra = %23.16lf\n", ofdate.ra);
|
|
printf("dec = %23.16lf\n", ofdate.dec);
|
|
hor = Astronomy_Horizon(&time, observer, ofdate.ra, ofdate.dec, REFRACTION_NONE);
|
|
printf("az = %23.16lf\n", hor.azimuth);
|
|
printf("alt = %23.16lf\n", hor.altitude);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int AberrationTest(void)
|
|
{
|
|
int error, lnum, nscanned;
|
|
int found_begin = 0;
|
|
int found_end = 0;
|
|
int count = 0;
|
|
FILE *infile = NULL;
|
|
const char *filename = "equatorial/Mars_j2000_ofdate_aberration.txt";
|
|
char line[100];
|
|
double jd, jra, jdec, dra, ddec, xra, xdec;
|
|
astro_time_t time;
|
|
astro_rotation_t rot;
|
|
astro_spherical_t eqj_sphere, eqd_sphere;
|
|
astro_vector_t eqj_vec, eqd_vec;
|
|
double factor, diff_seconds, max_diff_seconds = 0.0;
|
|
astro_state_vector_t eqj_earth;
|
|
const double THRESHOLD_SECONDS = 0.453;
|
|
|
|
infile = fopen(filename, "rt");
|
|
if (infile == NULL)
|
|
FFAIL("Cannot open input file: %s\n", filename);
|
|
|
|
lnum = 0;
|
|
while (!found_end && ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++lnum;
|
|
if (!found_begin)
|
|
{
|
|
if (strlen(line) >= 5 && !memcmp(line, "$$SOE", 5))
|
|
found_begin = 1;
|
|
}
|
|
else if (strlen(line) >= 5 && !memcmp(line, "$$EOE", 5))
|
|
{
|
|
found_end = 1;
|
|
}
|
|
else
|
|
{
|
|
/* 2459371.500000000 * 118.566080210 22.210647456 118.874086738 22.155784122 */
|
|
nscanned = sscanf(line+22, "%lf %lf %lf %lf", &jra, &jdec, &dra, &ddec);
|
|
if (nscanned != 4)
|
|
FAIL("C AberrationTest(%s line %d): invalid coordinates\n", filename, lnum);
|
|
|
|
nscanned = sscanf(line, "%lf", &jd);
|
|
if (nscanned != 1)
|
|
FAIL("C AberrationTest(%s line %d): invalid julian date\n", filename, lnum);
|
|
|
|
/* Convert julian day value to astro_time_t. */
|
|
time = Astronomy_TimeFromDays(jd - JD_2000);
|
|
|
|
/* Convert EQJ angular coordinates (jra, jdec) to an EQJ vector. */
|
|
eqj_sphere.status = ASTRO_SUCCESS;
|
|
eqj_sphere.lat = jdec;
|
|
eqj_sphere.lon = jra; /* JPL Horizons RA is already in degrees, not hours. */
|
|
eqj_sphere.dist = C_AUDAY; /* scale to the speed of light, to prepare for aberration correction. */
|
|
eqj_vec = Astronomy_VectorFromSphere(eqj_sphere, time);
|
|
CHECK_STATUS(eqj_vec);
|
|
|
|
/* Aberration correction: calculate the Earth's barycentric velocity vector in EQJ coordinates. */
|
|
eqj_earth = Astronomy_BaryState(BODY_EARTH, time);
|
|
CHECK_STATUS(eqj_earth);
|
|
|
|
/* Use non-relativistic approximation: add light vector to Earth velocity vector. */
|
|
/* This gives aberration-corrected apparent position of the star 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);
|
|
CHECK_STATUS(rot);
|
|
|
|
/* Use the rotation matrix to re-orient the EQJ vector to a EQD vector. */
|
|
eqd_vec = Astronomy_RotateVector(rot, eqj_vec);
|
|
CHECK_STATUS(eqd_vec);
|
|
|
|
/* Convert the EQD vector back to spherical angular coordinates. */
|
|
eqd_sphere = Astronomy_SphereFromVector(eqd_vec);
|
|
CHECK_STATUS(eqd_sphere);
|
|
|
|
/* Calculate the differences in RA and DEC between expected and calculated values. */
|
|
factor = cos(V(eqd_sphere.lat * DEG2RAD)); /* RA errors are less important toward the poles */
|
|
xra = factor * ABS(eqd_sphere.lon - dra);
|
|
xdec = ABS(eqd_sphere.lat - ddec);
|
|
diff_seconds = V(3600.0 * sqrt(xra*xra + xdec*xdec));
|
|
DEBUG("C AberrationTest(%s line %d): xra=%0.6lf deg, xdec=%0.6lf deg, diff_seconds=%0.3lf.\n", filename, lnum, xra, xdec, diff_seconds);
|
|
if (diff_seconds > THRESHOLD_SECONDS)
|
|
FAIL("C AberrationTest(%s line %d): EXCESSIVE ANGULAR ERROR = %0.3lf seconds\n", filename, lnum, diff_seconds);
|
|
if (diff_seconds > max_diff_seconds)
|
|
max_diff_seconds = diff_seconds;
|
|
|
|
/* We have completed one more test case. */
|
|
++count;
|
|
}
|
|
}
|
|
|
|
printf("C AberrationTest(%s): PASS - Tested %d cases. max_diff_seconds = %0.3lf\n", filename, count, max_diff_seconds);
|
|
error = 0;
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static double StateVectorDiff(int relative, const double vec[3], double x, double y, double z)
|
|
{
|
|
double dx = V(vec[0] - x);
|
|
double dy = V(vec[1] - y);
|
|
double dz = V(vec[2] - z);
|
|
double 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 V(sqrt(diff_squared));
|
|
}
|
|
|
|
struct _verify_state_context_t;
|
|
|
|
typedef astro_state_vector_t (* state_func_t) (struct _verify_state_context_t *, astro_body_t, astro_time_t);
|
|
|
|
typedef struct _verify_state_context_t
|
|
{
|
|
state_func_t func;
|
|
|
|
/* The following are used for Lagrange point testing only. */
|
|
astro_body_t major;
|
|
int point; /* 1=L1, 2=L2, ..., 5=L5. */
|
|
}
|
|
verify_state_context_t;
|
|
|
|
static double ArcCos(double x)
|
|
{
|
|
if (x <= -1.0)
|
|
return 180.0;
|
|
|
|
if (x >= +1.0)
|
|
return 0.0;
|
|
|
|
return RAD2DEG * acos(x);
|
|
}
|
|
|
|
static void PrintDiagnostic(double x, double y, double z, const double ref[3])
|
|
{
|
|
double calc_mag, ref_mag, angle;
|
|
|
|
calc_mag = sqrt(x*x + y*y + z*z);
|
|
ref_mag = sqrt(ref[0]*ref[0] + ref[1]*ref[1] + ref[2]*ref[2]);
|
|
angle = 60.0 * ArcCos((x*ref[0] + y*ref[1] + z*ref[2])/(calc_mag * ref_mag));
|
|
fprintf(stderr, "CALCULATED x = %22.16le, y = %22.16le, z = %22.16le [mag = %22.16le]\n", x, y, z, calc_mag);
|
|
fprintf(stderr, "REFERENCE x = %22.16le, y = %22.16le, z = %22.16le [mag = %22.16le]\n", ref[0], ref[1], ref[2], ref_mag);
|
|
fprintf(stderr, "ANGLE ERROR = %0.6lf arcmin, MAG ERROR = %0.6lf\n", angle, (calc_mag-ref_mag)/ref_mag);
|
|
}
|
|
|
|
static int VerifyState(
|
|
verify_state_context_t *context,
|
|
double *max_rdiff,
|
|
double *max_vdiff,
|
|
astro_body_t body,
|
|
const char *filename,
|
|
int lnum,
|
|
astro_time_t time,
|
|
const double pos[3],
|
|
const double vel[3],
|
|
double r_thresh,
|
|
double v_thresh)
|
|
{
|
|
int error;
|
|
astro_state_vector_t state;
|
|
double rdiff, vdiff;
|
|
|
|
state = context->func(context, body, time);
|
|
if (state.status != ASTRO_SUCCESS)
|
|
FAIL("C VerifyState(%s line %d): state function returned error %d\n", filename, lnum, state.status);
|
|
|
|
rdiff = StateVectorDiff((r_thresh > 0.0), pos, state.x, state.y, state.z);
|
|
if (rdiff > *max_rdiff)
|
|
*max_rdiff = rdiff;
|
|
|
|
vdiff = StateVectorDiff((v_thresh > 0.0), vel, state.vx, state.vy, state.vz);
|
|
if (vdiff > *max_vdiff)
|
|
*max_vdiff = vdiff;
|
|
|
|
if (rdiff > fabs(r_thresh))
|
|
{
|
|
PrintDiagnostic(state.x, state.y, state.z, pos);
|
|
FAIL("C VerifyState(%s line %d): EXCESSIVE position error = %0.4le\n", filename, lnum, rdiff);
|
|
}
|
|
|
|
if (vdiff > fabs(v_thresh))
|
|
{
|
|
PrintDiagnostic(state.vx, state.vy, state.vz, vel);
|
|
FAIL("C VerifyState(%s line %d): EXCESSIVE velocity error = %0.4le\n", filename, lnum, vdiff);
|
|
}
|
|
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
static int VerifyStateBody(
|
|
verify_state_context_t *context,
|
|
astro_body_t body,
|
|
const char *filename,
|
|
double r_thresh,
|
|
double v_thresh)
|
|
{
|
|
int error, lnum, nscanned;
|
|
int found_begin = 0;
|
|
int found_end = 0;
|
|
int count = 0;
|
|
int part = 0;
|
|
FILE *infile = NULL;
|
|
char line[100];
|
|
astro_time_t time;
|
|
double jd, pos[3], vel[3];
|
|
double max_rdiff = 0.0, max_vdiff = 0.0;
|
|
|
|
infile = fopen(filename, "rt");
|
|
if (infile == NULL)
|
|
FFAIL("Cannot open input file: %s\n", filename);
|
|
|
|
lnum = 0;
|
|
while (!found_end && ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++lnum;
|
|
if (!found_begin)
|
|
{
|
|
if (strlen(line) >= 5 && !memcmp(line, "$$SOE", 5))
|
|
found_begin = 1;
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
Input comes in triplets of lines:
|
|
|
|
2444249.500000000 = A.D. 1980-Jan-11 00:00:00.0000 TDB
|
|
X =-3.314860345089456E-01 Y = 8.463418210972562E-01 Z = 3.667227830514760E-01
|
|
VX=-1.642704711077836E-02 VY=-5.494770742558920E-03 VZ=-2.383170237527642E-03
|
|
|
|
Track which of these 3 cases we are in using the 'part' variable...
|
|
*/
|
|
|
|
switch (part)
|
|
{
|
|
case 0:
|
|
if (strlen(line) >= 5 && !memcmp(line, "$$EOE", 5))
|
|
{
|
|
found_end = 1;
|
|
}
|
|
else
|
|
{
|
|
nscanned = sscanf(line, "%lf", &jd);
|
|
if (nscanned != 1)
|
|
FAIL("C VerifyStateBody(%s line %d) ERROR reading Julian date.\n", filename, lnum);
|
|
V(jd);
|
|
|
|
/* Convert julian TT day value to astro_time_t. */
|
|
time = Astronomy_TerrestrialTime(jd - JD_2000);
|
|
}
|
|
break;
|
|
|
|
case 1:
|
|
nscanned = sscanf(line, " X =%lf Y =%lf Z =%lf", &pos[0], &pos[1], &pos[2]);
|
|
if (nscanned != 3)
|
|
FAIL("C VerifyStateBody(%s line %d) ERROR reading position vector.\n", filename, lnum);
|
|
V(pos[0]);
|
|
V(pos[1]);
|
|
V(pos[2]);
|
|
break;
|
|
|
|
case 2:
|
|
nscanned = sscanf(line, " VX=%lf VY=%lf VZ=%lf", &vel[0], &vel[1], &vel[2]);
|
|
if (nscanned != 3)
|
|
FAIL("C VerifyStateBody(%s line %d) ERROR reading velocity vector.\n", filename, lnum);
|
|
V(vel[0]);
|
|
V(vel[1]);
|
|
V(vel[2]);
|
|
CHECK(VerifyState(context, &max_rdiff, &max_vdiff, body, filename, lnum, time, pos, vel, r_thresh, v_thresh));
|
|
++count;
|
|
break;
|
|
|
|
default:
|
|
FFAIL("INTERNAL ERROR : part=%d\n", part);
|
|
}
|
|
|
|
part = (part + 1) % 3;
|
|
}
|
|
}
|
|
|
|
DEBUG("C VerifyStateBody(%-22s): PASS - Tested %d cases. max rdiff=%0.3le, vdiff=%0.3le\n", filename, count, max_rdiff, max_vdiff);
|
|
error = 0;
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
/* Constants for use inside unit tests only; they doesn't make sense for public consumption. */
|
|
#define BODY_GEOMOON ((astro_body_t)(-100))
|
|
#define BODY_GEO_EMB ((astro_body_t)(-101))
|
|
|
|
static astro_state_vector_t BaryState(verify_state_context_t *context, astro_body_t body, astro_time_t time)
|
|
{
|
|
(void)context;
|
|
|
|
if (body == BODY_GEOMOON)
|
|
return Astronomy_GeoMoonState(time);
|
|
|
|
if (body == BODY_GEO_EMB)
|
|
return Astronomy_GeoEmbState(time);
|
|
|
|
return Astronomy_BaryState(body, time);
|
|
}
|
|
|
|
static int BaryStateTest(void)
|
|
{
|
|
int error; /* set as a side-effect of CHECK macro */
|
|
verify_state_context_t context;
|
|
|
|
memset(&context, 0, sizeof(context));
|
|
context.func = BaryState;
|
|
|
|
CHECK(VerifyStateBody(&context, BODY_SUN, "barystate/Sun.txt", -1.224e-05, -1.134e-07));
|
|
CHECK(VerifyStateBody(&context, BODY_MERCURY, "barystate/Mercury.txt", 1.672e-04, 2.698e-04));
|
|
CHECK(VerifyStateBody(&context, BODY_VENUS, "barystate/Venus.txt", 4.123e-05, 4.308e-05));
|
|
CHECK(VerifyStateBody(&context, BODY_EARTH, "barystate/Earth.txt", 2.296e-05, 6.359e-05));
|
|
CHECK(VerifyStateBody(&context, BODY_MARS, "barystate/Mars.txt", 3.107e-05, 5.550e-05));
|
|
CHECK(VerifyStateBody(&context, BODY_JUPITER, "barystate/Jupiter.txt", 7.389e-05, 2.471e-04));
|
|
CHECK(VerifyStateBody(&context, BODY_SATURN, "barystate/Saturn.txt", 1.067e-04, 3.220e-04));
|
|
CHECK(VerifyStateBody(&context, BODY_URANUS, "barystate/Uranus.txt", 9.035e-05, 2.519e-04));
|
|
CHECK(VerifyStateBody(&context, BODY_NEPTUNE, "barystate/Neptune.txt", 9.838e-05, 4.446e-04));
|
|
CHECK(VerifyStateBody(&context, BODY_PLUTO, "barystate/Pluto.txt", 4.259e-05, 7.827e-05));
|
|
CHECK(VerifyStateBody(&context, BODY_MOON, "barystate/Moon.txt", 2.354e-05, 6.604e-05));
|
|
CHECK(VerifyStateBody(&context, BODY_EMB, "barystate/EMB.txt", 2.353e-05, 6.511e-05));
|
|
CHECK(VerifyStateBody(&context, BODY_GEOMOON, "barystate/GeoMoon.txt", 4.086e-05, 5.347e-05));
|
|
CHECK(VerifyStateBody(&context, BODY_GEO_EMB, "barystate/GeoEMB.txt", 4.076e-05, 5.335e-05));
|
|
|
|
FPASS();
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static astro_state_vector_t HelioStateFunc(verify_state_context_t *context, astro_body_t body, astro_time_t time)
|
|
{
|
|
(void)context;
|
|
return Astronomy_HelioState(body, time);
|
|
}
|
|
|
|
static int HelioStateTest(void)
|
|
{
|
|
int error; /* set as a side-effect of CHECK macro */
|
|
verify_state_context_t context;
|
|
|
|
memset(&context, 0, sizeof(context));
|
|
context.func = HelioStateFunc;
|
|
|
|
CHECK(VerifyStateBody(&context, BODY_SSB, "heliostate/SSB.txt", -1.209e-05, -1.125e-07));
|
|
CHECK(VerifyStateBody(&context, BODY_MERCURY, "heliostate/Mercury.txt", 1.481e-04, 2.756e-04));
|
|
CHECK(VerifyStateBody(&context, BODY_VENUS, "heliostate/Venus.txt", 3.528e-05, 4.485e-05));
|
|
CHECK(VerifyStateBody(&context, BODY_EARTH, "heliostate/Earth.txt", 1.476e-05, 6.105e-05));
|
|
CHECK(VerifyStateBody(&context, BODY_MARS, "heliostate/Mars.txt", 3.154e-05, 5.603e-05));
|
|
CHECK(VerifyStateBody(&context, BODY_JUPITER, "heliostate/Jupiter.txt", 7.455e-05, 2.562e-04));
|
|
CHECK(VerifyStateBody(&context, BODY_SATURN, "heliostate/Saturn.txt", 1.066e-04, 3.150e-04));
|
|
CHECK(VerifyStateBody(&context, BODY_URANUS, "heliostate/Uranus.txt", 9.034e-05, 2.712e-04));
|
|
CHECK(VerifyStateBody(&context, BODY_NEPTUNE, "heliostate/Neptune.txt", 9.834e-05, 4.534e-04));
|
|
CHECK(VerifyStateBody(&context, BODY_PLUTO, "heliostate/Pluto.txt", 4.271e-05, 1.198e-04));
|
|
CHECK(VerifyStateBody(&context, BODY_MOON, "heliostate/Moon.txt", 1.477e-05, 6.195e-05));
|
|
CHECK(VerifyStateBody(&context, BODY_EMB, "heliostate/EMB.txt", 1.476e-05, 6.106e-05));
|
|
|
|
FPASS();
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static astro_state_vector_t LagrangeFunc(verify_state_context_t *context, astro_body_t minor_body, astro_time_t time)
|
|
{
|
|
return Astronomy_LagrangePoint(
|
|
context->point,
|
|
time,
|
|
context->major,
|
|
minor_body
|
|
);
|
|
}
|
|
|
|
|
|
static int VerifyStateLagrange(
|
|
astro_body_t majorBody,
|
|
astro_body_t minorBody,
|
|
int point,
|
|
const char *filename,
|
|
double r_thresh,
|
|
double v_thresh)
|
|
{
|
|
verify_state_context_t context;
|
|
memset(&context, 0, sizeof(context));
|
|
context.func = LagrangeFunc;
|
|
context.major = majorBody;
|
|
context.point = point;
|
|
return VerifyStateBody(&context, minorBody, filename, r_thresh, v_thresh);
|
|
}
|
|
|
|
|
|
static int VerifyEquilateral(
|
|
const char *kind,
|
|
astro_body_t major_body,
|
|
astro_body_t minor_body,
|
|
int point,
|
|
astro_time_t time,
|
|
double mx,
|
|
double my,
|
|
double mz,
|
|
double px,
|
|
double py,
|
|
double pz,
|
|
double *max_diff,
|
|
double *max_arcmin)
|
|
{
|
|
int error = 0;
|
|
const double LENGTH_TOLERANCE = 1.0e-15;
|
|
const double ARCMIN_TOLERANCE = 3.0e-12;
|
|
double dx, dy, dz, dlength;
|
|
double mlength, plength, diff, dotprod;
|
|
double arcmin;
|
|
char tag[100];
|
|
|
|
snprintf(tag, sizeof(tag),
|
|
"C VerifyEquilateral(%s,%s,%s,L%d,tt=%0.3lf)",
|
|
kind,
|
|
Astronomy_BodyName(major_body),
|
|
Astronomy_BodyName(minor_body),
|
|
point,
|
|
time.tt
|
|
);
|
|
|
|
/* Verify minor body vector and Lagrange point vector are the same length. */
|
|
mlength = sqrt(mx*mx + my*my + mz*mz);
|
|
plength = sqrt(px*px + py*py + pz*pz);
|
|
diff = ABS((plength - mlength) / mlength);
|
|
if (diff > LENGTH_TOLERANCE)
|
|
FAIL("%s: FAIL mlength = %0.6le, plength = %0.6le, diff = %0.6le\n", tag, mlength, plength, diff);
|
|
if (diff > *max_diff)
|
|
*max_diff = diff;
|
|
|
|
/* Verify the third triangle leg (minor body to Lagrange point) is also the same length. */
|
|
dx = px - mx;
|
|
dy = py - my;
|
|
dz = pz - mz;
|
|
dlength = sqrt(dx*dx + dy*dy + dz*dz);
|
|
diff = ABS((dlength - mlength) / mlength);
|
|
if (diff > LENGTH_TOLERANCE)
|
|
FAIL("%s: FAIL mlength = %0.6le, dlength = %0.6le, diff = %0.6le\n", tag, mlength, dlength, diff);
|
|
if (diff > *max_diff)
|
|
*max_diff = diff;
|
|
|
|
/* The 3 mutual angles should all be 60 degrees. */
|
|
dotprod = (mx*px + my*py + mz*pz) / (mlength * plength);
|
|
arcmin = ABS(60.0 * (60.0 - ArcCos(dotprod)));
|
|
if (arcmin > ARCMIN_TOLERANCE)
|
|
FAIL("%s: FAIL m/p angle error = %0.6le arcmin.\n", tag, arcmin);
|
|
if (arcmin > *max_arcmin)
|
|
*max_arcmin = arcmin;
|
|
|
|
dotprod = -(mx*dx + my*dy + mz*dz) / (mlength * dlength); /* negate because pointing opposite dirs */
|
|
arcmin = ABS(60.0 * (60.0 - ArcCos(dotprod)));
|
|
if (arcmin > ARCMIN_TOLERANCE)
|
|
FAIL("%s: FAIL m/d angle error = %0.6le arcmin.\n", tag, arcmin);
|
|
if (arcmin > *max_arcmin)
|
|
*max_arcmin = arcmin;
|
|
|
|
dotprod = (px*dx + py*dy + pz*dz) / (plength * dlength); /* negate because pointing opposite dirs */
|
|
arcmin = ABS(60.0 * (60.0 - ArcCos(dotprod)));
|
|
if (arcmin > ARCMIN_TOLERANCE)
|
|
FAIL("%s: FAIL p/d angle error = %0.6le arcmin.\n", tag, arcmin);
|
|
if (arcmin > *max_arcmin)
|
|
*max_arcmin = arcmin;
|
|
|
|
fail:
|
|
if (error != 0) printf("%s: returning %d\n", tag, error);
|
|
return error;
|
|
}
|
|
|
|
|
|
static int VerifyLagrangeTriangle(astro_body_t major_body, astro_body_t minor_body, int point)
|
|
{
|
|
int error, count;
|
|
char tag[100];
|
|
astro_state_vector_t major_state, minor_state, point_state;
|
|
double major_mass, minor_mass;
|
|
const double tt1 = 7335.5; /* 2020-02-01T00:00Z */
|
|
const double tt2 = 7425.5; /* 2020-05-01T00:00Z */
|
|
const double dt = 0.125; /* 1/8 is exactly represented in binary */
|
|
astro_time_t time;
|
|
double max_pos_diff = 0.0, max_pos_arcmin = 0.0;
|
|
double max_vel_diff = 0.0, max_vel_arcmin = 0.0;
|
|
|
|
snprintf(tag, sizeof(tag),
|
|
"C VerifyLagrangeTriangle(%s,%s,L%d)",
|
|
Astronomy_BodyName(major_body),
|
|
Astronomy_BodyName(minor_body),
|
|
point
|
|
);
|
|
|
|
if (point != 4 && point != 5)
|
|
FAIL("%s: Invalid Lagrange point %d\n", tag, point);
|
|
|
|
major_mass = Astronomy_MassProduct(major_body);
|
|
if (major_mass <= 0.0)
|
|
FAIL("%s: Invalid mass product for major body.\n", tag);
|
|
|
|
minor_mass = Astronomy_MassProduct(minor_body);
|
|
if (minor_mass <= 0.0)
|
|
FAIL("%s: Invalid mass product for minor body.\n", tag);
|
|
|
|
count = 0;
|
|
time = Astronomy_TerrestrialTime(tt1);
|
|
while (time.tt <= tt2)
|
|
{
|
|
++count;
|
|
|
|
major_state = Astronomy_HelioState(major_body, time);
|
|
if (major_state.status != ASTRO_SUCCESS)
|
|
FAIL("%s: HelioState failed for major body.\n", tag);
|
|
|
|
minor_state = Astronomy_HelioState(minor_body, time);
|
|
if (minor_state.status != ASTRO_SUCCESS)
|
|
FAIL("%s: HelioState failed for minor body.\n", tag);
|
|
|
|
point_state = Astronomy_LagrangePointFast(point, major_state, major_mass, minor_state, minor_mass);
|
|
if (point_state.status != ASTRO_SUCCESS)
|
|
FAIL("%s: Astronomy_LagrangePoint returned status = %d\n", tag, point_state.status);
|
|
|
|
/* Verify the (major, minor, L4/L5) triangle is equilateral, both in position and velocity. */
|
|
CHECK(VerifyEquilateral(
|
|
"position",
|
|
major_body,
|
|
minor_body,
|
|
point,
|
|
time,
|
|
minor_state.x - major_state.x,
|
|
minor_state.y - major_state.y,
|
|
minor_state.z - major_state.z,
|
|
point_state.x,
|
|
point_state.y,
|
|
point_state.z,
|
|
&max_pos_diff,
|
|
&max_pos_arcmin
|
|
));
|
|
|
|
CHECK(VerifyEquilateral(
|
|
"velocity",
|
|
major_body,
|
|
minor_body,
|
|
point,
|
|
time,
|
|
minor_state.vx - major_state.vx,
|
|
minor_state.vy - major_state.vy,
|
|
minor_state.vz - major_state.vz,
|
|
point_state.vx,
|
|
point_state.vy,
|
|
point_state.vz,
|
|
&max_vel_diff,
|
|
&max_vel_arcmin
|
|
));
|
|
|
|
time = Astronomy_TerrestrialTime(time.tt + dt);
|
|
}
|
|
|
|
if (Verbose)
|
|
{
|
|
printf("%s: PASS (%d cases)\n", tag, count);
|
|
printf(" max_pos_diff = %0.3le, max_vel_diff = %0.3le\n", max_pos_diff, max_vel_diff);
|
|
printf(" max_pos_arcmin = %0.3le, max_vel_arcmin = %0.3le\n", max_pos_arcmin, max_vel_arcmin);
|
|
}
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
|
|
static int VerifyGeoMoon(const char *filename)
|
|
{
|
|
verify_state_context_t context;
|
|
|
|
memset(&context, 0, sizeof(context));
|
|
context.func = BaryState;
|
|
return VerifyStateBody(&context, BODY_GEOMOON, filename, 3.777e-5, 5.047e-5);
|
|
}
|
|
|
|
|
|
static double ErrorArcmin(
|
|
double ax, double ay, double az,
|
|
double bx, double by, double bz)
|
|
{
|
|
double dx = ax - bx;
|
|
double dy = ay - by;
|
|
double dz = az - bz;
|
|
double error = sqrt(dx*dx + dy*dy + dz*dz);
|
|
double mag = sqrt(ax*ax + ay*ay + az*az);
|
|
return (error / mag) * (RAD2DEG * 60.0);
|
|
}
|
|
|
|
|
|
static int LagrangeJplGeoMoon(const char *mb_filename, const char *lp_filename, int point)
|
|
{
|
|
int error, i;
|
|
double major_mass, minor_mass;
|
|
astro_state_vector_t major;
|
|
astro_state_vector_t m, p, q;
|
|
state_vector_batch_t mb = EmptyStateVectorBatch();
|
|
state_vector_batch_t lp = EmptyStateVectorBatch();
|
|
double arcmin;
|
|
double max_pos_arcmin = 0.0;
|
|
double max_vel_arcmin = 0.0;
|
|
|
|
/* The major body is the Earth, and we do everything geocentrically. */
|
|
major.x = major.y = major.z = 0.0;
|
|
major.vx = major.vy = major.vz = 0.0;
|
|
memset(&major.t, 0, sizeof(major.t));
|
|
major.status = ASTRO_SUCCESS;
|
|
|
|
major_mass = Astronomy_MassProduct(BODY_EARTH);
|
|
if (major_mass <= 0.0)
|
|
FAIL("LagrangeJplGeoMoon: cannot find Earth mass\n");
|
|
|
|
minor_mass = Astronomy_MassProduct(BODY_MOON);
|
|
if (minor_mass <= 0.0)
|
|
FAIL("LagrangeJplGeoMoon: cannot find Moon mass\n");
|
|
|
|
/* Use state vectors provided by JPL to calculate Lagrange points. */
|
|
/* Compare calculated Lagrange points against JPL Lagrange points. */
|
|
|
|
CHECK(LoadStateVectors(&mb, mb_filename));
|
|
CHECK(LoadStateVectors(&lp, lp_filename));
|
|
if (mb.length != lp.length)
|
|
FFAIL("%s has %d states, but %s has %d\n", mb_filename, mb.length, lp_filename, lp.length);
|
|
|
|
for (i = 0; i < mb.length; ++i)
|
|
{
|
|
m = mb.array[i];
|
|
p = lp.array[i];
|
|
if (m.t.tt != p.t.tt)
|
|
FAIL("C LagrangeJplGeoMoon(%s): mismatching time: %0.16lf != %0.16lf.\n", lp_filename, m.t.tt, p.t.tt);
|
|
|
|
major.t = m.t;
|
|
q = Astronomy_LagrangePointFast(point, major, major_mass, m, minor_mass);
|
|
if (q.status != ASTRO_SUCCESS)
|
|
FAIL("C LagrangeJplGeoMoon(%s): Astronomy_LagrangePoint returned status %d.\n", lp_filename, q.status);
|
|
|
|
arcmin = ErrorArcmin(p.x, p.y, p.z, q.x, q.y, q.z);
|
|
if (arcmin > max_pos_arcmin)
|
|
max_pos_arcmin = arcmin;
|
|
if (arcmin > 4.9e-5)
|
|
FAIL("C LagrangeJplGeoMoon(%s, %d): EXCESSIVE position error = %0.6lf arcmin.\n", lp_filename, i, arcmin);
|
|
|
|
arcmin = ErrorArcmin(p.vx, p.vy, p.vz, q.vx, q.vy, q.vz);
|
|
if (arcmin > max_vel_arcmin)
|
|
max_vel_arcmin = arcmin;
|
|
if (arcmin > 5.45) /* !!! REPORT TO JPL !!! */
|
|
FAIL("C LagrangeJplGeoMoon(%s, %d): EXCESSIVE velocity error = %0.6lf arcmin.\n", lp_filename, i, arcmin);
|
|
}
|
|
|
|
DEBUG("C LagrangeJplGeoMoon(%s): PASS: %d cases, max pos arcmin = %0.16lf, max vel arcmin = %0.16lf\n", lp_filename, mb.length, max_pos_arcmin, max_vel_arcmin);
|
|
fail:
|
|
FreeStateVectorBatch(&mb);
|
|
FreeStateVectorBatch(&lp);
|
|
return error;
|
|
}
|
|
|
|
|
|
static int LagrangeTest(void)
|
|
{
|
|
int error; /* set as a side-effect of CHECK macro */
|
|
|
|
/* Before verifying against JPL values, do self-consistency checks for L4/L5. */
|
|
CHECK(VerifyLagrangeTriangle(BODY_EARTH, BODY_MOON, 4));
|
|
CHECK(VerifyLagrangeTriangle(BODY_EARTH, BODY_MOON, 5));
|
|
|
|
/* Make sure our geocentric moon calculations match JPL's. */
|
|
CHECK(VerifyGeoMoon("lagrange/geo_moon.txt"));
|
|
|
|
/* Try feeding JPL states into Astronomy_LagrangePoint(). */
|
|
CHECK(LagrangeJplGeoMoon("lagrange/geo_moon.txt", "lagrange/em_L4.txt", 4));
|
|
CHECK(LagrangeJplGeoMoon("lagrange/geo_moon.txt", "lagrange/em_L5.txt", 5));
|
|
|
|
/* NOTE: JPL Horizons does not provide L3 calculations. */
|
|
|
|
CHECK(VerifyStateLagrange(BODY_SUN, BODY_EMB, 1, "lagrange/semb_L1.txt", 1.33e-5, 6.13e-5));
|
|
CHECK(VerifyStateLagrange(BODY_SUN, BODY_EMB, 2, "lagrange/semb_L2.txt", 1.33e-5, 6.13e-5));
|
|
CHECK(VerifyStateLagrange(BODY_SUN, BODY_EMB, 4, "lagrange/semb_L4.txt", 3.75e-5, 5.28e-5));
|
|
CHECK(VerifyStateLagrange(BODY_SUN, BODY_EMB, 5, "lagrange/semb_L5.txt", 3.75e-5, 5.28e-5));
|
|
|
|
CHECK(VerifyStateLagrange(BODY_EARTH, BODY_MOON, 1, "lagrange/em_L1.txt", 3.79e-5, 5.06e-5));
|
|
CHECK(VerifyStateLagrange(BODY_EARTH, BODY_MOON, 2, "lagrange/em_L2.txt", 3.79e-5, 5.06e-5));
|
|
CHECK(VerifyStateLagrange(BODY_EARTH, BODY_MOON, 4, "lagrange/em_L4.txt", 3.79e-5, 1.59e-3));
|
|
CHECK(VerifyStateLagrange(BODY_EARTH, BODY_MOON, 5, "lagrange/em_L5.txt", 3.79e-5, 1.59e-3));
|
|
|
|
FPASS();
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int LoadStateVectors(
|
|
state_vector_batch_t *batch,
|
|
const char *filename)
|
|
{
|
|
int error, lnum, nscanned;
|
|
int found_begin = 0;
|
|
int found_end = 0;
|
|
int part = 0;
|
|
FILE *infile = NULL;
|
|
char line[100];
|
|
double jd;
|
|
astro_state_vector_t state;
|
|
|
|
memset(&state, 0, sizeof(state));
|
|
|
|
infile = fopen(filename, "rt");
|
|
if (infile == NULL)
|
|
FFAIL("Cannot open input file: %s\n", filename);
|
|
|
|
lnum = 0;
|
|
while (!found_end && ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++lnum;
|
|
if (!found_begin)
|
|
{
|
|
if (strlen(line) >= 5 && !memcmp(line, "$$SOE", 5))
|
|
found_begin = 1;
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
Input comes in triplets of lines:
|
|
|
|
2444249.500000000 = A.D. 1980-Jan-11 00:00:00.0000 TDB
|
|
X =-3.314860345089456E-01 Y = 8.463418210972562E-01 Z = 3.667227830514760E-01
|
|
VX=-1.642704711077836E-02 VY=-5.494770742558920E-03 VZ=-2.383170237527642E-03
|
|
|
|
Track which of these 3 cases we are in using the 'part' variable...
|
|
*/
|
|
|
|
switch (part)
|
|
{
|
|
case 0:
|
|
if (strlen(line) >= 5 && !memcmp(line, "$$EOE", 5))
|
|
{
|
|
found_end = 1;
|
|
}
|
|
else
|
|
{
|
|
nscanned = sscanf(line, "%lf", &jd);
|
|
if (nscanned != 1)
|
|
FAIL("C LoadStateVectors(%s line %d) ERROR reading Julian date.\n", filename, lnum);
|
|
V(jd);
|
|
|
|
/* Convert julian TT day value to astro_time_t. */
|
|
state.t = Astronomy_TerrestrialTime(jd - JD_2000);
|
|
}
|
|
break;
|
|
|
|
case 1:
|
|
nscanned = sscanf(line, " X =%lf Y =%lf Z =%lf", &state.x, &state.y, &state.z);
|
|
if (nscanned != 3)
|
|
FAIL("C LoadStateVectors(%s line %d) ERROR reading position vector.\n", filename, lnum);
|
|
V(state.x);
|
|
V(state.y);
|
|
V(state.z);
|
|
break;
|
|
|
|
case 2:
|
|
nscanned = sscanf(line, " VX=%lf VY=%lf VZ=%lf", &state.vx, &state.vy, &state.vz);
|
|
if (nscanned != 3)
|
|
FAIL("C LoadStateVectors(%s line %d) ERROR reading velocity vector.\n", filename, lnum);
|
|
V(state.vx);
|
|
V(state.vy);
|
|
V(state.vz);
|
|
state.status = ASTRO_SUCCESS;
|
|
CHECK(AppendStateVector(batch, state));
|
|
break;
|
|
|
|
default:
|
|
FFAIL("INTERNAL ERROR : part=%d\n", part);
|
|
}
|
|
|
|
part = (part + 1) % 3;
|
|
}
|
|
}
|
|
|
|
error = 0;
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
|
|
static astro_vector_t CrossProduct(astro_vector_t a, astro_vector_t b)
|
|
{
|
|
astro_vector_t c;
|
|
|
|
c.status = ASTRO_SUCCESS;
|
|
c.t = a.t;
|
|
c.x = a.y*b.z - a.z*b.y;
|
|
c.y = a.z*b.x - a.x*b.z;
|
|
c.z = a.x*b.y - a.y*b.x;
|
|
|
|
return c;
|
|
}
|
|
|
|
|
|
static int LagrangeJplAnalyzeFiles(
|
|
const char *mb_filename, /* filename containing minor body state vectors relative to major body */
|
|
const char *lp_filename, /* filename containing Lagrange point state vectors relative to major body */
|
|
int point) /* Lagrange point 1..5 */
|
|
{
|
|
int error, i;
|
|
state_vector_batch_t mb = EmptyStateVectorBatch();
|
|
state_vector_batch_t lp = EmptyStateVectorBatch();
|
|
double pos_mag_ratio, vel_mag_ratio, angle;
|
|
double pos_dev, vel_dev, angle_dev;
|
|
double dr, dv, da;
|
|
double m_mag, p_mag;
|
|
astro_state_vector_t m, p;
|
|
|
|
/*
|
|
[Don Cross - 2022-02-12]
|
|
I am trying to understand how JPL Horizons calculates L4 and L5.
|
|
So I generated data for heliocentric EMB state vectors
|
|
and geocentric Moon vectors. This should be enough to run statistics
|
|
on distance ratios, angles, and planes of alignment (normal vectors).
|
|
|
|
Quantities I would like to find means and standard deviations for:
|
|
1. Distance from the major and minor bodies.
|
|
2. Angle away from the vector from the major body toward the minor body.
|
|
3. (L4/L5 only) Normal vector to the plane that contains both L4/L5 vector and minor body vector.
|
|
*/
|
|
|
|
CHECK(LoadStateVectors(&mb, mb_filename));
|
|
CHECK(LoadStateVectors(&lp, lp_filename));
|
|
if (mb.length != lp.length)
|
|
FFAIL("%d state vectors in %s, but %d in %s\n", mb.length, mb_filename, lp.length, lp_filename);
|
|
|
|
if (mb.length < 10)
|
|
FAIL("C LagrangeJplAnalyzeFiles(%s): %d state vectors is not enough for statistical analysis.\n", mb_filename, mb.length);
|
|
|
|
/* Calculate mean values. */
|
|
angle = pos_mag_ratio = vel_mag_ratio = 0.0;
|
|
for (i = 0; i < mb.length; ++i)
|
|
{
|
|
m = mb.array[i];
|
|
p = lp.array[i];
|
|
|
|
/* Sanity check that the time offsets match between the two files. */
|
|
if (m.t.tt != p.t.tt)
|
|
FAIL("C LagrangeJplAnalyzeFiles(%d, %s): time mismatch", i, lp_filename);
|
|
|
|
m_mag = sqrt(m.x*m.x + m.y*m.y + m.z*m.z);
|
|
p_mag = sqrt(p.x*p.x + p.y*p.y + p.z*p.z);
|
|
pos_mag_ratio += p_mag/m_mag;
|
|
vel_mag_ratio += sqrt((p.vx*p.vx + p.vy*p.vy + p.vz*p.vz) / (m.vx*m.vx + m.vy*m.vy + m.vz*m.vz));
|
|
angle += ArcCos((m.x*p.x + m.y*p.y + m.z*p.z) / (m_mag * p_mag));
|
|
}
|
|
pos_mag_ratio /= mb.length;
|
|
vel_mag_ratio /= mb.length;
|
|
angle /= mb.length;
|
|
|
|
/* Calculate standard deviations. */
|
|
pos_dev = vel_dev = angle_dev = 0.0;
|
|
for (i = 0; i < mb.length; ++i)
|
|
{
|
|
m = mb.array[i];
|
|
p = lp.array[i];
|
|
m_mag = sqrt(m.x*m.x + m.y*m.y + m.z*m.z);
|
|
p_mag = sqrt(p.x*p.x + p.y*p.y + p.z*p.z);
|
|
dr = pos_mag_ratio - p_mag/m_mag;
|
|
dv = vel_mag_ratio - sqrt((p.vx*p.vx + p.vy*p.vy + p.vz*p.vz) / (m.vx*m.vx + m.vy*m.vy + m.vz*m.vz));
|
|
da = angle - ArcCos((m.x*p.x + m.y*p.y + m.z*p.z) / (m_mag * p_mag));
|
|
pos_dev += (dr * dr);
|
|
vel_dev += (dv * dv);
|
|
angle_dev += (da * da);
|
|
}
|
|
pos_dev = sqrt(pos_dev / mb.length);
|
|
vel_dev = sqrt(vel_dev / mb.length);
|
|
angle_dev = sqrt(angle_dev / mb.length);
|
|
|
|
printf("C LagrangeJplAnalyzeFiles(%s): %d samples\n", lp_filename, mb.length);
|
|
printf(" mag [mean = %0.8lf, dev = %0.8lf]; vel [mean = %0.8lf, dev = %0.8lf]\n",
|
|
pos_mag_ratio, pos_dev,
|
|
vel_mag_ratio, vel_dev);
|
|
printf(" angle [mean = %0.8lf, dev = %0.8lf]\n",
|
|
angle, angle_dev);
|
|
|
|
/* Special case for L4, L5: try to understand the instantaneous co-orbital plane. */
|
|
if (point == 4 || point == 5)
|
|
{
|
|
double max_pole_diff = 0.0;
|
|
astro_angle_result_t pole_diff;
|
|
double v_mag;
|
|
astro_vector_t m_unit, p_unit, mp_norm;
|
|
astro_vector_t v_unit, mv_norm;
|
|
m_unit.status = p_unit.status = v_unit.status = ASTRO_SUCCESS;
|
|
|
|
for (i = 0; i < mb.length; ++i)
|
|
{
|
|
m = mb.array[i];
|
|
p = lp.array[i];
|
|
|
|
m_mag = sqrt(m.x*m.x + m.y*m.y + m.z*m.z);
|
|
m_unit.x = m.x / m_mag;
|
|
m_unit.y = m.y / m_mag;
|
|
m_unit.z = m.z / m_mag;
|
|
m_unit.t = m.t;
|
|
|
|
p_mag = sqrt(p.x*p.x + p.y*p.y + p.z*p.z);
|
|
p_unit.x = p.x / p_mag;
|
|
p_unit.y = p.y / p_mag;
|
|
p_unit.z = p.z / p_mag;
|
|
p_unit.t = p.t;
|
|
|
|
v_mag = sqrt(m.vx*m.vx + m.vy*m.vy + m.vz*m.vz);
|
|
v_unit.x = m.vx / v_mag;
|
|
v_unit.y = m.vy / v_mag;
|
|
v_unit.z = m.vz / v_mag;
|
|
v_unit.t = m.t;
|
|
|
|
if (point == 4)
|
|
mp_norm = CrossProduct(m_unit, p_unit);
|
|
else
|
|
mp_norm = CrossProduct(p_unit, m_unit);
|
|
|
|
mv_norm = CrossProduct(m_unit, v_unit);
|
|
pole_diff = Astronomy_AngleBetween(mp_norm, mv_norm);
|
|
CHECK_STATUS(pole_diff);
|
|
if (pole_diff.angle > max_pole_diff)
|
|
max_pole_diff = pole_diff.angle;
|
|
}
|
|
printf(" max L%d pole diff angle = %0.6lf degrees.\n", point, max_pole_diff);
|
|
}
|
|
|
|
/* Special case for L4, L5: confirm velocity vector would leave distances as an equilateral triangle. */
|
|
/* cppcheck-suppress[duplicateCondition] */
|
|
if (point == 4 || point == 5)
|
|
{
|
|
double a, b, c; /* distances before v*dt increment */
|
|
double dx, dy, dz;
|
|
double ratio;
|
|
double min_ratio_before = NAN, max_ratio_before = NAN;
|
|
double min_ratio_after = NAN, max_ratio_after = NAN;
|
|
const double dt = 1.0;
|
|
|
|
for (i = 0; i < mb.length; ++i)
|
|
{
|
|
m = mb.array[i];
|
|
p = lp.array[i];
|
|
|
|
/* a = distance from major body to minor body */
|
|
a = sqrt(m.x*m.x + m.y*m.y + m.z*m.z);
|
|
|
|
/* b = distance from major body to L4/L5 */
|
|
b = sqrt(p.x*p.x + p.y*p.y + p.z*p.z);
|
|
|
|
/* c = distance from minor body to L4/L5 */
|
|
dx = p.x - m.x;
|
|
dy = p.y - m.y;
|
|
dz = p.z - m.z;
|
|
c = sqrt(dx*dx + dy*dy + dz*dz);
|
|
|
|
ratio = b / a;
|
|
if (i == 0)
|
|
{
|
|
min_ratio_before = max_ratio_before = ratio;
|
|
}
|
|
else
|
|
{
|
|
if (ratio < min_ratio_before)
|
|
min_ratio_before = ratio;
|
|
if (ratio > max_ratio_before)
|
|
max_ratio_before = ratio;
|
|
}
|
|
|
|
ratio = c / a;
|
|
if (ratio < min_ratio_before)
|
|
min_ratio_before = ratio;
|
|
if (ratio > max_ratio_before)
|
|
max_ratio_before = ratio;
|
|
|
|
ratio = c / b;
|
|
if (ratio < min_ratio_before)
|
|
min_ratio_before = ratio;
|
|
if (ratio > max_ratio_before)
|
|
max_ratio_before = ratio;
|
|
|
|
/* simulate a small movement of the body over a time period dt */
|
|
dx = m.x + dt*m.vx;
|
|
dy = m.y + dt*m.vy;
|
|
dz = m.z + dt*m.vz;
|
|
a = sqrt(dx*dx + dy*dy + dz*dz);
|
|
|
|
/* simulate straight-line movement of Lagrange point over dt */
|
|
dx = p.x + dt*p.vx;
|
|
dy = p.y + dt*p.vy;
|
|
dz = p.z + dt*p.vz;
|
|
b = sqrt(dx*dx + dy*dy + dz*dz);
|
|
|
|
/* measure extrapolated distance between minor body and Lagrange point */
|
|
dx -= m.x + dt*m.vx;
|
|
dy -= m.y + dt*m.vy;
|
|
dz -= m.z + dt*m.vz;
|
|
c = sqrt(dx*dx + dy*dy + dz*dz);
|
|
|
|
/* All 3 distances (a, b, c) should be the same. */
|
|
|
|
ratio = b / a;
|
|
if (i == 0)
|
|
{
|
|
min_ratio_after = max_ratio_after = ratio;
|
|
}
|
|
else
|
|
{
|
|
if (ratio < min_ratio_after)
|
|
min_ratio_after = ratio;
|
|
if (ratio > max_ratio_after)
|
|
max_ratio_after = ratio;
|
|
}
|
|
|
|
ratio = c / a;
|
|
if (ratio < min_ratio_after)
|
|
min_ratio_after = ratio;
|
|
if (ratio > max_ratio_after)
|
|
max_ratio_after = ratio;
|
|
|
|
ratio = c / b;
|
|
if (ratio < min_ratio_after)
|
|
min_ratio_after = ratio;
|
|
if (ratio > max_ratio_after)
|
|
max_ratio_after = ratio;
|
|
|
|
if (ABS(1.0 - min_ratio_after) > 1.0e-7)
|
|
FAIL("LagrangeJplAnalyzeFiles(%s): BAD min_ratio_after = %0.15lf\n", lp_filename, min_ratio_after);
|
|
|
|
if (ABS(1.0 - max_ratio_after) > 1.0e-7)
|
|
FAIL("LagrangeJplAnalyzeFiles(%s): BAD max_ratio_after = %0.15lf\n", lp_filename, max_ratio_after);
|
|
}
|
|
|
|
printf(" length ratios before: min = %0.15lf, max = %0.15lf\n", min_ratio_before, max_ratio_before);
|
|
printf(" length ratios after : min = %0.15lf, max = %0.15lf\n", min_ratio_after, max_ratio_after );
|
|
}
|
|
|
|
/* Special case for L4/L5: confirm velocity vectors are 60 degrees apart. */
|
|
/* cppcheck-suppress[duplicateCondition] */
|
|
if (point == 4 || point == 5)
|
|
{
|
|
double speed, arcmin_error;
|
|
double mx, my, mz;
|
|
double px, py, pz;
|
|
double min_angle = NAN;
|
|
double max_angle = NAN;
|
|
|
|
for (i = 0; i < mb.length; ++i)
|
|
{
|
|
m = mb.array[i];
|
|
p = lp.array[i];
|
|
|
|
/* Calculate unit vector in direction of the minor body's velocity. */
|
|
speed = sqrt(m.vx*m.vx + m.vy*m.vy + m.vz*m.vz);
|
|
mx = m.vx / speed;
|
|
my = m.vy / speed;
|
|
mz = m.vz / speed;
|
|
|
|
/* Calculate unit vector in the direction of the Lagrange point's velocity. */
|
|
speed = sqrt(p.vx*p.vx + p.vy*p.vy + p.vz*p.vz);
|
|
px = p.vx / speed;
|
|
py = p.vy / speed;
|
|
pz = p.vz / speed;
|
|
|
|
/* The dot product should always be very close to 0.5 (an angle of 60 degrees). */
|
|
angle = ArcCos(px*mx + py*my + pz*mz);
|
|
arcmin_error = 60.0 * ABS(angle - 60.0);
|
|
if (arcmin_error > 0.0026)
|
|
FAIL("C LagrangeJplAnalyzeFiles(%d, %s): velocity angle is out of bounds: %0.16lf (error = %0.4le arcmin)\n", i, lp_filename, angle, arcmin_error);
|
|
|
|
if (i == 0)
|
|
{
|
|
min_angle = max_angle = angle;
|
|
}
|
|
else
|
|
{
|
|
if (angle < min_angle)
|
|
min_angle = angle;
|
|
if (angle > max_angle)
|
|
max_angle = angle;
|
|
}
|
|
}
|
|
|
|
printf(" speed angles: min_angle = %0.15lf, max_angle = %0.15lf, spread = %0.4le\n", min_angle, max_angle, max_angle - min_angle);
|
|
}
|
|
|
|
fail:
|
|
FreeStateVectorBatch(&mb);
|
|
FreeStateVectorBatch(&lp);
|
|
return error;
|
|
}
|
|
|
|
|
|
static int LagrangeJplAnalysis(void)
|
|
{
|
|
int error;
|
|
|
|
CHECK(LagrangeJplAnalyzeFiles("lagrange/helio_emb.txt", "lagrange/semb_L1.txt", 1));
|
|
CHECK(LagrangeJplAnalyzeFiles("lagrange/helio_emb.txt", "lagrange/semb_L2.txt", 2));
|
|
CHECK(LagrangeJplAnalyzeFiles("lagrange/helio_emb.txt", "lagrange/semb_L4.txt", 4));
|
|
CHECK(LagrangeJplAnalyzeFiles("lagrange/helio_emb.txt", "lagrange/semb_L5.txt", 5));
|
|
|
|
CHECK(LagrangeJplAnalyzeFiles("lagrange/geo_moon.txt", "lagrange/em_L1.txt", 1));
|
|
CHECK(LagrangeJplAnalyzeFiles("lagrange/geo_moon.txt", "lagrange/em_L2.txt", 2));
|
|
CHECK(LagrangeJplAnalyzeFiles("lagrange/geo_moon.txt", "lagrange/em_L4.txt", 4));
|
|
CHECK(LagrangeJplAnalyzeFiles("lagrange/geo_moon.txt", "lagrange/em_L5.txt", 5));
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static astro_state_vector_t TopoStateFunc(verify_state_context_t *context, astro_body_t body, astro_time_t time)
|
|
{
|
|
astro_observer_t observer;
|
|
astro_state_vector_t observer_state, state;
|
|
|
|
(void)context;
|
|
|
|
observer.latitude = 30.0;
|
|
observer.longitude = -80.0;
|
|
observer.height = 1000.0;
|
|
|
|
observer_state = Astronomy_ObserverState(&time, observer, EQUATOR_J2000);
|
|
if (observer_state.status != ASTRO_SUCCESS)
|
|
{
|
|
fprintf(stderr, "C TopoStateFunc: Astronomy_ObserverState returned error %d\n", observer_state.status);
|
|
return observer_state;
|
|
}
|
|
|
|
if (body == BODY_GEO_EMB)
|
|
{
|
|
state = Astronomy_GeoEmbState(time);
|
|
}
|
|
else if (body == BODY_EARTH)
|
|
{
|
|
state.status = ASTRO_SUCCESS;
|
|
state.t = time;
|
|
state.x = state.y = state.z = 0.0;
|
|
state.vx = state.vy = state.vz = 0.0;
|
|
}
|
|
else
|
|
{
|
|
fprintf(stderr, "C TopoStateFunc: Unsupported body %d\n", body);
|
|
memset(&state, 0, sizeof(state));
|
|
state.status = ASTRO_INVALID_BODY;
|
|
return state;
|
|
}
|
|
|
|
if (state.status != ASTRO_SUCCESS)
|
|
{
|
|
fprintf(stderr, "C TopoStateFunc: body %d resulted in error %d\n", body, state.status);
|
|
return state;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
static int TopoStateTest(void)
|
|
{
|
|
int error; /* set as a side-effect of CHECK macro */
|
|
verify_state_context_t context;
|
|
|
|
memset(&context, 0, sizeof(context));
|
|
context.func = TopoStateFunc;
|
|
CHECK(VerifyStateBody(&context, BODY_EARTH, "topostate/Earth_N30_W80_1000m.txt", 2.108e-04, 2.430e-04));
|
|
CHECK(VerifyStateBody(&context, BODY_GEO_EMB, "topostate/EMB_N30_W80_1000m.txt", 7.197e-04, 2.497e-04));
|
|
|
|
FPASS();
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int Twilight(void)
|
|
{
|
|
int error, lnum, nscanned, i;
|
|
char line[200];
|
|
char search_text[20];
|
|
char token[6][20];
|
|
astro_time_t search_time;
|
|
astro_time_t expected_time[6];
|
|
astro_search_result_t calc[6];
|
|
FILE *infile;
|
|
astro_observer_t observer;
|
|
double diff, max_diff = 0.0;
|
|
const double tolerance_seconds = 60.0;
|
|
const char *filename = "riseset/twilight.txt";
|
|
|
|
infile = fopen(filename, "rt");
|
|
if (infile == NULL)
|
|
FFAIL("Cannot open input file: %s\n", filename);
|
|
|
|
lnum = 0;
|
|
observer.height = 0.0;
|
|
while (ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++lnum;
|
|
|
|
nscanned = sscanf(line, "%lf %lf %17s %17s %17s %17s %17s %17s %17s",
|
|
&observer.latitude,
|
|
&observer.longitude,
|
|
search_text,
|
|
token[0], token[1], token[2], token[3], token[4], token[5]);
|
|
|
|
if (nscanned != 9)
|
|
FAIL("C Twilight(%s line %d): expected 9 tokens but found %d\n", filename, lnum, nscanned);
|
|
|
|
if (ParseDate(search_text, &search_time))
|
|
FAIL("C Twilight(%s line %d): BAD SEARCH TIME FORMAT.", filename, lnum);
|
|
|
|
for (i = 0; i < 6; ++i)
|
|
if (ParseDate(token[i], &expected_time[i]))
|
|
FAIL("C Twilight(%s line %d): BAD EXPECTED TIME [%d] FORMAT.\n", filename, lnum, i);
|
|
|
|
search_time = expected_time[0];
|
|
calc[0] = Astronomy_SearchAltitude(BODY_SUN, observer, DIRECTION_RISE, search_time, 1.0, -18.0); // astronomical dawn
|
|
calc[1] = Astronomy_SearchAltitude(BODY_SUN, observer, DIRECTION_RISE, search_time, 1.0, -12.0); // nautical dawn
|
|
calc[2] = Astronomy_SearchAltitude(BODY_SUN, observer, DIRECTION_RISE, search_time, 1.0, -6.0); // civil dawn
|
|
calc[3] = Astronomy_SearchAltitude(BODY_SUN, observer, DIRECTION_SET, search_time, 1.0, -6.0); // civil dusk
|
|
calc[4] = Astronomy_SearchAltitude(BODY_SUN, observer, DIRECTION_SET, search_time, 1.0, -12.0); // nautical dusk
|
|
calc[5] = Astronomy_SearchAltitude(BODY_SUN, observer, DIRECTION_SET, search_time, 1.0, -18.0); // astronomical dusk
|
|
|
|
for (i = 0; i < 6; ++i)
|
|
{
|
|
if (calc[i].status != ASTRO_SUCCESS)
|
|
FAIL("C Twilight(%s line %d): error %d in search[%d]\n", filename, lnum, calc[i].status, i);
|
|
|
|
diff = 86400.0 * ABS(calc[i].time.ut - expected_time[i].ut);
|
|
if (diff > tolerance_seconds)
|
|
FAIL("C TwilightTest(%s line %d): EXCESSIVE ERROR = %0.3lf seconds for test %d.\n", filename, lnum, diff, i);
|
|
|
|
if (diff > max_diff)
|
|
max_diff = diff;
|
|
}
|
|
}
|
|
|
|
FPASSA("(%d test cases, max error = %0.3lf seconds)\n", lnum, max_diff);
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int Libration(const char *filename, int *ndata, double *var_lon, double *var_lat)
|
|
{
|
|
int error;
|
|
FILE *infile;
|
|
int lnum, count, nscanned;
|
|
char line[200];
|
|
int day, month, year, hour, minute;
|
|
char mtext[4];
|
|
double phase, age, diam, dist, ra, dec, slon, slat, elon, elat, axisa;
|
|
astro_time_t time;
|
|
astro_libration_t lib;
|
|
double diff_elon, diff_elat, diff_distance, diff_diam;
|
|
double max_diff_elon = 0.0, max_diff_elat = 0.0, max_diff_distance = 0.0, max_diff_diam = 0.0;
|
|
double max_eclip_lon = -900.0;
|
|
|
|
infile = fopen(filename, "rt");
|
|
if (infile == NULL)
|
|
FFAIL("cannot open input file: %s\n", filename);
|
|
|
|
lnum = 0;
|
|
count = 0;
|
|
while (ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++lnum;
|
|
if (lnum == 1)
|
|
{
|
|
if (strcmp(line, " Date Time Phase Age Diam Dist RA Dec Slon Slat Elon Elat AxisA\n"))
|
|
FAIL("C Libration(%s line %d): unexpected header line\n", filename, lnum);
|
|
}
|
|
else
|
|
{
|
|
/* 01 Jan 2020 00:00 UT 29.95 5.783 1774.5 403898 23.2609 -10.0824 114.557 -0.045 0.773 6.360 336.353 */
|
|
nscanned = sscanf(line,
|
|
"%d %3s %d %d:%d UT %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf",
|
|
&day, mtext, &year, &hour, &minute,
|
|
&phase, &age, &diam, &dist, &ra, &dec, &slon, &slat, &elon, &elat, &axisa);
|
|
|
|
if (nscanned != 16)
|
|
FAIL("C Libration(%s line %d): expected 16 tokens, found %d\n", filename, lnum, nscanned);
|
|
|
|
/* Calculate the astronomy time value for this calendar date/time. */
|
|
if (ParseMonthName(mtext, &month))
|
|
FAIL("Libration(%s line %d): invalid month symbol '%s'\n", filename, lnum, mtext);
|
|
|
|
time = Astronomy_MakeTime(year, month, day, hour, minute, 0.0);
|
|
lib = Astronomy_Libration(time);
|
|
|
|
diff_elon = 60.0 * ABS(lib.elon - elon);
|
|
if (diff_elon > max_diff_elon)
|
|
max_diff_elon = diff_elon;
|
|
|
|
diff_elat = 60.0 * ABS(lib.elat - elat);
|
|
if (diff_elat > max_diff_elat)
|
|
max_diff_elat = diff_elat;
|
|
|
|
diff_distance = ABS(lib.dist_km - dist);
|
|
if (diff_distance > max_diff_distance)
|
|
max_diff_distance = diff_distance;
|
|
|
|
diff_diam = ABS(lib.diam_deg - diam/3600.0);
|
|
if (diff_diam > max_diff_diam)
|
|
max_diff_diam = diff_diam;
|
|
|
|
if (lib.mlon > max_eclip_lon)
|
|
max_eclip_lon = lib.mlon;
|
|
|
|
if (diff_elon > 0.1304)
|
|
FAIL("C Libration(%s line %d): EXCESSIVE diff_elon = %0.4lf arcmin\n", filename, lnum, diff_elon);
|
|
|
|
if (diff_elat > 1.6476)
|
|
FAIL("C Libration(%s line %d): EXCESSIVE diff_elat = %0.4lf arcmin\n", filename, lnum, diff_elat);
|
|
|
|
if (diff_distance > 54.377)
|
|
FAIL("C Libration(%s line %d): EXCESSIVE diff_distance = %0.3lf km\n", filename, lnum, diff_distance);
|
|
|
|
if (diff_diam > 0.00009)
|
|
FAIL("C Libration(%s line %d): EXCESSIVE diff_diam = %0.3le degrees\n", filename, lnum, diff_diam);
|
|
|
|
/* Update sum-of-squared-errors. */
|
|
*var_lon += diff_elon * diff_elon;
|
|
*var_lat += diff_elat * diff_elat;
|
|
|
|
++count;
|
|
}
|
|
}
|
|
|
|
if (max_eclip_lon < 359.0 || max_eclip_lon > 360.0)
|
|
FAIL("C Libration(%s): INVALID max ecliptic longitude %0.3lf degrees.\n", filename, max_eclip_lon);
|
|
|
|
printf("C Libration(%s): PASS (%d test cases, max_diff_elon = %0.4lf arcmin, max_diff_elat = %0.4lf arcmin, max_diff_distance = %0.3lf km, max_diff_diam = %0.12lf deg)\n",
|
|
filename, count, max_diff_elon, max_diff_elat, max_diff_distance, max_diff_diam);
|
|
|
|
*ndata += count;
|
|
error = 0;
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
static int LibrationTest(void)
|
|
{
|
|
int error;
|
|
int ndata = 0;
|
|
double var_lat = 0.0;
|
|
double var_lon = 0.0;
|
|
double dev_lat, dev_lon;
|
|
|
|
CHECK(Libration("libration/mooninfo_2020.txt", &ndata, &var_lon, &var_lat));
|
|
CHECK(Libration("libration/mooninfo_2021.txt", &ndata, &var_lon, &var_lat));
|
|
CHECK(Libration("libration/mooninfo_2022.txt", &ndata, &var_lon, &var_lat));
|
|
|
|
dev_lon = sqrt(var_lon / ndata);
|
|
dev_lat = sqrt(var_lat / ndata);
|
|
printf("C LibrationTest PASS: %d data points, dev_lon = %0.4lf arcmin, dev_lat = %0.4lf arcmin\n", ndata, dev_lon, dev_lat);
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int DE405_Check(void)
|
|
{
|
|
int error, lnum, nscanned;
|
|
const char *filename = "barystate/de405_state.txt";
|
|
FILE *infile;
|
|
double jd;
|
|
astro_time_t time;
|
|
char line[200];
|
|
char name[10];
|
|
double pos[3], vel[3];
|
|
astro_body_t body;
|
|
astro_state_vector_t state;
|
|
double dx, dy, dz, dr, dv;
|
|
double scale;
|
|
|
|
infile = fopen(filename, "rt");
|
|
if (infile == NULL)
|
|
FFAIL("cannot open input file: %s\n", filename);
|
|
|
|
lnum = 0;
|
|
while (ReadLine(line, sizeof(line), infile, filename, 0))
|
|
{
|
|
++lnum;
|
|
if (line[0] == '*')
|
|
break;
|
|
|
|
if (lnum == 1)
|
|
{
|
|
if (1 != sscanf(line, "%lf", &jd))
|
|
FAIL("DE405_Check(%s line %d): cannot scan Julian Date\n", filename, lnum);
|
|
time = Astronomy_TerrestrialTime(jd - JD_2000);
|
|
Astronomy_FormatTime(time, TIME_FORMAT_MILLI, line, sizeof(line));
|
|
FDEBUG("time = %s\n", line);
|
|
}
|
|
else
|
|
{
|
|
nscanned = sscanf(line, "%7[A-Za-z] %lf %lf %lf %lf %lf %lf", name, &pos[0], &pos[1], &pos[2], &vel[0], &vel[1], &vel[2]);
|
|
if (nscanned != 7)
|
|
FAIL("C DE405_Check(%s line %d): expected 7 tokens, found %d\n", filename, lnum, nscanned);
|
|
body = Astronomy_BodyCode(name);
|
|
if (body == BODY_INVALID)
|
|
FAIL("C DE405_Check(%s line %d): unrecognized body name '%s'\n", filename, lnum, name);
|
|
|
|
switch (body)
|
|
{
|
|
case BODY_MOON:
|
|
/* geocentric Moon */
|
|
state = Astronomy_GeoMoonState(time);
|
|
scale = sqrt(pos[0]*pos[0] + pos[1]*pos[1] + pos[2]*pos[2]);
|
|
break;
|
|
|
|
case BODY_SUN:
|
|
/* barycentric Sun */
|
|
state = Astronomy_BaryState(body, time);
|
|
scale = 1.0;
|
|
break;
|
|
|
|
default:
|
|
/* heliocentric planet */
|
|
state = Astronomy_HelioState(body, time);
|
|
scale = sqrt(pos[0]*pos[0] + pos[1]*pos[1] + pos[2]*pos[2]);
|
|
break;
|
|
}
|
|
|
|
if (state.status != ASTRO_SUCCESS)
|
|
FAIL("C DE405_Check(%s line %d): status = %d\n", filename, lnum, state.status);
|
|
|
|
dx = pos[0] - state.x;
|
|
dy = pos[1] - state.y;
|
|
dz = pos[2] - state.z;
|
|
dr = V(sqrt(dx*dx + dy*dy + dz*dz) / scale);
|
|
|
|
dx = vel[0] - state.vx;
|
|
dy = vel[1] - state.vy;
|
|
dz = vel[2] - state.vz;
|
|
dv = V(sqrt(dx*dx + dy*dy + dz*dz));
|
|
|
|
FDEBUG("%-10s dr=%0.4le dv=%0.4le\n", name, dr, dv);
|
|
if (dr > 8.7e-5)
|
|
FLNFAIL("EXCESSIVE POSITION ERROR\n");
|
|
|
|
if (dv > 7.3e-6)
|
|
FLNFAIL("EXCESSIVE VELOCITY ERROR\n");
|
|
}
|
|
}
|
|
|
|
FPASS();
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int AxisTestBody(astro_body_t body, const char *filename, double arcmin_tolerance)
|
|
{
|
|
int error, lnum, nscanned, found_data, count;
|
|
astro_axis_t axis;
|
|
astro_time_t time;
|
|
astro_spherical_t sphere;
|
|
astro_vector_t north;
|
|
astro_angle_result_t diff;
|
|
double jd, ra, dec, arcmin, max_arcmin;
|
|
FILE *infile;
|
|
char line[100];
|
|
|
|
infile = fopen(filename, "rt");
|
|
if (infile == NULL)
|
|
FAIL("AxisTestBody: cannot open input file: %s\n", filename);
|
|
|
|
count = 0;
|
|
lnum = 0;
|
|
found_data = 0;
|
|
max_arcmin = 0.0;
|
|
while (ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++lnum;
|
|
if (!found_data)
|
|
{
|
|
if (0 == memcmp(line, "$$SOE", 5))
|
|
found_data = 1;
|
|
}
|
|
else
|
|
{
|
|
if (0 == memcmp(line, "$$EOE", 5))
|
|
break;
|
|
|
|
/* [ 1979-Jun-13 00:00 2444037.500000000 181.44164 89.88493] */
|
|
if (strlen(line) < 61)
|
|
FAIL("C AxisTestBody(%s line %d): line is too short\n", filename, lnum);
|
|
|
|
nscanned = sscanf(&line[19], "%lf %lf %lf", &jd, &ra, &dec);
|
|
if (nscanned != 3)
|
|
FAIL("C AxisTestBody(%s line %d): could not scan data.\n", filename, lnum);
|
|
|
|
time = Astronomy_TimeFromDays(jd - JD_2000);
|
|
axis = Astronomy_RotationAxis(body, &time);
|
|
if (axis.status != ASTRO_SUCCESS)
|
|
FAIL("C AxisTestBody(%s line %d): Astronomy_Axis returned error %d\n", filename, lnum, axis.status);
|
|
|
|
/* Convert the reference angles to a reference north pole vector. */
|
|
sphere.status = ASTRO_SUCCESS;
|
|
sphere.dist = 1.0;
|
|
sphere.lat = dec;
|
|
sphere.lon = ra; /* tricky: RA is in degrees, not sidereal hours */
|
|
north = Astronomy_VectorFromSphere(sphere, time);
|
|
if (north.status != ASTRO_SUCCESS)
|
|
FAIL("C AxisTestBody(%s line %d): VectorFromSphere error %d\n", filename, lnum, north.status);
|
|
|
|
/* Find angle between two versions of the north pole. Use that as the measure of error. */
|
|
diff = Astronomy_AngleBetween(north, axis.north);
|
|
if (diff.status != ASTRO_SUCCESS)
|
|
FAIL("C AxisTestBody(%s line %d): AngleBetween error %d\n", filename, lnum, diff.status);
|
|
|
|
arcmin = diff.angle * 60.0; /* convert error degrees to arcminutes */
|
|
if (arcmin > max_arcmin)
|
|
max_arcmin = arcmin;
|
|
|
|
++count;
|
|
}
|
|
}
|
|
|
|
DEBUG("C AxisTestBody(%s): %d test cases, max arcmin error = %0.6lf.\n", filename, count, max_arcmin);
|
|
if (max_arcmin > arcmin_tolerance)
|
|
FAIL("C AxisTestBody(%s): EXCESSIVE ERROR = %lf arcmin\n", filename, max_arcmin);
|
|
error = 0;
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
static int AxisTest(void)
|
|
{
|
|
int error;
|
|
CHECK(AxisTestBody(BODY_SUN, "axis/Sun.txt", 0.0));
|
|
CHECK(AxisTestBody(BODY_MERCURY, "axis/Mercury.txt", 0.074340));
|
|
CHECK(AxisTestBody(BODY_VENUS, "axis/Venus.txt", 0.0));
|
|
CHECK(AxisTestBody(BODY_EARTH, "axis/Earth.txt", 0.002032));
|
|
CHECK(AxisTestBody(BODY_MOON, "axis/Moon.txt", 0.264845));
|
|
CHECK(AxisTestBody(BODY_MARS, "axis/Mars.txt", 0.075323));
|
|
CHECK(AxisTestBody(BODY_JUPITER, "axis/Jupiter.txt", 0.000324));
|
|
CHECK(AxisTestBody(BODY_SATURN, "axis/Saturn.txt", 0.000304));
|
|
CHECK(AxisTestBody(BODY_URANUS, "axis/Uranus.txt", 0.0));
|
|
CHECK(AxisTestBody(BODY_NEPTUNE, "axis/Neptune.txt", 0.000462));
|
|
CHECK(AxisTestBody(BODY_PLUTO, "axis/Pluto.txt", 0.0));
|
|
FPASS();
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int MapPerformanceTest(void)
|
|
{
|
|
int error;
|
|
int count;
|
|
astro_observer_t observer;
|
|
astro_time_t time;
|
|
astro_vector_t ovec;
|
|
astro_rotation_t rot;
|
|
|
|
time = Astronomy_MakeTime(2022, 1, 22, 12, 30, 0.0);
|
|
|
|
count = 0;
|
|
observer.height = 0.0;
|
|
for (observer.longitude = -180.0; observer.longitude < +180.0; observer.longitude += 0.01)
|
|
{
|
|
for (observer.latitude = -85.0; observer.latitude <= +85.0; observer.latitude += 0.01)
|
|
{
|
|
ovec = Astronomy_ObserverVector(&time, observer, EQUATOR_OF_DATE);
|
|
CHECK_STATUS(ovec);
|
|
rot = Astronomy_Rotation_EQD_HOR(&time, observer);
|
|
CHECK_STATUS(rot);
|
|
++count;
|
|
}
|
|
}
|
|
|
|
/*
|
|
612,017,000 geographic locations.
|
|
|
|
Before sidereal time optimization:
|
|
Trial #1: 236.420 seconds.
|
|
Trial #2: 235.906 seconds.
|
|
Trial #3: 238.809 seconds.
|
|
|
|
After sidereal time optimization:
|
|
Trial #1: 103.452 seconds.
|
|
Trial #2: 104.249 seconds.
|
|
Trial #3: 103.657 seconds.
|
|
|
|
Mean performance improvement ratio = 2.284.
|
|
*/
|
|
|
|
printf("MapPerformanceTest: PASS (%d geographic locations)\n", count);
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int GeoMoonPerformance(void)
|
|
{
|
|
/* Call EclipticGeoMoon for many different times to measure its speed. */
|
|
|
|
int error = 1;
|
|
int count = 0;
|
|
astro_time_t time = Astronomy_MakeTime(1800, 1, 1, 0, 0, 0.0);
|
|
astro_time_t stopTime = Astronomy_MakeTime(2200, 1, 1, 0, 0, 0.0);
|
|
while (time.ut <= stopTime.ut)
|
|
{
|
|
astro_spherical_t sphere = Astronomy_EclipticGeoMoon(time);
|
|
if (sphere.status != ASTRO_SUCCESS)
|
|
FAIL("GeoMoonPerformance: EclipticGeoMoon returned error %d.\n", sphere.status);
|
|
time = Astronomy_AddDays(time, 0.01);
|
|
++count;
|
|
}
|
|
printf("GeoMoonPerformance: PASS (called %d times)\n", count);
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int NutationPerformance(void)
|
|
{
|
|
/* Call SiderealTime for many different times to gauge speed of iau2000b nutation model. */
|
|
|
|
int count = 0;
|
|
astro_time_t time = Astronomy_MakeTime(1800, 1, 1, 0, 0, 0.0);
|
|
astro_time_t stopTime = Astronomy_MakeTime(2200, 1, 1, 0, 0, 0.0);
|
|
while (time.ut <= stopTime.ut)
|
|
{
|
|
Astronomy_SiderealTime(&time);
|
|
time = Astronomy_AddDays(time, 0.01);
|
|
++count;
|
|
}
|
|
printf("NutationPerformance: PASS (called %d times)\n", count);
|
|
return 0;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
|
|
static int MoonNodes(void)
|
|
{
|
|
const char *filename = "moon_nodes/moon_nodes.txt";
|
|
int error;
|
|
int lnum, nscanned;
|
|
char kind, prev_kind;
|
|
astro_time_t time;
|
|
double ra, dec;
|
|
FILE *infile;
|
|
char line[100];
|
|
char date[20];
|
|
astro_spherical_t ecl;
|
|
astro_vector_t vec_eqj, vec_eqd, vec_test;
|
|
double diff_lat, max_lat = 0.0;
|
|
double max_node_lat = 0.0;
|
|
double diff_angle, max_angle = 0.0;
|
|
double diff_minutes, max_minutes = 0.0;
|
|
double dt, min_dt = 1.0e+99, max_dt = 0.0;
|
|
astro_rotation_t rot;
|
|
astro_spherical_t sphere;
|
|
astro_angle_result_t result;
|
|
astro_node_event_t node;
|
|
|
|
memset(&node, 0, sizeof(node));
|
|
node.status = ASTRO_NOT_INITIALIZED;
|
|
|
|
infile = fopen(filename, "rt");
|
|
if (infile == NULL)
|
|
FAIL("MoonNodes: Cannot open input file: %s\n", filename);
|
|
|
|
lnum = 0;
|
|
prev_kind = '?';
|
|
while (ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
++lnum;
|
|
|
|
/* Parse the line from the test data file. */
|
|
/* A 2001-01-09T13:53Z 7.1233 22.5350 */
|
|
/* D 2001-01-22T22:22Z 19.1250 -21.4683 */
|
|
|
|
if (strlen(line) < 40)
|
|
FAIL("C MoonNodes(%s line %d): line is too short\n", filename, lnum);
|
|
|
|
nscanned = sscanf(line, "%c %17s %lf %lf", &kind, date, &ra, &dec);
|
|
if (nscanned != 4)
|
|
FAIL("C MoonNodes(%s line %d): syntax error\n", filename, lnum);
|
|
|
|
if (kind != 'A' && kind != 'D')
|
|
FAIL("C MoonNodes(%s line %d): invalid kind character.\n", filename, lnum);
|
|
|
|
if (kind == prev_kind)
|
|
FAIL("C MoonNodes(%s line %d): duplicate ascending/descending kind.\n", filename, lnum);
|
|
|
|
if (ParseDate(date, &time))
|
|
FAIL("C MoonNodes(%s line %d): invalid date/time.\n", filename, lnum);
|
|
|
|
if (!isfinite(ra) || ra < 0.0 || ra > 24.0)
|
|
FAIL("C MoonNodes(%s line %d): invalid right ascension.\n", filename, lnum);
|
|
|
|
if (!isfinite(dec) || dec < -90.0 || dec > +90.0)
|
|
FAIL("C MoonNodes(%s line %d): invalid declination.\n", filename, lnum);
|
|
|
|
/* Convert RA/DEC to a vector. */
|
|
sphere.status = ASTRO_SUCCESS;
|
|
sphere.lat = dec;
|
|
sphere.lon = 15.0 * ra;
|
|
sphere.dist = 1.0;
|
|
vec_test = Astronomy_VectorFromSphere(sphere, time);
|
|
CHECK_STATUS(vec_test);
|
|
|
|
/* Calculate the Moon's ecliptic angles. Verify latitude is very close to zero. */
|
|
ecl = Astronomy_EclipticGeoMoon(time);
|
|
CHECK_STATUS(ecl);
|
|
diff_lat = 60.0 * ABS(ecl.lat); /* calculate arcminute latitude error */
|
|
V(ecl.lon);
|
|
V(ecl.dist);
|
|
|
|
if (diff_lat > max_lat)
|
|
max_lat = diff_lat;
|
|
|
|
/* Calculate EQD coordinates. Verify against input file. */
|
|
vec_eqj = Astronomy_GeoMoon(time);
|
|
CHECK_STATUS(vec_eqj);
|
|
|
|
rot = Astronomy_Rotation_EQJ_EQD(&time);
|
|
CHECK_STATUS(rot);
|
|
vec_eqd = Astronomy_RotateVector(rot, vec_eqj);
|
|
CHECK_STATUS(vec_eqd);
|
|
|
|
/* Measure the error angle between the correct vector and the calculated vector. */
|
|
result = Astronomy_AngleBetween(vec_test, vec_eqd);
|
|
CHECK_STATUS(result);
|
|
diff_angle = 60.0 * ABS(result.angle);
|
|
if (diff_angle > max_angle)
|
|
max_angle = diff_angle;
|
|
if (diff_angle > 1.54)
|
|
FAIL("C MoonNodes(%s line %d): EXCESSIVE equatorial error = %0.3lf arcmin\n", filename, lnum, diff_angle);
|
|
|
|
/* Test the Astronomy Engine moon node searcher. */
|
|
if (lnum == 1)
|
|
{
|
|
/* The very first time, so search for the first node in the series. */
|
|
/* Back up a few days to make sure we really are finding it ourselves. */
|
|
astro_time_t earlier = Astronomy_AddDays(time, -6.5472); /* pick a weird amount of time */
|
|
node = Astronomy_SearchMoonNode(earlier);
|
|
}
|
|
else
|
|
{
|
|
double prev_tt = node.time.tt;
|
|
|
|
/* Use the previous node to find the next node. */
|
|
node = Astronomy_NextMoonNode(node);
|
|
|
|
/* Find the range of time intervals between consecutive nodes. */
|
|
dt = node.time.tt - prev_tt;
|
|
if (dt < min_dt)
|
|
min_dt = dt;
|
|
if (dt > max_dt)
|
|
max_dt = dt;
|
|
}
|
|
|
|
if (node.status != ASTRO_SUCCESS)
|
|
FAIL("C MoonNodes(%s line %d): error %d returned by node search\n", filename, lnum, node.status);
|
|
|
|
/* Verify the ecliptic latitude is very close to zero at the alleged node. */
|
|
ecl = Astronomy_EclipticGeoMoon(node.time);
|
|
CHECK_STATUS(ecl);
|
|
diff_lat = 60.0 * ABS(ecl.lat);
|
|
if (diff_lat > 8.06e-4)
|
|
FAIL("C MoonNodes(%s line %d): found node has excessive latitude = %0.4le arcmin\n", filename, lnum, diff_lat);
|
|
if (diff_lat > max_node_lat)
|
|
max_node_lat = diff_lat;
|
|
|
|
/* Verify the time agrees with Espenak's time to within a few minutes. */
|
|
diff_minutes = (24.0 * 60.0) * ABS(node.time.tt - time.tt);
|
|
if (diff_minutes > max_minutes)
|
|
max_minutes = diff_minutes;
|
|
|
|
/* Verify the kind of node matches what Espenak says (ascending or descending). */
|
|
if (kind == 'A' && node.kind != ASCENDING_NODE)
|
|
FAIL("C MoonNodes(%s line %d): did not find ascending node as expected.\n", filename, lnum);
|
|
|
|
if (kind == 'D' && node.kind != DESCENDING_NODE)
|
|
FAIL("C MoonNodes(%s line %d): did not find descending node as expected.\n", filename, lnum);
|
|
|
|
/* Prepare for the next iteration. */
|
|
prev_kind = kind;
|
|
}
|
|
|
|
if (max_lat > 0.183)
|
|
FFAIL("EXCESSIVE ecliptic latitude error = %0.3lf arcmin\n", max_lat);
|
|
|
|
if (max_minutes > 3.681)
|
|
FFAIL("EXCESSIVE time prediction error = %0.3lf minutes\n", max_minutes);
|
|
|
|
FDEBUG("min_dt = %0.3lf days, max_dt = %0.3lf days.\n", min_dt, max_dt);
|
|
FPASSA("(%d nodes, max lat error = %0.3lf arcmin, max equ error = %0.3lf arcmin, max time error = %0.3lf minutes, max node lat = %0.3le arcmin)\n", lnum, max_lat, max_angle, max_minutes, max_node_lat);
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int SiderealTimeTest(void)
|
|
{
|
|
int error;
|
|
astro_time_t time;
|
|
double gast, diff;
|
|
const double correct = 9.3983699280076483;
|
|
|
|
time = Astronomy_MakeTime(2022, 3, 15, 21, 50, 0.0);
|
|
gast = Astronomy_SiderealTime(&time);
|
|
diff = ABS(gast - correct);
|
|
printf("C %s: gast=%0.15f, correct=%0.15lf, diff=%0.3le hours.\n", __func__, gast, correct, diff);
|
|
if (diff > 1.0e-15)
|
|
FFAIL("EXCESSIVE ERROR\n");
|
|
|
|
FPASS();
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static double ArcminPosError(astro_state_vector_t correct, astro_state_vector_t calc)
|
|
{
|
|
double dx = V(calc.x - correct.x);
|
|
double dy = V(calc.y - correct.y);
|
|
double dz = V(calc.z - correct.z);
|
|
double diff_squared = dx*dx + dy*dy + dz*dz;
|
|
double mag_squared = correct.x*correct.x + correct.y*correct.y + correct.z*correct.z;
|
|
double radians = V(sqrt(diff_squared / mag_squared));
|
|
double arcmin = (RAD2DEG * 60.0) * radians;
|
|
return arcmin;
|
|
}
|
|
|
|
static double ArcminVelError(astro_state_vector_t correct, astro_state_vector_t calc)
|
|
{
|
|
double dx = V(calc.vx - correct.vx);
|
|
double dy = V(calc.vy - correct.vy);
|
|
double dz = V(calc.vz - correct.vz);
|
|
double diff_squared = dx*dx + dy*dy + dz*dz;
|
|
double mag_squared = correct.vx*correct.vx + correct.vy*correct.vy + correct.vz*correct.vz;
|
|
double radians = V(sqrt(diff_squared / mag_squared));
|
|
double arcmin = (RAD2DEG * 60.0) * radians;
|
|
return arcmin;
|
|
}
|
|
|
|
|
|
static double SSB_ArcminPosError(astro_state_vector_t correct, astro_state_vector_t calc)
|
|
{
|
|
/* Scale the SSB based on 1 AU, not on its absolute magnitude, which can become very close to zero. */
|
|
double dx = V(calc.x - correct.x);
|
|
double dy = V(calc.y - correct.y);
|
|
double dz = V(calc.z - correct.z);
|
|
double diff_squared = dx*dx + dy*dy + dz*dz;
|
|
double radians = V(sqrt(diff_squared));
|
|
double arcmin = (RAD2DEG * 60.0) * radians;
|
|
return arcmin;
|
|
}
|
|
|
|
|
|
static int GravSimFile(
|
|
const char *filename,
|
|
astro_body_t originBody,
|
|
int nsteps,
|
|
double *r_score,
|
|
double *v_score,
|
|
double r_thresh,
|
|
double v_thresh)
|
|
{
|
|
int error;
|
|
int i, k;
|
|
double rdiff, vdiff, tdiff;
|
|
double max_rdiff = 0.0, max_vdiff = 0.0;
|
|
astro_grav_sim_t *sim = NULL;
|
|
astro_state_vector_t state;
|
|
astro_status_t status;
|
|
state_vector_batch_t batch = EmptyStateVectorBatch();
|
|
astro_time_t time, checkTime;
|
|
astro_body_t checkOrigin;
|
|
|
|
if (nsteps < 1)
|
|
FAIL("C GravSimFile(%s): invalid nsteps=%d\n", filename, nsteps);
|
|
|
|
CHECK(LoadStateVectors(&batch, filename));
|
|
|
|
if (batch.length <= 0)
|
|
FAIL("C GravSimFile(%s): batch.length = %d is invalid.\n", filename, batch.length);
|
|
|
|
/* Use the state vector and time in the batch to initialize the gravity simulator. */
|
|
state = batch.array[0];
|
|
|
|
status = Astronomy_GravSimInit(&sim, originBody, state.t, 1, &state);
|
|
if (status != ASTRO_SUCCESS)
|
|
FAIL("C GravSimFile(%s): Astronomy_GravSimInit returned error %d\n", filename, status);
|
|
|
|
i = Astronomy_GravSimNumBodies(sim);
|
|
if (i != 1)
|
|
FAIL("C GravSimFile(%s): expected 1 body, found %d\n", filename, i);
|
|
|
|
checkOrigin = Astronomy_GravSimOrigin(sim);
|
|
if (checkOrigin != originBody)
|
|
FAIL("C GravSimFile(%s): expected body %d, found %d\n", filename, originBody, checkOrigin);
|
|
|
|
time = state.t;
|
|
for (i = 1; i < batch.length; ++i)
|
|
{
|
|
/* Split each entry of the input batch into `nsteps` simulation steps. */
|
|
double tt1 = batch.array[i-1].t.tt;
|
|
double tt2 = batch.array[i].t.tt;
|
|
double dt = (tt2 - tt1) / nsteps;
|
|
for (k = 1; k <= nsteps; ++k)
|
|
{
|
|
time = Astronomy_TerrestrialTime(tt1 + k*dt);
|
|
status = Astronomy_GravSimUpdate(sim, time, 1, &state);
|
|
if (status != ASTRO_SUCCESS)
|
|
FAIL("C GravSimFile(%s : i=%d): Astronomy_GravSimInit returned error %d\n", filename, i, status);
|
|
|
|
checkTime = Astronomy_GravSimTime(sim);
|
|
if (checkTime.tt != time.tt)
|
|
FAIL("C GravSimFile(%s): Expected tt=%0.16lf, found tt=%0.16lf", filename, time.tt, checkTime.tt);
|
|
}
|
|
|
|
tdiff = SECONDS_PER_DAY * ABS(time.tt - batch.array[i].t.tt);
|
|
if (tdiff > 1.0e-15)
|
|
FAIL("C GravSimFile(%s): terrestrial time error = %0.16lf seconds.\n", filename, tdiff);
|
|
|
|
/* Compare the simulated state with the reference state. */
|
|
rdiff = ArcminPosError(batch.array[i], state);
|
|
if (rdiff > max_rdiff)
|
|
max_rdiff = rdiff;
|
|
|
|
if (rdiff > r_thresh)
|
|
FAIL("C GravSimFile(%s : i=%d): EXCESSIVE position error = %0.4lf arcmin\n", filename, i, rdiff);
|
|
|
|
vdiff = ArcminVelError(batch.array[i], state);
|
|
if (vdiff > max_vdiff)
|
|
max_vdiff = vdiff;
|
|
|
|
if (vdiff > v_thresh)
|
|
FAIL("C GravSimFile(%s : i=%d): EXCESSIVE velocity error = %0.4lf arcmin\n", filename, i, vdiff);
|
|
}
|
|
|
|
if (max_rdiff > *r_score)
|
|
*r_score = max_rdiff;
|
|
|
|
if (max_vdiff > *v_score)
|
|
*v_score = max_vdiff;
|
|
|
|
DEBUG("C GravSimFile(%-22s): PASS (count = %d, pos error = %7.4lf arcmin, vel error = %7.4lf arcmin)\n", filename, batch.length, max_rdiff, max_vdiff);
|
|
error = 0;
|
|
fail:
|
|
Astronomy_GravSimFree(sim);
|
|
FreeStateVectorBatch(&batch);
|
|
return error;
|
|
}
|
|
|
|
|
|
static int GravSimEmpty(
|
|
const char *filename,
|
|
astro_body_t origin,
|
|
astro_body_t body,
|
|
double limit_rdiff,
|
|
double limit_vdiff)
|
|
{
|
|
int error;
|
|
int i;
|
|
astro_body_t checkOrigin;
|
|
astro_status_t status;
|
|
astro_grav_sim_t *sim = NULL;
|
|
state_vector_batch_t batch = EmptyStateVectorBatch();
|
|
astro_state_vector_t calc;
|
|
double rdiff, vdiff;
|
|
double max_rdiff = 0.0;
|
|
double max_vdiff = 0.0;
|
|
|
|
CHECK(LoadStateVectors(&batch, filename));
|
|
if (batch.length < 1)
|
|
FAIL("C GravSimEmpty(%s): invalid number of entries = %d\n", filename, batch.length);
|
|
|
|
/* Verify that we can create an empty simulator, and that we can calculate the SSB from it. */
|
|
status = Astronomy_GravSimInit(&sim, origin, batch.array[0].t, 0, NULL);
|
|
if (status != ASTRO_SUCCESS)
|
|
FAIL("C GravSimEmpty(%s): Astronomy_GravSimInit returned %d\n", filename, status);
|
|
|
|
i = Astronomy_GravSimNumBodies(sim);
|
|
if (i != 0)
|
|
FAIL("C GravSimEmpty(%s): expected 0 bodies, found %d\n", filename, i);
|
|
|
|
checkOrigin = Astronomy_GravSimOrigin(sim);
|
|
if (checkOrigin != origin)
|
|
FAIL("C GravSimEmpty(%s): expected body %d, found %d\n", filename, origin, checkOrigin);
|
|
|
|
for (i = 0; i < batch.length; ++i)
|
|
{
|
|
status = Astronomy_GravSimUpdate(sim, batch.array[i].t, 0, NULL);
|
|
if (status != ASTRO_SUCCESS)
|
|
FAIL("C GravSimEmpty(%s): i=%d, Astronomy_GravSimUpdate returned %d\n", filename, i, status);
|
|
|
|
calc = Astronomy_GravSimBodyState(sim, body);
|
|
CHECK_STATUS(calc);
|
|
|
|
if (origin==BODY_SSB && body==BODY_SUN)
|
|
rdiff = SSB_ArcminPosError(batch.array[i], calc);
|
|
else
|
|
rdiff = ArcminPosError(batch.array[i], calc);
|
|
|
|
if (rdiff > limit_rdiff)
|
|
FAIL("C GravSimEmpty(%s): excessive position error = %0.6lf arcmin\n", filename, rdiff);
|
|
|
|
if (rdiff > max_rdiff)
|
|
max_rdiff = rdiff;
|
|
|
|
vdiff = ArcminVelError(batch.array[i], calc);
|
|
if (vdiff > max_vdiff)
|
|
max_vdiff = vdiff;
|
|
|
|
if (vdiff > limit_vdiff)
|
|
FAIL("C GravSimEmpty(%s): excessive velocity error = %0.6lf arcmin\n", filename, vdiff);
|
|
}
|
|
|
|
DEBUG("C GravSimEmpty(%-22s): PASS - %d cases, pos error = %0.4lf arcmin, vel error = %0.4lf arcmin\n", filename, batch.length, max_rdiff, max_vdiff);
|
|
error = 0;
|
|
fail:
|
|
Astronomy_GravSimFree(sim);
|
|
return error;
|
|
}
|
|
|
|
|
|
static int GravitySimulatorTest(void)
|
|
{
|
|
int error;
|
|
const int nsteps = 20;
|
|
double rscore = 0.0;
|
|
double vscore = 0.0;
|
|
|
|
CHECK(GravSimEmpty("barystate/Sun.txt", BODY_SSB, BODY_SUN, 0.0269, 1.9635));
|
|
CHECK(GravSimEmpty("barystate/Mercury.txt", BODY_SSB, BODY_MERCURY, 0.5725, 0.9332));
|
|
CHECK(GravSimEmpty("barystate/Venus.txt", BODY_SSB, BODY_VENUS, 0.1433, 0.1458));
|
|
CHECK(GravSimEmpty("barystate/Earth.txt", BODY_SSB, BODY_EARTH, 0.0651, 0.2098));
|
|
CHECK(GravSimEmpty("barystate/Mars.txt", BODY_SSB, BODY_MARS, 0.1150, 0.1896));
|
|
CHECK(GravSimEmpty("barystate/Jupiter.txt", BODY_SSB, BODY_JUPITER, 0.2546, 0.8831));
|
|
CHECK(GravSimEmpty("barystate/Saturn.txt", BODY_SSB, BODY_SATURN, 0.3660, 1.0818));
|
|
CHECK(GravSimEmpty("barystate/Uranus.txt", BODY_SSB, BODY_URANUS, 0.3107, 0.9321));
|
|
CHECK(GravSimEmpty("barystate/Neptune.txt", BODY_SSB, BODY_NEPTUNE, 0.3382, 1.5586));
|
|
|
|
CHECK(GravSimEmpty("heliostate/Mercury.txt", BODY_SUN, BODY_MERCURY, 0.5088, 0.9473));
|
|
CHECK(GravSimEmpty("heliostate/Venus.txt", BODY_SUN, BODY_VENUS, 0.1214, 0.1543));
|
|
CHECK(GravSimEmpty("heliostate/Earth.txt", BODY_SUN, BODY_EARTH, 0.0508, 0.2099));
|
|
CHECK(GravSimEmpty("heliostate/Mars.txt", BODY_SUN, BODY_MARS, 0.1085, 0.1927));
|
|
CHECK(GravSimEmpty("heliostate/Jupiter.txt", BODY_SUN, BODY_JUPITER, 0.2564, 0.8805));
|
|
CHECK(GravSimEmpty("heliostate/Saturn.txt", BODY_SUN, BODY_SATURN, 0.3664, 1.0826));
|
|
CHECK(GravSimEmpty("heliostate/Uranus.txt", BODY_SUN, BODY_URANUS, 0.3106, 0.9322));
|
|
CHECK(GravSimEmpty("heliostate/Neptune.txt", BODY_SUN, BODY_NEPTUNE, 0.3381, 1.5584));
|
|
|
|
DEBUG("\n");
|
|
|
|
CHECK(GravSimFile("barystate/Ceres.txt", BODY_SSB, nsteps, &rscore, &vscore, 0.6640, 0.6226));
|
|
CHECK(GravSimFile("barystate/Pallas.txt", BODY_SSB, nsteps, &rscore, &vscore, 0.4687, 0.3474));
|
|
CHECK(GravSimFile("barystate/Vesta.txt", BODY_SSB, nsteps, &rscore, &vscore, 0.5806, 0.5462));
|
|
CHECK(GravSimFile("barystate/Juno.txt", BODY_SSB, nsteps, &rscore, &vscore, 0.6760, 0.5750));
|
|
CHECK(GravSimFile("barystate/Bennu.txt", BODY_SSB, nsteps, &rscore, &vscore, 3.7444, 2.6581));
|
|
CHECK(GravSimFile("barystate/Halley.txt", BODY_SSB, nsteps, &rscore, &vscore, 0.0539, 0.0825));
|
|
|
|
CHECK(GravSimFile("heliostate/Ceres.txt", BODY_SUN, nsteps, &rscore, &vscore, 0.0445, 0.0355));
|
|
CHECK(GravSimFile("heliostate/Pallas.txt", BODY_SUN, nsteps, &rscore, &vscore, 0.1062, 0.0854));
|
|
CHECK(GravSimFile("heliostate/Vesta.txt", BODY_SUN, nsteps, &rscore, &vscore, 0.1432, 0.1308));
|
|
CHECK(GravSimFile("heliostate/Juno.txt", BODY_SUN, nsteps, &rscore, &vscore, 0.1554, 0.1328));
|
|
|
|
CHECK(GravSimFile("geostate/Ceres.txt", BODY_EARTH, nsteps, &rscore, &vscore, 6.5689, 6.4797));
|
|
CHECK(GravSimFile("geostate/Pallas.txt", BODY_EARTH, nsteps, &rscore, &vscore, 9.3288, 7.3533));
|
|
CHECK(GravSimFile("geostate/Vesta.txt", BODY_EARTH, nsteps, &rscore, &vscore, 3.2980, 3.8863));
|
|
CHECK(GravSimFile("geostate/Juno.txt", BODY_EARTH, nsteps, &rscore, &vscore, 6.0962, 7.7147));
|
|
|
|
FPASSA("(pos score = %0.4lf arcmin, vel score = %0.4lf arcmin)\n", rscore, vscore);
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int CheckDecemberSolstice(int year, const char *expected)
|
|
{
|
|
int error = 1;
|
|
char actual[TIME_TEXT_BYTES];
|
|
astro_seasons_t si = Astronomy_Seasons(year);
|
|
astro_status_t status = Astronomy_FormatTime(si.dec_solstice, TIME_FORMAT_MILLI, actual, sizeof(actual));
|
|
if (status != ASTRO_SUCCESS)
|
|
FFAIL("FAIL: status=%d for year %d.\n", status, year);
|
|
if (strcmp(actual, expected))
|
|
FFAIL("FAIL: year %d, expected [%s], actual [%s]\n", year, expected, actual);
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
static int DatesIssue250(void)
|
|
{
|
|
/* Make sure we can handle dates outside the range supported by System.DateTime. */
|
|
/* https://github.com/cosinekitty/astronomy/issues/250 */
|
|
int error;
|
|
CHECK(CheckDecemberSolstice( 2022, "2022-12-21T21:47:54.455Z"));
|
|
CHECK(CheckDecemberSolstice(-2300, "-002300-12-19T16:22:27.929Z"));
|
|
CHECK(CheckDecemberSolstice(12345, "+012345-12-11T13:30:10.276Z"));
|
|
FPASS();
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int StarRiseSetCulmCase(
|
|
const char *starName,
|
|
double ra,
|
|
double dec,
|
|
double distLy,
|
|
astro_observer_t observer,
|
|
int year,
|
|
int month,
|
|
int day,
|
|
int riseHour,
|
|
int riseMinute,
|
|
int culmHour,
|
|
int culmMinute,
|
|
int setHour,
|
|
int setMinute)
|
|
{
|
|
int error;
|
|
astro_status_t status;
|
|
astro_time_t searchTime, expectedRiseTime, expectedSetTime, expectedCulmTime;
|
|
astro_search_result_t rise, set;
|
|
astro_hour_angle_t culm;
|
|
double rdiff, cdiff, sdiff;
|
|
|
|
/* Calculate expected event times. */
|
|
|
|
expectedRiseTime = Astronomy_MakeTime(year, month, day, riseHour, riseMinute, 0.0);
|
|
expectedCulmTime = Astronomy_MakeTime(year, month, day, culmHour, culmMinute, 0.0);
|
|
expectedSetTime = Astronomy_MakeTime(year, month, day, setHour, setMinute, 0.0);
|
|
|
|
/* Define a custom star object. */
|
|
|
|
status = Astronomy_DefineStar(BODY_STAR1, ra, dec, distLy);
|
|
if (status != ASTRO_SUCCESS)
|
|
FAIL("StarRiseSetCulm(%s): Astronomy_DefineStar returned %d.\n", starName, status);
|
|
|
|
/* Use Astronomy Engine to search for event times. */
|
|
|
|
searchTime = Astronomy_MakeTime(year, month, day, 0, 0, 0.0);
|
|
|
|
rise = Astronomy_SearchRiseSet(BODY_STAR1, observer, DIRECTION_RISE, searchTime, 1.0);
|
|
CHECK_STATUS(rise);
|
|
|
|
culm = Astronomy_SearchHourAngleEx(BODY_STAR1, observer, 0.0, searchTime, +1);
|
|
CHECK_STATUS(culm);
|
|
|
|
set = Astronomy_SearchRiseSet(BODY_STAR1, observer, DIRECTION_SET, searchTime, 1.0);
|
|
CHECK_STATUS(set);
|
|
|
|
/* Compare expected times with calculated times. */
|
|
|
|
rdiff = MINUTES_PER_DAY * ABS(expectedRiseTime.ut - rise.time.ut);
|
|
cdiff = MINUTES_PER_DAY * ABS(expectedCulmTime.ut - culm.time.ut);
|
|
sdiff = MINUTES_PER_DAY * ABS(expectedSetTime.ut - set.time.ut);
|
|
|
|
DEBUG("C StarRiseSetCulmCase(%s): minutes rdiff = %0.4lf, cdiff = %0.4lf, sdiff = %0.4lf\n", starName, rdiff, cdiff, sdiff);
|
|
|
|
if (rdiff > 0.5) FAIL("C StarRiseSetCulmCase(%s): exccessive rise time error = %0.4lf minutes.\n", starName, rdiff);
|
|
if (cdiff > 0.5) FAIL("C StarRiseSetCulmCase(%s): exccessive culm time error = %0.4lf minutes.\n", starName, cdiff);
|
|
if (sdiff > 0.5) FAIL("C StarRiseSetCulmCase(%s): exccessive set time error = %0.4lf minutes.\n", starName, sdiff);
|
|
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
static int StarRiseSetCulm(void)
|
|
{
|
|
int error;
|
|
astro_observer_t observer = Astronomy_MakeObserver(+25.77, -80.19, 0.0);
|
|
|
|
CHECK(StarRiseSetCulmCase("Sirius", 6.7525, -16.7183, 8.6, observer, 2022, 11, 21, 2, 37, 8, 6, 13, 34));
|
|
CHECK(StarRiseSetCulmCase("Sirius", 6.7525, -16.7183, 8.6, observer, 2022, 11, 25, 2, 22, 7, 50, 13, 18));
|
|
CHECK(StarRiseSetCulmCase("Canopus", 6.3992, -52.6956, 310.0, observer, 2022, 11, 21, 4, 17, 7, 44, 11, 11));
|
|
CHECK(StarRiseSetCulmCase("Canopus", 6.3992, -52.6956, 310.0, observer, 2022, 11, 25, 4, 1, 7, 28, 10, 56));
|
|
|
|
FPASS();
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int HourAngleCase(int year, int month, int day, double latitude, double longitude, double hourAngle, double *maxdiff)
|
|
{
|
|
int error;
|
|
astro_hour_angle_t search;
|
|
astro_observer_t observer;
|
|
astro_time_t startTime;
|
|
astro_func_result_t ha;
|
|
double diff;
|
|
const double threshold = 0.1 / 3600.0; /* Astronomy_SearchHourAngleEx() accuracy: 0.1 seconds converted to hours */
|
|
|
|
observer = Astronomy_MakeObserver(latitude, longitude, 0.0);
|
|
startTime = Astronomy_MakeTime(year, month, day, 0, 0, 0.0);
|
|
|
|
search = Astronomy_SearchHourAngleEx(BODY_SUN, observer, hourAngle, startTime, +1);
|
|
CHECK_STATUS(search);
|
|
|
|
ha = Astronomy_HourAngle(BODY_SUN, &search.time, observer);
|
|
CHECK_STATUS(ha);
|
|
diff = ABS(ha.value - hourAngle);
|
|
if (diff > 12.0)
|
|
diff = 24.0 - diff;
|
|
|
|
if (diff > *maxdiff)
|
|
*maxdiff = diff;
|
|
|
|
if (diff > threshold)
|
|
FFAIL("EXCESSIVE ERROR = %0.6le, calc HA=%0.16lf for hourAngle=%0.1lf, longitude=%0.1lf\n", diff, ha.value, hourAngle, longitude);
|
|
|
|
FDEBUG("Hour angle = %4.1lf, longitude = %6.1lf, diff = %7.4le hours\n", hourAngle, longitude, diff);
|
|
error = 0;
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
|
|
static int HourAngleTest(void)
|
|
{
|
|
int error, hour, longitude;
|
|
double maxdiff = 0.0;
|
|
int cases = 0;
|
|
|
|
for (longitude = -175; longitude <= 180; longitude += 5)
|
|
for (hour = 0; hour < 24; ++hour, ++cases)
|
|
CHECK(HourAngleCase(2023, 2, 11, 35.0, (double)longitude, (double)hour, &maxdiff));
|
|
|
|
FPASSA("%d cases, maxdiff = %0.4le hours.\n", cases, maxdiff);
|
|
fail:
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|
|
|
|
static int Atmosphere(void)
|
|
{
|
|
int error;
|
|
FILE *infile = NULL;
|
|
const char *filename = "riseset/atmosphere.csv";
|
|
int lnum = 0;
|
|
int ncases = 0;
|
|
int nscanned;
|
|
const double tolerance = 8.8e-11;
|
|
double diff, maxdiff = 0.0;
|
|
double elevation, temperature, pressure, ignore_density, relative_density;
|
|
astro_atmosphere_t atmos;
|
|
char line[200];
|
|
|
|
infile = fopen(filename, "rt");
|
|
if (infile == NULL)
|
|
FFAIL("Cannot open input file: %s\n", filename);
|
|
|
|
while (ReadLine(line, sizeof(line), infile, filename, lnum))
|
|
{
|
|
TrimWhiteSpace(line);
|
|
++lnum;
|
|
if (lnum == 1)
|
|
{
|
|
if (0 != strcmp(line, "elevation,temperature,pressure,density,relative_density"))
|
|
FLNFAIL("Expected header line, but found: [%s]\n", line);
|
|
}
|
|
else
|
|
{
|
|
/* 1000,281.65,89874.78809616930,319.1009696295730,0.9074655257711480 */
|
|
nscanned = sscanf(line, "%lf,%lf,%lf,%lf,%lf", &elevation, &temperature, &pressure, &ignore_density, &relative_density);
|
|
if (nscanned != 5)
|
|
FLNFAIL("Expected 5 tokens but found %d\n", nscanned);
|
|
|
|
atmos = Astronomy_Atmosphere(elevation);
|
|
CHECK_STATUS(atmos);
|
|
|
|
diff = ABS(atmos.temperature - temperature);
|
|
if (diff > maxdiff) maxdiff = diff;
|
|
if (diff > tolerance) FLNFAIL("EXCESSIVE temperature difference = %lg\n", diff);
|
|
|
|
diff = ABS(atmos.pressure - pressure);
|
|
if (diff > maxdiff) maxdiff = diff;
|
|
if (diff > tolerance) FLNFAIL("EXCESSIVE pressure difference = %lg\n", diff);
|
|
|
|
diff = ABS(atmos.density - relative_density);
|
|
if (diff > maxdiff) maxdiff = diff;
|
|
if (diff > tolerance) FLNFAIL("EXCESSIVE density difference = %lg\n", diff);
|
|
|
|
++ncases;
|
|
}
|
|
}
|
|
|
|
if (ncases != 34)
|
|
FFAIL("Expected 34 test cases but processed %d.\n", ncases);
|
|
|
|
FPASSA("%d cases, maxdiff = %lg.\n", ncases, maxdiff);
|
|
fail:
|
|
if (infile != NULL) fclose(infile);
|
|
return error;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------------------------------------*/
|