Files
astronomy/generate/generate.c
Don Cross 7bff374d57 Starting to add support for generating C# code.
Decided to move call to makedoc script from run script.
It was confusing that it was hidden inside unit_test_js,
especially because it invokes the code generator for
all supported languages.
2019-10-10 14:56:49 -04:00

1328 lines
41 KiB
C

/*
MIT License
Copyright (c) 2019 Don Cross <cosinekitty@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <io.h>
#define unlink _unlink
#else
#include <unistd.h>
#endif
#include "eph_manager.h"
#include "novas.h"
#include "astro_vector.h"
#include "chebyshev.h"
#include "codegen.h"
#include "earth.h"
#include "ephfile.h"
#include "novas_body.h"
#include "vsop.h"
#define VECTOR_DIM 3
#define MIN_YEAR 1700
#define MAX_YEAR 2200
typedef struct
{
double rmsPositionError;
double maxPositionError;
double rmsArcminError;
double maxArcminError;
long dataCount; /* a measure of how many floating point numbers need to be encoded for ephemeris translation */
}
error_stats_t;
typedef struct
{
int body; /* which body we are calculating for */
}
sample_context_t;
static double jd_begin;
static double jd_end;
static short int de_number;
static int OpenEphem(void);
static int PrintUsage(void);
static int GeneratePlanets(void);
static int GenerateSource(void);
static int TestVsopModel(vsop_model_t *model, int body, double threshold, double *max_arcmin, int *trunc_terms);
static int SaveVsopFile(const vsop_model_t *model);
static int PositionArcminError(int body, double jd, const double a[3], const double b[3], double *arcmin);
static double VectorLength(const double v[3]);
static int MeasureError(const char *inFileName, int nsamples, error_stats_t *stats);
static int Resample(int body, const char *outFileName, int npoly, int startYear, int stopYear, int nsections);
static int SampleFunc(const void *context, double jd, double pos[CHEB_MAX_DIM]);
static double VectorError(double a[3], double b[3]);
static int ManualResample(int body, int npoly, int nsections, int startYear, int stopYear);
static int CheckTestOutput(const char *filename);
static vsop_body_t LookupBody(const char *name);
static int CheckSkyPos(observer *location, const char *filename, int lnum, const char *line, double *arcmin_equ, double *arcmin_hor, vsop_body_t *body);
static int UnitTestChebyshev(void);
#define MOON_PERIGEE 0.00238
#define MERCURY_APHELION 0.466697
#define VENUS_APHELION 0.728213
#define EARTH_PERIHELION 0.98327
#define EARTH_APHELION 1.017
#define MARS_PERIHELION 1.382
#define JUPITER_PERIHELION 4.9501
#define SATURN_PERIHELION 9.0412
#define URANUS_PERIHELION 18.33
#define NEPTUNE_PERIHELION 29.81
#define PLUTO_PERIHELION 29.658
/*
ErrorRadius[body] is the worst-case (smallest) distance
in AU over which a position error affects Earth-based observations.
For example, an error in the Earth/Moon Barycenter (EMB) affects
observations of Venus the most, because that planet comes closest
to Earth at 0.277 AU.
*/
const double ErrorRadius[] =
{
EARTH_PERIHELION - MERCURY_APHELION, /* 0 = Mercury */
EARTH_PERIHELION - VENUS_APHELION, /* 1 = Venus */
EARTH_PERIHELION - VENUS_APHELION, /* 2 = Earth/Moon Barycenter (EMB) : Venus has closest approach */
MARS_PERIHELION - EARTH_APHELION, /* 3 = Mars */
JUPITER_PERIHELION - EARTH_APHELION, /* 4 = Jupiter */
SATURN_PERIHELION - EARTH_APHELION, /* 5 = Saturn */
URANUS_PERIHELION - EARTH_APHELION, /* 6 = Uranus */
NEPTUNE_PERIHELION - EARTH_APHELION, /* 7 = Neptune */
PLUTO_PERIHELION - EARTH_APHELION, /* 8 = Pluto */
MOON_PERIGEE, /* 9 = geocentric Moon */
EARTH_PERIHELION, /* 10 = Sun */
};
int main(int argc, const char *argv[])
{
if (argc == 2 && !strcmp(argv[1], "planets"))
return GeneratePlanets();
if (argc == 2 && !strcmp(argv[1], "source"))
return GenerateSource();
if (argc == 3 && !strcmp(argv[1], "check"))
return CheckTestOutput(argv[2]);
if (argc == 2 && !strcmp(argv[1], "chebyshev"))
return UnitTestChebyshev();
return PrintUsage();
}
static int PrintUsage(void)
{
fprintf(stderr,
"\n"
"USAGE:\n"
"\n"
"generate planets\n"
" Generate predictive models for all planets.\n"
"\n"
"generate check testfile\n"
" Verify the calculations in the testfile generated by a unit test.\n"
"\n"
"generate source\n"
" Generate source code after running 'generate planets'.\n"
"\n"
"generate chebyshev\n"
" Run unit test on Chebyshev interpolator.\n"
"\n"
);
return 1;
}
static int OpenEphem()
{
const char *filename;
int error;
filename = getenv("EPHEM");
if (filename == NULL)
filename = "lnxp1600p2200.405";
error = ephem_open((char *)filename, &jd_begin, &jd_end, &de_number);
if (error)
fprintf(stderr, "OpenEphem: Error %d trying to open ephemeris file: %s\n", error, filename);
return error;
}
static int NovasBodyPos(double jd, int body, double pos[3])
{
int error, k;
double jed[2];
double sun_pos[3], moon_pos[3], vel[3];
jed[0] = jd;
jed[1] = 0.0;
if (body == BODY_EARTH || body == BODY_MOON)
{
double factor;
/*
The caller is asking for the Earth's position or the Moon's position.
NOVAS does not directly represent either body.
Instead, we have to calculate the Earth or Moon using
the Earth/Moon Barycenter (EMB) and the Geocentric Moon (GM).
*/
error = state(jed, BODY_EMB, pos, vel);
if (error)
{
fprintf(stderr, "NovasBodyPos: state(%lf, EMB) returned %d\n", jd, error);
return error;
}
error = state(jed, BODY_GM, moon_pos, vel);
if (error)
{
fprintf(stderr, "NovasBodyPos: state(%lf, GM) returned %d\n", jd, error);
return error;
}
if (body == BODY_EARTH)
/* Calculate the Earth's position away from the EMB, opposite the direction of the Moon. */
factor = -1.0 / (1.0 + EarthMoonMassRatio);
else
/* Calculate the Moon's position away from the EMB, along the vector from the Earth to the Moon. */
factor = EarthMoonMassRatio / (1.0 + EarthMoonMassRatio);
for (k=0; k<3; ++k)
pos[k] += factor * moon_pos[k];
}
else
{
/* This is a body that NOVAS directly models in its ephemerides. */
error = (int)state(jed, (short)body, pos, vel);
if (error)
{
fprintf(stderr, "NovasBodyPos: state(%lf, %d) returned %d\n", jd, body, error);
return error;
}
/* Special case: geocentric moon should not be converted to heliocentric coordinates. */
if (body == BODY_GM) return 0;
}
error = (int)state(jed, BODY_SUN, sun_pos, vel);
if (error)
{
fprintf(stderr, "NovasBodyPos: state(%lf, SUN) returned %d\n", jd, error);
return error;
}
pos[0] -= sun_pos[0];
pos[1] -= sun_pos[1];
pos[2] -= sun_pos[2];
return 0;
}
static int LoadVsopFile(vsop_model_t *model, int body)
{
static const char * const BodyName[] =
{
"mer", "ven", "ear", "mar", "jup", "sat", "ura", "nep"
// 0 1 2 3 4 5 6 7
};
char filename[100];
if (body < 0 || body > 7)
{
fprintf(stderr, "LoadVsopFile: Invalid body=%d\n", body);
return 1;
}
snprintf(filename, sizeof(filename), "vsop/VSOP87B.%s", BodyName[body]);
return VsopLoadModel(model, filename);
}
static int SearchVsop(int body)
{
const double arcmin_limit = 0.4;
int error;
int trunc_terms, winner_terms = -1;
double thresh_lo = 1.0e-6;
double thresh_hi = 1.0e-2;
double max_arcmin, threshold;
double winner_threshold = 0.0;
double winner_arcmin = -1.0;
vsop_model_t model;
error = LoadVsopFile(&model, body);
if (error) goto fail;
while (thresh_hi / thresh_lo > 1.000001)
{
threshold = sqrt(thresh_lo * thresh_hi);
error = TestVsopModel(&model, body, threshold, &max_arcmin, &trunc_terms);
if (error) goto fail;
if (max_arcmin >= arcmin_limit)
{
/* not accurate enough, so decrease threshold next time */
thresh_hi = threshold;
}
else
{
/* we could tolerate larger inaccuracy to get smaller data */
thresh_lo = threshold;
/* track best-so-far (smallest data size with tolerable error) */
if (winner_terms < 0 || trunc_terms < winner_terms)
{
winner_terms = trunc_terms;
winner_arcmin = max_arcmin;
winner_threshold = threshold;
error = SaveVsopFile(&model);
if (error) goto fail;
}
}
}
if (winner_terms < 0)
{
fprintf(stderr, "SearchVsop: could not find a solution for body %d\n", body);
error = 1;
goto fail;
}
printf("SearchVsop(WINNER): body=%d, terms=%d, arcmin=%0.6lf, threshold=%0.4le\n", body, winner_terms, winner_arcmin, winner_threshold);
fflush(stdout);
fail:
VsopFreeModel(&model);
return error;
}
static int SaveVsopFile(const vsop_model_t *model)
{
char filename[100];
snprintf(filename, sizeof(filename), "output/vsop_%d.txt", (int)model->body);
return VsopWriteTrunc(model, filename);
}
static int TestVsopModel(vsop_model_t *model, int body, double threshold, double *max_arcmin, int *trunc_terms)
{
int error;
double jdStart, jdStop, jd, jdDelta;
double vpos[VSOP_MAX_COORDS];
double npos[3];
double arcmin;
*max_arcmin = -1.0;
*trunc_terms = -1;
if (body < 0 || body > 7)
{
fprintf(stderr, "TestVsopFile: Invalid body %d\n", body);
error = 1;
goto fail;
}
jdStart = julian_date(MIN_YEAR, 1, 1, 0.0);
jdStop = julian_date(MAX_YEAR, 1, 1, 0.0);
jdDelta = 1.0;
error = VsopTruncate(model, jdStart, jdStop, threshold);
if (error) goto fail;
*trunc_terms = VsopTermCount(model);
for (jd = jdStart; jd <= jdStop; jd += jdDelta)
{
VsopCalc(model, jd, vpos);
if (body == 2)
error = NovasEarth(jd, npos);
else
error = NovasBodyPos(jd, body, npos);
if (error) goto fail;
error = PositionArcminError(body, jd, npos, vpos, &arcmin);
if (error) goto fail;
if (arcmin > *max_arcmin) *max_arcmin = arcmin;
}
error = 0;
fail:
return error;
}
static int BuildVsopData(void)
{
int body;
int error;
for (body=0; body < 8; ++body)
if (0 != (error = SearchVsop(body)))
break;
return error;
}
static int GeneratePlanets(void)
{
int error;
CHECK(OpenEphem());
CHECK(BuildVsopData());
CHECK(ManualResample(8, 19, 7, MIN_YEAR, MAX_YEAR));
fail:
ephem_close();
return error;
}
static int GenerateSource(void)
{
int error;
CHECK(GenerateCode(CODEGEN_LANGUAGE_C, "../source/c/astronomy.c", "template/astronomy.c", "output"));
CHECK(GenerateCode(CODEGEN_LANGUAGE_CSHARP, "../source/csharp/astronomy.cs", "template/astronomy.cs", "output"));
CHECK(GenerateCode(CODEGEN_LANGUAGE_JS, "../source/js/astronomy.js", "template/astronomy.js", "output"));
CHECK(GenerateCode(CODEGEN_LANGUAGE_PYTHON, "../source/python/astronomy.py", "template/astronomy.py", "output"));
fail:
return error;
}
static int PositionArcminError(int body, double jd, const double a[3], const double b[3], double *arcmin)
{
int error, k;
double diff, sum, au;
double epos[3], evel[3], jed[2];
VectorType adiff, bdiff;
*arcmin = 99999.0;
if (body == BODY_SUN)
{
/* Trivial case: The Sun as always at heliocentric coordinates (0, 0, 0). */
/* Let's just check that both positions are very close to the origin. */
diff = a[0]*a[0] + a[1]*a[1] + a[2]*a[2];
diff += b[0]*b[0] + b[1]*b[1] + b[2]*b[2];
diff = sqrt(diff);
if (diff > 1.0e-6)
{
fprintf(stderr, "PositionArcminError: Excessive error (%lg) in heliocentric Sun.\n", diff);
return 1;
}
*arcmin = 0.0;
return 0;
}
if (body == BODY_MOON)
{
/*
Measure the position of the moon as seen from the *SURFACE* of the Earth,
at the point along a line from the geocenter to the object.
This is closer than the geocenter or the barycenter, so the anglular error is larger.
For other bodies, there is no significant difference between
topocentric error and geocentric error.
*/
const double scale = (ERAD / AU) / VectorLength(a);
error = NovasBodyPos(jd, BODY_EARTH, epos);
if (error) return error;
for (k=0; k<3; ++k) epos[k] += scale * a[k];
goto calc;
}
if (body == BODY_GM)
{
/*
The given position vectors are geocentric, not heliocentric.
Adjust them them to be topocentric in the direction of the Moon,
to maximize the error experienced by an observer on the surface of the Earth.
*/
const double dilate = (1.0 + EarthMoonMassRatio) / EarthMoonMassRatio;
adiff.t = jd; /* doesn't matter, but I don't like leaving undefined memory */
adiff.v[0] = a[0];
adiff.v[1] = a[1];
adiff.v[2] = a[2];
bdiff.t = jd; /* doesn't matter, but I don't like leaving undefined memory */
bdiff.v[0] = b[0];
bdiff.v[1] = b[1];
bdiff.v[2] = b[2];
*arcmin = dilate * (RAD2DEG * 60.0) * AngleBetween(adiff, bdiff);
return 0;
}
if (body == BODY_EMB || body == BODY_EARTH)
{
/*
For the Earth/Moon Barycenter (EMB=2), or Earth=11, we use a special estimate
of angular error. An error in the Earth's position affects the observed
position of all other bodies in the solar system.
It is appropriate to use a much stricter error measure than for other objects.
The worst case error is for Venus when it is closest to the Earth.
We approximate that worst case distance by EARTH_PERIHELION - VENUS_APHELION.
*/
diff = a[0] - b[0];
sum = diff*diff;
diff = a[1] - b[1];
sum += diff*diff;
diff = a[2] - b[2];
sum += diff*diff;
au = sqrt(sum);
*arcmin = (RAD2DEG * 60.0) * (au / (EARTH_PERIHELION - VENUS_APHELION));
return 0;
}
/* Exclude Sun and Geocentric Moon (GM) from error estimates. */
/* Can also use Earth position rather than EMB position. */
if (body < BODY_MERCURY || body > BODY_PLUTO)
{
fprintf(stderr, "PositionArcminError(FATAL): Invalid body = %d\n", body);
return 1;
}
/*
Any planet other than the EMB is assumed to be viewed from the Earth.
We use the EMB as a good enough estimate of the Earth's position.
Take two vectors: (a - epos) and (b - epos).
These are vectors from the EMB to the two calculated positions of the other planet.
Find the angle between the two vectors using inverse cosine of dot product.
*/
jed[0] = jd;
jed[1] = 0.0;
error = state(jed, BODY_EMB, epos, evel);
if (error)
{
fprintf(stderr, "PositionArcminError: state(%lf, EMB) returned %d\n", jd, error);
return error;
}
calc:
adiff.t = jd; /* doesn't matter, but I don't like leaving undefined memory */
adiff.v[0] = a[0] - epos[0];
adiff.v[1] = a[1] - epos[1];
adiff.v[2] = a[2] - epos[2];
bdiff.t = jd; /* doesn't matter, but I don't like leaving undefined memory */
bdiff.v[0] = b[0] - epos[0];
bdiff.v[1] = b[1] - epos[1];
bdiff.v[2] = b[2] - epos[2];
*arcmin = (RAD2DEG * 60.0) * AngleBetween(adiff, bdiff);
return 0;
}
static double VectorLength(const double v[3])
{
return sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
}
static int ManualResample(int body, int npoly, int nsections, int startYear, int stopYear)
{
int error, nsamples;
double daysPerSection;
error_stats_t stats;
char filename[80];
snprintf(filename, sizeof(filename), "output/%02d.eph", body);
CHECK(Resample(body, filename, npoly, startYear, stopYear, nsections));
daysPerSection = (365.244 * (stopYear - startYear + 1)) / nsections;
nsamples = (int)(24.0 * (60.0/10.0) * daysPerSection); /* approximately one sample per 10 minutes */
if (nsamples < 100*npoly)
nsamples = 100*npoly; /* but always oversample every segment beyond the polynomial count */
CHECK(MeasureError(filename, nsamples, &stats));
printf("Chebyshev body=%d, rms=%0.6lf, max=%0.6lf, data=%ld\n", body, stats.rmsArcminError, stats.maxArcminError, stats.dataCount);
fail:
return error;
}
static int MeasureError(const char *inFileName, int nsamples, error_stats_t *stats)
{
int i, nvectors;
int error = 0;
double jd, x, sumPosDiff, posDiff, maxPosDiff;
double arcmin, arcminSum;
double apos[3]; /* our approximate position vector */
double npos[3]; /* NOVAS exact position vector */
eph_file_reader_t reader;
eph_record_t record;
memset(stats, 0, sizeof(error_stats_t));
error = EphFileOpen(&reader, inFileName);
if (error)
{
fprintf(stderr, "MeasureError: Error %d trying to open file: %s\n", error, inFileName);
goto fail;
}
/* sanity check on the body value, to prevent invalid memory access */
if (reader.body < 0 || reader.body > 10)
{
fprintf(stderr, "MeasureError: Invalid body = %d\n", reader.body);
error = 1;
goto fail;
}
nvectors = 0;
sumPosDiff = 0.0;
maxPosDiff = 0.0;
arcminSum = 0.0;
while (EphReadRecord(&reader, &record))
{
stats->dataCount += (1 + 3*record.numpoly); /* tally the beginning Julian Date plus all the Chebyshev coefficients */
/*
We sample in such a way as to exclude the endpoints, because
we know error will be minimal there (and at all the other Chebyshev nodes).
If nsamples==1, we pick the midpoint.
If nsamples==2, we pick the 1/3 mark and 2/3 marks.
Thus we always divide the interval into (1+nsamples) sections.
*/
for (i=1; i <= nsamples; ++i)
{
jd = record.jdStart + (i * record.jdDelta)/(1 + nsamples);
/* Calculate "correct" position at the time 'jd' (according to NOVAS). */
error = NovasBodyPos(jd, reader.body, npos);
if (error) goto fail;
/* Calculate our approximation of the position at the time 'jd'. */
x = ChebScale(record.jdStart, record.jdStart + record.jdDelta, jd);
ChebApprox(record.numpoly, 3, record.coeff, x, apos);
/* Tally the root-mean-square error between the two position vectors. */
posDiff = VectorError(npos, apos);
if (posDiff > maxPosDiff)
maxPosDiff = posDiff;
/* Estimate angular error */
error = PositionArcminError(reader.body, jd, npos, apos, &arcmin);
if (error) goto fail;
arcminSum += arcmin * arcmin;
if (arcmin > stats->maxArcminError)
stats->maxArcminError = arcmin;
sumPosDiff += posDiff;
++nvectors;
}
}
error = record.error;
if (error)
{
fprintf(stderr, "MeasureError: Error %d reading record on line %d of file %s\n", error, reader.lnum, inFileName);
goto fail;
}
if (nvectors < 1)
{
fprintf(stderr, "MeasureError(ERROR): nvectors = %d\n", nvectors);
error = 1;
goto fail;
}
stats->rmsPositionError = sqrt(sumPosDiff / nvectors);
stats->maxPositionError = sqrt(maxPosDiff);
stats->rmsArcminError = sqrt(arcminSum / nvectors);
fail:
EphFileClose(&reader);
return error;
}
static int Resample(int body, const char *outFileName, int npoly, int startYear, int stopYear, int nsections)
{
FILE *outfile = NULL;
ChebEncoder encoder;
double coeff[CHEB_MAX_DIM][CHEB_MAX_POLYS]; /* independent Chebyshev coefficients for each position coordinate: x, y, z */
int error, k, i, section;
double jdStart, jdStop, jdDelta, jd;
sample_context_t context;
if (startYear < MIN_YEAR || stopYear < startYear || MAX_YEAR < stopYear)
{
fprintf(stderr, "ERROR: Invalid year range %d..%d\n", startYear, stopYear);
error = 1;
goto fail;
}
if (nsections < 1)
{
fprintf(stderr, "ERROR: nsections must be a positive integer.\n");
error = 2;
goto fail;
}
error = ChebInit(&encoder, npoly, VECTOR_DIM);
if (error)
{
fprintf(stderr, "ERROR %d returned by ChebInit()\n", error);
goto fail;
}
outfile = fopen(outFileName, "wt");
if (outfile == NULL)
{
fprintf(stderr, "ERROR: Cannot open output file '%s'\n", outFileName);
error = 3;
goto fail;
}
/* Write header to output file. */
fprintf(outfile, "body=%d\n", body);
fprintf(outfile, "\n"); /* blank line terminates the header */
jdStart = julian_date((short)startYear, 1, 1, 0.0) - 1.0; /* subtract 1 day for light travel time allowance */
jdStop = julian_date((short)(stopYear+1), 1, 1, 0.0);
jdDelta = (jdStop - jdStart) / nsections;
context.body = body;
for (section = 0; section < nsections; ++section)
{
jd = jdStart + (section * jdDelta);
/* encode coefficients for vector-valued position function */
error = ChebGenerate(&encoder, SampleFunc, &context, jd, jd + jdDelta, coeff);
if (error)
{
fprintf(stderr, "ERROR: ChebGenerate() returned %d\n", error);
goto fail;
}
/* Prefix each block of polynomial coefficients with the time range and the number of polynomials. */
fprintf(outfile, "%0.1lf %0.1lf %d\n", jd, jdDelta, npoly);
/* Write the Chebyshev coordinates we obtained for the position function. */
for (k=0; k < npoly; k++)
{
for (i=0; i < 3; ++i)
fprintf(outfile, " %16.12lf", coeff[i][k]);
fprintf(outfile, "\n");
}
}
fail:
if (outfile != NULL)
fclose(outfile);
if (error)
unlink(outFileName);
return error;
}
static int SampleFunc(const void *context, double jd, double pos[CHEB_MAX_DIM])
{
const sample_context_t *c = context;
int error;
double jed[2];
double sun_pos[3];
double vel[3]; /* we don't care about velocities... ignored */
jed[0] = jd;
jed[1] = 0.0;
error = state(jed, (short)c->body, pos, vel);
if (error) return error;
error = state(jed, BODY_SUN, sun_pos, vel);
if (error) return error;
/* Calculate heliocentric coordinates from barycenric coordinates. */
pos[0] -= sun_pos[0];
pos[1] -= sun_pos[1];
pos[2] -= sun_pos[2];
return 0;
}
static double VectorError(double a[3], double b[3])
{
double dx = a[0] - b[0];
double dy = a[1] - b[1];
double dz = a[2] - b[2];
return dx*dx + dy*dy + dz*dz;
}
static int ParseObserver(const char *filename, int lnum, const char *line, observer *location)
{
int error;
on_surface surface;
if (3 != sscanf(line, "o %lf %lf %lf", &surface.latitude, &surface.longitude, &surface.height))
{
fprintf(stderr, "ParseObserver: Invalid format on line %d of file %s\n", lnum, filename);
return 1;
}
surface.temperature = 20;
surface.pressure = 1000;
error = make_observer(1, &surface, NULL, location);
if (error)
{
fprintf(stderr, "ParseObserver: error %d returned by make_observer() for line %d of file %s\n", error, lnum, filename);
}
return error;
}
static int EclipticVector(double jd, vsop_body_t body, double ecl_pos[3])
{
int error;
double eq_pos[3];
/* Find equatorial coordinates for the body. */
CHECK(NovasBodyPos(jd, body, eq_pos));
/* Convert equatorial cartesian coordinates to ecliptic longitude. */
error = equ2ecl_vec(jd, 2, 0, eq_pos, ecl_pos);
if (error)
{
fprintf(stderr, "EclipticLongitude: equ2ecl_vec() returned %d\n", error);
goto fail;
}
fail:
return error;
}
static double EclipticLongitude(const double ecl_pos[3])
{
double lon = RAD2DEG * atan2(ecl_pos[1], ecl_pos[0]);
if (lon < 0.0)
lon += 360.0;
return lon;
}
static double LongitudeOffset(double diff)
{
double offset = diff;
while (offset <= -180) offset += 360;
while (offset > 180) offset -= 360;
return offset;
}
static int CheckEcliptic(const char *filename, int lnum, const char *line, double *arcmin, vsop_body_t *outbody)
{
int error;
vsop_body_t body;
char name[10];
char event[10];
double body_ecl[3];
double earth_ecl[3];
double geo[3];
double tt, jd, lon, dist, blon, elon, diff;
*arcmin = 99999.0;
*outbody = VSOP_INVALID_BODY;
/* Example line: "e Jupiter opp <tt> <au>" */
if (4 != sscanf(line, "e %9[A-Za-z] %9[a-z] %lf %lf", name, event, &tt, &dist))
{
fprintf(stderr, "CheckEcliptic: Invalid format on line %d of file %s\n", lnum, filename);
return 1;
}
*outbody = body = LookupBody(name);
if (body < 0)
{
fprintf(stderr, "CheckEcliptic: Unknown body '%s' on line %d of file %s\n", name, lnum, filename);
return 1;
}
jd = tt + T0;
CHECK(EclipticVector(jd, body, body_ecl));
CHECK(EclipticVector(jd, VSOP_EARTH, earth_ecl));
geo[0] = body_ecl[0] - earth_ecl[0];
geo[1] = body_ecl[1] - earth_ecl[1];
geo[2] = body_ecl[2] - earth_ecl[2];
dist = VectorLength(geo);
if (!strcmp(event, "opp"))
{
/* Opposition (superior planets only) */
lon = 0.0;
/* Distance could be anything... so ignore it. */
}
else if (!strcmp(event, "inf"))
{
/* Inferior conjunction (inferior planets only) */
lon = 0.0;
/* Distance from Earth must be less than 1 AU for an inferior conjunction. */
if (dist >= 1.0)
{
fprintf(stderr, "CheckEcliptic: Invalid distance %0.3lf AU for inferior conjunction; line %d file %s\n", dist, lnum, filename);
return 1;
}
}
else if (!strcmp(event, "sup"))
{
/* Superior conjunction (inferior or superior planets) */
lon = 180.0;
/* Distance from Earth must be greater than 1 AU for a superior conjunction. */
if (dist <= 1.0)
{
fprintf(stderr, "CheckEcliptic: Invalid distance %0.3lf AU for superior conjunction; line %d file %s\n", dist, lnum, filename);
return 1;
}
}
else
{
fprintf(stderr, "CheckEcliptic: Invalid event code '%s' on line %d of file %s\n", event, lnum, filename);
return 1;
}
blon = EclipticLongitude(body_ecl);
elon = EclipticLongitude(earth_ecl);
diff = LongitudeOffset((elon - blon) - lon);
*arcmin = fabs(diff * 60.0);
if (*arcmin > 1.0)
{
fprintf(stderr, "CheckEcliptic: Excessive arcminute error = %0.3lf on line %d of file %s\n", *arcmin, lnum, filename);
return 1;
}
fail:
return error;
}
static int CheckTestVector(const char *filename, int lnum, const char *line, double *arcmin, vsop_body_t *outbody)
{
int error;
vsop_body_t body;
char name[10];
double tt, jd, xpos[3], npos[3];
*outbody = VSOP_INVALID_BODY;
*arcmin = 99999.0;
if (5 != sscanf(line, "v %9[A-Za-z] %lf %lf %lf %lf", name, &tt, &xpos[0], &xpos[1], &xpos[2]))
{
fprintf(stderr, "CheckTestVector: Invalid format on line %d of file %s\n", lnum, filename);
return 1;
}
jd = tt + T0;
*outbody = body = LookupBody(name);
if (body < 0)
{
fprintf(stderr, "CheckTestVector: Unknown body '%s' on line %d of file %s\n", name, lnum, filename);
return 1;
}
error = NovasBodyPos(jd, body, npos);
if (error)
{
fprintf(stderr, "CheckTestVector: NovasBodyPos returned %d on line %d of file %s\n", error, lnum, filename);
return error;
}
error = PositionArcminError(body, jd, npos, xpos, arcmin);
if (error)
{
fprintf(stderr, "CheckTestVector: PositionArcminError returned %d on line %d of file %s\n", error, lnum, filename);
return error;
}
if (*arcmin > 0.4)
{
fprintf(stderr, "CheckTestVector: Excessive angular error (%lf arcmin) on line %d of file %s\n", *arcmin, lnum, filename);
fprintf(stderr, "check = (%22.16lf, %22.16lf, %22.16lf)\n", xpos[0], xpos[1], xpos[2]);
fprintf(stderr, "correct = (%22.16lf, %22.16lf, %22.16lf)\n", npos[0], npos[1], npos[2]);
return 1;
}
return 0; /* success */
}
static int CheckSkyPos(observer *location, const char *filename, int lnum, const char *line, double *arcmin_equ, double *arcmin_hor, vsop_body_t *outbody)
{
int body, error, bodyIndex;
char name[10];
double tt, ut, jd_tt, jd_utc, ra, dec, dist, azimuth, altitude;
double check_zenith, check_azimuth, check_altitude, check_ra, check_dec;
object obj;
sky_pos sky_ast; /* astrometric (RA,DEC) */
sky_pos sky_dat; /* (RA,DEC) using true equator and equinox of date*/
double delta_ra, delta_dec, delta_az, delta_alt;
*outbody = VSOP_INVALID_BODY;
*arcmin_equ = *arcmin_hor = 99999.0;
if (8 != sscanf(line, "s %9[A-Za-z] %lf %lf %lf %lf %lf %lf %lf", name, &tt, &ut, &ra, &dec, &dist, &azimuth, &altitude))
{
fprintf(stderr, "CheckSkyPos: Invalid format on line %d of file %s\n", lnum, filename);
return 1;
}
jd_tt = tt + T0;
jd_utc = ut + T0;
*outbody = body = LookupBody(name);
if (body < 0)
{
fprintf(stderr, "CheckSkyPos: Unknown body '%s' on line %d of file %s\n", name, lnum, filename);
return 1;
}
if (body == BODY_EARTH || body == BODY_EMB)
{
fprintf(stderr, "CheckSkyPos: Cannot calculate sky position of body '%s'\n", name);
return 1;
}
if (body == BODY_GM)
bodyIndex = 11;
else if (body == BODY_SUN)
bodyIndex = 10;
else
bodyIndex = 1 + body;
error = make_object(0, (short)bodyIndex, name, NULL, &obj);
if (error)
{
fprintf(stderr, "CheckSkyPos: make_object(%d) returned %d on line %d of file %s\n", bodyIndex, error, lnum, filename);
return error;
}
/* First call to place: ask for astrometric coordinates (J2000) : coord_sys=3 */
error = place(jd_tt, &obj, location, jd_tt-jd_utc, 3, 1, &sky_ast);
if (error)
{
fprintf(stderr, "CheckSkyPos: place(3) returned %d on line %d of file %s\n", error, lnum, filename);
return error;
}
/* Calculate the overall angular error between the two (RA,DEC) pairs. */
/* Convert both errors to arcminutes. */
delta_dec = (sky_ast.dec - dec) * 60.0;
delta_ra = fabs(sky_ast.ra - ra);
if (delta_ra > 12.0)
{
/*
Sometimes the two RA values can straddle the 24-hour mark.
For example, one of them is 0.001 and the other 23.999.
The actual error is then 0.002 hours, not 23.998.
In general, it is never "fair" to call the error greater than
12 hours or 180 degrees.
*/
delta_ra = 24.0 - delta_ra;
}
delta_ra *= (15.0 * 60.0);
/* Right Ascension errors are less significant as the declination approaches the poles. */
/* For example, a 12-hour RA error for Polaris would not matter very much to its observed position. */
/* So diminish the error measurement as appropriate for this declination. */
delta_ra *= cos(sky_ast.dec * DEG2RAD);
/* Calculate pythagorean error as if both were planar coordinates. */
*arcmin_equ = sqrt(delta_ra*delta_ra + delta_dec*delta_dec);
if (*arcmin_equ > 0.9)
{
fprintf(stderr, "CheckSkyPos: excessive (RA,DEC) error = %lf arcmin at line %d of file %s\n", *arcmin_equ, lnum, filename);
return 1;
}
/* We have tested RA,DEC. Now measure the error in horizontal coordinates. */
/* We have to call place() again, this time asking for equator and equinox of date (coord_sys=1). */
error = place(jd_tt, &obj, location, jd_tt-jd_utc, 1, 1, &sky_dat);
if (error)
{
fprintf(stderr, "CheckSkyPos: place(1) returned %d on line %d of file %s\n", error, lnum, filename);
return error;
}
equ2hor(jd_utc, jd_tt-jd_utc, 1, 0.0, 0.0,
&location->on_surf, sky_dat.ra, sky_dat.dec, 0,
&check_zenith, &check_azimuth, &check_ra, &check_dec);
check_altitude = 90.0 - check_zenith;
delta_az = fabs(azimuth - check_azimuth);
if (delta_az > 180.0)
delta_az = 360.0 - delta_az;
delta_az *= 60.0;
/* Just like RA, diminish the azimuth error as altitude approaches zenith/nadir... */
delta_az *= cos(check_altitude * DEG2RAD);
delta_alt = 60.0 * (altitude - check_altitude);
*arcmin_hor = sqrt(delta_az*delta_az + delta_alt*delta_alt);
if (*arcmin_hor > 0.9)
{
fprintf(stderr, "CheckSkyPos: excessive (az,alt) error = %lf arcmin for body %d at line %d of file %s\n", *arcmin_hor, body, lnum, filename);
return 1;
}
return 0;
}
static vsop_body_t LookupBody(const char *name)
{
if (!strcmp(name, "Mercury")) return BODY_MERCURY;
if (!strcmp(name, "Venus")) return BODY_VENUS;
if (!strcmp(name, "Earth")) return BODY_EARTH;
if (!strcmp(name, "Moon")) return BODY_MOON;
if (!strcmp(name, "Mars")) return BODY_MARS;
if (!strcmp(name, "Jupiter")) return BODY_JUPITER;
if (!strcmp(name, "Saturn")) return BODY_SATURN;
if (!strcmp(name, "Uranus")) return BODY_URANUS;
if (!strcmp(name, "Neptune")) return BODY_NEPTUNE;
if (!strcmp(name, "Pluto")) return BODY_PLUTO;
if (!strcmp(name, "Sun")) return BODY_SUN;
if (!strcmp(name, "EMB")) return BODY_EMB;
if (!strcmp(name, "GM")) return BODY_GM;
return BODY_INVALID;
}
typedef struct
{
int count;
double sum;
double max;
}
error_stat_t;
typedef struct
{
error_stat_t helio;
error_stat_t equ;
error_stat_t hor;
error_stat_t eclip;
}
error_bundle_t;
static void UpdateErrorStats(error_stat_t *stats, double arcmin)
{
++(stats->count);
stats->sum += arcmin * arcmin;
if (arcmin > stats->max)
stats->max = arcmin;
}
static int PrintErrorStats(vsop_body_t body, const char *tag, const error_stat_t *stats, error_stat_t *tally)
{
if (stats->count > 0)
{
const char *name;
switch ((int)body)
{
case VSOP_MERCURY: name = "Mercury"; break;
case VSOP_VENUS: name = "Venus"; break;
case VSOP_EARTH: name = "Earth"; break;
case VSOP_EMB: name = "EMB"; break;
case VSOP_MARS: name = "Mars"; break;
case VSOP_JUPITER: name = "Jupiter"; break;
case VSOP_SATURN: name = "Saturn"; break;
case VSOP_URANUS: name = "Uranus"; break;
case VSOP_NEPTUNE: name = "Neptune"; break;
case 8: name = "Pluto"; break;
case 9: name = "Moon"; break;
case VSOP_SUN: name = "Sun"; break;
default: name = ""; break;
}
printf("STATS(%-8s %s): count= %6d max= %10.8lf rms= %10.8lf\n", name, tag, stats->count, stats->max, sqrt(stats->sum / stats->count));
if (tally != NULL)
{
tally->count += stats->count;
tally->sum += stats->sum;
if (stats->max > tally->max)
tally->max = stats->max;
}
return 1; /* printed a line */
}
return 0; /* did not print a line */
}
static int CheckTestOutput(const char *filename)
{
int error, lnum, nprinted, nbodies;
vsop_body_t body;
FILE *infile = NULL;
char line[200];
double arcmin_helio, arcmin_eclip, arcmin_equ, arcmin_hor;
error_bundle_t bundle[VSOP_BODY_LIMIT];
error_stat_t tally;
observer location;
memset(&location, 0, sizeof(observer));
memset(bundle, 0, sizeof(bundle));
memset(&tally, 0, sizeof(tally));
CHECK(OpenEphem());
/* Check input file that contains lines of test output like this: */
/* v Jupiter 2458455.2291666665 -2.0544644667646907 -4.271606485974493 -1.899398554516329 */
infile = fopen(filename, "rt");
if (infile == NULL)
{
fprintf(stderr, "CheckTestOutput: Cannot open file: %s\n", filename);
error = 1;
goto fail;
}
lnum = 0;
while (fgets(line, sizeof(line), infile))
{
++lnum;
switch (line[0])
{
case '#':
break; /* ignore debug output */
case 'o':
/* The observer used for all future sky position calculations */
CHECK(ParseObserver(filename, lnum, line, &location));
break;
case 'v': /* heliocentric cartesian vector represented in J2000 equatorial plane */
CHECK(CheckTestVector(filename, lnum, line, &arcmin_helio, &body));
UpdateErrorStats(&bundle[body].helio, arcmin_helio);
break;
case 's': /* sky coordinates: RA, DEC, distance */
CHECK(CheckSkyPos(&location, filename, lnum, line, &arcmin_equ, &arcmin_hor, &body));
UpdateErrorStats(&bundle[body].equ, arcmin_equ);
UpdateErrorStats(&bundle[body].hor, arcmin_hor);
break;
case 'e':
CHECK(CheckEcliptic(filename, lnum, line, &arcmin_eclip, &body));
UpdateErrorStats(&bundle[body].eclip, arcmin_eclip);
break;
default:
fprintf(stderr, "CheckTestOutput: Invalid first character on line %d of file %s\n", lnum, filename);
error = 1;
goto fail;
}
}
printf("CheckTestOutput: Verified %d lines of file %s\n", lnum, filename);
nbodies = 0;
for (body=0; body < VSOP_BODY_LIMIT; ++body)
{
nprinted = PrintErrorStats(body, "hel", &bundle[body].helio, &tally);
nprinted += PrintErrorStats(body, "equ", &bundle[body].equ, &tally);
nprinted += PrintErrorStats(body, "hor", &bundle[body].hor, &tally);
nprinted += PrintErrorStats(body, "ecl", &bundle[body].eclip, &tally);
if (nprinted > 0)
++nbodies;
}
if (nbodies > 1)
{
printf("---------------------------------------------------------------------\n");
PrintErrorStats(-1, "ALL", &tally, NULL);
}
error = 0;
fail:
ephem_close();
if (infile != NULL) fclose(infile);
return error;
}
static int TestChebFunc(const void *context, double t, double f[CHEB_MAX_DIM])
{
(void)context;
f[0] = cos(t) - t;
return 0;
}
static void DumpAlpha(const double alpha[CHEB_MAX_POLYS][CHEB_MAX_POLYS], int npoly)
{
int j, k;
for (j=0; j < npoly; ++j)
{
printf("alpha[%d][...] = ", j);
for (k=0; k < npoly; ++k)
{
printf(" %15.12lf", alpha[j][k]);
}
printf("\n");
}
}
static int UnitTestChebyshev(void)
{
const int npoly = 5;
const int ndimen = 1;
const double t1 = 0.6;
const double t2 = 0.8;
int error;
int i;
double x, t, f_exact, f_approx;
ChebEncoder encoder;
double coeff[CHEB_MAX_DIM][CHEB_MAX_POLYS];
error = ChebInit(&encoder, npoly, ndimen);
if (error)
{
fprintf(stderr, "UnitTestChebyshev: ChebInit returned error %d\n", error);
goto fail;
}
error = ChebGenerate(&encoder, TestChebFunc, NULL, t1, t2, coeff);
if (error)
{
fprintf(stderr, "UnitTestChebyshev: ChebGenerate returned error %d\n", error);
goto fail;
}
DumpAlpha(encoder.Alpha, encoder.NumPoly);
/* Verify that the values are exact at all the sample points. */
for (i=0; i < npoly; ++i)
{
x = (2.0 * i)/(npoly-1) - 1.0;
t = t1 + (t2-t1)*((double)i / (npoly-1));
TestChebFunc(NULL, t, &f_exact);
ChebApprox(npoly, ndimen, coeff, x, &f_approx);
printf("UnitTestChebyshev: i=%d, x=%5.2lf, t=%0.4lf, f_exact=%12.9lf, f_approx=%12.9lf, error=%lg\n", i, x, t, f_exact, f_approx, f_approx - f_exact);
}
fail:
return error;
}